优化代码,修复已知bug

This commit is contained in:
jaylen
2023-06-10 10:04:35 +08:00
parent fa70ad608a
commit 9707895a7c
60 changed files with 11677 additions and 1299 deletions
@@ -0,0 +1,654 @@
<template>
<view class="tn-cascade-selection tn-cascade-selection-class">
<scroll-view
class="selection__scroll-view"
:class="[{'tn-border-solid-bottom': headerLine}]"
:style="[scrollViewStyle]"
scroll-x
scroll-with-animation
:scroll-into-view="scrollViewId"
>
<view class="selection__header" :class="[backgroundColorClass]" :style="[headerStyle]">
<view
v-for="(item, index) in selectedArr"
:key="index"
:id="`select__${index}`"
class="selection__header__item"
:class="[headerItemClass(index)]"
:style="[headerItemStyle(index)]"
@tap.stop="clickNav(index)"
>
{{ item.text }}
<view
v-if="index===currentTab && showActiveLine"
class="selection__header__line"
:style="{backgroundColor: activeLineColor}"
></view>
</view>
</view>
</scroll-view>
<swiper
class="selection__list"
:class="[backgroundColorClass]"
:style="[listStyle]"
:current="currentTab"
:duration="300"
@change="switchTab"
>
<swiper-item
v-for="(item, index) in selectedArr"
:key="index"
>
<scroll-view
class="selection__list__item"
:style="{height: selectionContainerHeight + 'rpx'}"
scroll-y
:scroll-into-view="item.scrollViewId"
>
<view class="selection__list__item--first"></view>
<view
v-for="(subItem, subIndex) in item.list"
:key="subIndex"
:id="`select__${subIndex}`"
class="selection__list__item__cell"
:style="[itemStyle]"
@tap="change(index, subIndex, subItem)"
>
<view
v-if="item.index === subIndex"
class="selection__list__item__icon tn-icon-success"
:style="[itemIconStyle]"
></view>
<image
v-if="subItem.src"
class="selection__list__item__image"
:style="[itemImageStyle]"
:src="subItem.src"
></image>
<view
class="selection__list__item__title"
:class="[{'tn-text-bold': item.index === subIndex && itemActiveBold}]"
:style="[itemTitleStyle(index, subIndex)]"
>
{{ subItem.text }}
</view>
<view
v-if="subItem.subText"
class="selection__list__item__title--sub"
:style="[itemSubTitleStyle]"
>
{{ subItem.subText }}
</view>
</view>
</scroll-view>
</swiper-item>
</swiper>
</view>
</template>
<script>
import componentsColorMixin from '../../libs/mixin/components_color.js'
export default {
name: 'tn-cascade-selection',
mixins: [ componentsColorMixin ],
props: {
// 如果下一级是请求返回,则为第一级数据,否则为所有数据
/* {
text: '', // 标题
subText: '', // 子标题
src: '', // 图片地址
value: 0, // 选中的值
children: [
{
text: '',
subText: '',
value: 0,
children: []
}
]
} */
list: {
type: Array,
default() {
return []
}
},
// 默认选中值
// ['value1','value2','value3']
defaultValue: {
type: Array,
default() {
return []
}
},
// 子集数据通过请求来获取
request: {
type: Boolean,
default: false
},
// request为true时生效, 获取到的子集数据
receiveData: {
type: Array,
default() {
return []
}
},
// 显示header底部细线
headerLine: {
type: Boolean,
default: true
},
// header背景颜色
headerBgColor: {
type: String,
default: ''
},
// 顶部标签栏高度,单位rpx
tabsHeight: {
type: Number,
default: 88
},
// 默认显示文字
text: {
type: String,
default: '请选择'
},
// 选中的颜色
activeColor: {
type: String,
default: '#01BEFF'
},
// 选中后加粗
activeBold: {
type: Boolean,
default: true
},
// 选中显示底部线条
showActiveLine: {
type: Boolean,
default: true
},
// 线条颜色
activeLineColor: {
type: String,
default: '#01BEFF'
},
// icon大小,单位rpx
activeIconSize: {
type: Number,
default: 0
},
// icon颜色
activeIconColor: {
type: String,
default: '#01BEFF'
},
// item图片宽度, 单位rpx
itemImgWidth: {
type: Number,
default: 0
},
// item图片高度, 单位rpx
itemImgHeight: {
type: Number,
default: 0
},
// item图片圆角
itemImgRadius: {
type: String,
default: '50%'
},
// item text颜色
itemTextColor: {
type: String,
default: ''
},
// item text选中颜色
itemActiveTextColor: {
type: String,
default: ''
},
// item text选中加粗
itemActiveBold: {
type: Boolean,
default: true
},
// item text文字大小, 单位rpx
itemTextSize: {
type: Number,
default: 0
},
// item subText颜色
itemSubTextColor: {
type: String,
default: ''
},
// item subText字体大小, 单位rpx
itemSubTextSize: {
type: Number,
default: 0
},
// item样式
itemStyle: {
type: Object,
default() {
return {}
}
},
// selection选项容器高度, 单位rpx
selectionContainerHeight: {
type: Number,
default: 300
}
},
computed: {
scrollViewStyle() {
let style = {}
if (this.headerBgColor) {
style.backgroundColor = this.headerBgColor
}
return style
},
headerStyle() {
let style = {}
style.height = `${this.tabsHeight}rpx`
if (this.backgroundColorStyle) {
style.backgroundColor = this.backgroundColorStyle
}
return style
},
headerItemClass() {
return (index) => {
let clazz = ''
if (index !== this.currentTab) {
clazz += ` ${this.fontColorClass}`
} else {
if (this.activeBold) {
clazz += ' tn-text-bold'
}
}
return clazz
}
},
headerItemStyle() {
return (index) => {
let style = {}
style.color = index === this.currentTab ? this.activeColor : (this.fontColorStyle ? this.fontColorStyle : '')
if (this.fontSizeStyle) {
style.fontSize = this.fontSizeStyle
}
return style
}
},
listStyle() {
let style = {}
style.height = `${this.selectionContainerHeight}rpx`
if (this.backgroundColorStyle) {
style.color = this.backgroundColorStyle
}
return style
},
itemIconStyle() {
let style = {}
if (this.activeIconColor) {
style.color = this.activeIconColor
}
if (this.activeIconSize) {
style.fontSize = this.activeIconSize + 'rpx'
}
return style
},
itemImageStyle() {
let style = {}
if (this.itemImgWidth) {
style.width = this.itemImgWidth + 'rpx'
}
if (this.itemImgHeight) {
style.height = this.itemImgHeight + 'rpx'
}
if (this.itemImgRadius) {
style.borderRadius = this.itemImgRadius
}
return style
},
itemTitleStyle() {
return (index, subIndex) => {
let style = {}
if (index === subIndex) {
if (this.itemActiveTextColor) {
style.color = this.itemActiveTextColor
}
} else {
if (this.itemTextColor) {
style.color = this.itemTextColor
}
}
if (this.itemTextSize) {
style.fontSize = this.itemTextSize + 'rpx'
}
return style
}
},
itemSubTitleStyle() {
let style = {}
if (this.itemSubTextColor) {
style.color = this.itemSubTextColor
}
if (this.itemSubTextSize) {
style.fontSize = this.itemSubTextSize + 'rpx'
}
return {}
}
},
watch: {
list(val) {
this.initData(val, -1)
},
defaultValue(val) {
this.setDefaultValue(val)
},
receiveData(val) {
this.addSubData(val, this.currentTab)
},
},
data() {
return {
// 当前选中的子集
currentTab: 0,
// tabs栏scrollView滚动的位置
scrollViewId: 'select__0',
// 选项数组
selectedArr: []
}
},
created() {
this.setDefaultValue(this.defaultValue)
},
methods: {
// 初始化数据
initData(data, index) {
if (!data || data.length === 0) return
if (this.request) {
// 第一级数据
this.addSubData(data, index)
} else {
this.addSubData(this.getItemList(index, -1), index)
}
},
// 重置数据
reset() {
this.initData(this.list, -1)
},
// 滚动切换
switchTab(e) {
this.currentTab = e.detail.current
this.checkSelectPosition()
},
// 点击标题切换
clickNav(index) {
if (this.currentTab !== index) {
this.currentTab = index
}
},
// 列表数据发生改变
change(index, subIndex, subItem) {
let item = this.selectedArr[index]
if (item.index === subIndex) return
item.index = subIndex
item.text = subItem.text
item.subText = subItem.subText || ''
item.value = subItem.value
item.src = subItem.src || ''
this.$emit('change', {
index: index,
subIndex: subIndex,
...subItem
})
// 如果不是异步加载,则取出对应的数据
if (!this.request) {
let data = this.getItemList(index, subIndex)
this.addSubData(data, index)
}
},
// 设置默认的数据
setDefaultValue(val) {
let defaultValues = val || []
if (defaultValues.length > 0) {
this.selectedArr = this.getItemListWithValues(JSON.parse(JSON.stringify(this.list)), defaultValues)
if (!this.selectedArr) return
this.currentTab = this.selectedArr.length - 1
this.$nextTick(() => {
this.checkSelectPosition()
})
// defaultItemList.map((item) => {
// item.scrollViewId = `select__${item.index}`
// })
// this.selectedArr = defaultItemList
// this.currentTab = defaultItemList.length - 1
// this.$nextTick(() => {
// this.checkSelectPosition()
// })
} else {
this.initData(this.list, -1)
}
},
// 获取对应选项的item数据
getItemList(index, subIndex) {
let list = []
let arr = JSON.parse(JSON.stringify(this.list))
// 初始化数据
if (index === -1) {
list = this.removeChildren(arr)
} else {
// 判断第一项是否已经选择
let value = this.selectedArr[0].index
value = value === -1 ? subIndex : value
list = arr[value].children || []
if (index > 0) {
for (let i = 1; i < index + 1; i++) {
// 获取当前数据选中的序号
let val = index === i ? subIndex : this.selectedArr[i].index
list = list[val].children || []
if (list.length === 0) break
}
}
list = this.removeChildren(list)
}
return list
},
// 根据数组中的值获取对应的item数据
getItemListWithValues(data, values) {
const defaultValues = JSON.parse(JSON.stringify(values))
if (!defaultValues || defaultValues.length === 0) return
// 取出第一个值所对应的item
const itemIndex = data.findIndex((item) => {
return item.value === defaultValues[0]
})
if (itemIndex === -1) return
const item = data[itemIndex]
item.index = itemIndex
item.scrollViewId = `select__${itemIndex}`
item.list = this.removeChildren(JSON.parse(JSON.stringify(data)))
// 判断是否只有1个值
if (defaultValues.length === 1 || (!item.hasOwnProperty('children') || item.children.length === 0)) {
return this.removeChildren([item])
} else {
let selectItemList = []
const children = item.children
selectItemList.push(item)
// 移除已经获取的值
defaultValues.splice(0, 1)
const childrenValue = this.getItemListWithValues(children, defaultValues)
selectItemList = selectItemList.concat(childrenValue)
return this.removeChildren(selectItemList)
}
},
// 删除子元素
removeChildren(data) {
let list = data.map((item) => {
if (item.hasOwnProperty('children')) {
delete item['children']
}
return item
})
return list
},
// 新增子集数据时处理
addSubData(data, index) {
// 判断是否已经完成选择数据或者为初始化数据
if (!data || data.length === 0) {
if (index == -1) return
// 完成选择
let arr = this.selectedArr
// 如果当前选中项的序号比已选数据的长度小,则表示当前重新选择了数据
if (index < arr.length - 1) {
let newArr = arr.slice(0, index + 1)
this.selectedArr = newArr
}
let result = JSON.parse(JSON.stringify(this.selectedArr))
let lastItem = result[result.length - 1] || {}
let text = ''
result.map(item => {
text += item.text
delete item['list']
delete item['scrollViewId']
return item
})
this.$emit('complete', {
result: result,
value: lastItem.value,
text: text,
subText: lastItem.subText,
src: lastItem.src
})
} else {
// 重置数据
let item = [{
text: this.text,
subText: '',
value: '',
src: '',
index: -1,
scrollViewId: 'select__0',
list: data
}]
// 初始化数据
if (index === -1) {
this.selectedArr = item
} else {
// 拼接新旧数据并且判断是否为重新选择了数据(如果为重新选择了数据则重置之后的选项数据)
let retainArr = this.selectedArr.slice(0, index + 1)
this.selectedArr = retainArr.concat(item)
}
this.$nextTick(() => {
this.currentTab = this.selectedArr.length - 1
})
}
},
// 检查当前选中项,并将选项设置位置信息
checkSelectPosition() {
let item = this.selectedArr[this.currentTab]
item.scrollViewId = 'select__0'
this.$nextTick(() => {
setTimeout(() => {
// 设置当前数据滚动到的位置
let val = item.index < 2 ? 0 : Number(item.index - 2)
item.scrollViewId = `select__${val}`
}, 10)
})
// 设置选项滚动到所在的位置
if (this.currentTab > 1) {
this.scrollViewId = `select__${this.currentTab - 1}`
} else {
this.scrollViewId = `select__0`
}
}
}
}
</script>
<style lang="scss" scoped>
.tn-cascade-selection {
width: 100%;
}
.selection {
&__scroll-view {
background-color: #FFFFFF;
}
&__header {
width: 100%;
display: flex;
align-items: center;
position: relative;
&__item {
max-width: 240rpx;
padding: 15rpx 30rpx;
flex-shrink: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
position: relative;
}
&__line {
width: 60rpx;
height: 6rpx;
border-radius: 4rpx;
position: absolute;
bottom: 0;
right: 0;
left: 50%;
transform: translateX(-50%);
}
}
&__list {
background-color: #FFFFFF;
&__item {
&--first {
width: 100%;
height: 20rpx;
}
&__cell {
width: 100%;
display: flex;
align-items: center;
padding: 20rpx 30rpx;
}
&__icon {
margin-right: 12rpx;
font-size: 24rpx;
}
&__image {
width: 40rpx;
height: 40rpx;
margin-right: 12rpx;
flex-shrink: 0;
}
&__title {
word-break: break-all;
color: #333333;
font-size: 28rpx;
&--sub {
margin-left: 20rpx;
word-break: break-all;
color: $tn-font-sub-color;
font-size: 24rpx;
}
}
}
}
}
</style>
+332
View File
@@ -0,0 +1,332 @@
var cropper = {
// 画布x轴起点
cutX: 0,
// 画布y轴起点
cutY: 0,
// 触摸点信息(手指与图片中心点的相对位置)
touchRelactive: [{
x: 0,
y: 0
}],
// 双指触摸时斜边的长度
hypotenuseLength:0,
// 是否结束触摸
touchEndFlag: false,
// 画布宽高
canvasWidth: 0,
canvasHeight: 0,
// 图片宽高
imgWidth: 0,
imgHeight: 0,
// 图片缩放比例
scale: 1,
// 图片旋转角度
angle: 0,
// 图片上边距
imgTop: 0,
// 图片左边距
imgLeft: 0,
// 窗口宽高
windowWidth: 0,
windowHeight: 0,
init: true
}
function bool(str) {
return str === 'true' || str === true
}
function propChange(prop, oldProp, ownerInstance, instance) {
if (prop && prop !== 'null') {
var params = prop.split(',')
var type = +params[0]
var dataset = instance.getDataset()
if (cropper.init || type == 4) {
cropper.canvasWidth = +dataset.width
cropper.canvasHeight = +dataset.height
cropper.imgTop = +dataset.windowheight / 2
cropper.imgLeft = +dataset.windowwidth / 2
cropper.imgWidth = +dataset.imgwidth
cropper.imgHeight = +dataset.imgheight
cropper.windowHeight = +dataset.windowheight
cropper.windowWidth = +dataset.windowwidth
cropper.init = false
} else if (type == 2 || type == 3) {
cropper.imgWidth = +dataset.imgwidth
cropper.imgHeight = +dataset.imgheight
}
cropper.angle = +dataset.angle
if (type == 3) {
imgTransform(ownerInstance)
}
switch(type) {
case 1:
setCutCenter(ownerInstance)
// // 设置裁剪框大小
computeCutSize(ownerInstance)
// // 检查裁剪框是否在范围内
cutDetectionPosition(ownerInstance)
break
case 2:
setCutCenter(ownerInstance)
break
case 3:
imgMarginDetectionScale(ownerInstance)
break
case 4:
imageReset(ownerInstance)
break
case 5:
setCutCenter(ownerInstance)
break
default:
break
}
}
}
function touchStart(event, ownerInstance) {
var touch = event.touches || event.changedTouches
cropper.touchEndFlag = false
if (touch.length === 1) {
cropper.touchRelactive[0] = {
x: touch[0].pageX - cropper.imgLeft,
y: touch[0].pageY - cropper.imgTop
}
} else {
var width = Math.abs(touch[0].pageX - touch[1].pageX)
var height = Math.abs(touch[0].pageY - touch[1].pageY)
cropper.touchRelactive = [{
x: touch[0].pageX - cropper.imgLeft,
y: touch[0].pageY - cropper.imgTop
},{
x: touch[1].pageX - cropper.imgLeft,
y: touch[1].pageY - cropper.imgTop
}]
cropper.hypotenuseLength = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2))
}
}
function touchMove(event, ownerInstance) {
var touch = event.touches || event.changedTouches
if (cropper.touchEndFlag) return
moveDuring(ownerInstance)
if (event.touches.length === 1) {
var left = touch[0].pageX - cropper.touchRelactive[0].x,
top = touch[0].pageY - cropper.touchRelactive[0].y;
cropper.imgLeft = left
cropper.imgTop = top
imgTransform(ownerInstance)
imgMarginDetectionPosition(ownerInstance)
} else {
var dataset = event.instance.getDataset()
var minScale = +dataset.minscale
var maxScale = +dataset.maxscale
var width = Math.abs(touch[0].pageX - touch[1].pageX),
height = Math.abs(touch[0].pageY - touch[1].pageY),
hypotenuse = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2)),
scale = cropper.scale * (hypotenuse / cropper.hypotenuseLength),
current_deg = 0;
scale = scale <= minScale ? minScale : scale
scale = scale >= maxScale ? maxScale : scale
cropper.scale = scale
imgMarginDetectionScale(ownerInstance, true)
var touchRelative = [{
x: touch[0].pageX - cropper.imgLeft,
y: touch[0].pageY - cropper.imgTop
}, {
x: touch[1].pageX - cropper.imgLeft,
y: touch[1].pageY - cropper.imgTop
}]
cropper.touchRelactive = touchRelative
cropper.hypotenuseLength = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2))
// 更新视图
cropper.angle = cropper.angle + current_deg
imgTransform(ownerInstance)
}
}
function touchEnd(event, ownerInstance) {
cropper.touchEndFlag = true
moveStop(ownerInstance)
updateData(ownerInstance)
}
function moveDuring(ownerInstance) {
if (!ownerInstance) return
ownerInstance.callMethod('moveDuring')
}
function moveStop(ownerInstance) {
if (!ownerInstance) return
ownerInstance.callMethod('moveStop')
}
function setCutCenter(ownerInstance) {
var cutX = (cropper.windowWidth - cropper.canvasWidth) * 0.5
var cutY = (cropper.windowHeight - cropper.canvasHeight) * 0.5
cropper.imgTop = cropper.imgTop - cropper.cutY + cutY
cropper.cutY = cutY
cropper.imgLeft = cropper.imgLeft - cropper.cutX + cutX
cropper.cutX = cutX
cutDetectionPosition(ownerInstance)
imgTransform(ownerInstance)
updateData(ownerInstance)
}
// 检测剪裁框位置是否在允许的范围内(屏幕内)
function cutDetectionPosition(ownerInstance) {
var windowHeight = cropper.windowHeight,
windowWidth = cropper.windowWidth;
// 检测上边距是否在范围内
var cutDetectionPositionTop = function() {
if (cropper.cutY < 0) {
cropper.cutY = 0
}
if (cropper.cutY > windowHeight - cropper.canvasHeight) {
cropper.cutY = windowHeight - cropper.canvasHeight
}
}
// 检测左边距是否在范围内
var cutDetectionPositionLeft = function() {
if (cropper.cutX < 0) {
cropper.cutX = 0
}
if (cropper.cutX > windowWidth - cropper.canvasWidth) {
cropper.cutX = windowWidth - cropper.canvasWidth
}
}
// 裁剪框坐标处理(如果只写一个参数则另一个默认为0,都不写默认为居中)
if (cropper.cutX === null && cropper.cutY === null) {
var cutX = (windowWidth - cropper.canvasWidth) * 0.5,
cutY = (windowHeight - cropper.canvasHeight) * 0.5;
cropper.cutX = cutX
cropper.cutY = cutY
} else if (cropper.cutX !== null && cropper.cutX !== null) {
cutDetectionPositionTop()
cutDetectionPositionLeft()
} else if (cropper.cutX !== null && cropper.cutY === null) {
cutDetectionPositionLeft()
cropper.cutY = (windowHeight - cropper.canvasHeight) / 2
} else if (cropper.cutX === null && cropper.cutY !== null) {
cutDetectionPositionTop()
cropper.cutX = (windowWidth - cropper.canvasWidth) / 2
}
}
// 图片边缘检测-缩放
function imgMarginDetectionScale(ownerInstance, delay) {
var scale = cropper.scale,
imgWidth = cropper.imgWidth,
imgHeight = cropper.imgHeight;
if ((cropper.angle / 90) % 2) {
imgWidth = cropper.imgHeight
imgHeight = cropper.imgWidth
}
if (imgWidth * scale < cropper.canvasWidth) {
scale = cropper.canvasWidth / imgWidth
}
if (imgHeight * scale < cropper.canvasHeight) {
scale = Math.max(scale, cropper.canvasHeight / imgHeight)
}
imgMarginDetectionPosition(ownerInstance, scale, delay)
}
// 图片边缘检测-位置
function imgMarginDetectionPosition(ownerInstance, scale, delay) {
var left = cropper.imgLeft,
top = cropper.imgTop,
imgWidth = cropper.imgWidth,
imgHeight = cropper.imgHeight;
scale = scale || cropper.scale
if ((cropper.angle / 90) % 2) {
imgWidth = cropper.imgHeight
imgHeight = cropper.imgWidth
}
left = cropper.cutX + (imgWidth * scale) / 2 >= left ? left : cropper.cutX + (imgWidth * scale) / 2
left = cropper.cutX + cropper.canvasWidth - (imgWidth * scale) / 2 <= left ? left : cropper.cutX + cropper.canvasWidth - (imgWidth * scale) / 2
top = cropper.cutY + (imgHeight * scale) / 2 >= top ? top : cropper.cutY + (imgHeight * scale) / 2
top = cropper.cutY + cropper.canvasHeight - (imgHeight * scale) / 2 <= top ? top : cropper.cutY + cropper.canvasHeight - (imgHeight * scale) / 2
cropper.imgLeft = left
cropper.imgTop = top
cropper.scale = scale
if (!delay || delay === 'null') {
imgTransform(ownerInstance)
}
}
// 改变截取值大小
function computeCutSize(ownerInstance) {
if (cropper.canvasWidth > cropper.windowWidth) {
cropper.canvasWidth = cropper.windowWidth
} else if (cropper.canvasWidth + cropper.cutX > cropper.windowWidth) {
cropper.cutX = cropper.windowWidth - cropper.cutX
}
if (cropper.canvasHeight > cropper.windowHeight) {
cropper.canvasHeight = cropper.windowHeight
} else if (cropper.canvasHeight + cropper.cutY > cropper.windowHeight) {
cropper.cutY = cropper.windowHeight - cropper.cutY
}
}
// 图片动画
function imgTransform(ownerInstance) {
try {
var image = ownerInstance.selectComponent('.tn-cropper__image')
if (!image) return
var x = cropper.imgLeft - cropper.imgWidth / 2,
y = cropper.imgTop - cropper.imgHeight / 2;
image.setStyle({
'transform': 'translate3d('+ x + 'px,' + y + 'px,0) scale(' + cropper.scale +') rotate(' + cropper.angle + 'deg)'
})
} catch(e) {
}
}
// 图片重置
function imageReset(ownerInstance) {
cropper.scale = 1
cropper.angle = 0
imgTransform(ownerInstance)
}
// 高度变化
function canvasHeight(ownerInstance) {
if (!ownerInstance) return
computeCutSize(ownerInstance)
}
// 宽度变化
function canvasWidth(ownerInstance) {
if (!ownerInstance) return
computeCutSize(ownerInstance)
}
// 更新数据
function updateData(ownerInstance) {
if (!ownerInstance) return
ownerInstance.callMethod('change', {
cutX: cropper.cutX,
cutY: cropper.cutY,
imgWidth: cropper.imgWidth,
imgHeight: cropper.imgHeight,
scale: cropper.scale,
angle: cropper.angle,
imgTop: cropper.imgTop,
imgLeft: cropper.imgLeft
})
}
module.exports = {
touchStart: touchStart,
touchMove: touchMove,
touchEnd: touchEnd,
propChange: propChange
}
@@ -0,0 +1,574 @@
<template>
<view class="tn-cropper-class tn-cropper" @touchmove.stop.prevent="stop">
<image
v-if="imageUrl"
:src="imageUrl"
class="tn-cropper__image"
:style="{
width: (imgWidth ? imgWidth : width) + 'px',
height: (imgHeight ? imgHeight : height) + 'px',
transitionDuration: (animation ? 0.3 : 0) + 's'
}"
mode="widthFix"
:data-minScale="minScale"
:data-maxScale="maxScale"
@load="imageLoad"
@error="imageLoad"
@touchstart="wxs.touchStart"
@touchmove="wxs.touchMove"
@touchend="wxs.touchEnd"
></image>
<view
class="tn-cropper__wrapper"
:style="{
width: width + 'px',
height: height + 'px',
borderRadius: isRound ? '50%' : '0'
}"
>
<view
class="tn-cropper__border"
:style="{
border: borderStyle,
borderRadius: isRound ? '50%' : '0',
}"
:prop="prop"
:change:prop="wxs.propChange"
:data-width="width"
:data-height="height"
:data-windowHeight="systemInfo.windowHeight || 600"
:data-windowWidth="systemInfo.windowWidth || 400"
:data-imgTop="imgTop"
:data-imgWidth="imgWidth"
:data-imgHeight="imgHeight"
:data-angle="angle"
></view>
</view>
<canvas
class="tn-cropper__canvas"
:style="{
width: width * scaleRatio + 'px',
height: height * scaleRatio + 'px'
}"
:canvas-id="CANVAS_ID"
:id="CANVAS_ID"
:disable-scroll="true"
></canvas>
<view
v-if="!custom"
class="tn-cropper__tabbar"
>
<view class="tn-cropper__tabbar__btn tn-cropper__tabber__cancel" @tap.stop="back">取消</view>
<view class="tn-cropper__tabbar__rotate" :class="[`tn-icon-${rotateIcon}`]" @tap.stop="setAngle"></view>
<view class="tn-cropper__tabbar__btn tn-cropper__tabber__confirm" @tap.stop="getCutImage">完成</view>
</view>
</view>
</template>
<script src="./index.wxs" lang="wxs" module="wxs"></script>
<script>
export default {
name: 'tn-cropper',
props: {
// 图片路径
imageUrl: {
type: String,
default: ''
},
// 裁剪框高度 px
height: {
type: Number,
default: 280
},
// 裁剪框的宽度 px
width: {
type: Number,
default: 280
},
// 是否为圆形裁剪框
isRound: {
type: Boolean,
default: false
},
// 裁剪框边框样式
borderStyle: {
type: String,
default: '1rpx solid #FFF'
},
// 生成的图片尺寸相对于裁剪框的比例
scaleRatio: {
type: Number,
default: 1
},
// 裁剪后的图片质量
// 取值范围为:(0, 1]
quality: {
type: Number,
default: 0.8
},
// 是否返回base64(H5默认为base64)
returnBase64: {
type: Boolean,
default: false
},
// 图片旋转角度
rotateAngle: {
type: Number,
default: 0
},
// 图片最小缩放比
minScale: {
type: Number,
default: 0.5
},
// 图片最大缩放比
maxScale: {
type: Number,
default: 2
},
// 自定义操作栏(设置后会隐藏默认的底部操作栏)
custom: {
type: Boolean,
default: false
},
// 是否在值发生改变的时候开始裁剪
// custom为true时生效
startCutting: {
type: Boolean,
default: false
},
// 裁剪时是否显示loading
loading: {
type: Boolean,
default: true
},
// 旋转图片图标
rotateIcon: {
type: String,
default: 'circle-arrow'
}
},
data() {
return {
// canvas容器id
CANVAS_ID: 'tn-cropper-canvas',
// 移动裁剪超时时间定时器
TIME_CUT_CENTER: null,
// canvas容器
ctx: null,
// 画布x轴起点
cutX: 0,
// 画布y轴起点
cutY: 0,
// 图片宽度
imgWidth: 0,
// 图片高度
imgHeight: 0,
// 图片底部位置
imgTop: 0,
// 图片左边位置
imgLeft: 0,
// 图片缩放比
scale: 1,
// 图片旋转角度
angle: 0,
// 开启动画过渡效果
animation: false,
// 动画定时器
animationTime: null,
// 系统信息
systemInfo: {},
// 传递的参数
prop: '',
// 标记是否发生改变
sizeChange: 0,
angleChange: 0,
resetChange: 0,
centerChange: 0
}
},
watch: {
imageUrl(val) {
this.imageReset()
this.showLoading()
uni.getImageInfo({
src: val,
success: (res) => {
// 计算图片尺寸
this.imgComputeSize(res.width, res.height)
this.angleChange++
this.prop = `3,${this.angleChange}`
},
fail: (err) => {
console.log(err);
this.imgComputeSize()
this.angleChange++
this.prop = `3,${this.angleChange}`
}
})
},
isRound(val) {
if (val) {
this.$nextTick(() => {
this.imageReset()
})
}
},
rotateAngle(val) {
this.animation = true
this.angle = val
this.angleChanged(val)
},
animation(val) {
clearTimeout(this.animationTime)
if (val) {
this.animationTime = setTimeout(() => {
this.animation = false
}, 200)
}
},
startCutting(val) {
if (this.custom && val) {
this.getCutImage()
}
}
},
mounted() {
this.systemInfo = uni.getSystemInfoSync()
this.imgTop = this.systemInfo.windowHeight / 2
this.imgLeft = this.systemInfo.windowWidth / 2
this.ctx = uni.createCanvasContext(this.CANVAS_ID, this)
// 初始化
this.$nextTick(() => {
this.prop = '1,1'
})
setTimeout(() => {
this.$emit('ready', {})
}, 200)
},
methods: {
// 将网络图片转换为本地图片【同步执行】
async getLocalImage(url) {
return await new Promise((resolve, reject) => {
uni.downloadFile({
url: url,
success: (res) => {
resolve(res.tempFilePath)
},
fail: (err) => {
reject(false)
}
})
})
},
// 返回裁剪后的图片信息
getCutImage() {
if (!this.imageUrl) {
uni.showToast({
title: '请选择图片',
icon: 'none'
})
return
}
this.loading && this.showLoading()
const draw = async () => {
// 图片实际大小
let imgWidth = this.imgWidth * this.scale * this.scaleRatio
let imgHeight = this.imgHeight * this.scale * this.scaleRatio
// canvas和图片的相对距离
let xpos = this.imgLeft - this.cutX
let ypos = this.imgTop - this.cutY
let imgUrl = this.imageUrl
// #ifdef APP-PLUS || MP-WEIXIN
if (~this.imageUrl.indexOf('https:')) {
imgUrl = await this.getLocalImage(this.imageUrl)
}
// #endif
// 旋转画布
this.ctx.translate(xpos * this.scaleRatio, ypos * this.scaleRatio)
// 如果时圆形则截取圆形
if (this.isRound) {
const r = this.width > this.height ? Math.floor(this.height / 2) : Math.floor(this.width / 2)
let translateX = Math.floor(this.width / 2)
let translateY = Math.floor(this.height / 2)
this.ctx.beginPath()
this.ctx.arc(translateX - (xpos * this.scaleRatio), translateY - (ypos * this.scaleRatio), r, 0, (360 * Math.PI) / 180)
this.ctx.closePath()
this.ctx.stroke()
this.ctx.clip()
}
this.ctx.rotate((this.angle * Math.PI) / 180)
this.ctx.drawImage(imgUrl, -imgWidth / 2, -imgHeight / 2, imgWidth, imgHeight)
// 清空后再继续绘制
this.ctx.draw(false, () => {
let params = {
width: this.width * this.scaleRatio,
height: Math.round(this.height * this.scaleRatio),
destWidth: this.width * this.scaleRatio,
destHeight: Math.round(this.height) * this.scaleRatio,
fileType: 'png',
quality: this.quality
}
let data = {
url: '',
base64: '',
width: this.width * this.scaleRatio,
height: this.height * this.scaleRatio
}
// #ifdef MP-ALIPAY
if (this.returnBase64) {
this.ctx.toDataURL(params).then((urlData) => {
data.base64 = urlData
this.loading && uni.hideLoading()
this.$emit('cropper', data)
})
} else {
this.ctx.toTempFilePath({
...params,
success: (res) => {
data.url = res.apFilePath
this.loading && uni.hideLoading()
this.$emit('cropper', data)
}
})
}
// #endif
let base64Flag = this.returnBase64
// #ifndef MP-ALIPAY
// #ifdef MP-BAIDU || MP-TOUTIAO || H5
base64Flag = false
// #endif
if (base64Flag) {
uni.canvasGetImageData({
canvasId: this.CANVAS_ID,
x: 0,
y: 0,
width: this.width * this.scaleRatio,
height: Math.round(this.height * this.scaleRatio),
success: (res) => {
const arrayBuffer = new Uint8Array(res.data)
const base64 = uni.arrayBufferToBase64(arrayBuffer)
data.base64 = base64
this.loading && uni.hideLoading()
this.$emit('cropper', data)
}
}, this)
} else {
uni.canvasToTempFilePath({
...params,
canvasId: this.CANVAS_ID,
success: (res) => {
data.url = res.tempFilePath
// #ifdef H5
data.base64 = res.tempFilePath
// #endif
this.loading && uni.hideLoading()
this.$emit('cropper', data)
}
}, this)
}
// #endif
})
}
draw()
},
// 修改图片后触发的函数
change(e) {
this.cutX = e.cutX || 0
this.cutY = e.cutY || 0
this.imgWidth = e.imgWidth || this.imgWidth
this.imgHeight = e.imgHeight || this.imgHeight
this.scale = e.scale || 1
this.angle = e.angle || 0
this.imgTop = e.imgTop || 0
this.imgLeft = e.imgLeft || 0
},
// 重置图片
imageReset() {
this.scale = 1
this.angle = 0
let systemInfo = this.systemInfo.windowHeight ? this.systemInfo : uni.getSystemInfoSync()
this.imgTop = systemInfo.windowHeight / 2
this.imgLeft = systemInfo.windowWidth / 2
this.resetChange++
this.prop = `4,${this.resetChange}`
// 初始旋转角度
this.$emit('initAngle', {})
},
// 图片的生成的尺寸
imgComputeSize(width, height) {
// 默认按图片的最小边 = 对应的裁剪框尺寸
let imgWidth = width,
imgHeight = height;
if (imgWidth && imgHeight) {
if (imgWidth / imgHeight > this.width / this.height) {
imgHeight = this.height
imgWidth = (width / height) * imgHeight
} else {
imgWidth = this.width
imgHeight = (height / width) * imgWidth
}
} else {
let systemInfo = this.systemInfo.windowHeight ? this.systemInfo : uni.getSystemInfoSync()
imgWidth = systemInfo.windowWidth
imgHeight = 0
}
this.imgWidth = imgWidth
this.imgHeight = imgHeight
this.sizeChange++
this.prop = `2,${this.sizeChange}`
},
// 图片加载完毕
imageLoad(e) {
this.imageReset()
uni.hideLoading()
this.$emit('imageLoad', {})
},
// 移动结束
moveStop() {
clearTimeout(this.TIME_CUT_CENTER)
this.TIME_CUT_CENTER = setTimeout(() => {
this.centerChange++
this.prop = `5,${this.centerChange}`
}, 688)
},
// 移动中
moveDuring() {
clearTimeout(this.TIME_CUT_CENTER)
},
// 显示加载框
showLoading() {
uni.showLoading({
title: '请稍等......',
mask: true
})
},
// 停止
stop() {},
// 取消/返回
back() {
uni.navigateBack()
},
// 角度改变
angleChanged(val) {
this.moveStop()
if (val % 90) {
this.angle = Math.round(val / 90) * 90
}
this.angleChange++
this.prop = `3,${this.angleChange}`
},
// 设置角度
setAngle() {
this.animation = true
this.angle = this.angle + 90
this.angleChanged(this.angle)
}
}
}
</script>
<style lang="scss" scoped>
.tn-cropper {
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.7);
position: fixed;
top: 0;
left: 0;
z-index: 1;
&__image {
width: 100%;
border-style: none;
position: absolute;
top: 0;
left: 0;
z-index: 2;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
transform-origin: center;
}
&__canvas {
position: fixed;
z-index: 10;
left: -2000px;
top: -2000px;
pointer-events: none;
}
&__wrapper {
position: fixed;
z-index: 4;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
border: 3000px solid rgba(0, 0, 0, 0.55);
pointer-events: none;
box-sizing: content-box;
}
&__border {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
pointer-events: none;
}
&__tabbar {
width: 100%;
height: 120rpx;
padding: 0 40rpx;
box-sizing: border-box;
position: fixed;
left: 0;
bottom: 0;
z-index: 99;
display: flex;
align-items: center;
justify-content: space-between;
color: #FFFFFF;
font-size: 32rpx;
&::after {
content: '';
position: absolute;
top: 0;
right: 0;
left: 0;
border-top: 1rpx solid rgba(255, 255, 255, 0.2);
-webkit-transform: scaleY(0.5) translateZ(0);
transform: scaleY(0.5) translateZ(0);
transform-origin: 0 100%;
}
&__btn {
height: 80rpx;
display: flex;
align-items: center;
}
&__rotate {
width: 44rpx;
height: 44rpx;
font-size: 40rpx;
text-align: center;
}
}
}
</style>
@@ -0,0 +1,288 @@
function setTimeout(instance, cb, time) {
if (time > 0) {
var s = getDate().getTime()
var fn = function () {
if (getDate().getTime() - s > time) {
cb && cb()
} else
instance.requestAnimationFrame(fn)
}
fn()
}
else
cb && cb()
}
// 判断触摸的移动方向
function decideSwiperDirection(startTouches, currentTouches, vertical) {
// 震动偏移容差
var toleranceShake = 150
// 移动容差
var toleranceTranslate = 10
if (!vertical) {
// 水平方向移动
if (Math.abs(currentTouches.y - startTouches.y) <= toleranceShake) {
// console.log(currentTouches.x, startTouches.x);
if (Math.abs(currentTouches.x - startTouches.x) > toleranceTranslate) {
if (currentTouches.x - startTouches.x > 0) {
return 'right'
} else if (currentTouches.x - startTouches.x < 0) {
return 'left'
}
}
}
} else {
// 垂直方向移动
if (Math.abs(currentTouches.x - startTouches.x) <= toleranceShake) {
// console.log(currentTouches.x, startTouches.x);
if (Math.abs(currentTouches.y - startTouches.y) > toleranceTranslate) {
if (currentTouches.y - startTouches.y > 0) {
return 'down'
} else if (currentTouches.y - startTouches.y < 0) {
return 'up'
}
}
}
}
return ''
}
// swiperItem参数数据更新
var itemDataObserver = function(newVal, oldVal, ownerInstance, instance) {
if (!newVal || newVal === 'undefined') return
var state = ownerInstance.getState()
state.itemData = newVal
}
// swiperIndex数据更新
var currentIndexObserver = function(newVal, oldVal, ownerInstance, instance) {
if ((!newVal && newVal != 0) || newVal === 'undefined') return
var state = ownerInstance.getState()
state.currentIndex = newVal
}
// containerData数据更新
var containerDataObserver = function(newVal, oldVal, ownerInstance, instance) {
if (!newVal || newVal === 'undefined') return
var state = ownerInstance.getState()
state.containerData = newVal
}
// 开始触摸
var touchStart = function(event, ownerInstance) {
console.log('touchStart');
var instance = event.instance
var dataset = instance.getDataset()
var state = ownerInstance.getState()
var itemData = state.itemData
var containerData = state.containerData
// 由于当前SwiperIndex初始为0,可能会导致swiperIndex数据没有更新
if (!state.currentIndex || state.currentIndex === 'undefined') {
state.currentIndex = 0
}
if (!containerData || containerData.circular === 'undefined') {
containerData.circular = false
}
state.containerData = containerData
// 如果当前切换动画还没执行结束,再次触摸会重新加载对应的swiperContainer的信息
// console.log(containerData.animationFinish);
if (!containerData.animationFinish) {
ownerInstance.callMethod('changeParentSwiperContainerStyleStatus',{
status: 'reload'
})
}
// 判断是否为为当前显示的SwiperItem
if (itemData.index != state.currentIndex) return
var touches = event.changedTouches[0]
if (!touches) return
// 标记滑动开始时间
state.touchStartTime = getDate().getTime()
// 记录当前滑动开始的x,y坐标
state.touchRelactive = {
x: touches.pageX,
y: touches.pageY
}
// 记录触摸id,用于处理多指的情况
state.touchId = touches.identifier
// 标记开始触摸
state.touching = true
ownerInstance.callMethod('updateTouchingStatus', {
status: true
})
}
// 正在移动
var touchMove = function(event, ownerInstance) {
console.log('touchMove');
var instance = event.instance
var dataset = instance.getDataset()
var state = ownerInstance.getState()
var itemData = state.itemData
var containerData = state.containerData
// 判断是否为为当前显示的SwiperItem
if (itemData.index != state.currentIndex) return
// 判断是否开始触摸
if (!state.touching) return
var touches = event.changedTouches[0]
if (!touches) return
// 判断是否为同一个触摸点
if (state.touchId != touches.identifier) return
var currentTouchRelactive = {
x: touches.pageX,
y: touches.pageY
}
// 计算相对位移比例
if (containerData.vertical) {
var touchDistance = currentTouchRelactive.y - state.touchRelactive.y
var itemHeight = itemData.itemHeight
var distanceRate = touchDistance / itemHeight
// console.log(currentTouchRelactive.y, touchDistance, itemHeight, distanceRate);
// 判断是否为衔接轮播,如果不是衔接轮播,如果当前为第一个swiperItem并且向下滑、当前为最后一个swiperItem并且向上滑时不进行操作
if (!containerData.circular &&
((state.currentIndex === 0 && touchDistance > 0) || (state.currentIndex === containerData.swiperItemLength - 1 && touchDistance < 0))
) {
return
}
// 如果超出了距离则不进行操作
if((Math.abs(touchDistance) > (itemData.itemTop + itemData.itemHeight))) {
ownerInstance.callMethod('updateParentSwiperContainerStyle', {
value: distanceRate < 0 ? -1 : 1
})
return
}
} else {
var touchDistance = currentTouchRelactive.x - state.touchRelactive.x
var itemWidth = itemData.itemWidth
var distanceRate = touchDistance / itemWidth
// console.log(currentTouchRelactive.x, touchDistance, itemWidth, distanceRate);
// 判断是否为衔接轮播,如果不是衔接轮播,如果当前为第一个swiperItem并且向右滑、当前为最后一个swiperItem并且向左滑时不进行操作
if (!containerData.circular &&
((state.currentIndex === 0 && touchDistance > 0) || (state.currentIndex === containerData.swiperItemLength - 1 && touchDistance < 0))
) {
return
}
// 如果超出了距离则不进行操作
if((Math.abs(touchDistance) > (itemData.itemLeft + itemData.itemWidth))) {
ownerInstance.callMethod('updateParentSwiperContainerStyle', {
value: distanceRate < 0 ? -1 : 1
})
return
}
}
ownerInstance.callMethod('updateParentSwiperContainerStyle', {
value: distanceRate
})
}
// 移动结束
var touchEnd = function(event, ownerInstance) {
console.log('touchEnd');
var instance = event.instance
var dataset = instance.getDataset()
var state = ownerInstance.getState()
var itemData = state.itemData
var containerData = state.containerData
// 判断是否为为当前显示的SwiperItem
if (itemData.index != state.currentIndex) return
// 判断是否开始触摸
if (!state.touching) return
var touches = event.changedTouches[0]
if (!touches) return
// 判断是否为同一个触摸点
if (state.touchId != touches.identifier) return
var currentTime = getDate().getTime()
var currentTouchRelactive = {
x: touches.pageX,
y: touches.pageY
}
if (containerData.vertical) {
// 判断触摸移动方向
var direction = decideSwiperDirection(state.touchRelactive, currentTouchRelactive, true)
// 判断是否为衔接轮播,如果不是衔接轮播,如果当前为第一个swiperItem并且向下滑、当前为最后一个swiperItem并且向上滑时不进行操作
if (containerData.circular ||
!((state.currentIndex === 0 && direction === 'down') || (state.currentIndex === containerData.swiperItemLength - 1 && direction === 'up'))
) {
// 判断触摸的时间和移动的距离是否超过了当前itemHeight的一半,如果是则执行切换操作
// console.log(currentTime - state.touchStartTime, Math.abs(currentTouchRelactive.y - state.touchRelactive.y));
if ((currentTime - state.touchStartTime) > 200 && Math.abs(currentTouchRelactive.y - state.touchRelactive.y) < itemData.itemHeight / 2) {
ownerInstance.callMethod('changeParentSwiperContainerStyleStatus',{
status: 'reset'
})
} else {
// console.log(direction, state.touchRelactive.y, currentTouchRelactive.y);
ownerInstance.callMethod('updateParentSwiperContainerStyleWithDirection', {
direction: direction
})
}
}
} else {
// 判断触摸移动方向
var direction = decideSwiperDirection(state.touchRelactive, currentTouchRelactive, false)
// 判断是否为衔接轮播,如果不是衔接轮播,如果当前为第一个swiperItem并且向右滑、当前为最后一个swiperItem并且向左滑时不进行操作
if (containerData.circular ||
!((state.currentIndex === 0 && direction === 'right') || (state.currentIndex === containerData.swiperItemLength - 1 && direction === 'left'))
) {
// 判断触摸的时间和移动的距离是否超过了当前itemWidth的一半,如果是则执行切换操作
// console.log(currentTime - state.touchStartTime, Math.abs(currentTouchRelactive.x - state.touchRelactive.x));
if ((currentTime - state.touchStartTime) > 200 && Math.abs(currentTouchRelactive.x - state.touchRelactive.x) < itemData.itemWidth / 2) {
ownerInstance.callMethod('changeParentSwiperContainerStyleStatus',{
status: 'reset'
})
} else {
// console.log(direction, state.touchRelactive.x, currentTouchRelactive.x);
ownerInstance.callMethod('updateParentSwiperContainerStyleWithDirection', {
direction: direction
})
}
}
}
// 清除标记
state.touchId = null
state.touchRelactive = null
state.touchStartTime = 0
// 标记停止触摸
state.touching = true
ownerInstance.callMethod('updateTouchingStatus', {
status: false
})
}
module.exports = {
itemDataObserver: itemDataObserver,
currentIndexObserver: currentIndexObserver,
containerDataObserver: containerDataObserver,
touchStart: touchStart,
touchMove: touchMove,
touchEnd: touchEnd
}
@@ -0,0 +1,277 @@
<template>
<!-- #ifdef MP-WEIXIN -->
<view
class="tn-c-swiper-item"
:style="[swiperStyle]"
:itemData="itemData"
:currentIndex="currentIndex"
:containerData="containerData"
:change:itemData="wxs.itemDataObserver"
:change:currentIndex="wxs.currentIndexObserver"
:change:containerData="wxs.containerDataObserver"
@touchstart="wxs.touchStart"
:catch:touchmove="touching?wxs.touchMove:''"
:catch:touchend="touching?wxs.touchEnd:''"
>
<view class="item__container tn-c-swiper-item__container" :style="[containerStyle]">
<slot></slot>
</view>
</view>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<view
class="tn-c-swiper-item"
:style="[swiperStyle]"
:itemData="itemData"
:currentIndex="currentIndex"
:containerData="containerData"
:change:itemData="wxs.itemDataObserver"
:change:currentIndex="wxs.currentIndexObserver"
:change:containerData="wxs.containerDataObserver"
@touchstart="wxs.touchStart"
@touchmove="wxs.touchMove"
@touchend="wxs.touchEnd"
>
<view class="item__container tn-c-swiper-item__container" :style="[containerStyle]">
<slot></slot>
</view>
</view>
<!-- #endif -->
</template>
<script src="./index.wxs" lang="wxs" module="wxs"></script>
<script>
export default {
name: 'tn-custom-swiper-item',
props: {
},
computed: {
// swiperItem公共数据
itemData() {
return {
index: this.index,
itemWidth: this.itemWidth,
itemHeight: this.itemHeight,
itemTop: this.itemTop,
itemLeft: this.itemLeft
}
},
currentIndex() {
return this.parentData.currentIndex
},
containerData() {
return {
duration: this.parentData.duration,
animationFinish: this.parentData.swiperContainerAnimationFinish,
circular: this.parentData.circular,
swiperItemLength: this.swiperItemLength,
vertical: this.parentData.vertical
}
},
swiperStyle() {
let style = {}
style.transform = `translate3d(${this.translateX}%, ${this.translateY}%, 0px)`
return style
},
containerStyle() {
let style = {}
if (this.parentData.customSwiperStyle && Object.keys(this.parentData.customSwiperStyle).length > 0) {
style = this.parentData.customSwiperStyle
}
if ((this.currentIndex === 0 && this.index === this.swiperItemLength - 1) || (this.index === this.currentIndex - 1) &&
(this.parentData.prevSwiperStyle && Object.keys(this.parentData.prevSwiperStyle).length > 0)
) {
// 前一个swiperItem
const copyStyle = JSON.parse(JSON.stringify(style))
style = Object.assign(copyStyle, this.parentData.prevSwiperStyle)
}
if ((this.currentIndex === this.swiperItemLength - 1 && this.index === 0) || (this.index === this.currentIndex + 1) &&
(this.parentData.nextSwiperStyle && Object.keys(this.parentData.nextSwiperStyle).length > 0)
) {
// 后一个swiperItem
const copyStyle = JSON.parse(JSON.stringify(style))
style = Object.assign(copyStyle, this.parentData.nextSwiperStyle)
}
return style
}
},
data() {
return {
// 父组件参数
parentData: {
duration: 500,
currentIndex: 0,
swiperContainerAnimationFinish: false,
circular: false,
vertical: false,
prevSwiperStyle: {},
customSwiperStyle: {},
nextSwiperStyle: {}
},
// 标记当前是否正在触摸
touching: true,
// 当前swiperItem的偏移位置
translateX: 0,
translateY: 0,
// 当前swiperItem的宽高
itemWidth: 0,
itemHeight: 0,
// 当前swiperItem的位置信息
itemTop: 0,
itemLeft: 0,
// 当前swiperItem的状态 prev current next
status: 'current',
// 当前swiperItem的index序号
index: 0,
// swiperItem的的数量
swiperItemLength: 0
}
},
created() {
this.parent = false
this.updateParentData()
// 获取当前父组件children的数量作为当前swiperItem的序号
this.index = this.parent.children.length
this.parent && this.parent.children.push(this)
},
mounted() {
this.$nextTick(() => {
this.initSwiperItem()
})
},
methods: {
// 初始化swiperItem
initSwiperItem() {
this.getSwiperItemRect(() => {
this.parent.updateAllSwiperItemStyle()
this.parentData.swiperContainerAnimationFinish = true
})
},
// 获取swiperItem的信息
async getSwiperItemRect(callback) {
const swiperItemRes = await this._tGetRect('.tn-c-swiper-item')
if (!swiperItemRes.height || !swiperItemRes.width) {
setTimeout(() => {
this.getSwiperItemRect()
}, 30)
return
}
this.itemWidth = swiperItemRes.width
this.itemHeight = swiperItemRes.height
this.itemTop = swiperItemRes.top
this.itemLeft = swiperItemRes.left
callback && callback()
},
// 更新swiperItem样式
updateSwiperItemStyle(swiperItemLength, currentIndex = undefined) {
currentIndex = currentIndex != undefined ? currentIndex : this.parentData.currentIndex
this.swiperItemLength = swiperItemLength
// 根据当前swiperItem的序号设置偏移位置
// 判断当前swiperItem是否为第一个,如果是则将最后的swiperItem移动到当前的前一个位置(即最前面)
if (currentIndex === 0 && this.index === swiperItemLength - 1) {
if (this.parentData.vertical) {
this.translateX = 0
this.translateY = -100
} else {
this.translateX = -100
this.translateY = 0
}
}
// 判断当前swiperItem是否为最后一个,如果是则将最前的swiperItem移动到当前的后一个位置(即最后面)
else if (currentIndex === swiperItemLength - 1 && this.index === 0) {
if (this.parentData.vertical) {
this.translateX = 0
this.translateY = swiperItemLength * 100
} else {
this.translateX = swiperItemLength * 100
this.translateY = 0
}
}
// 正常情况
else {
if (this.parentData.vertical) {
this.translateX = 0
this.translateY = this.index * 100
} else {
this.translateX = this.index * 100
this.translateY = 0
}
}
},
// 更新父组件的偏移位置信息
updateParentSwiperContainerStyle(e) {
this.parent.updateSwiperContainerStyleWithValue(e.value)
},
// 根据方向更新父组件的偏移位置信息
updateParentSwiperContainerStyleWithDirection(e) {
this.parent.updateSwiperContainerStyleWithDirection(e.direction)
},
// 修改父组件的偏移位置的状态
changeParentSwiperContainerStyleStatus(e) {
// reset -> 重置 reload -> 重载
this.parent.updateSwiperContainerStyleWithDirection(e.status)
},
// 更新父组件信息
updateParentData() {
this.getParentData('tn-custom-swiper')
},
// 更新触摸状态
updateTouchingStatus(e) {
this.touching = e.status
if (e.status) {
this.parent.stopAutoPlay()
} else {
this.parent.startAutoPlay()
}
},
// 提取对应用户自定义样式
extractCustomStyle(customStyle) {
let data = {
transform: {},
style: {}
}
if (!customStyle) return data
// 允许设置的transform参数
const allowTransformProps = ['scale','scaleX','scaleY','scaleZ','rotate','rotateX','rotateY','rotateZ']
for (let prop in customStyle) {
if (prop.startsWith('transformProp')) {
// transform里面的样式
let transformProp = prop.substring('transformProp'.length)
const index = allowTransformProps.findIndex((item) => {
return item.toLowerCase() === transformProp.toLowerCase()
})
if (index !== -1) {
transformProp = allowTransformProps[index]
data.transform[transformProp] = customStyle[prop]
}
} else {
// 普通样式
data.style[prop] = customStyle[prop]
}
}
return data
}
}
}
</script>
<style lang="scss" scoped>
.tn-c-swiper-item {
width: 100%;
height: 100%;
position: absolute;
display: block;
will-change: transform;
cursor: none;
transform: translate3d(0px, 0px, 0px);
.item__container {
width: 100%;
height: 100%;
display: block;
position: absolute;
}
}
</style>
@@ -0,0 +1,535 @@
<template>
<view
class="tn-c-swiper-class tn-c-swiper"
>
<!-- 轮播item容器-->
<view class="tn-swiper__container" :style="[swiperContainerStyle]" :animation="containerAnimation">
<slot></slot>
</view>
<!-- 轮播指示器-->
<view v-if="indicator" class="tn-swiper__indicator" :class="[`tn-swiper__indicator--${vertical ? 'vertical' : 'horizontal'}`]" :style="[indicatorStyle]">
<!-- 方形 -->
<block v-if="indicatorType === 'rect'">
<view
v-for="(item, index) in children.length"
:key="index"
class="tn-swiper__indicator__rect"
:class="[
`tn-swiper__indicator__rect--${vertical ? 'vertical' : 'horizontal'}`,
currentIndex === index ? `tn-swiper__indicator__rect--active tn-swiper__indicator__rect--active--${vertical ? 'vertical' : 'horizontal'}` : ''
]"
:style="[indicatorPointStyle(index)]"
></view>
</block>
<!-- -->
<block v-if="indicatorType === 'dot'">
<view
v-for="(item, index) in children.length"
:key="index"
class="tn-swiper__indicator__dot"
:class="[
`tn-swiper__indicator__dot--${vertical ? 'vertical' : 'horizontal'}`,
currentIndex === index ? `tn-swiper__indicator__dot--active tn-swiper__indicator__dot--active--${vertical ? 'vertical' : 'horizontal'}` : ''
]"
:style="[indicatorPointStyle(index)]"
></view>
</block>
<!-- 圆角方形 -->
<block v-if="indicatorType === 'round'">
<view
v-for="(item, index) in children.length"
:key="index"
class="tn-swiper__indicator__round"
:class="[
`tn-swiper__indicator__round--${vertical ? 'vertical' : 'horizontal'}`,
currentIndex === index ? `tn-swiper__indicator__round--active tn-swiper__indicator__round--active--${vertical ? 'vertical' : 'horizontal'}` : ''
]"
:style="[indicatorPointStyle(index)]"
></view>
</block>
<!-- 序号 -->
<block v-if="indicatorType === 'number' && !vertical">
<view class="tn-swiper__indicator__number">{{ currentIndex + 1 }}/{{ children.length }}</view>
</block>
</view>
</view>
</template>
<script>
export default {
name: 'tn-custom-swiper',
props: {
// 当前所在的轮播位置
current: {
type: Number,
default: 0
},
// 自动切换
autoplay: {
type: Boolean,
default: false
},
// 自动切换时间间隔
interval: {
type: Number,
default: 5000
},
// 滑动动画时长
duration: {
type: Number,
default: 500
},
// 是否采用衔接滑动
circular: {
type: Boolean,
default: false
},
// 滑动方向为纵向
vertical: {
type: Boolean,
default: false
},
// 显示指示点
indicator: {
type: Boolean,
default: false
},
// 指示点类型
// rect -> 方形 round -> 圆角方形 dot -> 点 number -> 轮播图下标
indicatorType: {
type: String,
default: 'dot'
},
// 指示点的位置
// topLeft \ topCenter \ topRight \ bottomLeft \ bottomCenter \ bottomRight
indicatorPosition: {
type: String,
default: 'bottomCenter'
},
// 指示点激活时颜色
indicatorActiveColor: {
type: String,
default: ''
},
// 指示点未激活时颜色
indicatorInactiveColor: {
type: String,
default: ''
},
// 前一个轮播的自定义样式
prevSwiperStyle: {
type: Object,
default() {
return {}
}
},
// 当前轮播的自定义样式
customSwiperStyle: {
type: Object,
default() {
return {}
}
},
// 后一个轮播的自定义样式
nextSwiperStyle: {
type: Object,
default() {
return {}
}
}
},
computed: {
parentData() {
return [
this.duration,
this.currentIndex,
this.swiperContainerAnimationFinish,
this.circular,
this.vertical,
this.prevSwiperStyle,
this.customSwiperStyle,
this.nextSwiperStyle
]
},
indicatorStyle() {
let style = {}
if (this.vertical) {
if (this.indicatorPosition === 'topLeft' || this.indicatorPosition === 'bottomLeft') style.justifyContent = 'flex-start'
if (this.indicatorPosition === 'topCenter' || this.indicatorPosition === 'bottomCenter') style.justifyContent = 'center'
if (this.indicatorPosition === 'topRight' || this.indicatorPosition === 'bottomRight') style.justifyContent = 'flex-end'
if (['topLeft','topCenter','topRight'].indexOf(this.indicatorPosition) >= 0) {
if (this.vertical) {
style.right = '12rpx'
style.left = 'auto'
} else {
style.top = '12rpx'
style.bottom = 'auto'
}
} else {
if (this.vertical) {
style.right = 'auto'
style.left = '12rpx'
} else {
style.top = 'auto'
style.bottom = '12rpx'
}
}
} else {
if (this.indicatorPosition === 'topLeft' || this.indicatorPosition === 'bottomLeft') style.justifyContent = 'flex-start'
if (this.indicatorPosition === 'topCenter' || this.indicatorPosition === 'bottomCenter') style.justifyContent = 'center'
if (this.indicatorPosition === 'topRight' || this.indicatorPosition === 'bottomRight') style.justifyContent = 'flex-end'
if (['topLeft','topCenter','topRight'].indexOf(this.indicatorPosition) >= 0) {
style.top = '12rpx'
style.bottom = 'auto'
} else {
style.top = 'auto'
style.bottom = '12rpx'
}
}
return style
},
indicatorPointStyle() {
return (index) => {
let style = {}
if (index === this.currentIndex && this.indicatorActiveColor !== '') {
style.backgroundColor = this.indicatorActiveColor
} else if (this.indicatorInactiveColor !== '') {
style.backgroundColor = this.indicatorInactiveColor
}
return style
}
}
},
watch: {
parentData() {
if (this.children.length) {
this.children.forEach((item) => {
// 判断子组件如果有updateParentData方法的话,就就执行(执行的结果是子组件重新从父组件拉取了最新的值)
typeof(item.updateParentData) === 'function' && item.updateParentData()
})
}
},
current(nVal, oVal) {
if (this.currentIndex === nVal) return
this.currentIndex = nVal > this.children.length ? this.children.length - 1 : nVal
this.swiperContainerAnimationFinish = false
// 设置动画过渡时间
this.swiperContainerStyle.transitionDuration = `${this.duration + 90}ms`
this.updateSwiperContainerItem(oVal)
}
},
data() {
return {
// 清除动画定时器
clearAnimationTimer: null,
// 前后衔接执行定时器
convergeTimer: null,
// 自动轮播Timer
autoPlayTimer: null,
// 当前选中的轮播
currentIndex: this.current,
// swiperContainer样式
swiperContainerStyle: {
transform: 'translate3d(0px, 0px, 0px)',
transitionDuration: '0ms'
},
// swiperContainer动画
containerAnimation: {},
// 滑动动画结束标记
swiperContainerAnimationFinish: false
}
},
created() {
this.children = []
},
mounted() {
this.$nextTick(() => {
const index = this.currentIndex > this.children.length ? this.children.length - 1 : this.currentIndex
this.updateSwiperContainerStyle(index)
this.startAutoPlay()
})
},
methods: {
// 更新全部swiperItem的样式
updateAllSwiperItemStyle() {
this.children.forEach((item, index) => {
typeof(item.updateSwiperItemStyle) === 'function' && item.updateSwiperItemStyle(this.children.length)
})
},
// 根据swiperIndex更新swiperItemContainer的样式
updateSwiperContainerStyle(index) {
if (this.vertical) {
this.swiperContainerStyle.transform = `translate3d(0px, ${-index * 100}%, 0px)`
} else {
this.swiperContainerStyle.transform = `translate3d(${-index * 100}%, 0px, 0px)`
}
},
// 根据传递的值更新swiperItemContainer的位置
updateSwiperContainerStyleWithValue(value) {
if (this.vertical) {
this.swiperContainerStyle.transform = `translate3d(0px, ${(-this.currentIndex * 100) + value * 100}%, 0px)`
} else {
this.swiperContainerStyle.transform = `translate3d(${(-this.currentIndex * 100) + value * 100}%, 0px, 0px)`
}
},
// 根据传递的方向更新swiperItemContainer的位置
updateSwiperContainerStyleWithDirection(direction) {
const oldCurrent = this.currentIndex
const childrenLength = this.children.length
const lastSwiperItemIndex = childrenLength - 1
this.swiperContainerAnimationFinish = false
// 向后切换一个SwiperItem
if (direction === 'reset') {
// 设置动画过渡时间
this.swiperContainerStyle.transitionDuration = `${this.duration}ms`
this.updateSwiperContainerStyle(this.currentIndex)
this.clearAnimationTimer = setTimeout(() => {
this.clearSwiperContainerAnimation()
}, this.duration)
} else if (direction === 'reload') {
this.clearConvergeSwiperItemTimer()
this.clearSwiperContainerAnimation()
this.updateSwiperItemStyle(0)
this.updateSwiperItemStyle(lastSwiperItemIndex)
} else {
if (direction === 'left' || direction === 'up') {
if (oldCurrent === childrenLength - 1 && !this.circular) {
this.clearSwiperContainerAnimation()
this.clearConvergeSwiperItemTimer()
return
}
this.currentIndex = oldCurrent + 1 >= childrenLength ? 0 : oldCurrent + 1
} else if (direction === 'right' || direction === 'down') {
if (oldCurrent === 0 && !this.circular) {
this.clearSwiperContainerAnimation()
this.clearConvergeSwiperItemTimer()
return
}
this.currentIndex = oldCurrent - 1 < 0 ? childrenLength - 1 : oldCurrent - 1
}
// 设置动画过渡时间
this.swiperContainerStyle.transitionDuration = `${this.duration + 90}ms`
// this.updateSwiperItemContainerRect(this.currentIndex)
}
// console.log(direction, oldCurrent, this.currentIndex);
this.updateSwiperContainerItem(oldCurrent)
// 切换轮播时触发事件
this.$emit('change', {
current: this.currentIndex
})
},
// 设置自动轮播
startAutoPlay() {
if (this.autoplay && !this.autoPlayTimer && this.circular) {
this.autoPlayTimer = setInterval(() => {
this.updateSwiperContainerStyleWithDirection('left')
}, this.interval)
}
},
// 停止自动轮播
stopAutoPlay() {
if (this.autoPlayTimer) {
clearInterval(this.autoPlayTimer)
this.autoPlayTimer = null
}
},
// 更新swiperContainer和swiperItem相关联信息
updateSwiperContainerItem(oldCurrent) {
const childrenLength = this.children.length
const lastSwiperItemIndex = childrenLength - 1
// 判断当前是否为头尾,如果是更新对应的头尾SwiperItem样式
// 更新swiperItemContainer的样式
if (oldCurrent === 0 && this.currentIndex === lastSwiperItemIndex) {
// 先移动到最左边然后再去除动画偏移到正常的位置
// this.swiperContainerStyle.transform = `translate3d(100%, 0px, 0px)`
this.updateSwiperContainerStyle(-1)
this.clearSwiperContainerAnimationTimer()
this.clearAnimationTimer = setTimeout(() => {
this.convergeSwiperItem()
}, this.duration)
} else if (oldCurrent === lastSwiperItemIndex && this.currentIndex === 0) {
// 先移动到最右边然后再去除动画偏移到正常的位置
// this.swiperContainerStyle.transform = `translate3d(${-childrenLength * 100}%, 0px, 0px)`
this.updateSwiperContainerStyle(childrenLength)
this.clearSwiperContainerAnimationTimer()
this.clearAnimationTimer = setTimeout(() => {
this.convergeSwiperItem()
}, this.duration)
} else {
this.updateSwiperContainerStyle(this.currentIndex)
this.updateSwiperItemStyle(0)
this.updateSwiperItemStyle(lastSwiperItemIndex)
this.clearAnimationTimer = setTimeout(() => {
this.clearSwiperContainerAnimation()
}, this.duration)
}
},
// 更新对应swiperItem的信息
updateSwiperItemStyle(index) {
const childrenLength = this.children.length
if (index < 0) index = 0
if (index > childrenLength - 1) index = childrenLength - 1
typeof(this.children[index].updateSwiperItemStyle) === 'function' && this.children[index].updateSwiperItemStyle(childrenLength, this.currentIndex)
},
// 更新对应swiperItem的容器信息
updateSwiperItemContainerRect(index) {
const childrenLength = this.children.length
if (index < 0) index = 0
if (index > childrenLength - 1) index = childrenLength - 1
typeof(this.children[index].getSwiperItemRect) === 'function' && this.children[index].getSwiperItemRect()
},
// 执行前后衔接
convergeSwiperItem() {
const lastSwiperItemIndex = this.children.length - 1
this.clearSwiperContainerAnimation()
this.clearConvergeSwiperItemTimer()
this.convergeTimer = setTimeout(() => {
this.updateSwiperItemStyle(0)
this.updateSwiperItemStyle(lastSwiperItemIndex)
this.updateSwiperContainerStyle(this.currentIndex)
this.clearConvergeSwiperItemTimer()
}, 30)
},
// 停止/清除切换动画
clearSwiperContainerAnimation() {
this.swiperContainerStyle.transitionDuration = `0ms`
this.swiperContainerAnimationFinish = true
this.clearSwiperContainerAnimationTimer()
},
// 停止/清除执行前后衔接定时器
clearConvergeSwiperItemTimer() {
if (this.convergeTimer) {
clearTimeout(this.convergeTimer)
this.convergeTimer = null
}
},
// 停止/清除切换动画定时器
clearSwiperContainerAnimationTimer() {
if (this.clearAnimationTimer) {
clearTimeout(this.clearAnimationTimer)
this.clearAnimationTimer = null
}
}
}
}
</script>
<style lang="scss" scoped>
.tn-c-swiper {
position: relative;
overflow: hidden;
width: 100%;
height: 100%;
.tn-swiper {
&__container {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
will-change: transform;
transition-property: all;
transition-timing-function: ease-out;
}
&__indicator {
position: absolute;
display: flex;
z-index: 1;
&--horizontal {
padding: 0 24rpx;
flex-direction: row;
width: 100%;
}
&--vertical {
padding: 24rpx 0;
flex-direction: column;
height: 100%;
}
&__rect {
background-color: rgba(0, 0, 0, 0.3);
transition: all 0.5s;
&--horizontal {
width: 26rpx;
height: 8rpx;
}
&--vertical {
width: 8rpx;
height: 26rpx;
}
&--active {
background-color: rgba(255, 255, 255, 0.8);
}
}
&__dot {
width: 14rpx;
height: 14rpx;
border-radius: 20rpx;
background-color: rgba(0, 0, 0, 0.3);
transition: all 0.5s;
&--horizontal {
margin: 0 6rpx;
}
&--vertical {
margin: 6rpx 0;
}
&--active {
background-color: rgba(255, 255, 255, 0.8);
}
}
&__round {
width: 14rpx;
height: 14rpx;
border-radius: 20rpx;
background-color: rgba(0, 0, 0, 0.3);
transition: all 0.5s;
&--horizontal {
margin: 0 6rpx;
}
&--vertical {
margin: 6rpx 0;
}
&--active {
background-color: rgba(255, 255, 255, 0.8);
&--horizontal {
width: 34rpx;
}
&--vertical {
height: 34rpx;
}
}
}
&__number {
padding: 6rpx 16rpx;
line-height: 1;
background-color: rgba(0, 0, 0, 0.3);
color: rgba(255, 255, 255, 0.8);
border-radius: 100rpx;
font-size: 26rpx;
}
}
}
}
</style>
+265
View File
@@ -0,0 +1,265 @@
// 判断是否出界
var isOutRange = function(x1, y1, x2, y2, x3, y3) {
return x1 < 0 || x1 >= y1 || x2 < 0 || x2 >= y2 || x3 < 0 || x3 >= y3
}
var edit = false
function bool(str) {
return str === 'true' || str === true
}
/**
* 排序核心
* @param {Object} startKey 开始时位置
* @param {Object} endKey 结束时位置
* @param {Object} instance wxs内的局部变量快照
*/
var sortCore = function(startKey, endKey, state) {
var basedata = state.basedata
var excludeFix = function(sortKey, type) {
// fixed 元素位置不会变化, 这里直接用 sortKey 获取,更加便捷
if (state.list[sortKey].fixed) {
var _sortKey = type ? --sortKey : ++sortKey
return excludeFix(sortKey, type)
}
return sortKey
}
// 先获取到 endKey 对应的 realKey, 防止下面排序过程中该 realKey 被修改
var endRealKey = -1
state.list.forEach(function(item) {
if (item.sortKey === endKey) endRealKey = item.realKey
})
return state.list.map(function(item) {
if (item.fixed) return item
var sortKey = item.sortKey
var realKey = item.realKey
if (startKey < endKey) {
// 正序拖动
if (sortKey > startKey && sortKey <= endKey) {
--realKey
sortKey = excludeFix(--sortKey, true)
} else if (sortKey === startKey) {
realKey = endRealKey
sortKey = endKey
}
} else if (startKey > endKey) {
// 倒序拖动
if (sortKey >= endKey && sortKey < startKey) {
++realKey
sortKey = excludeFix(++sortKey, false)
} else if (sortKey === startKey) {
realKey = endRealKey
sortKey = endKey
}
}
if (item.sortKey != sortKey) {
item.translateX = (sortKey % basedata.columns) * 100 + '%'
item.translateY = Math.floor(sortKey / basedata.columns) * 100 + '%'
item.sortKey = sortKey
item.realKey = realKey
}
return item
})
}
var triggerCustomEvent = function(list, type, instance) {
if (!instance) return
var _list = [],
listData = [];
list.forEach(function(item) {
_list[item.sortKey] = item
})
_list.forEach(function(item) {
listData.push(item.data)
})
// 编译到小程序 funcName作为参数传递导致事件不执行
switch(type) {
case 'change':
instance.callMethod('change', {data: listData})
break
case 'sortEnd':
instance.callMethod('sortEnd', {data: listData})
break
}
}
var listObserver = function(newVal, oldVal, ownerInstance, instance) {
var state = ownerInstance.getState()
state.itemsInstance = ownerInstance.selectAllComponents('.tn-drag__item')
state.list = newVal || []
state.list.forEach(function(item, index) {
var itemInstance = state.itemsInstance[index]
if (item && itemInstance) {
itemInstance.setStyle({
'transform': 'translate3d('+ item.translateX + ',' + item.translateY +', 0)'
})
if (item.fixed) itemInstance.addClass('tn-drag__fixed')
}
})
}
var baseDataObserver = function(newVal, oldVal, ownerInstance, instance) {
var state = ownerInstance.getState()
state.basedata = newVal
}
var longPress = function(event, ownerInstance) {
var instance = event.instance
var dataset = instance.getDataset()
var state = ownerInstance.getState()
edit = bool(dataset.edit)
if (!edit) return
if (!state.basedata || state.basedata === 'undefined') {
state.basedata = JSON.parse(dataset.basedata)
}
var basedata = state.basedata
var touches = event.changedTouches[0]
if (!touches) return
state.current = +dataset.index
// 初始项是固定项则返回
var item = state.list[state.current]
if (item && item.fixed) return
// 如果已经在 drag 中则返回, 防止多指触发 drag 动作, touchstart 事件中有效果
if (state.dragging) return
ownerInstance.callMethod("drag", {
dragging: true
})
// 计算X, Y轴初始位移,使item中心移动到点击处,单列的时候X轴初始不做位移
state.translateX = basedata.columns === 1 ? 0 : touches.pageX - (basedata.itemWidth / 2 + basedata.left)
state.translateY = touches.pageY - (basedata.itemHeight / 2 + basedata.top)
state.touchId = touches.identifier
instance.setStyle({
'transform': 'translate3d(' + state.translateX + 'px,' + state.translateY +'px, 0)'
})
state.itemsInstance.forEach(function(item, index) {
item.removeClass("tn-drag__transition").removeClass("tn-drag__current")
item.addClass(index === state.current ? "tn-drag__current" : "tn-drag__transition")
})
ownerInstance.callMethod("vibrate")
state.dragging = true
}
var touchStart = function(event, ownerInstance) {
var instance = event.instance
var dataset = instance.getDataset()
edit = bool(dataset.edit)
}
var touchMove = function(event, ownerInstance) {
var instance = event.instance
var dataset = instance.getDataset()
var state = ownerInstance.getState()
var basedata = state.basedata
if (!state.dragging || !edit) return
var touches = event.changedTouches[0]
if (!touches) return
// 如果不是同一个触发点则返回
if (state.touchId !== touches.identifier) return
// 计算X,Y轴位移, 单列时候X轴初始不做位移
var translateX = basedata.columns === 1 ? 0 : touches.pageX - (basedata.itemWidth / 2 + basedata.left)
var translateY = touches.pageY - (basedata.itemHeight / 2 + basedata.top)
// 到顶到低自动滑动
if (touches.clientY > basedata.windowHeight - basedata.itemHeight - basedata.realBottomSize) {
// 当前触摸点pageY + item高度 - (屏幕高度 - 底部固定区域高度)
ownerInstance.callMethod('pageScroll', {
scrollTop: touches.pageY + basedata.itemHeight - (basedata.windowHeight - basedata.realBottomSize)
})
} else if (touches.clientY < basedata.itemHeight + basedata.realTopSize) {
// 当前触摸点pageY - item高度 - 顶部固定区域高
ownerInstance.callMethod('pageScroll', {
scrollTop: touches.pageY - basedata.itemHeight - basedata.realTopSize
})
}
// 设置当前激活元素的偏移量
instance.setStyle({
'transform': 'translate3d('+ translateX + 'px,' + translateY + 'px, 0)'
})
var startKey = state.list[state.current].sortKey
var currentX = Math.round(translateX / basedata.itemWidth)
var currentY = Math.round(translateY / basedata.itemHeight)
var endKey = currentX + basedata.columns * currentY
// 目标项时固定项则返回
var item = state.list[endKey]
if (item && item.fixed) return
// X轴或者Y轴超出范围则返回
if (isOutRange(currentX, basedata.columns, currentY, basedata.rows, endKey, state.list.length)) return
// 防止拖拽过程中发生乱序问题
if (startKey === endKey || startKey === state.preStartKey) return
state.preStartKey = startKey
var list = sortCore(startKey, endKey, state)
state.itemsInstance.forEach(function(itemInstance, index) {
var item = list[index]
if (index !== state.current) {
itemInstance.setStyle({
'transform': 'translate3d('+ item.translateX + ',' + item.translateY +', 0)'
})
}
})
// ownerInstance.callMethod('vibrate')
ownerInstance.callMethod('listDataChange', {
data: list
})
triggerCustomEvent(list, "change", ownerInstance)
}
var touchEnd = function(event, ownerInstance) {
var instance = event.instance
var dataset = instance.getDataset()
var state = ownerInstance.getState()
var basedata = state.basedata
if (!state.dragging || !edit) return
triggerCustomEvent(state.list, "sortEnd", ownerInstance)
instance.addClass('tn-drag__transition')
instance.setStyle({
'transform': 'translate3d('+ state.list[state.current].translateX + ',' + state.list[state.current].translateY + ', 0)'
})
state.itemsInstance.forEach(function(item, index) {
item.removeClass('tn-drag__transition')
})
state.preStartKey = -1
state.dragging = false
ownerInstance.callMethod('drag', {
dragging: false
})
state.current = -1
state.translateX = 0
state.translateY = 0
}
module.exports = {
longPress: longPress,
touchStart: touchStart,
touchMove: touchMove,
touchEnd: touchEnd,
baseDataObserver: baseDataObserver,
listObserver: listObserver
}
+278
View File
@@ -0,0 +1,278 @@
<template>
<view
class="tn-drag-class tn-drag"
:style="{
height: wrapHeight + 'rpx'
}"
:list="listData"
:basedata="baseData"
:change:list="wxs.listObserver"
:change:basedata="wxs.baseDataObserver"
>
<!-- #ifdef MP-WEIXIN -->
<view
v-for="(item, index) in listData"
:key="item.id"
class="tn-drag__item"
:style="{
width: 100 / columns + '%',
height: itemHeight + 'rpx'
}"
:data-index="index"
:data-basedata="baseData"
:data-edit="edit"
@longpress="wxs.longPress"
@touchstart="wxs.touchStart"
:catch:touchmove="dragging?wxs.touchMove:''"
:catch:touchend="dragging?wxs.touchEnd:''"
>
<slot :entity="item.data" :fixed="item.fixed" :index="index" :height="itemHeight" :isEdit="edit"></slot>
</view>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<view
v-for="(item, index) in listData"
:key="item.id"
class="tn-drag__item"
:style="{
width: 100 / columns + '%',
height: itemHeight + 'rpx'
}"
@longpress="wxs.longPress"
:data-index="index"
:data-basedata="baseData"
:data-edit="edit"
@touchstart="wxs.touchStart"
@touchmove="wxs.touchMove"
@touchend="wxs.touchEnd"
>
<slot :entity="item.data" :fixed="item.fixed" :index="index" :height="itemHeight" :isEdit="edit"></slot>
</view>
<!-- #endif -->
</view>
</template>
<script src="./index.wxs" lang="wxs" module="wxs"></script>
<script>
export default {
name: 'tn-drag',
props: {
// 数据源
// 如果属性中包含fixed,则标识当前数据不允许拖动
list: {
type: Array,
default () {
return []
}
},
// 是否允许拖动编辑
edit: {
type: Boolean,
default: true
},
// 列数
columns: {
type: Number,
default: 3
},
// item元素高度, 单位rpx
itemHeight: {
type: Number,
default: 0
},
// 当前父元素滚动的高度
scrollTop: {
type: Number,
default: 0
}
},
computed: {
wrapHeight() {
return this.rows * this.itemHeight
}
},
data() {
return {
// 未渲染前节点数据
baseData: {},
// 拖动后的数据
dragData: [],
// 行数
rows: 0,
// 渲染数据
listData: [],
// 标记是否正在拖动
dragging: false
}
},
watch: {
list(val) {
this.listData = []
this.$nextTick(() => {
this.init()
})
},
columns(val) {
this.listData = []
this.$nextTick(() => {
this.init()
})
}
},
mounted() {
this.$nextTick(() => {
this.init()
})
},
methods: {
// 初始化
init() {
this.dragging = true
const initDragItem = item => {
const obj = {
...item
}
const fixed = obj?.fixed || false
delete obj["fixed"]
return {
id: this.unique(),
fixed,
data: {
...obj
}
}
}
let i = 0
const listData = (this.list || []).map((item, index) => {
let listItem = initDragItem(item)
// 真实排序
listItem.realKey = i++
// 整体排序
listItem.sortKey = index
listItem.translateX = `${(listItem.sortKey % this.columns) * 100}%`
listItem.translateY = `${Math.floor(listItem.sortKey / this.columns) * 100}%`
return listItem
})
this.rows = Math.ceil(listData.length / this.columns)
this.listData = listData
this.dragData = listData
if (listData.length === 0) return
// console.log(listData);
// 初始化dom元素
this.$nextTick(() => {
this.initRect()
})
},
// 初始化dom元素
initRect() {
const {
windowWidth,
windowHeight
} = uni.getSystemInfoSync()
let baseData = {}
baseData.windowHeight = windowHeight
baseData.realTopSize = 0
baseData.realBottomSize = 0
baseData.columns = this.columns
baseData.rows = this.rows
const query = uni.createSelectorQuery().in(this)
query.select('.tn-drag').boundingClientRect()
query.select('.tn-drag__item').boundingClientRect()
query.exec(res => {
if (!res) {
setTimeout(() => {
this.initRect()
}, 10)
return
}
baseData.itemWidth = res[1].width
baseData.itemHeight = res[1].height
baseData.left = res[0].left
baseData.top = res[0].top + this.scrollTop
this.dragging = false
this.baseData = baseData
})
},
// 触发震动
vibrate() {
uni.vibrateShort()
},
// 滚动到指定的位置
pageScroll(e) {
uni.pageScrollTo({
scrollTop: e.scrollTop,
duration: 0
})
},
// 修改拖动状态
drag(e) {
this.dragging = e.dragging
},
// 拖拽数据发生改变
listDataChange(e) {
this.dragData = e.data
},
// item被点击
itemClick(index) {
const item = this.dragData[index]
this.$emit('click', {
key: item.realKey,
data: item.data
})
},
// 拖拽结束事件
sortEnd(e) {
this.$emit('end', {
data: e.data
})
},
// 排序发生改变事件
change(e) {
this.$emit('change', {
data: e.data
})
},
// 生成元素唯一id
unique(n = 6) {
let id = ''
for (let i = 0; i < n; i++) id += Math.floor(Math.random() * 10)
return 'tn_' + new Date().getTime() + id
}
}
}
</script>
<style lang="scss" scoped>
.tn-drag {
position: relative;
&__item {
position: absolute;
z-index: 2;
top: 0;
left: 0;
}
&__transition {
transition: transform 0.25s !important;
}
&__current {
z-index: 10 !important;
}
&__fixed {
z-index: 1 !important;
}
}
</style>
@@ -0,0 +1,995 @@
<template>
<view v-if="!disabled" class="tn-image-upload-class tn-image-upload">
<movable-area
class="tn-image-upload__movable-area"
:style="{
height: movableAreaHeight
}"
@mouseenter="mouseEnterArea"
@mouseleave="mouseLeaveArea"
>
<block
v-for="(item, index) in lists"
:key="item.id"
>
<movable-view
class="tn-image-upload__movable-view"
:style="{
width: $tn.string.getLengthUnitValue(width),
height: $tn.string.getLengthUnitValue(height),
zIndex: item.zIndex,
opacity: item.opacity,
}"
:x="item.x"
:y="item.y"
direction="all"
:damping="40"
:disabled="item.disabled"
@change="movableChange($event, item)"
@touchstart="movableStart(item)"
@mousedown="movableStart(item)"
@touchend="movableEnd(item)"
@mouseup="movableEnd(item)"
@longpress="movableLongPress(item)"
>
<view
class="tn-image-upload__item tn-image-upload__item-preview"
:style="{
width: $tn.string.getLengthUnitValue(itemWidth, 'px'),
height: $tn.string.getLengthUnitValue(itemHeight, 'px'),
transform: `scale(${item.scale})`
}"
>
<!-- 删除按钮 -->
<view
v-if="deleteable"
class="tn-image-upload__item-preview__delete"
@tap.stop="deleteItem(index)"
:style="{
borderTopColor: deleteBackgroundColor
}"
>
<view
class="tn-image-upload__item-preview__delete--icon"
:class="[`tn-icon-${deleteIcon}`]"
:style="{
color: deleteColor
}"
></view>
</view>
<!-- 进度条 -->
<tn-line-progress
v-if="showProgress && item.data.progress > 0 && !item.data.error"
class="tn-image-upload__item-preview__progress"
:percent="item.data.progress"
:showPercent="false"
:round="false"
:height="8"
></tn-line-progress>
<!-- 重试按钮 -->
<view v-if="item.data.error" class="tn-image-upload__item-preview__error-btn" @tap.stop="retry(index)">点击重试</view>
<!-- 图片信息 -->
<image
class="tn-image-upload__item-preview__image"
:src="item.data.url || item.data.path"
:mode="imageMode"
@tap.stop="doPreviewImage(item.data.url || item.data.path, index)"
></image>
</view>
</movable-view>
</block>
<!-- 添加按钮 -->
<view
v-if="maxCount > lists.length"
class="tn-image-upload__add"
:style="{
top: addBtn.y + 'px',
left: addBtn.x + 'px',
width: $tn.string.getLengthUnitValue(itemWidth, 'px'),
height: $tn.string.getLengthUnitValue(itemHeight, 'px')
}"
@tap="selectFile"
>
<!-- 添加按钮 -->
<view
class="tn-image-upload__item tn-image-upload__item-add"
hover-class="tn-hover-class"
hover-stay-time="150"
:style="{
width: $tn.string.getLengthUnitValue(itemWidth, 'px'),
height: $tn.string.getLengthUnitValue(itemHeight, 'px')
}"
>
<view class="tn-image-upload__item-add--icon tn-icon-add"></view>
<view class="tn-image-upload__item-add__tips">{{ uploadText }}</view>
</view>
</view>
</movable-area>
</view>
</template>
<script>
export default {
name: 'tn-image-upload-drag',
props: {
// 已上传的文件列表
fileList: {
type: Array,
default() {
return []
}
},
// 上传图片地址
action: {
type: String,
default: ''
},
// 上传文件的字段名称
name: {
type: String,
default: 'file'
},
// 头部信息
header: {
type: Object,
default() {
return {}
}
},
// 携带的参数
formData: {
type: Object,
default() {
return {}
}
},
// 是否禁用
disabled: {
type: Boolean,
default: false
},
// 是否自动上传
autoUpload: {
type: Boolean,
default: true
},
// 最大上传数量
maxCount: {
type: Number,
default: 9
},
// 预览上传图片的裁剪模式
imageMode: {
type: String,
default: 'aspectFill'
},
// 点击图片是否全屏预览
previewFullImage: {
type: Boolean,
default: true
},
// 是否显示进度条
showProgress: {
type: Boolean,
default: true
},
// 是否显示删除按钮
deleteable: {
type: Boolean,
default: true
},
// 删除按钮图标
deleteIcon: {
type: String,
default: 'close'
},
// 删除按钮的背景颜色
deleteBackgroundColor: {
type: String,
default: ''
},
// 删除按钮的颜色
deleteColor: {
type: String,
default: ''
},
// 上传区域提示文字
uploadText: {
type: String,
default: '选择图片'
},
// 显示toast提示
showTips: {
type: Boolean,
default: true
},
// 预览图片和选择图片区域的宽度
width: {
type: Number,
default: 200
},
// 预览图片和选择图片区域的高度
height: {
type: Number,
default: 200
},
// 选择图片的尺寸
// 参考上传文档 https://uniapp.dcloud.io/api/media/image
sizeType: {
type: Array,
default() {
return ['original', 'compressed']
}
},
// 图片来源
sourceType: {
type: Array,
default() {
return ['album', 'camera']
}
},
// 是否可以多选
multiple: {
type: Boolean,
default: true
},
// 文件大小(byte)
maxSize: {
type: Number,
default: 10 * 1024 * 1024
},
// 允许上传的类型
limitType: {
type: Array,
default() {
return ['png','jpg','jpeg','webp','gif','image']
}
},
// 是否自定转换为json
toJson: {
type: Boolean,
default: true
},
// 上传前钩子函数,每个文件上传前都会执行
beforeUpload: {
type: Function,
default: null
},
// 删除文件前钩子函数
beforeRemove: {
type: Function,
default: null
},
index: {
type: [Number, String],
default: ''
}
},
computed: {
movableAreaHeight() {
if (this.lists.length < this.maxCount) {
return Math.ceil((this.lists.length + 1) / this.baseData.columns) * uni.upx2px(this.height) + 'px'
} else {
return Math.ceil(this.lists.length / this.baseData.columns) * uni.upx2px(this.height) + 'px'
}
},
itemWidth() {
return uni.upx2px(this.width) - (uni.upx2px(10) * 2)
},
itemHeight() {
return uni.upx2px(this.height) - (uni.upx2px(10) * 2)
}
},
data() {
return {
lists: [],
uploading: false,
baseData: {
windowWidth: 0,
columns: 0,
dragItem: null,
widthPx: 0,
heightPx: 0
},
addBtn: {
x: 0,
y: 0
},
timer: null,
dragging: false
}
},
watch: {
// fileList: {
// handler(val) {
// val.map(value => {
// // 首先检查内部是否已经添加过这张图片,因为外部绑定了一个对象给fileList的话(对象引用),进行修改外部fileList时,
// // 会触发watch,导致重新把原来的图片再次添加到this.lists
// // 数组的some方法意思是,只要数组元素有任意一个元素条件符合,就返回true,而另一个数组的every方法的意思是数组所有元素都符合条件才返回true
// let tmp = this.lists.some(listVal => {
// return listVal.url === value.url
// })
// // 如果内部没有这张图片,则添加到内部
// !tmp && this.lists.push({ url: value.url, error: false, progress: 100 })
// })
// },
// immediate: true
// },
lists(val) {
this.$emit('on-list-change', this.sortList(), this.index)
}
},
created() {
this.baseData.windowWidth = uni.getSystemInfoSync().windowWidth
},
mounted() {
this.$nextTick(() => {
this.updateDragInfo()
})
},
methods: {
// 清除列表
clear() {
this.lists = []
this.updateAddBtnPositioin()
},
// 重新上传队列中上传失败所有文件
reUpload() {
this.uploadFile()
},
// 选择图片
selectFile() {
if (this.disabled) return
const {
name = '',
maxCount,
multiple,
maxSize,
sizeType,
lists,
camera,
compressed,
sourceType
} = this
let chooseFile = null
const newMaxCount = maxCount - lists.length
// 只选择图片的时候使用 chooseImage 来实现
chooseFile = new Promise((resolve, reject) => {
uni.chooseImage({
count: multiple ? (newMaxCount > 9 ? 9 : newMaxCount) : 1,
sourceType,
sizeType,
success: resolve,
fail: reject
})
})
chooseFile.then(res => {
let file = null
let listOldLength = lists.length
res.tempFiles.map((val, index) => {
if (!this.checkFileExt(val)) return
// 是否超出最大限制数量
if (!multiple && index >= 1) return
if (val.size > maxSize) {
this.$emit('on-oversize', val, this.sortList(), this.index)
this.showToast('超出可允许文件大小')
} else {
if (maxCount <= lists.length) {
this.$emit('on-exceed', val, this.sortList(), this.index)
this.showToast('超出最大允许的文件数')
return
}
lists.push(this.handleDragListItem({
url: val.path,
progress: 0,
error: false,
file: val
}))
this.updateAddBtnPositioin()
}
})
this.$emit('on-choose-complete', this.sortList(), this.index)
if (this.autoUpload) this.uploadFile(listOldLength)
}).catch(err => {
this.$emit('on-choose-fail', err)
})
},
// 提示用户信息
showToast(message, force = false) {
if (this.showTips || force) {
this.$tn.message.toast(message)
}
},
// 手动上传,通过ref进行调用
upload() {
this.uploadFile()
},
// 对失败图片进行再次上传
retry(index) {
this.lists[index].data.progress = 0
this.lists[index].data.error = false
this.lists[index].data.response = null
this.$tn.message.loading('重新上传')
this.uploadFile(index)
},
// 上传文件
async uploadFile(index = 0) {
if (this.disabled) return
if (this.uploading) return
// 全部上传完成
if (index >= this.lists.length) {
this.$emit('on-uploaded', this.sortList(), this.index)
return
}
// 检查是否已经全部上传或者上传中
if (this.lists[index].data.progress === 100) {
this.lists[index].data.uploadTask = null
if (this.autoUpload) this.uploadFile(index + 1)
return
}
// 执行before-upload钩子
if (this.beforeUpload && typeof(this.beforeUpload) === 'function') {
// 在微信,支付宝等环境(H5正常),会导致父组件定义的函数体中的this变成子组件的this
// 通过bind()方法,绑定父组件的this,让this的this为父组件的上下文
// 因为upload组件可能会被嵌套在其他组件内,比如tn-form,这时this.$parent其实为tn-form的this
// 非页面的this,所以这里需要往上历遍,一直寻找到最顶端的$parent,这里用了this.$u.$parent.call(this)
let beforeResponse = this.beforeUpload.bind(this.$tn.$parent.call(this))(index, this.lists)
// 判断是否返回了Promise
if (!!beforeResponse && typeof beforeResponse.then === 'function') {
await beforeResponse.then(res => {
// promise返回成功,不进行操作继续
}).catch(err => {
// 进入catch回调的话,继续下一张
return this.uploadFile(index + 1)
})
} else if (beforeResponse === false) {
// 如果返回flase,继续下一张图片上传
return this.uploadFile(index + 1)
} else {
// 为true的情况,不进行操作
}
}
// 检查上传地址
if (!this.action) {
this.showToast('请配置上传地址', true)
return
}
this.lists[index].data.error = false
this.uploading = true
// 创建上传对象
const task = uni.uploadFile({
url: this.action,
filePath: this.lists[index].data.url,
name: this.name,
formData: this.formData,
header: this.header,
success: res => {
// 判断啊是否为json字符串,将其转换为json格式
let data = this.toJson && this.$tn.test.jsonString(res.data) ? JSON.parse(res.data) : res.data
if (![200, 201, 204].includes(res.statusCode)) {
this.uploadError(index, data)
} else {
this.lists[index].data.response = data
this.lists[index].data.progress = 100
this.lists[index].data.error = false
this.$emit('on-success', data, index, this.sortList(), this.index)
}
},
fail: err => {
this.uploadError(index, err)
},
complete: res => {
this.$tn.message.closeLoading()
this.uploading = false
this.uploadFile(index + 1)
this.$emit('on-change', res, index, this.sortList(), this.index)
}
})
this.lists[index].uploadTask = task
task.onProgressUpdate(res => {
if (res.progress > 0) {
this.lists[index].data.progress = res.progress
this.$emit('on-progress', res, index, this.sortList(), this.index)
}
})
},
// 上传失败
uploadError(index, err) {
this.lists[index].data.progress = 0
this.lists[index].data.error = true
this.lists[index].data.response = null
this.showToast('上传失败,请重试')
this.$emit('on-error', err, index, this.sortList(), this.index)
},
// 删除一个图片
deleteItem(index) {
if (!this.deleteable) return
this.$tn.message.modal(
'提示',
'您确定要删除吗?',
async () => {
// 先检查是否有定义before-remove移除前钩子
// 执行before-remove钩子
if (this.beforeRemove && typeof(this.beforeRemove) === 'function') {
let beforeResponse = this.beforeRemove.bind(this.$tn.$parent.call(this))(index, this.lists)
// 判断是否返回promise
if (!!beforeResponse && typeof beforeResponse.then === 'function') {
await beforeResponse.then(res => {
// promise返回成功不进行操作
this.handlerDeleteItem(index)
}).catch(err => {
this.showToast('删除操作被中断')
})
} else if (beforeResponse === false) {
this.showToast('删除操作被中断')
} else {
this.handlerDeleteItem(index)
}
} else {
this.handlerDeleteItem(index)
}
}, true)
},
// 移除文件操作
handlerDeleteItem(index) {
// 如果文件正在上传中,终止上传任务
if (this.lists[index].data.progress < 100 && this.lists[index].data.progress > 0) {
typeof this.lists[index].data.uploadTask !== 'undefined' && this.lists[index].data.uploadTask.abort()
}
this.remove(index)
this.$forceUpdate()
this.$emit('on-remove', index, this.sortList(), this.index)
this.showToast('删除成功')
},
// 移除文件,通过ref手动形式进行调用
remove(index) {
if (!this.deleteable) return
// 判断索引合法
if (index >= 0 && index < this.lists.length) {
let currentItemIndex = this.lists[index].index
this.lists.splice(index, 1)
// 重新排列列表信息
for (let item of this.lists) {
if (item.index > currentItemIndex) {
item.index -= 1
item.x = item.positionX * this.baseData.widthPx
item.y = item.positionY * this.baseData.heightPx
item.positionX = item.index % this.baseData.columns
item.positionY = Math.floor(item.index / this.baseData.columns)
this.$nextTick(() => {
item.x = item.positionX * this.baseData.widthPx
item.y = item.positionY * this.baseData.heightPx
})
}
}
this.updateAddBtnPositioin()
}
},
// 预览图片
doPreviewImage(url, index) {
if (!this.previewFullImage) return
const images = this.lists.sort((l1, l2) => { return l1.index - l2.index}).map(item => item.data.url || item.data.path)
uni.previewImage({
urls: images,
current: url,
success: () => {
this.$emit('on-preview', url, this.sortList(), this.index)
},
fail: () => {
this.showToast('预览图片失败')
}
})
},
// 检查文件后缀是否合法
checkFileExt(file) {
// 是否为合法后缀
let noArrowExt = false
// 后缀名
let fileExt = ''
const reg = /.+\./
// #ifdef H5
fileExt = file.name.replace(reg, '').toLowerCase()
// #endif
// #ifndef H5
fileExt = file.path.replace(reg, '').toLowerCase()
// #endif
noArrowExt = this.limitType.some(ext => {
return ext.toLowerCase() === fileExt
})
if (!noArrowExt) this.showToast(`不支持${fileExt}格式的文件`)
return noArrowExt
},
/********************* 拖拽处理 ********************/
// 更新拖拽信息
updateDragInfo() {
this.baseData.widthPx = uni.upx2px(this.width)
this.baseData.heightPx = uni.upx2px(this.height)
// 获取可移动区域的信息,用于判断当前有多少列
const query = uni.createSelectorQuery().in(this)
query.select('.tn-image-upload__movable-area').boundingClientRect()
query.exec((res) => {
if (!res) {
setTimeout(() => {
this.updateDragInfo()
}, 10)
return
}
this.baseData.columns = Math.floor(res[0].width / this.baseData.widthPx)
// 初始化可拖拽列表信息
this.lists = []
this.fileList.forEach((item) => {
// 首先检查内部是否已经添加过这张图片,因为外部绑定了一个对象给fileList的话(对象引用),进行修改外部fileList时,
// 会触发watch,导致重新把原来的图片再次添加到this.lists
// 数组的some方法意思是,只要数组元素有任意一个元素条件符合,就返回true,而另一个数组的every方法的意思是数组所有元素都符合条件才返回true
let tmp = this.lists.map(value => {
return value.data
}).some(listVal => {
return listVal.url === item.url
})
// 如果内部没有这张图片,则添加到内部
!tmp && this.lists.push(this.handleDragListItem({
url: item.url,
error: false,
progress: 100
}))
})
// 更新添加按钮位置
this.updateAddBtnPositioin()
})
},
// 处理拖拽列表信息
handleDragListItem(item) {
const positionX = this.lists.length % this.baseData.columns
const positionY = Math.floor(this.lists.length / this.baseData.columns)
const x = positionX * this.baseData.widthPx
const y = positionY * this.baseData.heightPx
return {
id: this.unique(),
x,
y,
preX: x,
preY: y,
positionX,
positionY,
zIndex:1,
disabled: true,
opacity: 1,
scale: 1,
index: this.lists.length,
offset: 0,
moveEnd: false,
moving: false,
data: {
...item
}
}
},
// 生成元素唯一id
unique(n = 6) {
let id = ''
for (let i = 0; i < n; i++) id += Math.floor(Math.random() * 10)
return 'tn_' + new Date().getTime() + id
},
// 更新添加按钮位置
updateAddBtnPositioin() {
if (this.lists.length >= this.maxCount) return
this.addBtn.x = (this.lists.length % this.baseData.columns) * this.baseData.widthPx
this.addBtn.y = Math.floor(this.lists.length / this.baseData.columns) * this.baseData.heightPx
},
// 获取排序后数据
sortList() {
const list = this.lists.slice()
list.sort((l1, l2) => {
return l1.index - l2.index
})
return list.map(item => {
return item.data
})
},
mouseEnterArea () {
// #ifdef H5
this.lists.forEach(item => {
item.disabled = false
})
// #endif
},
mouseLeaveArea () {
// #ifdef H5
if (this.baseData.dragItem) {
this.lists.forEach(item => {
item.disabled = true
item.zIndex = 1
item.offset = 0
item.moveEnd = true
if (item.id === this.baseData.dragItem.id) {
if (this.timer) {
clearTimeout(this.timer)
this.timer = null
}
item.x = item.preX
item.y = item.preY
this.$nextTick(() => {
item.x = item.positionX * this.baseData.widthPx
item.y = item.positionY * this.baseData.heightPx
this.baseData.dragItem = null
})
}
})
this.dragging = false
}
// #endif
},
movableLongPress(item) {
// #ifndef H5
uni.vibrateShort()
// console.log("LongPress--------------------------------------------------------------");
this.lists.forEach(value => {
value.moving = false
})
this.dragging = true
// 设置对应的元素允许拖动
const index = this.lists.findIndex(obj => {
return obj.id === item.id
})
item.disabled = false
item.opacity = 0.7
item.scale = 1.1
this.$set(this.lists, index, item)
// #endif
},
movableChange (e, item) {
if (!item || !this.dragging) return
// console.log("movableChange");
item.moving = true
item.preX = e.detail.x
item.preY = e.detail.y
// console.log(item.preX, item.preY);
if (e.detail.source === 'touch') {
if (!item.moveEnd) {
item.offset = Math.sqrt(
Math.pow(item.preX - item.positionX * this.baseData.widthPx, 2) +
Math.pow(item.preY - item.positionY * this.baseData.heightPx, 2))
}
let x = Math.floor((e.detail.x + this.baseData.widthPx / 2) / this.baseData.widthPx)
if (x > this.baseData.columns) return
let y = Math.floor((e.detail.y + this.baseData.heightPx / 2) / this.baseData.heightPx)
let index = this.baseData.columns * y + x
if (item.index !== index && index < this.lists.length) {
for (let obj of this.lists) {
if (item.index > index && obj.index >= index && obj.index < item.index) {
this.updateItemPosition(obj, 1)
} else if (item.index < index && obj.index <= index && obj.index > item.index) {
this.updateItemPosition(obj, -1)
} else if (item.id != obj.id) {
// obj.offset = 0
// console.log(obj.moving);
// if (!obj.moving) {
// obj.preX = obj.positionX * this.baseData.widthPx
// obj.preY = obj.positionY * this.baseData.heightPx
// console.log("moving", obj.id, obj.preX, obj.preY);
// }
// obj.x = obj.preX
// obj.y = obj.preY
// // console.log(obj.id, obj.preX, obj.preY);
// this.$nextTick(() => {
// obj.x = obj.positionX * this.baseData.widthPx
// obj.y = obj.positionY * this.baseData.heightPx
// })
}
}
item.index = index
item.positionX = x
item.positionY = y
// TODO 发送事件
}
}
},
movableStart (item) {
// console.log("movableStart");
this.lists.forEach(item => {
item.zIndex = 1
// #ifdef H5
item.disabled = false
// #endif
})
item.zIndex = 10
item.moveEnd = false
this.baseData.dragItem = item
// #ifdef H5
this.dragging =true
this.timer = setTimeout(() => {
item.opacity = 0.7
item.scale = 1.1
clearTimeout(this.timer)
this.timer = null
}, 200)
// #endif
},
movableEnd (item) {
if (!this.dragging) return
// console.log("movableEnd");
const index = this.lists.findIndex(obj => {
return obj.id === item.id
})
if (!item.moving) {
item.preX = item.positionX * this.baseData.widthPx
item.preY = item.positionY * this.baseData.heightPx
}
item.x = item.preX
item.y = item.preY
item.offset = 0
item.moveEnd = true
item.moving = false
item.disabled = true
// console.log(item.x, item.y);
// console.log(item.id, item.moving);
// this.$set(this.lists, index, item)
// this.lists[index] = item
// console.log(this.lists[index]);
this.lists.forEach(listValue => {
listValue.moving = false
listValue.disabled = true
})
this.$nextTick(() => {
item.x = item.positionX * this.baseData.widthPx
item.y = item.positionY * this.baseData.heightPx
item.opacity = 1
item.scale = 1
this.baseData.dragItem = null
this.dragging = false
// console.log(item.x, item.y);
this.$set(this.lists, index, item)
})
this.$emit('sort-list', this.sortList())
},
// 更新图片位置信息
updateItemPosition(item, offset) {
const index = this.lists.findIndex(obj => {
return obj.id === item.id
})
item.index += offset
item.offset = 0
item.positionX = item.index % this.baseData.columns
item.positionY = Math.floor(item.index / this.baseData.columns)
if (!item.moving) {
item.preX = item.positionX * this.baseData.widthPx
item.preY = item.positionY * this.baseData.heightPx
}
item.x = item.preX
item.y = item.preY
// console.log("updateItemPosition", item.id, item.preX, item.preY);
// this.$set(this.lists, index, item)
this.$nextTick(() => {
item.x = item.positionX * this.baseData.widthPx
item.y = item.positionY * this.baseData.heightPx
this.$set(this.lists, index, item)
})
}
}
}
</script>
<style lang="scss" scoped>
.tn-image-upload {
position: relative;
&__movable-area {
width: 100%;
}
&__movable-view {
border-radius: 10rpx;
overflow: hidden;
}
&__item {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
align-items: center;
justify-content: center;
width: 200rpx;
height: 200rpx;
background-color: transparent;
position: relative;
border-radius: 10rpx;
overflow: hidden;
&-preview {
border: 1rpx solid $tn-border-solid-color;
&__delete {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
top: 0;
right: 0;
z-index: 10;
border-top: 60rpx solid;
border-left: 60rpx solid transparent;
border-top-color: $tn-color-red;
width: 0rpx;
height: 0rpx;
&--icon {
position: absolute;
top: -50rpx;
right: 6rpx;
color: #FFFFFF;
font-size: 24rpx;
line-height: 1;
}
}
&__progress {
position: absolute;
width: auto;
bottom: 0rpx;
left: 0rpx;
right: 0rpx;
z-index: 9;
/* #ifdef MP-WEIXIN */
display: inline-flex;
/* #endif */
}
&__error-btn {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background-color: $tn-color-red;
color: #FFFFFF;
font-size: 20rpx;
padding: 8rpx 0;
text-align: center;
z-index: 9;
line-height: 1;
}
&__image {
display: block;
width: 100%;
height: 100%;
border-radius: 10rpx;
}
}
&-add {
flex-direction: column;
color: $tn-content-color;
font-size: 26rpx;
&--icon {
font-size: 40rpx;
}
&__tips {
margin-top: 20rpx;
line-height: 40rpx;
}
}
}
&__add {
background-color: $tn-font-holder-color;
position: absolute;
// margin: 10rpx;
// margin-left: 0;
}
}
</style>
@@ -1,5 +1,5 @@
<template>
<view v-if="!disabled" class="tn-image-upload-class tn-image-upload">
<view class="tn-image-upload-class tn-image-upload">
<block v-if="showUploadList">
<view
v-for="(item, index) in lists"
@@ -88,6 +88,10 @@
return []
}
},
isUniCloud:{
type: Boolean,
default: false
},
// 上传图片地址
action: {
type: String,
@@ -312,7 +316,7 @@
let file = null
let listOldLength = lists.length
res.tempFiles.map((val, index) => {
if (!this.checkFileExt(val)) return
if (!this.checkFileExt(val)) return
// 是否超出最大限制数量
if (!multiple && index >= 1) return
@@ -329,7 +333,13 @@
url: val.path,
progress: 0,
error: false,
file: val
file: val,
//#ifdef H5
extension: val.name.substring(val.name.lastIndexOf(".")) //图片文件后缀名
//#endif
//#ifndef H5
extension: val.path.substring(val.path.lastIndexOf(".")) //图片文件后缀名
//#endif
})
}
})
@@ -359,6 +369,7 @@
},
// 上传文件
async uploadFile(index = 0) {
var that=this;
if (this.disabled) return
if (this.uploading) return
// 全部上传完成
@@ -395,48 +406,87 @@
}
}
// 检查上传地址
if (!this.action) {
this.showToast('请配置上传地址', true)
return
}
this.lists[index].error = false
this.uploading = true
// 创建上传对象
const task = uni.uploadFile({
url: this.action,
filePath: this.lists[index].url,
name: this.name,
formData: this.formData,
header: this.header,
success: res => {
// 判断啊是否为json字符串,将其转换为json格式
let data = this.toJson && this.$tn.test.jsonString(res.data) ? JSON.parse(res.data) : res.data
if (![200, 201, 204].includes(res.statusCode)) {
this.uploadError(index, data)
} else {
this.lists[index].response = data
this.lists[index].progress = 100
this.lists[index].error = false
this.$emit('on-success', data, index, this.lists, this.index)
}
},
fail: err => {
this.uploadError(index, err)
},
complete: res => {
this.$tn.message.closeLoading()
this.uploading = false
this.uploadFile(index + 1)
this.$emit('on-change', res, index, this.lists, this.index)
}
})
this.lists[index].uploadTask = task
task.onProgressUpdate(res => {
if (res.progress > 0) {
this.lists[index].progress = res.progress
this.$emit('on-progress', res, index, this.lists, this.index)
}
})
if(this.isUniCloud){
//如果是uinCloud
this.lists[index].error = false
this.uploading = true
// 创建上传对象
const task = uniCloud.uploadFile({
filePath: that.lists[index].url,
cloudPath: that.$tn.uuid() + that.lists[index].extension, //随机文件名+文件后缀
onUploadProgress(progressEvent) {
if (progressEvent.loaded > 0) {
that.lists[index].progress = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
that.$emit('on-progress', progressEvent, index, that.lists, that.index)
}
},
success(res) {
if (res.success) {
that.lists[index].response = res
that.lists[index].progress = 100
that.lists[index].error = false
that.$emit('success', res, index, that.lists, that.index)
} else {
that.uploadError(index, res)
}
},
fail: (err) => {
that.uploadError(index, err)
},
complete: (res) => {
// this.$t.message.closeLoading()
that.uploading = false
that.uploadFile(index + 1)
that.$emit('on-change', res, index, that.lists, that.index)
}
});
that.lists[index].uploadTask = task
}else{
if (!this.action) {
this.showToast('请配置上传地址', true)
return
}
this.lists[index].error = false
this.uploading = true
// 创建上传对象
const task = uni.uploadFile({
url: this.action,
filePath: this.lists[index].url,
name: this.name,
formData: this.formData,
header: this.header,
success: res => {
// 判断啊是否为json字符串,将其转换为json格式
let data = this.toJson && this.$tn.test.jsonString(res.data) ? JSON.parse(res.data) : res.data
if (![200, 201, 204].includes(res.statusCode)) {
this.uploadError(index, data)
} else {
this.lists[index].response = data
this.lists[index].progress = 100
this.lists[index].error = false
this.$emit('on-success', data, index, this.lists, this.index)
}
},
fail: err => {
this.uploadError(index, err)
},
complete: res => {
this.$tn.message.closeLoading()
this.uploading = false
this.uploadFile(index + 1)
this.$emit('on-change', res, index, this.lists, this.index)
}
})
this.lists[index].uploadTask = task
task.onProgressUpdate(res => {
if (res.progress > 0) {
this.lists[index].progress = res.progress
this.$emit('on-progress', res, index, this.lists, this.index)
}
})
}
},
// 上传失败
uploadError(index, err) {
+318 -327
View File
@@ -1,334 +1,325 @@
<template>
<view
:id="elId"
class="tn-rate-class tn-rate"
@touchmove.stop.prevent="touchMove"
>
<view class="tn-rate__wrap" :class="[elClass]" v-for="(item, index) in count" :key="index">
<view
class="tn-rate__wrap__icon"
:class="[`tn-icon-${(allowHalf && halfIcon ? activeIndex > index + 1 : activeIndex > index) ? elActionIcon : elInactionIcon}`]"
:style="[iconStyle(index)]"
@tap="click(index + 1, $event)"
>
<!-- 半图标 -->
<view
v-if="showHalfIcon(index)"
class="tn-rate__wrap__icon--half"
:class="[`tn-icon-${elActionIcon}`]"
:style="[halfIconStyle]"
></view>
</view>
</view>
</view>
<view :id="elId" class="tn-rate-class tn-rate" @touchmove.stop.prevent="touchMove">
<view class="tn-rate__wrap" :class="[elClass]" v-for="(item, index) in count" :key="index">
<view class="tn-rate__wrap__icon"
:class="[`tn-icon-${(allowHalf && halfIcon ? activeIndex > index + 1 : activeIndex > index) ? elActionIcon : elInactionIcon}`]"
:style="[iconStyle(index)]" @tap="click(index + 1, $event)">
<!-- 半图标 -->
<view v-if="showHalfIcon(index)" class="tn-rate__wrap__icon--half" :class="[`tn-icon-${elActionIcon}`]"
:style="[halfIconStyle]"></view>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'tn-rate',
props: {
// 选中星星的数量
value: {
type: Number,
default: 0
},
// 显示的星星数
count: {
type: Number,
default: 5
},
// 最小能选择的星星数
minCount: {
type: Number,
default: 0
},
// 禁用状态
disabled: {
type: Boolean,
default: false
},
// 是否可以选择半星
allowHalf: {
type: Boolean,
default: false
},
// 星星大小
size: {
type: Number,
default: 32
},
// 被选中的图标
activeIcon: {
type: String,
default: 'star-fill'
},
// 未被选中的图标
inactiveIcon: {
type: String,
default: 'star'
},
// 被选中的颜色
activeColor: {
type: String,
default: '#01BEFF'
},
// 默认颜色
inactiveColor: {
type: String,
default: '#AAAAAA'
},
// 星星之间的距离
gutter: {
type: Number,
default: 10
},
// 自定义颜色
colors: {
type: Array,
default() {
return []
}
},
// 自定义图标
icons: {
type: Array,
default() {
return []
}
}
},
computed: {
// 图标显示的比例
showHalfIcon(index) {
return index => {
return this.allowHalf && Math.ceil(this.activeIndex) === index + 1 && this.halfIcon
}
},
// 被激活的图标
elActionIcon() {
const len = this.icons.length
// icons参数传递了3个图标,当选中2个时,用第一个图标,4个时,用第二个图标
// 5个时,用第三个图标作为激活的图标
if (len && len <= this.count) {
const step = Math.round(Math.ceil(this.activeIndex) / Math.round(this.count / len))
if (step < 1) return this.icons[0]
if (step > len) return this.icons[len - 1]
return this.icons[step - 1]
}
return this.activeIcon
},
// 未被激活的图标
elInactionIcon() {
const len = this.icons.length
// icons参数传递了3个图标,当选中2个时,用第一个图标,4个时,用第二个图标
// 5个时,用第三个图标作为激活的图标
if (len && len <= this.count) {
const step = Math.round(Math.ceil(this.activeIndex) / Math.round(this.count / len))
if (step < 1) return this.icons[0]
if (step > len) return this.icons[len - 1]
return this.icons[step - 1]
}
return this.inactiveIcon
},
// 被激活的颜色
elActionColor() {
const len = this.colors.length
// 如果有设置colors参数(此参数用于将图标分段,比如一共5颗星,colors传3个颜色值,那么根据一定的规则,2颗星可能为第一个颜色
// 4颗星为第二个颜色值,5颗星为第三个颜色值)
if (len && len <= this.count) {
const step = Math.round(Math.ceil(this.activeIndex) / Math.round(this.count / len))
if (step < 1) return this.colors[0]
if (step > len) return this.colors[len - 1]
return this.colors[step - 1]
}
return this.activeColor
},
// 图标的样式
iconStyle() {
return index => {
let style = {}
style.fontSize = this.$tn.string.getLengthUnitValue(this.size)
style.padding = `0 ${this.$tn.string.getLengthUnitValue(this.gutter / 2)}`
// 当前图标的颜色
if (this.allowHalf && this.halfIcon) {
style.color = this.activeIndex > index + 1 ? this.elActionColor : this.inactiveColor
} else {
style.color = this.activeIndex > index ? this.elActionColor : this.inactiveColor
}
return style
}
},
// 半图标样式
halfIconStyle() {
let style = {}
style.fontSize = this.$tn.string.getLengthUnitValue(this.size)
style.padding = `0 ${this.$tn.string.getLengthUnitValue(this.gutter / 2)}`
style.color = this.elActionColor
return style
}
},
data() {
return {
// 保证控件的唯一性
elId: this.$tn.uuid(),
elClass: this.$tn.uuid(),
// 评分盒子左边到屏幕左边的距离,用于滑动选择时计算距离
starBoxLeft: 0,
// 当前激活的星星的序号
activeIndex: this.value,
// 每个星星的宽度
starWidth: 0,
// 每个星星最右边到盒子组件最左边的距离
starWidthArr: [],
// 标记是否为半图标
halfIcon: false,
}
},
watch: {
value(val) {
this.activeIndex = val
if (this.allowHalf && (val % 1 === 0.5)) {
this.halfIcon = true
} else {
this.halfIcon = false
}
},
size() {
// 当尺寸修改的时候重新获取布局尺寸信息
this.$nextTick(() => {
this.getElRectById()
this.getElRectByClass()
})
}
},
mounted() {
this.getElRectById()
this.getElRectByClass()
},
methods: {
// 获取评分组件盒子的布局信息
getElRectById() {
this._tGetRect('#'+this.elId).then(res => {
this.starBoxLeft = res.left
})
},
// 获取单个星星的尺寸
getElRectByClass() {
this._tGetRect('.'+this.elClass).then(res => {
this.starWidth = res.width
// 把每个星星最右边到盒子最左边的距离
for (let i = 0; i < this.count; i++) {
this.starWidthArr[i] = (i + 1) * this.starWidth
}
})
},
// 手指滑动
touchMove(e) {
if (this.disabled) return
if (!e.changedTouches[0]) return
const movePageX = e.changedTouches[0].pageX
// 滑动点相对于评分盒子左边的距离
const distance = movePageX - this.starBoxLeft
// 如果滑动到了评分盒子左边界,设置为0星
if (distance <= 0) {
this.activeIndex = 0
}
// 计算滑动的距离相当于点击多少颗星星
let index = Math.ceil(distance / this.starWidth)
if (this.allowHalf) {
const iconHalfWidth = this.starWidthArr[index - 1] - (this.starWidth / 2)
if (distance < iconHalfWidth) {
this.halfIcon = true
index -= 0.5
} else {
this.halfIcon = false
}
}
this.activeIndex = index > this.count ? this.count : index
if (this.activeIndex < this.minCount) this.activeIndex = this.minCount
this.emitEvent()
},
// 通过点击直接选中
click(index, e) {
if (this.disabled) return
// 半星选择
if (this.allowHalf) {
if (!e.changedTouches[0]) return
const movePageX = e.changedTouches[0].pageX
// 点击点相对于当前图标左边的距离
const distance = movePageX - this.starBoxLeft
const iconHalfWidth = this.starWidthArr[index - 1] - (this.starWidth / 2)
if (distance < iconHalfWidth) {
this.halfIcon = true
} else {
this.halfIcon = false
}
}
// 对第一个星星特殊处理,只有一个的时候,点击可以取消,否则无法作0星评价
if (index == 1) {
if (this.allowHalf && this.allowHalf) {
if ((this.activeIndex === 0.5 && this.halfIcon) ||
(this.activeIndex === 1 && !this.halfIcon)) {
this.activeIndex = 0
} else {
this.activeIndex = this.halfIcon ? 0.5 : 1
}
} else {
if (this.activeIndex == 1) {
this.activeIndex = 0
} else {
this.activeIndex = 1
}
}
} else {
this.activeIndex = (this.allowHalf && this.halfIcon) ? index - 0.5 : index
}
if (this.activeIndex < this.minCount) this.activeIndex = this.minCount
this.emitEvent()
},
// 发送事件
emitEvent() {
this.$emit('change', this.activeIndex)
// 修改v-model的值
this.$emit('input', this.activeIndex)
}
}
}
export default {
name: 'tn-rate',
props: {
// 选中星星的数量
value: {
type: Number,
default: 0
},
// 显示的星星数
count: {
type: Number,
default: 5
},
// 最小能选择的星星数
minCount: {
type: Number,
default: 0
},
// 禁用状态
disabled: {
type: Boolean,
default: false
},
// 是否可以选择半星
allowHalf: {
type: Boolean,
default: false
},
// 星星大小
size: {
type: Number,
default: 32
},
// 被选中的图标
activeIcon: {
type: String,
default: 'star-fill'
},
// 未被选中的图标
inactiveIcon: {
type: String,
default: 'star'
},
// 被选中的颜色
activeColor: {
type: String,
default: '#01BEFF'
},
// 默认颜色
inactiveColor: {
type: String,
default: '#AAAAAA'
},
// 星星之间的距离
gutter: {
type: Number,
default: 10
},
// 自定义颜色
colors: {
type: Array,
default () {
return []
}
},
// 自定义图标
icons: {
type: Array,
default () {
return []
}
}
},
computed: {
// 图标显示的比例
showHalfIcon(index) {
return index => {
return this.allowHalf && Math.ceil(this.activeIndex) === index + 1 && this.halfIcon
}
},
// 被激活的图标
elActionIcon() {
const len = this.icons.length
// icons参数传递了3个图标,当选中2个时,用第一个图标,4个时,用第二个图标
// 5个时,用第三个图标作为激活的图标
if (len && len <= this.count) {
const step = Math.round(Math.ceil(this.activeIndex) / Math.round(this.count / len))
if (step < 1) return this.icons[0]
if (step > len) return this.icons[len - 1]
return this.icons[step - 1]
}
return this.activeIcon
},
// 未被激活的图标
elInactionIcon() {
const len = this.icons.length
// icons参数传递了3个图标,当选中2个时,用第一个图标,4个时,用第二个图标
// 5个时,用第三个图标作为激活的图标
if (len && len <= this.count) {
const step = Math.round(Math.ceil(this.activeIndex) / Math.round(this.count / len))
if (step < 1) return this.icons[0]
if (step > len) return this.icons[len - 1]
return this.icons[step - 1]
}
return this.inactiveIcon
},
// 被激活的颜色
elActionColor() {
const len = this.colors.length
// 如果有设置colors参数(此参数用于将图标分段,比如一共5颗星,colors传3个颜色值,那么根据一定的规则,2颗星可能为第一个颜色
// 4颗星为第二个颜色值,5颗星为第三个颜色值)
if (len && len <= this.count) {
const step = Math.round(Math.ceil(this.activeIndex) / Math.round(this.count / len))
if (step < 1) return this.colors[0]
if (step > len) return this.colors[len - 1]
return this.colors[step - 1]
}
return this.activeColor
},
// 图标的样式
iconStyle() {
return index => {
let style = {}
style.fontSize = this.$tn.string.getLengthUnitValue(this.size)
style.padding = `0 ${this.$tn.string.getLengthUnitValue(this.gutter / 2)}`
// 当前图标的颜色
if (this.allowHalf && this.halfIcon) {
style.color = this.activeIndex > index + 1 ? this.elActionColor : this.inactiveColor
} else {
style.color = this.activeIndex > index ? this.elActionColor : this.inactiveColor
}
return style
}
},
// 半图标样式
halfIconStyle() {
let style = {}
style.fontSize = this.$tn.string.getLengthUnitValue(this.size)
style.padding = `0 ${this.$tn.string.getLengthUnitValue(this.gutter / 2)}`
style.color = this.elActionColor
return style
}
},
data() {
return {
// 保证控件的唯一性
elId: this.$tn.uuid(),
elClass: this.$tn.uuid(),
// 评分盒子左边到屏幕左边的距离,用于滑动选择时计算距离
starBoxLeft: 0,
// 当前激活的星星的序号
activeIndex: this.value,
// 每个星星的宽度
starWidth: 0,
// 每个星星最右边到盒子组件最左边的距离
starWidthArr: [],
// 标记是否为半图标
halfIcon: false,
}
},
watch: {
value: {
handler(newName, oldName) {
this.activeIndex = newName
if (this.allowHalf && (newName % 1 === 0.5)) {
this.halfIcon = true
} else {
this.halfIcon = false
}
},
immediate: true
},
size() {
// 当尺寸修改的时候重新获取布局尺寸信息
this.$nextTick(() => {
this.getElRectById()
this.getElRectByClass()
})
}
},
mounted() {
this.getElRectById()
this.getElRectByClass()
},
methods: {
// 获取评分组件盒子的布局信息
getElRectById() {
this._tGetRect('#' + this.elId).then(res => {
this.starBoxLeft = res.left
})
},
// 获取单个星星的尺寸
getElRectByClass() {
this._tGetRect('.' + this.elClass).then(res => {
this.starWidth = res.width
// 把每个星星最右边到盒子最左边的距离
for (let i = 0; i < this.count; i++) {
this.starWidthArr[i] = (i + 1) * this.starWidth
}
})
},
// 手指滑动
touchMove(e) {
if (this.disabled) return
if (!e.changedTouches[0]) return
const movePageX = e.changedTouches[0].pageX
// 滑动点相对于评分盒子左边的距离
const distance = movePageX - this.starBoxLeft
// 如果滑动到了评分盒子的左边界,设置为0星
if (distance <= 0) {
this.activeIndex = 0
}
// 计算滑动的距离相当于点击多少颗星星
let index = Math.ceil(distance / this.starWidth)
if (this.allowHalf) {
const iconHalfWidth = this.starWidthArr[index - 1] - (this.starWidth / 2)
if (distance < iconHalfWidth) {
this.halfIcon = true
index -= 0.5
} else {
this.halfIcon = false
}
}
this.activeIndex = index > this.count ? this.count : index
if (this.activeIndex < this.minCount) this.activeIndex = this.minCount
this.emitEvent()
},
// 通过点击直接选中
click(index, e) {
if (this.disabled) return
// 半星选择
if (this.allowHalf) {
if (!e.changedTouches[0]) return
const movePageX = e.changedTouches[0].pageX
// 点击点相对于当前图标左边的距离
const distance = movePageX - this.starBoxLeft
const iconHalfWidth = this.starWidthArr[index - 1] - (this.starWidth / 2)
if (distance < iconHalfWidth) {
this.halfIcon = true
} else {
this.halfIcon = false
}
}
// 对第一个星星特殊处理,只有一个的时候,点击可以取消,否则无法作0星评价
if (index == 1) {
if (this.allowHalf && this.allowHalf) {
if ((this.activeIndex === 0.5 && this.halfIcon) ||
(this.activeIndex === 1 && !this.halfIcon)) {
this.activeIndex = 0
} else {
this.activeIndex = this.halfIcon ? 0.5 : 1
}
} else {
if (this.activeIndex == 1) {
this.activeIndex = 0
} else {
this.activeIndex = 1
}
}
} else {
this.activeIndex = (this.allowHalf && this.halfIcon) ? index - 0.5 : index
}
if (this.activeIndex < this.minCount) this.activeIndex = this.minCount
this.emitEvent()
},
// 发送事件
emitEvent() {
this.$emit('change', this.activeIndex)
// 修改v-model的值
this.$emit('input', this.activeIndex)
}
}
}
</script>
<style lang="scss" scoped>
.tn-rate {
display: inline-flex;
align-items: center;
margin: 0;
padding: 0;
&__wrap {
&__icon {
position: relative;
box-sizing: border-box;
&--half {
position: absolute;
top: 0;
left: 0;
display: inline-block;
overflow: hidden;
width: 50%;
}
}
}
}
</style>
.tn-rate {
display: inline-flex;
align-items: center;
margin: 0;
padding: 0;
&__wrap {
&__icon {
position: relative;
box-sizing: border-box;
&--half {
position: absolute;
top: 0;
left: 0;
display: inline-block;
overflow: hidden;
width: 50%;
}
}
}
}
</style>
@@ -0,0 +1,401 @@
<template>
<view class="tn-scroll-view-class tn-scroll-view">
<scroll-view
class="scroll-view"
:style="[scrollViewStyle]"
scroll-y
scroll-anchoring
enable-back-to-top
:throttle="false"
:scroll-top="scrollTop"
:lower-threshold="lowerThreshold"
@scroll="handleScroll"
@touchend="handleTouchEnd"
@touchmove.prevent.stop="handleTouchMove"
@touchstart="handleTouchStart"
@scrolltolower="handleScrollTolower"
>
<view class="scroll__content" :style="[scrollContentStyle]">
<view class="scroll__pull-down">
<slot name="pulldown">
<view class="scroll__refresh" :style="[refreshStyle]">
<view><tn-loading :animation="refreshing"></tn-loading></view>
<view class="scroll__refresh--text" :style="[refreshTextStyle]">{{ refreshStateText }}</view>
</view>
</slot>
</view>
<view :id="elScrollDataId" class="scroll__data">
<slot></slot>
</view>
</view>
</scroll-view>
</view>
</template>
<script>
import componentsColor from '../../libs/mixin/components_color.js'
export default {
name: 'tn-scroll-view',
mixins: [ componentsColor ],
props: {
// H5顶部导航栏的高度
h5NavHeight: {
type: Number,
default: 45
},
// 自定义顶部导航栏高度
customNavHeight: {
type: Number,
default: 0
},
// 可滚动区域顶部偏移高度
offsetTop: {
type: Number,
default: 0
},
// 可滚动区域底部偏移高度
offsetBottom: {
type: Number,
default: 0
},
// 容器高度 (不设置则自动计算)
height: {
type: Number,
default: null
},
// 是否禁用
disabled: {
type: Boolean,
default: false
},
// 禁用下拉刷新
pullDownDisabled: {
type: Boolean,
default: false
},
// 下拉速率
pullDownSpeed: {
type: Number,
default: 0.5
},
// 刷新延迟
refreshDelayed: {
type: Number,
default: 800
},
// 刷新完成后延迟
refreshFinishDelayed: {
type: Number,
default: 800
},
// 下拉刷新距离
refresherThreshold: {
type: Number,
default: 70
},
// 上拉加载距离
lowerThreshold: {
type: Number,
default: 40
},
// 刷新状态
refreshState: {
type: Boolean,
default: false
},
// 正在刷新文字
refreshingText: {
type: String,
default: '正在刷新'
},
// 刷新成功文字
refreshSuccessText: {
type: String,
default: '刷新成功'
},
// 下拉中的文字
pulldownText: {
type: String,
default: '下拉刷新'
},
// 下拉完成的文字
pulldownFinishText: {
type: String,
default: '松开刷新'
}
},
data() {
return {
// 滚动容器内容id
elScrollDataId: '',
// 系统信息
systemInfo: {
height: 0,
statusBarHeight: 0
},
// 距离顶部滚动高度
scrollTop: 0,
// 滚动内容视图顶部位置
scrollDataTop: -1,
// 滚动内容视图顶部位置偏移
scrollDataOffsetTop: -1,
// 滚动区域的高度
scrollViewHeight: 0,
// 当前滚动高度
currentScrollTop: 0,
// 当前触摸点Y轴开始坐标
currentTouchStartY: 0,
// 刷新状态文字
refreshStateText: '下拉刷新',
// 是否刷新中
refreshing: false,
// 是否刷新完成
refreshFinish: false,
// 是否正在下拉
pulldowning: false,
// 下拉高度
pullDownHeight: 0,
// 是否显示下拉加载
showPullDown: false
}
},
computed: {
scrollViewStyle() {
let style = {}
style.height = this.scrollViewHeight + 'px'
if (!this.backgroundColorClass) {
style.backgroundColor = this.backgroundColorStyle
}
return style
},
scrollContentStyle() {
let style = {}
style.transform = this.showPullDown ? `translateY(${this.pullDownHeight}px)` : `translateY(0px)`
style.transition = this.pulldowning ? `transform 100ms ease-out` : `transform 500ms cubic-bezier(0.19,1.64,0.42,0.72)`
return style
},
refreshStyle() {
let style = {}
style.opacity = this.showPullDown ? 1 : 0
return style
},
refreshTextStyle() {
let style = {}
if (!this.fontColorClass) {
style.color = this.fontColorStyle
}
return style
},
loadTextStyle() {
let style = {}
if (!this.fontColorClass) {
style.color = this.fontColorStyle
}
return style
}
},
watch: {
refreshState(nVal, oVal) {
if (!nVal) {
if (this.showPullDown) {
// 关闭正在下拉
this.pulldowning = false
// 隐藏下拉刷新
this.showPullDown = false
// 关闭正在刷新
this.refreshing = false
}
}
}
},
created() {
this.elScrollDataId = this.$tn.uuid()
this.getSystemInfo()
},
mounted() {
this.$nextTick(() => {
this.init()
})
},
methods: {
// 组件初始化
init() {
this.refreshStateText = this.pulldownText
// 初始化scrollView信息
this.updateScrollViewInfo()
},
// 获取系统信息
getSystemInfo() {
const systemInfo = uni.getSystemInfoSync()
this.systemInfo.height = systemInfo.safeArea.height
this.systemInfo.statusBarHeight = systemInfo.statusBarHeight
},
// 更新scrollView信息
updateScrollViewInfo() {
if (this.height) {
this.scrollViewHeight = this.height
} else {
// 设置scrollView的高度和组件顶部位置
// console.log(this.systemInfo, this.offsetTop, this.customNavHeight);
// #ifdef H5
this.scrollViewHeight = this.systemInfo.height - (
this.offsetTop +
(this.customNavHeight ? this.customNavHeight : this.h5NavHeight) +
this.offsetBottom)
this.scrollDataOffsetTop = this.offsetTop + (this.customNavHeight ? this.customNavHeight : this.h5NavHeight)
// #endif
// #ifndef H5
this.scrollViewHeight = this.systemInfo.height - (
this.offsetTop +
this.systemInfo.statusBarHeight +
this.offsetBottom)
this.scrollDataOffsetTop = this.offsetTop + this.systemInfo.statusBarHeight
// #endif
}
},
// 获取scrollView内容信息
async getScrollDataInfo() {
const scrollInfo = await this._tGetRect(`#${this.elScrollDataId}`)
this.scrollDataTop = scrollInfo.top
},
// 上拉触底事件
handleScrollTolower(e) {
if (this.pullUpDisabled) return
this.$emit('scrolltolower', e)
},
// 滚动事件
handleScroll(e) {
this.currentScrollTop = e.detail.scrollTop
this.$emit('scroll', e.detail)
},
// 触摸按下事件
handleTouchStart(e) {
if (this.disabled) return
this.currentTouchStartY = e.touches[0].clientY
this.getScrollDataInfo()
this.$emit('touchStart', e)
},
// 触摸下滑事件
handleTouchMove(e) {
if (this.disabled) return
if (this.currentScrollTop == 0 && e.touches[0].clientY >= this.currentTouchStartY) {
// 容器滑动的偏移
const moveOffset = this.scrollDataTop > 0 ?
(this.scrollDataOffsetTop - this.scrollDataTop) :
(Math.abs(this.scrollDataTop) + this.scrollDataOffsetTop)
this.pulldowning = true
this.showPullDown = true
let pullDownDistance = ((e.touches[0].clientY - this.currentTouchStartY) - moveOffset) * this.pullDownSpeed
this.pullDownHeight = pullDownDistance
// this.pullDownHeight = pullDownDistance > this.refresherThreshold ? this.refresherThreshold : pullDownDistance
this.refreshStateText = this.pullDownHeight >= this.refresherThreshold ? this.pulldownFinishText : this.pulldownText
if (pullDownDistance > this.refresherThreshold) {
this.$emit('refreshReady')
}
}
this.$emit('touchMove', e)
},
// 触摸松开处理
handleTouchEnd(e) {
if (this.disabled) return
// 处理下拉刷新
if (this.showPullDown) {
// 当下拉高度小于下拉阈值
if (this.pullDownHeight < this.refresherThreshold) {
// 关闭正在下拉
this.pulldowning = false
// 重置下拉高度
this.pullDownHeight = 0
// 隐藏下拉刷新
this.showPullDown = false
// 触发下拉中断事件
this.$emit('refreshStop')
} else {
this.pullDownHeight = this.pullDownHeight > this.refresherThreshold ? this.refresherThreshold : this.pullDownHeight
this.refresh()
}
}
// 触发下拉触摸松开事件
this.$emit('touchEnd', e)
},
// 刷新数据
refresh() {
// 设置刷新未完成
this.refreshFinish = false
// 开启正在刷新
this.refreshing = true
// 设置正在刷新状态文字
this.refreshStateText = this.refreshingText
// 触发refresh事件
setTimeout(() => {
this.$emit('refresh')
}, this.refreshDelayed)
},
}
}
</script>
<style lang="scss" scoped>
.tn-scroll-view {
.scroll-view {
position: relative;
touch-action: none;
.scroll__content {
display: flex;
will-change: transform;
flex-direction: column;
.scroll {
&__pull-down {
position: absolute;
left: 0;
width: 100%;
display: flex;
padding: 30rpx 0;
align-items: center;
justify-content: center;
transform: translateY(-100%);
}
&__refresh {
display: flex;
align-items: center;
justify-content: center;
&--text {
margin-left: 10rpx;
}
}
&__data {
}
&__pull-up {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
transform: translateY(100%);
}
&__load {
padding: 20rpx 0;
display: flex;
align-items: center;
justify-content: center;
&--text {
margin-left: 10rpx;
}
}
}
}
}
}
</style>
+75 -13
View File
@@ -30,7 +30,14 @@
>{{ confirmText }}</view>
</view>
<!-- 列表内容 -->
<view class="tn-select__content__body">
<view v-if="searchShow && mode==='single'">
<view class="tn-flex tn-select__content__body__search">
<view class="tn-icon-search tn-padding-sm" ></view>
<input class="tn-select__content__body__search__input" :placeholder="searchPlaceholder" maxlength="255" confirm-type="search" @input="searchInput" @confirm="search" >
</view>
</view>
<picker-view
class="tn-select__content__body__view"
:value="defaultSelector"
@@ -38,6 +45,7 @@
@pickend="pickEnd"
@change="columnChange"
>
<picker-view-column v-for="(item, index) in columnData" :key="index">
<view class="tn-select__content__body__item" v-for="(sub_item, sub_index) in item" :key="sub_index">
<view class="tn-text-ellipsis">
@@ -134,6 +142,16 @@
zIndex: {
type: Number,
default: 0
},
// 是否开启模糊搜索(只在单列模式生效)
searchShow:{
type:Boolean,
default:true
},
//搜索框placeholder
searchPlaceholder:{
type:String,
default:'搜索'
}
},
computed: {
@@ -154,7 +172,8 @@
// 上一次改变时的index
lastSelectIndex: [],
// 列数
columnNum: 0
columnNum: 0,
}
},
watch: {
@@ -167,6 +186,33 @@
}
},
methods: {
//搜索输入监听
searchInput(e){
console.log(e.detail.value);
this.searchResult(e.detail.value)
},
//搜索完成监听
search(e){
console.log(e.detail.value)
this.searchResult(e.detail.value)
},
//执行搜索方法
searchResult(value) {
let result = [];
// console.log(this.list)
let data = this.list.filter(item => item.label.indexOf(value) > -1);
if (data.length > 0) {
result.push(data
);
}
// console.log(result)
this.columnData = result;
this.selectValue=[]
if (this.columnData.length>0){
this.setSelectValue()
}
},
// 标识滑动开始,只有微信小程序才有这样的事件
pickStart() {
// #ifdef MP-WEIXIN
@@ -248,12 +294,13 @@
if (tmp && tmp.extra) data.extra = tmp.extra
this.selectValue.push(data)
}
// console.log("默认",this.selectValue)
},
// 列选项
columnChange(event) {
let index = null
let columnIndex = event.detail.value
this.selectValue = []
if (this.mode === 'multi-auto') {
// 对比前后两个数组,判断变更的是那一列
@@ -314,12 +361,12 @@
</script>
<style lang="scss" scoped>
.tn-select {
&__content {
position: relative;
&__header {
position: relative;
display: flex;
@@ -332,38 +379,53 @@
box-sizing: border-box;
font-size: 30rpx;
background-color: #FFFFFF;
&__btn {
padding: 16rpx;
box-sizing: border-box;
text-align: center;
text-decoration: none;
}
&__title {
color: $tn-font-color;
}
&--cancel {
color: $tn-font-sub-color;
}
&--confirm {
color: $tn-main-color;
}
}
&__body {
width: 100%;
height: 500rpx;
overflow: hidden;
background-color: #FFFFFF;
&__search{
z-index: 5;
align-items: center;
border-radius: 19px;
background: #f8f8f8;
width: calc(100% - 60rpx);
margin: 0 auto;
position: relative;
top: 15px;
&__input{
width: 600rpx;
}
}
&__view {
height: 100%;
box-sizing: border-box;
}
&__item {
display: flex;
flex-direction: row;
@@ -375,6 +437,6 @@
}
}
}
}
</style>
@@ -0,0 +1,657 @@
function setTimeout(instance, cb, time) {
if (time > 0) {
var s = getDate().getTime()
var fn = function () {
if (getDate().getTime() - s > time) {
cb && cb()
} else
instance.requestAnimationFrame(fn)
}
fn()
}
else
cb && cb()
}
// 判断触摸的移动方向
function decideSwiperDirection(startTouches, currentTouches, direction) {
// 震动偏移容差
var toleranceShake = 30
// 移动容差
var toleranceTranslate = 10
if (direction === 'horizontal') {
// 水平方向移动
if (Math.abs(currentTouches.y - startTouches.y) <= toleranceShake) {
// console.log(currentTouches.x, startTouches.x);
if (Math.abs(currentTouches.x - startTouches.x) > toleranceTranslate) {
if (currentTouches.x - startTouches.x > 0) {
return 'right'
} else if (currentTouches.x - startTouches.x < 0) {
return 'left'
}
}
}
} else if (direction === 'vertical') {
// 垂直方向移动
if (Math.abs(currentTouches.x - startTouches.x) <= toleranceShake) {
// console.log(currentTouches.x, startTouches.x);
if (Math.abs(currentTouches.y - startTouches.y) > toleranceTranslate) {
if (currentTouches.y - startTouches.y > 0) {
return 'down'
} else if (currentTouches.y - startTouches.y < 0) {
return 'up'
}
}
}
}
return ''
}
// 更新轮播样式信息
function updateSwiperStyle(currentTouches, instance, state) {
var itemData = state.itemData
var itemsInstance = state.itemsInstance
var list = state.list
var currentIndex = state.currentIndex
var touchRelactive = state.touchRelactive
// console.log(itemAnimationWidth);
if (itemData.direction === 'horizontal') {
// 水平方向
var itemAnimationWidth = state.itemAnimationWidth
// 偏移的x轴距离
var translateX = currentTouches.x - touchRelactive.x
if (currentTouches.x > itemData.windowWidth || currentTouches.x < 0) return
// console.log(translateX);
// 更新其他轮播样式
if (state.direction == 'left') {
// 设置当前激活元素的偏移量
instance.setStyle({
'transform': 'translate3d('+ translateX + 'px, 0px, 0px)',
'z-index': list[currentIndex].zIndex + 1,
'opacity': list[currentIndex].opacity
})
// 移动距离是否超过了指定的容器宽度
if (Math.abs(translateX) > itemAnimationWidth) {
state.itemsInstance.forEach( function(itemInstance, index) {
if (index != currentIndex) {
var preIndex = (index == 0) ? list.length - 1 : index - 1
var distanceRate = (Math.abs(translateX) - itemAnimationWidth) / (itemData.itemWidth - itemAnimationWidth)
var itemTranslateX = list[index].translateX - (list[index].translateX - list[preIndex].translateX) * distanceRate
var itemScale = list[index].scale + (list[preIndex].scale - list[index].scale) * distanceRate
var itemOpacity = list[index].opacity + (list[preIndex].opacity - list[index].opacity) * distanceRate
// console.log(preIndex);
// console.log(list[index]);
// console.log(distanceRate);
// console.log(itemTranslateX);
// console.log(itemScale);
// console.log(itemOpacity);
// console.log('-----------------------------------------------------------');
itemInstance.setStyle({
'transform': 'translate3d(' + itemTranslateX + 'px, 0px, 0px) scale(' + itemScale + ')',
'z-index': list[index].zIndex,
'opacity': itemOpacity
})
}
})
}
} else if (state.direction == 'right') {
var preIndex = (currentIndex == 0) ? list.length - 1 : currentIndex - 1
// 右滑的时候把最底部的取出,并放到最高层级
state.itemsInstance[preIndex].setStyle({
'transform': 'translate3d(-' + (itemData.itemWidth - translateX) + 'px, 0px, 0px) scale(1)',
'z-index': list[currentIndex].zIndex + 1,
'opacity': list[currentIndex].opacity
})
// 当前轮播逐渐缩小
if (Math.abs(translateX) < itemAnimationWidth) {
state.itemsInstance.forEach( function(itemInstance, index) {
if (index != preIndex) {
var replaceIndex = index == list.length - 1 ? 0 : index + 1
var distanceRate = Math.abs(translateX) / itemAnimationWidth
var itemTranslateX = list[index].translateX + (list[replaceIndex].translateX - list[index].translateX) * distanceRate
var itemScale = list[index].scale - (list[index].scale - list[replaceIndex].scale) * distanceRate
var itemOpacity = list[index].opacity - (list[index].opacity - list[replaceIndex].opacity) * distanceRate
// console.log(preIndex);
// console.log(index);
// console.log(replaceIndex);
// console.log(list[index]);
// console.log(list[replaceIndex].translateX - list[index].translateX);
// console.log(distanceRate);
// console.log(itemTranslateX);
// console.log(itemScale);
// console.log('-----------------------------------------------------------');
itemInstance.setStyle({
'transform': 'translate3d(' + itemTranslateX + 'px, 0px, 0px) scale(' + itemScale + ')',
'z-index': list[index].zIndex,
'opacity': itemOpacity
})
}
})
}
}
} else if (itemData.direction === 'vertical') {
// 垂直方向
var itemAnimationHeight = state.itemAnimationHeight
// 偏移的y轴距离
var translateY = currentTouches.y - touchRelactive.y
if (currentTouches.y > itemData.windowHeight || currentTouches.y < 0) return
// console.log(translateX);
// 更新其他轮播样式
if (state.direction == 'up') {
// 设置当前激活元素的偏移量
instance.setStyle({
'transform': 'translate3d(0px, '+ translateY + 'px, 0px)',
'z-index': list[currentIndex].zIndex + 1,
'opacity': list[currentIndex].opacity
})
// 移动距离是否超过了指定的容器宽度
if (Math.abs(translateY) > itemAnimationHeight) {
state.itemsInstance.forEach( function(itemInstance, index) {
if (index != currentIndex) {
var preIndex = (index == 0) ? list.length - 1 : index - 1
var distanceRate = (Math.abs(translateY) - itemAnimationHeight) / (itemData.itemHeight - itemAnimationHeight)
var itemTranslateY = list[index].translateY - (list[index].translateY - list[preIndex].translateY) * distanceRate
var itemScale = list[index].scale + (list[preIndex].scale - list[index].scale) * distanceRate
var itemOpacity = list[index].opacity + (list[preIndex].opacity - list[index].opacity) * distanceRate
// console.log(preIndex);
// console.log(list[index]);
// console.log(distanceRate);
// console.log(itemTranslateX);
// console.log(itemScale);
// console.log('-----------------------------------------------------------');
itemInstance.setStyle({
'transform': 'translate3d(0px, ' + itemTranslateY + 'px, 0px) scale(' + itemScale + ')',
'z-index': list[index].zIndex,
'opacity': itemOpacity
})
}
})
}
} else if (state.direction == 'down') {
var preIndex = (currentIndex == 0) ? list.length - 1 : currentIndex - 1
// 下滑的时候把最底部的取出,并放到最高层级
state.itemsInstance[preIndex].setStyle({
'transform': 'translate3d(0px, -' + (itemData.itemHeight - translateY) + 'px, 0px) scale(1)',
'z-index': list[currentIndex].zIndex + 1,
'opacity': list[currentIndex].opacity
})
// 当前轮播逐渐缩小
if (Math.abs(translateY) < itemAnimationHeight) {
state.itemsInstance.forEach( function(itemInstance, index) {
if (index != preIndex) {
var replaceIndex = index == list.length - 1 ? 0 : index + 1
var distanceRate = Math.abs(translateY) / itemAnimationHeight
var itemTranslateY = list[index].translateY + (list[replaceIndex].translateY - list[index].translateY) * distanceRate
var itemScale = list[index].scale - (list[index].scale - list[replaceIndex].scale) * distanceRate
var itemOpacity = list[index].opacity - (list[index].opacity - list[replaceIndex].opacity) * distanceRate
// console.log(preIndex);
// console.log(index);
// console.log(replaceIndex);
// console.log(list[index]);
// console.log(list[replaceIndex].translateX - list[index].translateX);
// console.log(distanceRate);
// console.log(itemTranslateX);
// console.log(itemScale);
// console.log('-----------------------------------------------------------');
itemInstance.setStyle({
'transform': 'translate3d(0px, ' + itemTranslateY + 'px, 0px) scale(' + itemScale + ')',
'z-index': list[index].zIndex,
'opacity': itemOpacity
})
}
})
}
}
}
}
// 更新当前轮播序号
function updateCurrentSwiperIndex(index, ownerInstance, state) {
state.currentIndex = index
ownerInstance.callMethod('changeSwiperIndex', {
index: index
})
}
// 切换到下一个轮播
function switchNextSwiper(newIndex, touches, instance, state) {
var currentIndex = state.currentIndex
var list = state.list
var direction = state.itemData.direction
var touchRelactive = state.touchRelactive || {x: 0, y: 0}
// 已经完成轮播切换
var currentListItemData = JSON.parse(JSON.stringify(list))
if (direction === 'horizontal') {
// 水平方向移动
var itemWidth = state.itemData.itemWidth
// 当前轮播移动到最左边
instance.setStyle({
'transform': 'translate3d(-'+ itemWidth + 'px, 0px, 0px) scale(1)',
'z-index': list[currentIndex].zIndex + 1,
'opacity': list[currentIndex].opacity
})
// 计算当前移动需要的剩余时间
var time = Math.floor((itemWidth - Math.abs(touches.pageX - touchRelactive.x)) / itemWidth * 250)
setTimeout(instance, function() {
for (var i = list.length - 1; i >= 0; i--) {
var replaceIndex = i - 1 < 0 ? list.length - 1 : i - 1
// console.log(i);
// console.log(replaceIndex);
state.itemsInstance[i].setStyle({
'transform': 'translate3d('+ currentListItemData[replaceIndex].translateX + 'px, 0px, 0px) scale(' + currentListItemData[replaceIndex].scale + ')',
'z-index': currentListItemData[replaceIndex].zIndex,
'opacity': currentListItemData[replaceIndex].opacity
})
state.list[i] = currentListItemData[replaceIndex]
}
}, time)
} else if (direction === 'vertical') {
// 垂直方向移动
var itemHeight = state.itemData.itemHeight
// 当前轮播移动到最上边
instance.setStyle({
'transform': 'translate3d(0px, -'+ itemHeight + 'px, 0px) scale(1)',
'z-index': list[currentIndex].zIndex + 1,
'opacity': list[currentIndex].opacity
})
// 计算当前移动需要的剩余时间
var time = Math.floor((itemHeight - Math.abs(touches.pageY - touchRelactive.y)) / itemHeight * 250)
setTimeout(instance, function() {
for (var i = list.length - 1; i >= 0; i--) {
var replaceIndex = i - 1 < 0 ? list.length - 1 : i - 1
// console.log(i);
// console.log(replaceIndex);
state.itemsInstance[i].setStyle({
'transform': 'translate3d(0px, '+ currentListItemData[replaceIndex].translateY + 'px, 0px) scale(' + currentListItemData[replaceIndex].scale + ')',
'z-index': currentListItemData[replaceIndex].zIndex,
'opacity': currentListItemData[replaceIndex].opacity
})
state.list[i] = currentListItemData[replaceIndex]
}
}, time)
}
}
// 切换到上一个轮播
function switchPrevSwiper(newIndex, touches, instance, state) {
var currentIndex = state.currentIndex
var list = state.list
var direction = state.itemData.direction
var touchRelactive = state.touchRelactive || {x: 0, y: 0}
var currentListItemData = JSON.parse(JSON.stringify(list))
if (direction === 'horizontal') {
// 水平方向移动
var itemWidth = state.itemData.itemWidth
// 当前上一个轮播移动到正常位置
state.itemsInstance[newIndex].setStyle({
'transform': 'translate3d(0px, 0px, 0px) scale(1)',
'z-index': list[currentIndex].zIndex + 1,
'opacity': list[currentIndex].opacity
})
// 计算当前移动需要的剩余时间
var time = Math.floor((itemWidth - Math.abs(touches.pageX - touchRelactive.x)) / itemWidth * 250)
// 更新除当前上一个轮播外的其他轮播,向后移动一个层级
// 更新列表位置相关数据
setTimeout(instance, function() {
for (var i = 0; i < list.length; i++) {
var replaceIndex = (i + 1 > list.length - 1) ? 0 : i + 1
state.itemsInstance[i].setStyle({
'transform': 'translate3d('+ currentListItemData[replaceIndex].translateX + 'px, 0px, 0px) scale(' + currentListItemData[replaceIndex].scale + ')',
'z-index': currentListItemData[replaceIndex].zIndex,
'opacity': currentListItemData[replaceIndex].opacity
})
state.list[i] = currentListItemData[replaceIndex]
}
}, time)
} else if (direction === 'vertical') {
// 垂直方向移动
var itemHeight = state.itemData.itemHeight
// 当前上一个轮播移动到正常位置
state.itemsInstance[newIndex].setStyle({
'transform': 'translate3d(0px, 0px, 0px) scale(1)',
'z-index': list[currentIndex].zIndex + 1,
'opacity': list[currentIndex].opacity
})
// 计算当前移动需要的剩余时间
var time = Math.floor((itemHeight - Math.abs(touches.pageY - touchRelactive.y)) / itemHeight * 250)
// 更新除当前上一个轮播外的其他轮播,向后移动一个层级
// 更新列表位置相关数据
setTimeout(instance, function() {
for (var i = 0; i < list.length; i++) {
var replaceIndex = (i + 1 > list.length - 1) ? 0 : i + 1
state.itemsInstance[i].setStyle({
'transform': 'translate3d(0px, '+ currentListItemData[replaceIndex].translateY + 'px, 0px) scale(' + currentListItemData[replaceIndex].scale + ')',
'z-index': currentListItemData[replaceIndex].zIndex,
'opacity': currentListItemData[replaceIndex].opacity
})
state.list[i] = currentListItemData[replaceIndex]
}
}, time)
}
}
// 反转动画
function toggleSwiperAnimation(state, add) {
if (!state.itemsInstance) return
if (add === true) {
state.itemsInstance.forEach(function(item, index) {
if (!item.hasClass('tn-stack-swiper__item__transition')) {
item.addClass('tn-stack-swiper__item__transition')
}
})
} else {
state.itemsInstance.forEach(function(item, index) {
if (item.hasClass('tn-stack-swiper__item__transition')) {
item.removeClass('tn-stack-swiper__item__transition')
}
})
}
}
// 更新数据
var itemDataObserver = function (newVal, oldVal, ownerInstance, instance) {
var state = ownerInstance.getState()
state.itemData = newVal
}
// 列表初始化
var listObserver = function(newVal, oldVal, ownerInstance, instance) {
var state = ownerInstance.getState()
var itemData = state.itemData
state.itemsInstance = ownerInstance.selectAllComponents('.tn-stack-swiper__item')
state.list = newVal || []
state.list.forEach(function(item, index) {
var itemInstance = state.itemsInstance[index]
if (item && itemInstance) {
if (itemData.direction === 'horizontal') {
itemInstance.setStyle({
'transform': 'translate3d('+ item.translateX + 'px, 0px, 0px) scale(' + item.scale + ')',
'z-index': item.zIndex,
'opacity': item.opacity
})
} else if (itemData.direction === 'vertical') {
itemInstance.setStyle({
'transform': 'translate3d(0px, '+ item.translateY + 'px, 0px) scale(' + item.scale + ')',
'z-index': item.zIndex,
'opacity': item.opacity
})
}
}
})
}
// 切换轮播位置
var swiperIndexChange = function(newVal, oldVal, ownerInstance, instance) {
var state = ownerInstance.getState()
// console.log(newVal);
// ownerInstance.callMethod('printLog', newVal)
// console.log(oldVal);
// ownerInstance.callMethod('printLog', oldVal)
// 排除第一次初始化和手动切换的情况
if (oldVal < 0 || typeof oldVal == 'undefined' || state.currentIndex == newVal) {
if (oldVal < 0 || typeof oldVal == 'undefined') {
state.currentIndex = 0
}
return
}
state.currentIndex = newVal
// console.log(state.currentIndex);
if (newVal > oldVal || (oldVal == state.list.length - 1 && newVal == 0)) {
// console.log("next");
// state.itemsInstance.forEach(function(item, index) {
// item.addClass("tn-stack-swiper__item__transition")
// })
switchNextSwiper(newVal, {
pageX: 0
}, state.itemsInstance[oldVal], state)
} else if (newVal < oldVal || (oldVal == 0 && newVal == state.list.length - 1)) {
// console.log("prev");
}
}
// 自动轮播切换状态
var autoplayFlagChange = function(newVal, oldVal, ownerInstance, instance) {
var state = ownerInstance.getState()
if (newVal === true) {
toggleSwiperAnimation(state, true)
} else {
toggleSwiperAnimation(state, false)
}
}
// 开始触摸
var touchStart = function (event, ownerInstance) {
// console.log('touchStart');
var instance = event.instance
var dataset = instance.getDataset()
var state = ownerInstance.getState()
var itemData = state.itemData
// 判断是否为为当前显示的轮播
if (dataset.index != state.currentIndex) return
var touches = event.changedTouches[0]
if (!touches) return
// 记录当前滑动开始的x,y坐标
state.touchRelactive = {
x: touches.pageX,
y: touches.pageY
}
// 记录触摸id,用于处理多指的情况
state.touchId = touches.identifier
if (itemData.direction === 'horizontal') {
// 水平方向移动
// 设置左右滑动时相对偏移距离
state.itemAnimationWidth = itemData.itemWidth * (dataset.switchrate / 100)
} else if (itemData.direction === 'vertical') {
// 垂直方向移动
// 设置上下滑动时相对偏移距离
state.itemAnimationHeight = itemData.itemHeight * (dataset.switchrate / 100)
}
// 移除运动动画时间
toggleSwiperAnimation(state, false)
// 标记开始触摸
state.touching = true
ownerInstance.callMethod('changeTouchState', {
touching: true
})
// 停止执行自动轮播
ownerInstance.callMethod('clearAutoPlayTimer')
}
// 开始移动
var touchMove = function (event, ownerInstance) {
// console.log('touchMove');
var instance = event.instance
var dataset = instance.getDataset()
var state = ownerInstance.getState()
var itemData = state.itemData
// 判断是否为为当前显示的轮播
if (dataset.index != state.currentIndex) return
// 还没开始触摸直接返回
if (!state.touching) return
var touches = event.changedTouches[0]
if (!touches) return
// 判断是否为同一个触摸点
if (state.touchId != touches.identifier) return
var currentTouchRelactive = {
x: touches.pageX,
y: touches.pageY
}
// 是否已经确定了移动方向
if (!state.direction) {
state.direction = decideSwiperDirection(state.touchRelactive, currentTouchRelactive, itemData.direction)
}
// console.log(decideSwiperDirection(state.touchRelactive, currentTouchRelactive));
updateSwiperStyle(currentTouchRelactive, instance, state)
}
// 移动结束
var touchEnd = function (event, ownerInstance) {
// console.log('touchEnd');
var instance = event.instance
var dataset = instance.getDataset()
var state = ownerInstance.getState()
var itemData = state.itemData
var list = state.list
var touchRelactive = state.touchRelactive
// 判断是否为为当前显示的轮播
if (dataset.index != state.currentIndex) return
// 还没开始触摸直接返回
if (!state.touching) return
var touches = event.changedTouches[0]
if (!touches) return
// 判断是否为同一个触摸点
if (state.touchId != touches.identifier) return
// 添加运动动画时间
toggleSwiperAnimation(state, true)
if (itemData.direction === 'horizontal') {
// 水平方向移动
var itemAnimationWidth = state.itemAnimationWidth
// 判断时左滑还是右滑
// 判断是否超过自动滚动到下一页还是回滚
if (state.direction == 'left') {
if (Math.abs(touches.pageX - touchRelactive.x) < itemAnimationWidth) {
list.forEach(function(item, index) {
var itemInstance = state.itemsInstance[index]
if (item && itemInstance) {
itemInstance.setStyle({
'transform': 'translate3d('+ item.translateX + 'px, 0px, 0px) scale(' + item.scale + ')',
'z-index': item.zIndex
})
}
})
} else {
var newIndex = state.currentIndex + 1 > list.length - 1 ? 0 : state.currentIndex + 1
switchNextSwiper(newIndex, touches, instance, state)
updateCurrentSwiperIndex(newIndex, ownerInstance, state)
}
} else if (state.direction == 'right') {
if (Math.abs(touches.pageX - touchRelactive.x) < itemAnimationWidth) {
// 滑动显示图片回滚
var preIndex = (state.currentIndex == 0) ? list.length - 1 : state.currentIndex - 1
state.itemsInstance[preIndex].setStyle({
'transform': 'translate3d(-' + itemData.itemWidth + 'px, 0px, 0px) scale(1)',
'z-index': list[state.currentIndex].zIndex + 1,
'opacity': list[state.currentIndex].opacity
})
list.forEach(function(item, index) {
var itemInstance = state.itemsInstance[index]
if (item && itemInstance) {
itemInstance.setStyle({
'transform': 'translate3d('+ item.translateX + 'px, 0px, 0px) scale(' + item.scale + ')',
'z-index': item.zIndex,
'opacity': item.opacity
})
}
})
} else {
var newIndex = (state.currentIndex - 1 < 0) ? list.length - 1 : state.currentIndex - 1
switchPrevSwiper(newIndex, touches, instance, state)
updateCurrentSwiperIndex(newIndex, ownerInstance, state)
}
}
} else if (itemData.direction === 'vertical') {
// 垂直方向移动
var itemAnimationHeight = state.itemAnimationHeight
// 判断时上滑还是下滑
// 判断是否超过自动滚动到下一页还是回滚
if (state.direction == 'up') {
if (Math.abs(touches.pageY - touchRelactive.y) < itemAnimationHeight) {
list.forEach(function(item, index) {
var itemInstance = state.itemsInstance[index]
if (item && itemInstance) {
itemInstance.setStyle({
'transform': 'translate3d(0px, '+ item.translateY + 'px, 0px) scale(' + item.scale + ')',
'z-index': item.zIndex,
'opacity': item.opacity
})
}
})
} else {
var newIndex = state.currentIndex + 1 > list.length - 1 ? 0 : state.currentIndex + 1
switchNextSwiper(newIndex, touches, instance, state)
updateCurrentSwiperIndex(newIndex, ownerInstance, state)
}
} else if (state.direction == 'down') {
if (Math.abs(touches.pageY - touchRelactive.y) < itemAnimationHeight) {
// 滑动显示图片回滚
var preIndex = (state.currentIndex == 0) ? list.length - 1 : state.currentIndex - 1
state.itemsInstance[preIndex].setStyle({
'transform': 'translate3d(0px, -' + itemData.itemHeight + 'px, 0px) scale(1)',
'z-index': list[state.currentIndex].zIndex + 1,
'opacity': list[state.currentIndex].opacity
})
list.forEach(function(item, index) {
var itemInstance = state.itemsInstance[index]
if (item && itemInstance) {
itemInstance.setStyle({
'transform': 'translate3d(0px, '+ item.translateY + 'px, 0px) scale(' + item.scale + ')',
'z-index': item.zIndex,
'opacity': item.opacity
})
}
})
} else {
var newIndex = (state.currentIndex - 1 < 0) ? list.length - 1 : state.currentIndex - 1
switchPrevSwiper(newIndex, touches, instance, state)
updateCurrentSwiperIndex(newIndex, ownerInstance, state)
}
}
}
// 清除对应的标志位
state.touchRelactive = null
state.touching = false
state.direction = null
state.touchId = null
ownerInstance.callMethod('changeTouchState', {
touching: false
})
// 重新开始执行自动轮播
ownerInstance.callMethod('setAutoPlay')
}
module.exports = {
itemDataObserver: itemDataObserver,
listObserver: listObserver,
swiperIndexChange: swiperIndexChange,
autoplayFlagChange: autoplayFlagChange,
touchStart: touchStart,
touchMove: touchMove,
touchEnd: touchEnd
}
@@ -0,0 +1,657 @@
function setTimeout(instance, cb, time) {
if (time > 0) {
var s = getDate().getTime()
var fn = function () {
if (getDate().getTime() - s > time) {
cb && cb()
} else
instance.requestAnimationFrame(fn)
}
fn()
}
else
cb && cb()
}
// 判断触摸的移动方向
function decideSwiperDirection(startTouches, currentTouches, direction) {
// 震动偏移容差
var toleranceShake = 30
// 移动容差
var toleranceTranslate = 10
if (direction === 'horizontal') {
// 水平方向移动
if (Math.abs(currentTouches.y - startTouches.y) <= toleranceShake) {
// console.log(currentTouches.x, startTouches.x);
if (Math.abs(currentTouches.x - startTouches.x) > toleranceTranslate) {
if (currentTouches.x - startTouches.x > 0) {
return 'right'
} else if (currentTouches.x - startTouches.x < 0) {
return 'left'
}
}
}
} else if (direction === 'vertical') {
// 垂直方向移动
if (Math.abs(currentTouches.x - startTouches.x) <= toleranceShake) {
// console.log(currentTouches.x, startTouches.x);
if (Math.abs(currentTouches.y - startTouches.y) > toleranceTranslate) {
if (currentTouches.y - startTouches.y > 0) {
return 'down'
} else if (currentTouches.y - startTouches.y < 0) {
return 'up'
}
}
}
}
return ''
}
// 更新轮播样式信息
function updateSwiperStyle(currentTouches, instance, state) {
var itemData = state.itemData
var itemsInstance = state.itemsInstance
var list = state.list
var currentIndex = state.currentIndex
var touchRelactive = state.touchRelactive
// console.log(itemAnimationWidth);
if (itemData.direction === 'horizontal') {
// 水平方向
var itemAnimationWidth = state.itemAnimationWidth
// 偏移的x轴距离
var translateX = currentTouches.x - touchRelactive.x
if (currentTouches.x > itemData.windowWidth || currentTouches.x < 0) return
// console.log(translateX);
// 更新其他轮播样式
if (state.direction == 'left') {
// 设置当前激活元素的偏移量
instance.setStyle({
'transform': 'translate3d('+ translateX + 'px, 0px, 0px)',
'z-index': list[currentIndex].zIndex + 1,
'opacity': list[currentIndex].opacity
})
// 移动距离是否超过了指定的容器宽度
if (Math.abs(translateX) > itemAnimationWidth) {
state.itemsInstance.forEach( function(itemInstance, index) {
if (index != currentIndex) {
var preIndex = (index == 0) ? list.length - 1 : index - 1
var distanceRate = (Math.abs(translateX) - itemAnimationWidth) / (itemData.itemWidth - itemAnimationWidth)
var itemTranslateX = list[index].translateX - (list[index].translateX - list[preIndex].translateX) * distanceRate
var itemScale = list[index].scale + (list[preIndex].scale - list[index].scale) * distanceRate
var itemOpacity = list[index].opacity + (list[preIndex].opacity - list[index].opacity) * distanceRate
// console.log(preIndex);
// console.log(list[index]);
// console.log(distanceRate);
// console.log(itemTranslateX);
// console.log(itemScale);
// console.log(itemOpacity);
// console.log('-----------------------------------------------------------');
itemInstance.setStyle({
'transform': 'translate3d(' + itemTranslateX + 'px, 0px, 0px) scale(' + itemScale + ')',
'z-index': list[index].zIndex,
'opacity': itemOpacity
})
}
})
}
} else if (state.direction == 'right') {
var preIndex = (currentIndex == 0) ? list.length - 1 : currentIndex - 1
// 右滑的时候把最底部的取出,并放到最高层级
state.itemsInstance[preIndex].setStyle({
'transform': 'translate3d(-' + (itemData.itemWidth - translateX) + 'px, 0px, 0px) scale(1)',
'z-index': list[currentIndex].zIndex + 1,
'opacity': list[currentIndex].opacity
})
// 当前轮播逐渐缩小
if (Math.abs(translateX) < itemAnimationWidth) {
state.itemsInstance.forEach( function(itemInstance, index) {
if (index != preIndex) {
var replaceIndex = index == list.length - 1 ? 0 : index + 1
var distanceRate = Math.abs(translateX) / itemAnimationWidth
var itemTranslateX = list[index].translateX + (list[replaceIndex].translateX - list[index].translateX) * distanceRate
var itemScale = list[index].scale - (list[index].scale - list[replaceIndex].scale) * distanceRate
var itemOpacity = list[index].opacity - (list[index].opacity - list[replaceIndex].opacity) * distanceRate
// console.log(preIndex);
// console.log(index);
// console.log(replaceIndex);
// console.log(list[index]);
// console.log(list[replaceIndex].translateX - list[index].translateX);
// console.log(distanceRate);
// console.log(itemTranslateX);
// console.log(itemScale);
// console.log('-----------------------------------------------------------');
itemInstance.setStyle({
'transform': 'translate3d(' + itemTranslateX + 'px, 0px, 0px) scale(' + itemScale + ')',
'z-index': list[index].zIndex,
'opacity': itemOpacity
})
}
})
}
}
} else if (itemData.direction === 'vertical') {
// 垂直方向
var itemAnimationHeight = state.itemAnimationHeight
// 偏移的y轴距离
var translateY = currentTouches.y - touchRelactive.y
if (currentTouches.y > itemData.windowHeight || currentTouches.y < 0) return
// console.log(translateX);
// 更新其他轮播样式
if (state.direction == 'up') {
// 设置当前激活元素的偏移量
instance.setStyle({
'transform': 'translate3d(0px, '+ translateY + 'px, 0px)',
'z-index': list[currentIndex].zIndex + 1,
'opacity': list[currentIndex].opacity
})
// 移动距离是否超过了指定的容器宽度
if (Math.abs(translateY) > itemAnimationHeight) {
state.itemsInstance.forEach( function(itemInstance, index) {
if (index != currentIndex) {
var preIndex = (index == 0) ? list.length - 1 : index - 1
var distanceRate = (Math.abs(translateY) - itemAnimationHeight) / (itemData.itemHeight - itemAnimationHeight)
var itemTranslateY = list[index].translateY - (list[index].translateY - list[preIndex].translateY) * distanceRate
var itemScale = list[index].scale + (list[preIndex].scale - list[index].scale) * distanceRate
var itemOpacity = list[index].opacity + (list[preIndex].opacity - list[index].opacity) * distanceRate
// console.log(preIndex);
// console.log(list[index]);
// console.log(distanceRate);
// console.log(itemTranslateX);
// console.log(itemScale);
// console.log('-----------------------------------------------------------');
itemInstance.setStyle({
'transform': 'translate3d(0px, ' + itemTranslateY + 'px, 0px) scale(' + itemScale + ')',
'z-index': list[index].zIndex,
'opacity': itemOpacity
})
}
})
}
} else if (state.direction == 'down') {
var preIndex = (currentIndex == 0) ? list.length - 1 : currentIndex - 1
// 下滑的时候把最底部的取出,并放到最高层级
state.itemsInstance[preIndex].setStyle({
'transform': 'translate3d(0px, -' + (itemData.itemHeight - translateY) + 'px, 0px) scale(1)',
'z-index': list[currentIndex].zIndex + 1,
'opacity': list[currentIndex].opacity
})
// 当前轮播逐渐缩小
if (Math.abs(translateY) < itemAnimationHeight) {
state.itemsInstance.forEach( function(itemInstance, index) {
if (index != preIndex) {
var replaceIndex = index == list.length - 1 ? 0 : index + 1
var distanceRate = Math.abs(translateY) / itemAnimationHeight
var itemTranslateY = list[index].translateY + (list[replaceIndex].translateY - list[index].translateY) * distanceRate
var itemScale = list[index].scale - (list[index].scale - list[replaceIndex].scale) * distanceRate
var itemOpacity = list[index].opacity - (list[index].opacity - list[replaceIndex].opacity) * distanceRate
// console.log(preIndex);
// console.log(index);
// console.log(replaceIndex);
// console.log(list[index]);
// console.log(list[replaceIndex].translateX - list[index].translateX);
// console.log(distanceRate);
// console.log(itemTranslateX);
// console.log(itemScale);
// console.log('-----------------------------------------------------------');
itemInstance.setStyle({
'transform': 'translate3d(0px, ' + itemTranslateY + 'px, 0px) scale(' + itemScale + ')',
'z-index': list[index].zIndex,
'opacity': itemOpacity
})
}
})
}
}
}
}
// 更新当前轮播序号
function updateCurrentSwiperIndex(index, ownerInstance, state) {
state.currentIndex = index
ownerInstance.callMethod('changeSwiperIndex', {
index: index
})
}
// 切换到下一个轮播
function switchNextSwiper(newIndex, touches, instance, state) {
var currentIndex = state.currentIndex
var list = state.list
var direction = state.itemData.direction
var touchRelactive = state.touchRelactive || {x: 0, y: 0}
// 已经完成轮播切换
var currentListItemData = JSON.parse(JSON.stringify(list))
if (direction === 'horizontal') {
// 水平方向移动
var itemWidth = state.itemData.itemWidth
// 当前轮播移动到最左边
instance.setStyle({
'transform': 'translate3d(-'+ itemWidth + 'px, 0px, 0px) scale(1)',
'z-index': list[currentIndex].zIndex + 1,
'opacity': list[currentIndex].opacity
})
// 计算当前移动需要的剩余时间
var time = Math.floor((itemWidth - Math.abs(touches.pageX - touchRelactive.x)) / itemWidth * 250)
setTimeout(instance, function() {
for (var i = list.length - 1; i >= 0; i--) {
var replaceIndex = i - 1 < 0 ? list.length - 1 : i - 1
// console.log(i);
// console.log(replaceIndex);
state.itemsInstance[i].setStyle({
'transform': 'translate3d('+ currentListItemData[replaceIndex].translateX + 'px, 0px, 0px) scale(' + currentListItemData[replaceIndex].scale + ')',
'z-index': currentListItemData[replaceIndex].zIndex,
'opacity': currentListItemData[replaceIndex].opacity
})
state.list[i] = currentListItemData[replaceIndex]
}
}, time)
} else if (direction === 'vertical') {
// 垂直方向移动
var itemHeight = state.itemData.itemHeight
// 当前轮播移动到最上边
instance.setStyle({
'transform': 'translate3d(0px, -'+ itemHeight + 'px, 0px) scale(1)',
'z-index': list[currentIndex].zIndex + 1,
'opacity': list[currentIndex].opacity
})
// 计算当前移动需要的剩余时间
var time = Math.floor((itemHeight - Math.abs(touches.pageY - touchRelactive.y)) / itemHeight * 250)
setTimeout(instance, function() {
for (var i = list.length - 1; i >= 0; i--) {
var replaceIndex = i - 1 < 0 ? list.length - 1 : i - 1
// console.log(i);
// console.log(replaceIndex);
state.itemsInstance[i].setStyle({
'transform': 'translate3d(0px, '+ currentListItemData[replaceIndex].translateY + 'px, 0px) scale(' + currentListItemData[replaceIndex].scale + ')',
'z-index': currentListItemData[replaceIndex].zIndex,
'opacity': currentListItemData[replaceIndex].opacity
})
state.list[i] = currentListItemData[replaceIndex]
}
}, time)
}
}
// 切换到上一个轮播
function switchPrevSwiper(newIndex, touches, instance, state) {
var currentIndex = state.currentIndex
var list = state.list
var direction = state.itemData.direction
var touchRelactive = state.touchRelactive || {x: 0, y: 0}
var currentListItemData = JSON.parse(JSON.stringify(list))
if (direction === 'horizontal') {
// 水平方向移动
var itemWidth = state.itemData.itemWidth
// 当前上一个轮播移动到正常位置
state.itemsInstance[newIndex].setStyle({
'transform': 'translate3d(0px, 0px, 0px) scale(1)',
'z-index': list[currentIndex].zIndex + 1,
'opacity': list[currentIndex].opacity
})
// 计算当前移动需要的剩余时间
var time = Math.floor((itemWidth - Math.abs(touches.pageX - touchRelactive.x)) / itemWidth * 250)
// 更新除当前上一个轮播外的其他轮播,向后移动一个层级
// 更新列表位置相关数据
setTimeout(instance, function() {
for (var i = 0; i < list.length; i++) {
var replaceIndex = (i + 1 > list.length - 1) ? 0 : i + 1
state.itemsInstance[i].setStyle({
'transform': 'translate3d('+ currentListItemData[replaceIndex].translateX + 'px, 0px, 0px) scale(' + currentListItemData[replaceIndex].scale + ')',
'z-index': currentListItemData[replaceIndex].zIndex,
'opacity': currentListItemData[replaceIndex].opacity
})
state.list[i] = currentListItemData[replaceIndex]
}
}, time)
} else if (direction === 'vertical') {
// 垂直方向移动
var itemHeight = state.itemData.itemHeight
// 当前上一个轮播移动到正常位置
state.itemsInstance[newIndex].setStyle({
'transform': 'translate3d(0px, 0px, 0px) scale(1)',
'z-index': list[currentIndex].zIndex + 1,
'opacity': list[currentIndex].opacity
})
// 计算当前移动需要的剩余时间
var time = Math.floor((itemHeight - Math.abs(touches.pageY - touchRelactive.y)) / itemHeight * 250)
// 更新除当前上一个轮播外的其他轮播,向后移动一个层级
// 更新列表位置相关数据
setTimeout(instance, function() {
for (var i = 0; i < list.length; i++) {
var replaceIndex = (i + 1 > list.length - 1) ? 0 : i + 1
state.itemsInstance[i].setStyle({
'transform': 'translate3d(0px, '+ currentListItemData[replaceIndex].translateY + 'px, 0px) scale(' + currentListItemData[replaceIndex].scale + ')',
'z-index': currentListItemData[replaceIndex].zIndex,
'opacity': currentListItemData[replaceIndex].opacity
})
state.list[i] = currentListItemData[replaceIndex]
}
}, time)
}
}
// 反转动画
function toggleSwiperAnimation(state, add) {
if (!state.itemsInstance) return
if (add === true) {
state.itemsInstance.forEach(function(item, index) {
if (!item.hasClass('tn-stack-swiper__item__transition')) {
item.addClass('tn-stack-swiper__item__transition')
}
})
} else {
state.itemsInstance.forEach(function(item, index) {
if (item.hasClass('tn-stack-swiper__item__transition')) {
item.removeClass('tn-stack-swiper__item__transition')
}
})
}
}
// 更新数据
var itemDataObserver = function (newVal, oldVal, ownerInstance, instance) {
var state = ownerInstance.getState()
state.itemData = newVal
}
// 列表初始化
var listObserver = function(newVal, oldVal, ownerInstance, instance) {
var state = ownerInstance.getState()
var itemData = state.itemData
state.itemsInstance = ownerInstance.selectAllComponents('.tn-stack-swiper__item')
state.list = newVal || []
state.list.forEach(function(item, index) {
var itemInstance = state.itemsInstance[index]
if (item && itemInstance) {
if (itemData.direction === 'horizontal') {
itemInstance.setStyle({
'transform': 'translate3d('+ item.translateX + 'px, 0px, 0px) scale(' + item.scale + ')',
'z-index': item.zIndex,
'opacity': item.opacity
})
} else if (itemData.direction === 'vertical') {
itemInstance.setStyle({
'transform': 'translate3d(0px, '+ item.translateY + 'px, 0px) scale(' + item.scale + ')',
'z-index': item.zIndex,
'opacity': item.opacity
})
}
}
})
}
// 切换轮播位置
var swiperIndexChange = function(newVal, oldVal, ownerInstance, instance) {
var state = ownerInstance.getState()
// console.log(newVal);
// ownerInstance.callMethod('printLog', newVal)
// console.log(oldVal);
// ownerInstance.callMethod('printLog', oldVal)
// 排除第一次初始化和手动切换的情况
if (oldVal < 0 || typeof oldVal == 'undefined' || state.currentIndex == newVal) {
if (oldVal < 0 || typeof oldVal == 'undefined') {
state.currentIndex = 0
}
return
}
state.currentIndex = newVal
// console.log(state.currentIndex);
if (newVal > oldVal || (oldVal == state.list.length - 1 && newVal == 0)) {
// console.log("next");
// state.itemsInstance.forEach(function(item, index) {
// item.addClass("tn-stack-swiper__item__transition")
// })
switchNextSwiper(newVal, {
pageX: 0
}, state.itemsInstance[oldVal], state)
} else if (newVal < oldVal || (oldVal == 0 && newVal == state.list.length - 1)) {
// console.log("prev");
}
}
// 自动轮播切换状态
var autoplayFlagChange = function(newVal, oldVal, ownerInstance, instance) {
var state = ownerInstance.getState()
if (newVal === true) {
toggleSwiperAnimation(state, true)
} else {
toggleSwiperAnimation(state, false)
}
}
// 开始触摸
var touchStart = function (event, ownerInstance) {
// console.log('touchStart');
var instance = event.instance
var dataset = instance.getDataset()
var state = ownerInstance.getState()
var itemData = state.itemData
// 判断是否为为当前显示的轮播
if (dataset.index != state.currentIndex) return
var touches = event.changedTouches[0]
if (!touches) return
// 记录当前滑动开始的x,y坐标
state.touchRelactive = {
x: touches.pageX,
y: touches.pageY
}
// 记录触摸id,用于处理多指的情况
state.touchId = touches.identifier
if (itemData.direction === 'horizontal') {
// 水平方向移动
// 设置左右滑动时相对偏移距离
state.itemAnimationWidth = itemData.itemWidth * (dataset.switchrate / 100)
} else if (itemData.direction === 'vertical') {
// 垂直方向移动
// 设置上下滑动时相对偏移距离
state.itemAnimationHeight = itemData.itemHeight * (dataset.switchrate / 100)
}
// 移除运动动画时间
toggleSwiperAnimation(state, false)
// 标记开始触摸
state.touching = true
ownerInstance.callMethod('changeTouchState', {
touching: true
})
// 停止执行自动轮播
ownerInstance.callMethod('clearAutoPlayTimer')
}
// 开始移动
var touchMove = function (event, ownerInstance) {
// console.log('touchMove');
var instance = event.instance
var dataset = instance.getDataset()
var state = ownerInstance.getState()
var itemData = state.itemData
// 判断是否为为当前显示的轮播
if (dataset.index != state.currentIndex) return
// 还没开始触摸直接返回
if (!state.touching) return
var touches = event.changedTouches[0]
if (!touches) return
// 判断是否为同一个触摸点
if (state.touchId != touches.identifier) return
var currentTouchRelactive = {
x: touches.pageX,
y: touches.pageY
}
// 是否已经确定了移动方向
if (!state.direction) {
state.direction = decideSwiperDirection(state.touchRelactive, currentTouchRelactive, itemData.direction)
}
// console.log(decideSwiperDirection(state.touchRelactive, currentTouchRelactive));
updateSwiperStyle(currentTouchRelactive, instance, state)
}
// 移动结束
var touchEnd = function (event, ownerInstance) {
// console.log('touchEnd');
var instance = event.instance
var dataset = instance.getDataset()
var state = ownerInstance.getState()
var itemData = state.itemData
var list = state.list
var touchRelactive = state.touchRelactive
// 判断是否为为当前显示的轮播
if (dataset.index != state.currentIndex) return
// 还没开始触摸直接返回
if (!state.touching) return
var touches = event.changedTouches[0]
if (!touches) return
// 判断是否为同一个触摸点
if (state.touchId != touches.identifier) return
// 添加运动动画时间
toggleSwiperAnimation(state, true)
if (itemData.direction === 'horizontal') {
// 水平方向移动
var itemAnimationWidth = state.itemAnimationWidth
// 判断时左滑还是右滑
// 判断是否超过自动滚动到下一页还是回滚
if (state.direction == 'left') {
if (Math.abs(touches.pageX - touchRelactive.x) < itemAnimationWidth) {
list.forEach(function(item, index) {
var itemInstance = state.itemsInstance[index]
if (item && itemInstance) {
itemInstance.setStyle({
'transform': 'translate3d('+ item.translateX + 'px, 0px, 0px) scale(' + item.scale + ')',
'z-index': item.zIndex
})
}
})
} else {
var newIndex = state.currentIndex + 1 > list.length - 1 ? 0 : state.currentIndex + 1
switchNextSwiper(newIndex, touches, instance, state)
updateCurrentSwiperIndex(newIndex, ownerInstance, state)
}
} else if (state.direction == 'right') {
if (Math.abs(touches.pageX - touchRelactive.x) < itemAnimationWidth) {
// 滑动显示图片回滚
var preIndex = (state.currentIndex == 0) ? list.length - 1 : state.currentIndex - 1
state.itemsInstance[preIndex].setStyle({
'transform': 'translate3d(-' + itemData.itemWidth + 'px, 0px, 0px) scale(1)',
'z-index': list[state.currentIndex].zIndex + 1,
'opacity': list[state.currentIndex].opacity
})
list.forEach(function(item, index) {
var itemInstance = state.itemsInstance[index]
if (item && itemInstance) {
itemInstance.setStyle({
'transform': 'translate3d('+ item.translateX + 'px, 0px, 0px) scale(' + item.scale + ')',
'z-index': item.zIndex,
'opacity': item.opacity
})
}
})
} else {
var newIndex = (state.currentIndex - 1 < 0) ? list.length - 1 : state.currentIndex - 1
switchPrevSwiper(newIndex, touches, instance, state)
updateCurrentSwiperIndex(newIndex, ownerInstance, state)
}
}
} else if (itemData.direction === 'vertical') {
// 垂直方向移动
var itemAnimationHeight = state.itemAnimationHeight
// 判断时上滑还是下滑
// 判断是否超过自动滚动到下一页还是回滚
if (state.direction == 'up') {
if (Math.abs(touches.pageY - touchRelactive.y) < itemAnimationHeight) {
list.forEach(function(item, index) {
var itemInstance = state.itemsInstance[index]
if (item && itemInstance) {
itemInstance.setStyle({
'transform': 'translate3d(0px, '+ item.translateY + 'px, 0px) scale(' + item.scale + ')',
'z-index': item.zIndex,
'opacity': item.opacity
})
}
})
} else {
var newIndex = state.currentIndex + 1 > list.length - 1 ? 0 : state.currentIndex + 1
switchNextSwiper(newIndex, touches, instance, state)
updateCurrentSwiperIndex(newIndex, ownerInstance, state)
}
} else if (state.direction == 'down') {
if (Math.abs(touches.pageY - touchRelactive.y) < itemAnimationHeight) {
// 滑动显示图片回滚
var preIndex = (state.currentIndex == 0) ? list.length - 1 : state.currentIndex - 1
state.itemsInstance[preIndex].setStyle({
'transform': 'translate3d(0px, -' + itemData.itemHeight + 'px, 0px) scale(1)',
'z-index': list[state.currentIndex].zIndex + 1,
'opacity': list[state.currentIndex].opacity
})
list.forEach(function(item, index) {
var itemInstance = state.itemsInstance[index]
if (item && itemInstance) {
itemInstance.setStyle({
'transform': 'translate3d(0px, '+ item.translateY + 'px, 0px) scale(' + item.scale + ')',
'z-index': item.zIndex,
'opacity': item.opacity
})
}
})
} else {
var newIndex = (state.currentIndex - 1 < 0) ? list.length - 1 : state.currentIndex - 1
switchPrevSwiper(newIndex, touches, instance, state)
updateCurrentSwiperIndex(newIndex, ownerInstance, state)
}
}
}
// 清除对应的标志位
state.touchRelactive = null
state.touching = false
state.direction = null
state.touchId = null
ownerInstance.callMethod('changeTouchState', {
touching: false
})
// 重新开始执行自动轮播
ownerInstance.callMethod('setAutoPlay')
}
module.exports = {
itemDataObserver: itemDataObserver,
listObserver: listObserver,
swiperIndexChange: swiperIndexChange,
autoplayFlagChange: autoplayFlagChange,
touchStart: touchStart,
touchMove: touchMove,
touchEnd: touchEnd
}
@@ -0,0 +1,284 @@
<template>
<view
class="tn-stack-swiper-class tn-stack-swiper"
:style="{
width: $tn.string.getLengthUnitValue(width),
height: $tn.string.getLengthUnitValue(height)
}"
:list="swiperList"
:itemData="itemData"
:currentIndex="swiperIndex"
:autoplayFlag="autoplayFlag"
:change:list="wxs.listObserver"
:change:itemData="wxs.itemDataObserver"
:change:currentIndex="wxs.swiperIndexChange"
:change:autoplayFlag="wxs.autoplayFlagChange"
>
<block v-for="(item, index) in list" :key="index">
<!-- #ifdef MP-WEIXIN -->
<view
class="tn-stack-swiper__item tn-stack-swiper__item__transition"
:class="[`tn-stack-swiper__item--${direction}`]"
:data-index="index"
:data-switchRate="switchRate"
@touchstart="wxs.touchStart"
:catch:touchmove="touching?wxs.touchMove:''"
:catch:touchend="touching?wxs.touchEnd:''"
>
<image class="tn-stack-swiper__image" :src="item.image"></image>
</view>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<view
class="tn-stack-swiper__item"
:class="[`tn-stack-swiper__item--${direction}`]"
:data-index="index"
:data-switchRate="switchRate"
@touchstart="wxs.touchStart"
@touchmove="wxs.touchMove"
@touchend="wxs.touchEnd"
>
<image class="tn-stack-swiper__image" :src="item.image"></image>
</view>
<!-- #endif -->
</block>
</view>
</template>
<!-- #ifdef MP-WEIXIN -->
<script src="./index.wxs" lang="wxs" module="wxs"></script>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<script src="./index-h5.wxs" lang="wxs" module="wxs"></script>
<!-- #endif -->
<script>
export default {
name: 'tn-stack-swiper',
props: {
// 显示图片的列表数据
// {
// // 图片地址
// image: 'xxx'
// }
list: {
type: Array,
default() {
return []
}
},
// 轮播容器的宽度 rpx
width: {
type: [String, Number],
default: '100%'
},
// 轮播容器的高度 rpx
height: {
type: [String, Number],
default: 500
},
// 自动切换
autoplay: {
type: Boolean,
default: false
},
// 自动切换时长 ms
interval: {
type: Number,
default: 5000
},
// 滑动切换移动比例, [0 - 100]
// 比例相对于item的宽度
switchRate: {
type: Number,
default: 30
},
// 缩放比例 [0-1]
scaleRate: {
type: Number,
default: 0.1
},
// 下一轮播偏移比例
translateRate: {
type: Number,
default: 16
},
// 下一轮播透明比例
opacityRate: {
type: Number,
default:10
},
// 滑动方向
// horizontal -> 水平 vertical -> 垂直
direction: {
type: String,
default: 'horizontal'
}
},
data() {
return {
autoplayTimer: null,
// window窗口的宽度
windowWidth: 0,
// 轮播item的宽度
swiperItemWidth: 0,
// 轮播item的高度
swiperItemHeight: 0,
// 当前选中的轮播item
swiperIndex: -1,
// 标记是否开始触摸
touching: true,
// 轮播列表信息
swiperList: [],
// 标记当前是否为自动播放
autoplayFlag: false
}
},
computed: {
itemData() {
return {
windowWidth: this.windowWidth,
itemWidth: this.swiperItemWidth,
itemHeight: this.swiperItemHeight,
direction: this.direction,
autoplaying: this.autoplayFlag
}
}
},
watch: {
list(val) {
this.swiperList = []
this.$nextTick(() => {
this.initSwiperRectInfo()
})
},
autoplay(val) {
if (!val) {
this.clearAutoPlayTimer()
} else {
this.setAutoPlay()
}
}
},
created() {
this.autoplayFlag = this.autoplay
},
mounted() {
this.$nextTick(() => {
this.initSwiperRectInfo()
})
},
destroyed() {
this.clearAutoPlayTimer()
},
methods: {
// 初始化轮播容器信息
async initSwiperRectInfo() {
// 用于一开始绑定事件
// this.touching = true
// 获取轮播item的宽度
const swiperItemRect = await this._tGetRect('.tn-stack-swiper__item')
if (!swiperItemRect || !swiperItemRect.width || !swiperItemRect.height) {
setTimeout(() => {
this.initSwiperRectInfo()
}, 50)
return
}
this.swiperItemWidth = swiperItemRect.width
this.swiperItemHeight = swiperItemRect.height
// this.touching = false
// 获取系统的窗口宽度信息
const systemInfo = uni.getSystemInfoSync()
this.windowWidth = systemInfo.windowWidth
this.swiperIndex = 0
// 设置对应swiper元素的位置和层级信息
this.swiperList = this.list.map((item, index) => {
const scale = 1 - (this.scaleRate * index)
if (this.direction === 'horizontal') {
item.translateX = ((index * this.translateRate) * 0.01 * this.swiperItemWidth)
} else if (this.direction === 'vertical') {
item.translateY = ((index * this.translateRate) * 0.01 * this.swiperItemHeight)
}
item.opacity = (1 - ((index * this.opacityRate) * 0.01))
item.zIndex = this.list.length - index
item.scale = scale <= 0 ? 0 : scale
return item
})
this.setAutoPlay()
},
// 设置自动切换轮播
setAutoPlay() {
if (this.autoplay) {
this.clearAutoPlayTimer()
this.autoplayFlag = true
this.autoplayTimer = setInterval(() => {
this.swiperIndex = this.swiperIndex + 1 > this.swiperList.length - 1 ? 0 : this.swiperIndex + 1
}, this.interval)
}
},
// 清除自动切换定时器
clearAutoPlayTimer() {
if (this.autoplayTimer != null) {
this.autoplayFlag = false
clearInterval(this.autoplayTimer)
}
},
// 修改轮播选中index
changeSwiperIndex(e) {
// console.log(e.index);
this.swiperIndex = e.index
this.$emit('change', { index: e.index })
},
// 修改触摸状态
changeTouchState(e) {
this.touching = e.touching
},
// 打印日志
printLog(data) {
console.log("log", data);
}
}
}
</script>
<style lang="scss" scoped>
.tn-stack-swiper {
position: relative;
&__item {
position: absolute;
border-radius: 20rpx;
overflow: hidden;
&--horizontal {
width: 88%;
height: 100%;
transform-origin: left center;
}
&--vertical {
width: 100%;
height: 88%;
transform-origin: top center;
}
&__transition {
transition-property: transform,opacity;
transition-duration: 0.25s;
transition-timing-function: ease-out;
// transition: transform, opacity 0.25s ease-in-out !important;
}
}
&__image {
width: 100%;
height: 100%;
}
}
</style>
+100
View File
@@ -0,0 +1,100 @@
<template>
<view class="tn-table-class tn-table" :class="[tableClass]" :style="[tableStyle]">
<slot></slot>
</view>
</template>
<script>
export default {
name: 'tn-table',
props: {
// 边框宽度
borderWidth: {
type: [String, Number],
default: ''
},
// 边框颜色
borderColor: {
type: String,
default: ''
},
// 显示上边框
borderTop: {
type: Boolean,
default: true
},
// 显示右边框
borderRight: {
type: Boolean,
default: false
},
// 显示下边框
borderBottom: {
type: Boolean,
default: false
},
// 显示左边框
borderLeft: {
type: Boolean,
default: true
}
},
computed: {
parentData() {
return [this.borderWidth, this.borderColor]
},
tableClass() {
let clazz = ''
return clazz
},
tableStyle() {
let style = {}
if (this.borderWidth !== '') {
style.borderWidth = this.$tn.string.getLengthUnitValue(this.borderWidth)
}
if (this.borderColor) {
style.borderColor = this.borderColor
}
if (this.borderLeft) {
style.borderLeftStyle = 'solid'
}
if (this.borderRight) {
style.borderRightStyle = 'solid'
}
if (this.borderTop) {
style.borderTopStyle = 'solid'
}
if (this.borderBottom) {
style.borderBottomStyle = 'solid'
}
return style
}
},
data() {
return {}
},
created() {
this.children = []
},
watch: {
parentData() {
// 更新子组件的数据
if (this.children.length) {
this.children.map((child) => {
typeof(child.updateParentData) === 'function' && child.updateParentData()
})
}
}
}
}
</script>
<style lang="scss" scoped>
.tn-table {
box-sizing: border-box;
border-width: 1rpx;
border-style: none;
border-color: #AAAAAA;
}
</style>
+307
View File
@@ -0,0 +1,307 @@
<template>
<view class="tn-td-class tn-td" :class="[tdClass]" :style="[tdStyle]" @tap.stop="handleClick">
<slot></slot>
</view>
</template>
<script>
import componentsColorMixin from '../../libs/mixin/components_color.js'
export default {
name: 'tn-td',
options: {
// 在微信小程序中将组件节点渲染为虚拟节点,更加接近Vue组件的表现(不会出现shadow节点下再去创建元素)
virtualHost: true
},
mixins: [componentsColorMixin],
props: {
// 占整个表格的宽度跨度
// [1-24]
span: {
type: Number,
default: 24
},
// 宽度
// 优先级比span高
width: {
type: [String, Number],
default: ''
},
// 高度
height: {
type: [String, Number],
default: ''
},
// 字体加粗
bold: {
type: Boolean,
default: false
},
// 格内边距
padding: {
type: String,
default: ''
},
// 边框颜色
borderColor: {
type: String,
default: ''
},
// 边框宽度
borderWidth: {
type: [String, Number],
default: ''
},
// 左边框
borderLeft: {
type: Boolean,
default: false
},
// 下边框
borderBottom: {
type: Boolean,
default: false
},
// 右边框
borderRight: {
type: Boolean,
default: true
},
// 文字超出隐藏
ellipsis: {
type: Boolean,
default: false
},
// 文本对齐方式
// left center right
textAlign: {
type: String,
default: 'left'
},
// 排列方式
// left center right
alignItems: {
type: String,
default: 'left'
},
// 收缩表格
shrink: {
type: Boolean,
default: true
},
// 铺满剩余空间
grow: {
type: Boolean,
default: false
},
// 隐藏
hidden: {
type: Boolean,
default: false
},
// 固定列数据
fixed: {
type: Boolean,
default: false
},
// zIndex
zIndex: {
type: Number,
default: 0
},
// 列数
index: {
type: [String, Number],
default: 0
},
// keys
keys: {
type: [String, Number],
default: ''
}
},
computed: {
tdClass() {
let clazz = ''
clazz += `${this.ellipsis ? 'tn-td--ellipsis' : 'tn-td--normal'}`
if (this.backgroundColorClass) {
clazz += ` ${this.backgroundColorClass}`
}
if (this.fontColorClass) {
clazz += ` ${this.fontColorClass}`
}
if (this.alignItems) {
clazz += ` tn-td--${this.alignItems}`
}
if (this.textAlign) {
clazz += ` tn-td__text--${this.textAlign}`
}
if (!this.shrink) {
clazz += ' tn-td--shrink'
}
if (this.grow) {
clazz += ' tn-td--grow'
}
if (this.hidden) {
clazz += ' tn-td--hidden'
}
return clazz
},
tdStyle() {
let style = {}
if (this.backgroundColorStyle) {
style.backgroundColor = this.backgroundColorStyle
}
if (this.fontColorStyle) {
style.color = this.fontColorStyle
}
if (this.fontSizeStyle) {
style.fontSize = this.fontSizeStyle
}
style.width = this.getWidth()
if (this.height) {
style.height = this.$tn.string.getLengthUnitValue(this.height)
}
style.fontWeight = this.bold ? 'bold' : 'normal'
if (this.padding) {
style.padding = this.padding
}
if (this.borderWidth !== '' || this.parentData.borderWidthValue !== '') {
style.borderWidth = this.borderWidth !== '' ? this.$tn.string.getLengthUnitValue(this.borderWidth) : this.$tn.string.getLengthUnitValue(this.parentData.borderWidthValue)
}
if (this.borderColor || this.parentData.borderColorValue) {
style.borderColor = this.borderColor || this.parentData.borderColorValue
}
if (this.borderLeft) {
style.borderLeftStyle = 'solid'
}
if (this.borderRight) {
style.borderRightStyle = 'solid'
}
if (this.borderBottom) {
style.borderBottomStyle = 'solid'
}
if (this.fixed) {
style.zIndex = this.zIndex ? this.zIndex : this.$tn.zIndex.tableTd
}
return style
}
},
data() {
return {
parentData: {
borderColorValue: null,
borderWidthValue: null
}
}
},
created() {
this.parent = false
this.updateParentData()
this.parent && this.parent.children.push(this)
},
methods: {
// 获取表格宽度
getWidth() {
if (this.width) {
return this.$tn.string.getLengthUnitValue(this.width)
}
return [
'4.16666667%',
'8.33333333%',
'12.5%',
'16.66666667%',
'20.83333333%',
'25%',
'29.16666667%',
'33.33333333%',
'37.5%',
'41.66666667%',
'45.83333333%',
'50%',
'54.16666667%',
'58.33333333%',
'62.5%',
'66.66666667%',
'70.83333333%',
'75%',
'79.16666667%',
'83.33333333%',
'87.5%',
'91.66666667%',
'95.83333333%',
'100%'
][this.span - 1]
},
// 点击事件
handleClick() {
this.$emit('click', {
index: this.index,
key: this.keys
})
},
// 更新父组件信息
updateParentData() {
this.getParentData('tn-tr')
}
}
}
</script>
<style lang="scss" scoped>
.tn-td {
box-sizing: border-box;
position: relative;
word-break: break-all;
background-color: transparent;
height: auto;
padding: 12rpx;
border-width: 1rpx;
border-style: none;
border-color: #AAAAAA;
&--normal {
display: inline-flex;
align-items: center;
}
&--ellipsis {
display: inline-block;
overflow: hidden;
white-space: nowrap !important;
text-overflow: ellipsis;
}
&--shrink {
flex-shrink: 0;
}
&--grow {
flex-grow: 1;
}
&--left {
justify-content: flex-start;
}
&--center {
justify-content: center;
}
&--right {
justify-content: flex-end;
}
&__text {
&--left {
text-align: left;
}
&--center {
text-align: center;
}
&--right {
text-align: right;
}
}
&--hidden {
visibility: hidden;
}
}
</style>
+210
View File
@@ -0,0 +1,210 @@
<template>
<view class="tn-tr-class tn-tr" :class="[trClass]" :style="[trStyle]">
<slot></slot>
</view>
</template>
<script>
import componentsColorMixin from '../../libs/mixin/components_color.js'
export default {
name: 'tn-tr',
options: {
// 在微信小程序中将组件节点渲染为虚拟节点,更加接近Vue组件的表现(不会出现shadow节点下再去创建元素)
virtualHost: true
},
mixins: [componentsColorMixin],
props: {
// 宽度
width: {
type: [String, Number],
default: ''
},
// 边框颜色
borderColor: {
type: String,
default: ''
},
// 边框宽度
borderWidth: {
type: [String, Number],
default: ''
},
// 左边框
borderLeft: {
type: Boolean,
default: false
},
// 上边框
borderTop: {
type: Boolean,
default: false
},
// 换行显示
wrap: {
type: Boolean,
default: false
},
// 固定表格
fixed: {
type: Boolean,
default: false
},
// left偏移值
left: {
type: [String, Number],
default: 0
},
// right偏移值
right: {
type: [String, Number],
default: 0
},
// top偏移值(自定义顶部导航栏时用到)
top: {
type: [String, Number],
default: 0
},
// 外边距
margin: {
type: String,
default: ''
},
// zIndex
zIndex: {
type: Number,
default: 0
},
// 行数索引
index: {
type: [String, Number],
default: 0
},
// 参数
params: {
type: String,
default: ''
}
},
computed: {
borderWidthValue() {
return this.borderWidth || this.parentData.borderWidth || ''
},
borderColorValue() {
return this.borderColor || this.parentData.borderColor || ''
},
trClass() {
let clazz = ''
if (this.backgroundColorClass) {
clazz += ` ${this.backgroundColorClass}`
}
if (this.fontColorClass) {
clazz += ` ${this.fontColorClass}`
}
if (this.wrap) {
clazz += ' tn-tr--wrap'
}
if (this.fixed) {
clazz += ' tn-tr--fixed'
}
return clazz
},
trStyle() {
let style = {}
if (this.width) {
style.width = this.$tn.string.getLengthUnitValue(this.width)
}
if (this.backgroundColorStyle) {
style.backgroundColor = this.backgroundColorStyle
}
if (this.fontColorStyle) {
style.color = this.fontColorStyle
}
if (this.fontSizeStyle) {
style.fontSize = this.fontSizeStyle
}
if (this.borderWidth !== '' || this.parentData.borderWidth !== '') {
style.borderWidth = this.borderWidth !== '' ? this.$tn.string.getLengthUnitValue(this.borderWidth) : this.$tn.string.getLengthUnitValue(this.parentData.borderWidth)
}
if (this.borderColor || this.parentData.borderColor) {
style.borderColor = this.borderColor || this.parentData.borderColor
}
if (this.borderLeft) {
style.borderLeftStyle = 'solid'
}
if (this.borderTop) {
style.borderTopStyle = 'solid'
}
if (this.fixed) {
style.left = this.left ? this.$tn.string.getLengthUnitValue(this.left) : 'auto'
style.right = this.right ? this.$tn.string.getLengthUnitValue(this.right) : 'auto'
style.top = this.top ? this.$tn.string.getLengthUnitValue(this.top) : 'auto'
}
if (this.margin) {
style.margin = this.margin
}
style.zIndex = this.zIndex ? this.zIndex : this.$tn.zIndex.tableTr
return style
}
},
data() {
return {
parentData: {
borderColor: null,
borderWidth: null
}
}
},
watch: {
parentData: {
handler() {
// 更新子组件的数据
if (this.children.length) {
this.children.map((child) => {
typeof(child.updateParentData) === 'function' && child.updateParentData()
})
}
},
deep: true
}
},
created() {
this.children = []
this.parent = false
this.updateParentData()
this.parent && this.parent.children.push(this)
},
methods: {
handleClick() {
this.$emit('click', {
index: this.index,
params: this.params
})
},
// 更新父组件信息
updateParentData() {
this.getParentData('tn-table')
}
}
}
</script>
<style lang="scss" scoped>
.tn-tr {
width: 100%;
display: flex;
box-sizing: border-box;
background-color: #FFFFFF;
border-width: 1rpx;
border-style: none none solid none;
border-color: #AAAAAA;
&--wrap {
flex-wrap: wrap;
}
&--fixed {
position: fixed;
}
}
</style>
@@ -0,0 +1,143 @@
<template>
<view class="tn-tree-node-class tn-tree-node">
<view class="tn-tree__label" @tap="handleClick">
<view
v-if="node.children && node.children.length > 0 && triangle"
class="tn-tree__triangle"
:class="[{'tn-tree__triangle--90deg': !collapsed}]"
></view>
<view class="tn-tree__label__item">
<view v-if="collapsed && node.image" class="tn-tree__label__item__image">
<image :src="node.image" mode="widthFix"></image>
</view>
<view v-if="!collapsed && node.activeImage" class="tn-tree__label__item__image">
<image :src="node.activeImage" mode="widthFix"></image>
</view>
<view class="tn-tree__label__item__text">{{ node.text }}</view>
</view>
</view>
<view v-if="!collapsed && node.children && node.children.length > 0" class="tn-tree__children">
<tn-tree-node
v-for="(item, index) in node.children"
:key="index"
:node="item"
:collapsible="collapsible"
:triangle="triangle"
@click="nodeClick"
></tn-tree-node>
</view>
</view>
</template>
<script>
//如果未开启easycom模式,请自行引入tn-tree-node组件
export default {
name: 'tn-tree-node',
props: {
// 节点信息
node: {
type: Object,
default() {
return {}
}
},
// 可以折叠
collapsible: {
type: Boolean,
default: true
},
// 显示三角形
triangle: {
type: Boolean,
default: true
}
},
watch: {
node(val) {
if (val.collapsed !== this.collapsed && this.node.children && this.node.children.length > 0) {
this.collapsed = val.collapsed
}
}
},
data() {
return {
// 标记是否折叠
collapsed: true
}
},
created() {
if (this.node.collapsed === false) {
this.collapsed = false
}
},
methods: {
// 处理点击
handleClick(e) {
if (this.collapsible && this.node.children && this.node.children.length > 0) {
this.collapsed = !this.collapsed
}
this.$emit('click', this.node)
},
nodeClick(e) {
this.$emit('click', e)
}
}
}
</script>
<style lang="scss" scoped>
.tn-tree-node {
.tn-tree {
&__label {
position: relative;
display: inline-flex;
align-items: center;
padding: 20rpx 30rpx;
background-color: transparent;
/* #ifdef H5 */
cursor: pointer;
/* #endif */
// 字体抗锯齿
-webkit-font-smoothing: antialiased;
&__item {
display: flex;
flex-direction: row;
align-items: center;
&__image {
width: 40rpx;
height: 40rpx;
margin-right: 16rpx;
image {
width: 100%;
height: 100%;
}
}
}
}
&__children {
padding-left: 60rpx;
position: relative;
}
&__triangle {
width: 0;
height: 0;
border-top: 12rpx solid transparent;
border-bottom: 12rpx solid transparent;
border-left: 16rpx solid #080808;
margin-right: 20rpx;
transition: transform 0.25s ease-out;
flex-shrink: 0;
&--90deg {
transform:rotate(90deg) translate3d(0, 0, 0);
}
}
}
}
</style>
@@ -0,0 +1,50 @@
<template>
<view class="tn-tree-view-class tn-tree-view">
<tn-tree-node
v-for="(item, index) in treeData"
:key="index"
:node="item"
:collapsible="collapsible"
:triangle="triangle"
@click="handleClick"
></tn-tree-node>
</view>
</template>
<script>
//如果未开启easycom模式,请自行引入tn-tree-node组件
export default {
name: 'tn-tree-view',
props: {
// 节点信息
treeData: {
type: Array,
default() {
return []
}
},
// 可以折叠
collapsible: {
type: Boolean,
default: true
},
// 显示三角形
triangle: {
type: Boolean,
default: true
}
},
methods: {
handleClick(e) {
this.$emit('click', e)
}
}
}
</script>
<style lang="scss" scoped>
.tn-tree-view {
width: 100%;
position: relative;
}
</style>
@@ -0,0 +1,165 @@
<template>
<view class="tn-waterfall-class tn-waterfall">
<view id="tn-waterfall-left" class="tn-waterfall__column"><slot name="left" :leftList="leftList"></slot></view>
<view id="tn-waterfall-right" class="tn-waterfall__column"><slot name="right" :rightList="rightList"></slot></view>
</view>
</template>
<script>
export default {
name: 'tn-waterfall',
props: {
// 瀑布流数据
value: {
type: Array,
default() {
return []
}
},
// 数据的id值,根据id值对数据执行删除操作
// 如数据为:{id: 1, name: 'tuniao'},那么该值设置为id
idKey: {
type: String,
default: 'id'
},
// 每次插入数据的事件间隔,间隔越长能保证两列高度相近,但是用户体验不好
// 单位ms
addTime: {
type: Number,
default: 200
}
},
computed: {
// 破坏value变量引用,否则数据会保持不变
copyValue() {
return this.cloneData(this.value)
}
},
watch: {
copyValue(nVal, oVal) {
// 取出数组发生变化的部分
let startIndex = Array.isArray(oVal) && oVal.length > 0 ? oVal.length : 0
// 拼接原有数据
this.tempList = this.tempList.concat(this.cloneData(nVal.slice(startIndex)))
this.splitData()
}
},
data() {
return {
// 左列表
leftList: [],
// 右列表
rightList: [],
// 临时列表
tempList: []
}
},
mounted() {
this.tempList = this.cloneData(this.copyValue)
this.splitData()
},
methods: {
// 拆分数据
async splitData() {
if (!this.tempList.length) return
let leftRect = await this._tGetRect('#tn-waterfall-left')
let rightRect = await this._tGetRect('#tn-waterfall-right')
let item = this.tempList[0]
// 因为经过上面两个await节点查询和定时器,数组有可能会变成空[],导致item的值为undefined
// 解决多次快速滚动会导致数据乱的问题
if (!item) return
// 如果左边小于或者等于右边,就添加到左边,否则添加到右边
if (leftRect.height < rightRect.height) {
this.leftList.push(item)
} else if (leftRect.height > rightRect.height) {
this.rightList.push(item)
} else {
// 为了保证前两项添加时,左右两边都还没有内容,这时候根据队列长度判断下一项该放在哪一边
if (this.leftList.length <= this.rightList.length) {
this.leftList.push(item)
} else {
this.rightList.push(item)
}
}
// 移除临时数组中已处理的数据
this.tempList.splice(0, 1)
// 如果还有数据则继续执行
if (this.tempList.length) {
setTimeout(() => {
this.splitData()
}, this.addTime)
} else {
this.$emit('finish')
}
},
// 复制对象和数组(深度复制不会影响原对象和数组)
cloneData(data) {
return JSON.parse(JSON.stringify(data))
},
// 清空数据列表
clear() {
this.leftList = []
this.rightList = []
this.$emit('input', [])
this.tempList = []
},
// 清除指定的某一条数据,根据id来实现
remove(id) {
// 如果查找不到就返回-1
let index = -1
index = this.leftList.findIndex(val => val[this.idKey] == id)
if (index != -1) {
// 如果index不等于-1,说明已经找到了指定的数据
this.leftList.splice(index, 1)
} else {
// 同理于上面的方法
index = this.rightList.findIndex(val => val[this.idKey] == id)
if (index != -1) this.rightList.splice(index, 1)
}
// 同时删除父组件对应的数据
index = this.value.findIndex(val => val[this.idKey] == id)
if (index != -1) this.$emit('input', this.value.splice(index, 1))
},
// 修改指定数据的属性
modify(id, key, value) {
// 如果查找不到就返回-1
let index = -1
index = this.leftList.findIndex(val => val[this.idKey] == id)
if (index != -1) {
// 如果index不等于-1,说明已经找到了指定的数据
this.leftList[index][key] = value
} else {
// 同理于上面的方法
index = this.rightList.findIndex(val => val[this.idKey] == id)
if (index != -1) this.rightList[index][key] = value
}
// 同时删除父组件对应的数据
index = this.value.findIndex(val => val[this.idKey] == id)
if(index != -1) {
let data = this.cloneData(this.value)
data[index][key] = value
this.$emit('input', data)
}
}
}
}
</script>
<style lang="scss" scoped>
.tn-waterfall {
display: flex;
flex-direction: row;
align-items: flex-start;
&__column {
display: flex;
flex-direction: column;
flex: 1;
height: auto;
}
}
</style>
+1164 -31
View File
File diff suppressed because one or more lines are too long
+3
View File
@@ -1,5 +1,7 @@
// 引入全局mixin
import mixin from './libs/mixin/mixin.js'
// 全局挂载引入http相关请求拦截插件
import Request from './libs/luch-request'
// 调试输出信息
function wranning(str) {
@@ -39,6 +41,7 @@ import zIndex from './libs/config/zIndex.js'
import colorInfo from './libs/config/color.js'
const $tn = {
http: new Request(),
updateCustomBar: updateCustomBarInfo,
color,
message,
@@ -0,0 +1,99 @@
import buildURL from '../helpers/buildURL'
import buildFullPath from '../core/buildFullPath'
import settle from '../core/settle'
import { isUndefined } from "../utils"
/**
* 返回可选值存在的配置
* @param {Array} keys - 可选值数组
* @param {Object} config2 - 配置
* @return {{}} - 存在的配置项
*/
const mergeKeys = (keys, config2) => {
let config = {}
keys.forEach(prop => {
if (!isUndefined(config2[prop])) {
config[prop] = config2[prop]
}
})
return config
}
export default (config) => {
return new Promise((resolve, reject) => {
let fullPath = buildURL(buildFullPath(config.baseURL, config.url), config.params)
const _config = {
url: fullPath,
header: config.header,
complete: (response) => {
config.fullPath = fullPath
response.config = config
try {
// 对可能字符串不是json 的情况容错
if (typeof response.data === 'string') {
response.data = JSON.parse(response.data)
}
// eslint-disable-next-line no-empty
} catch (e) {
}
settle(resolve, reject, response)
}
}
let requestTask
if (config.method === 'UPLOAD') {
delete _config.header['content-type']
delete _config.header['Content-Type']
let otherConfig = {
// #ifdef MP-ALIPAY
fileType: config.fileType,
// #endif
filePath: config.filePath,
name: config.name
}
const optionalKeys = [
// #ifdef APP-PLUS || H5
'files',
// #endif
// #ifdef H5
'file',
// #endif
// #ifdef H5 || APP-PLUS
'timeout',
// #endif
'formData'
]
requestTask = uni.uploadFile({..._config, ...otherConfig, ...mergeKeys(optionalKeys, config)})
} else if (config.method === 'DOWNLOAD') {
// #ifdef H5 || APP-PLUS
if (!isUndefined(config['timeout'])) {
_config['timeout'] = config['timeout']
}
// #endif
requestTask = uni.downloadFile(_config)
} else {
const optionalKeys = [
'data',
'method',
// #ifdef H5 || APP-PLUS || MP-ALIPAY || MP-WEIXIN
'timeout',
// #endif
'dataType',
// #ifndef MP-ALIPAY
'responseType',
// #endif
// #ifdef APP-PLUS
'sslVerify',
// #endif
// #ifdef H5
'withCredentials',
// #endif
// #ifdef APP-PLUS
'firstIpv4',
// #endif
]
requestTask = uni.request({..._config,...mergeKeys(optionalKeys, config)})
}
if (config.getTask) {
config.getTask(requestTask, config)
}
})
}
@@ -0,0 +1,51 @@
'use strict'
function InterceptorManager() {
this.handlers = []
}
/**
* Add a new interceptor to the stack
*
* @param {Function} fulfilled The function to handle `then` for a `Promise`
* @param {Function} rejected The function to handle `reject` for a `Promise`
*
* @return {Number} An ID used to remove interceptor later
*/
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected
})
return this.handlers.length - 1
}
/**
* Remove an interceptor from the stack
*
* @param {Number} id The ID that was returned by `use`
*/
InterceptorManager.prototype.eject = function eject(id) {
if (this.handlers[id]) {
this.handlers[id] = null
}
}
/**
* Iterate over all the registered interceptors
*
* This method is particularly useful for skipping over any
* interceptors that may have become `null` calling `eject`.
*
* @param {Function} fn The function to call for each interceptor
*/
InterceptorManager.prototype.forEach = function forEach(fn) {
this.handlers.forEach(h => {
if (h !== null) {
fn(h)
}
})
}
export default InterceptorManager
+200
View File
@@ -0,0 +1,200 @@
/**
* @Class Request
* @description luch-request http请求插件
* @version 3.0.7
* @Author lu-ch
* @Date 2021-09-04
* @Email webwork.s@qq.com
* 文档: https://www.quanzhan.co/luch-request/
* github: https://github.com/lei-mu/luch-request
* DCloud: http://ext.dcloud.net.cn/plugin?id=392
* HBuilderX: beat-3.0.4 alpha-3.0.4
*/
import dispatchRequest from './dispatchRequest'
import InterceptorManager from './InterceptorManager'
import mergeConfig from './mergeConfig'
import defaults from './defaults'
import { isPlainObject } from '../utils'
import clone from '../utils/clone'
export default class Request {
/**
* @param {Object} arg - 全局配置
* @param {String} arg.baseURL - 全局根路径
* @param {Object} arg.header - 全局header
* @param {String} arg.method = [GET|POST|PUT|DELETE|CONNECT|HEAD|OPTIONS|TRACE] - 全局默认请求方式
* @param {String} arg.dataType = [json] - 全局默认的dataType
* @param {String} arg.responseType = [text|arraybuffer] - 全局默认的responseType。支付宝小程序不支持
* @param {Object} arg.custom - 全局默认的自定义参数
* @param {Number} arg.timeout - 全局默认的超时时间,单位 ms。默认60000。H5(HBuilderX 2.9.9+)、APP(HBuilderX 2.9.9+)、微信小程序(2.10.0)、支付宝小程序
* @param {Boolean} arg.sslVerify - 全局默认的是否验证 ssl 证书。默认true.仅App安卓端支持(HBuilderX 2.3.3+
* @param {Boolean} arg.withCredentials - 全局默认的跨域请求时是否携带凭证(cookies)。默认false。仅H5支持(HBuilderX 2.6.15+
* @param {Boolean} arg.firstIpv4 - 全DNS解析时优先使用ipv4。默认false。仅 App-Android 支持 (HBuilderX 2.8.0+)
* @param {Function(statusCode):Boolean} arg.validateStatus - 全局默认的自定义验证器。默认statusCode >= 200 && statusCode < 300
*/
constructor(arg = {}) {
if (!isPlainObject(arg)) {
arg = {}
console.warn('设置全局参数必须接收一个Object')
}
this.config = clone({...defaults, ...arg})
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
}
}
/**
* @Function
* @param {Request~setConfigCallback} f - 设置全局默认配置
*/
setConfig(f) {
this.config = f(this.config)
}
middleware(config) {
config = mergeConfig(this.config, config)
let chain = [dispatchRequest, undefined]
let promise = Promise.resolve(config)
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected)
})
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected)
})
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift())
}
return promise
}
/**
* @Function
* @param {Object} config - 请求配置项
* @prop {String} options.url - 请求路径
* @prop {Object} options.data - 请求参数
* @prop {Object} [options.responseType = config.responseType] [text|arraybuffer] - 响应的数据类型
* @prop {Object} [options.dataType = config.dataType] - 如果设为 json,会尝试对返回的数据做一次 JSON.parse
* @prop {Object} [options.header = config.header] - 请求header
* @prop {Object} [options.method = config.method] - 请求方法
* @returns {Promise<unknown>}
*/
request(config = {}) {
return this.middleware(config)
}
get(url, options = {}) {
return this.middleware({
url,
method: 'GET',
...options
})
}
post(url, data, options = {}) {
return this.middleware({
url,
data,
method: 'POST',
...options
})
}
// #ifndef MP-ALIPAY
put(url, data, options = {}) {
return this.middleware({
url,
data,
method: 'PUT',
...options
})
}
// #endif
// #ifdef APP-PLUS || H5 || MP-WEIXIN || MP-BAIDU
delete(url, data, options = {}) {
return this.middleware({
url,
data,
method: 'DELETE',
...options
})
}
// #endif
// #ifdef H5 || MP-WEIXIN
connect(url, data, options = {}) {
return this.middleware({
url,
data,
method: 'CONNECT',
...options
})
}
// #endif
// #ifdef H5 || MP-WEIXIN || MP-BAIDU
head(url, data, options = {}) {
return this.middleware({
url,
data,
method: 'HEAD',
...options
})
}
// #endif
// #ifdef APP-PLUS || H5 || MP-WEIXIN || MP-BAIDU
options(url, data, options = {}) {
return this.middleware({
url,
data,
method: 'OPTIONS',
...options
})
}
// #endif
// #ifdef H5 || MP-WEIXIN
trace(url, data, options = {}) {
return this.middleware({
url,
data,
method: 'TRACE',
...options
})
}
// #endif
upload(url, config = {}) {
config.url = url
config.method = 'UPLOAD'
return this.middleware(config)
}
download(url, config = {}) {
config.url = url
config.method = 'DOWNLOAD'
return this.middleware(config)
}
}
/**
* setConfig回调
* @return {Object} - 返回操作后的config
* @callback Request~setConfigCallback
* @param {Object} config - 全局默认config
*/
@@ -0,0 +1,20 @@
'use strict'
import isAbsoluteURL from '../helpers/isAbsoluteURL'
import combineURLs from '../helpers/combineURLs'
/**
* Creates a new URL by combining the baseURL with the requestedURL,
* only when the requestedURL is not already an absolute URL.
* If the requestURL is absolute, this function returns the requestedURL untouched.
*
* @param {string} baseURL The base URL
* @param {string} requestedURL Absolute or relative URL to combine
* @returns {string} The combined full path
*/
export default function buildFullPath(baseURL, requestedURL) {
if (baseURL && !isAbsoluteURL(requestedURL)) {
return combineURLs(baseURL, requestedURL)
}
return requestedURL
}
@@ -0,0 +1,30 @@
/**
* 默认的全局配置
*/
export default {
baseURL: '',
header: {},
method: 'GET',
dataType: 'json',
// #ifndef MP-ALIPAY
responseType: 'text',
// #endif
custom: {},
// #ifdef H5 || APP-PLUS || MP-ALIPAY || MP-WEIXIN
timeout: 60000,
// #endif
// #ifdef APP-PLUS
sslVerify: true,
// #endif
// #ifdef H5
withCredentials: false,
// #endif
// #ifdef APP-PLUS
firstIpv4: false,
// #endif
validateStatus: function validateStatus(status) {
return status >= 200 && status < 300
}
}
@@ -0,0 +1,6 @@
import adapter from '../adapters/index'
export default (config) => {
return adapter(config)
}
@@ -0,0 +1,103 @@
import {deepMerge, isUndefined} from '../utils'
/**
* 合并局部配置优先的配置,如果局部有该配置项则用局部,如果全局有该配置项则用全局
* @param {Array} keys - 配置项
* @param {Object} globalsConfig - 当前的全局配置
* @param {Object} config2 - 局部配置
* @return {{}}
*/
const mergeKeys = (keys, globalsConfig, config2) => {
let config = {}
keys.forEach(prop => {
if (!isUndefined(config2[prop])) {
config[prop] = config2[prop]
} else if (!isUndefined(globalsConfig[prop])) {
config[prop] = globalsConfig[prop]
}
})
return config
}
/**
*
* @param globalsConfig - 当前实例的全局配置
* @param config2 - 当前的局部配置
* @return - 合并后的配置
*/
export default (globalsConfig, config2 = {}) => {
const method = config2.method || globalsConfig.method || 'GET'
let config = {
baseURL: globalsConfig.baseURL || '',
method: method,
url: config2.url || '',
params: config2.params || {},
custom: {...(globalsConfig.custom || {}), ...(config2.custom || {})},
header: deepMerge(globalsConfig.header || {}, config2.header || {})
}
const defaultToConfig2Keys = ['getTask', 'validateStatus']
config = {...config, ...mergeKeys(defaultToConfig2Keys, globalsConfig, config2)}
// eslint-disable-next-line no-empty
if (method === 'DOWNLOAD') {
// #ifdef H5 || APP-PLUS
if (!isUndefined(config2.timeout)) {
config['timeout'] = config2['timeout']
} else if (!isUndefined(globalsConfig.timeout)) {
config['timeout'] = globalsConfig['timeout']
}
// #endif
} else if (method === 'UPLOAD') {
delete config.header['content-type']
delete config.header['Content-Type']
const uploadKeys = [
// #ifdef APP-PLUS || H5
'files',
// #endif
// #ifdef MP-ALIPAY
'fileType',
// #endif
// #ifdef H5
'file',
// #endif
'filePath',
'name',
// #ifdef H5 || APP-PLUS
'timeout',
// #endif
'formData',
]
uploadKeys.forEach(prop => {
if (!isUndefined(config2[prop])) {
config[prop] = config2[prop]
}
})
// #ifdef H5 || APP-PLUS
if (isUndefined(config.timeout) && !isUndefined(globalsConfig.timeout)) {
config['timeout'] = globalsConfig['timeout']
}
// #endif
} else {
const defaultsKeys = [
'data',
// #ifdef H5 || APP-PLUS || MP-ALIPAY || MP-WEIXIN
'timeout',
// #endif
'dataType',
// #ifndef MP-ALIPAY
'responseType',
// #endif
// #ifdef APP-PLUS
'sslVerify',
// #endif
// #ifdef H5
'withCredentials',
// #endif
// #ifdef APP-PLUS
'firstIpv4',
// #endif
]
config = {...config, ...mergeKeys(defaultsKeys, globalsConfig, config2)}
}
return config
}
@@ -0,0 +1,16 @@
/**
* Resolve or reject a Promise based on response status.
*
* @param {Function} resolve A function that resolves the promise.
* @param {Function} reject A function that rejects the promise.
* @param {object} response The response.
*/
export default function settle(resolve, reject, response) {
const validateStatus = response.config.validateStatus
const status = response.statusCode
if (status && (!validateStatus || validateStatus(status))) {
resolve(response)
} else {
reject(response)
}
}
@@ -0,0 +1,69 @@
'use strict'
import * as utils from './../utils'
function encode(val) {
return encodeURIComponent(val).
replace(/%40/gi, '@').
replace(/%3A/gi, ':').
replace(/%24/g, '$').
replace(/%2C/gi, ',').
replace(/%20/g, '+').
replace(/%5B/gi, '[').
replace(/%5D/gi, ']')
}
/**
* Build a URL by appending params to the end
*
* @param {string} url The base of the url (e.g., http://www.google.com)
* @param {object} [params] The params to be appended
* @returns {string} The formatted url
*/
export default function buildURL(url, params) {
/*eslint no-param-reassign:0*/
if (!params) {
return url
}
var serializedParams
if (utils.isURLSearchParams(params)) {
serializedParams = params.toString()
} else {
var parts = []
utils.forEach(params, function serialize(val, key) {
if (val === null || typeof val === 'undefined') {
return
}
if (utils.isArray(val)) {
key = key + '[]'
} else {
val = [val]
}
utils.forEach(val, function parseValue(v) {
if (utils.isDate(v)) {
v = v.toISOString()
} else if (utils.isObject(v)) {
v = JSON.stringify(v)
}
parts.push(encode(key) + '=' + encode(v))
})
})
serializedParams = parts.join('&')
}
if (serializedParams) {
var hashmarkIndex = url.indexOf('#')
if (hashmarkIndex !== -1) {
url = url.slice(0, hashmarkIndex)
}
url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams
}
return url
}
@@ -0,0 +1,14 @@
'use strict'
/**
* Creates a new URL by combining the specified URLs
*
* @param {string} baseURL The base URL
* @param {string} relativeURL The relative URL
* @returns {string} The combined URL
*/
export default function combineURLs(baseURL, relativeURL) {
return relativeURL
? baseURL.replace(/\/+$/, '') + '/' + relativeURL.replace(/^\/+/, '')
: baseURL
}
@@ -0,0 +1,14 @@
'use strict'
/**
* Determines whether the specified URL is absolute
*
* @param {string} url The URL to test
* @returns {boolean} True if the specified URL is absolute, otherwise false
*/
export default function isAbsoluteURL(url) {
// A URL is considered absolute if it begins with "<scheme>://" or "//" (protocol-relative URL).
// RFC 3986 defines scheme name as a sequence of characters beginning with a letter and followed
// by any combination of letters, digits, plus, period, or hyphen.
return /^([a-z][a-z\d+\-.]*:)?\/\//i.test(url)
}
+116
View File
@@ -0,0 +1,116 @@
type AnyObject = Record<string | number | symbol, any>
type HttpPromise<T> = Promise<HttpResponse<T>>;
type Tasks = UniApp.RequestTask | UniApp.UploadTask | UniApp.DownloadTask
export interface RequestTask {
abort: () => void;
offHeadersReceived: () => void;
onHeadersReceived: () => void;
}
export interface HttpRequestConfig<T = Tasks> {
/** 请求基地址 */
baseURL?: string;
/** 请求服务器接口地址 */
url?: string;
/** 请求查询参数,自动拼接为查询字符串 */
params?: AnyObject;
/** 请求体参数 */
data?: AnyObject;
/** 文件对应的 key */
name?: string;
/** HTTP 请求中其他额外的 form data */
formData?: AnyObject;
/** 要上传文件资源的路径。 */
filePath?: string;
/** 需要上传的文件列表。使用 files 时,filePath 和 name 不生效,App、H5 2.6.15+ */
files?: Array<{
name?: string;
file?: File;
uri: string;
}>;
/** 要上传的文件对象,仅H5(2.6.15+)支持 */
file?: File;
/** 请求头信息 */
header?: AnyObject;
/** 请求方式 */
method?: "GET" | "POST" | "PUT" | "DELETE" | "CONNECT" | "HEAD" | "OPTIONS" | "TRACE" | "UPLOAD" | "DOWNLOAD";
/** 如果设为 json,会尝试对返回的数据做一次 JSON.parse */
dataType?: string;
/** 设置响应的数据类型,支付宝小程序不支持 */
responseType?: "text" | "arraybuffer";
/** 自定义参数 */
custom?: AnyObject;
/** 超时时间,仅微信小程序(2.10.0)、支付宝小程序支持 */
timeout?: number;
/** DNS解析时优先使用ipv4,仅 App-Android 支持 (HBuilderX 2.8.0+) */
firstIpv4?: boolean;
/** 验证 ssl 证书 仅5+App安卓端支持(HBuilderX 2.3.3+ */
sslVerify?: boolean;
/** 跨域请求时是否携带凭证(cookies)仅H5支持(HBuilderX 2.6.15+ */
withCredentials?: boolean;
/** 返回当前请求的task, options。请勿在此处修改options。 */
getTask?: (task: T, options: HttpRequestConfig<T>) => void;
/** 全局自定义验证器 */
validateStatus?: (statusCode: number) => boolean | void;
}
export interface HttpResponse<T = any> {
config: HttpRequestConfig;
statusCode: number;
cookies: Array<string>;
data: T;
errMsg: string;
header: AnyObject;
}
export interface HttpUploadResponse<T = any> {
config: HttpRequestConfig;
statusCode: number;
data: T;
errMsg: string;
}
export interface HttpDownloadResponse extends HttpResponse {
tempFilePath: string;
}
export interface HttpError {
config: HttpRequestConfig;
statusCode?: number;
cookies?: Array<string>;
data?: any;
errMsg: string;
header?: AnyObject;
}
export interface HttpInterceptorManager<V, E = V> {
use(
onFulfilled?: (config: V) => Promise<V> | V,
onRejected?: (config: E) => Promise<E> | E
): void;
eject(id: number): void;
}
export abstract class HttpRequestAbstract {
constructor(config?: HttpRequestConfig);
config: HttpRequestConfig;
interceptors: {
request: HttpInterceptorManager<HttpRequestConfig, HttpRequestConfig>;
response: HttpInterceptorManager<HttpResponse, HttpError>;
}
middleware<T = any>(config: HttpRequestConfig): HttpPromise<T>;
request<T = any>(config: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
get<T = any>(url: string, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
upload<T = any>(url: string, config?: HttpRequestConfig<UniApp.UploadTask>): HttpPromise<T>;
delete<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
head<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
post<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
put<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
connect<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
options<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
trace<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
download(url: string, config?: HttpRequestConfig<UniApp.DownloadTask>): Promise<HttpDownloadResponse>;
setConfig(onSend: (config: HttpRequestConfig) => HttpRequestConfig): void;
}
declare class HttpRequest extends HttpRequestAbstract { }
export default HttpRequest;
+2
View File
@@ -0,0 +1,2 @@
import Request from './core/Request'
export default Request
+135
View File
@@ -0,0 +1,135 @@
'use strict'
// utils is a library of generic helper functions non-specific to axios
var toString = Object.prototype.toString
/**
* Determine if a value is an Array
*
* @param {Object} val The value to test
* @returns {boolean} True if value is an Array, otherwise false
*/
export function isArray (val) {
return toString.call(val) === '[object Array]'
}
/**
* Determine if a value is an Object
*
* @param {Object} val The value to test
* @returns {boolean} True if value is an Object, otherwise false
*/
export function isObject (val) {
return val !== null && typeof val === 'object'
}
/**
* Determine if a value is a Date
*
* @param {Object} val The value to test
* @returns {boolean} True if value is a Date, otherwise false
*/
export function isDate (val) {
return toString.call(val) === '[object Date]'
}
/**
* Determine if a value is a URLSearchParams object
*
* @param {Object} val The value to test
* @returns {boolean} True if value is a URLSearchParams object, otherwise false
*/
export function isURLSearchParams (val) {
return typeof URLSearchParams !== 'undefined' && val instanceof URLSearchParams
}
/**
* Iterate over an Array or an Object invoking a function for each item.
*
* If `obj` is an Array callback will be called passing
* the value, index, and complete array for each item.
*
* If 'obj' is an Object callback will be called passing
* the value, key, and complete object for each property.
*
* @param {Object|Array} obj The object to iterate
* @param {Function} fn The callback to invoke for each item
*/
export function forEach (obj, fn) {
// Don't bother if no value provided
if (obj === null || typeof obj === 'undefined') {
return
}
// Force an array if not already something iterable
if (typeof obj !== 'object') {
/*eslint no-param-reassign:0*/
obj = [obj]
}
if (isArray(obj)) {
// Iterate over array values
for (var i = 0, l = obj.length; i < l; i++) {
fn.call(null, obj[i], i, obj)
}
} else {
// Iterate over object keys
for (var key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
fn.call(null, obj[key], key, obj)
}
}
}
}
/**
* 是否为boolean 值
* @param val
* @returns {boolean}
*/
export function isBoolean(val) {
return typeof val === 'boolean'
}
/**
* 是否为真正的对象{} new Object
* @param {any} obj - 检测的对象
* @returns {boolean}
*/
export function isPlainObject(obj) {
return Object.prototype.toString.call(obj) === '[object Object]'
}
/**
* Function equal to merge with the difference being that no reference
* to original objects is kept.
*
* @see merge
* @param {Object} obj1 Object to merge
* @returns {Object} Result of all merge properties
*/
export function deepMerge(/* obj1, obj2, obj3, ... */) {
let result = {}
function assignValue(val, key) {
if (typeof result[key] === 'object' && typeof val === 'object') {
result[key] = deepMerge(result[key], val)
} else if (typeof val === 'object') {
result[key] = deepMerge({}, val)
} else {
result[key] = val
}
}
for (let i = 0, l = arguments.length; i < l; i++) {
forEach(arguments[i], assignValue)
}
return result
}
export function isUndefined (val) {
return typeof val === 'undefined'
}
+264
View File
@@ -0,0 +1,264 @@
/* eslint-disable */
var clone = (function() {
'use strict';
function _instanceof(obj, type) {
return type != null && obj instanceof type;
}
var nativeMap;
try {
nativeMap = Map;
} catch(_) {
// maybe a reference error because no `Map`. Give it a dummy value that no
// value will ever be an instanceof.
nativeMap = function() {};
}
var nativeSet;
try {
nativeSet = Set;
} catch(_) {
nativeSet = function() {};
}
var nativePromise;
try {
nativePromise = Promise;
} catch(_) {
nativePromise = function() {};
}
/**
* Clones (copies) an Object using deep copying.
*
* This function supports circular references by default, but if you are certain
* there are no circular references in your object, you can save some CPU time
* by calling clone(obj, false).
*
* Caution: if `circular` is false and `parent` contains circular references,
* your program may enter an infinite loop and crash.
*
* @param `parent` - the object to be cloned
* @param `circular` - set to true if the object to be cloned may contain
* circular references. (optional - true by default)
* @param `depth` - set to a number if the object is only to be cloned to
* a particular depth. (optional - defaults to Infinity)
* @param `prototype` - sets the prototype to be used when cloning an object.
* (optional - defaults to parent prototype).
* @param `includeNonEnumerable` - set to true if the non-enumerable properties
* should be cloned as well. Non-enumerable properties on the prototype
* chain will be ignored. (optional - false by default)
*/
function clone(parent, circular, depth, prototype, includeNonEnumerable) {
if (typeof circular === 'object') {
depth = circular.depth;
prototype = circular.prototype;
includeNonEnumerable = circular.includeNonEnumerable;
circular = circular.circular;
}
// maintain two arrays for circular references, where corresponding parents
// and children have the same index
var allParents = [];
var allChildren = [];
var useBuffer = typeof Buffer != 'undefined';
if (typeof circular == 'undefined')
circular = true;
if (typeof depth == 'undefined')
depth = Infinity;
// recurse this function so we don't reset allParents and allChildren
function _clone(parent, depth) {
// cloning null always returns null
if (parent === null)
return null;
if (depth === 0)
return parent;
var child;
var proto;
if (typeof parent != 'object') {
return parent;
}
if (_instanceof(parent, nativeMap)) {
child = new nativeMap();
} else if (_instanceof(parent, nativeSet)) {
child = new nativeSet();
} else if (_instanceof(parent, nativePromise)) {
child = new nativePromise(function (resolve, reject) {
parent.then(function(value) {
resolve(_clone(value, depth - 1));
}, function(err) {
reject(_clone(err, depth - 1));
});
});
} else if (clone.__isArray(parent)) {
child = [];
} else if (clone.__isRegExp(parent)) {
child = new RegExp(parent.source, __getRegExpFlags(parent));
if (parent.lastIndex) child.lastIndex = parent.lastIndex;
} else if (clone.__isDate(parent)) {
child = new Date(parent.getTime());
} else if (useBuffer && Buffer.isBuffer(parent)) {
if (Buffer.from) {
// Node.js >= 5.10.0
child = Buffer.from(parent);
} else {
// Older Node.js versions
child = new Buffer(parent.length);
parent.copy(child);
}
return child;
} else if (_instanceof(parent, Error)) {
child = Object.create(parent);
} else {
if (typeof prototype == 'undefined') {
proto = Object.getPrototypeOf(parent);
child = Object.create(proto);
}
else {
child = Object.create(prototype);
proto = prototype;
}
}
if (circular) {
var index = allParents.indexOf(parent);
if (index != -1) {
return allChildren[index];
}
allParents.push(parent);
allChildren.push(child);
}
if (_instanceof(parent, nativeMap)) {
parent.forEach(function(value, key) {
var keyChild = _clone(key, depth - 1);
var valueChild = _clone(value, depth - 1);
child.set(keyChild, valueChild);
});
}
if (_instanceof(parent, nativeSet)) {
parent.forEach(function(value) {
var entryChild = _clone(value, depth - 1);
child.add(entryChild);
});
}
for (var i in parent) {
var attrs = Object.getOwnPropertyDescriptor(parent, i);
if (attrs) {
child[i] = _clone(parent[i], depth - 1);
}
try {
var objProperty = Object.getOwnPropertyDescriptor(parent, i);
if (objProperty.set === 'undefined') {
// no setter defined. Skip cloning this property
continue;
}
child[i] = _clone(parent[i], depth - 1);
} catch(e){
if (e instanceof TypeError) {
// when in strict mode, TypeError will be thrown if child[i] property only has a getter
// we can't do anything about this, other than inform the user that this property cannot be set.
continue
} else if (e instanceof ReferenceError) {
//this may happen in non strict mode
continue
}
}
}
if (Object.getOwnPropertySymbols) {
var symbols = Object.getOwnPropertySymbols(parent);
for (var i = 0; i < symbols.length; i++) {
// Don't need to worry about cloning a symbol because it is a primitive,
// like a number or string.
var symbol = symbols[i];
var descriptor = Object.getOwnPropertyDescriptor(parent, symbol);
if (descriptor && !descriptor.enumerable && !includeNonEnumerable) {
continue;
}
child[symbol] = _clone(parent[symbol], depth - 1);
Object.defineProperty(child, symbol, descriptor);
}
}
if (includeNonEnumerable) {
var allPropertyNames = Object.getOwnPropertyNames(parent);
for (var i = 0; i < allPropertyNames.length; i++) {
var propertyName = allPropertyNames[i];
var descriptor = Object.getOwnPropertyDescriptor(parent, propertyName);
if (descriptor && descriptor.enumerable) {
continue;
}
child[propertyName] = _clone(parent[propertyName], depth - 1);
Object.defineProperty(child, propertyName, descriptor);
}
}
return child;
}
return _clone(parent, depth);
}
/**
* Simple flat clone using prototype, accepts only objects, usefull for property
* override on FLAT configuration object (no nested props).
*
* USE WITH CAUTION! This may not behave as you wish if you do not know how this
* works.
*/
clone.clonePrototype = function clonePrototype(parent) {
if (parent === null)
return null;
var c = function () {};
c.prototype = parent;
return new c();
};
// private utility functions
function __objToStr(o) {
return Object.prototype.toString.call(o);
}
clone.__objToStr = __objToStr;
function __isDate(o) {
return typeof o === 'object' && __objToStr(o) === '[object Date]';
}
clone.__isDate = __isDate;
function __isArray(o) {
return typeof o === 'object' && __objToStr(o) === '[object Array]';
}
clone.__isArray = __isArray;
function __isRegExp(o) {
return typeof o === 'object' && __objToStr(o) === '[object RegExp]';
}
clone.__isRegExp = __isRegExp;
function __getRegExpFlags(re) {
var flags = '';
if (re.global) flags += 'g';
if (re.ignoreCase) flags += 'i';
if (re.multiline) flags += 'm';
return flags;
}
clone.__getRegExpFlags = __getRegExpFlags;
return clone;
})();
export default clone