Browse Source

feat: dialog组件提交

dushoujun 5 years ago
parent
commit
a9ef2a5848

+ 10 - 0
src/config.ts

@@ -180,6 +180,16 @@ export const nav = [
         show: true,
         desc: '头像',
         author: 'ailululu'
+      },
+      {
+        name: 'Dialog',
+        sort: 8,
+        cName: '对话框',
+        type: 'component',
+        show: true,
+        desc:
+          '模态对话框,在浮层中显示,引导用户进行相关操作,支持图片对话框。',
+        author: 'dsj'
       }
     ]
   },

File diff suppressed because it is too large
+ 18 - 0
src/packages/dialog/close.svg


File diff suppressed because it is too large
+ 279 - 0
src/packages/dialog/demo.vue


+ 157 - 0
src/packages/dialog/doc.md

@@ -0,0 +1,157 @@
+# Dialog 对话框
+
+模态对话框,在浮层中显示,引导用户进行相关操作,支持图片对话框。
+
+## 基本用法
+
+```javascript
+import Dialog from './index';  
+// 全局注册  
+const app = createApp();  
+app.use(Dialog);  
+```
+
+```javascript
+Dialog({
+  title: "确定删除此订单?",
+  content: "删除后将从你的记录里消失,无法找回"
+});
+```
+## 直接关闭当前dialog
+```javascript
+Dialog.closed()  //可以直接关闭当前dialog
+```
+
+## ID
+
+同一个页面中,id相同的Dialog的DOM只会同时存在一个,不指定id时,id的默认值为**nut-dialog-default-id**。
+
+```javascript
+Dialog({
+  id:'my-dialog',
+  title: "确定删除此订单?",
+  content: "删除后将从你的记录里消失,无法找回"
+});
+```
+> 如果希望同时弹出多个Dialog,请给不同的Dialog设置不同的id。
+
+## 事件
+```javascript
+Dialog({
+        title: "自定义Dialog标题",
+        content: "小屏或移动端浏览效果最佳",
+        closeBtn:true,  //显式右上角关闭按钮
+        onOkBtn(event) {  //确定按钮点击事件
+          alert("okBtn");
+          this.close(); //关闭对话框
+        },
+        onCancelBtn(event) {  //取消按钮点击事件,默认行为关闭对话框
+          alert("cancelBtn");
+          //return false;  //阻止默认“关闭对话框”的行为
+        },
+        onCloseBtn(event) { //右上角关闭按钮点击事件
+          alert("closeBtn");
+          //return false;  //阻止默认“关闭对话框”的行为
+        },
+        closeCallback(target) {
+          alert("will close");  //对话框关闭回调函数,无论通过何种方式关闭都会触发
+        }
+});
+        
+```
+## 关闭dialog不销毁实例
+```javascript
+ Dialog({
+        animation: false, //禁用弹出动效
+        title: "注册说明",
+        canDestroy:false,
+        content:
+          "原账号为您本人所有,建议直接登录或找回密码。原账号内的订单资产可能丢失,可联系京东客服找回。"
+      });
+        
+```
+## 页面滚动锁定
+
+**lockBgScroll** 值设为 **true** 时,可在弹窗出现时锁定页面滚动,且不影响窗体内部滚动。
+
+```javascript
+Dialog({
+        title: "背景滚动锁定",
+        lockBgScroll:true,
+        content:"弹窗弹出后,页面滚动锁止。在窗体和遮罩层上滑动时,页面不再跟随滚动。"
+});
+```
+
+## 图片弹窗
+
+**type** 值为 **image** 时为图片弹窗,需要配置一张图片,可带链接(非必须)。默认展示关闭按钮。点击图片触发 **onClickImageLink** 事件,返回**false**可阻止默认的跳转链接行为。
+
+```javascript
+Dialog({
+  type:"image", //设置弹窗类型为”图片弹窗“
+  link:"http://m.jd.com", //点击图片跳转的Url
+  imgSrc:"https://m.360buyimg.com/mobilecms/s750x750_jfs/t1/4875/23/1968/285655/5b9549eeE4997a18c/070eaf5bddf26be8.jpg", //图片Url
+  onClickImageLink:function(){ //图片点击事件,默认行为是跳转Url
+    console.log(this); //this指向该Dialog实例
+    return false;  //返回false可阻止默认的链接跳转行为
+  }
+});
+```
+
+## 标签式写法
+
+如果Dialog内容有复杂交互,可使用Dialog的标签式用法。注意标签使用的时候,属性不建议使用驼峰,推荐使用如下写法
+
+```html
+<nut-dialog title="标签形式调用" :visible="dialogShow" @ok-btn-click="dialogShow=false" @cancel-btn-click="dialogShow=false" @close="dialogShow=false">
+    <a href="javascript:;" @click="dialogShow=false" :noCancelBtn="true">点我可以直接关闭对话框</a>
+</nut-dialog>
+```
+
+```javascript
+export default {
+  data() {
+    return {
+      dialogShow: false
+    };
+  }
+}
+```
+
+## prop
+
+| 字段 | 说明 | 类型 | 默认值
+|----- | ----- | ----- | ----- 
+| id | 标识符,相同者共享一个实例 | String/Number | nut-dialog-default-id
+| canDestroy | 是否关闭弹窗时销毁实例 | Boolean | true
+| title | 标题 | String | -
+| content | 内容,支持HTML | String | -
+| type | 弹窗类型,值为**image**时为图片弹窗 | String | -
+| closeOnClickModal | 点击蒙层是否关闭对话框 | Boolean | true
+| noFooter | 是否隐藏底部按钮栏 | Boolean | false
+| noOkBtn | 是否隐藏确定按钮 | Boolean | false
+| noCancelBtn | 是否隐藏取消按钮 | Boolean | false
+| cancelBtnTxt | 取消按钮文案 | String | ”取 消“
+| okBtnTxt | 确定按钮文案 | String | ”确 定“
+| okBtnDisabled | 禁用确定按钮 | Boolean | false
+| cancelAutoClose | 取消按钮是否默认关闭弹窗 | Boolean | true
+| textAlign | 文字对齐方向,可选值同css的text-align | String | "center"
+| maskBgStyle | 遮罩层样式(颜色、透明度) | String | -
+| customClass | 增加一个自定义class | String | -
+| link | 点击图片跳转的Url,仅对图片类型弹窗有效 | String | -
+| imgSrc | 图片Url,仅对图片类型弹窗有效 | String | -
+| animation | 是否开启默认动效 | Boolean | true
+| closeOnPopstate | 是否在页面回退时自动关闭 | Boolean | false
+| lockBgScroll | 锁定遮罩层滚动,不影响弹窗内部滚动(实验性质)会给body添加posotion:fix属性,注意 | Boolean | false
+
+
+## 事件
+
+| 字段 | 说明 | 类型 | 默认值
+|----- | ----- | ----- | ----- 
+| onOkBtn | 确定按钮回调 | Function | -
+| onCancelBtn | 取消按钮回调 | Function | -
+| onCloseBtn | 关闭按钮回调 | Function | -
+| closeCallback | 关闭回调,任何情况关闭弹窗都会触发 | Function | -
+| onClickImageLink | 图片链接点击回调,仅对图片类型弹窗有效 | Function | -
+| closed | 关闭dialog | Function | -

+ 190 - 0
src/packages/dialog/index.scss

@@ -0,0 +1,190 @@
+@import '../../styles/variables.scss';
+@import '../../styles/mixins/make-animation';
+@import '../../styles/mixins/text-ellipsis.scss';
+@import '../../styles/animation/fade';
+@import '../../styles/animation/ease';
+$mask-bg: rgba(0, 0, 0, 0.5) !default;
+$font-size-base: 14px !default;
+body.dialog-open {
+  position: fixed;
+}
+
+.nut-dialog-wrapper {
+  position: relative;
+  z-index: $zindex-mask;
+}
+
+.nut-dialog-box {
+  position: fixed;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: $text-color;
+}
+
+.nut-dialog-mask,
+.nut-dialog-box {
+  left: 0;
+  top: 0;
+  right: 0;
+  bottom: 0;
+}
+
+.nut-dialog-mask {
+  position: fixed;
+  background: $mask-bg;
+}
+
+.nut-dialog {
+  position: relative;
+  width: 86%;
+  max-height: 70vh;
+  background: #fff;
+  border-radius: 12px;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+}
+
+.nut-dialog-title {
+  display: block;
+  line-height: 1.5;
+  color: #262626;
+  font-size: 16px;
+  text-align: center;
+  flex-shrink: 0;
+  @include text-ellipsis;
+  padding-bottom: 11px;
+  // &:only-child {
+  //     padding-bottom: 0;
+  // }
+}
+
+.nut-dialog-close {
+  position: absolute;
+  right: 0;
+  top: 0;
+  width: 36px;
+  height: 46px;
+  font-size: 20px;
+  text-align: center;
+  text-decoration: none;
+  background: url('//img13.360buyimg.com/imagetools/jfs/t1/144349/40/19537/4004/5fe1ca9bE2daa4196/7afe4a2ac681804a.png')
+    no-repeat center;
+  background-size: 10px 10px;
+  img {
+    height: 10px;
+  }
+}
+
+.nut-dialog-image-wrapper {
+  position: relative;
+  .nut-dialog {
+    width: auto;
+    max-width: 80%;
+    max-height: 75%;
+    background: transparent;
+    border-radius: none;
+    display: inline-block;
+    overflow: visible;
+  }
+  .nut-dialog-close {
+    position: absolute;
+    left: 50%;
+    top: auto;
+    bottom: -48px;
+    width: 24px;
+    height: 24px;
+    margin-left: -12px;
+    background: url(./close.svg) no-repeat center;
+    background-size: 100%;
+  }
+}
+
+.nut-dialog-link {
+  display: inline-block;
+}
+
+.nut-dialog-image {
+  max-width: 100%;
+  max-height: 100%;
+  vertical-align: bottom;
+}
+
+.nut-dialog-body {
+  box-sizing: border-box;
+  padding: 30px 20px 20px;
+  display: flex;
+  flex-direction: column;
+  flex: 0 1 auto;
+  overflow: auto;
+}
+
+.nut-dialog-content {
+  flex: 1;
+  justify-content: center;
+  overflow: auto;
+  font-size: $font-size-base;
+  word-break: break-all;
+  padding-bottom: 10px;
+  -webkit-overflow-scrolling: touch;
+}
+
+.nut-dialog-footer {
+  height: 50px;
+  width: 100%;
+  line-height: 50px;
+  display: flex;
+  flex-shrink: 0;
+  overflow: hidden;
+  flex-direction: row;
+  justify-content: center;
+  padding: 0 30px;
+}
+
+.nut-dialog-btn {
+  display: block;
+  max-width: 104px;
+  height: 30px;
+  border-radius: 17px;
+  position: relative;
+  flex: 1;
+  font-size: $font-size-base;
+  border: none;
+  background: transparent;
+  appearance: none;
+  outline: none;
+  user-select: none;
+  margin: 0 10px;
+  &.nut-dialog-ok {
+    width: 128px;
+    color: #fff;
+    background: linear-gradient(
+      135deg,
+      #fa2c19 0%,
+      #fa3f19 45%,
+      #fa5919 83%,
+      #fa6419 100%
+    );
+  }
+
+  &.nut-dialog-cancel {
+    color: #fa2c19;
+    border: 1px solid #fa2c19;
+  }
+  &.disabled {
+    cursor: not-allowed;
+    opacity: 0.68;
+  }
+  &:only-child {
+    max-width: 128px;
+    color: #fff;
+    background: linear-gradient(
+      135deg,
+      #fa2c19 0%,
+      #fa3f19 45%,
+      #fa5919 83%,
+      #fa6419 100%
+    );
+  }
+}

+ 50 - 0
src/packages/dialog/index.ts

@@ -0,0 +1,50 @@
+import dialog from './index.vue';
+import { defineComponent, createVNode, render, toRef, watch } from 'vue';
+import type {App} from "vue";
+
+const confirmConstructor = defineComponent(dialog);
+
+let instance;
+const Dialog = options => {
+
+  options = options ? options : {};
+
+  options.id = options.id || 'nut-dialog-default-id';
+  options.visible = true;
+  if (options.type === 'image' && typeof options.closeBtn === 'undefined') {
+    options.closeBtn = true;
+  }
+
+  // 生成组件实例
+  instance = createVNode(confirmConstructor, options);
+
+  // 渲染挂载组件
+  const container = document.createElement('div');
+  render(instance, container);
+  const dialogDom = document.querySelector('#' + options.id);
+  if (options.id && dialogDom) {
+    dialogDom.parentNode.replaceChild(instance.el, dialogDom);
+  } else {
+    document.body.appendChild(instance.el);
+  }
+
+  // 初始化组件参数
+  const props = instance.component.props;
+  Object.keys(options).forEach(key => {
+    props[key] = options[key];
+  });
+};
+Dialog.close = function () {
+  if (instance) {
+    instance.component.ctx.close()
+  }
+};
+
+
+Dialog.install = function (app) {
+  app.use(dialog);
+  app.config.globalProperties.$dialog = Dialog;
+};
+
+Dialog.Component = dialog;
+export default Dialog;

+ 344 - 0
src/packages/dialog/index.vue

@@ -0,0 +1,344 @@
+<template>
+  <view :class="classes" @click="handleClick">
+    <div
+      v-if="destroy"
+      :class="[
+        'nut-dialog-wrapper',
+        customClass,
+        { 'nut-dialog-image-wrapper': type === 'image' }
+      ]"
+      :id="id"
+    >
+      <transition :name="animation ? 'nutFade' : ''">
+        <div
+          :class="'nut-dialog-mask'"
+          :style="{ background: maskBgStyle }"
+          @click="modalClick"
+          v-show="curVisible"
+        ></div>
+      </transition>
+      <transition :name="animation ? 'nutEase' : ''">
+        <div class="nut-dialog-box" v-show="curVisible" @click="modalClick">
+          <div class="nut-dialog" @click.stop>
+            <a
+              href="javascript:;"
+              v-if="closeBtn"
+              @click="closeBtnClick"
+              class="nut-dialog-close"
+            ></a>
+            <template v-if="type === 'image'">
+              <a
+                href="javascript:;"
+                @click="imageLinkClick"
+                class="nut-dialog-link"
+              >
+                <img :src="imgSrc" class="nut-dialog-image" alt />
+              </a>
+            </template>
+            <template v-else>
+              <div class="nut-dialog-body">
+                <span
+                  class="nut-dialog-title"
+                  v-html="title"
+                  v-if="title"
+                ></span>
+                <div
+                  class="nut-dialog-content"
+                  v-if="isShowContent"
+                  :style="{ textAlign }"
+                >
+                  <slot></slot>
+                </div>
+                <div
+                  class="nut-dialog-content"
+                  v-html="content"
+                  v-else-if="content"
+                  :style="{ textAlign }"
+                ></div>
+              </div>
+              <div class="nut-dialog-footer" v-if="!noFooter">
+                <button
+                  class="nut-dialog-btn nut-dialog-cancel"
+                  v-if="!noCancelBtn"
+                  @click="cancelBtnClick(cancelAutoClose)"
+                  >{{ cancelBtnTxt }}</button
+                >
+                <button
+                  class="nut-dialog-btn nut-dialog-ok"
+                  v-if="!noOkBtn"
+                  :class="{ disabled: okBtnDisabled }"
+                  :disabled="okBtnDisabled"
+                  @click="okBtnClick"
+                  >{{ okBtnTxt }}</button
+                >
+              </div>
+            </template>
+          </div>
+        </div>
+      </transition>
+    </div>
+  </view>
+</template>
+<script lang="ts">
+import { ref, onMounted, watch, watchEffect, computed } from 'vue';
+import { createComponent } from '@/utils/create';
+const { componentName, create } = createComponent('dialog');
+
+const lockMaskScroll = (bodyCls => {
+  let scrollTop = 0;
+  return {
+    afterOpen: function() {
+      scrollTop =
+        (document.scrollingElement && document.scrollingElement.scrollTop) ||
+        document.body.scrollTop;
+      document.body.classList.add(bodyCls);
+      document.body.style.top = -scrollTop + 'px';
+    },
+    beforeClose: function() {
+      if (document.body.classList.contains(bodyCls)) {
+        document.body.classList.remove(bodyCls);
+        if (document.scrollingElement) {
+          document.scrollingElement.scrollTop = scrollTop;
+        }
+      }
+    }
+  };
+})('dialog-open');
+export default create({
+  props: {
+    id: {
+      type: String,
+      default: ''
+    },
+    title: {
+      type: String,
+      default: ''
+    },
+    content: {
+      type: String,
+      default: ''
+    },
+    type: {
+      type: String,
+      default: ''
+    },
+    link: {
+      type: String,
+      default: ''
+    },
+    imgSrc: {
+      type: String,
+      default: ''
+    },
+    animation: {
+      type: Boolean,
+      default: true
+    },
+    lockBgScroll: {
+      type: Boolean,
+      default: false
+    },
+    visible: {
+      type: Boolean,
+      default: false
+    },
+    closeBtn: {
+      type: Boolean,
+      default: false
+    },
+    closeOnClickModal: {
+      type: Boolean,
+      default: true
+    },
+    noFooter: {
+      type: Boolean,
+      default: false
+    },
+    noOkBtn: {
+      type: Boolean,
+      default: false
+    },
+    noCancelBtn: {
+      type: Boolean,
+      default: false
+    },
+    cancelBtnTxt: {
+      type: String,
+      default: '取消'
+    },
+    okBtnTxt: {
+      type: String,
+      default: '确定'
+    },
+    okBtnDisabled: {
+      type: Boolean,
+      default: false
+    },
+    cancelAutoClose: {
+      type: Boolean,
+      default: true
+    },
+    textAlign: {
+      type: String,
+      default: 'center'
+    },
+    onOkBtn: {
+      type: Function,
+      default: null
+    },
+    onCloseBtn: {
+      type: Function,
+      default: null
+    },
+    onCancelBtn: {
+      type: Function,
+      default: null
+    },
+    closeCallback: {
+      type: Function,
+      default: null
+    },
+    onClickImageLink: {
+      type: Function,
+      default: null
+    },
+    maskBgStyle: {
+      type: String,
+      default: ''
+    },
+    canDestroy: {
+      type: Boolean,
+      default: true
+    },
+    customClass: {
+      type: String,
+      default: ''
+    },
+    closeOnPopstate: {
+      type: Boolean,
+      default: false
+    }
+  },
+  components: {},
+  // emits: ['click'],
+
+  setup(props, { emit, slots }) {
+    const curVisible = ref(false);
+    let destroy = ref(true);
+    onMounted(() => {
+      curVisible.value = props.visible;
+    });
+    const isShowContent = computed(() => {
+      return slots.default;
+    });
+
+    const todestroy = () => {
+      if (!props.canDestroy) {
+        destroy = ref(false);
+      }
+    };
+    const close = (target?: string) => {
+      emit('close', target);
+      emit('close-callback', target);
+      todestroy();
+      if (
+        typeof props.closeCallback === 'function' &&
+        props.closeCallback(target) === false
+      ) {
+        return;
+      }
+      curVisible.value = false;
+    };
+    const modalClick = () => {
+      if (!props.closeOnClickModal) {
+        return;
+      }
+      close('modal');
+    };
+    const okBtnClick = () => {
+      emit('ok-btn-click');
+      if (typeof props.onOkBtn === 'function') {
+        props.onOkBtn.call(props);
+      }
+    };
+    const cancelBtnClick = (autoClose: boolean) => {
+      emit('cancel-btn-click');
+      if (!autoClose) {
+        return;
+      }
+      if (typeof props.onCancelBtn === 'function') {
+        if (props.onCancelBtn.call(props) === false) {
+          return;
+        }
+      }
+      close('cancelBtn');
+    };
+    const closeBtnClick = () => {
+      if (typeof props.onCloseBtn === 'function') {
+        if (props.onCloseBtn.call(props) === false) {
+          return;
+        }
+      }
+      close('closeBtn');
+    };
+    //图片类型弹窗中的链接点击事件,默认跳转
+    const imageLinkClick = () => {
+      if (
+        props.onClickImageLink &&
+        props.onClickImageLink.call(props) === false
+      ) {
+        return;
+      }
+      if (props.link) {
+        location.href = props.link;
+      }
+    };
+    const handleClick = (event: Event) => {
+      emit('click', event);
+    };
+    onMounted(() => {
+      if (props.closeOnPopstate) {
+        window.addEventListener('popstate', function() {
+          close();
+        });
+      }
+    });
+    watchEffect(() => {
+      if (props.lockBgScroll) {
+        //锁定or解锁页面滚动
+        lockMaskScroll[curVisible.value ? 'afterOpen' : 'beforeClose']();
+      }
+    });
+    watch(
+      () => props.visible,
+      val => {
+        curVisible.value = val;
+      }
+    );
+
+    const classes = computed(() => {
+      return {
+        [componentName]: true
+      };
+    });
+    return {
+      handleClick,
+      curVisible,
+      destroy,
+      modalClick,
+      close,
+      todestroy,
+      okBtnClick,
+      cancelBtnClick,
+      closeBtnClick,
+      imageLinkClick,
+      isShowContent,
+      classes
+    };
+  }
+});
+</script>
+
+<style lang="scss">
+@import 'index.scss';
+</style>

+ 23 - 0
src/styles/animation/ease.scss

@@ -0,0 +1,23 @@
+@keyframes nutEaseIn {
+  0% {
+    opacity: 0;
+    transform: scale(0.9);
+  }
+  100% {
+    opacity: 1;
+    transform: scale(1);
+  }
+}
+
+@keyframes nutEaseOut {
+  0% {
+    opacity: 1;
+    transform: scale(1);
+  }
+  100% {
+    opacity: 0;
+    transform: scale(0.9);
+  }
+}
+
+@include make-animation(nutEase);

+ 20 - 0
src/styles/animation/fade.scss

@@ -0,0 +1,20 @@
+@keyframes nutFadeIn {
+  from {
+    opacity: 0;
+  }
+
+  to {
+    opacity: 1;
+  }
+}
+
+@keyframes nutFadeOut {
+  from {
+    opacity: 1;
+  }
+
+  to {
+    opacity: 0;
+  }
+}
+@include make-animation(nutFade);

+ 20 - 0
src/styles/mixins/make-animation.scss

@@ -0,0 +1,20 @@
+@mixin make-animation($keyframeName, $timingFun: $animation-timing-fun, $duration: $animation-duration) {
+  .#{$keyframeName}-enter-active,
+  .#{$keyframeName}In,
+  .#{$keyframeName}-leave-active,
+  .#{$keyframeName}Out {
+    animation-duration: $duration;
+    animation-fill-mode: both;
+    animation-timing-function: $timingFun;
+  }
+
+  .#{$keyframeName}-enter-active,
+  .#{$keyframeName}In {
+    animation-name: #{$keyframeName}In;
+  }
+
+  .#{$keyframeName}-leave-active,
+  .#{$keyframeName}Out {
+    animation-name: #{$keyframeName}Out;
+  }
+}

+ 5 - 0
src/styles/mixins/text-ellipsis.scss

@@ -0,0 +1,5 @@
+@mixin text-ellipsis() {
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}

+ 15 - 0
src/styles/variables.scss

@@ -109,3 +109,18 @@ $inputnumber-input-background-color: $help-color;
 $inputnumber-input-border-radius: 8px;
 $inputnumber-input-width: 40px;
 $inputnumber-input-height: 20px;
+
+// ---- Animation ----
+$animation-duration: 0.25s !default;
+$transition-duration: 0.2s !default;
+$transition-duration-fast: 0.2s !default;
+$transition-duration-slow: 0.4s !default;
+$animation-timing-fun: cubic-bezier(0.55, 0.085, 0.68, 0.53) !default;
+$ease-in-out: cubic-bezier(0.445, 0.05, 0.55, 0.95);
+$ease-out: cubic-bezier(0.895, 0.03, 0.685, 0.22);
+
+// ---- z-index ----
+$zindex-mask: 9998 !default;
+$zindex-actionsheet: 10001 !default;
+$zindex-dialog: 10000 !default;
+$zindex-picker: 10050 !default;