首页 > 其他分享 >第116期:二次确认提示框开(类似popConfirm)发示例

第116期:二次确认提示框开(类似popConfirm)发示例

时间:2022-10-26 15:09:07浏览次数:72  
标签:popConfirm const 示例 top value 116 triggerRect props left

封面图

第116期:二次确认提示框开(类似popConfirm)发示例_sed

魔女宅急便剧照

背景

在某个项目中,传统的二次弹框总是使用一个大的modal来进行,对界面的流程有一定的阻断效果。

同时,用户看到弹框习惯直接点击确认按钮,导致操作失误。

分析

二次确认的主要作用是防止误操作,以及警示操作带来的后果,避免用户无意之间执行了本不想做的操作。从用户流程图中我们不难看出,二次确认是一种打断用户流程的设计,只是迫不得已的折中方案。所以在是否使用,如何使用上需要有一定的考虑,否则会适得其反。

解决方案

针对上述问题,两种比较好的设计方案如下:

  • 误操作左后,给定撤销时间

第116期:二次确认提示框开(类似popConfirm)发示例_前端_02

  • 多次询问

确认时输入指定字符

第116期:二次确认提示框开(类似popConfirm)发示例_sed_03

存在问题

这种弹框虽然可以解决问题,但是整个Modal弹出时,遮住整个页面,用户无法看清楚页面的其他信息。

第116期:二次确认提示框开(类似popConfirm)发示例_sed_04

基于此,决定将二次弹框封装到propConfirm中。这样做有两个好处:

  • 一是占用较少的界面空间,propConfirm弹出时不会占用太多界面空间。
  • 二是用户的关注点更加聚焦,一定程度上可以减少操作失误的概率。

设计方案

第116期:二次确认提示框开(类似popConfirm)发示例_前端_05

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​​等...

最终效果如下:

第116期:二次确认提示框开(类似popConfirm)发示例_前端_06

第116期:二次确认提示框开(类似popConfirm)发示例_sed_07

标签:popConfirm,const,示例,top,value,116,triggerRect,props,left
From: https://blog.51cto.com/u_15531399/5797914

相关文章

  • WPF-后台动态创建窗口添加控件示例
    Windowwindow1=newWindow();window1.Title="新窗口";window1.Background=Brushes.LightBlue;//SolidColorBrushbrush1......
  • VueRouter 实现登录后跳转到之前相要访问的页面的简单示例
    简介该功能主要用于判定用户权限,在用户无权限时重定向至登录页,并在用户完成登录后,再定向至用户之前想要访问的路由;或者用户在任意路由点击登录时,登录成功后返回当前路由。......
  • istio部署demoapp多版本应用示例
    环境说明frontend(proxy):前端应用,会请求后端的demoappservice:proxydemoapp:后端应用同时部署两个版本 部署demoappv1.0deploy-demoapp-v10.yamla......
  • 华科云商golang详细示例代码
    packagemainimport("net/url""net/http""bytes""fmt""io/ioutil")constProxyServer="ip.hahado.cn:39......
  • 国网B接口资源上报(Push_Resourse)接口描述和消息示例
    上篇blog,梳理了国网B接口的REGISTER接口描述和消息示例,前端系统加电启动并初次注册成功后,向平台上报前端系统的设备资源信息(包括:视频服务器、DVR/DVS、摄像机、告警设备、环......
  • 【转】VUE 组件注册使用示例
    首先是main.jsimport{createApp}from'vue'importAppfrom'./App.vue'import'./index.css'importSwiperfrom'./components/01.globalReg/Swiper.vue'imp......
  • golang隧道模式示例
    packagemainimport("net/url""net/http""bytes""fmt""io/ioutil")constProxyServer="ip.hahado.cn:3......
  • golang隧道模式代码示例
    packagemainimport("net/url""net/http""bytes""fmt""io/ioutil")constProxyServer="ip.hahado.cn:39010"typeProxyAuthstruct{LicensestringSecretKe......
  • Sequence Flow示例
    准备实践环境[root@master~]#knservicecreatesq-appender-01--imageikubernetes/appender--envMESSAGE="-HandledbySQ-01"Creatingservice'sq-appender-0......
  • P1165 日志分析
    日志分析题目描述\(M\)海运公司最近要对旗下仓库的货物进出情况进行统计。目前他们所拥有的唯一记录就是一个记录集装箱进出情况的日志。该日志记录了两类操作:第一类操......