vue3中setup使用方法和技巧

7526次阅读 648人点赞 作者: WuBin 发布时间: 2023-07-05 10:11:40
扫码到手机查看

ref和nextTick

在vue3中的setup里使用ref,需要先注册,再返回,并且使用.value去访问。

<template>
    <div>
        <progress ref="barRef" :width="progress"><progress>
    </div>
</template>


<script type="text/ecmascript-6">
// setup中使用计算 监听 ref属性必须先获取
import { computed, watch, ref, nextTick } from 'vue'
export default {
    name: 'player',
    components: {
        Progress,
    },
    setup() {
        // 1、获取的时候要先定义,如果获取的是组件或者dom那么ref(null)
        const barRef = ref(null);

        // 设置一个响应式的值,必须要返回到模板
        const progress = ref(100);
        
        // 3、监听某个值 执行barRef组件中的方法
        watch(fullScreen, async (newFullScreen) => {
            if (newFullScreen) {
                // 如果需要nextTick也必须要引用
                await nextTick();
                // barRef.value就可以访问到这个组件实例
                // 如果想要使用progress中的数值,必须访问progress.value
                barRef.value.setOffset(progress.value)
            }
        })


        // 2、要成功访问必须在setup中返回
        return {
            barRef,
            progress
        }
    }
}
</script>

当使用外部文件的时候,在外部文件中获取组件的ref,比如:

// 父组件中
<div class="slider-wrapper"
      ref="sliderWrapperRef"
>
  ...
</div>

<script>
  import useMiniSlider from './user-mini-slider'

  setup() {
    const { sliderWrapperRef } = useMiniSlider()

    return {
      sliderWrapperRef
    }
  }
</script>

// 在同级目录下创建user-mini-slider.js
import { ref } from 'vue'

export default function useMiniSlider() {
  const sliderWrapperRef = ref(null)  // 这里如果要使用组件的实例,必须使用sliderWrapperRef.value  // 比如使用组件中的方法sliderWrapperRef.value.xx()

  return {
    sliderWrapperRef
  }
}

setup中引用外部文件,并在外部文件中请求数据

import useSwiper from './useSwiper';
setup() {
    const {
        videos
    } = useSwiper();

    console.log(videos)

    return {
        videos
    }
}

// useSwiper.js
import { ref, onBeforeMount} from 'vue';
import { getData } from "@/api/get-data";

export default function useSwiper() {
    // 所有视频数据
    const videos = ref([]);

    onBeforeMount( async() => {
        // 注意!赋值一定要给value赋值!
        videos.value = await getData(1);
        console.log(videos)
    });

    return {
        videos
    }
}

在setup中以及外部js文件使用props,emits

比如我们基于bscroll封装一个scroll组件(真实可用):

<template>
  <!-- ref对象通常要加上Ref标识 -->
  <div ref="rootRef">
    <!-- 内容插槽 -->
    <slot></slot>
  </div>
</template>

<script type="text/ecmascript-6">
// https://better-scroll.github.io/docs/zh-CN/plugins/
import useScroll from './use-scroll'
import { ref } from 'vue'

export default {
  name: 'scroll',
  // scroll的props全都是BS配置项的属性
  props: {
    click: {
      type: Boolean,
      default: true
    },
    // 决定是否可以监听到向外派发的滚动事件 3就是只要滚动就派发
    probeType: {
      type: Number,
      default: 0
    }
  },
  // 在vue3中 定义自定义事件(向外派发),建议在组件对象中配置emits属性(代表自定义事件的名称)
  emits: ['scroll'],
  // setup(props)通过参数拿到props的值
  // setup第二个参数是一个对象,在对象中可以拿到emit这个方法,在钩子函数中当参数穿进去
  setup(props, { emit }) {
    const rootRef = ref(null)
    // 接收到返回的scroll实例,并将返回的实例再暴漏(return)出去,之后就可以通过scroll组件的实例,就可以访问到这个scroll变量了
    // 并通过这个变量拿到BS的实例了 通过在组件上添加ref拿到组件实例
    const scroll = useScroll(rootRef, props, emit)

    // 一定要return一个对象包含这个ref才会有效
    return {
      rootRef,
      scroll
    }
  }
}
</script>

use-scroll.js

import BScroll from '@better-scroll/core/'
// 自动探测更新插件https://better-scroll.github.io/docs/zh-CN/plugins/observe-dom.html
import ObserveDOM from '@better-scroll/observe-dom'

import { onMounted, onUnmounted, ref } from 'vue'

// 在引入后一定要use一下插件
BScroll.use(ObserveDOM)

// 切记传入的一定要是一个ref对象 因为ref对象是响应式的 会在mounted的阶段得到dom元素
// options传递进来的对bs的配置项 emit方法,外部传递进来获取向外派发的自定义事件名称
export default function useScroll(wrapperRef, options, emit) {
  const scroll = ref(null)

  onMounted(() => {
    // debugger 如果需要调试可以打一个debugger会中断当前进程 阻止后续代码执行
    // 当debugger执行的时候 内容层实际是没有的 会导致不能滚动
    // 当bs2.0有了一个新特性,当dom变化的时候会自动刷新高度
    // 同样的还有observe-image插件
    // scroll也是一个ref对象,所以他的值要存在value上,才会被数据响应
    const scrollVal = scroll.value = new BScroll(wrapperRef.value, {
      observeDOM: true,
      // 使用扩展运算符传入options
      ...options
    })
    // 当大于0的时候,监听scroll事件,拿到滚动的位置
    if (options.probeType > 0) {
      scrollVal.on('scroll', (pos) => {
        // 将位置信息向外派发出去
        emit('scroll', pos)
      })
    }
  })

  // 对scroll实例卸载逻辑
  onUnmounted(() => {
    scroll.value.destroy()
  })

  // 将scroll实例暴漏出去
  return scroll
}

在setup中,如果使用props,emits需要手工传入:

 setup(props, { emit }) {
  useScroll(props, emits)
  。。。
}

使用computed技巧,返回ref.value中的方法

比如我们使用上面的组件,需要在scroll组件的基础上,返回scrollRef.value中的方法:

<scroll ref="scrollRef"></scroll>

setup() {
    const scrollRef = ref(null);

    // 使用计算属性的技巧,返回scrollRef.value中的属性,这里返回的scroll是组件中的实例
    // 当不访问计算属性的时候不会执行内部逻辑 只有当访问的时候才会执行
    const scroll = computed(() => {
       return scrollRef.value.scroll
    })

    return {
        scrollRef,
        scroll
    }
}

切记不能这样写:

<scroll ref="scrollRef"></scroll>

setup() {
    const scrollRef = ref(null);


    return {
        scrollRef,
        /* 返回不能这样写:scroll:scrollRef.value.scroll
        因为开始定义的scrollRef是null,所以scrollRef.value也是null
        所以在调用的时候必须保证scroll是已经渲染的,如果没渲染就返回肯定不行
        */
        scroll:scrollRef.value.scroll 错的!!
    }
}

获取ref组件的DOM元素-$el

<transition-group
  ref="listRef"
  name="list"
  tag="ul"
>
  <li class="item">...</li>
</transition-group>

setup() {
  const listRef = ref(null)
  const index = 10;

  // 获取ul中的所有li
  const target2 = listRef.value.$el.children;

  // 获取ul中的某一个li
  const target1 = listRef.value.$el.children[index]

  return {
    listRef
  }
}

setup中使用钩子

mounted,onUnmounted,computed

import { onMounted, onUnmounted, computed } from 'vue'

onMounted(() => {
    ...
})

onUnmounted(() => {
    ...
})

const xx = computed(() => {
  return 'xxxx'
});
使用计算属性中的值的时候,要用xx.value
因为setup是围绕beforeCreatecreated生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在setup函数中编写。

生命周期函数有:onBeforeMountonMountedonBeforeUpdateonUpdatedonBeforeUnmountonUnmountedonErrorCapturedonRenderTrackedonRenderTriggeredonActivatedonDeactivated

reactive定义响应式的data数据

import { reactive } from 'vue'
export default {
    setup() {
        const  info = reactive ({
            name:'张三'age:18
        }),
    }
    return { info};
}

在setup函数中使用数据也无需this.xxx,而是用你定义的名字,我定义的是info(也可也换成其他)则用info.name

  • reactive是 Vue3 中提供的实现响应式数据的方法。
  • reactive 参数必须是对象 (json / arr)
  • 如果给 reactive 传递了其它对象
    • 默认情况下,修改对象无法实现界面的数据绑定更新。
    • 如果需要更新,需要进行重新赋值。(即不允许直接操作数据,需要放个新的数据来替代原数据)

reactive不会实现响应式数据(渲染界面数据也变化)

如果在外部js文件或者setup中访问并使用该reactive数据时候,直接访问就行,比如:

const  info = reactive ({
            name:'张三'age:18
}),

function demo() {
  console.log( info.name ) 
}

return { info, demo };

如果需要监听reactive值的变化:

const state = reactive({
  count: 0,
});
 
watch(
  () => state.count,
  (newVal, oldVal) => {
    console.log(`count 从 ${oldVal} 变为 ${newVal}`);
  }
);

或者使用deep:

const state = reactive({
  count: 0,
  age:37
});

watch(
   () => state,
  (nv, ov) => {  ...   },
  { deep: true }
)

如果要修改reactive的值,可以单个值式的修改,但是不能整体赋值,那样会失去响应性。

 import {reactive} from 'vue';
let obj = reactive({
         id:1,
         name:'张三',
         age:18
})
// 可以单个值的修改
obj.id = 2;
obj.name = '李四';

但是不能直接赋值,这样会失去响应性(这样不行)

obj = {id:2,name:'李四'}

所以如果涉及到多个属性值的修改,应该使用Object.assign

obj = Object.assign(obj, {id:2,name:'李四'})

另附一些reactive修改的其他方法:

清空reactive定义的数组

arr.length = 0

修改reactive定义的数组

// 先清空数组再赋值,防止arr中的数据遗留
arr.length = 0 ;
Object.assign(arr,newArr)

清空reactive定义的对象

// 1、将obj中所有属性值变为null
Object.keys(obj).forEach(key => {
  obj[key] = null
})
 
// 2、将obj变为空对象
Object.keys(obj).forEach(key => {
  delete obj[key]
})

修改reactive定义的对象

// 1、如果obj和newObj的键值对不一致  先清空obj再赋值
Object.keys(obj).forEach(key => {
  delete obj[key]
})
Object.assign(obj,newObj);
 
// 2、如果一致
Object.assign(obj,newObj);

watch

import { watch } from 'vue'

const sliderShow = computed(() => {
  // !!双非转换成布尔类型
  return !!xx.value
})

onMounted(() => {

    watch(sliderShow, (newSliderShow, old) => {
        ...
    })
    
})

如果发现watch的参数,第一次不执行,只有第二次才执行的时候,需要为watch()方法添加额外的配置项。

比如我监听路由中的参数变化:

import { watch, ref } from 'vue';
import { useRoute } from 'vue-router';

export default function useTplget() {
    const route = useRoute();
    const template = ref(null);

    // 监听
    watch(
        () => route.params.id,
        (newid) => {
            console.log(newid)
            if (!newid) {
                return;
            }

        },
        // 页面加载时自动执行一次
        {
            immediate:true,
            deep:true // 开启深度监听 监听对象的变化等 一个值的时候不需要
        }
    );

    return template;
}

vuex和router

在setup中使用vuex是比较繁琐,但是常用的方式也就那么几个,首先我们来构建一个vuex的store

store文件夹中包含:

  • action.js
  • getters.js
  • index.js
  • mutation.js
  • state.js

action.js 这里记录一些行为函数

// 定义动作 对一个操作 可以操作多个mutations的一个封装


// 定义选择播放的动作 action第一个参数是一个对象可以直接结构 第二个参数是他的参数
// { list, index } 传入一个当前的列表 和 点击的索引
// commit是提交的意思 提交mutation中定义的函数
export function selectPlay({ commit, state }, { list, index }) {
  // 点击的时候顺序播放
  commit('setPlayMode', PLAY_MODE.sequence)
  commit('setSequenceList', list)
  // 当点击播放的时候,那么播放状态一定是true
  commit('setPlayingState', true)
  // 播放的时候会全屏播放
  commit('setFullScreen', true)
  // 当不随机播放的时候,播放列表和顺序播放列表相同
  commit('setPlaylist', list)
  // 设置播放索引
  commit('setCurrentIndex', index)
}

// 实现随机播放 这里的参数就不需要一个对象 直接一个参数即可
// 洗牌算法https://rosettacode.org/wiki/Knuth_shuffle
export function randomPlay ({ commit }, list) {
  // 点击的时候顺序播放
  commit('setPlayMode', PLAY_MODE.random)
  commit('setSequenceList', list)
  // 当点击播放的时候,那么播放状态一定是true
  commit('setPlayingState', true)
  // 播放的时候会全屏播放
  commit('setFullScreen', true)
  // 当不随机播放的时候,播放列表和顺序播放列表相同
  commit('setPlaylist', list)
  // 设置播放索引,洗牌后播放第一首歌
  commit('setCurrentIndex', 0)
}

// 封装切换播放模式的方法 getters也可以被解构
export function changeMode({ commit, state, getters }, mode) {
  
  // 从而保证这个新列表中 也是播放当前正在播放的歌曲, 就不会更改当前播放的这首歌了
  commit('setCurrentIndex', index)
  commit('setPlayMode', mode)
}

getters.js

// 定义一些获取数据的方法
export const currentSong = (state) => {
  // 这里如果获取不到值 会返回undefined会引起一些报错,所以改为无值返回一个空对象
  return state.playlist[state.currentIndex] || {}
}

mutations.js

// 提交数据 对state数据做修改

const mutations = {
  // 修改播放状态 第一个参数是state, 第二个参数是要修改为的值
  setPlayingState(state, playing) {
    state.playing = playing
  },
  // 设置顺序(原始)播放列表
  setSequenceList(state, list) {
    state.sequenceList = list
  },
  // 设置播放列表 当有随机播放情况的时候,playlist和sequencelist就会不同
  setPlaylist(state, list) {
    state.playlist = list
  },
  // 设置播放模式
  setPlayMode(state, mode) {
    state.playMode = mode
  },
  // 设置当前播放索引
  setCurrentIndex(state, index) {
    state.currentIndex = index
  },
  // 设置播放器全屏或者折叠模式
  setFullScreen(state, fullScreen) {
    state.fullScreen = fullScreen
  },
  setFavoriteList(state, list) {
    state.favoriteList = list
  },
  // 给song添加歌词的属性
  addSongLyric(state, { song, lyric }) {
    // 使用这种方式修改sequenceList
    // 这样是保留对象引用 一旦修改playlist也会发生变化 因为他们都指向了同一个对象
    state.sequenceList.map((item) => {
      // 相等证明是同一首歌
      if (item.mid === song.mid) {
        item.lyric = lyric
      }
      return item
    })
  }
}

export default mutationssstat

state.js

// 定义全局的基础数据 定义播放器的基本状态
// vuex就是一个内存级别的存储,刷新的时候就会重置
// 而本地存储就会永久存于浏览器中

import { PLAY_MODE, FAVORITE_KEY } from '@/assets/js/constant'

const state = {
  // 原始播放列表 歌曲列表
  sequenceList: [],
  // 真正的播放列表,因为有可能歌曲是顺序 逆序 倒序
  playlist: [],
  // 是否正在播放
  playing: false,
  // 播放模式 定义到常量文件中
  playMode: PLAY_MODE.sequence,
  // 当前播放的歌曲
  currentIndex: 0,
  // 播放器的状态 全屏还是收缩
  fullScreen: false,
  // 歌曲收藏列表 将歌曲添加到收藏就是添加到favoriteList
  // 取消收藏就是将歌曲从favoriteList中移除 维护一个已收藏的列表
  // 初始数据不写死 从缓存中获取
  favoriteList: []
}

export default state

index.js

// createLogger 开发环境下可以查看状态
import { createStore, createLogger } from 'vuex'
import state from './state'
import mutations from './mutations'
// 引入文件中所有的方法
import * as getters from './getters'
import * as actions from './actions'

const debug = process.env.NODE_ENV !== 'production'

export default createStore({
  state: state,
  mutations: mutations,
  // 可以同下方这样写 也可以与上面一样写
  getters,
  actions,
  // vuex提供了严格模式,帮忙检测state修改是否是在提交mutation的时候,如果不是就会报错,
//开启时候会深度watch所以线上模式就关闭这个模式
  strict: debug,
  // 插件,如果线上模式就不用插件 线下模式使用插件
  plugins: debug ? [createLogger()] : []
})

在setup中使用

import { useStore } from 'vuex'

setup() {
    const store = useStore()
}

在外部文件中引用

比如有user-mini-slider.js,我们先在father.vue文件中

import useMiniSlider from './user-mini-slider'

setup() {
    const { sliderWrapperRef } = useMiniSlider()
    return{
      sliderWrapperRef
    }
}

在外部的js文件中:

import { ref,  computed } from 'vue'
import { useStore } from 'vuex'

export default function useMiniSlider() {
  const store = useStore()
  // 从state中获取数据 注意获取的数据要为响应式的,需要使用计算属性
  const fullScreen = computed(() => store.state.fullScreen)

  // 修改state中的属性的值,执行的是mutations.js文件中注册的方法
  store.commit('setCurrentIndex', 99)
}

获取getters中的数据

const currentSong = computed(() => store.getters.currentSong)

比如currentSong是一个对象,那么要获取这个对象中的id属性的值,需要:

currentSong.value.id
注意 只要是用computed或者ref之类得到的都是proxy,那么要获取它的真实值,必须使用.value

使用action

在action.js中添加一个方法

export function removeSong ({ commit, state }, song) {
  // 从playlist和seqlist中找到并删除
  // 这里必须使用.slice()创建一个副本获取
  const sequenceList = state.sequenceList.slice()
  const playlist = state.playlist.slice()
  // 找出song在两个列表中的索引
  const sequenceIndex = findIndex(sequenceList, song)
  const playIndex = findIndex(playlist, song)

  sequenceList.splice(sequenceIndex, 1)
  playlist.splice(playIndex, 1)

  let currentIndex = state.currentIndex

  commit('setSequenceList', sequenceList)
  commit('setPlaylist', playlist)
  commit('setCurrentIndex', currentIndex)
}

function findIndex (list, song) {
  return list.findIndex((item) => {
    return item.id === song.id
  })
}

注意,如果从store中获取数组,然后直接对数组操作会报错!

const sequenceList = state.sequenceList

这样是错误的!,必须创建一个副本,然后对副本进行增加、删除操作,然后再commit提交到store中

const sequenceList = state.sequenceList.slice() // 正确

在vue文件的setup中使用:

 <span class="delete" @click.stop="removeSong(song)">
      <i class="icon-delete"></i>
 </span>

import { useStore } from 'vuex'
setup() {
// 执行action中的函数
function removeSong (song) {
     store.dispatch('removeSong', song)
}

return { removeSong }
}

watch组件中props中的值的变化

比如一个组件中,props的值是这样:

props: {
    query: String,
    showSinger: {
      type: Boolean,
      default: true
    }
}

当我们要监听props里面的query的变化的时候,我们不能这样写:

 watch(props.query, (newQuery) => {
     ... 这样写不对,
})

watch除了可以watch一个响应式变量,也可以watch一个getter函数,要像这样写:

watch(() => props.query, (newQuery) => {
     这样才对,watch一个getter函数
})

使用router

首先在setup中引入useRouter,然后在使用:

import { useRouter } from 'vue-router'

setup(props, { emit }) {
    const router = useRouter();

    function selectSinger (singer) {
        router.push({
          path: `/search/${singer.mid}`
        })
    }

    return {
        selectSinger
    }
}

其语法与this.$router.push等基本一致。

如果要获取路由中的参数,就要使用route了。

import { useRouter, useRoute } from 'vue-router'

export default {
  setup() {
    const router = useRouter()
    const route = useRoute()

    function pushWithQuery(query) {
      router.push({
        name: 'search',
        query: {
          ...route.query,
          ...query,
        },
      })
    }
  },
}

route 对象是一个响应式对象,所以它的任何属性都可以被监听,但你应该避免监听整个 route 对象。在大多数情况下,你应该直接监听你期望改变的参数。

import { useRoute } from 'vue-router'
import { ref, watch } from 'vue'

export default {
  setup() {
    const route = useRoute()
    const userData = ref()

    // 当参数更改时获取用户信息
    watch(
      () => route.params.id,
      async newId => {
        userData.value = await fetchUser(newId)
      }
    )
  },
}

问题快速定位

getters

store.getters数据不更新

Vuex 4+中的getters默认是非响应式的,这意味着它们不会跟踪依赖的变化。如果getter依赖于state的某个属性,那么当这个属性变化时,getter的结果不会自动更新。

如果你在组件中通过store.getters.xx来调用getter,但没有正确地使用Vue的响应式系统,那么可能会遇到不更新的问题。比如

模板:

<ul class="layer-ul">
    <li v-for="(layer, lindex) in layersData"
        :key="lindex"
    >
       ....
    </li>
</ul>

错误写法

import {useStore} from 'vuex';

const store = useStore();
const layerData = store.getters.layers; // 错误!这样不是响应式

return {
    layerData
}

正确写法

import {useStore} from 'vuex';
import { computed } from 'vue';

const store = useStore();
const layersData = computed(() => store.getters.layersData);

return {
    layerData
}

外部js文件提交mutations

比如mutations.js中有如下方法

const mutations = {
    setLayers(state, layers) {
      state.layers = layers;
    },
}

在外部JS文件中提交

const mutations = {
    setLayers(state, layers) {
      state.layers = layers;
    },
}

// 提交
import {useStore} from 'vuex';
store = useStore()
store.commit('setLayers', 数据);
点赞 支持一下 觉得不错?客官您就稍微鼓励一下吧!
关键词:setup,ref,nextTick
推荐阅读
  • uniapp实现被浏览器唤起的功能

    当用户打开h5链接时候,点击打开app若用户在已经安装过app的情况下直接打开app,若未安装过跳到应用市场下载安装这个功能在实现上主要分为两种场景,从普通浏览器唤醒以及从微信唤醒。

    9603次阅读 623人点赞 发布时间: 2022-12-14 16:34:53 立即查看
  • Vue

    盘点Vue2和Vue3的10种组件通信方式

    Vue中组件通信方式有很多,其中Vue2和Vue3实现起来也会有很多差异;本文将通过选项式API组合式API以及setup三种不同实现方式全面介绍Vue2和Vue3的组件通信方式。

    4297次阅读 317人点赞 发布时间: 2022-08-19 09:40:16 立即查看
  • JS

    几个高级前端常用的API

    推荐4个前端开发中常用的高端API,分别是MutationObserver、IntersectionObserver、getComputedstyle、getBoundingClientRect、requ...

    14452次阅读 948人点赞 发布时间: 2021-11-11 09:39:54 立即查看
  • PHP

    【正则】一些常用的正则表达式总结

    在日常开发中,正则表达式是非常有用的,正则表达式在每个语言中都是可以使用的,他就跟JSON一样,是通用的。了解一些常用的正则表达式,能大大提高你的工作效率。

    13497次阅读 491人点赞 发布时间: 2021-10-09 15:58:58 立即查看
  • 【中文】免费可商用字体下载与考证

    65款免费、可商用、无任何限制中文字体打包下载,这些字体都是经过长期验证,经得住市场考验的,让您规避被无良厂商起诉的风险。

    12015次阅读 963人点赞 发布时间: 2021-07-05 15:28:45 立即查看
  • Vue

    Vue3开发一个v-loading的自定义指令

    在vue3中实现一个自定义的指令,有助于我们简化开发,简化复用,通过一个指令的调用即可实现一些可高度复用的交互。

    16376次阅读 1307人点赞 发布时间: 2021-07-02 15:58:35 立即查看
  • JS

    关于手机上滚动穿透问题的解决

    当页面出现浮层的时候,滑动浮层的内容,正常情况下预期应该是浮层下边的内容不会滚动;然而事实并非如此。在PC上使用css即可解决,但是在手机端,情况就变的比较复杂,就需要禁止触摸事件才可以。

    15187次阅读 1234人点赞 发布时间: 2021-05-31 09:25:50 立即查看
  • Vue

    Vue+html2canvas截图空白的问题

    在使用vue做信网单页专题时,有海报生成的功能,这里推荐2个插件:一个是html2canvas,构造好DOM然后转canvas进行截图;另外使用vue-canvas-poster(这个截止到2021年3月...

    29853次阅读 2347人点赞 发布时间: 2021-03-02 09:04:51 立即查看
  • Vue

    vue-router4过度动画无效解决方案

    在初次使用vue3+vue-router4时候,先后遇到了过度动画transition进入和退出分别无效的情况,搜遍百度没没找到合适解决方法,包括vue-route4有一些API都进行了变化,以前的一些操...

    25911次阅读 1994人点赞 发布时间: 2021-02-23 13:37:20 立即查看
交流 收藏 目录