Vue3+ElementPlus基础教程

第一章 Vue3

1. Vue3的基础

1.1 Vue3环境安装

Vue官网地址:

https://cn.vuejs.org/
  • 安装nodejs

    安装地址

    官网地址:
    https://nodejs.org/en
    安装地址:
    https://nodejs.org/dist/v20.11.1/node-v20.11.1-x64.msi
    
  • 设置npm的安装源(可选)

    • 临时指定淘宝镜像源

      $ npm --registry https://registry.npm.taobao.org install express
      
    • 永久指定淘宝镜像源

      $ npm config set registry https://registry.npm.taobao.org
      
    • 检查vue包信息

      npm info vue
      
      C:\Users\004>npm info vue
      
      vue@3.4.21 | MIT | deps: 5 | versions: 491
      The progressive JavaScript framework for building modern web UI.
      https://github.com/vuejs/core/tree/main/packages/vue#readme
      
      keywords: vue
      
      dist
      .tarball: https://registry.npmjs.org/vue/-/vue-3.4.21.tgz
      .shasum: 69ec30e267d358ee3a0ce16612ba89e00aaeb731
      .integrity: sha512-5hjyV/jLEIKD/jYl4cavMcnzKwjMKohureP8ejn3hhEjwhWIhWeuzL2kJAjzl/WyVsgPY56Sy4Z40C3lVshxXA==
      .unpackedSize: 2.2 MB
      
      dependencies:
      @vue/compiler-dom: 3.4.21    @vue/runtime-dom: 3.4.21     @vue/shared: 3.4.21
      @vue/compiler-sfc: 3.4.21    @vue/server-renderer: 3.4.21
      
      maintainers:
      - soda <npm@haoqun.me>
      - yyx990803 <yyx990803@gmail.com>
      - posva <posva13@gmail.com>
      
      dist-tags:
      alpha: 3.4.0-alpha.4  csp: 1.0.28-csp       legacy: 2.7.16        v2-latest: 2.7.16
      beta: 3.4.0-beta.4    latest: 3.4.21        rc: 3.4.0-rc.3
      
  • 用最版本创建Vue3项目:

    在控制台,执行npm命令,

    $ npm create vue@latest
    
    ✔ Project name: … <your-project-name>
    ✔ Add TypeScript? … No / Yes
    ✔ Add JSX Support? … No / Yes
    ✔ Add Vue Router for Single Page Application development? … No / Yes
    ✔ Add Pinia for state management? … No / Yes
    ✔ Add Vitest for Unit testing? … No / Yes
    ✔ Add an End-to-End Testing Solution? … No / Cypress / Playwright
    ✔ Add ESLint for code quality? … No / Yes
    ✔ Add Prettier for code formatting? … No / Yes
    
    说明:
    	1. Vue3拥抱TypeScript, 因此在Add TypeScript 选Yes
    	2. 其他默认选No.
    
  • vue2的创建方式

    • 安装脚手架

      $ npm install -g @vue/cli
      
    • 创建项目

      $ vue create my-project
      
    • 运行项目

      $ cd my-project
      $ npm run serve
      

1.2 Vue项目运行

  • 进入项目路径,执行npm install ,系统将下载相关依赖到node_modules的依赖

    $ cd <project_dir>
    $ npm install
    
  • 执行 npm run dev, 运行vue3项目

    $ npm run dev
    
    说明:
    	如果要发布项目,则执行npm run build, 则脚手架将吧代码发布到 dist目录, 后续只需将dist目录部署到http服务器即可
    

1.3 Vue项目目录结构

  • 入口文件 index.html

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8">
        <link rel="icon" href="/favicon.ico">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Vite App</title>
      </head>
      <body>
        <div id="app"></div>
        <script type="module" src="/src/main.ts"></script>
      </body>
    </html>
    
    说明:
    	1. 所有Vue项目,军放置在一个<div>标签里,需要对该div标签指定一个id, 然后需要在脚本中指定该id
    	2. 入口函数,定义在src/main.ts里
    	3. 通常开发一个Vue3项目,该index文件不需要更改
    
  • src目录

    该目录是游戏开发的主要目录,几乎所有开发工作均在该目录内部
    
    • main.ts

      // 引入createApp用于创建应用
      import {createApp} from 'vue'
      // 引入 App跟组件
      import App from './App.vue'
      
      createApp(App).mount('#app');
      
      说明:
      	1. 该文件定义了整个Vue项目的根组件
      	2. 该文件需要指定根的id, 并且启动该组件
      
    • App.vue

      <template>
          <Person/>
      </template>
      
      <script lang="ts">
          import Person from './components/Person.vue';
          // 脚本, ts或js
          export default {
              name:"App",  // 组件名
              components:{Person}
          }
      </script>
      
      说明:
      	1. 该文件是整个Vue的根组件定义了整个项目的框架, 在模板中,可以自定义多个自定义组件
      	2. Vue3可以支持相同组件,定义多次,Veu2中不支持
      	3. 因为脚本使用的ts, 因此在<Script>标签中,需要添加lang="ts"
      	
      
    • components目录

      通常所有的子组件,均放置在 components目录下,

1.4 Vue3 Setup用法

  • Vue3中可以兼容Vue2的语法,但是Vue2中不能包含Vue3的语法

  • Vue2的结构:

    • data: 存放数据
    • methods: 存放用户自定义函数
    • beforeCreate: 执行create前的钩子函数
    • created: 在实例创建完成立即被调用
    • mounted:在挂载之前被调用,此时可以访问当前组件的dom对象
  • Vue3 的核心是,组件开发方式,功能封装在一起,而Vue2在是在data中封装各个功能的数据,对于每个钩子函数,也都放置各个功能的代码,这样功能和功能之间穿插在一起.

  • Vue3的Setup函数是和 data这些平级的, 其内部处理所有的数据,和功能

    <script lang="ts">
        export default {
            name: 'Person',
            setup(){
                console.log('@@', this) // setup里的this 是 undefined, vue3中弱化this了
                // 数据, 原来 写在data中,且是响应式,写这里,不是响应式的
                let name = "张三"; // 此时变量不是响应式
                let age = 18;
                let tel = '13300334556';
                // 方法
                function changeName(){
                    name = '李四';
                }
                function changeAge(){
                    age += 1;
                }
    
                function showTel(){
                    alert(tel);
                }
                //return ()=>"<div><h1>Setup返回值</h1></div>"
                //return function(){return "haha"}
                // 返回这个字典,实际就是通过模板进行渲染
                // return {name:name, changeName:changeName}
                // 返回的也可能是渲染函数
                return {name, age, changeName, changeAge, showTel}
            }
        }
    </script>
    
    说明:
    	1. 返回值: 实际返回的是渲染函数, 如果传递的是一个字典, 则将返回的字典作为变量,对模板进行渲染,如果返回的是一个字符串,则直接渲染字符串
    	2. 在模板中,模板语法,和vue2一致, 使用{{变量名}}进行访问,只是对于默认的setup里的let, 其变量不是响应式变量,如果需要响应式变量,需要用ref或reactive进行修饰.
    	3. vue3兼容vue2的写法,因此,在有setup函数时,在data中仍然可以有数据,因为setup执行时机是在beforeCreate之前, 所有在setup中可以访问vue2中data定义的属性,但是反过来,不行
    	4. 建议如果使用vue3语法,中间不要夹杂vue2的语法.
    

1.5 Setup语法糖的使用

​ 因为每次在setup中,定义了变量后,需要手动在返回值中将需要渲染的变量或者函数进行返回,对于开发者不友好,所以有了Setup语法糖的概念

<!--
    npm i vite-plugin-vue-setup-extend -D 
    <script setup lang="ts" name:"Person">
-->
<script lang="ts">
    export default {
        name: 'Person'
    }
</script>

<script setup lang="ts">
    let name = "张三"; // 此时变量不是响应式
    let age = 18;
    let tel = "13300334556";

    function changeName(){
        name = '李四';
    }
    function changeAge(){
        age += 1;
    }

    function showTel(){
        alert(tel);
    }

</script>
说明:
	1. 需要在scrpt中指定语言是ts
	2. 在script中,不需要手动指定哪些变量,哪些函数是需要渲染,这个过程由底层帮助实现
	3. 如果组件名和组件文件的名称始终一致,可以不需要上面的 export default {name:'Person'}, 但是如果一致,需要保留,或者通过第三方插件vite-plugin-vue-setup-extend来移除该脚本. 此时,名称需要写在 setup的script中的 name属性中, <script setup lang="ts" name:"Person">

2. Vue核心语法

2.1 响应式数据

2.1.1 基本数据响应式实现 Ref的使用

对于基本数据,通过Ref将原来数据进行包裹

<script setup lang="ts">
    import {ref} from 'vue'

    // ref 返回一个RefImpl对象,这个对象是一个响应式对象, 其value,是实际的对象
    // ref是对基本类型的响应数据;
    let name = ref("张三"); 
    let age = ref(18);
    let tel = "13300334556";    // 此时变量不是响应式
    function changeName(){
        // 因为name是 RefImpl对象,那么实际访问该对象时,需要用.value进行访问
        name.value = '李四';
    }
    function changeAge(){
        age.value += 1;
    }
    function showTel(){
        alert(tel);
    }

</script>
说明:
	1. 需要从vue脚本中引入ref函数
	2. ref返回的是一个RefImpl对象, 在模板中,范围时,直接用对象名称,而在ts脚本中,访问,采用obj.value进行
	3. ref值对基础类型有用.
2.1.2 复杂响应类性对象 ref的使用
<script setup lang="ts">
    import {ref} from 'vue'
    // reactive, 返回 Proxy对象
    // 包裹的对象放置.Target对象内部, reactive内,还可以包含数组,等
    let car = ref({
        brand:'奔驰',
        price:100
    })

    let games = ref([
        {id:'abc1001', name:'王者荣耀'},
        {id:'abc1002', name:'元神'},
        {id:'abc1003', name:'三国志'}
    ])

    console.log(car)
    function changePrice(){
        car.value.price += 10;
        console.log("car price:", car.value.price)
    }

    function changeGameName() {
        games.value[0].name = '魔兽世界';
    }
</script>
说明:
	1. Ref除了可以修饰基本数据类型,也可以修饰类和列表类型
	2. Ref如果是基本类型,其内部实现的value则是基本类型,如果其修饰的是对象类型,其内部value则是6.3节中的reactive类型,即Ref对对象的响应式实现,是借助于reactive函数.
	3. Ref实现对象的响应式,其使用,仍然需要 RelImpl.value.xxx进行访问.
2.1.3 复杂响应类型对象 reactive的使用

复杂对象包含 对象(字典), 列表, reactive只能包裹 对象

template:

<template>
    <div class="person">
        <h2>一辆{{car.brand}}车,价值{{car.price}}w</h2>\
        <button @click="changePrice">修改价格</button>

        <div>
            <ul>
                <li v-for="game in games" :key="game.id">
                    <span>{{game.id}}</span>----<span>{{ game.name }}</span>
                </li>
            </ul>
            <button @click="changeGameName">更改游戏名字</button>
        </div>
        <div>
            <div>
                <a> obj.b.c.d val: {{ obj.b.c.d }}</a>
            </div>
            <button @click="changeVal">更改obj.b.c.d</button>
        </div>
    </div>
</template>

script:

<script setup lang="ts">
    import {reactive} from 'vue'
    // reactive, 返回 Proxy对象
    // 包裹的对象放置.Target对象内部, reactive内,还可以包含数组,等
    let car = reactive({
        brand:'奔驰',
        price:100
    })

    let games = reactive([
        {id:'abc1001', name:'王者荣耀'},
        {id:'abc1002', name:'元神'},
        {id:'abc1003', name:'三国志'}
    ])

    let obj = reactive({
        b: {
            c: {
                d:125
            }
        }
    })

    console.log(car)
    function changePrice(){
        car.price += 10;
        console.log("car price:", car.price)
    }

    function changeGameName() {
        games[0].name = '魔兽世界';
    }

    function changeVal() {
        obj.b.c.d += 1
    }
</script>
说明:
	1. 通过reactive函数包裹,返回实际的对象是Proxy对象, 而真实际的数据,在proxy.Target对象内部. 而reactive函数参数,是{}和[]数组对象。
	2. 在ts代码中,访问和直接访问完全是一致的。
	3. 对于Proxy对象,我们通常称之为响应式对象, 而对于reactive的参数里的字典{}称为原生对象。
2.1.4 Ref和reactive的区别和使用场景
  • 各自功能
    • ref 用来定义: 基本类型数据、对象类型数据
    • reactive 用来定义: 对象类型数据
  • 区别:
    • ref 创建的变量必须使用.value(可使用volar插件自动添加.value)
    • reactive 重新分配一个新对象, 会失去响应式(可以使用Object.assign去整体替换)。
  • 使用场景:
    • 若需要一个基本类型的响应式数据,必须用ref
    • 若需要一个响应式对象,层级不慎,ref, reactive都可以使用
    • 若需要一个响应式对象,且层级较深,推荐使用reactive.
2.1.5 toRefs和toRef的使用

当需要对一个对象的字段进行解包时, 如果直接解包,则数据是非响应式的,只有使用toRefs和toRef解包,数据才是响应式的.

<!-- eslint-disable vue/multi-word-component-names -->
<template>
    <div class="person">
        <h3>{{ person.name }}</h3>
        <h3>{{ person.age }}</h3>
        <button @click="ChangeName">修改名称</button>
        <button @click="ChangeAge">修改年龄</button>
        <button @click="ChangeAge2">修改年龄2</button>
    </div>
</template>
<script lang="ts">
    export default {
        name: 'PersonVue'
    }
</script>

<script setup lang="ts">
    import { reactive, toRefs, toRef} from 'vue'
    let person = reactive({
        name: "张三",
        age: 18
    })

    // 默认对象解包,其属性,不是响应式的.
    // let {name, age} = person; 
    // 使用 toRefs, 将对具体的内容的字段进行分别解包,其顺序和传入的顺序是一致的. 当使用toRefs解包后, 属性需要用.value进行访问,而用以前的对象person.name, 仍然可以更改和响应.
    let {name, age} = toRefs(person);
    // 使用 toRef, 对指定的字段进行获取,更改.
    let age3 = toRef(person, 'age');
    
    function ChangeName(){
        name.value += "~";
    }
    function ChangeAge() {
        age.value += 1;
    }

    function ChangeAge2() {
        person.age += 1;
        age3.value += 10;
    }

</script>


说明:
	1. 使用toRefs, 对对象里具体的数据,按照传入的键值顺序,进行一次解包.
	2. 当解包后,其变量,可以直接在模板中使用,如果在ts脚本中使用的话,则需要按照ref的使用规则,添加.value再进行使用
	3. 解包后的变量,实际和原来对象里的变量,是指向同一个数据,当更改了解包后的变量后,其原来对象的变量也同时被更改
	4. 使用解包后的Ref变量进行更改后,原来对象的属性,仍然可以直接更改.

2.2 Computed计算属性的使用

<template>
    <div class="person">
        <div>:<input type="text" v-model="firstName"/>
        </div>
        <div>:<input type="text" v-model="lastName"/>
        </div>
        <h3>{{ fullName }}</h3>
        <input type="button" @click="ChangeFullName" value="更改"/>
    </div>
</template>
<script lang="ts">
    export default {
        name: 'PersonVue'
    }
</script>

<script setup lang="ts">
    import {ref, computed} from 'vue'
    let firstName = ref("zhang")
    let lastName = ref("san")

    let fullName = computed({
        get() {
            return firstName.value.slice(0, 1).toUpperCase() + firstName.value.slice(1) + '-' + lastName.value;
        },
        set(v){
            let [nameF, nameL] = v.split('-');
            firstName.value = nameF;
            lastName.value = nameL;
        }
    })

    function ChangeFullName() {
        fullName.value = 'li-si'
    }

</script>
说明:
	1. Computed用于在html模板中属性更新后的回调, 当有属性依赖于其他属性时, 需要在该函数内进行属性的更新.
	2. Vue设计原则, 在模板中,尽量少的逻辑,或者没有逻辑.
	3. 用Computed参数直接放置箭头函数,则该属性是只读的.
	4. Computed函数返回的对象是 ComputedRefImpl, 脚本内部访问时,需要写.value才能进行操作.
	5. 当把fullname作为数据保存,则逻辑不需要放置在模板中,而可以在脚本中进行实现.

2.3 Watch的使用

  • 作用: 监视数据的变化
  • Vue3中Watch能监视的四种数据
    • ref定义的数据
    • reactive定义的数据
    • 函数返回的一个值(getter函数)
    • 一个包含上述内容的数组
2.3.1 情况1: Ref定义的基本类型
<template>
    <div class="person">
        <h3>{{sum}}</h3>
        <div><button @click="changeSum">自加</button></div>
        <div><h4>{{tips}}</h4></div>
    </div>
</template>
<script lang="ts">
    export default {
        name: 'PersonVue'
    }
</script>

<script setup lang="ts">
    import {ref, watch} from 'vue'
    let sum = ref(0);
    let tips = ref('');
    function changeSum(){
        sum.value += 1;
    }

    let sumStopWatcher = watch(sum, (newVal, oldVal)=>{
        if (newVal>10){
            sumStopWatcher();
        }
        tips.value = `sum 变化前:${oldVal} 变化后:${newVal}`;
    })
</script>
2.3.2 情况2: Ref定义的对象类型
<template>
    <div class="person">
        <div> 
            <h2>情况二: 监视对象数据类型</h2>
            <h3>{{ person.name }}</h3>
            <h3>{{ person.age }}</h3>
            <div>
                <button @click="changeName">改变名称</button>
                <button @click="changeAge">改变年龄</button>
                <button @click="changeObject">改变对象</button>
            </div>
        </div>
    </div>
</template>
<script lang="ts">
    export default {
        name: 'PersonVue'
    }
</script>

<script setup lang="ts">
    import {ref, watch} from 'vue'
    let person = ref({
        name: "zhangsan",
        age:20
    })


    // 函数
    function changeName() {
        person.value.name += "~";
    }

    function changeAge(){
        person.value.age += 1;
    }

    function changeObject() {
        person.value = {"name":"李四", "age":999}
    }

    /* 监视对象,
    参数1: 需要监视的对象
    参数2: 变化时的回调函数,如果是更改对象的属性,实际对象并没有变化,因此,newVal和oldVal是一致. 
    参数3: 配置参数(deep, 是否监听属性变化, immediate, 是否已启动就监听)
    */
    watch(person, (newVal, oldVal)=>{
        console.log(oldVal,oldVal);
    }, {deep:true})

</script>
2.3.3 情况3: reactive定义的对象类型
<template>
    <div class="person">
        <div> 
            <h2>情况三: 监视对象数据(reactive)类型</h2>
            <h3>{{ person.name }}</h3>
            <h3>{{ person.age }}</h3>
            <div>
                <button @click="changeName">改变名称</button>
                <button @click="changeAge">改变年龄</button>
                <button @click="changeObject">改变对象</button>
            </div>
        </div>
    </div>
</template>
<script lang="ts">
    export default {
        name: 'PersonVue'
    }
</script>
<script setup lang="ts">
    import {reactive, watch} from 'vue'
    let person = reactive({
        name: "zhangsan",
        age:20
    })
    // 函数
    function changeName() {
        person.name += "~";
    }

    function changeAge(){
        person.age += 1;
    }

    function changeObject() {
        Object.assign(person, {"name":"李四", "age":999})
    }
    /* 监视对象,
    参数1: 需要监视的reactive对象
    参数2: 变化时的回调函数,如果是更改对象的属性,实际对象并没有变化,因此,newVal和oldVal是一致. 
    参数3: 对于reactive对象,部需要deep:true, 默认就是深度监测,且手动都无法更改成deep:false;
    */
    watch(person, (newVal, oldVal)=>{
        console.log(oldVal,oldVal);
    }, {deep:true})

</script>
2.3.4 情况四: 监视对象中的某个属性

监视ref或reactive定义的【对象类型】数据中的某个属性,注意点如下:

  1. 若改属性值不是【对象类型】, 需要写出函数形式
  2. 若该属性值依然是【对象类型】,可以直接便,也可以写成函数,不过建议写出函数
  3. 监视的是对象,直接写函数式,需要关注对象内部,需要手动开启深度监视
<template>
    <div class="person">
        <div> 
            <h2>情况四: 监视对象数据的指定属性</h2>
            <h3>{{ person.name }}</h3>
            <h3>{{ person.age }}</h3>
            <h4>1:{{person.car.car1}}2:{{person.car.car2}}</h4>
            <div>
                <button @click="changeName">改变名称</button>
                <button @click="changeAge">改变年龄</button>
                <button @click="changeCar1">改变车1</button>
                <button @click="changeCar2">改变车2</button>
                <button @click="changeAllCar">改变整个车</button>
            </div>
        </div>
    </div>
</template>
<script lang="ts">
    export default {
        name: 'PersonVue'
    }
</script>

<script setup lang="ts">
    import {reactive, watch} from 'vue'
    let person = reactive({
        name: "zhangsan",
        age:20,
        car:{
            car1:"奥迪",
            car2:"奔驰"
        }
    })


    // 函数
    function changeName() {
        person.name += "~";
    }

    function changeAge(){
        person.age += 1;
    }

    function changeCar1() {
        person.car.car1 = "宝马"
    }

    function changeCar2() {
        person.car.car2 = "捷豹"
    }
 
    function changeAllCar() {
        person.car = {car1:"艾玛", car2:"乐迪"}
    }
    // 直接监视对象的基础属性, v1和v2是基础值
    watch(()=>person.name, (v1, v2)=>{
        console.log("name:",v1, v2)
    })

    // 监视对象, 虽然对象可以直接监视,但是当整个对象变化时,无法监视,因此最好直接将对象也写函数式
    watch(()=>person.car, (v1, v2)=>{
        console.log("car changed, ", v1.car1, v2.car2, v2.car1, v2.car2)
    }, {deep:true})
</script>
2.3.5 情况五: 同时监视一个对象的多个属性
<script setup lang="ts">
    import {reactive, watch} from 'vue'
    let person = reactive({
        name: "zhangsan",
        age:20,
        car:{
            car1:"奥迪",
            car2:"奔驰"
        }
    })

    // 函数
    function changeName() {
        person.name += "~";
    }

    function changeAge(){
        person.age += 1;
    }

    function changeCar1() {
        person.car.car1 = "宝马"
    }

    function changeCar2() {
        person.car.car2 = "捷豹"
    }
 
    function changeAllCar() {
        person.car = {car1:"艾玛", car2:"乐迪"}
    }
    // 需要监视对象的多个属性,则需要将多个属性放置到一个数组中,且每个数组都是函数式, 其变化值v1,v2,将也是一个数组,数组的每个元素和前面声明的类型一致
    watch([()=>person.name, ()=>person.car.car1, ()=>person.car.car2], (v1, v2)=>{
        console.log("name:",v1, v2)
    })

</script>

2.4 WatchEffect

  • 立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时,重新执行该函数。
  • watch和watchEffect
    • 都能监听响应式数据的变化,不同的是监听数据变化的方式不同
    • watch: 要明确指出监视的数据
    • watchEffect: 不用明确指出监视的数据(函数中用到哪些属性,那就监视哪些属性)
<template>
    <div class="person">
        <div> 
            <h2>watchEffect使用</h2>
            <h3>英语:{{ student.English }}</h3>
            <h3>语文:{{ student.Chinese }}</h3>
            <h3>数学:{{ student.Math }}</h3>
            <div>
                <button @click="changeEnglish">改变英语</button>
                <button @click="changeChinese">改变语文</button>
                <button @click="changeMath">改变数学</button>
            </div>
        </div>
    </div>
</template>
<script lang="ts">
    export default {
        name: 'PersonVue'
    }
</script>

<script setup lang="ts">
    import {reactive, watchEffect} from 'vue'
    let student = reactive({
        English: 10,
        Chinese:10,
        Math:10,
    })


    // 函数
    function changeChinese() {
        student.Chinese += 10;
    }

    function changeEnglish() {
        student.English += 10;
    }

    function changeMath() {
        student.Math += 10;
    }

    watchEffect(()=>{
        if (student.Chinese + student.English + student.Math>=100) {
            console.log(`总分超过100分了! English${student.English}  Chinese:${student.Chinese} Math:${student.Math}`)
        }
    })

</script>

2.5 Ref标签属性

Ref标签的目的,是为了保证每个组件(vue)内部定义的标签,不会和其他的vue进行重名。 如果采用id,进行设计,则如果多个组件重名,则会爆发冲突.

  • Ref可以用在普通html标签上,其实际拿到的是html的dom元素
  • Ref还可以用在组件对象上, 其拿到的是当前组件对象的实例
    • 如果当前组件对象实例中,要访问其子对象,不能直接获取,而要使用defineExpose来进行先把对象暴露出来,然后才可以访问对应的子对象

person.vue

<!-- eslint-disable vue/multi-word-component-names -->
<template>
    <div class="person">
        <div> 
            <h2>watchEffect使用</h2>
            <h3 ref="title">我是标题</h3>
            <div>
                <button @click="obtainTitle">获取标题</button>
            </div>
        </div>
    </div>
</template>
<script lang="ts">
    export default {
        name: 'PersonVue'
    }
</script>

<script setup lang="ts">
    import {ref} from 'vue'
    // 这里的title必须要和标签中的ref的名字一致
    let title = ref();
    // 函数
    function obtainTitle() {
        alert(title.value.innerHTML)
    }
</script>

app.vue

<template>
    <h3 ref="title">AppVue</h3>
    <button @click="getTitle">获取标题</button>
    <Person/>
</template>

<script lang="ts">
    export default {
        name:"AppInstance"  // 组件名
    }
</script>

<!--这里采用Vue3写法, 当引入Person组件后,系统会自动进行注册和返回,所有只需要导入即可-->
<script lang="ts" setup>
    import {ref} from 'vue';
    import Person from './components/Person.vue';
    
    let title = ref();

    function getTitle() {
        //console.log(title.value.innerHTML);
        alert(title.value.innerHTML);
    }
</script>
说明:
	1. 如果需要从App中访问 Person中, 需要引入 import {defineExpose} from 'vue', 在Person.vue中,执行脚本 defineExpose({a,b,c}), 如果a,b,c是对应的Ref对象,那么,在App中则可以访问person.a, person.b, person.c;

2.6 TypeScript的接口, 泛型,以及自定义类型

  • TypeScript 定义接口,目的的为了规范代码,在写代码时,进行规避编译错误。

    • 通常定义接口没单独放置一个ts文件,建议放置到types目录下

    • 定义接口时,interface 关键字, 定义字段时,需要指定每个字段的类型

    • 接口定义后,需要用关键字export进行到出(也可以全部统一导出类型)

      export interface IPerson {
          id: string;
          name: string;
          age: number;
      }
      
    • 使用接口时,需要先要引用该接口, 引用时,为了区分和变量区别,需要增添关键字type

      import { type IPerson, type IPersonList } from "@/types"
      
  • 泛型, 常用的泛型容器主要是数组

    let personList:Array<IPerson> = {[xxx], [xxx]};
    
  • 自定义类型

    export type IPersonList = Array<IPerson>; // 使用时,可以用IPersonList代替Array<IPerson>;
    

完整例子:

types.ts:

// 人的接口定义
export interface IPerson {
    id: string;
    name: string;
    age: number;
}

// 人这个数组的接口定义, 可以直接用泛型Array定义,也可以用[]定义数组.
export type IPersonList = Array<IPerson>;
//export type IPersonList = IPerson[];

Person.vue:

<script setup lang="ts">
    // 导入接口, 不管从哪个位置,只要添加@/, 则都会定位到 src目录, 导入的接口和普通变量为了区分,因此增加一个 type 关键字.
    import { type IPerson, type IPersonList } from "@/types"
    let person:IPerson = {name:"zhangsan", age:20, id:"xab0001"};
    let personList : IPersonList = [
        {name:"lisi", id:"xab0002", age:21},
        {name:"wangwu", id:"xab0003", age:24},
        {name:"zhaoliu", id:"xab0004", age:18}
    ]
</script>

2.7 Prop的使用【父节点向子节点传递数据】

  • 普通传值

App.vue(父组件)

<template>
    <Person title="四年级二班学生" :studentList="personList"/>
</template>

<script lang="ts">
    export default {
        name:"AppInstance"  // 组件名
    }
</script>

<!--这里采用Vue3写法, 当引入Person组件后,系统会自动进行注册和返回,所有只需要导入即可-->
<script lang="ts" setup>
    import {reactive} from 'vue';
    import Person from './components/Person.vue';
    import {type IPersonList} from '@/types'
    let personList:IPersonList = reactive([
        {name:"zhangsan",age:23,"id":"AP0001"},
        {name:"lisi",age:43,"id":"AP0002"},
        {name:"wangwu",age:37,"id":"AP0003"}
    ]);
</script>

Person.vue(子组件)

<script setup lang="ts">
    import {withDefaults} from 'vue'
    import {type IPersonList} from '@/types'
    //defineProps(["title", "studentList"])
    /*
    let parentData = defineProps(["title", "studentList"])
    console.log(parentData.title)
    console.log(parentData.studentList)
    */
   // 指定参数类型,来约束父组件传递的值.
   //defineProps<{studentList:IPersonList, title:string}>()
   withDefaults(defineProps<{studentList:IPersonList, title:string}>(), {title:"标题默认值", studentList:[
   {name:"unknown",age:99,"id":"AP0001"}]})

</script>
  • 子组件指定父组件传递的类型

    传递类型时,使用defineProps时, 采用泛型方法,泛型内部,传递一个需要的结构
    defineProps<{studentList:IPersonList, title:string}>()
    
  • 子组件在父组件不传递时赋予默认值, 通过withDefaults进行

    withDefaults(defineProps<{studentList:IPersonList, title:string}>(), {title:"标题默认值", studentList:[
       {name:"unknown",age:99,"id":"AP0001"}]})
    

2.8 组件的生命周期

又称生命周期函数,生命周期钩子,指组件会经历的关键时间点, 测试可以在函数内增添debugger;

  • Vue2生命周期

    • 创建

      刚刚分配内存时

      • 创建前 beforeCreate
      • 创建完毕 created
    • 挂载

      挂载是当前界面准备好,已经可以访问dom对象了.

      • 挂载前 beforeMount
      • 挂载完毕 mounted
    • 更新

      • 更新前 beforeUpdate
      • 更新完毕 updated
    • 销毁

      • 销毁前 beforeDestroy
      • 销毁完毕 destroyed
  • Vue3 生命周期

    • 创建
      • setup
    • 挂载
      • onBeforeMount
      • onMounted
    • 更新
      • onBeforeUpdate
      • onUpdated
    • 销毁
      • onBeforeUnmount
      • onUnmount
  • 说明:

    • 对于有父组件,还有子组件,只有子组件挂载完成后,父组件才能完成

App.vue

<template>
    <Person v-if="showPerson"/>
    <button @click="togglePerson">{{ showTips }}person</button>
</template>

<script lang="ts">
    export default {
        name:"AppInstance"  // 组件名
    }
</script>

<!--这里采用Vue3写法, 当引入Person组件后,系统会自动进行注册和返回,所有只需要导入即可-->
<script lang="ts" setup>
    import Person from './components/Person.vue';
    import {ref} from 'vue'

    let showPerson = ref(true);
    let showTips = ref("隐藏");

    function togglePerson() {
        showPerson.value = !showPerson.value
        showTips.value = showPerson.value?"隐藏":"显示";
    }

</script>

Person.vue

<!-- eslint-disable vue/multi-word-component-names -->
<template>
    <div class="person">
        <h3>{{ sum }}</h3>
        <button @click="addSum">sum+1</button>
    </div>
</template>
<script lang="ts">
    export default {
        name: 'PersonVue'
    }
</script>

<script setup lang="ts">
    
    import {ref} from 'vue'
    import {onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted} from 'vue'
    let sum = ref(0)
    console.log("创建前");
    function addSum(){
        sum.value += 1;
    }

    onBeforeMount(()=>{
        console.log("挂载前");
    });

    onMounted(()=>{
        console.log("挂载后");
    })

    onBeforeUpdate(()=>{
        console.log("更新前");
    })

    onUpdated(()=>{
        console.log("更新后");
    })
    onBeforeUnmount(()=>{
        console.log("卸载前");
    })
    onUnmounted(()=>{
        console.log("卸载后");
    })
    console.log("创建后");

</script>

2.9 自定义Hooks

  • 准备条件: 安装axios 客户端向服务器发送网络请求

    $ npm i axios
    

    狗图片网站:

    https://dog.ceo/api/breed/pembroke/images/random
    

hooks的目的是将setup里的功能,按照功能单独存放至一个脚本文件中,然后在vue中导入脚本文件,并进行暴露到模板中.

src/hooks/usePerson.ts

import type { IPerson } from '@/types';
import {ref, reactive, computed, onMounted} from 'vue'

export default function(){
    // 数据
    let person:IPerson = reactive({name:"张三", age:25, id:"P0001"})
    let tips = computed(()=>{
        return person.name + "今年" + person.age + "岁了";
    })
    // 方法
    function modifyPersonAge() {
        person.age += 1;
    }
    // 在hook里面可以使用生命周期函数;
    onMounted(()=>{
        person.age=10;
    })
    // 返回需要暴露的数据和方法
    return {person, modifyPersonAge, tips};
}

src/hooks/useDog.ts

import axios from 'axios'
import {ref, reactive} from 'vue'

export default function(){
    // 数据
    let dogList:string[] = reactive(["https://images.dog.ceo//breeds//pembroke//n02113023_2919.jpg"])
    
    // 方法
    async function addDog() {
        try {
            let result = await axios.get('https://dog.ceo/api/breed/pembroke/images/random')
            dogList.push(result.data.message)
        }
        catch(error){
            alert(error);
        }
    }
    return {dogList, addDog};
}

Person.vue

<template>
    <div class="person">
        <h3>玩家信息:</h3>
        <h3>姓名:{{ person.name }}</h3>
        <h3>年龄:{{ person.age }}</h3>
        <button @click="modifyPersonAge">修改年龄</button>{{ tips }}
        <br>
        <div>
            <img v-for="(dog, index) in dogList" :src="dog" :key="index" alt=""/>
        </div>
        <button @click="addDog">增加一条狗</button>
    </div>
</template>
<script lang="ts">
    export default {
        name: 'PersonVue'
    }
</script>
<script setup lang="ts">
    import usePerson from '@/hooks/usePerson';
    import useDog from '@/hooks/useDog';

    const {person, modifyPersonAge, tips} = usePerson();
    const {dogList, addDog} = useDog();
</script>
说明:
	1. 按照模块划分的脚本文件,约定以use+功能名.ts进行命名.
	2. 功能脚本中,定义一个暴露的方法,然后数据和内部方法,均在该函数内部实现,最后暴露出来.
	3. 外部导入时,把方法进行导入,然后用结构化分解赋值, 进行使用函数,达到暴露的目的.
	4. 每个功能脚本中,可以单独使用生命周期里的回调函数.

3. 路由器

3.1 路由器的基本使用

  • 安装路由器:

    $ npm install vue-router@next --save;
    
  • 创建路由器

    router/index.ts

    import {createRouter, createWebHistory } from 'vue-router'
    import AboutVue from '@/components/AboutVue.vue'
    import HomeVue from '@/components/HomeVue.vue'
    import NewsVue from '@/components/NewsVue.vue'
    const router = createRouter({
        history:createWebHistory(),
        // 指定具体的url路由
        routes:[
            {path:"/home", component:HomeVue},
            {path:"/news", component:NewsVue},
            {path:"/about", component:AboutVue}
        ]
    })
    // 暴露路由器
    export default router;
    
  • 使用路由器

    main.ts

    // 引入createApp用于创建应用
    import {createApp} from 'vue'
    // 引入 App跟组件
    import App from './App.vue'
    import router from './router'
    const app = createApp(App);
    app.use(router);
    app.mount('#app');
    
  • 根组件使用路由器

    <template>
        <div class="App">
            <!--标题区域-->
            <div ><h2 class="title">{{ title }}</h2></div>
            <!--导航区域-->
            <div class="navigate">
                <RouterLink to="/home" class="item">首页</RouterLink>
                <RouterLink to="/news" class="item">新闻</RouterLink>
                <RouterLink to="/about" class="item">关于</RouterLink>
            </div>
            <!--正文区域-->
            <div class="content">
                <RouterView></RouterView>
            </div>
        </div>
    </template>
    
    <script lang="ts">
        export default {
            name:"AppInstance"  // 组件名
        }
    </script>
    
    <script lang="ts" setup>
        import {ref} from 'vue'
        import {RouterLink, RouterView} from 'vue-router'
        let title = ref("学生管理系统")
    </script>
    
  • 工程化组织

    • 路由器组件(不直接写标签,而是通过路由器指定规则对应的组件),通常放置在views/pages目录,而一般的组件,则放置在components目录下面
    • 通过导航,视觉效果上消失的组件,默认是被卸载掉的,需要的时候,再去挂载.
  • to的两种写法

    <!--第一种 to的字符串写法, to的内容无法改变-->
    <router-link active-class="active" to="/home">主页</router-link>
    <!--第二种, to的对象写法,可以动态更改路由的匹配路径-->
    <router-link active-class="active" :to="{path:'/home'}">主页</router-link>
    <!--第三种 to的对象写法,  路由名称,需要在  routes = {name:"zhuye", path:"/home", component:xx} 中指定-->
    <router-link active-class="active" :to="{name:'zhuye'}">主页</router-link>
    

3.2 路由器的工作模式

  • history模式

    • 优点:

      URL更加美观,不带有#, 更接近传统的网站URL。
      
    • 缺点:

      后期项目上线,需要服务器配合处理路径问题,否则刷新会有404错误
      
    • 使用方式:

      const router = createRouter({
          history:createWebHistory(),
          routes:[...]
      });
      
    • 使用场景:

      前台网站,因为需要考虑到优化,以及美观, 同时注重性能

  • hash模式

    • 优点:

      兼容性更好, 因为不需要服务器端处理路径。
      
    • 缺点:

      URL带有#不太美观,且SEO优化方面相对较差.
      
      网站的SEO优化是指通过一系列技术和策略手段,提高网站在搜索引擎中的排名,从而增加网站的曝光度和访问量。SEO,即搜索引擎优化(Search Engine Optimization),是一种通过了解搜索引擎的排名机制,对网站进行内部及外部的调整优化,以改进网站在搜索引擎中的关键词自然排名,获得更多流量,从而达成网站销售及品牌建设的目标。
      
    • 使用方式

      const router = createRouter({
          history:createWebHashHistroy(), // hash模式
          routes:[...]
      })
      
    • 使用场景

      通常的后台项目,因为不需要考虑性能,以及seo,而是项目稳定,所以通常hash模式

3.3 嵌套路由

app.vue(父组件)

import {createRouter, createWebHistory } from 'vue-router'
import AboutVue from '@/pages/AboutVue.vue'
import HomeVue from '@/pages/HomeVue.vue'
import NewsVue from '@/pages/NewsVue.vue'
import DetailVue from '@/pages/DetailVue.vue'
const router = createRouter({
    history:createWebHistory(),
    // 指定具体的url路由
    routes:[
        {path:"/", component:HomeVue},
        {path:"/home", component:HomeVue},
        {name:"news", path:"/news", component:NewsVue,
        children:[{
            name:"detail",
            path:"detail",
            component:DetailVue
        }]},
        {path:"/about", component:AboutVue}
    ]
})
// 暴露路由器
export default router;

news.vue

<template>
    <div container style="float:left">
        <div class="navigate">
            <ul>
                <li  v-for="news in newsList" :key="news.id">
                    <RouterLink
                    :to="{
                     name:'detail',
                     query:{
                        id:news.id,
                        title:news.title,
                        content:news.content
                     }
                    }">
                    {{ news.title }}
                    </RouterLink>
                </li>
            </ul>
        </div>
        <div class="content">
            <RouterView></RouterView>
        </div>
    </div>
</template>
<script lang="ts">
    export default {
        name:"NewsVue"  // 组件名
    }
</script>
<script lang="ts" setup>
    import {reactive} from 'vue';
    import { RouterLink } from 'vue-router';
    let newsList = reactive([
        {id:"news001", title:"巴以战争正愈演愈烈!", content:"巴伊战争不但没有结束,且愈演愈烈,以色列的经济也越来越糟糕"},
        {id:"news002", title:"懂王能战胜老拜?", content:"目前的名义,特朗普的支持率明显高于老拜,但是由于特烂狗的管事"},
        {id:"news003", title:"俄乌战争即将结束?", content:"巴伊战争不但没有2323434355越来越糟糕"},
        {id:"news004", title:"菲律宾继续挑战我国底线!", content:"巴伊战争不655656来越糟糕"}
    ])
</script>
说明:
	1. 对于嵌套路由,因为子组件的内容,需要父组件进行传递,因此,在传递参数是,采用to的对象写法,对于参数传递,有两种模式,一种是query, 一种是params.
	2. 在子路由时,to中如果写入的是路径,需要写绝对路径,即/news/detail, 而写名称更方便,这里只需要写入子路由的名称detail即可.

子路由参数的接收

detail.vue

<template>
    <h2>{{ route.query.title }}</h2>
    <a> {{ route.query.content }}</a>
</template>

<script lang="ts">
    export default {
        name:"HomeVue"  // 组件名
    }
</script>

<script lang="ts" setup>
    //import {toRefs} from 'vue';
    import {useRoute} from 'vue-router';
    let route = useRoute(); 
	//let query = toRefs(useRoute())
</script>

<style scoped>
    h2 {
        text-align: center;
    }
    a {
        color: blue;
    }
</style>
  • params参数传递

    • 传递前,需要在路由配置中,路径需要使用占位符, 最后一个如果可能为空,需要添加?

      children:[{
                  name:"detail",
                  path:"detail/:id/:title/:content?",
                  component:DetailVue
              }]},
      
    • 在跳转中指定子路由,和传递参数时,不能使用path路径,而只能使用name.

    • params不能传递数组和对象

3.4 路由的props配置

作用: 让路由组件更方便的收到参数(可以将路由参数作为props传给组件)

  • 方法一:

    在路由配置中其中将props:true, 可以将params参数传递到 子组件上去

  • 方法二:

    在props:配置一个字典,直接传递到子组件上去,很少用

  • 方法三

    props配置函数式方法, 函数内部可以把query参数和params参数都传递到子组件上去

    const router = createRouter({
        history:createWebHistory(),
        // 指定具体的url路由
        routes:[
            {path:"/", component:HomeVue},
            {path:"/home", component:HomeVue},
            {name:"news", path:"/news", component:NewsVue,
            children:[{
                name:"detail",
                path:"detail/:id/:title/:content",
                component:DetailVue,
                /*第一种写法,直接将params的参数传递到props */
                //props:true
                /*第二种写法,传递一个对象 */
                //props:{id:'xx11',title:'xx11',content:"content22"}
                /*第三种写法  =>route.query*/
                props:(route)=>route.params
    
            }]},
            {path:"/about", component:AboutVue}
        ]
    })
    

3.5 路由的push和replace模式

  • push模式

    • 浏览器访问某个网页后,希望点击路由器进行返回,或者再前进,这就是push模式,即当前有个堆栈保留了历史浏览网页的记录,可以回退和前进
    • vue默认是push模式
  • replace模式

    • 浏览器访问某个网页,不让返回,则是replace模式

    • 更改replace模式方法:

      <RouterLink replace to="/about" class="item">关于</RouterLink>
      

3.6 编程式路由导航

脱离<RouterLink> 通过脚本实现导航
使用步骤:
	1. 获取router.
	2. 通过router.push或replace进行路由到目标页面
	3. push的参数,和routerLink中的to的写法完全一致.
使用场景:
	所有需要进行条件判断的才能跳转的,都需要使用编程式路由导航。

主页隔三秒主动跳转到新闻标签

<script lang="ts" setup>
    import {onMounted} from 'vue'
    import {useRouter} from 'vue-router';
    const router = useRouter();

    onMounted(()=>{
        setTimeout(() => {
            router.push({
                name:"news"
            });
        }, 3000);
    })
</script>

新闻中心,点击按钮跳转详细新闻

<template>
    <div container style="float:left">
        <div class="navigate">
            <ul>
                <li  v-for="news in newsList" :key="news.id">
                    <Button class="go-btn" @click="goto(news)" >Go</Button>
                    <RouterLink
                    :to="{
                     name:'detail',
                     params:{
                        id:news.id,
                        title:news.title,
                        content:news.content
                     }
                    }">
                    {{ news.title }}
                    </RouterLink>
                </li>
            </ul>
        </div>
        <div class="content">
            <RouterView></RouterView>
        </div>
    </div>
</template>

<script lang="ts">
    export default {
        name:"NewsVue"  // 组件名
    }
</script>
<script lang="ts" setup>
    import {reactive} from 'vue';
    import { RouterLink , useRouter} from 'vue-router';
    import {INews} from '@/types'
    let newsList = reactive([
        {id:"news001", title:"巴以战争正愈演愈烈!", content:"巴伊战争不但没有结束,且愈演愈烈,以色列的经济也越来越糟糕"},
        {id:"news002", title:"懂王能战胜老拜?", content:"目前的名义,特朗普的支持率明显高于老拜,但是由于特烂狗的管事"},
        {id:"news003", title:"俄乌战争即将结束?", content:"巴伊战争不但没有2323434355越来越糟糕"},
        {id:"news004", title:"菲律宾继续挑战我国底线!", content:"巴伊战争不655656来越糟糕"}
    ])
    let router = useRouter();

    function goto(news:INews){
        router.push({
            name:"detail",
            params: {
                id:news.id,
                title:news.title,
                content:news.content
            }
        })
    }
</script>

3.6 路由重定向

const router = createRouter({
    history:createWebHistory(),
    // 指定具体的url路由
    routes:[
        {name:"home",path:"/home", component:HomeVue},
        {name:"news", path:"/news", component:NewsVue,
        children:[{
            name:"detail",
            path:"detail/:id/:title/:content",
            component:DetailVue,
            /*第一种写法,直接将params的参数传递到props */
            //props:true
            /*第二种写法,传递一个对象 */
            //props:{id:'xx11',title:'xx11',content:"content22"}
            /*第三种写法  =>route.query*/
            props:(route)=>route.params

        }]},
        {name:"about", path:"/about", component:AboutVue},
        // 默认重定向到home组件
        {path:"/", redirect:"/home"},
    ]
})

4. Pinia 状态管理库

官网地址:https://pinia.vuejs.org/zh/
Pinia符合直觉的Vue.js 状态管理库, 和vuex功能类似,但是Pinia提供原生的typescript支持, 而vuex需要额外插件才能使用typescript支持
vuex采用集中式的状态管理模式, 整个状态被集中存储在一个store中,而pinia采用了去中心化的架构,在模块化和代码组织更灵活。

4.1 准备环境

  • 使用场景:

  • 当数据需要供其他组件进行共享时,建议把数据存储在Pinia中,如果仅仅自己组件使用,那么仅仅存在自己的组件内部即可。

  • 准备环境

    • 获取土味情话地址:

      https://api.uomg.com/api/rand.qinghua?format=json
      
    • 生成唯一id第三方库:

      $ npm i uuid
      
      $ npm i nanoid
      
      import {nanoid} from 'nanoid'
      let id = nanoid();
      
    • 安装pinia库

      $ npm i pinia
      
    • 引入pinia

      main.ts

      import {createApp} from 'vue'
      // 1. 引入pinia
      import { createPinia } from 'pinia';
      
      import App from './App.vue'
      const app = createApp(App);
      
      // 2. 创建pinia;
      const pinia = createPinia();
      
      // 3. 使用pinia
      app.use(pinia);
      
      app.mount('#app');
      

4.2 存储读取数据

1. 需要在src创建store目录.
2. 创建和需要存储数据的Vue一样的脚本,命名采用useXXX.ts.
3. 引入库中pinia中defineStore函数.
4. 实现一个defineStore,并且进行导出.

useLoveStore.ts:

import {defineStore } from 'pinia'
import  {nanoid} from 'nanoid';

export const useLoveStore =  defineStore("love", {
    state(){
        return {
            talkList:[
                {
                    id:nanoid(),
                    content:"我觉得你好像一款游戏。什么游戏?我的世界。"
                }
            ]
        }
    }
});

LoveVue.vue

<template>
    <div class="LoveArea">
        <button @click="GenerateTalk">产生一组土味情话</button>
        <ul>
            <li v-for="talk in loveStore.talkList" :key="talk.id">
                {{ talk.content }}
            </li>
        </ul>
    </div>
</template>

<script lang="ts">
    export default {
        name: 'LoveVue'
    }
</script>

<script lang="ts" setup>
    import {reactive} from 'vue';
    import  {nanoid} from 'nanoid';
    import axios from 'axios';

    import { useLoveStore } from '@/store/useLoveVue';
    const loveStore = useLoveStore();
</script>

4.3 修改数据的三种方式

  • 直接修改
  • 使用$patch进行碎片修改
  • 使用action修改, 更改数据的位置放置在store内部;
<template>
    <div class="CountArea">
        <h4>当前和是{{countStore.sum}}</h4>
        <h5>姓名:{{countStore.name}} 年龄:{{countStore.age}}</h5>
        <div>
            <select v-model.number="count">
                <option>1</option>
                <option>2</option>
                <option>3</option>
            </select>
            <button @click="Add">Add</button>
            <button @click="Sub">Sub</button>
        </div>
    </div>
</template>

<script lang="ts">
    export default {
        name: 'CountVue'
    }
</script>

<script lang="ts" setup>
    import {ref} from 'vue';
    import {useCountStore} from '@/store/useCountVue';

    const countStore = useCountStore();

    let count = ref(1);

    function Add() {
        //sum.value += count.value;

        // 第一种数据修改方式: 直接获取字段修改, 或 countStore.$state.sum += count.value;
        // countStore.sum += count.value;

        // // 第二中数据修改方式:
        // countStore.$patch({
        //     sum:222,
        //     name:"lisi",
        //     age:27
        // })

        // 第三种数据修改方式:
        countStore.addVal(count.value);
    }

    function Sub() {
        //sum.value -= count.value;
        countStore.sum -= count.value;
    }
</script>
import {defineStore } from 'pinia'

export const useCountStore =  defineStore("count", {
    state(){
        return {
            sum:1,
            name:"zhangsan",
            age:20
        }
    },
    actions:{
        addVal(val:number){
            if(this.sum +val <= 10)
                this.sum += val;
        }
    }
});
  • storeToRefs使用

    当要对store进行解构赋值时(需要响应式数据), 使用toRefs可以达到要求,但是效率太低,因此更推荐使用storeToRefs
    
    <script lang="ts" setup>
        import {ref} from 'vue';
        import {storeToRefs } from 'pinia'
        import {useCountStore} from '@/store/useCountVue';
    
        const countStore = useCountStore();
        let {sum, age, name} = storeToRefs(countStore);
    </script>
    

4.4 getter的使用

当对默认data中的数据不能满足需求,需要进行加工时,可以通过getter来实现,而在上层访问时,可以直接按照state中的字段进行访问.

useCountVue.ts

export const useCountStore =  defineStore("count", {
    state(){
        return {
            sum:1,
            name:"zhangsan",
            age:20
        }
    },
    actions:{
        addVal(val:number){
            if(this.sum +val <= 10)
                this.sum += val;
        },
        subVal(val:number) {
            if(this.sum-val<0){
                this.sum = 0;
            }
            else {
                this.sum -= val;
            }
        }
    },
    getters:{
        bigSum1:state=>state.sum*10,
        bigSum2(state) {
            return state.sum*10;
        },
        bigSum3(state) {
            return this.sum * 10;
        },
        introduce(state) {
            return `我的名字叫:${state.name} 我今年:${state.age}`;
        }
    }
});
<script lang="ts" setup>
    import {ref} from 'vue';
    import {storeToRefs } from 'pinia'
    import {useCountStore} from '@/store/useCountVue';

    const countStore = useCountStore();
    let {sum, age, name, bigSum1, bigSum2, bigSum3, introduce} = storeToRefs(countStore);
</script>

4.5 $subscribe使用

subscribe如同组件中的watch, 只是这里监听的是pinia的store

  • 监听用法:

    <script lang="ts" setup>
        import axios from 'axios';
    
        import { useLoveStore } from '@/store/useLoveVue';
        const loveStore = useLoveStore();
    
        loveStore.$subscribe((mutate, state:any)=>{
            localStorage.setItem("LoveTalks", JSON.stringify(state.talkList));
        });
    
        // 方法
        function GenerateTalk() {
            loveStore.GenerateTalk();
        }
    </script>
    
  • 适用场景

    当保存的数据发生变化时,进行本地化的时候

    当数据变化,需要向服务器发送消息的时候

    lovevue.vue

    <script lang="ts" setup>
        import axios from 'axios';
    
        import { useLoveStore } from '@/store/useLoveVue';
        const loveStore = useLoveStore();
    
        loveStore.$subscribe((mutate, state:any)=>{
            localStorage.setItem("LoveTalks", JSON.stringify(state.talkList));
        });
    
        // 方法
        function GenerateTalk() {
            loveStore.GenerateTalk();
        }
    </script>
    
  • localStorage本地化

    通常将数据本地化,都需要进行JSON序列号,而在读取的时候进行反序列化.

    在数据本地化后,项目启动时,初始化数据,应该从localStorage中获取.

4.6 Pinia的组合式写法

useLoveVue.vue

import {defineStore } from 'pinia'
import  {nanoid} from 'nanoid';
import axios from 'axios';
import {reactive} from 'vue';

export const useLoveStore = defineStore("love", ()=>{
    let talkList = reactive(JSON.parse(localStorage.getItem("LoveTalks") as string) || []);

    async function GenerateTalk() {
        const {data:{content}} = await axios.get("https://api.uomg.com/api/rand.qinghua?format=json");
        console.log(content);
        talkList.unshift({id:nanoid(), content});
    }
    return {talkList, GenerateTalk}
})
说明:
	1. 使用defineStore的第二个参数,不再是一个字典,而是一个函数
	2. 在函数中,需要将需要使用的变量和函数均要返回出来.
	3. 这个函数相当于组件中的Setup函数

5. 组件之间的通讯

5.1 Props传递

father.vue

<template>
    <div class="father">
        <h2> Prop父组件 </h2>
        <h3>汽车:{{car}}</h3>
        <h3 v-show="toy">来自子的玩家:{{toy}}</h3>
        <Child :car="car" :sendToy="getToy"/>
    </div>
</template>

<script lang="ts">
    export default {
        name:"PropFather"  // 组件名
    }
</script>

<script lang="ts" setup>
    import Child from './Child.vue'
    import {ref} from 'vue';
    let car = ref('奔驰')
    let toy = ref('');
    function getToy(val:string) {
        toy.value = val;
    }
</script>

child.vue

<template>
    <div class="child">
        <h2> 子界面 </h2>
        <h4> 玩具:{{toy}}</h4>
        <h4> 来自父的汽车:{{car}}</h4>
        
        <button @click="sendToy(toy)" >玩具传父</button>
        <!-- <button @click="ManualSendToy" >玩具传父</button> -->
    </div>
</template>

<script lang="ts">
    export default {
        name:"PropFather"  // 组件名
    }
</script>

<script lang="ts" setup>
    import {ref} from 'vue';
    let toy = ref("奥特曼");
    defineProps(['car', 'sendToy'])
    /* 方式1: 直接通过返回值props,调用父节点的函数,将值传递到父组件.
    let props = defineProps(['car', 'sendToy'])
    function ManualSendToy() {
        props.sendToy(toy);
    }
    */
</script>
说明:
	1. 父传递给子数据,直接通过props的值进行传递
	2. 子传递给父数据,需要先在父定义一个函数,带有参数,而后在子进行调用参数,来将子数据传递到父.

5.2 自定义事件(CustomEvent)

自定义事件,注意是由父组件定义事件,而由子组件进行调用

Father.vue

<template>
    <div class="father">
        <h2> Event 父组件 </h2>
        <h4 v-show="toy"> 来自子组件的Toy:{{toy}}</h4>
        <Child @send-toy="GetToy" />
    </div>
</template>

<script lang="ts" setup>
    import {ref} from 'vue'
    import Child from './Child.vue'
    
    let toy = ref("");

    function GetToy(v:string) {
        toy.value = v;
    }
</script>

Child.vue

<template>
    <div class="child">
        <h2> 子界面 </h2>
        <h3>玩家:{{toy}}</h3>
        <button @click="emit('send-toy', toy)">发送父组件</button>
        <GrandSon @send-toy="GetToy"/>
    </div>
</template>

<script lang="ts" setup>
    import {ref} from 'vue'
    import GrandSon from './GrandSon.vue'
    let toy = ref("奥特曼");

    const emit = defineEmits(['send-toy'])

    function GetToy(v:string) {
        emit("send-toy", v)
    }
</script>
说明:
	1. 自定义事件, 是在模板标签中,子组件上定义标签,标签用@开头, 事件的名称,官方建议用烤串的方式进行命名, 如send-key, 而后的函数名字,采用驼峰命名.
	2. 需要自定义事件中可以自定义多个参数.
	3. 在子组件中, 需要使用父组件的自定义消息,需要用defineEmits进行声明可以使用的自定义事件, 在调用自定义事件时,擦用emit('事件名', 附加参数...)进行调用.

5.3 mitt通讯

和mitt类似的功能由 1. pubsub, 2. $bus, 3. mitt.

mitt,是组件需要监听事件,在mitt中提前监听事件, 而需要发送事件的,找到mitt,发送事件,则会触发mitt中监听的所有组件,并回调之前监听事件的回调。

环境安装

$npm i mitt;
  • 定义一个全局的emitter对象

    utils/emitter.ts

    // 引入mitt包
    import mitt from "mitt";
    
    // 生成一个emitter对象
    const emitter = mitt();
    
    // 暴露出去
    export default emitter;
    
  • 需要监听事件的vue组件,注册事件

    Father.vue

    <template>
        <div class="father">
            <h2> Event 父组件 </h2>
            <h4 v-show="toy"> 来自子组件的Toy:{{toy}}</h4>
            <Child />
        </div>
    </template>
    
    
    <script lang="ts" setup>
        import {ref, onMounted, onUnmounted} from 'vue'
        import Child from './Child.vue'
        import emitter from '@/utils/emitter';
        let toy = ref("");
    
        onMounted(()=>{
            emitter.on("send-toy", (val:any)=>{
                toy.value = val;
            })
        });
        onUnmounted(()=>{
            emitter.off("send-toy");
        });
    
    </script>
    
  • 需要触发事件的vue组件

    grandson.vue

    <template>
        <div class="grandson">
            <h2> 孙子界面 </h2>
            <h3>孙子玩具:{{toy}}</h3>
            <button @click="emitter.emit('send-toy', toy)">发送父组件</button>
        </div>
    </template>
    
    <script lang="ts" setup>
        import {ref} from 'vue'
        import emitter from '@/utils/emitter';
        let toy = ref("超级飞侠");
    </script>
    
说明:
	1. mitt通讯,可以适用于所有组件之间的通讯
    2. mitt通讯的参数,和自定义事件类似.
    3. 当接收者,在组件卸载后,应该要移除事件, 即要监听onUnmount事件,并调用emitter.off进行移除

5.4 v-model通讯

说明:
	1. v-model 大量用关于ui库,
	2. v-model 本质是由 :value 和@input进行实现
<template>
    <div class="father">
        <h2> v-Model 父组件 </h2>
        <!-- <input type="text" v-model="username"> -->
        <input type="text" :value="username" @input="username=(<HTMLInputElement>$event.target).value">
    </div>
</template>
$event到底是啥?
	- 对于原生事件, $event就是事件对象, ===>能.target
	- 对于自定义事件, $event就是触发事件时, 所传递的数据.
v-model, 对于html标签和对于组件标签,底层实现原理不一样

组件的v-model实现原理:

        <CustomInput  v-model="username"></CustomInput>
        <CustomInput :modelValue="username" @update:modelValue="username=$event"/>

CustomInput.vue:

<template>
    <div >
        <input type="text" :value="modelValue" @input="emit('update:modelValue', $event.target.value)">
    </div>
</template>

<script lang="ts">
    export default {
        name:"CustomInput"  // 组件名
    }
</script>

<script lang="ts" setup>
    defineProps(['modelValue']);
    const emit = defineEmits(['update:modelValue'])
</script>

Parent.vue

<template>
    <div class="father">
        <h2> v-Model 父组件 </h2>
        <!-- <input type="text" v-model="username"> -->
        <input type="text" :value="username" @input="username=(<HTMLInputElement>$event.target).value">
        <!-- <input type="text" :value="username" @input="onInputChanged"> -->
        <CustomInput  v-model="username"></CustomInput>
		<!--上面等效于下面语句-->
        <CustomInput :modelValue="username" @update:modelValue="username=$event"/>
    </div>
</template>

<script lang="ts">
    export default {
        name:"PropFather"  // 组件名
    }
</script>

<script lang="ts" setup>
    import {ref} from 'vue';
    import CustomInput from './CustomInput.vue'
    let username = ref("zhangsan");
</script>
说明:
	v-model, 即可以父传子,又可以子传父.
  • modelValue这个字段可以更改,但是在绑定v-model时需要进行相应的调整.
<CustomInput  v-model:acc="username" v-model:pwd="password"></CustomInput>

father.vue

<template>
    <div class="father">
        <h2> v-Model 父组件 </h2>
        <CustomInput  v-model:acc="username" v-model:pwd="password"></CustomInput>
        <h4>你输入的账号为:{{username}}</h4>
        <h4>你输入的密码为:{{password}}</h4>
    </div>
</template>

<script lang="ts">
    export default {
        name:"PropFather"  // 组件名
    }
</script>

<script lang="ts" setup>
    import {ref} from 'vue';
    import CustomInput from './CustomInput.vue'
    let username = ref("zhangsan");
    let password = ref("");
</script>

<style scoped>
    .father {
        background-color: aliceblue;
        padding: 20px;
        border-radius: 10px;
    }
</style>

CustomInput.vue

<template>
    <div >
        <input type="text" :value="acc" @input="emit('update:acc', (<HTMLInputElement>$event.target).value)">
        <br/>
        <input type="password" :value="pwd" @input="emit('update:pwd', (<HTMLInputElement>$event.target).value)">
    </div>
</template>

<script lang="ts">
    export default {
        name:"CustomInput"  // 组件名
    }
</script>

<script lang="ts" setup>
    defineProps(["acc", "pwd"]);
    const emit = defineEmits(['update:acc', 'update:pwd'])
</script>

5.5 $attr通讯

  • 概述: $attrs用于实现当前组件的父组件,向当前组件的子组件通讯(祖->孙)

  • 具体说明:

    $attrs是一个对象,包含所有父组件传入的标签属性.

    $attrs会自动排除props中声明的属性

v-bind="{x:100,y:100}"
等效于
:x="100" :y="100"

Father.vue:

<template>
    <div class="father">
        <h2> Event 父组件 </h2>
        <h3> 父亲名字:{{ parentName }} </h3>
        <h4 v-show="toy"> 来自孙子组件的Toy:{{toy}}</h4>
        <Child :parentName="parentName" v-bind="{parentAge:66}" :updateToy="GetToy"/>
    </div>
</template>


<script lang="ts" setup>
    import {ref} from 'vue'
    import Child from './Child.vue'
    let parentName = ref("我是父亲");
    let toy = ref("");

    function GetToy(v:string) {
        toy.value = v;
    }

</script>

Child.vue

<template>
    <div class="child">
        <h2> 子界面 </h2>
        <GrandSon v-bind="$attrs"/>
    </div>
</template>

<script lang="ts" setup>
    import GrandSon from './GrandSon.vue'
</script>

GrandSon.vue

<template>
    <div class="grandson">
        <h2> 孙子界面 </h2>
        <h3>孙子玩具:{{toy}}</h3>
        <h4>父亲名字:{{ parentName }}</h4>
        <h4>父亲年龄:{{ parentAge }}</h4>
        <button @click="updateToy(toy)">发送父组件</button>
    </div>
</template>

<script lang="ts" setup>
    import {ref} from 'vue'
    let toy = ref("超级飞侠");

    defineProps(["parentName", "parentAge", "updateToy"])
</script>

5.6 $refs && $parent

$refs 父传子

$parent 子传父

说明:
	1. 父访问子对象中的成员,需要子组件中,调用 defineExposed({})进行指定哪些需要暴露
	2. $refs存放的是当前组件的子组件的所有列表,存放到一个{(key:string):object}类型

parent.vue

<template>
    <div class="father">
        <h2> refChild 父组件 </h2>
        <h4> 拥有房产:{{ house }}</h4>
        <div>
            <button @click="ChangeChild1Toy()"> 更改c1玩具</button>
            <button @click="ChangeChild2Computer()" > 更改c2电脑</button>
            <button @click="IncrementBooks($refs)" > 增加书籍</button>
        </div>
        <ChildX ref="c1"/>
        <ChildY ref="c2"/>
    </div>
</template>

<script lang="ts">
    export default {
        name:"PropFather"  // 组件名
    }
</script>

<script lang="ts" setup>
    import {ref} from 'vue'
    import ChildX from './Child1.vue'
    import ChildY from './Child2.vue'

    let c1 = ref();
    let c2 = ref();

    let house = ref(4)

    function ChangeChild1Toy() {
        c1.value.toy = "超级飞侠"
    }

    function ChangeChild2Computer() {
        c2.value.computer = "华为"
    }

    function IncrementBooks(refs) {
        for(let key in refs) {
            refs[key].book += 2
        }
    }
    defineExpose({house})
</script>

child1.vue

<template>
    <div class="child">
        <h2> 子界面1 </h2>
        <h4>玩具:{{toy}}</h4>
        <h4>:{{book}}</h4>
        <button @click="addParentHouse($parent)"> 增加父亲房产</button>
    </div>
</template>

<script lang="ts">
    export default {
        name:"PropFather"  // 组件名
    }
</script>

<script lang="ts" setup>
    import {ref} from 'vue'
    let toy = ref("奥特曼")
    let book = ref(5)

    function addParentHouse(parent:any) {
        parent.house += 1;
    }

    defineExpose({toy, book})
</script>

5.7 provide和inject通讯

说明:
	1. provide提供数据, 在其子以及孙力,都可以通过inject来获取对应的数据
	2. 如果需要子传递给父数据,则在父传递一个回调函数
	3. 传递数据时,需要传递响应式数据(ref或reactive)

parent.vue

<template>
    <div class="father">
        <h2>inject 父组件 </h2>
        <h3>存款: {{ money }} 万元 </h3>
        <h4>我有一辆{{car.brand}}车,价值{{car.price}}万元</h4>
        <Child/>
    </div>
</template>

<script lang="ts">
    export default {
        name:"PropFather"  // 组件名
    }
</script>

<script lang="ts" setup>
    import {ref, reactive, provide} from 'vue'
    import Child from './Child.vue'
    let money = ref(100)
    let car = reactive({
        brand:"奥迪",
        price:100
    })

    function changeMoney(val:number) {
        money.value -= val;
    }

    provide('moneyContext', {money, changeMoney})
    provide('car', car)
</script>

Grand.vue

<template>
    <div class="child">
        <h2> 孙界面 </h2>
        <h4> 父亲有一辆{{ car.brand }}, 价值:{{ car.price }}万元 </h4>
        <button @click="changeMoney(6)">更改父亲价格</button>
    </div>
</template>

<script lang="ts">
    export default {
        name:"PropFather"  // 组件名
    }
</script>

<script lang="ts" setup>
    import {ref, inject} from 'vue'
    let {money, changeMoney} = inject('moneyContext', {money:0, changeMoney:(v:number)=>{}})
    let car = inject("car",{brand:'', price:0})
</script>

5.8 Slot通讯

5.8.1 默认插槽
slot是为了让一个组件进行通用, 通常需要在父组件中,自定义子组件中展现的不同的html标签.
img: https://z1.ax1x.com/2023/11/19/piNxLo4.jpg
mp4: http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4

Father.vue

<template>
    <div class="father">
        <h2>slot 父组件 </h2>
        <div class="content">
        <Category title="热门游戏">
            <ul>
                <li v-for="game in gameList" :key="game.id">
                    {{ game.name }}
                </li>
            </ul>
        </Category>
        <Category title="热门城市">
            <img :src="hotCityImg" alt=""/>
        </Category>
        <Category title="热门电影"/>
            <video :src="hotMovieUrl"></video>
        </div>
    </div>
</template>

<script lang="ts">
    export default {
        name:"PropFather"  // 组件名
    }
</script>

<script lang="ts" setup>
    import {ref, reactive} from 'vue'

    import Category from './Category.vue'

    let gameList = reactive([
        {id:"gm001", name:"王者荣耀"},
        {id:"gm002", name:"绝地求生"},
        {id:"gm003", name:"星际争霸"},
        {id:"gm004", name:"梦幻西游"}
    ])
    let hotCityImg = ref('https://z1.ax1x.com/2023/11/19/piNxLo4.jpg')
    let hotMovieUrl = ref('http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4')
</script>

Category.vue

<template>
    <div class="category">
        <h3 class="title"> {{ title}} </h3>
        <slot></slot>
    </div>
</template>

<script lang="ts">
    export default {
        name:"CategoryVue"  // 组件名
    }
</script>

<script lang="ts" setup>
    import {ref} from 'vue';
    defineProps(['title'])
</script>
5.8.2 具名插槽
  • :v-slot标签 可以写在子组件上,也可以写在template标签上,但是如果写在子组件标签上,则也只能填充一个插槽,因此推荐写到tempalte插槽上, 插入的内容用template进行包裹

    <template v-slot:title>
        <h3 class="title">热门游戏</h3>
    </template>
    <!--简写-->
    <template #title>
        <h3 class="title">热门游戏</h3>
    </template>
    
  • 插槽需要定义名字:, 默认不写名字的默认插槽,其名字默认是default.

    <template>
        <div class="category">
            <slot name="title"></slot>
            <slot name="content"></slot>
        </div>
    </template>
    
  • 语法糖, :v-slot 可以直接用 #s1替代, 即<template #s1>

Father.vue

<template>
    <div class="father">
        <h2>slot 父组件 </h2>
        <div class="content">
            <Category title="热门游戏">
                <template v-slot:title>
                    <h3 class="title">热门游戏</h3>
                </template>
                <template #content>
                    <ul>
                        <li v-for="game in gameList" :key="game.id">
                            {{ game.name }}
                        </li>
                    </ul>
                </template>
            </Category>
            <Category title="热门城市">
                <template #content>
                    <img :src="hotCityImg" alt=""/>
                </template>
                <template v-slot:title>
                    <h3 class="title">热门城市</h3>
                </template>
            </Category>
            <Category title="热门电影">
                <template #content>
                    <img :src="hotCityImg" alt=""/>
                </template>
                <template v-slot:title>
                    <h3 class="title">热门电影</h3>
                </template>
            </Category>
        </div>
    </div>
</template>

<script lang="ts">
    export default {
        name:"PropFather"  // 组件名
    }
</script>

<script lang="ts" setup>
    import {ref, reactive} from 'vue'

    import Category from './Category.vue'

    let gameList = reactive([
        {id:"gm001", name:"王者荣耀"},
        {id:"gm002", name:"绝地求生"},
        {id:"gm003", name:"星际争霸"},
        {id:"gm004", name:"梦幻西游"}
    ])
    let hotCityImg = ref('https://z1.ax1x.com/2023/11/19/piNxLo4.jpg')
    let hotMovieUrl = ref('http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4')
</script>

<style scoped>
    .father {
        background-color: aliceblue;
        padding: 20px;
        border-radius: 10px;
    }

    .content {
        display: flex;
        justify-content: space-evenly;
    }

    img, video {
        width:100%;
    }

    .title {
        background-color: goldenrod;
        text-align: center;
        font-size: 20;
    }
</style>

Category.vue

<template>
    <div class="category">
        <slot name="title"></slot>
        <slot name="content"></slot>
    </div>
</template>

<script lang="ts">
    export default {
        name:"CategoryVue"  // 组件名
    }
</script>
5.8.3 作用域插槽
作用域插槽,是指从子组件中,存放数据,通过slot的prop存入到slot中,再由调用插槽的父类进行获取插槽里存放的props数据

parent.vue

<template>
    <div class="father">
        <h2>slot 父组件 </h2>
        <div class="content">
            <Category title="热门游戏">
                <template v-slot:title>
                    <h3 class="title">热门游戏</h3>
                </template>
                <template #content>
                    <ul>
                        <li v-for="game in gameList" :key="game.id">
                            {{ game.name }}
                        </li>
                    </ul>
                </template>
                <template #movieContent="{moveData}">
                    <ol>
                        <li v-for="movie in moveData" :key="movie.id">
                            {{ movie.name }}
                        </li>
                    </ol>
                </template>
            </Category>

Category.vue

<template>
    <div class="category">
        <slot name="title"></slot>
        <slot name="content"></slot>
        <slot name="movieContent" :moveData="movieList"></slot>
    </div>
</template>

<script lang="ts">
    export default {
        name:"CategoryVue"  // 组件名
    }
</script>

<script lang="ts" setup>
    import {reactive} from 'vue'
    let movieList = reactive([
        {id:"m001", name:"终结者4"},
        {id:"m002", name:"侏罗纪公园"},
        {id:"m003", name:"中国医生"},
        {id:"m004", name:"孤注一掷"},
    ])
</script>
说明:
	1. 可以插槽接受数据,同时指定名字, <template #movieContent="{moveData}"> 等效于
	<template v-slot:movieContent="data">
	 <li v-for="item in data.movieData">

6. 其他api

6.1 shallowRef

处理一级层次的数据,改成响应式的,对于深层次的,则不是响应式的。
但是第一次展开则是正常的。
  • 使用场景

    1. 有些时候对象很大,比如一个对象有一百多个属性,每个属性,有可能多层次,如果这个大对象全部用Ref,则树层次很大,而我们只关心部分数据,或者整个数据,所以用shallowref, 关心整个数据
    

6.2 shallowReactive

使用同shallowRef。

6.3 readonly与shallowReadonly

readonly

1. 作用: 用于创建一个对象的深只读副本
2. 用法:
	const original = reactive({...})
	const original2 = ref(...)
	const readOnlyCopy = readonly(original)
	const readOnlyCopy2 = readonly(original2)
3. 特点:
	对象的所有嵌套都将变为只读,对于radOnlyCopy
	但是当original更改后,对应的readOnlyCopy也会跟着改变.
4. 应用场景
	a. 创建不可变的状态快照.
	b. 保护全局状态或配置不被修改.
	c. 模块之间分享当前模块数据给其他模块,只能当前模块数据更改,而其他模块不能修改数据,

shallowReadonly

作用: 与readonly类似,但只作用于对象的顶层属性.
特点:
	只将顶层属性设置为只读, 对象内部的嵌套属性仍然是可变的。

6.4 toRaw与markRow

  • toRaw

    • 作用: 用于获取一个响应式对象的原始对象, toRaw返回的对象不再是响应式的,不会触发视图更新。

      • 这时一个可以用于临时读取而不引起代理访问/跟踪开销,或是写入而不粗发更改的特殊方法,不建议保存对原始对象的持久引用,请谨慎使用。
    • 何时使用?

    • 需要将响应式对象传递给非Vue的库或外部系统时, 使用toRaw可以确保他们收到的是普通对象;.

    • 具体编码

      import {reactive, toRaw, markRaw, isReactive} from 'vue'
      
      let person = reactive({name:'tony', age:18})
      let rawPerson = toRaw(person)
      
  • markRaw

    • 作用: 标记一个对象,使其永远不会变成响应式的。

      • 例如使用mockjs时,为了防止误把mockjs变成响应式对象,可以使用markRaw去标记mockjs
    • 编码:

      <hr>  <!--分割线-->
      let car = markRaw({brand:'Benz', price:100})
      <!--尽管car2是reactive包裹的,但是其源头标记不是响应式,所以car2也不会是响应式-->
      let car2 = reactive(car)
      
    • 使用场景

      • mockjs库

        $ npm i mockjs
        
        import mockjs from 'mockjs'
        let mockjs = markRaw(mockjs)
        // 以后使用mockjs用于不会是响应式对象了.
        

6.5 customRef


作用: 创建一个自定义的ref, 并对其依赖项跟踪和更新触发进行逻辑控制.

<template>
    <div class="person">
        <h2>{{ name }}</h2>
        <input type="text" v-model = "name">
    </div>
</template>

<script lang="ts">
    export default {
        name: 'Person'
    }
</script>
<script lang="ts" setup>
    import {customRef} from 'vue'
    let rawValue = "666";
    let name = customRef((track, trigger)=>{
        return {
            get() {
                track();
                return rawValue
            },
            set(val) {
                rawValue = val;
                trigger();
            }
        }
    })
</script>
说明:
	1. 应用场景, 可以在调用set时, 不立即进行赋值,而是增加一个timeout,这样可以达到延时刷新的目的.

第二章 Elementplus

1.1 Elementplus的简介

Element Plus,一套为开发者、设计师和产品经理准备的基于 Vue 3.0 的桌面端组件库

1.2 Elementplus的环境搭建

  • npm安装element plus

    $ npm install element-plus --save
    
  • 整体导入

    • 浏览器直接引入

      • index.html
      <!DOCTYPE html>
      <html lang="en">
        <head>
          <meta charset="UTF-8">
          <link rel="icon" href="/favicon.ico">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <link rel="stylesheet" href="/css/bootstrap.css"/>
          <link rel="stylesheet" href="//unpkg.com/element-plus/dist/index.css" />
          <!-- 引入 Vue -->
          <script src="//unpkg.com/vue@3"></script>
          <!-- 引入组件库 -->
          <script src="//unpkg.com/element-plus"></script>
          <title>Vite App</title>
        </head>
        <body>
          <div id="app"></div>
          <script type="module" src="/src/main.ts"></script>
        </body>
      </html>
      
    • 修改引入的main.ts

    // 引入createApp用于创建应用
    import {createApp} from 'vue'
    // 引入 App跟组件
    import App from './App.vue'
    
    // 整体导入elementplus;
    import ElementPlus from 'element-plus' // 导入ElementPlus组件库的所有模块和功能
    import 'element-plus/dist/index.css' // 导入ElementPlus组件库所需的全局css样式
    
    const app = createApp(App)
    // 将ElementPlus插件注册到Vue应用中.
    app.use(ElementPlus)
    app.mount('#app')
    
    • 在组件中使用element plus的控件

    person.vue

    <template>
        <div class="person">
            <h2>{{ name }}</h2>
            <input type="text" v-model = "name">
            <div>
                <el-row>
                    <el-button>默认按钮</el-button>
                    <el-button type="primary">主要按钮</el-button>
                    <el-button type="success">成功按钮</el-button>
                    <el-button type="info">信息按钮</el-button>
                    <el-button type="warning">警告按钮</el-button>
                    <el-button type="danger">危险按钮</el-button>
                </el-row>
            </div>
        </div>
    </template>
    
  • 按需导入

    • 安装插件 unplugin-auto-import\unplugin-vue-components\unplugin-icons

      $ npm install -D unplugin-vue-components unplugin-auto-import, unplugin-icons
      
    • 修改vite.config.js

      import AutoImport from 'unplugin-auto-import/vite'
      import Components from 'unplugin-vue-components/vite'
      import {ElementPlusResolver} from 'unplugin-vue-components/resolvers'
      
      export default defineConfig({
        plugins: [
          vue(),
          //VueSetupExtend()
          AutoImport({
              imports: ['vue'],
              resolvers:[
                  ElementPlusResolver(),
                  IconsResolver()
              ],
          }),
          Components([
              resolvers:[
              ElementPlusResolver(),
            	IconsResolver({
                enabledCollections:['ep'],
              })
            ]
          ]),
          Icons({
          	autoInstall:true,
      	})
        ]
      })
      
      说明:
      	1. 使用自动导入图标时,需要使用前缀, i-e-p-, IEp, iEp
      	如: <el-icon><i-ep-Plus/></el-icon>
      	<el-icon><IEpPlus/></el-icon>
      	<el-icon><iEpPlus/></el-icon>
      

1.3 Elementplus基础

1.3.1 Layout布局

通过基础的 24 分栏,迅速简便地创建布局。
  • 通过 :span=“x” 来标记当前分割大小, x=24代表100%, x=12代表50%.

            <el-row>
                <el-col :span="12">
                    <h2>{{ name }}</h2>
                </el-col>
                <el-col :span="12">
                    <input type="text" v-model = "name">
                </el-col>
            </el-row>
    
  • 在el-row中,使用属性 :gutter=“20”, 来设定分割栏之间的距离

            <el-row :gutter="20"> <!--每列间隔20个像素-->
                <el-col :span="6"><div class="grid-content bg-purple"></div></el-col>
                <el-col :span="6"><div class="grid-content bg-purple"></div></el-col>
                <el-col :span="6"><div class="grid-content bg-purple"></div></el-col>
                <el-col :span="6"><div class="grid-content bg-purple"></div></el-col>
            </el-row>
    
  • 在el-row中,可以混合进行布局

            <el-row :gutter="20">
                <el-col :span="16"><div class="grid-content bg-purple"></div></el-col>
                <el-col :span="8"><div class="grid-content bg-purple"></div></el-col>
            </el-row>
            <el-row :gutter="20">
                <el-col :span="8"><div class="grid-content bg-purple"></div></el-col>
                <el-col :span="8"><div class="grid-content bg-purple"></div></el-col>
                <el-col :span="4"><div class="grid-content bg-purple"></div></el-col>
                <el-col :span="4"><div class="grid-content bg-purple"></div></el-col>
            </el-row>
                <el-row :gutter="20">
                <el-col :span="4"><div class="grid-content bg-purple"></div></el-col>
                <el-col :span="16"><div class="grid-content bg-purple"></div></el-col>
                <el-col :span="4"><div class="grid-content bg-purple"></div></el-col>
            </el-row>
    
  • 在el-row中,可以使用justify属性定义子标签的对齐方式

    • start: 居左显示;
    • center: 居中显示;
    • end: 居右显示;
    • space-between: 第一个子布局靠左显示,第二个子布局靠右显示,剩下的均匀分布
    • space-around: 所有子布局均匀分布.
            <!--下面两个layout进行局中显示-->
    		<el-row :gutter="20" justify="center">
                <el-col :span="5"><div class="grid-content bg-purple"></div></el-col>
                <el-col :span="5"><div class="grid-content bg-purple"></div></el-col>
            </el-row>
    
  • 响应式布局

    • 五个响应尺寸(参考BootsTrap)

      • xs

        width < 768px

      • sm

        768px <= width < 992px

      • md

        992px <= width < 1200px

      • lg

        1200px <= width < 1920px

      • xl

        1920 <= width

    • 在响应式布局中, 需要将列,通常设计成,不同响应尺寸模式,不同的尺寸比例

              <el-row :gutter="10">
                  <el-col :xs="8" :sm="6" :md="4" :lg="3" :xl="1"
                      ><div class="grid-content bg-purple">a</div
                  ></el-col>
                  <el-col :xs="4" :sm="6" :md="8" :lg="9" :xl="11"
                      ><div class="grid-content bg-purple-light">b</div
                  ></el-col>
                  <el-col :xs="4" :sm="6" :md="8" :lg="9" :xl="11"
                      ><div class="grid-content bg-purple">c</div
                  ></el-col>
                  <el-col :xs="8" :sm="6" :md="4" :lg="3" :xl="1"
                      ><div class="grid-content bg-purple-light">d</div
                  ></el-col>
              </el-row>
      
  • el-row的属性

    • glutter: 栅格间隔, 类型:number
    • justify: flex 布局下的水平排列方式 start/end/center/space-around/space-between, 默认: start
    • align: flex 布局下的垂直排列方式, top/middle/bottom, 默认: top
    • tag: 自定义标签
  • el-col的属性

    • span:栅格占的格数
    • offset: 栅格左侧间隔格数
    • push: 栅格向右移动格数
    • pull: 栅格向左移动格数
    • xs/sm/md/lg/xl: 不同分辨率下对应的栅格个数

1.3.2 Container 布局容器

​ 用于布局的容器组件,方便快速搭建页面的基本结构:

  • : 外层容器。当子元素包含 或 时, 全部子元素会垂直上下排列,否则会水平左右排列

  • : 顶栏容器

  • : 侧边栏容器

  • : 主要区域容器

  • : 底栏容器

    说明:
    	1. el-container的直接子元素必须是后四个组件中的一个或多个.
    	2. el-header, el-aside, el-main, el-footer的父元素必须是el-container
    

    常用的组合

  • 常用的组合:

    • Header + Main

      <template>
        <div class="common-layout">
          <el-container>
            <el-header>Header</el-header>
            <el-main>Main</el-main>
          </el-container>
        </div>
      </template>
      
    • Header + Main + Footer

      <template>
        <div class="common-layout">
          <el-container>
            <el-header>Header</el-header>
            <el-main>Main</el-main>
            <el-footer>Footer</el-footer>
          </el-container>
        </div>
      </template>
      
    • Aside + Main

      <template>
        <div class="common-layout">
          <el-container>
            <el-aside width="200px">Aside</el-aside>
            <el-main>Main</el-main>
          </el-container>
        </div>
      </template>
      
    • Header + (Aside + Main)

      <template>
        <div class="common-layout">
          <el-container>
            <el-header>Header</el-header>
            <el-container>
              <el-aside width="200px">Aside</el-aside>
              <el-main>Main</el-main>
            </el-container>
          </el-container>
        </div>
      </template>
      
    • Header + (Aside + (Main + Footer))

      <template>
        <div class="common-layout">
          <el-container>
            <el-header>Header</el-header>
            <el-container>
              <el-aside width="200px">Aside</el-aside>
              <el-container>
                <el-main>Main</el-main>
                <el-footer>Footer</el-footer>
              </el-container>
            </el-container>
          </el-container>
        </div>
      </template>
      
    • Aside+ (Header+ Main)

      <template>
        <div class="common-layout">
          <el-container>
            <el-header>Header</el-header>
            <el-container>
              <el-aside width="200px">Aside</el-aside>
              <el-container>
                <el-main>Main</el-main>
                <el-footer>Footer</el-footer>
              </el-container>
            </el-container>
          </el-container>
        </div>
      </template>
      
    • Aside + (Header + Main + Footer)

      <template>
        <div class="common-layout">
          <el-container>
            <el-aside width="200px">Aside</el-aside>
            <el-container>
              <el-header>Header</el-header>
              <el-main>Main</el-main>
              <el-footer>Footer</el-footer>
            </el-container>
          </el-container>
        </div>
      </template>
      

1.3.3 图标

  • 安装:

    npm install @element-plus/icons-vue
    
  • 注册所有图标

    main.ts

    // main.ts
    
    // 如果您正在使用CDN引入,请删除下面一行。
    import * as ElementPlusIconsVue from '@element-plus/icons-vue'
    
    const app = createApp(App)
    for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
      app.component(key, component)
    }
    
  • 图标属性

    • size 图标的尺寸
    • color 图标的颜色
  • 例子

    <el-row>
        <el-icon><Plus/> </el-icon>
        <el-icon><Edit/> </el-icon>
        <el-icon><Delete/> </el-icon>
        <el-icon color="red" size="40"><ArrowLeft/> </el-icon>
    </el-row>
    
    <!--搜索图标和按钮的组合-->
    <el-row>
        <el-button type="primary"> <el-icon><Search/> </el-icon><span> 搜索</span></el-button>
    </el-row>
    
    <!--按钮组-->
    <el-button-group>
        <el-button type="primary"> <el-icon><Plus/> </el-icon></el-button>
        <el-button type="primary"> <el-icon><Edit/> </el-icon></el-button>
        <el-button type="primary"> <el-icon><Delete/> </el-icon></el-button>
    </el-button-group>
    

1.3.4 按钮

  • 按钮的种类(type)

        <el-button>Default</el-button>
        <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>
    
  • 按钮的扁平模式(plain)

    <el-button type="primary" plain>Primary</el-button>
    
  • 圆角按钮(round)

    <el-button type="primary" round>Primary</el-button>
    
  • 圆形图标按钮(circle)

    <el-button type="primary" :icon="Edit" circle />
    
  • 按钮的禁用状态(disabled)

    <el-button type="success" disabled>Success</el-button>
    <el-button type="primary" :disabled="isDisabled" > <el-icon><Edit/> </el-icon></el-button>
    <!--点击Delete按钮, 将isDisabled改为true, 让按钮Edit按钮变成可用状态.-->
    <el-button type="primary" @click="isDisabled=false" > <el-icon><Delete/> </el-icon></el-button>
    
    <script lang="ts" setup>
        import {ref} from 'vue'
        let name = ref("Hello, world")
        let isDisabled = ref(true) // Edit按钮,默认禁用状态.
    </script>
    
  • 链接按钮(link)

    <el-button type="primary" link> <el-icon><Search/> </el-icon><span> 搜索</span></el-button>
    
  • 文字按钮(text)

    <el-button type="primary" link>  搜索</el-button>
    <!--文字按钮和连接按钮主要区别,文字按钮鼠标上面,有背景和边框,链接按钮没有-->
    
  • 图标按钮

    <!--图标按钮,是按钮和图标el-icon搭配使用-->
    <el-button type="primary" > <el-icon><Edit/> </el-icon></el-button>
    
  • 按钮组

      <el-button-group>
        <el-button type="primary" :icon="Edit" />
        <el-button type="primary" :icon="Share" />
        <el-button type="primary" :icon="Delete" />
      </el-button-group>
    <!--按钮组,会处理两边按钮的边框,使其融为一体-->
    
  • 加载按钮

    <el-button type="primary" loading>Loading</el-button>
    <el-button type="primary" :loading="isLoading">Loading</el-button>
    <!--loadingIcon 可以自定义加载图标 -->
    <el-button type="primary" :loading-icon="Eleme" loading>Loading</el-button>
    
  • 调整尺寸(size)

    <el-button type="primary" size="large">  搜索</el-button>
    <el-button type="primary">  搜索</el-button>
    <el-button type="primary" size="small">  搜索</el-button>
    <!--size进仅三个尺寸,默认,大,小-->
    
  • 自定义颜色(color)

    <el-button color="#626aef" :dark="isDark">Default</el-button>
    <!--dark=true, 则hover时颜色会更深,否则颜色会更亮-->
    

1.3.5 提示框

  • ElMessage

    import {ElMessage} from 'element-plus'
    const openMsg = ()=>{
        ElMessage({
            type:"success", // success, warning, info, error
            message:'恭喜你成功获取屠龙刀',
            showClose:true
        })
    }
    
  • ElMessageBox

    import {ElMessageBox} from 'element-plus'
    const openConfirm = ()=>{
        ElMessageBox.confirm("确认删除", '移除物品', {
            type:'warning',
            confirmButtonText: '确认',
            cancelButtonText: '取消'
        }).then(()=>{
            console.log("confirm")
        }).catch(()=>{
            console.log('cancel')
        })
    }
    
    • ElNotification
import {ElNotification} from 'element-plus'
const openNotify1 = ()=>{
    ElNotification({
        title:"标题",
        message:'通知内容',
        duration: 1500 // 展示事件,单位毫秒
    })
}
const openNotify2 = () => {
    ElNotification ({
        type:'success', // success | warning | info | error
        title: '标题',
        message: "通知内容",
        duration : 1500,
        position: 'bottom-right'
    })
}

1.3.5 导航

导航通过el-menu、el-sub-menu, el-menu-item组合进行

  • 垂直导航

    <template>
        <div class="content">
            <div class="common-layout">
                <el-container>
                    <el-menu  :default-active="selectedIndex" @select="selected">
                        <el-menu-item index="1"> 主页 </el-menu-item>
                        <el-menu-item index="2"> 新闻 </el-menu-item>
                        <el-sub-menu index="3">
                            <template #title>产品</template>
                            <el-menu-item index="3-1">家电</el-menu-item>
                            <el-menu-item index="3-2">厨具</el-menu-item>
                        </el-sub-menu>
                        <el-menu-item index="4">联系我们</el-menu-item>
                        <el-menu-item index="5">关于</el-menu-item>
                    </el-menu>
                </el-container>
            </div>
        </div>
    </template>
    
    <script lang="ts" setup>
        import {ref} from 'vue'
        
        let selectedIndex = ref("1")
    
        const selected = (index, indexPath) =>{
            console.log("index", index, "indexPath", indexPath)
        }
        
    </script>
    
  • 水平导航

    <template>
        <div class="content">
            <div class="common-layout">
                <el-container>
                    <el-menu mode="horizontal" :default-active="selectedIndex" @select="selected">
                        <el-menu-item index="1"> 主页 </el-menu-item>
                        <el-menu-item index="2"> 新闻 </el-menu-item>
                        <el-sub-menu index="3">
                            <template #title>产品</template>
                            <el-menu-item index="3-1">家电</el-menu-item>
                            <el-menu-item index="3-2">厨具</el-menu-item>
                        </el-sub-menu>
                        <el-menu-item index="4">联系我们</el-menu-item>
                        <el-menu-item index="5">关于</el-menu-item>
                    </el-menu>
                </el-container>
            </div>
        </div>
    </template>
    
    <script lang="ts" setup>
        import {ref} from 'vue'
        
        let selectedIndex = ref("1")
    
        const selected = (index, indexPath) =>{
            console.log("index", index, "indexPath", indexPath)
        }
        
    </script>
    
  • el-menu 属性

    • background-color: 菜单背景颜色
    • text-color: 文本颜色
    • active-text-color: 选中文本颜色
    • style: 样式,可以设为宽度和高度
    <el-menu  mode="horizontal" :default-active="selectedIndex" @select="selected" background-color="#0064" text-color="#0ff" active-text-color="#00ff00" style="height:40px;width:600px">
        <el-menu-item index="1"> 主页 </el-menu-item>
        <el-menu-item index="2"> 新闻 </el-menu-item>
        <el-sub-menu index="3">
            <template #title>产品</template>
            <el-menu-item index="3-1">家电</el-menu-item>
            <el-menu-item index="3-2">厨具</el-menu-item>
        </el-sub-menu>
        <el-menu-item index="4">联系我们</el-menu-item>
        <el-menu-item index="5">关于</el-menu-item>
    </el-menu>
    
  • 面包屑

    面包屑用于描述当前页面的路径,可在路径上点击返回到上一级路径

    <h3> 面包屑 </h3>
    <el-breadcrumb separator=">"> <!--separator用于指定分隔符-->
        <el-breadcrumb-item><a href="#">首页</a></el-breadcrumb-item>
        <el-breadcrumb-item><a href="#">产品</a></el-breadcrumb-item>
        <el-breadcrumb-item><a href="#">关于我们</a></el-breadcrumb-item>
    </el-breadcrumb>
    
    展示效果:
    首页>产品>关于我们
    
  • 下拉菜单

    <h3>下拉菜单</h3>
    <el-dropdown @command="userCommand">
        <span>
        个人中心<el-icon><User/></el-icon>
    </span>
    <template #dropdown>
        <el-dropdown-menu>
            <el-dropdown-item command="order">订单</el-dropdown-item>
            <el-dropdown-item command="logout">退出</el-dropdown-item>
        </el-dropdown-menu>
    </template>
    </el-dropdown>
    
    <script lang="ts" setup>
        <!--当用户点击具体的el-dropdown-item, 则会将对应的command传递到该函数-->
        const userCommand = (command:String) => {
            console.log("command:", command)
        }
    </script>
    

1.3.6 标签页

<h3> 标签页 </h3>
<el-tabs v-model="selectedName" @tab-click="tabClick">
    <el-tab-pane label="排行榜" name="1">这里是排行榜</el-tab-pane>
    <el-tab-pane label="好友" name="2">这里是好友</el-tab-pane>
    <el-tab-pane label="背包" name="3">这里是背包</el-tab-pane>
    <el-tab-pane label="上帝" name="4">这里是商店</el-tab-pane>
</el-tabs>
<script lang="ts" setup>
    import {ref} from 'vue'
    
    // 默认选中的标签名称
    let selectedName = ref("2")

    // 选中标签触发的回调
    const tabClick = (tab:any, event:any) =>{
        console.log("tab", tab.props, 'event', event, tab.props.name)
    }

</script>
说明:
	1. 每个标签定义一个名字name属性, el-tabs的v-model和对应的name对应
	2. tabClick的tab.props中可以获取每个item的属性. 获取name,需要tab.props.name 

如果要将标签也设置成卡片风格,只需要在el-tabs增加type=“card”, 有标签的卡片风格: border-card

<el-tabs v-model="selectedName" @tab-click="tabClick" type="card">
  • 动态标签页

    <h3>动态添加</h3>
    <el-button @click="tabAdd">添加</el-button>
    <el-tabs v-model="selectedName2" @tab-remove="tabRemove" closable type="card">
        <el-tab-pane v-for="value in tab.arr" :key="value.name" :label="value.title" :name="value.name">
            {{ value.content }}
        </el-tab-pane>
    </el-tabs>
    
    <script lang="ts" setup>
        import {ref, reactive} from 'vue'
        let selectedName2 = ref("1")
        const tabAdd = () =>{
            let index = tab.arr.length
            index++
            tab.arr.push({
                name:index.toString(),
                title:"新建选项"+index,
                content:"这是新建内容"+index
            })
        }
        const tabRemove = (name:string)=>{
            console.log("tabremove", name)
            const index = tab.arr.findIndex((v)=>v.name === name);
            tab.arr.splice(index, 1) // 移除
        }
    
        const tab = reactive({
            arr:[
                {name:"1", title:"语文", content:"语文分数"},
                {name:"2", title:"数学", content:"数学分数"},
                {name:"3", title:"英语", content:"英语分数"},
            ]
        })
    
    </script>
    

1.3.7 输入框

  • 文本输入框

    <el-input v-model="name" maxlength="10" show-word-limit clearable :placeholder="nameHolder"/>
    
  • 密码框

    <el-input v-model="password" show-password :placeholder="passwordHolder"/>
    
  • 文本域

    <el-input v-model="introduce"  maxlength="50" show-word-limit type="textarea" rows="3" placeholder="这家伙太懒了"/>
    
  • 输入框属性

    • size

      • large
      • small
    • 输入框前置

      <el-row>
          <el-col :span="6">
              个人主页1:
          </el-col>
          <el-col :span="18">
              <el-input v-model="webUrl">
                  <template #prepend>https://</template>
              </el-input>
          </el-col>
      </el-row>
      
    • 输入框后置

      <el-row>
          <el-col :span="6">
              联系QQ邮箱:
          </el-col>
          <el-col :span="18">
              <el-input v-model="qqEmail">
                  <template #append>qq.com</template>
              </el-input>
          </el-col>
      </el-row> 
      
    • 输入框中置

      <el-row>
          <el-col :span="6">
              个人主页2:
          </el-col>
          <el-col :span="18">
              <el-input v-model="webUrl2">
                  <template #prepend>https://</template>
                  <template #append>.com</template>
              </el-input>
          </el-col>
      </el-row> 
      

完整例子

<template>
    <div class="content">
        <div class="common-layout">
            <h3 style="text-align:center margin-bottom:100px"> 用户注册 </h3>
            <el-row>
                <el-col :span="6">
                    账号:
                </el-col>
                <el-col :span="18">
                    <el-input v-model="name" maxlength="10" show-word-limit clearable :placeholder="nameHolder"/>
                </el-col>
            </el-row>
            <el-row>
                <el-col :span="6">
                    密码:
                </el-col>
                <el-col :span="18">
                    <el-input v-model="password" maxlength="10" show-password :placeholder="passwordHolder"/>
                </el-col>
            </el-row>
            <el-row>
                <el-col :span="6">
                    自我介绍:
                </el-col>
                <el-col :span="18">
                    <el-input v-model="introduce"  maxlength="50" show-word-limit type="textarea" rows="3" placeholder="这家伙太懒了"/>
                </el-col>
            </el-row>
            <el-row>
                <el-col :span="6">
                    个人主页1:
                </el-col>
                <el-col :span="18">
                    <el-input v-model="webUrl">
                        <template #prepend>https://</template>
                    </el-input>
                </el-col>
            </el-row>
            <el-row>
                <el-col :span="6">
                    个人主页2:
                </el-col>
                <el-col :span="18">
                    <el-input v-model="webUrl2">
                        <template #prepend>https://</template>
                        <template #append>.com</template>
                    </el-input>
                </el-col>
            </el-row> 
            <el-row>
                <el-col :span="6">
                    联系QQ邮箱:
                </el-col>
                <el-col :span="18">
                    <el-input v-model="qqEmail">
                        <template #append>qq.com</template>
                    </el-input>
                </el-col>
            </el-row> 
            <el-row>
                <el-col :span="6">
                    请输入擅长学科:
                </el-col>
                <el-col :span="18">
                    <el-input placeholder="请输入课程名称">
                        <template #prepend>
                            <el-select v-model="selectedClass" place="请选择" style="width:100px" >
                                <el-option label="语文" value="1"/>
                                <el-option label="数学" value="2"/>
                                <el-option label="英语" value="3"/>
                            </el-select>
                        </template>
                        <template #append>
                            <el-button>
                                <el-icon><Search/></el-icon>
                            </el-button>
                        </template>
                    </el-input>
                </el-col>
            </el-row>
        </div>
    </div>
</template>
<script lang="ts" setup>
    import {ref} from 'vue'
    let name = ref("")
    let nameHolder = ref("请输入账号")
    let password = ref("")
    let passwordHolder = ref("请输入密码")
    let introduce = ref("")
    let webUrl = ref("")
    let qqEmail = ref("")
    let webUrl2 = ref("")
    let selectedClass = ref("1")
</script>

1.3.8 单选框复选框

  • 单选框

    <el-row>
        <el-col :span="6">
            语言:
        </el-col>
        <el-col :span="18">
            <el-radio v-model="lang" @change="langChange" value="1">英语</el-radio>
            <el-radio v-model="lang" @change="langChange" value="2">日语</el-radio>
            <el-radio v-model="lang" @change="langChange" value="3">汉语</el-radio>
        </el-col>
    </el-row>
    
    <script lang="ts" setup>
        import {ref} from 'vue'
        let lang = ref("1")
        const langChange = (v:string)=>{
            console.log("langChanged", v)
        }
    </script>
    
    说明: 
    	1. 将每个el-radio单独进行绑定一个career变量
    	2. 绑定的事件的参数是实际切换的选项的value值
    
  • 单选框组

    <el-row>
        <el-col :span="6">
            籍贯:
        </el-col>
        <el-radio-group v-model="city" @change="cityChange">
            <el-radio value="cd">成都</el-radio>
            <el-radio value="sh">上海</el-radio>
            <el-radio value="bj">北京</el-radio>
        </el-radio-group>
    </el-row>
    
    说明: 
    	1. 单选框组,不需要每个选项绑定一个变量,而只需在组中绑定就可以
    
  • 复选框

    <el-row>
        <el-col :span="6">
            学科:
        </el-col>
        <el-checkbox-group v-model="subjects" @change="subjectChanged">
            <el-checkbox value="1">高等数学</el-checkbox>
            <el-checkbox value="2">大学英语</el-checkbox>
            <el-checkbox value="3">马列主义</el-checkbox>
            <el-checkbox value="4">电子电路</el-checkbox>
        </el-checkbox-group>
    </el-row>
    
    <script lang="ts" setup>
        import {ref} from 'vue'
        let subjects = ref(["1", "2"])
        const subjectChanged = (v:any)=>{
            console.log(v)
        }
    </script>
    
    说明:
    	1. 数组,这里实验了下,必须使用ref定义响应式数组,用reactive无效.
    	2. 回调函数的v,是当前选择的所有的数组.
    

1.3.9 下拉框

  • 静态下拉框

                <el-row>
                    <el-col :span="6">
                        城市:
                    </el-col>
                    <el-col :span="18">
                        <el-select v-model="city" placeholder="请选择" @change="selectCity">
                            <el-option value="cd" label="成都"/>
                            <el-option value="cd" label="上海"/>
                            <el-option value="cd" label="北京"/>
                        </el-select>
                    </el-col>
                </el-row>
    
  • 动态下拉框

    <el-row>
        <el-col :span="6">
            年龄:
        </el-col>
        <el-col :span="18">
            <el-select v-model="ageIndex" placeholder="请选择" @change="selectAges">
                <el-option v-for="item in ageRanges" :value="item.value" :key="item.value" :label="item.label"/>
            </el-select>
        </el-col>
    </el-row>
    
  • 动态多选下拉框

<el-row>
    <el-col :span="6">
        爱好:
    </el-col>
    <el-col :span="18">
        <el-select v-model="selectHobbies" multiple placeholder="请选择" @change="hobbiesChange">
            <el-option v-for="item in hobbies" :value="item.value" :key="item.value" :label="item.label"/>
        </el-select>
    </el-col>
</el-row>
 说明:
 	1. 多选下拉框,只需添加 multiple即可.

完整例子:

<template>
    <div class="content">
        <div class="common-layout">
            <h3 style="text-align:center">  </h3>
            <el-row>
                <el-col :span="6">
                    城市:
                </el-col>
                <el-col :span="18">
                    <el-select v-model="city" placeholder="请选择" @change="selectCity">
                        <el-option value="cd" label="成都"/>
                        <el-option value="cd" label="上海"/>
                        <el-option value="cd" label="北京"/>
                    </el-select>
                </el-col>
            </el-row>
            <el-row>
                <el-col :span="6">
                    年龄:
                </el-col>
                <el-col :span="18">
                    <el-select v-model="ageIndex" placeholder="请选择" @change="selectAges">
                        <el-option v-for="item in ageRanges" :value="item.value" :key="item.value" :label="item.label"/>
                    </el-select>
                </el-col>
            </el-row>
            <el-row>
                <el-col :span="6">
                    爱好:
                </el-col>
                <el-col :span="18">
                    <el-select v-model="selectHobbies" multiple placeholder="请选择" @change="hobbiesChange">
                        <el-option v-for="item in hobbies" :value="item.value" :key="item.value" :label="item.label"/>
                    </el-select>
                </el-col>
            </el-row>
        </div>
    </div>
</template>

<script lang="ts">
    export default {
        name: 'Content'
    }
</script>
<script lang="ts" setup>
    import {ref} from 'vue'
    
    let city = ref("cd")
    const selectCity = (v:any)=>{
        console.log("cityChanged", v)
    }

    let ageRanges = ref([
        {value:1,label:"6岁以下"},
        {value:2,label:"6岁-12岁"},
        {value:3,label:"13岁-18岁"},
        {value:4,label:"18岁-60岁"},
        {value:5,label:"60岁以上"},
    ])

    let hobbies = ref([
        {value:1,label:"篮球"},
        {value:2,label:"足球"},
        {value:3,label:"健美"},
        {value:4,label:"体操"},
        {value:5,label:"游泳"},
    ])

    let ageIndex = ref(2)
    const selectAges = (v:string)=>{
        console.log("selectAges", v)
    }

    let selectHobbies = ref([1,3])

    const hobbiesChange = (v:any)=>{
        console.log("hobbiesChange", v)
    }
</script>

1.3.10 日期选择器

  • 导入中文语言包

    main.ts

    // 引入createApp用于创建应用
    import {createApp} from 'vue'
    // 引入 App跟组件
    import App from './App.vue'
    import ElementPlus from 'element-plus'
    import zhCn from 'element-plus/dist/locale/zh-cn.mjs' // ElementPlus中文语言包
    import 'element-plus/dist/index.css'
    
    // 使用图标
    import * as ElementPlusIconsVue from '@element-plus/icons-vue' // 导入ElementPlus组件库中所有图标.
    
    const app = createApp(App)
    
    // 注册 ElementPlus组件库中所有图标到全局Vue应用中.
    
    for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
        app.component(key, component)
      }
    
    app.use(ElementPlus, {
      locale: zhCn
    })
    app.mount('#app')
    
  • 日期选择器

    <el-row>
        <el-col :span="6">
            出生日期:
        </el-col>
        <el-col :span="18">
            <el-date-picker v-model="bornDate" type="date" place="请选择"/>
        </el-col>
    </el-row>
    
  • 日期时间选择器

    <el-row>
        <el-col :span="6">
            注册时间:
        </el-col>
        <el-col :span="18">
            <el-date-picker v-model="loginTime" type="datetime" place="请选择"/>
        </el-col>
    </el-row>
    
  • 日期时间选择器+响应时间

    <el-row>
        <el-col :span="6">
            完成时间:
    </el-col>
    <el-col :span="18">
        <el-date-picker v-model="finishTime" value-format="YYYY-MM-DD HH:mm:ss" @change="finishTimeChange" type="datetime" place="请选择"/>
            </el-col>
    </el-row>
    
    <script>
        let finishTime = ref("")
        const finishTimeChange = (val:any)=>{
            console.log("finishTimeChanged:", val)
        }
    </script>
    
    说明:
    	1. 默认的时间,格式是通用的格式时间,不是我们常见的格式,需要设定value-format转换
    

1.3.11 表单

表单:el-form, 每个提交项:el-form-item, 提交项可以添加 label="用户名" 作为标签

用户登录例子:

<template>
    <div class="content">
        <div class="common-layout">
            <h3>用户注册</h3>
            <el-form label-width="80" style="width:100%;">
                <el-row>
                    <el-col :span="6">
                        账号名称:
                    </el-col>
                    <el-col :span="18">
                        <el-form-item >
                            <el-input v-model="user.account" placeholder="请填写账户名称"/>
                        </el-form-item>
                    </el-col>
                </el-row>
                <el-row>
                    <el-col :span="6">
                        性别:
                    </el-col>
                    <el-col :span="18">
                        <el-form-item>
                            <el-radio-group v-model="user.gender">
                                <el-radio v-for="item in genders" :key="item.index" :value="item.index">{{item.label}}</el-radio>
                            </el-radio-group>
                        </el-form-item>
                    </el-col>
                </el-row>
                <el-row>
                    <el-col :span="6">
                        城市:
                    </el-col>
                    <el-col :span="18">
                        <el-form-item>
                            <el-radio-group v-model="user.city">
                                <el-radio v-for="item in citys" :key="item.index" :value="item.index">{{item.label}}</el-radio>
                            </el-radio-group>
                        </el-form-item>
                    </el-col>
                </el-row>
                <el-row>
                <el-col :span="6">
                    出生日期:
                </el-col>
                <el-col :span="18">
                    <el-date-picker v-model="user.bornDate" type="date" place="请选择"/>
                </el-col>
            </el-row>
            <el-row>
                <el-col :span="6">
                    注册时间:
                </el-col>
                <el-col :span="18">
                    <el-date-picker v-model="user.loginTime" type="datetime" place="请选择"/>
                </el-col>
            </el-row>
            <el-row>
                <el-col :span="6">
                    完成时间:
                </el-col>
                <el-col :span="18">
                    <el-date-picker v-model="user.finishTime" value-format="YYYY-MM-DD HHmmss" @change="finishTimeChange" type="datetime" place="请选择"/>
                </el-col>
            </el-row>
            <el-row>
                <el-col :span="6">
                    学科个数:
                </el-col>
                <el-col :span="18">
                    <el-input-number v-model="user.subjectNum" :min="1" :max="10" @change="handleChange" />
                </el-col>
            </el-row>
            <el-row>
                <el-col :span="12" :offset="12">
                    <el-button-group>
                        <el-button type="primary" @click="submitUser">提交</el-button>
                        <el-button type="primary" @click="resetUser">重置</el-button>
                    </el-button-group>
                </el-col>
            </el-row>
            </el-form>
            <h3 style="text-align:center">  </h3>
        </div>
    </div>
</template>
<script lang="ts" setup>
    import {ref, reactive} from 'vue'
    
    const citys = ref([
        {index:1, label:"成都"},
        {index:2, label:"北京"},
        {index:3, label:"上海"},
    ])

    const genders = ref ([
        {index:1, label:"男"},
        {index:2, label:"女"},
    ])

    let user = reactive({
        account: "",
        gender: 1,
        city: 1,
        bornDate:"",
        loginTime:"",
        finishTime:"",
        subjectNum:2
    })

    
    const finishTimeChange = (val:any)=>{
        console.log("finishTimeChanged:", val)
    }

    let num = ref(1)
    const handleChange = (v:number)=>{
        num.value = v
    }

    const submitUser = ()=>{
        console.log(user)
    }
    const resetUser = ()=>{
        Object.assign(user, {
            account: "",
            gender: 1,
            city: 1,
            bornDate:"",
            loginTime:"",
            finishTime:"",
            subjectNum:1
        })
    }
</script>

1.3.12 对话框

对话框使用标签el-dialog, v-model绑定一个布尔值变量,代表是否显示,
    <el-dialog v-model="showDialog" width="800" title="用户登录" draggable @close="showDialog=false">
        <el-form label-width="100">
            <el-form-item label="用户名:">
                <el-input v-model="user.account" placeholder="请输入账号"></el-input>
            </el-form-item>
            <el-form-item label="密码:">
                <el-input v-model="user.password" placeholder="请输入密码"></el-input>
            </el-form-item>
            <el-row>
                <el-col :offset="10" :span="8">
                <el-button-group>
                    <el-button type="primary" @click="showDialog=false">提交</el-button>
                    <el-button type="primary" @click="showDialog=false">重置</el-button>
                </el-button-group>
            </el-col>
        </el-row>
        </el-form>
    </el-dialog>
<script lang="ts" setup>
    import {ref, reactive} from 'vue'
    let showDialog = ref(false)
    let user = reactive({
        account:"",
        password:""
    })
</script>

1.3.13 分页

  • 分页使用的标签是el-pagination
    • 属性:
      • page-size
      • total
      • layout
        • prev 显示上一页
        • pager // 页码
        • next // 显示下一页
        • jumper // 显示跳转
        • total // 显示总条数
<template>
    <el-row>
        <el-col :span="12">
            <h5> page-size:每页显示记录数 total: 总记录数</h5>
        </el-col>
        <el-col :span="12">
            <el-pagination layout="prev, pager, next" :page-size="10" :total="500"/>
        </el-col>
    </el-row>
    <el-row>
        <el-col :span="12">
            <h5> 显示背景</h5>
        </el-col>
        <el-col :span="12">
            <el-pagination layout="prev, pager, next" :page-size="10" :total="500" background/>
        </el-col>
    </el-row>
    <el-row>
        <el-col :span="12">
            <h5> 显示总数</h5>
        </el-col>
        <el-col :span="12">
            <el-pagination layout="prev, pager, next, total" :page-size="10" :total="500" background/>
        </el-col>
    </el-row>
    <el-row>
        <el-col :span="12">
            <h5> 跳转</h5>
        </el-col>
        <el-col :span="12">
            <el-pagination layout="prev, pager, next, total, jumper" :page-size="10" :total="500" background/>
        </el-col>
    </el-row>
    <el-row>
        <el-col :span="12">
            <h5> 事件绑定</h5>
        </el-col>
        <el-col :span="12">
            <el-pagination layout="prev, pager, next, total, jumper" @current-change="currentPage" :page-size="10" :total="500" background/>
        </el-col>
    </el-row>
</template>

1.3.12 表格

  • 普通表格

    <h3>学生表1</h3>
    <el-table :data="data.students" height="150" border style="width:800px">
        <el-table-column prop="id" label="编号" width="80"/>
        <el-table-column prop="name" label="编号"/>
        <el-table-column prop="age" label="年龄"/>
        <el-table-column prop="gender" label="性别"/>
    </el-table>
    
  • 可多选的表格

    <h3>学生表2</h3>
    <el-table :data="data.students" height="150" border style="width:800px">
        <el-table-column type="selection" width="80"/>
        <el-table-column prop="id" label="编号" width="80"/>
        <el-table-column prop="name" label="姓名"/>
        <el-table-column prop="age" label="年龄"/>
        <el-table-column prop="gender" label="性别"/>
    </el-table>
    
  • 动态表格+动态分页+操作多选和单选

    <template>
        <h3> 学生表3</h3>
        <el-table :data="data.students.slice(currentPage*pageSize, (currentPage+1)*pageSize)"
            @selection-change="selected" border style="width:100%">
            <el-table-column type="selection" width="80"/>
    
            <el-table-column v-for="(column, index) in tableColumns"
                :key="index"
                :prop="column.prop"
                :label="column.label">
            </el-table-column>
    
            <el-table-column label="操作" width="200">
                <template #default="scope">
                    <el-button size="small" type="primary" @click="edit(scope.$index, scope.row)">编辑</el-button>
                    <el-button size="small" type="danger">删除</el-button>
                </template>
            </el-table-column>
        </el-table>
        <el-pagination layout="prev, pager, next, jumper, total" 
            @current-change="handleCurrentChange"
            :page-size="pageSize"  
            :total="data.students.length"
            :current-page="currentPage + 1" />
        <el-button type="danger" @click="del">删除选中项</el-button>
    
    </template>
    
    <script lang="ts" setup>
        import {ref, reactive} from 'vue'
    	// 动态定义表格需要显示的字段
        const tableColumns = ref([
            {prop:"id", label:"编号"},
            {prop:"name", label:"姓名"},
            {prop:"age", label:"年龄"},
            {prop:"gender", label:"性别"},
            {prop:"city", label:"城市"},
        ])
    	
        // 显示的动态数据.
        const data = ref({
            students : [
                {id:1, name:"张三", age:24,  gender:1, city:'cd'},
                {id:2, name:"李四", age:25,  gender:2, city:'sh'},
                {id:3, name:"王五", age:27,  gender:1, city:'tj'},
                {id:4, name:"赵六", age:22,  gender:2, city:'bj'},
                {id:5, name:"李七", age:23,  gender:2, city:'bj'},
                {id:6, name:"张三2", age:24,  gender:1, city:'cd'},
                {id:7, name:"李四3", age:25,  gender:2, city:'sh'},
                {id:8, name:"王五4", age:27,  gender:1, city:'tj'},
                {id:9, name:"赵六5", age:22,  gender:2, city:'bj'},
                {id:10, name:"李七6", age:23,  gender:2, city:'bj'},
                {id:11, name:"张三7", age:24,  gender:1, city:'cd'},
                {id:12, name:"李四8", age:25,  gender:2, city:'sh'},
                {id:13, name:"王五9", age:27,  gender:1, city:'tj'},
                {id:14, name:"赵六10", age:22,  gender:2, city:'bj'},
                {id:15, name:"李七11", age:23,  gender:2, city:'bj'},
                {id:16, name:"张三12", age:24,  gender:1, city:'cd'},
                {id:17, name:"李四13", age:25,  gender:2, city:'sh'},
                {id:18, name:"王五14", age:27,  gender:1, city:'tj'},
                {id:19, name:"赵六15", age:22,  gender:2, city:'bj'},
                {id:20, name:"李七16", age:23,  gender:2, city:'bj'},        ]
        })
    
        // 当前显示的页
        let currentPage = ref(0)
        // 每一页显示的数据条数
        const pageSize = ref(5)
    
        // 当前页发生变化,需要动态刷新数据
        const handleCurrentChange = (val:number) =>{
            // Element 的分页是从 1 开始的,但数组索引是从 0 开始的,所以需要减 1 
            currentPage.value = val - 1
        }
        // 保存当前选中的列,以帮助点击删除是,进行删除.
        let selectedIds:Array<number> = []
        const selected = (data:any) =>{
            selectedIds = []
            data.forEach(val => {
                selectedIds.push(val.id)
            });
            console.log("selectedIds", selectedIds)
        }
    
        const edit = (index:number,row:number) =>{
            console.log("index", index, "row", row)
        }
    
        const del = () =>{
            console.log("del:", selectedIds)
        }
    </script>
    
  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值