vue3-vite-ts-pinia

news2024/12/28 21:55:29

Vue3 + vite + Ts + pinia + 实战 + 源码 +electron

仓库地址:https://gitee.com/szxio/vue3-vite-ts-pinia

视频地址:小满Vue3(课程导读)_哔哩哔哩_bilibili

课件地址:Vue3_小满zs的博客-CSDN博客

初始化Vue3项目

方式一

npm init vite@latest

image-20230903162122600

生成的目录结构

vite-demo
├── .vscode
│   └── extensions.json
├── public
│   └── vite.svg
├── src
│   ├── assets
│   │   └── vue.svg
│   ├── components
│   │   └── HelloWorld.vue
│   ├── App.vue
│   ├── main.ts
│   ├── style.css
│   └── vite-env.d.ts
├── README.md
├── index.html
├── package.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts

启动

npm run dev

image-20230903162616723

方式二

npm init vue@latest

image-20230903162819151

生成的目录结构

vue-demo
├── .vscode
│   └── extensions.json
├── public
│   └── favicon.ico
├── src
│   ├── assets
│   │   ├── base.css
│   │   ├── logo.svg
│   │   └── main.css
│   ├── components
│   │   ├── __tests__
│   │   ├── icons
│   │   ├── HelloWorld.vue
│   │   ├── TheWelcome.vue
│   │   └── WelcomeItem.vue
│   ├── router
│   │   └── index.ts
│   ├── stores
│   │   └── counter.ts
│   ├── views
│   │   ├── AboutView.vue
│   │   └── HomeView.vue
│   ├── App.vue
│   └── main.ts
├── .eslintrc.cjs
├── .prettierrc.json
├── README.md
├── env.d.ts
├── index.html
├── package.json
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
├── tsconfig.vitest.json
├── vite.config.ts
└── vitest.config.ts

用这种方式生成的项目会全一点

启动

npm run dev

image-20230903163023078

自动生成路由

添加 gen-router.js 文件

var fs = require('fs');
const readline = require('readline');
const os = require('os');

const vueDir = './src/views/';

fs.readdir(vueDir, function (err, files) {
    if (err) {
        console.log(err);
        return;
    }
    let routers = ``;

    // 对文件进行排序
    let sortFiles = files.sort((a,b)=>{
        return a.split("_")[0] - b.split("_")[0]
    });

    for (const filename of sortFiles) {
        if (filename.indexOf('.') < 0) {
            continue;
        }
        var [name, ext] = filename.split('.');
        if (ext != 'vue') {
            continue;
        }
        let routerName = null

        const contentFull = fs.readFileSync( `${vueDir}${filename}`, 'utf-8' );
        var match = /\<\!\-\-\s*(.*)\s*\-\-\>/g.exec(contentFull.split(os.EOL)[0]);
        if (match) {
            routerName = match[1];
        }


        routers += `  {path: '/${name === 'root' ? '' : encodeURIComponent(name)}',name:'${name}', component: ()=> import(/* webpackChunkName: "${name}" */ "@/views/${filename}") ${ routerName ? ',name: "' + routerName + '"' : ''} },\n`;
    }

const result = `
import { createRouter, createWebHistory } from 'vue-router'
import Layout from '@/layout/index.vue'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'index',
      component: Layout,
      redirect: '/index',
      children:[
        ${routers}
      ]
    },
    
  ]
})

export default router
`



    // console.log(result);
    fs.writeFile('./src/router/index.ts',result, 'utf-8',
        (err) => {
            if (err) throw err;
        });
});

修改 package.json 中的启动命令

"scripts": {
  "dev": "node gen-router.js &&  vite",
},

这样每次新建完一个文件后需要重启一下服务,然后会自动生成路由文件,配置菜单动态显示即可

Ref全家桶

ref

接受一个内部值并返回一个可变响应式的 ref 对象。ref 对象仅有一个 .value property,指向该内部值。

<template>
  <div>
    {{ product }}
  </div>
  <hr>

  <button @click="change">点击</button>
</template>

<script setup lang="ts">

import {ref} from "vue";

const product = ref({
  id:"001",
  name:"小米手机"
})

const change = () => {
  product.value.name = "华为手机"
  console.log(product)
}

</script>

调试小技巧

我们打印 ref 对象时需要点开两层才能看到信息,如下

image-20230903201400943

可以打开 启用自定义格式化程序

image-20230903201422321

image-20230903201441480

之后打印就会直接展示具体的信息

image-20230903201520363

isRef

判断一个对象是否是响应式对象

import { ref, isRef } from "vue";

const product = ref({
  id: "001",
  name: "小米手机"
})

const change = () => {
  product.value.name = "华为手机"
  // isRef判断一个对象是否是响应式对象
  console.log(isRef(product)) // true
}

shallowRef

创建一个跟踪自身 .value 变化的 ref,但不会使其值也变成响应式的

import { ref, isRef, shallowRef } from "vue";

const shaRef = shallowRef({
  price: 100
})

const change = () => {
  // product.value.name = "华为手机"
  // isRef判断一个对象是否是响应式对象
  console.log(isRef(product)) // true

  shaRef.value.price = 200

  console.log(shaRef.value);
}

上面的例子中页面不会发生变化

triggerRef

强制更新页面

import { ref, isRef, shallowRef, triggerRef } from "vue";

const product = ref({
  id: "001",
  name: "小米手机"
})

const shaRef = shallowRef({
  price: 100
})

const change = () => {
  // product.value.name = "华为手机"
  // isRef判断一个对象是否是响应式对象
  console.log(isRef(product)) // true

  shaRef.value.price = 200

  console.log(shaRef.value);

  triggerRef(shaRef)
}

需要传入一个要更新的对象

customRef

自定义一个ref响应式数据

import { customRef } from "vue";

function myRef<T>(value: T) {
  return customRef((track, trigger) => {
    return {
      get() {
        track()
        return value
      },
      set(newVal) {
        value = newVal
        trigger()
      },
    }
  })
}

const song1 = myRef("123")

const change = () => {
  song1.value = "456"

}

Reactive全家桶

Reactive

用来绑定复杂的数据类型 例如 对象 数组

源码中限定只能传入类型是Object的数据

image-20230903211342898

<template>
    <div>
        {{ form }}
    </div>
    <button @click="change">改变</button>

    <hr>
    <ul>
        <li v-for="item in list.value">{{ item }}</li>
    </ul>
    <button @click="getList">获取</button>
</template>

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

let form = reactive({
    name: "张三",
    age: 18
})
function change() {
    form.age++
}

let list = reactive({
    value: ["lisi", "wangwu"]
})
function getList() {
    setTimeout(() => {
        let res = ["Anly", "Jack"]
        // 直接给reactive赋值会破坏原有的响应式
        list.value = res
        console.log(list);
    }, 1000);
}
</script>

Readonly

将一个对象设置为只读

import { reactive, readonly } from 'vue';

let form = reactive({
    name: "张三",
    age: 18
})
let readOnlyForm = readonly(form)
function change() {
    readOnlyForm.age++
}

image-20230903212107449

shallowReactive

浅层的响应式

import { shallowReactive } from 'vue';

let shaReactive = shallowReactive({
    a: {
        b: 123
    }
})
function chageSha() {
    shaReactive.a.b = 456 // 页面不会发生改变
    console.log(shaReactive); // 打印的数据发生改变
}

to系列全家桶

toRef

将对象中的某个属性变成响应式的

如果原始数据是非响应式的,则经过 toRef 之后也不会更新视图,但是数据会发生变化

<template>
    <div>{{ student }}</div>
    <div>likeRef:{{ likeRef }}</div>
    <button @click="change">修改</button>
</template>
<script setup lang='ts'>
import { toRef } from "vue"

const student = {
    name: "Jack",
    age: 18,
    like: "画画"
}

let likeRef = toRef(student, "like")

function change() {

    // 如果源数据是非响应式的,则经过toRef后也不会触发页面更新
    likeRef.value = "足球"
    console.log(student);
    console.log(likeRef);

}

</script>

如果源数据就是响应式的,则会触发页面更新

<template>
    <div>{{ student }}</div>
    <div>likeRef:{{ likeRef }}</div>
    <button @click="change">修改</button>
</template>
<script setup lang='ts'>
import { toRef, reactive } from "vue"

const student = reactive({
    name: "Jack",
    age: 18,
    like: "画画"
})

let likeRef = toRef(student, "like")

function change() {

    // 如果源数据是非响应式的,则经过toRef后也不会触发页面更新
    likeRef.value = "足球"
    console.log(student);
    console.log(likeRef);

}

</script>

toRefs

将对象的所有数据都变成响应式数据

import { toRef, toRefs, toRaw, ref, reactive } from "vue"

const student = reactive({
    name: "Jack",
    age: 18,
    like: "画画",
    code: [1, 2]
})

// 自实现toRefs
function myToRefs<T extends Object>(object: T) {
    let map: any = {}
    for (const key in object) {
        map[key] = toRef(object, key)
    }
    return map
}
function refs() {
    console.log(myToRefs(student)); // 打印结果如下图
}

// 使用场景:对象解构
let { name, age, code } = toRefs(student)
function fun1() {
    name.value = "Tim"
    age.value = 16
    code.value.push(3)
}

myToRefs 打印结果

image-20230903223545771

toRaw

返回对象的原始信息

function fun2() {
    console.log(toRaw(student));
}

打印

image-20230903223501841

Vue3响应式源码实现

初始化项目结构

vue-proxy
├── effect.js
├── effect.ts
├── index.html
├── index.js
├── package.json
├── reactive.js
├── reactive.ts
└── webpack.config.js

reactive.ts

import { track, trigger } from "./effect"

// 判断是否是对象
const isObject = (target) => target !== null && typeof target === "object"

// 泛型约束只能传入Object类型
export const reactive = <T extends object>(target: T) => {

    return new Proxy(target, {
        get(target, key, receiver) {
            console.log(target);
            console.log(key);
            console.log(receiver);

            let res = Reflect.get(target, key, receiver)

            track(target, key)

            if (isObject(res)) {
                return reactive(res)
            }

            return res
        },
        set(target, key, value, receiver) {
            let res = Reflect.set(target, key, value, receiver)
            console.log(target, key, value);

            trigger(target, key)
            return res
        }
    })

}

effect.ts

// 更新视图的方法
let activeEffect;
export const effect = (fn: Function) => {
    const _effect = function () {
        activeEffect = _effect;
        fn()
    }
    _effect()
}

// 收集依赖
const targetMap = new WeakMap()
export const track = (target, key) => {
    let depsMap = targetMap.get(target)
    if (!depsMap) {
        depsMap = new Map()
        targetMap.set(target, depsMap)
    }
    let deps = depsMap.get(key)
    if (!deps) {
        deps = new Set()
        depsMap.set(key, deps)
    }
    deps.add(activeEffect)
}

// 触发更新
export const trigger = (target, key) => {
    const depsMap = targetMap.get(target)
    const deps = depsMap.get(key)
    deps.forEach(effect => effect())
}

测试

执行 tsc 转成 js 代码,没有 tsc 的全局安装 typescript

npm install typescript -g

新建 index.js,分别引入 effect.jsreactive.js

import { effect } from "./effect.js";
import { reactive } from "./reactive.js";

let data = reactive({
    name: "lisit",
    age: 18,
    foor: {
        bar: "汽车"
    }
})

effect(() => {
    document.getElementById("app").innerText = `数据绑定:${data.name} -- ${data.age} -- ${data.foor.bar}`
})

document.getElementById("btn").addEventListener("click", () => {
    data.age++
})

新建index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <div id="app"></div>
    <button id="btn">按钮</button>
</body>

然后再根目录执行

npm init -y

安装依赖

npm install webpack webpack-cli webpack-dev-server html-webpack-plugin -D

然后新建 webpack.config.js

const path = require("path")
const HtmlWebpakcPlugin = require("html-webpack-plugin")

module.exports = {
    entry: "./index.js",
    output: {
        path: path.resolve(__dirname, "dist")
    },

    plugins: [
        new HtmlWebpakcPlugin({
            template: path.resolve(__dirname, "./index.html")
        })
    ],
    mode: "development",
    // 开发服务器
    devServer: {
        host: "localhost", // 启动服务器域名
        port: "3000", // 启动服务器端口号
        open: true, // 是否自动打开浏览器
    },
}

执行命令启动项目

npx webpack serve

image-20230904232457817

image-20230904232522425

computed的简单使用

<template>
  <div>
    <table border width="600" cellspacing="0" cellpadding="0">
      <thead>
        <th>名称</th>
        <th>价格</th>
        <th>数量</th>
        <th>总价</th>
        <th>操作</th>
      </thead>

      <tbody>
        <tr v-for="(item,index) in choosList" style="text-align: center">
          <td>{{ item.name }}</td>
          <td>{{ item.price }}</td>
          <td>{{ item.count }}</td>
          <td>
            <el-button type="primary" @click="item.count--">-</el-button>
            {{ item.price * item.count }}
            <el-button type="primary" @click="item.count++">+</el-button>
          </td>
          <td><el-button type="danger" @click="remove">删除</el-button></td>
        </tr>
      </tbody>
      <tfoot align="right">
      <tr>
        <td colspan="5">总价:{{total}}</td>
      </tr>
      </tfoot>
    </table>
  </div>
</template>

<script setup>
import {reactive,computed} from "vue";

let choosList = reactive([
  {
    name:"裤子",
    price:100,
    count:1,
  },
  {
    name:"衣服",
    price:200,
    count:1,
  },
  {
    name:"鞋子",
    price:300,
    count:1,
  },
  {
    name:"帽子",
    price:400,
    count:1,
  }
])
let total = computed(()=>{
  let total = 0;
  choosList.forEach(item=>{
    total += item.price * item.count;
  })
  return total;
})

function remove(index){
  choosList.splice(index,1);
}
</script>

<style scoped>

</style>

image-20230910152223957

computed源码实现

effect.ts

// 更新视图方法
let activeEffect
export const effect = (fn:Function,options) => {
    console.log("effect触发")
    const _effect = function () {
        activeEffect = _effect
        return fn()
    }
    _effect.options = options
    _effect()
    return _effect
}

// 依赖收集
const targetMap = new WeakMap()
export const track = (target, key) => {
    let depsMap = targetMap.get(key)
    if (!depsMap) {
        depsMap = new Map()
        targetMap.set(target, depsMap)
    }
    let deps = depsMap.get(key)
    if (!deps) {
        deps = new Set()
        depsMap.set(key, deps)
    }
    deps.add(activeEffect)
}


// 触发更新
export const trigger = (target, key) => {
    const depsMap = targetMap.get(target)
    const deps = depsMap.get(key)
    deps.forEach(effect => {
        if (effect.options.scheduler){
            effect.options.scheduler()
        }else{
            effect()
        }
    })
}

reactive.ts

import {track, trigger} from "./effect"
// 判断是否是对象类型
const isObject = (target) => typeof target === 'object' && target !== null

export const reactive = (target) => {
    return new Proxy(target, {
        get(target, key, receiver) {
            console.log("reactive.get-",key)
            const res = Reflect.get(target, key, receiver)
            // 收集依赖
            track(target, key)
            // 递归
            return isObject(res) ? reactive(res) : res
        },
        set(target, key, value, receiver) {
            console.log("reactive.set-",key)
            const res = Reflect.set(target, key, value, receiver)
            // 触发依赖
            trigger(target, key)
            return res
        }
    })
}

computed.ts

import {effect} from  "./effect"

export const myComputed = (getter:Function)=>{
    let _value = effect(getter,{
        scheduler:()=>{
            _dirty = true
        }
    })
    // 判断是否需要重新计算结果
    let _dirty = true
    // 缓存结果
    let catchValue

    class ComputedRefImpl{
        get value(){
            if(_dirty){
                console.log("依赖发生变化时执行")
                catchValue = _value()
                _dirty = false
            }
            return catchValue
        }
    }
    return new ComputedRefImpl()
}

watch监听器

监听单属性值

let name = ref("李四")

watch(name,(newValue,oldValue)=>{
  console.log(newValue,oldValue)
})

同时监听多个属性

let name = ref("李四")
let age = ref(20)

watch([name,age],(newValue,oldValue)=>{
  console.log(newValue,oldValue)
})

image-20230910154510725

深度监听

let obj = ref({
  foo:{
    bar:{
      name:"张三"
    }
  }
})

watch(obj,(newValue,oldValue)=>{
  console.log(obj.value.foo.bar.name)
},{
  deep:true, //  深度监听
  immediate:true, // 立即执行
})

监听对象中的某一个属性

let obj = ref({
  foo:{
    bar:{
      name:"张三",
      age:18
    }
  }
})

// 监听某个属性是要传入一个函数来返回要监听的属性值
watch(()=>obj.value.foo.bar.age,(newValue,oldValue)=>{
  console.log(obj.value.foo.bar.age)
},{
  immediate:true
})

watchEffect

简介

watchEffect不需要传入任何参数,它是一个函数,当依赖变化时,这个函数就会执行,它内部会根据响应式数据的依赖关系,自动执行监听函数

使用

<template>
  <el-input id="msg1" v-model="msg1" placeholder="placeholder"></el-input>
  <el-input v-model="msg2" placeholder="placeholder"></el-input>
  <el-button type="primary" @click="stopWatch">停止监听</el-button>
</template>

<script setup>
import {ref, watchEffect,nextTick} from 'vue'

let msg1 = ref("msg1")
let msg2 = ref("msg2")

// watchEffect不需要传入任何参数,它是一个函数,当依赖变化时,这个函数就会执行
// 它内部会根据响应式数据的依赖关系,自动执行监听函数
const stop = watchEffect(()=>{
  console.log(msg1.value)
  console.log(msg2.value)
})

function stopWatch(){
  // 停止监听
  stop()
}

</script>

BEM架构和Layout布局

Layout目录结构

layout
├── Content
│   └── index.vue
├── Header
│   └── index.vue
├── Menu
│   └── index.vue
├── css
│   └── bem.scss
└── index.vue

新建 bem.scss

$namespace: "zx" !default;
$block-sel:"-" !default;
$element-sel:"__" !default;
$modifier-sel:"--" !default;

@mixin bfc{
  height:100%;
  overflow: hidden;
}

@mixin b($block){
  // 拼接的结果为:zx-xxx
  $B:$namespace + $block-sel + $block;
  .#{$B}{
    @content;
  }
}

@mixin e($element){
  // 拼接的结果为:zx-xxx__xxx
  $selector:&;
  @at-root {
    $E:$selector + $element-sel + $element;
    #{$E}{
      @content;
    }
  }
}

@mixin m($modifier){
  // 拼接的结果为:zx-xxx--xxx
  $selector:&;
  @at-root {
    $M:$selector + $modifier-sel + $modifier;
    #{$M}{
      @content;
    }
  }
}

配置全局生效

import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    vueJsx(),
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [ElementPlusResolver()],
    }),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  },
  css: {
    preprocessorOptions: {
      // 配置全局CSS
      scss: {
        additionalData: "@import './src/layout_v2/css/bem.scss';"
      }
    }
  }
})

index.vue

<template>
  <div class="zx-box">
    <div class="zx-box__menu">
      <Menu/>
    </div>
    <div class="zx-box__main">
      <Header/>
      <Content/>
    </div>
  </div>
</template>

<script setup>

import Menu from "@/layout_v2/Menu/index.vue";
import Header from "@/layout_v2/Header/index.vue";
import Content from "@/layout_v2/Content/index.vue";
</script>

<style scoped lang="scss">
@include b('box'){
  height: 100%;
  display: flex;

  @include e("menu"){
    width: 250px;
    height: 100%;
    border-right: 1px solid #ebebeb;
  }

  @include e("main"){
    flex: 1;
    display: flex;
    flex-direction: column;
  }
}

</style>

Menu/index

<template>
  <div>
    Menu
  </div>
</template>

<script setup>

</script>

Header/index.vue

<template>
<div class="zx-header">
  Header
</div>
</template>

<script setup>
</script>

<style scoped lang="scss">
@include b('header'){
  width: 100%;
  height: 60px;
  line-height: 60px;
  border-bottom: 1px solid #ccc;
}
</style>

Content/index.vue

<template>
  <div class="zx-content">
    <div v-for="item in 50" class="zx-content__item">
      {{item}}
    </div>
  </div>
</template>

<script setup>

</script>

<style scoped lang="scss">
@include b(content){
  height: 100%;
  overflow: auto;
  @include e(item){
    height: 60px;
    line-height: 60px;
    text-align: center;
    border-radius: 5px;
    border: 1px solid pink;
    margin: 10px;
  }
}
</style>

布局效果

image-20230913205111297

父子组件传值

简单使用

定义父组件

<template>
  <div class="parent-box">
    父组件
    <div>子组件传过来的值:{{count}}</div>

    <button @click="getSubInfo">获取子组件的所有属性和方法</button>

    <SubComponent ref="subCom" :value="title" @changeCount="changeCount"/>
  </div>
</template>

<script setup lang="ts">
import SubComponent from "@/components/SubComponent.vue";
import {ref} from "vue";

let title = "给儿子传值";
let count = ref<number>()

// 定义组件类型
let subCom = ref<InstanceType<typeof SubComponent>>()

// 子组件触发的父组件方法
const changeCount = (newVal) => {
  count.value = newVal;
}

const getSubInfo = () => {
  // 调用子组件的实例方法
  subCom.value.open()

  // 获取子组件的属性
  console.log(subCom.value.order)
}
</script>

<style scoped>
.parent-box{
  width: 300px;
  height: 300px;
  border: 1px solid #ccc;
  padding: 30px;
}
</style>

子组件

<template>
  <div class="children-box">
   父组件传递的值: {{ value }}
    <button @click="changeParentCount">改变父组件的值</button>
  </div>
</template>

<script setup lang="ts">
import {defineProps, defineEmits, defineExpose, ref} from 'vue';

// 父组件传过来的值,带个问号表示可选
const props = defineProps<{
  value: string
}>()

// JS中获取父组件传过来的值
console.log(props.value)

// 点击按钮,触发父组件的自定义事件
const changeParentCount = () => {
  emit("changeCount", 10)
}

// 触发父组件的自定义事件
const emit = defineEmits(["changeCount"])

// 定义对外暴露的属性
let order = ref(10)
const open = () => {
  console.log("open")
}
// 使用defineExpose暴露出去
defineExpose({
  order,
  open
})
</script>

<style scoped>
.children-box{
  border: 1px solid #ccc;
  padding: 30px;
}
</style>

image-20230913222206507

实现瀑布流布局

父组件

<template>
  <WaterfallFlow :list="list"/>
</template>

<script setup lang="ts">

import WaterfallFlow from "@/components/WaterfallFlow.vue";
import {reactive} from "vue";
type listType = {
  height:number,
  color:string
}
// 随机生成100个高度和颜色的对象
let list = reactive<listType[]>([
  ...Array.from({length:100},()=>({
    height:Math.floor(Math.random()*250)+50,
    color:`rgb(${Math.floor(Math.random()*255)},${Math.floor(Math.random()*255)},${Math.floor(Math.random()*255)})`
  }))
 ])
</script>

子组件

<template>
  <div class="wraps">
    <div v-for="item in list" class="item" :style="{
      left: item.left + 'px',
      top: item.top + 'px',
      height: item.height + 'px',
      backgroundColor: item.color,
    }"></div>
  </div>
</template>

<script setup lang="ts">
import {defineProps, onMounted} from "vue"

const props = defineProps<{
  list: any[]
}>()

const initLayout = () => {
  // 上下左右间隙距离
  let margin = 10
  // 每个元素的宽度
  let elWidth = 120 + margin
  // 每行展示的列数
  let colNumber = Math.floor(document.querySelector(".app-content").clientWidth / elWidth)
  // 存放元素高度的list
  let heightList = []

  // 遍历所有元素
  for (let i = 0; i < props.list.length; i++) {
    let el = props.list[i]
    // i小于colNumber表示第一行元素
    if(i < colNumber){
      el.top = 0
      el.left = elWidth * i
      heightList.push(el.height)
    }else{
      // 找出最小的高度
      let minHeight = Math.min(...heightList)
      // 找出最小高度的索引
      let minHeightIndex = heightList.indexOf(minHeight)
      // 设置元素的位置
      el.left = elWidth * minHeightIndex
      el.top = minHeight + margin
      // 更新高度集合
      heightList[minHeightIndex] = minHeight + el.height + margin
    }
  }
}

// 监听app-content元素的宽度变化
window.onresize = () => {
  initLayout()
}

onMounted(() => {
  initLayout()
})
</script>

<style scoped lang="scss">
.wraps{
  height: 100%;
  position: relative;
  .item{
    position: absolute;
    width: 120px;
  }
}
</style>

效果展示

image-20230913224830334

组件递归

实现一个如下的东西

image-20230915214800791

父组件

<template>
  <TreeVue :treeData='treeData'/>
</template>

<script setup>

import {reactive} from  "vue"
import TreeVue from "@/components/TreeVue.vue";

let treeData = reactive([
  {
    label:"1",
    checked:false,
    children:[
      {
        label:"1-1",
        checked:false,
      },
      {
        label: "1-2",
        checked:true,
      }
    ]
  },
  {
    label:"2",
    checked:false,
    children: [
      {
        label: "2-1",
        checked:false,
        children:[
          {
            label: "2-1-1",
            checked:false,
            children:[
              {
                label: "2-1-1-1",
                checked:false,
              }
            ]
          }
        ]
      }
    ]
  },
  {
    label:"3",
    checked:false,
  }
])

</script>

TreeVue.vue

<template>
  <div v-for="item in treeData" style="margin-left: 15px" @click.stop="getCurrNode(item,$event)">
    <input type="checkbox" v-model="item.checked"/>
    <span>{{item.label}}</span>
    <TreeVue v-if="item.children" :tree-data="item.children"/>
  </div>
</template>

<script setup>
import {defineProps} from "vue"
defineProps(["treeData"])

const getCurrNode = (currNode,event) => {
  console.log(currNode)
  console.log(event)
}
</script>

控制台打印的东西

image-20230915214931139

动态组件

image-20230915222313132

<template>
  <div style="display: flex;gap: 15px">
    <div v-for="(item,index) in tabData" :key="index"
         class="tab-item"
         :class="{
         active:active === index
       }"
         @click="switchCom(item,index)"
    >
      <div>
        {{item.tab}}
      </div>
    </div>
  </div>
  <component :is="currCom"></component>
</template>

<script setup>
import {reactive, ref, shallowRef,markRaw} from "vue"
import ComA from "@/components/13/ComA.vue"
import ComB from "@/components/13/ComB.vue"
import ComC from "@/components/13/ComC.vue"

// 使用shallowRef避免深层相应
let currCom = shallowRef(ComA)
let active = ref(0)

let tabData = reactive([
  {
    tab:"组件A",
    // 使用markRaw使组件不会被vue进行响应式处理,提高性能
    com:markRaw(ComA)
  },
  {
    tab:"组件B",
    com:markRaw(ComB)
  },
  {
    tab:"组件C",
    com:markRaw(ComC)
  }
])

const switchCom = (item,index) => {
  currCom.value = item.com
  active.value = index
}
</script>

<style scoped>
.tab-item{
  padding: 5px 15px;
  border: 1px solid black;
}
.active{
  background-color: deepskyblue;
}
</style>

插槽

定义子组件

<template>
    <div class="box">
        <div class="header">
          <slot name="header"></slot>
        </div>
        <div class="main">
          <!--默认插槽-->
          <slot :link="link" :age="age"></slot>
        </div>
        <div class="footer">
          <slot name="footer"></slot>
        </div>
    </div>
</template>
<script setup lang='ts'>
import {ref} from "vue";

const link = ref("Tome")
const age = ref(18)
</script>

<style scoped lang="scss">
// 父元素高度100%
.box{
  height: 100%;
  display: flex;
  flex-direction: column;
}
.header {
  height: 100px;
  background: pink;
  width: 100%;
}
.main{
  flex: 1;
  background-color: #c6e2ff;
}
.footer{
  height: 100px;
  background: blueviolet;
  width: 100%;
}
</style>

定义父组件

<template>
  <Dialog>
    <template #header>
      具名插槽-header
    </template>

    <template #default="{link,age}">
      这是默认插槽
      {{link}} -- {{age}}
    </template>

    <template #footer>
      具名插槽-footer
    </template>
  </Dialog>
</template>

<script setup lang='ts'>
import Dialog from "@/components/14/Dialog.vue"
</script>

效果

image-20230917182606728

异步组件

添加骨架屏组件

Skeleton.vue

<template>
  <el-skeleton style="--el-skeleton-circle-size: 100px">
    <template #template>
      <el-skeleton-item variant="circle" />
    </template>
  </el-skeleton>
  <br />
  <el-skeleton />
</template>

效果是这个样子

image-20230917214033824

添加新闻组件

添加新闻数据,在 public 文件夹中添加 newinfo.json

[
  {
    "title": "秋粮陆续成熟 多措并举保粮食丰收",
    "description": "眼下,从南到北,各地秋粮陆续成熟。人们全力以赴抓好秋粮生产,多措并举保粮食丰收。\n\n金秋时节,安徽水稻主产区无为市85万亩水稻进入收割期,当地组织机械作业服务队,帮助农民机耕机收,颗粒归仓。今年,安徽计划投入各类农机具240万台套,力争玉米、大豆、中晚稻机收水平达八成以上。",
    "url": "https://baijiahao.baidu.com/s?id=1777244368223895628",
    "image": "https://szx-bucket1.oss-cn-hangzhou.aliyuncs.com/picgo/img.png"
  }
]

引入 axios,请求这个文件

src/api/index.js

import axios from 'axios'

export function getNewDataFun(){
    return axios("../public/newinfo.json")
}

编写组件 NewCar.vue

<template>
  <div v-for="item in newData" class="new-box">
    <div class="image">
      <img :src="item.image" alt=""/>
    </div>
    <div class="content">
      <div class="title">{{item.title}}</div>
      <div class="desc">{{item.description}}</div>
    </div>
  </div>
</template>

<script setup lang="ts">
import {onMounted, reactive, ref} from "vue";
import {getNewDataFun} from "@/api/index";

type dataType = {
  title:string,
  description:string,
  url:string,
  image:string,
}

const newData = ref<dataType[]>([])

await getNewDataFun().then(res=>{
  setTimeout(()=>{
    newData.value = res.data
  },2000)
})
</script>

<style scoped lang="scss">
.new-box{
  display: flex;
  gap: 15px;
  .image{
    width: 200px;
    border-radius: 5px;
    overflow: hidden;
    img{
      width: 100%;
    }
  }
  .content{
    width: 80%;
    display: flex;
    flex-direction: column;
    gap: 10px;
    .title{
      font-weight: 700;
      font-size: 16px;
    }
  }
}
</style>

效果展示

image-20230917214242665

使用异步组件

Suspense 是vue内置的一个组件,有两个插槽

  • default:默认插槽,展示等待结果返回后的组件
  • fallback:等待过程中展示的组件
<template>
  <div style="padding: 20px">
    <Suspense>
      <template #default>
        <NewCar/>
      </template>

      <template #fallback>
        <Skeleton/>
      </template>
    </Suspense>
  </div>
</template>

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

import Skeleton from "@/components/15/Skeleton.vue"

const NewCar = defineAsyncComponent(()=>import("@/components/15/NewCar.vue"))
</script>

异步组件必须使用 defineAsyncComponent 函数来导入,接收一个回调函数

TelePore传送组件

自定义一个弹框组件

<template>
  <div class="zx-dialog">
    <slot></slot>
    <div class="zx-dialog__footer">
      <slot name="footer"></slot>
    </div>
  </div>
</template>

<style scoped lang="scss">
@include b("dialog"){
  width: 200px;
  height: 200px;
  position: absolute;
  left: 50%;
  top: 50%;
  margin-left: -100px;
  margin-top: -100px;
  border: 1px solid #ccc;
  background-color: #c6e2ff;
  @include e("footer"){
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    height: 50px;
    line-height: 50px;
    text-align: right;
  }
}
</style>

使用TelePore

父组件使用这个组件

<template>
  <div class="box">
    <el-button type="primary" @click="switchDialog = !switchDialog">打开Dialog</el-button>

    <!--使用teleport将组件渲染到body标签下面,避免受到父组件的position: absolute;定位影响-->
    <teleport to="body" v-if="switchDialog">
      <MyDialog>
        <template #default>
          弹框内容
        </template>
        <template #footer>
          <el-button @click="switchDialog = !switchDialog">关闭</el-button>
        </template>
      </MyDialog>
    </teleport>
  </div>
</template>

<script setup>
import {ref} from "vue"
import MyDialog from "@/components/MyDialog.vue";

let switchDialog = ref(false);

</script>

<style scoped>
.box{
  width: 100%;
  height: 50%;
  background-color: gold;
  position: absolute;

}
</style>

效果

image-20230918075355087

KeepAlive

可以缓存组件内容

默认使用

切换组件显示后,组件内容不会丢失

<template>
  <div>
    <div>
      <el-button type="primary" @click="switchFlag">切换组件</el-button>
    </div>

    <keep-alive>
      <AliveA v-if="flag"/>
      <AliveB v-else/>
    </keep-alive>
  </div>
</template>

<script setup>
import {ref} from "vue";
import AliveA from "@/components/AliveA.vue";
import AliveB from "@/components/AliveB.vue";

let flag = ref(true)

const switchFlag = () => {
  flag.value = !flag.value
}
</script>

includes

只缓存AliveA组件

<keep-alive :include="['AliveA']">
  <AliveA v-if="flag"/>
  <AliveB v-else/>
</keep-alive>

exclude

不缓存AliveA组件

<keep-alive :exclude="['AliveA']">
  <AliveA v-if="flag"/>
  <AliveB v-else/>
</keep-alive>

max

最多缓存的组件个数

<keep-alive :max="10">
  <AliveA v-if="flag"/>
  <AliveB v-else/>
</keep-alive>

keep-alive的钩子函数

<script lang="ts" setup>
import { ref,onMounted,onActivated,onDeactivated,onUnmounted, } from 'vue'

// 组件显示时只会触发一次
onMounted(()=>{
  console.log('mounted')
})

// 组件显示时触发
onActivated(()=>{
  console.log('activated')
})
// 组件隐藏时触发
onDeactivated(()=>{
  console.log('deactivated')
})
// 被keepalive包裹时,组件销毁不会触发unmounted
onUnmounted(()=>{
  console.log('unmounted')
})

transition

基本用法

在进入/离开的过渡中,会有 6 个 class 切换。

v-enter-from:定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。

v-enter-active:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。

v-enter-to:定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时 v-enter-from 被移除),在过渡/动画完成之后移除。

v-leave-from:定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。

v-leave-active:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。

v-leave-to:离开过渡的结束状态。在离开过渡被触发之后下一帧生效 (与此同时 v-leave-from 被移除),在过渡/动画完成之后移除。

<template>
<div>
  <el-button type="primary" @click="flag = !flag">切换</el-button>

  <transition name="fade">
    <div class="box" v-if="flag"></div>
  </transition>
</div>
</template>

<script setup>
import {ref} from "vue";

let flag = ref(true)
</script>

<style scoped lang="scss">
.box{
  width: 200px;
  height: 200px;
  background-color: red;
}

//开始过度
  .fade-enter-from{
    background:red;
    width:0px;
    height:0px;
    transform:rotate(360deg)
  }
//开始过度了
  .fade-enter-active{
    transition: all 1s ease;
  }
//过度完成
  .fade-enter-to{
    background:yellow;
    width:200px;
    height:200px;
  }
//离开的过度
  .fade-leave-from{
    width:200px;
    height:200px;
    transform:rotate(360deg)
  }
//离开中过度
  .fade-leave-active{
    transition: all 1s linear;
  }
//离开完成
  .fade-leave-to{
    width:0px;
    height:0px;
  }
</style>

结合animate

安装

npm install animate.css -D

官网中有很多动画示例 Animate.css | A cross-browser library of CSS animations.

<template>
  <div class="root-box">
    <div class="app-menu">
      <Menu />
    </div>
    <div class="app-content">
      <!-- 路由出口 -->
      <!-- 路由匹配到的组件将渲染在这里 -->
      <router-view v-slot="{ Component,route  }">
        <transition enter-active-class="animate__animated animate__fadeInUp">
          <!-- 这里加一个div是防止页面没有根组件时,动画失效 -->
          <div :key="route.name" style="height: 100%">
            <component :is="Component" />
          </div>
        </transition>
      </router-view>

    </div>
  </div>
</template>
<script setup>
import Menu from './Menu.vue'
import 'animate.css';
// 设置所有动画的时间在0.3秒内完成
document.documentElement.style.setProperty('--animate-duration', '0.3s');
</script>

<style scoped lang="less">
.root-box {
  display: flex;
  width: 100%;
  height: 100vh;
  .app-menu {
    width: 200px;
    height: 100vh;
    background-color: #a18cd1;
    text-overflow: ellipsis;
    white-space: nowrap;
    overflow: auto;
  }
  .app-content {
    flex: 1;
    height: 100vh;
    background-color: white;
    overflow: auto;
  }
}
</style>

transtion生命周期

<transition @before-enter="beforeEnter" @enter="enter" @leave="leave">
  <div v-if="gsapFlag" class="gsap-box"></div>
</transition>
 @before-enter="beforeEnter" //对应enter-from
 @enter="enter"//对应enter-active
 @after-enter="afterEnter"//对应enter-to
 @enter-cancelled="enterCancelled"//显示过度打断
 @before-leave="beforeLeave"//对应leave-from
 @leave="leave"//对应enter-active
 @after-leave="afterLeave"//对应leave-to
 @leave-cancelled="leaveCancelled"//离开过度打断

结合gsap

安装,官网:https://greensock.com/

npm install gsap

使用

html

<el-button type="primary" @click="gsapFlag = !gsapFlag">切换</el-button>
<transition @before-enter="beforeEnter" @enter="enter" @leave="leave">
  <div v-if="gsapFlag" class="gsap-box"></div>
</transition>

js

<script setup>
import gsap from "gsap";
import {ref} from "vue";

let gsapFlag = ref(true)

const beforeEnter = (el) => {
  console.log("显示之前")
  gsap.set(el,{
    width:0,
    height:0,
    background:"green"
  })
}
const enter = (el,done) => {
  gsap.to(el,{
    width:"200px",
    height:"200px",
    background:"red",
    rotate:"360dge",
    duration:1, // 动画时长,单位是秒
    onComplete:done, // 动画完成后的回调函数
  })
}
const leave = (el,done) => {
  gsap.to(el,{
    width:0,
    height:0,
    background:"green",
    rotate:"-360dge",
    duration:1, // 动画时长,单位是秒
    onComplete:done
  })
}

效果

appear属性

在 transtion 组件中添加 appear 可以在进入页面时就触发对应的样式代码

  • appear-class:初始样式
  • appear-to-class:结束样式
  • appear-active-class:动画曲线
<transition appear appear-class="" appear-to-class="" appear-active-class="animate__animated animate__rubberBand" name="fade">
  <div class="box" v-if="flag"></div>
</transition>

结合animate__animated实现一个进入页面就执行的一个动画效果

transition-group

在遍历数组的时候可以给每一个元素添加过度动画,生命周期和transition一致,我们结合animate来实现一个列表的动画效果

  <div>
    <el-button type="primary" @click="add">add</el-button>
    <el-button type="danger" @click="pop">pop</el-button>
  </div>
  <div class="warp">
    <transition-group
        enter-active-class="animate__animated animate__bounceInLeft"
        leave-active-class="animate__animated animate__fadeOutRight"
    >
      <div v-for="item in groupList" :key="item" class="item">
        {{item}}
      </div>
    </transition-group>
  </div>
import {ref,reactive} from "vue";
import "animate.css"

const groupList = reactive([1,2,3,4,5])

const add = () => {
  groupList.push(groupList.length + 1)
}
const pop = () => {
  groupList.pop()
}

动画效果

实现一个炫酷的动画效果

安装lodash库 Lodash 简介 | Lodash中文文档 | Lodash中文网 (lodashjs.com)

npm i --save lodash

实现代码

<div style="margin-top: 20px">平面动画过度效果</div>
<el-button type="primary" @click="shuffle">动画</el-button>
<div class="num-wrap">
  <transition-group move-class="move-class">
    <div v-for="item in numList" :key="item.id" class="num-item">
      {{item.value}}
    </div>
  </transition-group>
</div>
import {ref,reactive} from "vue";
import _ from "lodash"

let numList = ref(Array.apply(null, {length: 81}).map((_,index)=>{
  return {
    id:index,
    value:(index % 9) + 1
  }
}))

const shuffle = () => {
  // shuffle 用来创建一个被打乱值的集合
  numList.value = _.shuffle(numList.value)
}
$numWidth:60px;

.move-class{
  transition: all 1s ease;
}
.num-wrap{
  display: flex;
  flex-wrap: wrap;
  width: calc(#{$numWidth} * 9 + 5px * 8);
  gap: 5px;
  .num-item{
    width: $numWidth;
    height: $numWidth;
    line-height: $numWidth;
    text-align: center;
    border: 1px solid #ccc;
  }
}

实现效果

使用gsap实现数字滚动

<div style="margin-top: 20px;height: 2px">使用gsap实现数字滚动</div>
<el-input v-model="rolling.num" placeholder="placeholder" style="width: 200px"></el-input>
<h1>
  {{rolling.numRul.toFixed(0)}}
</h1>
import gsap from "gsap";
import {ref,reactive,watch} from "vue";

let rolling = reactive({
  num:10,
  numRul:10
})
watch(()=>rolling.num,(newVal)=>{
  gsap.to(rolling,{
    numRul:newVal,
    duration:1,
  })
})

依赖注入provide和inject

爷爷组件

<template>
  <h1>爷爷组件</h1>
  <el-button type="primary" @click="setColor('red')">红色</el-button>
  <el-button type="primary" @click="setColor('blue')">蓝色</el-button>
  <el-button type="primary" @click="setColor('pink')">粉色</el-button>
  <div class="box"></div>

  <hr>
  <ProvideA/>

  <hr>
  <ProvideB/>


</template>

<script setup lang="ts">
import {provide, inject, ref} from "vue"
import ProvideA from "@/components/ProvideA.vue";
import ProvideB from "@/components/ProvideB.vue";

let color = ref("red")

provide("color",color)

const setColor = (c) => {
  color.value = c
}

</script>

<style scoped>
.box{
  width: 100px;
  height: 100px;
  background-color: v-bind(color);
}
</style>

ProvideA

<template>
  <div>
    <h1>爸爸组件</h1>
    <div class="box"></div>
  </div>
</template>

<script setup lang="ts">
import {inject} from "vue"
import type {Ref} from "vue"
let color:Ref<string> = inject("color")
</script>

<style scoped>
.box{
  width: 100px;
  height: 100px;
  background-color: v-bind(color);
}
</style>

ProvideB

<template>
  <div>
    <h1>孙子组件</h1>
    <div class="box"></div>
    <button @click="setColor">设置粉色</button>
  </div>
</template>

<script setup lang="ts">
import {inject} from "vue"
import type {Ref} from "vue"
let color:Ref<string> = inject("color")

const setColor = () => {
  color.value = "pink"
}
</script>

<style scoped>
.box{
  width: 100px;
  height: 100px;
  background-color: v-bind(color);
}
</style>

实现效果

兄弟传参

Mitt

安装

npm install mitt
局部使用

添加一个JS文件导出

utils/mitt.js

import mitt from "mitt"
export default mitt()

使用,分别定义 A B两个组件

BusA

<template>
  <div class="box">
    我是A组件
    <el-button type="primary" @click="changeFlag">改变</el-button>
  </div>
</template>

<script setup>
import mitt from "../utils/mitt"
import {ref} from "vue";

let flag = ref(false)
const changeFlag = () => {
  flag.value = !flag.value
  mitt.emit("changeFlag",flag.value)
}

</script>

BusB

<template>
  <div class="box">
    我是B组件
    {{flag}}
  </div>
</template>

<script setup>
import mitt from "../utils/mitt";
import {ref,onBeforeUnmount} from "vue";
let flag = ref(false)

mitt.on('changeFlag', data=>{
  flag.value = data
})

onBeforeUnmount(()=>{
  mitt.off("changeFlag")
})
</script>

在父组件引入

<template>
  <BusA/>
  <BusB/>
</template>

<script setup>
import BusA from "@/components/BusA.vue";
import BusB from "@/components/BusB.vue";

</script>

效果

全局使用

main文件添加

import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import App from './App.vue'
import router from './router'
import 'element-plus/dist/index.css'
import zhCn from 'element-plus/dist/locale/zh-cn.min.js'
import 'dayjs/locale/zh-cn'

+ import mitt from "mitt"
+ const Mitt = mitt()

const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(ElementPlus, { locale: zhCn })
app.mount('#app')

+ declare module 'vue'{
+   export interface ComponentCustomProperties {
+       $Bus: typeof Mitt
+   }
+ }
+ app.config.globalProperties.$bus = Mitt

文件内部通过从 vue 中导出 getCurrentInstance 方法获取当前实例获取定义的全局变量使用

BusA

<template>
  <div class="box">
    我是A组件
    <el-button type="primary" @click="changeFlag">改变</el-button>
  </div>
</template>

<script setup>
import {ref,getCurrentInstance} from "vue";

let flag = ref(false)
let instance = getCurrentInstance()

const changeFlag = () => {
  flag.value = !flag.value
  instance?.proxy?.$bus.emit("changeFlag",flag.value)
}
</script>

BusB

<template>
  <div class="box">
    我是B组件
    {{flag}}
  </div>
</template>

<script setup>
import {ref, onBeforeUnmount, getCurrentInstance} from "vue";
let flag = ref(false)
let instance = getCurrentInstance()

instance?.proxy?.$bus.on('changeFlag', data=>{
  flag.value = data
})

onBeforeUnmount(()=>{
  instance?.proxy?.$bus.off("changeFlag")
})
</script>

手写Bus

class MyBus{
    constructor() {
        this.list = {}
    }
    emit(event, ...args){
        let funs = this.list[event]
        funs.forEach((fun) =>{
            fun.apply(this,args)
        })
    }
    on(event, callback){
        let funs = this.list[event]
        if(funs){
            funs.push(callback)
        }else{
            funs = [callback]
        }
        this.list[event] = funs
    }
    off(event){
        delete this.list[event]
    }
}
export default new MyBus()

jsx插件

安装

npm in stall @vitejs/plugin-vue-jsx -D

在 vite.config.js 中使用

import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

// https://vitejs.dev/config/
export default defineConfig({
  module:"es2022",
  plugins: [
    vue(),
    vueJsx(),
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [ElementPlusResolver()],
    }),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  },
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: "@import './src/layout_v2/css/bem.scss';"
      }
    }
  }
})

新建 JsxCom.tsx

import {defineComponent, reactive, ref} from "vue"
import {ElButton} from "element-plus"

interface propType  {
    msg?:string
}

export default defineComponent({
    props:{
        msg:String,
    },
    emits:[],
    setup(prop:propType,{emit,attrs,slots,expose}){
        let flag = ref(false)

        const chagneFlag = () => {
            flag.value = true
        }

        let list = reactive([1,2,3,4,5])

        return ()=> <>
            {/*遍历循环*/}
            {list.map(item => <h1>{item}</h1>)}
            <hr/>
            {/*按钮事件,使用οnclick={()=>chagneFlag()}*/}
            <ElButton type="primary" οnclick={()=>chagneFlag()}>改变这个值</ElButton>
            {flag.value && <h1>改变后的值</h1>}
            <hr/>
            <div>父组件传递的值:{prop.msg}</div>
        </>
    },
})

在vue中可以把这个当成普通的组件使用

<template>
  <JsxCom msg="Hello Jsx"/>
</template>
<script setup>
import JsxCom from '../components/JsxCom'
</script>

页面效果

image-20230924150109742

自动引入插件

安装

npm istall unplugin-auto-import/vite

配置

import AutoImport from 'unplugin-auto-import/vite'

export default defineConfig({
  module:"es2022",
  plugins: [
    vue(),
    vueJsx(),
    AutoImport({
      resolvers: [ElementPlusResolver()],
      imports: ['vue', 'vue-router'], // 自动引入vue,和vue-router相关
      dts: 'src/auto-imports.d.ts' // 自动生成的依赖文件
    }),
    Components({
      resolvers: [ElementPlusResolver()],
    }),
  ],
  
})

保存后查看 src/auto-imports.d.ts 内容

image-20230924150915291

里面自动的帮我们了引入

然后再组件中不需要手动的导入 vue,就可以使用vue中的各种声明

<template>
  <el-button type="primary" @click="flag = !flag">buttonCont</el-button>

  <div>
      {{flag}}
  </div>
</template>
<script setup>
let flag = ref(false)
</script>

v-model在组件中的使用

基本使用

vue3中在组件上绑定v-model时,默认的prop变成了modelValue

子组件 Vmodel

<template>
  <div>
    <el-input v-model="input" placeholder="placeholder" @input="changeValue" style="width: 200px"></el-input>
    <el-button>关闭</el-button>
  </div>
</template>

<script setup lang="ts">
import {defineProps,defineEmits} from "vue"

const props = defineProps<{
  modelValue:string,
}>()
// 更新model绑定的值固定写法: update:modelValue
const emit = defineEmits(['update:modelValue'])

let input = ref("")

onMounted(()=>{
  input.value = props.modelValue
})

const changeValue = (e) => {
  // 修改父组件的值
  emit('update:modelValue',e)
}

</script>

父组件

<template>
  父组件的值:{{value}}
  <div class="box">
    <Vmodel v-model="value" />
  </div>
</template>

<script setup lang="ts">
import Vmodel from "@/components/Vmodel.vue";

let value = ref("你好")
</script>

<style scoped>
.box{
  border: 2px solid black;
  padding: 30px;
}
</style>

绑定多个v-model

父组件

<template>
  父组件的值:{{value}}
  <el-button @click="isShow = !isShow">切换显示</el-button>
  
  <div class="box">
    <Vmodel v-model="value" v-model:isShow="isShow"/>
  </div>
</template>

<script setup lang="ts">
import Vmodel from "@/components/Vmodel.vue";

let value = ref("你好")
let isShow = ref(true)
</script>

<style scoped>
.box{
  border: 2px solid black;
  padding: 30px;
}
</style>

子组件

<template>
  <div v-if="isShow">
    <el-input v-model="input" placeholder="placeholder" @input="changeValue" style="width: 200px"></el-input>
    <el-button @click="close">关闭</el-button>
  </div>
</template>

<script setup lang="ts">
import {defineProps,defineEmits} from "vue"

const props = defineProps<{
  modelValue:string,
  isShow:boolean
}>()
// 更新model绑定的值固定写法: update:modelValue
const emit = defineEmits(['update:modelValue','update:isShow'])

let input = ref("")

onMounted(()=>{
  input.value = props.modelValue
})

const changeValue = (e) => {
  // 修改父组件的值
  emit('update:modelValue',e)
}

const close = () => {
  emit("update:isShow",false)
}
</script>

自定义指令

自定义指令的声明周期

<template>
  <div class="box" v-resize="onResize">

  </div>
</template>

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

const onResize = () => {
  console.log("宽高变化")
}

// 声明一个局部自定义指令,必须以v开头
const vResize:Directive = {
  created(){
    console.log("created")
  },
  beforeMount(){
    console.log("beforeMount")
  },
  mounted(...arg){
    console.log("mounted")
    console.log(arg)
  },
  beforeUpdate(){
    console.log("beforeUpdate")
  },
  updated(){
    console.log("updated")
  },
  beforeUnmount(){
    console.log("beforeUnmount")
  },
  unmounted(){
    console.log("unmounted")
  }
}
</script>

<style scoped>
.box{
  height: 100%;
  background-color: #f5f5f5;
}
</style>

在任意一个钩子函数头能拿到自定义指令绑定的参数,我们通过打印 arg 看看参数有什么

image-20230924171641529

我们利用这两个参数实现监听元素宽高变化的指令,当元素宽高发生变化时调用绑定的函数

mounted(el,bindings){
  console.log("mounted")
  // 监听元素宽高变化
  const resizeObserver = new ResizeObserver(entries => {
    let width = entries[0].contentRect.width;
    let height = entries[0].contentRect.height;
    console.log(`元素宽度:${width},元素高度:${height}`)
    bindings.value()
  });
  resizeObserver.observe(el);
},

修改 mounted 钩子的内容,通过observe 观察 el,然后调用 bindings.value

自定义指令的简写方式

我们也可以通过函数的方式来自定义指令

<template>
  <el-button v-has-show="'order:add'" type="primary">新增</el-button>
  <el-button v-has-show="'order:update'" type="warning">修改</el-button>
  <el-button v-has-show="'order:delete'" type="danger">删除</el-button>
</template>

<script setup lang="ts">
import { Directive } from 'vue'
//permission
localStorage.setItem('userId','songzx')

//mock后台返回的数据
const permission = [
  // 'songzx:order:add',
  'songzx:order:update',
  'songzx:order:delete'
]

const userId = localStorage.getItem('userId') as string

const vHasShow:Directive<HTMLElement,string> = (el,binding)=>{
  if(!permission.includes(`${userId}:${binding.value}`)){
    // 直接移除这个元素,比使用 el.style.display = 'none' 更安全
    el.remove()
  }
}

</script>

上面的例子是一个按钮级别权限的demo

鼠标拖动元素案例

<template>
  <div class="root">
    <div class="box" v-move>
      <div class="header"></div>
    </div>
  </div>

</template>

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

const vMove:Directive<HTMLElement> = (el)=>{
  let moveEl:HTMLElement = el.querySelector(".header")

  const mousedown = (e:MouseEvent)=> {
    // 鼠标按下时获取当前鼠标的位置和移动物体相对于浏览器的位置
    let X = e.x - el.offsetLeft
    let Y = e.y - el.offsetTop

    // 移动
    const move = (e:MouseEvent)=>{
      // 在移动物体时,需要减去偏移量
      el.style.left = e.clientX - X + "px"
      el.style.top = e.clientY - Y + "px"
    }
    document.addEventListener("mousemove", move)
    document.addEventListener("mouseup", ()=>{
      document.removeEventListener("mousemove", move)
    })
  }
  // 鼠标按下头部时触发
  moveEl.addEventListener("mousedown",mousedown)
}

</script>

<style scoped lang="scss">
.root{
  position: relative;
}
.box{
  position: absolute;
  width: 200px;
  height: 200px;
  left: 100px;
  top: 100px;
  border: 2px solid black;
  .header{
    width: 100%;
    height: 20px;
    background-color: black;
  }
}
</style>

图片懒加载案例

<template>
  <div class="root">
    <img v-for="item in arr" v-lazy="item" style="width: 100%;"/>
  </div>

</template>

<script setup lang="ts">
import { Directive,DirectiveBinding } from 'vue'

// import.meta.glob 引入目标路径中的所有文件,返回一个对象,默认使用module引入
// 添加了eager: true,则变成同步引入
let imgList = import.meta.glob("../assets/images/*.*",{eager: true})
// 得到所有图片地址
let arr = Object.values(imgList).map(item=>item.default)

// 自定义懒加载指令
const vLazy:Directive = async (el:HTMLImageElement,binding:DirectiveBinding)=>{
  // 先给一个默认值
  const def = await import("../assets/logo.svg")
  el.src = def.default
  // 判断元素是否在可视范围内
  const  intersection = new IntersectionObserver((e)=>{
    // 判断是否在可视范围内
    if(e[0].intersectionRatio > 0){
      setTimeout(()=>{
        el.src = binding.value
      },500)
      intersection.unobserve(el)
    }
  })
  intersection.observe(el)
}

</script>

<style scoped lang="scss">
.root{
  width: 361px;
  height: 800px;
  overflow: auto;
}
</style>

自定义Hook

好用的第三方库

vueuse

npm i @vueuse/core

网址:Get Started | VueUse — 开始使用 |Vueuse

图片转base64

新建 useImgToBase64.ts

import {onMounted} from 'vue'

type optionsType = {
    el:String
}

export default function (options:optionsType):Promise<string>{
    return new Promise((resolve, reject) =>{
        onMounted(()=>{
            let img:HTMLImageElement = document.querySelector(options.el)
            img.onload = ()=>{
                resolve(toBase64(img))
            }
            const toBase64 = (img:HTMLImageElement) => {
                let canvas:HTMLCanvasElement = document.createElement('canvas')
                let ctx:CanvasRenderingContext2D = canvas.getContext('2d')
                canvas.width = img.width
                canvas.height = img.height
                ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
                return canvas.toDataURL("image/jpeg")
            }
        })
    })
}

使用

<template>
  <img src="../assets/images/1.jpeg"/>
</template>

<script setup lang="ts">
import useImgToBase64 from "@/utils/useImgToBase64";

useImgToBase64({el:"img"}).then(res=>{
  console.log(res)
})
</script>

image-20230924222116173

自定义Vite库并发布到NPM

封装useResize

用于监听绑定元素的宽高变化,当元素宽高发生变化时触发回调并获取最新的宽高

新建项目

结合上面学到的 Hook 和 自定义指令封装一个监听元素宽高变化的指令,并发布到 npm

项目结构

useResize            
├── src              
│   └── index.ts     
├── README.md        
├── index.d.ts       
├── package-lock.json
├── package.json     
├── tsconfig.json    
└── vite.config.ts

src/index.ts

import type {App} from "vue";

/**
 * 自定义Hook
 * @param el
 * @param callback
 */
const weakMap = new WeakMap<HTMLElement, Function>();
const resizeObserver = new ResizeObserver((entries) => {
    for (const entry of entries) {
        const handle = weakMap.get(entry.target as HTMLElement);
        handle && handle(entry)
    }
})

function useResize(el: HTMLElement, callback: Function) {
    if (weakMap.get(el)) {
        return
    }
    weakMap.set(el, callback)
    resizeObserver.observe(el)
}

/**
 * 定义vite插件时,vue会在底层调用插件的install方法
 * @param app
 */
function install(app: App) {
    app.directive('resize', {
        mounted(el: HTMLElement, binding: { value: Function }) {
            useResize(el, binding.value)
        }
    })
}

useResize.install = install

export default useResize

vite.config.ts

import {defineConfig} from "vite"

export default defineConfig({
    build:{
        lib:{
            // 打包入口文件
            entry:"src/index.ts",
            // name
            name:"useResize"
        },
        rollupOptions:{
            // 忽略打包的文件
            external:['vue'],
            output:{
                globals:{
                    useResize:"useResize"
                }
            }
        }
    }
})

index.d.ts

declare const useResize:{
    (element:HTMLElement, callback:Function):void
    install:(app:any) => void
}

export default useResize

package.json

{
  "name": "v-resize-songzx",
  "version": "1.0.0",
  "description": "",
  "main": "dist/v-resize-songzx.umd.js",
  "module": "dist/v-resize-songzx.mjs",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "vite build"
  },
  "keywords": [],
  "author": "songzx",
  "files": [
    "dist",
    "index.d.ts"
  ],
  "license": "ISC",
  "devDependencies": {
    "vue": "^3.3.4"
  },
  "dependencies": {
    "vite": "^4.4.9"
  }
}

pachage.json 文件属性说明:

  • name:对应打包后生成的包名,也就是上传到npm上面的包名,不能包含数字和特殊符号
  • version:包的版本号
  • main:对应打包后的 umd.js 文件,在使用 app.use 时会访问使用文件
  • module:使用import、require等方式引入时会使用 mjs 文件
  • files:指定那些文件需要上传
打包
npm run build
登录npm
npm login

image-20230924231941434

发布
npm publish

image-20230924232715692

打开 npm 网站,搜索查看是否发布成功

image-20230925090849623

使用自己的库

安装
npm i v-resize-songzx
使用方式一

全局注册 v-resze 指令

main.ts 引入

import useResize from "v-resize-songzx";

const app = createApp(App)

app.use(useResize)
app.mount('#app')
<template>
  <div class="resize" v-resize="getNewWH"></div>
</template>

<script setup lang="ts">
const getNewWH = (e) => {
  console.log(e.contentRect.width, e.contentRect.height);
}

</script>

<style scoped>
/*把一个元素设置成可以改变宽高的样子*/
.resize {
  resize: both;
  width: 200px;
  height: 200px;
  border: 1px solid;
  overflow: hidden;
}
</style>
使用方式二

使用Hook的方式

<template>
  <div class="resize"></div>
</template>

<script setup lang="ts">

import useResize from "v-resize-songzx";

onMounted(() => {
  useResize(document.querySelector(".resize"), e => {
    console.log(e.contentRect.width, e.contentRect.height);
  })
})

</script>

<style scoped>
/*把一个元素设置成可以改变宽高的样子*/
.resize {
  resize: both;
  width: 200px;
  height: 200px;
  border: 1px solid;
  overflow: hidden;
}
</style>

定义全局变量和方法

main.ts 中添加

import dayjs from "dayjs"
import mitt from "mitt"

const Mitt = mitt()

// 定义全局变量
app.config.globalProperties.$bus = Mitt
app.config.globalProperties.$BaseUrl = 'http://localhost'
app.config.globalProperties.$formatDate = (date: Date) => dayjs(date).format('YYYY-MM-DD HH:mm:ss')

// 定义声明文件
declare module 'vue' {
    export interface ComponentCustomProperties {
        $bus: typeof Mitt,
        $BaseUrl: string,
        $formatDate: Date
    }
}

在任何组件中都可以使用

<template>
  <div>
    {{ $BaseUrl }}
  </div>
</template>

<script setup lang="ts">
import {getCurrentInstance} from 'vue'
// 获取当前实例
const instance = getCurrentInstance()

console.log(instance.proxy.$BaseUrl) //=> http://localhost
console.log(instance.proxy.$formatDate(new Date())) //=> 2023-09-25 13:51:23

</script>

自定义插件之全局Loading

ElementPlus的默认全局Loading

如果完整引入了 Element Plus,那么 app.config.globalProperties 上会有一个全局方法$loading,同样会返回一个 Loading 实例。

名称说明类型默认
targetLoading 需要覆盖的 DOM 节点。 可传入一个 DOM 对象或字符串; 若传入字符串,则会将其作为参数传入 document.querySelector以获取到对应 DOM 节点string / HTMLElementdocument.body
bodyv-loading 指令中的 body 修饰符booleanfalse
fullscreenv-loading 指令中的 fullscreen 修饰符booleantrue
lockv-loading 指令中的 lock 修饰符booleanfalse
text显示在加载图标下方的加载文案string
spinner自定义加载图标类名string
background遮罩背景色string
customClassLoading 的自定义类名string
指令的方式使用
<template>
   <div class="box" v-loading="isLoading">
      content
   </div>
   <el-button type="primary" @click="showDivLoading">显示loading</el-button>
</template>

<script setup lang="ts">
// 显示局部loading
let isLoading = ref(false)

const showDivLoading = () => {
  isLoading.value = !isLoading.value
}

</script>

<style scoped>
.box {
  width: 200px;
  height: 200px;
  border: 1px solid;
}
</style>
函数式调用
<template>
  <el-button type="primary" @click="showLoading">showLoading</el-button>
</template>

<script setup lang="ts">
import {getCurrentInstance} from 'vue'
// 获取当前实例
const {proxy} = getCurrentInstance()

// 显示全局loading
const showLoading = () => {
  const loading = proxy.$loading()
  setTimeout(() => {
    loading.close()
  }, 2000)
}
</script>

自定义全局Loading

我们自己动手来实现一个和ElementPlus的Loading,同时支持函数调用和指令调用

添加MyLoading.vue
<template>
  <transition enter-active-class="animate__animated animate__fadeIn"
              leave-active-class="animate__animated animate__fadeOut">
    <div class="root-box" v-if="show">
      <div class="wrap">
        <img src="../assets/images/loading.gif"/>
      </div>
    </div>
  </transition>
</template>

<script setup>
let show = ref(false)

const showLoading = () => {
  show.value = true
}
const hideLoading = (callback) => {
  show.value = false
  callback && setTimeout(() => callback(), 500)
}

defineExpose({
  show,
  showLoading,
  hideLoading
})

</script>

<style scoped lang="scss">
.animate__animated.animate__fadeIn {
  --animate-duration: 0.5s;
}

.animate__animated.animate__fadeOut {
  --animate-duration: 0.5s;
}

.root-box {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  right: 0;
  margin: 0;
  background-color: rgba(255, 255, 255, 0.9);
  z-index: 2000;
  display: flex;
  justify-content: center;
  align-items: center;

  .wrap {
    width: 100px;
    height: 100px;
    display: flex;
    justify-content: center;
    align-items: center;
    overflow: hidden;

    img {
      width: 100%;
      transform: scale(2.5);
    }
  }
}
</style>
添加MyLoading.ts
import type {App, VNode,} from "vue"
import {createVNode, render, cloneVNode} from "vue"
import MyLoading from "@/components/MyLoading.vue"

export default {
    install(app: App) {
        // 使用vue底层的createVNode方法将组件渲染为虚拟节点
        const VNode: VNode = createVNode(MyLoading)
        // 使用render函数将组件挂载到body中
        render(VNode, document.body)
        // 定义全局方法设置组件的显示和隐藏
        app.config.globalProperties.$showLoading = VNode.component?.exposed.showLoading
        app.config.globalProperties.$hideLoading = VNode.component?.exposed.hideLoading

        const weakMap = new WeakMap()

        // 自定义Loading指令
        app.directive("zx-loading", {
            mounted(el) {
                if (weakMap.get(el)) return
                //  记录当前绑定元素的position
                weakMap.set(el, window.getComputedStyle(el).position)
            },
            updated(el: HTMLElement, binding: { value: Boolean }) {
                const oldPosition = weakMap.get(el);
                // 如果不是position: relative或者absolute,就设置为relative
                // 这里的目的是确保loading组件正确覆盖当前绑定的元素
                if (oldPosition !== 'absolute' && oldPosition !== 'relative') {
                    el.style.position = 'relative'
                }
                // 克隆一份loading元素,
                // 作用是当页面上有多个zx-loading时,每个dom都维护一份属于自己的loading,不会冲突
                const newVNode = cloneVNode(VNode)
                // 挂载当前节点
                render(newVNode, el)
                // 判断绑定的值
                if (binding.value) {
                    newVNode.component?.exposed.showLoading()
                } else {
                    newVNode.component?.exposed.hideLoading(() => {
                        // 还原布局方式
                        el.style.position = oldPosition
                    })
                }
            }
        })
    }
}

在上面的文件中定义了两个全局函数和一个自定义指令

  • $showLoading:全局显示一个Loading
  • $hideLoading:关闭全局的Loading
  • zx-loading:自定义指令
在main.ts中挂载

main.ts 中去挂载我们自定义的 Loading

import {createApp} from 'vue'
import MyLoading from "@/utils/MyLoading";

const app = createApp(App)
// 引入自定义的全局Loading
app.use(MyLoading)

app.mount('#app')
使用方法一:函数式使用

调用全局方法弹出Loading

<template>
    <!--自定义全局loading-->
    <el-button type="primary" @click="showMyLoading">显示自定义的全局loading</el-button>
</template>

<script setup lang="ts">
import {getCurrentInstance} from 'vue'
// 获取当前实例
const {proxy} = getCurrentInstance()

// 全局显示自定义loading
const showMyLoading = () => {
  proxy.$showLoading()
  setTimeout(() => {
    proxy.$hideLoading()
  }, 2000)
}
</script>

image-20230925171920861

使用方法二:指令式使用
<template>
  <div>
    <!--自定义的loading指令使用-->
    <div class="box" v-zx-loading="isLoading">
      指令的方式使用
    </div>
    <el-button type="primary" @click="showDivLoading">显示loading</el-button>

	<!--自定义的loading指令使用-->      
    <div class="parent">
      <div class="child" v-zx-loading="childLoading">
      </div>
    </div>
    <el-button type="primary" @click="showChildLoading">显示childLoading</el-button>
  </div>
</template>

<script setup lang="ts">
// 显示局部loading
let isLoading = ref(false)
const showDivLoading = () => {
  isLoading.value = !isLoading.value
}

const childLoading = ref(false)
const showChildLoading = () => {
  childLoading.value = !childLoading.value
}
</script>

<style scoped lang="scss">
.box {
  width: 200px;
  height: 200px;
  border: 1px solid;
}

.parent {
  position: relative;
  width: 300px;
  height: 300px;
  border: 1px solid;
  padding: 30px;

  .child {
    position: absolute;
    right: 30px;
    bottom: 30px;
    width: 200px;
    height: 200px;
    border: 1px solid;
  }
}
</style>

image-20230925172100385

use函数源码实现

添加 MyUse.ts

import type {App} from "vue"
import {app} from "@/main"

// 定义一个接口,声明install方法必传
interface Use {
    install: (app: App, ...options: any[]) => void
}

const installList = new Set()

export default function myUse<T extends Use>(plugin: T, ...options: any[]) {
    // 判断这个插件是否已经注册过了,如果注册过了则报错
    if (installList.has(plugin)) {
        console.error("Plugin already installed")
        return
    }
    // 调用插件身上的install方法,并传入main.ts导出的app
    plugin.install(app, ...options)
    installList.add(plugin)
}

使用自定义的myUse方法注册我们自定义的Loading

import {createApp} from 'vue'

// 自定义全局Loading
import MyLoading from "@/utils/MyLoading";
// 自定义app.use方法
import myUse from "@/utils/MyUse";


export const app = createApp(App)
// 引入自定义的全局Loading
myUse(MyLoading)

app.mount('#app')

CSS选择器

:deep

使用 :deep() 将选择器包裹起来可以将第三方库的样式进行修改

<template>
  <div>
    <el-input placeholder="placeholder" v-model="name"/>
  </div>
</template>

<script setup>
let name = ref("")
</script>

<style scoped lang="scss">
.el-input{
  :deep(.el-input__inner) {
    background-color: red;
  }
}
</style>

image-20230925223153879

:slotted

使用 :slotted() 将插槽中的类名包裹起来,可以修改插槽中的元素样式

SlotTestCom.vue

<template>
  <div>
    父组件
    <slot></slot>
  </div>
</template>

<style scoped>
:slotted(.msg) {
  font-weight: bold;
  color: red;
}
</style>
<SlotTestCom>
  <div class="msg">私人订制DIV</div>
</SlotTestCom>

image-20230927103314117

:global

使用 :global() 用于设置全局样式

:global(div){
  font-size: 17px;
  color: #222222;
}

全局设置div的样式

css中使用v-bind

let color = ref("pink")
// 随机一个颜色
const randomColor = () => {
  color.value = `rgb(${Math.random() * 255},${Math.random() * 255},${Math.random() * 255})`
}

使用 v-bind() 将JS中变量包裹起来即可使用

.el-input {
  width: 300px;

  :deep(.el-input__inner) {
    background-color: v-bind(color);

  }
}

Vue3集成Tailwind CSS

官网地址Tailwind CSS 中文文档 - 无需离开您的HTML,即可快速建立现代网站。

安装

npm install -D tailwindcss@latest postcss@latest autoprefixer@latest

生成配置文件

npx tailwindcss init -p

修改配置文件 tailwind.config.js

2.6版本

module.exports = {
  purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
  theme: {
    extend: {},
  },
  plugins: [],
}

3.0版本

module.exports = {
  content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
  theme: {
    extend: {},
  },
  plugins: [],
}

新建 index.css 并在 main.ts 中引入

@tailwind base;
@tailwind components;
@tailwind utilities;

image-20230927132455491

基础使用

详细类名见文档:https://www.tailwindcss.cn/docs/font-family

<template>
  <div class="h-full flex justify-center items-center bg-teal-400">
    <div class="text-8xl text-rose-700font-bold text-white">Hello Word</div>
  </div>
</template>

image-20230927132522243

nextTick

vue 中更新DOM操作是异步的,但是JS程序是同步的,所以当遇到操作DOM时可能会出现延迟更新的情况,vue 也给了一个解决方案,就是可以将操作 DOM 的代码放在 nextTick 中执行,nextTick 会执行一个 Promise 函数去更新DOM,来实现同步更新DOM的操作

这样做的好处是可以提高程序性能,例如执行一个for循环,每次循环会改变变量的值,然后吧这个变量输出到页面上。用一个watch去监听这个变量,watch函数并不会触发多次,而是只会执行一次

下面是一个小案例

<template>
  <div class="box" ref="box">
    <div class="item" v-for="(item,index) in msgList" :key="index">
      {{ item.msg }}
    </div>
  </div>
  <el-input v-model="msg" style="width: 200px"/>
  <el-button type="primary" @click="send">发送</el-button>
</template>


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

let msgList = reactive([
  {
    msg: "Hello world"
  }
])
let msg = ref("")
let box = ref<HTMLDivElement>()

const send = () => {
  msgList.push({msg: msg.value})
  nextTick(() => {
    // 发送完消息后自动滚动到底部
    box.value.scrollTop = box.value.scrollHeight
  })
}
</script>

<style scoped lang="scss">
.box {
  width: 300px;
  border: 2px solid #ddd;
  height: 400px;
  overflow: auto;

  .item {
    height: 30px;
    line-height: 30px;
    padding-left: 1em;
    background-color: #dddddd;
    margin: 2px;
  }
}
</style>

Vue3开发安卓和IOS

参照博客:https://xiaoman.blog.csdn.net/article/details/131507483

安装安卓开发工具

image-20230927161024413

image-20230927161057832

image-20230927161139275

image-20230927161213097

image-20230927161303242

安装完成后打开

image-20230927161352447

首次运行需要安装一些SDK

image-20230927161623550

ionic安装

npm install -g @ionic/cli

初始化项目

ionic start app tabs --type vue
  • app 项目名称
  • tabs 使用的预设
  • –type vue 使用的是vue就写vue,react就写react

image-20230927163158065

image-20230927165645993

启动项目

npm run dev

image-20230927165717819

打包和构建

先执行打包命令

npm run build

再执行构建命令,将程序打包成Android包

ionic capacitor copy android

运行成功后会自动多一个android文件夹

image-20230927165845466

image-20230927165905649

然后运行下面命令进行预览

ionic capacitor open android

会自动打开安卓编辑器

等待项目加载完成后,点击绿色的箭头即可启动

image-20230927171133698

H5适配

添加meat信息

<meta name="viewport" content="width=device-width, initial-scale=1.0">

清除默认样式

<style>
  html,body,#app{
      height: 100%;
      overflow: hidden;
  }
  *{
      padding: 0;
      margin: 0;
  }
</style>

圣杯布局

<template>
  <div class="header">
    <div></div>
    <div></div>
    <div></div>
  </div>
</template>

<style scoped lang="scss">
.header{
  width: 100%;
  height: 50px;
  line-height: 50px;
  display: flex;
  div:nth-child(1),div:nth-child(3){
    width: 100px;
    background-color: deepskyblue;
  }
  div:nth-child(2){
    flex: 1;
    background-color: pink;
  }
}
</style>
image-20231007222439590

使用postCSS将px单位转成vh和vw

百分比是相对于父元素

vw和vh相对于视口

编写postCSS插件

新建 plugins/PxToVwVh.ts

import {Plugin} from "postcss"

let Options = {
    defaultWidth: 390,
    defaultHeight: 844,
}
interface OptionsTypes {
    defaultWidth?:number,
    defaultHeight?:number,
}

export function PxToVwVh(options:OptionsTypes=Options):Plugin{
    let opt = Object.assign({}, options)
    return {
        postcssPlugin:"px-to-vw-vh",
        // 钩子函数
        Declaration(node){
            if(node.value.includes("px")){
                const num = parseFloat(node.value)
                if(node.prop.includes("width")){
                    node.value = `${((num / opt.defaultWidth) * 100).toFixed(2)}vw`
                }else if(node.prop.includes("height")){
                    node.value = `${((num / opt.defaultHeight) * 100).toFixed(2)}vh`
                }
            }
        }
    }
}

tsconfig.node.json 中引入

{
  "extends": "@tsconfig/node18/tsconfig.json",
  "include": [
    "vite.config.*",
    "vitest.config.*",
    "cypress.config.*",
    "nightwatch.conf.*",
    "playwright.config.*",
    "plugins/**/*"
  ],
  "compilerOptions": {
    "composite": true,
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "types": ["node"],
    "noImplicitAny": false
  }
}
  • include中添加 plugins/**/*
  • noImplicitAny 允许隐式的使用any

使用插件

vite.config.ts 中使用

import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import {PxToVwVh} from "./plugins/PxToVwVh";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
  ],
  css: {
    postcss: {
      plugins: [
        PxToVwVh()
      ]
    },
  },
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
})

效果展示

我们通过编写插件,实现了将PX单位转换成相对于视口,这样保证了在不同尺寸的屏幕上都会有一个相同的展示布局

image-20231007224908253

image-20231007224848623

全局控制字体大小

设置全局CSS变量

:root{
    --font-size:16px;
}

然后全局可以通过 var(–font-size) 使用

<template>
  <div class="header">
    <div>返回</div>
    <div>H5适配</div>
    <div>取消</div>
  </div>

  <button @click="changeFontSize(15)">默认</button>
  <button @click="changeFontSize(24)">大</button>
  <button @click="changeFontSize(36)">特大</button>
</template>

<script setup>

import {onMounted} from "vue";

onMounted(()=>{
  document.documentElement.style.setProperty("--font-size",localStorage.getItem("fontSize") || "16px")
})

const changeFontSize = (size) => {
  document.documentElement.style.setProperty("--font-size",size + 'px')
  localStorage.setItem("fontSize",size + 'px');
}
</script>

<style scoped lang="scss">
.header{
  width: 100%;
  height: 50px;
  line-height: 50px;
  display: flex;
  text-align: center;
  font-size: var(--font-size);
  div:nth-child(1),div:nth-child(3){
    width: 100px;
    background-color: deepskyblue;
  }
  div:nth-child(2){
    flex: 1;
    background-color: pink;
  }
}

button{
  margin-right: 10px;
}
</style>

点击按钮可以实现字体大小切换

image-20231007230159198

unoCss原子化

官网:https://unocss.dev/

什么是css原子化?

CSS原子化的优缺点

1.减少了css体积,提高了css复用

2.减少起名的复杂度

3.增加了记忆成本 将css拆分为原子之后,你势必要记住一些class才能书写,哪怕tailwindcss提供了完善的工具链,你写background,也要记住开头是bg

安装

npm i -D unocss

配置插件

// vite.config.ts
import UnoCSS from 'unocss/vite'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [
    UnoCSS(),
  ],
})

创建一个 uno.config.js 文件

// uno.config.js
import { defineConfig } from 'unocss'

export default defineConfig({
    // 自定义规则
    rules:[
        ["red",{ color:"red",'font-size':"25px" }]
    ]
})

main.ts 文件中添加

// main.ts
import 'virtual:uno.css'

使用

直接在页面中使用类名即可

<div class="red">
  Hello Word
</div>

image-20231008221631938

动态配置类名

rules: [
  [/^m-(\d+)$/, ([, d]) => ({ margin: `${Number(d) * 10}px` })],
  ['flex', { display: "flex" }]
]

使用

<div class="red m-10">
  Hello Word
</div>

image-20231008222106144

使用预设

修改 uno.config.js

// uno.config.js
import { defineConfig,presetIcons,presetAttributify,presetUno } from 'unocss'

export default defineConfig({
    // 自定义规则
    rules:[
        [/^m-(\d+)$/, ([, d]) => ({ margin: `${Number(d) * 10}px` })],
        ["red",{ color:"red",'font-size':"25px" }],
    ],
    // 使用预设
    presets:[presetIcons(),presetAttributify(),presetUno()]
})
  • presetIcons 这个是图标

  • presetAttributify 这个是美化CSS

  • presetUno 预设(实验阶段)是一系列流行的原子化框架的 通用超集,包括了 Tailwind CSS,Windi CSS,Bootstrap,Tachyons 等。

    例如,ml-3(Tailwind),ms-2(Bootstrap),ma4(Tachyons),mt-10px(Windi CSS)均会生效。

使用图标

在官网中找到自己需要的图标:https://icones.js.org/

然后选中后安装

image-20231008223505900

查看页面路径上的单词,然后安装

npm i -D @iconify-json/svg-spinners

点击某个要使用的图标,复制类名即可

image-20231008223618270

<div class="i-svg-spinners-bars-fade font-size-50px color-pink"></div>

image-20231008224316603

Vue编译宏

首先vue版本必须是3.3及以上版本

子组件

<template>
  <el-button type="primary" @click="add">添加</el-button>

  <ul>
    <li v-for="item in props.nameList">
      {{item}}
    </li>
  </ul>
</template>

<script setup lang="ts">
import {defineProps,defineOptions,defineEmits,defineSlots} from "vue"

// defineProps,可以定义类型
const props = defineProps<{
  nameList:string[]
}>()

const add = () => {
  emit("addName",'Tome')
}
// defineEmits,可以定义事件
// 第一个参数是事件名称,第二个参数是事件参数类型,问号表示可选
const emit = defineEmits<{
  (event:'addName',args?:any):void
}>()

// defineOptions常用来定义组件名字
defineOptions({
  name:"DefineComponents"
})

</script>

父组件

<template>
  <DefineComponents :nameList="nameList" @addName="addName"/>
</template>

<script setup lang="ts">
import DefineComponents from "@/components/DefineComponents.vue";

let nameList:string[] = reactive(["张三","李四", "王五"])

const addName = (args) => {
  nameList.push(args)
}

</script>
函数名称含义
defineProps接收父组件传递过来的参数
defineEmits定义事件名称
defineOptions配置组件名称和其他信息

Vue环境变量

在项目根目录新建两个文件,分别表示开发环境配置、生成环境配置

注意:设置环境变量时必须以 VITE_ 开头,否则不生效

.env.development

# .env.development
VITE_API=http://localhost:8080

.env.production

# .env.production
VITE_API=/prod-api

修改 package.json 中的运行命令,在启动dev是设置mode是development,表示读取开发环境配置,名称可以自定义,但是要和上面新建的配置文件后缀名保持一致

"scripts": {
  "dev": "vite --mode development",
},

然后在 vue 文件中通过下面方式获取配置项

console.log(import.meta.env)

image-20231015105913888

这里是开发环境,读取到的 VITE_API 是 http://localhost:8080

然后打包项目,再看一下打印结果

image-20231015110224421

vite.config.ts 中获取环境变量时通过如下方式获取

import { defineConfig,loadEnv } from 'vite'


let {VITE_API} = loadEnv(process.env.NODE_ENV,process.cwd())

console.log(VITE_API)

控制台会打印出定义的环境变量

image-20231015110925175

Webpack从0到1构建Vue3工程

项目结构

webpack-vue            
├── config             
│   ├── webpack.dev.js 
│   └── webpack.prod.js
├── src                
│   ├── App.vue        
│   └── Child.vue      
├── index.html         
├── main.js            
├── package.json       
└── pnpm-lock.yaml

package.json

{
  "name": "webpack-vue",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --config config/webpack.prod.js",
    "dev": "webpack serve --config config/webpack.dev.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@vue/compiler-sfc": "^3.3.4",
    "clean-webpack-plugin": "^4.0.0",
    "css-loader": "^6.8.1",
    "friendly-errors-webpack-plugin": "^1.7.0",
    "html-webpack-plugin": "^5.5.3",
    "less": "^4.2.0",
    "less-loader": "^11.1.3",
    "style-loader": "^3.3.3",
    "typescript": "^5.2.2",
    "vue": "^3.3.4",
    "vue-loader": "^17.3.0",
    "webpack": "^5.89.0",
    "webpack-cli": "^5.1.4",
    "webpack-dev-server": "^4.15.1"
  }
}

webpack.dev.js

const path = require("path")
const HtmlWebpackPlugin =  require("html-webpack-plugin");
const {CleanWebpackPlugin} = require("clean-webpack-plugin");
const {VueLoaderPlugin} = require("vue-loader");

module.exports = {
    mode:"development",
    entry: "./main.js",
    output: {
        filename: "js/[name].[contenthash:10].js",
        path: path.resolve(__dirname, "dist")
    },
    module: {
        rules: [
            {
                test:/\.vue$/,
                use: "vue-loader"
            },
            {
                test: /\.css$/, //解析css
                use: ["style-loader", "css-loader"],
            },
            {
                test:/\.less/,
                use: ["style-loader","css-loader", "less-loader"],
            }
        ]
    },
    resolve: {
        alias: {
            "@/": path.resolve(__dirname, './src') // 别名
        },
        extensions: ['.js', '.json', '.vue', '.ts', '.tsx'] //识别后缀
    },
    plugins: [
        new CleanWebpackPlugin(),
        new VueLoaderPlugin(),
       new HtmlWebpackPlugin({
           template: "./index.html",
       }),
    ],
    devServer: {
        port: 8088,
        open: true,
        host: "localhost",
        historyApiFallback: true, // 解决vue-router刷新404问题
        proxy: {
            "/api": {
                changeOrigin: true,
                pathRewrite: {
                    "^/api": ""
                }
            }
        }
    }
}

webpack.prod.js

const path = require("path")
const HtmlWebpackPlugin =  require("html-webpack-plugin");
const {CleanWebpackPlugin} = require("clean-webpack-plugin");
const {VueLoaderPlugin} = require("vue-loader");

module.exports = {
    mode:"production",
    entry: "./main.js",
    output: {
        filename: "js/[name].[contenthash:10].js",
        path: path.resolve(__dirname, "../dist")
    },
    module: {
        rules: [
            {
                test:/\.vue$/,
                use: "vue-loader"
            },
            {
                test: /\.css$/, //解析css
                use: ["style-loader", "css-loader"],
            },
            {
                test:/\.less/,
                use: ["style-loader","css-loader", "less-loader"],
            }
        ]
    },
    resolve: {
        alias: {
            "@": path.resolve(__dirname, './src') // 别名
        },
        extensions: ['.js', '.json', '.vue', '.ts', '.tsx'] //识别后缀
    },
    plugins: [
        new CleanWebpackPlugin(),
        new VueLoaderPlugin(),
       new HtmlWebpackPlugin({
           template: "./index.html",
       }),
    ],
}

Vite性能优化

打包优化

vite.config.js 添加 build 配置项

import { fileURLToPath, URL } from 'node:url'

import { defineConfig,loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import unocss from 'unocss/vite'



let {VITE_API} = loadEnv(process.env.NODE_ENV,process.cwd())

console.log(VITE_API)

// https://vitejs.dev/config/
export default defineConfig({
  module:"es2022",
  plugins: [
    vue(),
    vueJsx(),
    AutoImport({
      resolvers: [ElementPlusResolver()],
      imports: ['vue', 'vue-router'],
      dts: 'src/auto-imports.d.ts'
    }),
    Components({
      resolvers: [ElementPlusResolver()],
    }),
    unocss(),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  },
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: "@import './src/layout_v2/css/bem.scss';"
      }
    }
  },
  build:{
    minify:"esbuild", // esbuild打包速度最快,terser 打包体积最小
    cssCodeSplit:true,// 拆分CSS文件
    chunkSizeWarningLimit:2000, // 单文件超过2000kb警告
    assetsInlineLimit:1024*10, // 静态资源文件低于10KB时自动转Base64
  }
})

Pinia

安装

npm install pinia

在 main.ts 中引入

import {createApp} from 'vue'
import {createPinia} from 'pinia'

export const app = createApp(App)
app.use(createPinia())

app.mount('#app')

基本使用

userInfoStore.js

import {defineStore} from 'pinia'

export const useUserInfoStore = defineStore('userInfo', {
  state: () => {
    return {
      name: "李斯特",
      age: 18
    }
  },
  getters: {
    userMsg() {
      return this.name + '---' + this.age
    }
  },
  actions: {
    setName(newName) {
      console.log(this.name)
      this.name = newName
    }
  }
})

actions 中的函数也是支持异步的,this 指向指向的是 state 中返回的对象地址,所以可以通过this来获取到 state 中的属性值

vue文件中使用方法

<template>
  <div>
    <ul>
      <li>{{ userInfoStore.name }}</li>
      <li>{{ userInfoStore.age }}</li>
      <li>{{ userInfoStore.userMsg }}</li>
    </ul>
    <el-button type="primary" @click="change">change</el-button>
  </div>
</template>

<script setup>
import {useUserInfoStore} from "@/stores/userInfoStore";

const userInfoStore = useUserInfoStore()

const change = () => {
  userInfoStore.setName("张三丰")
}

</script>

Pinia的一些API

  • $reset 重置数据
  • $subscribe 监听数据变化
  • $onAction 监听 action 数据变化
import {useUserInfoStore} from "@/stores/userInfoStore";

const userInfoStore = useUserInfoStore()

const change = () => {
  userInfoStore.setName("张三丰")
}

// $reset 重置数据
const reset = () => {
  userInfoStore.$reset()
}

// $subscribe 监听数据变化
userInfoStore.$subscribe((mutation, state) =>{
  console.log(mutation, state)
})

// $onAction 监听 action 数据变化
userInfoStore.$onAction((action, state) =>{
  console.log(action, state)
})

Pinia持久化缓存

安装

npm install pinia-plugin-persistedstate

配置

import {createApp} from 'vue'
import {createPinia} from 'pinia'
import PiniaPluginPersistedstate from "pinia-plugin-persistedstate"

export const app = createApp(App)
// 配置Pinia并设置持久化缓存
const Pinia = createPinia()
Pinia.use(PiniaPluginPersistedstate)

app.use(Pinia)
app.mount('#app')

然后在需要设置持久化缓存的pinia文件中开启persist配置

import {defineStore} from 'pinia'

export const useUserInfoStore = defineStore('userInfo', {
  state: () => {
    return {
      name: "李斯特",
      age: 18
    }
  },
  getters: {
    userMsg() {
      return this.name + '---' + this.age
    }
  },
  actions: {
    setName(newName) {
      console.log(this.name)
      this.name = newName
    }
  },
  // 开启数据持久化
  persist: true
})

效果展示

它原理是将pinia数据保存到 localStorage 缓存中,刷新页面后优先从缓存中读取,如果缓存中没有则再从代码中读取

Echarts展示地图

效果图

image-20231019201753090

安装

npm install echarts

默认安装的是 5.x 版本

在这个版本中的引入方式必须是下面这种方法

import * as echarts from 'echarts'

源码

首先要下载好地图数据 china.js

下载地址:https://szx-bucket1.oss-cn-hangzhou.aliyuncs.com/picgo/china.js,下载到本地使用即可

地图实现源码

<template>
  <div class='h-full flex justify-center items-center'>
    <div id='mapDom' class='h-full w-full'>

    </div>
  </div>
</template>

<script setup>
import { onMounted } from 'vue'
import * as echarts from 'echarts'
import '../assets/china'
import { getCityPositionByName } from '@/assets/cityPostion'

// 模拟10条数据
let mockData = [
  { name: '北京', value: 500 },
  { name: '天津', value: 200 },
  { name: '河南', value: 300 },
  { name: '广西', value: 300 },
  { name: '广东', value: 300 },
  { name: '河北', value: 300 },
]

onMounted(() => {
  let data = mockData.map(i => {
    let cityPosition = getCityPositionByName(i.name).value
    return {
      name: i.name,
      value: cityPosition.concat(i.value),
    }
  })

  let initMap = echarts.init(document.querySelector('#mapDom'))
  initMap.setOption({
    backgroundColor: 'transparent', // 设置背景色透明
    // 必须设置
    tooltip: {
      show: false,
    },
    // 地图阴影配置
    geo: {
      map: 'china',
      // 这里必须定义,不然后面series里面不生效
      tooltip: {
        show: false,
      },
      label: {
        show: false,
      },
      zoom: 1.03,
      silent: true, // 不响应鼠标时间
      show: true,
      roam: false, // 地图缩放和平移
      aspectScale: 0.75, // scale 地图的长宽比
      itemStyle: {
        borderColor: '#0FA3F0',
        borderWidth: 1,
        areaColor: '#070f71',
        shadowColor: 'rgba(1,34,73,0.48)',
        shadowBlur: 10,
        shadowOffsetX: -10,
        shadowOffsetY: 10,
      },
      select: {
        disabled: true,
      },
      emphasis: {
        disabled: true,
        areaColor: '#00F1FF',
      },
      // 地图区域的多边形 图形样式 阴影效果
      // z值小的图形会被z值大的图形覆盖
      top: '10%',
      left: 'center',
      // 去除南海诸岛阴影 series map里面没有此属性
      regions: [{
        name: '南海诸岛',
        selected: false,
        emphasis: {
          disabled: true,
        },
        itemStyle: {
          areaColor: '#00000000',
          borderColor: '#00000000',
        },
      }],
      z: 1,
    },
    series: [
      // 地图配置
      {
        type: 'map',
        map: 'china',
        zoom: 1,
        tooltip: {
          show: false,
        },
        label: {
          show: true, // 显示省份名称
          color: '#ffffff',
          align: 'center',
        },
        top: '10%',
        left: 'center',
        aspectScale: 0.75,
        roam: false, // 地图缩放和平移
        itemStyle: {
          borderColor: '#3ad6ff', // 省分界线颜色  阴影效果的
          borderWidth: 1,
          areaColor: '#17348b',
          opacity: 1,
        },
        // 去除选中状态
        select: {
          disabled: true,
        },
        // 控制鼠标悬浮上去的效果
        emphasis: { // 聚焦后颜色
          disabled: false, // 开启高亮
          label: {
            align: 'center',
            color: '#ffffff',
          },
          itemStyle: {
            color: '#ffffff',
            areaColor: '#0075f4',// 阴影效果 鼠标移动上去的颜色
          },
        },
        z: 2,
        data: data,
      },
      {
        type: 'scatter',
        coordinateSystem: 'geo',
        symbol: 'pin',
        symbolSize: [50, 50],
        label: {
          show: true,
          color: '#fff',
          formatter(value) {
            return value.data.value[2]
          },
        },
        itemStyle: {
          color: '#e30707', //标志颜色
        },
        z: 2,
        data: data,
      },
    ],
  })
})
</script>

cityPostion.js 文件代码,这个文件主要是通过省份名称获取经纬度

const positionArr = [
  { name: '北京', value: ['116.3979471', '39.9081726'] },
  { name: '上海', value: ['121.4692688', '31.2381763'] },
  { name: '天津', value: ['117.2523808', '39.1038561'] },
  { name: '重庆', value: ['106.548425', '29.5549144'] },
  { name: '河北', value: ['114.4897766', '38.0451279'] },
  { name: '山西', value: ['112.5223053', '37.8357424'] },
  { name: '辽宁', value: ['123.4116821', '41.7966156'] },
  { name: '吉林', value: ['125.3154297', '43.8925629'] },
  { name: '黑龙江', value: ['126.6433411', '45.7414932'] },
  { name: '浙江', value: ['120.1592484', '30.265995'] },
  { name: '福建', value: ['119.2978134', '26.0785904'] },
  { name: '山东', value: ['117.0056', '36.6670723'] },
  { name: '河南', value: ['113.6500473', '34.7570343'] },
  { name: '湖北', value: ['114.2919388', '30.5675144'] },
  { name: '湖南', value: ['112.9812698', '28.2008247'] },
  { name: '广东', value: ['113.2614288', '23.1189117'] },
  { name: '海南', value: ['110.3465118', '20.0317936'] },
  { name: '四川', value: ['104.0817566', '30.6610565'] },
  { name: '贵州', value: ['106.7113724', '26.5768738'] },
  { name: '云南', value: ['102.704567', '25.0438442'] },
  { name: '江西', value: ['115.8999176', '28.6759911'] },
  { name: '陕西', value: ['108.949028', '34.2616844'] },
  { name: '青海', value: ['101.7874527', '36.6094475'] },
  { name: '甘肃', value: ['103.7500534', '36.0680389'] },
  { name: '广西', value: ['108.3117676', '22.8065434'] },
  { name: '新疆', value: ['87.6061172', '43.7909393'] },
  { name: '内蒙古', value: ['111.6632996', '40.8209419'] },
  { name: '西藏', value: ['91.1320496', '29.657589'] },
  { name: '宁夏', value: ['106.2719421', '38.4680099'] },
  { name: '台湾', value: ['120.9581316', '23.8516062'] },
  { name: '香港', value: ['114.139452', '22.391577'] },
  { name: '澳门', value: ['113.5678411', '22.167654'] },
  { name: '安徽', value: ['117.2757034', '31.8632545'] },
  { name: '江苏', value: ['118.7727814', '32.0476151'] },
]

export function getCityPositionByName(name) {
  return positionArr.find(item => item.name === name)
}

Vue-Router

安装

npm install vue-router

安装完成后检查一下安装的版本是否是 4.x 版本,确保在 vue3 中可以使用

image-20231022094941965

定义路由和404

新建 router/index.js

import {createRouter,createWebHashHistory} from "vue-router"

const router = createRouter({
    // 定义路由模式:哈希模式
    history:createWebHashHistory(),
    routes:[
        {
            path:"/",
            component:()=>import("../views/home.vue")
        },
        {
            path:"/about",
            component:()=>import("../views/about.vue")
        },
        // 匹配404页面,当所有路径都匹配不到时,就跳转到404
        {
            path: "/:pathMatch(.*)",
            component: ()=>import("../views/404.vue"),
        },
    ]
})

// 导出路由
export default router

注册路由

main.js

import { createApp } from 'vue'
import App from './App.vue'
import router from "./router"

const app = createApp(App)

app.use(router)

app.mount('#app')

定义路由出口

App.vue

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

image-20231022100705985

路由跳转

方式一:router-link

<router-link class="mr-10" to="/">home</router-link>
<router-link to="/about">about</router-link>

router-link是vue-router内置的组件,通过to属性定义要跳转的地址,属性值要和路由中的 path 相对应

方式二:通过js的方式跳转

定义两个按钮,点击按钮实现跳转

<button class="mr-10" @click="toPath('/')">home</button>
<button @click="toPath('/about')">about</button>

js方法

import {useRouter} from "vue-router"

const router = useRouter()

const toPath = (url) => {
  router.push({
    path:url
  })
}

控制路由返回与前进

定义两个按钮分别实现返回和前进

<button class="mr-10" @click="back()">返回</button>
<button class="mr-10" @click="advance()">前进</button>

实现两个方法

const back = () => {
  // 方式一
  // router.go(-1)

  // 方式二
  router.back()
}

const advance = () => {
  router.go(1)
}

replace

默认通过 push 的方式跳转会留下历史记录。如果不想留下历史记录,可以通过 replace 这种方法跳转。

例如在登录成功后就可以使用 replace 来跳转

在 router-link 标签上添加 replace 属性

<router-link replace class="mr-10" to="/">home</router-link>
<router-link replace class="mr-10" to="/about">about</router-link>

或者通过 router.replace

const toPath = (url) => {
  router.replace({
    path:url
  })
}

这种跳转方式不会留下历史记录

路由传参

通过添加 query 参数来实现传参

const toPath = (url) => {
  router.push({
    path:url,
    query:{
      id:1,
      name:"李四",
    }
  })
}

通过如下方法接收路由参数

<template>
  我是详情页,接收到的路由参数是:{{route.query}}
</template>

<script setup>
import {useRoute} from "vue-router";

const route = useRoute()

console.log(route.query)

</script>

image-20231022102319527

接收到到的是一个对象

动态URL

我们也可以将参数作为页面URL的一部分

首先定义路由

注意:

这里要多定义一个参数:name,动态路由跳转时,需要通过 name 来跳转

使用 /dyDetail/:xxx/:xxx 这种方式定义动态参数名称

{
    path:"/dyDetail/:id/:name",
    name:"DyDetail",
    component:()=>import("../views/dyDetail.vue")
},

添加跳转方法

const toDyDetail = () => {
  router.push({
    // 这里使用name来跳转,name名称也要和路由中定义的name一致
    name:"DyDetail",
    // 这里传递的属性名必须和路由中定义的参数名一致
    params:{
      id:"1",
      name:"张三"
    }
  })
}

获取动态路由参数方法,通过 route.params 方法获取

<template>
  <div>id:{{route.params.id}}</div>
  <div>name:{{route.params.name}}</div>
</template>

<script setup>
import {useRoute} from "vue-router";

const route = useRoute()

console.log(route.params)

</script>

image-20231022103152777

这里观察地址栏中的显示方式,直接将参数获取url的一部分来显示

路由嵌套

定义路由

{
    path:"/system",
    component:()=>import("../views/system/index.vue"),
    children:[
        {
            path:"menu",
            component:()=>import("../views/system/menu.vue")
        },
        {
            path:"role",
            component:()=>import("../views/system/role.vue")
        },
    ]
}

system/index.vue

<template>
  <div class="parent">
    <button @click="toPath('menu')">菜单管理</button>
    <button @click="toPath('role')">角色管理</button>
  </div>
  <router-view/>
</template>

<script setup>
import {useRouter} from "vue-router";

const router = useRouter()
const toPath = (url) => {
  router.push({
    path:`/system/${url}`
  })
}

</script>

<style scoped>
.parent{
  height: 45px;
  background-color: pink;
  display: flex;
  gap: 15px;
  align-items: center;
  justify-content: center;
}
</style>

跳转到子路由时,需要加上父路由地址

image-20231022104829861

重定向

{
    path:"/system",
    // 重定向到第一个子菜单
    redirect:"/system/menu",
    component:()=>import("../views/system/index.vue"),
    children:[
        {
            path:"menu",
            component:()=>import("../views/system/menu.vue")
        },
        {
            path:"role",
            component:()=>import("../views/system/role.vue")
        },
    ]
}

路由守卫

全局前置路由守卫

// 全局前置路由守卫
router.beforeResolve((to,from,next)=>{
    console.log(to) // 去哪个页面
    console.log(from) // 从哪个页面来
    next() // 下一步,必须要写,否则无法跳转
})

全局后置路由守卫

// 全局后置路由守卫
router.afterEach((to,from)=>{
    console.log(to) // 去哪个页面
    console.log(from) // 从哪个页面来
})

局部路由守卫

{
    path:"menu",
    component:()=>import("../views/system/menu.vue"),
    // 局部前置路由守卫
    beforeEnter:((to,from,next)=>{
        console.log(to,'局部前置路由守卫')
        console.log(from,'局部前置路由守卫')
        next()
    })
},

滚动行为

import {createRouter,createWebHashHistory} from "vue-router"

const router = createRouter({
    // 定义路由模式:哈希模式
    history:createWebHashHistory(),
    // 滚动模式
    scrollBehavior:(to,from,savedPosition)=>{
        if(savedPosition){
            // 如果有滚动的位置,则重新回到之前滚动的位置
            return savedPosition
        }else{
            // 否则页面滚动到顶部
            return {x:0,y:0}
        }
    },
    routes:[
        {
            path:"/",
            component:()=>import("../views/home.vue")
        },
        {
            path:"/about",
            component:()=>import("../views/about.vue")
        },
        {
            path:"/detail",
            component:()=>import("../views/detail.vue")
        },
    ]
})

// 导出路由
export default router

动态路由

在后台管理系统中常见的场景,根据不同的角色,显示不同的菜单

编写方法,根据不同的账号名,返回不同的菜单

export function getDynamicRouting(name){
    return new Promise((resolve,reject)=>{
        // root角色登录
        if(name === "admin"){
            resolve([
                {
                    path:"/about",
                    component:"about.vue"
                },
                {
                    path:"/detail",
                    component:"detail.vue"
                },
                {
                    path:"/system",
                    redirect:"/system/menu",
                    component:"system/index.vue",
                    children:[
                        {
                            path:"menu",
                            component:"system/menu.vue",
                        },
                        {
                            path:"role",
                            component:"system/role.vue"
                        },
                    ],
                },
            ])
        }
        // 普通人员登录
        if(name === "tome"){
            resolve([
                {
                    path:"/about",
                    component:"about.vue"
                },
                {
                    path:"/detail",
                    component:"detail.vue"
                },
            ])
        }
    })
}

login.vue

登录成功后根据返回的路由信息,添加路由

<template>
  <div>
    <input placeholder="请输入账号" v-model="name"/>
    <input placeholder="请输入密码" type="password" v-model="pwd"/>
    <button @click="login">登录</button>
  </div>
</template>

<script setup>
import {ref} from "vue";
import router from "../router"
import {getDynamicRouting} from "../../mock/mockRouter.js";

let name = ref("")
let pwd = ref("")

const login = () => {
  getDynamicRouting(name.value).then(routers=>{
     let dyRouter = setDyRouter(routers)
     // 只需要添加一级路由信息即可
     dyRouter.forEach(rootRouter=>{
       router.addRoute(rootRouter)
     })
  })
}

const setDyRouter = (routers,parentPath) => {
  routers.forEach(item=>{
    item.component = import(`../views/${item.component}`)
    if(!item.path.startsWith("/")){
      item.path = `${parentPath}/${item.path}`
    }
    if(item.children){
      setDyRouter(item.children,item.path)
    }
  })
  return routers
}
</script>

测试

首先用admin登录,然后点击菜单管理可以正常返回

image-20231022203258691

然后刷新页面,使用tome登录,然后点击菜单管理发现是404

image-20231022203424279

上面的例子只是简单的实现了一个动态路由,实际开发中,我们会根据接口返回的路由数据渲染不同的菜单来显示

MarkDown语法高亮

安装

npm install marked highlight.js --save
or
pnpm add marked highlight.js --save

注册

import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import highlight from 'highlight.js'
import "highlight.js/styles/atom-one-dark.css"

const app = createApp(App)

app.use(createPinia())
app.use(router)

app.directive("highlight",function(el){
  let blocks = el.querySelectorAll('pre code');
  blocks.forEach((block)=>{
    highlight.highlightBlock(block);
  })
})

app.mount('#app')

使用

<div v-highlight v-html='content'></div>

<script>
import { marked } from 'marked'
const content = ref("")
// 需要使用marked方法吧语法转成html页面
content = marked(content)
</script>

效果

image-20231028152820833

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1144254.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

分布式数据库Apache Doris简易体验

&#x1f4e2;&#x1f4e2;&#x1f4e2;&#x1f4e3;&#x1f4e3;&#x1f4e3; 哈喽&#xff01;大家好&#xff0c;我是【IT邦德】&#xff0c;江湖人称jeames007&#xff0c;10余年DBA及大数据工作经验 一位上进心十足的【大数据领域博主】&#xff01;&#x1f61c;&am…

公司电脑禁用U盘的方法

公司电脑禁用U盘的方法 安企神U盘管理系统下载使用 在这个复杂的数据时代&#xff0c;保护公司数据的安全性至关重要。其中&#xff0c;防止未经授权的数据泄露是其中的一个关键环节。U盘作为一种常用的数据传输工具&#xff0c;也成为了潜在的安全风险。因此&#xff0c;公司…

DOM节点学习

喜欢的东西太贵了&#xff0c;我一咬牙&#xff0c;狠下心决定不喜欢了&#xff01; 【文档节点--DOM有哪些节点】 仔细看下面文档的html标签的不同 1.li标签没换行 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"&…

【代码随想录】算法训练计划04

1、24. 两两交换链表中的节点 题目&#xff1a; 给你一个链表&#xff0c;两两交换其中相邻的节点&#xff0c;并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题&#xff08;即&#xff0c;只能进行节点交换&#xff09;。 思路&#xff1a; 链表这种题…

自己动手搭建一个传奇是什么体验?下面是我搭建的详细教程,大家跟着教程做,不光是学会了技术,平时还可以帮朋友搭建

传奇游戏是一代人的回忆&#xff0c;它曾经风靡一时&#xff0c;让无数玩家沉迷其中。这款游戏以其独特的玩法、丰富的故事背景和深刻的角色刻画&#xff0c;吸引了一大批忠实粉丝。 在传奇游戏中&#xff0c;玩家可以体验到各种不同的职业和角色&#xff0c;每个角色都有自己…

计算机毕业设计 基于SpringBoot高校竞赛管理系统的设计与实现 Javaweb项目 Java实战项目 前后端分离 文档报告 代码讲解 安装调试

&#x1f34a;作者&#xff1a;计算机编程-吉哥 &#x1f34a;简介&#xff1a;专业从事JavaWeb程序开发&#xff0c;微信小程序开发&#xff0c;定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事&#xff0c;生活就是快乐的。 &#x1f34a;心愿&#xff1a;点…

javascript数据类型

目录 原始数据类型 引用数据类型 类型检测 类型转换 总结 原始数据类型 JavaScript 中有六种原始数据类型&#xff0c;它们是&#xff1a; Undefined&#xff08;未定义&#xff09;: 表示一个未被赋值的变量。Null&#xff08;空值&#xff09;: 表示一个空对象指针。B…

jetson nano刷机更新Jetpack

只是记录个人在使用英伟达jetson Nano的经历,由于头一次尝试,所以特此记录需要的问题和经验。 一,英伟达刷机教程(jetson nano 版本) 本次我是直接刷机到TF卡,然后TF卡作为启动盘进行启动,我看网上有带EMMC版本的,好像可以直接把系统镜像安装到EMMC里面。但是有个问题…

【每日一题】2558. 从数量最多的堆取走礼物-2023.10.28

题目&#xff1a; 2558. 从数量最多的堆取走礼物 给你一个整数数组 gifts &#xff0c;表示各堆礼物的数量。每一秒&#xff0c;你需要执行以下操作&#xff1a; 选择礼物数量最多的那一堆。如果不止一堆都符合礼物数量最多&#xff0c;从中选择任一堆即可。选中的那一堆留下…

ssm164学院学生论坛的设计与实现+vue

项目名称&#xff1a;ssm164学院学生论坛的设计与实现vue 点击这里进入源码目录 声明&#xff1a; 适用范围&#xff1a; 本文档适用于广泛的学术和教育用途&#xff0c;包括但不限于个人学习、毕业设计和课程设计。免责声明&#xff1a; 特此声明&#xff0c;本文仅供参考学…

赴日IT培训 日本IT行业为啥吃香?

确实现在有许多小伙伴尝到了赴日IT的甜头&#xff0c;可是去日本从事IT行业真的很简单吗&#xff1f;为什么日本的IT行业这么缺人呢&#xff1f;那今天小编就跟大家聊一聊日本的IT行业。 咱们先来说说日本的IT行业为什么缺人&#xff1f;其实不只是IT行业&#xff0c;可以说日…

Azure云工作站上做Machine Learning模型开发 - 全流程演示

目录 本文内容先决条件从“笔记本”开始设置用于原型制作的新环境&#xff08;可选&#xff09;创建笔记本开发训练脚本迭代检查结果 关注TechLead&#xff0c;分享AI全维度知识。作者拥有10年互联网服务架构、AI产品研发经验、团队管理经验&#xff0c;同济本复旦硕&#xff0…

53. 寻宝(第七期模拟笔试)(最小生成树练习)

本题链接&#xff1a;卡码网KamaCoder 题目&#xff1a; 样例&#xff1a; 输入 7 11 1 2 1 1 3 1 1 5 2 2 6 1 2 4 2 2 3 2 3 4 1 4 5 1 5 6 2 5 7 1 6 7 1 输出 6 思路&#xff1a; 由题意&#xff0c;这里是需要遍历完全部的顶点&#xff0c;求遍历完全部点的花费最短距离…

java基础 特殊文件

1.Properties属性文件&#xff1a; 1.1使用Properties读取属性文件里的键值对数据&#xff1a; package specialFile;import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.util.Enumeration; import java.util.Propert…

【C++】C++入门(中)--引用

目录 一 引用概念 二 引用特性 三 常引用 四 引用使用场景 1 做参数 2. 做返回值 1 例一 2 例二 3 例三 4 例四 五 传值, 传引用效率比较 六 值和引用的作为返回值类型的性能比较 七 引用和指针的区别 一 引用概念 引用不是新定义一个变量&#xff0c;而是给已存…

视频智能视觉分析真的遥不可及吗?有没有那种下载就能用的视频分析服务?

我一直有一个感觉&#xff0c;就是市面上很难找到那么一个带视频算法的软件&#xff0c;能让我们很直观地看到视频分析的效果&#xff0c;大部分都要内置在某种算力硬件上&#xff0c;或者对GPU要求比较严格&#xff0c;很难做到像以前我们做的视频直播软件那样&#xff0c;下载…

Springboot+vue地方废品回收机构管理系统(有报告)。Javaee项目,springboot vue前后端分离项目。

演示视频&#xff1a; Springbootvue地方废品回收机构管理系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;springboot vue前后端分离项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&…

牛客网刷题-(7)

&#x1f308;write in front&#x1f308; &#x1f9f8;大家好&#xff0c;我是Aileen&#x1f9f8;.希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流. &#x1f194;本文由Aileen_0v0&#x1f9f8; 原创 CSDN首发&#x1f412; 如…

RK3568平台 sys虚拟文件系统添加节点

一.常见的linux文件系统 1. EXT2: EXT2是最早的Linux文件系统之一&#xff0c;它被广泛应用于Linux操作系统中。它支持大小为16TB的分区和最大文件大小为2TB。由于其简单性和高可靠性&#xff0c;在很长一段时间内仍被许多用户所选择。 2. EXT3: 2001年&#xff0c;Linux社区…

【100天精通Python】Day72:Python可视化_一文掌握Seaborn库的使用《二》_分类数据可视化,线性模型和参数拟合的可视化,示例+代码

目录 1. 分类数据的可视化 1.1 类别散点图&#xff08;Categorical Scatter Plot&#xff09; 1.2 类别分布图&#xff08;Categorical Distribution Plot&#xff09; 1.3 类别估计图&#xff08;Categorical Estimate Plot&#xff09; 1.4 类别单变量图&#xff08;Cat…