• vuejs怎样封装一个插件(以封装vue-toast为例扩展)


    插件介绍

    插件通常会为 Vue 添加全局功能。插件的范围没有限制——一般有下面几种:

    • 1.添加全局方法或者属性,如: vue-custom-element
    • 2.添加全局资源:指令/过滤器/过渡等,如 vue-touch
    • 3.通过全局 mixin 方法添加一些组件选项,如: vue-router
    • 4.添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现。
    • 5.一个库,提供自己的 API,同时提供上面提到的一个或多个功能,如 vue-router

    Vue.js 的插件应当有一个公开方法 install 。这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象:

    MyPlugin.install = function (Vue, options) {
      // 1. 添加全局方法或属性
      Vue.myGlobalMethod = function () {
        // 逻辑...
      }
    
      // 2. 添加全局资源
      Vue.directive('my-directive', {
        bind (el, binding, vnode, oldVnode) {
          // 逻辑...
        }
        ...
      })
    
      // 3. 注入组件
      Vue.mixin({
        created: function () {
          // 逻辑...
        }
        ...
      })
    
      // 4. 添加实例方法
      Vue.prototype.$myMethod = function (methodOptions) {
        // 逻辑...
      }
    }

    使用方式很简单,通过全局方法Vue.use()来使用插件。

    下面开发一个简单的在控制台打印消息插件,新建一个toast.js:

    var Toast = {};
    Toast.install = function (Vue, options) {
        Vue.prototype.$msg = 'Hello World';
    }
    export default Toast;

    在main.js中引入该toast.js并使用vue.use()安装该toast插件:

    import Vue from 'vue';
    import Toast from './toast.js';
    Vue.use(Toast);

    然后我们在任意vue页面就可以调用:

    export default {
        mounted(){
            console.log(this.$msg);         // Hello World
        }
    }

     开发vue-toast插件

    该插件实现的效果:

    • 1.提示内容可以显示在不同的位置(顶部、底部、中间),调用方式this.$toast.top('填写的提示内容')this.$toast.center('填写的提示内容')this.$toast.bottom('填写的提示内容')
    • 2.通过this.$toast('填写的提示内容')调用该插件,默认展示在底部;
    • 3.toast有默认的展示持续时间,也可配置toast展示的持续时间。

    toast.js实现代码如下:

    let Toast = {};
    Toast.install = function(Vue,options) {
      let opts = {
        type: 'bottom', // 显示位置
        duration: 2500  // 持续时间
      }
      for(let prop in options) {
        if(options.hasOwnProperty(prop)) {
          opts[prop] = options[prop];
        }
      }
      
      Vue.prototype.$toast = (tips,type) => {
        if(type) {
          opts.type = type;
        }
        if(document.getElementsByClassName('vue-toast').length) {
          return false;
        }
        let ToastEl = Vue.extend({
          template: `<div class="vue-toast vue-toast__${opts.type}">${tips}</div>`
        })
        let toastElement = new ToastEl().$mount().$el;
        document.body.append(toastElement);
        setTimeout(() => {
          document.body.removeChild(toastElement);
        },opts.duration);
      }
      
      ['top','center','bottom'].forEach((type) => {
        Vue.prototype.$toast[type] = (tips) => {
         return Vue.prototype.$toast(tips,type)
        }
      })
    }
    
    export default Toast;

     优化vue-toast

    调用方式更通用,如下所示:

    this.toast = this.$toast({
            message: '这是一段信息',
            position: 'top',
            duration: 0
          });

    可以手动关闭弹框

    this.toast.close();

    实现方式就是封装成一个构造函数,整体代码如下:

    function Toast() {
      this.toastTimer = false; // toastTimer:toast定时器
      this.toastVM = null; // toastVM:存储toast VM
      this.install = function(Vue,options) {
        let opts = {
          message: '', // 文本内容
          position: 'bottom', // 显示位置
          duration: 2500,  // 展示时长(ms),值为 0 时,toast 不会消失
          className: ''  // 自定义类名
        }
    
        /** toast提示方法
         * @params {Object|String} config 配置,如果为字符串=message
         * */
        Vue.prototype.$toast = (config) => {
          let option = {};
          let self = this;
          Object.assign(option,opts,options);
    
          if(typeof config === 'object') {
            Object.assign(option,config)
          } else { // 如果是字符串,传递的是message文本内容
            option.message = config;
          }
    
          if(this.toastTimer) { // 如果toast还在,则取消上次消失时间
            clearTimeout(this.toastTimer);
            this.toastVM.show = false;
          }
    
          if(!this.toastVM) {
            let ToastEl = Vue.extend({
              data() {
                return {
                  show: false,
                  message: option.message,
                  className: option.className
                }
              },
              methods: {
                // 关闭toast
                close() {
                  self.toastVM.show = false;
                }
              },
              render(createElement) {
                if(!this.show) {
                  return false;
                }
                return createElement(
                'div',
                {
                  class: ['lx-toast',`lx-toast-${option.position}`,this.className],
                  show: this.show,
                  domProps: {
                    innerHTML: this.message
                  }
                },
                )
              }
            })
            this.toastVM = new ToastEl();
            document.body.append(this.toastVM.$mount().$el);
          }
          this.toastVM.show = true;
          this.toastVM.message = option.message;
          this.toastVM.className = option.className;
          this.toastVM.position = option.position;
    
          if(option.duration != 0) {// 为0的时候一直显示
            this.toastTimer = setTimeout(() => {
                this.toastVM.show = this.toastTimer = false;
            },option.duration)
          }
          return this.toastVM;
        }
      }
    }
    
    export default new Toast();

    使用方式:

    <template>
      <div class="test">
        <button @click="open">开启弹框</button>
        <button @click="close">关闭弹框</button>
      </div>
    </template>
    
    <script>
    export default {
      data () {
        return {
          toast: null
        }
      },
    
      methods: {
        open() {
          this.toast = this.$toast({
            message: '这是一段信息',
            position: 'top',
            duration: 0
          });
        },
        close() {
          this.toast.close();
        }
      }
    }
    </script>

    添加loading方法

    完整代码:

    function Toast() {
      this.toastTimer = false; // toastTimer:toast定时器
      this.toastVM = null; // toastVM:存储toast VM
      this.showLoad = false;  // 存储loading显示状态
      this.loadNode = null; // 存储loading节点元素
      this.install = function(Vue,options) {
        let opts = {
          message: '', // 文本内容
          position: 'bottom', // 显示位置
          duration: 2500,  // 展示时长(ms),值为 0 时,toast 不会消失
          className: ''  // 自定义类名
        }
    
        /** toast提示方法
         * @params {Object|String} config 配置,如果为字符串=message
         * */
        Vue.prototype.$toast = (config) => {
          let option = {};
          let self = this;
          Object.assign(option,opts,options);
    
          if(typeof config === 'object') {
            Object.assign(option,config)
          } else { // 如果是字符串,传递的是message文本内容
            option.message = config;
          }
    
          if(this.toastTimer) { // 如果toast还在,则取消上次消失时间
            clearTimeout(this.toastTimer);
            this.toastVM.show = false;
          }
    
          if(!this.toastVM) {
            let ToastEl = Vue.extend({
              data() {
                return {
                  show: false,
                  message: option.message,
                  className: option.className
                }
              },
              methods: {
                // 关闭toast
                close() {
                  self.toastVM.show = false;
                }
              },
              render(createElement) {
                if(!this.show) {
                  return false;
                }
                return createElement(
                'div',
                {
                  class: ['lx-toast',`lx-toast-${option.position}`,this.className],
                  show: this.show,
                  domProps: {
                    innerHTML: this.message
                  }
                },
                )
              }
            })
            this.toastVM = new ToastEl();
            document.body.append(this.toastVM.$mount().$el);
          }
          this.toastVM.show = true;
          this.toastVM.message = option.message;
          this.toastVM.className = option.className;
          this.toastVM.position = option.position;
    
          if(option.duration != 0) {// 为0的时候一直显示
            this.toastTimer = setTimeout(() => {
                this.toastVM.show = this.toastTimer = false;
            },option.duration)
          }
          return this.toastVM;
        }
    
        /** $loading加载
         * @params {Object|String} config 配置,如果为字符串=message
         * */
        Vue.prototype.$loading = (config) => {
          let option = {};
          let self = this;
          if(typeof config === 'object') {
            Object.assign(option,config)
          } else { // 传递的字符串
            option.message = config;
          }
    
          if(option.type == 'close') {
            if(this.loadNode) {
               this.loadNode.show = this.showLoad = false;
            }
          } else {
            if(this.showLoad && this.loadNode) {
              this.loadNode.message = option.message;
              return false;
            }
            const loadEl = Vue.extend({
              data() {
                return {
                  show: false,
                  message: option.message
                }
              },
              methods: {
                close() {
                  self.loadNode.show = self.showLoad = false;
                }
              },
              render(h) {
                if(!this.show) {
                  return false;
                }
                return h('div',{
                  class: 'lx-load-mark',
                  show: this.show
                },[
                  h('div',{
                    class: 'lx-load-box'
                  },[
                    h('div',{
                      class: this.message ? 'lx-loading':'lx-loading-nocontent'
                    },Array.apply(null,{length: 12}).map((value,index) => {
                      return h('div',{
                        class: ['loading_leaf',`loading_leaf_${index}`]
                      })
                    })),
                    h('div',{
                      class: 'lx-load-content',
                      domProps: {
                        innerHTML: this.message
                      }
                    })
                  ])
                ])
              }
            })
    
            this.loadNode = new loadEl();
            document.body.appendChild(this.loadNode.$mount().$el);
            this.loadNode.show = this.showLoad = true;
          }
          return this.loadNode;
        }
    
        ['open','close'].forEach(type => {
          Vue.prototype.$loading[type] = (message) => {
            return Vue.prototype.$loading({type,message})
          }
        })
      }
    }
    
    export default new Toast();

    完整的调用方式:

    <template>
      <div class="test">
        <button @click="open">开启弹框</button>
        <button @click="close">关闭弹框</button>
      </div>
    </template>
    
    <script>
    export default {
      data () {
        return {
          toast: null
        }
      },
    
      mounted() {
        let loading = this.$loading({
          message: '这是loading文字'
        })
    
        setTimeout(() => { // 5s 后关闭弹框
          loading.close();
        },5000);
      },
    
      methods: {
        open() {
          this.toast = this.$toast({
            message: '这是一段信息',
            position: 'top',
            duration: 0
          });
        },
        close() {
          this.toast.close();
        }
      }
    }
    </script>

     toast.css代码:

    .lx-toast {
        position: fixed;
        bottom: 100px;
        left: 50%;
        -webkit-box-sizing: border-box;
                box-sizing: border-box;
        max-width: 80%;
        height: 40px;
        line-height: 20px;
        padding: 10px 20px;
        transform: translateX(-50%);
        -webkit-transform: translateX(-50%);
        text-align: center;
        z-index: 9999;
        font-size: 14px;
        color: #fff;
        border-radius: 5px;
        background: rgba(0, 0, 0, 0.7);
        animation: show-toast .5s;
        -webkit-animation: show-toast .5s;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
    
    .lx-toast.lx-word-wrap {
        width: 80%;
        white-space: inherit;
        height: auto;
    }
    
    .lx-toast.lx-toast-top {
        top: 50px;
        bottom: inherit;
    }
    
    .lx-toast.lx-toast-center {
        top: 50%;
        margin-top: -20px;
        bottom: inherit;
    }
    
    @-webkit-keyframes show-toast {
        from {
            opacity: 0;
        }
        to {
            opacity: 1;
        }
    }
    
    @keyframes show-toast {
        from {
            opacity: 0;
        }
        to {
            opacity: 1;
        }
    }
    
    .lx-load-mark {
        position: fixed;
        left: 0;
        top: 0;
        width: 100%;
        height: 100%;
        z-index: 9999;
    }
    
    .lx-load-box {
        position: fixed;
        z-index: 3;
        width: 7.6em;
        min-height: 7.6em;
        top: 180px;
        left: 50%;
        margin-left: -3.8em;
        background: rgba(0, 0, 0, 0.7);
        text-align: center;
        border-radius: 5px;
        color: #FFFFFF;
    }
    
    .lx-load-content {
        margin-top: 64%;
        font-size: 14px;
    }
    
    .lx-loading, .lx-loading-nocontent {
        position: absolute;
        width: 0px;
        left: 50%;
        top: 38%;
    }
    
    .lx-loading-nocontent {
        top: 50%;
    }
    
    .loading_leaf {
        position: absolute;
        top: -1px;
        opacity: 0.25;
    }
    
    .loading_leaf:before {
        content: " ";
        position: absolute;
        width: 9.14px;
        height: 3.08px;
        background: #d1d1d5;
        -webkit-box-shadow: rgba(0, 0, 0, 0.0980392) 0px 0px 1px;
                box-shadow: rgba(0, 0, 0, 0.0980392) 0px 0px 1px;
        border-radius: 1px;
        -webkit-transform-origin: left 50% 0px;
        transform-origin: left 50% 0px;
    }
    
    .loading_leaf_0 {
        -webkit-animation: opacity-0 1.25s linear infinite;
        animation: opacity-0 1.25s linear infinite;
    }
    
    .loading_leaf_0:before {
        -webkit-transform: rotate(0deg) translate(7.92px, 0px);
        transform: rotate(0deg) translate(7.92px, 0px);
    }
    
    .loading_leaf_1 {
        -webkit-animation: opacity-1 1.25s linear infinite;
        animation: opacity-1 1.25s linear infinite;
    }
    
    .loading_leaf_1:before {
        -webkit-transform: rotate(30deg) translate(7.92px, 0px);
        transform: rotate(30deg) translate(7.92px, 0px);
    }
    
    .loading_leaf_2 {
        -webkit-animation: opacity-2 1.25s linear infinite;
        animation: opacity-2 1.25s linear infinite;
    }
    
    .loading_leaf_2:before {
        -webkit-transform: rotate(60deg) translate(7.92px, 0px);
        transform: rotate(60deg) translate(7.92px, 0px);
    }
    
    .loading_leaf_3 {
        -webkit-animation: opacity-3 1.25s linear infinite;
        animation: opacity-3 1.25s linear infinite;
    }
    
    .loading_leaf_3:before {
        -webkit-transform: rotate(90deg) translate(7.92px, 0px);
        transform: rotate(90deg) translate(7.92px, 0px);
    }
    
    .loading_leaf_4 {
        -webkit-animation: opacity-4 1.25s linear infinite;
        animation: opacity-4 1.25s linear infinite;
    }
    
    .loading_leaf_4:before {
        -webkit-transform: rotate(120deg) translate(7.92px, 0px);
        transform: rotate(120deg) translate(7.92px, 0px);
    }
    
    .loading_leaf_5 {
        -webkit-animation: opacity-5 1.25s linear infinite;
        animation: opacity-5 1.25s linear infinite;
    }
    
    .loading_leaf_5:before {
        -webkit-transform: rotate(150deg) translate(7.92px, 0px);
        transform: rotate(150deg) translate(7.92px, 0px);
    }
    
    .loading_leaf_6 {
        -webkit-animation: opacity-6 1.25s linear infinite;
        animation: opacity-6 1.25s linear infinite;
    }
    
    .loading_leaf_6:before {
        -webkit-transform: rotate(180deg) translate(7.92px, 0px);
        transform: rotate(180deg) translate(7.92px, 0px);
    }
    
    .loading_leaf_7 {
        -webkit-animation: opacity-7 1.25s linear infinite;
        animation: opacity-7 1.25s linear infinite;
    }
    
    .loading_leaf_7:before {
        -webkit-transform: rotate(210deg) translate(7.92px, 0px);
        transform: rotate(210deg) translate(7.92px, 0px);
    }
    
    .loading_leaf_8 {
        -webkit-animation: opacity-8 1.25s linear infinite;
        animation: opacity-8 1.25s linear infinite;
    }
    
    .loading_leaf_8:before {
        -webkit-transform: rotate(240deg) translate(7.92px, 0px);
        transform: rotate(240deg) translate(7.92px, 0px);
    }
    
    .loading_leaf_9 {
        -webkit-animation: opacity-9 1.25s linear infinite;
        animation: opacity-9 1.25s linear infinite;
    }
    
    .loading_leaf_9:before {
        -webkit-transform: rotate(270deg) translate(7.92px, 0px);
        transform: rotate(270deg) translate(7.92px, 0px);
    }
    
    .loading_leaf_10 {
        -webkit-animation: opacity-10 1.25s linear infinite;
        animation: opacity-10 1.25s linear infinite;
    }
    
    .loading_leaf_10:before {
        -webkit-transform: rotate(300deg) translate(7.92px, 0px);
        transform: rotate(300deg) translate(7.92px, 0px);
    }
    
    .loading_leaf_11 {
        -webkit-animation: opacity-11 1.25s linear infinite;
        animation: opacity-11 1.25s linear infinite;
    }
    
    .loading_leaf_11:before {
        -webkit-transform: rotate(330deg) translate(7.92px, 0px);
        transform: rotate(330deg) translate(7.92px, 0px);
    }
    
    @-webkit-keyframes opacity-0 {
        0% {
            opacity: 0.25;
        }
        0.01% {
            opacity: 0.25;
        }
        0.02% {
            opacity: 1;
        }
        60.01% {
            opacity: 0.25;
        }
        100% {
            opacity: 0.25;
        }
    }
    
    @keyframes opacity-0 {
        0% {
            opacity: 0.25;
        }
        0.01% {
            opacity: 0.25;
        }
        0.02% {
            opacity: 1;
        }
        60.01% {
            opacity: 0.25;
        }
        100% {
            opacity: 0.25;
        }
    }
    
    @-webkit-keyframes opacity-1 {
        0% {
            opacity: 0.25;
        }
        8.34333% {
            opacity: 0.25;
        }
        8.35333% {
            opacity: 1;
        }
        68.3433% {
            opacity: 0.25;
        }
        100% {
            opacity: 0.25;
        }
    }
    
    @keyframes opacity-1 {
        0% {
            opacity: 0.25;
        }
        8.34333% {
            opacity: 0.25;
        }
        8.35333% {
            opacity: 1;
        }
        68.3433% {
            opacity: 0.25;
        }
        100% {
            opacity: 0.25;
        }
    }
    
    @-webkit-keyframes opacity-2 {
        0% {
            opacity: 0.25;
        }
        16.6767% {
            opacity: 0.25;
        }
        16.6867% {
            opacity: 1;
        }
        76.6767% {
            opacity: 0.25;
        }
        100% {
            opacity: 0.25;
        }
    }
    
    @keyframes opacity-2 {
        0% {
            opacity: 0.25;
        }
        16.6767% {
            opacity: 0.25;
        }
        16.6867% {
            opacity: 1;
        }
        76.6767% {
            opacity: 0.25;
        }
        100% {
            opacity: 0.25;
        }
    }
    
    @-webkit-keyframes opacity-3 {
        0% {
            opacity: 0.25;
        }
        25.01% {
            opacity: 0.25;
        }
        25.02% {
            opacity: 1;
        }
        85.01% {
            opacity: 0.25;
        }
        100% {
            opacity: 0.25;
        }
    }
    
    @keyframes opacity-3 {
        0% {
            opacity: 0.25;
        }
        25.01% {
            opacity: 0.25;
        }
        25.02% {
            opacity: 1;
        }
        85.01% {
            opacity: 0.25;
        }
        100% {
            opacity: 0.25;
        }
    }
    
    @-webkit-keyframes opacity-4 {
        0% {
            opacity: 0.25;
        }
        33.3433% {
            opacity: 0.25;
        }
        33.3533% {
            opacity: 1;
        }
        93.3433% {
            opacity: 0.25;
        }
        100% {
            opacity: 0.25;
        }
    }
    
    @keyframes opacity-4 {
        0% {
            opacity: 0.25;
        }
        33.3433% {
            opacity: 0.25;
        }
        33.3533% {
            opacity: 1;
        }
        93.3433% {
            opacity: 0.25;
        }
        100% {
            opacity: 0.25;
        }
    }
    
    @-webkit-keyframes opacity-5 {
        0% {
            opacity: 0.270958333333333;
        }
        41.6767% {
            opacity: 0.25;
        }
        41.6867% {
            opacity: 1;
        }
        1.67667% {
            opacity: 0.25;
        }
        100% {
            opacity: 0.270958333333333;
        }
    }
    
    @keyframes opacity-5 {
        0% {
            opacity: 0.270958333333333;
        }
        41.6767% {
            opacity: 0.25;
        }
        41.6867% {
            opacity: 1;
        }
        1.67667% {
            opacity: 0.25;
        }
        100% {
            opacity: 0.270958333333333;
        }
    }
    
    @-webkit-keyframes opacity-6 {
        0% {
            opacity: 0.375125;
        }
        50.01% {
            opacity: 0.25;
        }
        50.02% {
            opacity: 1;
        }
        10.01% {
            opacity: 0.25;
        }
        100% {
            opacity: 0.375125;
        }
    }
    
    @keyframes opacity-6 {
        0% {
            opacity: 0.375125;
        }
        50.01% {
            opacity: 0.25;
        }
        50.02% {
            opacity: 1;
        }
        10.01% {
            opacity: 0.25;
        }
        100% {
            opacity: 0.375125;
        }
    }
    
    @-webkit-keyframes opacity-7 {
        0% {
            opacity: 0.479291666666667;
        }
        58.3433% {
            opacity: 0.25;
        }
        58.3533% {
            opacity: 1;
        }
        18.3433% {
            opacity: 0.25;
        }
        100% {
            opacity: 0.479291666666667;
        }
    }
    
    @keyframes opacity-7 {
        0% {
            opacity: 0.479291666666667;
        }
        58.3433% {
            opacity: 0.25;
        }
        58.3533% {
            opacity: 1;
        }
        18.3433% {
            opacity: 0.25;
        }
        100% {
            opacity: 0.479291666666667;
        }
    }
    
    @-webkit-keyframes opacity-8 {
        0% {
            opacity: 0.583458333333333;
        }
        66.6767% {
            opacity: 0.25;
        }
        66.6867% {
            opacity: 1;
        }
        26.6767% {
            opacity: 0.25;
        }
        100% {
            opacity: 0.583458333333333;
        }
    }
    
    @keyframes opacity-8 {
        0% {
            opacity: 0.583458333333333;
        }
        66.6767% {
            opacity: 0.25;
        }
        66.6867% {
            opacity: 1;
        }
        26.6767% {
            opacity: 0.25;
        }
        100% {
            opacity: 0.583458333333333;
        }
    }
    
    @-webkit-keyframes opacity-9 {
        0% {
            opacity: 0.687625;
        }
        75.01% {
            opacity: 0.25;
        }
        75.02% {
            opacity: 1;
        }
        35.01% {
            opacity: 0.25;
        }
        100% {
            opacity: 0.687625;
        }
    }
    
    @keyframes opacity-9 {
        0% {
            opacity: 0.687625;
        }
        75.01% {
            opacity: 0.25;
        }
        75.02% {
            opacity: 1;
        }
        35.01% {
            opacity: 0.25;
        }
        100% {
            opacity: 0.687625;
        }
    }
    
    @-webkit-keyframes opacity-10 {
        0% {
            opacity: 0.791791666666667;
        }
        83.3433% {
            opacity: 0.25;
        }
        83.3533% {
            opacity: 1;
        }
        43.3433% {
            opacity: 0.25;
        }
        100% {
            opacity: 0.791791666666667;
        }
    }
    
    @keyframes opacity-10 {
        0% {
            opacity: 0.791791666666667;
        }
        83.3433% {
            opacity: 0.25;
        }
        83.3533% {
            opacity: 1;
        }
        43.3433% {
            opacity: 0.25;
        }
        100% {
            opacity: 0.791791666666667;
        }
    }
    
    @-webkit-keyframes opacity-11 {
        0% {
            opacity: 0.895958333333333;
        }
        91.6767% {
            opacity: 0.25;
        }
        91.6867% {
            opacity: 1;
        }
        51.6767% {
            opacity: 0.25;
        }
        100% {
            opacity: 0.895958333333333;
        }
    }
    
    @keyframes opacity-11 {
        0% {
            opacity: 0.895958333333333;
        }
        91.6767% {
            opacity: 0.25;
        }
        91.6867% {
            opacity: 1;
        }
        51.6767% {
            opacity: 0.25;
        }
        100% {
            opacity: 0.895958333333333;
        }
    }

    参考

  • 相关阅读:
    day3 集合
    进度条
    day3 文件操作 seek tell 修改
    day3 函数
    同学满分代码,购物车。
    day2杂---三元运算 is
    模块sys os
    day2--列表/元组/字符串/字典
    一、Git配置
    四、TestNG 批量执行脚本Runner.xml
  • 原文地址:https://www.cnblogs.com/moqiutao/p/14394973.html
Copyright © 2020-2023  润新知