Files
cmgd-mini-app/uni_modules/nutui-uni/components/toast/toast.vue
2026-01-05 12:47:14 +08:00

270 lines
5.8 KiB
Vue

<script lang="ts" setup>
import type { CSSProperties, Ref } from 'vue'
import { computed, defineComponent, inject, onBeforeUnmount, ref, useSlots, watch } from 'vue'
import { CLOSE_EVENT, CLOSED_EVENT, PREFIX, UPDATE_VISIBLE_EVENT } from '../_constants'
import { cloneDeep, getMainClass, getMainStyle, pxCheck } from '../_utils'
import NutIcon from '../icon/icon.vue'
import NutTransition from '../transition/transition.vue'
import { toastDefaultOptions, toastDefaultOptionsKey, toastEmits, toastProps } from './toast'
import type { ToastOptions, ToastType } from './types'
const props = defineProps(toastProps)
const emit = defineEmits(toastEmits)
const slots = useSlots()
const toastOptionsKey = `${toastDefaultOptionsKey}${props.selector || ''}`
const injectToastOptions: Ref<ToastOptions> = inject(toastOptionsKey, ref(cloneDeep(toastDefaultOptions)))
const typeIcons: Record<ToastType, string> = {
text: '',
success: 'success',
error: 'failure',
warning: 'tips',
loading: 'loading',
}
const innerVisible = ref(false)
const toastOptions = ref<ToastOptions>(cloneDeep(props))
const iconName = computed(() => {
const { icon, type } = toastOptions.value
return icon || typeIcons[type!]
})
const hasIcon = computed(() => {
return Boolean(iconName.value)
})
const classes = computed(() => {
const { size, cover, center, type, loadingRotate } = toastOptions.value
return getMainClass(props, componentName, {
[`nut-toast-${size}`]: true,
'nut-toast-cover': cover,
'nut-toast-center': center,
'nut-toast-loading': type === 'loading',
'nut-toast-loading-rotate': loadingRotate,
'nut-toast-has-icon': hasIcon.value,
})
})
const styles = computed(() => {
return getMainStyle(props, {
zIndex: toastOptions.value.zIndex,
})
})
const wrapperStyles = computed(() => {
const value: CSSProperties = {}
const { cover, coverColor, center, bottom } = toastOptions.value
if (cover) {
value.backgroundColor = coverColor
}
else {
if (!center)
value.bottom = pxCheck(bottom!)
}
return value
})
const innerStyles = computed(() => {
const { textAlignCenter, bgColor, cover, center, bottom } = toastOptions.value
const value: CSSProperties = {
textAlign: textAlignCenter ? 'center' : 'left',
backgroundColor: bgColor,
}
if (cover) {
if (!center)
value.bottom = pxCheck(bottom!)
}
return value
})
let timer: NodeJS.Timeout | null = null
function startTimer() {
timer = setTimeout(() => {
hide()
}, toastOptions.value.duration)
}
function destroyTimer() {
if (timer == null)
return
clearTimeout(timer)
timer = null
}
function show(type: ToastType, msg: string, options?: ToastOptions) {
destroyTimer()
toastOptions.value = Object.assign(cloneDeep(toastDefaultOptions), {
visible: true,
type,
msg,
}, options)
innerVisible.value = true
if (toastOptions.value.duration! > 0)
startTimer()
}
function showText(msg: string, options?: ToastOptions) {
show('text', msg, options)
}
function showSuccess(msg: string, options?: ToastOptions) {
show('success', msg, options)
}
function showError(msg: string, options?: ToastOptions) {
show('error', msg, options)
}
function showWarning(msg: string, options?: ToastOptions) {
show('warning', msg, options)
}
function showLoading(msg: string, options?: ToastOptions) {
show('loading', msg, Object.assign({
duration: 0,
cover: true,
}, options))
}
function hide() {
destroyTimer()
innerVisible.value = false
toastOptions.value.visible = false
emit(UPDATE_VISIBLE_EVENT, false)
emit(CLOSE_EVENT)
if (toastOptions.value.onClose) {
toastOptions.value.onClose()
}
}
function handleAfterLeave() {
emit(CLOSED_EVENT)
if (toastOptions.value.onClosed) {
toastOptions.value.onClosed()
}
}
function handleCoverClick() {
if (!toastOptions.value.closeOnClickOverlay)
return
hide()
}
watch(() => props, (value) => {
toastOptions.value = Object.assign(cloneDeep(toastDefaultOptions), value)
if (value.visible)
show(toastOptions.value.type!, toastOptions.value.msg!, toastOptions.value)
else
hide()
}, { deep: true })
watch(injectToastOptions, (value) => {
toastOptions.value = Object.assign(cloneDeep(toastDefaultOptions), value)
if (value.visible)
show(toastOptions.value.type!, toastOptions.value.msg!, toastOptions.value)
else
hide()
})
onBeforeUnmount(() => {
destroyTimer()
})
defineExpose({
showToast: {
text: showText,
success: showSuccess,
fail: showError,
warn: showWarning,
loading: showLoading,
},
hideToast: hide,
text: showText,
success: showSuccess,
error: showError,
warning: showWarning,
loading: showLoading,
hide,
})
</script>
<script lang="ts">
const componentName = `${PREFIX}-toast`
export default defineComponent({
name: componentName,
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
})
</script>
<template>
<NutTransition
:custom-class="classes"
:custom-style="styles"
:show="innerVisible"
name="fade"
@after-leave="handleAfterLeave"
>
<view
class="nut-toast-wrapper"
:style="wrapperStyles"
@click="handleCoverClick"
>
<template v-if="slots.default">
<slot />
</template>
<template v-else>
<view
class="nut-toast-inner"
:style="innerStyles"
>
<view v-if="hasIcon" class="nut-toast-icon-wrapper">
<NutIcon :name="iconName" :size="toastOptions.iconSize" custom-color="#ffffff" />
</view>
<text v-if="toastOptions.title" class="nut-toast-title">
{{ toastOptions.title }}
</text>
<rich-text v-if="toastOptions.msg" class="nut-toast-text" :nodes="toastOptions.msg" />
</view>
</template>
</view>
</NutTransition>
</template>
<style lang="scss">
@import "./index";
</style>