自用vue3,已标记慎看(日常更新)

认识Vue3

1. Vue2 选项式 API vs Vue3 组合式API

<script>
export default {
  data(){
    return {
      count:0
    }
  },
  methods:{
    addCount(){
      this.count++
    }
  }
}
</script>
<script setup>
import { ref } from 'vue'
const count = ref(0)
const addCount = ()=> count.value++
</script>

特点:

  1. 代码量变少
  2. 分散式维护变成集中式维护

2. Vue3的优势

使用create-vue搭建Vue3项目

1. 认识create-vue

create-vue是Vue官方新的脚手架工具,底层切换到了 vite (下一代前端工具链),为开发提供极速响应

2. 使用create-vue创建项目 新电脑创建vue3项目

前置条件 - 已安装16.0或更高版本的Node.js
powershell中输入node -v

执行如下命令,这一指令将会安装并执行 create-vue

npm init vue@latest

项目名字:vue3-demo
除了ESLint剩下都No

创建成功后cd 到 vue3-demo

3.安装依赖

npm install

4.启动项目

npm run dev

熟悉项目和关键文件

  1. vite.config.js - 项目的配置文件 基于vite的配置

  2. package.json - 项目包文件 核心依赖项变成了Vue3.x 和 vite

  3. main.js - 入口文件 createApp函数创建应用实例
    //new Vue() 创建一个应用实例 => createApp()
    // createRouter() createStore()
    // 将创建实例进行了封装,保证每个实例的独立封闭性

  4. app.vue - 根组件 SFC单文件组件 script - template - style
    变化一: 脚本 script 和 模板 template 顺序调整
    变化二: 模板 template 不再要求唯一根元素
    变化三: 脚本 script 添加 setup 标识支持组合式 API

  5. index.html - 单页入口 提供 id 为 app 的挂载点

Vue2变Vue3需要替换的插件: 新电脑

禁用Vetur 安装volar(Vue Language Eeature)

组合式API - setup选项

vue2 vue3区别:
回忆:之前 vue 文件内容架子是 template + script + style(scoped) 结构 js 样式
如今是 script(set up) + template + style(scoped) js 结构 样式

  之前组件使用方法:导入注册 + 使用
  如今: 导入 + 使用
  import ... from + template中使用 

 vue2中template要求唯一根元素
 vue3中不要求(header , main..)

1. setup选项的写法和执行时机

加上setup 允许在script中直接编写组合式API

写法

<script>
  export default {
    setup(){
      
    },
    beforeCreate(){
      
    }
  }
</script>

执行时机

在beforeCreate钩子之前执行 非常早

2. setup中写代码的特点 setup用法

在setup函数中写的数据和方法需要在末尾以对象的方式return,才能给模版使用
定义+导出+使用
定义
数据形式:const 名 = 值
函数形式:const 名 = () =>{ }
导出 (在script后加setup可以省略此步)
return{名,名…}
使用
{{名}}…

<script>
  export default {
    setup(){
      const message = 'this is message'
      const logMessage = ()=>{
        console.log(message)
      }
      // 必须return才可以
      return {
        message,
        logMessage
      }
    }
  }
</script>

3.

script标签添加 setup标记,不需要再写导出语句,默认会添加导出语句

<script setup>
  const message = 'this is message'
  const logMessage = ()=>{
    console.log(message)
  }
</script>

组合式API - reactive和ref函数

1. reactive 响应的对象(obj)

接受对象类型数据的参数传入并返回一个响应式的对象

vue3中实现 响应式 对象类型数据 的方法,
应用:比如点击按钮加1,没有reactive进行包起来就不能对数字加1

步骤:从vue导入reactive + reactive包对象 + 使用

<script setup>
 // 导入
 import { reactive } from 'vue'
 // 执行函数 传入参数 变量接收   (对象)
 const state = reactive({
   msg:666,
   count:1
 })
 const setSate = ()=>{
   // 修改数据更新视图
   state.msg += 1
 }
</script>

<template>
  <button @click="setState">{{state.msg}}</button>
  此时点击按钮 666 +1
</template>

2. ref (常用 更强大) 响应的 简单类型 或 复杂类型

接收 简单类型 或者 对象类型 的数据传入并返回一个响应式的对象

注意:1. js中访问数据,需要.value
2.template中,.value不用加(帮我们扒了一层)

步骤:导入ref + ref包数据 + 数据.value使用(js中,template不加)

<script setup>
 // 导入
 import { ref } from 'vue'
 // 执行函数 传入参数 变量接收
 const count = ref(0)
 const setCount = ()=>{
   // 修改数据更新视图必须加上.value
   count.value++
 }
</script>

<template>
  <button @click="setCount">{{count}}</button>
</template>

3. reactive 对比 ref

  1. 都是用来生成响应式数据
  2. 不同点
    1. reactive不能处理简单类型的数据
    2. ref参数类型支持更好,但是必须通过.value做访问修改
    3. ref函数内部的实现依赖于reactive函数
  3. 在实际工作中的推荐
    推荐使用ref函数,减少记忆负担,小兔鲜项目都使用ref

组合式API - computed 计算属性函数写法

计算属性基本思想和Vue2保持一致,组合式API下的计算属性只是修改了API写法

计算函数格式:

const 名 =computed( () =>{
return 计算公式
} )

步骤: 导入ref和computed + 定义计算函数用computed包起来 + 使用函数名

过滤数组案例 筛选大于二的
<script setup>
// 导入
import {ref, computed } from 'vue'
// 原始数据(数组),ref后得到对象,再.value得到新数组
const list = ref([1,2,3,4,5,6,7,8,])
// 计算属性
const newList = computed( ()=>{
  return list.value.filter(item => item>2)
} ) 

</script>

<template>
  <div>
     原始数组:{{ list }}
     大于二得到的数组:{{ newList }}
  </div>
</template>

组合式API - watch 监听写成函数写法

侦听一个或者多个数据的变化,数据变化时执行回调函数,俩个额外参数 immediate控制立刻执行,deep开启深度侦听

步骤:导入ref,watch + 写事件函数(比如点击改名字) + 监听改前后的名字watch
watch( const的值,(新,老) => { } )
watch( [const的值1,const值2],(新,老) => { },{ immediate/deep:true } )

1. 侦听单个简单数据

watch 单个数据监听 格式

watch( (监听的值ref对象"无.value",(新值,旧值)) => {

} )

<script setup>
  // 1. 导入watch
  import { ref, watch } from 'vue'
  const count = ref(0)
  // 2. 调用watch 侦听变化
  watch(count, (newValue, oldValue)=>{
    console.log(`count发生了变化,老值为${oldValue},新值为${newValue}`)
  })
</script>

2. 侦听多个简单数据 无非多个数组包const

侦听多个数据(多个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>

3. immediate 一进页面立即执行一次 无非多个immediate:true

使用方法:在watch函数后面多加一个对象,写上immediate:true

执行单次案例后面加上immediate:true 一进页面控制台会显示 0 undefined

在侦听器创建时立即出发回调,响应式数据变化之后继续执行回调

<script setup>
  // 1. 导入watch
  import { ref, watch } from 'vue'
  const count = ref(0)
  // 2. 调用watch 侦听变化
  watch(count, (newValue, oldValue)=>{
    console.log(`count发生了变化,老值为${oldValue},新值为${newValue}`)
  },{
    immediate: true
  })
</script>

4. deep 复杂类型(obj系列)用 监听整个obj

通过watch监听的ref对象默认是 浅层侦听 的,

直接修改嵌套的对象属性不会触发回调执行,需要开启deep

<script setup>
  // 1. 导入watch
  import { ref, watch } from 'vue'
  const state = ref({ count: 0 })
  // 2. 监听对象state
  watch(state, ()=>{
    console.log('数据变化了')
  })
  const changeStateByCount = ()=>{
    // 直接修改不会引发回调执行
    state.value.count++
  }
</script>

<script setup>
  // 1. 导入watch
  import { ref, watch } from 'vue'
  const state = ref({ count: 0 })
  // 2. 监听对象state 并开启deep
  watch(state, ()=>{
    console.log('数据变化了')
  },{deep:true})
  const changeStateByCount = ()=>{
    // 此时修改可以触发回调
    state.value.count++
  }
</script>

5.精确监听对象中的某个属性(补充第四点)

写法:obj可变具体对象名,key可变具体键值对
watch(
()=>obj.value.key,
(新,老)=>console.log(新,老)
)

组合式API - 生命周期函数

1. 选项式对比组合式

Vue3的生命周期API(选项式 VS 组合式)

选项式API 组合式API

beforeCreate/created setup
创建响应式数据/
发送初始化渲染请求 在setup里直接请求+调用

beforeMount onBeforeMount
数据已经渲染完成

mounted onMounted
操作dom 渲染之后才可以(操作dom:修改数据或者方法…)

beforeUpdate onBeforeUpdate

updated onUpdated

beforeDestroy onBeforeUnmount

destroyed onUnmounted

2. 生命周期函数基本使用

  1. 导入生命周期函数
  2. 执行生命周期函数,传入回调
<scirpt setup>
import { onMounted } from 'vue'
onMounted(()=>{
  // 自定义逻辑
})
</script>

3. 执行多次

生命周期函数执行多次的时候,会按照顺序依次执行

<scirpt setup>
import { onMounted } from 'vue'
onMounted(()=>{
  // 自定义逻辑
})

onMounted(()=>{
  // 自定义逻辑
})
</script>

组合式API - 父子通信

1. 父传子数据 const props = defineProps({子:String})

1.父准备数据 const 父 =ref(‘李白’)
2.在子标签上加属性的 传值 :子=“父”
3. 子中props接收数据 vue3中不能写props:[]
借助编译器宏 defineProps()
const props = defineProps({
子:String
})
4. 渲染即可
{{ 子 }} 无需props.子

2. 子传父想改

例子:孩子花钱
子组件内部
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
}

组合式API - 模版引用 ref获取dom别组件的数据或方法

概念:通过 ref标识 获取真实的
dom对象 或者 组件实例对象(一个组件获取另一个组件的数据或者方法)

1. 基本使用 dom对象

案例:点击获取焦点 获取真实的 dom对象

实现步骤:

  1. 调用ref函数生成一个ref对象
    import ref from ‘vue’
    const 绑定属性名 =ref(null)
  1. 标签处通过ref标识绑定ref对象
    ref = ‘属性名’
    3.注册点击事件及方法
    @click=‘focuFn’
    const focuFn=()=>{
    属性名.value.focus()
    }

2. defineExpose 组件实例对象(一个组件获取另一个组件的数据或者方法) 拿其他组件的数据/属性

默认情况下在

案例:点击’获取组件数据’时控制台显示子组件数据
步骤:1.父中ref个属性名
2.子组件标签上ref=“属性名”
3.注册点击事件及方法

 @click='getState()'
 const getState=()={
  console.log(属性名.value.数据名/方法名)
 }

子:4.利用编译器宏把允许被外部访问的数据或方法放进去
数据:const count=999
const sayHi=()=>{
console.log(‘你好’)
}
打开权限允许访问:
difineExpose({
count,
sayHi
})

组合式API - provide和inject 父传数据给儿、孙

1. 作用和场景

顶层组件向任意的底层组件传递数据和方法,实现跨层组件通信

2. 跨层传递普通数据

实现步骤
0. 父导入 provide 或子孙导入 inject 函数

  1. 顶层通过 provide 提供数据
    provide(‘属性’,值) 🐖:值 无.value
  1. 底层通过 inject 接收数据
    const 子属性 = inject(‘属性’)

3.渲染
{{ 子属性 }}

注:不是自己的组件不能直接改,想改看 ##3

3. 跨层传递响应式数据 控制权还在爹(在爹传方法)

案例:爹的500传下去儿想改

在调用provide函数时,第二个参数设置为ref对象
newCount实际为底层的数据参数
顶层:
provide(‘爹方法名’,(newCount)=>{
爹数据.value = newCount
})
底层:
count 儿方法名 = inject(‘爹方法名’)

@click = ‘changeState’
const changeState=()=>{
儿方法名(参数)
}

4. 跨层传递方法 电脑进度

顶层组件可以向底层组件传递方法,底层组件调用方法修改顶层组件的数据

案例:父给儿、孙传数据 关键词:provide inject

  1. 父导入 provide 或子孙导入 inject 函数
  2. 顶层通过 provide 提供数据
    provide(‘属性’,值)
  1. 底层通过 inject 接收数据
    const 子属性 = inject(‘属性’)

3.渲染
{{ 子属性 }} */

<!-- 案例:爹的500传下去儿想改

在调用provide函数时,第二个参数设置为ref对象
newCount实际为底层的数据参数
顶层:
provide(‘爹方法名’,(newCount)=>{
爹数据.value = newCount
})
底层:
count 儿方法名 = inject(‘爹方法名’)

@click = ‘changeState’
const changeState=()=>{
儿方法名(参数)
} -->

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可以在项目创建时自动添加,现在我们初次学习,从零开始:

  1. 使用 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

  1. 按照官方文档安装 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

案例:计数器案例

  1. 建store文件
    src/store/库名.js

  2. 导入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. 组件 导入’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/

  1. 安装插件 pinia-plugin-persistedstate
npm i pinia-plugin-persistedstate
  1. 使用 main.js 导入插件 + .use
import persist from 'pinia-plugin-persistedstate'
...
app.use(createPinia().use(persist))
  1. 配置 store/counter.js
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'

export const useCounterStore = defineStore('counter', () => {
  ...
  return {
    count,
    doubleCount,
    increment
  }
}, {
  persist: true            //开启当前模块的持久化
})
  1. 其他配置,看官网文档即可
    {
    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 配置代码风格 新电脑

环境同步:

  1. 安装了插件 ESlint,开启保存自动修复
  2. 禁用了插件 Prettier,并关闭保存自动格式化
// ESlint插件 + Vscode配置 实现自动格式化修复
"editor.codeActionsOnSave": {
    "source.fixAll": true
},
"editor.formatOnSave": false,

配置文件 .eslintrc.cjs

  1. prettier 风格配置 https://prettier.io

    1. 单引号
    2. 不使用分号
    3. 每行宽度至多80字符
    4. 不加对象|数组最后逗号
    5. 换行符号不限制(win mac 不一致)
  2. vue组件名称多单词组成(忽略index.vue)

  3. 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 解构的校验
    // 💡 添加未定义变量错误提示,create-vue@3.6.3 关闭,这里加上是为了支持下一个章节演示。
    'no-undef': 'error'
  }

基于husky的提交代码前检查工作流 新电脑

husky 是一个 git hooks 工具 ( git的钩子工具,可以在特定时机执行特定的命令 )
没有git就去pc.qq下载 以下命令全是git bash终端

husky 配置

  1. git初始化 git init

  2. 初始化 husky 工具配置 https://typicode.github.io/husky/

pnpm dlx husky-init && pnpm install
  1. 修改 .husky/pre-commit 文件
pnpm lint       暂时的,以下面的为准

**问题:**pnpm lint默认进行的是全量检查,耗时问题,历史问题。
暂存区新添加的代码的 eslint 校验
lint-staged 配置

  1. 安装
pnpm i lint-staged -D
  1. 配置 package.json
{
  
  "lint-staged": {
    "*.{js,ts,vue}": [
      "eslint --fix"
    ]
  }
}

scripts配置项后面加
{
  "scripts": {
    "lint-staged": "lint-staged"
  }
}
  1. 修改 .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

自动按需:

  1. 安装插件
pnpm add -D unplugin-vue-components unplugin-auto-import
  1. 然后把下列代码插入到你的 ViteWebpack 的配置文件中 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()]
    })
  ]
})

  1. 直接使用
<template>
  <div>
    <el-button type="primary">Primary</el-button>
    <el-button type="success">Success</el-button>
    <el-button type="info">Info</el-button>
    <el-button type="warning">Warning</el-button>
    <el-button type="danger">Danger</el-button>
    ...
  </div>
</template>

**彩蛋:**默认 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 进行基本的二次封装, 单独封装到一个模块中, 便于使用

  1. 安装 axios
pnpm add axios
  1. 新建 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文件功能组件名路由级别
/loginviews/login/LoginPage.vue登录&注册LoginPage一级路由
/views/layout/LayoutContainer.vue布局架子LayoutContainer一级路由
├─ /article/manageviews/article/ArticleManage.vue文章管理ArticleManage二级路由
├─ /article/channelviews/article/ArticleChannel.vue频道管理ArticleChannel二级路由
├─ /user/profileviews/user/UserProfile.vue个人详情UserProfile二级路由
├─ /user/avatarviews/user/UserAvatar.vue更换头像UserAvatar二级路由
├─ /user/passwordviews/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 表单 & 表单校验]

注册登录 静态结构 & 基本切换(注册页与登录页)

网页布局: 左边图片 + 右边登录信息

  1. 安装 element-plus 图标库
pnpm i @element-plus/icons-vue
  1. 静态结构准备
<script setup>
import { User, Lock } from '@element-plus/icons-vue'
import { ref } from 'vue'
const isRegister = ref(true)   /* 控制注册成功跳转登录 */
</script>
/* 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)=>{写函数}
<!-- 
rule,  当前校验规则相关的信息(不用)
value, 表单元素目前值
callback  无论成功失败都要callback回调 
- callback() 校验成功
- callback(new Error(错误信息)) 失败
-->
*/
<template>

  <el-row class="login-page">
    <el-col :span="12" class="bg"></el-col>

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

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

注册功能

实现注册校验

接口文档: https://apifox.com/apidoc/shared-26c67aee-0233-4d23-aab7-08448fdf95ff/api-93850835

【需求】注册页面基本校验

  1. 用户名非空,长度校验5-10位
  2. 密码非空,长度校验6-15位 pattern:*/S{正则}$\
  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)=>{写函数}

 repassword:[{},{}, 第一第二个对象与password相同
   {
    validitor:(rule,value,callback)=>{
         <!-- 判断value与from中的password值是否相同 -->
      if (value === formModel.password.value){
        callback()
      }else{
        callback(new Error('您输入的密码不一致'))
      }
    }trigger:'blur' /* 失焦校验 */
   }
 ] 
  1. model 属性绑定 form 数据对象 加在表单外层

这些变量要到接口文档获取(将来要提交后台)

const formModel = ref({
  username: '',
  password: '',
  repassword: ''
})

<el-form :model="formModel" >    
  1. rules 配置校验规则 加在表单外层
    要基于1 2点 :model的变量 配的规则
<el-form :rules="rules" >          
    
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'
    }
  ]
}
  1. v-model 绑定 form 数据对象的子属性 写在每个表单填写处
<el-input
  v-model="formModel.username"
></el-input>
... 
(其他两个也要绑定)
  1. prop 绑定校验规则
<el-form-item prop="username">
  <el-input
    v-model="formModel.username"
    :prefix-icon="User"
    placeholder="请输入用户名"
  ></el-input>
</el-form-item>
... 
(其他两个也要绑定prop)

注册成功前的预校验 解决注册时未填写但是点注册的提示 再输入的validate()方法通过就发请求

注册成功之前,先进行校验,校验成功 -》 请求 ,校验失败-》自动提示

官网Form Exposes暴露出去的方法 官网-表单-右侧FormExpose
so在注册的el-from中获取表单组件

const 获取名 = ref(null) + 标签ref=“获取名”
点击注册前校验
@click=“zhuC”
const zhuC=()=>{
await 获取名.value.validate()
console.log(‘开始注册请求’)
}
需求:点击注册按钮,注册之前,需要先校验(信息校验通过后才可以点击)

  1. 通过 ref 获取到 表单组件
const form = ref()

<el-form ref="form">
  1. 注册之前进行校验
<el-button
  @click="register"
  class="button"
  type="primary"
  auto-insert-space
>
  注册
</el-button>

const register = async () => {
  await form.value.validate()
  console.log('开始注册请求')
}

封装 api 实现注册功能

user.js中导入request请求+导出方法 + 组件内导入+调用方法

需求:封装注册api,进行注册,注册成功切换到登录

  1. 新建 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 })
}
简写如上

  1. 注册登录页面中调用
import {userRegisterService} from '@/api/user.js'

const register = async () => {
  await form.value.validate()
  await userRegisterService(formModel.value)
  
  //Feedback反馈组件 - Massage消息提示
  ElMessage.success('注册成功')
  // 切换到登录
  isRegister.value = false
}
  1. eslintrc 中声明全局变量名, 解决 ElMessage 未导入的报错问题 新电脑 无需导入组件即可直接使用
    导入element-plus组件
module.exports = {
  ...
  globals: {
    ElMessage: 'readonly',/* 值是全局的意思,以后需要的组件都需要写进来就不会报错 */
    ElMessageBox: 'readonly',
    ElLoading: 'readonly'
  }
}

注册总结 : 静态 + 校验 + 注册前的预处理 + 发注册请求 + 跳转登录(改布尔值) AK47

登录功能

实现登录校验

【需求说明】给输入框添加表单校验

  1. 用户名不能为空,用户名必须是5-10位的字符,失去焦点 和 修改内容时触发校验
  2. 密码不能为空,密码必须是6-15位的字符,失去焦点 和 修改内容时触发校验

操作步骤: 可共用属性 方法

  1. model 属性绑定 form 数据对象,直接绑定之前提供好的数据对象即可
<el-form :model="formModel" >
  1. rules 配置校验规则,共用注册的规则即可
<el-form :rules="rules" >
  1. v-model 绑定 form 数据对象的子属性
<el-input
  v-model="formModel.username"
  :prefix-icon="User"
  placeholder="请输入用户名"
></el-input>

<el-input
  v-model="formModel.password"
  name="password"
  :prefix-icon="Lock"
  type="password"
  placeholder="请输入密码"
></el-input>
  1. prop 绑定校验规则
<el-form-item prop="username">
  <el-input
    v-model="formModel.username"
    :prefix-icon="User"
    placeholder="请输入用户名"
  ></el-input>
</el-form-item>
... 
  1. 切换的时候重置账号密码
    监视这个切换值,变化了咋样
    将上面的三绑数据 对象 清空
watch(isRegister, () => {
  formModel.value = {
    username: '',
    password: '',
    repassword: ''
  }
})

登录前的预校验 & 登录成功

【需求说明1】登录之前的预校验

  • 登录请求之前,需要对用户的输入内容,进行校验
  • 校验通过才发送请求

【需求说明2】登录功能

  1. 封装登录API,点击按钮发送登录请求
  2. 登录成功存储token,存入pinia 和 持久化本地storage
  3. 跳转到首页,给提示

【测试账号】

  • 登录的测试账号: shuaipeng

  • 登录测试密码: 123456

PS: 每天账号会重置,如果被重置了,可以去注册页,注册一个新号

实现步骤:

  1. 注册事件,进行登录前的预校验 (获取到组件调用方法)
<el-form ref="form">
    
const login = async () => {
  await form.value.validate()
  console.log('开始登录')
}
  1. 封装接口 API api/user.js文件

export const userLoginService = ({ username, password }) =>
  request.post('api/login', { username, password })
  1. 调用库 以及调用库内方法将 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
<script setup>
import {
  Management,
  Promotion,
  UserFilled,
  User,
  Crop,
  EditPen,
  SwitchButton,
  CaretBottom
} from '@element-plus/icons-vue'
import avatar from '@/assets/default.png'
</script>
/* 
   el-menu 整个菜单组件
     active-text-color="#ffd04b"  点击时字体颜色

     :default-active="$route.path"   配置默认高亮的菜单项
     router     router选项开启,下面el-menu-item 的index 是点击跳转的路径(相当于vant组件的to)
  
  <el-menu-item   单级菜单
    index="/article/channel">   item 的index 是点击跳转的路径

  <el-sub-menu    多级菜单
     个人中心 #title 具名插槽
      <el-menu-item
*/
<template>

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

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

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

登录访问拦截 不登录输网址进不来 未完成

未登录自动跳转登录页 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(()=>{})钩子内调用库内方法名
+
渲染(自用名.对象名.属性)

  1. api/user.js封装接口
export const userGetInfoService = () => request.get('/my/userinfo')
  1. stores/modules/user.js 定义数据
const user = ref({})
const getUser = async () => {
  const res = await userGetInfoService() // 请求获取数据
  user.value = res.data.data
}
  1. layout/LayoutContainer页面中调用
import { useUserStore } from '@/stores'
const userStore = useUserStore()
/*mounted                 onMounted
操作dom  渲染之后才可以(操作dom:修改数据或者方法...)  */
onMounted(() => {
  userStore.getUser()
})
  1. 动态渲染
    可以先去演示网址修改再自己演示
<div>优先前面名字
  黑马程序员:<strong>{{ userStore.user.nickname || userStore.user.username }}</strong>
</div>

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

退出功能 [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信息的方法
}

  1. 注册点击事件
<el-dropdown placement="bottom-end" @command="onCommand">

/* command属性要跟配路由时的path最后一项相同 */
<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>
  1. 添加退出功能 就两种功能:退出、跳转
    退出时:

本地的用户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}`)
  }
}
  1. 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

  1. 基本结构样式,用到了 el-card 组件
<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>
  1. 考虑到多个页面复用,封装成组件
    • props 定制标题 组件父传子
    • 默认插槽 default 定制内容主体
    • 具名插槽 extra 定制头部右侧额外的按钮
<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>                                  定制1
        </div>
      </div>
    </template>
    <slot></slot>                                                     定制2
  </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>
  1. 页面中直接使用测试
    ( unplugin-vue-components 会自动注册组件,全局啥都不用,局部要导入(import…))
  • 文章分类测试:
<template>
  <page-container title="文章分类">                               这是个组件,父传子title
    <template #extra>
      <el-button type="primary"> 添加分类 </el-button>            定制1
    </template>

    主体部分-表格                                                 定制2
  </page-container>
</template>
  • 文章管理测试:
<template> 
  <page-container title="文章管理">                              这是个组件,父传子title
    <template #extra>
      <el-button type="primary">发布文章</el-button>             定制1
    </template>

    主体部分-表格+分页                                            定制2
  </page-container>
</template>

文章分类渲染 某个路由页面渲染表格

因为关于分类的请求并不多,可以不用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…

<template #default=“scope”>
<el-button size=“small” @click=“handleEdit(scope.$index, scope.row)”
>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 - 请求获取表格数据

  1. 新建 api/article.js 封装获取频道列表的接口
import request from '@/utils/request'
export const artGetChannelsService = () => request.get('/my/cate/list')
  1. 页面中调用接口,获取数据存储
const channelList = ref([])

const getChannelList = async () => {
  const res = await artGetChannelsService()
  channelList.value = res.data.data
}

el-table 表格动态渲染

<el-table :data="channelList" style="width: 100%">
  <el-table-column label="序号" width="100" type="index"> </el-table-column>     列
  <el-table-column label="分类名称" prop="cate_name"></el-table-column>     
  <el-table-column label="分类别名" prop="cate_alias"></el-table-column>
  <el-table-column label="操作" width="100">
    <template #default="{ row }">
      <el-button
        :icon="Edit"
        circle
        plain
        type="primary"
        @click="onEditChannel(row)"
      ></el-button>
      <el-button
        :icon="Delete"
        circle
        plain
        type="danger"
        @click="onDelChannel(row)"
      ></el-button>
    </template>
  </el-table-column>
  <template #empty>
    <el-empty description="没有数据" />
  </template>
</el-table>


const onEditChannel = (row) => {
  console.log(row)
}
const onDelChannel = (row) => {
  console.log(row)
}

el-table 表格 loading 效果

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

<el-table v-loading="loading">
  1. 发送请求前开启,请求结束关闭
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)

添加功能 编辑功能
点击跳出对话框时,添加无内容,编辑有内容

  1. open({ }) => 添加操作,点击对话框无数据
  2. open({ id: xx, … }) => 编辑操作,有数据
  3. 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 =()=>{
         渲染表格数据的方法()
    }
<!-- 
例子:孩子花钱

子组件内部
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
}
–>

自写(改)表单内容 已完成:绑定规则 对话框标题由formModel.id决定 绑定“确认”方法 表单样式








<template #footer>

<el-button @click=“dialogVisible = false”>取消
<el-button type=“primary” @click=“onSubmit”> 确认


点击显示弹层

  1. 准备弹层
const dialogVisible = ref(false)

<el-dialog v-model="dialogVisible" title="添加弹层" width="30%" >
  <div>我是内容部分</div>
  <template #footer>
    <span class="dialog-footer">
      <el-button @click="dialogVisible = false">取消</el-button>
      <el-button type="primary"> 确认 </el-button>
    </span>
  </template>
</el-dialog>
  1. 点击事件
<template #extra><el-button type="primary" @click="onAddChannel">添加分类</el-button></template>

const onAddChannel = () => {
  dialogVisible.value = true
}

封装弹层组件 ChannelEdit

添加 和 编辑,可以共用一个弹层,所以可以将弹层封装成一个组件

组件对外暴露一个方法 open, 基于 open 的参数,初始化表单数据,并判断区分是添加 还是 编辑

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

具体实现:

  1. 封装组件 article/components/ChannelEdit.vue
<script setup>
import { ref } from 'vue'
const dialogVisible = ref(false)

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

defineExpose({
  open
})
</script>

<template>
  <el-dialog v-model="dialogVisible" title="添加弹层" width="30%">
    <div>我是内容部分</div>
    <template #footer>
      <span class="dialog-footer">
        <el-button @click="dialogVisible = false">取消</el-button>
        <el-button type="primary"> 确认 </el-button>
      </span>
    </template>
  </el-dialog>
</template>
  1. 通过 ref 绑定
const dialog = ref()

<!-- 弹窗 -->
<channel-edit ref="dialog"></channel-edit>
  1. 点击调用方法显示弹窗
const onAddChannel = () => {
  dialog.value.open({})
}
const onEditChannel = (row) => {
  dialog.value.open(row)
}

准备弹层表单

  1. 准备数据 和 校验规则
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'
    }
  ]
}
  1. 准备表单
<el-form
  :model="formModel"
  :rules="rules"
  label-width="100px"
  style="padding-right: 30px"
>
  <el-form-item label="分类名称" prop="cate_name">
    <el-input
      v-model="formModel.cate_name"
      minlength="1"
      maxlength="10"
    ></el-input>
  </el-form-item>
  <el-form-item label="分类别名" prop="cate_alias">
    <el-input
      v-model="formModel.cate_alias"
      minlength="1"
      maxlength="15"
    ></el-input>
  </el-form-item>
</el-form>
  1. 编辑需要回显,表单数据需要初始化
const open = async (row) => {
  dialogVisible.value = true
  formModel.value = { ...row }
}
  1. 基于传过来的表单数据,进行标题控制,有 id 的是编辑
:title="formModel.id ? '编辑分类' : '添加分类'"

确认提交

  1. api/article.js 封装请求 API
// 添加文章分类
export const artAddChannelService = (data) => request.post('/my/cate/add', data)
// 编辑文章分类
export const artEditChannelService = (data) =>
  request.put('/my/cate/info', data)
  1. 页面中校验,判断,提交请求
<el-form ref="formRef">
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
}
  1. 通知父组件进行回显
const emit = defineEmits(['success'])

const onSubmit = async () => {
  ...
  emit('success')
}
  1. 父组件监听 success 事件,进行调用回显
<channel-edit ref="dialog" @success="onSuccess"></channel-edit>

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('删除成功!')
   渲染数据的方法()
 }
  1. api/article.js封装接口 api
// 删除文章分类
export const artDelChannelService = (id) =>
  request.delete('/my/cate/del', {
    params: { id }
  })
  1. 页面中添加确认框,调用接口进行提示
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的发布时间里加:
    <template #default=“{ row }”>
    {{ 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='<el-radio 的 value值>' 
    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='<el-radio 的 value值>' 
    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:

<el-drawer
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属性) 创建本地预览的地址,来预览

<el-upload
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” 关闭自动上传

<img v-if="imgUrl" :src="imgUrl" class="avatar" />                封面的图片
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>   无封面图片时,显示“+”

图片样式:

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

  • 30
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值