670 lines
16 KiB
Plaintext
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> |