Files
2026-09-24 16:25:22 +08:00

670 lines
16 KiB
Plaintext

<!-- #ifdef MP-WEIXIN -->
<!-- @vue-docgen-ignore-next-line -->
<script module="xfs" lang="wxs" src="./xfs.wxs"></script>
<!-- #endif -->
<script lang="ts">
import { getUid, checkIsCssUnit } from '../../core/util/xCoreUtil.uts'
import { getDefaultColor } from '../../core/util/xCoreColorUtil.uts'
import { SlotsType } from 'vue'
import { xConfig } from "../../config/xConfig.uts"
type POSITION_TYPE_XY = {
x : number,
y : number
}
/**
* @name 左滑菜单 xSwitchSlider
* @description 常用于对话聊天,订单列表等一些隐藏式按钮设计场景。如果子菜单无法定宽或者被挤夺请写style:flex-shrink: 0;避免。
* @page /pages/index/switch-slider
* @category 反馈组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
export default {
data() {
return {
opened: false,
startX: 0,
startY: 0,
slideX: 0,
_x: 0,
_y: 0,
_tempPop_x:0,
_tempPop_y:0,
dirs: "right",
id: "xSwitchSilder" + getUid(),
idMenuRight: "xSwitchSilderItems" + getUid(),
isMove: false,
dateTime: 0,
diffX: 0,
isSwiper:'none'
}
},
emits: [
/**
* 提供这个函数是因为如果你把此组件套在了scroll中
* 你在ios端根本 无法去阻止 底部的scrollview的滚动,这大概官方是解决不了的
* 冒泡 事件。我只能绕开。
* 此函数会发现是否禁用你外部滚动的指示。
*/
"disabledScrollChange",
/**
* 活动区域被点击时触发。
*/
"click",
/**
* 打开菜单时触发
*/
"open",
/**
* 关闭时触发
*/
"close",
/**
* 触摸开始
*/
"start",
/**
* 触摸结束
*/
"end",
/**
* 触摸中
*/
"move",
/**
* 等同v-model:status
*/
"update:status"
],
slots: Object as SlotsType<{
default : {
status : boolean
},
menu : {
status : boolean
}
}>,
props: {
/**
* 被拖动层的自定义样式
*/
custonmStyle: {
type: String,
default: ""
},
/**
* 菜单容器层自定义样式
*/
custonmMenuStyle: {
type: String,
default: ""
},
/**
* 宽度
*/
width: {
type: String,
default: "100%"
},
/**
* 高度,单位随意
*/
height: {
type: String,
default: "50"
},
/**
* 是否禁用
*/
disabled: {
type: Boolean,
default: false
},
/**
* 当滑动时小于此值,会回弹到原位。
*/
threshold: {
type: Number,
default: 15
},
/**
* 当打开或者松开时的动画时间
*/
duration: {
type: Number,
default: 450
},
/**
* 当前打开状态
*/
status: {
type: Boolean,
default: false
},
/**
* 下边线的颜色
*/
borderColor:{
type:String,
default:"#f5f5f5"
},
/**
* 下边线暗黑的颜色,如果不提供,取全局borderDarkColor
*/
borderDarkColor:{
type:String,
default:""
},
/**
* 让拖动层内容失去响应,会导致顶层里面的内容无法触发事件响应
* 但好处是:拖动更流畅,没有断断续续。请自行根据场景打开和关闭。
* false关闭让内容响应事件,true让内容失去响应。
*/
eventNone:{
type:Boolean,
default:true
}
},
computed: {
_height() : string {
return checkIsCssUnit(this.height, xConfig.unit)
},
_disabled() : boolean {
return this.disabled
},
_custonmStyle() : string {
return this.custonmStyle
},
_custonmMenuStyle() : string {
return this.custonmStyle
},
_borderColor() : string {
if (xConfig.dark == 'dark'&&this.borderDarkColor=='') return getDefaultColor(xConfig.borderDarkColor)
if (xConfig.dark == 'dark'&&this.borderDarkColor!='') return getDefaultColor(this.borderDarkColor)
return getDefaultColor(this.borderColor)
},
_animationFun() : string {
return xConfig.animationFun
}
},
watch: {
status(newval : boolean) {
if (newval == this.opened) return;
if (this.status) {
this.open()
} else {
this.close()
}
}
},
mounted() {
this.getNodeInfo();
},
methods: {
// #ifdef MP-WEIXIN
setOpts(opts){
if(opts?.isMove!=null){
this.isMove = opts.isMove;
}
if(opts?.opened!=null){
this.opened = opts.opened;
}
if(opts?.isSwiper!=null){
this.isSwiper = opts.isSwiper;
}
},
callEmits(args){
if(!args.name) return;
if(args.args!=null&&args.args!=undefined){
this.$emit(args.name,args.args)
}else{
this.$emit(args.name)
if(args.name=='open'|| args.name=='close'){
this.$emit("update:status", args.name=='open')
}
}
},
// #endif
resetMoveData() {
this.startX = 0;
this.startY = 0;
let ele = this.$refs['xSwitchSilder'] as UniElement
ele.style.setProperty("transition-duration", '0ms')
},
getNodeInfo() {
uni.createSelectorQuery().in(this)
.select(".xSwitchSilderItems")
.boundingClientRect().exec((ret) => {
let el = uni.getElementById(this.id as string) as UniElement|null
let nodeinfo = ret[0] as NodeInfo
let width = nodeinfo.width as number
if (this.status&&el!=null) {
this.opened = false;
this.dirs = "left"
el.style.setProperty("transition-duration", this.duration.toString() + 'ms')
el.style.setProperty("left", '-' + width.toString() + 'px')
this.slideX = -width;
}
})
},
getMenuRightwidth() : number {
let idMenuRight = this.$refs['xSwitchSilderItems'] as UniElement
let menurightWidth = idMenuRight.getBoundingClientRect().width;
return menurightWidth
},
eventTransform_start(evt : POSITION_TYPE_XY) {
this.diffX = 0
this.dateTime = new Date().getTime()
this.resetMoveData();
if (this._disabled) return;
this.startX = evt.x - this.slideX;
this._x = evt.x
this._y = evt.y
this.startY = evt.y;
this.isMove = true;
},
eventTransform_move(evt : POSITION_TYPE_XY) {
let el = this.$refs['xSwitchSilder'] as UniElement|null;
if(el==null) return;
let x = evt.x - this.startX;
let realleftdom = el.style.getPropertyValue("left") as string | null
let leftpos = parseInt(realleftdom == null ? '0' : realleftdom)
if (isNaN(leftpos)) {
leftpos = 0
}
let menuwidth = this.getMenuRightwidth()
let diffX = evt.x - this._x;
this._x = evt.x
if (leftpos > 0) {
x = 0
} else if (leftpos < -menuwidth) {
x = -menuwidth
}
x = Math.min(0, x)
x = Math.max(-menuwidth, x)
this.slideX = x;
if (Math.abs(x) > this.threshold) {
if (diffX > 0) {
this.dirs = "right"
} else {
this.dirs = "left"
}
}
if (leftpos >= -menuwidth && leftpos <= 0 && Math.abs(diffX) > 0) {
el.style.setProperty("left", x + 'px')
}
this.diffX = diffX;
},
mStart(evt : UniTouchEvent) {
/**
* 触发时触发
*/
this.$emit("start")
this.dateTime = Date.now()
let event = evt.changedTouches[0];
this._tempPop_x = event.clientX
this._tempPop_y = event.clientY
this.resetMoveData();
if (this._disabled) return;
this.isMove = true;
this.isSwiper = 'none';
this.eventTransform_start({ x: event.clientX, y: event.clientY } as POSITION_TYPE_XY)
},
mMove(evt : UniTouchEvent) {
/**
* 触摸中时触发
*/
this.$emit("move")
let event = evt.changedTouches[0]
if (this._disabled) return;
// 如果y距离大于x距离就允许滑动页面,否则左滑组件。
let y = Math.abs(event.clientY - this._tempPop_y)
let x = Math.abs(event.clientX - this._tempPop_x)
// 判断是否有明显的方向
const hasDirection = Math.max(x, y) > 10
// 判断是否为水平方向
const isHorizontal = x > y
if (hasDirection && this.isSwiper == 'none') {
if (isHorizontal) {
// 水平滑动,启用左滑并阻止页面滚动
this.isSwiper = 'swiper'
evt.preventDefault()
evt.stopPropagation()
} else {
// 垂直滑动,禁用左滑
this.isSwiper = 'off'
}
}
// 只有在启用左滑状态下才执行滑动
if (this.isSwiper == 'swiper') {
this.eventTransform_move({ x: event.clientX, y: event.clientY } as POSITION_TYPE_XY)
}
},
mEnd(evt : UniTouchEvent) {
/**
* 触摸结束时触发
*/
this.$emit("end")
let diffdate = Date.now() -this.dateTime
let diffx = evt.changedTouches[0].clientX - this._tempPop_x
let diffy = evt.changedTouches[0].clientY - this._tempPop_y
if(Math.abs(diffx) == Math.abs(diffy) && diffx==0 && diffdate>50&&diffdate<=250){
this.mClick()
}
let el = this.$refs['xSwitchSilder'] as UniElement | null;
let idMenuRight = this.$refs['xSwitchSilderItems'] as UniElement|null
if(el==null||idMenuRight==null) return;
this.isMove = false;
if (this._disabled||this.isSwiper=='off'){
return
}
let menuwidth = idMenuRight.getBoundingClientRect().width
el.style.setProperty("transition-duration", this.duration.toString() + 'ms')
if (this.dirs == 'left') {
this.slideX = -menuwidth;
el.style.setProperty("left", (-menuwidth).toString() + 'px')
this.opened = true;
/**
* 打开菜单时触发
*/
this.$emit("open")
} else if (this.dirs == 'right') {
el.style.setProperty("left", '0px')
this.slideX = 0
this.opened = false;
/**
* 关闭时触发
*/
this.$emit("close")
}
},
onAniEnd() {
// #ifdef APP||WEB
if (this.dirs == 'left') {
/** 等同v-model:status **/
this.$emit("update:status", true)
this.isMove = false;
}
if (this.dirs == 'right') {
/** 等同v-model:status **/
this.$emit("update:status", false)
this.isMove = false;
}
// #endif
},
mClick() {
/**
* 活动区域被点击时触发。
*/
this.$emit("click")
},
// #ifdef WEB
mmStart(evt : UniMouseEvent) {
window.xSwitchSliderId = this.id
/**
* 触发时触发
*/
this.$emit("start")
this.dateTime = Date.now()
this.resetMoveData();
this._tempPop_x = evt.clientX;
this._tempPop_y = evt.clientY;
if (this._disabled) return;
this.isSwiper = 'none';
this.isMove = true;
this.eventTransform_start({ x: evt.clientX, y: evt.clientY } as POSITION_TYPE_XY)
},
mmMove(evt : UniMouseEvent) {
if(window.xSwitchSliderId != this.id) return;
/**
* 触摸中时触发
*/
this.$emit("move")
if (this._disabled||!this.isMove) return;
// 如果y距离大于x距离就允许滑动页面,否则左滑组件。
let y = Math.abs(evt.clientY - this._tempPop_y)
let x = Math.abs(evt.clientX - this._tempPop_x)
// 判断是否有明显的方向
const hasDirection = Math.max(x, y) > 10
// 判断是否为水平方向
const isHorizontal = x > y
if (hasDirection && this.isSwiper == 'none') {
if (isHorizontal) {
// 水平滑动,启用左滑并阻止页面滚动
this.isSwiper = 'swiper'
evt.preventDefault()
evt.stopPropagation()
} else {
// 垂直滑动,禁用左滑
this.isSwiper = 'off'
}
}
if (this.isSwiper == 'swiper') {
this.eventTransform_move({ x: evt.clientX, y: evt.clientY } as POSITION_TYPE_XY)
}
},
mmEnd(evt:UniMouseEvent) {
if(window.xSwitchSliderId != this.id||!this.isMove) return;
/**
* 触摸结束时触发
*/
this.$emit("end")
let diffdate = Date.now() -this.dateTime
let diffx = evt.clientX - this._tempPop_x
let diffy = evt.clientY - this._tempPop_y
if(Math.abs(diffx) == Math.abs(diffy) && diffx==0 && diffdate>50&&diffdate<=250){
this.mClick()
}
let el = this.$refs['xSwitchSilder'] as UniElement
let idMenuRight = this.$refs['xSwitchSilderItems'] as UniElement
this.isMove = false;
if (this._disabled||this.isSwiper == 'off') return;
let menuwidth = idMenuRight!.getBoundingClientRect().width
el!.style.setProperty("transition-duration", this.duration.toString() + 'ms')
if (this.dirs == 'left') {
this.slideX = -menuwidth;
el!.style.setProperty("left", (-menuwidth).toString() + 'px')
this.opened = true;
/**
* 打开菜单时触发
*/
this.$emit("open")
} else if (this.dirs == 'right') {
el!.style.setProperty("left", '0px')
this.slideX = 0
this.opened = false;
/**
* 关闭时触发
*/
this.$emit("close")
}
},
// #endif
/**
* 打开
* @public
*/
open() {
this.opened = true;
this.dirs = "left"
// #ifdef APP||WEB
let menuwidth = this.getMenuRightwidth()
let el = this.$refs['xSwitchSilder'] as UniElement|null;
if(el==null) return;
el.style.setProperty("transition-duration", this.duration.toString() + 'ms')
el.style.setProperty("left", '-' + menuwidth.toString() + 'px')
this.slideX = -menuwidth;
// #endif
},
/**
* 关闭
* @public
*/
close() {
this.opened = false;
this.dirs = "right"
// #ifdef APP||WEB
let el = this.$refs['xSwitchSilder'] as UniElement|null;
if(el!=null){
el.style.setProperty("transition-duration", this.duration.toString() + 'ms')
el.style.setProperty("left", '0px')
}
// #endif
this.slideX = 0;
}
}
}
</script>
<template>
<view>
<view
class="xSwitchSilder" :style="{minHeight:_height}">
<view
<!-- #ifdef APP||WEB -->
@touchstart="mStart"
@touchmove="mMove"
@touchend="mEnd"
@touchcancel="mEnd"
<!-- #endif -->
<!-- #ifdef WEB -->
@mousedown="mmStart"
@mousemove="mmMove"
@mouseup="mmEnd"
@mouseleave="mmEnd"
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
@touchstart="xfs.mStart"
@touchmove="xfs.mMove"
@touchend="xfs.mEnd"
@touchcancel="xfs.mEnd"
:data-opts="{
disabled:_disabled,
slideX:slideX,
duration:duration,
animationFun:_animationFun,
opened:opened,
threshold:threshold
}"
:change:prop="xfs.propObserver"
:prop="opened"
<!-- #endif -->
:id="id" ref="xSwitchSilder"
:style="[{'transition-timing-function':_animationFun},_custonmStyle]"
class="xSwitchSilderWrap "
@transitionend="onAniEnd">
<view :class="[eventNone?'noEventBySwitchSlider':'']" class="" style="width: 100%;height: 100%;">
<!--
@slot 默认插槽,你的顶层布局可以在这里。 由于vue 的diff机制,去掉5ms的延迟。内容层会失去事件响应。请通过组件的@click
-->
<slot></slot>
</view>
</view>
<view class="xSwitchSilderItemsWrap">
<view ref="xSwitchSilderItems" :id="idMenuRight" class="xSwitchSilderItems" :style="_custonmMenuStyle">
<!--
@slot 菜单插槽
@prop {boolean} status - 当前打开的状态
-->
<slot name="menu" :status="opened"></slot>
</view>
</view>
</view>
<view :style="{backgroundColor:_borderColor,height:'1px'}"></view>
</view>
</template>
<style>
.noEventBySwitchSlider {
pointer-events: none;
}
.xSwitchSilder {
display: flex;
flex-direction: column;
/* #ifdef WEB */
cursor: grab;
/* #endif */
}
/* #ifdef WEB */
.xSwitchSilder:active {
cursor: grabbing;
}
/* #endif */
.xSwitchSilderWrap {
position: absolute;
width: 100%;
height: 100%;
z-index: 2;
right: 0px;
transition-property: left;
transition-duration: 0ms;
}
.xSwitchSilderItemsWrap {
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
}
.xSwitchSilderItems {
flex-shrink: 0;
height: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
/* #ifdef WEB */
cursor: pointer;
/* #endif */
}
</style>