Files
schisandra-cloud-album-front/src/components/MyUI/Message/Message.vue
landaiqing 33d76461f1 refine MyUI
2024-11-07 21:39:16 +08:00

430 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import type {CSSProperties, VNode} from 'vue';
import {computed, ref, watch} from 'vue';
import {cancelRaf, rafTimeout} from '../Utils';
interface Props {
content?: string // 提示内容
duration?: number // 自动关闭的延时,单位 ms设置 null 时,不自动关闭
top?: string | number // 消息距离顶部的位置,单位 px
}
const props = withDefaults(defineProps<Props>(), {
content: undefined,
duration: 3000,
top: 30
});
interface Message {
content?: string // 提示内容
icon?: VNode // 自定义图标
duration?: number | null // 自动关闭的延时时长,单位 ms设置 null 时,不自动关闭
top?: string | number // 消息距离顶部的位置,单位 px
class?: string // 自定义类名
style?: CSSProperties // 自定义样式
onClick?: () => void // 点击 message 时的回调函数
onClose?: () => void // 关闭时的回调函数
mode?: 'open' | 'info' | 'success' | 'error' | 'warning' | 'loading' // 类型
}
const resetTimer = ref();
const showMessage = ref<boolean[]>([]);
const hideTimers = ref<any[]>([]);
const messageContent = ref<Message[]>([]);
const closeDuration = ref<number | null>(null); // 自动关闭延时
const emits = defineEmits(['click', 'close']);
const messageTop = ref<string>();
const clear = computed(() => {
// 所有提示是否已经全部变为false
return showMessage.value.every((show) => !show);
});
watch(clear, (to, from) => {
// 所有提示都消失后重置
if (!from && to) {
resetTimer.value = rafTimeout(() => {
messageContent.value.splice(0);
showMessage.value.splice(0);
}, 300);
}
});
function onEnter(index: number) {
if (hideTimers.value[index]) {
cancelRaf(hideTimers.value[index]);
}
}
function onLeave(index: number) {
hideMessage(index);
}
function onClick(e: Event, index: number) {
if (messageContent.value[index].onClick) {
messageContent.value[index].onClick();
}
emits('click', e);
}
function hideMessage(index: number) {
if (closeDuration.value !== null) {
hideTimers.value[index] = rafTimeout(() => {
showMessage.value[index] = false;
if (messageContent.value[index].onClose) {
messageContent.value[index].onClose();
}
emits('close');
}, closeDuration.value);
}
}
function show() {
if (resetTimer.value) {
cancelRaf(resetTimer.value);
}
const index = messageContent.value.length - 1;
const last = messageContent.value[index];
if (last.top !== undefined) {
messageTop.value = typeof last.top === 'number' ? `${last.top}px` : last.top;
} else {
messageTop.value = typeof props.top === 'number' ? `${props.top}px` : props.top;
}
showMessage.value[index] = true;
if (last.duration !== null) {
closeDuration.value = last.duration || props.duration;
hideMessage(index);
} else {
closeDuration.value = null;
}
}
function open(message: string | Message) {
if (typeof message === 'string') {
messageContent.value.push({
content: message,
mode: 'open'
});
} else {
messageContent.value.push({
...message,
mode: 'open'
});
}
show();
}
function info(message: string | Message) {
if (typeof message === 'string') {
messageContent.value.push({
content: message,
mode: 'info'
});
} else {
messageContent.value.push({
...message,
mode: 'info'
});
}
show();
}
function success(message: string | Message) {
if (typeof message === 'string') {
messageContent.value.push({
content: message,
mode: 'success'
});
} else {
messageContent.value.push({
...message,
mode: 'success'
});
}
show();
}
function error(message: string | Message) {
if (typeof message === 'string') {
messageContent.value.push({
content: message,
mode: 'error'
});
} else {
messageContent.value.push({
...message,
mode: 'error'
});
}
show();
}
function warning(message: string | Message) {
if (typeof message === 'string') {
messageContent.value.push({
content: message,
mode: 'warning'
});
} else {
messageContent.value.push({
...message,
mode: 'warning'
});
}
show();
}
function loading(message: string | Message) {
if (typeof message === 'string') {
messageContent.value.push({
content: message,
mode: 'loading'
});
} else {
messageContent.value.push({
...message,
mode: 'loading'
});
}
show();
}
defineExpose({
open,
info,
success,
error,
warning,
loading
});
</script>
<template>
<div class="m-message-wrap" :style="`top: ${messageTop};`">
<TransitionGroup name="slide-fade">
<div
v-show="showMessage[index]"
class="m-message"
:class="message.class"
:style="message.style"
v-for="(message, index) in messageContent"
:key="index"
>
<div
class="m-message-content"
:class="`icon-${message.mode}`"
@mouseenter="onEnter(index)"
@mouseleave="onLeave(index)"
@click="onClick($event, index)"
>
<component v-if="message.icon" :is="message.icon" class="icon-svg"/>
<svg
v-else-if="message.mode === 'info'"
class="icon-svg"
focusable="false"
data-icon="info-circle"
width="1em"
height="1em"
fill="currentColor"
aria-hidden="true"
viewBox="64 64 896 896"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm32 664c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V456c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272zm-32-344a48.01 48.01 0 010-96 48.01 48.01 0 010 96z"
></path>
</svg>
<svg
v-else-if="message.mode === 'success'"
class="icon-svg"
focusable="false"
data-icon="check-circle"
width="1em"
height="1em"
fill="currentColor"
aria-hidden="true"
viewBox="64 64 896 896"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z"
></path>
</svg>
<svg
v-else-if="message.mode === 'error'"
class="icon-svg"
focusable="false"
data-icon="close-circle"
width="1em"
height="1em"
fill="currentColor"
aria-hidden="true"
fill-rule="evenodd"
viewBox="64 64 896 896"
>
<path
d="M512 64c247.4 0 448 200.6 448 448S759.4 960 512 960 64 759.4 64 512 264.6 64 512 64zm127.98 274.82h-.04l-.08.06L512 466.75 384.14 338.88c-.04-.05-.06-.06-.08-.06a.12.12 0 00-.07 0c-.03 0-.05.01-.09.05l-45.02 45.02a.2.2 0 00-.05.09.12.12 0 000 .07v.02a.27.27 0 00.06.06L466.75 512 338.88 639.86c-.05.04-.06.06-.06.08a.12.12 0 000 .07c0 .03.01.05.05.09l45.02 45.02a.2.2 0 00.09.05.12.12 0 00.07 0c.02 0 .04-.01.08-.05L512 557.25l127.86 127.87c.04.04.06.05.08.05a.12.12 0 00.07 0c.03 0 .05-.01.09-.05l45.02-45.02a.2.2 0 00.05-.09.12.12 0 000-.07v-.02a.27.27 0 00-.05-.06L557.25 512l127.87-127.86c.04-.04.05-.06.05-.08a.12.12 0 000-.07c0-.03-.01-.05-.05-.09l-45.02-45.02a.2.2 0 00-.09-.05.12.12 0 00-.07 0z"
></path>
</svg>
<svg
v-else-if="message.mode === 'warning'"
class="icon-svg"
focusable="false"
data-icon="exclamation-circle"
width="1em"
height="1em"
fill="currentColor"
aria-hidden="true"
viewBox="64 64 896 896"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm-32 232c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V296zm32 440a48.01 48.01 0 010-96 48.01 48.01 0 010 96z"
></path>
</svg>
<svg
v-else-if="message.mode === 'loading'"
width="1em"
height="1em"
fill="currentColor"
class="icon-svg circle"
viewBox="0 0 50 50"
>
<circle class="path" cx="25" cy="25" r="20" fill="none"></circle>
</svg>
<div class="message-content">
{{ message.content || content }}
</div>
</div>
</div>
</TransitionGroup>
</div>
</template>
<style lang="less" scoped>
// 滑动渐变过渡效果
.slide-fade-move,
.slide-fade-enter-active,
.slide-fade-leave-active {
transition: all 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
}
.slide-fade-enter-from,
.slide-fade-leave-to {
transform: translateY(-16px);
-ms-transform: translateY(-16px); /* IE 9 */
-webkit-transform: translateY(-16px); /* Safari and Chrome */
opacity: 0;
}
.slide-fade-leave-active {
position: absolute;
left: 0;
right: 0;
margin: 0 auto;
}
.m-message-wrap {
font-size: 14px;
color: rgba(0, 0, 0, 0.88);
line-height: 1.5714285714285714;
position: fixed;
z-index: 999; // 突出显示该层级
width: 100%;
left: 0;
right: 0;
pointer-events: none; // 保证整个message区域不遮挡背后元素响应鼠标事件
.m-message {
text-align: center;
&:not(:last-child) {
margin-bottom: 8px;
}
.m-message-content {
display: inline-flex;
gap: 8px;
align-items: center;
padding: 9px 12px;
background: #fff;
border-radius: 8px;
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08),
0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 9px 28px 8px rgba(0, 0, 0, 0.05);
pointer-events: auto; // 保证内容区域部分可以正常响应鼠标事件
:deep(.icon-svg) {
display: inline-block;
font-size: 16px;
fill: currentColor;
}
.circle {
display: inline-block;
stroke: currentColor;
animation: loading-rotate 2s linear infinite;
@keyframes loading-rotate {
100% {
transform: rotate(360deg);
}
}
.path {
stroke-dasharray: 90, 150;
stroke-dashoffset: 0;
stroke-width: 5;
stroke-linecap: round;
animation: loading-dash 1.5s ease-in-out infinite;
@keyframes loading-dash {
0% {
stroke-dasharray: 1, 200;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -40px;
}
100% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -120px;
}
}
}
}
.message-content {
display: inline-block;
}
}
.icon-open {
:deep(svg) {
fill: currentColor;
}
}
.icon-info,
.icon-loading {
:deep(svg) {
color: #909399;
fill: currentColor;
}
}
.icon-success {
:deep(svg) {
color: #52c41a;
fill: currentColor;
}
}
.icon-warning {
:deep(svg) {
color: #faad14;
fill: currentColor;
}
}
.icon-error {
:deep(svg) {
color: #ff4d4f;
fill: currentColor;
}
}
}
}
</style>