Commit dffcec96 by lijiabin

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

parent c883880d
<template> <template>
<Teleport to="body"> <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"> <div v-if="visible" class="fixed inset-0 z-3000 flex items-center justify-center p-4">
<!-- Backdrop --> <!-- Backdrop -->
<div class="confirm-backdrop absolute inset-0 bg-black/20" /> <div class="confirm-backdrop absolute inset-0 bg-black/20" />
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
:style="{ width: normalizedWidth }" :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> <el-icon><Close /></el-icon>
</div> </div>
<div class="px-6 py-5"> <div class="px-6 py-5">
...@@ -55,7 +55,7 @@ ...@@ -55,7 +55,7 @@
<!-- Message --> <!-- Message -->
<p v-if="message" class="mt-1.5 text-center text-13px text-gray-400 leading-5"> <p v-if="message" class="mt-1.5 text-center text-13px text-gray-400 leading-5">
{{ message }} <component :is="normalizedMessage" />
</p> </p>
<!-- Actions --> <!-- Actions -->
...@@ -64,7 +64,7 @@ ...@@ -64,7 +64,7 @@
v-if="showCancelButton" v-if="showCancelButton"
type="button" type="button"
class="confirm-btn cancel-btn" class="confirm-btn cancel-btn"
@click="onCancel" @click.stop="onCancel"
> >
{{ cancelText }} {{ cancelText }}
</button> </button>
...@@ -74,7 +74,7 @@ ...@@ -74,7 +74,7 @@
class="confirm-btn primary-btn" class="confirm-btn primary-btn"
:class="{ 'danger-btn': type === 'danger', 'warning-btn': type === 'warning' }" :class="{ 'danger-btn': type === 'danger', 'warning-btn': type === 'warning' }"
:disabled="loading" :disabled="loading"
@click="onConfirm" @click.stop="onConfirm"
> >
<svg <svg
v-if="loading" v-if="loading"
...@@ -119,16 +119,46 @@ const { ...@@ -119,16 +119,46 @@ const {
type = 'primary', type = 'primary',
showCancelButton = true, showCancelButton = true,
showConfirmButton = true, showConfirmButton = true,
validate = null,
mousePosition,
} = defineProps<MessageBoxProps>() } = defineProps<MessageBoxProps>()
const normalizedWidth = computed(() => (typeof width === 'number' ? `${width}px` : width)) const normalizedWidth = computed(() => (typeof width === 'number' ? `${width}px` : width))
const visible = defineModel<boolean>({ default: false }) 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<{ const emit = defineEmits<{
confirm: [] confirm: [res: string | number | undefined]
cancel: [] cancel: []
close: [] close: []
afterLeave: []
}>() }>()
const close = () => { const close = () => {
...@@ -141,9 +171,16 @@ const onCancel = () => { ...@@ -141,9 +171,16 @@ const onCancel = () => {
close() close()
} }
const onConfirm = () => { const onConfirm = async () => {
emit('confirm') if (validate) {
const res = await validate?.()
emit('confirm', res as string | number)
close()
} else {
emit('confirm', undefined)
if (!loading) close() if (!loading) close()
}
} }
const open = () => { const open = () => {
...@@ -154,32 +191,65 @@ defineExpose({ open, close }) ...@@ -154,32 +191,65 @@ defineExpose({ open, close })
</script> </script>
<style scoped> <style scoped>
.confirm-enter-active { /* 背景透明度过渡:打开时 0→1,关闭时 1→0 */
transition: opacity 0.22s ease; .confirm-enter-active,
}
.confirm-leave-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; opacity: 0;
} }
.confirm-enter-to,
.confirm-leave-from { .confirm-leave-to {
opacity: 1; opacity: 0;
} }
/* card 动画 */
.confirm-enter-active .confirm-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 { .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 { .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 { .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 { .confirm-card {
......
import type { VNode, Component } from 'vue'
export interface MessageBoxProps { export interface MessageBoxProps {
title?: string title?: string
message?: string message?: string | (() => VNode) | Component
confirmText?: string confirmText?: string
cancelText?: string cancelText?: string
width?: string | number width?: string | number
...@@ -8,4 +9,6 @@ export interface MessageBoxProps { ...@@ -8,4 +9,6 @@ export interface MessageBoxProps {
type?: 'primary' | 'warning' | 'danger' type?: 'primary' | 'warning' | 'danger'
showCancelButton?: boolean showCancelButton?: boolean
showConfirmButton?: boolean showConfirmButton?: boolean
validate?: () => Promise<string | number> | null
mousePosition: { x: number; y: number }
} }
import MessageBox from '@/components/common/MessageBox/index.vue' import MessageBox from '@/components/common/MessageBox/index.vue'
import type { MessageBoxProps } from '@/components/common/MessageBox/types' import type { MessageBoxProps } from '@/components/common/MessageBox/types'
import type { FormInstance } from 'element-plus' import type { FormInstance } from 'element-plus'
import { render, effect } from 'vue' import { render, effect, stop } from 'vue'
interface ConfirmProps extends MessageBoxProps { interface ConfirmProps extends MessageBoxProps {
useLoading?: boolean // 是否用到loading useLoading?: boolean // 是否用到loading
...@@ -10,10 +10,28 @@ interface PromptProps extends MessageBoxProps { ...@@ -10,10 +10,28 @@ interface PromptProps extends MessageBoxProps {
inputPattern?: RegExp // 输入框的正则 inputPattern?: RegExp // 输入框的正则
inputErrorMessage?: string // 输入框的错误信息 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 = () => { export const useMessageBox = () => {
const confirm = (props: ConfirmProps) => { const confirm = (props: Omit<ConfirmProps, 'mousePosition'>) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
shouldUpdateMousePosition = false
const visible = ref(false) const visible = ref(false)
const loading = ref(false) const loading = ref(false)
const onConfirm = () => { const onConfirm = () => {
...@@ -40,15 +58,21 @@ export const useMessageBox = () => { ...@@ -40,15 +58,21 @@ export const useMessageBox = () => {
reject(false) reject(false)
} }
effect(() => { const effectRunner = effect(() => {
render( render(
<MessageBox <MessageBox
v-model={visible.value} v-model={visible.value}
{...props} {...props}
mousePosition={mousePosition.value}
loading={loading.value} loading={loading.value}
onConfirm={onConfirm} onConfirm={onConfirm}
onCancel={onCancel} onCancel={onCancel}
onClose={onClose} onClose={onClose}
onAfterLeave={() => {
stop(effectRunner)
render(null, document.body)
shouldUpdateMousePosition = true
}}
/>, />,
document.body, document.body,
) )
...@@ -58,8 +82,11 @@ export const useMessageBox = () => { ...@@ -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) => { return new Promise((resolve, reject) => {
shouldUpdateMousePosition = false
const visible = ref(false) const visible = ref(false)
const loading = ref(false) const loading = ref(false)
const rules = { const rules = {
...@@ -104,7 +131,7 @@ export const useMessageBox = () => { ...@@ -104,7 +131,7 @@ export const useMessageBox = () => {
reject(false) reject(false)
} }
effect(() => { const effectRunner = effect(() => {
render( render(
<MessageBox <MessageBox
{...props} {...props}
...@@ -112,9 +139,15 @@ export const useMessageBox = () => { ...@@ -112,9 +139,15 @@ export const useMessageBox = () => {
validate={validate} validate={validate}
message={messageFC} message={messageFC}
loading={loading.value} loading={loading.value}
mousePosition={mousePosition.value}
onConfirm={(res: string | number | undefined) => onConfirm(res as T)} onConfirm={(res: string | number | undefined) => onConfirm(res as T)}
onCancel={onCancel} onCancel={onCancel}
onClose={onClose} onClose={onClose}
onAfterLeave={() => {
stop(effectRunner)
render(null, document.body)
shouldUpdateMousePosition = true
}}
/>, />,
document.body, 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