375 lines
10 KiB
Plaintext
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> |