Commit dffcec96 by lijiabin

【需求 20331】 perf: 优化 确认弹窗组件等

parent c883880d
<template>
<Teleport to="body">
<Transition name="confirm">
<Transition name="confirm" @after-leave="emit('afterLeave')">
<div v-if="visible" class="fixed inset-0 z-3000 flex items-center justify-center p-4">
<!-- Backdrop -->
<div class="confirm-backdrop absolute inset-0 bg-black/20" />
......@@ -11,7 +11,7 @@
:style="{ width: normalizedWidth }"
>
<!-- 右上角关闭按钮 -->
<div class="absolute top-2 right-2 cursor-pointer" @click="close">
<div class="absolute top-2 right-2 cursor-pointer" @click.stop="close">
<el-icon><Close /></el-icon>
</div>
<div class="px-6 py-5">
......@@ -55,7 +55,7 @@
<!-- Message -->
<p v-if="message" class="mt-1.5 text-center text-13px text-gray-400 leading-5">
{{ message }}
<component :is="normalizedMessage" />
</p>
<!-- Actions -->
......@@ -64,7 +64,7 @@
v-if="showCancelButton"
type="button"
class="confirm-btn cancel-btn"
@click="onCancel"
@click.stop="onCancel"
>
{{ cancelText }}
</button>
......@@ -74,7 +74,7 @@
class="confirm-btn primary-btn"
:class="{ 'danger-btn': type === 'danger', 'warning-btn': type === 'warning' }"
:disabled="loading"
@click="onConfirm"
@click.stop="onConfirm"
>
<svg
v-if="loading"
......@@ -119,16 +119,46 @@ const {
type = 'primary',
showCancelButton = true,
showConfirmButton = true,
validate = null,
mousePosition,
} = defineProps<MessageBoxProps>()
const normalizedWidth = computed(() => (typeof width === 'number' ? `${width}px` : width))
const visible = defineModel<boolean>({ default: false })
const normalizedMousePosition = computed(() => {
return {
x: mousePosition.x + 'px',
y: mousePosition.y + 'px',
}
})
onMounted(() => {
console.log('onMounted')
})
onUpdated(() => {
console.log('onUpdated')
})
onUnmounted(() => {
console.log('onUnmounted')
})
const normalizedMessage = computed(() => {
if (!message) return ''
if (typeof message === 'function') {
return message
} else {
return () => message
}
})
const emit = defineEmits<{
confirm: []
confirm: [res: string | number | undefined]
cancel: []
close: []
afterLeave: []
}>()
const close = () => {
......@@ -141,9 +171,16 @@ const onCancel = () => {
close()
}
const onConfirm = () => {
emit('confirm')
if (!loading) close()
const onConfirm = async () => {
if (validate) {
const res = await validate?.()
emit('confirm', res as string | number)
close()
} else {
emit('confirm', undefined)
if (!loading) close()
}
}
const open = () => {
......@@ -154,32 +191,65 @@ defineExpose({ open, close })
</script>
<style scoped>
.confirm-enter-active {
transition: opacity 0.22s ease;
}
/* 背景透明度过渡:打开时 0→1,关闭时 1→0 */
.confirm-enter-active,
.confirm-leave-active {
transition: opacity 0.16s ease;
transition: opacity 0.2s ease;
}
.confirm-enter-from,
.confirm-leave-to {
.confirm-enter-from {
opacity: 0;
}
.confirm-enter-to,
.confirm-leave-from {
opacity: 1;
.confirm-leave-to {
opacity: 0;
}
/* card 动画 */
.confirm-enter-active .confirm-card {
transition: transform 0.3s cubic-bezier(0.22, 1.2, 0.36, 1);
transition:
transform 0.2s cubic-bezier(0.22, 1.2, 0.36, 1),
opacity 0.2s ease;
}
.confirm-leave-active .confirm-card {
transition: transform 0.16s ease;
transition:
transform 0.2s ease,
opacity 0.2s ease;
}
/* 从点击点展开(先用固定坐标 200px,200px 测试动画是否生效) */
.confirm-enter-from .confirm-card {
transform: scale(0.82);
/* transform: translate(calc(200px - 50vw), calc(200px - 50vh)) scale(0.1); */
transform: translate(
calc(v-bind('normalizedMousePosition.x') - 50vw),
calc(v-bind('normalizedMousePosition.y') - 50vh)
)
scale(0.1);
opacity: 0;
}
.confirm-enter-to .confirm-card {
transform: translate(0, 0) scale(1);
opacity: 1;
}
/* 关闭时缩回点击点 */
.confirm-leave-from .confirm-card {
transform: translate(0, 0) scale(1);
opacity: 1;
}
.confirm-leave-to .confirm-card {
transform: scale(0.96);
transform: translate(
calc(v-bind('normalizedMousePosition.x') - 50vw),
calc(v-bind('normalizedMousePosition.y') - 50vh)
)
scale(0.1);
opacity: 0;
}
.confirm-card {
......
import type { VNode, Component } from 'vue'
export interface MessageBoxProps {
title?: string
message?: string
message?: string | (() => VNode) | Component
confirmText?: string
cancelText?: string
width?: string | number
......@@ -8,4 +9,6 @@ export interface MessageBoxProps {
type?: 'primary' | 'warning' | 'danger'
showCancelButton?: boolean
showConfirmButton?: boolean
validate?: () => Promise<string | number> | null
mousePosition: { x: number; y: number }
}
import MessageBox from '@/components/common/MessageBox/index.vue'
import type { MessageBoxProps } from '@/components/common/MessageBox/types'
import type { FormInstance } from 'element-plus'
import { render, effect } from 'vue'
import { render, effect, stop } from 'vue'
interface ConfirmProps extends MessageBoxProps {
useLoading?: boolean // 是否用到loading
......@@ -10,10 +10,28 @@ interface PromptProps extends MessageBoxProps {
inputPattern?: RegExp // 输入框的正则
inputErrorMessage?: string // 输入框的错误信息
}
let hasBindEvent = false
// 记录每次鼠标点击的位置 x y
const mousePosition = ref({ x: 0, y: 0 })
let shouldUpdateMousePosition = true
const handler = (e: MouseEvent) => {
if (!shouldUpdateMousePosition) return
mousePosition.value = { x: e.clientX, y: e.clientY }
}
if (!hasBindEvent) {
window.addEventListener('click', handler, {
capture: true,
})
hasBindEvent = true
}
export const useMessageBox = () => {
const confirm = (props: ConfirmProps) => {
const confirm = (props: Omit<ConfirmProps, 'mousePosition'>) => {
return new Promise((resolve, reject) => {
shouldUpdateMousePosition = false
const visible = ref(false)
const loading = ref(false)
const onConfirm = () => {
......@@ -40,15 +58,21 @@ export const useMessageBox = () => {
reject(false)
}
effect(() => {
const effectRunner = effect(() => {
render(
<MessageBox
v-model={visible.value}
{...props}
mousePosition={mousePosition.value}
loading={loading.value}
onConfirm={onConfirm}
onCancel={onCancel}
onClose={onClose}
onAfterLeave={() => {
stop(effectRunner)
render(null, document.body)
shouldUpdateMousePosition = true
}}
/>,
document.body,
)
......@@ -58,8 +82,11 @@ export const useMessageBox = () => {
})
}
const prompt = <T extends string | number = string>(props: PromptProps): Promise<T> => {
const prompt = <T extends string | number = string>(
props: Omit<PromptProps, 'mousePosition'>,
): Promise<T> => {
return new Promise((resolve, reject) => {
shouldUpdateMousePosition = false
const visible = ref(false)
const loading = ref(false)
const rules = {
......@@ -104,7 +131,7 @@ export const useMessageBox = () => {
reject(false)
}
effect(() => {
const effectRunner = effect(() => {
render(
<MessageBox
{...props}
......@@ -112,9 +139,15 @@ export const useMessageBox = () => {
validate={validate}
message={messageFC}
loading={loading.value}
mousePosition={mousePosition.value}
onConfirm={(res: string | number | undefined) => onConfirm(res as T)}
onCancel={onCancel}
onClose={onClose}
onAfterLeave={() => {
stop(effectRunner)
render(null, document.body)
shouldUpdateMousePosition = true
}}
/>,
document.body,
)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment