Files
hospital-rescue-app-v2/uni_modules/tmx-ui/components/x-msg-notice/x-msg-notice.uvue
T
2026-09-24 16:25:22 +08:00

375 lines
10 KiB
Plaintext

<script lang="ts" setup>
import { ref, computed, watch, onMounted, nextTick } from "vue"
import { getUid } from "../../core/util/xCoreUtil.uts"
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
type positionType = "top" | "bottom" | "left" | "right"
/**
* @name 消息通知 xMsgNotice
* @description 本组件可以通过左右拖拉关闭消息,往左拉,左关闭,右拉右关闭,有阻尼回弹效果,可单独设置。
* @page /pages/index/msg-notice
* @category 反馈组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
defineOptions({ name: "xMsgNotice" })
const emits = defineEmits([
/**
* 组件点击事件
*/
'click',
/**
* 等同v-model
*/
'update:modelValue'
])
type xMsgNoticePropsType = {
/**
* 打开和关闭状态
* 等同v-model,如果使用:modelValue将不受控.
*/
modelValue : boolean,
/**
* 动画时间
*/
duration : number,
/**
* 当滑动时小于此值,会回弹到原位。而不执行关闭
*/
threshold : number,
/**
* 圆角,空值时取全局的drawr圆角。
*/
round : string,
/**
* 显示位置top,bottom
*/
position : positionType,
/**
* 距离边界的偏移量
*/
offset : string,
/**
* 背景
*/
bgColor : string,
/**
* 暗黑的背景,如果不提供,读取sheetDarkColor
*/
darkBgColor : string,
/**
* 点击组件是否关闭通知
*/
clickClose : boolean
}
const props = withDefaults(defineProps<xMsgNoticePropsType>(), {
modelValue: true,
duration: 300,
threshold: 50,
round: "",
position: "bottom",
offset: "12px",
bgColor: "white",
darkBgColor: "",
clickClose: true
})
// 响应式数据
const opened = ref<boolean>(false)
const isMoveing = ref<boolean>(false)
const xMsgNoticeRef = ref<UniElement | null>(null)
const id = ref<string>("xMsgNotice-" + getUid())
const _x = ref<number>(0)
const _y = ref<number>(0)
const windowTop = ref<number>(0)
const posStatus = ref<string>('')
// 计算属性
const _round = computed(() : string => {
if (props.round == "") return checkIsCssUnit(xConfig.drawerRadius, xConfig.unit)
return checkIsCssUnit(props.round, xConfig.unit)
})
const _offset = computed(() : object => {
let color = getDefaultColor(props.bgColor);
if (xConfig.dark == 'dark') {
if (props.darkBgColor != '') {
color = getDefaultColor(props.darkBgColor)
} else {
color = getDefaultColor(xConfig.sheetDarkColor)
}
}
if (props.position == 'top') {
let top = checkIsCssUnit(props.offset, xConfig.unit);
// #ifdef WEB
top = `calc(${top} + ${windowTop.value}px)`
// #endif
return {
top: top,
borderRadius: _round.value,
backgroundColor: color
}
}
return {
bottom: checkIsCssUnit(props.offset, xConfig.unit),
borderRadius: _round.value,
backgroundColor: color
}
})
// 方法
function setProperty() : void {
let node = xMsgNoticeRef.value
if (node == null) return;
node.style.setProperty("transition-duration", props.duration.toString() + 'ms')
if (opened.value) {
node.style.setProperty("transform", `scale(1) translate(0%,0%)`)
node.style.setProperty("opacity", `1`)
} else {
if (props.position == 'left' || posStatus.value == 'right') {
node.style.setProperty("transform", `scale(0.84) translate(100%,0%)`)
} else if (props.position == 'top') {
node.style.setProperty("transform", `scale(0.84) translate(0%,-100%)`)
} else if (props.position == 'bottom') {
node.style.setProperty("transform", `scale(0.84) translate(0%,100%)`)
}
node.style.setProperty("opacity", `0`)
}
}
function onClick() : void {
/**
* 组件点击事件
*/
emits('click')
if (props.clickClose) {
opened.value = false;
/**
* 等同v-model,
* 在使用时,要选同步关闭,再同步打开这样循环,
* 类似于ref方法on,off方式。
*/
emits("update:modelValue", opened.value)
setProperty();
}
}
function translateStart(x : number, y : number) {
isMoveing.value = true
let node = xMsgNoticeRef.value
if (node == null) return;
_x.value = x
_y.value = y
node.style.setProperty("transition-duration", '0ms')
}
function translateMove(x0 : number, y0 : number) {
let node = xMsgNoticeRef.value
if (node == null) return;
let x = x0 - _x.value
let y = y0 - _y.value
let absx = Math.abs(x);
let absy = Math.abs(y);
// 竖。
if (absy > absx) {
let xscale = absy < 10 ? 1 : Math.max(0, 1 - Math.abs(y) / 1000)
// 阻尼函数:距离越大,系数越小,形成强阻尼
let dampRatio = 1 / (1 + Math.abs(y) / 50)
if (props.position == 'top') {
if (y < 0) {
// 顶部向上拉:正常跟随
node.style.setProperty("transform", `translateY(${y}px)`)
node.style.setProperty("opacity", xscale)
} else if (y > 0) {
// 顶部向下拉:反向阻尼反馈
let dampY = y * dampRatio
node.style.setProperty("transform", `translateY(${dampY}px)`)
node.style.setProperty("opacity", xscale)
}
} else if (props.position == 'bottom') {
if (y > 0) {
// 底部向下拉:正常跟随
node.style.setProperty("transform", `translateY(${y}px)`)
node.style.setProperty("opacity", xscale)
} else if (y < 0) {
// 底部向上拉:反向阻尼反馈
let dampY = y * dampRatio
node.style.setProperty("transform", `translateY(${dampY}px)`)
node.style.setProperty("opacity", xscale)
}
}
} else {
let xscale = absx < 10 ? 1 : Math.max(0, 1 - Math.abs(x) / 1000)
node.style.setProperty("transform", `translateX(${x}px)`)
node.style.setProperty("opacity", xscale)
}
}
function translateEnd(x0 : number, y0 : number) {
isMoveing.value = false
let node = xMsgNoticeRef.value
if (node == null) return;
let x = x0 - _x.value
let y = y0 - _y.value
let absx = Math.abs(x);
let absy = Math.abs(y);
node.style.setProperty("transition-duration", props.duration.toString() + 'ms')
let didClose = false
if (absy > absx) {
// 垂直方向仅在正确方向且超过阈值时关闭
if (props.position == 'top' && y < 0 && absy > props.threshold) {
opened.value = false
node.style.setProperty("transform", `scale(0.64) translate(0,-100%)`)
node.style.setProperty("opacity", `0`);
posStatus.value = 'top'
didClose = true
} else if (props.position == 'bottom' && y > 0 && absy > props.threshold) {
opened.value = false
node.style.setProperty("transform", `scale(0.64) translate(0,100%)`)
node.style.setProperty("opacity", `0`);
posStatus.value = 'bottom'
didClose = true
}
} else {
// 水平方向任一方向超过阈值关闭
if (absx > props.threshold) {
opened.value = false
if (x > 0) {
node.style.setProperty("transform", `scale(0.64) translate(100%,0)`)
posStatus.value = 'right'
} else {
node.style.setProperty("transform", `scale(0.64) translate(-100%,0)`)
posStatus.value = 'left'
}
node.style.setProperty("opacity", `0`);
didClose = true
}
}
if (didClose) {
/**
* 等同v-model,
* 在使用时,要选同步关闭,再同步打开这样循环,
* 类似于ref方法on,off方式。
*/
emits("update:modelValue", opened.value)
} else {
// 回弹复位
node.style.setProperty("transform", `scale(1) translate(0%,0%)`)
node.style.setProperty("opacity", `1`)
}
}
function mStart(evt : TouchEvent) : void {
const x = evt.changedTouches[0].clientX;
const y = evt.changedTouches[0].clientY
translateStart(x, y)
}
function mMove(evt : TouchEvent) : void {
evt.preventDefault();
let x = evt.changedTouches[0].clientX
let y = evt.changedTouches[0].clientY
translateMove(x, y)
}
function mEnd(evt : TouchEvent) : void {
let x = evt.changedTouches[0].clientX
let y = evt.changedTouches[0].clientY
translateEnd(x, y)
}
// #ifdef WEB
function mouseDown(evt:UniMouseEvent) {
if(evt.type.toLocaleLowerCase().indexOf('mouse')==-1) return;
const x = evt.clientX;
const y = evt.clientY
translateStart(x, y)
}
function mouseMove(evt:UniMouseEvent) {
if(evt.type.toLocaleLowerCase().indexOf('mouse')==-1||!isMoveing.value) return;
evt.preventDefault();
let x = evt.clientX
let y = evt.clientY
translateMove(x, y)
}
function mouseUp(evt:UniMouseEvent) {
if(evt.type.toLocaleLowerCase().indexOf('mouse')==-1 || !isMoveing.value) return;
let x = evt.clientX
let y = evt.clientY
translateEnd(x, y)
}
// #endif
// 监听器
watch(() : boolean => props.modelValue, (newValue : boolean) => {
if (opened.value == newValue) return;
opened.value = newValue;
setProperty();
})
// 生命周期
onMounted(() => {
opened.value = props.modelValue
// #ifdef WEB
windowTop.value = uni.getWindowInfo().windowTop
// #endif
nextTick(() => {
setProperty()
})
})
</script>
<template>
<view :id="id" ref="xMsgNoticeRef" @click="onClick" @touchstart.stop="mStart" @touchmove.stop="mMove"
@touchend="mEnd"
<!-- #ifdef WEB -->
@mousedown="mouseDown"
@mousemove="mouseMove"
@mouseup="mouseUp"
@mouseleave="mouseUp"
<!-- #endif -->
:style="_offset"
class="xMsgNotice">
<!--
@slot 默认插槽
-->
<slot></slot>
</view>
</template>
<style scoped>
.xMsgNotice {
position: fixed;
width: 90%;
left: 5%;
min-height: 20rpx;
transition-duration: 0ms;
transition-property: transform, opacity;
transition-timing-function: cubic-bezier(0, 0.55, 0.45, 1);
transform: scale(0.84) translate(0%, -100%);
opacity: 0;
/* #ifndef APP-HARMONY */
box-shadow: 0 5px 24px rgba(0, 0, 0, 0.06);
/* #endif */
z-index: 88;
/* #ifdef WEB */
user-select: none;
/* 标准浏览器 */
-webkit-user-select: none;
/* WebKit 内核浏览器 */
-moz-user-select: none;
/* Firefox */
-ms-user-select: none;
/* 旧版 IE */
/* #endif */
}
</style>