天天看點

第116期:二次确認提示框開(類似popConfirm)發示例

封面圖

第116期:二次确認提示框開(類似popConfirm)發示例

魔女宅急便劇照

背景

在某個項目中,傳統的二次彈框總是使用一個大的modal來進行,對界面的流程有一定的阻斷效果。

同時,使用者看到彈框習慣直接點選确認按鈕,導緻操作失誤。

分析

二次确認的主要作用是防止誤操作,以及警示操作帶來的後果,避免使用者無意之間執行了本不想做的操作。從使用者流程圖中我們不難看出,二次确認是一種打斷使用者流程的設計,隻是迫不得已的折中方案。是以在是否使用,如何使用上需要有一定的考慮,否則會适得其反。

解決方案

針對上述問題,兩種比較好的設計方案如下:

  • 誤操作左後,給定撤銷時間
第116期:二次确認提示框開(類似popConfirm)發示例
  • 多次詢問

确認時輸入指定字元

第116期:二次确認提示框開(類似popConfirm)發示例

存在問題

這種彈框雖然可以解決問題,但是整個Modal彈出時,遮住整個頁面,使用者無法看清楚頁面的其他資訊。

第116期:二次确認提示框開(類似popConfirm)發示例

基于此,決定将二次彈框封裝到propConfirm中。這樣做有兩個好處:

  • 一是占用較少的界面空間,propConfirm彈出時不會占用太多界面空間。
  • 二是使用者的關注點更加聚焦,一定程度上可以減少操作失誤的機率。

設計方案

第116期:二次确認提示框開(類似popConfirm)發示例

RemindModal 提供常用的元件方案,RemindModalHook提供快捷方法。即使用者即可以使用常用的元件使用模式,在元件上添加各種參數,也可以用HOOK直接進行快捷注冊。

注意事項

  • 鈎子函數​

    ​regist​

    ​的首要任務是在元件挂載時,設定參數中傳遞過來的props。
  • 子元素需要用​

    ​cloneVNodes​

    ​方法進行複制,在上面添加onclick方法、
  • 位置資訊的計算

問題

​slot​

​進來元素不随父元素變化,怎麼實作?

  • teleport 挂載到body
  • 動态計算placement位置
  • 提示框在頁面滾動時不随按鈕一起滾動

解決方案

  1. 子元素不采用slot , 而是用cloneVnodes複制一份
  2. 根據triggerRect 和 contentRect動态計算提示位置
  3. body添加onscroll 事件

相關代碼

// position.ts
export const useClientRect = (ele) => {
  const res = ele && ele.$el ? ele.$el.getClientRects() : ele.getClientRects()
  const { top, left, width, height, x, y } = res[0]
  return {
    top,
    left,
    width,
    height,
    x,
    y
  }
}

interface Position {
  top?: number
  left?: number
  width?: number
  height?: number
  x?: number
  y?: number
}

export const usePlaceMent = ({ triggerRect, contentRect, placeMent }) => {
  if (placeMent === 'left') {
    return {
      top: triggerRect.top - contentRect.height / 2 + triggerRect.height / 2,
      left: triggerRect.left - contentRect.width - 10
    } as Position
  }

  if (placeMent === 'top') {
    const lastScrollTop = document.body.scrollTop
    return {
      top: triggerRect.top - contentRect.height - 10 + lastScrollTop - document.body.scrollTop,
      left: triggerRect.left + triggerRect.width / 2 - contentRect.width / 2
    } as Position
  }
  if (placeMent === 'right') {
    return {
      top: triggerRect.top - contentRect.height / 2 + triggerRect.height / 2,
      left: triggerRect.left + contentRect.width / 2 - triggerRect.width / 2 + 10
    } as Position
  }
  if (placeMent === 'bottom') {
    return {
      top: triggerRect.top + triggerRect.height + 10,
      left: triggerRect.left + triggerRect.width / 2 - contentRect.width / 2
    } as Position
  }
}

export const useScroll = (ele, onScroll: (evt: Event) => void) => {
  const handleScroll = (evt: Event) => {
    onScroll(evt)
  }
  const bindScroll = () => {
    document.body.addEventListener('scroll', handleScroll, true)
  }
  bindScroll()
}      

Reminder.vue

<script lang="tsx">
  // ref, unref, computed, reactive, watchEffect
  import type { Ref } from 'vue'
  import { defineComponent, ref, computed, Teleport, onMounted } from 'vue'
  import { cloneVNodes } from '../tool'
  import { Button, Form, FormItem, Input } from 'ant-design-vue'
  import { ExclamationCircleOutlined } from '@ant-design/icons-vue'
  // import BasicRemind from './basic.vue'
  import { useDesign } from '/@/hooks/web/useDesign'
  import { useClientRect, usePlaceMent, useScroll } from '../hooks/position'

  const props = {
    title: {
      type: String as PropType<String>,
      default: '确定執行此操作嗎~'
    },
    duration: {
      type: Number as PropType<number>
      // default: 5
    },
    userText: {
      type: String as PropType<String>
    },
    cancleText: {
      type: String as PropType<String>,
      default: '取消'
    },
    confirmText: {
      type: String as PropType<String>,
      default: '确定'
    },
    placement: {
      type: String as PropType<String>,
      default: 'left'
    },
    reminderType: {
      type: String as PropType<String> // userInput | countDown
    },
    onOk: {
      type: Function as PropType<Function>,
      default: function () {
        console.log('onOk')
      }
    },
    onCancle: {
      type: Function as PropType<Function>,
      default: function () {
        console.log('onCancle')
      }
    }
  }

  export default defineComponent({
    name: 'RemindModal',
    components: { Button },
    props,
    emits: ['ok', 'cancle'],
    //  emit, expose
    setup(props, { slots }) {
      let disabled: Ref<boolean> = ref(false)
      let countDownNum: Ref<number> = ref(props.duration)
      let countDownTimer: Ref<any> = ref(null)
      let triggerEl: Ref<any> = ref(null)
      let position: Ref<Object> = ref({ top: 0, left: 0 })
      let opacity: Ref<any> = ref(0)
      let remindContainer: Ref<any> = ref({})
      let contentEl: Ref<any> = ref(null)
      let formRef: Ref<any> = ref(null)
      let userConfirmText: Ref<any> = ref('')

      const rules = {
        userConfirmText: [
          {
            required: true,
            // message: '請輸入相關确認字元~',
            trigger: 'blur',
            validator: () => {
              if (userConfirmText.value === '') {
                return Promise.reject('請輸入相關确認字元!')
              } else if (userConfirmText.value !== props.userText) {
                console.log('輸入和預期不一緻-----', userConfirmText.value)
                return Promise.reject('輸入和預期不一緻!')
              } else {
                return Promise.resolve()
              }
            }
          }
        ]
      }
      const onKeyDown = (e: KeyboardEvent) => {
        console.log('i am copyed el', e)
      }
      const { prefixCls } = useDesign('remind')
      const getRemindClass = computed(() => {
        return [prefixCls, [`${prefixCls}-modal`]]
      })

      const getRemindContainerClass = computed(() => {
        return [
          prefixCls,
          [`${prefixCls}-modal-content`, `${prefixCls}-modal-content-${props.placement}`]
        ]
      })
      console.log('getRemindContainerClass', getRemindContainerClass)
      const countDown = () => {
        disabled.value = true
        countDownTimer.value = setInterval(() => {
          countDownNum.value -= 1
          if (countDownNum.value <= 0) {
            clearInterval(countDownTimer.value)
            disabled.value = false
            countDownNum.value = props.duration
            opacity.value = 0
          }
        }, 1000)
      }

      const calcPlaceMent = () => {
        let triggerRect = useClientRect(triggerEl.value)
        let contentRect = useClientRect(contentEl.value)
        let calcPos = usePlaceMent({
          triggerRect,
          contentRect,
          placeMent: props.placement
        })
        position.value = calcPos
      }

      const onConfirm = () => {
        if (props.reminderType === 'userInput') {
          console.log('formRef', formRef)
          formRef.value
            .validate()
            .then(() => {
              props.onOk()
            })
            .catch((error) => {
              console.log('error', error)
            })
        }
        if (props.reminderType === 'countDown') {
          if (props.duration) {
            countDown()
          }
        }
      }

      const conCancle = () => {
        if (disabled.value && countDownNum.value) {
          clearInterval(countDownTimer.value)
          disabled.value = false
          countDownNum.value = props.duration
        }
        if (props.reminderType === 'userInput' && props.userText) {
          formRef.value.resetFields()
          userConfirmText.value = ''
        }
        opacity.value = 0
      }

      onMounted(() => {
        calcPlaceMent()
        useScroll(triggerEl.value, () => {
          calcPlaceMent()
        })
      })

      const renderUserText = () => {
        return (
          <>
            {props.userText ? (
              <div>
                <div>請輸入以下内容:</div>
                <div>{`${props.userText}`}</div>
                <Form ref={formRef} model={userConfirmText} rules={rules}>
                  <FormItem name={'userConfirmText'}>
                    <Input v-model:value={userConfirmText.value} placeholder="請輸入"
                  </FormItem>
                </Form>
              </div>
            ) : null}
          </>
        )
      }

      const renderTip = () => {
        return (
          <Teleport to="body">
            <div
              ref={contentEl}
              class={getRemindContainerClass.value}
              style={{
                top: `${position.value?.top}px`,
                left: `${position.value?.left}px`,
                opacity: opacity.value
              <div class="haomo-remind-modal-arrow">
                <span class="haomo-remind-modal-arrow-content"></span>
              </div>
              <div class="haomo-remind-modal-inner">
                <div class="ant-popover-inner-content">
                  <div class="ant-popover-message">
                    <span class="anticon anticon-exclamation-circle">
                      <ExclamationCircleOutlined style={{ color: '#faad14' }} />
                    </span>
                    <div class="ant-popover-message-title">{props.title}</div>
                  </div>
                  {renderUserText()}
                  <div class="ant-popover-buttons">
                    <Button type="default" size="small" onClick={() conCancle()}>
                      {props.cancleText}
                    </Button>
                    {disabled.value ? (
                      <Button
                        type="primary"
                        danger={true}
                        size="small"
                        disabled
                        style={{ background: '#ff7875', color: '#fff' }}
                      >
                        {`${countDownNum.value}s後執行`}
                      </Button>
                    ) : (
                      <Button type="primary" size="small" onClick={() onConfirm()}>
                        {props.confirmText}
                      </Button>
                    )}
                  </div>
                </div>
              </div>
            </div>
          </Teleport>
        )
      }
      return () => {
        return (
          <div class={getRemindClass.value} ref={remindContainer}>
            {renderTip()}
            {cloneVNodes(
              slots.default?.() || [],
              {
                onKeydown: (e: KeyboardEvent) => {
                  onKeyDown(e)
                },
                onclick: () => {
                  opacity.value = opacity.value === 1 ? 0 : 1
                  calcPlaceMent()
                },
                ref: triggerEl
              },
              false
            )}
          </div>      

最後

此開發案例并沒有采用popConfirm ,而是使用了它的樣式。在實際開發過程中,看了popConfirm的源碼,是基于多個基礎元件進行封裝的。比如​

​tooltip​

​​,​

​trigger​

​等...

繼續閱讀