vue3项目练习-大事件项目(完成3/4)
vue3大事件项目
创建项目(pnpm)
先安装pnpm,然后使用pnpm创建项目
注意安装pnpm的时候在一个空文件夹下安装,不要按照到电脑磁盘根目录,可能权限不够
pnpm
pnpm创建项目
为项目安装依赖
项目接口文档https://apifox.com/apidoc/shared-26c67aee-0233-4d23-aab7-08448fdf95ff/api-93850835
本项目的技术栈 本项目技术栈基于 ES6、vue3、pinia、vue-router 、vite 、axios 和 element-plus
配置
Eslint+prettier配置代码风格
理解:让代码更严谨规范
配置文件.eslintrc.cjs (eslint.config.js)
- 二者结合起来配置
Eslint规范纠错(校验),prettier风格美观配置(格式化) - vue组件名称多单词组成(忽略index.vue)
- props解构(关闭)
props解构默认报错,因为会丢失响应式,但是后续会有响应式,关闭这个报错
视频提供的代码如下,但是由于视频项目修改的配置文件和我项目拥有的配置文件不一样,所以我问了chat帮我把以下代码添加到我的文件中
配置的规则: - 不适用双引号,全部使用单引号
- 每行代码的长度不超过80(做项目通常分屏,一边写代码一边看效果演示,80长度适合)
- 无分号
- 对象最后一个属性后面没有逗号
- 换行符号不做显示(因为win和mac这个不一致)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21rules: {
'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不报错的正确代码到仓库
初始化 git 仓库,执行 git init 即可
初始化 husky 工具配置,执行 pnpm dlx husky-init && pnpm install 即可https://typicode.github.io/husky/
修改 .husky/pre-commit 文件
操作:
- 在vscode打开项目终端,注意这里打开bash终端,因为这是和git相关的操作
2.初始化git
git init - 安装
pnpm dlx husky-init && pnpm install
成功后我的显示以下内容4.安装后在项目中自动新建了一个.husky文件夹,里面有pre-commit文件,文件里就是代码提交前要做的事情(根据写入的从package.json文件夹找规则)1
2
3
4
5
6husky - Git hooks installed
devDependencies:
+ husky 8.0.3 (9.1.6 is available)
Done in 2.4s
把这个文件里的npm test进行修改
如果改为pnpm lint是全量检查,会有耗时问题和历史问题
实际上更推荐暂存区eslint校验,保证每次新写的代码是规范的
暂存区Eslint校验
理解:不让把暂存区中的报错代码上传到仓库
安装lint-staged包(还是在bash里面)
pnpm i lint-staged -D对
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"
}修改 .husky/pre-commit 文件
1
pnpm lint-staged
我的这里中间出了问题,chat帮我解决了,写在报错部分了
测试
我在main.js中写了一行错误代码然后尝试添加提交到仓库,在提交时被发现拦截
调整目录
默认生成的目录结构不满足我们的开发需求,所以这里需要做一些自定义改动:
- 删除一些初始化的默认文件
- 修改剩余代码内容
- 新增调整我们需要的目录结构
- 拷贝初始化资源文件,安装预处理器插件
- 删除文件
src/assets文件夹文件
src/components文件夹文件
src/stores文件夹文件 - 修改内容
src/router/index.js修改为以下内容(把routers内容清空了)
1
2
3
4
5
6
7
8import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: []
})
export default routersrc/App.vue内容清空
1 | <script setup></script> |
- src/main.js内容清空
1 | import { createApp } from 'vue' |
新增需要目录:api(请求模块)文件夹和utils(工具函数)文件夹
将项目需要的全局样式和图片文件,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 | import { createRouter, createWebHistory } from 'vue-router' |
按需引入-自动导入Element Plus
先安装,再按需导入,然后直接使用组件
成功后你要用哪个插件不需要像vue2项目里一样为它单独导入注册再使用,而是直接使用即可,它发现你使用会自动帮你导入!!!
- 安装
官方文档:https://element-plus.org/zh-CN/
打开文档-导航栏右侧指南-左侧基础-安装
pnpm install element-plus - 使用
文档的基础-快速开始-按需导入-自动导入(根据文档操作)
首先你需要安装unplugin-vue-components 和 unplugin-auto-import这两款插件
pnpm add -D unplugin-vue-components unplugin-auto-import - 配置
根据文档cv代码到vite.config.js文件最后,然后修改:
导包部分:已经有了的删掉,没有的移动到前面
defineConfig- plugins部分:加入文档内容
使用一下!
配置结束后重新启动项目
然后就可以在项目的任意位置使用组件啦
还是官方文档-右上角组件-找到想要的样式-样式右下角查看源代码-找到想要的直接CV到项目!
成功
小问题
我好像把东西安装到了项目文件上层的一个文件夹里了,而不是项目本身这个文件夹,后来删除项目父文件夹的其他文件,在项目里重新安装导入了
注意打开项目的时候一定要只打开项目根文件夹,不要再打错啦!!
Pinia构建持久化仓库
理解:解决多组件共享一个数据的情况
Pinia构建用户仓库
Pinia:在main.js中默认已经配好了(创建项目时配置的),所以只需要在stores文件夹中新建index.js文件和modules文件夹-建立仓库文件即可
新建用户仓库模块user.js,然后在App.vue里面测试
1 | import { defineStore } from 'pinia' |
测试一下!
App.vue测试
1 | <script setup> |
仓库持久化
需要用pinia的持久化插件(官方文档步骤)
- 先安装
pnpm add pinia-plugin-persistedstate -D - main.js中使用
1 | import persist from 'pinia-plugin-persistedstate' |
- 配置 stores/user.js,传递第三个参数persist配置后重启服务,token会持久化,刷新后token还在不会丢
1
2
3
4
5
6
7
export const useUserStore = defineStore(
....,
{
persist: true // 持久化
}
)
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 | //main.js中剪切过来的 |
axios请求工具设置
和vue2大项目类似,看官方文档有步骤
项目接口文档https://apifox.com/apidoc/shared-26c67aee-0233-4d23-aab7-08448fdf95ff/api-93850835
安装axios
pnpm add axios新建 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
29import 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 instanceaxios 基本配置
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
57import 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 | <script setup> |
路由配置
根据页面分析路由
根据提供的完整项目地址看项目观察: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 | routes: [ |
配置成功后在地址栏输入不同的地址有对应的显示
登录页
需求
静态解构,登录和注册的基本切换,注册功能的校验+调用后端成功注册,登录功能的校验+登录+存token
静态结构使用element-plus表单
静态布局
- 清空布局
把之前测试组件路由的代码都清空掉,只留一个路由出口(我这里把之前的作为副本保存然后新建了一个空的app.vue) - 安装图标库
页面用到的一些小图标来自element-plus图标库
pnpm i @element-plus/icons-vue
然后重启项目 - 登录页静态布局
在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 | <el-row class="login-page"> |
逻辑处理
- 表达切换
进入login页面默认是登录表单,点击表单左下角注册按钮切换到注册,再点击返回按钮切换到登录
使用const isRegister = ref(false)进行切换,true和false对应注册和登录 - 表单校验
还是使用组件库,Form表单-表单校验
https://element-plus.org/zh-CN/component/form.html#%E8%A1%A8%E5%8D%95%E6%A0%A1%E9%AA%8C
用来收集全部数据,这样后面提交或者重置都方便
(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> - 在表单中给表单元素配置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> - 提供数据对象和校验规则
提供一个整个的用于提交的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 | const formModel = ref({ |
注册预校验
在注册提交之前要有一次预校验,比如空着点注册肯定不行,通过校验才能完成提交请求
文档Form表单组件-Form API最下面Form Exposes,form组件实例暴露出来的一些方法
validate方法 对整个表单的内容进行验证。 接收一个回调函数,或返回 Promise
要想拿到form组件实例,要添加const form = ref()
1 | const register = async () => { |
注册登录请求
- 创建api文件夹-user.js文件,根据后端接口写
1 | import request from '@/utils/request' |
- 同时在app.vue导入api,修改注册请求(登录同理)
1
2
3
4
5
6
7const register = async () => {
// 注册成功之前,先进行校验,校验成功 → 请求,校验失败 → 自动提示
await form.value.validate()
await userRegisterService(formModel.value)
alert('注册成功')
isRegister.value = false //注册成功后切换到登录表单
} - 优化提示弹窗
上面的alert明显不好看,我们要更好看的
官方文档-左侧导航栏往下找到Feedback反馈组件-Message消息提示-不同状态-查看代码-poen四种表示方式
这里用第二中提示注册成功修改后运行程序注册后能成功弹出,但是vscode上还是显示红色报错,这是因为前面我们按需自动导入了,但是eslink不知道,所以给eslink中配置全局变量规则可以不报错1
ElMessage.success('注册成功')
给eslint.config.js文件最后添加
(视频成功我开始没成功,我的这个文件是eslint.config.js,视频是.eslintrc.cjs文件,前面做配置的时候就出现了问题我让AI帮我变成我的文件适用的代码,这里没有变)
(后来关掉vscode重新打开项目发现不报错了,重启是个好方法!)1
2
3
4
5
6
7
8module.exports = {
...
globals: {
ElMessage: 'readonly',
ElMessageBox: 'readonly',
ElLoading: 'readonly'
}
}
首页
需求
- 基本架子拆解(菜单组件的使用):采用分成左右两部分的架子,左侧是菜单(el-menu包含el-menu-item;二级菜单嵌套el-sub-menu,el-menu-item),官方文档学习
- 登录访问拦截:首页的内容必须是登录过的用户才能访问,没有登录的用户不能访问
- 用户基本信息获取和渲染:首页-个人中心-基本信息,展示用户的基本资料,右上角用户头像小菜单也能到达
- 右上角小菜单和退出功能
- 优化-退出询问
首页基本架子
views/layout/LayoutContauer.vue
主要用到el-menu组件,Container布局容器(官方文档)
el-container
el-aside 左侧
- el-menu 左侧边栏菜单
el-container 右侧
- el-header 右侧头部
- el-dropdown
- el-main 右侧主体
- router-view
- el-header 右侧头部
架子代码
1 | <script setup> |
在
登录访问拦截
和vue2项目的登录拦截一样,在router文件夹/index.js文件添加
一定记得要从仓库导入用户仓库!!不然永远拿不到用户的token
vue router官方文档-导航守卫https://router.vuejs.org/zh/guide/advanced/navigation-guards.html
登录访问拦截 => 默认是直接放行的
根据返回值决定,是放行还是拦截!!!!
返回值:
- undefined / true 直接放行
- false 拦回from的地址页面
- 具体路径 或 路径对象 拦截到对应的地址
‘/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'
})
用户基本信息的获取和渲染
- 封装接口:
去后端拿获取用户的的基本信息的接口,api/user.js后面新建方法:获取用户基本信息(注意方法命名规范)1
2export const userGetInfoService = () => request.get('/my/userinfo')
- 个人信息放到仓库中:
和token的维护类似,个人信息也会在多个地方使用,放到用户仓库中统一维护。在store/modules/user.js文件中定义数据user和获取用户信息的getUser方法,最后在return里暴露出去1
2
3
4
5
6const user = ref({})
const getUser = async () => {
const res = await userGetInfoService() // 请求获取数据
user.value = res.data.data
//这里到底几个.data怎么拿到数据存从后端接口文档返回的数据信息看
} - 页面调用
回到首页页面,从仓库里导入用户仓库然后使用生命周期钩子,让页面加载完后发送请求,获取到用户信息1
import { useUserStore } from '@/stores'
1
2
3
4
5const userStore = useUserStore()
//onMounted生命周期钩子,页面加载完发送请求
onMounted(() => {
userStore.getUser()
})
这里面user对象是empty,因为还没有设置个人信息 - 动态渲染信息
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 | const handleCommand = async (key) => { |
优化-退出询问
用户可能不小心点到退出,所以在退出前进行弹窗询问
官方文档-左侧导航栏Message Box 消息弹出框-确认消息
1 | const handleCommand = async (key) => { |
文章分类页面
需求
- 基本架子-PageContainer
- 封装文章分类渲染 &loading 处理
- 文章分类添加编辑 [element-plus 弹层]
- 文章分类删除
文章分类架子
article/ArticleChannel.vue
官方文档-左侧导航栏Data数据展示-Card卡片
1 | <template> |
封装组件
由于内容部分的样式不止是在文章分类页面会使用,其他页面展示内容部分也需要用,所以把他们封装成组件更好地复用
封装到:components文件夹/PageContainer.vue文件
写死的地方使用插槽:文章标题(props 父传子),文章内容(default默认插槽),右侧按钮(extra具名插槽)
1 | <script setup> |
使用组件改进页面
页面中直接使用测试 (unplugin-vue-components 会自动注册)
- 文章分类测试:
1 | <template> |
- 文章管理测试:
1 | <template> |
文章分类渲染和加载处理
api接口
无论是渲染什么都要发请求获取数据,所以要在api文件夹创建artical.js文件,从接口文档找-获取-文章分类
注意接口说明:获取分类每个用户之间是独立的
我这里把文章相关的所有接口都放进来了
1 | import request from '@/utils/request' |
页面调用接口获取数据
在页面中发送获取接口请求
1 | import {artGetChannelsService} from '../../api/article' |
页面渲染数据(表格组件)(用的多)
官方文档-Table表格
下面是官方文档的代码,下面的tableData对象包裹表格的数据
:data–表格名称
prop对象中找对应属性名渲染
label 列名
1 | <template> |
- 在文档下面-Table表格-单选有有说:如果需要显示索引,可以增加
- el-table-column,设置type属性为Index.即可显示从哪个1开始的序号,跟着它给表格加索引号
- 图标还是从组件库拿
加载处理
还是官方文档-反馈组件-Loading加载
- 定义变量,v-loading绑定
1 | const loading = ref(false) |
- 发送请求前开启,请求结束关闭
1 | const getChannelList = async () => { |
文章分类的添加编辑和删除
点击添加和编辑都会显示element plues弹层,放在一起实现
Dialog对话框组件
在官方文档-左侧导航栏反馈组件-Dialog对话框中
看他给的代码
点击按钮,把他dialogVisible的布尔值改成true就会展示对话框
记得去上面提供一个变量dialogVisible
:before-closes 是关闭对话框之前要做的事情(比如说关闭提醒),这里不要就把这局删了
1 | <el-button plain @click="dialogVisible = true"> |
把它封装成组件(放到article文件夹/components文件夹/ChannelEdit.vue中),组件对外暴露一个方法open,基于open传递的参数来区分是添加还是编辑
1 | //open({}) => 表单无需渲染,说明是添加 |
页面导入对话框组件
先导入,然后页面直接用(记得加个ref给这个组件做绑定)
如果是添加那么对话框里没东西,如果是编辑里面是row,就是 channelList 的一项
1 | import ChannelEdit from './components/ChannelEdit.vue' |
完善组件-添加和编辑
- 提供数据
回到封装对话框组件的地方,在里面提供要处理的数据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
20const 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> - 添加规则
给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'
}
]
} - open方法优化
新增 ormModel.value = { …row },但这里没有听懂,大概意思是要编辑的时候把原来有的内容显示到对话框里面1
2
3
4const open = (row) => {
dialogVisible.value = true
formModel.value = { ...row } // 添加 → 重置了表单内容,编辑 → 存储了需要回显的数据
} - 修改对话框题目
柑橘能否获取到id判断是编辑还是添加
提交前的校验
类似用户注册前最后的校验
先给form一个ref,然后对它进行判断
formModel.value是表单对象
1 | <el-form ref="formRef"> |
成功后让页面再请求一下数据,立刻更新页面
1 | //在调用组件的页面添加 |
成功后效果是这样的
文章分类的删除
调用ElMessageBox提示框确认是否删除
删除这里的后端接口参数是query,传Id过去,这里和前面的有区别
1 | const onDelChannel = async (row) => { |
文章管理页面
需求
- 文章列表渲染,上面有搜索功能,下面有分页跳转功能
- 添加文章功能(抽屉样式展示,包括文件上传)
- 编辑文章功能(公用抽屉)
- 删除文章功能
静态布局
常用的布局和按钮组件虽然官方文档有,但是不加以总是去翻,建议自己记住
article/components/ArticleManage.vue
一个表单区域一个表格区域,表单上方有搜索框,三个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>表格:还是先去接口看后台会返回什么样的数据,然后根据数据写表格
给表格做渲染记得跟:data1
<el-table :data="articleList">
先给一些假数据做渲染,从接口拿出后来给的返回的数据样式
1 | import { Delete, Edit } from '@element-plus/icons-vue' |
- 表格中要链接一样的效果用
还可以加属性删除下划线修改颜色 - 学用作用域插槽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
2
3
4
5
6<template>
<el-select>
<el-option></el-option>
<el-option></el-option>
</el-select>
</template> - 导入api接口方法
方法返回的数组存到res里面,1
2
3
4
5
6
7
8
9
10
11
12import { 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() - 基于数据让菜单动态渲染
注意用v-for一定要加:key1
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> - 数据传递(难点)
(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 | defineProps({ |
父传子传递下来的的给el-select菜单组件,所以给el-select绑定传递过来的值,要拆开v-model,同时值更新了要触发事件从组件往上传递给父亲
1 | <el-select |
获取文章分类完善和文章状态菜单
父组件完善
获取文章列表后来传递的参数和cateId进一步优化:不再使用cateId,变成获取请求参数对象params,在父组件定义参数绑定
1 | const params = ref({ |
使用子组件时,把原来v-model绑定的cateId修改成对象.参数
1 | <channel-select v-model="params.cate_id"></channel-select> |
同时发布状态的下拉菜单也和后端的请求绑定
1 | <el-select v-model="params.state"> |
文章列表渲染
- 看接口文档,获取接口
api/article.js然后再ArticleManage.vue文件导入接口1
2
3
4
5// 文章:获取文章列表
export const artGetListService = (params) =>
request.get('/my/article/list', {
params
})看接口文档返回的数据,然后声明total,下面进行渲染1
2import { artGetListService } from '@/api/article.js'
1
2
3
4
5
6
7
8
9
10const 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
} - 刷新页面,发现页面已经能够渲染了,但是获取到的发表时间和我们平时的时间格式不一样,这里需要微调
封装一个工具函数,utils文件夹下新建format.js文件然后在.vue文件导入并调用1
2
3
4import { dayjs } from 'element-plus'
export const formatTime = (time) => dayjs(time).format('YYYY年MM月DD日')1
2
3
4
5
6
7import { formatTime } from '@/utils/format.js'
//在发布时间的地方调用
<el-table-column label="发表时间" prop="pub_date">
<template #default="{ row }">
{{ formatTime(row.pub_date) }}
</template>
</el-table-column>
分页功能
- 官方文档-左侧导航栏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"
/> - 写处理分页逻辑的函数
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效果
- 先在上面定义loading
1
const loading = ref(false) // loading状态
- 当要发送请求获取数据时,刚发送时让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
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()
}
添加和编辑文章(共用一个抽屉)
抽屉组件
- 抽屉和dialog对话框差不多,但是对话框很小,抽屉组件能够放更多的东西,整体的思路是一样的
官方文档-左侧导航栏Feedback反馈组件-Drawer抽屉-查看源代码/el-drawer1
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> - 在页面使用(后面要封装成组件)
组件放在分页区域下面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
- 添加和编辑,可以共用一个抽屉,所以可以将抽屉封装成一个组件。组件对外暴露一个方法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>
- 通过 ref 绑定
1
2
3const articleEditRef = ref()
<!-- 弹窗 -->
<article-edit ref="articleEditRef"></article-edit> - 点击调用方法显示弹窗
1
2
3
4
5
6
7// 编辑新增逻辑
const onAddArticle = () => {
articleEditRef.value.open({})
}
const onEditArticle = (row) => {
articleEditRef.value.open(row)
}
- 完善组件内容
准备数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16const formModel = ref({
title: '',
cate_id: '',
cover_img: '',
content: '',
state: ''
})
const open = async (row) => {
visibleDrawer.value = true
if (row.id) {
console.log('编辑回显')
} else {
console.log('添加功能')
}
}准备 form 表单结构
1 | import ChannelSelect from './ChannelSelect.vue' |
- 一打开默认重置添加的 form 表单数据
1 | const defaultForm = { |
- 扩展 下拉菜单 width props
1 | defineProps({ |
文件上传
文件上传通常有两种方式
1.点击文件上传的加号后,就一定把图片上传到后台,后台返回一个url地址,存储这个文件的url地址,点击发布的时候提交。
弊端:可能产生垃圾图片
2.用户不管怎么选择上传文件都是本地的预览,只有点击发布或者草稿的时候才会真正地提交上传到后台。
弊端:一次上传的东西多可能回导致卡顿(添加loading效果)
到底选择哪个看看接口文档。官方文档有其他的信息-左侧导航栏上传部分
在这个项目中选择第二种上传方式
关闭自动上传,准备结构
此处需要关闭 element-plus 的自动上传,不需要配置 action 等参数,只需要做前端的本地预览图片即可,无需在提交前上传图标
语法:URL.createObjectURL(…) 创建本地预览的地址,来预览1
2
3
4
5
6
7
8
9
10
11import { 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>准备数据 和 选择图片的处理逻辑
1 | const imgUrl = ref('') |
- 样式美化
1 | .avatar-uploader { |
文章内容(富文本编辑器)
使用VueQuill富文本编辑器,官方文档查看
https://vueup.github.io/vue-quill/
- 安装包,注册成局部组件
1 | pnpm add @vueup/vue-quill@latest |
官方文档有在但文件中注册和全局注册,由于这里只是这个地方需要这个功能,所以用局部注册
1 | import { QuillEditor } from '@vueup/vue-quill' |
- 页面中使用绑定
1 | <div class="editor"> |
- 样式美化
1 | .editor { |
发布
- 封装添加接口
1 | export const artPublishService = (data) => |
- 注册点击事件调用
1 | <el-form-item> |
- 父组件监听事件,重新渲染
1 | <article-edit ref="articleEditRef" @success="onSuccess"></article-edit> |
添加完成后的内容重置
1 | const formRef = ref() |
编辑功能
封装接口
如果是编辑操作,一打开抽屉,就需要发送请求,获取数据进行回显
- 封装接口,根据 id 获取详情数据
1
2export const artGetDetailService = (id) =>
request.get('my/article/info', { params: { id } }) - 页面中调用渲染
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15const 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 | // 将网络图片地址转换为File对象 |
编辑文章功能
- 封装编辑接口
1
export const artEditService = (data) => request.put('my/article/info', data)
- 提交时调用
1
2
3
4
5
6
7
8
9
10
11
12const onPublish = async (state) => {
...
if (formModel.value.id) {
await artEditService(fd)
ElMessage.success('编辑成功')
visibleDrawer.value = false
emit('success', 'edit')
} else {
// 添加请求
...
}
}
删除文章
- 封装删除接口
1 | export const artDelService = (id) => request.delete('my/article/info', { params: { id } }) |
- 页面中添加确认框调用
1 | const onDeleteArticle = async (row) => { |
个人中心板块(借助AI大模型开发)
报错及解决
配置-配置代码检查工作流-暂存区eslint校验
我开始完成这部分代码后发现添加内容到暂存区后无法提交到仓库,终端git add . 然后git commit -m ‘vue3项目学习’报错内容如下,在文件夹中使用小乌龟工具提交也是出现一样的报错内容,无法提交:
1 | TypeError: Key "rules": Key "prettier/prettier": Could not find plugin "prettier". |
解决: 没找到我的prettier,开始chat建议我在vscode里面安装prettier插件,但是课程视频里不让使用这个vscode的插件,而是用创建项目在package.json里面提供的prettier,我看了我的这个文件里面依赖也是有"prettier": "^3.3.3"的,继续问,AI说是我的eslint.config.js中没有导入这个依赖,在里面加入两行代码然后成功提交
1 | import pluginPrettier from 'eslint-plugin-prettier' // 引入 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重新打开项目发现不报错了,重启是个好方法!