特点:
create-vue是Vue官方新的脚手架工具,底层切换到了 vite (下一代前端工具链),为开发提供极速响应
前置条件 - 已安装16.0或更高版本的Node.js
powershell中输入node -v
执行如下命令,这一指令将会安装并执行 create-vue
npm init vue@latest
项目名字:vue3-demo
除了ESLint剩下都No
创建成功后cd 到 vue3-demo
npm install
npm run dev
vite.config.js - 项目的配置文件 基于vite的配置
package.json - 项目包文件 核心依赖项变成了Vue3.x 和 vite
main.js - 入口文件 createApp函数创建应用实例
//new Vue() 创建一个应用实例 => createApp()
// createRouter() createStore()
// 将创建实例进行了封装,保证每个实例的独立封闭性
app.vue - 根组件 SFC单文件组件 script - template - style
变化一: 脚本 script 和 模板 template 顺序调整
变化二: 模板 template 不再要求唯一根元素
变化三: 脚本 script 添加 setup 标识支持组合式 API
index.html - 单页入口 提供 id 为 app 的挂载点
禁用Vetur 安装volar(Vue Language Eeature)
vue2 vue3区别:
回忆:之前 vue 文件内容架子是 template + script + style(scoped) 结构 js 样式
如今是 script(set up) + template + style(scoped) js 结构 样式
之前组件使用方法:导入注册 + 使用
如今: 导入 + 使用
import ... from + template中使用
vue2中template要求唯一根元素
vue3中不要求(header , main..)
加上setup 允许在script中直接编写组合式API
写法
执行时机
在beforeCreate钩子之前执行 非常早
在setup函数中写的数据和方法需要在末尾以对象的方式return,才能给模版使用
定义+导出+使用
定义
数据形式:const 名 = 值
函数形式:const 名 = () =>{ }
导出 (在script后加setup可以省略此步)
return{名,名…}
使用
{{名}}…
script标签添加 setup标记,不需要再写导出语句,默认会添加导出语句
接受对象类型数据的参数传入并返回一个响应式的对象
vue3中实现 响应式 对象类型数据 的方法,
应用:比如点击按钮加1,没有reactive进行包起来就不能对数字加1
步骤:从vue导入reactive + reactive包对象 + 使用
此时点击按钮 666 +1
接收 简单类型 或者 对象类型 的数据传入并返回一个响应式的对象
注意:1. js中访问数据,需要.value
2.template中,.value不用加(帮我们扒了一层)
步骤:导入ref + ref包数据 + 数据.value使用(js中,template不加)
计算属性基本思想和Vue2保持一致,组合式API下的计算属性只是修改了API写法
const 名 =computed( () =>{
return 计算公式
} )
步骤: 导入ref和computed + 定义计算函数用computed包起来 + 使用函数名
过滤数组案例 筛选大于二的
原始数组:{{ list }}
大于二得到的数组:{{ newList }}
侦听一个或者多个数据的变化,数据变化时执行回调函数,俩个额外参数 immediate控制立刻执行,deep开启深度侦听
步骤:导入ref,watch + 写事件函数(比如点击改名字) + 监听改前后的名字watch
watch( const的值,(新,老) => { } )
watch( [const的值1,const值2],(新,老) => { },{ immediate/deep:true } )
watch( (监听的值ref对象"无.value",(新值,旧值)) => {
} )
侦听多个数据(多个count),第一个参数改写成数组的写法
watch括号里边: 值是对象的话写对象不用加.velue
[值1,值2],(新,老)
<script setup>
// 1. 导入watch
import { ref, watch } from 'vue'
const count = ref(0) 两个都是简单的
const name = ref('cp')
// 2. 调用watch 侦听变化
watch([count, name], (newValue,oldValue)=>{
console.log(`count或者name变化了${oldValue},${newValue}`)
})
</script>
使用方法:在watch函数后面多加一个对象,写上immediate:true
执行单次案例后面加上immediate:true 一进页面控制台会显示 0 undefined
在侦听器创建时立即出发回调,响应式数据变化之后继续执行回调
通过watch监听的ref对象默认是 浅层侦听 的,
直接修改嵌套的对象属性不会触发回调执行,需要开启deep
写法:obj可变具体对象名,key可变具体键值对
watch(
()=>obj.value.key,
(新,老)=>console.log(新,老)
)
Vue3的生命周期API(选项式 VS 组合式)
选项式API 组合式API
beforeCreate/created setup
创建响应式数据/
发送初始化渲染请求 在setup里直接请求+调用
beforeMount onBeforeMount
数据已经渲染完成
mounted onMounted
操作dom 渲染之后才可以(操作dom:修改数据或者方法…)
beforeUpdate onBeforeUpdate
updated onUpdated
beforeDestroy onBeforeUnmount
destroyed onUnmounted
- 导入生命周期函数
- 执行生命周期函数,传入回调
<scirpt setup>
import { onMounted } from 'vue'
onMounted(()=>{
// 自定义逻辑
})
</script>
生命周期函数执行多次的时候,会按照顺序依次执行
<scirpt setup>
import { onMounted } from 'vue'
onMounted(()=>{
// 自定义逻辑
})
onMounted(()=>{
// 自定义逻辑
})
</script>
1.父准备数据 const 父 =ref(‘李白’)
2.在子标签上加属性的 传值 :子=“父”
3. 子中props接收数据 vue3中不能写props:[]
借助编译器宏 defineProps()
const props = defineProps({
子:String
})
4. 渲染即可
{{ 子 }} 无需props.子
例子:孩子花钱
子组件内部
1.写点击事件,@click=“spend()”
2.定义emit
(需要 e m i t 触发事件,给父发送消息通知 , 以前是 t h i s . emit触发事件,给父发送消息通知,以前是this. emit触发事件,给父发送消息通知,以前是this.emit,现在没有,要先自借助编译器宏)
const emit = defineEmits([‘wantSpend’])
3.写方法
const spend=()=>{
emit(‘wantSpend’,5)
}
父组件中
const money=ref(100)
1.给子组件标签通过@绑定事件 此时’5’传了过来
@wantSpend =dieGive
2.爹把参数放到方法里改
const dieGive=(newValue)=>{
money.value = newValue
}
概念:通过 ref标识 获取真实的
dom对象 或者 组件实例对象(一个组件获取另一个组件的数据或者方法)
案例:点击获取焦点 获取真实的 dom对象
实现步骤:
- 调用ref函数生成一个ref对象
import ref from ‘vue’
const 绑定属性名 =ref(null)
- 标签处通过ref标识绑定ref对象
ref = ‘属性名’
3.注册点击事件及方法
@click=‘focuFn’
const focuFn=()=>{
属性名.value.focus()
}
默认情况下在
案例:点击’获取组件数据’时控制台显示子组件数据
步骤:1.父中ref个属性名
2.子组件标签上ref=“属性名”
3.注册点击事件及方法
@click='getState()'
const getState=()={
console.log(属性名.value.数据名/方法名)
}
子:4.利用编译器宏把允许被外部访问的数据或方法放进去
数据:const count=999
const sayHi=()=>{
console.log(‘你好’)
}
打开权限允许访问:
difineExpose({
count,
sayHi
})
顶层组件向任意的底层组件传递数据和方法,实现跨层组件通信
实现步骤
0. 父导入 provide 或子孙导入 inject 函数
- 顶层通过
provide
提供数据
provide(‘属性’,值) :值 无.value
- 底层通过
inject
接收数据
const 子属性 = inject(‘属性’)
3.渲染
{{ 子属性 }}
注:不是自己的组件不能直接改,想改看 ##3
案例:爹的500传下去儿想改
在调用provide函数时,第二个参数设置为ref对象
newCount实际为底层的数据参数
顶层:
provide(‘爹方法名’,(newCount)=>{
爹数据.value = newCount
})
底层:
count 儿方法名 = inject(‘爹方法名’)
@click = ‘changeState’
const changeState=()=>{
儿方法名(参数)
}
顶层组件可以向底层组件传递方法,底层组件调用方法修改顶层组件的数据
案例:父给儿、孙传数据 关键词:provide inject
- 父导入 provide 或子孙导入 inject 函数
- 顶层通过
provide
提供数据
provide(‘属性’,值)
- 底层通过
inject
接收数据
const 子属性 = inject(‘属性’)
3.渲染
{{ 子属性 }} */
Vue3.3 新特性-defineOptions 想给组件起名字defineOption({ name:‘’ })
背景说明:
有
但是用了
为了解决这一问题,引入了 defineProps 与 defineEmits 这两个宏。但这只解决了 props 与 emits 这两个属性。
如果我们要定义组件的 name 或其他自定义的属性,还是得回到最原始的用法——再添加一个普通的
这样就会存在两个
所以在 Vue 3.3 中新引入了 defineOptions 宏。顾名思义,
主要是用来定义 Options API 的选项。可以用 defineOptions 定义任意的选项,
props, emits, expose, slots 除外(因为这些可以使用 defineXXX 来做到)
Vue3.3新特性-defineModel 快速实现双向绑定
在Vue3中,自定义组件上使用v-model, 相当于传递一个modelValue属性,同时触发 update:modelValue 事件
我们需要先定义 props,再定义 emits 。其中有许多重复的代码。如果需要修改此值,还需要手动调用 emit 函数。
于是乎 defineModel 诞生了。
步骤: vite文件配 defineModel:true + A组件v-model=“值” + (B组件中导入 + const 属性=defineModel() )
- B使用’属性’
生效需要配置 vite.config.js
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue({
script: {
defineModel: true
}
}),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})
Vue3 状态管理 - Pinia
1. 什么是Pinia
Pinia 是 Vue 的专属的最新状态管理库 ,是 Vuex 状态管理工具的替代品
2. 手动添加Pinia到Vue项目
后面在实际开发项目的时候,Pinia可以在项目创建时自动添加,现在我们初次学习,从零开始:
- 使用 Vite 创建一个空的 Vue3项目
npm init vite@latest
名字:vue3-pinia-demo
eslint yes,其他全NO
+
cd 到项目中
+
安装依赖: npm i
+
启动项目:npm run dev
之前vuex库中有 5个核心概念:state数据 mutations方法 action异步实现 getters计算 modules小库
如今是 3 个 : state action getters
- 按照官方文档安装 pinia 到项目中
使用pinia: (新电脑)
安装pinia:
yarn add pinia 或者 npm install pinia
+
创建pinia实例 main.js
import {createPinia} from ‘pinia’
const pinia=createPinia() // 创建pinia实例
const app=createApp(App) // 创建根实例
app.use(pinia).mount(‘#app’) //pinia插件的安装配置,视图的挂载
3. Pinia基础使用 AK47 vue官网-生态-pinia-定义store vue3库 相当于xuex
案例:计数器案例
-
建store文件
src/store/库名.js
-
导入defineStore + 定义store + 输出数据/方法
import { defineStore } from ‘pinia’
import { ref } from ‘vue’
//定义Store
// defineStore(仓库的唯一标识仓库名,()=<{…}>)
export const 组件中导入名 = defineStore(‘库名’,()=>{
//其他配置 选项式/组合式(常用)
const count = ref(0) //定义数据 ref就是state属性
const addCount =()=>{
count.value++ // 普通函数 + 异步 写法
}
//计算属性写法:
const 名 =computed( ()=>count.value*2 )
return { count,adedCount,名 } //输出数据或方法
})
- 组件 导入’1中的组件中导入名’ + 接收’组件中导入名’ + 使用store
import {导入名} from ‘@/src/store/库名’
const 接收名 = 导入名()
{{ 接收名.count }}
@click=“接收名.库内方法名addCount”
4. getters实现
Pinia中的 getters 直接使用 computed函数 进行模拟, 组件中需要使用需要把 getters return出去
如上3
5. action异步实现
方式:异步action函数的写法和组件中获取异步数据的写法完全一致
-
接口地址:http://geek.itheima.net/v1_0/channels
-
请求方式:get
-
请求参数:无
需求:在Pinia中获取频道列表数据并把数据渲染App组件的模板中
安装axios: yarn add axios 或 npm i axios
重新启动项目:yarn dev 或 npm run dev
1.建文件 + 导入defineStore + 定义Store + 输出数据/方法
import {defineStore} from ‘pinia’
import {ref} from ‘vue’
export const 组件中导入名 = defineStore(‘库名’,()=>{
//声明数据
const 空数组 = ref([])
//声明操作数据的方法
const 请求方法 = async ()=>{
//支持异步
const res=await axios({ //可以解构res: {data:{data}}
method:'get',
url:'http://geek.itheima.net/v1_0/channels'
})
console.log(res)
空数组.value=res.data.data.channels //channel是控制台得到的数组
}
//声明getters相关
return{
数据,方法
}
})
2.组件中: 导入’1中组件中导入名’ + 接收’组件中导入名’ + 点击事件
import {组件中导入名} from ‘@/src/store/库名.js’
const 接收名 = 组件中导入名()
@click=“接收名.库内方法名” //此时控制台有数据
v-for渲染:
- {{ item.name }}
6. storeToRefs解构工具函数 使用store时 解构数据的问题:破坏响应性
数据较多时解构会比较方便
使用storeToRefs函数可以辅助保持数据(state + getter)的响应式解构
组件中解构库中的数据:
import {storeToRefs} from 'pinia'
import {组件中导入名} from '@/src/store/库名.js'
const 接收名 = 组件中导入名()
const {库中数据} = storeToRefs(接收名) //数据需要解构,方法不需要
const 库中方法名 = 接收名
{{库中数据}} //此时不需要 接收名.库中数据
@click="方法"
7. Pinia的调试
Vue官方的 dev-tools 调试工具 对 Pinia直接支持,可以直接进行调试
检查 - vue - App(Pinia)
8. Pinia持久化插件 新电脑 安装插件直接持久化贼猛
官方文档:https://prazdevs.github.io/pinia-plugin-persistedstate/zh/
- 安装插件 pinia-plugin-persistedstate
npm i pinia-plugin-persistedstate
- 使用 main.js 导入插件 + .use
import persist from 'pinia-plugin-persistedstate'
...
app.use(createPinia().use(persist))
- 配置 store/counter.js
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
export const useCounterStore = defineStore('counter', () => {
...
return {
count,
doubleCount,
increment
}
}, {
persist: true //开启当前模块的持久化
})
- 其他配置,看官网文档即可
{
persist: {
key:‘默认store.$id’,
storage:‘默认localStorage’,
paths:[‘默认全部数据持久化’],
}
}
后台数据管理系统 - 项目架构设计 AK47
在线演示:https://fe-bigevent-web.itheima.net/login
接口地址
接口文档: https://apifox.com/apidoc/shared-26c67aee-0233-4d23-aab7-08448fdf95ff/api-93850835
接口根路径: http://big-event-vue-api-t.itheima.net
本项目技术栈 本项目技术栈基于
ES6、
vue3、 CompositionAPI
pinia、 库,持久化
vue-router 、
vite 、axios 和
element-plus (表单校验,表格处理,组件封装)
新东西:
pnpm 包管理升级 (创建项目快)
Eslint + prettier 更规范的配置
husky (Git hooks工具) 代码提交前进行校验的工具
请求模块设计
VueRouter4 路由设计
AI 大模型开发一整个项目模块 (掌握最新的开发方式)
项目页面介绍
登录页(注册) + 文章分类 + 文章管理 + 个人中心
pnpm 包管理器 - 创建项目 新电脑
一些优势:比同类工具快 2倍 左右、节省磁盘空间… https://www.pnpm.cn/
安装方式: 在磁盘的某个文件安装
npm install -g pnpm
创建vue3项目:
pnpm create vue
name: vue3-big-event-admin
选项Yes的有: VueRouter Pinia ESLint Prettier
安装依赖:
pnpm install
启动pnpm项目
pnpm dev
拓展pnpm 常用命令
安装axios
pnpm add axios -D 依赖冲突加-D
移除axios
pnpm remove axios
ESLint & prettier 配置代码风格 新电脑
环境同步:
- 安装了插件 ESlint,开启保存自动修复
- 禁用了插件 Prettier,并关闭保存自动格式化
// ESlint插件 + Vscode配置 实现自动格式化修复
"editor.codeActionsOnSave": {
"source.fixAll": true
},
"editor.formatOnSave": false,
配置文件 .eslintrc.cjs
-
prettier 风格配置 https://prettier.io
- 单引号
- 不使用分号
- 每行宽度至多80字符
- 不加对象|数组最后逗号
- 换行符号不限制(win mac 不一致)
-
vue组件名称多单词组成(忽略index.vue)
-
props解构(关闭)
添加插件ESLint
改设置文件(vscode左下角setting + 右上角打开设置)
添加以下配置
//ESLint插件 + Vscode配置 实现自动格式化修复
"editor.codeActionsOnSave": {
"source.fixAll": true
},
// 关闭保存自动格式化
"editor.formatOnSave":false
改目录中.eslintrc.cjs文件
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 解构的校验
// 添加未定义变量错误提示,[email protected] 关闭,这里加上是为了支持下一个章节演示。
'no-undef': 'error'
}
基于husky的提交代码前检查工作流 新电脑
husky 是一个 git hooks 工具 ( git的钩子工具,可以在特定时机执行特定的命令 )
没有git就去pc.qq下载 以下命令全是git bash终端
husky 配置
-
git初始化 git init
-
初始化 husky 工具配置 https://typicode.github.io/husky/
pnpm dlx husky-init && pnpm install
- 修改 .husky/pre-commit 文件
pnpm lint 暂时的,以下面的为准
**问题:**pnpm lint默认进行的是全量检查,耗时问题,历史问题。
暂存区新添加的代码的 eslint 校验
lint-staged 配置
- 安装
pnpm i lint-staged -D
- 配置
package.json
{
"lint-staged": {
"*.{js,ts,vue}": [
"eslint --fix"
]
}
}
scripts配置项后面加
{
"scripts": {
"lint-staged": "lint-staged"
}
}
- 修改 .husky/pre-commit 文件
pnpm lint-staged
调整项目目录
默认生成的目录结构不满足我们的开发需求,所以这里需要做一些自定义改动。主要是两个工作:
- 删除初始化的默认文件
以下只留文件夹:
assets,components,stores,views
- 修改剩余代码内容
src/router/index.js
的routes:[]内清空
App.vue中只留空架子
src/main.js
中导入css文件删掉
- 新增调整我们需要的目录结构
utils 工具函数
api 请求封装
- 拷贝初始化资源文件,安装预处理器插件
将项目需要的全局样式 和 图片文件,复制到 assets 文件夹中, 并将全局样式在main.js中引入
import '@/assets/main.scss'
- 安装 sass 依赖
pnpm add sass -D
VueRouter4 路由代码解析 点击跳转页面
// createRouter 创建路由实例,===> new VueRouter()
// 1. history模式: createWebHistory() 常用
// 2. hash模式: createWebHashHistory()
区别有无#
vue2 与 vue3 点击跳转不同:
如果 @click="$router.push('/路径path')" 生效
但是,@click="tiaoZhuan"
const tiaoZhuan =()=>{
this.$router.push('/路径path') 不成立
}
vue3 CompositionAPI中获取路由
import { useRouter,useRoute } from 'vue-router'
1. 获取路由对象 router useRouter
const router = useRouter()
//点击跳转
const 方法=()=>{
router.push('/路径path')
}
2. 获取路由参数 route useRoute 点击时携带的参数
const route = useRoute()
import.meta.env.BASE_URL 是路径前缀,可在vite文件配置 base:'/前缀路径' (pluugins同级)
// vite 的配置 import.meta.env.BASE_URL 是路由的基准地址,默认是 ’/‘
// https://vitejs.dev/guide/build.html#public-base-path vite官网说明
基础代码解析
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history:createWebHistory(import.meta.env.BASE_URL),
routes: []
})
export default router
引入 element-ui 组件库 新电脑 gitbash终端
官方文档: https://element-plus.org/zh-CN/
- 安装
pnpm add element-plus
自动按需:
- 安装插件
pnpm add -D unplugin-vue-components unplugin-auto-import
- 然后把下列代码插入到你的
Vite
或 Webpack
的配置文件中 vite.js
...
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
AutoImport({
resolvers: [ElementPlusResolver()]
}),
Components({
resolvers: [ElementPlusResolver()]
})
]
})
- 直接使用
Primary
Success
Info
Warning
Danger
...
**彩蛋:**默认 components 下的文件也会被自动注册~不用import导入,直接 <组件>组件>
Pinia - 构建用户user仓库 和 持久化 新电脑
pinia官方文档:https://prazdevs.github.io/pinia-plugin-persistedstate/zh/
1. 安装插件 pinia-plugin-persistedstate
创建项目时yes pinia就不用
pnpm add pinia-plugin-persistedstate -D
2. 使用 main.js 创建项目时yes pinia就不用
import persist from 'pinia-plugin-persistedstate'
...
app.use(createPinia().use(persist))
3. 配置 stores/user.js
1.导入‘定义库’和ref(响应数据) +
2.export定义模块 +
3.return数据/方法 +
4.persist持久化
import { defineStore } from 'pinia'
import { ref } from 'vue'
// 用户模块 useUserStore 是组件内导入名
export const useUserStore = defineStore('big-user',() => {
const token = ref('') // 定义 token
const setToken = (newToken) => {
token.value=newToken
}// 设置 token
return { token, setToken }
},
{
persist: true // 持久化
}
)
4.使用库内数据/方法 导入‘组件内导入名’ + 接受名=‘组件内导入名’() + 使用/渲染
import {useUserStore} from '@/stores/库名.js'
count userStore = useUserStore()
{{ userStore.数据}}
@click="userStore.setToken('参数')"
Pinia - 配置仓库统一管理 解决路径较长防写错 新电脑
pinia 独立维护 防止路径写错
- 现在:初始化代码在 main.js 中,仓库代码在 stores 中,代码分散职能不单一
- 优化:由 stores 统一维护,在 stores/index.js 中完成 pinia 初始化,交付 main.js 使用
步骤:
main.js 中 pinia 相关的放到 stores/index.js
import { createPinia } from 'pinia'
import persist...
app.use(createPinia().use(persist))
// createPinia().use(persist)放到stores/index.js
创建stores/index.js,加内容并导出
import { createPinia } from 'pinia'
import persist...
const pinia=createPinia()
pinia.use(persist)
export default pinia
main.js中导入pinia
import pinia from ‘@/stores/index’
app.use(pinia)
重新启动项目
pnpm dev
仓库 统一导出 某个组件下想调用两个库
- 现在:使用一个仓库 import { useUserStore } from ./stores/user.js
不同仓库路径不一致
- 优化:由 stores/index.js 统一导出,导入路径统一 ./stores
,而且仓库维护在 stores/modules 中
import {useUserStore} from '@/stores/modules/user'
import {useCountStore} from '@/stores/modules/counter'
理想型:
import {useUserStore,useCountStore} from '@/stores'
实现:index.js中添加
export * from './modules/user'
export * from './modules/counter'
数据交互 - 请求工具设计 axios封装 新电脑
1. 创建 axios 实例
们会使用 axios 来请求后端接口, 一般都会对 axios 进行一些配置 (比如: 配置基础地址等)
一般项目开发中, 都会对 axios 进行基本的二次封装, 单独封装到一个模块中, 便于使用
- 安装 axios
pnpm add axios
-
新建 utils/request.js
封装 axios 模块
利用 axios.create 创建一个自定义的 axios 来使用
http://www.axios-js.com/zh-cn/docs/#axios-create-config
创建实例:
const instance = axios.create({
baseURL: 'https://some-domain.com/api/',
timeout: 1000,
headers: {'X-Custom-Header': 'foobar'}
});
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
2. 完成 axios 基本配置 请求库前提 @/utils/request
import { useUserStore } from '@/stores/user'
import axios from 'axios'
import router from '@/router'
import { ElMessage } from 'element-plus'
const baseURL = 'http://big-event-vue-api-t.itheima.net'
const instance = axios.create({
baseURL,
timeout: 100000
})
instance.interceptors.request.use(
(config) => {
// 在发送请求之前做些什么,用库的token验证
const userStore = useUserStore()
if (userStore.token) {
config.headers.Authorization = userStore.token
}
return config
},
// 对请求错误做些什么
(err) => Promise.reject(err)
)
instance.interceptors.response.use(
(res) => {
// 对响应数据做点什么
if (res.data.code === 0) {
return res
}
ElMessage.error(res.data.message || '服务异常')
return Promise.reject(res.data)
},
(err) => {
// 对响应错误做点什么
//错误的特殊情况=>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 }
首页整体路由设计
实现目标:
- 完成整体路由规划【搞清楚要做几个页面,它们分别在哪个路由下面,怎么跳转的…】
- 通过观察, 点击左侧导航, 右侧区域在切换, 那右侧区域内容一直在变, 那这个地方就是一个路由的出口
- 我们需要搭建嵌套路由
目标:
- 把项目中所有用到的组件及路由表, 约定下来
约定路由规则
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
二级路由
明确了路由规则,可以全部配完,也可以边写边配。
配置一级路由
@/views下新建以及每个路由文件(文件名全小写),
且在每个路由文件下再创建一个主核心文件index.vue(export default里加name,名字如LayoutIndex)
@/router/index.js里添加:
import (可现命名)组件名 from ‘@/views/.vue文件所在文件,可以省略.vue文件不写’(二级路由要写)
{path:‘设置地址栏路径比如/rearch’,component:上面import的组件名} */
二级路由配置:创文件 + 配路由 + 配出口
配置二级路由(即导航部分)
创建文件
首页的文件下新建四个二级路由文件 因为是首页四个导航的二级路由,所以是首页的chidren
views/layout/xxx.vue
+
配路由
router/index.js的layout后边加上children:[{path:‘/xxx’,component:组件名 },{ }…]
+
配出口(2个:App.vue + chidren所在组件)
在配置chidren的组件上加 布局架子路由组件加
重定向 默认页面
配路由处多配一项
{path:‘/’,redirect:‘/默认页面的路径’}
404 用户搜索不存在的路径 找不到网页
登录注册页面 [element-plus 表单 & 表单校验]
注册登录 静态结构 & 基本切换(注册页与登录页)
网页布局: 左边图片 + 右边登录信息
- 安装 element-plus 图标库
pnpm i @element-plus/icons-vue
- 静态结构准备
/* el-row表示一行,一行分成24分
el-col表示列
(1):span="12" 代表在一行中占用12分,也就是网页的一半
(2):span="6" 表示在一行中,占6份
(3):span="3" 代表在一行中,左侧margin份数:离左侧有多少
el-form 整个表单组件
el-form-item 表单的一行
h1、el-input 表单元素(标题,输入框)
:prefix-icon="" element-plus的图标
接口文档: https://apifox.com/apidoc/shared-26c67aee-0233-4d23-aab7-08448fdf95ff/api-93850835
【需求】注册页面基本校验
1. 用户名非空,长度校验5-10位
2. 密码非空,长度校验6-15位
3. 再次输入密码,非空,长度校验6-15位
【进阶】再次输入密码需要自定义校验规则,和密码框值一致(可选)
注意:配规则
form标签:
1.:model="对象"
2.:rules="规则对象名"
imput处:
3.v-model="对象.接口变量"
每一个填写的form-item处:
4.prop="规则名"
13小点总结:
按照官网所写,一个表单数据绑定一个变量(name),
如果出现多个数据这么写会非常麻烦,
所以把他们方到同一个对象中,
这些变量名都需要看接口文档,以便后期提交
将来 :model="对象名" + v-model="对象名.变量"
24小店总结:
:rules='规则对象名' 和 prop="规则名" 对应
规则写法:
const 规则对象名 ={
规则名:[{ 未输入时的规则 },{ 输入后的规则 }]
}
自定义校验(常用于再次输入密码):官网有
一个属性加在上面的规则对象里:validitor:(rule,value,callback)=>{写函数}
*/
注册
注册
← 返回
登录
记住我
忘记密码?
登录
注册 →
注册功能
实现注册校验
接口文档: https://apifox.com/apidoc/shared-26c67aee-0233-4d23-aab7-08448fdf95ff/api-93850835
【需求】注册页面基本校验
- 用户名非空,长度校验5-10位
- 密码非空,长度校验6-15位 pattern:*/S{正则}$\
- 再次输入密码,非空,长度校验6-15位
【进阶】再次输入密码需要自定义校验规则,和密码框值一致(可选)
注意:
form标签:
1.:model=“对象”
2.:rules=“规则对象名”
imput处:
3.v-model=“对象.接口变量”
每一个填写的form-item处:
4.prop=“规则名”
13小点总结:
按照官网所写,一个表单数据绑定一个变量(name),
如果出现多个数据这么写会非常麻烦,
所以把他们方到同一个对象中,
这些变量名都需要看接口文档,以便后期提交
将来 :model=“对象名” + v-model=“对象名.变量”
24小店总结:
:rules=‘规则对象名’ 和 prop=“规则名” 对应
规则写法:
const 规则对象名 ={
规则名:[{ 未输入时的规则 },{ 输入后的规则 }]
}
自定义校验(常用于再次输入密码):官网有
一个属性加在上面的规则对象里:validitor:(rule,value,callback)=>{写函数}
repassword:[{},{}, 第一第二个对象与password相同
{
validitor:(rule,value,callback)=>{
<!-- 判断value与from中的password值是否相同 -->
if (value === formModel.password.value){
callback()
}else{
callback(new Error('您输入的密码不一致'))
}
},
trigger:'blur' /* 失焦校验 */
}
]
- model 属性绑定 form 数据对象 加在表单外层
这些变量要到接口文档获取(将来要提交后台)
const formModel = ref({
username: '',
password: '',
repassword: ''
})
- rules 配置校验规则 加在表单外层
要基于1 2点 :model的变量 配的规则
const rules = {
username: [
/* 非空校验:未输入时的东西,校验提示,啥时候校验 */
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 5, max: 10, message: '用户名必须是5-10位的字符', trigger: 'blur' }
/* 字符长度最短5,最长10,提示,输入完(焦点离开)校验 */
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{
pattern: /^\S{6,15}$/, /*密码属性patten,值需要写正则表达式: /*\S{ 开始,结束 }$/ */
message: '密码必须是6-15位的非空字符',
trigger: 'blur'
}
],
repassword: [
{ required: true, message: '请再次输入密码', trigger: 'blur' },
{
pattern: /^\S{6,15}$/,
message: '密码必须是6-15的非空字符',
trigger: 'blur'
},
{ /* 二次校验属于自定义校验,看element-plus官网 */
validator: (rule, value, callback) => {
if (value !== formModel.value.password) {
callback(new Error('两次输入密码不一致!'))
} else {
callback()
}
},
trigger: 'blur'
}
]
}
- v-model 绑定 form 数据对象的子属性 写在每个表单填写处
...
(其他两个也要绑定)
- prop 绑定校验规则
...
(其他两个也要绑定prop)
注册成功前的预校验 解决注册时未填写但是点注册的提示 再输入的validate()方法通过就发请求
注册成功之前,先进行校验,校验成功 -》 请求 ,校验失败-》自动提示
官网Form Exposes暴露出去的方法 官网-表单-右侧FormExpose
so在注册的el-from中获取表单组件
const 获取名 = ref(null) + 标签ref=“获取名”
点击注册前校验
@click=“zhuC”
const zhuC=()=>{
await 获取名.value.validate()
console.log(‘开始注册请求’)
}
需求:点击注册按钮,注册之前,需要先校验(信息校验通过后才可以点击)
- 通过 ref 获取到 表单组件
const form = ref()
- 注册之前进行校验
注册
const register = async () => {
await form.value.validate()
console.log('开始注册请求')
}
封装 api 实现注册功能
user.js中导入request请求+导出方法 + 组件内导入+调用方法
需求:封装注册api,进行注册,注册成功切换到登录
- 新建 api/user.js 封装
导入请求前提 + 导出方法即可
import request from '@/utils/request'
export const userRegisterService = ({ username, password, repassword }) =>
request.post('/api/reg', { username, password, repassword })
参数解构(第一个解构是咱的,第二个是接口需要的)+
{
return request.post(‘/api/reg’, { username, password, repassword })
}
简写如上
- 注册登录页面中调用
import {userRegisterService} from '@/api/user.js'
const register = async () => {
await form.value.validate()
await userRegisterService(formModel.value)
//Feedback反馈组件 - Massage消息提示
ElMessage.success('注册成功')
// 切换到登录
isRegister.value = false
}
- eslintrc 中声明全局变量名, 解决 ElMessage 未导入的报错问题 新电脑 无需导入组件即可直接使用
导入element-plus组件
module.exports = {
...
globals: {
ElMessage: 'readonly',/* 值是全局的意思,以后需要的组件都需要写进来就不会报错 */
ElMessageBox: 'readonly',
ElLoading: 'readonly'
}
}
注册总结 : 静态 + 校验 + 注册前的预处理 + 发注册请求 + 跳转登录(改布尔值) AK47
登录功能
实现登录校验
【需求说明】给输入框添加表单校验
- 用户名不能为空,用户名必须是5-10位的字符,失去焦点 和 修改内容时触发校验
- 密码不能为空,密码必须是6-15位的字符,失去焦点 和 修改内容时触发校验
操作步骤: 可共用属性 方法
- model 属性绑定 form 数据对象,直接绑定之前提供好的数据对象即可
- rules 配置校验规则,共用注册的规则即可
- v-model 绑定 form 数据对象的子属性
- prop 绑定校验规则
...
- 切换的时候重置账号密码
监视这个切换值,变化了咋样
将上面的三绑数据 对象 清空
watch(isRegister, () => {
formModel.value = {
username: '',
password: '',
repassword: ''
}
})
登录前的预校验 & 登录成功
【需求说明1】登录之前的预校验
- 登录请求之前,需要对用户的输入内容,进行校验
- 校验通过才发送请求
【需求说明2】登录功能
- 封装登录API,点击按钮发送登录请求
- 登录成功存储token,存入pinia 和 持久化本地storage
- 跳转到首页,给提示
【测试账号】
-
登录的测试账号: shuaipeng
-
登录测试密码: 123456
PS: 每天账号会重置,如果被重置了,可以去注册页,注册一个新号
实现步骤:
- 注册事件,进行登录前的预校验 (获取到组件调用方法)
const login = async () => {
await form.value.validate()
console.log('开始登录')
}
- 封装接口 API api/user.js文件
export const userLoginService = ({ username, password }) =>
request.post('api/login', { username, password })
- 调用库 以及调用库内方法将 token 存入 pinia 并 自动持久化本地
import { userLoginService } from '@/api/user.js'
import { useUserStore } from '@/stores/modules/user.js'
const userStore = useUserStore()
const router = useRouter()
const login = async () => {
await form.value.validate()
const res = await userLoginService(formModel.value)
console.log(res) /* 此时发现接口会返回token值,调进user库,用那里的方法 */
userStore.setToken(res.data.token)
ElMessage.success('登录成功')
router.push('/')
}
登录总结AK47
共用校验属性/规则 +
切换时清空表单(watch监听切换的值) +
注册前的预处理 +
发注册请求(携带请求返回来的token放到api/user.js)+
跳转到"/"页
首页 layout 架子 [element-plus 菜单组件]
基本架子拆解
架子组件列表:
el-container
- el-aside 左侧
- el-menu 左侧边栏菜单
·
- el-container 右侧
- el-header 右侧头部
- el-dropdown
- el-main 右侧主体
- router-view
/*
el-menu 整个菜单组件
active-text-color="#ffd04b" 点击时字体颜色
:default-active="$route.path" 配置默认高亮的菜单项
router router选项开启,下面el-menu-item 的index 是点击跳转的路径(相当于vant组件的to)
item 的index 是点击跳转的路径
文章分类
文章管理
个人中心
基本资料
更换头像
重置密码
黑马程序员:小帅鹏
基本资料
更换头像
重置密码
退出登录
大事件 ©2023 Created by 黑马程序员
登录访问拦截 不登录输网址进不来 未完成
未登录自动跳转登录页 172
router/index里加
需求:只有登录页,可以未授权的时候访问,其他所有页面,都需要先登录再访问
vueRouter 前置守卫
根据 有无router+去的是不是除了登录页的页面 来判断,true就去登录页
import { useUserStore } from '@/stores'
// 登录访问拦截 =>默认直接放行
//根据返回值决定,是放行还是拦截
//返回值:
//1. undefined/true 直接放行
//2. false 拦回from的地址页面
//3. 具体路径 或 路径对象 拦截到对应的地址
// '/login' 或 {name:'login'}
router.beforeEach((to) => {
const userStore = useUserStore()
/* 无token且去的是非登录页,拦截到登录 */
if (!userStore.token && to.path !== '/login') return '/login'
})
用户基本信息获取&渲染 已完成
封装接口(api/user)-发请求(user的)
export const 方法名=()=>request.method(‘url’)
+
pinia库内
导入请求库(api/user)
+
定义空对象(响应式 ref({}) )
+
定义个方法,调用请求方法
(async await 请求方法) ,const res接收
+
存到空对象 ( 对象名.value=res.具体数组 )
+
对象和请求方法暴露出去(return{})
+
组件中 layout/LayoutContainer
导入pinia+接收 user库
(const 自用名=组件内导入名)
+
onMounted(()=>{})钩子内调用库内方法名
+
渲染(自用名.对象名.属性)
api/user.js
封装接口
export const userGetInfoService = () => request.get('/my/userinfo')
- stores/modules/user.js 定义数据
const user = ref({})
const getUser = async () => {
const res = await userGetInfoService() // 请求获取数据
user.value = res.data.data
}
layout/LayoutContainer
页面中调用
import { useUserStore } from '@/stores'
const userStore = useUserStore()
/*mounted onMounted
操作dom 渲染之后才可以(操作dom:修改数据或者方法...) */
onMounted(() => {
userStore.getUser()
})
- 动态渲染
可以先去演示网址修改再自己演示
优先前面名字
黑马程序员:{{ userStore.user.nickname || userStore.user.username }}
退出功能 [element-plus 下拉框+确认框] 下拉选项:跳转 和 退出 一步到位 已完成运行无效
自做错误点: 未注意弹窗的promise 加上async awiat, 忘记删除用户信息, 判断command === 'logout’时未加引号
退出(判断是否退出,给弹窗(MessageBox),退出时删除本地token值,user的id名,头像…)、跳转时(退出到登录页,跳转到其他对应页面)
官网 - dropdown下拉菜单 - 指令事件
官网 - MessageBox消息弹框 - 确认消息(有写promise返回,记得async await)
组件中:
用官网方法监听command属性做出行为
@command=“onCommand”
+
导入+接收 useRouter
import { useRouter } from ‘vue-router’
const router = useRouter()
+
导入+接收 user库
import { 组件中导入名 } from ‘@/stores/user.js’
const 自用名 = 组件中导入名()
+
定义方法
const 方法名= async(command)=>{
if(command === 退出command值){
弹窗给提示
自用名.删token的方法()
自用名.删user信息的方法()
router.push('登录的路径')
}else{
router.push(`.../${command}`)
}
}
+
pinia库里
const 删token的方法=()=>{
token.value=‘’
}
const 删user信息的方法=()=>{
user.value= {}
}
return{
删token的方法,删user信息的方法
}
- 注册点击事件
/* command属性要跟配路由时的path最后一项相同 */
基本资料
更换头像
重置密码
退出登录
- 添加退出功能 就两种功能:退出、跳转
退出时:
本地的用户token 和 用户信息清除 库内方法
跳到登录页
import { 组件中导入名 } from '@/stores/user.js'
import { useRouter } from 'vue-router'
const router = useRouter()
const onCommand = async (command) => {
if (command === 'logout') {
/* 消息弹出框 加await:官网原文:在这里我们返回了一个 Promise 来处理后续响应。 有promise返回不想加下面 需要awiat */
await ElMessageBox.confirm('你确认退出大事件吗?', '温馨提示', {
type: 'warning', /* 警告 */
confirmButtonText: '确认', /* 绑定了router.push */
cancelButtonText: '取消'
})
userStore.removeToken()
userStore.setUser({})
router.push(`/login`)
} else {
router.push(`/user/${command}`)
}
}
- pinia user.js 模块 提供 setUser 方法
const 删token的方法=()=>{
token.value=''
}
const 删user信息的方法=()=>{
user.value= {}
}
return{
删token的方法,删user信息的方法
}
文章分类页面 - [element-plus 表格]
一个页面中穿插 组件 和 插槽,具名插槽会有按钮 ,默认插槽会有表格、分页
方法使用: el-card + el-button + defineProps + slot(name=名 定个位 + template/div#名 里面写内容 )
基本架子 - PageContainer
- 基本结构样式,用到了 el-card 组件
文章分类 此处默认插槽
添加分类 此处 具名插槽
... 此处默认插槽
- 考虑到多个页面复用,封装成组件
- props 定制标题 组件父传子
- 默认插槽 default 定制内容主体
- 具名插槽 extra 定制头部右侧额外的按钮
{{ title }} 父传子
定制1
定制2
- 页面中直接使用测试
( unplugin-vue-components 会自动注册组件,全局啥都不用,局部要导入(import…))
- 文章分类测试:
这是个组件,父传子title
添加分类 定制1
主体部分-表格 定制2
- 文章管理测试:
这是个组件,父传子title
发布文章 定制1
主体部分-表格+分页 定制2
文章分类渲染 某个路由页面渲染表格
因为关于分类的请求并不多,可以不用pinia
api库
封装接口(api/article)-发请求(获取文章分类的)
imprt request from ‘@/utils/request.js’
export const 请求方法名=()=>request.method(‘url’)
+
组件中
+
导入ref, 导入api模块
import { ref } from ‘vue’
import { 请求方法名 } from ‘@/api/article’
+
定义 响应式 空数组(因为获取数据一般都是数组包对象)
const 数组名 = ref([])
+
定义方法,调用 请求方法,定义res接收 ,并在下面直接调用
const 方法名 = async () =>{
const res = await 请求方法名()
数组名.value = res.data.具体
}
方法名()
+
添加表格 直接 实现渲染
表头: Table表格-基础表格
序号列: Table表格-单选(添加序号 type=“index”)
拿数据编辑删除: Table表格-自定义列模板
按钮及图标: Button 按钮-基础用法
+
添加加载效果
默认关闭,请求得数据前一直loading,得到数据后就取消加载 (关开关)
官网 - Loading加载 - 区域加载 - el-table标签处v-loading=“布尔值”
const loading=ref(true)
const 方法名 = async () =>{
loading.value=true
const res = await 请求方法名()
数组名.value = res.data.具体
loading.value=false
}
+
请求无数据处理
官网 - Empty 空状态 - 基础用法
el-table里边的下面加上
以下是表格制作:
使用到官网-Table表格-基础表格
<template>
<el-table :data="tableData" style="width: 100%"> 通过:data="tableData" 注入数据
<el-table-column prop="date" label="Date" width="180" /> 每一列,lable是列名(第一列..)
<el-table-column prop="name" label="Name" width="180" />
<el-table-column prop="address" label="Address" />
</el-table>
</template>
<script lang="ts" setup>
/* 上面通过prop="属性" 渲染 */
const tableData = [
{
date: '2016-05-03',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
}
]
</script>
使用到官网-Table表格-单选(添加序号 在需要的列标签加 type=“index”)
使用到官网-Table表格-自定义列模板 (操作数据)
想操作每行的数据:
在里添加template属性+默认插槽,得到一个对象,里边有row,$index…
>Edit
+(自写一个表格,写在页面中的“主体部分-表格 ”)官网为准
width 设置列宽,
添加序号 type=“index”
template + #default=“{ row,KaTeX parse error: Expected 'EOF', got '}' at position 7: index }̲" row是数组的每一项,index下标
编辑、删除图标(官网 - Button 按钮 - 基础用法)
:icon=“Edit” :icon=“Delete”
圆形:circle 颜色属性 type=”"
镂空效果:plain
...
封装API - 请求获取表格数据
- 新建
api/article.js
封装获取频道列表的接口
import request from '@/utils/request'
export const artGetChannelsService = () => request.get('/my/cate/list')
- 页面中调用接口,获取数据存储
const channelList = ref([])
const getChannelList = async () => {
const res = await artGetChannelsService()
channelList.value = res.data.data
}
el-table 表格动态渲染
列
const onEditChannel = (row) => {
console.log(row)
}
const onDelChannel = (row) => {
console.log(row)
}
el-table 表格 loading 效果
- 定义变量,v-loading绑定
const loading = ref(false)
- 发送请求前开启,请求结束关闭
const getChannelList = async () => {
loading.value = true
const res = await artGetChannelsService()
channelList.value = res.data.data
loading.value = false
}
文章分类添加 编辑 [element-plus 弹层]
核心思路:
弹窗封装成组件,1变量控制弹窗开关( 关开关 ),定义open方法打开窗口并处理数据,暴露出去
导入组件到路由的,路由获取组件dom,再定义编辑(传row)、添加方法(传空对象)通过调用open方法,
表单配规则,加样式,open方法得到参数展开({…参数})到 绑定接口的对象,加个三代目运算(?:)改标题 此时点开会渲染对应信息
写请求,表单中校验(await validate()),判定是编辑还是添加(传过来的row有无id)调用且传参输入框内容,
关弹窗,此时添加成功但是路由页面未渲染,需要子传父想改一波(defineEmits)
添加功能 编辑功能
点击跳出对话框时,添加无内容,编辑有内容
- open({ }) => 添加操作,点击对话框无数据
- open({ id: xx, … }) => 编辑操作,有数据
- open调用时打开弹窗
获取初始对话框
官网 - Dialog 对话框 - 基础用法
不要以下(再次确认X的内容)
js title="Tips" width="500" :before-close="handleClose"
+
由布尔值控制对话框 关开关
const dialogVisible = ref(false)
const 控方法名open = ()=>{
dialogVisible.value=true
}
+
对话框封装成组件(添加编辑框 结构相似)
新建 @/views/article/components/ChannelEdit组件,(你没看错)
里边是对话框内容(包括变量 dialogVisible 和 方法open)
+
新建组件中 共用的弹窗组件
有el-dialog内容 变量dialogVisible 添加、编辑方法判定
定义控制开关的变量
const dialogVisible = false
定义方法,将来操作 编辑 和 添加
const open =(row)=>{
console.log(row)
dialogVisible.value = true
}
+
暴露方法出去 将来那边传来row每行的属性供我判断
defineExpose({
open
})
路由中
导入+用 局部组件(全局不用导入,局部要导入不注册)
import { ChannelEdit } from '@/views/article/components/ChannelEdit'
<channel-edit> </channel-edit> 组件名有大写的,组件全小写,大写字母前面加-
+
操作组件中的dom
const dialog = ref()
<channel-edit ref="dialog"> </channel-edit>
+
编辑和添加时弹出对话框,so调用组件内方法
@click="编辑方法名" @click="添加方法名"
const 编辑方法名 = (row)=>{ 编辑时有数据,so传这行数据过去
dialog.value.open(row)
}
const 添加方法名 = ()=>{
dialog.value.open({}) 添加时有空表单,so传空对象(row是对象)
}
回到组件中 AK47
为 添加 表单配规则 (lable标识处有 “*” 表示加上了)
const formModel = ref({
接口文档名字 cate_name:'', cate_alias:''
})
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'
}]
}
···绑定规则
form标签:
1.:model="对象"
2.:rules="规则对象名"
imput处:
3.v-model="对象.接口变量"
每一个填写的form-item处:
4.prop="规则名"
给表单一些样式
el-form-item处加上lable标识(输入框前的提示)
lable="分类名称" lable="分类别名"
+
el-input 处加上未输入时的提示信息
placeholder="请输入分类名称" placeholder="请输入分类别名"
+
el-from 处加style属性 调下距离
style="padding-right:30px"
+
将组件传过来的数据进行操作 (添加或者操作取决于路由页传过来的数据row) :
对formModel进行 赋值+展开运算符 操作
const open = (row)=>{
dialogVisible.value = true
formModel.value={ ...row } 点添加 ->重置了表单信息 ,点编辑 ->存储了需要回显的数据
}
+
弹窗标题
根据传来的数据是否有 id 从而改弹窗标题
el-dialog标签处
:title="formModel.id ? "编辑分类":"添加分类"
api库article
导入request
import request from '@/utils/request.js'
发请求添加和编辑 (接口文档
添加:增加-文章分类 (记得穿过来一个对象-表单数据)
expost const add方法名 = (obj)=>{
request.方法名('url',obj)
}
编辑:更新-文章分类
expost const 编辑方法名 = (obj)=>{
request.方法名('url',obj)
}
组件中
导入俩请求方法
import {add方法名,编辑方法名 } from '@/api/acticle.js'
先校验,再发请求(子传父想改)
(表单特性:validate 对整个表单的内容进行验证。 接收一个回调函数,或返回 Promise。)
获取弹窗组件操作dom
const formRef = ref()
el-dialog标签处:
ref="formRef"
定义方法,点“确认”时执行
@click="onSubmit"
const emit = defineEmits(['success'])
const onSubmit= async()=>{
await formRef.value.validate() 校验
const isEdit = formModel.value.id 有id,编辑,没id,添加 随后关闭,且表格重新渲染(子传父想改数据)
if(isEdit){
await 编辑方法名(formModel.value)
ElMessage,success('编辑成功!')
}else{
await add方法名(formModel.value)
ElMessage,success('添加成功!')
}
控制开关的变量.value = false
emit('success')
}
路由组件中:
监听emit,进行成功操作
@success ="onSuccess"
const onSuccess =()=>{
渲染表格数据的方法()
}
- 点击调用方法显示弹窗
const onAddChannel = () => {
dialog.value.open({})
}
const onEditChannel = (row) => {
dialog.value.open(row)
}
准备弹层表单
- 准备数据 和 校验规则
const formModel = ref({
cate_name: '',
cate_alias: ''
})
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'
}
]
}
- 准备表单
- 编辑需要回显,表单数据需要初始化
const open = async (row) => {
dialogVisible.value = true
formModel.value = { ...row }
}
- 基于传过来的表单数据,进行标题控制,有 id 的是编辑
:title="formModel.id ? '编辑分类' : '添加分类'"
确认提交
api/article.js
封装请求 API
// 添加文章分类
export const artAddChannelService = (data) => request.post('/my/cate/add', data)
// 编辑文章分类
export const artEditChannelService = (data) =>
request.put('/my/cate/info', data)
- 页面中校验,判断,提交请求
const formRef = ref()
const onSubmit = async () => {
await formRef.value.validate()
formModel.value.id
? await artEditChannelService(formModel.value)
: await artAddChannelService(formModel.value)
ElMessage({
type: 'success',
message: formModel.value.id ? '编辑成功' : '添加成功'
})
dialogVisible.value = false
}
- 通知父组件进行回显
const emit = defineEmits(['success'])
const onSubmit = async () => {
...
emit('success')
}
- 父组件监听 success 事件,进行调用回显
const onSuccess = () => {
getChannelList()
}
文章分类删除功能 AK47
GET请求,接口请求参数:Query参数,对应 params:{}传参(一般是一个)
POST请求,Body参数,对应data传参(一般是对象)
核心思路:封装删除请求 + 路由内调用(传某行上的id)后 问一问是否要删除() 给提示(“删除成功”)再渲染页面
封装接口api(@/api/article.js中)
export const 删除请求名 =()={
request.DELETE('/my/cate/del',{
params:{
id
}
})
}
路由页面中:
导入+调用 删除方法
问一问:官网 - MessageBox 消息弹框 - 确认消息
import { 删除请求名 } from @/api/article.js
@click="删除方法名"
const 删除方法名 = async(row)=>{
ElMessageBox.confirm(
'您确定要删除此条信息吗?',
'温馨提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
awiat 删除请求名(row.id)
ElMessage.success('删除成功!')
渲染数据的方法()
}
api/article.js
封装接口 api
// 删除文章分类
export const artDelChannelService = (id) =>
request.delete('/my/cate/del', {
params: { id }
})
- 页面中添加确认框,调用接口进行提示
const onDelChannel = async (row) => {
await ElMessageBox.confirm('你确认删除该分类信息吗?', '温馨提示', {
type: 'warning',
confirmButtonText: '确认',
cancelButtonText: '取消'
})
await artDelChannelService(row.id)
ElMessage({ type: 'success', message: '删除成功' })
getChannelList()
}
文章管理初始化 表单+表格+分页栏 AK47
官网 - Select选择器 - 典型表单
官网 - Select选择器 - 行内表单 inline
制作初始页面
三行表单,
放在一行显示 el-form处加inline
1、2:-下拉菜单el-select + 下拉选项el-option label=“用户看的” value=“给后台的”
3:按钮el-button type=“primary” 按钮样式
<script setup>
import { ref } from 'vue'
import { Delete,Edit } from '@element-plus/icons-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": "体育"
}
])
/* 编辑功能 */
const onEditArticle =( row )=>{
console.log(row)
}
/* 删除功能 */
const onDeleteArticle =( row )=>{
console.log(row)
}
</script>
<template>
<page-container title="文章管理"> 这是个组件,父传子title
<template #extra>
<el-button type="primary">发布文章</el-button> 定制1
</template>
<!-- 主体部分-表单 + 表格 + 分页栏 定制2 -->
<!-- 表单区域:表单 - Select选择器+按钮Button -->
<el-form inline>
<el-form-item lable="文章分类:">
<el-select> 以后这里是组件
<el-option label="新闻" value="110"></el-option>
<el-option label="体育" value="137"></el-option>
</el-select>
</el-form-item>
<el-form-item lable="发布状态:">
<el-select>
<el-option lable="已发布" value="已发布"></el-option>
<el-option lable="草稿" 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>
<!-- 表格区域-获取文章列表 -->
<el-table :data="articleList" >
<el-table-column prop="title" label="文章标题" />
<template #default="{ row }" >
<el-link type="primary" :underline="false" > {{ row.title }}</el-link>
</template>
<el-table-column>
<el-table-column prop="cate_name" label="分类" /> <el-table-column>
<el-table-column prop="pub_date" label="发布时间"/> <el-table-column>
<el-table-column prop="state" label="状态" />
<template #default="{ row }" >
<el-button
circle
plain
type="primary"
:icon="Edit"
@click="onEditArticle(row)" >
</el-button>
<el-button
circle
plain
type="primary"
:icon="Delete"
@click="onDeleteArticle(row)" >
</el-button>
<el-button circle plain type="danger" :icon="Delete" ></el-button>
</template>
<el-table-column>
</el-table>
</page-container>
</template>
中文国际化处理
element-plus默认是英文的字体内容,想改成汉化
官网 - Config Provider 全局配置 - i18n 配置
zhCn 中文
en 英文
App.vue
<script setup>
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
</script>
<template>
<div>
<el-config-provider :locale="zhCn" >
<router-view></router-view>
</el-config-provider >
</div>
</template>
文章管理- 文章分类下拉框组件封装
新建文件
@/article/components/ChannelSelect.vue
+
编写 文章分类 下拉框组件
+
导入+使用组件
+
组件内
写方法:导入+调用 获取文章分类的请求 (api/article.js写过),res接收,赋值给一个空数组,一进页面调用
+
渲染出请求的数据(v-for) 点"文章分类"下拉框的数据展示
+
实现点击下拉选项可以对应渲染到"请选择"上 (父传子+子传父+Vue3 v-model特性)
新建文件
( @/article/components/ChannelSelect.vue)
文章分类 下拉框代码 这部分会使用多次so
<el-select>
<el-option label="新闻" value="110"></el-option>
<el-option label="体育" value="137"></el-option>
</el-select>
导入+使用组件
import { ChannelSelect } from ‘@/article/components/ChannelSelect.vue’
…
…
导入+调用 获取文章分类 的请求方法 (api/article.js写过),一进页面调用
import { artGetChannelsServise } from ‘@/api/article.js’
import { ref } from ‘vue’
const 空数组名 = ref([])
const 请求名 = async()=>{
const res = await artGetChannelsServise()
console.log(res)
空数组名.value = res.data.data
}
请求名() // 控制台有id:44173,cate_name:“体育”,cate_alias:‘boy’
组件上渲染出请求的数据 点"文章分类"下拉框的数据展示
/* lable是给用户看的 value是提交给后台的 */
<el-select>
<el-option v-for="(item) in 空数组" :key="item.id" :label="item.cate_name" :value="item.id" >
</el-option>
</el-select>
实现点击下拉选项可以对应渲染到"请选择"上 (父传子+子传父+Vue3 v-model特性)
组建与路由数据 双向绑定 v-model
vue2: :value + @input
**** vue3: :modelValue + @update:modelValue 值 + 输入框内容
:modelVlue可以改名(灵活),其他区跟着变 传数据+接收+设置+触发
const cateId = ref(44173)
父上的子组件标签处(父传子id):
子组件接收:
defineProps({
modelValue:{
type:[Number,String] <!-- 数组字符串都支持 -->
}
})
子想改数据:
定义设置事件(设置输入框的内容):
const emit = defineEmits(['update:modelValue'])
el-select处触发修改(默认值不变,改的话就 监听下拉选项改):
:modelValue="modelValue"
@update:modelValue="emit('update:modelValue',$event)"
控制台测试:
vue - 右上角定位小工具点下拉框 - 找到组件 - 修改了点一下组件(maybe不太灵) - modelValue跟着变
id不写死,且获取-文章列表 的所有接口 请求参数 收集到对象维护,父中cateId改成
const params =ref({
pagenum:1, //当前页
pagesize:5, //当前生效的每页条数
cate_id:'', //文章的id
state:'' //文章的发布状态
})
v-model="params.cate_id"
发布状态绑定
v-model=“params.state”
文章管理- 文章列表 表格渲染
核心思路:
发请求 (@/api/article.js)
+
页面中调用(@/article/ArticleManage.vue)
+
渲染 (el-table处 :data=articleList + prop=“属性名”)
+
发布时间格式化(dayjs方法封装+ 页面导入+调用)
发请求(@/api/article.js)
export const artGetListService = (params) =>
request.get('接口url',{
params
})
路由页面中调用接口发请求 (@/article/ArticleManage.vue)
准备空数组接收数据,准备空值0接收文章页数。
+
导入+调用+进页面调用(组件内的方法) 请求方法
+
res接收返回的东西,res.data.data到空数组 res.data.total到空值0
```js
import { artGetListService } from ‘@/api/article.js’
const articleList = ref([])
const total = ref(0) /* 总页数 */
const getArticleList = async()=>{
const res = await artGetListService(params.value)
articleList.value = res.data.data /* 渲染列表用 */
total.value = res.data.total /* 获取总页数 */
}
getArticleList()
```
渲染:
el-table处 :data=articleList + prop=“属性名”
+
发布时间格式化(时间部分只要人看得懂的)
element-plus自带 dayjs 工具
新建@/utils/format.js文件
+
导入dayjs
+
封装formatTime方法
+
import { dayjs } from 'element-plus'
export const formatTime = (time) => dayjs(time).format('YYYY年MM月DD日')
-
导入+调用 formatTime方法
import { formatTime } from ‘@/utils/format.js’
el-table-column的发布时间里加:
{{ formatTime(row.pub_date) }}
文章管理 - 文章列表 - 分页渲染 分页功能
官网 - Pagination分页 - 附件功能- All combined(所有都包含 解读)
丢在页面表格区域下面
+
绑定params里的参数
+
写页数变化的方法(两个:点n页/条,点第n页)
/* 分页区域 */
<el-pagination
style="magin-top:20px;justify-content:flex-end"
v-model:current-page="params.pagenum" //当前页(绑定参数)
v-model:page-size="params.pagesize" //生效条数(绑定参数)
:page-sizes="[2,3,5,10]" //可供选择的每页条数(必须包含咱params的每页条数)
:small="small" //是否要小一点 (no)
:disabled="disabled" // 是否要禁用(no)
:background="true" // 背景颜色(值为true就是蓝)
layout=" jumper,total, sizes, prev, pager, next" // 工具栏(控制显示和顺序,看需求)
:total="total" // 总页数(绑定参数,获取文章列表时定义过)
@size-change="onSizeChange" // 可供选择的每页条数会触发(可接受到参数value,写方法)
@current-change="onCurrentChange" // 当前页变化会触发(可接受到参数value,写方法)
/>
const params =ref({
pagenum:1, //当前页
pagesize:5, //当前生效的每页条数
cate_id:'', //文章的id
state:'' //文章的发布状态
})
// 可供选择的每页条数会触发 (可接受到参数value(点选择的条数得到))
const onSizeChange= (size) => {
console.log('当前每页条数',size)
// 只要供选择每页条数被改了,当前页的数据和位置肯定不一样!!
// 重新从第一页渲染即可(当前页为1)
params.value.pagenum = 1
// 生效条数(上面的参数)
params.value.pagesize = size
// 基于最新的当前页 和 每页条数 ,渲染数据(调用获取列表的请求)
getArticleList()
/* 此时点 n条/页 会出现对应条数,但是点某一页没跳转反应(下面的函数控制的) */
}
// 当前页变化会触发(可接受到参数value(点某一页得到))
const onCurrentChange = (page) => {
console.log('页码变化了',page)
/* 从点击的那一页开始渲染即可 */
params.value.pagenum = page
}
文章管理 - 文章列表 - 加一个分页切换时的加载效果(loading)
官网 - Loading 加载 - 区域加载 某一块需要加载的块加v-loading=“布尔值”
逻辑:默认关 - 获取文章列表时开 - 获取到数据后关 关开关
默认关
const loading = ref(false)
const getArticleList = async()=>{
获取文章列表时开
loading.value = true
const res = await artGetListService(params.value)
articleList.value = res.data.data /* 渲染列表用 */
total.value = res.data.total /* 获取总页数 */
获取到数据后关
loading.value = false
}
getArticleList()
文章管理 - 文章列表 - 搜索和重置
点击文章分类某个下拉框选项(eg:体育) + 搜索按钮/重置按钮 实现重新渲染
注册点击事件
@click = "onSearch()"
@click = "onReset()"
写方法
逻辑:
/* 搜索逻辑:按照最新的条件,重新检索,从第一页开始展示(不用传下拉框选项id了,因为之前ChannelSelect组件v-model绑定了 cate_id) */
const onSearch = () =>{
params.value.pagenum = 1 //当前页
getArticleList()
}
/* 重置逻辑:筛选条件清空,重新检索,也是从第一页展示 */
const onReset = () =>{
params.value.cate_id = '' //文章的id
params.value.state = '' //文章的发布状态
params.value.pagenum = 1 //当前页
getArticleList()
}
文章管理 - 发布文章按钮 - 抽屉组件drawer
官网拿组件关键代码 - 开关控制 - 封装组件 - 页面中调用
官网 - drawer抽屉 - 基础用法 - 如下关键
官网拿组件关键代码
<!-- 抽屉:默认从右边出来 -->
<el-drawer
v-model="drawer" //drawer是布尔值变量,开关 (关开关)
title="发布文章" // 将来标题由有无id控制
:direction='rtl' // 抽屉从那边出来 想改别的加:direction=''
size:'50%' // 抽屉尺寸
>
<span>Hi, there!</span>
</el-drawer>
暂放分页区域下,将来封装成组件
+
开关控制
注册 点击事件 “发布文章”处:
type =“primary” @click = “onAddArticle”
“编辑按钮”处:
@click = “onEditArticle”
+
写添加文章方法
// 抽屉的开关 封成组件就拿到那边
const drawer = ref(false)
//添加逻辑
const onAddArticle =()=>{
drawer.value = true //将来组件控制开关即可,咱们要open方法
}
封装组件
因为文章管理 - 添加文章和操作编辑 两个按钮弹出的抽屉一样 so封装成组件
注意:方法跟文章分类的open方法类似
新建 @/views/article/component/ArticleEdit 组件
+
内容:官网拿组件关键代码 + 抽屉的开关变量drawer
<script setup>
const drawer = ref(false)
const open = (row) =>{
drawer.value = true /* 抽屉开 */
console.log(row) /* 等会那边调用看看有没有得到row */
}
/* 暴露出去方法 */
defineExport{
open
}
</script>
<template>
<el-drawer
v-model="drawer" //drawer是布尔值变量,开关 (关开关)
title="发布文章"
:direction='rtl' // 抽屉从那边出来 想改别的加:direction=''
size:'50%' // 抽屉尺寸
>
<span>Hi, there!</span>
</el-drawer>
</template>
页面中
导入+使用 组件,且获取一下dom将来操作
import { ArticleEdit } from '@/views/article/component/ArticleEdit.vue'
const articleEditRef = ref()
<article-edit ref="articleEditRef">
</article-edit>
添加文章抽屉逻辑
const onAddArtcle = () =>{
articleEditRef.value.open({})
}
编辑文章抽屉逻辑
const onEditArtcle = () =>{
articleEditRef.value.open(row)
}
完善抽屉表单结构
内容:输入框 + 下拉菜单(channelSelect组件) + 添加封面 + 文章内容 + 按钮(发布,草稿)
思路:找到接口文档 - 文章管理 - 发布文章,需要传Body参数
组件内收集参数
/* 默认表单 */
const defaultModel = ref({
title:'', // 标题
cate_id:'', // 分类id
cover_img:'', //封面 file对象
content:'', //内容
state:'' //发布状态
})
/* 准备数据 添加,编辑用到*/
const fromModel = ref({
...defaultModel
})
发现得到的数据缺少参数,so父编辑器没办法回显,so open方法做判断(row.id)
const open = async (row) =>{
drawer.value = true
// 需要基于 row.id 发送请求, 获取 编辑 对应的详情数据,进行回显
if(row.id){
console.log('编辑操作')
}else{
/* 添加时,重置from数据 */
formModel.value = { ...defaultModel }
console.log('添加,重置表单')
}
}
抽屉template:
v-model=“drawer”
title=“发布文章”
:direction=‘rtl’
size:‘50%’
>
文章上传
富文本编辑器
发布 草稿
导入下拉菜单组件 ChannelSelect
import ChannelSelect from ‘./ChannelSelect.vue’
+
样式:拉长输入框
下拉菜单的宽 width=“100%” 没有作用
在ChannelSelect组件中加上
defineProps({
...
width:{
type:String
}
})
el-select标签处加:
:style="{ width }"
此时已拉长输入框
文章管理 - 抽屉组件 - 文件上传部分(封面设置)
用户选出图片,是预览,点发布/草稿再上传到服务器
el-upload组件获取及修改 + 设置图片默认值 + 无封面图片时,显示“+” + 实现监听文件并预览 + 加图片样式 + 图片存储到服务器提交的表单对象
官网 - Upload上传
el-upload组件获取及修改(往下看)
关闭 自动上传,不需要action等参数
加入 关闭自动上传的 属性
+
设置图片默认值
const imgUrl = ref(‘’)
+
无封面图片时,显示“+”
导入+标签使用Plus
import {Plus} from ‘@element-plus/icons-vue’
此时可打开电脑文件,但没有实现监听预览
+
实现监听文件并预览
Upload - on-change钩子
标签处添加
:on-change="选择文件的方法名" onSelectFile
写方法(该钩子可得到uploadFile单文件,uploadFiles多文件)
const 方法名 = (uploadFile) =>{
console.log(uploadFile) //控制台可看到raw属性,基于raw创建本地的预览地址
imgUrl.value = URL.createObjectURL(uploadFile.raw)
}
此时点"+" 上传文件成功,但是图片样式没有约束(默认的大小)
+
加图片样式(往下看)
+
本地预览时存的图片 收集到服务器提交的对象
const 方法名 = (uploadFile) =>{
console.log(uploadFile) //控制台可看到raw属性,基于raw创建本地的预览地址
imgUrl.value = URL.createObjectURL(uploadFile.raw)
//立刻将图片对象,存入 formModel.value.cover_img 将来用于提交
formModel.value.cover_img = uploadFile.raw
}
结构:
这里要关闭 自动上传,不需要action等参数,只要做前端的本地预览,无需在提交前上传到服务器
语法:URL.createObjectURL(文件的raw属性) 创建本地预览的地址,来预览
class=“avatar-uploader”
//action=“https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15” 上传到服务器地址(no
:show-file-list=“false”
//:on-success=“handleAvatarSuccess” 自动上传才用的(no)
//:before-upload=“beforeAvatarUpload” 提交前的校验(no)
:auto-upload=“false” 关闭自动上传
封面的图片
无封面图片时,显示“+”
图片样式:
<style lang="scss" scoped>
.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;
}
}
}
<style>
富文本编辑器 vue-quill
官网地址:https://vueup.github.io/vue-quill/
需要看 installation 和 Usage
安装 + 导入使用 + 数据绑定 + 富文本样式
安装
pnpm add @vueup/vue-quill@latest
+
导入使用
import { QuillEditor } from ‘@vueup/vue-quill’
import ‘@vueup/vue-quill/dist/vue-quill.snow.css’;
+
数据绑定到提交的数据对象
<quill-editor theme="snow"
v-model:content="formModel.content" 内容绑定
conten-type="html" 内容格式
></quill-editor>
此时打开Vue控制台ArticleEdit组件可以测试是否响应
+
富文本样式美化 (该类审查元素慢慢找)
.editor{
width:100%;
:deep(.ql-editor){
min-height:200px;
}
}
官网:
安装包 新电脑
pnpm add @vueup/vue-quill@latest
导入+使用
Global Registration: 全局
import { createApp } from 'vue'
import { QuillEditor } from '@vueup/vue-quill'
import '@vueup/vue-quill/dist/vue-quill.snow.css';
const app = createApp()
app.component('QuillEditor', QuillEditor)
or Local Registration: 局部 (本项目用)
import { QuillEditor } from '@vueup/vue-quill'
import '@vueup/vue-quill/dist/vue-quill.snow.css';
export default {
components: {
QuillEditor
}
}
Basic Usage: 引用
文章管理 - 添加文章 - 抽屉组件 - 发布,草稿按钮
接口文章 - 文章管理 - 发布文章
method:post
url: /my/article/add
请求所需参数:title
cate_id
content
cover_img
state // 只能是发布/草稿,也是唯一不同
封装请求
api/article.js
// 注意:data需要是一个formData格式的对象
export const 发布文章 = (data)=> request.post('/my/article/add',data) //发布文章 artPublishService
页面导入+调用请求,监听按钮写方法
状态由 按钮固定值 决定,将formModel普通对象 变成 formData对象,有id编辑否则就是发布(发添加请求,给提示,关抽屉)
@click="发布" // 发布 onPublish('已发布') 方法一样,state由参数决定
@click="草稿" // 草稿 onPublish('草稿')
import {artPublishService} from '@/api/article.js'
const onPublish = async(state) =>{
// 点击时状态参数存到 formModel
formModel.value.state = state
// 注意:当前接口需要的是formData对象,
// so需要 将普通对象 => 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('添加成功')
// 关抽屉
drawer.value = false
)
}
}
此时添加成功,也显示在表格中,但是在最后一页而且要手动找(不方便)
+
重新渲染到最后一页
通知父组件,重新渲染
const emit = defineEmits(['success']) // 定义 成功了 的消息
const onPublish = async(state) =>{
...
// 通知父组件,添加成功了
emit('success')
}