Skip to content
本页目录

教你用Vue3实现移动端Dialog

背景

公司最近在做组件库,本菜鸡被分配到dialog组件,接到任务,本来想使用cv大法“借鉴”一下市面上的dialog组件,奈何ui同学脑洞太大,要往dialog里塞一些奇怪的东西,像这样

对话框 (2).png

这样

对话框.png

还有这样...

对话框 (1).png

还是自己写一个叭

开始

废话不多说,直接上代码,核心思想就是通过传入不同的props值来给组件不同的样式

本文引用了两个基础组件,Overlay和Icon,代码就不贴出来了,这些部分可以自行实现

template部分,页面布局分为3个部分:头部header、中间部分content、按钮区域button

vue
<template>
  <Overlay :show="visible">
    <div class="my-dialog-wrapper">
      <transition name="my-dialog-bounce">
        <div :class="classes">
          <Icon
            @click="handleAction('close')"
            v-if="showClose"
            name="extra"
            class="my-icon my-icon-quxiao my-dialog-box-close"
          />
          <div v-if="imageUrl" class="my-dialog-box-img">
            <img :src="imageUrl" alt="" />
          </div>
          <div v-if="avatar" class="my-dialog-box-avatar">
            <img :src="avatar" alt="" />
          </div>
          <div class="my-dialog-box-title">{{ title }}</div>
          <div class="my-dialog-box-content">{{ message }}</div>
          <div class="my-dialog-box-tags">
            <div
              v-for="(item, index) in tags"
              :key="`tag${index}`"
              class="my-dialog-box-tags-item"
            >
              {{ item }}
            </div>
          </div>
          <slot name="content"></slot>
          <div :class="buttonClasses">
            <div
              v-if="showCancelButton"
              @click="handleAction('cancel')"
              :class="cancelBtnCls"
            >
              {{ cancelText }}
            </div>
            <div
              v-if="showConfirmButton"
              @click="handleAction('confirm')"
              :class="confirmBtnCls"
            >
              {{ confirmText }}
            </div>
          </div>
        </div>
      </transition>
    </div>
  </Overlay>
</template>

js部分

支持使用v-model控制dialog隐藏和显示,传入props modelValue 具体可以参考vue3官方文档

vue
<script>
import Overlay from "../overlay";
import Icon from "../icon";
import {
  defineComponent,
  computed,
  ref,
  watch,
} from "vue";
const prefixCls = "my-dialog";
export default defineComponent({
  name: "my-dialog",
  inheritAttrs: false,
  props: {
    avatar: {
      type: String,
      default: "",
    },
    imageUrl: {
      type: String,
      default: "",
    },
    theme: {
      type: String,
      default: "",
    },
    showConfirmButton: {
      type: Boolean,
      default: true,
    },
    showCancelButton: {
      type: Boolean,
      default: false,
    },
    buttonSize: {
      type: String,
      default: "normal",
    },
    confirmText: {
      type: String,
      default: "确认",
    },
    cancelText: {
      type: String,
      default: "取消",
    },
    message: {
      type: String,
      default: "",
    },
    tags: {
      type: Array,
      default: () => [],
    },
    showClose: {
      type: Boolean,
      default: false,
    },
    modelValue: {
      type: null,
      default: false,
    },
    title: {
      type: String,
      default: "",
    },
  },
  components: {
    Overlay,
    Icon
  },
  emits: ["update:modelValue", "action"],
  setup(props, { emit }) {
    let visible = ref(!!props.modelValue);

    const classes = computed(() => {
      return [
        `${prefixCls}-box`,
        props.showClose ? `${prefixCls}-box-with-close` : "",
        props.imageUrl ? `${prefixCls}-box-no-image` : "",
        (props.buttonSize === "large" && props.showCancelButton) || !props.theme
          ? `${prefixCls}-box-no-padding-botm`
          : "",
      ];
    });

    const buttonClasses = computed(() => {
      return props.theme === "round"
        ? [
            `${prefixCls}-box-btns`,
            props.buttonSize === "large" ? `${prefixCls}-box-btns-large` : "",
          ]
        : [
            `${prefixCls}-box-btns-line`,
            props.buttonSize === "large" ? `${prefixCls}-box-btns-large` : "",
          ];
    });
    const confirmBtnCls = computed(() => {
      return props.theme === "round"
        ? [
            `${prefixCls}-box-btns-normal`,
            `${prefixCls}-box-btns-normal-confirm`,
            props.buttonSize === "large"
              ? `${prefixCls}-box-btns-large-confirm`
              : "",
          ]
        : ["line-button", `${prefixCls}-box-btns-line-confirm`];
    });

    const cancelBtnCls = computed(() => {
      return props.theme === "round"
        ? [
            `${prefixCls}-box-btns-normal`,
            `${prefixCls}-box-btns-normal-cancel`,
            props.buttonSize === "large"
              ? `${prefixCls}-box-btns-large-cancel`
              : "",
          ]
        : ["line-button", `${prefixCls}-box-btns-line-cancel`];
    });
    
    const closeDialog = () => {
      if(!visible) return;
      visible.value = false;
      emit("update:modelValue", !props.modelValue);
    };

    const handleAction = (action) => {
      emit('action',action);
      closeDialog();
    };

    //监听v-model
    watch(()=> props.modelValue,(val)=>{
      visible.value = val
    })

    return {
      visible,
      classes,
      buttonClasses,
      cancelBtnCls,
      confirmBtnCls,
      closeDialog,
      handleAction,
    };
  },
});
</script>

css部分就不说了,大家可以根据自己的需求去自定义css

使用

到这里,可以引入组件使用,传入我们定义的props

vue
<my-dialog
    :title="我的第一篇掘金文章"
    :buttonSize="large"
    :message="大家多多指教"
    :theme="round"
    v-model="dialogShow"
    showClose
    showCancelButton
  >

image.png

函数调用

我们平时调用dialog的方式是不是像这样,在vue3中,这又该如何实现呢?

js
this.$dialog
    .confirm({
      title: this.title,
      message: this.content,
      theme: "round",
    })
    .catch(() => {
      console.log("取消");
    });

基于上面已经实现的组件,我们可以通过动态挂载component的方式,在调用this.$dialog的时候,在页面上创建一个div元素,并将我们的组件挂载上去

Vue.extend在Vue3中已经不再支持了,而且已经没有构造器这个概念,我们使用官方提供的render函数来实现

js
import DialogConstructor from "./Dialog.vue";
import { h, render } from "vue";

const dialogInstance = new Map();

const initInstance = (props, container) => {
  // 生成vnode
  const vnode = h(DialogConstructor, props);
  render(vnode, container);
  //挂载
  document.body.appendChild(container);

  //返回组件实例
  return vnode.component;
};

const getContainer = () => {
  return document.createElement("div");
};

const showDialog = (options) => {
  
  options.onAction = (action) => {
    const currentDialog = dialogInstance.get(vm);
    let resolve = action;

    if (options.callback) {
      options.callback(resolve, instance.proxy);
    } else {
      if (action === "cancel" || action === "close") {
        currentDialog.reject("cancel");
      } else {
        currentDialog.resolve(resolve);
      }
    }
  };
  const container = getContainer(); // 获取组件容器

  const instance = initInstance(options, container);

  const vm = instance.proxy;

  for (const prop in options) {
    if (options.hasOwnProperty(prop) && !vm.$props.hasOwnProperty(prop)) {
      vm[prop] = options[prop];
    }
  }
  // 显示dialog
  vm.visible = true;

  return vm;
};
function Dialog(options) {
  let callback;
  if (typeof options === "string") {
    options = {
      message: options,
    };
  } else {
    callback = options.callback;
  }
  return new Promise((resolve, reject) => {
    const vm = showDialog(options);

    dialogInstance.set(vm, {
      options,
      callback,
      resolve,
      reject,
    });
  });
}

Dialog.alert = (options) => {
  return Dialog(Object.assign({}, options, { boxType: "alert" }));
};

Dialog.confirm = (options) => {
  return Dialog(
    Object.assign({ showCancelButton: true }, options, { boxType: "confirm" })
  );
};

Dialog.close = () => {
  dialogInstance.forEach((_, vm) => {
    vm.closeDialog();
  });
  dialogInstance.clear();
};

DialogConstructor.install = (app) =>{
  const { name } = DialogConstructor;
  app.component(name, DialogConstructor)
}
Dialog.Component = DialogConstructor;

// 注册组件
Dialog.install = (app) => {
  app.use(Dialog.Component);
  app.config.globalProperties.$dialog = Dialog;
};

export default Dialog;

完成

以上只是本人在学习过程中产出的一些东西,希望和大家分享一下,如果有更好的方案,欢迎讨论~

Released under the MIT License.