parent
a30bb3da6a
commit
408720618c
1095 changed files with 128370 additions and 0 deletions
@ -0,0 +1,11 @@ |
|||||||
|
{ // launch.json 配置了启动调试时相关设置,configurations下节点名称可为 app-plus/h5/mp-weixin/mp-baidu/mp-alipay/mp-qq/mp-toutiao/mp-360/ |
||||||
|
// launchtype项可配置值为local或remote, local代表前端连本地云函数,remote代表前端连云端云函数 |
||||||
|
"version": "0.0", |
||||||
|
"configurations": [{ |
||||||
|
"type": "uniCloud", |
||||||
|
"default": { |
||||||
|
"launchtype": "remote" |
||||||
|
} |
||||||
|
} |
||||||
|
] |
||||||
|
} |
@ -0,0 +1,21 @@ |
|||||||
|
<script> |
||||||
|
export default { |
||||||
|
onLaunch: function() { |
||||||
|
console.log('App Launch') |
||||||
|
}, |
||||||
|
onShow: function() { |
||||||
|
// console.log('App Show') |
||||||
|
}, |
||||||
|
onHide: function() { |
||||||
|
// console.log('App Hide') |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style> |
||||||
|
@import "colorui/main.css"; |
||||||
|
@import "colorui/icon.css"; |
||||||
|
|
||||||
|
/*每个页面公共css */ |
||||||
|
@import "static/css/main.css"; |
||||||
|
</style> |
@ -0,0 +1,184 @@ |
|||||||
|
/* |
||||||
|
Animation 微动画 |
||||||
|
基于ColorUI组建库的动画模块 by 文晓港 2019年3月26日19:52:28 |
||||||
|
*/ |
||||||
|
|
||||||
|
/* css 滤镜 控制黑白底色gif的 */ |
||||||
|
.gif-black{ |
||||||
|
mix-blend-mode: screen; |
||||||
|
} |
||||||
|
.gif-white{ |
||||||
|
mix-blend-mode: multiply; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/* Animation css */ |
||||||
|
[class*=animation-] { |
||||||
|
animation-duration: .5s; |
||||||
|
animation-timing-function: ease-out; |
||||||
|
animation-fill-mode: both |
||||||
|
} |
||||||
|
|
||||||
|
.animation-fade { |
||||||
|
animation-name: fade; |
||||||
|
animation-duration: .8s; |
||||||
|
animation-timing-function: linear |
||||||
|
} |
||||||
|
|
||||||
|
.animation-scale-up { |
||||||
|
animation-name: scale-up |
||||||
|
} |
||||||
|
|
||||||
|
.animation-scale-down { |
||||||
|
animation-name: scale-down |
||||||
|
} |
||||||
|
|
||||||
|
.animation-slide-top { |
||||||
|
animation-name: slide-top |
||||||
|
} |
||||||
|
|
||||||
|
.animation-slide-bottom { |
||||||
|
animation-name: slide-bottom |
||||||
|
} |
||||||
|
|
||||||
|
.animation-slide-left { |
||||||
|
animation-name: slide-left |
||||||
|
} |
||||||
|
|
||||||
|
.animation-slide-right { |
||||||
|
animation-name: slide-right |
||||||
|
} |
||||||
|
|
||||||
|
.animation-shake { |
||||||
|
animation-name: shake |
||||||
|
} |
||||||
|
|
||||||
|
.animation-reverse { |
||||||
|
animation-direction: reverse |
||||||
|
} |
||||||
|
|
||||||
|
@keyframes fade { |
||||||
|
0% { |
||||||
|
opacity: 0 |
||||||
|
} |
||||||
|
|
||||||
|
100% { |
||||||
|
opacity: 1 |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@keyframes scale-up { |
||||||
|
0% { |
||||||
|
opacity: 0; |
||||||
|
transform: scale(.2) |
||||||
|
} |
||||||
|
|
||||||
|
100% { |
||||||
|
opacity: 1; |
||||||
|
transform: scale(1) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@keyframes scale-down { |
||||||
|
0% { |
||||||
|
opacity: 0; |
||||||
|
transform: scale(1.8) |
||||||
|
} |
||||||
|
|
||||||
|
100% { |
||||||
|
opacity: 1; |
||||||
|
transform: scale(1) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@keyframes slide-top { |
||||||
|
0% { |
||||||
|
opacity: 0; |
||||||
|
transform: translateY(-100%) |
||||||
|
} |
||||||
|
|
||||||
|
100% { |
||||||
|
opacity: 1; |
||||||
|
transform: translateY(0) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@keyframes slide-bottom { |
||||||
|
0% { |
||||||
|
opacity: 0; |
||||||
|
transform: translateY(100%) |
||||||
|
} |
||||||
|
|
||||||
|
100% { |
||||||
|
opacity: 1; |
||||||
|
transform: translateY(0) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@keyframes shake { |
||||||
|
|
||||||
|
0%, |
||||||
|
100% { |
||||||
|
transform: translateX(0) |
||||||
|
} |
||||||
|
|
||||||
|
10% { |
||||||
|
transform: translateX(-9px) |
||||||
|
} |
||||||
|
|
||||||
|
20% { |
||||||
|
transform: translateX(8px) |
||||||
|
} |
||||||
|
|
||||||
|
30% { |
||||||
|
transform: translateX(-7px) |
||||||
|
} |
||||||
|
|
||||||
|
40% { |
||||||
|
transform: translateX(6px) |
||||||
|
} |
||||||
|
|
||||||
|
50% { |
||||||
|
transform: translateX(-5px) |
||||||
|
} |
||||||
|
|
||||||
|
60% { |
||||||
|
transform: translateX(4px) |
||||||
|
} |
||||||
|
|
||||||
|
70% { |
||||||
|
transform: translateX(-3px) |
||||||
|
} |
||||||
|
|
||||||
|
80% { |
||||||
|
transform: translateX(2px) |
||||||
|
} |
||||||
|
|
||||||
|
90% { |
||||||
|
transform: translateX(-1px) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@keyframes slide-left { |
||||||
|
0% { |
||||||
|
opacity: 0; |
||||||
|
transform: translateX(-100%) |
||||||
|
} |
||||||
|
|
||||||
|
100% { |
||||||
|
opacity: 1; |
||||||
|
transform: translateX(0) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@keyframes slide-right { |
||||||
|
0% { |
||||||
|
opacity: 0; |
||||||
|
transform: translateX(100%) |
||||||
|
} |
||||||
|
|
||||||
|
100% { |
||||||
|
opacity: 1; |
||||||
|
transform: translateX(0) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,69 @@ |
|||||||
|
<template> |
||||||
|
<view> |
||||||
|
<view class="cu-custom" :style="[{height:CustomBar + 'px'}]"> |
||||||
|
<view class="cu-bar fixed" :style="style" :class="[bgImage!=''?'none-bg text-white bg-img':'',bgColor]"> |
||||||
|
<view class="action" @tap="BackPage" v-if="isBack"> |
||||||
|
<text class="cuIcon-back"></text> |
||||||
|
<slot name="backText"></slot> |
||||||
|
</view> |
||||||
|
<view class="content" :style="[{top:StatusBar + 'px'}]"> |
||||||
|
<slot name="content"></slot> |
||||||
|
</view> |
||||||
|
<slot name="right"></slot> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
data() { |
||||||
|
return { |
||||||
|
StatusBar: this.StatusBar, |
||||||
|
CustomBar: this.CustomBar |
||||||
|
}; |
||||||
|
}, |
||||||
|
name: 'cu-custom', |
||||||
|
computed: { |
||||||
|
style() { |
||||||
|
var StatusBar= this.StatusBar; |
||||||
|
var CustomBar= this.CustomBar; |
||||||
|
var bgImage = this.bgImage; |
||||||
|
var style = `height:${CustomBar}px;padding-top:${StatusBar}px;`; |
||||||
|
if (this.bgImage) { |
||||||
|
style = `${style}background-image:url(${bgImage});`; |
||||||
|
} |
||||||
|
return style |
||||||
|
} |
||||||
|
}, |
||||||
|
props: { |
||||||
|
bgColor: { |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
isBack: { |
||||||
|
type: [Boolean, String], |
||||||
|
default: false |
||||||
|
}, |
||||||
|
bgImage: { |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
BackPage() { |
||||||
|
if (getCurrentPages().length < 2 && 'undefined' !== typeof __wxConfig) { |
||||||
|
let url = '/' + __wxConfig.pages[0] |
||||||
|
return uni.redirectTo({url}) |
||||||
|
} |
||||||
|
uni.navigateBack({ |
||||||
|
delta: 1 |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style> |
||||||
|
|
||||||
|
</style> |
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,354 @@ |
|||||||
|
<template> |
||||||
|
<view class="uni-combox"> |
||||||
|
<view v-if="label" class="uni-combox__label" :style="labelStyle"> |
||||||
|
<text>{{label}}</text> |
||||||
|
</view> |
||||||
|
<view class="uni-combox__input-box"> |
||||||
|
<input class="uni-combox__input" type="text" :placeholder="placeholder" :disabled="isDisabled" v-model="inputVal" |
||||||
|
@input="onInput" @focus="onFocus" @blur="onBlur" /> |
||||||
|
<icon type="clear" size="16" @tap="clearInputValue" v-if="!isDisabled" /> |
||||||
|
<image class="uni-combox__input-arrow" src="../../static/cuihai-combox/arrow_down.png" style="width: 30rpx;height: 30rpx;" @click="toggleSelector"></image> |
||||||
|
<!-- <uni-icons class="uni-combox__input-arrow" type="arrowdown" size="14" @click="toggleSelector"></uni-icons> --> |
||||||
|
<view class="uni-combox__selector" v-if="showSelector"> |
||||||
|
<scroll-view scroll-y="true" class="uni-combox__selector-scroll"> |
||||||
|
<view class="uni-combox__selector-empty" v-if="filterCandidatesLength === 0"> |
||||||
|
<text>{{emptyTips}}</text> |
||||||
|
</view> |
||||||
|
<view class="uni-combox__selector-item" v-for="(item,index) in filterCandidates" :key="index" @click="onSelectorClick(index)" |
||||||
|
:style="isCheck(index)?'color:'+selectColor+';background-color:'+selectBgColor+';':'color:'+color+';'"> |
||||||
|
<text>{{isJSON?item[keyName]:item}}</text> |
||||||
|
</view> |
||||||
|
</scroll-view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
/** |
||||||
|
* Combox 组合输入框 |
||||||
|
* @description 组合输入框一般用于既可以输入也可以选择的场景 |
||||||
|
* @property {String} label 左侧文字 |
||||||
|
* @property {String} labelWidth 左侧内容宽度 |
||||||
|
* @property {String} placeholder 输入框占位符 |
||||||
|
* @property {Array} candidates 候选项列表 |
||||||
|
* @property {String} emptyTips 筛选结果为空时显示的文字 |
||||||
|
* @property {String} value 单选时组合框的初始值 |
||||||
|
* @property {Array} initValue 多选时组合框的初始值(下标集合) |
||||||
|
* @property {String} keyName json数组显示的字段值 |
||||||
|
* @property {Boolean} isJSON 是否是json数组,默认不是 |
||||||
|
* @property {Boolean} isDisabled 是否是禁用输入,默认不禁用 |
||||||
|
* @property {Boolean} isCheckBox 是否是多选,默认不是,多选时不能输入查询 |
||||||
|
* @property {String} color 默认字体颜色,默认#000000 |
||||||
|
* @property {String} selectColor 选中字体颜色,默认#0081ff |
||||||
|
* @property {String} selectBgColor 选中背景颜色,默认#e8e8e8 |
||||||
|
*/ |
||||||
|
export default { |
||||||
|
name: 'comboxSearch', |
||||||
|
props: { |
||||||
|
label: { |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
labelWidth: { |
||||||
|
type: String, |
||||||
|
default: 'auto' |
||||||
|
}, |
||||||
|
placeholder: { |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
candidates: { |
||||||
|
type: Array, |
||||||
|
default () { |
||||||
|
return [] |
||||||
|
} |
||||||
|
}, |
||||||
|
emptyTips: { |
||||||
|
type: String, |
||||||
|
default: '无匹配项' |
||||||
|
}, |
||||||
|
value: { |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
initValue: { |
||||||
|
type: Array, |
||||||
|
default: null |
||||||
|
}, |
||||||
|
keyName: { |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
isJSON: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
isDisabled: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
isCheckBox: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
color: { |
||||||
|
default: "#000000", |
||||||
|
type: String |
||||||
|
}, |
||||||
|
selectColor: { |
||||||
|
default: "#0081ff", |
||||||
|
type: String |
||||||
|
}, |
||||||
|
selectBgColor: { |
||||||
|
default: "#e8e8e8", |
||||||
|
type: String |
||||||
|
} |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
showSelector: false, |
||||||
|
inputVal: '', |
||||||
|
arrays: [], |
||||||
|
gid: `sm-org-dropDown-${(new Date()).getTime()}${Math.random()}` |
||||||
|
} |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
labelStyle() { |
||||||
|
if (this.labelWidth === 'auto') { |
||||||
|
return {} |
||||||
|
} |
||||||
|
return { |
||||||
|
width: this.labelWidth |
||||||
|
} |
||||||
|
}, |
||||||
|
filterCandidates() { |
||||||
|
if (!this.isDisabled) { |
||||||
|
if (this.isJSON) { |
||||||
|
let index = 0; |
||||||
|
return this.candidates.filter((item) => { |
||||||
|
item.index = index; |
||||||
|
index++; |
||||||
|
return item[this.keyName].indexOf(this.inputVal) > -1 |
||||||
|
}) |
||||||
|
} else { |
||||||
|
return this.candidates.filter((item) => { |
||||||
|
return item.indexOf(this.inputVal) > -1 |
||||||
|
}) |
||||||
|
} |
||||||
|
} else { |
||||||
|
if (this.isJSON) { |
||||||
|
let index = 0; |
||||||
|
return this.candidates.filter((item) => { |
||||||
|
item.index = index; |
||||||
|
index++; |
||||||
|
return item[this.keyName] != undefined; |
||||||
|
}) |
||||||
|
} else { |
||||||
|
return this.candidates |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
filterCandidatesLength() { |
||||||
|
return this.filterCandidates.length |
||||||
|
} |
||||||
|
}, |
||||||
|
created() { |
||||||
|
if (this.initValue != null) { |
||||||
|
this.arrays = this.initValue; |
||||||
|
this.inputVal = this.getInpuevals(); |
||||||
|
} |
||||||
|
//在created的时候,给子组件放置一个监听,这个时候只是监听器被建立,此时这段代码不会生效 |
||||||
|
//uni.$on接收一个sm-org-dropDown-show的广播 |
||||||
|
uni.$on('sm-org-dropDown-show', (targetId) => { |
||||||
|
//接收广播,当该组件处于下拉框被打开的状态,并且跟下一个被点击的下拉框的gid不同时,将该组件的下拉框关闭,产生一个互斥效果。 |
||||||
|
if (this.showSelector && this.gid != targetId) { |
||||||
|
this.showSelector = false |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
//当子组件销毁的时候,将建立的监听销毁,这里不会影响代码效果,但是在多次反复引用组件时,会在根节点定义越来越多的监听,长此以往影响性能,所以在不用的时候及时销毁,养成好习惯 |
||||||
|
beforeDestroy() { |
||||||
|
uni.$off('sm-org-dropDown-show') |
||||||
|
}, |
||||||
|
watch: { |
||||||
|
value: { |
||||||
|
handler(newVal) { |
||||||
|
this.inputVal = newVal |
||||||
|
}, |
||||||
|
immediate: true |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
toggleSelector() { |
||||||
|
this.showSelector = !this.showSelector |
||||||
|
if (this.showSelector) { |
||||||
|
uni.$emit('sm-org-dropDown-show', this.gid) |
||||||
|
} |
||||||
|
}, |
||||||
|
onFocus() { |
||||||
|
this.showSelector = true; |
||||||
|
uni.$emit('sm-org-dropDown-show', this.gid) |
||||||
|
}, |
||||||
|
onBlur() { |
||||||
|
/* setTimeout(() => { |
||||||
|
this.showSelector = false; |
||||||
|
}, 50) */ |
||||||
|
}, |
||||||
|
onSelectorClick(index) { |
||||||
|
if (this.isCheckBox) { |
||||||
|
let flag = this.arrays.indexOf(index); |
||||||
|
if (flag != -1) { |
||||||
|
let x = (this.arrays || []).findIndex((item) => item === index) |
||||||
|
this.arrays.splice(x, 1); |
||||||
|
} else { |
||||||
|
this.arrays.push(index); |
||||||
|
} |
||||||
|
this.inputVal = this.getInpuevals(); |
||||||
|
if (this.isJSON) { |
||||||
|
this.$emit('getValue', this.arrays); |
||||||
|
} else { |
||||||
|
this.$emit('getValue', this.trimSpace(this.inputVal.split(" "))); |
||||||
|
} |
||||||
|
} else { |
||||||
|
this.showSelector = false |
||||||
|
if (this.isJSON) { |
||||||
|
this.$emit('getValue', this.filterCandidates[index].index); |
||||||
|
this.inputVal = this.filterCandidates[index][this.keyName]; |
||||||
|
} else { |
||||||
|
this.$emit('getValue', this.filterCandidates[index]); |
||||||
|
this.inputVal = this.filterCandidates[index] |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
trimSpace(array) { |
||||||
|
for (var i = 0; i < array.length; i++) { |
||||||
|
if (array[i] == "") { |
||||||
|
array.splice(i, 1); |
||||||
|
i = i - 1; |
||||||
|
} |
||||||
|
} |
||||||
|
return array; |
||||||
|
}, |
||||||
|
onInput() { |
||||||
|
setTimeout(() => { |
||||||
|
this.$emit('input', this.inputVal) |
||||||
|
}) |
||||||
|
}, |
||||||
|
clearInputValue() { |
||||||
|
this.inputVal = ""; |
||||||
|
this.showSelector = false; |
||||||
|
}, |
||||||
|
getInpuevals() { |
||||||
|
if (this.arrays.length == 0) { |
||||||
|
this.inputVal = ""; |
||||||
|
} else { |
||||||
|
this.arrays.sort(function(a, b) { |
||||||
|
return a - b |
||||||
|
}) |
||||||
|
this.inputVal = ""; |
||||||
|
if (this.isJSON) { |
||||||
|
this.arrays.forEach(v => { |
||||||
|
this.inputVal += this.candidates[v][this.keyName] + " "; |
||||||
|
}); |
||||||
|
} else { |
||||||
|
this.arrays.forEach(v => { |
||||||
|
this.inputVal += this.candidates[v] + " "; |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
return this.inputVal; |
||||||
|
}, |
||||||
|
isCheck(index) { |
||||||
|
return this.arrays.indexOf(index) != -1; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style scoped> |
||||||
|
.uni-combox { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
height: 40px; |
||||||
|
flex-direction: row; |
||||||
|
align-items: center; |
||||||
|
/* border-bottom: solid 1px #DDDDDD; */ |
||||||
|
} |
||||||
|
|
||||||
|
.uni-combox__label { |
||||||
|
font-size: 16px; |
||||||
|
line-height: 22px; |
||||||
|
padding-right: 10px; |
||||||
|
color: #999999; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-combox__input-box { |
||||||
|
position: relative; |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
flex: 1; |
||||||
|
flex-direction: row; |
||||||
|
align-items: center; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-combox__input { |
||||||
|
flex: 1; |
||||||
|
font-size: 16px; |
||||||
|
height: 22px; |
||||||
|
line-height: 22px; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-combox__input-arrow { |
||||||
|
padding: 10px; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-combox__selector { |
||||||
|
box-sizing: border-box; |
||||||
|
position: absolute; |
||||||
|
top: 42px; |
||||||
|
left: 0; |
||||||
|
width: 100%; |
||||||
|
background-color: #FFFFFF; |
||||||
|
border-radius: 6px; |
||||||
|
box-shadow: #DDDDDD 4px 4px 8px, #DDDDDD -4px -4px 8px; |
||||||
|
z-index: 2; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-combox__selector-scroll { |
||||||
|
max-height: 200px; |
||||||
|
box-sizing: border-box; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-combox__selector::before { |
||||||
|
content: ''; |
||||||
|
position: absolute; |
||||||
|
width: 0; |
||||||
|
height: 0; |
||||||
|
border-bottom: solid 6px #FFFFFF; |
||||||
|
border-right: solid 6px transparent; |
||||||
|
border-left: solid 6px transparent; |
||||||
|
left: 50%; |
||||||
|
top: -6px; |
||||||
|
margin-left: -6px; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-combox__selector-empty, |
||||||
|
.uni-combox__selector-item { |
||||||
|
/* #ifdef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
line-height: 36px; |
||||||
|
font-size: 14px; |
||||||
|
text-align: center; |
||||||
|
border-bottom: solid 1px #DDDDDD; |
||||||
|
/* margin: 0px 10px; */ |
||||||
|
} |
||||||
|
|
||||||
|
.uni-combox__selector-empty:last-child, |
||||||
|
.uni-combox__selector-item:last-child { |
||||||
|
border-bottom: none; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,87 @@ |
|||||||
|
<!-- 商品列表组件 <good-list :list="xx"></good-list> --> |
||||||
|
<template> |
||||||
|
<view class="good-list"> |
||||||
|
<view class="charge" v-for="(item,index) in list" :key="index" :id="'index-' + item.name" :data-index="item.name"> |
||||||
|
<view class="charge-title flex-between"> |
||||||
|
<text>{{item.name}}</text> |
||||||
|
<text class="charge-status">{{item.status}}</text> |
||||||
|
</view> |
||||||
|
<view class="charge-text"> |
||||||
|
<text>{{item.IDNumber}}</text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
props:{ |
||||||
|
list: { |
||||||
|
type: Array, |
||||||
|
default(){ |
||||||
|
return [] |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.good-list{ |
||||||
|
background-color: #fff; |
||||||
|
|
||||||
|
.good-li{ |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
padding: 20upx; |
||||||
|
border-bottom: 1upx solid #eee; |
||||||
|
|
||||||
|
.good-img{ |
||||||
|
width: 160upx; |
||||||
|
height: 160upx; |
||||||
|
margin-right: 20rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.flex-item{ |
||||||
|
flex: 1; |
||||||
|
|
||||||
|
.good-name{ |
||||||
|
font-size: 26upx; |
||||||
|
line-height: 40upx; |
||||||
|
height: 80upx; |
||||||
|
margin-bottom: 20upx; |
||||||
|
overflow: hidden; |
||||||
|
} |
||||||
|
.good-price{ |
||||||
|
font-size: 26upx; |
||||||
|
color: red; |
||||||
|
} |
||||||
|
.good-sold{ |
||||||
|
font-size: 24upx; |
||||||
|
margin-left: 16upx; |
||||||
|
color: gray; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
.charge{ |
||||||
|
margin: 0 50rpx; |
||||||
|
border-radius: 20rpx; |
||||||
|
padding: 0 30rpx; |
||||||
|
box-shadow: 0 0 16rpx #ccc; |
||||||
|
margin-top: 40rpx; |
||||||
|
.charge-title{ |
||||||
|
padding: 16rpx 0; |
||||||
|
border-bottom: 4rpx solid #F2F2F2; |
||||||
|
color: #000; |
||||||
|
.charge-status{ |
||||||
|
color: #ccc; |
||||||
|
} |
||||||
|
} |
||||||
|
.charge-text{ |
||||||
|
padding: 16rpx 0; |
||||||
|
color: #707070; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,166 @@ |
|||||||
|
<template> |
||||||
|
<view> |
||||||
|
<slot name="formItem" v-if="$slots.formItem"></slot> |
||||||
|
<view v-else class="evan-form-item-container" :class="'evan-form-item-container--'+mLabelPosition" :style="{borderWidth:border?'1rpx':0}"> |
||||||
|
<view v-if="label" class="evan-form-item-container__label" :class="{showAsteriskRect:hasRequiredAsterisk,isRequired:showRequiredAsterisk}" |
||||||
|
:style="mLabelStyle">{{label}}</view> |
||||||
|
<view class="evan-form-item-container__main" :style="mContentStyle"> |
||||||
|
<slot></slot> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
name: 'EvanFormItem', |
||||||
|
props: { |
||||||
|
labelStyle: Object, |
||||||
|
label: String, |
||||||
|
contentStyle: { |
||||||
|
type: Object, |
||||||
|
default: () => { |
||||||
|
return {} |
||||||
|
} |
||||||
|
}, |
||||||
|
prop: String, |
||||||
|
border: { |
||||||
|
type: Boolean, |
||||||
|
default: true |
||||||
|
}, |
||||||
|
labelPosition: { |
||||||
|
validator: function(value) { |
||||||
|
if (!value) { |
||||||
|
return true |
||||||
|
} |
||||||
|
return ['top', 'left'].indexOf(value) !== -1 |
||||||
|
}, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
required: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
message: { |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
rules: { |
||||||
|
type: [Object, Array], |
||||||
|
default: null |
||||||
|
} |
||||||
|
}, |
||||||
|
inject: ['evanForm'], |
||||||
|
computed: { |
||||||
|
mLabelStyle() { |
||||||
|
const parent = this.getParent() |
||||||
|
let labelStyle = Object.assign({}, (parent.labelStyle || {}), (this.labelStyle || {})) |
||||||
|
let arr = Object.keys(labelStyle).map((key) => `${key}:${labelStyle[key]}`) |
||||||
|
return arr.join(';') |
||||||
|
}, |
||||||
|
mContentStyle() { |
||||||
|
let contentStyle = Object.assign({}, this.contentStyle || {}) |
||||||
|
let arr = Object.keys(contentStyle).map((key) => `${key}:${contentStyle[key]}`) |
||||||
|
return arr.join(';') |
||||||
|
}, |
||||||
|
mLabelPosition() { |
||||||
|
if (this.labelPosition) { |
||||||
|
return this.labelPosition |
||||||
|
} |
||||||
|
const parent = this.getParent() |
||||||
|
if (parent) { |
||||||
|
return parent.labelPosition |
||||||
|
} |
||||||
|
return 'left' |
||||||
|
}, |
||||||
|
// 整个表单是否有*号 |
||||||
|
hasRequiredAsterisk() { |
||||||
|
const parent = this.getParent() |
||||||
|
if (parent) { |
||||||
|
return parent.hasRequiredAsterisk |
||||||
|
} |
||||||
|
return false |
||||||
|
}, |
||||||
|
// 当前formItem是否显示*号 |
||||||
|
showRequiredAsterisk() { |
||||||
|
const parent = this.getParent() |
||||||
|
if (parent && parent.hideRequiredAsterisk) { |
||||||
|
return false |
||||||
|
} |
||||||
|
const rules = this.getRules() |
||||||
|
if (rules && rules.length > 0) { |
||||||
|
if (rules.find((rule) => rule.required === true)) { |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
return this.required |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
// 获取EvanForm组件 |
||||||
|
getParent() { |
||||||
|
return this.evanForm |
||||||
|
}, |
||||||
|
getRules() { |
||||||
|
let form = this.getParent() |
||||||
|
const formRules = form.mRules && form.mRules[this.prop] ? form.mRules[this.prop] : []; |
||||||
|
const selfRules = this.rules |
||||||
|
const requiredRules = this.required ? { |
||||||
|
required: true, |
||||||
|
message: this.message || `${this.label}必填` |
||||||
|
} : [] |
||||||
|
return [].concat(selfRules || formRules || []).concat(requiredRules) |
||||||
|
} |
||||||
|
}, |
||||||
|
created() { |
||||||
|
this.evanForm.$emit('evan.form.addField', this) |
||||||
|
}, |
||||||
|
beforeDestroy() { |
||||||
|
this.evanForm.$emit('evan.form.removeField', this) |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss"> |
||||||
|
.evan-form-item-container { |
||||||
|
|
||||||
|
&__label { |
||||||
|
font-size: 28rpx; |
||||||
|
color: #666; |
||||||
|
line-height: 40rpx; |
||||||
|
padding: 25rpx 0; |
||||||
|
display: inline-block; |
||||||
|
|
||||||
|
&.showAsteriskRect::before { |
||||||
|
content: ''; |
||||||
|
color: #F56C6C; |
||||||
|
width: 20rpx; |
||||||
|
display: inline-block; |
||||||
|
} |
||||||
|
|
||||||
|
&.isRequired::before { |
||||||
|
content: '*'; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
&__main { |
||||||
|
flex: 1; |
||||||
|
min-height: 90rpx; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
overflow: hidden; |
||||||
|
} |
||||||
|
|
||||||
|
&--left { |
||||||
|
display: flex; |
||||||
|
flex-direction: row; |
||||||
|
align-items: flex-start; |
||||||
|
} |
||||||
|
|
||||||
|
&--top { |
||||||
|
.evan-form-item-container__label { |
||||||
|
padding-bottom: 10rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,168 @@ |
|||||||
|
<template> |
||||||
|
<view class="evan-form-container"> |
||||||
|
<slot></slot> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import utils from './utils.js' |
||||||
|
export default { |
||||||
|
name: 'EvanForm', |
||||||
|
props: { |
||||||
|
labelStyle: { |
||||||
|
type: Object, |
||||||
|
default: () => { |
||||||
|
return {} |
||||||
|
} |
||||||
|
}, |
||||||
|
model: Object, |
||||||
|
hideRequiredAsterisk: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
showMessage: { |
||||||
|
type: Boolean, |
||||||
|
default: true |
||||||
|
}, |
||||||
|
labelPosition: { |
||||||
|
validator: function(value) { |
||||||
|
return ['top', 'left'].indexOf(value) !== -1 |
||||||
|
}, |
||||||
|
default: 'left' |
||||||
|
}, |
||||||
|
rules: { |
||||||
|
type: Object, |
||||||
|
default: () => { |
||||||
|
return {} |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
provide() { |
||||||
|
return { |
||||||
|
evanForm: this |
||||||
|
} |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
// 整个form是否有*号,为了保证label对齐,而不是和*号对齐 |
||||||
|
hasRequiredAsterisk() { |
||||||
|
if (this.hideRequiredAsterisk) { |
||||||
|
return false |
||||||
|
} |
||||||
|
if (this.mRules) { |
||||||
|
const values = Object.values(this.mRules) |
||||||
|
if (values && values.length > 0) { |
||||||
|
for (let i = 0; i < values.length; i++) { |
||||||
|
const value = values[i] |
||||||
|
if (Array.isArray(value) && value.length > 0) { |
||||||
|
const requiredItem = value.find((v) => v.required === true) |
||||||
|
if (requiredItem) { |
||||||
|
return true |
||||||
|
} |
||||||
|
} else { |
||||||
|
if (value && value.required) { |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return this.childHasRequired |
||||||
|
} |
||||||
|
}, |
||||||
|
watch: { |
||||||
|
rules: { |
||||||
|
immediate: true, |
||||||
|
deep: true, |
||||||
|
handler(value) { |
||||||
|
this.mRules = value || {} |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
mRules: {}, |
||||||
|
fields: [], |
||||||
|
childHasRequired: false |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
setRules(rules) { |
||||||
|
this.mRules = rules || {} |
||||||
|
}, |
||||||
|
async validate(callback) { |
||||||
|
const rules = this.getRules() |
||||||
|
if (typeof callback === 'function') { |
||||||
|
utils.validate(this.model, rules, callback, { |
||||||
|
showMessage: this.showMessage |
||||||
|
}) |
||||||
|
} else { |
||||||
|
return await utils.validate(this.model, rules, callback, { |
||||||
|
showMessage: this.showMessage |
||||||
|
}) |
||||||
|
} |
||||||
|
}, |
||||||
|
async validateField(props, callback) { |
||||||
|
const rules = this.getRules() |
||||||
|
if (typeof callback === 'function') { |
||||||
|
utils.validateField(this.model, rules, props, callback, { |
||||||
|
showMessage: this.showMessage |
||||||
|
}) |
||||||
|
} else { |
||||||
|
return await utils.validateField(this.model, rules, props, callback, { |
||||||
|
showMessage: this.showMessage |
||||||
|
}) |
||||||
|
} |
||||||
|
}, |
||||||
|
getRules() { |
||||||
|
const rules = {} |
||||||
|
this.fields.forEach((field) => { |
||||||
|
if (field.prop) { |
||||||
|
const requiredRules = field.required ? { |
||||||
|
required: true, |
||||||
|
message: field.message || `${field.label}必填` |
||||||
|
} : [] |
||||||
|
const formRules = this.mRules && this.mRules[field.prop] ? this.mRules[field.prop] : [] |
||||||
|
rules[field.prop] = [].concat(field.rules || formRules || []).concat(requiredRules) |
||||||
|
} |
||||||
|
}) |
||||||
|
return rules |
||||||
|
} |
||||||
|
}, |
||||||
|
created() { |
||||||
|
this.$on('evan.form.addField', (field) => { |
||||||
|
// 小程序中直接push field报错 |
||||||
|
if (field.prop) { |
||||||
|
this.fields.push({ |
||||||
|
rules: field.rules, |
||||||
|
prop: field.prop, |
||||||
|
required: field.required, |
||||||
|
label: field.label, |
||||||
|
message: field.message, |
||||||
|
_uid: field._uid |
||||||
|
}) |
||||||
|
if (!this.childHasRequired) { |
||||||
|
if (field.required) { |
||||||
|
this.childHasRequired = field.required |
||||||
|
return |
||||||
|
} |
||||||
|
if (field.rules) { |
||||||
|
const fieldRules = [].concat(field.rules) |
||||||
|
fieldRules.forEach((item) => { |
||||||
|
if (item.required) { |
||||||
|
this.childHasRequired = true |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
this.$on('evan.form.removeField', (field) => { |
||||||
|
this.fields.splice(this.fields.findIndex((item) => item._uid === field._uid), 1) |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss"> |
||||||
|
.evan-form-container {} |
||||||
|
</style> |
@ -0,0 +1,148 @@ |
|||||||
|
import AsyncValidator from 'async-validator' |
||||||
|
const utils = { |
||||||
|
validate: (model, rules, callback, options) => { |
||||||
|
const initOptions = { |
||||||
|
showMessage: true |
||||||
|
} |
||||||
|
options = Object.assign({}, initOptions, options || {}) |
||||||
|
let promise = null; |
||||||
|
if (typeof callback !== 'function') { |
||||||
|
promise = new Promise((resolve, reject) => { |
||||||
|
callback = function(valid) { |
||||||
|
valid ? resolve(valid) : reject(valid) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
// 如果需要验证的fields为空,调用验证时立刻返回callback
|
||||||
|
if (!rules || (Array.isArray(rules) && rules.length === 0) || (typeof rules === 'object' && Object.keys(rules).length === |
||||||
|
0)) { |
||||||
|
callback(true, null); |
||||||
|
if(promise){ |
||||||
|
return promise |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
let errors = [] |
||||||
|
const props = Object.keys(rules) |
||||||
|
let count = 0 |
||||||
|
for (let i in props) { |
||||||
|
const prop = props[i] |
||||||
|
const value = utils.getValueByProp(model, prop) |
||||||
|
utils.validateItem(rules, prop, value, (err) => { |
||||||
|
if (err && err.length > 0) { |
||||||
|
errors = errors.concat(err) |
||||||
|
} |
||||||
|
// 处理异步校验,等所有校验都结束时再callback
|
||||||
|
count++ |
||||||
|
if (count === props.length) { |
||||||
|
if (errors.length > 0) { |
||||||
|
if (options.showMessage) { |
||||||
|
utils.showToast(errors[0].message) |
||||||
|
} |
||||||
|
callback(false, errors) |
||||||
|
} else { |
||||||
|
callback(true, null) |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
if (promise) { |
||||||
|
return promise |
||||||
|
} |
||||||
|
}, |
||||||
|
validateField: (model, rules, props, callback, options) => { |
||||||
|
const initOptions = { |
||||||
|
showMessage: true |
||||||
|
} |
||||||
|
options = Object.assign({}, initOptions, options || {}) |
||||||
|
let promise = null; |
||||||
|
if (typeof callback !== 'function') { |
||||||
|
promise = new Promise((resolve, reject) => { |
||||||
|
callback = function(valid) { |
||||||
|
valid ? resolve(valid) : reject(valid) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
props = [].concat(props) |
||||||
|
if (props.length === 0) { |
||||||
|
return |
||||||
|
} |
||||||
|
let errors = [] |
||||||
|
let count = 0 |
||||||
|
for (let i in props) { |
||||||
|
const prop = props[i] |
||||||
|
const value = utils.getValueByProp(model, prop) |
||||||
|
utils.validateItem(rules, prop, value, (err) => { |
||||||
|
if (err && err.length > 0) { |
||||||
|
errors = errors.concat(err) |
||||||
|
} |
||||||
|
// 处理异步校验,等所有校验都结束时再callback
|
||||||
|
count++ |
||||||
|
if (count === props.length) { |
||||||
|
if (errors.length > 0) { |
||||||
|
if (options.showMessage) { |
||||||
|
utils.showToast(errors[0].message) |
||||||
|
} |
||||||
|
callback(false, errors) |
||||||
|
} else { |
||||||
|
callback(true, null) |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
if (promise) { |
||||||
|
return promise |
||||||
|
} |
||||||
|
}, |
||||||
|
validateItem(rules, prop, value, callback) { |
||||||
|
if (!rules || JSON.stringify(rules) === '{}') { |
||||||
|
if (callback instanceof Function) { |
||||||
|
callback(); |
||||||
|
} |
||||||
|
return true; |
||||||
|
} |
||||||
|
const propRules = [].concat(rules[prop] || []); |
||||||
|
propRules.forEach((rule) => { |
||||||
|
if (rule.pattern) { |
||||||
|
rule.pattern = new RegExp(rule.pattern) |
||||||
|
} |
||||||
|
}) |
||||||
|
const descriptor = { |
||||||
|
[prop]: propRules |
||||||
|
}; |
||||||
|
const validator = new AsyncValidator(descriptor); |
||||||
|
const model = { |
||||||
|
[prop]: value |
||||||
|
}; |
||||||
|
validator.validate(model, { |
||||||
|
firstFields: true |
||||||
|
}, (errors) => { |
||||||
|
callback(errors); |
||||||
|
}); |
||||||
|
}, |
||||||
|
getValueByProp: (obj, prop) => { |
||||||
|
let tempObj = obj; |
||||||
|
prop = prop.replace(/\[(\w+)\]/g, '.$1').replace(/^\./, ''); |
||||||
|
let keyArr = prop.split('.'); |
||||||
|
let i = 0; |
||||||
|
for (let len = keyArr.length; i < len - 1; ++i) { |
||||||
|
if (!tempObj) break; |
||||||
|
let key = keyArr[i]; |
||||||
|
if (key in tempObj) { |
||||||
|
tempObj = tempObj[key]; |
||||||
|
} else { |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
return tempObj ? (typeof tempObj[keyArr[i]] === 'string' ? tempObj[keyArr[i]].trim() : tempObj[keyArr[i]]) : |
||||||
|
null |
||||||
|
}, |
||||||
|
showToast: (message) => { |
||||||
|
uni.showToast({ |
||||||
|
title: message, |
||||||
|
icon: 'none' |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export default utils |
@ -0,0 +1,110 @@ |
|||||||
|
<!-- 商品列表组件 <good-list :list="xx"></good-list> --> |
||||||
|
<template> |
||||||
|
<view class="good-list"> |
||||||
|
<view class="charge" v-for="(item,index) in list" :key="index" :id="'index-' + item.name" :data-index="item.name"> |
||||||
|
<view class="charge-title flex-between"> |
||||||
|
<text>{{item.name}}的贷款申请</text> |
||||||
|
</view> |
||||||
|
<view class="charge-text"> |
||||||
|
<view> |
||||||
|
<text>业务编号:</text> |
||||||
|
<text class="mgl30">{{item.IDNumber}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>所在部门:</text> |
||||||
|
<text class="mgl30">{{item.department}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>客户名称:</text> |
||||||
|
<text class="mgl30">{{item.cusName}}</text> |
||||||
|
</view> |
||||||
|
<view class="flex-between"> |
||||||
|
<text class="time-text">{{item.time}}</text> |
||||||
|
<text class="status-text" :style="'background-color:'+item.status.bgColor+';color:'+item.status.textColor">{{item.status.text}}</text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
props:{ |
||||||
|
list: { |
||||||
|
type: Array, |
||||||
|
default(){ |
||||||
|
return [] |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.good-list{ |
||||||
|
background-color: #fff; |
||||||
|
|
||||||
|
.good-li{ |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
padding: 20upx; |
||||||
|
border-bottom: 1upx solid #eee; |
||||||
|
|
||||||
|
.good-img{ |
||||||
|
width: 160upx; |
||||||
|
height: 160upx; |
||||||
|
margin-right: 20rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.flex-item{ |
||||||
|
flex: 1; |
||||||
|
|
||||||
|
.good-name{ |
||||||
|
font-size: 26upx; |
||||||
|
line-height: 40upx; |
||||||
|
height: 80upx; |
||||||
|
margin-bottom: 20upx; |
||||||
|
overflow: hidden; |
||||||
|
} |
||||||
|
.good-price{ |
||||||
|
font-size: 26upx; |
||||||
|
color: red; |
||||||
|
} |
||||||
|
.good-sold{ |
||||||
|
font-size: 24upx; |
||||||
|
margin-left: 16upx; |
||||||
|
color: gray; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
.charge{ |
||||||
|
margin: 0 50rpx; |
||||||
|
border-radius: 20rpx; |
||||||
|
padding: 0 30rpx; |
||||||
|
box-shadow: 0 0 16rpx #ccc; |
||||||
|
margin-top: 40rpx; |
||||||
|
.charge-title{ |
||||||
|
padding: 16rpx 0; |
||||||
|
border-bottom: 4rpx solid #F2F2F2; |
||||||
|
color: #000; |
||||||
|
font-weight: bold; |
||||||
|
.charge-status{ |
||||||
|
color: #ccc; |
||||||
|
} |
||||||
|
} |
||||||
|
.charge-text{ |
||||||
|
padding: 16rpx 0; |
||||||
|
color: #707070; |
||||||
|
.time-text{ |
||||||
|
color: #ccc; |
||||||
|
} |
||||||
|
.status-text{ |
||||||
|
font-size: 28rpx; |
||||||
|
padding: 8rpx 12rpx; |
||||||
|
border-radius: 10rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,450 @@ |
|||||||
|
<template> |
||||||
|
<!-- 展示关联人组件 --> |
||||||
|
<view> |
||||||
|
<!-- 仅处于企业和是否有关联人(有)的状态下展示,由父级空值隐藏 --> |
||||||
|
<view> |
||||||
|
<block v-for="(item,index) in list" :key="index"> |
||||||
|
<view class="bottom-border mab15"> |
||||||
|
<view class="left-border"><text class="mgl10">关联人{{index+1}}</text></view> |
||||||
|
</view> |
||||||
|
<uni-forms style="padding-bottom: 0;"> |
||||||
|
<uni-forms-item style="padding-bottom: 0;" required name="type" label="关联人类型"> |
||||||
|
<uni-data-checkbox v-model="item.type" :localdata="linkTypeList"></uni-data-checkbox> |
||||||
|
</uni-forms-item> |
||||||
|
</uni-forms> |
||||||
|
<!-- 个人关联人 --> |
||||||
|
<uni-forms class="from-position" v-if="item.type == 0" :value="item" ref="manItem" validate-trigger="bind" err-show-type="toast"> |
||||||
|
<uni-forms-item name="relatedName" required label="关联人名称"> |
||||||
|
<uni-easyinput :disabled="handle" type="text" :inputBorder="true" v-model="item.relatedName" placeholder="请输入关联人名称"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="phone" required label="联系电话"> |
||||||
|
<uni-easyinput :disabled="handle" type="number" maxlength="11" :inputBorder="true" v-model="item.phone" placeholder="请输入联系电话"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="maritalStatus" required label="婚姻状况"> |
||||||
|
<picker :disabled="handle" :class="handle=='watch'||handle=='audit'?'disabled':''" @change="relatedMaritalChange($event,item)" :value="item.maritalStatusVal" :range="maritalList" :range-key="'name'"> |
||||||
|
<view :style="handle?'background-color:#eee;':''" class="picker-view flex-between"> |
||||||
|
<text >{{item.maritalStatusVal!==""? maritalList[item.maritalStatusVal].name : '请选择'}}</text> |
||||||
|
<text class="cuIcon-unfold lg text-gray"></text> |
||||||
|
</view> |
||||||
|
</picker> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="idCard" required label="身份证号码"> |
||||||
|
<uni-easyinput :disabled="handle" type="idcard" :inputBorder="true" v-model="item.idCard" placeholder="请输入身份证号码"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="age" required label="年龄"> |
||||||
|
<uni-easyinput :disabled="handle" type="number" maxlength="2" :inputBorder="true" v-model="item.age" placeholder="请输入年龄"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="gender" required label="性别"> |
||||||
|
<uni-data-checkbox :disabled="handle" v-model="item.gender" :localdata="genders"></uni-data-checkbox> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="address" required label="联系地址"> |
||||||
|
<uni-easyinput :disabled="handle" type="text" :inputBorder="true" v-model="item.address" placeholder="请输入联系地址"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="relatedEducation(index)" required label="学历"> |
||||||
|
<picker :disabled="handle" :class="handle=='watch'||handle=='audit'?'disabled':''" @change="educationChange($event,index)" :value="item.educationVal" :range="educationList" :range-key="'name'"> |
||||||
|
<view :style="handle?'background-color:#eee;':''" class="picker-view flex-between"> |
||||||
|
<text>{{item.educationVal!=='' ? educationList[item.educationVal].name : '请选择'}}</text> |
||||||
|
<text class="cuIcon-unfold lg text-gray"></text> |
||||||
|
</view> |
||||||
|
</picker> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="employer" required label="工作单位"> |
||||||
|
<uni-easyinput :disabled="handle" type="text" :inputBorder="true" v-model="item.employer" placeholder="请输入工作单位"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="position" required label="职务"> |
||||||
|
<uni-easyinput :disabled="handle" type="text" :inputBorder="true" v-model="item.position" placeholder="请输入职务"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="relationship" required label="关联关系"> |
||||||
|
<uni-easyinput :disabled="handle" type="text" :inputBorder="true" v-model="item.relationship" placeholder="请输入关联关系"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
</uni-forms> |
||||||
|
<!-- 企业关联人 enterprise--> |
||||||
|
<uni-forms class="from-position" v-if="item.type == 1" :value="item" ref="enterprise" validate-trigger="bind" err-show-type="toast"> |
||||||
|
<uni-forms-item name="relatedName" required label="关联人名称"> |
||||||
|
<uni-easyinput :disabled="handle" type="text" :inputBorder="true" v-model="item.relatedName" placeholder="请输入关联人名称"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="address" required label="联系住址"> |
||||||
|
<uni-easyinput :disabled="handle" type="text" :inputBorder="true" v-model="item.address" placeholder="请输入联系住址"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="socialUnifiedCode" required label="社会统一代码"> |
||||||
|
<uni-easyinput :disabled="handle" type="text" :inputBorder="true" v-model="item.socialUnifiedCode" placeholder="请输入社会统一代码"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="registeredCapital" required label="注册资金(万元)"> |
||||||
|
<uni-easyinput :disabled="handle" type="digit" :inputBorder="true" v-model="item.registeredCapital" placeholder="请输入注册资金"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="registeredTime" required label="注册时间"> |
||||||
|
<picker :disabled="handle" :class="handle?'disabled':''" mode="date" :value="item.registeredTime" @change="relatedtimeChange($event,item)"> |
||||||
|
<view class="picker-view flex-between"> |
||||||
|
<text>{{item.registeredTime ? item.registeredTime : '请选择'}}</text> |
||||||
|
<text class="cuIcon-unfold lg text-gray"></text> |
||||||
|
</view> |
||||||
|
</picker> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="industry" required label="所属行业"> |
||||||
|
<uni-easyinput :disabled="handle" type="text" :inputBorder="true" v-model="item.industry" placeholder="请输入所属行业"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="legalPersonName" required label="法人姓名"> |
||||||
|
<uni-easyinput :disabled="handle" type="text" :inputBorder="true" v-model="item.legalPersonName" placeholder="请输入法人姓名"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="phone" required label="联系电话"> |
||||||
|
<uni-easyinput :disabled="handle" type="number" maxlength="11" :inputBorder="true" v-model="item.phone" placeholder="请输入联系电话"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="relationship" required label="关联关系"> |
||||||
|
<uni-easyinput :disabled="handle" type="text" :inputBorder="true" v-model="item.relationship" placeholder="请输入关联关系"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
<uni-forms-item name="shareholdersSituation" required label="股东情况"> |
||||||
|
<uni-easyinput :disabled="handle" type="textarea" :inputBorder="true" v-model="item.shareholdersSituation" placeholder="请输入关联关系"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
</uni-forms> |
||||||
|
<!-- 企业和个人共用,可进行保存和删除 --> |
||||||
|
<view class="foot-btn btn-rig pad-bt"> |
||||||
|
<button v-if="!handle" class="mini-btn round def-btn mar-lr" type="primary" size="mini" @click="submitForm(item,index)">保存</button> |
||||||
|
<button v-if="!handle" class="mini-btn round" type="warn" size="mini" @click="deleteContact(index,item)">删除</button> |
||||||
|
</view> |
||||||
|
</block> |
||||||
|
</view> |
||||||
|
<!-- 末尾的新增关联人,点击生成一个新的关联人表单,无删除按钮。有新增按钮,新增完毕重新请求关联人列表 --> |
||||||
|
<!-- 且清除已有的值 --> |
||||||
|
<!-- 仅企业+有 关联人展示 --> |
||||||
|
<button v-if="!handle" class="cu-btn block bg-blue margin-tb-sm lg round" @click="addContact">新增关联人</button> |
||||||
|
|
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
name:"linkMan", |
||||||
|
props:{ |
||||||
|
// 禁用修改,隐藏新增/保存功能,false为不做限制 |
||||||
|
handle:{ |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
// 用于保存,必传 |
||||||
|
companyId:{}, |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
// 切换关联人企业和客户 |
||||||
|
linkTypeList: [ |
||||||
|
{ |
||||||
|
text: '个人', |
||||||
|
value: '0', |
||||||
|
// disabled: true |
||||||
|
}, |
||||||
|
{ |
||||||
|
text: '企业', |
||||||
|
value: '1', |
||||||
|
// disabled: false |
||||||
|
}, |
||||||
|
], |
||||||
|
//性别列表-禁用 |
||||||
|
genders: [ |
||||||
|
{ |
||||||
|
text: '男', |
||||||
|
value: 1, |
||||||
|
}, |
||||||
|
{ |
||||||
|
text: '女', |
||||||
|
value: 0, |
||||||
|
}, |
||||||
|
], |
||||||
|
//婚姻状况列表 |
||||||
|
maritalList: [ |
||||||
|
{ |
||||||
|
name: '未婚', |
||||||
|
value: 0 |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: '已婚', |
||||||
|
value: 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: '离异', |
||||||
|
value: 2 |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: '再婚', |
||||||
|
value: 3 |
||||||
|
}, |
||||||
|
], |
||||||
|
//学历列表 |
||||||
|
educationList: [ |
||||||
|
{ |
||||||
|
name: '本科', |
||||||
|
value: 0 |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: '大专', |
||||||
|
value: 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: '高职', |
||||||
|
value: 2 |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: '中专', |
||||||
|
value: 3 |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: '其他', |
||||||
|
value: 4 |
||||||
|
}, |
||||||
|
], |
||||||
|
//关联人列表,默认留一个初始化的对象 |
||||||
|
// 企业和个人共用了部分字段,提交时需要删除部分 |
||||||
|
list: [ |
||||||
|
{ // 公用 |
||||||
|
type: '0', |
||||||
|
relatedName: '',//关联人名称 |
||||||
|
relationship: '',//关联关系 |
||||||
|
phone: '',//联系电话 |
||||||
|
address: '',//联系地址 |
||||||
|
|
||||||
|
// 个人关联人 |
||||||
|
maritalStatusVal: '', |
||||||
|
maritalStatus: '',//婚姻状况 |
||||||
|
idCard: '',//身份证号码 |
||||||
|
age: '',//年龄 |
||||||
|
gender: 1,//性别 |
||||||
|
educationVal: '', |
||||||
|
education: '',//学历 |
||||||
|
employer: '',//工作单位 |
||||||
|
position: '',//职务 |
||||||
|
//企业关联人 |
||||||
|
socialUnifiedCode: '',//社会统一代码 |
||||||
|
registeredCapital: '',//注册资金 |
||||||
|
registeredTime: '',//注册时间 |
||||||
|
industry: '',//所属行业 |
||||||
|
legalPersonName: '',//法人姓名 |
||||||
|
shareholdersSituation: ''//股东情况 |
||||||
|
} |
||||||
|
] |
||||||
|
}; |
||||||
|
}, |
||||||
|
created() { |
||||||
|
console.log(this.companyId) |
||||||
|
if(this.companyId) this.getLinkmanList() |
||||||
|
console.log(this.handle,'当前操作状态') |
||||||
|
if(this.handle){ |
||||||
|
this.linkTypeList.map(e=>{ |
||||||
|
e.disabled = true |
||||||
|
}) |
||||||
|
this.genders.map(e=>{ |
||||||
|
e.disabled = true |
||||||
|
}) |
||||||
|
this.maritalList.map(e=>{ |
||||||
|
e.disabled = true |
||||||
|
}) |
||||||
|
this.educationList.map(e=>{ |
||||||
|
e.disabled = true |
||||||
|
}) |
||||||
|
} |
||||||
|
// this.companyId |
||||||
|
}, |
||||||
|
watch:{ |
||||||
|
// 监听传值,若为true,禁用掉CheckBox |
||||||
|
handle:function(val){ |
||||||
|
console.log(val,'handlue-0-0linmk') |
||||||
|
if(val){ |
||||||
|
this.linkTypeList.map(e=>{ |
||||||
|
e.disabled = true |
||||||
|
}) |
||||||
|
this.genders.map(e=>{ |
||||||
|
e.disabled = true |
||||||
|
}) |
||||||
|
this.maritalList.map(e=>{ |
||||||
|
e.disabled = true |
||||||
|
}) |
||||||
|
this.educationList.map(e=>{ |
||||||
|
e.disabled = true |
||||||
|
}) |
||||||
|
} |
||||||
|
}, |
||||||
|
// 监听传递的企业id,调用关联人接口 |
||||||
|
companyId:function(val,old){ |
||||||
|
console.log(val,'当前的id值',old) |
||||||
|
if(val!==''||val!==null||val!==undefined){ |
||||||
|
this.getLinkmanList() |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
onReady() { |
||||||
|
}, |
||||||
|
mounted() {}, |
||||||
|
methods:{ |
||||||
|
//企业关联人注册时间选择 |
||||||
|
relatedtimeChange(e,item) { |
||||||
|
item.registeredTime = e.detail.value |
||||||
|
console.log(item.registeredTime ,' 当前时间的值') |
||||||
|
}, |
||||||
|
//关联人婚姻状况选择 |
||||||
|
relatedMaritalChange(e,item) { |
||||||
|
item.maritalStatusVal = e.detail.value |
||||||
|
item.maritalStatus = this.maritalList[e.detail.value].value |
||||||
|
}, |
||||||
|
//关联人性别选择 |
||||||
|
legalGenderChange(e,index) { |
||||||
|
this.list[index].legalGender = e.detail.value |
||||||
|
}, |
||||||
|
//学历选择 |
||||||
|
educationChange(e,index) { |
||||||
|
this.list[index].educationVal = e.detail.value |
||||||
|
this.list[index].education = this.educationList[e.detail.value].value |
||||||
|
}, |
||||||
|
// 提交表单 |
||||||
|
submitForm(item,index) { |
||||||
|
// 0 个人 : 企业 |
||||||
|
let obj // 用obj提交 |
||||||
|
if(!this.companyId) return uni.showToast({title: '请先选择客户!',icon:'none'}) |
||||||
|
// 共用验证 |
||||||
|
if (!item.relatedName)return uni.showToast({title:'请输入关联人名称',icon:"none"}) |
||||||
|
if (!item.relationship)return uni.showToast({title:'请输入关联关系',icon:"none"}) |
||||||
|
if (!item.phone)return uni.showToast({title:'请输入联系电话',icon:"none"}) |
||||||
|
if (!item.address)return uni.showToast({title:'请输入联系地址',icon:"none"}) |
||||||
|
|
||||||
|
obj = { |
||||||
|
type:item.type, |
||||||
|
relatedName:item.relatedName, |
||||||
|
relationship:item.relationship, |
||||||
|
phone:item.phone, |
||||||
|
address:item.address, |
||||||
|
relatedCompanyId:this.companyId |
||||||
|
} |
||||||
|
// 验证和赋值 |
||||||
|
if(item.type==0){ |
||||||
|
if (!item.maritalStatusVal)return uni.showToast({title:'请输入婚姻状况',icon:"none"}) |
||||||
|
if (!item.idCard)return uni.showToast({title:'请输入身份证号码',icon:"none"}) |
||||||
|
if (!item.age)return uni.showToast({title:'请输入年龄',icon:"none"}) |
||||||
|
if (item.gender!=0&&item.gender!=1)return uni.showToast({title:'请选择性别',icon:"none"}) |
||||||
|
if (!item.educationVal)return uni.showToast({title:'请选择学历',icon:"none"}) |
||||||
|
if (!item.employer)return uni.showToast({title:'请输入工作单位',icon:"none"}) |
||||||
|
if (!item.position)return uni.showToast({title:'请输入职务',icon:"none"}) |
||||||
|
obj.maritalStatus = item.maritalStatus |
||||||
|
obj.idCard = item.idCard |
||||||
|
obj.age = item.age |
||||||
|
obj.gender = item.gender |
||||||
|
obj.education = item.education |
||||||
|
obj.employer = item.employer |
||||||
|
obj.position = item.position |
||||||
|
}else{ |
||||||
|
if (!item.socialUnifiedCode)return uni.showToast({title:'请输入社会统一代码',icon:"none"}) |
||||||
|
if (!item.registeredCapital)return uni.showToast({title:'请输入注册资金',icon:"none"}) |
||||||
|
if (!item.registeredTime)return uni.showToast({title:'请选择注册时间',icon:"none"}) |
||||||
|
if (!item.industry)return uni.showToast({title:'请输入所属行业',icon:"none"}) |
||||||
|
if (!item.legalPersonName)return uni.showToast({title:'请输入法人姓名',icon:"none"}) |
||||||
|
if (!item.shareholdersSituation)return uni.showToast({title:'请输入股东情况',icon:"none"}) |
||||||
|
obj.socialUnifiedCode = item.socialUnifiedCode |
||||||
|
obj.registeredCapital = item.registeredCapital |
||||||
|
obj.registeredTime = item.registeredTime |
||||||
|
obj.industry = item.industry |
||||||
|
obj.legalPersonName = item.legalPersonName |
||||||
|
obj.shareholdersSituation = item.shareholdersSituation |
||||||
|
} |
||||||
|
// 老数据才有id--修改操作 |
||||||
|
if(item.id){ |
||||||
|
obj.id = item.id |
||||||
|
// 修改接口 |
||||||
|
this.$http.post('/api-crms/crms-company-personal/updateCompanyPersonal',obj).then(res=>{ |
||||||
|
uni.showToast({title: '修改成功'}) |
||||||
|
console.log('修改关联人的操作') |
||||||
|
this.getLinkmanList() |
||||||
|
}) |
||||||
|
} |
||||||
|
else{ // 下面为新增操作 |
||||||
|
console.log(obj,'新增操作') |
||||||
|
this.$http.post('/api-crms/crms-company-personal/insertCompanyPersonal',obj).then(res => { |
||||||
|
uni.showToast({title: '添加成功'}) |
||||||
|
// item.id = res.data[0].id // 添加成功,重新请求就有了 |
||||||
|
this.list.splice(list[this.list.length-1],1)// 删除添加的框,并请求一次接口 |
||||||
|
console.log('新增关联人操作') |
||||||
|
this.getLinkmanList()//调关联人接口 |
||||||
|
}).catch(function (error) {}); |
||||||
|
} |
||||||
|
}, |
||||||
|
//添加关联人按钮 |
||||||
|
addContact(){ |
||||||
|
// console.log('添加关联人按钮') |
||||||
|
// if(!this.businessApply.companyId) return uni.showToast({title: '请先选择客户!',icon:'none'}) |
||||||
|
// console.log('添加关联人按钮---没出发') |
||||||
|
// if(!this.enterpriseForm.isExistRelated) return uni.showToast({title: '请先选择客户!'}) |
||||||
|
// 生成一个新的表可输入 |
||||||
|
let CONTACT_INFO = { |
||||||
|
type: 0, |
||||||
|
relatedName: '', |
||||||
|
phone: '', |
||||||
|
maritalStatusVal: '', |
||||||
|
maritalStatus: '', |
||||||
|
idCard: '', |
||||||
|
age: '', |
||||||
|
gender: '', |
||||||
|
address: '', |
||||||
|
educationVal: '', |
||||||
|
education: '', |
||||||
|
employer: '', |
||||||
|
position: '', |
||||||
|
relationship: '', |
||||||
|
|
||||||
|
socialUnifiedCode: '', |
||||||
|
registeredCapital: '', |
||||||
|
registeredTime: '', |
||||||
|
industry: '', |
||||||
|
legalPersonName: '', |
||||||
|
shareholdersSituation: '' |
||||||
|
} |
||||||
|
this.list.push({...CONTACT_INFO}) |
||||||
|
}, |
||||||
|
//删除关联人 |
||||||
|
deleteContact(index,item){ |
||||||
|
let _this = this |
||||||
|
uni.showModal({ |
||||||
|
title: '提示', |
||||||
|
content: '是否删除该关联人', |
||||||
|
success: function (res) { |
||||||
|
if (res.confirm) { |
||||||
|
if(item.id){ |
||||||
|
let arr = [] |
||||||
|
arr.push(item.id) |
||||||
|
_this.$http.post('/api-crms/crms-company-personal/deleteCompanyPersonal',arr).then(res => { |
||||||
|
_this.getLinkmanList() |
||||||
|
}).catch(function (error) {}); |
||||||
|
} |
||||||
|
_this.list.splice(index,1) |
||||||
|
_this.total = _this.total-1 |
||||||
|
uni.showToast({title: '删除成功'}) |
||||||
|
} else if (res.cancel) {} |
||||||
|
} |
||||||
|
}); |
||||||
|
}, |
||||||
|
// 关联人列表接口 |
||||||
|
getLinkmanList(){ |
||||||
|
console.log('关联人列表接口',this.companyId) |
||||||
|
if(!this.companyId) return uni.showToast({title: '请先选择客户!',icon:'none'}) |
||||||
|
this.$http.get("/api-crms/crms-company-personal/companyPersonalList",{ |
||||||
|
customerId:this.companyId, |
||||||
|
page:1, |
||||||
|
size:999 // 不做分页,直接展示所有的关联人或企业 |
||||||
|
}).then(res=>{ |
||||||
|
console.log(res,'读取关联人list') |
||||||
|
this.list = [] |
||||||
|
if(res.data.list&&res.data.list.length!==0){ |
||||||
|
this.list = res.data.list |
||||||
|
this.list.map(e=>{ |
||||||
|
e.educationVal = +e.education |
||||||
|
e.maritalStatusVal = +e.maritalStatus |
||||||
|
}) |
||||||
|
console.log(this.list,'关联人的list') |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
}, |
||||||
|
destroyed() {} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss"> |
||||||
|
.from-position{ |
||||||
|
position: relative; |
||||||
|
top: -30rpx; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,200 @@ |
|||||||
|
<template> |
||||||
|
<text :class="classObj.wrapper" @click.stop="handleClick"> |
||||||
|
<text :class="[classObj.input, {'is-indeterminate': indeterminate, 'is-checked': checked, 'is-disabled': disabled}]"> |
||||||
|
<text :class="classObj.inner"></text> |
||||||
|
</text> |
||||||
|
</text> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
data() { |
||||||
|
return { |
||||||
|
classObj: {} |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
props: { |
||||||
|
type: { |
||||||
|
type: String, |
||||||
|
validator(t) { |
||||||
|
return t === 'radio' || t === 'checkbox' |
||||||
|
} |
||||||
|
}, |
||||||
|
checked: Boolean, |
||||||
|
disabled: Boolean, |
||||||
|
indeterminate: Boolean |
||||||
|
}, |
||||||
|
|
||||||
|
created() { |
||||||
|
this.classObj = { |
||||||
|
wrapper: `ly-${this.type}`, |
||||||
|
input: `ly-${this.type}__input`, |
||||||
|
inner: `ly-${this.type}__inner` |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
methods: { |
||||||
|
handleClick() { |
||||||
|
this.$emit('check', this.checked); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style> |
||||||
|
/* lyRadio/lyCheckbox-start */ |
||||||
|
.ly-checkbox, |
||||||
|
.ly-radio { |
||||||
|
color: #606266; |
||||||
|
font-weight: 500; |
||||||
|
font-size: 28rpx; |
||||||
|
cursor: pointer; |
||||||
|
user-select: none; |
||||||
|
padding-right: 16rpx |
||||||
|
} |
||||||
|
|
||||||
|
.ly-checkbox__input, |
||||||
|
.ly-radio__input { |
||||||
|
cursor: pointer; |
||||||
|
outline: 0; |
||||||
|
line-height: 1; |
||||||
|
vertical-align: middle |
||||||
|
} |
||||||
|
|
||||||
|
.ly-checkbox__input.is-disabled .ly-checkbox__inner, |
||||||
|
.ly-radio__input.is-disabled .ly-radio__inner { |
||||||
|
background-color: #edf2fc; |
||||||
|
border-color: #DCDFE6; |
||||||
|
cursor: not-allowed |
||||||
|
} |
||||||
|
|
||||||
|
.ly-checkbox__input.is-disabled .ly-checkbox__inner::after, |
||||||
|
.ly-radio__input.is-disabled .ly-radio__inner::after { |
||||||
|
cursor: not-allowed; |
||||||
|
border-color: #C0C4CC |
||||||
|
} |
||||||
|
|
||||||
|
.ly-checkbox__input.is-disabled .ly-checkbox__inner+.ly-checkbox__label, |
||||||
|
.ly-radio__input.is-disabled .ly-radio__inner+.ly-radio__label { |
||||||
|
cursor: not-allowed |
||||||
|
} |
||||||
|
|
||||||
|
.ly-checkbox__input.is-disabled.is-checked .ly-checkbox__inner, |
||||||
|
.ly-radio__input.is-disabled.is-checked .ly-radio__inner { |
||||||
|
background-color: #F2F6FC; |
||||||
|
border-color: #DCDFE6 |
||||||
|
} |
||||||
|
|
||||||
|
.ly-checkbox__input.is-disabled.is-checked .ly-checkbox__inner::after, |
||||||
|
.ly-radio__input.is-disabled.is-checked .ly-radio__inner::after { |
||||||
|
border-color: #C0C4CC |
||||||
|
} |
||||||
|
|
||||||
|
.ly-checkbox__input.is-disabled.is-indeterminate .ly-checkbox__inner { |
||||||
|
background-color: #F2F6FC; |
||||||
|
border-color: #DCDFE6 |
||||||
|
} |
||||||
|
|
||||||
|
.ly-checkbox__input.is-disabled.is-indeterminate .ly-checkbox__inner::before { |
||||||
|
background-color: #C0C4CC; |
||||||
|
border-color: #C0C4CC |
||||||
|
} |
||||||
|
|
||||||
|
.ly-checkbox__input.is-checked .ly-checkbox__inner, |
||||||
|
.ly-radio__input.is-checked .ly-radio__inner, |
||||||
|
.ly-checkbox__input.is-indeterminate .ly-checkbox__inner { |
||||||
|
background-color: #409EFF; |
||||||
|
border-color: #409EFF |
||||||
|
} |
||||||
|
|
||||||
|
.ly-checkbox__input.is-disabled+text.ly-checkbox__label, |
||||||
|
.ly-radio__input.is-disabled+text.ly-radio__label { |
||||||
|
color: #C0C4CC; |
||||||
|
cursor: not-allowed |
||||||
|
} |
||||||
|
|
||||||
|
.ly-checkbox__input.is-checked .ly-checkbox__inner::after, |
||||||
|
.ly-radio__input.is-checked .ly-radio__inner::after { |
||||||
|
-webkit-transform: rotate(45deg) scaleY(1); |
||||||
|
transform: rotate(45deg) scaleY(1) |
||||||
|
} |
||||||
|
|
||||||
|
.ly-checkbox__input.is-checked+.ly-checkbox__label, |
||||||
|
.ly-radio__input.is-checked+.ly-radio__label { |
||||||
|
color: #409EFF |
||||||
|
} |
||||||
|
|
||||||
|
.ly-checkbox__input.is-focus .ly-checkbox__inner, |
||||||
|
.ly-radio__input.is-focus .ly-radio__inner { |
||||||
|
border-color: #409EFF |
||||||
|
} |
||||||
|
|
||||||
|
.ly-checkbox__input.is-indeterminate .ly-checkbox__inner::before { |
||||||
|
content: ''; |
||||||
|
position: absolute; |
||||||
|
display: block; |
||||||
|
background-color: #FFF; |
||||||
|
height: 6rpx; |
||||||
|
-webkit-transform: scale(.5); |
||||||
|
transform: scale(.5); |
||||||
|
left: 0; |
||||||
|
right: 0; |
||||||
|
top: 10rpx |
||||||
|
} |
||||||
|
|
||||||
|
.ly-checkbox__input.is-indeterminate .ly-checkbox__inner::after { |
||||||
|
display: none |
||||||
|
} |
||||||
|
|
||||||
|
.ly-checkbox__inner, |
||||||
|
.ly-radio__inner { |
||||||
|
display: inline-block; |
||||||
|
position: relative; |
||||||
|
border: 2rpx solid #DCDFE6; |
||||||
|
border-radius: 4rpx; |
||||||
|
-webkit-box-sizing: border-box; |
||||||
|
box-sizing: border-box; |
||||||
|
width: 28rpx; |
||||||
|
height: 28rpx; |
||||||
|
background-color: #FFF; |
||||||
|
z-index: 1; |
||||||
|
-webkit-transition: border-color .25s cubic-bezier(.71, -.46, .29, 1.46), background-color .25s cubic-bezier(.71, -.46, .29, 1.46); |
||||||
|
transition: border-color .25s cubic-bezier(.71, -.46, .29, 1.46), background-color .25s cubic-bezier(.71, -.46, .29, 1.46) |
||||||
|
} |
||||||
|
|
||||||
|
.ly-radio__inner { |
||||||
|
border-radius: 50%; |
||||||
|
width: 34rpx !important; |
||||||
|
height: 34rpx !important; |
||||||
|
} |
||||||
|
|
||||||
|
.ly-checkbox__inner::after, |
||||||
|
.ly-radio__inner::after { |
||||||
|
-webkit-box-sizing: content-box; |
||||||
|
box-sizing: content-box; |
||||||
|
content: ""; |
||||||
|
border: 2rpx solid #FFF; |
||||||
|
border-left: 0; |
||||||
|
border-top: 0; |
||||||
|
height: 14rpx; |
||||||
|
left: 10rpx; |
||||||
|
position: absolute; |
||||||
|
top: 2rpx; |
||||||
|
-webkit-transform: rotate(45deg) scaleY(0); |
||||||
|
transform: rotate(45deg) scaleY(0); |
||||||
|
width: 6rpx; |
||||||
|
-webkit-transition: -webkit-transform .15s ease-in .05s; |
||||||
|
transition: -webkit-transform .15s ease-in .05s; |
||||||
|
transition: transform .15s ease-in .05s; |
||||||
|
transition: transform .15s ease-in .05s, -webkit-transform .15s ease-in .05s; |
||||||
|
-webkit-transform-origin: center; |
||||||
|
transform-origin: center |
||||||
|
} |
||||||
|
|
||||||
|
.ly-radio__inner::after { |
||||||
|
left: 12rpx !important; |
||||||
|
top: 6rpx !important; |
||||||
|
} |
||||||
|
/* lyRadio/lyCheckbox-end */ |
||||||
|
</style> |
@ -0,0 +1,436 @@ |
|||||||
|
<template> |
||||||
|
<view ref="node" |
||||||
|
name="LyTreeNode" |
||||||
|
v-show="node.visible" |
||||||
|
class="ly-tree-node" |
||||||
|
:class="{ |
||||||
|
'is-expanded': expanded, |
||||||
|
'is-hidden': !node.visible, |
||||||
|
'is-checked': !node.disabled && node.checked |
||||||
|
}" |
||||||
|
role="treeitem" |
||||||
|
@tap.stop="handleClick" > |
||||||
|
<view class="ly-tree-node__content" |
||||||
|
:class="{ |
||||||
|
'is-current': node.isCurrent && highlightCurrent |
||||||
|
}" |
||||||
|
:style="{ |
||||||
|
'padding-left': (node.level - 1) * indent + 'px' |
||||||
|
}"> |
||||||
|
<!-- 左边三角图标 --> |
||||||
|
<text |
||||||
|
@tap.stop="handleExpandIconClick" |
||||||
|
:class="[ |
||||||
|
{ |
||||||
|
'is-leaf': node.isLeaf, |
||||||
|
expanded: !node.isLeaf && node.expanded |
||||||
|
}, |
||||||
|
'ly-tree-node__expand-icon', |
||||||
|
iconClass ? iconClass : 'ly-iconfont ly-icon-caret-right' |
||||||
|
]"> |
||||||
|
</text> |
||||||
|
<!-- 勾选 --> |
||||||
|
<ly-checkbox v-if="checkboxVisible || radioVisible" |
||||||
|
:type="checkboxVisible ? 'checkbox' : 'radio'" |
||||||
|
:checked="node.checked" |
||||||
|
:indeterminate="node.indeterminate" |
||||||
|
:disabled="!!node.disabled" |
||||||
|
@check="handleCheckChange(!node.checked)"/> |
||||||
|
<!-- 文字左边,三角的右边,写在本行 --> |
||||||
|
<text v-if="node.loading" |
||||||
|
class="ly-tree-node__loading-icon ly-iconfont ly-icon-loading"> |
||||||
|
</text> |
||||||
|
|
||||||
|
<template v-if="node.icon && node.icon.length > 0"> |
||||||
|
<image |
||||||
|
v-if="node.icon.indexOf('/') !== -1" |
||||||
|
class="ly-tree-node__icon" |
||||||
|
mode="widthFix" |
||||||
|
:src="node.icon" |
||||||
|
@error="handleImageError" |
||||||
|
> |
||||||
|
</image> |
||||||
|
<text v-else |
||||||
|
class="ly-tree-node__icon" |
||||||
|
:class="node.icon"> |
||||||
|
</text> |
||||||
|
</template> |
||||||
|
<!-- 文字所在 --> |
||||||
|
<view class="ly-tree-node__label df"> |
||||||
|
{{node.label}} |
||||||
|
<!-- <image v-if="node.account" style="width: 20rpx;height: 20rpx;" src="../../static/img/aboutUs.png" width="15rpx" height="15rpx" mode=""></image> --> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view v-if="!renderAfterExpand || childNodeRendered" |
||||||
|
v-show="expanded" |
||||||
|
class="ly-tree-node__children" |
||||||
|
role="group"> |
||||||
|
<ly-tree-node v-for="cNodeId in node.childNodesId" |
||||||
|
:nodeId="cNodeId" |
||||||
|
:render-after-expand="renderAfterExpand" |
||||||
|
:show-checkbox="showCheckbox" |
||||||
|
:show-radio="showRadio" |
||||||
|
:check-only-leaf="checkOnlyLeaf" |
||||||
|
:key="getNodeKey(cNodeId)" |
||||||
|
:indent="indent" |
||||||
|
:icon-class="iconClass"> |
||||||
|
</ly-tree-node> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import {getNodeKey} from './tool/util.js'; |
||||||
|
import lyCheckbox from './components/ly-checkbox.vue'; |
||||||
|
|
||||||
|
export default { |
||||||
|
name: 'LyTreeNode', |
||||||
|
|
||||||
|
componentName: 'LyTreeNode', |
||||||
|
|
||||||
|
components: { |
||||||
|
lyCheckbox |
||||||
|
}, |
||||||
|
|
||||||
|
props: { |
||||||
|
nodeId: [Number, String], |
||||||
|
renderAfterExpand: { |
||||||
|
type: Boolean, |
||||||
|
default: true |
||||||
|
}, |
||||||
|
checkOnlyLeaf: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
showCheckbox: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
showRadio: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
indent: Number, |
||||||
|
iconClass: String |
||||||
|
}, |
||||||
|
|
||||||
|
data() { |
||||||
|
return { |
||||||
|
node: { |
||||||
|
indeterminate: false, |
||||||
|
checked: false, |
||||||
|
expanded: false |
||||||
|
}, |
||||||
|
expanded: false, |
||||||
|
childNodeRendered: false, |
||||||
|
oldChecked: null, |
||||||
|
oldIndeterminate: null, |
||||||
|
highlightCurrent: false |
||||||
|
}; |
||||||
|
}, |
||||||
|
|
||||||
|
inject: ['tree'], |
||||||
|
|
||||||
|
computed: { |
||||||
|
checkboxVisible() { |
||||||
|
if (this.checkOnlyLeaf) { |
||||||
|
return this.showCheckbox && this.node.isLeaf; |
||||||
|
} |
||||||
|
|
||||||
|
return this.showCheckbox; |
||||||
|
}, |
||||||
|
radioVisible() { |
||||||
|
if (this.checkOnlyLeaf) { |
||||||
|
return this.showRadio && this.node.isLeaf; |
||||||
|
} |
||||||
|
|
||||||
|
return this.showRadio; |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
watch: { |
||||||
|
'node.indeterminate'(val) { |
||||||
|
this.handleSelectChange(this.node.checked, val); |
||||||
|
}, |
||||||
|
'node.checked'(val) { |
||||||
|
this.handleSelectChange(val, this.node.indeterminate); |
||||||
|
}, |
||||||
|
'node.expanded'(val) { |
||||||
|
this.$nextTick(() => this.expanded = val); |
||||||
|
if (val) { |
||||||
|
this.childNodeRendered = true; |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
methods: { |
||||||
|
getNodeKey(nodeId) { |
||||||
|
let node = this.tree.store.root.getChildNodes([nodeId])[0]; |
||||||
|
return getNodeKey(this.tree.nodeKey, node.data); |
||||||
|
}, |
||||||
|
|
||||||
|
handleSelectChange(checked, indeterminate) { |
||||||
|
if (this.oldChecked !== checked && this.oldIndeterminate !== indeterminate) { |
||||||
|
|
||||||
|
if (this.checkOnlyLeaf && !this.node.isLeaf) return; |
||||||
|
|
||||||
|
if (this.checkboxVisible) { |
||||||
|
const allNodes = this.tree.store._getAllNodes(); |
||||||
|
this.tree.$emit('check-change', { |
||||||
|
checked, |
||||||
|
indeterminate, |
||||||
|
node: this.node, |
||||||
|
data: this.node.data, |
||||||
|
checkedall: allNodes.every(item => item.checked) |
||||||
|
}); |
||||||
|
} else { |
||||||
|
this.tree.$emit('radio-change', { |
||||||
|
checked, |
||||||
|
node: this.node, |
||||||
|
data: this.node.data, |
||||||
|
checkedall: false |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (!this.expanded && this.tree.expandOnCheckNode && checked) { |
||||||
|
this.handleExpandIconClick(); |
||||||
|
} |
||||||
|
|
||||||
|
this.oldChecked = checked; |
||||||
|
this.indeterminate = indeterminate; |
||||||
|
}, |
||||||
|
|
||||||
|
handleClick() { |
||||||
|
this.tree.store.setCurrentNode(this.node); |
||||||
|
this.tree.$emit('current-change', { |
||||||
|
node: this.node, |
||||||
|
data: this.tree.store.currentNode ? this.tree.store.currentNode.data : null, |
||||||
|
currentNode: this.tree.store.currentNode |
||||||
|
}); |
||||||
|
this.tree.currentNode = this.node; |
||||||
|
|
||||||
|
if (this.tree.expandOnClickNode) { |
||||||
|
this.handleExpandIconClick(); |
||||||
|
} |
||||||
|
|
||||||
|
if (this.tree.checkOnClickNode && !this.node.disabled) { |
||||||
|
(this.checkboxVisible || this.radioVisible) && this.handleCheckChange(!this.node.checked); |
||||||
|
} |
||||||
|
|
||||||
|
this.tree.$emit('node-click', this.node); |
||||||
|
}, |
||||||
|
|
||||||
|
handleExpandIconClick() { |
||||||
|
if (this.node.isLeaf) return; |
||||||
|
|
||||||
|
if (this.expanded) { |
||||||
|
this.tree.$emit('node-collapse', this.node); |
||||||
|
this.node.collapse(); |
||||||
|
} else { |
||||||
|
this.node.expand(); |
||||||
|
this.tree.$emit('node-expand', this.node); |
||||||
|
|
||||||
|
if (this.tree.accordion) { |
||||||
|
uni.$emit(`${this.tree.elId}-tree-node-expand`, this.node); |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
handleCheckChange(checked) { |
||||||
|
if (this.node.disabled) return; |
||||||
|
|
||||||
|
if (this.checkboxVisible) { |
||||||
|
this.node.setChecked(checked, !(this.tree.checkStrictly || this.checkOnlyLeaf)); |
||||||
|
} else { |
||||||
|
this.node.setRadioChecked(checked); |
||||||
|
} |
||||||
|
|
||||||
|
this.$nextTick(() => { |
||||||
|
this.tree.$emit('check', { |
||||||
|
node: this.node, |
||||||
|
data: this.node.data, |
||||||
|
checkedNodes: this.tree.store.getCheckedNodes(), |
||||||
|
checkedKeys: this.tree.store.getCheckedKeys(), |
||||||
|
halfCheckedNodes: this.tree.store.getHalfCheckedNodes(), |
||||||
|
halfCheckedKeys: this.tree.store.getHalfCheckedKeys() |
||||||
|
}); |
||||||
|
}); |
||||||
|
}, |
||||||
|
|
||||||
|
handleImageError() { |
||||||
|
this.node.icon = this.tree.defaultNodeIcon; |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
created() { |
||||||
|
if (!this.tree) { |
||||||
|
throw new Error('Can not find node\'s tree.'); |
||||||
|
} |
||||||
|
|
||||||
|
this.node = this.tree.store.nodesMap[this.nodeId]; |
||||||
|
this.highlightCurrent = this.tree.highlightCurrent; |
||||||
|
|
||||||
|
if (this.node.expanded) { |
||||||
|
this.expanded = true; |
||||||
|
this.childNodeRendered = true; |
||||||
|
} |
||||||
|
|
||||||
|
const props = this.tree.props || {}; |
||||||
|
const childrenKey = props['children'] || 'children'; |
||||||
|
this.$watch(`node.data.${childrenKey}`, () => { |
||||||
|
this.node.updateChildren(); |
||||||
|
}); |
||||||
|
|
||||||
|
if (this.tree.accordion) { |
||||||
|
uni.$on(`${this.tree.elId}-tree-node-expand`, node => { |
||||||
|
if (this.node.id !== node.id && this.node.level === node.level) { |
||||||
|
this.node.collapse(); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
beforeDestroy() { |
||||||
|
this.$parent = null; |
||||||
|
} |
||||||
|
}; |
||||||
|
</script> |
||||||
|
|
||||||
|
<style> |
||||||
|
.ly-tree-node { |
||||||
|
white-space: nowrap; |
||||||
|
outline: 0 |
||||||
|
} |
||||||
|
|
||||||
|
.ly-tree-node__content { |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
height: 70rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.ly-tree-node__content.is-current { |
||||||
|
background-color: #F5F7FA; |
||||||
|
} |
||||||
|
|
||||||
|
.ly-tree-node__content>.ly-tree-node__expand-icon { |
||||||
|
padding: 12rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.ly-tree-node__checkbox { |
||||||
|
display: flex; |
||||||
|
margin-right: 16rpx; |
||||||
|
width: 40rpx; |
||||||
|
height: 40rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.ly-tree-node__checkbox>image { |
||||||
|
width: 40rpx; |
||||||
|
height: 40rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.ly-tree-node__expand-icon { |
||||||
|
color: #C0C4CC; |
||||||
|
font-size: 28rpx; |
||||||
|
-webkit-transform: rotate(0); |
||||||
|
transform: rotate(0); |
||||||
|
-webkit-transition: -webkit-transform .3s ease-in-out; |
||||||
|
transition: -webkit-transform .3s ease-in-out; |
||||||
|
transition: transform .3s ease-in-out; |
||||||
|
transition: transform .3s ease-in-out, -webkit-transform .3s ease-in-out |
||||||
|
} |
||||||
|
|
||||||
|
.ly-tree-node__expand-icon.expanded { |
||||||
|
-webkit-transform: rotate(90deg); |
||||||
|
transform: rotate(90deg) |
||||||
|
} |
||||||
|
|
||||||
|
.ly-tree-node__expand-icon.is-leaf { |
||||||
|
color: transparent; |
||||||
|
} |
||||||
|
|
||||||
|
.ly-tree-node__icon { |
||||||
|
width: 34rpx; |
||||||
|
height: 34rpx; |
||||||
|
overflow: hidden; |
||||||
|
margin-right: 16rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.ly-tree-node__label { |
||||||
|
font-size: 28rpx |
||||||
|
} |
||||||
|
|
||||||
|
.ly-tree-node__loading-icon { |
||||||
|
margin-right: 16rpx; |
||||||
|
font-size: 28rpx; |
||||||
|
color: #C0C4CC; |
||||||
|
-webkit-animation: rotating 2s linear infinite; |
||||||
|
animation: rotating 2s linear infinite |
||||||
|
} |
||||||
|
|
||||||
|
.ly-tree-node>.ly-tree-node__children { |
||||||
|
overflow: hidden; |
||||||
|
background-color: transparent |
||||||
|
} |
||||||
|
|
||||||
|
.ly-tree-node>.ly-tree-node__children.collapse-transition { |
||||||
|
transition: height .3s ease-in-out; |
||||||
|
} |
||||||
|
|
||||||
|
.ly-tree-node.is-expanded>.ly-tree-node__children { |
||||||
|
display: block |
||||||
|
} |
||||||
|
|
||||||
|
.ly-tree-node_collapse { |
||||||
|
overflow: hidden; |
||||||
|
padding-top: 0; |
||||||
|
padding-bottom: 0; |
||||||
|
} |
||||||
|
/* lyTree-end */ |
||||||
|
|
||||||
|
/* iconfont-start */ |
||||||
|
@font-face { |
||||||
|
font-family: "ly-iconfont"; |
||||||
|
src: url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAPsAAsAAAAACKwAAAOeAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCDBgqFDIQPATYCJAMMCwgABCAFhG0HQBtfB8gekiSCdAwUAKgCFMA5Hj7H0PeTlABUr57PVyGqugqzSWJnNwWoWJjx/9rUr4TPL1ZSQpU2mycqwoRwIN3p+MkqMqyEW+OtMBLPSUBb8v//XtWMKTavxYIUsT/Wy1qbQzkBDOYEKGB7dVpPyVqgCnJNwvMvhZl10nMCtQbFoPVhY8ZDncJfF4grbqpQ13AqE52hWqgcOFrEQ6hWnW5VfMCD7Pfjn4WoI6nI/K0bl0MNGPBz0qcflVqYnvCA4vNDPUXGPFCIw8HgtsqiOK9SrW2smm6sVITElWlpISMdVBn8wyMJopLfXg+myZ48KCrSkvj9g37U1ItbXYke4APwXxK3N4TuehyBfmM0I3zbNdt7uk3VnjPtzX0rnIl7z7bZvb/thHohsu9QuykKo+Cws4nL7LsPmI3n2qN9B9upZEIKd4hu0NCKi0rt7fNtdl+I1N25hOJMDQK6odS123tROR7Pg8toEhDaF+kR0TYjxW6M58F5+ZNQOxmZHtE2g+IYjxjlNy/yIRQpCmrgq5R4/3jx8PvT8Ha8d3/xiLnt4EGyaDnznzRv8vpyZ+9TFHf/ntX9e59A+b6+fPHd5+dy0wYHVvHOroWbnWe879O9DnL53bN/gUHuwm28b/n8i/V3ry4E3IoXNqS6Rvs0LhJxeNVjoUkM3LKosU+0a6rh45FVvLt+2oz7Zd53b4QOy7/9snDXHbqVu+A+f8r7PnM2H8kXrWm5c8/vLu7LqRee7HW637mz3kHc5U/RCXf25d7G8tkdgEfwIpzpkknGpaMw3ww55q9Mn9OQNyua/wB/49OOWydn4eL/6roCfjx6FMmcxfJStYRKfd3UwoHiML4rF4uMSK+SvYTuNxMHrpl8yd3Q6v32cAeo/KFaowBJlQHIqo3zi3geKtRZhErVlqDWnOGn67QRKkWpwaw1AkKza5A0egFZszf8In4HFTp9h0rNUQm1NqP1lXUmgyuDBVUlNYi2gHA98FnokUreOZaac1xV1JlMMZGKEs+QdCLVrgynPhUcO0pzzYyUjDAReGSYeBl13YCEIrCpLhOWlGE+mWRD35TQAw8UawRKJVEGQrMAwekCPpaMlpTOz49FmeZwqcREX1t3Ikoo4dMTaQmpBfzhRn9R30uZXTKXKUOSmLSKEQIeYhjqKZcrcIzhMLLRrJMSrA35UF4yGMaWGhPHm733dwJq+Z/NkSJHUXemCirjgpuWrHMD1eC+mQUAAAA=') format('woff2'); |
||||||
|
} |
||||||
|
|
||||||
|
.ly-iconfont { |
||||||
|
font-family: "ly-iconfont" !important; |
||||||
|
font-size: 30rpx; |
||||||
|
font-style: normal; |
||||||
|
-webkit-font-smoothing: antialiased; |
||||||
|
-moz-osx-font-smoothing: grayscale; |
||||||
|
} |
||||||
|
|
||||||
|
.ly-icon-caret-right:before { |
||||||
|
content: "\e8ee"; |
||||||
|
} |
||||||
|
|
||||||
|
.ly-icon-loading:before { |
||||||
|
content: "\e657"; |
||||||
|
} |
||||||
|
/* iconfont-end */ |
||||||
|
|
||||||
|
/* animate-start */ |
||||||
|
@keyframes rotating { |
||||||
|
0% { |
||||||
|
-webkit-transform: rotateZ(0); |
||||||
|
transform: rotateZ(0) |
||||||
|
} |
||||||
|
|
||||||
|
100% { |
||||||
|
-webkit-transform: rotateZ(360deg); |
||||||
|
transform: rotateZ(360deg) |
||||||
|
} |
||||||
|
} |
||||||
|
/* animate-end */ |
||||||
|
.df{ |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
/* .image{ |
||||||
|
width: 15rpx; |
||||||
|
height: 15rpx; |
||||||
|
} */ |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,607 @@ |
|||||||
|
<template> |
||||||
|
<view> |
||||||
|
<template v-if="showLoading"> |
||||||
|
<view class="ly-loader ly-flex-center"> |
||||||
|
<view class="ly-loader-inner">加载中...</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
<template v-else> |
||||||
|
<view v-if="isEmpty || !visible" |
||||||
|
class="ly-empty"> |
||||||
|
{{emptyText}} |
||||||
|
</view> |
||||||
|
<view |
||||||
|
:key="updateKey" |
||||||
|
class="ly-tree" |
||||||
|
:class="{'is-empty': isEmpty || !visible}" |
||||||
|
role="tree" |
||||||
|
name="LyTreeExpand"> |
||||||
|
<ly-tree-node |
||||||
|
v-for="nodeId in childNodesId" |
||||||
|
:nodeId="nodeId" |
||||||
|
:render-after-expand="renderAfterExpand" |
||||||
|
:show-checkbox="showCheckbox" |
||||||
|
:show-radio="showRadio" |
||||||
|
:check-only-leaf="checkOnlyLeaf" |
||||||
|
:key="getNodeKey(nodeId)" |
||||||
|
:indent="indent" |
||||||
|
:icon-class="iconClass"> |
||||||
|
<text>我是值</text> |
||||||
|
</ly-tree-node> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import Vue from 'vue' |
||||||
|
import TreeStore from './model/tree-store.js'; |
||||||
|
import {getNodeKey} from './tool/util.js'; |
||||||
|
import LyTreeNode from './ly-tree-node.vue'; |
||||||
|
|
||||||
|
export default { |
||||||
|
name: 'LyTree', |
||||||
|
|
||||||
|
componentName: 'LyTree', |
||||||
|
|
||||||
|
components: { |
||||||
|
LyTreeNode |
||||||
|
}, |
||||||
|
|
||||||
|
data() { |
||||||
|
return { |
||||||
|
updateKey: new Date().getTime(), // 数据更新的时候,重新渲染树 |
||||||
|
elId: `ly_${Math.ceil(Math.random() * 10e5).toString(36)}`, |
||||||
|
visible: true, |
||||||
|
store: { |
||||||
|
ready: false |
||||||
|
}, |
||||||
|
currentNode: null, |
||||||
|
childNodesId: [] |
||||||
|
}; |
||||||
|
}, |
||||||
|
|
||||||
|
provide() { |
||||||
|
return { |
||||||
|
tree: this |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
props: { |
||||||
|
// 展示数据 |
||||||
|
treeData: Array, |
||||||
|
|
||||||
|
// 自主控制loading加载,避免数据还没获取到的空档出现“暂无数据”字样 |
||||||
|
ready: { |
||||||
|
type: Boolean, |
||||||
|
default: true |
||||||
|
}, |
||||||
|
|
||||||
|
// 内容为空的时候展示的文本 |
||||||
|
emptyText: { |
||||||
|
type: String, |
||||||
|
default: '暂无数据' |
||||||
|
}, |
||||||
|
|
||||||
|
// 是否在第一次展开某个树节点后才渲染其子节点 |
||||||
|
renderAfterExpand: { |
||||||
|
type: Boolean, |
||||||
|
default: true |
||||||
|
}, |
||||||
|
|
||||||
|
// 每个树节点用来作为唯一标识的属性,整棵树应该是唯一的 |
||||||
|
nodeKey: String, |
||||||
|
|
||||||
|
// 在显示复选框的情况下,是否严格的遵循父子不互相关联的做法,默认为 false |
||||||
|
checkStrictly: Boolean, |
||||||
|
|
||||||
|
// 是否默认展开所有节点 |
||||||
|
defaultExpandAll: Boolean, |
||||||
|
|
||||||
|
// 切换全部展开、全部折叠 |
||||||
|
toggleExpendAll: Boolean, |
||||||
|
|
||||||
|
// 是否在点击节点的时候展开或者收缩节点, 默认值为 true,如果为 false,则只有点箭头图标的时候才会展开或者收缩节点 |
||||||
|
expandOnClickNode: { |
||||||
|
type: Boolean, |
||||||
|
default: true |
||||||
|
}, |
||||||
|
|
||||||
|
// 选中的时候展开节点 |
||||||
|
expandOnCheckNode: { |
||||||
|
type: Boolean, |
||||||
|
default: true |
||||||
|
}, |
||||||
|
|
||||||
|
// 是否在点击节点的时候选中节点,默认值为 false,即只有在点击复选框时才会选中节点 |
||||||
|
checkOnClickNode: Boolean, |
||||||
|
checkDescendants: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
|
||||||
|
// 展开子节点的时候是否自动展开父节点 |
||||||
|
autoExpandParent: { |
||||||
|
type: Boolean, |
||||||
|
default: true |
||||||
|
}, |
||||||
|
|
||||||
|
// 默认勾选的节点的 key 的数组 |
||||||
|
defaultCheckedKeys: Array, |
||||||
|
|
||||||
|
// 默认展开的节点的 key 的数组 |
||||||
|
defaultExpandedKeys: Array, |
||||||
|
|
||||||
|
// 是否展开当前节点的父节点 |
||||||
|
expandCurrentNodeParent: Boolean, |
||||||
|
|
||||||
|
// 当前选中的节点 |
||||||
|
currentNodeKey: [String, Number], |
||||||
|
|
||||||
|
// 是否最后一层叶子节点才显示单选/多选框 |
||||||
|
checkOnlyLeaf: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
|
||||||
|
// 节点是否可被选择 |
||||||
|
showCheckbox: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
|
||||||
|
// 节点单选 |
||||||
|
showRadio: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
|
||||||
|
// 配置选项 |
||||||
|
props: { |
||||||
|
type: [Object, Function], |
||||||
|
default () { |
||||||
|
return { |
||||||
|
children: 'children', // 指定子树为节点对象的某个属性值 |
||||||
|
label: 'label', // 指定节点标签为节点对象的某个属性值 |
||||||
|
disabled: 'disabled' // 指定节点选择框是否禁用为节点对象的某个属性值 |
||||||
|
}; |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
// 是否懒加载子节点,需与 load 方法结合使用 |
||||||
|
lazy: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
|
||||||
|
// 是否高亮当前选中节点,默认值是 false |
||||||
|
highlightCurrent: Boolean, |
||||||
|
|
||||||
|
// 加载子树数据的方法,仅当 lazy 属性为true 时生效 |
||||||
|
load: Function, |
||||||
|
|
||||||
|
// 对树节点进行筛选时执行的方法,返回 true 表示这个节点可以显示,返回 false 则表示这个节点会被隐藏 |
||||||
|
filterNodeMethod: Function, |
||||||
|
|
||||||
|
// 搜索时是否展示匹配项的所有子节点 |
||||||
|
childVisibleForFilterNode: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
|
||||||
|
// 是否每次只打开一个同级树节点展开 |
||||||
|
accordion: Boolean, |
||||||
|
|
||||||
|
// 相邻级节点间的水平缩进,单位为像素 |
||||||
|
indent: { |
||||||
|
type: Number, |
||||||
|
default: 18 |
||||||
|
}, |
||||||
|
|
||||||
|
// 自定义树节点的展开图标 |
||||||
|
iconClass: String, |
||||||
|
|
||||||
|
// 是否显示节点图标,如果配置为true,需要配置props中对应的图标属性名称 |
||||||
|
showNodeIcon: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
|
||||||
|
// 当节点图标显示出错时,显示的默认图标 |
||||||
|
defaultNodeIcon: { |
||||||
|
type: String, |
||||||
|
default: 'https://img-cdn-qiniu.dcloud.net.cn/uniapp/doc/github.svg' |
||||||
|
}, |
||||||
|
|
||||||
|
// 如果数据量较大,建议不要在node节点中添加parent属性,会造成性能损耗 |
||||||
|
isInjectParentInNode: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
computed: { |
||||||
|
isEmpty() { |
||||||
|
if (this.store.root) { |
||||||
|
const childNodes = this.store.root.getChildNodes(this.childNodesId); |
||||||
|
|
||||||
|
return !childNodes || childNodes.length === 0 || childNodes.every(({visible}) => !visible); |
||||||
|
} |
||||||
|
|
||||||
|
return true; |
||||||
|
}, |
||||||
|
showLoading() { |
||||||
|
return !(this.store.ready && this.ready); |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
watch: { |
||||||
|
toggleExpendAll(newVal) { |
||||||
|
this.store.toggleExpendAll(newVal); |
||||||
|
}, |
||||||
|
defaultCheckedKeys(newVal) { |
||||||
|
this.store.setDefaultCheckedKey(newVal); |
||||||
|
}, |
||||||
|
defaultExpandedKeys(newVal) { |
||||||
|
this.store.defaultExpandedKeys = newVal; |
||||||
|
this.store.setDefaultExpandedKeys(newVal); |
||||||
|
}, |
||||||
|
checkStrictly(newVal) { |
||||||
|
this.store.checkStrictly = newVal || this.checkOnlyLeaf; |
||||||
|
}, |
||||||
|
'store.root.childNodesId'(newVal) { |
||||||
|
this.childNodesId = newVal; |
||||||
|
}, |
||||||
|
'store.root.visible'(newVal) { |
||||||
|
this.visible = newVal; |
||||||
|
}, |
||||||
|
childNodesId(){ |
||||||
|
this.$nextTick(() => { |
||||||
|
this.$emit('ly-tree-render-completed'); |
||||||
|
}); |
||||||
|
}, |
||||||
|
treeData: { |
||||||
|
handler(newVal) { |
||||||
|
this.updateKey = new Date().getTime(); |
||||||
|
this.store.setData(newVal); |
||||||
|
}, |
||||||
|
deep: true |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
methods: { |
||||||
|
/* |
||||||
|
* @description 对树节点进行筛选操作 |
||||||
|
* @method filter |
||||||
|
* @param {all} value 在 filter-node-method 中作为第一个参数 |
||||||
|
* @param {Object} data 搜索指定节点的节点数据,不传代表搜索所有节点,假如要搜索A节点下面的数据,那么nodeData代表treeData中A节点的数据 |
||||||
|
*/ |
||||||
|
filter(value, data) { |
||||||
|
if (!this.filterNodeMethod) throw new Error('[Tree] filterNodeMethod is required when filter'); |
||||||
|
this.store.filter(value, data); |
||||||
|
}, |
||||||
|
|
||||||
|
/* |
||||||
|
* @description 获取节点的唯一标识符 |
||||||
|
* @method getNodeKey |
||||||
|
* @param {String, Number} nodeId |
||||||
|
* @return {String, Number} 匹配到的数据中的某一项数据 |
||||||
|
*/ |
||||||
|
getNodeKey(nodeId) { |
||||||
|
let node = this.store.root.getChildNodes([nodeId])[0]; |
||||||
|
return getNodeKey(this.nodeKey, node.data); |
||||||
|
}, |
||||||
|
|
||||||
|
/* |
||||||
|
* @description 获取节点路径 |
||||||
|
* @method getNodePath |
||||||
|
* @param {Object} data 节点数据 |
||||||
|
* @return {Array} 路径数组 |
||||||
|
*/ |
||||||
|
getNodePath(data) { |
||||||
|
return this.store.getNodePath(data); |
||||||
|
}, |
||||||
|
|
||||||
|
/* |
||||||
|
* @description 若节点可被选择(即 show-checkbox 为 true),则返回目前被选中的节点所组成的数组 |
||||||
|
* @method getCheckedNodes |
||||||
|
* @param {Boolean} leafOnly 是否只是叶子节点,默认false |
||||||
|
* @param {Boolean} includeHalfChecked 是否包含半选节点,默认false |
||||||
|
* @return {Array} 目前被选中的节点所组成的数组 |
||||||
|
*/ |
||||||
|
getCheckedNodes(leafOnly, includeHalfChecked) { |
||||||
|
return this.store.getCheckedNodes(leafOnly, includeHalfChecked); |
||||||
|
}, |
||||||
|
|
||||||
|
/* |
||||||
|
* @description 若节点可被选择(即 show-checkbox 为 true),则返回目前被选中的节点的 key 所组成的数组 |
||||||
|
* @method getCheckedKeys |
||||||
|
* @param {Boolean} leafOnly 是否只是叶子节点,默认false,若为 true 则仅返回被选中的叶子节点的 keys |
||||||
|
* @param {Boolean} includeHalfChecked 是否返回indeterminate为true的节点,默认false |
||||||
|
* @return {Array} 目前被选中的节点所组成的数组 |
||||||
|
*/ |
||||||
|
getCheckedKeys(leafOnly, includeHalfChecked) { |
||||||
|
return this.store.getCheckedKeys(leafOnly, includeHalfChecked); |
||||||
|
}, |
||||||
|
|
||||||
|
/* |
||||||
|
* @description 获取当前被选中节点的 data,若没有节点被选中则返回 null |
||||||
|
* @method getCurrentNode |
||||||
|
* @return {Object} 当前被选中节点的 data,若没有节点被选中则返回 null |
||||||
|
*/ |
||||||
|
getCurrentNode() { |
||||||
|
const currentNode = this.store.getCurrentNode(); |
||||||
|
return currentNode ? currentNode.data : null; |
||||||
|
}, |
||||||
|
|
||||||
|
/* |
||||||
|
* @description 获取当前被选中节点的 key,若没有节点被选中则返回 null |
||||||
|
* @method getCurrentKey |
||||||
|
* @return {all} 当前被选中节点的 key, 若没有节点被选中则返回 null |
||||||
|
*/ |
||||||
|
getCurrentKey() { |
||||||
|
const currentNode = this.getCurrentNode(); |
||||||
|
return currentNode ? currentNode[this.nodeKey] : null; |
||||||
|
}, |
||||||
|
|
||||||
|
/* |
||||||
|
* @description 设置全选/取消全选 |
||||||
|
* @method setCheckAll |
||||||
|
* @param {Boolean} isCheckAll 选中状态,默认为true |
||||||
|
*/ |
||||||
|
setCheckAll(isCheckAll = true) { |
||||||
|
if (this.showRadio) throw new Error('You set the "show-radio" property, so you cannot select all nodes'); |
||||||
|
|
||||||
|
if (!this.showCheckbox) console.warn('You have not set the property "show-checkbox". Please check your settings'); |
||||||
|
|
||||||
|
this.store.setCheckAll(isCheckAll); |
||||||
|
}, |
||||||
|
|
||||||
|
/* |
||||||
|
* @description 设置目前勾选的节点 |
||||||
|
* @method setCheckedNodes |
||||||
|
* @param {Array} nodes 接收勾选节点数据的数组 |
||||||
|
* @param {Boolean} leafOnly 是否只是叶子节点, 若为 true 则仅设置叶子节点的选中状态,默认值为 false |
||||||
|
*/ |
||||||
|
setCheckedNodes(nodes, leafOnly) { |
||||||
|
this.store.setCheckedNodes(nodes, leafOnly); |
||||||
|
}, |
||||||
|
|
||||||
|
/* |
||||||
|
* @description 通过 keys 设置目前勾选的节点 |
||||||
|
* @method setCheckedKeys |
||||||
|
* @param {Array} keys 勾选节点的 key 的数组 |
||||||
|
* @param {Boolean} leafOnly 是否只是叶子节点, 若为 true 则仅设置叶子节点的选中状态,默认值为 false |
||||||
|
*/ |
||||||
|
setCheckedKeys(keys, leafOnly) { |
||||||
|
if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in setCheckedKeys'); |
||||||
|
this.store.setCheckedKeys(keys, leafOnly); |
||||||
|
}, |
||||||
|
|
||||||
|
/* |
||||||
|
* @description 通过 key / data 设置某个节点的勾选状态 |
||||||
|
* @method setChecked |
||||||
|
* @param {all} data 勾选节点的 key 或者 data |
||||||
|
* @param {Boolean} checked 节点是否选中 |
||||||
|
* @param {Boolean} deep 是否设置子节点 ,默认为 false |
||||||
|
*/ |
||||||
|
setChecked(data, checked, deep) { |
||||||
|
this.store.setChecked(data, checked, deep); |
||||||
|
}, |
||||||
|
|
||||||
|
/* |
||||||
|
* @description 若节点可被选择(即 show-checkbox 为 true),则返回目前半选中的节点所组成的数组 |
||||||
|
* @method getHalfCheckedNodes |
||||||
|
* @return {Array} 目前半选中的节点所组成的数组 |
||||||
|
*/ |
||||||
|
getHalfCheckedNodes() { |
||||||
|
return this.store.getHalfCheckedNodes(); |
||||||
|
}, |
||||||
|
|
||||||
|
/* |
||||||
|
* @description 若节点可被选择(即 show-checkbox 为 true),则返回目前半选中的节点的 key 所组成的数组 |
||||||
|
* @method getHalfCheckedKeys |
||||||
|
* @return {Array} 目前半选中的节点的 key 所组成的数组 |
||||||
|
*/ |
||||||
|
getHalfCheckedKeys() { |
||||||
|
return this.store.getHalfCheckedKeys(); |
||||||
|
}, |
||||||
|
|
||||||
|
/* |
||||||
|
* @description 通过 node 设置某个节点的当前选中状态 |
||||||
|
* @method setCurrentNode |
||||||
|
* @param {Object} node 待被选节点的 node |
||||||
|
*/ |
||||||
|
setCurrentNode(node) { |
||||||
|
if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in setCurrentNode'); |
||||||
|
this.store.setUserCurrentNode(node); |
||||||
|
}, |
||||||
|
|
||||||
|
/* |
||||||
|
* @description 通过 key 设置某个节点的当前选中状态 |
||||||
|
* @method setCurrentKey |
||||||
|
* @param {all} key 待被选节点的 key,若为 null 则取消当前高亮的节点 |
||||||
|
*/ |
||||||
|
setCurrentKey(key) { |
||||||
|
if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in setCurrentKey'); |
||||||
|
this.store.setCurrentNodeKey(key); |
||||||
|
}, |
||||||
|
|
||||||
|
/* |
||||||
|
* @description 根据 data 或者 key 拿到 Tree 组件中的 node |
||||||
|
* @method getNode |
||||||
|
* @param {all} data 要获得 node 的 key 或者 data |
||||||
|
*/ |
||||||
|
getNode(data) { |
||||||
|
return this.store.getNode(data); |
||||||
|
}, |
||||||
|
|
||||||
|
/* |
||||||
|
* @description 删除 Tree 中的一个节点 |
||||||
|
* @method remove |
||||||
|
* @param {all} data 要删除的节点的 data 或者 node |
||||||
|
*/ |
||||||
|
remove(data) { |
||||||
|
this.store.remove(data); |
||||||
|
}, |
||||||
|
|
||||||
|
/* |
||||||
|
* @description 为 Tree 中的一个节点追加一个子节点 |
||||||
|
* @method append |
||||||
|
* @param {Object} data 要追加的子节点的 data |
||||||
|
* @param {Object} parentNode 子节点的 parent 的 data、key 或者 node |
||||||
|
*/ |
||||||
|
append(data, parentNode) { |
||||||
|
this.store.append(data, parentNode); |
||||||
|
}, |
||||||
|
|
||||||
|
/* |
||||||
|
* @description 为 Tree 的一个节点的前面增加一个节点 |
||||||
|
* @method insertBefore |
||||||
|
* @param {Object} data 要增加的节点的 data |
||||||
|
* @param {all} refNode 要增加的节点的后一个节点的 data、key 或者 node |
||||||
|
*/ |
||||||
|
insertBefore(data, refNode) { |
||||||
|
this.store.insertBefore(data, refNode); |
||||||
|
}, |
||||||
|
|
||||||
|
/* |
||||||
|
* @description 为 Tree 的一个节点的后面增加一个节点 |
||||||
|
* @method insertAfter |
||||||
|
* @param {Object} data 要增加的节点的 data |
||||||
|
* @param {all} refNode 要增加的节点的前一个节点的 data、key 或者 node |
||||||
|
*/ |
||||||
|
insertAfter(data, refNode) { |
||||||
|
this.store.insertAfter(data, refNode); |
||||||
|
}, |
||||||
|
|
||||||
|
/* |
||||||
|
* @description 通过 keys 设置节点子元素 |
||||||
|
* @method updateKeyChildren |
||||||
|
* @param {String, Number} key 节点 key |
||||||
|
* @param {Object} data 节点数据的数组 |
||||||
|
*/ |
||||||
|
updateKeyChildren(key, data) { |
||||||
|
if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in updateKeyChild'); |
||||||
|
this.store.updateChildren(key, data); |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
created() { |
||||||
|
this.isTree = true; |
||||||
|
|
||||||
|
let props = this.props; |
||||||
|
if (typeof this.props === 'function') props = this.props(); |
||||||
|
if (typeof props !== 'object') throw new Error('props must be of object type.'); |
||||||
|
|
||||||
|
this.store = new TreeStore({ |
||||||
|
key: this.nodeKey, |
||||||
|
data: this.treeData, |
||||||
|
lazy: this.lazy, |
||||||
|
props: props, |
||||||
|
load: this.load, |
||||||
|
showCheckbox: this.showCheckbox, |
||||||
|
showRadio: this.showRadio, |
||||||
|
currentNodeKey: this.currentNodeKey, |
||||||
|
checkStrictly: this.checkStrictly || this.checkOnlyLeaf, |
||||||
|
checkDescendants: this.checkDescendants, |
||||||
|
expandOnCheckNode: this.expandOnCheckNode, |
||||||
|
defaultCheckedKeys: this.defaultCheckedKeys, |
||||||
|
defaultExpandedKeys: this.defaultExpandedKeys, |
||||||
|
expandCurrentNodeParent: this.expandCurrentNodeParent, |
||||||
|
autoExpandParent: this.autoExpandParent, |
||||||
|
defaultExpandAll: this.defaultExpandAll, |
||||||
|
filterNodeMethod: this.filterNodeMethod, |
||||||
|
childVisibleForFilterNode: this.childVisibleForFilterNode, |
||||||
|
showNodeIcon: this.showNodeIcon, |
||||||
|
isInjectParentInNode: this.isInjectParentInNode |
||||||
|
}); |
||||||
|
|
||||||
|
this.childNodesId = this.store.root.childNodesId; |
||||||
|
}, |
||||||
|
|
||||||
|
beforeDestroy() { |
||||||
|
if (this.accordion) { |
||||||
|
uni.$off(`${this.elId}-tree-node-expand`) |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
</script> |
||||||
|
|
||||||
|
<style> |
||||||
|
.ly-tree { |
||||||
|
position: relative; |
||||||
|
cursor: default; |
||||||
|
background: #FFF; |
||||||
|
color: #606266; |
||||||
|
padding: 30rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.ly-tree.is-empty { |
||||||
|
background: transparent; |
||||||
|
} |
||||||
|
|
||||||
|
/* lyEmpty-start */ |
||||||
|
.ly-empty { |
||||||
|
width: 100%; |
||||||
|
display: flex; |
||||||
|
justify-content: center; |
||||||
|
margin-top: 100rpx; |
||||||
|
} |
||||||
|
/* lyEmpty-end */ |
||||||
|
|
||||||
|
/* lyLoader-start */ |
||||||
|
.ly-loader { |
||||||
|
margin-top: 100rpx; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
justify-content: center; |
||||||
|
} |
||||||
|
|
||||||
|
.ly-loader-inner, |
||||||
|
.ly-loader-inner:before, |
||||||
|
.ly-loader-inner:after { |
||||||
|
background: #efefef; |
||||||
|
animation: load 1s infinite ease-in-out; |
||||||
|
width: .5em; |
||||||
|
height: 1em; |
||||||
|
} |
||||||
|
|
||||||
|
.ly-loader-inner:before, |
||||||
|
.ly-loader-inner:after { |
||||||
|
position: absolute; |
||||||
|
top: 0; |
||||||
|
content: ''; |
||||||
|
} |
||||||
|
|
||||||
|
.ly-loader-inner:before { |
||||||
|
left: -1em; |
||||||
|
} |
||||||
|
|
||||||
|
.ly-loader-inner { |
||||||
|
text-indent: -9999em; |
||||||
|
position: relative; |
||||||
|
font-size: 22rpx; |
||||||
|
animation-delay: 0.16s; |
||||||
|
} |
||||||
|
|
||||||
|
.ly-loader-inner:after { |
||||||
|
left: 1em; |
||||||
|
animation-delay: 0.32s; |
||||||
|
} |
||||||
|
/* lyLoader-end */ |
||||||
|
|
||||||
|
@keyframes load { |
||||||
|
0%, |
||||||
|
80%, |
||||||
|
100% { |
||||||
|
box-shadow: 0 0 #efefef; |
||||||
|
height: 1em; |
||||||
|
} |
||||||
|
|
||||||
|
40% { |
||||||
|
box-shadow: 0 -1.5em #efefef; |
||||||
|
height: 1.5em; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,538 @@ |
|||||||
|
import { |
||||||
|
markNodeData, |
||||||
|
objectAssign, |
||||||
|
arrayFindIndex, |
||||||
|
getChildState, |
||||||
|
reInitChecked, |
||||||
|
getPropertyFromData, |
||||||
|
isNull, |
||||||
|
NODE_KEY |
||||||
|
} from '../tool/util'; |
||||||
|
|
||||||
|
const getStore = function(store) { |
||||||
|
let thisStore = store; |
||||||
|
|
||||||
|
return function() { |
||||||
|
return thisStore; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
let nodeIdSeed = 0; |
||||||
|
|
||||||
|
export default class Node { |
||||||
|
constructor(options) { |
||||||
|
this.time = new Date().getTime(); |
||||||
|
this.id = nodeIdSeed++; |
||||||
|
this.text = null; |
||||||
|
this.checked = false; |
||||||
|
this.indeterminate = false; |
||||||
|
this.data = null; |
||||||
|
this.expanded = false; |
||||||
|
this.parentId = null; |
||||||
|
this.visible = true; |
||||||
|
this.isCurrent = false; |
||||||
|
|
||||||
|
for (let name in options) { |
||||||
|
if (options.hasOwnProperty(name)) { |
||||||
|
if (name === 'store') { |
||||||
|
this.store = getStore(options[name]); |
||||||
|
} else { |
||||||
|
this[name] = options[name]; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (!this.store()) { |
||||||
|
throw new Error('[Node]store is required!'); |
||||||
|
} |
||||||
|
|
||||||
|
// internal
|
||||||
|
this.level = 0; |
||||||
|
this.loaded = false; |
||||||
|
this.childNodesId = []; |
||||||
|
this.loading = false; |
||||||
|
this.label = getPropertyFromData(this, 'label'); |
||||||
|
this.key = this._getKey(); |
||||||
|
this.disabled = getPropertyFromData(this, 'disabled'); |
||||||
|
this.nextSibling = null; |
||||||
|
this.previousSibling = null; |
||||||
|
this.icon = ''; |
||||||
|
|
||||||
|
this._handleParentAndLevel(); |
||||||
|
this._handleProps(); |
||||||
|
this._handleExpand(); |
||||||
|
this._handleCurrent(); |
||||||
|
|
||||||
|
if (this.store().lazy) { |
||||||
|
this.store()._initDefaultCheckedNode(this); |
||||||
|
} |
||||||
|
|
||||||
|
this.updateLeafState(); |
||||||
|
} |
||||||
|
|
||||||
|
_getKey() { |
||||||
|
if (!this.data || Array.isArray(this.data)) return null; |
||||||
|
|
||||||
|
if (typeof this.data === 'object') { |
||||||
|
const nodeKey = this.store().key; |
||||||
|
const key = this.data[nodeKey]; |
||||||
|
|
||||||
|
if (typeof key === 'undefined') { |
||||||
|
throw new Error(`您配置的node-key为"${nodeKey}",但数据中并未找到对应"${nodeKey}"属性的值,请检查node-key的配置是否合理`) |
||||||
|
} |
||||||
|
|
||||||
|
return key; |
||||||
|
} |
||||||
|
|
||||||
|
throw new Error('不合法的data数据'); |
||||||
|
} |
||||||
|
|
||||||
|
_handleParentAndLevel() { |
||||||
|
if (this.parentId !== null) { |
||||||
|
let parent = this.getParent(this.parentId); |
||||||
|
|
||||||
|
if (this.store().isInjectParentInNode) { |
||||||
|
this.parent = parent; |
||||||
|
} |
||||||
|
|
||||||
|
// 由于这里做了修改,默认第一个对象不会被注册到nodesMap中,所以找不到parent会报错,所以默认parent的level是0
|
||||||
|
if (!parent) { |
||||||
|
parent = { |
||||||
|
level: 0 |
||||||
|
} |
||||||
|
} else { |
||||||
|
const parentChildNodes = parent.getChildNodes(parent.childNodesId); |
||||||
|
const index = parent.childNodesId.indexOf(this.key); |
||||||
|
this.nextSibling = index > -1 ? parentChildNodes[index + 1] : null; |
||||||
|
this.previousSibling = index > 0 ? parentChildNodes[index - 1] : null; |
||||||
|
} |
||||||
|
this.level = parent.level + 1; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
_handleProps() { |
||||||
|
const props = this.store().props; |
||||||
|
|
||||||
|
if (this.store().showNodeIcon) { |
||||||
|
if (props && typeof props.icon !== 'undefined') { |
||||||
|
this.icon = getPropertyFromData(this, 'icon'); |
||||||
|
} else { |
||||||
|
console.warn('请配置props属性中的"icon"字段') |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
this.store().registerNode(this); |
||||||
|
|
||||||
|
if (props && typeof props.isLeaf !== 'undefined') { |
||||||
|
const isLeaf = getPropertyFromData(this, 'isLeaf'); |
||||||
|
if (typeof isLeaf === 'boolean') { |
||||||
|
this.isLeafByUser = isLeaf; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
_handleExpand() { |
||||||
|
if (this.store().lazy !== true && this.data) { |
||||||
|
this.setData(this.data); |
||||||
|
|
||||||
|
if (this.store().defaultExpandAll) { |
||||||
|
this.expanded = true; |
||||||
|
} |
||||||
|
} else if (this.level > 0 && this.store().lazy && this.store().defaultExpandAll) { |
||||||
|
this.expand(); |
||||||
|
} |
||||||
|
|
||||||
|
if (!Array.isArray(this.data)) { |
||||||
|
markNodeData(this, this.data); |
||||||
|
} |
||||||
|
|
||||||
|
if (!this.data) return; |
||||||
|
|
||||||
|
const defaultExpandedKeys = this.store().defaultExpandedKeys; |
||||||
|
const key = this.store().key; |
||||||
|
if (key && defaultExpandedKeys && defaultExpandedKeys.indexOf(this.key) !== -1) { |
||||||
|
this.expand(null, this.store().autoExpandparent); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
_handleCurrent() { |
||||||
|
const key = this.store().key; |
||||||
|
|
||||||
|
if (key && this.store().currentNodeKey !== undefined && this.key === this.store().currentNodeKey) { |
||||||
|
this.store().currentNode = this; |
||||||
|
this.store().currentNode.isCurrent = true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
destroyStore() { |
||||||
|
getStore(null) |
||||||
|
} |
||||||
|
|
||||||
|
setData(data) { |
||||||
|
if (!Array.isArray(data)) { |
||||||
|
markNodeData(this, data); |
||||||
|
} |
||||||
|
|
||||||
|
this.data = data; |
||||||
|
this.childNodesId = []; |
||||||
|
|
||||||
|
let children; |
||||||
|
if (this.level === 0 && Array.isArray(this.data)) { |
||||||
|
children = this.data; |
||||||
|
} else { |
||||||
|
children = getPropertyFromData(this, 'children') || []; |
||||||
|
} |
||||||
|
|
||||||
|
for (let i = 0, j = children.length; i < j; i++) { |
||||||
|
this.insertChild({ |
||||||
|
data: children[i] |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
contains(target, deep = true) { |
||||||
|
const walk = function(parent) { |
||||||
|
const children = parent.getChildNodes(parent.childNodesId) || []; |
||||||
|
let result = false; |
||||||
|
for (let i = 0, j = children.length; i < j; i++) { |
||||||
|
const child = children[i]; |
||||||
|
if (child === target || (deep && walk(child))) { |
||||||
|
result = true; |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
return result; |
||||||
|
}; |
||||||
|
|
||||||
|
return walk(this); |
||||||
|
} |
||||||
|
|
||||||
|
remove() { |
||||||
|
if (this.parentId !== null) { |
||||||
|
const parent = this.getParent(this.parentId); |
||||||
|
parent.removeChild(this); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
insertChild(child, index, batch) { |
||||||
|
if (!child) throw new Error('insertChild error: child is required.'); |
||||||
|
|
||||||
|
if (!(child instanceof Node)) { |
||||||
|
if (!batch) { |
||||||
|
const children = this.getChildren(true); |
||||||
|
if (children.indexOf(child.data) === -1) { |
||||||
|
if (typeof index === 'undefined' || index < 0) { |
||||||
|
children.push(child.data); |
||||||
|
} else { |
||||||
|
children.splice(index, 0, child.data); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
objectAssign(child, { |
||||||
|
parentId: isNull(this.key) ? '' : this.key, |
||||||
|
store: this.store() |
||||||
|
}); |
||||||
|
child = new Node(child); |
||||||
|
} |
||||||
|
|
||||||
|
child.level = this.level + 1; |
||||||
|
|
||||||
|
if (typeof index === 'undefined' || index < 0) { |
||||||
|
this.childNodesId.push(child.key); |
||||||
|
} else { |
||||||
|
this.childNodesId.splice(index, 0, child.key); |
||||||
|
} |
||||||
|
|
||||||
|
this.updateLeafState(); |
||||||
|
} |
||||||
|
|
||||||
|
insertBefore(child, ref) { |
||||||
|
let index; |
||||||
|
if (ref) { |
||||||
|
index = this.childNodesId.indexOf(ref.id); |
||||||
|
} |
||||||
|
this.insertChild(child, index); |
||||||
|
} |
||||||
|
|
||||||
|
insertAfter(child, ref) { |
||||||
|
let index; |
||||||
|
if (ref) { |
||||||
|
index = this.childNodesId.indexOf(ref.id); |
||||||
|
if (index !== -1) index += 1; |
||||||
|
} |
||||||
|
this.insertChild(child, index); |
||||||
|
} |
||||||
|
|
||||||
|
removeChild(child) { |
||||||
|
const children = this.getChildren() || []; |
||||||
|
const dataIndex = children.indexOf(child.data); |
||||||
|
if (dataIndex > -1) { |
||||||
|
children.splice(dataIndex, 1); |
||||||
|
} |
||||||
|
|
||||||
|
const index = this.childNodesId.indexOf(child.key); |
||||||
|
|
||||||
|
if (index > -1) { |
||||||
|
this.store() && this.store().deregisterNode(child); |
||||||
|
child.parentId = null; |
||||||
|
this.childNodesId.splice(index, 1); |
||||||
|
} |
||||||
|
|
||||||
|
this.updateLeafState(); |
||||||
|
} |
||||||
|
|
||||||
|
removeChildByData(data) { |
||||||
|
let targetNode = null; |
||||||
|
|
||||||
|
for (let i = 0; i < this.childNodesId.length; i++) { |
||||||
|
let node = this.getChildNodes(this.childNodesId); |
||||||
|
if (node[i].data === data) { |
||||||
|
targetNode = node[i]; |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (targetNode) { |
||||||
|
this.removeChild(targetNode); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 为了避免APP端parent嵌套结构导致报错,这里parent需要从nodesMap中获取
|
||||||
|
getParent(parentId) { |
||||||
|
try { |
||||||
|
if (!parentId.toString()) return null; |
||||||
|
return this.store().nodesMap[parentId]; |
||||||
|
} catch (error) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 为了避免APP端childNodes嵌套结构导致报错,这里childNodes需要从nodesMap中获取
|
||||||
|
getChildNodes(childNodesId) { |
||||||
|
let childNodes = []; |
||||||
|
if (childNodesId.length === 0) return childNodes; |
||||||
|
childNodesId.forEach((key) => { |
||||||
|
childNodes.push(this.store().nodesMap[key]); |
||||||
|
}) |
||||||
|
return childNodes; |
||||||
|
} |
||||||
|
|
||||||
|
expand(callback, expandparent) { |
||||||
|
const done = () => { |
||||||
|
if (expandparent) { |
||||||
|
let parent = this.getParent(this.parentId); |
||||||
|
while (parent && parent.level > 0) { |
||||||
|
parent.expanded = true; |
||||||
|
parent = this.getParent(parent.parentId); |
||||||
|
} |
||||||
|
} |
||||||
|
this.expanded = true; |
||||||
|
if (callback) callback(); |
||||||
|
}; |
||||||
|
|
||||||
|
if (this.shouldLoadData()) { |
||||||
|
this.loadData(function(data) { |
||||||
|
if (Array.isArray(data)) { |
||||||
|
if (this.checked) { |
||||||
|
this.setChecked(true, true); |
||||||
|
} else if (!this.store().checkStrictly) { |
||||||
|
reInitChecked(this); |
||||||
|
} |
||||||
|
done(); |
||||||
|
} |
||||||
|
}); |
||||||
|
} else { |
||||||
|
done(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
doCreateChildren(array, defaultProps = {}) { |
||||||
|
array.forEach((item) => { |
||||||
|
this.insertChild(objectAssign({ |
||||||
|
data: item |
||||||
|
}, defaultProps), undefined, true); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
collapse() { |
||||||
|
this.expanded = false; |
||||||
|
} |
||||||
|
|
||||||
|
shouldLoadData() { |
||||||
|
return this.store().lazy === true && this.store().load && !this.loaded; |
||||||
|
} |
||||||
|
|
||||||
|
updateLeafState() { |
||||||
|
if (this.store().lazy === true && this.loaded !== true && typeof this.isLeafByUser !== 'undefined') { |
||||||
|
this.isLeaf = this.isLeafByUser; |
||||||
|
return; |
||||||
|
} |
||||||
|
const childNodesId = this.childNodesId; |
||||||
|
if (!this.store().lazy || (this.store().lazy === true && this.loaded === true)) { |
||||||
|
this.isLeaf = !childNodesId || childNodesId.length === 0; |
||||||
|
return; |
||||||
|
} |
||||||
|
this.isLeaf = false; |
||||||
|
} |
||||||
|
|
||||||
|
setChecked(value, deep, recursion, passValue) { |
||||||
|
this.indeterminate = value === 'half'; |
||||||
|
this.checked = value === true; |
||||||
|
|
||||||
|
if (this.checked && this.store().expandOnCheckNode) { |
||||||
|
this.expand(null, true) |
||||||
|
} |
||||||
|
|
||||||
|
if (this.store().checkStrictly) return; |
||||||
|
if (this.store().showRadio) return; |
||||||
|
|
||||||
|
if (!(this.shouldLoadData() && !this.store().checkDescendants)) { |
||||||
|
let childNodes = this.getChildNodes(this.childNodesId); |
||||||
|
let { |
||||||
|
all, |
||||||
|
allWithoutDisable |
||||||
|
} = getChildState(childNodes); |
||||||
|
|
||||||
|
if (!this.isLeaf && (!all && allWithoutDisable)) { |
||||||
|
this.checked = false; |
||||||
|
value = false; |
||||||
|
} |
||||||
|
|
||||||
|
const handleDescendants = () => { |
||||||
|
if (deep) { |
||||||
|
let childNodes = this.getChildNodes(this.childNodesId) |
||||||
|
for (let i = 0, j = childNodes.length; i < j; i++) { |
||||||
|
const child = childNodes[i]; |
||||||
|
passValue = passValue || value !== false; |
||||||
|
const isCheck = child.disabled ? child.checked : passValue; |
||||||
|
child.setChecked(isCheck, deep, true, passValue); |
||||||
|
} |
||||||
|
const { |
||||||
|
half, |
||||||
|
all |
||||||
|
} = getChildState(childNodes); |
||||||
|
|
||||||
|
if (!all) { |
||||||
|
this.checked = all; |
||||||
|
this.indeterminate = half; |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
if (this.shouldLoadData()) { |
||||||
|
this.loadData(() => { |
||||||
|
handleDescendants(); |
||||||
|
reInitChecked(this); |
||||||
|
}, { |
||||||
|
checked: value !== false |
||||||
|
}); |
||||||
|
return; |
||||||
|
} else { |
||||||
|
handleDescendants(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (!this.parentId) return; |
||||||
|
|
||||||
|
let parent = this.getParent(this.parentId); |
||||||
|
if (parent && parent.level === 0) return; |
||||||
|
|
||||||
|
if (!recursion) { |
||||||
|
reInitChecked(parent); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
setRadioChecked(value) { |
||||||
|
const allNodes = this.store()._getAllNodes().sort((a, b) => b.level - a.level); |
||||||
|
allNodes.forEach(node => node.setChecked(false, false)); |
||||||
|
this.checked = value === true; |
||||||
|
} |
||||||
|
|
||||||
|
getChildren(forceInit = false) { |
||||||
|
if (this.level === 0) return this.data; |
||||||
|
const data = this.data; |
||||||
|
if (!data) return null; |
||||||
|
|
||||||
|
const props = this.store().props; |
||||||
|
let children = 'children'; |
||||||
|
if (props) { |
||||||
|
children = props.children || 'children'; |
||||||
|
} |
||||||
|
|
||||||
|
if (data[children] === undefined) { |
||||||
|
data[children] = null; |
||||||
|
} |
||||||
|
|
||||||
|
if (forceInit && !data[children]) { |
||||||
|
data[children] = []; |
||||||
|
} |
||||||
|
|
||||||
|
return data[children]; |
||||||
|
} |
||||||
|
|
||||||
|
updateChildren() { |
||||||
|
let childNodes = this.getChildNodes(this.childNodesId); |
||||||
|
const newData = this.getChildren() || []; |
||||||
|
const oldData = childNodes.map((node) => node.data); |
||||||
|
|
||||||
|
const newDataMap = {}; |
||||||
|
const newNodes = []; |
||||||
|
|
||||||
|
newData.forEach((item, index) => { |
||||||
|
const key = item[NODE_KEY]; |
||||||
|
const isNodeExists = !!key && arrayFindIndex(oldData, data => data[NODE_KEY] === key) >= 0; |
||||||
|
if (isNodeExists) { |
||||||
|
newDataMap[key] = { |
||||||
|
index, |
||||||
|
data: item |
||||||
|
}; |
||||||
|
} else { |
||||||
|
newNodes.push({ |
||||||
|
index, |
||||||
|
data: item |
||||||
|
}); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
if (!this.store().lazy) { |
||||||
|
oldData.forEach((item) => { |
||||||
|
if (!newDataMap[item[NODE_KEY]]) this.removeChildByData(item); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
newNodes.forEach(({ |
||||||
|
index, |
||||||
|
data |
||||||
|
}) => { |
||||||
|
this.insertChild({ |
||||||
|
data |
||||||
|
}, index); |
||||||
|
}); |
||||||
|
|
||||||
|
this.updateLeafState(); |
||||||
|
} |
||||||
|
|
||||||
|
loadData(callback, defaultProps = {}) { |
||||||
|
if (this.store().lazy === true &&
|
||||||
|
this.store().load && !this.loaded &&
|
||||||
|
(!this.loading || Object.keys(defaultProps).length) |
||||||
|
) { |
||||||
|
this.loading = true; |
||||||
|
|
||||||
|
const resolve = (children) => { |
||||||
|
this.loaded = true; |
||||||
|
this.loading = false; |
||||||
|
this.childNodesId = []; |
||||||
|
this.doCreateChildren(children, defaultProps); |
||||||
|
this.updateLeafState(); |
||||||
|
|
||||||
|
callback && callback.call(this,children); |
||||||
|
}; |
||||||
|
|
||||||
|
this.store().load(this, resolve); |
||||||
|
} else { |
||||||
|
callback && callback.call(this); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,419 @@ |
|||||||
|
import Node from './node'; |
||||||
|
import { |
||||||
|
getNodeKey, |
||||||
|
getPropertyFromData |
||||||
|
} from '../tool/util'; |
||||||
|
|
||||||
|
export default class TreeStore { |
||||||
|
constructor(options) { |
||||||
|
this.ready = false; |
||||||
|
this.currentNode = null; |
||||||
|
this.currentNodeKey = null; |
||||||
|
|
||||||
|
Object.assign(this, options); |
||||||
|
|
||||||
|
if (!this.key) { |
||||||
|
throw new Error('[Tree] nodeKey is required'); |
||||||
|
} |
||||||
|
|
||||||
|
this.nodesMap = {}; |
||||||
|
this.root = new Node({ |
||||||
|
data: this.data, |
||||||
|
store: this |
||||||
|
}); |
||||||
|
|
||||||
|
if (this.lazy && this.load) { |
||||||
|
const loadFn = this.load; |
||||||
|
loadFn(this.root, (data) => { |
||||||
|
this.root.doCreateChildren(data); |
||||||
|
this._initDefaultCheckedNodes(); |
||||||
|
this.ready = true; |
||||||
|
}); |
||||||
|
} else { |
||||||
|
this._initDefaultCheckedNodes(); |
||||||
|
this.ready = true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
filter(value, data) { |
||||||
|
const filterNodeMethod = this.filterNodeMethod; |
||||||
|
const lazy = this.lazy; |
||||||
|
const _self = this; |
||||||
|
const traverse = function(node) { |
||||||
|
const childNodes = node.root ? node.root.getChildNodes(node.root.childNodesId) : node.getChildNodes(node.childNodesId); |
||||||
|
|
||||||
|
childNodes.forEach((child) => { |
||||||
|
if (data && typeof data === 'object') { |
||||||
|
let nodePath = _self.getNodePath(child.data); |
||||||
|
if (!nodePath.some(pathItem => pathItem[_self.key] === data[_self.key])) { |
||||||
|
child.visible = false; |
||||||
|
traverse(child); |
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (_self.childVisibleForFilterNode) { |
||||||
|
let parent = child.getParent(child.parentId); |
||||||
|
child.visible = filterNodeMethod.call(child, value, child.data, child) || (parent && parent.visible); |
||||||
|
} else { |
||||||
|
child.visible = filterNodeMethod.call(child, value, child.data, child); |
||||||
|
} |
||||||
|
|
||||||
|
traverse(child); |
||||||
|
}); |
||||||
|
|
||||||
|
if (!node.visible && childNodes.length) { |
||||||
|
let allHidden = true; |
||||||
|
allHidden = !childNodes.some(child => child.visible); |
||||||
|
|
||||||
|
if (node.root) { |
||||||
|
node.root.visible = allHidden === false; |
||||||
|
} else { |
||||||
|
node.visible = allHidden === false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (!value) return; |
||||||
|
|
||||||
|
if (node.visible && !node.isLeaf && !lazy) node.expand(); |
||||||
|
}; |
||||||
|
|
||||||
|
traverse(this); |
||||||
|
} |
||||||
|
|
||||||
|
setData(newVal) { |
||||||
|
const instanceChanged = newVal !== this.root.data; |
||||||
|
if (instanceChanged) { |
||||||
|
this.root.setData(newVal); |
||||||
|
this._initDefaultCheckedNodes(); |
||||||
|
} else { |
||||||
|
this.root.updateChildren(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
getNode(data) { |
||||||
|
if (data instanceof Node) return data; |
||||||
|
const key = typeof data !== 'object' ? data : getNodeKey(this.key, data); |
||||||
|
if (!key) return null; |
||||||
|
return this.nodesMap[key] || null; |
||||||
|
} |
||||||
|
|
||||||
|
insertBefore(data, refData) { |
||||||
|
const refNode = this.getNode(refData); |
||||||
|
let parent = refNode.getParent(refNode.parentId); |
||||||
|
parent.insertBefore({ |
||||||
|
data |
||||||
|
}, refNode); |
||||||
|
} |
||||||
|
|
||||||
|
insertAfter(data, refData) { |
||||||
|
const refNode = this.getNode(refData); |
||||||
|
let parent = refNode.getParent(refNode.parentId); |
||||||
|
parent.insertAfter({ |
||||||
|
data |
||||||
|
}, refNode); |
||||||
|
} |
||||||
|
|
||||||
|
remove(data) { |
||||||
|
const node = this.getNode(data); |
||||||
|
|
||||||
|
if (node && node.parentId !== null) { |
||||||
|
let parent = node.getParent(node.parentId); |
||||||
|
if (node === this.currentNode) { |
||||||
|
this.currentNode = null; |
||||||
|
} |
||||||
|
parent.removeChild(node); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
append(data, parentData) { |
||||||
|
const parentNode = parentData ? this.getNode(parentData) : this.root; |
||||||
|
|
||||||
|
if (parentNode) { |
||||||
|
parentNode.insertChild({ |
||||||
|
data |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
_initDefaultCheckedNodes() { |
||||||
|
const defaultCheckedKeys = this.defaultCheckedKeys || []; |
||||||
|
const nodesMap = this.nodesMap; |
||||||
|
let checkedKeyfromData = []; |
||||||
|
let totalCheckedKeys = [] |
||||||
|
|
||||||
|
for (let key in nodesMap) { |
||||||
|
let checked = getPropertyFromData(nodesMap[key], 'checked') || false; |
||||||
|
checked && checkedKeyfromData.push(key); |
||||||
|
} |
||||||
|
|
||||||
|
totalCheckedKeys = Array.from(new Set([...defaultCheckedKeys, ...checkedKeyfromData])); |
||||||
|
totalCheckedKeys.forEach((checkedKey) => { |
||||||
|
const node = nodesMap[checkedKey]; |
||||||
|
|
||||||
|
if (node) { |
||||||
|
node.setChecked(true, !this.checkStrictly); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
_initDefaultCheckedNode(node) { |
||||||
|
const defaultCheckedKeys = this.defaultCheckedKeys || []; |
||||||
|
|
||||||
|
if (defaultCheckedKeys.indexOf(node.key) !== -1) { |
||||||
|
node.setChecked(true, !this.checkStrictly); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
toggleExpendAll(isExpandAll) { |
||||||
|
const allNodes = this._getAllNodes(); |
||||||
|
|
||||||
|
allNodes.forEach(item => { |
||||||
|
const node = this.getNode(item.key);
|
||||||
|
|
||||||
|
if (node) isExpandAll ? node.expand() : node.collapse(); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
setCheckAll(isCkeckAll) { |
||||||
|
const allNodes = this._getAllNodes(); |
||||||
|
|
||||||
|
allNodes.forEach(item => { |
||||||
|
item.setChecked(isCkeckAll, false); |
||||||
|
});
|
||||||
|
} |
||||||
|
|
||||||
|
setDefaultCheckedKey(newVal) { |
||||||
|
if (newVal !== this.defaultCheckedKeys) { |
||||||
|
this.defaultCheckedKeys = newVal; |
||||||
|
this._initDefaultCheckedNodes(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
registerNode(node) { |
||||||
|
|
||||||
|
const key = this.key; |
||||||
|
if (!key || !node || !node.data) return; |
||||||
|
|
||||||
|
const nodeKey = node.key; |
||||||
|
if (nodeKey !== undefined) this.nodesMap[node.key] = node; |
||||||
|
} |
||||||
|
|
||||||
|
deregisterNode(node) { |
||||||
|
const key = this.key; |
||||||
|
if (!key || !node || !node.data) return; |
||||||
|
|
||||||
|
let childNodes = node.getChildNodes(node.childNodesId); |
||||||
|
childNodes.forEach(child => { |
||||||
|
this.deregisterNode(child); |
||||||
|
}); |
||||||
|
|
||||||
|
delete this.nodesMap[node.key]; |
||||||
|
} |
||||||
|
|
||||||
|
getNodePath(data) { |
||||||
|
if (!this.key) throw new Error('[Tree] nodeKey is required in getNodePath'); |
||||||
|
const node = this.getNode(data); |
||||||
|
if (!node) return []; |
||||||
|
|
||||||
|
const path = [node.data]; |
||||||
|
let parent = node.getParent(node.parentId); |
||||||
|
while (parent && parent !== this.root) { |
||||||
|
path.push(parent.data); |
||||||
|
parent = parent.getParent(parent.parentId); |
||||||
|
} |
||||||
|
return path.reverse(); |
||||||
|
} |
||||||
|
|
||||||
|
getCheckedNodes(leafOnly = false, includeHalfChecked = false) { |
||||||
|
const checkedNodes = []; |
||||||
|
const traverse = function(node) { |
||||||
|
const childNodes = node.root ? node.root.getChildNodes(node.root.childNodesId) : node.getChildNodes(node.childNodesId); |
||||||
|
|
||||||
|
childNodes.forEach((child) => { |
||||||
|
if ((child.checked || (includeHalfChecked && child.indeterminate)) && (!leafOnly || (leafOnly && child.isLeaf))) { |
||||||
|
checkedNodes.push(child.data); |
||||||
|
} |
||||||
|
|
||||||
|
traverse(child); |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
traverse(this); |
||||||
|
|
||||||
|
return checkedNodes; |
||||||
|
} |
||||||
|
|
||||||
|
getCheckedKeys(leafOnly = false, includeHalfChecked = false) { |
||||||
|
return this.getCheckedNodes(leafOnly, includeHalfChecked).map((data) => (data || {})[this.key]); |
||||||
|
} |
||||||
|
|
||||||
|
getHalfCheckedNodes() { |
||||||
|
const nodes = []; |
||||||
|
const traverse = function(node) { |
||||||
|
const childNodes = node.root ? node.root.getChildNodes(node.root.childNodesId) : node.getChildNodes(node.childNodesId); |
||||||
|
|
||||||
|
childNodes.forEach((child) => { |
||||||
|
if (child.indeterminate) { |
||||||
|
nodes.push(child.data); |
||||||
|
} |
||||||
|
|
||||||
|
traverse(child); |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
traverse(this); |
||||||
|
|
||||||
|
return nodes; |
||||||
|
} |
||||||
|
|
||||||
|
getHalfCheckedKeys() { |
||||||
|
return this.getHalfCheckedNodes().map((data) => (data || {})[this.key]); |
||||||
|
} |
||||||
|
|
||||||
|
_getAllNodes() { |
||||||
|
const allNodes = []; |
||||||
|
const nodesMap = this.nodesMap; |
||||||
|
for (let nodeKey in nodesMap) { |
||||||
|
if (nodesMap.hasOwnProperty(nodeKey)) { |
||||||
|
allNodes.push(nodesMap[nodeKey]); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return allNodes; |
||||||
|
} |
||||||
|
|
||||||
|
updateChildren(key, data) { |
||||||
|
const node = this.nodesMap[key]; |
||||||
|
if (!node) return; |
||||||
|
const childNodes = node.getChildNodes(node.childNodesId); |
||||||
|
for (let i = childNodes.length - 1; i >= 0; i--) { |
||||||
|
const child = childNodes[i]; |
||||||
|
this.remove(child.data); |
||||||
|
} |
||||||
|
for (let i = 0, j = data.length; i < j; i++) { |
||||||
|
const child = data[i]; |
||||||
|
this.append(child, node.data); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
_setCheckedKeys(key, leafOnly = false, checkedKeys) { |
||||||
|
const allNodes = this._getAllNodes().sort((a, b) => b.level - a.level); |
||||||
|
const cache = Object.create(null); |
||||||
|
const keys = Object.keys(checkedKeys); |
||||||
|
allNodes.forEach(node => node.setChecked(false, false)); |
||||||
|
for (let i = 0, j = allNodes.length; i < j; i++) { |
||||||
|
const node = allNodes[i]; |
||||||
|
let nodeKey = node.data[key]; |
||||||
|
|
||||||
|
if (typeof nodeKey === 'undefined') continue; |
||||||
|
|
||||||
|
nodeKey = nodeKey.toString(); |
||||||
|
let checked = keys.indexOf(nodeKey) > -1; |
||||||
|
if (!checked) { |
||||||
|
if (node.checked && !cache[nodeKey]) { |
||||||
|
node.setChecked(false, false); |
||||||
|
} |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
let parent = node.getParent(node.parentId); |
||||||
|
while (parent && parent.level > 0) { |
||||||
|
cache[parent.data[key]] = true; |
||||||
|
parent = parent.getParent(parent.parentId); |
||||||
|
} |
||||||
|
|
||||||
|
if (node.isLeaf || this.checkStrictly) { |
||||||
|
node.setChecked(true, false); |
||||||
|
continue; |
||||||
|
} |
||||||
|
node.setChecked(true, true); |
||||||
|
|
||||||
|
if (leafOnly) { |
||||||
|
node.setChecked(false, false); |
||||||
|
const traverse = function(node) { |
||||||
|
const childNodes = node.getChildNodes(node.childNodesId); |
||||||
|
childNodes.forEach((child) => { |
||||||
|
if (!child.isLeaf) { |
||||||
|
child.setChecked(false, false); |
||||||
|
} |
||||||
|
traverse(child); |
||||||
|
}); |
||||||
|
}; |
||||||
|
traverse(node); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
setCheckedNodes(array, leafOnly = false) { |
||||||
|
const key = this.key; |
||||||
|
const checkedKeys = {}; |
||||||
|
array.forEach((item) => { |
||||||
|
checkedKeys[(item || {})[key]] = true; |
||||||
|
}); |
||||||
|
|
||||||
|
this._setCheckedKeys(key, leafOnly, checkedKeys); |
||||||
|
} |
||||||
|
|
||||||
|
setCheckedKeys(keys, leafOnly = false) { |
||||||
|
this.defaultCheckedKeys = keys; |
||||||
|
const key = this.key; |
||||||
|
const checkedKeys = {}; |
||||||
|
keys.forEach((key) => { |
||||||
|
checkedKeys[key] = true; |
||||||
|
}); |
||||||
|
|
||||||
|
this._setCheckedKeys(key, leafOnly, checkedKeys); |
||||||
|
} |
||||||
|
|
||||||
|
setDefaultExpandedKeys(keys) { |
||||||
|
keys = keys || []; |
||||||
|
this.defaultExpandedKeys = keys; |
||||||
|
|
||||||
|
keys.forEach((key) => { |
||||||
|
const node = this.getNode(key); |
||||||
|
if (node) node.expand(null, this.autoExpandParent); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
setChecked(data, checked, deep) { |
||||||
|
const node = this.getNode(data); |
||||||
|
|
||||||
|
if (node) { |
||||||
|
node.setChecked(!!checked, deep); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
getCurrentNode() { |
||||||
|
return this.currentNode; |
||||||
|
} |
||||||
|
|
||||||
|
setCurrentNode(currentNode) { |
||||||
|
const prevCurrentNode = this.currentNode; |
||||||
|
if (prevCurrentNode) { |
||||||
|
prevCurrentNode.isCurrent = false; |
||||||
|
} |
||||||
|
this.currentNode = currentNode; |
||||||
|
this.currentNode.isCurrent = true; |
||||||
|
|
||||||
|
this.expandCurrentNodeParent && this.currentNode.expand(null, true) |
||||||
|
} |
||||||
|
|
||||||
|
setUserCurrentNode(node) { |
||||||
|
const key = node[this.key]; |
||||||
|
const currNode = this.nodesMap[key]; |
||||||
|
this.setCurrentNode(currNode); |
||||||
|
} |
||||||
|
|
||||||
|
setCurrentNodeKey(key) { |
||||||
|
if (key === null || key === undefined) { |
||||||
|
this.currentNode && (this.currentNode.isCurrent = false); |
||||||
|
this.currentNode = null; |
||||||
|
return; |
||||||
|
} |
||||||
|
const node = this.getNode(key); |
||||||
|
if (node) { |
||||||
|
this.setCurrentNode(node); |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
@ -0,0 +1,115 @@ |
|||||||
|
export const NODE_KEY = '$treeNodeId'; |
||||||
|
|
||||||
|
export const markNodeData = function(node, data) { |
||||||
|
if (!data || data[NODE_KEY]) return; |
||||||
|
Object.defineProperty(data, NODE_KEY, { |
||||||
|
value: node.id, |
||||||
|
enumerable: false, |
||||||
|
configurable: false, |
||||||
|
writable: false |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
export const getNodeKey = function(key, data) { |
||||||
|
if (!data) return null; |
||||||
|
if (!key) return data[NODE_KEY]; |
||||||
|
return data[key]; |
||||||
|
}; |
||||||
|
|
||||||
|
export const objectAssign = function(target) { |
||||||
|
for (let i = 1, j = arguments.length; i < j; i++) { |
||||||
|
let source = arguments[i] || {}; |
||||||
|
for (let prop in source) { |
||||||
|
if (source.hasOwnProperty(prop)) { |
||||||
|
let value = source[prop]; |
||||||
|
if (value !== undefined) { |
||||||
|
target[prop] = value; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return target; |
||||||
|
}; |
||||||
|
|
||||||
|
// TODO: use native Array.find, Array.findIndex when IE support is dropped
|
||||||
|
export const arrayFindIndex = function(arr, pred) { |
||||||
|
for (let i = 0; i !== arr.length; ++i) { |
||||||
|
if (pred(arr[i])) { |
||||||
|
return i; |
||||||
|
} |
||||||
|
} |
||||||
|
return -1; |
||||||
|
}; |
||||||
|
|
||||||
|
export const getChildState = function(node) { |
||||||
|
let all = true; |
||||||
|
let none = true; |
||||||
|
let allWithoutDisable = true; |
||||||
|
for (let i = 0, j = node.length; i < j; i++) { |
||||||
|
const n = node[i]; |
||||||
|
if (n.checked !== true || n.indeterminate) { |
||||||
|
all = false; |
||||||
|
if (!n.disabled) { |
||||||
|
allWithoutDisable = false; |
||||||
|
} |
||||||
|
} |
||||||
|
if (n.checked !== false || n.indeterminate) { |
||||||
|
none = false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
all, |
||||||
|
none, |
||||||
|
allWithoutDisable, |
||||||
|
half: !all && !none |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
export const reInitChecked = function(node) { |
||||||
|
if (!node || node.childNodesId.length === 0) return; |
||||||
|
|
||||||
|
let childNodes = node.getChildNodes(node.childNodesId); |
||||||
|
const { |
||||||
|
all, |
||||||
|
none, |
||||||
|
half |
||||||
|
} = getChildState(childNodes); |
||||||
|
if (all) { |
||||||
|
node.checked = true; |
||||||
|
node.indeterminate = false; |
||||||
|
} else if (half) { |
||||||
|
node.checked = false; |
||||||
|
node.indeterminate = true; |
||||||
|
} else if (none) { |
||||||
|
node.checked = false; |
||||||
|
node.indeterminate = false; |
||||||
|
} |
||||||
|
|
||||||
|
let parent = node.getParent(node.parentId); |
||||||
|
if (!parent || parent.level === 0) return; |
||||||
|
|
||||||
|
if (!node.store().checkStrictly) { |
||||||
|
reInitChecked(parent); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
export const getPropertyFromData = function(node, prop) { |
||||||
|
const props = node.store().props; |
||||||
|
const data = node.data || {}; |
||||||
|
const config = props[prop]; |
||||||
|
|
||||||
|
if (typeof config === 'function') { |
||||||
|
return config(data, node); |
||||||
|
} else if (typeof config === 'string') { |
||||||
|
return data[config]; |
||||||
|
} else if (typeof config === 'undefined') { |
||||||
|
const dataProp = data[prop]; |
||||||
|
return dataProp === undefined ? '' : dataProp; |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
export const isNull = function(v) { |
||||||
|
return v === undefined || v === null || v === ''; |
||||||
|
} |
@ -0,0 +1,184 @@ |
|||||||
|
<!-- tab组件: <me-tabs v-model="tabIndex" :tabs="tabs" @change="tabChange"></me-tabs> --> |
||||||
|
<template> |
||||||
|
<view class="me-tabs" :class="{'tabs-fixed': fixed}" :style="{height: tabHeightVal}"> |
||||||
|
<scroll-view v-if="tabs.length" :id="viewId" :scroll-left="scrollLeft" scroll-x scroll-with-animation :scroll-animation-duration="300"> |
||||||
|
<view class="tabs-item" :class="{'tabs-flex':!isScroll, 'tabs-scroll':isScroll}"> |
||||||
|
<!-- tab --> |
||||||
|
<view class="tab-item" :style="{width: tabWidthVal, height: tabHeightVal, 'line-height':tabHeightVal}" v-for="(tab, i) in tabs" :class="{'active': value===i}" :key="i" @click="tabClick(i)"> |
||||||
|
{{getTabName(tab)}} |
||||||
|
</view> |
||||||
|
<!-- 下划线 --> |
||||||
|
<view class="tabs-line" :style="{left:lineLeft}"></view> |
||||||
|
</view> |
||||||
|
</scroll-view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
props:{ |
||||||
|
tabs: { // 支持格式: ['全部', '待付款'] 或 [{name:'全部'}, {name:'待付款'}] |
||||||
|
type: Array, |
||||||
|
default(){ |
||||||
|
return [] |
||||||
|
} |
||||||
|
}, |
||||||
|
nameKey: { // 取name的字段 |
||||||
|
type: String, |
||||||
|
default: 'name' |
||||||
|
}, |
||||||
|
value: { // 当前显示的下标 (使用v-model语法糖: 1.props需为value; 2.需回调input事件) |
||||||
|
type: [String, Number], |
||||||
|
default: 0 |
||||||
|
}, |
||||||
|
fixed: Boolean, // 是否悬浮,默认false |
||||||
|
tabWidth: Number, // 每个tab的宽度,默认不设置值,为flex平均分配; 如果指定宽度,则不使用flex,每个tab居左,超过则水平滑动(单位默认rpx) |
||||||
|
height: { // 高度,单位rpx |
||||||
|
type: Number, |
||||||
|
default: 90 |
||||||
|
} |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
viewId: 'id_' + Math.random().toString(36).substr(2,16), |
||||||
|
scrollLeft: 0 |
||||||
|
} |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
isScroll(){ |
||||||
|
return this.tabWidth && this.tabs.length // 指定了tabWidth的宽度,则支持水平滑动 |
||||||
|
}, |
||||||
|
tabHeightPx(){ |
||||||
|
return uni.upx2px(this.height) |
||||||
|
}, |
||||||
|
tabHeightVal(){ |
||||||
|
return this.tabHeightPx+'px' |
||||||
|
}, |
||||||
|
tabWidthPx(){ |
||||||
|
return uni.upx2px(this.tabWidth) |
||||||
|
}, |
||||||
|
tabWidthVal(){ |
||||||
|
return this.isScroll ? this.tabWidthPx+'px' : '' |
||||||
|
}, |
||||||
|
lineLeft() { |
||||||
|
if (this.isScroll) { |
||||||
|
return this.tabWidthPx * this.value + this.tabWidthPx/2 + 'px' // 需转为px (用rpx的话iOS真机显示有误差) |
||||||
|
} else{ |
||||||
|
return 100/this.tabs.length*(this.value + 1) - 100/(this.tabs.length*2) + '%' |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
watch: { |
||||||
|
tabs() { |
||||||
|
this.warpWidth = null; // 重新计算容器宽度 |
||||||
|
this.scrollCenter(); // 水平滚动到中间 |
||||||
|
}, |
||||||
|
value() { |
||||||
|
this.scrollCenter(); // 水平滚动到中间 |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
getTabName(tab){ |
||||||
|
return typeof tab === "object" ? tab[this.nameKey] : tab |
||||||
|
}, |
||||||
|
tabClick(i){ |
||||||
|
if(this.value!=i){ |
||||||
|
this.$emit("input",i); |
||||||
|
this.$emit("change",i); |
||||||
|
} |
||||||
|
}, |
||||||
|
async scrollCenter(){ |
||||||
|
if(!this.isScroll) return; |
||||||
|
if(!this.warpWidth){ // tabs容器的宽度 |
||||||
|
let rect = await this.initWarpRect() |
||||||
|
this.warpWidth = rect ? rect.width : uni.getSystemInfoSync().windowWidth; // 某些情况下取不到宽度,暂时取屏幕宽度 |
||||||
|
} |
||||||
|
let tabLeft = this.tabWidthPx * this.value + this.tabWidthPx/2; // 当前tab中心点到左边的距离 |
||||||
|
let diff = tabLeft - this.warpWidth/2 // 如果超过tabs容器的一半,则滚动差值 |
||||||
|
this.scrollLeft = diff; |
||||||
|
// #ifdef MP-TOUTIAO |
||||||
|
this.scrollTimer && clearTimeout(this.scrollTimer) |
||||||
|
this.scrollTimer = setTimeout(()=>{ // 字节跳动小程序,需延时再次设置scrollLeft,否则tab切换跨度较大时不生效 |
||||||
|
this.scrollLeft = Math.ceil(diff) |
||||||
|
},400) |
||||||
|
// #endif |
||||||
|
}, |
||||||
|
initWarpRect(){ |
||||||
|
return new Promise(resolve=>{ |
||||||
|
setTimeout(()=>{ // 延时确保dom已渲染, 不使用$nextclick |
||||||
|
let query = uni.createSelectorQuery(); |
||||||
|
// #ifndef MP-ALIPAY |
||||||
|
query = query.in(this) // 支付宝小程序不支持in(this),而字节跳动小程序必须写in(this), 否则都取不到值 |
||||||
|
// #endif |
||||||
|
query.select('#'+this.viewId).boundingClientRect(data => { |
||||||
|
resolve(data) |
||||||
|
}).exec(); |
||||||
|
},20) |
||||||
|
}) |
||||||
|
} |
||||||
|
}, |
||||||
|
mounted() { |
||||||
|
this.scrollCenter() // 滚动到当前下标 |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss"> |
||||||
|
.me-tabs{ |
||||||
|
position: relative; |
||||||
|
font-size: 28rpx; |
||||||
|
color: #707070; |
||||||
|
box-sizing: border-box; |
||||||
|
overflow-y: hidden; |
||||||
|
background-color: #fff; |
||||||
|
&.tabs-fixed{ |
||||||
|
z-index: 990; |
||||||
|
position: fixed; |
||||||
|
top: var(--window-top); |
||||||
|
left: 0; |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.tabs-item{ |
||||||
|
position: relative; |
||||||
|
white-space: nowrap; |
||||||
|
padding-bottom: 30rpx; // 撑开高度,再配合me-tabs的overflow-y: hidden,以达到隐藏滚动条的目的 |
||||||
|
box-sizing: border-box; |
||||||
|
.tab-item{ |
||||||
|
position: relative; |
||||||
|
text-align: center; |
||||||
|
box-sizing: border-box; |
||||||
|
&.active{ |
||||||
|
color: #00B9FF; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 平分的方式显示item |
||||||
|
.tabs-flex{ |
||||||
|
display: flex; |
||||||
|
.tab-item{ |
||||||
|
flex: 1; |
||||||
|
} |
||||||
|
} |
||||||
|
// 居左显示item,支持水平滑动 |
||||||
|
.tabs-scroll{ |
||||||
|
.tab-item{ |
||||||
|
display: inline-block; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 选中tab的线 |
||||||
|
.tabs-line{ |
||||||
|
z-index: 1; |
||||||
|
position: absolute; |
||||||
|
bottom: 30rpx; // 至少与.tabs-item的padding-bottom一致,才能保证在底部边缘 |
||||||
|
width: 132rpx; |
||||||
|
height: 8rpx; |
||||||
|
transform: translateX(-50%); |
||||||
|
border-radius: 4rpx; |
||||||
|
transition: left .3s; |
||||||
|
background: #00B9FF; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,179 @@ |
|||||||
|
<!-- 视频组件: <me-video src="视频地址" poster="封面图"></me-video> |
||||||
|
video标签在APP端是原生组件, 真机APP端下拉时会渲染不及时, 出现悬浮错位现象; |
||||||
|
me-video组件, 未播放时自动展示image封面, 播放时才显示video, 提高性能; 如果播放中执行下拉,会自动显示封面, 避免视频下拉悬浮错位; |
||||||
|
--> |
||||||
|
<template> |
||||||
|
<view class="me-video" :style="{width:width, height:height}"> |
||||||
|
<!-- 播放的时候才渲染video标签 --> |
||||||
|
<video v-if="showVideo" ref="videoRef" class="video" :class="{'full-play': fullplay&&!autoplay, 'mescroll-dowload': mescrollDownLoad}" :src="src" autoplay :loop="loop" @click="videoClick" x5-playsinline="true" x5-video-player-type="h5" playsinline="true" webkit-playsinline="true" x5-video-player-fullscreen="false"></video> |
||||||
|
<!-- 播放按钮 --> |
||||||
|
<view v-else class="btn-play"> <view class="triangle"></view> </view> |
||||||
|
<!-- 封面 --> |
||||||
|
<image v-if="(!showVideo || mescrollDownLoad) && poster" class="poster" :src="poster" @click="play()" mode="aspectFit"></image> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
props: { |
||||||
|
src: String, // 视频地址 |
||||||
|
poster: String, // 封面图 |
||||||
|
autoplay: { // 是否自动播放 |
||||||
|
type: Boolean, |
||||||
|
default(){ |
||||||
|
return false |
||||||
|
} |
||||||
|
}, |
||||||
|
fullplay: { // 是否全屏播放,默认不全屏 |
||||||
|
type: Boolean, |
||||||
|
default(){ |
||||||
|
return false |
||||||
|
} |
||||||
|
}, |
||||||
|
loop: { // 是否循环播放 |
||||||
|
type: Boolean, |
||||||
|
default(){ |
||||||
|
return true // 循环播放可避免Android微信播放完毕显示广告 |
||||||
|
} |
||||||
|
}, |
||||||
|
width: { // 宽度 (需带单位,支持格式: '100%', '300px', '300rpx') |
||||||
|
type: String, |
||||||
|
default: "100%" |
||||||
|
}, |
||||||
|
height: { // 高度 (需带单位,支持格式: '100%', '300px', '300rpx') |
||||||
|
type: String, |
||||||
|
default: "225px" |
||||||
|
}, |
||||||
|
mescroll: { // mescroll对象,APP端下拉刷新时显示封面,隐藏视频.缓解APP端视频下拉悬浮错位问题 |
||||||
|
type: Object, |
||||||
|
default(){ |
||||||
|
return {} |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
showVideo: this.autoplay // 是否播放视频 |
||||||
|
} |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
// 是否下拉中 (下拉隐藏视频,显示封面, 仅APP端生效) |
||||||
|
mescrollDownLoad() { |
||||||
|
// #ifdef APP-PLUS |
||||||
|
return this.mescroll.downLoadType |
||||||
|
// #endif |
||||||
|
// #ifndef APP-PLUS |
||||||
|
return false |
||||||
|
// #endif |
||||||
|
} |
||||||
|
}, |
||||||
|
watch: { |
||||||
|
autoplay(val) { |
||||||
|
if(val) this.play() |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
// 播放 |
||||||
|
play(){ |
||||||
|
this.showVideo = true |
||||||
|
this.wxAutoPlay() |
||||||
|
}, |
||||||
|
// 视频点击事件 |
||||||
|
videoClick(){ |
||||||
|
// 全屏播放时,点击视频退出 |
||||||
|
if(this.fullplay) this.showVideo = false |
||||||
|
}, |
||||||
|
// 解决微信端视频无法自动播放的问题 |
||||||
|
wxAutoPlay(){ |
||||||
|
// #ifdef H5 |
||||||
|
// 微信端 |
||||||
|
if(navigator.userAgent.toLowerCase().match(/MicroMessenger/i) == 'micromessenger'){ |
||||||
|
// iOS |
||||||
|
let head = document.getElementsByTagName("head")[0] |
||||||
|
let wxscript = document.createElement("script"); |
||||||
|
wxscript.type = "text/javascript" |
||||||
|
wxscript.src = "https://res.wx.qq.com/open/js/jweixin-1.6.0.js" |
||||||
|
head.appendChild(wxscript) |
||||||
|
let vm = this |
||||||
|
let doPlay = function(){ |
||||||
|
vm.$refs.videoRef && vm.$refs.videoRef.play() |
||||||
|
} |
||||||
|
wxscript.onload = function(){ |
||||||
|
window.wx.config({ |
||||||
|
debug: !1, |
||||||
|
appId: "", |
||||||
|
timestamp: 1, |
||||||
|
nonceStr: "", |
||||||
|
signature: "", |
||||||
|
jsApiList: [] |
||||||
|
}) |
||||||
|
window.wx.ready(doPlay) |
||||||
|
} |
||||||
|
// Android |
||||||
|
document.addEventListener("WeixinJSBridgeReady", doPlay, false); |
||||||
|
// 先尝试播放 |
||||||
|
setTimeout(()=>{ |
||||||
|
doPlay() |
||||||
|
},20) |
||||||
|
} |
||||||
|
// #endif |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss"> |
||||||
|
.me-video{ |
||||||
|
position: relative; |
||||||
|
background-color: #000; |
||||||
|
overflow: hidden; |
||||||
|
// 播放按钮 |
||||||
|
.btn-play{ |
||||||
|
z-index: 9; |
||||||
|
position: absolute; |
||||||
|
left: 50%; |
||||||
|
top: 50%; |
||||||
|
transform: translate(-50%, -50%); |
||||||
|
width: 100rpx; |
||||||
|
height: 100rpx; |
||||||
|
border-radius: 50%; |
||||||
|
background-color: rgba(0,0,0,.75); |
||||||
|
pointer-events: none; |
||||||
|
.triangle{ |
||||||
|
position: absolute; |
||||||
|
left: 50%; |
||||||
|
top: 50%; |
||||||
|
transform: translate(-25%, -50%); |
||||||
|
width: 0; |
||||||
|
height: 0; |
||||||
|
border-top: 16rpx solid transparent; |
||||||
|
border-left: 24rpx solid #fff; |
||||||
|
border-bottom: 16rpx solid transparent; |
||||||
|
} |
||||||
|
} |
||||||
|
// 封面图 |
||||||
|
.poster{ |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
vertical-align: bottom; |
||||||
|
} |
||||||
|
// 视频 (默认非全屏播放) |
||||||
|
.video{ |
||||||
|
z-index: 8; |
||||||
|
position: absolute; |
||||||
|
top: 0; |
||||||
|
left: 0; |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
// 全屏播放 |
||||||
|
&.full-play{ |
||||||
|
z-index: 999; |
||||||
|
position: fixed; |
||||||
|
} |
||||||
|
// 下拉时隐藏视频 |
||||||
|
&.mescroll-dowload{ |
||||||
|
display: none; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,47 @@ |
|||||||
|
/*下拉刷新--标语*/ |
||||||
|
.mescroll-downwarp .downwarp-slogan{ |
||||||
|
display: block; |
||||||
|
width: 420rpx; |
||||||
|
height: 168rpx; |
||||||
|
margin: auto; |
||||||
|
} |
||||||
|
/*下拉刷新--向下进度动画*/ |
||||||
|
.mescroll-downwarp .downwarp-progress{ |
||||||
|
display: inline-block; |
||||||
|
width: 40rpx; |
||||||
|
height: 40rpx; |
||||||
|
border: none; |
||||||
|
margin: auto; |
||||||
|
background-size: contain; |
||||||
|
background-repeat: no-repeat; |
||||||
|
background-position: center; |
||||||
|
background-image: url(https://www.mescroll.com/img/beibei/mescroll-progress.png); |
||||||
|
transition: all 300ms; |
||||||
|
} |
||||||
|
/*下拉刷新--进度条*/ |
||||||
|
.mescroll-downwarp .downwarp-loading{ |
||||||
|
display: inline-block; |
||||||
|
width: 32rpx; |
||||||
|
height: 32rpx; |
||||||
|
border-radius: 50%; |
||||||
|
border: 2rpx solid #FF8095; |
||||||
|
border-bottom-color: transparent; |
||||||
|
} |
||||||
|
/*下拉刷新--吉祥物*/ |
||||||
|
.mescroll-downwarp .downwarp-mascot{ |
||||||
|
position: absolute; |
||||||
|
right: 16rpx; |
||||||
|
bottom: 0; |
||||||
|
width: 100rpx; |
||||||
|
height: 100rpx; |
||||||
|
background-size: contain; |
||||||
|
background-repeat: no-repeat; |
||||||
|
animation: animMascot .6s steps(1,end) infinite; |
||||||
|
} |
||||||
|
@keyframes animMascot { |
||||||
|
0% {background-image: url(https://www.mescroll.com/img/beibei/mescroll-bb1.png)} |
||||||
|
25% {background-image: url(https://www.mescroll.com/img/beibei/mescroll-bb2.png)} |
||||||
|
50% {background-image: url(https://www.mescroll.com/img/beibei/mescroll-bb3.png)} |
||||||
|
75% {background-image: url(https://www.mescroll.com/img/beibei/mescroll-bb4.png)} |
||||||
|
100% {background-image: url(https://www.mescroll.com/img/beibei/mescroll-bb1.png)} |
||||||
|
} |
@ -0,0 +1,39 @@ |
|||||||
|
<!-- 下拉刷新区域 --> |
||||||
|
<template> |
||||||
|
<view v-if="mOption.use" class="mescroll-downwarp" :style="{'background':mOption.bgColor,'color':mOption.textColor}"> |
||||||
|
<view class="downwarp-content"> |
||||||
|
<image class="downwarp-slogan" src="https://www.mescroll.com/img/beibei/mescroll-slogan.jpg?v=1" mode="widthFix"/> |
||||||
|
<view v-if="isDownLoading" class="downwarp-loading mescroll-rotate"></view> |
||||||
|
<view v-else class="downwarp-progress" :style="{'transform':downRotate}"></view> |
||||||
|
<view class="downwarp-mascot"></view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
props: { |
||||||
|
option: Object , // down的配置项 |
||||||
|
type: Number // 下拉状态(inOffset:1, outOffset:2, showLoading:3, endDownScroll:4) |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
// 支付宝小程序需写成计算属性,prop定义default仍报错 |
||||||
|
mOption(){ |
||||||
|
return this.option || {} |
||||||
|
}, |
||||||
|
// 是否在加载中 |
||||||
|
isDownLoading(){ |
||||||
|
return this.type === 3 |
||||||
|
}, |
||||||
|
// 旋转的角度 |
||||||
|
downRotate(){ |
||||||
|
return this.type === 2 ? 'rotate(180deg)' : 'rotate(0deg)' |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
</script> |
||||||
|
|
||||||
|
<style> |
||||||
|
@import "../../../mescroll-uni/components/mescroll-down.css"; |
||||||
|
@import "./mescroll-down.css"; |
||||||
|
</style> |
@ -0,0 +1,330 @@ |
|||||||
|
<template> |
||||||
|
<view |
||||||
|
class="mescroll-body mescroll-render-touch" |
||||||
|
:style="{'minHeight':minHeight, 'padding-top': padTop, 'padding-bottom': padBottom}" |
||||||
|
:class="{'mescorll-sticky': sticky}" |
||||||
|
@touchstart="wxsBiz.touchstartEvent" |
||||||
|
@touchmove="wxsBiz.touchmoveEvent" |
||||||
|
@touchend="wxsBiz.touchendEvent" |
||||||
|
@touchcancel="wxsBiz.touchendEvent" |
||||||
|
:change:prop="wxsBiz.propObserver" |
||||||
|
:prop="wxsProp" |
||||||
|
> |
||||||
|
|
||||||
|
<!-- 状态栏 --> |
||||||
|
<view v-if="topbar&&statusBarHeight" class="mescroll-topbar" :style="{height: statusBarHeight+'px', background: topbar}"></view> |
||||||
|
|
||||||
|
<view class="mescroll-body-content mescroll-wxs-content" :style="{ transform: translateY, transition: transition }" :change:prop="wxsBiz.callObserver" :prop="callProp"> |
||||||
|
<!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)--> |
||||||
|
<!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType"></mescroll-down> --> |
||||||
|
<view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}"> |
||||||
|
<view class="downwarp-content"> |
||||||
|
<image class="downwarp-slogan" src="https://www.mescroll.com/img/beibei/mescroll-slogan.jpg?v=1" mode="widthFix"/> |
||||||
|
<view v-if="isDownLoading" class="downwarp-loading mescroll-rotate"></view> |
||||||
|
<view v-else class="downwarp-progress" :style="{'transform':downRotate}"></view> |
||||||
|
<view class="downwarp-mascot"></view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<!-- 列表内容 --> |
||||||
|
<slot></slot> |
||||||
|
|
||||||
|
<!-- 空布局 --> |
||||||
|
<mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty> |
||||||
|
|
||||||
|
<!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)--> |
||||||
|
<!-- <mescroll-up v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> --> |
||||||
|
<view v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" class="mescroll-upwarp" :style="{'background':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}"> |
||||||
|
<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) --> |
||||||
|
<view v-show="upLoadType===1"> |
||||||
|
<view class="upwarp-progress mescroll-rotate" :style="{'border-color':mescroll.optUp.textColor}"></view> |
||||||
|
<view class="upwarp-tip">{{ mescroll.optUp.textLoading }}</view> |
||||||
|
</view> |
||||||
|
<!-- 无数据 --> |
||||||
|
<view v-if="upLoadType===2" class="upwarp-nodata">{{ mescroll.optUp.textNoMore }}</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<!-- 底部是否偏移TabBar的高度(仅H5端生效) --> |
||||||
|
<!-- #ifdef H5 --> |
||||||
|
<view v-if="bottombar && windowBottom>0" class="mescroll-bottombar" :style="{height: windowBottom+'px'}"></view> |
||||||
|
<!-- #endif --> |
||||||
|
|
||||||
|
<!-- 适配iPhoneX --> |
||||||
|
<view v-if="safearea" class="mescroll-safearea"></view> |
||||||
|
|
||||||
|
<!-- 回到顶部按钮 (fixed元素需写在transform外面,防止降级为absolute)--> |
||||||
|
<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top> |
||||||
|
|
||||||
|
<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 --> |
||||||
|
<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 --> |
||||||
|
<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view> |
||||||
|
<!-- #endif --> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<!-- 微信小程序, QQ小程序, app, h5使用wxs --> |
||||||
|
<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 --> |
||||||
|
<script src="../../mescroll-uni/wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script> |
||||||
|
<!-- #endif --> |
||||||
|
|
||||||
|
<!-- app, h5使用renderjs --> |
||||||
|
<!-- #ifdef APP-PLUS || H5 --> |
||||||
|
<script module="renderBiz" lang="renderjs"> |
||||||
|
import renderBiz from '../../mescroll-uni/wxs/renderjs.js'; |
||||||
|
export default { |
||||||
|
mixins: [renderBiz] |
||||||
|
} |
||||||
|
</script> |
||||||
|
<!-- #endif --> |
||||||
|
|
||||||
|
<script> |
||||||
|
import MeScroll from '../../mescroll-uni/mescroll-uni.js'; |
||||||
|
import MescrollEmpty from '../../mescroll-uni/components/mescroll-empty.vue'; |
||||||
|
import MescrollTop from '../../mescroll-uni/components/mescroll-top.vue'; |
||||||
|
import GlobalOption from './mescroll-uni-option.js'; |
||||||
|
import WxsMixin from '../../mescroll-uni/wxs/mixins.js'; |
||||||
|
|
||||||
|
export default { |
||||||
|
mixins: [WxsMixin], |
||||||
|
components: { |
||||||
|
MescrollEmpty, |
||||||
|
MescrollTop |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
mescroll: null, // mescroll实例 |
||||||
|
downHight: 0, //下拉刷新: 容器高度 |
||||||
|
downLoadType: 0, // 下拉刷新状态: 0(loading前), 1(inOffset), 2(outOffset), 3(showLoading), 4(endDownScroll) |
||||||
|
upLoadType: 0, // 上拉加载状态:0(loading前),1(loading中),2(没有更多了,显示END文本提示),3(没有更多了,不显示END文本提示) |
||||||
|
isShowEmpty: false, // 是否显示空布局 |
||||||
|
isShowToTop: false, // 是否显示回到顶部按钮 |
||||||
|
windowHeight: 0, // 可使用窗口的高度 |
||||||
|
windowBottom: 0, // 可使用窗口的底部位置 |
||||||
|
statusBarHeight: 0 // 状态栏高度 |
||||||
|
}; |
||||||
|
}, |
||||||
|
props: { |
||||||
|
down: Object, // 下拉刷新的参数配置 |
||||||
|
up: Object, // 上拉加载的参数配置 |
||||||
|
top: [String, Number], // 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight) |
||||||
|
topbar: [Boolean, String], // top的偏移量是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变) |
||||||
|
bottom: [String, Number], // 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight) |
||||||
|
safearea: Boolean, // bottom的偏移量是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用) |
||||||
|
height: [String, Number], // 指定mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉 |
||||||
|
bottombar:{ // 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效) |
||||||
|
type: Boolean, |
||||||
|
default: true |
||||||
|
}, |
||||||
|
sticky: Boolean // 是否支持sticky,默认false; 当值配置true时,需避免在mescroll-body标签前面加非定位的元素,否则下拉区域无法会隐藏 |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
// mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉 |
||||||
|
minHeight(){ |
||||||
|
return this.toPx(this.height || '100%') + 'px' |
||||||
|
}, |
||||||
|
// 下拉布局往下偏移的距离 (px) |
||||||
|
numTop() { |
||||||
|
return this.toPx(this.top) |
||||||
|
}, |
||||||
|
padTop() { |
||||||
|
return this.numTop + 'px'; |
||||||
|
}, |
||||||
|
// 上拉布局往上偏移 (px) |
||||||
|
numBottom() { |
||||||
|
return this.toPx(this.bottom); |
||||||
|
}, |
||||||
|
padBottom() { |
||||||
|
return this.numBottom + 'px'; |
||||||
|
}, |
||||||
|
// 是否为重置下拉的状态 |
||||||
|
isDownReset() { |
||||||
|
return this.downLoadType === 3 || this.downLoadType === 4; |
||||||
|
}, |
||||||
|
// 过渡 |
||||||
|
transition() { |
||||||
|
return this.isDownReset ? 'transform 300ms' : ''; |
||||||
|
}, |
||||||
|
translateY() { |
||||||
|
return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' : ''; // transform会使fixed失效,需注意把fixed元素写在mescroll之外 |
||||||
|
}, |
||||||
|
// 是否在加载中 |
||||||
|
isDownLoading(){ |
||||||
|
return this.downLoadType === 3 |
||||||
|
}, |
||||||
|
// 旋转的角度 |
||||||
|
downRotate(){ |
||||||
|
return this.downLoadType === 2 ? 'rotate(180deg)' : 'rotate(0deg)' |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
//number,rpx,upx,px,% --> px的数值 |
||||||
|
toPx(num) { |
||||||
|
if (typeof num === 'string') { |
||||||
|
if (num.indexOf('px') !== -1) { |
||||||
|
if (num.indexOf('rpx') !== -1) { |
||||||
|
// "10rpx" |
||||||
|
num = num.replace('rpx', ''); |
||||||
|
} else if (num.indexOf('upx') !== -1) { |
||||||
|
// "10upx" |
||||||
|
num = num.replace('upx', ''); |
||||||
|
} else { |
||||||
|
// "10px" |
||||||
|
return Number(num.replace('px', '')); |
||||||
|
} |
||||||
|
} else if (num.indexOf('%') !== -1) { |
||||||
|
// 传百分比,则相对于windowHeight,传"10%"则等于windowHeight的10% |
||||||
|
let rate = Number(num.replace('%', '')) / 100; |
||||||
|
return this.windowHeight * rate; |
||||||
|
} |
||||||
|
} |
||||||
|
return num ? uni.upx2px(Number(num)) : 0; |
||||||
|
}, |
||||||
|
// 点击空布局的按钮回调 |
||||||
|
emptyClick() { |
||||||
|
this.$emit('emptyclick', this.mescroll); |
||||||
|
}, |
||||||
|
// 点击回到顶部的按钮回调 |
||||||
|
toTopClick() { |
||||||
|
this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部 |
||||||
|
this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调 |
||||||
|
} |
||||||
|
}, |
||||||
|
// 使用created初始化mescroll对象; 如果用mounted部分css样式编译到H5会失效 |
||||||
|
created() { |
||||||
|
let vm = this; |
||||||
|
|
||||||
|
let diyOption = { |
||||||
|
// 下拉刷新的配置 |
||||||
|
down: { |
||||||
|
inOffset() { |
||||||
|
vm.downLoadType = 1; // 下拉的距离进入offset范围内那一刻的回调 (自定义mescroll组件时,此行不可删) |
||||||
|
}, |
||||||
|
outOffset() { |
||||||
|
vm.downLoadType = 2; // 下拉的距离大于offset那一刻的回调 (自定义mescroll组件时,此行不可删) |
||||||
|
}, |
||||||
|
onMoving(mescroll, rate, downHight) { |
||||||
|
// 下拉过程中的回调,滑动过程一直在执行; |
||||||
|
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删) |
||||||
|
}, |
||||||
|
showLoading(mescroll, downHight) { |
||||||
|
vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删) |
||||||
|
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删) |
||||||
|
}, |
||||||
|
endDownScroll() { |
||||||
|
vm.downLoadType = 4; // 结束下拉 (自定义mescroll组件时,此行不可删) |
||||||
|
vm.downHight = 0; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删) |
||||||
|
if(vm.downResetTimer) {clearTimeout(vm.downResetTimer); vm.downResetTimer = null} // 移除重置倒计时 |
||||||
|
vm.downResetTimer = setTimeout(()=>{ // 过渡动画执行完毕后,需重置为0的状态,避免下次inOffset不及时显示textInOffset |
||||||
|
if(vm.downLoadType === 4) vm.downLoadType = 0 |
||||||
|
},300) |
||||||
|
}, |
||||||
|
// 派发下拉刷新的回调 |
||||||
|
callback: function(mescroll) { |
||||||
|
vm.$emit('down', mescroll); |
||||||
|
} |
||||||
|
}, |
||||||
|
// 上拉加载的配置 |
||||||
|
up: { |
||||||
|
// 显示加载中的回调 |
||||||
|
showLoading() { |
||||||
|
vm.upLoadType = 1; |
||||||
|
}, |
||||||
|
// 显示无更多数据的回调 |
||||||
|
showNoMore() { |
||||||
|
vm.upLoadType = 2; |
||||||
|
}, |
||||||
|
// 隐藏上拉加载的回调 |
||||||
|
hideUpScroll(mescroll) { |
||||||
|
vm.upLoadType = mescroll.optUp.hasNext ? 0 : 3; |
||||||
|
}, |
||||||
|
// 空布局 |
||||||
|
empty: { |
||||||
|
onShow(isShow) { |
||||||
|
// 显示隐藏的回调 |
||||||
|
vm.isShowEmpty = isShow; |
||||||
|
} |
||||||
|
}, |
||||||
|
// 回到顶部 |
||||||
|
toTop: { |
||||||
|
onShow(isShow) { |
||||||
|
// 显示隐藏的回调 |
||||||
|
vm.isShowToTop = isShow; |
||||||
|
} |
||||||
|
}, |
||||||
|
// 派发上拉加载的回调 |
||||||
|
callback: function(mescroll) { |
||||||
|
vm.$emit('up', mescroll); |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
MeScroll.extend(diyOption, GlobalOption); // 混入全局的配置 |
||||||
|
let myOption = JSON.parse(JSON.stringify({down: vm.down,up: vm.up})); // 深拷贝,避免对props的影响 |
||||||
|
MeScroll.extend(myOption, diyOption); // 混入具体界面的配置 |
||||||
|
|
||||||
|
// 初始化MeScroll对象 |
||||||
|
vm.mescroll = new MeScroll(myOption, true); // 传入true,标记body为滚动区域 |
||||||
|
// init回调mescroll对象 |
||||||
|
vm.$emit('init', vm.mescroll); |
||||||
|
|
||||||
|
// 设置高度 |
||||||
|
const sys = uni.getSystemInfoSync(); |
||||||
|
if (sys.windowHeight) vm.windowHeight = sys.windowHeight; |
||||||
|
if (sys.windowBottom) vm.windowBottom = sys.windowBottom; |
||||||
|
if (sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight; |
||||||
|
// 使down的bottomOffset生效 |
||||||
|
vm.mescroll.setBodyHeight(sys.windowHeight); |
||||||
|
|
||||||
|
// 因为使用的是page的scroll,这里需自定义scrollTo |
||||||
|
vm.mescroll.resetScrollTo((y, t) => { |
||||||
|
if(typeof y === 'string'){ |
||||||
|
// 滚动到指定view (y为css选择器) |
||||||
|
setTimeout(()=>{ // 延时确保view已渲染; 不使用$nextTick |
||||||
|
let selector; |
||||||
|
if(y.indexOf('#')==-1 && y.indexOf('.')==-1){ |
||||||
|
selector = '#'+y // 不带#和. 则默认为id选择器 |
||||||
|
}else{ |
||||||
|
selector = y |
||||||
|
// #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-DINGTALK |
||||||
|
if(y.indexOf('>>>')!=-1){ // 不支持跨自定义组件的后代选择器 (转为普通的选择器即可跨组件查询) |
||||||
|
selector = y.split('>>>')[1].trim() |
||||||
|
} |
||||||
|
// #endif |
||||||
|
} |
||||||
|
uni.createSelectorQuery().select(selector).boundingClientRect(function(rect){ |
||||||
|
if (rect) { |
||||||
|
let top = rect.top |
||||||
|
top += vm.mescroll.getScrollTop() |
||||||
|
uni.pageScrollTo({ |
||||||
|
scrollTop: top, |
||||||
|
duration: t |
||||||
|
}) |
||||||
|
} else{ |
||||||
|
console.error(selector + ' does not exist'); |
||||||
|
} |
||||||
|
}).exec() |
||||||
|
},30) |
||||||
|
} else{ |
||||||
|
// 滚动到指定位置 (y必须为数字) |
||||||
|
uni.pageScrollTo({ |
||||||
|
scrollTop: y, |
||||||
|
duration: t |
||||||
|
}) |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
// 具体的界面如果不配置up.toTop.safearea,则取本vue的safearea值 |
||||||
|
if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else { |
||||||
|
vm.mescroll.optUp.toTop.safearea = vm.safearea; |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
</script> |
||||||
|
|
||||||
|
<style> |
||||||
|
@import "../../mescroll-uni/mescroll-body.css"; |
||||||
|
@import "../../mescroll-uni/components/mescroll-down.css"; |
||||||
|
@import "../../mescroll-uni/components/mescroll-up.css"; |
||||||
|
@import "./components/mescroll-down.css"; |
||||||
|
</style> |
@ -0,0 +1,29 @@ |
|||||||
|
// mescroll-uni和mescroll-body 的全局配置
|
||||||
|
const GlobalOption = { |
||||||
|
down: { |
||||||
|
// 其他down的配置参数也可以写,这里只展示了常用的配置:
|
||||||
|
offset: uni.upx2px(140), // 在列表顶部,下拉大于140upx,松手即可触发下拉刷新的回调
|
||||||
|
native: false // 是否使用系统自带的下拉刷新; 默认false; 仅在mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
|
||||||
|
}, |
||||||
|
up: { |
||||||
|
// 其他up的配置参数也可以写,这里只展示了常用的配置:
|
||||||
|
textLoading: '加载中 ...', // 加载中的提示文本
|
||||||
|
textNoMore: '-- END --', // 没有更多数据的提示文本
|
||||||
|
offset: 150, // 距底部多远时,触发upCallback
|
||||||
|
toTop: { |
||||||
|
// 回到顶部按钮,需配置src才显示
|
||||||
|
src: "https://www.mescroll.com/img/mescroll-totop.png", // 图片路径 (建议放入static目录, 如 /static/img/mescroll-totop.png )
|
||||||
|
offset: 1000, // 列表滚动多少距离才显示回到顶部按钮,默认1000px
|
||||||
|
right: 20, // 到右边的距离, 默认20 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
|
||||||
|
bottom: 120, // 到底部的距离, 默认120 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
|
||||||
|
width: 72 // 回到顶部图标的宽度, 默认72 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
|
||||||
|
}, |
||||||
|
empty: { |
||||||
|
use: true, // 是否显示空布局
|
||||||
|
icon: "https://www.mescroll.com/img/mescroll-empty.png", // 图标路径 (建议放入static目录, 如 /static/img/mescroll-empty.png )
|
||||||
|
tip: '~ 暂无相关数据 ~' // 提示
|
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export default GlobalOption |
@ -0,0 +1,406 @@ |
|||||||
|
<template> |
||||||
|
<view class="mescroll-uni-warp"> |
||||||
|
<scroll-view :id="viewId" class="mescroll-uni" :class="{'mescroll-uni-fixed':isFixed}" :style="{'height':scrollHeight,'padding-top':padTop,'padding-bottom':padBottom,'top':fixedTop,'bottom':fixedBottom}" :scroll-top="scrollTop" :scroll-with-animation="scrollAnim" @scroll="scroll" :scroll-y='scrollable' :enable-back-to-top="true" :throttle="false"> |
||||||
|
<view class="mescroll-uni-content mescroll-render-touch" |
||||||
|
@touchstart="wxsBiz.touchstartEvent" |
||||||
|
@touchmove="wxsBiz.touchmoveEvent" |
||||||
|
@touchend="wxsBiz.touchendEvent" |
||||||
|
@touchcancel="wxsBiz.touchendEvent" |
||||||
|
:change:prop="wxsBiz.propObserver" |
||||||
|
:prop="wxsProp"> |
||||||
|
|
||||||
|
<!-- 状态栏 --> |
||||||
|
<view v-if="topbar&&statusBarHeight" class="mescroll-topbar" :style="{height: statusBarHeight+'px', background: topbar}"></view> |
||||||
|
|
||||||
|
<view class="mescroll-wxs-content" :style="{'transform': translateY, 'transition': transition}" :change:prop="wxsBiz.callObserver" :prop="callProp"> |
||||||
|
<!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)--> |
||||||
|
<!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType"></mescroll-down> --> |
||||||
|
<view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}"> |
||||||
|
<view class="downwarp-content"> |
||||||
|
<image class="downwarp-slogan" src="https://www.mescroll.com/img/beibei/mescroll-slogan.jpg?v=1" mode="widthFix"/> |
||||||
|
<view v-if="isDownLoading" class="downwarp-loading mescroll-rotate"></view> |
||||||
|
<view v-else class="downwarp-progress" :style="{'transform':downRotate}"></view> |
||||||
|
<view class="downwarp-mascot"></view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<!-- 列表内容 --> |
||||||
|
<slot></slot> |
||||||
|
|
||||||
|
<!-- 空布局 --> |
||||||
|
<mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty> |
||||||
|
|
||||||
|
<!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)--> |
||||||
|
<!-- <mescroll-up v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> --> |
||||||
|
<view v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" class="mescroll-upwarp" :style="{'background':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}"> |
||||||
|
<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) --> |
||||||
|
<view v-show="upLoadType===1"> |
||||||
|
<view class="upwarp-progress mescroll-rotate" :style="{'border-color':mescroll.optUp.textColor}"></view> |
||||||
|
<view class="upwarp-tip">{{ mescroll.optUp.textLoading }}</view> |
||||||
|
</view> |
||||||
|
<!-- 无数据 --> |
||||||
|
<view v-if="upLoadType===2" class="upwarp-nodata">{{ mescroll.optUp.textNoMore }}</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<!-- 底部是否偏移TabBar的高度(仅H5端生效) --> |
||||||
|
<!-- #ifdef H5 --> |
||||||
|
<view v-if="bottombar && windowBottom>0" class="mescroll-bottombar" :style="{height: windowBottom+'px'}"></view> |
||||||
|
<!-- #endif --> |
||||||
|
|
||||||
|
<!-- 适配iPhoneX --> |
||||||
|
<view v-if="safearea" class="mescroll-safearea"></view> |
||||||
|
</view> |
||||||
|
</scroll-view> |
||||||
|
|
||||||
|
<!-- 回到顶部按钮 (fixed元素,需写在scroll-view外面,防止滚动的时候抖动)--> |
||||||
|
<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top> |
||||||
|
|
||||||
|
<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 --> |
||||||
|
<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 --> |
||||||
|
<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view> |
||||||
|
<!-- #endif --> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<!-- 微信小程序, QQ小程序, app, h5使用wxs --> |
||||||
|
<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 --> |
||||||
|
<script src="../../mescroll-uni/wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script> |
||||||
|
<!-- #endif --> |
||||||
|
|
||||||
|
<!-- app, h5使用renderjs --> |
||||||
|
<!-- #ifdef APP-PLUS || H5 --> |
||||||
|
<script module="renderBiz" lang="renderjs"> |
||||||
|
import renderBiz from '../../mescroll-uni/wxs/renderjs.js'; |
||||||
|
export default { |
||||||
|
mixins: [renderBiz] |
||||||
|
} |
||||||
|
</script> |
||||||
|
<!-- #endif --> |
||||||
|
|
||||||
|
<script> |
||||||
|
import MeScroll from '../../mescroll-uni/mescroll-uni.js'; |
||||||
|
import MescrollEmpty from '../../mescroll-uni/components/mescroll-empty.vue'; |
||||||
|
import MescrollTop from '../../mescroll-uni/components/mescroll-top.vue'; |
||||||
|
import GlobalOption from './mescroll-uni-option.js'; |
||||||
|
import WxsMixin from '../../mescroll-uni/wxs/mixins.js'; |
||||||
|
|
||||||
|
export default { |
||||||
|
mixins: [WxsMixin], |
||||||
|
components: { |
||||||
|
MescrollEmpty, |
||||||
|
MescrollTop |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
mescroll: null, // mescroll实例 |
||||||
|
viewId: 'id_' + Math.random().toString(36).substr(2,16), // 随机生成mescroll的id(不能数字开头,否则找不到元素) |
||||||
|
downHight: 0, //下拉刷新: 容器高度 |
||||||
|
downLoadType: 0, // 下拉刷新状态: 0(loading前), 1(inOffset), 2(outOffset), 3(showLoading), 4(endDownScroll) |
||||||
|
upLoadType: 0, // 上拉加载状态: 0(loading前), 1loading中, 2没有更多了,显示END文本提示, 3(没有更多了,不显示END文本提示) |
||||||
|
isShowEmpty: false, // 是否显示空布局 |
||||||
|
isShowToTop: false, // 是否显示回到顶部按钮 |
||||||
|
scrollTop: 0, // 滚动条的位置 |
||||||
|
scrollAnim: false, // 是否开启滚动动画 |
||||||
|
windowTop: 0, // 可使用窗口的顶部位置 |
||||||
|
windowBottom: 0, // 可使用窗口的底部位置 |
||||||
|
windowHeight: 0, // 可使用窗口的高度 |
||||||
|
statusBarHeight: 0 // 状态栏高度 |
||||||
|
} |
||||||
|
}, |
||||||
|
props: { |
||||||
|
down: Object, // 下拉刷新的参数配置 |
||||||
|
up: Object, // 上拉加载的参数配置 |
||||||
|
top: [String, Number], // 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight) |
||||||
|
topbar: [Boolean, String], // top的偏移量是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变) |
||||||
|
bottom: [String, Number], // 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight) |
||||||
|
safearea: Boolean, // bottom的偏移量是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用) |
||||||
|
fixed: { // 是否通过fixed固定mescroll的高度, 默认true |
||||||
|
type: Boolean, |
||||||
|
default: true |
||||||
|
}, |
||||||
|
height: [String, Number], // 指定mescroll的高度, 此项有值,则不使用fixed. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight) |
||||||
|
bottombar:{ // 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效) |
||||||
|
type: Boolean, |
||||||
|
default: true |
||||||
|
} |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
// 是否使用fixed定位 (当height有值,则不使用) |
||||||
|
isFixed(){ |
||||||
|
return !this.height && this.fixed |
||||||
|
}, |
||||||
|
// mescroll的高度 |
||||||
|
scrollHeight(){ |
||||||
|
if (this.isFixed) { |
||||||
|
return "auto" |
||||||
|
} else if(this.height){ |
||||||
|
return this.toPx(this.height) + 'px' |
||||||
|
}else{ |
||||||
|
return "100%" |
||||||
|
} |
||||||
|
}, |
||||||
|
// 下拉布局往下偏移的距离 (px) |
||||||
|
numTop() { |
||||||
|
return this.toPx(this.top) |
||||||
|
}, |
||||||
|
fixedTop() { |
||||||
|
return this.isFixed ? (this.numTop + this.windowTop) + 'px' : 0 |
||||||
|
}, |
||||||
|
padTop() { |
||||||
|
return !this.isFixed ? this.numTop + 'px' : 0 |
||||||
|
}, |
||||||
|
// 上拉布局往上偏移 (px) |
||||||
|
numBottom() { |
||||||
|
return this.toPx(this.bottom) |
||||||
|
}, |
||||||
|
fixedBottom() { |
||||||
|
return this.isFixed ? (this.numBottom + this.windowBottom) + 'px' : 0 |
||||||
|
}, |
||||||
|
padBottom() { |
||||||
|
return !this.isFixed ? this.numBottom + 'px' : 0 |
||||||
|
}, |
||||||
|
// 是否为重置下拉的状态 |
||||||
|
isDownReset(){ |
||||||
|
return this.downLoadType===3 || this.downLoadType===4 |
||||||
|
}, |
||||||
|
// 过渡 |
||||||
|
transition() { |
||||||
|
return this.isDownReset ? 'transform 300ms' : '' |
||||||
|
}, |
||||||
|
translateY() { |
||||||
|
return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' : '' // transform会使fixed失效,需注意把fixed元素写在mescroll之外 |
||||||
|
}, |
||||||
|
// 列表是否可滑动 |
||||||
|
scrollable(){ |
||||||
|
return this.downLoadType===0 || this.isDownReset |
||||||
|
}, |
||||||
|
// 是否在加载中 |
||||||
|
isDownLoading(){ |
||||||
|
return this.downLoadType === 3 |
||||||
|
}, |
||||||
|
// 旋转的角度 |
||||||
|
downRotate(){ |
||||||
|
return this.downLoadType === 2 ? 'rotate(180deg)' : 'rotate(0deg)' |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
//number,rpx,upx,px,% --> px的数值 |
||||||
|
toPx(num){ |
||||||
|
if(typeof num === "string"){ |
||||||
|
if (num.indexOf('px') !== -1) { |
||||||
|
if(num.indexOf('rpx') !== -1) { // "10rpx" |
||||||
|
num = num.replace('rpx', ''); |
||||||
|
} else if(num.indexOf('upx') !== -1) { // "10upx" |
||||||
|
num = num.replace('upx', ''); |
||||||
|
} else { // "10px" |
||||||
|
return Number(num.replace('px', '')) |
||||||
|
} |
||||||
|
}else if (num.indexOf('%') !== -1){ |
||||||
|
// 传百分比,则相对于windowHeight,传"10%"则等于windowHeight的10% |
||||||
|
let rate = Number(num.replace("%","")) / 100 |
||||||
|
return this.windowHeight * rate |
||||||
|
} |
||||||
|
} |
||||||
|
return num ? uni.upx2px(Number(num)) : 0 |
||||||
|
}, |
||||||
|
//注册列表滚动事件,用于下拉刷新和上拉加载 |
||||||
|
scroll(e) { |
||||||
|
this.mescroll.scroll(e.detail, () => { |
||||||
|
this.$emit('scroll', this.mescroll) // 此时可直接通过 this.mescroll.scrollTop获取滚动条位置; this.mescroll.isScrollUp获取是否向上滑动 |
||||||
|
}) |
||||||
|
}, |
||||||
|
// 点击空布局的按钮回调 |
||||||
|
emptyClick() { |
||||||
|
this.$emit('emptyclick', this.mescroll) |
||||||
|
}, |
||||||
|
// 点击回到顶部的按钮回调 |
||||||
|
toTopClick() { |
||||||
|
this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部 |
||||||
|
this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调 |
||||||
|
}, |
||||||
|
// 更新滚动区域的高度 (使内容不满屏和到底,都可继续翻页) |
||||||
|
setClientHeight() { |
||||||
|
if (this.mescroll.getClientHeight(true) === 0 && !this.isExec) { |
||||||
|
this.isExec = true; // 避免多次获取 |
||||||
|
this.$nextTick(() => { // 确保dom已渲染 |
||||||
|
this.getClientInfo(data=>{ |
||||||
|
this.isExec = false; |
||||||
|
if (data) { |
||||||
|
this.mescroll.setClientHeight(data.height); |
||||||
|
} else if (this.clientNum != 3) { // 极少部分情况,可能dom还未渲染完毕,递归获取,最多重试3次 |
||||||
|
this.clientNum = this.clientNum == null ? 1 : this.clientNum + 1; |
||||||
|
setTimeout(() => { |
||||||
|
this.setClientHeight() |
||||||
|
}, this.clientNum * 100) |
||||||
|
} |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
}, |
||||||
|
// 获取滚动区域的信息 |
||||||
|
getClientInfo(success){ |
||||||
|
let query = uni.createSelectorQuery(); |
||||||
|
// #ifndef MP-ALIPAY || MP-DINGTALK |
||||||
|
query = query.in(this) // 支付宝小程序不支持in(this),而字节跳动小程序必须写in(this), 否则都取不到值 |
||||||
|
// #endif |
||||||
|
let view = query.select('#' + this.viewId); |
||||||
|
view.boundingClientRect(data => { |
||||||
|
success(data) |
||||||
|
}).exec(); |
||||||
|
} |
||||||
|
}, |
||||||
|
// 使用created初始化mescroll对象; 如果用mounted部分css样式编译到H5会失效 |
||||||
|
created() { |
||||||
|
let vm = this; |
||||||
|
|
||||||
|
let diyOption = { |
||||||
|
// 下拉刷新的配置 |
||||||
|
down: { |
||||||
|
inOffset() { |
||||||
|
vm.downLoadType = 1; // 下拉的距离进入offset范围内那一刻的回调 (自定义mescroll组件时,此行不可删) |
||||||
|
}, |
||||||
|
outOffset() { |
||||||
|
vm.downLoadType = 2; // 下拉的距离大于offset那一刻的回调 (自定义mescroll组件时,此行不可删) |
||||||
|
}, |
||||||
|
onMoving(mescroll, rate, downHight) { |
||||||
|
// 下拉过程中的回调,滑动过程一直在执行; |
||||||
|
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删) |
||||||
|
}, |
||||||
|
showLoading(mescroll, downHight) { |
||||||
|
vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删) |
||||||
|
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删) |
||||||
|
}, |
||||||
|
endDownScroll() { |
||||||
|
vm.downLoadType = 4; // 结束下拉 (自定义mescroll组件时,此行不可删) |
||||||
|
vm.downHight = 0; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删) |
||||||
|
vm.downResetTimer && clearTimeout(vm.downResetTimer) |
||||||
|
vm.downResetTimer = setTimeout(()=>{ // 过渡动画执行完毕后,需重置为0的状态,以便置空this.transition,避免iOS小程序列表渲染不完整 |
||||||
|
if(vm.downLoadType===4) vm.downLoadType = 0 |
||||||
|
},300) |
||||||
|
}, |
||||||
|
// 派发下拉刷新的回调 |
||||||
|
callback: function(mescroll) { |
||||||
|
vm.$emit('down', mescroll) |
||||||
|
} |
||||||
|
}, |
||||||
|
// 上拉加载的配置 |
||||||
|
up: { |
||||||
|
// 显示加载中的回调 |
||||||
|
showLoading() { |
||||||
|
vm.upLoadType = 1; |
||||||
|
}, |
||||||
|
// 显示无更多数据的回调 |
||||||
|
showNoMore() { |
||||||
|
vm.upLoadType = 2; |
||||||
|
}, |
||||||
|
// 隐藏上拉加载的回调 |
||||||
|
hideUpScroll(mescroll) { |
||||||
|
vm.upLoadType = mescroll.optUp.hasNext ? 0 : 3; |
||||||
|
}, |
||||||
|
// 空布局 |
||||||
|
empty: { |
||||||
|
onShow(isShow) { // 显示隐藏的回调 |
||||||
|
vm.isShowEmpty = isShow; |
||||||
|
} |
||||||
|
}, |
||||||
|
// 回到顶部 |
||||||
|
toTop: { |
||||||
|
onShow(isShow) { // 显示隐藏的回调 |
||||||
|
vm.isShowToTop = isShow; |
||||||
|
} |
||||||
|
}, |
||||||
|
// 派发上拉加载的回调 |
||||||
|
callback: function(mescroll) { |
||||||
|
vm.$emit('up', mescroll); |
||||||
|
// 更新容器的高度 (多mescroll的情况) |
||||||
|
vm.setClientHeight() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
MeScroll.extend(diyOption, GlobalOption); // 混入全局的配置 |
||||||
|
let myOption = JSON.parse(JSON.stringify({'down': vm.down,'up': vm.up})) // 深拷贝,避免对props的影响 |
||||||
|
MeScroll.extend(myOption, diyOption); // 混入具体界面的配置 |
||||||
|
|
||||||
|
// 初始化MeScroll对象 |
||||||
|
vm.mescroll = new MeScroll(myOption); |
||||||
|
vm.mescroll.viewId = vm.viewId; // 附带id |
||||||
|
// init回调mescroll对象 |
||||||
|
vm.$emit('init', vm.mescroll); |
||||||
|
|
||||||
|
// 设置高度 |
||||||
|
const sys = uni.getSystemInfoSync(); |
||||||
|
if(sys.windowTop) vm.windowTop = sys.windowTop; |
||||||
|
if(sys.windowBottom) vm.windowBottom = sys.windowBottom; |
||||||
|
if(sys.windowHeight) vm.windowHeight = sys.windowHeight; |
||||||
|
if(sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight; |
||||||
|
// 使down的bottomOffset生效 |
||||||
|
vm.mescroll.setBodyHeight(sys.windowHeight); |
||||||
|
|
||||||
|
// 因为使用的是scrollview,这里需自定义scrollTo |
||||||
|
vm.mescroll.resetScrollTo((y, t) => { |
||||||
|
vm.scrollAnim = (t !== 0); // t为0,则不使用动画过渡 |
||||||
|
if(typeof y === 'string'){ |
||||||
|
// 小程序不支持slot里面的scroll-into-view, 统一使用计算的方式实现 |
||||||
|
vm.getClientInfo(function(rect){ |
||||||
|
let mescrollTop = rect.top // mescroll到顶部的距离 |
||||||
|
let selector; |
||||||
|
if(y.indexOf('#')==-1 && y.indexOf('.')==-1){ |
||||||
|
selector = '#'+y // 不带#和. 则默认为id选择器 |
||||||
|
}else{ |
||||||
|
selector = y |
||||||
|
// #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-DINGTALK |
||||||
|
if(y.indexOf('>>>')!=-1){ // 不支持跨自定义组件的后代选择器 (转为普通的选择器即可跨组件查询) |
||||||
|
selector = y.split('>>>')[1].trim() |
||||||
|
} |
||||||
|
// #endif |
||||||
|
} |
||||||
|
uni.createSelectorQuery().select(selector).boundingClientRect(function(rect){ |
||||||
|
if (rect) { |
||||||
|
let curY = vm.mescroll.getScrollTop() |
||||||
|
let top = rect.top - mescrollTop |
||||||
|
top += curY |
||||||
|
if(!vm.isFixed) top -= vm.numTop |
||||||
|
vm.scrollTop = curY; |
||||||
|
vm.$nextTick(function() { |
||||||
|
vm.scrollTop = top |
||||||
|
}) |
||||||
|
} else{ |
||||||
|
console.error(selector + ' does not exist'); |
||||||
|
} |
||||||
|
}).exec() |
||||||
|
}) |
||||||
|
return; |
||||||
|
} |
||||||
|
let curY = vm.mescroll.getScrollTop() |
||||||
|
if (t === 0 || t === 300) { // 当t使用默认配置的300时,则使用系统自带的动画过渡 |
||||||
|
vm.scrollTop = curY; |
||||||
|
vm.$nextTick(function() { |
||||||
|
vm.scrollTop = y |
||||||
|
}) |
||||||
|
} else { |
||||||
|
vm.mescroll.getStep(curY, y, step => { // 此写法可支持配置t |
||||||
|
vm.scrollTop = step |
||||||
|
}, t) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
// 具体的界面如果不配置up.toTop.safearea,则取本vue的safearea值 |
||||||
|
if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else { |
||||||
|
vm.mescroll.optUp.toTop.safearea = vm.safearea; |
||||||
|
} |
||||||
|
}, |
||||||
|
mounted() { |
||||||
|
// 设置容器的高度 |
||||||
|
this.setClientHeight() |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style> |
||||||
|
@import "../../mescroll-uni/mescroll-uni.css"; |
||||||
|
@import "../../mescroll-uni/components/mescroll-down.css"; |
||||||
|
@import "../../mescroll-uni/components/mescroll-up.css"; |
||||||
|
@import "./components/mescroll-down.css"; |
||||||
|
</style> |
@ -0,0 +1,44 @@ |
|||||||
|
/*下拉刷新--上下箭头*/ |
||||||
|
.mescroll-downwarp .downwarp-arrow { |
||||||
|
display: inline-block; |
||||||
|
width: 20px; |
||||||
|
height: 20px; |
||||||
|
margin: 10px; |
||||||
|
background-image: url(https://www.mescroll.com/img/xinlang/mescroll-arrow.png); |
||||||
|
background-size: contain; |
||||||
|
vertical-align: middle; |
||||||
|
transition: all 300ms; |
||||||
|
} |
||||||
|
|
||||||
|
/*下拉刷新--旋转进度条*/ |
||||||
|
.mescroll-downwarp .downwarp-progress{ |
||||||
|
width: 36px; |
||||||
|
height: 36px; |
||||||
|
border: none; |
||||||
|
margin: auto; |
||||||
|
background-size: contain; |
||||||
|
animation: progressRotate 0.6s steps(6, start) infinite; |
||||||
|
} |
||||||
|
@keyframes progressRotate { |
||||||
|
0% { |
||||||
|
background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress1.png); |
||||||
|
} |
||||||
|
16% { |
||||||
|
background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress2.png); |
||||||
|
} |
||||||
|
32% { |
||||||
|
background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress3.png); |
||||||
|
} |
||||||
|
48% { |
||||||
|
background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress4.png); |
||||||
|
} |
||||||
|
64% { |
||||||
|
background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress5.png); |
||||||
|
} |
||||||
|
80% { |
||||||
|
background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress6.png); |
||||||
|
} |
||||||
|
100% { |
||||||
|
background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress1.png); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,53 @@ |
|||||||
|
<!-- 下拉刷新区域 --> |
||||||
|
<template> |
||||||
|
<view v-if="mOption.use" class="mescroll-downwarp" :style="{'background':mOption.bgColor,'color':mOption.textColor}"> |
||||||
|
<view class="downwarp-content"> |
||||||
|
<view v-if="isDownLoading" class="downwarp-progress"></view> |
||||||
|
<view v-else class="downwarp-arrow" :style="{ transform: downRotate }"></view> |
||||||
|
<view class="downwarp-tip">{{ downText }}</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
props: { |
||||||
|
option: Object, // down的配置项 |
||||||
|
type: Number // 下拉状态(inOffset:1, outOffset:2, showLoading:3, endDownScroll:4) |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
// 支付宝小程序需写成计算属性,prop定义default仍报错 |
||||||
|
mOption() { |
||||||
|
return this.option || {}; |
||||||
|
}, |
||||||
|
// 是否在加载中 |
||||||
|
isDownLoading() { |
||||||
|
return this.type === 3; |
||||||
|
}, |
||||||
|
// 旋转的角度 |
||||||
|
downRotate() { |
||||||
|
return this.type === 2 ? 'rotate(-180deg)' : 'rotate(0deg)'; |
||||||
|
}, |
||||||
|
// 文本提示 |
||||||
|
downText() { |
||||||
|
switch (this.type) { |
||||||
|
case 1: |
||||||
|
return this.mOption.textInOffset; |
||||||
|
case 2: |
||||||
|
return this.mOption.textOutOffset; |
||||||
|
case 3: |
||||||
|
return this.mOption.textLoading; |
||||||
|
case 4: |
||||||
|
return this.mOption.textLoading; |
||||||
|
default: |
||||||
|
return this.mOption.textInOffset; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
</script> |
||||||
|
|
||||||
|
<style> |
||||||
|
@import '../../../mescroll-uni/components/mescroll-down.css'; |
||||||
|
@import './mescroll-down.css'; |
||||||
|
</style> |
@ -0,0 +1,32 @@ |
|||||||
|
/*上拉加载--旋转进度条*/ |
||||||
|
.mescroll-upwarp .upwarp-progress { |
||||||
|
width: 36px; |
||||||
|
height: 36px; |
||||||
|
border: none; |
||||||
|
margin: auto; |
||||||
|
background-size: contain; |
||||||
|
animation: progressRotate 0.6s steps(6, start) infinite; |
||||||
|
} |
||||||
|
@keyframes progressRotate { |
||||||
|
0% { |
||||||
|
background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress1.png); |
||||||
|
} |
||||||
|
16% { |
||||||
|
background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress2.png); |
||||||
|
} |
||||||
|
32% { |
||||||
|
background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress3.png); |
||||||
|
} |
||||||
|
48% { |
||||||
|
background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress4.png); |
||||||
|
} |
||||||
|
64% { |
||||||
|
background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress5.png); |
||||||
|
} |
||||||
|
80% { |
||||||
|
background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress6.png); |
||||||
|
} |
||||||
|
100% { |
||||||
|
background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress1.png); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,40 @@ |
|||||||
|
<!-- 上拉加载区域 --> |
||||||
|
<template> |
||||||
|
<view class="mescroll-upwarp" :style="{'background':mOption.bgColor,'color':mOption.textColor}"> |
||||||
|
<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) --> |
||||||
|
<view v-show="isUpLoading"> |
||||||
|
<view class="upwarp-progress mescroll-rotate"></view> |
||||||
|
<view class="upwarp-tip">{{ mOption.textLoading }}</view> |
||||||
|
</view> |
||||||
|
<!-- 无数据 --> |
||||||
|
<view v-if="isUpNoMore" class="upwarp-nodata">{{ mOption.textNoMore }}</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
props: { |
||||||
|
option: Object, // up的配置项 |
||||||
|
type: Number // 上拉加载的状态:0(loading前),1(loading中),2(没有更多了,显示END文本提示),3(没有更多了,不显示END文本提示) |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
// 支付宝小程序需写成计算属性,prop定义default仍报错 |
||||||
|
mOption() { |
||||||
|
return this.option || {}; |
||||||
|
}, |
||||||
|
// 加载中 |
||||||
|
isUpLoading() { |
||||||
|
return this.type === 1; |
||||||
|
}, |
||||||
|
// 没有更多了 |
||||||
|
isUpNoMore() { |
||||||
|
return this.type === 2; |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
</script> |
||||||
|
|
||||||
|
<style> |
||||||
|
@import '../../../mescroll-uni/components/mescroll-up.css'; |
||||||
|
@import './mescroll-up.css'; |
||||||
|
</style> |
@ -0,0 +1,350 @@ |
|||||||
|
<template> |
||||||
|
<view |
||||||
|
class="mescroll-body mescroll-render-touch" |
||||||
|
:style="{'minHeight':minHeight, 'padding-top': padTop, 'padding-bottom': padBottom}" |
||||||
|
:class="{'mescorll-sticky': sticky}" |
||||||
|
@touchstart="wxsBiz.touchstartEvent" |
||||||
|
@touchmove="wxsBiz.touchmoveEvent" |
||||||
|
@touchend="wxsBiz.touchendEvent" |
||||||
|
@touchcancel="wxsBiz.touchendEvent" |
||||||
|
:change:prop="wxsBiz.propObserver" |
||||||
|
:prop="wxsProp" |
||||||
|
> |
||||||
|
|
||||||
|
<!-- 状态栏 --> |
||||||
|
<view v-if="topbar&&statusBarHeight" class="mescroll-topbar" :style="{height: statusBarHeight+'px', background: topbar}"></view> |
||||||
|
|
||||||
|
<view class="mescroll-body-content mescroll-wxs-content" :style="{ transform: translateY, transition: transition }" :change:prop="wxsBiz.callObserver" :prop="callProp"> |
||||||
|
<!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)--> |
||||||
|
<!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType"></mescroll-down> --> |
||||||
|
<view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}"> |
||||||
|
<view class="downwarp-content"> |
||||||
|
<view v-if="isDownLoading" class="downwarp-progress"></view> |
||||||
|
<view v-else class="downwarp-arrow" :style="{ transform: downRotate }"></view> |
||||||
|
<view class="downwarp-tip">{{ downText }}</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<!-- 列表内容 --> |
||||||
|
<slot></slot> |
||||||
|
|
||||||
|
<!-- 空布局 --> |
||||||
|
<mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty> |
||||||
|
|
||||||
|
<!-- 上拉加载区域 (下拉刷新时不显示,支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)--> |
||||||
|
<!-- <mescroll-up v-if="mescroll.optUp.use && downLoadType !== 3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> --> |
||||||
|
<view class="mescroll-upwarp" :style="{'background':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}"> |
||||||
|
<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) --> |
||||||
|
<view v-show="upLoadType===1"> |
||||||
|
<view class="upwarp-progress mescroll-rotate"></view> |
||||||
|
<view class="upwarp-tip">{{ mescroll.optUp.textLoading }}</view> |
||||||
|
</view> |
||||||
|
<!-- 无数据 --> |
||||||
|
<view v-if="upLoadType===2" class="upwarp-nodata">{{ mescroll.optUp.textNoMore }}</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<!-- 底部是否偏移TabBar的高度(仅H5端生效) --> |
||||||
|
<!-- #ifdef H5 --> |
||||||
|
<view v-if="bottombar && windowBottom>0" class="mescroll-bottombar" :style="{height: windowBottom+'px'}"></view> |
||||||
|
<!-- #endif --> |
||||||
|
|
||||||
|
<!-- 适配iPhoneX --> |
||||||
|
<view v-if="safearea" class="mescroll-safearea"></view> |
||||||
|
|
||||||
|
<!-- 回到顶部按钮 (fixed元素需写在transform外面,防止降级为absolute)--> |
||||||
|
<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top> |
||||||
|
|
||||||
|
<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 --> |
||||||
|
<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 --> |
||||||
|
<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view> |
||||||
|
<!-- #endif --> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<!-- 微信小程序, QQ小程序, app, h5使用wxs --> |
||||||
|
<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 --> |
||||||
|
<script src="../../mescroll-uni/wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script> |
||||||
|
<!-- #endif --> |
||||||
|
|
||||||
|
<!-- app, h5使用renderjs --> |
||||||
|
<!-- #ifdef APP-PLUS || H5 --> |
||||||
|
<script module="renderBiz" lang="renderjs"> |
||||||
|
import renderBiz from '../../mescroll-uni/wxs/renderjs.js'; |
||||||
|
export default { |
||||||
|
mixins: [renderBiz] |
||||||
|
} |
||||||
|
</script> |
||||||
|
<!-- #endif --> |
||||||
|
|
||||||
|
<script> |
||||||
|
import MeScroll from '../../mescroll-uni/mescroll-uni.js'; |
||||||
|
import GlobalOption from './mescroll-uni-option.js'; |
||||||
|
import MescrollEmpty from '../../mescroll-uni/components/mescroll-empty.vue'; |
||||||
|
import MescrollTop from '../../mescroll-uni/components/mescroll-top.vue'; |
||||||
|
import WxsMixin from '../../mescroll-uni/wxs/mixins.js'; |
||||||
|
|
||||||
|
export default { |
||||||
|
mixins: [WxsMixin], |
||||||
|
components: { |
||||||
|
MescrollEmpty, |
||||||
|
MescrollTop |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
mescroll: null, // mescroll实例 |
||||||
|
downHight: 0, //下拉刷新: 容器高度 |
||||||
|
downLoadType: 0, // 下拉刷新状态: 0(loading前), 1(inOffset), 2(outOffset), 3(showLoading), 4(endDownScroll) |
||||||
|
upLoadType: 0, // 上拉加载状态:0(loading前),1(loading中),2(没有更多了,显示END文本提示),3(没有更多了,不显示END文本提示) |
||||||
|
isShowEmpty: false, // 是否显示空布局 |
||||||
|
isShowToTop: false, // 是否显示回到顶部按钮 |
||||||
|
windowHeight: 0, // 可使用窗口的高度 |
||||||
|
windowBottom: 0, // 可使用窗口的底部位置 |
||||||
|
statusBarHeight: 0 // 状态栏高度 |
||||||
|
}; |
||||||
|
}, |
||||||
|
props: { |
||||||
|
down: Object, // 下拉刷新的参数配置 |
||||||
|
up: Object, // 上拉加载的参数配置 |
||||||
|
top: [String, Number], // 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight) |
||||||
|
topbar: [Boolean, String], // top的偏移量是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变) |
||||||
|
bottom: [String, Number], // 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight) |
||||||
|
safearea: Boolean, // bottom的偏移量是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用) |
||||||
|
height: [String, Number], // 指定mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉 |
||||||
|
bottombar:{ // 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效) |
||||||
|
type: Boolean, |
||||||
|
default: true |
||||||
|
}, |
||||||
|
sticky: Boolean // 是否支持sticky,默认false; 当值配置true时,需避免在mescroll-body标签前面加非定位的元素,否则下拉区域无法会隐藏 |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
// mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉 |
||||||
|
minHeight(){ |
||||||
|
return this.toPx(this.height || '100%') + 'px' |
||||||
|
}, |
||||||
|
// 下拉布局往下偏移的距离 (px) |
||||||
|
numTop() { |
||||||
|
return this.toPx(this.top) |
||||||
|
}, |
||||||
|
padTop() { |
||||||
|
return this.numTop + 'px'; |
||||||
|
}, |
||||||
|
// 上拉布局往上偏移 (px) |
||||||
|
numBottom() { |
||||||
|
return this.toPx(this.bottom); |
||||||
|
}, |
||||||
|
padBottom() { |
||||||
|
return this.numBottom + 'px'; |
||||||
|
}, |
||||||
|
// 是否为重置下拉的状态 |
||||||
|
isDownReset() { |
||||||
|
return this.downLoadType === 3 || this.downLoadType === 4; |
||||||
|
}, |
||||||
|
// 过渡 |
||||||
|
transition() { |
||||||
|
return this.isDownReset ? 'transform 300ms' : ''; |
||||||
|
}, |
||||||
|
translateY() { |
||||||
|
return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' : ''; // transform会使fixed失效,需注意把fixed元素写在mescroll之外 |
||||||
|
}, |
||||||
|
// 是否在加载中 |
||||||
|
isDownLoading() { |
||||||
|
return this.downLoadType === 3; |
||||||
|
}, |
||||||
|
// 旋转的角度 |
||||||
|
downRotate() { |
||||||
|
return this.downLoadType === 2 ? 'rotate(-180deg)' : 'rotate(0deg)'; |
||||||
|
}, |
||||||
|
// 文本提示 |
||||||
|
downText() { |
||||||
|
if(!this.mescroll) return ""; |
||||||
|
switch (this.downLoadType) { |
||||||
|
case 1: |
||||||
|
return this.mescroll.optDown.textInOffset; |
||||||
|
case 2: |
||||||
|
return this.mescroll.optDown.textOutOffset; |
||||||
|
case 3: |
||||||
|
return this.mescroll.optDown.textLoading; |
||||||
|
case 4: |
||||||
|
return this.mescroll.isDownEndSuccess ? this.mescroll.optDown.textSuccess : this.mescroll.isDownEndSuccess==false ? this.mescroll.optDown.textErr : this.mescroll.optDown.textInOffset; |
||||||
|
default: |
||||||
|
return this.mescroll.optDown.textInOffset; |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
//number,rpx,upx,px,% --> px的数值 |
||||||
|
toPx(num) { |
||||||
|
if (typeof num === 'string') { |
||||||
|
if (num.indexOf('px') !== -1) { |
||||||
|
if (num.indexOf('rpx') !== -1) { |
||||||
|
// "10rpx" |
||||||
|
num = num.replace('rpx', ''); |
||||||
|
} else if (num.indexOf('upx') !== -1) { |
||||||
|
// "10upx" |
||||||
|
num = num.replace('upx', ''); |
||||||
|
} else { |
||||||
|
// "10px" |
||||||
|
return Number(num.replace('px', '')); |
||||||
|
} |
||||||
|
} else if (num.indexOf('%') !== -1) { |
||||||
|
// 传百分比,则相对于windowHeight,传"10%"则等于windowHeight的10% |
||||||
|
let rate = Number(num.replace('%', '')) / 100; |
||||||
|
return this.windowHeight * rate; |
||||||
|
} |
||||||
|
} |
||||||
|
return num ? uni.upx2px(Number(num)) : 0; |
||||||
|
}, |
||||||
|
// 点击空布局的按钮回调 |
||||||
|
emptyClick() { |
||||||
|
this.$emit('emptyclick', this.mescroll); |
||||||
|
}, |
||||||
|
// 点击回到顶部的按钮回调 |
||||||
|
toTopClick() { |
||||||
|
this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部 |
||||||
|
this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调 |
||||||
|
} |
||||||
|
}, |
||||||
|
// 使用created初始化mescroll对象; 如果用mounted部分css样式编译到H5会失效 |
||||||
|
created() { |
||||||
|
let vm = this; |
||||||
|
|
||||||
|
let diyOption = { |
||||||
|
// 下拉刷新的配置 |
||||||
|
down: { |
||||||
|
inOffset() { |
||||||
|
vm.downLoadType = 1; // 下拉的距离进入offset范围内那一刻的回调 (自定义mescroll组件时,此行不可删) |
||||||
|
}, |
||||||
|
outOffset() { |
||||||
|
vm.downLoadType = 2; // 下拉的距离大于offset那一刻的回调 (自定义mescroll组件时,此行不可删) |
||||||
|
}, |
||||||
|
onMoving(mescroll, rate, downHight) { |
||||||
|
// 下拉过程中的回调,滑动过程一直在执行; |
||||||
|
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删) |
||||||
|
}, |
||||||
|
showLoading(mescroll, downHight) { |
||||||
|
vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删) |
||||||
|
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删) |
||||||
|
}, |
||||||
|
beforeEndDownScroll(mescroll){ |
||||||
|
vm.downLoadType = 4; |
||||||
|
return mescroll.optDown.beforeEndDelay // 延时结束的时长 |
||||||
|
}, |
||||||
|
endDownScroll() { |
||||||
|
vm.downLoadType = 4; // 结束下拉 (自定义mescroll组件时,此行不可删) |
||||||
|
vm.downHight = 0; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删) |
||||||
|
if(vm.downResetTimer) {clearTimeout(vm.downResetTimer); vm.downResetTimer = null} // 移除重置倒计时 |
||||||
|
vm.downResetTimer = setTimeout(()=>{ // 过渡动画执行完毕后,需重置为0的状态,避免下次inOffset不及时显示textInOffset |
||||||
|
if(vm.downLoadType === 4) vm.downLoadType = 0 |
||||||
|
},300) |
||||||
|
}, |
||||||
|
// 派发下拉刷新的回调 |
||||||
|
callback: function(mescroll) { |
||||||
|
vm.$emit('down', mescroll); |
||||||
|
} |
||||||
|
}, |
||||||
|
// 上拉加载的配置 |
||||||
|
up: { |
||||||
|
// 显示加载中的回调 |
||||||
|
showLoading() { |
||||||
|
vm.upLoadType = 1; |
||||||
|
}, |
||||||
|
// 显示无更多数据的回调 |
||||||
|
showNoMore() { |
||||||
|
vm.upLoadType = 2; |
||||||
|
}, |
||||||
|
// 隐藏上拉加载的回调 |
||||||
|
hideUpScroll(mescroll) { |
||||||
|
vm.upLoadType = mescroll.optUp.hasNext ? 0 : 3; |
||||||
|
}, |
||||||
|
// 空布局 |
||||||
|
empty: { |
||||||
|
onShow(isShow) { |
||||||
|
// 显示隐藏的回调 |
||||||
|
vm.isShowEmpty = isShow; |
||||||
|
} |
||||||
|
}, |
||||||
|
// 回到顶部 |
||||||
|
toTop: { |
||||||
|
onShow(isShow) { |
||||||
|
// 显示隐藏的回调 |
||||||
|
vm.isShowToTop = isShow; |
||||||
|
} |
||||||
|
}, |
||||||
|
// 派发上拉加载的回调 |
||||||
|
callback: function(mescroll) { |
||||||
|
vm.$emit('up', mescroll); |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
MeScroll.extend(diyOption, GlobalOption); // 混入全局的配置 |
||||||
|
let myOption = JSON.parse(JSON.stringify({down: vm.down,up: vm.up})); // 深拷贝,避免对props的影响 |
||||||
|
MeScroll.extend(myOption, diyOption); // 混入具体界面的配置 |
||||||
|
|
||||||
|
// 初始化MeScroll对象 |
||||||
|
vm.mescroll = new MeScroll(myOption, true); // 传入true,标记body为滚动区域 |
||||||
|
// init回调mescroll对象 |
||||||
|
vm.$emit('init', vm.mescroll); |
||||||
|
|
||||||
|
// 设置高度 |
||||||
|
const sys = uni.getSystemInfoSync(); |
||||||
|
if (sys.windowHeight) vm.windowHeight = sys.windowHeight; |
||||||
|
if (sys.windowBottom) vm.windowBottom = sys.windowBottom; |
||||||
|
if (sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight; |
||||||
|
// 使down的bottomOffset生效 |
||||||
|
vm.mescroll.setBodyHeight(sys.windowHeight); |
||||||
|
|
||||||
|
// 因为使用的是page的scroll,这里需自定义scrollTo |
||||||
|
vm.mescroll.resetScrollTo((y, t) => { |
||||||
|
if(typeof y === 'string'){ |
||||||
|
// 滚动到指定view (y为css选择器) |
||||||
|
setTimeout(()=>{ // 延时确保view已渲染; 不使用$nextTick |
||||||
|
let selector; |
||||||
|
if(y.indexOf('#')==-1 && y.indexOf('.')==-1){ |
||||||
|
selector = '#'+y // 不带#和. 则默认为id选择器 |
||||||
|
}else{ |
||||||
|
selector = y |
||||||
|
// #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-DINGTALK |
||||||
|
if(y.indexOf('>>>')!=-1){ // 不支持跨自定义组件的后代选择器 (转为普通的选择器即可跨组件查询) |
||||||
|
selector = y.split('>>>')[1].trim() |
||||||
|
} |
||||||
|
// #endif |
||||||
|
} |
||||||
|
uni.createSelectorQuery().select(selector).boundingClientRect(function(rect){ |
||||||
|
if (rect) { |
||||||
|
let top = rect.top |
||||||
|
top += vm.mescroll.getScrollTop() |
||||||
|
uni.pageScrollTo({ |
||||||
|
scrollTop: top, |
||||||
|
duration: t |
||||||
|
}) |
||||||
|
} else{ |
||||||
|
console.error(selector + ' does not exist'); |
||||||
|
} |
||||||
|
}).exec() |
||||||
|
},30) |
||||||
|
} else{ |
||||||
|
// 滚动到指定位置 (y必须为数字) |
||||||
|
uni.pageScrollTo({ |
||||||
|
scrollTop: y, |
||||||
|
duration: t |
||||||
|
}) |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
// 具体的界面如果不配置up.toTop.safearea,则取本vue的safearea值 |
||||||
|
if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else { |
||||||
|
vm.mescroll.optUp.toTop.safearea = vm.safearea; |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
</script> |
||||||
|
|
||||||
|
<style> |
||||||
|
@import "../../mescroll-uni/mescroll-uni.css"; |
||||||
|
@import "../../mescroll-uni/components/mescroll-down.css"; |
||||||
|
@import "../../mescroll-uni/components/mescroll-up.css"; |
||||||
|
@import "./components/mescroll-down.css"; |
||||||
|
@import "./components/mescroll-up.css"; |
||||||
|
</style> |
@ -0,0 +1,36 @@ |
|||||||
|
// 全局配置
|
||||||
|
// mescroll-body 和 mescroll-uni 通用
|
||||||
|
const GlobalOption = { |
||||||
|
down: { |
||||||
|
// 其他down的配置参数也可以写,这里只展示了常用的配置:
|
||||||
|
textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本
|
||||||
|
textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本
|
||||||
|
textLoading: '加载中 ...', // 加载中的提示文本
|
||||||
|
textSuccess: '加载成功', // 加载成功的文本
|
||||||
|
textErr: '加载失败', // 加载失败的文本
|
||||||
|
beforeEndDelay: 0, // 延时结束的时长 (此处设置为0)
|
||||||
|
offset: 80, // 在列表顶部,下拉大于80px,松手即可触发下拉刷新的回调
|
||||||
|
native: false // 是否使用系统自带的下拉刷新; 默认false; 仅在mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
|
||||||
|
}, |
||||||
|
up: { |
||||||
|
// 其他up的配置参数也可以写,这里只展示了常用的配置:
|
||||||
|
textLoading: '加载中 ...', // 加载中的提示文本
|
||||||
|
textNoMore: '-- END --', // 没有更多数据的提示文本
|
||||||
|
offset: 150, // 距底部多远时,触发upCallback,仅mescroll-uni生效 ( mescroll-body配置的是pages.json的 onReachBottomDistance )
|
||||||
|
toTop: { |
||||||
|
// 回到顶部按钮,需配置src才显示
|
||||||
|
src: "https://www.mescroll.com/img/mescroll-totop.png", // 图片路径 (建议放入static目录, 如 /static/img/mescroll-totop.png )
|
||||||
|
offset: 1000, // 列表滚动多少距离才显示回到顶部按钮,默认1000px
|
||||||
|
right: 20, // 到右边的距离, 默认20 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
|
||||||
|
bottom: 120, // 到底部的距离, 默认120 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
|
||||||
|
width: 72 // 回到顶部图标的宽度, 默认72 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
|
||||||
|
}, |
||||||
|
empty: { |
||||||
|
use: true, // 是否显示空布局
|
||||||
|
icon: "https://www.mescroll.com/img/mescroll-empty.png", // 图标路径 (建议放入static目录, 如 /static/img/mescroll-empty.png )
|
||||||
|
tip: '~ 空空如也 ~' // 提示
|
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export default GlobalOption |
@ -0,0 +1,430 @@ |
|||||||
|
<template> |
||||||
|
<view class="mescroll-uni-warp"> |
||||||
|
<scroll-view :id="viewId" class="mescroll-uni" :class="{'mescroll-uni-fixed':isFixed}" :style="{'height':scrollHeight,'padding-top':padTop,'padding-bottom':padBottom,'top':fixedTop,'bottom':fixedBottom}" :scroll-top="scrollTop" :scroll-with-animation="scrollAnim" @scroll="scroll" :scroll-y='scrollable' :enable-back-to-top="true" :throttle="false"> |
||||||
|
<view class="mescroll-uni-content mescroll-render-touch" |
||||||
|
@touchstart="wxsBiz.touchstartEvent" |
||||||
|
@touchmove="wxsBiz.touchmoveEvent" |
||||||
|
@touchend="wxsBiz.touchendEvent" |
||||||
|
@touchcancel="wxsBiz.touchendEvent" |
||||||
|
:change:prop="wxsBiz.propObserver" |
||||||
|
:prop="wxsProp"> |
||||||
|
|
||||||
|
<!-- 状态栏 --> |
||||||
|
<view v-if="topbar&&statusBarHeight" class="mescroll-topbar" :style="{height: statusBarHeight+'px', background: topbar}"></view> |
||||||
|
|
||||||
|
<view class="mescroll-wxs-content" :style="{'transform': translateY, 'transition': transition}" :change:prop="wxsBiz.callObserver" :prop="callProp"> |
||||||
|
<!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)--> |
||||||
|
<!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType"></mescroll-down> --> |
||||||
|
<view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}"> |
||||||
|
<view class="downwarp-content"> |
||||||
|
<view v-if="isDownLoading" class="downwarp-progress"></view> |
||||||
|
<view v-else class="downwarp-arrow" :style="{ transform: downRotate }"></view> |
||||||
|
<view class="downwarp-tip">{{ downText }}</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<!-- 列表内容 --> |
||||||
|
<slot></slot> |
||||||
|
|
||||||
|
<!-- 空布局 --> |
||||||
|
<mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty> |
||||||
|
|
||||||
|
<!-- 上拉加载区域 (下拉刷新时不显示,支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)--> |
||||||
|
<!-- <mescroll-up v-if="mescroll.optUp.use && downLoadType !== 3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> --> |
||||||
|
<view class="mescroll-upwarp" :style="{'background':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}"> |
||||||
|
<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) --> |
||||||
|
<view v-show="upLoadType===1"> |
||||||
|
<view class="upwarp-progress mescroll-rotate"></view> |
||||||
|
<view class="upwarp-tip">{{ mescroll.optUp.textLoading }}</view> |
||||||
|
</view> |
||||||
|
<!-- 无数据 --> |
||||||
|
<view v-if="upLoadType===2" class="upwarp-nodata">{{ mescroll.optUp.textNoMore }}</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<!-- 底部是否偏移TabBar的高度(仅H5端生效) --> |
||||||
|
<!-- #ifdef H5 --> |
||||||
|
<view v-if="bottombar && windowBottom>0" class="mescroll-bottombar" :style="{height: windowBottom+'px'}"></view> |
||||||
|
<!-- #endif --> |
||||||
|
|
||||||
|
<!-- 适配iPhoneX --> |
||||||
|
<view v-if="safearea" class="mescroll-safearea"></view> |
||||||
|
|
||||||
|
</view> |
||||||
|
</scroll-view> |
||||||
|
|
||||||
|
<!-- 回到顶部按钮 (fixed元素,需写在scroll-view外面,防止滚动的时候抖动)--> |
||||||
|
<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top> |
||||||
|
|
||||||
|
<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 --> |
||||||
|
<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 --> |
||||||
|
<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view> |
||||||
|
<!-- #endif --> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<!-- 微信小程序, QQ小程序, app, h5使用wxs --> |
||||||
|
<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 --> |
||||||
|
<script src="../../mescroll-uni/wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script> |
||||||
|
<!-- #endif --> |
||||||
|
|
||||||
|
<!-- app, h5使用renderjs --> |
||||||
|
<!-- #ifdef APP-PLUS || H5 --> |
||||||
|
<script module="renderBiz" lang="renderjs"> |
||||||
|
import renderBiz from '../../mescroll-uni/wxs/renderjs.js'; |
||||||
|
export default { |
||||||
|
mixins: [renderBiz] |
||||||
|
} |
||||||
|
</script> |
||||||
|
<!-- #endif --> |
||||||
|
|
||||||
|
<script> |
||||||
|
import MeScroll from '../../mescroll-uni/mescroll-uni.js'; |
||||||
|
import GlobalOption from './mescroll-uni-option.js'; |
||||||
|
import MescrollEmpty from '../../mescroll-uni/components/mescroll-empty.vue'; |
||||||
|
import MescrollTop from '../../mescroll-uni/components/mescroll-top.vue'; |
||||||
|
import WxsMixin from '../../mescroll-uni/wxs/mixins.js'; |
||||||
|
|
||||||
|
export default { |
||||||
|
mixins: [WxsMixin], |
||||||
|
components: { |
||||||
|
MescrollEmpty, |
||||||
|
MescrollTop |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
mescroll: null, // mescroll实例 |
||||||
|
viewId: 'id_' + Math.random().toString(36).substr(2,16), // 随机生成mescroll的id(不能数字开头,否则找不到元素) |
||||||
|
downHight: 0, //下拉刷新: 容器高度 |
||||||
|
downLoadType: 0, // 下拉刷新状态: 0(loading前), 1(inOffset), 2(outOffset), 3(showLoading), 4(endDownScroll) |
||||||
|
upLoadType: 0, // 上拉加载状态: 0(loading前), 1loading中, 2没有更多了,显示END文本提示, 3(没有更多了,不显示END文本提示) |
||||||
|
isShowEmpty: false, // 是否显示空布局 |
||||||
|
isShowToTop: false, // 是否显示回到顶部按钮 |
||||||
|
scrollTop: 0, // 滚动条的位置 |
||||||
|
scrollAnim: false, // 是否开启滚动动画 |
||||||
|
windowTop: 0, // 可使用窗口的顶部位置 |
||||||
|
windowBottom: 0, // 可使用窗口的底部位置 |
||||||
|
windowHeight: 0, // 可使用窗口的高度 |
||||||
|
statusBarHeight: 0 // 状态栏高度 |
||||||
|
} |
||||||
|
}, |
||||||
|
props: { |
||||||
|
down: Object, // 下拉刷新的参数配置 |
||||||
|
up: Object, // 上拉加载的参数配置 |
||||||
|
top: [String, Number], // 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight) |
||||||
|
topbar: [Boolean, String], // top的偏移量是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变) |
||||||
|
bottom: [String, Number], // 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight) |
||||||
|
safearea: Boolean, // bottom的偏移量是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用) |
||||||
|
fixed: { // 是否通过fixed固定mescroll的高度, 默认true |
||||||
|
type: Boolean, |
||||||
|
default: true |
||||||
|
}, |
||||||
|
height: [String, Number], // 指定mescroll的高度, 此项有值,则不使用fixed. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight) |
||||||
|
bottombar:{ // 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效) |
||||||
|
type: Boolean, |
||||||
|
default: true |
||||||
|
} |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
// 是否使用fixed定位 (当height有值,则不使用) |
||||||
|
isFixed(){ |
||||||
|
return !this.height && this.fixed |
||||||
|
}, |
||||||
|
// mescroll的高度 |
||||||
|
scrollHeight(){ |
||||||
|
if (this.isFixed) { |
||||||
|
return "auto" |
||||||
|
} else if(this.height){ |
||||||
|
return this.toPx(this.height) + 'px' |
||||||
|
}else{ |
||||||
|
return "100%" |
||||||
|
} |
||||||
|
}, |
||||||
|
// 下拉布局往下偏移的距离 (px) |
||||||
|
numTop() { |
||||||
|
return this.toPx(this.top) |
||||||
|
}, |
||||||
|
fixedTop() { |
||||||
|
return this.isFixed ? (this.numTop + this.windowTop) + 'px' : 0 |
||||||
|
}, |
||||||
|
padTop() { |
||||||
|
return !this.isFixed ? this.numTop + 'px' : 0 |
||||||
|
}, |
||||||
|
// 上拉布局往上偏移 (px) |
||||||
|
numBottom() { |
||||||
|
return this.toPx(this.bottom) |
||||||
|
}, |
||||||
|
fixedBottom() { |
||||||
|
return this.isFixed ? (this.numBottom + this.windowBottom) + 'px' : 0 |
||||||
|
}, |
||||||
|
padBottom() { |
||||||
|
return !this.isFixed ? this.numBottom + 'px' : 0 |
||||||
|
}, |
||||||
|
// 是否为重置下拉的状态 |
||||||
|
isDownReset(){ |
||||||
|
return this.downLoadType===3 || this.downLoadType===4 |
||||||
|
}, |
||||||
|
// 过渡 |
||||||
|
transition() { |
||||||
|
return this.isDownReset ? 'transform 300ms' : '' |
||||||
|
}, |
||||||
|
translateY() { |
||||||
|
return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' : '' // transform会使fixed失效,需注意把fixed元素写在mescroll之外 |
||||||
|
}, |
||||||
|
// 列表是否可滑动 |
||||||
|
scrollable(){ |
||||||
|
return this.downLoadType===0 || this.isDownReset |
||||||
|
}, |
||||||
|
// 是否在加载中 |
||||||
|
isDownLoading() { |
||||||
|
return this.downLoadType === 3; |
||||||
|
}, |
||||||
|
// 旋转的角度 |
||||||
|
downRotate() { |
||||||
|
return this.downLoadType === 2 ? 'rotate(-180deg)' : 'rotate(0deg)'; |
||||||
|
}, |
||||||
|
// 文本提示 |
||||||
|
downText() { |
||||||
|
if(!this.mescroll) return ""; |
||||||
|
switch (this.downLoadType) { |
||||||
|
case 1: |
||||||
|
return this.mescroll.optDown.textInOffset; |
||||||
|
case 2: |
||||||
|
return this.mescroll.optDown.textOutOffset; |
||||||
|
case 3: |
||||||
|
return this.mescroll.optDown.textLoading; |
||||||
|
case 4: |
||||||
|
return this.mescroll.isDownEndSuccess ? this.mescroll.optDown.textSuccess : this.mescroll.isDownEndSuccess==false ? this.mescroll.optDown.textErr : this.mescroll.optDown.textInOffset; |
||||||
|
default: |
||||||
|
return this.mescroll.optDown.textInOffset; |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
//number,rpx,upx,px,% --> px的数值 |
||||||
|
toPx(num){ |
||||||
|
if(typeof num === "string"){ |
||||||
|
if (num.indexOf('px') !== -1) { |
||||||
|
if(num.indexOf('rpx') !== -1) { // "10rpx" |
||||||
|
num = num.replace('rpx', ''); |
||||||
|
} else if(num.indexOf('upx') !== -1) { // "10upx" |
||||||
|
num = num.replace('upx', ''); |
||||||
|
} else { // "10px" |
||||||
|
return Number(num.replace('px', '')) |
||||||
|
} |
||||||
|
}else if (num.indexOf('%') !== -1){ |
||||||
|
// 传百分比,则相对于windowHeight,传"10%"则等于windowHeight的10% |
||||||
|
let rate = Number(num.replace("%","")) / 100 |
||||||
|
return this.windowHeight * rate |
||||||
|
} |
||||||
|
} |
||||||
|
return num ? uni.upx2px(Number(num)) : 0 |
||||||
|
}, |
||||||
|
//注册列表滚动事件,用于下拉刷新和上拉加载 |
||||||
|
scroll(e) { |
||||||
|
this.mescroll.scroll(e.detail, () => { |
||||||
|
this.$emit('scroll', this.mescroll) // 此时可直接通过 this.mescroll.scrollTop获取滚动条位置; this.mescroll.isScrollUp获取是否向上滑动 |
||||||
|
}) |
||||||
|
}, |
||||||
|
// 点击空布局的按钮回调 |
||||||
|
emptyClick() { |
||||||
|
this.$emit('emptyclick', this.mescroll) |
||||||
|
}, |
||||||
|
// 点击回到顶部的按钮回调 |
||||||
|
toTopClick() { |
||||||
|
this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部 |
||||||
|
this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调 |
||||||
|
}, |
||||||
|
// 更新滚动区域的高度 (使内容不满屏和到底,都可继续翻页) |
||||||
|
setClientHeight() { |
||||||
|
if (this.mescroll.getClientHeight(true) === 0 && !this.isExec) { |
||||||
|
this.isExec = true; // 避免多次获取 |
||||||
|
this.$nextTick(() => { // 确保dom已渲染 |
||||||
|
this.getClientInfo(data=>{ |
||||||
|
this.isExec = false; |
||||||
|
if (data) { |
||||||
|
this.mescroll.setClientHeight(data.height); |
||||||
|
} else if (this.clientNum != 3) { // 极少部分情况,可能dom还未渲染完毕,递归获取,最多重试3次 |
||||||
|
this.clientNum = this.clientNum == null ? 1 : this.clientNum + 1; |
||||||
|
setTimeout(() => { |
||||||
|
this.setClientHeight() |
||||||
|
}, this.clientNum * 100) |
||||||
|
} |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
}, |
||||||
|
// 获取滚动区域的信息 |
||||||
|
getClientInfo(success){ |
||||||
|
let query = uni.createSelectorQuery(); |
||||||
|
// #ifndef MP-ALIPAY || MP-DINGTALK |
||||||
|
query = query.in(this) // 支付宝小程序不支持in(this),而字节跳动小程序必须写in(this), 否则都取不到值 |
||||||
|
// #endif |
||||||
|
let view = query.select('#' + this.viewId); |
||||||
|
view.boundingClientRect(data => { |
||||||
|
success(data) |
||||||
|
}).exec(); |
||||||
|
} |
||||||
|
}, |
||||||
|
// 使用created初始化mescroll对象; 如果用mounted部分css样式编译到H5会失效 |
||||||
|
created() { |
||||||
|
let vm = this; |
||||||
|
|
||||||
|
let diyOption = { |
||||||
|
// 下拉刷新的配置 |
||||||
|
down: { |
||||||
|
inOffset() { |
||||||
|
vm.downLoadType = 1; // 下拉的距离进入offset范围内那一刻的回调 (自定义mescroll组件时,此行不可删) |
||||||
|
}, |
||||||
|
outOffset() { |
||||||
|
vm.downLoadType = 2; // 下拉的距离大于offset那一刻的回调 (自定义mescroll组件时,此行不可删) |
||||||
|
}, |
||||||
|
onMoving(mescroll, rate, downHight) { |
||||||
|
// 下拉过程中的回调,滑动过程一直在执行; |
||||||
|
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删) |
||||||
|
}, |
||||||
|
showLoading(mescroll, downHight) { |
||||||
|
vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删) |
||||||
|
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删) |
||||||
|
}, |
||||||
|
beforeEndDownScroll(mescroll){ |
||||||
|
vm.downLoadType = 4; |
||||||
|
return mescroll.optDown.beforeEndDelay // 延时结束的时长 |
||||||
|
}, |
||||||
|
endDownScroll() { |
||||||
|
vm.downLoadType = 4; // 结束下拉 (自定义mescroll组件时,此行不可删) |
||||||
|
vm.downHight = 0; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删) |
||||||
|
vm.downResetTimer && clearTimeout(vm.downResetTimer) |
||||||
|
vm.downResetTimer = setTimeout(()=>{ // 过渡动画执行完毕后,需重置为0的状态,以便置空this.transition,避免iOS小程序列表渲染不完整 |
||||||
|
if(vm.downLoadType===4) vm.downLoadType = 0 |
||||||
|
},300) |
||||||
|
}, |
||||||
|
// 派发下拉刷新的回调 |
||||||
|
callback: function(mescroll) { |
||||||
|
vm.$emit('down', mescroll) |
||||||
|
} |
||||||
|
}, |
||||||
|
// 上拉加载的配置 |
||||||
|
up: { |
||||||
|
// 显示加载中的回调 |
||||||
|
showLoading() { |
||||||
|
vm.upLoadType = 1; |
||||||
|
}, |
||||||
|
// 显示无更多数据的回调 |
||||||
|
showNoMore() { |
||||||
|
vm.upLoadType = 2; |
||||||
|
}, |
||||||
|
// 隐藏上拉加载的回调 |
||||||
|
hideUpScroll(mescroll) { |
||||||
|
vm.upLoadType = mescroll.optUp.hasNext ? 0 : 3; |
||||||
|
}, |
||||||
|
// 空布局 |
||||||
|
empty: { |
||||||
|
onShow(isShow) { // 显示隐藏的回调 |
||||||
|
vm.isShowEmpty = isShow; |
||||||
|
} |
||||||
|
}, |
||||||
|
// 回到顶部 |
||||||
|
toTop: { |
||||||
|
onShow(isShow) { // 显示隐藏的回调 |
||||||
|
vm.isShowToTop = isShow; |
||||||
|
} |
||||||
|
}, |
||||||
|
// 派发上拉加载的回调 |
||||||
|
callback: function(mescroll) { |
||||||
|
vm.$emit('up', mescroll); |
||||||
|
// 更新容器的高度 (多mescroll的情况) |
||||||
|
vm.setClientHeight() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
MeScroll.extend(diyOption, GlobalOption); // 混入全局的配置 |
||||||
|
let myOption = JSON.parse(JSON.stringify({ |
||||||
|
'down': vm.down, |
||||||
|
'up': vm.up |
||||||
|
})) // 深拷贝,避免对props的影响 |
||||||
|
MeScroll.extend(myOption, diyOption); // 混入具体界面的配置 |
||||||
|
|
||||||
|
// 初始化MeScroll对象 |
||||||
|
vm.mescroll = new MeScroll(myOption); |
||||||
|
vm.mescroll.viewId = vm.viewId; // 附带id |
||||||
|
// init回调mescroll对象 |
||||||
|
vm.$emit('init', vm.mescroll); |
||||||
|
|
||||||
|
// 设置高度 |
||||||
|
const sys = uni.getSystemInfoSync(); |
||||||
|
if(sys.windowTop) vm.windowTop = sys.windowTop; |
||||||
|
if(sys.windowBottom) vm.windowBottom = sys.windowBottom; |
||||||
|
if(sys.windowHeight) vm.windowHeight = sys.windowHeight; |
||||||
|
if(sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight; |
||||||
|
// 使down的bottomOffset生效 |
||||||
|
vm.mescroll.setBodyHeight(sys.windowHeight); |
||||||
|
|
||||||
|
// 因为使用的是scrollview,这里需自定义scrollTo |
||||||
|
vm.mescroll.resetScrollTo((y, t) => { |
||||||
|
vm.scrollAnim = (t !== 0); // t为0,则不使用动画过渡 |
||||||
|
if(typeof y === 'string'){ |
||||||
|
// 小程序不支持slot里面的scroll-into-view, 统一使用计算的方式实现 |
||||||
|
vm.getClientInfo(function(rect){ |
||||||
|
let mescrollTop = rect.top // mescroll到顶部的距离 |
||||||
|
let selector; |
||||||
|
if(y.indexOf('#')==-1 && y.indexOf('.')==-1){ |
||||||
|
selector = '#'+y // 不带#和. 则默认为id选择器 |
||||||
|
}else{ |
||||||
|
selector = y |
||||||
|
// #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-DINGTALK |
||||||
|
if(y.indexOf('>>>')!=-1){ // 不支持跨自定义组件的后代选择器 (转为普通的选择器即可跨组件查询) |
||||||
|
selector = y.split('>>>')[1].trim() |
||||||
|
} |
||||||
|
// #endif |
||||||
|
} |
||||||
|
uni.createSelectorQuery().select(selector).boundingClientRect(function(rect){ |
||||||
|
if (rect) { |
||||||
|
let curY = vm.mescroll.getScrollTop() |
||||||
|
let top = rect.top - mescrollTop |
||||||
|
top += curY |
||||||
|
if(!vm.isFixed) top -= vm.numTop |
||||||
|
vm.scrollTop = curY; |
||||||
|
vm.$nextTick(function() { |
||||||
|
vm.scrollTop = top |
||||||
|
}) |
||||||
|
} else{ |
||||||
|
console.error(selector + ' does not exist'); |
||||||
|
} |
||||||
|
}).exec() |
||||||
|
}) |
||||||
|
return; |
||||||
|
} |
||||||
|
let curY = vm.mescroll.getScrollTop() |
||||||
|
if (t === 0 || t === 300) { // 当t使用默认配置的300时,则使用系统自带的动画过渡 |
||||||
|
vm.scrollTop = curY; |
||||||
|
vm.$nextTick(function() { |
||||||
|
vm.scrollTop = y |
||||||
|
}) |
||||||
|
} else { |
||||||
|
vm.mescroll.getStep(curY, y, step => { // 此写法可支持配置t |
||||||
|
vm.scrollTop = step |
||||||
|
}, t) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
// 具体的界面如果不配置up.toTop.safearea,则取本vue的safearea值 |
||||||
|
if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else { |
||||||
|
vm.mescroll.optUp.toTop.safearea = vm.safearea; |
||||||
|
} |
||||||
|
}, |
||||||
|
mounted() { |
||||||
|
// 设置容器的高度 |
||||||
|
this.setClientHeight() |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style> |
||||||
|
@import "../../mescroll-uni/mescroll-uni.css"; |
||||||
|
@import "../../mescroll-uni/components/mescroll-down.css"; |
||||||
|
@import "../../mescroll-uni/components/mescroll-up.css"; |
||||||
|
@import "./components/mescroll-down.css"; |
||||||
|
@import "./components/mescroll-up.css"; |
||||||
|
</style> |
@ -0,0 +1,55 @@ |
|||||||
|
/* 下拉刷新区域 */ |
||||||
|
.mescroll-downwarp { |
||||||
|
position: absolute; |
||||||
|
top: -100%; |
||||||
|
left: 0; |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
text-align: center; |
||||||
|
} |
||||||
|
|
||||||
|
/* 下拉刷新--内容区,定位于区域底部 */ |
||||||
|
.mescroll-downwarp .downwarp-content { |
||||||
|
position: absolute; |
||||||
|
left: 0; |
||||||
|
bottom: 0; |
||||||
|
width: 100%; |
||||||
|
min-height: 60rpx; |
||||||
|
padding: 20rpx 0; |
||||||
|
text-align: center; |
||||||
|
} |
||||||
|
|
||||||
|
/* 下拉刷新--提示文本 */ |
||||||
|
.mescroll-downwarp .downwarp-tip { |
||||||
|
display: inline-block; |
||||||
|
font-size: 28rpx; |
||||||
|
vertical-align: middle; |
||||||
|
margin-left: 16rpx; |
||||||
|
/* color: gray; 已在style设置color,此处删去*/ |
||||||
|
} |
||||||
|
|
||||||
|
/* 下拉刷新--旋转进度条 */ |
||||||
|
.mescroll-downwarp .downwarp-progress { |
||||||
|
display: inline-block; |
||||||
|
width: 32rpx; |
||||||
|
height: 32rpx; |
||||||
|
border-radius: 50%; |
||||||
|
border: 2rpx solid gray; |
||||||
|
border-bottom-color: transparent !important; /*已在style设置border-color,此处需加 !important*/ |
||||||
|
vertical-align: middle; |
||||||
|
} |
||||||
|
|
||||||
|
/* 旋转动画 */ |
||||||
|
.mescroll-downwarp .mescroll-rotate { |
||||||
|
animation: mescrollDownRotate 0.6s linear infinite; |
||||||
|
} |
||||||
|
|
||||||
|
@keyframes mescrollDownRotate { |
||||||
|
0% { |
||||||
|
transform: rotate(0deg); |
||||||
|
} |
||||||
|
|
||||||
|
100% { |
||||||
|
transform: rotate(360deg); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,47 @@ |
|||||||
|
<!-- 下拉刷新区域 --> |
||||||
|
<template> |
||||||
|
<view v-if="mOption.use" class="mescroll-downwarp" :style="{'background-color':mOption.bgColor,'color':mOption.textColor}"> |
||||||
|
<view class="downwarp-content"> |
||||||
|
<view class="downwarp-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'border-color':mOption.textColor, 'transform':downRotate}"></view> |
||||||
|
<view class="downwarp-tip">{{downText}}</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
props: { |
||||||
|
option: Object , // down的配置项 |
||||||
|
type: Number, // 下拉状态(inOffset:1, outOffset:2, showLoading:3, endDownScroll:4) |
||||||
|
rate: Number // 下拉比率 (inOffset: rate<1; outOffset: rate>=1) |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
// 支付宝小程序需写成计算属性,prop定义default仍报错 |
||||||
|
mOption(){ |
||||||
|
return this.option || {} |
||||||
|
}, |
||||||
|
// 是否在加载中 |
||||||
|
isDownLoading(){ |
||||||
|
return this.type === 3 |
||||||
|
}, |
||||||
|
// 旋转的角度 |
||||||
|
downRotate(){ |
||||||
|
return 'rotate(' + 360 * this.rate + 'deg)' |
||||||
|
}, |
||||||
|
// 文本提示 |
||||||
|
downText(){ |
||||||
|
switch (this.type){ |
||||||
|
case 1: return this.mOption.textInOffset; |
||||||
|
case 2: return this.mOption.textOutOffset; |
||||||
|
case 3: return this.mOption.textLoading; |
||||||
|
case 4: return this.mOption.textLoading; |
||||||
|
default: return this.mOption.textInOffset; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
</script> |
||||||
|
|
||||||
|
<style> |
||||||
|
@import "./mescroll-down.css"; |
||||||
|
</style> |
@ -0,0 +1,90 @@ |
|||||||
|
<!--空布局 |
||||||
|
|
||||||
|
可作为独立的组件, 不使用mescroll的页面也能单独引入, 以便APP全局统一管理: |
||||||
|
import MescrollEmpty from '@/components/mescroll-uni/components/mescroll-empty.vue'; |
||||||
|
<mescroll-empty v-if="isShowEmpty" :option="optEmpty" @emptyclick="emptyClick"></mescroll-empty> |
||||||
|
|
||||||
|
--> |
||||||
|
<template> |
||||||
|
<view class="mescroll-empty" :class="{ 'empty-fixed': option.fixed }" :style="{ 'z-index': option.zIndex, top: option.top }"> |
||||||
|
<view> <image v-if="icon" class="empty-icon" :src="icon" mode="widthFix" /> </view> |
||||||
|
<view v-if="tip" class="empty-tip">{{ tip }}</view> |
||||||
|
<view v-if="option.btnText" class="empty-btn" @click="emptyClick">{{ option.btnText }}</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
// 引入全局配置 |
||||||
|
import GlobalOption from './../mescroll-uni-option.js'; |
||||||
|
export default { |
||||||
|
props: { |
||||||
|
// empty的配置项: 默认为GlobalOption.up.empty |
||||||
|
option: { |
||||||
|
type: Object, |
||||||
|
default() { |
||||||
|
return {}; |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
// 使用computed获取配置,用于支持option的动态配置 |
||||||
|
computed: { |
||||||
|
// 图标 |
||||||
|
icon() { |
||||||
|
return this.option.icon == null ? GlobalOption.up.empty.icon : this.option.icon; // 此处不使用短路求值, 用于支持传空串不显示图标 |
||||||
|
}, |
||||||
|
// 文本提示 |
||||||
|
tip() { |
||||||
|
return this.option.tip == null ? GlobalOption.up.empty.tip : this.option.tip; // 此处不使用短路求值, 用于支持传空串不显示文本提示 |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
// 点击按钮 |
||||||
|
emptyClick() { |
||||||
|
this.$emit('emptyclick'); |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
</script> |
||||||
|
|
||||||
|
<style> |
||||||
|
/* 无任何数据的空布局 */ |
||||||
|
.mescroll-empty { |
||||||
|
box-sizing: border-box; |
||||||
|
width: 100%; |
||||||
|
padding: 100rpx 50rpx; |
||||||
|
text-align: center; |
||||||
|
} |
||||||
|
|
||||||
|
.mescroll-empty.empty-fixed { |
||||||
|
z-index: 99; |
||||||
|
position: absolute; /*transform会使fixed失效,最终会降级为absolute */ |
||||||
|
top: 100rpx; |
||||||
|
left: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.mescroll-empty .empty-icon { |
||||||
|
width: 280rpx; |
||||||
|
height: 280rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.mescroll-empty .empty-tip { |
||||||
|
margin-top: 20rpx; |
||||||
|
font-size: 24rpx; |
||||||
|
color: gray; |
||||||
|
} |
||||||
|
|
||||||
|
.mescroll-empty .empty-btn { |
||||||
|
display: inline-block; |
||||||
|
margin-top: 40rpx; |
||||||
|
min-width: 200rpx; |
||||||
|
padding: 18rpx; |
||||||
|
font-size: 28rpx; |
||||||
|
border: 1rpx solid #e04b28; |
||||||
|
border-radius: 60rpx; |
||||||
|
color: #e04b28; |
||||||
|
} |
||||||
|
|
||||||
|
.mescroll-empty .empty-btn:active { |
||||||
|
opacity: 0.75; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,83 @@ |
|||||||
|
<!-- 回到顶部的按钮 --> |
||||||
|
<template> |
||||||
|
<image |
||||||
|
v-if="mOption.src" |
||||||
|
class="mescroll-totop" |
||||||
|
:class="[value ? 'mescroll-totop-in' : 'mescroll-totop-out', {'mescroll-totop-safearea': mOption.safearea}]" |
||||||
|
:style="{'z-index':mOption.zIndex, 'left': left, 'right': right, 'bottom':addUnit(mOption.bottom), 'width':addUnit(mOption.width), 'border-radius':addUnit(mOption.radius)}" |
||||||
|
:src="mOption.src" |
||||||
|
mode="widthFix" |
||||||
|
@click="toTopClick" |
||||||
|
/> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
props: { |
||||||
|
// up.toTop的配置项 |
||||||
|
option: Object, |
||||||
|
// 是否显示 |
||||||
|
value: false |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
// 支付宝小程序需写成计算属性,prop定义default仍报错 |
||||||
|
mOption(){ |
||||||
|
return this.option || {} |
||||||
|
}, |
||||||
|
// 优先显示左边 |
||||||
|
left(){ |
||||||
|
return this.mOption.left ? this.addUnit(this.mOption.left) : 'auto'; |
||||||
|
}, |
||||||
|
// 右边距离 (优先显示左边) |
||||||
|
right() { |
||||||
|
return this.mOption.left ? 'auto' : this.addUnit(this.mOption.right); |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
addUnit(num){ |
||||||
|
if(!num) return 0; |
||||||
|
if(typeof num === 'number') return num + 'rpx'; |
||||||
|
return num |
||||||
|
}, |
||||||
|
toTopClick() { |
||||||
|
this.$emit('input', false); // 使v-model生效 |
||||||
|
this.$emit('click'); // 派发点击事件 |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
</script> |
||||||
|
|
||||||
|
<style> |
||||||
|
/* 回到顶部的按钮 */ |
||||||
|
.mescroll-totop { |
||||||
|
z-index: 9990; |
||||||
|
position: fixed !important; /* 加上important避免编译到H5,在多mescroll中定位失效 */ |
||||||
|
right: 20rpx; |
||||||
|
bottom: 120rpx; |
||||||
|
width: 72rpx; |
||||||
|
height: auto; |
||||||
|
border-radius: 50%; |
||||||
|
opacity: 0; |
||||||
|
transition: opacity 0.5s; /* 过渡 */ |
||||||
|
margin-bottom: var(--window-bottom); /* css变量 */ |
||||||
|
} |
||||||
|
|
||||||
|
/* 适配 iPhoneX */ |
||||||
|
@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) { |
||||||
|
.mescroll-totop-safearea { |
||||||
|
margin-bottom: calc(var(--window-bottom) + constant(safe-area-inset-bottom)); /* window-bottom + 适配 iPhoneX */ |
||||||
|
margin-bottom: calc(var(--window-bottom) + env(safe-area-inset-bottom)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/* 显示 -- 淡入 */ |
||||||
|
.mescroll-totop-in { |
||||||
|
opacity: 1; |
||||||
|
} |
||||||
|
|
||||||
|
/* 隐藏 -- 淡出且不接收事件*/ |
||||||
|
.mescroll-totop-out { |
||||||
|
opacity: 0; |
||||||
|
pointer-events: none; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,47 @@ |
|||||||
|
/* 上拉加载区域 */ |
||||||
|
.mescroll-upwarp { |
||||||
|
box-sizing: border-box; |
||||||
|
min-height: 110rpx; |
||||||
|
padding: 30rpx 0; |
||||||
|
text-align: center; |
||||||
|
clear: both; |
||||||
|
} |
||||||
|
|
||||||
|
/*提示文本 */ |
||||||
|
.mescroll-upwarp .upwarp-tip, |
||||||
|
.mescroll-upwarp .upwarp-nodata { |
||||||
|
display: inline-block; |
||||||
|
font-size: 28rpx; |
||||||
|
vertical-align: middle; |
||||||
|
/* color: gray; 已在style设置color,此处删去*/ |
||||||
|
} |
||||||
|
|
||||||
|
.mescroll-upwarp .upwarp-tip { |
||||||
|
margin-left: 16rpx; |
||||||
|
} |
||||||
|
|
||||||
|
/*旋转进度条 */ |
||||||
|
.mescroll-upwarp .upwarp-progress { |
||||||
|
display: inline-block; |
||||||
|
width: 32rpx; |
||||||
|
height: 32rpx; |
||||||
|
border-radius: 50%; |
||||||
|
border: 2rpx solid gray; |
||||||
|
border-bottom-color: transparent !important; /*已在style设置border-color,此处需加 !important*/ |
||||||
|
vertical-align: middle; |
||||||
|
} |
||||||
|
|
||||||
|
/* 旋转动画 */ |
||||||
|
.mescroll-upwarp .mescroll-rotate { |
||||||
|
animation: mescrollUpRotate 0.6s linear infinite; |
||||||
|
} |
||||||
|
|
||||||
|
@keyframes mescrollUpRotate { |
||||||
|
0% { |
||||||
|
transform: rotate(0deg); |
||||||
|
} |
||||||
|
|
||||||
|
100% { |
||||||
|
transform: rotate(360deg); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,39 @@ |
|||||||
|
<!-- 上拉加载区域 --> |
||||||
|
<template> |
||||||
|
<view class="mescroll-upwarp" :style="{'background-color':mOption.bgColor,'color':mOption.textColor}"> |
||||||
|
<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) --> |
||||||
|
<view v-show="isUpLoading"> |
||||||
|
<view class="upwarp-progress mescroll-rotate" :style="{'border-color':mOption.textColor}"></view> |
||||||
|
<view class="upwarp-tip">{{ mOption.textLoading }}</view> |
||||||
|
</view> |
||||||
|
<!-- 无数据 --> |
||||||
|
<view v-if="isUpNoMore" class="upwarp-nodata">{{ mOption.textNoMore }}</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
props: { |
||||||
|
option: Object, // up的配置项 |
||||||
|
type: Number // 上拉加载的状态:0(loading前),1(loading中),2(没有更多了) |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
// 支付宝小程序需写成计算属性,prop定义default仍报错 |
||||||
|
mOption() { |
||||||
|
return this.option || {}; |
||||||
|
}, |
||||||
|
// 加载中 |
||||||
|
isUpLoading() { |
||||||
|
return this.type === 1; |
||||||
|
}, |
||||||
|
// 没有更多了 |
||||||
|
isUpNoMore() { |
||||||
|
return this.type === 2; |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
</script> |
||||||
|
|
||||||
|
<style> |
||||||
|
@import './mescroll-up.css'; |
||||||
|
</style> |
@ -0,0 +1,19 @@ |
|||||||
|
.mescroll-body { |
||||||
|
position: relative; /* 下拉刷新区域相对自身定位 */ |
||||||
|
height: auto; /* 不可固定高度,否则overflow:hidden导致无法滑动; 同时使设置的最小高生效,实现列表不满屏仍可下拉*/ |
||||||
|
overflow: hidden; /* 当有元素写在mescroll-body标签前面时,可遮住下拉刷新区域 */ |
||||||
|
box-sizing: border-box; /* 避免设置padding出现双滚动条的问题 */ |
||||||
|
} |
||||||
|
|
||||||
|
/* 使sticky生效: 父元素不能overflow:hidden或者overflow:auto属性 */ |
||||||
|
.mescroll-body.mescorll-sticky{ |
||||||
|
overflow: unset !important |
||||||
|
} |
||||||
|
|
||||||
|
/* 适配 iPhoneX */ |
||||||
|
@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) { |
||||||
|
.mescroll-safearea { |
||||||
|
padding-bottom: constant(safe-area-inset-bottom); |
||||||
|
padding-bottom: env(safe-area-inset-bottom); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,348 @@ |
|||||||
|
<template> |
||||||
|
<view |
||||||
|
class="mescroll-body mescroll-render-touch" |
||||||
|
:class="{'mescorll-sticky': sticky}" |
||||||
|
:style="{'minHeight':minHeight, 'padding-top': padTop, 'padding-bottom': padBottom}" |
||||||
|
@touchstart="wxsBiz.touchstartEvent" |
||||||
|
@touchmove="wxsBiz.touchmoveEvent" |
||||||
|
@touchend="wxsBiz.touchendEvent" |
||||||
|
@touchcancel="wxsBiz.touchendEvent" |
||||||
|
:change:prop="wxsBiz.propObserver" |
||||||
|
:prop="wxsProp" |
||||||
|
> |
||||||
|
<!-- 状态栏 --> |
||||||
|
<view v-if="topbar&&statusBarHeight" class="mescroll-topbar" :style="{height: statusBarHeight+'px', background: topbar}"></view> |
||||||
|
|
||||||
|
<view class="mescroll-body-content mescroll-wxs-content" :style="{ transform: translateY, transition: transition }" :change:prop="wxsBiz.callObserver" :prop="callProp"> |
||||||
|
<!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)--> |
||||||
|
<!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType" :rate="downRate"></mescroll-down> --> |
||||||
|
<view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}"> |
||||||
|
<view class="downwarp-content"> |
||||||
|
<view class="downwarp-progress mescroll-wxs-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'border-color':mescroll.optDown.textColor, 'transform': downRotate}"></view> |
||||||
|
<view class="downwarp-tip">{{downText}}</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<!-- 列表内容 --> |
||||||
|
<slot></slot> |
||||||
|
|
||||||
|
<!-- 空布局 --> |
||||||
|
<mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty> |
||||||
|
|
||||||
|
<!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)--> |
||||||
|
<!-- <mescroll-up v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> --> |
||||||
|
<view v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" class="mescroll-upwarp" :style="{'background':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}"> |
||||||
|
<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) --> |
||||||
|
<view v-show="upLoadType===1"> |
||||||
|
<view class="upwarp-progress mescroll-rotate" :style="{'border-color':mescroll.optUp.textColor}"></view> |
||||||
|
<view class="upwarp-tip">{{ mescroll.optUp.textLoading }}</view> |
||||||
|
</view> |
||||||
|
<!-- 无数据 --> |
||||||
|
<view v-if="upLoadType===2" class="upwarp-nodata">{{ mescroll.optUp.textNoMore }}</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<!-- 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效) --> |
||||||
|
<!-- #ifdef H5 --> |
||||||
|
<view v-if="bottombar && windowBottom>0" class="mescroll-bottombar" :style="{height: windowBottom+'px'}"></view> |
||||||
|
<!-- #endif --> |
||||||
|
|
||||||
|
<!-- 适配iPhoneX --> |
||||||
|
<view v-if="safearea" class="mescroll-safearea"></view> |
||||||
|
|
||||||
|
<!-- 回到顶部按钮 (fixed元素需写在transform外面,防止降级为absolute)--> |
||||||
|
<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top> |
||||||
|
|
||||||
|
<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 --> |
||||||
|
<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 --> |
||||||
|
<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view> |
||||||
|
<!-- #endif --> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<!-- 微信小程序, QQ小程序, app, h5使用wxs --> |
||||||
|
<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 --> |
||||||
|
<script src="./wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script> |
||||||
|
<!-- #endif --> |
||||||
|
|
||||||
|
<!-- app, h5使用renderjs --> |
||||||
|
<!-- #ifdef APP-PLUS || H5 --> |
||||||
|
<script module="renderBiz" lang="renderjs"> |
||||||
|
import renderBiz from './wxs/renderjs.js'; |
||||||
|
export default { |
||||||
|
mixins: [renderBiz] |
||||||
|
} |
||||||
|
</script> |
||||||
|
<!-- #endif --> |
||||||
|
|
||||||
|
<script> |
||||||
|
// 引入mescroll-uni.js,处理核心逻辑 |
||||||
|
import MeScroll from './mescroll-uni.js'; |
||||||
|
// 引入全局配置 |
||||||
|
import GlobalOption from './mescroll-uni-option.js'; |
||||||
|
// 引入空布局组件 |
||||||
|
import MescrollEmpty from './components/mescroll-empty.vue'; |
||||||
|
// 引入回到顶部组件 |
||||||
|
import MescrollTop from './components/mescroll-top.vue'; |
||||||
|
// 引入兼容wxs(含renderjs)写法的mixins |
||||||
|
import WxsMixin from './wxs/mixins.js'; |
||||||
|
|
||||||
|
export default { |
||||||
|
mixins: [WxsMixin], |
||||||
|
components: { |
||||||
|
MescrollEmpty, |
||||||
|
MescrollTop |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
mescroll: {optDown:{},optUp:{}}, // mescroll实例 |
||||||
|
downHight: 0, //下拉刷新: 容器高度 |
||||||
|
downRate: 0, // 下拉比率(inOffset: rate<1; outOffset: rate>=1) |
||||||
|
downLoadType: 0, // 下拉刷新状态: 0(loading前), 1(inOffset), 2(outOffset), 3(showLoading), 4(endDownScroll) |
||||||
|
upLoadType: 0, // 上拉加载状态:0(loading前),1(loading中),2(没有更多了,显示END文本提示),3(没有更多了,不显示END文本提示) |
||||||
|
isShowEmpty: false, // 是否显示空布局 |
||||||
|
isShowToTop: false, // 是否显示回到顶部按钮 |
||||||
|
windowHeight: 0, // 可使用窗口的高度 |
||||||
|
windowBottom: 0, // 可使用窗口的底部位置 |
||||||
|
statusBarHeight: 0 // 状态栏高度 |
||||||
|
}; |
||||||
|
}, |
||||||
|
props: { |
||||||
|
down: Object, // 下拉刷新的参数配置 |
||||||
|
up: Object, // 上拉加载的参数配置 |
||||||
|
top: [String, Number], // 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight) |
||||||
|
topbar: [Boolean, String], // top的偏移量是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变) |
||||||
|
bottom: [String, Number], // 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight) |
||||||
|
safearea: Boolean, // bottom的偏移量是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用) |
||||||
|
height: [String, Number], // 指定mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉 |
||||||
|
bottombar:{ // 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效) |
||||||
|
type: Boolean, |
||||||
|
default: true |
||||||
|
}, |
||||||
|
sticky: Boolean // 是否支持sticky,默认false; 当值配置true时,需避免在mescroll-body标签前面加非定位的元素,否则下拉区域无法会隐藏 |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
// mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉 |
||||||
|
minHeight(){ |
||||||
|
return this.toPx(this.height || '100%') + 'px' |
||||||
|
}, |
||||||
|
// 下拉布局往下偏移的距离 (px) |
||||||
|
numTop() { |
||||||
|
return this.toPx(this.top) |
||||||
|
}, |
||||||
|
padTop() { |
||||||
|
return this.numTop + 'px'; |
||||||
|
}, |
||||||
|
// 上拉布局往上偏移 (px) |
||||||
|
numBottom() { |
||||||
|
return this.toPx(this.bottom); |
||||||
|
}, |
||||||
|
padBottom() { |
||||||
|
return this.numBottom + 'px'; |
||||||
|
}, |
||||||
|
// 是否为重置下拉的状态 |
||||||
|
isDownReset() { |
||||||
|
return this.downLoadType === 3 || this.downLoadType === 4; |
||||||
|
}, |
||||||
|
// 过渡 |
||||||
|
transition() { |
||||||
|
return this.isDownReset ? 'transform 300ms' : ''; |
||||||
|
}, |
||||||
|
translateY() { |
||||||
|
return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' : ''; // transform会使fixed失效,需注意把fixed元素写在mescroll之外 |
||||||
|
}, |
||||||
|
// 是否在加载中 |
||||||
|
isDownLoading(){ |
||||||
|
return this.downLoadType === 3 |
||||||
|
}, |
||||||
|
// 旋转的角度 |
||||||
|
downRotate(){ |
||||||
|
return 'rotate(' + 360 * this.downRate + 'deg)' |
||||||
|
}, |
||||||
|
// 文本提示 |
||||||
|
downText(){ |
||||||
|
if(!this.mescroll) return ""; // 避免头条小程序初始化时报错 |
||||||
|
switch (this.downLoadType){ |
||||||
|
case 1: return this.mescroll.optDown.textInOffset; |
||||||
|
case 2: return this.mescroll.optDown.textOutOffset; |
||||||
|
case 3: return this.mescroll.optDown.textLoading; |
||||||
|
case 4: return this.mescroll.isDownEndSuccess ? this.mescroll.optDown.textSuccess : this.mescroll.isDownEndSuccess==false ? this.mescroll.optDown.textErr : this.mescroll.optDown.textInOffset; |
||||||
|
default: return this.mescroll.optDown.textInOffset; |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
//number,rpx,upx,px,% --> px的数值 |
||||||
|
toPx(num) { |
||||||
|
if (typeof num === 'string') { |
||||||
|
if (num.indexOf('px') !== -1) { |
||||||
|
if (num.indexOf('rpx') !== -1) { |
||||||
|
// "10rpx" |
||||||
|
num = num.replace('rpx', ''); |
||||||
|
} else if (num.indexOf('upx') !== -1) { |
||||||
|
// "10upx" |
||||||
|
num = num.replace('upx', ''); |
||||||
|
} else { |
||||||
|
// "10px" |
||||||
|
return Number(num.replace('px', '')); |
||||||
|
} |
||||||
|
} else if (num.indexOf('%') !== -1) { |
||||||
|
// 传百分比,则相对于windowHeight,传"10%"则等于windowHeight的10% |
||||||
|
let rate = Number(num.replace('%', '')) / 100; |
||||||
|
return this.windowHeight * rate; |
||||||
|
} |
||||||
|
} |
||||||
|
return num ? uni.upx2px(Number(num)) : 0; |
||||||
|
}, |
||||||
|
// 点击空布局的按钮回调 |
||||||
|
emptyClick() { |
||||||
|
this.$emit('emptyclick', this.mescroll); |
||||||
|
}, |
||||||
|
// 点击回到顶部的按钮回调 |
||||||
|
toTopClick() { |
||||||
|
this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部 |
||||||
|
this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调 |
||||||
|
} |
||||||
|
}, |
||||||
|
// 使用created初始化mescroll对象; 如果用mounted部分css样式编译到H5会失效 |
||||||
|
created() { |
||||||
|
let vm = this; |
||||||
|
|
||||||
|
let diyOption = { |
||||||
|
// 下拉刷新的配置 |
||||||
|
down: { |
||||||
|
inOffset() { |
||||||
|
vm.downLoadType = 1; // 下拉的距离进入offset范围内那一刻的回调 (自定义mescroll组件时,此行不可删) |
||||||
|
}, |
||||||
|
outOffset() { |
||||||
|
vm.downLoadType = 2; // 下拉的距离大于offset那一刻的回调 (自定义mescroll组件时,此行不可删) |
||||||
|
}, |
||||||
|
onMoving(mescroll, rate, downHight) { |
||||||
|
// 下拉过程中的回调,滑动过程一直在执行; |
||||||
|
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删) |
||||||
|
vm.downRate = rate; //下拉比率 (inOffset: rate<1; outOffset: rate>=1) |
||||||
|
}, |
||||||
|
showLoading(mescroll, downHight) { |
||||||
|
vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删) |
||||||
|
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删) |
||||||
|
}, |
||||||
|
beforeEndDownScroll(mescroll){ |
||||||
|
vm.downLoadType = 4; |
||||||
|
return mescroll.optDown.beforeEndDelay // 延时结束的时长 |
||||||
|
}, |
||||||
|
endDownScroll() { |
||||||
|
vm.downLoadType = 4; // 结束下拉 (自定义mescroll组件时,此行不可删) |
||||||
|
vm.downHight = 0; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删) |
||||||
|
if(vm.downResetTimer) {clearTimeout(vm.downResetTimer); vm.downResetTimer = null} // 移除重置倒计时 |
||||||
|
vm.downResetTimer = setTimeout(()=>{ // 过渡动画执行完毕后,需重置为0的状态,避免下次inOffset不及时显示textInOffset |
||||||
|
if(vm.downLoadType === 4) vm.downLoadType = 0 |
||||||
|
},300) |
||||||
|
}, |
||||||
|
// 派发下拉刷新的回调 |
||||||
|
callback: function(mescroll) { |
||||||
|
vm.$emit('down', mescroll); |
||||||
|
} |
||||||
|
}, |
||||||
|
// 上拉加载的配置 |
||||||
|
up: { |
||||||
|
// 显示加载中的回调 |
||||||
|
showLoading() { |
||||||
|
vm.upLoadType = 1; |
||||||
|
}, |
||||||
|
// 显示无更多数据的回调 |
||||||
|
showNoMore() { |
||||||
|
vm.upLoadType = 2; |
||||||
|
}, |
||||||
|
// 隐藏上拉加载的回调 |
||||||
|
hideUpScroll(mescroll) { |
||||||
|
vm.upLoadType = mescroll.optUp.hasNext ? 0 : 3; |
||||||
|
}, |
||||||
|
// 空布局 |
||||||
|
empty: { |
||||||
|
onShow(isShow) { |
||||||
|
// 显示隐藏的回调 |
||||||
|
vm.isShowEmpty = isShow; |
||||||
|
} |
||||||
|
}, |
||||||
|
// 回到顶部 |
||||||
|
toTop: { |
||||||
|
onShow(isShow) { |
||||||
|
// 显示隐藏的回调 |
||||||
|
vm.isShowToTop = isShow; |
||||||
|
} |
||||||
|
}, |
||||||
|
// 派发上拉加载的回调 |
||||||
|
callback: function(mescroll) { |
||||||
|
vm.$emit('up', mescroll); |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
MeScroll.extend(diyOption, GlobalOption); // 混入全局的配置 |
||||||
|
let myOption = JSON.parse(JSON.stringify({down: vm.down,up: vm.up})); // 深拷贝,避免对props的影响 |
||||||
|
MeScroll.extend(myOption, diyOption); // 混入具体界面的配置 |
||||||
|
|
||||||
|
// 初始化MeScroll对象 |
||||||
|
vm.mescroll = new MeScroll(myOption, true); // 传入true,标记body为滚动区域 |
||||||
|
// init回调mescroll对象 |
||||||
|
vm.$emit('init', vm.mescroll); |
||||||
|
|
||||||
|
// 设置高度 |
||||||
|
const sys = uni.getSystemInfoSync(); |
||||||
|
if (sys.windowHeight) vm.windowHeight = sys.windowHeight; |
||||||
|
if (sys.windowBottom) vm.windowBottom = sys.windowBottom; |
||||||
|
if (sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight; |
||||||
|
// 使down的bottomOffset生效 |
||||||
|
vm.mescroll.setBodyHeight(sys.windowHeight); |
||||||
|
|
||||||
|
// 因为使用的是page的scroll,这里需自定义scrollTo |
||||||
|
vm.mescroll.resetScrollTo((y, t) => { |
||||||
|
if(typeof y === 'string'){ |
||||||
|
// 滚动到指定view (y为css选择器) |
||||||
|
setTimeout(()=>{ // 延时确保view已渲染; 不使用$nextTick |
||||||
|
let selector; |
||||||
|
if(y.indexOf('#')==-1 && y.indexOf('.')==-1){ |
||||||
|
selector = '#'+y // 不带#和. 则默认为id选择器 |
||||||
|
}else{ |
||||||
|
selector = y |
||||||
|
// #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-DINGTALK |
||||||
|
if(y.indexOf('>>>')!=-1){ // 不支持跨自定义组件的后代选择器 (转为普通的选择器即可跨组件查询) |
||||||
|
selector = y.split('>>>')[1].trim() |
||||||
|
} |
||||||
|
// #endif |
||||||
|
} |
||||||
|
uni.createSelectorQuery().select(selector).boundingClientRect(function(rect){ |
||||||
|
if (rect) { |
||||||
|
let top = rect.top |
||||||
|
top += vm.mescroll.getScrollTop() |
||||||
|
uni.pageScrollTo({ |
||||||
|
scrollTop: top, |
||||||
|
duration: t |
||||||
|
}) |
||||||
|
} else{ |
||||||
|
console.error(selector + ' does not exist'); |
||||||
|
} |
||||||
|
}).exec() |
||||||
|
},30) |
||||||
|
} else{ |
||||||
|
// 滚动到指定位置 (y必须为数字) |
||||||
|
uni.pageScrollTo({ |
||||||
|
scrollTop: y, |
||||||
|
duration: t |
||||||
|
}) |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
// 具体的界面如果不配置up.toTop.safearea,则取本vue的safearea值 |
||||||
|
if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else { |
||||||
|
vm.mescroll.optUp.toTop.safearea = vm.safearea; |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
</script> |
||||||
|
|
||||||
|
<style> |
||||||
|
@import "./mescroll-body.css"; |
||||||
|
@import "./components/mescroll-down.css"; |
||||||
|
@import './components/mescroll-up.css'; |
||||||
|
</style> |
@ -0,0 +1,65 @@ |
|||||||
|
// mescroll-body 和 mescroll-uni 通用
|
||||||
|
|
||||||
|
// import MescrollUni from "./mescroll-uni.vue";
|
||||||
|
// import MescrollBody from "./mescroll-body.vue";
|
||||||
|
|
||||||
|
const MescrollMixin = { |
||||||
|
// components: { // 非H5端无法通过mixin注册组件, 只能在main.js中注册全局组件或具体界面中注册
|
||||||
|
// MescrollUni,
|
||||||
|
// MescrollBody
|
||||||
|
// },
|
||||||
|
data() { |
||||||
|
return { |
||||||
|
mescroll: null //mescroll实例对象
|
||||||
|
} |
||||||
|
}, |
||||||
|
// 注册系统自带的下拉刷新 (配置down.native为true时生效, 还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
|
||||||
|
onPullDownRefresh(){ |
||||||
|
this.mescroll && this.mescroll.onPullDownRefresh(); |
||||||
|
}, |
||||||
|
// 注册列表滚动事件,用于判定在顶部可下拉刷新,在指定位置可显示隐藏回到顶部按钮 (此方法为页面生命周期,无法在子组件中触发, 仅在mescroll-body生效)
|
||||||
|
onPageScroll(e) { |
||||||
|
this.mescroll && this.mescroll.onPageScroll(e); |
||||||
|
}, |
||||||
|
// 注册滚动到底部的事件,用于上拉加载 (此方法为页面生命周期,无法在子组件中触发, 仅在mescroll-body生效)
|
||||||
|
onReachBottom() { |
||||||
|
this.mescroll && this.mescroll.onReachBottom(); |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
// mescroll组件初始化的回调,可获取到mescroll对象
|
||||||
|
mescrollInit(mescroll) { |
||||||
|
this.mescroll = mescroll; |
||||||
|
this.mescrollInitByRef(); // 兼容字节跳动小程序
|
||||||
|
}, |
||||||
|
// 以ref的方式初始化mescroll对象 (兼容字节跳动小程序)
|
||||||
|
mescrollInitByRef() { |
||||||
|
if(!this.mescroll || !this.mescroll.resetUpScroll){ |
||||||
|
let mescrollRef = this.$refs.mescrollRef; |
||||||
|
if(mescrollRef) this.mescroll = mescrollRef.mescroll |
||||||
|
} |
||||||
|
}, |
||||||
|
// 下拉刷新的回调 (mixin默认resetUpScroll)
|
||||||
|
downCallback() { |
||||||
|
if(this.mescroll.optUp.use){ |
||||||
|
this.mescroll.resetUpScroll() |
||||||
|
}else{ |
||||||
|
setTimeout(()=>{ |
||||||
|
this.mescroll.endSuccess(); |
||||||
|
}, 500) |
||||||
|
} |
||||||
|
}, |
||||||
|
// 上拉加载的回调
|
||||||
|
upCallback() { |
||||||
|
// mixin默认延时500自动结束加载
|
||||||
|
setTimeout(()=>{ |
||||||
|
this.mescroll.endErr(); |
||||||
|
}, 500) |
||||||
|
} |
||||||
|
}, |
||||||
|
mounted() { |
||||||
|
this.mescrollInitByRef(); // 兼容字节跳动小程序, 避免未设置@init或@init此时未能取到ref的情况
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
export default MescrollMixin; |
@ -0,0 +1,36 @@ |
|||||||
|
// 全局配置
|
||||||
|
// mescroll-body 和 mescroll-uni 通用
|
||||||
|
const GlobalOption = { |
||||||
|
down: { |
||||||
|
// 其他down的配置参数也可以写,这里只展示了常用的配置:
|
||||||
|
textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本
|
||||||
|
textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本
|
||||||
|
textLoading: '加载中 ...', // 加载中的提示文本
|
||||||
|
textSuccess: '加载成功', // 加载成功的文本
|
||||||
|
textErr: '加载失败', // 加载失败的文本
|
||||||
|
beforeEndDelay: 100, // 延时结束的时长 (显示加载成功/失败的时长)
|
||||||
|
offset: 80, // 在列表顶部,下拉大于80px,松手即可触发下拉刷新的回调
|
||||||
|
native: false // 是否使用系统自带的下拉刷新; 默认false; 仅在mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
|
||||||
|
}, |
||||||
|
up: { |
||||||
|
// 其他up的配置参数也可以写,这里只展示了常用的配置:
|
||||||
|
textLoading: '加载中 ...', // 加载中的提示文本
|
||||||
|
textNoMore: '-- END --', // 没有更多数据的提示文本
|
||||||
|
offset: 150, // 距底部多远时,触发upCallback,仅mescroll-uni生效 ( mescroll-body配置的是pages.json的 onReachBottomDistance )
|
||||||
|
toTop: { |
||||||
|
// 回到顶部按钮,需配置src才显示
|
||||||
|
src: "https://www.mescroll.com/img/mescroll-totop.png", // 图片路径 (建议放入static目录, 如 /static/img/mescroll-totop.png )
|
||||||
|
offset: 1000, // 列表滚动多少距离才显示回到顶部按钮,默认1000px
|
||||||
|
right: 20, // 到右边的距离, 默认20 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
|
||||||
|
bottom: 120, // 到底部的距离, 默认120 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
|
||||||
|
width: 72 // 回到顶部图标的宽度, 默认72 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
|
||||||
|
}, |
||||||
|
empty: { |
||||||
|
use: true, // 是否显示空布局
|
||||||
|
icon: "https://www.mescroll.com/img/mescroll-empty.png", // 图标路径 (建议放入static目录, 如 /static/img/mescroll-empty.png )
|
||||||
|
tip: '~ 空空如也 ~' // 提示
|
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export default GlobalOption |
@ -0,0 +1,36 @@ |
|||||||
|
.mescroll-uni-warp{ |
||||||
|
height: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.mescroll-uni-content{ |
||||||
|
height: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.mescroll-uni { |
||||||
|
position: relative; |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
min-height: 200rpx; |
||||||
|
overflow-y: auto; |
||||||
|
box-sizing: border-box; /* 避免设置padding出现双滚动条的问题 */ |
||||||
|
} |
||||||
|
|
||||||
|
/* 定位的方式固定高度 */ |
||||||
|
.mescroll-uni-fixed{ |
||||||
|
z-index: 1; |
||||||
|
position: fixed; |
||||||
|
top: 0; |
||||||
|
left: 0; |
||||||
|
right: 0; |
||||||
|
bottom: 0; |
||||||
|
width: auto; /* 使right生效 */ |
||||||
|
height: auto; /* 使bottom生效 */ |
||||||
|
} |
||||||
|
|
||||||
|
/* 适配 iPhoneX */ |
||||||
|
@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) { |
||||||
|
.mescroll-safearea { |
||||||
|
padding-bottom: constant(safe-area-inset-bottom); |
||||||
|
padding-bottom: env(safe-area-inset-bottom); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,799 @@ |
|||||||
|
/* mescroll |
||||||
|
* version 1.3.3 |
||||||
|
* 2020-09-15 wenju |
||||||
|
* https://www.mescroll.com
|
||||||
|
*/ |
||||||
|
|
||||||
|
export default function MeScroll(options, isScrollBody) { |
||||||
|
let me = this; |
||||||
|
me.version = '1.3.3'; // mescroll版本号
|
||||||
|
me.options = options || {}; // 配置
|
||||||
|
me.isScrollBody = isScrollBody || false; // 滚动区域是否为原生页面滚动; 默认为scroll-view
|
||||||
|
|
||||||
|
me.isDownScrolling = false; // 是否在执行下拉刷新的回调
|
||||||
|
me.isUpScrolling = false; // 是否在执行上拉加载的回调
|
||||||
|
let hasDownCallback = me.options.down && me.options.down.callback; // 是否配置了down的callback
|
||||||
|
|
||||||
|
// 初始化下拉刷新
|
||||||
|
me.initDownScroll(); |
||||||
|
// 初始化上拉加载,则初始化
|
||||||
|
me.initUpScroll(); |
||||||
|
|
||||||
|
// 自动加载
|
||||||
|
setTimeout(function() { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
|
||||||
|
// 自动触发下拉刷新 (只有配置了down的callback才自动触发下拉刷新)
|
||||||
|
if ((me.optDown.use || me.optDown.native) && me.optDown.auto && hasDownCallback) { |
||||||
|
if (me.optDown.autoShowLoading) { |
||||||
|
me.triggerDownScroll(); // 显示下拉进度,执行下拉回调
|
||||||
|
} else { |
||||||
|
me.optDown.callback && me.optDown.callback(me); // 不显示下拉进度,直接执行下拉回调
|
||||||
|
} |
||||||
|
} |
||||||
|
// 自动触发上拉加载
|
||||||
|
if(!me.isUpAutoLoad){ // 部分小程序(头条小程序)emit是异步, 会导致isUpAutoLoad判断有误, 先延时确保先执行down的callback,再执行up的callback
|
||||||
|
setTimeout(function(){ |
||||||
|
me.optUp.use && me.optUp.auto && !me.isUpAutoLoad && me.triggerUpScroll(); |
||||||
|
},100) |
||||||
|
} |
||||||
|
}, 30); // 需让me.optDown.inited和me.optUp.inited先执行
|
||||||
|
} |
||||||
|
|
||||||
|
/* 配置参数:下拉刷新 */ |
||||||
|
MeScroll.prototype.extendDownScroll = function(optDown) { |
||||||
|
// 下拉刷新的配置
|
||||||
|
MeScroll.extend(optDown, { |
||||||
|
use: true, // 是否启用下拉刷新; 默认true
|
||||||
|
auto: true, // 是否在初始化完毕之后自动执行下拉刷新的回调; 默认true
|
||||||
|
native: false, // 是否使用系统自带的下拉刷新; 默认false; 仅mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
|
||||||
|
autoShowLoading: false, // 如果设置auto=true(在初始化完毕之后自动执行下拉刷新的回调),那么是否显示下拉刷新的进度; 默认false
|
||||||
|
isLock: false, // 是否锁定下拉刷新,默认false;
|
||||||
|
offset: 80, // 在列表顶部,下拉大于80px,松手即可触发下拉刷新的回调
|
||||||
|
startTop: 100, // scroll-view快速滚动到顶部时,此时的scroll-top可能大于0, 此值用于控制最大的误差
|
||||||
|
inOffsetRate: 1, // 在列表顶部,下拉的距离小于offset时,改变下拉区域高度比例;值小于1且越接近0,高度变化越小,表现为越往下越难拉
|
||||||
|
outOffsetRate: 0.2, // 在列表顶部,下拉的距离大于offset时,改变下拉区域高度比例;值小于1且越接近0,高度变化越小,表现为越往下越难拉
|
||||||
|
bottomOffset: 20, // 当手指touchmove位置在距离body底部20px范围内的时候结束上拉刷新,避免Webview嵌套导致touchend事件不执行
|
||||||
|
minAngle: 45, // 向下滑动最少偏移的角度,取值区间 [0,90];默认45度,即向下滑动的角度大于45度则触发下拉;而小于45度,将不触发下拉,避免与左右滑动的轮播等组件冲突;
|
||||||
|
textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本
|
||||||
|
textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本
|
||||||
|
textLoading: '加载中 ...', // 加载中的提示文本
|
||||||
|
textSuccess: '加载成功', // 加载成功的文本
|
||||||
|
textErr: '加载失败', // 加载失败的文本
|
||||||
|
beforeEndDelay: 100, // 延时结束的时长 (显示加载成功/失败的时长)
|
||||||
|
bgColor: "transparent", // 背景颜色 (建议在pages.json中再设置一下backgroundColorTop)
|
||||||
|
textColor: "gray", // 文本颜色 (当bgColor配置了颜色,而textColor未配置时,则textColor会默认为白色)
|
||||||
|
inited: null, // 下拉刷新初始化完毕的回调
|
||||||
|
inOffset: null, // 下拉的距离进入offset范围内那一刻的回调
|
||||||
|
outOffset: null, // 下拉的距离大于offset那一刻的回调
|
||||||
|
onMoving: null, // 下拉过程中的回调,滑动过程一直在执行; rate下拉区域当前高度与指定距离的比值(inOffset: rate<1; outOffset: rate>=1); downHight当前下拉区域的高度
|
||||||
|
beforeLoading: null, // 准备触发下拉刷新的回调: 如果return true,将不触发showLoading和callback回调; 常用来完全自定义下拉刷新, 参考案例【淘宝 v6.8.0】
|
||||||
|
showLoading: null, // 显示下拉刷新进度的回调
|
||||||
|
afterLoading: null, // 显示下拉刷新进度的回调之后,马上要执行的代码 (如: 在wxs中使用)
|
||||||
|
beforeEndDownScroll: null, // 准备结束下拉的回调. 返回结束下拉的延时执行时间,默认0ms; 常用于结束下拉之前再显示另外一小段动画,才去隐藏下拉刷新的场景, 参考案例【dotJump】
|
||||||
|
endDownScroll: null, // 结束下拉刷新的回调
|
||||||
|
afterEndDownScroll: null, // 结束下拉刷新的回调,马上要执行的代码 (如: 在wxs中使用)
|
||||||
|
callback: function(mescroll) { |
||||||
|
// 下拉刷新的回调;默认重置上拉加载列表为第一页
|
||||||
|
mescroll.resetUpScroll(); |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
/* 配置参数:上拉加载 */ |
||||||
|
MeScroll.prototype.extendUpScroll = function(optUp) { |
||||||
|
// 上拉加载的配置
|
||||||
|
MeScroll.extend(optUp, { |
||||||
|
use: true, // 是否启用上拉加载; 默认true
|
||||||
|
auto: true, // 是否在初始化完毕之后自动执行上拉加载的回调; 默认true
|
||||||
|
isLock: false, // 是否锁定上拉加载,默认false;
|
||||||
|
isBoth: true, // 上拉加载时,如果滑动到列表顶部是否可以同时触发下拉刷新;默认true,两者可同时触发;
|
||||||
|
callback: null, // 上拉加载的回调;function(page,mescroll){ }
|
||||||
|
page: { |
||||||
|
num: 0, // 当前页码,默认0,回调之前会加1,即callback(page)会从1开始
|
||||||
|
size: 10, // 每页数据的数量
|
||||||
|
time: null // 加载第一页数据服务器返回的时间; 防止用户翻页时,后台新增了数据从而导致下一页数据重复;
|
||||||
|
}, |
||||||
|
noMoreSize: 5, // 如果列表已无数据,可设置列表的总数量要大于等于5条才显示无更多数据;避免列表数据过少(比如只有一条数据),显示无更多数据会不好看
|
||||||
|
offset: 150, // 距底部多远时,触发upCallback,仅mescroll-uni生效 ( mescroll-body配置的是pages.json的 onReachBottomDistance )
|
||||||
|
textLoading: '加载中 ...', // 加载中的提示文本
|
||||||
|
textNoMore: '-- END --', // 没有更多数据的提示文本
|
||||||
|
bgColor: "transparent", // 背景颜色 (建议在pages.json中再设置一下backgroundColorBottom)
|
||||||
|
textColor: "gray", // 文本颜色 (当bgColor配置了颜色,而textColor未配置时,则textColor会默认为白色)
|
||||||
|
inited: null, // 初始化完毕的回调
|
||||||
|
showLoading: null, // 显示加载中的回调
|
||||||
|
showNoMore: null, // 显示无更多数据的回调
|
||||||
|
hideUpScroll: null, // 隐藏上拉加载的回调
|
||||||
|
errDistance: 60, // endErr的时候需往上滑动一段距离,使其往下滑动时再次触发onReachBottom,仅mescroll-body生效
|
||||||
|
toTop: { |
||||||
|
// 回到顶部按钮,需配置src才显示
|
||||||
|
src: null, // 图片路径,默认null (绝对路径或网络图)
|
||||||
|
offset: 1000, // 列表滚动多少距离才显示回到顶部按钮,默认1000
|
||||||
|
duration: 300, // 回到顶部的动画时长,默认300ms (当值为0或300则使用系统自带回到顶部,更流畅; 其他值则通过step模拟,部分机型可能不够流畅,所以非特殊情况不建议修改此项)
|
||||||
|
btnClick: null, // 点击按钮的回调
|
||||||
|
onShow: null, // 是否显示的回调
|
||||||
|
zIndex: 9990, // fixed定位z-index值
|
||||||
|
left: null, // 到左边的距离, 默认null. 此项有值时,right不生效. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
|
||||||
|
right: 20, // 到右边的距离, 默认20 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
|
||||||
|
bottom: 120, // 到底部的距离, 默认120 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
|
||||||
|
safearea: false, // bottom的偏移量是否加上底部安全区的距离, 默认false, 需要适配iPhoneX时使用 (具体的界面如果不配置此项,则取本vue的safearea值)
|
||||||
|
width: 72, // 回到顶部图标的宽度, 默认72 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
|
||||||
|
radius: "50%" // 圆角, 默认"50%" (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
|
||||||
|
}, |
||||||
|
empty: { |
||||||
|
use: true, // 是否显示空布局
|
||||||
|
icon: null, // 图标路径
|
||||||
|
tip: '~ 暂无相关数据 ~', // 提示
|
||||||
|
btnText: '', // 按钮
|
||||||
|
btnClick: null, // 点击按钮的回调
|
||||||
|
onShow: null, // 是否显示的回调
|
||||||
|
fixed: false, // 是否使用fixed定位,默认false; 配置fixed为true,以下的top和zIndex才生效 (transform会使fixed失效,最终会降级为absolute)
|
||||||
|
top: "100rpx", // fixed定位的top值 (完整的单位值,如 "10%"; "100rpx")
|
||||||
|
zIndex: 99 // fixed定位z-index值
|
||||||
|
}, |
||||||
|
onScroll: false // 是否监听滚动事件
|
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
/* 配置参数 */ |
||||||
|
MeScroll.extend = function(userOption, defaultOption) { |
||||||
|
if (!userOption) return defaultOption; |
||||||
|
for (let key in defaultOption) { |
||||||
|
if (userOption[key] == null) { |
||||||
|
let def = defaultOption[key]; |
||||||
|
if (def != null && typeof def === 'object') { |
||||||
|
userOption[key] = MeScroll.extend({}, def); // 深度匹配
|
||||||
|
} else { |
||||||
|
userOption[key] = def; |
||||||
|
} |
||||||
|
} else if (typeof userOption[key] === 'object') { |
||||||
|
MeScroll.extend(userOption[key], defaultOption[key]); // 深度匹配
|
||||||
|
} |
||||||
|
} |
||||||
|
return userOption; |
||||||
|
} |
||||||
|
|
||||||
|
/* 简单判断是否配置了颜色 (非透明,非白色) */ |
||||||
|
MeScroll.prototype.hasColor = function(color) { |
||||||
|
if(!color) return false; |
||||||
|
let c = color.toLowerCase(); |
||||||
|
return c != "#fff" && c != "#ffffff" && c != "transparent" && c != "white" |
||||||
|
} |
||||||
|
|
||||||
|
/* -------初始化下拉刷新------- */ |
||||||
|
MeScroll.prototype.initDownScroll = function() { |
||||||
|
let me = this; |
||||||
|
// 配置参数
|
||||||
|
me.optDown = me.options.down || {}; |
||||||
|
if(!me.optDown.textColor && me.hasColor(me.optDown.bgColor)) me.optDown.textColor = "#fff"; // 当bgColor有值且textColor未设置,则textColor默认白色
|
||||||
|
me.extendDownScroll(me.optDown); |
||||||
|
|
||||||
|
// 如果是mescroll-body且配置了native,则禁止自定义的下拉刷新
|
||||||
|
if(me.isScrollBody && me.optDown.native){ |
||||||
|
me.optDown.use = false |
||||||
|
}else{ |
||||||
|
me.optDown.native = false // 仅mescroll-body支持,mescroll-uni不支持
|
||||||
|
} |
||||||
|
|
||||||
|
me.downHight = 0; // 下拉区域的高度
|
||||||
|
|
||||||
|
// 在页面中加入下拉布局
|
||||||
|
if (me.optDown.use && me.optDown.inited) { |
||||||
|
// 初始化完毕的回调
|
||||||
|
setTimeout(function() { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
|
||||||
|
me.optDown.inited(me); |
||||||
|
}, 0) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/* 列表touchstart事件 */ |
||||||
|
MeScroll.prototype.touchstartEvent = function(e) { |
||||||
|
if (!this.optDown.use) return; |
||||||
|
|
||||||
|
this.startPoint = this.getPoint(e); // 记录起点
|
||||||
|
this.startTop = this.getScrollTop(); // 记录此时的滚动条位置
|
||||||
|
this.startAngle = 0; // 初始角度
|
||||||
|
this.lastPoint = this.startPoint; // 重置上次move的点
|
||||||
|
this.maxTouchmoveY = this.getBodyHeight() - this.optDown.bottomOffset; // 手指触摸的最大范围(写在touchstart避免body获取高度为0的情况)
|
||||||
|
this.inTouchend = false; // 标记不是touchend
|
||||||
|
} |
||||||
|
|
||||||
|
/* 列表touchmove事件 */ |
||||||
|
MeScroll.prototype.touchmoveEvent = function(e) { |
||||||
|
if (!this.optDown.use) return; |
||||||
|
let me = this; |
||||||
|
|
||||||
|
let scrollTop = me.getScrollTop(); // 当前滚动条的距离
|
||||||
|
let curPoint = me.getPoint(e); // 当前点
|
||||||
|
|
||||||
|
let moveY = curPoint.y - me.startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
|
||||||
|
|
||||||
|
// 向下拉 && 在顶部
|
||||||
|
// mescroll-body,直接判定在顶部即可
|
||||||
|
// scroll-view在滚动时不会触发touchmove,当触顶/底/左/右时,才会触发touchmove
|
||||||
|
// scroll-view滚动到顶部时,scrollTop不一定为0,也有可能大于0; 在iOS的APP中scrollTop可能为负数,不一定和startTop相等
|
||||||
|
if (moveY > 0 && ( |
||||||
|
(me.isScrollBody && scrollTop <= 0) |
||||||
|
|| |
||||||
|
(!me.isScrollBody && (scrollTop <= 0 || (scrollTop <= me.optDown.startTop && scrollTop === me.startTop)) ) |
||||||
|
)) { |
||||||
|
// 可下拉的条件
|
||||||
|
if (!me.inTouchend && !me.isDownScrolling && !me.optDown.isLock && (!me.isUpScrolling || (me.isUpScrolling && |
||||||
|
me.optUp.isBoth))) { |
||||||
|
|
||||||
|
// 下拉的初始角度是否在配置的范围内
|
||||||
|
if(!me.startAngle) me.startAngle = me.getAngle(me.lastPoint, curPoint); // 两点之间的角度,区间 [0,90]
|
||||||
|
if (me.startAngle < me.optDown.minAngle) return; // 如果小于配置的角度,则不往下执行下拉刷新
|
||||||
|
|
||||||
|
// 如果手指的位置超过配置的距离,则提前结束下拉,避免Webview嵌套导致touchend无法触发
|
||||||
|
if (me.maxTouchmoveY > 0 && curPoint.y >= me.maxTouchmoveY) { |
||||||
|
me.inTouchend = true; // 标记执行touchend
|
||||||
|
me.touchendEvent(); // 提前触发touchend
|
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
me.preventDefault(e); // 阻止默认事件
|
||||||
|
|
||||||
|
let diff = curPoint.y - me.lastPoint.y; // 和上次比,移动的距离 (大于0向下,小于0向上)
|
||||||
|
|
||||||
|
// 下拉距离 < 指定距离
|
||||||
|
if (me.downHight < me.optDown.offset) { |
||||||
|
if (me.movetype !== 1) { |
||||||
|
me.movetype = 1; // 加入标记,保证只执行一次
|
||||||
|
me.isDownEndSuccess = null; // 重置是否加载成功的状态 (wxs执行的是wxs.wxs)
|
||||||
|
me.optDown.inOffset && me.optDown.inOffset(me); // 进入指定距离范围内那一刻的回调,只执行一次
|
||||||
|
me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
|
||||||
|
} |
||||||
|
me.downHight += diff * me.optDown.inOffsetRate; // 越往下,高度变化越小
|
||||||
|
|
||||||
|
// 指定距离 <= 下拉距离
|
||||||
|
} else { |
||||||
|
if (me.movetype !== 2) { |
||||||
|
me.movetype = 2; // 加入标记,保证只执行一次
|
||||||
|
me.optDown.outOffset && me.optDown.outOffset(me); // 下拉超过指定距离那一刻的回调,只执行一次
|
||||||
|
me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
|
||||||
|
} |
||||||
|
if (diff > 0) { // 向下拉
|
||||||
|
me.downHight += diff * me.optDown.outOffsetRate; // 越往下,高度变化越小
|
||||||
|
} else { // 向上收
|
||||||
|
me.downHight += diff; // 向上收回高度,则向上滑多少收多少高度
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
me.downHight = Math.round(me.downHight) // 取整
|
||||||
|
let rate = me.downHight / me.optDown.offset; // 下拉区域当前高度与指定距离的比值
|
||||||
|
me.optDown.onMoving && me.optDown.onMoving(me, rate, me.downHight); // 下拉过程中的回调,一直在执行
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
me.lastPoint = curPoint; // 记录本次移动的点
|
||||||
|
} |
||||||
|
|
||||||
|
/* 列表touchend事件 */ |
||||||
|
MeScroll.prototype.touchendEvent = function(e) { |
||||||
|
if (!this.optDown.use) return; |
||||||
|
// 如果下拉区域高度已改变,则需重置回来
|
||||||
|
if (this.isMoveDown) { |
||||||
|
if (this.downHight >= this.optDown.offset) { |
||||||
|
// 符合触发刷新的条件
|
||||||
|
this.triggerDownScroll(); |
||||||
|
} else { |
||||||
|
// 不符合的话 则重置
|
||||||
|
this.downHight = 0; |
||||||
|
this.endDownScrollCall(this); |
||||||
|
} |
||||||
|
this.movetype = 0; |
||||||
|
this.isMoveDown = false; |
||||||
|
} else if (!this.isScrollBody && this.getScrollTop() === this.startTop) { // scroll-view到顶/左/右/底的滑动事件
|
||||||
|
let isScrollUp = this.getPoint(e).y - this.startPoint.y < 0; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
|
||||||
|
// 上滑
|
||||||
|
if (isScrollUp) { |
||||||
|
// 需检查滑动的角度
|
||||||
|
let angle = this.getAngle(this.getPoint(e), this.startPoint); // 两点之间的角度,区间 [0,90]
|
||||||
|
if (angle > 80) { |
||||||
|
// 检查并触发上拉
|
||||||
|
this.triggerUpScroll(true); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/* 根据点击滑动事件获取第一个手指的坐标 */ |
||||||
|
MeScroll.prototype.getPoint = function(e) { |
||||||
|
if (!e) { |
||||||
|
return { |
||||||
|
x: 0, |
||||||
|
y: 0 |
||||||
|
} |
||||||
|
} |
||||||
|
if (e.touches && e.touches[0]) { |
||||||
|
return { |
||||||
|
x: e.touches[0].pageX, |
||||||
|
y: e.touches[0].pageY |
||||||
|
} |
||||||
|
} else if (e.changedTouches && e.changedTouches[0]) { |
||||||
|
return { |
||||||
|
x: e.changedTouches[0].pageX, |
||||||
|
y: e.changedTouches[0].pageY |
||||||
|
} |
||||||
|
} else { |
||||||
|
return { |
||||||
|
x: e.clientX, |
||||||
|
y: e.clientY |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/* 计算两点之间的角度: 区间 [0,90]*/ |
||||||
|
MeScroll.prototype.getAngle = function(p1, p2) { |
||||||
|
let x = Math.abs(p1.x - p2.x); |
||||||
|
let y = Math.abs(p1.y - p2.y); |
||||||
|
let z = Math.sqrt(x * x + y * y); |
||||||
|
let angle = 0; |
||||||
|
if (z !== 0) { |
||||||
|
angle = Math.asin(y / z) / Math.PI * 180; |
||||||
|
} |
||||||
|
return angle |
||||||
|
} |
||||||
|
|
||||||
|
/* 触发下拉刷新 */ |
||||||
|
MeScroll.prototype.triggerDownScroll = function() { |
||||||
|
if (this.optDown.beforeLoading && this.optDown.beforeLoading(this)) { |
||||||
|
//return true则处于完全自定义状态
|
||||||
|
} else { |
||||||
|
this.showDownScroll(); // 下拉刷新中...
|
||||||
|
!this.optDown.native && this.optDown.callback && this.optDown.callback(this); // 执行回调,联网加载数据
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/* 显示下拉进度布局 */ |
||||||
|
MeScroll.prototype.showDownScroll = function() { |
||||||
|
this.isDownScrolling = true; // 标记下拉中
|
||||||
|
if (this.optDown.native) { |
||||||
|
uni.startPullDownRefresh(); // 系统自带的下拉刷新
|
||||||
|
this.showDownLoadingCall(0); // 仍触发showLoading,因为上拉加载用到
|
||||||
|
} else{ |
||||||
|
this.downHight = this.optDown.offset; // 更新下拉区域高度
|
||||||
|
this.showDownLoadingCall(this.downHight); // 下拉刷新中...
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
MeScroll.prototype.showDownLoadingCall = function(downHight) { |
||||||
|
this.optDown.showLoading && this.optDown.showLoading(this, downHight); // 下拉刷新中...
|
||||||
|
this.optDown.afterLoading && this.optDown.afterLoading(this, downHight); // 下拉刷新中...触发之后马上要执行的代码
|
||||||
|
} |
||||||
|
|
||||||
|
/* 显示系统自带的下拉刷新时需要处理的业务 */ |
||||||
|
MeScroll.prototype.onPullDownRefresh = function() { |
||||||
|
this.isDownScrolling = true; // 标记下拉中
|
||||||
|
this.showDownLoadingCall(0); // 仍触发showLoading,因为上拉加载用到
|
||||||
|
this.optDown.callback && this.optDown.callback(this); // 执行回调,联网加载数据
|
||||||
|
} |
||||||
|
|
||||||
|
/* 结束下拉刷新 */ |
||||||
|
MeScroll.prototype.endDownScroll = function() { |
||||||
|
if (this.optDown.native) { // 结束原生下拉刷新
|
||||||
|
this.isDownScrolling = false; |
||||||
|
this.endDownScrollCall(this); |
||||||
|
uni.stopPullDownRefresh(); |
||||||
|
return |
||||||
|
} |
||||||
|
let me = this; |
||||||
|
// 结束下拉刷新的方法
|
||||||
|
let endScroll = function() { |
||||||
|
me.downHight = 0; |
||||||
|
me.isDownScrolling = false; |
||||||
|
me.endDownScrollCall(me); |
||||||
|
if(!me.isScrollBody){ |
||||||
|
me.setScrollHeight(0) // scroll-view重置滚动区域,使数据不满屏时仍可检查触发翻页
|
||||||
|
me.scrollTo(0,0) // scroll-view需重置滚动条到顶部,避免startTop大于0时,对下拉刷新的影响
|
||||||
|
} |
||||||
|
} |
||||||
|
// 结束下拉刷新时的回调
|
||||||
|
let delay = 0; |
||||||
|
if (me.optDown.beforeEndDownScroll) { |
||||||
|
delay = me.optDown.beforeEndDownScroll(me); // 结束下拉刷新的延时,单位ms
|
||||||
|
if(me.isDownEndSuccess == null) delay = 0; // 没有执行加载中,则不延时
|
||||||
|
} |
||||||
|
if (typeof delay === 'number' && delay > 0) { |
||||||
|
setTimeout(endScroll, delay); |
||||||
|
} else { |
||||||
|
endScroll(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
MeScroll.prototype.endDownScrollCall = function() { |
||||||
|
this.optDown.endDownScroll && this.optDown.endDownScroll(this); |
||||||
|
this.optDown.afterEndDownScroll && this.optDown.afterEndDownScroll(this); |
||||||
|
} |
||||||
|
|
||||||
|
/* 锁定下拉刷新:isLock=ture,null锁定;isLock=false解锁 */ |
||||||
|
MeScroll.prototype.lockDownScroll = function(isLock) { |
||||||
|
if (isLock == null) isLock = true; |
||||||
|
this.optDown.isLock = isLock; |
||||||
|
} |
||||||
|
|
||||||
|
/* 锁定上拉加载:isLock=ture,null锁定;isLock=false解锁 */ |
||||||
|
MeScroll.prototype.lockUpScroll = function(isLock) { |
||||||
|
if (isLock == null) isLock = true; |
||||||
|
this.optUp.isLock = isLock; |
||||||
|
} |
||||||
|
|
||||||
|
/* -------初始化上拉加载------- */ |
||||||
|
MeScroll.prototype.initUpScroll = function() { |
||||||
|
let me = this; |
||||||
|
// 配置参数
|
||||||
|
me.optUp = me.options.up || {use: false} |
||||||
|
if(!me.optUp.textColor && me.hasColor(me.optUp.bgColor)) me.optUp.textColor = "#fff"; // 当bgColor有值且textColor未设置,则textColor默认白色
|
||||||
|
me.extendUpScroll(me.optUp); |
||||||
|
|
||||||
|
if (me.optUp.use === false) return; // 配置不使用上拉加载时,则不初始化上拉布局
|
||||||
|
me.optUp.hasNext = true; // 如果使用上拉,则默认有下一页
|
||||||
|
me.startNum = me.optUp.page.num + 1; // 记录page开始的页码
|
||||||
|
|
||||||
|
// 初始化完毕的回调
|
||||||
|
if (me.optUp.inited) { |
||||||
|
setTimeout(function() { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
|
||||||
|
me.optUp.inited(me); |
||||||
|
}, 0) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/*滚动到底部的事件 (仅mescroll-body生效)*/ |
||||||
|
MeScroll.prototype.onReachBottom = function() { |
||||||
|
if (this.isScrollBody && !this.isUpScrolling) { // 只能支持下拉刷新的时候同时可以触发上拉加载,否则滚动到底部就需要上滑一点才能触发onReachBottom
|
||||||
|
if (!this.optUp.isLock && this.optUp.hasNext) { |
||||||
|
this.triggerUpScroll(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/*列表滚动事件 (仅mescroll-body生效)*/ |
||||||
|
MeScroll.prototype.onPageScroll = function(e) { |
||||||
|
if (!this.isScrollBody) return; |
||||||
|
|
||||||
|
// 更新滚动条的位置 (主要用于判断下拉刷新时,滚动条是否在顶部)
|
||||||
|
this.setScrollTop(e.scrollTop); |
||||||
|
|
||||||
|
// 顶部按钮的显示隐藏
|
||||||
|
if (e.scrollTop >= this.optUp.toTop.offset) { |
||||||
|
this.showTopBtn(); |
||||||
|
} else { |
||||||
|
this.hideTopBtn(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/*列表滚动事件*/ |
||||||
|
MeScroll.prototype.scroll = function(e, onScroll) { |
||||||
|
// 更新滚动条的位置
|
||||||
|
this.setScrollTop(e.scrollTop); |
||||||
|
// 更新滚动内容高度
|
||||||
|
this.setScrollHeight(e.scrollHeight); |
||||||
|
|
||||||
|
// 向上滑还是向下滑动
|
||||||
|
if (this.preScrollY == null) this.preScrollY = 0; |
||||||
|
this.isScrollUp = e.scrollTop - this.preScrollY > 0; |
||||||
|
this.preScrollY = e.scrollTop; |
||||||
|
|
||||||
|
// 上滑 && 检查并触发上拉
|
||||||
|
this.isScrollUp && this.triggerUpScroll(true); |
||||||
|
|
||||||
|
// 顶部按钮的显示隐藏
|
||||||
|
if (e.scrollTop >= this.optUp.toTop.offset) { |
||||||
|
this.showTopBtn(); |
||||||
|
} else { |
||||||
|
this.hideTopBtn(); |
||||||
|
} |
||||||
|
|
||||||
|
// 滑动监听
|
||||||
|
this.optUp.onScroll && onScroll && onScroll() |
||||||
|
} |
||||||
|
|
||||||
|
/* 触发上拉加载 */ |
||||||
|
MeScroll.prototype.triggerUpScroll = function(isCheck) { |
||||||
|
if (!this.isUpScrolling && this.optUp.use && this.optUp.callback) { |
||||||
|
// 是否校验在底部; 默认不校验
|
||||||
|
if (isCheck === true) { |
||||||
|
let canUp = false; |
||||||
|
// 还有下一页 && 没有锁定 && 不在下拉中
|
||||||
|
if (this.optUp.hasNext && !this.optUp.isLock && !this.isDownScrolling) { |
||||||
|
if (this.getScrollBottom() <= this.optUp.offset) { // 到底部
|
||||||
|
canUp = true; // 标记可上拉
|
||||||
|
} |
||||||
|
} |
||||||
|
if (canUp === false) return; |
||||||
|
} |
||||||
|
this.showUpScroll(); // 上拉加载中...
|
||||||
|
this.optUp.page.num++; // 预先加一页,如果失败则减回
|
||||||
|
this.isUpAutoLoad = true; // 标记上拉已经自动执行过,避免初始化时多次触发上拉回调
|
||||||
|
this.num = this.optUp.page.num; // 把最新的页数赋值在mescroll上,避免对page的影响
|
||||||
|
this.size = this.optUp.page.size; // 把最新的页码赋值在mescroll上,避免对page的影响
|
||||||
|
this.time = this.optUp.page.time; // 把最新的页码赋值在mescroll上,避免对page的影响
|
||||||
|
this.optUp.callback(this); // 执行回调,联网加载数据
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/* 显示上拉加载中 */ |
||||||
|
MeScroll.prototype.showUpScroll = function() { |
||||||
|
this.isUpScrolling = true; // 标记上拉加载中
|
||||||
|
this.optUp.showLoading && this.optUp.showLoading(this); // 回调
|
||||||
|
} |
||||||
|
|
||||||
|
/* 显示上拉无更多数据 */ |
||||||
|
MeScroll.prototype.showNoMore = function() { |
||||||
|
this.optUp.hasNext = false; // 标记无更多数据
|
||||||
|
this.optUp.showNoMore && this.optUp.showNoMore(this); // 回调
|
||||||
|
} |
||||||
|
|
||||||
|
/* 隐藏上拉区域**/ |
||||||
|
MeScroll.prototype.hideUpScroll = function() { |
||||||
|
this.optUp.hideUpScroll && this.optUp.hideUpScroll(this); // 回调
|
||||||
|
} |
||||||
|
|
||||||
|
/* 结束上拉加载 */ |
||||||
|
MeScroll.prototype.endUpScroll = function(isShowNoMore) { |
||||||
|
if (isShowNoMore != null) { // isShowNoMore=null,不处理下拉状态,下拉刷新的时候调用
|
||||||
|
if (isShowNoMore) { |
||||||
|
this.showNoMore(); // isShowNoMore=true,显示无更多数据
|
||||||
|
} else { |
||||||
|
this.hideUpScroll(); // isShowNoMore=false,隐藏上拉加载
|
||||||
|
} |
||||||
|
} |
||||||
|
this.isUpScrolling = false; // 标记结束上拉加载
|
||||||
|
} |
||||||
|
|
||||||
|
/* 重置上拉加载列表为第一页 |
||||||
|
*isShowLoading 是否显示进度布局; |
||||||
|
* 1.默认null,不传参,则显示上拉加载的进度布局 |
||||||
|
* 2.传参true, 则显示下拉刷新的进度布局 |
||||||
|
* 3.传参false,则不显示上拉和下拉的进度 (常用于静默更新列表数据) |
||||||
|
*/ |
||||||
|
MeScroll.prototype.resetUpScroll = function(isShowLoading) { |
||||||
|
if (this.optUp && this.optUp.use) { |
||||||
|
let page = this.optUp.page; |
||||||
|
this.prePageNum = page.num; // 缓存重置前的页码,加载失败可退回
|
||||||
|
this.prePageTime = page.time; // 缓存重置前的时间,加载失败可退回
|
||||||
|
page.num = this.startNum; // 重置为第一页
|
||||||
|
page.time = null; // 重置时间为空
|
||||||
|
if (!this.isDownScrolling && isShowLoading !== false) { // 如果不是下拉刷新触发的resetUpScroll并且不配置列表静默更新,则显示进度;
|
||||||
|
if (isShowLoading == null) { |
||||||
|
this.removeEmpty(); // 移除空布局
|
||||||
|
this.showUpScroll(); // 不传参,默认显示上拉加载的进度布局
|
||||||
|
} else { |
||||||
|
this.showDownScroll(); // 传true,显示下拉刷新的进度布局,不清空列表
|
||||||
|
} |
||||||
|
} |
||||||
|
this.isUpAutoLoad = true; // 标记上拉已经自动执行过,避免初始化时多次触发上拉回调
|
||||||
|
this.num = page.num; // 把最新的页数赋值在mescroll上,避免对page的影响
|
||||||
|
this.size = page.size; // 把最新的页码赋值在mescroll上,避免对page的影响
|
||||||
|
this.time = page.time; // 把最新的页码赋值在mescroll上,避免对page的影响
|
||||||
|
this.optUp.callback && this.optUp.callback(this); // 执行上拉回调
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/* 设置page.num的值 */ |
||||||
|
MeScroll.prototype.setPageNum = function(num) { |
||||||
|
this.optUp.page.num = num - 1; |
||||||
|
} |
||||||
|
|
||||||
|
/* 设置page.size的值 */ |
||||||
|
MeScroll.prototype.setPageSize = function(size) { |
||||||
|
this.optUp.page.size = size; |
||||||
|
} |
||||||
|
|
||||||
|
/* 联网回调成功,结束下拉刷新和上拉加载 |
||||||
|
* dataSize: 当前页的数据量(必传) |
||||||
|
* totalPage: 总页数(必传) |
||||||
|
* systime: 服务器时间 (可空) |
||||||
|
*/ |
||||||
|
MeScroll.prototype.endByPage = function(dataSize, totalPage, systime) { |
||||||
|
let hasNext; |
||||||
|
if (this.optUp.use && totalPage != null) hasNext = this.optUp.page.num < totalPage; // 是否还有下一页
|
||||||
|
this.endSuccess(dataSize, hasNext, systime); |
||||||
|
} |
||||||
|
|
||||||
|
/* 联网回调成功,结束下拉刷新和上拉加载 |
||||||
|
* dataSize: 当前页的数据量(必传) |
||||||
|
* totalSize: 列表所有数据总数量(必传) |
||||||
|
* systime: 服务器时间 (可空) |
||||||
|
*/ |
||||||
|
MeScroll.prototype.endBySize = function(dataSize, totalSize, systime) { |
||||||
|
let hasNext; |
||||||
|
if (this.optUp.use && totalSize != null) { |
||||||
|
let loadSize = (this.optUp.page.num - 1) * this.optUp.page.size + dataSize; // 已加载的数据总数
|
||||||
|
hasNext = loadSize < totalSize; // 是否还有下一页
|
||||||
|
} |
||||||
|
this.endSuccess(dataSize, hasNext, systime); |
||||||
|
} |
||||||
|
|
||||||
|
/* 联网回调成功,结束下拉刷新和上拉加载 |
||||||
|
* dataSize: 当前页的数据个数(不是所有页的数据总和),用于上拉加载判断是否还有下一页.如果不传,则会判断还有下一页 |
||||||
|
* hasNext: 是否还有下一页,布尔类型;用来解决这个小问题:比如列表共有20条数据,每页加载10条,共2页.如果只根据dataSize判断,则需翻到第三页才会知道无更多数据,如果传了hasNext,则翻到第二页即可显示无更多数据. |
||||||
|
* systime: 服务器时间(可空);用来解决这个小问题:当准备翻下一页时,数据库新增了几条记录,此时翻下一页,前面的几条数据会和上一页的重复;这里传入了systime,那么upCallback的page.time就会有值,把page.time传给服务器,让后台过滤新加入的那几条记录 |
||||||
|
*/ |
||||||
|
MeScroll.prototype.endSuccess = function(dataSize, hasNext, systime) { |
||||||
|
let me = this; |
||||||
|
// 结束下拉刷新
|
||||||
|
if (me.isDownScrolling) { |
||||||
|
me.isDownEndSuccess = true |
||||||
|
me.endDownScroll(); |
||||||
|
} |
||||||
|
|
||||||
|
// 结束上拉加载
|
||||||
|
if (me.optUp.use) { |
||||||
|
let isShowNoMore; // 是否已无更多数据
|
||||||
|
if (dataSize != null) { |
||||||
|
let pageNum = me.optUp.page.num; // 当前页码
|
||||||
|
let pageSize = me.optUp.page.size; // 每页长度
|
||||||
|
// 如果是第一页
|
||||||
|
if (pageNum === 1) { |
||||||
|
if (systime) me.optUp.page.time = systime; // 设置加载列表数据第一页的时间
|
||||||
|
} |
||||||
|
if (dataSize < pageSize || hasNext === false) { |
||||||
|
// 返回的数据不满一页时,则说明已无更多数据
|
||||||
|
me.optUp.hasNext = false; |
||||||
|
if (dataSize === 0 && pageNum === 1) { |
||||||
|
// 如果第一页无任何数据且配置了空布局
|
||||||
|
isShowNoMore = false; |
||||||
|
me.showEmpty(); |
||||||
|
} else { |
||||||
|
// 总列表数少于配置的数量,则不显示无更多数据
|
||||||
|
let allDataSize = (pageNum - 1) * pageSize + dataSize; |
||||||
|
if (allDataSize < me.optUp.noMoreSize) { |
||||||
|
isShowNoMore = false; |
||||||
|
} else { |
||||||
|
isShowNoMore = true; |
||||||
|
} |
||||||
|
me.removeEmpty(); // 移除空布局
|
||||||
|
} |
||||||
|
} else { |
||||||
|
// 还有下一页
|
||||||
|
isShowNoMore = false; |
||||||
|
me.optUp.hasNext = true; |
||||||
|
me.removeEmpty(); // 移除空布局
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 隐藏上拉
|
||||||
|
me.endUpScroll(isShowNoMore); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/* 回调失败,结束下拉刷新和上拉加载 */ |
||||||
|
MeScroll.prototype.endErr = function(errDistance) { |
||||||
|
// 结束下拉,回调失败重置回原来的页码和时间
|
||||||
|
if (this.isDownScrolling) { |
||||||
|
this.isDownEndSuccess = false |
||||||
|
let page = this.optUp.page; |
||||||
|
if (page && this.prePageNum) { |
||||||
|
page.num = this.prePageNum; |
||||||
|
page.time = this.prePageTime; |
||||||
|
} |
||||||
|
this.endDownScroll(); |
||||||
|
} |
||||||
|
// 结束上拉,回调失败重置回原来的页码
|
||||||
|
if (this.isUpScrolling) { |
||||||
|
this.optUp.page.num--; |
||||||
|
this.endUpScroll(false); |
||||||
|
// 如果是mescroll-body,则需往回滚一定距离
|
||||||
|
if(this.isScrollBody && errDistance !== 0){ // 不处理0
|
||||||
|
if(!errDistance) errDistance = this.optUp.errDistance; // 不传,则取默认
|
||||||
|
this.scrollTo(this.getScrollTop() - errDistance, 0) // 往上回滚的距离
|
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/* 显示空布局 */ |
||||||
|
MeScroll.prototype.showEmpty = function() { |
||||||
|
this.optUp.empty.use && this.optUp.empty.onShow && this.optUp.empty.onShow(true) |
||||||
|
} |
||||||
|
|
||||||
|
/* 移除空布局 */ |
||||||
|
MeScroll.prototype.removeEmpty = function() { |
||||||
|
this.optUp.empty.use && this.optUp.empty.onShow && this.optUp.empty.onShow(false) |
||||||
|
} |
||||||
|
|
||||||
|
/* 显示回到顶部的按钮 */ |
||||||
|
MeScroll.prototype.showTopBtn = function() { |
||||||
|
if (!this.topBtnShow) { |
||||||
|
this.topBtnShow = true; |
||||||
|
this.optUp.toTop.onShow && this.optUp.toTop.onShow(true); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/* 隐藏回到顶部的按钮 */ |
||||||
|
MeScroll.prototype.hideTopBtn = function() { |
||||||
|
if (this.topBtnShow) { |
||||||
|
this.topBtnShow = false; |
||||||
|
this.optUp.toTop.onShow && this.optUp.toTop.onShow(false); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/* 获取滚动条的位置 */ |
||||||
|
MeScroll.prototype.getScrollTop = function() { |
||||||
|
return this.scrollTop || 0 |
||||||
|
} |
||||||
|
|
||||||
|
/* 记录滚动条的位置 */ |
||||||
|
MeScroll.prototype.setScrollTop = function(y) { |
||||||
|
this.scrollTop = y; |
||||||
|
} |
||||||
|
|
||||||
|
/* 滚动到指定位置 */ |
||||||
|
MeScroll.prototype.scrollTo = function(y, t) { |
||||||
|
this.myScrollTo && this.myScrollTo(y, t) // scrollview需自定义回到顶部方法
|
||||||
|
} |
||||||
|
|
||||||
|
/* 自定义scrollTo */ |
||||||
|
MeScroll.prototype.resetScrollTo = function(myScrollTo) { |
||||||
|
this.myScrollTo = myScrollTo |
||||||
|
} |
||||||
|
|
||||||
|
/* 滚动条到底部的距离 */ |
||||||
|
MeScroll.prototype.getScrollBottom = function() { |
||||||
|
return this.getScrollHeight() - this.getClientHeight() - this.getScrollTop() |
||||||
|
} |
||||||
|
|
||||||
|
/* 计步器 |
||||||
|
star: 开始值 |
||||||
|
end: 结束值 |
||||||
|
callback(step,timer): 回调step值,计步器timer,可自行通过window.clearInterval(timer)结束计步器; |
||||||
|
t: 计步时长,传0则直接回调end值;不传则默认300ms |
||||||
|
rate: 周期;不传则默认30ms计步一次 |
||||||
|
* */ |
||||||
|
MeScroll.prototype.getStep = function(star, end, callback, t, rate) { |
||||||
|
let diff = end - star; // 差值
|
||||||
|
if (t === 0 || diff === 0) { |
||||||
|
callback && callback(end); |
||||||
|
return; |
||||||
|
} |
||||||
|
t = t || 300; // 时长 300ms
|
||||||
|
rate = rate || 30; // 周期 30ms
|
||||||
|
let count = t / rate; // 次数
|
||||||
|
let step = diff / count; // 步长
|
||||||
|
let i = 0; // 计数
|
||||||
|
let timer = setInterval(function() { |
||||||
|
if (i < count - 1) { |
||||||
|
star += step; |
||||||
|
callback && callback(star, timer); |
||||||
|
i++; |
||||||
|
} else { |
||||||
|
callback && callback(end, timer); // 最后一次直接设置end,避免计算误差
|
||||||
|
clearInterval(timer); |
||||||
|
} |
||||||
|
}, rate); |
||||||
|
} |
||||||
|
|
||||||
|
/* 滚动容器的高度 */ |
||||||
|
MeScroll.prototype.getClientHeight = function(isReal) { |
||||||
|
let h = this.clientHeight || 0 |
||||||
|
if (h === 0 && isReal !== true) { // 未获取到容器的高度,可临时取body的高度 (可能会有误差)
|
||||||
|
h = this.getBodyHeight() |
||||||
|
} |
||||||
|
return h |
||||||
|
} |
||||||
|
MeScroll.prototype.setClientHeight = function(h) { |
||||||
|
this.clientHeight = h; |
||||||
|
} |
||||||
|
|
||||||
|
/* 滚动内容的高度 */ |
||||||
|
MeScroll.prototype.getScrollHeight = function() { |
||||||
|
return this.scrollHeight || 0; |
||||||
|
} |
||||||
|
MeScroll.prototype.setScrollHeight = function(h) { |
||||||
|
this.scrollHeight = h; |
||||||
|
} |
||||||
|
|
||||||
|
/* body的高度 */ |
||||||
|
MeScroll.prototype.getBodyHeight = function() { |
||||||
|
return this.bodyHeight || 0; |
||||||
|
} |
||||||
|
MeScroll.prototype.setBodyHeight = function(h) { |
||||||
|
this.bodyHeight = h; |
||||||
|
} |
||||||
|
|
||||||
|
/* 阻止浏览器默认滚动事件 */ |
||||||
|
MeScroll.prototype.preventDefault = function(e) { |
||||||
|
// 小程序不支持e.preventDefault, 已在wxs中禁止
|
||||||
|
// app的bounce只能通过配置pages.json的style.app-plus.bounce为"none"来禁止, 或使用renderjs禁止
|
||||||
|
// cancelable:是否可以被禁用; defaultPrevented:是否已经被禁用
|
||||||
|
if (e && e.cancelable && !e.defaultPrevented) e.preventDefault() |
||||||
|
} |
@ -0,0 +1,424 @@ |
|||||||
|
<template> |
||||||
|
<view class="mescroll-uni-warp"> |
||||||
|
<scroll-view :id="viewId" class="mescroll-uni" :class="{'mescroll-uni-fixed':isFixed}" :style="{'height':scrollHeight,'padding-top':padTop,'padding-bottom':padBottom,'top':fixedTop,'bottom':fixedBottom}" :scroll-top="scrollTop" :scroll-with-animation="scrollAnim" @scroll="scroll" :scroll-y='scrollable' :enable-back-to-top="true" :throttle="false"> |
||||||
|
<view class="mescroll-uni-content mescroll-render-touch" |
||||||
|
@touchstart="wxsBiz.touchstartEvent" |
||||||
|
@touchmove="wxsBiz.touchmoveEvent" |
||||||
|
@touchend="wxsBiz.touchendEvent" |
||||||
|
@touchcancel="wxsBiz.touchendEvent" |
||||||
|
:change:prop="wxsBiz.propObserver" |
||||||
|
:prop="wxsProp"> |
||||||
|
<!-- 状态栏 --> |
||||||
|
<view v-if="topbar&&statusBarHeight" class="mescroll-topbar" :style="{height: statusBarHeight+'px', background: topbar}"></view> |
||||||
|
|
||||||
|
<view class="mescroll-wxs-content" :style="{'transform': translateY, 'transition': transition}" :change:prop="wxsBiz.callObserver" :prop="callProp"> |
||||||
|
<!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)--> |
||||||
|
<!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType" :rate="downRate"></mescroll-down> --> |
||||||
|
<view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}"> |
||||||
|
<view class="downwarp-content"> |
||||||
|
<view class="downwarp-progress mescroll-wxs-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'border-color':mescroll.optDown.textColor, 'transform': downRotate}"></view> |
||||||
|
<view class="downwarp-tip">{{downText}}</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<!-- 列表内容 --> |
||||||
|
<slot></slot> |
||||||
|
|
||||||
|
<!-- 空布局 --> |
||||||
|
<mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty> |
||||||
|
|
||||||
|
<!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)--> |
||||||
|
<!-- <mescroll-up v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> --> |
||||||
|
<view v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" class="mescroll-upwarp" :style="{'background':mescroll.optUp.bgColor,'color':mescroll.optUp.textColor}"> |
||||||
|
<!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) --> |
||||||
|
<view v-show="upLoadType===1"> |
||||||
|
<view class="upwarp-progress mescroll-rotate" :style="{'border-color':mescroll.optUp.textColor}"></view> |
||||||
|
<view class="upwarp-tip">{{ mescroll.optUp.textLoading }}</view> |
||||||
|
</view> |
||||||
|
<!-- 无数据 --> |
||||||
|
<view v-if="upLoadType===2" class="upwarp-nodata">{{ mescroll.optUp.textNoMore }}</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<!-- 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效) --> |
||||||
|
<!-- #ifdef H5 --> |
||||||
|
<view v-if="bottombar && windowBottom>0" class="mescroll-bottombar" :style="{height: windowBottom+'px'}"></view> |
||||||
|
<!-- #endif --> |
||||||
|
|
||||||
|
<!-- 适配iPhoneX --> |
||||||
|
<view v-if="safearea" class="mescroll-safearea"></view> |
||||||
|
</view> |
||||||
|
</scroll-view> |
||||||
|
|
||||||
|
<!-- 回到顶部按钮 (fixed元素,需写在scroll-view外面,防止滚动的时候抖动)--> |
||||||
|
<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top> |
||||||
|
|
||||||
|
<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 --> |
||||||
|
<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 --> |
||||||
|
<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view> |
||||||
|
<!-- #endif --> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<!-- 微信小程序, QQ小程序, app, h5使用wxs --> |
||||||
|
<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 --> |
||||||
|
<script src="./wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script> |
||||||
|
<!-- #endif --> |
||||||
|
|
||||||
|
<!-- app, h5使用renderjs --> |
||||||
|
<!-- #ifdef APP-PLUS || H5 --> |
||||||
|
<script module="renderBiz" lang="renderjs"> |
||||||
|
import renderBiz from './wxs/renderjs.js'; |
||||||
|
export default { |
||||||
|
mixins:[renderBiz] |
||||||
|
} |
||||||
|
</script> |
||||||
|
<!-- #endif --> |
||||||
|
|
||||||
|
<script> |
||||||
|
// 引入mescroll-uni.js,处理核心逻辑 |
||||||
|
import MeScroll from './mescroll-uni.js'; |
||||||
|
// 引入全局配置 |
||||||
|
import GlobalOption from './mescroll-uni-option.js'; |
||||||
|
// 引入空布局组件 |
||||||
|
import MescrollEmpty from './components/mescroll-empty.vue'; |
||||||
|
// 引入回到顶部组件 |
||||||
|
import MescrollTop from './components/mescroll-top.vue'; |
||||||
|
// 引入兼容wxs(含renderjs)写法的mixins |
||||||
|
import WxsMixin from './wxs/mixins.js'; |
||||||
|
|
||||||
|
export default { |
||||||
|
mixins: [WxsMixin], |
||||||
|
components: { |
||||||
|
MescrollEmpty, |
||||||
|
MescrollTop |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
mescroll: {optDown:{},optUp:{}}, // mescroll实例 |
||||||
|
viewId: 'id_' + Math.random().toString(36).substr(2,16), // 随机生成mescroll的id(不能数字开头,否则找不到元素) |
||||||
|
downHight: 0, //下拉刷新: 容器高度 |
||||||
|
downRate: 0, // 下拉比率(inOffset: rate<1; outOffset: rate>=1) |
||||||
|
downLoadType: 0, // 下拉刷新状态: 0(loading前), 1(inOffset), 2(outOffset), 3(showLoading), 4(endDownScroll) |
||||||
|
upLoadType: 0, // 上拉加载状态: 0(loading前), 1loading中, 2没有更多了,显示END文本提示, 3(没有更多了,不显示END文本提示) |
||||||
|
isShowEmpty: false, // 是否显示空布局 |
||||||
|
isShowToTop: false, // 是否显示回到顶部按钮 |
||||||
|
scrollTop: 0, // 滚动条的位置 |
||||||
|
scrollAnim: false, // 是否开启滚动动画 |
||||||
|
windowTop: 0, // 可使用窗口的顶部位置 |
||||||
|
windowBottom: 0, // 可使用窗口的底部位置 |
||||||
|
windowHeight: 0, // 可使用窗口的高度 |
||||||
|
statusBarHeight: 0 // 状态栏高度 |
||||||
|
} |
||||||
|
}, |
||||||
|
props: { |
||||||
|
down: Object, // 下拉刷新的参数配置 |
||||||
|
up: Object, // 上拉加载的参数配置 |
||||||
|
top: [String, Number], // 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight) |
||||||
|
topbar: [Boolean, String], // top的偏移量是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变) |
||||||
|
bottom: [String, Number], // 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight) |
||||||
|
safearea: Boolean, // bottom的偏移量是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用) |
||||||
|
fixed: { // 是否通过fixed固定mescroll的高度, 默认true |
||||||
|
type: Boolean, |
||||||
|
default: true |
||||||
|
}, |
||||||
|
height: [String, Number], // 指定mescroll的高度, 此项有值,则不使用fixed. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight) |
||||||
|
bottombar:{ // 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效) |
||||||
|
type: Boolean, |
||||||
|
default: true |
||||||
|
} |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
// 是否使用fixed定位 (当height有值,则不使用) |
||||||
|
isFixed(){ |
||||||
|
return !this.height && this.fixed |
||||||
|
}, |
||||||
|
// mescroll的高度 |
||||||
|
scrollHeight(){ |
||||||
|
if (this.isFixed) { |
||||||
|
return "auto" |
||||||
|
} else if(this.height){ |
||||||
|
return this.toPx(this.height) + 'px' |
||||||
|
}else{ |
||||||
|
return "100%" |
||||||
|
} |
||||||
|
}, |
||||||
|
// 下拉布局往下偏移的距离 (px) |
||||||
|
numTop() { |
||||||
|
return this.toPx(this.top) |
||||||
|
}, |
||||||
|
fixedTop() { |
||||||
|
return this.isFixed ? (this.numTop + this.windowTop) + 'px' : 0 |
||||||
|
}, |
||||||
|
padTop() { |
||||||
|
return !this.isFixed ? this.numTop + 'px' : 0 |
||||||
|
}, |
||||||
|
// 上拉布局往上偏移 (px) |
||||||
|
numBottom() { |
||||||
|
return this.toPx(this.bottom) |
||||||
|
}, |
||||||
|
fixedBottom() { |
||||||
|
return this.isFixed ? (this.numBottom + this.windowBottom) + 'px' : 0 |
||||||
|
}, |
||||||
|
padBottom() { |
||||||
|
return !this.isFixed ? this.numBottom + 'px' : 0 |
||||||
|
}, |
||||||
|
// 是否为重置下拉的状态 |
||||||
|
isDownReset(){ |
||||||
|
return this.downLoadType===3 || this.downLoadType===4 |
||||||
|
}, |
||||||
|
// 过渡 |
||||||
|
transition() { |
||||||
|
return this.isDownReset ? 'transform 300ms' : ''; |
||||||
|
}, |
||||||
|
translateY() { |
||||||
|
return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' : ''; // transform会使fixed失效,需注意把fixed元素写在mescroll之外 |
||||||
|
}, |
||||||
|
// 列表是否可滑动 |
||||||
|
scrollable(){ |
||||||
|
return this.downLoadType===0 || this.isDownReset |
||||||
|
}, |
||||||
|
// 是否在加载中 |
||||||
|
isDownLoading(){ |
||||||
|
return this.downLoadType === 3 |
||||||
|
}, |
||||||
|
// 旋转的角度 |
||||||
|
downRotate(){ |
||||||
|
return 'rotate(' + 360 * this.downRate + 'deg)' |
||||||
|
}, |
||||||
|
// 文本提示 |
||||||
|
downText(){ |
||||||
|
if(!this.mescroll) return ""; // 避免头条小程序初始化时报错 |
||||||
|
switch (this.downLoadType){ |
||||||
|
case 1: return this.mescroll.optDown.textInOffset; |
||||||
|
case 2: return this.mescroll.optDown.textOutOffset; |
||||||
|
case 3: return this.mescroll.optDown.textLoading; |
||||||
|
case 4: return this.mescroll.isDownEndSuccess ? this.mescroll.optDown.textSuccess : this.mescroll.isDownEndSuccess==false ? this.mescroll.optDown.textErr : this.mescroll.optDown.textInOffset; |
||||||
|
default: return this.mescroll.optDown.textInOffset; |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
//number,rpx,upx,px,% --> px的数值 |
||||||
|
toPx(num){ |
||||||
|
if(typeof num === "string"){ |
||||||
|
if (num.indexOf('px') !== -1) { |
||||||
|
if(num.indexOf('rpx') !== -1) { // "10rpx" |
||||||
|
num = num.replace('rpx', ''); |
||||||
|
} else if(num.indexOf('upx') !== -1) { // "10upx" |
||||||
|
num = num.replace('upx', ''); |
||||||
|
} else { // "10px" |
||||||
|
return Number(num.replace('px', '')) |
||||||
|
} |
||||||
|
}else if (num.indexOf('%') !== -1){ |
||||||
|
// 传百分比,则相对于windowHeight,传"10%"则等于windowHeight的10% |
||||||
|
let rate = Number(num.replace("%","")) / 100 |
||||||
|
return this.windowHeight * rate |
||||||
|
} |
||||||
|
} |
||||||
|
return num ? uni.upx2px(Number(num)) : 0 |
||||||
|
}, |
||||||
|
//注册列表滚动事件,用于下拉刷新和上拉加载 |
||||||
|
scroll(e) { |
||||||
|
this.mescroll.scroll(e.detail, () => { |
||||||
|
this.$emit('scroll', this.mescroll) // 此时可直接通过 this.mescroll.scrollTop获取滚动条位置; this.mescroll.isScrollUp获取是否向上滑动 |
||||||
|
}) |
||||||
|
}, |
||||||
|
// 点击空布局的按钮回调 |
||||||
|
emptyClick() { |
||||||
|
this.$emit('emptyclick', this.mescroll) |
||||||
|
}, |
||||||
|
// 点击回到顶部的按钮回调 |
||||||
|
toTopClick() { |
||||||
|
this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部 |
||||||
|
this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调 |
||||||
|
}, |
||||||
|
// 更新滚动区域的高度 (使内容不满屏和到底,都可继续翻页) |
||||||
|
setClientHeight() { |
||||||
|
if (this.mescroll.getClientHeight(true) === 0 && !this.isExec) { |
||||||
|
this.isExec = true; // 避免多次获取 |
||||||
|
this.$nextTick(() => { // 确保dom已渲染 |
||||||
|
this.getClientInfo(data=>{ |
||||||
|
this.isExec = false; |
||||||
|
if (data) { |
||||||
|
this.mescroll.setClientHeight(data.height); |
||||||
|
} else if (this.clientNum != 3) { // 极少部分情况,可能dom还未渲染完毕,递归获取,最多重试3次 |
||||||
|
this.clientNum = this.clientNum == null ? 1 : this.clientNum + 1; |
||||||
|
setTimeout(() => { |
||||||
|
this.setClientHeight() |
||||||
|
}, this.clientNum * 100) |
||||||
|
} |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
}, |
||||||
|
// 获取滚动区域的信息 |
||||||
|
getClientInfo(success){ |
||||||
|
let query = uni.createSelectorQuery(); |
||||||
|
// #ifndef MP-ALIPAY || MP-DINGTALK |
||||||
|
query = query.in(this) // 支付宝小程序不支持in(this),而字节跳动小程序必须写in(this), 否则都取不到值 |
||||||
|
// #endif |
||||||
|
let view = query.select('#' + this.viewId); |
||||||
|
view.boundingClientRect(data => { |
||||||
|
success(data) |
||||||
|
}).exec(); |
||||||
|
} |
||||||
|
}, |
||||||
|
// 使用created初始化mescroll对象; 如果用mounted部分css样式编译到H5会失效 |
||||||
|
created() { |
||||||
|
let vm = this; |
||||||
|
|
||||||
|
let diyOption = { |
||||||
|
// 下拉刷新的配置 |
||||||
|
down: { |
||||||
|
inOffset() { |
||||||
|
vm.downLoadType = 1; // 下拉的距离进入offset范围内那一刻的回调 (自定义mescroll组件时,此行不可删) |
||||||
|
}, |
||||||
|
outOffset() { |
||||||
|
vm.downLoadType = 2; // 下拉的距离大于offset那一刻的回调 (自定义mescroll组件时,此行不可删) |
||||||
|
}, |
||||||
|
onMoving(mescroll, rate, downHight) { |
||||||
|
// 下拉过程中的回调,滑动过程一直在执行; |
||||||
|
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删) |
||||||
|
vm.downRate = rate; //下拉比率 (inOffset: rate<1; outOffset: rate>=1) |
||||||
|
}, |
||||||
|
showLoading(mescroll, downHight) { |
||||||
|
vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删) |
||||||
|
vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删) |
||||||
|
}, |
||||||
|
beforeEndDownScroll(mescroll){ |
||||||
|
vm.downLoadType = 4; |
||||||
|
return mescroll.optDown.beforeEndDelay // 延时结束的时长 |
||||||
|
}, |
||||||
|
endDownScroll() { |
||||||
|
vm.downLoadType = 4; // 结束下拉 (自定义mescroll组件时,此行不可删) |
||||||
|
vm.downHight = 0; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删) |
||||||
|
vm.downResetTimer && clearTimeout(vm.downResetTimer) |
||||||
|
vm.downResetTimer = setTimeout(()=>{ // 过渡动画执行完毕后,需重置为0的状态,以便置空this.transition,避免iOS小程序列表渲染不完整 |
||||||
|
if(vm.downLoadType===4) vm.downLoadType = 0 |
||||||
|
},300) |
||||||
|
}, |
||||||
|
// 派发下拉刷新的回调 |
||||||
|
callback: function(mescroll) { |
||||||
|
vm.$emit('down', mescroll) |
||||||
|
} |
||||||
|
}, |
||||||
|
// 上拉加载的配置 |
||||||
|
up: { |
||||||
|
// 显示加载中的回调 |
||||||
|
showLoading() { |
||||||
|
vm.upLoadType = 1; |
||||||
|
}, |
||||||
|
// 显示无更多数据的回调 |
||||||
|
showNoMore() { |
||||||
|
vm.upLoadType = 2; |
||||||
|
}, |
||||||
|
// 隐藏上拉加载的回调 |
||||||
|
hideUpScroll(mescroll) { |
||||||
|
vm.upLoadType = mescroll.optUp.hasNext ? 0 : 3; |
||||||
|
}, |
||||||
|
// 空布局 |
||||||
|
empty: { |
||||||
|
onShow(isShow) { // 显示隐藏的回调 |
||||||
|
vm.isShowEmpty = isShow; |
||||||
|
} |
||||||
|
}, |
||||||
|
// 回到顶部 |
||||||
|
toTop: { |
||||||
|
onShow(isShow) { // 显示隐藏的回调 |
||||||
|
vm.isShowToTop = isShow; |
||||||
|
} |
||||||
|
}, |
||||||
|
// 派发上拉加载的回调 |
||||||
|
callback: function(mescroll) { |
||||||
|
vm.$emit('up', mescroll); |
||||||
|
// 更新容器的高度 (多mescroll的情况) |
||||||
|
vm.setClientHeight() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
MeScroll.extend(diyOption, GlobalOption); // 混入全局的配置 |
||||||
|
let myOption = JSON.parse(JSON.stringify({'down': vm.down,'up': vm.up})) // 深拷贝,避免对props的影响 |
||||||
|
MeScroll.extend(myOption, diyOption); // 混入具体界面的配置 |
||||||
|
|
||||||
|
// 初始化MeScroll对象 |
||||||
|
vm.mescroll = new MeScroll(myOption); |
||||||
|
vm.mescroll.viewId = vm.viewId; // 附带id |
||||||
|
// init回调mescroll对象 |
||||||
|
vm.$emit('init', vm.mescroll); |
||||||
|
|
||||||
|
// 设置高度 |
||||||
|
const sys = uni.getSystemInfoSync(); |
||||||
|
if(sys.windowTop) vm.windowTop = sys.windowTop; |
||||||
|
if(sys.windowBottom) vm.windowBottom = sys.windowBottom; |
||||||
|
if(sys.windowHeight) vm.windowHeight = sys.windowHeight; |
||||||
|
if(sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight; |
||||||
|
// 使down的bottomOffset生效 |
||||||
|
vm.mescroll.setBodyHeight(sys.windowHeight); |
||||||
|
|
||||||
|
// 因为使用的是scrollview,这里需自定义scrollTo |
||||||
|
vm.mescroll.resetScrollTo((y, t) => { |
||||||
|
vm.scrollAnim = (t !== 0); // t为0,则不使用动画过渡 |
||||||
|
if(typeof y === 'string'){ |
||||||
|
// 小程序不支持slot里面的scroll-into-view, 统一使用计算的方式实现 |
||||||
|
vm.getClientInfo(function(rect){ |
||||||
|
let mescrollTop = rect.top // mescroll到顶部的距离 |
||||||
|
let selector; |
||||||
|
if(y.indexOf('#')==-1 && y.indexOf('.')==-1){ |
||||||
|
selector = '#'+y // 不带#和. 则默认为id选择器 |
||||||
|
}else{ |
||||||
|
selector = y |
||||||
|
// #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-DINGTALK |
||||||
|
if(y.indexOf('>>>')!=-1){ // 不支持跨自定义组件的后代选择器 (转为普通的选择器即可跨组件查询) |
||||||
|
selector = y.split('>>>')[1].trim() |
||||||
|
} |
||||||
|
// #endif |
||||||
|
} |
||||||
|
uni.createSelectorQuery().select(selector).boundingClientRect(function(rect){ |
||||||
|
if (rect) { |
||||||
|
let curY = vm.mescroll.getScrollTop() |
||||||
|
let top = rect.top - mescrollTop |
||||||
|
top += curY |
||||||
|
if(!vm.isFixed) top -= vm.numTop |
||||||
|
vm.scrollTop = curY; |
||||||
|
vm.$nextTick(function() { |
||||||
|
vm.scrollTop = top |
||||||
|
}) |
||||||
|
} else{ |
||||||
|
console.error(selector + ' does not exist'); |
||||||
|
} |
||||||
|
}).exec() |
||||||
|
}) |
||||||
|
return; |
||||||
|
} |
||||||
|
let curY = vm.mescroll.getScrollTop() |
||||||
|
if (t === 0 || t === 300) { // 当t使用默认配置的300时,则使用系统自带的动画过渡 |
||||||
|
vm.scrollTop = curY; |
||||||
|
vm.$nextTick(function() { |
||||||
|
vm.scrollTop = y |
||||||
|
}) |
||||||
|
} else { |
||||||
|
vm.mescroll.getStep(curY, y, step => { // 此写法可支持配置t |
||||||
|
vm.scrollTop = step |
||||||
|
}, t) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
// 具体的界面如果不配置up.toTop.safearea,则取本vue的safearea值 |
||||||
|
if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else { |
||||||
|
vm.mescroll.optUp.toTop.safearea = vm.safearea; |
||||||
|
} |
||||||
|
}, |
||||||
|
mounted() { |
||||||
|
// 设置容器的高度 |
||||||
|
this.setClientHeight() |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style> |
||||||
|
@import "./mescroll-uni.css"; |
||||||
|
@import "./components/mescroll-down.css"; |
||||||
|
@import './components/mescroll-up.css'; |
||||||
|
</style> |
@ -0,0 +1,48 @@ |
|||||||
|
/** |
||||||
|
* mescroll-body写在子组件时,需通过mescroll的mixins补充子组件缺少的生命周期 |
||||||
|
*/ |
||||||
|
const MescrollCompMixin = { |
||||||
|
// 因为子组件无onPageScroll和onReachBottom的页面生命周期,需在页面传递进到子组件 (一级)
|
||||||
|
onPageScroll(e) { |
||||||
|
this.handlePageScroll(e) |
||||||
|
}, |
||||||
|
onReachBottom() { |
||||||
|
this.handleReachBottom() |
||||||
|
}, |
||||||
|
// 当down的native: true时, 还需传递此方法进到子组件
|
||||||
|
onPullDownRefresh(){ |
||||||
|
this.handlePullDownRefresh() |
||||||
|
}, |
||||||
|
// mescroll-body写在子子子...组件的情况 (多级)
|
||||||
|
data() { |
||||||
|
return { |
||||||
|
mescroll: { |
||||||
|
onPageScroll: e=>{ |
||||||
|
this.handlePageScroll(e) |
||||||
|
}, |
||||||
|
onReachBottom: ()=>{ |
||||||
|
this.handleReachBottom() |
||||||
|
}, |
||||||
|
onPullDownRefresh: ()=>{ |
||||||
|
this.handlePullDownRefresh() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
methods:{ |
||||||
|
handlePageScroll(e){ |
||||||
|
let item = this.$refs["mescrollItem"]; |
||||||
|
if(item && item.mescroll) item.mescroll.onPageScroll(e); |
||||||
|
}, |
||||||
|
handleReachBottom(){ |
||||||
|
let item = this.$refs["mescrollItem"]; |
||||||
|
if(item && item.mescroll) item.mescroll.onReachBottom(); |
||||||
|
}, |
||||||
|
handlePullDownRefresh(){ |
||||||
|
let item = this.$refs["mescrollItem"]; |
||||||
|
if(item && item.mescroll) item.mescroll.onPullDownRefresh(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export default MescrollCompMixin; |
@ -0,0 +1,59 @@ |
|||||||
|
/** |
||||||
|
* mescroll-more-item的mixins, 仅在多个 mescroll-body 写在子组件时使用 (参考 mescroll-more 案例) |
||||||
|
*/ |
||||||
|
const MescrollMoreItemMixin = { |
||||||
|
// 支付宝小程序不支持props的mixin,需写在具体的页面中
|
||||||
|
// #ifndef MP-ALIPAY || MP-DINGTALK
|
||||||
|
props:{ |
||||||
|
i: Number, // 每个tab页的专属下标
|
||||||
|
index: { // 当前tab的下标
|
||||||
|
type: Number, |
||||||
|
default(){ |
||||||
|
return 0 |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
// #endif
|
||||||
|
data() { |
||||||
|
return { |
||||||
|
downOption:{ |
||||||
|
auto:false // 不自动加载
|
||||||
|
}, |
||||||
|
upOption:{ |
||||||
|
auto:false // 不自动加载
|
||||||
|
}, |
||||||
|
isInit: false // 当前tab是否已初始化
|
||||||
|
} |
||||||
|
}, |
||||||
|
watch:{ |
||||||
|
// 监听下标的变化
|
||||||
|
index(val){ |
||||||
|
if (this.i === val && !this.isInit) { |
||||||
|
this.isInit = true; // 标记为true
|
||||||
|
this.mescroll && this.mescroll.triggerDownScroll(); |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
// 以ref的方式初始化mescroll对象 (兼容字节跳动小程序)
|
||||||
|
mescrollInitByRef() { |
||||||
|
if(!this.mescroll || !this.mescroll.resetUpScroll){ |
||||||
|
// 字节跳动小程序编辑器不支持一个页面存在相同的ref, 多mescroll的ref需动态生成, 格式为'mescrollRef下标'
|
||||||
|
let mescrollRef = this.$refs.mescrollRef || this.$refs['mescrollRef'+this.i]; |
||||||
|
if(mescrollRef) this.mescroll = mescrollRef.mescroll |
||||||
|
} |
||||||
|
}, |
||||||
|
// mescroll组件初始化的回调,可获取到mescroll对象 (覆盖mescroll-mixins.js的mescrollInit, 为了标记isInit)
|
||||||
|
mescrollInit(mescroll) { |
||||||
|
this.mescroll = mescroll; |
||||||
|
this.mescrollInitByRef && this.mescrollInitByRef(); // 兼容字节跳动小程序
|
||||||
|
// 自动加载当前tab的数据
|
||||||
|
if(this.i === this.index){ |
||||||
|
this.isInit = true; // 标记为true
|
||||||
|
this.mescroll.triggerDownScroll(); |
||||||
|
} |
||||||
|
}, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export default MescrollMoreItemMixin; |
@ -0,0 +1,74 @@ |
|||||||
|
/** |
||||||
|
* mescroll-body写在子组件时, 需通过mescroll的mixins补充子组件缺少的生命周期 |
||||||
|
*/ |
||||||
|
const MescrollMoreMixin = { |
||||||
|
data() { |
||||||
|
return { |
||||||
|
tabIndex: 0, // 当前tab下标
|
||||||
|
mescroll: { |
||||||
|
onPageScroll: e=>{ |
||||||
|
this.handlePageScroll(e) |
||||||
|
}, |
||||||
|
onReachBottom: ()=>{ |
||||||
|
this.handleReachBottom() |
||||||
|
}, |
||||||
|
onPullDownRefresh: ()=>{ |
||||||
|
this.handlePullDownRefresh() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
// 因为子组件无onPageScroll和onReachBottom的页面生命周期,需在页面传递进到子组件
|
||||||
|
onPageScroll(e) { |
||||||
|
this.handlePageScroll(e) |
||||||
|
}, |
||||||
|
onReachBottom() { |
||||||
|
this.handleReachBottom() |
||||||
|
}, |
||||||
|
// 当down的native: true时, 还需传递此方法进到子组件
|
||||||
|
onPullDownRefresh(){ |
||||||
|
this.handlePullDownRefresh() |
||||||
|
}, |
||||||
|
methods:{ |
||||||
|
handlePageScroll(e){ |
||||||
|
let mescroll = this.getMescroll(this.tabIndex); |
||||||
|
mescroll && mescroll.onPageScroll(e); |
||||||
|
}, |
||||||
|
handleReachBottom(){ |
||||||
|
let mescroll = this.getMescroll(this.tabIndex); |
||||||
|
mescroll && mescroll.onReachBottom(); |
||||||
|
}, |
||||||
|
handlePullDownRefresh(){ |
||||||
|
let mescroll = this.getMescroll(this.tabIndex); |
||||||
|
mescroll && mescroll.onPullDownRefresh(); |
||||||
|
}, |
||||||
|
// 根据下标获取对应子组件的mescroll
|
||||||
|
getMescroll(i){ |
||||||
|
if(!this.mescrollItems) this.mescrollItems = []; |
||||||
|
if(!this.mescrollItems[i]) { |
||||||
|
// v-for中的refs
|
||||||
|
let vForItem = this.$refs["mescrollItem"]; |
||||||
|
if(vForItem){ |
||||||
|
this.mescrollItems[i] = vForItem[i] |
||||||
|
}else{ |
||||||
|
// 普通的refs,不可重复
|
||||||
|
this.mescrollItems[i] = this.$refs["mescrollItem"+i]; |
||||||
|
} |
||||||
|
} |
||||||
|
let item = this.mescrollItems[i] |
||||||
|
return item ? item.mescroll : null |
||||||
|
}, |
||||||
|
// 切换tab,恢复滚动条位置
|
||||||
|
tabChange(i){ |
||||||
|
let mescroll = this.getMescroll(i); |
||||||
|
if(mescroll){ |
||||||
|
// 延时(比$nextTick靠谱一些),确保元素已渲染
|
||||||
|
setTimeout(()=>{ |
||||||
|
mescroll.scrollTo(mescroll.getScrollTop(),0) |
||||||
|
},30) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export default MescrollMoreMixin; |
@ -0,0 +1,109 @@ |
|||||||
|
// 定义在wxs (含renderjs) 逻辑层的数据和方法, 与视图层相互通信
|
||||||
|
const WxsMixin = { |
||||||
|
data() { |
||||||
|
return { |
||||||
|
// 传入wxs视图层的数据 (响应式)
|
||||||
|
wxsProp: { |
||||||
|
optDown:{}, // 下拉刷新的配置
|
||||||
|
scrollTop:0, // 滚动条的距离
|
||||||
|
bodyHeight:0, // body的高度
|
||||||
|
isDownScrolling:false, // 是否正在下拉刷新中
|
||||||
|
isUpScrolling:false, // 是否正在上拉加载中
|
||||||
|
isScrollBody:true, // 是否为mescroll-body滚动
|
||||||
|
isUpBoth:true, // 上拉加载时,是否同时可以下拉刷新
|
||||||
|
t: 0 // 数据更新的标记 (只有数据更新了,才会触发wxs的Observer)
|
||||||
|
}, |
||||||
|
|
||||||
|
// 标记调用wxs视图层的方法
|
||||||
|
callProp: { |
||||||
|
callType: '', // 方法名
|
||||||
|
t: 0 // 数据更新的标记 (只有数据更新了,才会触发wxs的Observer)
|
||||||
|
}, |
||||||
|
|
||||||
|
// 不用wxs的平台使用此处的wxsBiz对象,抹平wxs的写法 (微信小程序和APP使用的wxsBiz对象是./wxs/wxs.wxs)
|
||||||
|
// #ifndef MP-WEIXIN || MP-QQ || APP-PLUS || H5
|
||||||
|
wxsBiz: { |
||||||
|
//注册列表touchstart事件,用于下拉刷新
|
||||||
|
touchstartEvent: e=> { |
||||||
|
this.mescroll.touchstartEvent(e); |
||||||
|
}, |
||||||
|
//注册列表touchmove事件,用于下拉刷新
|
||||||
|
touchmoveEvent: e=> { |
||||||
|
this.mescroll.touchmoveEvent(e); |
||||||
|
}, |
||||||
|
//注册列表touchend事件,用于下拉刷新
|
||||||
|
touchendEvent: e=> { |
||||||
|
this.mescroll.touchendEvent(e); |
||||||
|
}, |
||||||
|
propObserver(){}, // 抹平wxs的写法
|
||||||
|
callObserver(){} // 抹平wxs的写法
|
||||||
|
}, |
||||||
|
// #endif
|
||||||
|
|
||||||
|
// 不用renderjs的平台使用此处的renderBiz对象,抹平renderjs的写法 (app 和 h5 使用的renderBiz对象是./wxs/renderjs.js)
|
||||||
|
// #ifndef APP-PLUS || H5
|
||||||
|
renderBiz: { |
||||||
|
propObserver(){} // 抹平renderjs的写法
|
||||||
|
} |
||||||
|
// #endif
|
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
// wxs视图层调用逻辑层的回调
|
||||||
|
wxsCall(msg){ |
||||||
|
if(msg.type === 'setWxsProp'){ |
||||||
|
// 更新wxsProp数据 (值改变才触发更新)
|
||||||
|
this.wxsProp = { |
||||||
|
optDown: this.mescroll.optDown, |
||||||
|
scrollTop: this.mescroll.getScrollTop(), |
||||||
|
bodyHeight: this.mescroll.getBodyHeight(), |
||||||
|
isDownScrolling: this.mescroll.isDownScrolling, |
||||||
|
isUpScrolling: this.mescroll.isUpScrolling, |
||||||
|
isUpBoth: this.mescroll.optUp.isBoth, |
||||||
|
isScrollBody:this.mescroll.isScrollBody, |
||||||
|
t: Date.now() |
||||||
|
} |
||||||
|
}else if(msg.type === 'setLoadType'){ |
||||||
|
// 设置inOffset,outOffset的状态
|
||||||
|
this.downLoadType = msg.downLoadType |
||||||
|
// 状态挂载到mescroll对象, 以便在其他组件中使用, 比如<me-video>中
|
||||||
|
this.$set(this.mescroll, 'downLoadType', this.downLoadType) |
||||||
|
// 重置是否加载成功的状态
|
||||||
|
this.$set(this.mescroll, 'isDownEndSuccess', null) |
||||||
|
}else if(msg.type === 'triggerDownScroll'){ |
||||||
|
// 主动触发下拉刷新
|
||||||
|
this.mescroll.triggerDownScroll(); |
||||||
|
}else if(msg.type === 'endDownScroll'){ |
||||||
|
// 结束下拉刷新
|
||||||
|
this.mescroll.endDownScroll(); |
||||||
|
}else if(msg.type === 'triggerUpScroll'){ |
||||||
|
// 主动触发上拉加载
|
||||||
|
this.mescroll.triggerUpScroll(true); |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
mounted() { |
||||||
|
// #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5
|
||||||
|
// 配置主动触发wxs显示加载进度的回调
|
||||||
|
this.mescroll.optDown.afterLoading = ()=>{ |
||||||
|
this.callProp = {callType: "showLoading", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
|
||||||
|
} |
||||||
|
// 配置主动触发wxs隐藏加载进度的回调
|
||||||
|
this.mescroll.optDown.afterEndDownScroll = ()=>{ |
||||||
|
this.callProp = {callType: "endDownScroll", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
|
||||||
|
let delay = 300 + (this.mescroll.optDown.beforeEndDelay || 0) |
||||||
|
setTimeout(()=>{ |
||||||
|
if(this.downLoadType === 4 || this.downLoadType === 0){ |
||||||
|
this.callProp = {callType: "clearTransform", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
|
||||||
|
} |
||||||
|
// 状态挂载到mescroll对象, 以便在其他组件中使用, 比如<me-video>中
|
||||||
|
this.$set(this.mescroll, 'downLoadType', this.downLoadType) |
||||||
|
}, delay) |
||||||
|
} |
||||||
|
// 初始化wxs的数据
|
||||||
|
this.wxsCall({type: 'setWxsProp'}) |
||||||
|
// #endif
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export default WxsMixin; |
@ -0,0 +1,92 @@ |
|||||||
|
// 使用renderjs直接操作window对象,实现动态控制app和h5的bounce
|
||||||
|
// bounce: iOS橡皮筋,Android半月弧,h5浏览器下拉背景等效果 (下拉刷新时禁止)
|
||||||
|
// https://uniapp.dcloud.io/frame?id=renderjs
|
||||||
|
|
||||||
|
// 与wxs的me实例一致
|
||||||
|
var me = {} |
||||||
|
|
||||||
|
// 初始化window对象的touch事件 (仅初始化一次)
|
||||||
|
if(window && !window.$mescrollRenderInit){ |
||||||
|
window.$mescrollRenderInit = true |
||||||
|
|
||||||
|
|
||||||
|
window.addEventListener('touchstart', function(e){ |
||||||
|
if (me.disabled()) return; |
||||||
|
me.startPoint = me.getPoint(e); // 记录起点
|
||||||
|
}, {passive: true}) |
||||||
|
|
||||||
|
|
||||||
|
window.addEventListener('touchmove', function(e){ |
||||||
|
if (me.disabled()) return; |
||||||
|
if (me.getScrollTop() > 0) return; // 需在顶部下拉,才禁止bounce
|
||||||
|
|
||||||
|
var curPoint = me.getPoint(e); // 当前点
|
||||||
|
var moveY = curPoint.y - me.startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
|
||||||
|
// 向下拉
|
||||||
|
if (moveY > 0) { |
||||||
|
// 可下拉的条件
|
||||||
|
if (!me.isDownScrolling && !me.optDown.isLock && (!me.isUpScrolling || (me.isUpScrolling && me.isUpBoth))) { |
||||||
|
|
||||||
|
// 只有touch在mescroll的view上面,才禁止bounce
|
||||||
|
var el = e.target; |
||||||
|
var isMescrollTouch = false; |
||||||
|
while (el && el.tagName && el.tagName !== 'UNI-PAGE-BODY' && el.tagName != "BODY") { |
||||||
|
var cls = el.classList; |
||||||
|
if (cls && cls.contains('mescroll-render-touch')) { |
||||||
|
isMescrollTouch = true |
||||||
|
break; |
||||||
|
} |
||||||
|
el = el.parentNode; // 继续检查其父元素
|
||||||
|
} |
||||||
|
// 禁止bounce (不会对swiper和iOS侧滑返回造成影响)
|
||||||
|
if (isMescrollTouch && e.cancelable && !e.defaultPrevented) e.preventDefault(); |
||||||
|
} |
||||||
|
} |
||||||
|
}, {passive: false}) |
||||||
|
} |
||||||
|
|
||||||
|
/* 获取滚动条的位置 */ |
||||||
|
me.getScrollTop = function() { |
||||||
|
return me.scrollTop || 0 |
||||||
|
} |
||||||
|
|
||||||
|
/* 是否禁用下拉刷新 */ |
||||||
|
me.disabled = function(){ |
||||||
|
return !me.optDown || !me.optDown.use || me.optDown.native |
||||||
|
} |
||||||
|
|
||||||
|
/* 根据点击滑动事件获取第一个手指的坐标 */ |
||||||
|
me.getPoint = function(e) { |
||||||
|
if (!e) { |
||||||
|
return {x: 0,y: 0} |
||||||
|
} |
||||||
|
if (e.touches && e.touches[0]) { |
||||||
|
return {x: e.touches[0].pageX,y: e.touches[0].pageY} |
||||||
|
} else if (e.changedTouches && e.changedTouches[0]) { |
||||||
|
return {x: e.changedTouches[0].pageX,y: e.changedTouches[0].pageY} |
||||||
|
} else { |
||||||
|
return {x: e.clientX,y: e.clientY} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 监听逻辑层数据的变化 (实时更新数据) |
||||||
|
*/ |
||||||
|
function propObserver(wxsProp) { |
||||||
|
me.optDown = wxsProp.optDown |
||||||
|
me.scrollTop = wxsProp.scrollTop |
||||||
|
me.isDownScrolling = wxsProp.isDownScrolling |
||||||
|
me.isUpScrolling = wxsProp.isUpScrolling |
||||||
|
me.isUpBoth = wxsProp.isUpBoth |
||||||
|
} |
||||||
|
|
||||||
|
/* 导出模块 */ |
||||||
|
const renderBiz = { |
||||||
|
data() { |
||||||
|
return { |
||||||
|
propObserver: propObserver, |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export default renderBiz; |
@ -0,0 +1,268 @@ |
|||||||
|
// 使用wxs处理交互动画, 提高性能, 同时避免小程序bounce对下拉刷新的影响 |
||||||
|
// https://uniapp.dcloud.io/frame?id=wxs |
||||||
|
// https://developers.weixin.qq.com/miniprogram/dev/framework/view/interactive-animation.html |
||||||
|
|
||||||
|
// 模拟mescroll实例, 与mescroll.js的写法尽量保持一致 |
||||||
|
var me = {} |
||||||
|
|
||||||
|
// ------ 自定义下拉刷新动画 start ------ |
||||||
|
|
||||||
|
/* 下拉过程中的回调,滑动过程一直在执行 (rate<1为inOffset; rate>1为outOffset) */ |
||||||
|
me.onMoving = function (ins, rate, downHight){ |
||||||
|
ins.requestAnimationFrame(function () { |
||||||
|
ins.selectComponent('.mescroll-wxs-content').setStyle({ |
||||||
|
'will-change': 'transform', // 可解决下拉过程中, image和swiper脱离文档流的问题 |
||||||
|
'transform': 'translateY(' + downHight + 'px)', |
||||||
|
'transition': '' |
||||||
|
}) |
||||||
|
// 环形进度条 |
||||||
|
var progress = ins.selectComponent('.mescroll-wxs-progress') |
||||||
|
progress && progress.setStyle({transform: 'rotate(' + 360 * rate + 'deg)'}) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
/* 显示下拉刷新进度 */ |
||||||
|
me.showLoading = function (ins){ |
||||||
|
me.downHight = me.optDown.offset |
||||||
|
ins.requestAnimationFrame(function () { |
||||||
|
ins.selectComponent('.mescroll-wxs-content').setStyle({ |
||||||
|
'will-change': 'auto', |
||||||
|
'transform': 'translateY(' + me.downHight + 'px)', |
||||||
|
'transition': 'transform 300ms' |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
/* 结束下拉 */ |
||||||
|
me.endDownScroll = function (ins){ |
||||||
|
me.downHight = 0; |
||||||
|
me.isDownScrolling = false; |
||||||
|
ins.requestAnimationFrame(function () { |
||||||
|
ins.selectComponent('.mescroll-wxs-content').setStyle({ |
||||||
|
'will-change': 'auto', |
||||||
|
'transform': 'translateY(0)', // 不可以写空串,否则scroll-view渲染不完整 (延时350ms会调clearTransform置空) |
||||||
|
'transition': 'transform 300ms' |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
/* 结束下拉动画执行完毕后, 清除transform和transition, 避免对列表内容样式造成影响, 如: h5的list-msg示例下拉进度条漏出来等 */ |
||||||
|
me.clearTransform = function (ins){ |
||||||
|
ins.requestAnimationFrame(function () { |
||||||
|
ins.selectComponent('.mescroll-wxs-content').setStyle({ |
||||||
|
'will-change': '', |
||||||
|
'transform': '', |
||||||
|
'transition': '' |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// ------ 自定义下拉刷新动画 end ------ |
||||||
|
|
||||||
|
/** |
||||||
|
* 监听逻辑层数据的变化 (实时更新数据) |
||||||
|
*/ |
||||||
|
function propObserver(wxsProp) { |
||||||
|
me.optDown = wxsProp.optDown |
||||||
|
me.scrollTop = wxsProp.scrollTop |
||||||
|
me.bodyHeight = wxsProp.bodyHeight |
||||||
|
me.isDownScrolling = wxsProp.isDownScrolling |
||||||
|
me.isUpScrolling = wxsProp.isUpScrolling |
||||||
|
me.isUpBoth = wxsProp.isUpBoth |
||||||
|
me.isScrollBody = wxsProp.isScrollBody |
||||||
|
me.startTop = wxsProp.scrollTop // 及时更新touchstart触发的startTop, 避免scroll-view快速惯性滚动到顶部取值不准确 |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 监听逻辑层数据的变化 (调用wxs的方法) |
||||||
|
*/ |
||||||
|
function callObserver(callProp, oldValue, ins) { |
||||||
|
if (me.disabled()) return; |
||||||
|
if(callProp.callType){ |
||||||
|
// 逻辑层(App Service)的style已失效,需在视图层(Webview)设置style |
||||||
|
if(callProp.callType === 'showLoading'){ |
||||||
|
me.showLoading(ins) |
||||||
|
}else if(callProp.callType === 'endDownScroll'){ |
||||||
|
me.endDownScroll(ins) |
||||||
|
}else if(callProp.callType === 'clearTransform'){ |
||||||
|
me.clearTransform(ins) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* touch事件 |
||||||
|
*/ |
||||||
|
function touchstartEvent(e, ins) { |
||||||
|
me.downHight = 0; // 下拉的距离 |
||||||
|
me.startPoint = me.getPoint(e); // 记录起点 |
||||||
|
me.startTop = me.getScrollTop(); // 记录此时的滚动条位置 |
||||||
|
me.startAngle = 0; // 初始角度 |
||||||
|
me.lastPoint = me.startPoint; // 重置上次move的点 |
||||||
|
me.maxTouchmoveY = me.getBodyHeight() - me.optDown.bottomOffset; // 手指触摸的最大范围(写在touchstart避免body获取高度为0的情况) |
||||||
|
me.inTouchend = false; // 标记不是touchend |
||||||
|
|
||||||
|
me.callMethod(ins, {type: 'setWxsProp'}) // 同步更新wxsProp的数据 (小程序是异步的,可能touchmove先执行,才到propObserver; h5和app是同步) |
||||||
|
} |
||||||
|
|
||||||
|
function touchmoveEvent(e, ins) { |
||||||
|
var isPrevent = true // false表示不往上冒泡,相当于调用了同时调用了stopPropagation和preventDefault (对小程序生效, h5和app无效) |
||||||
|
|
||||||
|
if (me.disabled()) return isPrevent; |
||||||
|
|
||||||
|
var scrollTop = me.getScrollTop(); // 当前滚动条的距离 |
||||||
|
var curPoint = me.getPoint(e); // 当前点 |
||||||
|
|
||||||
|
var moveY = curPoint.y - me.startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉 |
||||||
|
|
||||||
|
// 向下拉 && 在顶部 |
||||||
|
// mescroll-body,直接判定在顶部即可 |
||||||
|
// scroll-view在滚动时不会触发touchmove,当触顶/底/左/右时,才会触发touchmove |
||||||
|
// scroll-view滚动到顶部时,scrollTop不一定为0,也有可能大于0; 在iOS的APP中scrollTop可能为负数,不一定和startTop相等 |
||||||
|
if (moveY > 0 && ( |
||||||
|
(me.isScrollBody && scrollTop <= 0) |
||||||
|
|| |
||||||
|
(!me.isScrollBody && (scrollTop <= 0 || (scrollTop <= me.optDown.startTop && scrollTop === me.startTop)) ) |
||||||
|
)) { |
||||||
|
// 可下拉的条件 |
||||||
|
if (!me.inTouchend && !me.isDownScrolling && !me.optDown.isLock && (!me.isUpScrolling || (me.isUpScrolling && |
||||||
|
me.isUpBoth))) { |
||||||
|
|
||||||
|
// 下拉的角度是否在配置的范围内 |
||||||
|
if(!me.startAngle) me.startAngle = me.getAngle(me.lastPoint, curPoint); // 两点之间的角度,区间 [0,90] |
||||||
|
if (me.startAngle < me.optDown.minAngle) return isPrevent; // 如果小于配置的角度,则不往下执行下拉刷新 |
||||||
|
|
||||||
|
// 如果手指的位置超过配置的距离,则提前结束下拉,避免Webview嵌套导致touchend无法触发 |
||||||
|
if (me.maxTouchmoveY > 0 && curPoint.y >= me.maxTouchmoveY) { |
||||||
|
me.inTouchend = true; // 标记执行touchend |
||||||
|
touchendEvent(e, ins); // 提前触发touchend |
||||||
|
return isPrevent; |
||||||
|
} |
||||||
|
|
||||||
|
isPrevent = false // 小程序是return false |
||||||
|
|
||||||
|
var diff = curPoint.y - me.lastPoint.y; // 和上次比,移动的距离 (大于0向下,小于0向上) |
||||||
|
|
||||||
|
// 下拉距离 < 指定距离 |
||||||
|
if (me.downHight < me.optDown.offset) { |
||||||
|
if (me.movetype !== 1) { |
||||||
|
me.movetype = 1; // 加入标记,保证只执行一次 |
||||||
|
// me.optDown.inOffset && me.optDown.inOffset(me); // 进入指定距离范围内那一刻的回调,只执行一次 |
||||||
|
me.callMethod(ins, {type: 'setLoadType', downLoadType: 1}) |
||||||
|
me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来 |
||||||
|
} |
||||||
|
me.downHight += diff * me.optDown.inOffsetRate; // 越往下,高度变化越小 |
||||||
|
|
||||||
|
// 指定距离 <= 下拉距离 |
||||||
|
} else { |
||||||
|
if (me.movetype !== 2) { |
||||||
|
me.movetype = 2; // 加入标记,保证只执行一次 |
||||||
|
// me.optDown.outOffset && me.optDown.outOffset(me); // 下拉超过指定距离那一刻的回调,只执行一次 |
||||||
|
me.callMethod(ins, {type: 'setLoadType', downLoadType: 2}) |
||||||
|
me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来 |
||||||
|
} |
||||||
|
if (diff > 0) { // 向下拉 |
||||||
|
me.downHight += diff * me.optDown.outOffsetRate; // 越往下,高度变化越小 |
||||||
|
} else { // 向上收 |
||||||
|
me.downHight += diff; // 向上收回高度,则向上滑多少收多少高度 |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
me.downHight = Math.round(me.downHight) // 取整 |
||||||
|
var rate = me.downHight / me.optDown.offset; // 下拉区域当前高度与指定距离的比值 |
||||||
|
// me.optDown.onMoving && me.optDown.onMoving(me, rate, me.downHight); // 下拉过程中的回调,一直在执行 |
||||||
|
me.onMoving(ins, rate, me.downHight) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
me.lastPoint = curPoint; // 记录本次移动的点 |
||||||
|
|
||||||
|
return isPrevent // false表示不往上冒泡,相当于调用了同时调用了stopPropagation和preventDefault (对小程序生效, h5和app无效) |
||||||
|
} |
||||||
|
|
||||||
|
function touchendEvent(e, ins) { |
||||||
|
// 如果下拉区域高度已改变,则需重置回来 |
||||||
|
if (me.isMoveDown) { |
||||||
|
if (me.downHight >= me.optDown.offset) { |
||||||
|
// 符合触发刷新的条件 |
||||||
|
me.downHight = me.optDown.offset; // 更新下拉区域高度 |
||||||
|
// me.triggerDownScroll(); |
||||||
|
me.callMethod(ins, {type: 'triggerDownScroll'}) |
||||||
|
} else { |
||||||
|
// 不符合的话 则重置 |
||||||
|
me.downHight = 0; |
||||||
|
// me.optDown.endDownScroll && me.optDown.endDownScroll(me); |
||||||
|
me.callMethod(ins, {type: 'endDownScroll'}) |
||||||
|
} |
||||||
|
me.movetype = 0; |
||||||
|
me.isMoveDown = false; |
||||||
|
} else if (!me.isScrollBody && me.getScrollTop() === me.startTop) { // scroll-view到顶/左/右/底的滑动事件 |
||||||
|
var isScrollUp = me.getPoint(e).y - me.startPoint.y < 0; // 和起点比,移动的距离,大于0向下拉,小于0向上拉 |
||||||
|
// 上滑 |
||||||
|
if (isScrollUp) { |
||||||
|
// 需检查滑动的角度 |
||||||
|
var angle = me.getAngle(me.getPoint(e), me.startPoint); // 两点之间的角度,区间 [0,90] |
||||||
|
if (angle > 80) { |
||||||
|
// 检查并触发上拉 |
||||||
|
// me.triggerUpScroll(true); |
||||||
|
me.callMethod(ins, {type: 'triggerUpScroll'}) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
me.callMethod(ins, {type: 'setWxsProp'}) // 同步更新wxsProp的数据 (小程序是异步的,可能touchmove先执行,才到propObserver; h5和app是同步) |
||||||
|
} |
||||||
|
|
||||||
|
/* 是否禁用下拉刷新 */ |
||||||
|
me.disabled = function(){ |
||||||
|
return !me.optDown || !me.optDown.use || me.optDown.native |
||||||
|
} |
||||||
|
|
||||||
|
/* 根据点击滑动事件获取第一个手指的坐标 */ |
||||||
|
me.getPoint = function(e) { |
||||||
|
if (!e) { |
||||||
|
return {x: 0,y: 0} |
||||||
|
} |
||||||
|
if (e.touches && e.touches[0]) { |
||||||
|
return {x: e.touches[0].pageX,y: e.touches[0].pageY} |
||||||
|
} else if (e.changedTouches && e.changedTouches[0]) { |
||||||
|
return {x: e.changedTouches[0].pageX,y: e.changedTouches[0].pageY} |
||||||
|
} else { |
||||||
|
return {x: e.clientX,y: e.clientY} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/* 计算两点之间的角度: 区间 [0,90]*/ |
||||||
|
me.getAngle = function (p1, p2) { |
||||||
|
var x = Math.abs(p1.x - p2.x); |
||||||
|
var y = Math.abs(p1.y - p2.y); |
||||||
|
var z = Math.sqrt(x * x + y * y); |
||||||
|
var angle = 0; |
||||||
|
if (z !== 0) { |
||||||
|
angle = Math.asin(y / z) / Math.PI * 180; |
||||||
|
} |
||||||
|
return angle |
||||||
|
} |
||||||
|
|
||||||
|
/* 获取滚动条的位置 */ |
||||||
|
me.getScrollTop = function() { |
||||||
|
return me.scrollTop || 0 |
||||||
|
} |
||||||
|
|
||||||
|
/* 获取body的高度 */ |
||||||
|
me.getBodyHeight = function() { |
||||||
|
return me.bodyHeight || 0; |
||||||
|
} |
||||||
|
|
||||||
|
/* 调用逻辑层的方法 */ |
||||||
|
me.callMethod = function(ins, param) { |
||||||
|
if(ins) ins.callMethod('wxsCall', param) |
||||||
|
} |
||||||
|
|
||||||
|
/* 导出模块 */ |
||||||
|
module.exports = { |
||||||
|
propObserver: propObserver, |
||||||
|
callObserver: callObserver, |
||||||
|
touchstartEvent: touchstartEvent, |
||||||
|
touchmoveEvent: touchmoveEvent, |
||||||
|
touchendEvent: touchendEvent |
||||||
|
} |
@ -0,0 +1,310 @@ |
|||||||
|
<template> |
||||||
|
<view class="content"> |
||||||
|
<view class="mix-tree-list"> |
||||||
|
<block v-for="(item, index) in treeList" :key="index"> |
||||||
|
<view |
||||||
|
class="mix-tree-item flex-between" |
||||||
|
:style="[{ |
||||||
|
paddingLeft: item.rank*15 + 'px', |
||||||
|
zIndex: item.rank*-1 +50 |
||||||
|
}]" |
||||||
|
:class="{ |
||||||
|
border: treeParams.border === true, |
||||||
|
show: item.show, |
||||||
|
last: item.lastRank, |
||||||
|
showchild: item.showChild |
||||||
|
}" |
||||||
|
@click.stop="treeItemTap(item, index)" |
||||||
|
> |
||||||
|
<view class=""> |
||||||
|
<image class="mix-tree-icon" :src="item.lastRank ? treeParams.lastIcon : item.showChild ? treeParams.currentIcon : treeParams.defaultIcon"></image> |
||||||
|
{{item.name}} |
||||||
|
</view> |
||||||
|
<view v-if="item.account" @click="changeRund(item)" class=""> |
||||||
|
<image class="img-round" :src="item.checked?checkedRund:uncheckRund" mode=""></image> |
||||||
|
<!-- <image class="img-round" src="" mode=""></image> --> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</block> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
props: { |
||||||
|
list: { // 数据来源 |
||||||
|
type: Array, |
||||||
|
default(){ |
||||||
|
return []; |
||||||
|
} |
||||||
|
}, |
||||||
|
params: { |
||||||
|
type: Object, |
||||||
|
default(){ |
||||||
|
return {} |
||||||
|
} |
||||||
|
}, |
||||||
|
fliter:{// 搜索传值 |
||||||
|
type:String, |
||||||
|
}, |
||||||
|
multiple:{// 多选是否开启 |
||||||
|
type:Boolean, |
||||||
|
default(){ |
||||||
|
return false |
||||||
|
} |
||||||
|
}, |
||||||
|
limit:{// 限制一次选择多少个 |
||||||
|
type:Number, |
||||||
|
default(){// 默认是99个,相当于不做限制了 |
||||||
|
return 99 |
||||||
|
} |
||||||
|
}, |
||||||
|
unChecked:{// 判断固定评委和摇号评委是否重复,取消该勾选 |
||||||
|
type:String, |
||||||
|
default(){ |
||||||
|
return '' |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
treeList: [], |
||||||
|
treeParams: { |
||||||
|
defaultIcon: '../../static/img/defaultIcon.png', |
||||||
|
currentIcon: '../../static/img/currentIcon.png', |
||||||
|
lastIcon: '', |
||||||
|
border: false |
||||||
|
}, |
||||||
|
bSearch:'', |
||||||
|
aSearch:'', |
||||||
|
colonData:'', |
||||||
|
checkedRund:'../../static/img/btn_pre.png',// 选中样式 |
||||||
|
uncheckRund:'../../static/img/btn_per_un.png',// 没选中样式 |
||||||
|
idsList:[],// ids |
||||||
|
cloneData:[],// 克隆数据 |
||||||
|
nameList:[],// 名称数组 |
||||||
|
|
||||||
|
} |
||||||
|
}, |
||||||
|
watch: { |
||||||
|
list(list){ |
||||||
|
// console.log('重置了列表') |
||||||
|
this.treeList = []// 重置list列表 |
||||||
|
this.treeParams = Object.assign(this.treeParams, this.params); |
||||||
|
this.renderTreeList(list); |
||||||
|
this.$forceUpdate() |
||||||
|
}, |
||||||
|
fliter(val,old){ |
||||||
|
if(!val){// 深拷贝还原 |
||||||
|
// 把ids的值赋值给原数组 |
||||||
|
this.cloneData.forEach(e=>{ |
||||||
|
if(this.idsList.includes(e.id)){ |
||||||
|
e.checked = true |
||||||
|
} |
||||||
|
}) |
||||||
|
this.treeList = this.cloneData |
||||||
|
}else{// 执行搜索 |
||||||
|
this.cloneData.forEach(e=>{ |
||||||
|
if(this.idsList.includes(e.id)){ |
||||||
|
e.checked = true |
||||||
|
} |
||||||
|
}) |
||||||
|
this.treeList = this.cloneData |
||||||
|
let arrData = JSON.parse(JSON.stringify(this.treeList)) |
||||||
|
this.treeList = [] |
||||||
|
arrData.forEach(e=>{// 处理传值 |
||||||
|
if (e.name.includes(val)){ |
||||||
|
e.show = true |
||||||
|
e.showChild = true |
||||||
|
this.treeList.push(e) |
||||||
|
} |
||||||
|
}) |
||||||
|
this.$forceUpdate() |
||||||
|
// this.treeList = [...new Set(this.treeList)] |
||||||
|
} |
||||||
|
}, |
||||||
|
// 取得需要删除的值,匹配删除该值 |
||||||
|
// 需要阻断取消勾选 |
||||||
|
unChecked(val){ |
||||||
|
if(!val||val=='') return |
||||||
|
this.changeChecked(val) |
||||||
|
} |
||||||
|
|
||||||
|
}, |
||||||
|
methods: { |
||||||
|
// 判断固定和摇号评委重复时的取消勾选功能-- |
||||||
|
changeChecked(name){ |
||||||
|
this.treeList.forEach(e=>{ |
||||||
|
if(e.name == name){ |
||||||
|
e.checked = false |
||||||
|
// 若要删除id,在这里加上即可 |
||||||
|
if(this.nameList.indexOf(name)!==-1){ |
||||||
|
this.nameList.splice(this.nameList.indexOf(name),1)// 删除对应的名称 |
||||||
|
} |
||||||
|
if(this.idsList.indexOf(e.id)!==-1){ |
||||||
|
this.idsList.splice(this.idsList.indexOf(e.id),1) |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
this.$emit('checkedRund',this.nameList,this.idsList)// 传递到父级当前选中的 |
||||||
|
this.$emit('delstr','')// 改变值 |
||||||
|
|
||||||
|
}, |
||||||
|
//扁平化树结构 |
||||||
|
renderTreeList(list=[], rank=0, parentId=[]){ |
||||||
|
// 构思:首先循环把每一个item先push |
||||||
|
// 再判断item是否有children,有的话再 |
||||||
|
list.forEach(item=>{ |
||||||
|
this.treeList.push({ |
||||||
|
id: item.id, |
||||||
|
name: item.name, |
||||||
|
parentId, // 父级id数组 |
||||||
|
rank, // 层级 |
||||||
|
showChild: false, //子级是否显示 |
||||||
|
show: rank === 0 ,// 自身是否显示 |
||||||
|
// 自定义 |
||||||
|
account: item.account?item.account:false,// 是否展示勾选 |
||||||
|
// 展示勾选否 , 预留勾选传值功能,未做 |
||||||
|
checked:this.checkedIDS?this.checkedIDS.map(e=>{if(e===item.id){return true }else{return false}}):false |
||||||
|
|
||||||
|
}) |
||||||
|
if(Array.isArray(item.children) && item.children.length > 0){ |
||||||
|
let parents = [...parentId]; |
||||||
|
parents.push(item.id); |
||||||
|
this.renderTreeList(item.children, rank+1, parents); |
||||||
|
}else{ |
||||||
|
this.treeList[this.treeList.length-1].lastRank = true; |
||||||
|
} |
||||||
|
}) |
||||||
|
this.cloneData = JSON.parse(JSON.stringify(this.treeList))// 深度克隆当前的数组 |
||||||
|
}, |
||||||
|
// 点击树节点 |
||||||
|
treeItemTap(item){ |
||||||
|
let list = this.treeList; |
||||||
|
let id = item.id; |
||||||
|
if(item.lastRank === true){ |
||||||
|
//点击最后一级时触发事件 |
||||||
|
this.$emit('treeItemClick', item); |
||||||
|
return; |
||||||
|
} |
||||||
|
item.showChild = !item.showChild; |
||||||
|
list.forEach(childItem=>{ |
||||||
|
if(item.showChild === false){ |
||||||
|
//隐藏所有子级 |
||||||
|
if(!childItem.parentId.includes(id)){ |
||||||
|
return; |
||||||
|
} |
||||||
|
if(childItem.lastRank !== true){ |
||||||
|
childItem.showChild = false; |
||||||
|
} |
||||||
|
childItem.show = false; |
||||||
|
}else{ |
||||||
|
if(childItem.parentId[childItem.parentId.length-1] === id){ |
||||||
|
childItem.show = true; |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
// 点击圆圈事件 |
||||||
|
changeRund(item){ |
||||||
|
if(this.multiple){// 多选操作 |
||||||
|
// 重复选中的判断 |
||||||
|
if(this.idsList.includes(item.id)){// 如果包含了已有的id(人员可能重复) |
||||||
|
item.checked = false |
||||||
|
// 执行删除操作 |
||||||
|
this.idsList.splice(this.idsList.indexOf(item.id),1) |
||||||
|
this.nameList.splice(this.nameList.indexOf(item.name),1) |
||||||
|
}else{ |
||||||
|
if(this.limit){// 限制选中多少个 |
||||||
|
if( this.idsList.length>=this.limit){ |
||||||
|
this.idsList.splice(this.limit,1) |
||||||
|
this.nameList.splice(this.limit,1) |
||||||
|
return uni.showToast({ |
||||||
|
title:`请不要选择超过${this.limit }个!`, |
||||||
|
icon:'none' |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
item.checked = !item.checked |
||||||
|
if(item.checked){// push |
||||||
|
this.idsList.push(item.id) |
||||||
|
this.nameList.push(item.name) |
||||||
|
}else{// 删除 |
||||||
|
this.idsList.splice(this.idsList.indexOf(item.id),1) |
||||||
|
this.nameList.splice(this.nameList.indexOf(item.name),1) |
||||||
|
} |
||||||
|
} |
||||||
|
// 记录所有的数组? |
||||||
|
}else{// 单选操作 |
||||||
|
item.checked = !item.checked// 切换选中状态 |
||||||
|
if(item.checked){// push |
||||||
|
this.idsList.push(item.id) |
||||||
|
this.nameList.push(item.name) |
||||||
|
}else{// 删除操作 |
||||||
|
this.idsList.splice(this.idsList.indexOf(item.id),1) |
||||||
|
this.nameList.splice(this.nameList.indexOf(item.name),1) |
||||||
|
} |
||||||
|
// console.log('单选',this.idsList,this.treeList) |
||||||
|
if(this.idsList.length>1){ |
||||||
|
this.treeList.forEach(e=>{ |
||||||
|
if(e.id === this.idsList[0]){ |
||||||
|
e.checked = false// 变成false |
||||||
|
} |
||||||
|
}) |
||||||
|
this.nameList.splice(0,1)// 名称 |
||||||
|
this.idsList.splice(0,1)// 删除前一个 |
||||||
|
// this.$emit('checkedRund',item,this.idsList)// 传递到父级当前选中的 |
||||||
|
} |
||||||
|
} |
||||||
|
this.$emit('checkedRund',this.nameList,this.idsList)// 传递到父级当前选中的 |
||||||
|
return |
||||||
|
}, |
||||||
|
|
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style> |
||||||
|
.mix-tree-list{ |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
padding-left: 30upx; |
||||||
|
} |
||||||
|
.mix-tree-item{ |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
font-size: 30upx; |
||||||
|
color: #333; |
||||||
|
height: 0; |
||||||
|
opacity: 0; |
||||||
|
transition: .2s; |
||||||
|
position: relative; |
||||||
|
} |
||||||
|
.mix-tree-item.border{ |
||||||
|
border-bottom: 2rpx solid #eee; |
||||||
|
} |
||||||
|
.mix-tree-item.show{ |
||||||
|
height: 80upx; |
||||||
|
opacity: 1; |
||||||
|
} |
||||||
|
.mix-tree-icon{ |
||||||
|
width: 26upx; |
||||||
|
height: 26upx; |
||||||
|
margin-right: 8upx; |
||||||
|
opacity: .9; |
||||||
|
} |
||||||
|
|
||||||
|
.mix-tree-item.showchild:before{ |
||||||
|
transform: rotate(90deg); |
||||||
|
} |
||||||
|
.mix-tree-item.last:before{ |
||||||
|
opacity: 0; |
||||||
|
} |
||||||
|
.img-round{ |
||||||
|
width: 25rpx; |
||||||
|
height: 25rpx; |
||||||
|
margin-right: 100rpx; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,324 @@ |
|||||||
|
<template> |
||||||
|
<view class="select-container" v-show="show" @touchmove.stop.prevent> |
||||||
|
<view class="mask" :class="activeClass ? 'mask-show' : ''" @tap="onCancel(true)"></view> |
||||||
|
<view class="select-box" :class="activeClass ? 'select-box-show' : ''"> |
||||||
|
<view class="header"> |
||||||
|
<text class="cancel" @tap="onCancel">{{cancelText}}</text> |
||||||
|
<view class="all" @tap="onAllToggle" v-if="allShow"> |
||||||
|
<text :class="isAll ? 'all-active' : ''">全选</text> |
||||||
|
</view> |
||||||
|
<text class="confirm" @tap="onConfirm">{{confirmText}}</text> |
||||||
|
</view> |
||||||
|
<view class="body-warp"> |
||||||
|
<scroll-view class="body" scroll-y="true"> |
||||||
|
<slot v-if="!data.length" name="tips"> |
||||||
|
<view class="empty-tips">暂无数据~</view> |
||||||
|
</slot> |
||||||
|
<view |
||||||
|
class="select-item" |
||||||
|
:class="[item.disabled ? 'disabled' : '',selectedArr[index] ? 'selected' : '']" |
||||||
|
v-for="(item,index) in data" |
||||||
|
:key="item[valueName]" |
||||||
|
@tap="onSelected(index)" |
||||||
|
> |
||||||
|
<view class="label">{{item[labelName]}}</view> |
||||||
|
<text v-show="selectedArr[index]" class="selected-icon">✔</text> |
||||||
|
</view> |
||||||
|
</scroll-view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
<!-- 多选组件 --> |
||||||
|
<script> |
||||||
|
export default { |
||||||
|
model: { |
||||||
|
prop: "value", |
||||||
|
event: ["input"] |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
show: false, //是否显示 |
||||||
|
activeClass: false, //激活样式状态 |
||||||
|
selectedArr: [], //选择对照列表 |
||||||
|
selectedArrOld: [] //选择对照列表上一次的数据 |
||||||
|
}; |
||||||
|
}, |
||||||
|
onShow() { |
||||||
|
this.show = this.value; |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
// 返回是否全选 |
||||||
|
isAll() { |
||||||
|
let wipeDisabledList = this.returnWipeDisabledList(); |
||||||
|
if (!wipeDisabledList.length) return false; |
||||||
|
return !wipeDisabledList.includes(false); |
||||||
|
} |
||||||
|
}, |
||||||
|
props: { |
||||||
|
// 双向绑定--是否调起底部 |
||||||
|
value: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
// 取消按钮文字 |
||||||
|
cancelText: { |
||||||
|
type: String, |
||||||
|
default: "取消" |
||||||
|
}, |
||||||
|
// 确认按钮文字 |
||||||
|
confirmText: { |
||||||
|
type: String, |
||||||
|
default: "确认" |
||||||
|
}, |
||||||
|
// label对应的key名称 |
||||||
|
labelName: { |
||||||
|
type: String, |
||||||
|
default: "label" |
||||||
|
}, |
||||||
|
// value对应的key名称 |
||||||
|
valueName: { |
||||||
|
type: String, |
||||||
|
default: "value" |
||||||
|
}, |
||||||
|
// 是否允许点击遮罩层关闭 |
||||||
|
maskCloseAble: { |
||||||
|
type: Boolean, |
||||||
|
default: true |
||||||
|
}, |
||||||
|
// 是否显示全选 |
||||||
|
allShow: { |
||||||
|
type: Boolean, |
||||||
|
default: true |
||||||
|
}, |
||||||
|
// 模式 |
||||||
|
mode: { |
||||||
|
type: String, |
||||||
|
default: "multiple" |
||||||
|
}, |
||||||
|
// 默认选中值 |
||||||
|
defaultSelected: { |
||||||
|
type: Array, |
||||||
|
default: function() { |
||||||
|
return []; |
||||||
|
} |
||||||
|
}, |
||||||
|
// 数据源 |
||||||
|
data: { |
||||||
|
type: Array, |
||||||
|
required: true, |
||||||
|
default: () => { |
||||||
|
return []; |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
watch: { |
||||||
|
async value(newVal) { |
||||||
|
this.show = newVal; |
||||||
|
await this.$nextTick(); |
||||||
|
this.activeClass = newVal; |
||||||
|
if (newVal) { |
||||||
|
this.selectedArrOld = JSON.parse(JSON.stringify(this.selectedArr)); |
||||||
|
} |
||||||
|
}, |
||||||
|
show(newVal) { |
||||||
|
this.$emit("input", newVal); |
||||||
|
this.$emit("change", newVal); |
||||||
|
}, |
||||||
|
data: { |
||||||
|
// 设置初始选择对照列表 |
||||||
|
handler(list) { |
||||||
|
this.selectedArr = list.map(el => false); |
||||||
|
this.setItemActiveState(); |
||||||
|
}, |
||||||
|
deep: true, |
||||||
|
immediate: true |
||||||
|
}, |
||||||
|
defaultSelected: { |
||||||
|
handler() { |
||||||
|
this.setItemActiveState(); |
||||||
|
}, |
||||||
|
deep: true, |
||||||
|
immediate: true |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
// 设置默认选中通用办法 |
||||||
|
setItemActiveState() { |
||||||
|
if (this.data.length && this.defaultSelected.length) { |
||||||
|
this.data.forEach((item, i) => { |
||||||
|
for (let n = 0; n < this.defaultSelected.length; n++) { |
||||||
|
if ( |
||||||
|
!item.disabled && |
||||||
|
item[this.valueName] === this.defaultSelected[n] |
||||||
|
) { |
||||||
|
this.selectedArr.splice(i, 1, true); |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
}, |
||||||
|
/** |
||||||
|
* 选择事件 |
||||||
|
* @index {Number} 点击下标 |
||||||
|
*/ |
||||||
|
onSelected(index) { |
||||||
|
if (this.data[index].disabled) return; |
||||||
|
let index2Active = this.selectedArr[index]; |
||||||
|
this.selectedArr.splice(index, 1, !index2Active); |
||||||
|
}, |
||||||
|
// 取消事件 |
||||||
|
onCancel(isMask) { |
||||||
|
if (!isMask || this.maskCloseAble) { |
||||||
|
this.show = false; |
||||||
|
this.selectedArr = JSON.parse(JSON.stringify(this.selectedArrOld)); |
||||||
|
} else { |
||||||
|
return; |
||||||
|
} |
||||||
|
this.$emit("cancel"); |
||||||
|
}, |
||||||
|
// 返回去除了disabled状态后的对照列表 |
||||||
|
returnWipeDisabledList() { |
||||||
|
let arr = []; |
||||||
|
this.selectedArr.forEach((el, index) => { |
||||||
|
if (!this.data[index].disabled) arr.push(el); |
||||||
|
}); |
||||||
|
return arr; |
||||||
|
}, |
||||||
|
// 全选/非全选事件 |
||||||
|
onAllToggle() { |
||||||
|
let wipeDisabledList = this.returnWipeDisabledList(); |
||||||
|
// 如果去除了disabled的对照列表有false的数据,代表未全选 |
||||||
|
if (wipeDisabledList.includes(false)) { |
||||||
|
this.selectedArr.forEach((el, index) => { |
||||||
|
if (!this.data[index].disabled) |
||||||
|
this.selectedArr.splice(index, 1, true); |
||||||
|
}); |
||||||
|
} else { |
||||||
|
this.selectedArr.forEach((el, index) => { |
||||||
|
if (!this.data[index].disabled) |
||||||
|
el = this.selectedArr.splice(index, 1, false); |
||||||
|
}); |
||||||
|
} |
||||||
|
}, |
||||||
|
// 确定事件 |
||||||
|
onConfirm() { |
||||||
|
this.show = false; |
||||||
|
let selectedData = []; |
||||||
|
this.selectedArr.forEach((el, index) => { |
||||||
|
if (el) { |
||||||
|
selectedData.push(this.data[index]); |
||||||
|
} |
||||||
|
}); |
||||||
|
if (this.mode === "multiple") { |
||||||
|
this.$emit("confirm", selectedData); |
||||||
|
} else { |
||||||
|
let backData = selectedData[0] || {}; |
||||||
|
this.$emit("confirm", backData); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
</script> |
||||||
|
<style lang="scss" scoped> |
||||||
|
.select-container { |
||||||
|
width: 100vw; |
||||||
|
height: 100vh; |
||||||
|
position: fixed; |
||||||
|
left: 0; |
||||||
|
top: 0; |
||||||
|
z-index: 999; |
||||||
|
$paddingLR: 18rpx; |
||||||
|
.mask { |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
background-color: $uni-bg-color-mask; |
||||||
|
opacity: 0; |
||||||
|
transition: opacity 0.3s; |
||||||
|
&.mask-show { |
||||||
|
opacity: 1; |
||||||
|
} |
||||||
|
} |
||||||
|
// 选择器内容区域 |
||||||
|
.select-box { |
||||||
|
width: 100%; |
||||||
|
position: absolute; |
||||||
|
bottom: 0; |
||||||
|
left: 0; |
||||||
|
transform: translate3d(0px, 100%, 0px); |
||||||
|
background-color: $uni-bg-color; |
||||||
|
transition: all 0.3s; |
||||||
|
&.select-box-show { |
||||||
|
transform: translateZ(0); |
||||||
|
} |
||||||
|
.header { |
||||||
|
display: flex; |
||||||
|
box-sizing: border-box; |
||||||
|
width: 100%; |
||||||
|
justify-content: space-between; |
||||||
|
border-bottom: 1px solid $uni-border-color; |
||||||
|
line-height: 76rpx; |
||||||
|
font-size: 30rpx; |
||||||
|
padding: 0 $paddingLR; |
||||||
|
.cancel { |
||||||
|
color: $uni-text-color-grey; |
||||||
|
} |
||||||
|
.all { |
||||||
|
color: $uni-color-success; |
||||||
|
.all-active { |
||||||
|
&::after { |
||||||
|
display: inline-block; |
||||||
|
content: "✔"; |
||||||
|
padding-left: 8rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
.confirm { |
||||||
|
color: $uni-color-primary; |
||||||
|
} |
||||||
|
} |
||||||
|
.body-warp { |
||||||
|
width: 100%; |
||||||
|
height: 30vh; |
||||||
|
box-sizing: border-box; |
||||||
|
padding: 20rpx $paddingLR; |
||||||
|
} |
||||||
|
.body { |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
overflow-y: auto; |
||||||
|
.empty-tips { |
||||||
|
margin-top: 25%; |
||||||
|
text-align: center; |
||||||
|
font-size: 26rpx; |
||||||
|
color: $uni-color-error; |
||||||
|
} |
||||||
|
.select-item { |
||||||
|
display: flex; |
||||||
|
font-size: 26rpx; |
||||||
|
line-height: 58rpx; |
||||||
|
color: #303133; |
||||||
|
position: relative; |
||||||
|
transition: all 0.3s; |
||||||
|
&.selected { |
||||||
|
color: $uni-color-primary; |
||||||
|
} |
||||||
|
&.disabled { |
||||||
|
color: $uni-text-color-disable; |
||||||
|
} |
||||||
|
> .label { |
||||||
|
flex: 1; |
||||||
|
text-align: center; |
||||||
|
} |
||||||
|
> .selected-icon { |
||||||
|
position: absolute; |
||||||
|
right: 0; |
||||||
|
top: 50%; |
||||||
|
transform: translateY(-50%); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,824 @@ |
|||||||
|
<template> |
||||||
|
<view v-if="isShow" class="picker"> |
||||||
|
<!-- 日期选择器 --> |
||||||
|
<view v-if="type!='time'" class="picker-modal"> |
||||||
|
<view class="picker-modal-header"> |
||||||
|
<view class="picker-icon picker-icon-zuozuo" :hover-stay-time="100" hover-class="picker-icon-active" @click="onSetYear('-1')"></view> |
||||||
|
<view class="picker-icon picker-icon-zuo" :hover-stay-time="100" hover-class="picker-icon-active" @click="onSetMonth('-1')"></view> |
||||||
|
<text class="picker-modal-header-title">{{title}}</text> |
||||||
|
<view class="picker-icon picker-icon-you" :hover-stay-time="100" hover-class="picker-icon-active" @click="onSetMonth('+1')"></view> |
||||||
|
<view class="picker-icon picker-icon-youyou" :hover-stay-time="100" hover-class="picker-icon-active" @click="onSetYear('+1')"></view> |
||||||
|
</view> |
||||||
|
<swiper class="picker-modal-body" :circular="true" :duration="200" :skip-hidden-item-layout="true" :current="calendarIndex" @change="onSwiperChange"> |
||||||
|
<swiper-item class="picker-calendar" v-for="(calendar,calendarIndex2) in calendars" :key="calendarIndex2"> |
||||||
|
<view class="picker-calendar-view" v-for="(week,index) in weeks" :key="index - 7"> |
||||||
|
<view class="picker-calendar-view-item">{{week}}</view> |
||||||
|
</view> |
||||||
|
<view class="picker-calendar-view" v-for="(date,dateIndex) in calendar" :key="dateIndex" @click="onSelectDate(date)"> |
||||||
|
<!-- 背景样式 --> |
||||||
|
<view v-show="date.bgStyle.type" :class="'picker-calendar-view-'+date.bgStyle.type" :style="{background: date.bgStyle.background}"></view> |
||||||
|
<!-- 正常和选中样式 --> |
||||||
|
<view class="picker-calendar-view-item" :style="{opacity: date.statusStyle.opacity, color: date.statusStyle.color, background: date.statusStyle.background}"> |
||||||
|
<text>{{date.title}}</text> |
||||||
|
</view> |
||||||
|
<!-- 小圆点样式 --> |
||||||
|
<view class="picker-calendar-view-dot" :style="{opacity: date.dotStyle.opacity, background: date.dotStyle.background}"></view> |
||||||
|
<!-- 信息样式 --> |
||||||
|
<view v-show="date.tips" class="picker-calendar-view-tips">{{date.tips}}</view> |
||||||
|
</view> |
||||||
|
</swiper-item> |
||||||
|
</swiper> |
||||||
|
<view class="picker-modal-footer"> |
||||||
|
<view class="picker-modal-footer-info"> |
||||||
|
<block v-if="isMultiSelect"> |
||||||
|
<view class="picker-display"> |
||||||
|
<text>{{beginText}}日期</text> |
||||||
|
<text class="picker-display-text">{{BeginTitle}}</text> |
||||||
|
<view v-if="isContainTime" class="picker-display-link" :hover-stay-time="100" hover-class="picker-display-link-active" |
||||||
|
:style="{color}" @click="onShowTimePicker('begin')">{{BeginTimeTitle}}</view> |
||||||
|
</view> |
||||||
|
<view class="picker-display"> |
||||||
|
<text>{{endText}}日期</text> |
||||||
|
<text class="picker-display-text">{{EndTitle}}</text> |
||||||
|
<view v-if="isContainTime" class="picker-display-link" :hover-stay-time="100" hover-class="picker-display-link-active" |
||||||
|
:style="{color}" @click="onShowTimePicker('end')">{{EndTimeTitle}}</view> |
||||||
|
</view> |
||||||
|
</block> |
||||||
|
<block v-else> |
||||||
|
<view class="picker-display"> |
||||||
|
<text>当前选择</text> |
||||||
|
<text class="picker-display-text">{{BeginTitle}}</text> |
||||||
|
<view v-if="isContainTime" class="picker-display-link" :hover-stay-time="100" hover-class="picker-display-link-active" |
||||||
|
:style="{color}" @click="onShowTimePicker('begin')">{{BeginTimeTitle}}</view> |
||||||
|
</view> |
||||||
|
</block> |
||||||
|
</view> |
||||||
|
<view class="picker-modal-footer-btn"> |
||||||
|
<view class="picker-btn" :hover-stay-time="100" hover-class="picker-btn-active" @click="onReset">重置</view> |
||||||
|
<view class="picker-btn" :hover-stay-time="100" hover-class="picker-btn-active" @click="onCancel">取消</view> |
||||||
|
<view class="picker-btn" :style="{color}" :hover-stay-time="100" hover-class="picker-btn-active" @click="onConfirm">确定</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<!-- 时间选择器 --> |
||||||
|
<view v-if="showTimePicker" class="picker"> |
||||||
|
<view class="picker-modal picker-time"> |
||||||
|
<view class="picker-modal-header"> |
||||||
|
<text class="picker-modal-header-title">选择日期</text> |
||||||
|
</view> |
||||||
|
<picker-view class="picker-modal-time" indicator-class="picker-modal-time-item" :value="timeValue" @change="onTimeChange"> |
||||||
|
<picker-view-column> |
||||||
|
<view v-for="(v,i) in 24" :key="i">{{i<10?'0'+i:i}}时</view> |
||||||
|
</picker-view-column> |
||||||
|
<picker-view-column> |
||||||
|
<view v-for="(v,i) in 60" :key="i">{{i<10?'0'+i:i}}分</view> |
||||||
|
</picker-view-column> |
||||||
|
<picker-view-column v-if="showSeconds"> |
||||||
|
<view v-for="(v,i) in 60" :key="i">{{i<10?'0'+i:i}}秒</view> |
||||||
|
</picker-view-column> |
||||||
|
</picker-view> |
||||||
|
<view class="picker-modal-footer"> |
||||||
|
<view class="picker-modal-footer-info"> |
||||||
|
<view class="picker-display"> |
||||||
|
<text>当前选择</text> |
||||||
|
<text class="picker-display-text">{{PickerTimeTitle}}</text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="picker-modal-footer-btn"> |
||||||
|
<view class="picker-btn" :hover-stay-time="100" hover-class="picker-btn-active" @click="onCancelTime">取消</view> |
||||||
|
<view class="picker-btn" :style="{color}" :hover-stay-time="100" hover-class="picker-btn-active" @click="onConfirmTime">确定</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
/** |
||||||
|
* 工具函数库 |
||||||
|
*/ |
||||||
|
const DateTools = { |
||||||
|
/** |
||||||
|
* 获取公历节日 |
||||||
|
* @param date Date对象 |
||||||
|
*/ |
||||||
|
getHoliday(date) { |
||||||
|
let holidays = { |
||||||
|
'0101': '元旦', |
||||||
|
'0214': '情人', |
||||||
|
'0308': '妇女', |
||||||
|
'0312': '植树', |
||||||
|
'0401': '愚人', |
||||||
|
'0501': '劳动', |
||||||
|
'0504': '青年', |
||||||
|
'0601': '儿童', |
||||||
|
'0701': '建党', |
||||||
|
'0801': '建军', |
||||||
|
'0903': '抗日', |
||||||
|
'0910': '教师', |
||||||
|
'1001': '国庆', |
||||||
|
'1031': '万圣', |
||||||
|
'1224': '平安', |
||||||
|
'1225': '圣诞' |
||||||
|
}; |
||||||
|
let value = this.format(date, 'mmdd'); |
||||||
|
if (holidays[value]) return holidays[value]; |
||||||
|
return false; |
||||||
|
}, |
||||||
|
/** |
||||||
|
* 解析标准日期格式 |
||||||
|
* @param s 日期字符串 |
||||||
|
* @return 返回Date对象 |
||||||
|
*/ |
||||||
|
parse: s => new Date(s.replace(/(年|月|-)/g, '/').replace(/(日)/g, '')), |
||||||
|
/** |
||||||
|
* 比较日期是否为同一天 |
||||||
|
* @param a Date对象 |
||||||
|
* @param b Date对象 |
||||||
|
* @return Boolean |
||||||
|
*/ |
||||||
|
isSameDay: (a, b) => a.getMonth() == b.getMonth() && a.getFullYear() == b.getFullYear() && a.getDate() == b.getDate(), |
||||||
|
/** |
||||||
|
* 格式化Date对象 |
||||||
|
* @param d 日期对象 |
||||||
|
* @param f 格式字符串 |
||||||
|
* @return 返回格式化后的字符串 |
||||||
|
*/ |
||||||
|
format(d, f) { |
||||||
|
var o = { |
||||||
|
"m+": d.getMonth() + 1, |
||||||
|
"d+": d.getDate(), |
||||||
|
"h+": d.getHours(), |
||||||
|
"i+": d.getMinutes(), |
||||||
|
"s+": d.getSeconds(), |
||||||
|
"q+": Math.floor((d.getMonth() + 3) / 3), |
||||||
|
}; |
||||||
|
if (/(y+)/.test(f)) |
||||||
|
f = f.replace(RegExp.$1, (d.getFullYear() + "").substr(4 - RegExp.$1.length)); |
||||||
|
for (var k in o) |
||||||
|
if (new RegExp("(" + k + ")").test(f)) |
||||||
|
f = f.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length))); |
||||||
|
return f; |
||||||
|
}, |
||||||
|
/** |
||||||
|
* 用于format格式化后的反解析 |
||||||
|
* @param s 日期字符串 |
||||||
|
* @param f 格式字符串 |
||||||
|
* @return 返回Date对象 |
||||||
|
*/ |
||||||
|
inverse(s, f) { |
||||||
|
var o = { |
||||||
|
"y": '', |
||||||
|
"m": '', |
||||||
|
"d": '', |
||||||
|
"h": '', |
||||||
|
"i": '', |
||||||
|
"s": '', |
||||||
|
}; |
||||||
|
let d = new Date(); |
||||||
|
if (s.length != f.length) return d; |
||||||
|
for (let i in f) |
||||||
|
if (o[f[i]] != undefined) o[f[i]] += s[i]; |
||||||
|
if (o.y) d.setFullYear(o.y.length < 4 ? (d.getFullYear() + '').substr(0, 4 - o.y.length) + o.y : o.y); |
||||||
|
o.m && d.setMonth(o.m - 1, 1); |
||||||
|
o.d && d.setDate(o.d - 0); |
||||||
|
o.h && d.setHours(o.h - 0); |
||||||
|
o.i && d.setMinutes(o.i - 0); |
||||||
|
o.s && d.setSeconds(o.s - 0); |
||||||
|
return d; |
||||||
|
}, |
||||||
|
/** |
||||||
|
* 获取日历数组(42天) |
||||||
|
* @param date 日期对象或日期字符串 |
||||||
|
* @param proc 处理日历(和forEach类似),传递一个数组中的item |
||||||
|
* @return Array |
||||||
|
*/ |
||||||
|
getCalendar(date, proc) { |
||||||
|
let it = new Date(date), |
||||||
|
calendars = []; |
||||||
|
it.setDate(1); |
||||||
|
it.setDate(it.getDate() - ((it.getDay() == 0 ? 7 : it.getDay()) - 1)); //偏移量 |
||||||
|
for (let i = 0; i < 42; i++) { |
||||||
|
let tmp = { |
||||||
|
dateObj: new Date(it), |
||||||
|
title: it.getDate(), |
||||||
|
isOtherMonth: it.getMonth() < date.getMonth() || it.getMonth() > date.getMonth() |
||||||
|
}; |
||||||
|
calendars.push(Object.assign(tmp, proc ? proc(tmp) : {})); |
||||||
|
it.setDate(it.getDate() + 1); |
||||||
|
} |
||||||
|
return calendars; |
||||||
|
}, |
||||||
|
/** |
||||||
|
* 获取日期到指定的月份1号(不改变原来的date对象) |
||||||
|
* @param d Date对象 |
||||||
|
* @param v 指定的月份 |
||||||
|
* @return Date对象 |
||||||
|
*/ |
||||||
|
getDateToMonth(d, v) { |
||||||
|
let n = new Date(d); |
||||||
|
n.setMonth(v, 1); |
||||||
|
return n; |
||||||
|
}, |
||||||
|
/** |
||||||
|
* 把时间数组转为时间字符串 |
||||||
|
* @param t Array[时,分,秒] |
||||||
|
* @param showSecinds 是否显示秒 |
||||||
|
* @return 字符串 时:分[:秒] |
||||||
|
*/ |
||||||
|
formatTimeArray(t, s) { |
||||||
|
let r = [...t]; |
||||||
|
if (!s) r.length = 2; |
||||||
|
r.forEach((v, k) => r[k] = ('0' + v).slice(-2)); |
||||||
|
return r.join(':'); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
export default { |
||||||
|
props: { |
||||||
|
//颜色 |
||||||
|
color: { |
||||||
|
type: String, |
||||||
|
default: '#409eff' |
||||||
|
}, |
||||||
|
//是否显示秒 针对type为datetime或time时生效 |
||||||
|
showSeconds: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
//初始的值 |
||||||
|
value: [String, Array], |
||||||
|
//类型date time datetime range rangetime |
||||||
|
type: { |
||||||
|
type: String, |
||||||
|
default: 'range' |
||||||
|
}, |
||||||
|
//是否显示 |
||||||
|
show: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
//初始格式 |
||||||
|
format: { |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
//显示公历节日 |
||||||
|
showHoliday: { |
||||||
|
type: Boolean, |
||||||
|
default: true |
||||||
|
}, |
||||||
|
//显示提示 |
||||||
|
showTips: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
//开始文案 针对type为范围选择时生效 |
||||||
|
beginText: { |
||||||
|
type: String, |
||||||
|
default: '开始' |
||||||
|
}, |
||||||
|
//结束文案 针对type为范围选择时生效 |
||||||
|
endText: { |
||||||
|
type: String, |
||||||
|
default: '结束' |
||||||
|
} |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
isShow: false, //是否显示 |
||||||
|
isMultiSelect: false, //是否为多选 |
||||||
|
isContainTime: false, //是否包含时间 |
||||||
|
date: {}, //当前日期对象 |
||||||
|
weeks: ["一", "二", "三", "四", "五", "六", "日"], |
||||||
|
title: '初始化', //标题 |
||||||
|
calendars: [[],[],[]], //日历数组 |
||||||
|
calendarIndex: 1, //当前日历索引 |
||||||
|
checkeds: [], //选中的日期对象集合 |
||||||
|
showTimePicker: false, //是否显示时间选择器 |
||||||
|
timeValue: [0, 0, 0], //时间选择器的值 |
||||||
|
timeType: 'begin', //当前时间选择的类型 |
||||||
|
beginTime: [0, 0, 0], //当前所选的开始时间值 |
||||||
|
endTime: [0, 0, 0], //当前所选的结束时间值 |
||||||
|
}; |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
//设置值 |
||||||
|
setValue(value) { |
||||||
|
this.date = new Date(); |
||||||
|
this.checkeds = []; |
||||||
|
this.isMultiSelect = this.type.indexOf('range') >= 0; |
||||||
|
this.isContainTime = this.type.indexOf('time') >= 0; |
||||||
|
//将字符串解析为Date对象 |
||||||
|
let parseDateStr = (str) => (this.format ? DateTools.inverse(str, this.format) : DateTools.parse(str)); |
||||||
|
if (value) { |
||||||
|
if (this.isMultiSelect) { |
||||||
|
Array.isArray(value) && value.forEach((dateStr, index) => { |
||||||
|
let date = parseDateStr(dateStr); |
||||||
|
let time = [date.getHours(), date.getMinutes(), date.getSeconds()]; |
||||||
|
if (index == 0) this.beginTime = time; |
||||||
|
else this.endTime = time; |
||||||
|
this.checkeds.push(date); |
||||||
|
}); |
||||||
|
} else { |
||||||
|
if (this.type == 'time') { |
||||||
|
let date = parseDateStr('2019/1/1 ' + value); |
||||||
|
this.beginTime = [date.getHours(), date.getMinutes(), date.getSeconds()]; |
||||||
|
this.onShowTimePicker('begin'); |
||||||
|
} else { |
||||||
|
this.checkeds.push(parseDateStr(value)); |
||||||
|
if (this.isContainTime) this.beginTime = [ |
||||||
|
this.checkeds[0].getHours(), |
||||||
|
this.checkeds[0].getMinutes(), |
||||||
|
this.checkeds[0].getSeconds() |
||||||
|
]; |
||||||
|
} |
||||||
|
} |
||||||
|
if (this.checkeds.length) this.date = new Date(this.checkeds[0]); |
||||||
|
} else { |
||||||
|
if (this.isContainTime) { |
||||||
|
this.beginTime = [this.date.getHours(), this.date.getMinutes(), this.date.getSeconds()]; |
||||||
|
if (this.isMultiSelect) this.endTime = [...this.beginTime]; |
||||||
|
} |
||||||
|
this.checkeds.push(new Date(this.date)); |
||||||
|
} |
||||||
|
if (this.type != 'time') this.refreshCalendars(true); |
||||||
|
else this.onShowTimePicker('begin'); |
||||||
|
}, |
||||||
|
//改变年份 |
||||||
|
onSetYear(value) { |
||||||
|
this.date.setFullYear(this.date.getFullYear() + parseInt(value)); |
||||||
|
this.refreshCalendars(true); |
||||||
|
}, |
||||||
|
//改变月份 |
||||||
|
onSetMonth(value) { |
||||||
|
this.date.setMonth(this.date.getMonth() + parseInt(value)); |
||||||
|
this.refreshCalendars(true); |
||||||
|
}, |
||||||
|
//时间选择变更 |
||||||
|
onTimeChange(e) { |
||||||
|
this.timeValue = e.detail.value; |
||||||
|
}, |
||||||
|
//设置时间选择器的显示状态 |
||||||
|
onShowTimePicker(type) { |
||||||
|
this.showTimePicker = true; |
||||||
|
this.timeType = type; |
||||||
|
this.timeValue = type == 'begin' ? [...this.beginTime] : [...this.endTime]; |
||||||
|
}, |
||||||
|
//处理日历 |
||||||
|
procCalendar(item) { |
||||||
|
//定义初始样式 |
||||||
|
item.statusStyle = { |
||||||
|
opacity: 1, |
||||||
|
color: item.isOtherMonth ? '#ddd' : '#000', |
||||||
|
background: 'transparent' |
||||||
|
}; |
||||||
|
item.bgStyle = { |
||||||
|
type: '', |
||||||
|
background: 'transparent' |
||||||
|
}; |
||||||
|
item.dotStyle = { |
||||||
|
opacity: 1, |
||||||
|
background: 'transparent' |
||||||
|
}; |
||||||
|
item.tips = ""; |
||||||
|
//标记今天的日期 |
||||||
|
if (DateTools.isSameDay(new Date(), item.dateObj)) { |
||||||
|
item.statusStyle.color = this.color; |
||||||
|
if (item.isOtherMonth) item.statusStyle.opacity = 0.3; |
||||||
|
} |
||||||
|
//标记选中项 |
||||||
|
this.checkeds.forEach(date => { |
||||||
|
if (DateTools.isSameDay(date, item.dateObj)) { |
||||||
|
item.statusStyle.background = this.color; |
||||||
|
item.statusStyle.color = '#fff'; |
||||||
|
item.statusStyle.opacity = 1; |
||||||
|
if (this.isMultiSelect && this.showTips) item.tips = this.beginText; |
||||||
|
} |
||||||
|
}); |
||||||
|
//节假日或今日的日期标点 |
||||||
|
if (item.statusStyle.background != this.color) { |
||||||
|
let holiday = this.showHoliday ? DateTools.getHoliday(item.dateObj) : false; |
||||||
|
if (holiday || DateTools.isSameDay(new Date(), item.dateObj)) { |
||||||
|
item.title = holiday || item.title; |
||||||
|
item.dotStyle.background = this.color; |
||||||
|
if (item.isOtherMonth) item.dotStyle.opacity = 0.2; |
||||||
|
} |
||||||
|
} else { |
||||||
|
item.title = item.dateObj.getDate(); |
||||||
|
} |
||||||
|
//有两个日期 |
||||||
|
if (this.checkeds.length == 2) { |
||||||
|
if (DateTools.isSameDay(this.checkeds[0], item.dateObj)) { //开始日期 |
||||||
|
item.bgStyle.type = 'bgbegin'; |
||||||
|
} |
||||||
|
if (DateTools.isSameDay(this.checkeds[1], item.dateObj)) { //结束日期 |
||||||
|
if (this.isMultiSelect && this.showTips) item.tips = item.bgStyle.type ? this.beginText + ' / ' + this.endText : this.endText; |
||||||
|
if (!item.bgStyle.type) { //开始日期不等于结束日期 |
||||||
|
item.bgStyle.type = 'bgend'; |
||||||
|
} else { |
||||||
|
item.bgStyle.type = ''; |
||||||
|
} |
||||||
|
} |
||||||
|
if (!item.bgStyle.type && (+item.dateObj > +this.checkeds[0] && +item.dateObj < +this.checkeds[1])) { //中间的日期 |
||||||
|
item.bgStyle.type = 'bg'; |
||||||
|
item.statusStyle.color = this.color; |
||||||
|
} |
||||||
|
if (item.bgStyle.type) { |
||||||
|
item.bgStyle.background = this.color; |
||||||
|
item.dotStyle.opacity = 1; |
||||||
|
item.statusStyle.opacity = 1; |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
//刷新日历 |
||||||
|
refreshCalendars(refresh = false) { |
||||||
|
let date = new Date(this.date); |
||||||
|
let before = DateTools.getDateToMonth(date, date.getMonth() - 1); |
||||||
|
let after = DateTools.getDateToMonth(date, date.getMonth() + 1); |
||||||
|
if (this.calendarIndex == 0) { |
||||||
|
if(refresh) this.calendars.splice(0, 1, DateTools.getCalendar(date, this.procCalendar)); |
||||||
|
this.calendars.splice(1, 1, DateTools.getCalendar(after, this.procCalendar)); |
||||||
|
this.calendars.splice(2, 1, DateTools.getCalendar(before, this.procCalendar)); |
||||||
|
} else if (this.calendarIndex == 1) { |
||||||
|
this.calendars.splice(0, 1, DateTools.getCalendar(before, this.procCalendar)); |
||||||
|
if(refresh) this.calendars.splice(1, 1, DateTools.getCalendar(date, this.procCalendar)); |
||||||
|
this.calendars.splice(2, 1, DateTools.getCalendar(after, this.procCalendar)); |
||||||
|
} else if (this.calendarIndex == 2) { |
||||||
|
this.calendars.splice(0, 1, DateTools.getCalendar(after, this.procCalendar)); |
||||||
|
this.calendars.splice(1, 1, DateTools.getCalendar(before, this.procCalendar)); |
||||||
|
if(refresh) this.calendars.splice(2, 1, DateTools.getCalendar(date, this.procCalendar)); |
||||||
|
} |
||||||
|
this.title = DateTools.format(this.date, 'yyyy年mm月'); |
||||||
|
}, |
||||||
|
//滑块切换 |
||||||
|
onSwiperChange(e) { |
||||||
|
this.calendarIndex = e.detail.current; |
||||||
|
let calendar = this.calendars[this.calendarIndex]; |
||||||
|
this.date = new Date(calendar[22].dateObj); //取中间一天,保证是当前的月份 |
||||||
|
this.refreshCalendars(); |
||||||
|
}, |
||||||
|
//选中日期 |
||||||
|
onSelectDate(date) { |
||||||
|
if (~this.type.indexOf('range') && this.checkeds.length == 2) this.checkeds = []; |
||||||
|
else if (!(~this.type.indexOf('range')) && this.checkeds.length) this.checkeds = []; |
||||||
|
this.checkeds.push(new Date(date.dateObj)); |
||||||
|
this.checkeds.sort((a, b) => a - b); //从小到大排序 |
||||||
|
this.calendars.forEach(calendar => { |
||||||
|
calendar.forEach(this.procCalendar); //重新处理 |
||||||
|
}); |
||||||
|
}, |
||||||
|
//时间选择取消 |
||||||
|
onCancelTime() { |
||||||
|
this.showTimePicker = false; |
||||||
|
this.type == 'time' && this.onCancel(); |
||||||
|
}, |
||||||
|
//时间选择确定 |
||||||
|
onConfirmTime() { |
||||||
|
if (this.timeType == 'begin') this.beginTime = this.timeValue; |
||||||
|
else this.endTime = this.timeValue; |
||||||
|
this.showTimePicker = false; |
||||||
|
this.type == 'time' && this.onConfirm(); |
||||||
|
}, |
||||||
|
//取消 |
||||||
|
onCancel() { |
||||||
|
this.$emit('cancel', false); |
||||||
|
}, |
||||||
|
//取消 |
||||||
|
onReset() { |
||||||
|
this.$emit('reset', false); |
||||||
|
}, |
||||||
|
//确定 |
||||||
|
onConfirm() { |
||||||
|
let result = { |
||||||
|
value: null, |
||||||
|
date: null |
||||||
|
}; |
||||||
|
//定义默认格式 |
||||||
|
let defaultFormat = { |
||||||
|
'date': 'yyyy/mm/dd', |
||||||
|
'time': 'hh:ii' + (this.showSeconds ? ':ss' : ''), |
||||||
|
'datetime': '' |
||||||
|
}; |
||||||
|
defaultFormat['datetime'] = defaultFormat.date + ' ' + defaultFormat.time; |
||||||
|
let fillTime = (date, timeArr) => { |
||||||
|
date.setHours(timeArr[0], timeArr[1]); |
||||||
|
if (this.showSeconds) date.setSeconds(timeArr[2]); |
||||||
|
}; |
||||||
|
if (this.type == 'time') { |
||||||
|
let date = new Date(); |
||||||
|
fillTime(date, this.beginTime); |
||||||
|
result.value = DateTools.format(date, this.format ? this.format : defaultFormat.time); |
||||||
|
result.date = date; |
||||||
|
} else { |
||||||
|
if (this.isMultiSelect) { |
||||||
|
let values = [], |
||||||
|
dates = []; |
||||||
|
if (this.checkeds.length < 2) return uni.showToast({ |
||||||
|
icon: 'none', |
||||||
|
title: '请选择两个日期' |
||||||
|
}); |
||||||
|
this.checkeds.forEach((date, index) => { |
||||||
|
let newDate = new Date(date); |
||||||
|
if (this.isContainTime) { |
||||||
|
let time = [this.beginTime, this.endTime]; |
||||||
|
fillTime(newDate, time[index]); |
||||||
|
} |
||||||
|
values.push(DateTools.format(newDate, this.format ? this.format : defaultFormat[this.isContainTime ? |
||||||
|
'datetime' : 'date'])); |
||||||
|
dates.push(newDate); |
||||||
|
}); |
||||||
|
result.value = values; |
||||||
|
result.date = dates; |
||||||
|
} else { |
||||||
|
let newDate = new Date(this.checkeds[0]); |
||||||
|
if (this.isContainTime) { |
||||||
|
newDate.setHours(this.beginTime[0], this.beginTime[1]); |
||||||
|
if (this.showSeconds) newDate.setSeconds(this.beginTime[2]); |
||||||
|
} |
||||||
|
result.value = DateTools.format(newDate, this.format ? this.format : defaultFormat[this.isContainTime ? |
||||||
|
'datetime' : 'date']); |
||||||
|
result.date = newDate; |
||||||
|
} |
||||||
|
} |
||||||
|
this.$emit('confirm', result); |
||||||
|
} |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
BeginTitle() { |
||||||
|
let value = '未选择'; |
||||||
|
if (this.checkeds.length) value = DateTools.format(this.checkeds[0], 'yy/mm/dd'); |
||||||
|
return value; |
||||||
|
}, |
||||||
|
EndTitle() { |
||||||
|
let value = '未选择'; |
||||||
|
if (this.checkeds.length == 2) value = DateTools.format(this.checkeds[1], 'yy/mm/dd'); |
||||||
|
return value; |
||||||
|
}, |
||||||
|
PickerTimeTitle() { |
||||||
|
return DateTools.formatTimeArray(this.timeValue, this.showSeconds); |
||||||
|
}, |
||||||
|
BeginTimeTitle() { |
||||||
|
return this.BeginTitle != '未选择' ? DateTools.formatTimeArray(this.beginTime, this.showSeconds) : ''; |
||||||
|
}, |
||||||
|
EndTimeTitle() { |
||||||
|
return this.EndTitle != '未选择' ? DateTools.formatTimeArray(this.endTime, this.showSeconds) : ''; |
||||||
|
} |
||||||
|
}, |
||||||
|
watch: { |
||||||
|
show(newValue, oldValue) { |
||||||
|
newValue && this.setValue(this.value); |
||||||
|
this.isShow = newValue; |
||||||
|
}, |
||||||
|
value(newValue, oldValue) { |
||||||
|
setTimeout(()=>{ |
||||||
|
this.setValue(newValue); |
||||||
|
}, 0); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
$z-index: 100; |
||||||
|
$cell-spacing: 20upx; |
||||||
|
$calendar-size: 630upx; |
||||||
|
$calendar-item-size: 90upx; |
||||||
|
|
||||||
|
.picker { |
||||||
|
position: fixed; |
||||||
|
z-index: $z-index; |
||||||
|
background: rgba(255, 255, 255, 0); |
||||||
|
left: 0; |
||||||
|
top: 0; |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
font-size: 28upx; |
||||||
|
|
||||||
|
&-btn { |
||||||
|
padding: $cell-spacing*0.5 $cell-spacing; |
||||||
|
border-radius: 12upx; |
||||||
|
color: #666; |
||||||
|
|
||||||
|
&-active { |
||||||
|
background: rgba(0, 0, 0, .1); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
&-display { |
||||||
|
color: #666; |
||||||
|
|
||||||
|
&-text { |
||||||
|
color: #000; |
||||||
|
margin: 0 $cell-spacing*0.5; |
||||||
|
} |
||||||
|
|
||||||
|
&-link { |
||||||
|
display: inline-block; |
||||||
|
|
||||||
|
&-active { |
||||||
|
background: rgba(0, 0, 0, .1); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
&-time { |
||||||
|
width: $calendar-size - 80upx !important; |
||||||
|
left: ((750upx - $calendar-size) / 2 + 40upx) !important; |
||||||
|
} |
||||||
|
|
||||||
|
&-modal { |
||||||
|
background: #fff; |
||||||
|
position: absolute; |
||||||
|
top: 50%; |
||||||
|
left: (750upx - $calendar-size) / 2; |
||||||
|
width: $calendar-size; |
||||||
|
transform: translateY(-50%); |
||||||
|
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.1); |
||||||
|
border-radius: 12upx; |
||||||
|
|
||||||
|
&-header { |
||||||
|
text-align: center; |
||||||
|
line-height: 80upx; |
||||||
|
font-size: 32upx; |
||||||
|
|
||||||
|
&-title { |
||||||
|
display: inline-block; |
||||||
|
width: 40%; |
||||||
|
} |
||||||
|
|
||||||
|
.picker-icon { |
||||||
|
display: inline-block; |
||||||
|
line-height: 50upx; |
||||||
|
width: 50upx; |
||||||
|
height: 50upx; |
||||||
|
border-radius: 50upx; |
||||||
|
text-align: center; |
||||||
|
margin: 10upx; |
||||||
|
background: #fff; |
||||||
|
font-size: 36upx; |
||||||
|
|
||||||
|
&-active { |
||||||
|
background: rgba(0, 0, 0, .1); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
&-body { |
||||||
|
width: $calendar-size !important; |
||||||
|
height: $calendar-size !important; |
||||||
|
position: relative; |
||||||
|
} |
||||||
|
|
||||||
|
&-time { |
||||||
|
width: 100%; |
||||||
|
height: 180upx; |
||||||
|
text-align: center; |
||||||
|
line-height: 60upx; |
||||||
|
} |
||||||
|
|
||||||
|
&-footer { |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
align-items: center; |
||||||
|
padding: $cell-spacing; |
||||||
|
|
||||||
|
&-info { |
||||||
|
flex-grow: 1; |
||||||
|
} |
||||||
|
|
||||||
|
&-btn { |
||||||
|
flex-shrink: 0; |
||||||
|
display: flex; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
&-calendar { |
||||||
|
position: absolute; |
||||||
|
left: 0; |
||||||
|
top: 0; |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
flex-wrap: wrap; |
||||||
|
|
||||||
|
&-view { |
||||||
|
position: relative; |
||||||
|
width: $calendar-item-size; |
||||||
|
height: $calendar-item-size; |
||||||
|
text-align: center; |
||||||
|
|
||||||
|
&-bgbegin, |
||||||
|
&-bg, |
||||||
|
&-bgend, |
||||||
|
&-item, |
||||||
|
&-dot, |
||||||
|
&-tips { |
||||||
|
position: absolute; |
||||||
|
transition: .2s; |
||||||
|
} |
||||||
|
|
||||||
|
&-bgbegin, |
||||||
|
&-bg, |
||||||
|
&-bgend { |
||||||
|
opacity: .15; |
||||||
|
height: 80%; |
||||||
|
} |
||||||
|
|
||||||
|
&-bg { |
||||||
|
left: 0; |
||||||
|
top: 10%; |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
&-bgbegin { |
||||||
|
border-radius: $calendar-item-size 0 0 $calendar-item-size; |
||||||
|
top: 10%; |
||||||
|
left: 10%; |
||||||
|
width: 90%; |
||||||
|
} |
||||||
|
|
||||||
|
&-bgend { |
||||||
|
border-radius: 0 $calendar-item-size $calendar-item-size 0; |
||||||
|
top: 10%; |
||||||
|
left: 0%; |
||||||
|
width: 90%; |
||||||
|
} |
||||||
|
|
||||||
|
&-item { |
||||||
|
left: 5%; |
||||||
|
top: 5%; |
||||||
|
width: 90%; |
||||||
|
height: 90%; |
||||||
|
border-radius: $calendar-item-size; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
justify-content: center; |
||||||
|
} |
||||||
|
|
||||||
|
&-dot { |
||||||
|
right: 10%; |
||||||
|
top: 10%; |
||||||
|
width: 12upx; |
||||||
|
height: 12upx; |
||||||
|
border-radius: 12upx; |
||||||
|
} |
||||||
|
|
||||||
|
&-tips { |
||||||
|
bottom: 100%; |
||||||
|
left: 50%; |
||||||
|
transform: translateX(-50%); |
||||||
|
background: #4E4B46; |
||||||
|
color: #fff; |
||||||
|
border-radius: 12upx; |
||||||
|
padding: 10upx 20upx; |
||||||
|
font-size: 24upx; |
||||||
|
width: max-content; |
||||||
|
margin-bottom: 5px; |
||||||
|
pointer-events: none; |
||||||
|
|
||||||
|
&:after { |
||||||
|
content: ""; |
||||||
|
position: absolute; |
||||||
|
top: 100%; |
||||||
|
left: 50%; |
||||||
|
transform: translateX(-50%); |
||||||
|
width: 0; |
||||||
|
height: 0; |
||||||
|
border-style: solid; |
||||||
|
border-width: 5px 5px 0 5px; |
||||||
|
border-color: #4E4B46 transparent transparent transparent; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@font-face { |
||||||
|
font-family: "mxdatepickericon"; |
||||||
|
src: url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAMYAAsAAAAACBgAAALMAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCDIgqDRIJiATYCJAMUCwwABCAFhG0HSRvfBsg+QCa3noNAyAQ9w6GDvbwpNp2vloCyn8bD/x+y+/5qDhtj+T4eRVEcbsCoKMFASzCgLdDkmqYDwgxkWQ6YH5L/YnppOlLEjlnter43YRjU7M6vJ3iGADVAgJn5kqjv/wEii23T86UsAQT+04fV+o97VTMx4PPZt4DlorLXwIQiGMA5uhaVrBWqGHfQXcTEiE+PE+g2SUlxWlLVBHwUYFMgrgwSB3wstTKSGzqF1nOyiGeeOtNjV4An/vvxR58PSc3AzrMViyDvPo/7dVEUzn5GROfIWAcU4rLXfMFdhte56y4We9gGNEVIezkBOOaQXUrbTf/hJVkhGpDdCw7dSOEzByMEn3kIic98hMxnAfeFPKWCbjRcA148/HxhCEkaA94eGWFaGolsblpaWz8/Po2WVuNHh1fmBpZHIpqal9fOjizhTteY+RZ9rv02I/pq0W6QVH3pSncBz3m55r9ZIPycHfmenvxe4uyutIgfT5u4bgkDusl9gcF0rnfnz+b2NpSaQWBFeu8GIL1xQj5AH/6FAsEr/50F28e/gA9ny6KjLrxIp0TE+UucmQOl5AFNLXkzZufWamWHYEI39PEP2If97CMdm51N6DSmIekwAVmneXTBr0PVYx+aTgfQbU3p+R4jKHdRurBq0oEw6AKSfm+QDbpGF/w3VOP+oBnMHbqdx409FjP4RRHHkAj5IWgQiBUjHfMTuQ1Icpg5avI4sQVRu8EHdWptM1aKrIjuscfeL+kZwxBTYoElztOQ2UygjRIjEphaZsyWodHgvm9SC8QC/JygEA6DiCDeEMhAQFhhOpvxa/18A0TiYMahIy0L2hYIZWeYH9JR085Al4qts1re5St2/SR6DINBGEVYQCWOETHDMAHZ+pcZIQJGTV4RtMmg8UbhuWL1+VLLA2RFHYC71kiRo0SNpjwQh8pj2EFU3oTNmS1WqgIA') format('woff2'); |
||||||
|
} |
||||||
|
|
||||||
|
.picker-icon { |
||||||
|
font-family: "mxdatepickericon" !important; |
||||||
|
} |
||||||
|
|
||||||
|
.picker-icon-you:before { |
||||||
|
content: "\e63e"; |
||||||
|
} |
||||||
|
|
||||||
|
.picker-icon-zuo:before { |
||||||
|
content: "\e640"; |
||||||
|
} |
||||||
|
|
||||||
|
.picker-icon-zuozuo:before { |
||||||
|
content: "\e641"; |
||||||
|
} |
||||||
|
|
||||||
|
.picker-icon-youyou:before { |
||||||
|
content: "\e642"; |
||||||
|
} |
||||||
|
</style> |
File diff suppressed because one or more lines are too long
@ -0,0 +1,237 @@ |
|||||||
|
<template> |
||||||
|
<view> |
||||||
|
<!-- 上传文件 begin --> |
||||||
|
<view class="display_flex"> |
||||||
|
<view class="img_list" v-for="(item,index) in fileList" :key="index"> |
||||||
|
<image v-show="item.ext!=='mp4'" class="img_item" :src="item.url" @click="preview(item.url)" mode="aspectFill" v-if="item.book == 'img'"></image> |
||||||
|
<video object-fit="cover" @play="videoPlay" id="myVideo" @fullscreenchange="fullscreenchange" v-if="item.ext=='mp4'" class="img_item" :src="item.url" controls></video> |
||||||
|
<view :initial-time="0" play-btn-position="center" class="upload-video " @click="prePdf(item.url,item.ext)" v-else> |
||||||
|
<!-- <text class="iconfont iconexcel" style="color:#FF6969;font-size:88rpx"></text> --> |
||||||
|
<image v-show="item.ext == 'pdf'" src="../../static/img/PDF.png" mode="aspectFill" style="width:88rpx;height: 88rpx;"></image> |
||||||
|
<image v-show="item.ext == 'doc' || item.ext == 'docx'" src="../../static/img/word.png" mode="aspectFill" style="width:88rpx;height: 88rpx;"></image> |
||||||
|
<image v-show="item.ext == 'xlsx' || item.ext == 'xls'" src="../../static/img/excel.png" mode="aspectFill" style="width:88rpx;height: 88rpx;"></image> |
||||||
|
</view> |
||||||
|
<view v-if="handle" class="iconfont deleteicon iconshanchu2" @click.stop="delteFile(index)"></view> |
||||||
|
</view> |
||||||
|
<view v-if="handle" class="upload img_list" @click="upload('img')"> |
||||||
|
<view v-if="handle" class="iconfont iconziyuan" style="font-size:40rpx;"></view> |
||||||
|
上传照片 |
||||||
|
</view> |
||||||
|
<view v-if="handle" class="upload img_list" @click="upload('file')"> |
||||||
|
<view v-if="handle" class="iconfont iconshangchuanwenjian fileicon"></view> |
||||||
|
上传文件 |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<!-- 上传文件 end --> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import { |
||||||
|
get, |
||||||
|
post |
||||||
|
} from '../utils/request.js' |
||||||
|
import { |
||||||
|
uploadImage, |
||||||
|
uploadPdf |
||||||
|
} from '../utils/uploadimage.js' |
||||||
|
import { |
||||||
|
previepdf |
||||||
|
} from '../utils/openpdf.js' |
||||||
|
export default { |
||||||
|
name: 'upload', |
||||||
|
model: { |
||||||
|
prop: "showPop", |
||||||
|
event: "change" |
||||||
|
}, |
||||||
|
props: { |
||||||
|
limitnumber: { |
||||||
|
type: Number, |
||||||
|
default: 100 |
||||||
|
}, |
||||||
|
// 展示的文件 |
||||||
|
files:{ |
||||||
|
type:Array, |
||||||
|
default:[] |
||||||
|
}, |
||||||
|
// 是否可操作 |
||||||
|
handle:{ |
||||||
|
type:Boolean, |
||||||
|
default:true |
||||||
|
} |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
fileList: [] |
||||||
|
} |
||||||
|
}, |
||||||
|
watch:{ |
||||||
|
files(val){ |
||||||
|
if(val&&val.length!==0){ |
||||||
|
this.fileList = val |
||||||
|
// 替傻屌后端擦屁股,返回特么的ul和“” |
||||||
|
if(val[0].url==''||val[0].url=='ul'){ |
||||||
|
this.fileList.splice(0,1) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
methods: { |
||||||
|
/** |
||||||
|
* @description 上传文件 |
||||||
|
*/ |
||||||
|
upload(book) { |
||||||
|
if (this.fileList.length == this.$props.limitnumber) { |
||||||
|
uni.showToast({ |
||||||
|
title: '文件上传已上限', |
||||||
|
icon: 'none' |
||||||
|
}) |
||||||
|
return |
||||||
|
} |
||||||
|
if (book == 'img') { |
||||||
|
uploadImage().then(res => { |
||||||
|
this.fileList.push({ |
||||||
|
url: res.imageurl, |
||||||
|
ext: res.ext, |
||||||
|
book: 'img' |
||||||
|
}) |
||||||
|
this.$emit('upload',this.fileList) |
||||||
|
}) |
||||||
|
} else { |
||||||
|
uploadPdf().then(res => { |
||||||
|
this.fileList.push({ |
||||||
|
url: res.pdfurl, |
||||||
|
ext: res.ext, |
||||||
|
book: 'file' |
||||||
|
}) |
||||||
|
this.$emit('upload',this.fileList) |
||||||
|
}) |
||||||
|
} |
||||||
|
}, |
||||||
|
// 上传视频 |
||||||
|
test() { |
||||||
|
var self = this; |
||||||
|
uni.chooseMedia({ |
||||||
|
count: 9, |
||||||
|
mediaType: ['image','video'], |
||||||
|
sourceType: ['album', 'camera'], |
||||||
|
maxDuration: 30, |
||||||
|
camera: 'back', |
||||||
|
success(res) { |
||||||
|
console.log(res.tempFilest,res,'选择图片或者视频') |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
videoPlay(e){ |
||||||
|
console.log(e,'播放触发') |
||||||
|
this.videoContext = uni.createVideoContext('myVideo',this) |
||||||
|
this.videoContext.requestFullScreen(); |
||||||
|
}, |
||||||
|
// 退出全屏播放时关闭生成的全屏视频 |
||||||
|
fullscreenchange (e){ |
||||||
|
console.log(e,'看下退出') |
||||||
|
if(!e.detail.fullScreen){ |
||||||
|
this.videoContext.stop() |
||||||
|
e.timeStamp = 0 // 时间错归零 |
||||||
|
} |
||||||
|
}, |
||||||
|
/** |
||||||
|
* @description 预览图片 |
||||||
|
*/ |
||||||
|
preview(url) { |
||||||
|
uni.previewImage({ |
||||||
|
urls: [url], |
||||||
|
current: url |
||||||
|
}) |
||||||
|
}, |
||||||
|
/** |
||||||
|
* @description 预览pdf |
||||||
|
*/ |
||||||
|
|
||||||
|
prePdf(e,ext) { |
||||||
|
previepdf(e,ext) |
||||||
|
}, |
||||||
|
/** |
||||||
|
* @description 删除上传的文件 |
||||||
|
*/ |
||||||
|
delteFile(index) { |
||||||
|
this.fileList.splice(index, 1) |
||||||
|
this.$emit('upload',this.fileList) |
||||||
|
}, |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style> |
||||||
|
@import "./pretty-uploadFile.css"; |
||||||
|
|
||||||
|
.display_flex { |
||||||
|
display: flex; |
||||||
|
flex-wrap: wrap; |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.fileicon { |
||||||
|
font-size: 50rpx; |
||||||
|
} |
||||||
|
|
||||||
|
/* 上传图片 */ |
||||||
|
.img_list { |
||||||
|
width: 140rpx; |
||||||
|
height: 140rpx; |
||||||
|
border-radius: 10rpx; |
||||||
|
margin: 20rpx 10rpx; |
||||||
|
position: relative; |
||||||
|
} |
||||||
|
|
||||||
|
.img>image { |
||||||
|
width: 140rpx; |
||||||
|
height: 140rpx; |
||||||
|
border-radius: 10rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.img_item { |
||||||
|
width: 140rpx; |
||||||
|
height: 140rpx; |
||||||
|
border-radius: 10rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.upload { |
||||||
|
height: 140rpx; |
||||||
|
width: 140rpx; |
||||||
|
background-color: #f7f7f7; |
||||||
|
border: 2rpx dashed rgb(224, 223, 223); |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
justify-content: center; |
||||||
|
align-items: center; |
||||||
|
font-size: 28rpx; |
||||||
|
color: #aaa; |
||||||
|
word-break: break-all; |
||||||
|
overflow: hidden; |
||||||
|
} |
||||||
|
.upload-video { |
||||||
|
height: 140rpx; |
||||||
|
width: 140rpx; |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
justify-content: center; |
||||||
|
align-items: center; |
||||||
|
font-size: 28rpx; |
||||||
|
color: #aaa; |
||||||
|
word-break: break-all; |
||||||
|
overflow: hidden; |
||||||
|
} |
||||||
|
|
||||||
|
.deleteicon { |
||||||
|
position: absolute; |
||||||
|
font-size: 40rpx; |
||||||
|
color: #F93313; |
||||||
|
top: -20rpx; |
||||||
|
right: -12rpx; |
||||||
|
background-color: #fff; |
||||||
|
border-radius: 50%; |
||||||
|
z-index: 999; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,133 @@ |
|||||||
|
<template> |
||||||
|
<view class="upload"> |
||||||
|
<view class="list"> |
||||||
|
<view class="item interval" v-for="(item,index) in fileList" :key="index"> |
||||||
|
<image :src="item" v-if="item"></image> |
||||||
|
<view class="icon-close" @click.stop="handleRemove(index)"> |
||||||
|
<uni-icon type="closeempty" size="20" color="#fff"></uni-icon> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="item" @click="chooseImage" v-if="fileList.length<limit"> |
||||||
|
<uni-icon type="image" size="30" color="#cccccd"></uni-icon> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
props: { |
||||||
|
limit: { |
||||||
|
type: Number, |
||||||
|
default: 5 |
||||||
|
}, |
||||||
|
url: { |
||||||
|
type: String, |
||||||
|
required: true |
||||||
|
}, |
||||||
|
name: { |
||||||
|
type: String, |
||||||
|
default: 'file' |
||||||
|
}, |
||||||
|
formData: { |
||||||
|
type: Object, |
||||||
|
default () { |
||||||
|
return {} |
||||||
|
} |
||||||
|
}, |
||||||
|
header: { |
||||||
|
type: Object, |
||||||
|
default () { |
||||||
|
return {} |
||||||
|
} |
||||||
|
}, |
||||||
|
fileList: { |
||||||
|
type: Array, |
||||||
|
default () { |
||||||
|
return [] |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return {} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
chooseImage() { |
||||||
|
uni.chooseImage({ |
||||||
|
success: (chooseImageRes) => { |
||||||
|
const uploadTask = uni.uploadFile({ |
||||||
|
url: 'https://www.huorantech.cn/api-guarantee/dg-apply-amount-info/uploadFile', |
||||||
|
name: 'file', |
||||||
|
filePath: chooseImageRes.tempFilePaths[0], |
||||||
|
header: { |
||||||
|
"Content-Type": "multipart/form-data", |
||||||
|
"token":"eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxIiwic3ViIjoidG9rZW4iLCJpc3MiOiJBZG1pbiIsImlhdCI6MTYxMTcxMjUwNiwiZXhwIjoxNjExNzk4OTA2fQ.Hp6r0Abux__NoAp-8ssmcz4FPdiW_BRdNoXX5UDw_1w" |
||||||
|
}, |
||||||
|
timeOut:30000, |
||||||
|
success: (uploadFileRes) => { |
||||||
|
console.log(uploadFileRes) |
||||||
|
this.$emit('on-success', JSON.parse(uploadFileRes.data)) |
||||||
|
}, |
||||||
|
fail: (err) => { |
||||||
|
this.$emit('on-error', err) |
||||||
|
} |
||||||
|
}) |
||||||
|
uploadTask.onProgressUpdate((res) => { |
||||||
|
this.$emit('on-process', res) |
||||||
|
}) |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
handleRemove(index) { |
||||||
|
this.$emit('on-remove', index) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.upload { |
||||||
|
.list { |
||||||
|
display: flex; |
||||||
|
flex-wrap: wrap; |
||||||
|
.item { |
||||||
|
position: relative; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
justify-content: center; |
||||||
|
width: 140rpx; |
||||||
|
height: 140rpx; |
||||||
|
border-radius: 8rpx; |
||||||
|
border: 2rpx solid #D9D9D9; |
||||||
|
background-color: #f3f3f3; |
||||||
|
margin-bottom: 20rpx; |
||||||
|
&.interval { |
||||||
|
margin-right: 16rpx; |
||||||
|
border: none; |
||||||
|
} |
||||||
|
image { |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
border-radius: 8rpx; |
||||||
|
margin: 0; |
||||||
|
} |
||||||
|
.icon-close { |
||||||
|
position: absolute; |
||||||
|
top: 0; |
||||||
|
right: 0; |
||||||
|
width: 28rpx; |
||||||
|
height: 28rpx; |
||||||
|
background-color: red; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
justify-content: center; |
||||||
|
border-radius: 0 8rpx 0 0; |
||||||
|
} |
||||||
|
.icon { |
||||||
|
width: 48rpx; |
||||||
|
height: 48rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,134 @@ |
|||||||
|
<template> |
||||||
|
<!-- 这是底部流程图 --> |
||||||
|
<view> |
||||||
|
<!-- <view class=""> |
||||||
|
<text style="width:120rpx;display: block;" class="tx-center">抄送人</text> |
||||||
|
<button class="margin-top cu-btn round lines-grey line-grey shadow" v-for="(item,index) in checkedData" :key="index">{{item.name}}</button> |
||||||
|
</view> --> |
||||||
|
<view class="cu-timeline"> |
||||||
|
<view class="cu-time" style="color: #0081ff;">审核流程进程</view> |
||||||
|
<view class="cu-item text-blue" v-for="(item,index) in fromData" :key="index"> |
||||||
|
<view class="content"> |
||||||
|
<view class="content radius10 pdlr10"> |
||||||
|
<text class="mgr10">部门:</text> |
||||||
|
<text>{{item.deptName||''}}</text> |
||||||
|
</view> |
||||||
|
<view class="content radius10 pdlr10"> |
||||||
|
<text class="mgr10">时间:</text> |
||||||
|
<text>{{item.createTime||''}}</text> |
||||||
|
</view> |
||||||
|
<view class="content radius10 pdlr10"> |
||||||
|
<text class="mgr10">审批人:</text> |
||||||
|
<text>{{item.approver||''}}</text> |
||||||
|
</view> |
||||||
|
<view class="content radius10 pdlr10"> |
||||||
|
<text class="mgr10">审批意见:</text> |
||||||
|
<text>{{item.auditOpinion||''}}</text> |
||||||
|
</view> |
||||||
|
<view class="content radius10 pdlr10"> |
||||||
|
<text class="mgr10">原因:</text> |
||||||
|
<text>{{item.reason||''}}</text> |
||||||
|
</view> |
||||||
|
<view class="content radius10 pdlr10"> |
||||||
|
<text class="mgr10">金额(万元):</text> |
||||||
|
<text>{{item.loanMoney||''}}</text> |
||||||
|
</view> |
||||||
|
<view class="content radius10 pdlr10"> |
||||||
|
<text class="mgr10">期限:</text> |
||||||
|
<text>{{item.loanTern||''}}</text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="cu-timeline mat15 pdb10"> |
||||||
|
<view class="cu-time" style="color: #1cbbb4;">业务流程进程</view> |
||||||
|
<view class="cu-item text-blue" v-for="item,index in fromData1" :key="index"> |
||||||
|
<view class="content"> |
||||||
|
<view class=" content radius10 pdlr10"> |
||||||
|
<text class="mgr10">部门:</text> |
||||||
|
<text>{{item.deptName||''}}</text> |
||||||
|
</view> |
||||||
|
<view class=" content radius10 pdlr10"> |
||||||
|
<text class="mgr10">时间:</text> |
||||||
|
<text>{{item.createTime||''}}</text> |
||||||
|
</view> |
||||||
|
<view class=" content radius10 pdlr10"> |
||||||
|
<text class="mgr10">审批人:</text> |
||||||
|
<text>{{item.approver||''}}</text> |
||||||
|
</view> |
||||||
|
<view class=" content radius10 pdlr10"> |
||||||
|
<text class="mgr10">审批意见:</text> |
||||||
|
<text>{{item.auditOpinion||''}}</text> |
||||||
|
</view> |
||||||
|
<view class=" content radius10 pdlr10"> |
||||||
|
<text class="mgr10">原因:</text> |
||||||
|
<text>{{item.reason||''}}</text> |
||||||
|
</view> |
||||||
|
<view class=" content radius10 pdlr10"> |
||||||
|
<text class="mgr10">金额(万元):</text> |
||||||
|
<text>{{item.loanMoney||''}}</text> |
||||||
|
</view> |
||||||
|
<view class=" content radius10 pdlr10"> |
||||||
|
<text class="mgr10">期限:</text> |
||||||
|
<text>{{item.loanTern||''}}</text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default{ |
||||||
|
data(){ |
||||||
|
return{ |
||||||
|
fromData:[],// 上的值 |
||||||
|
fromData1:[],// 下的值 |
||||||
|
processId:'',// 当前模块值 |
||||||
|
busnissID:'',//业务的id |
||||||
|
checkedData:[],//抄送的值 |
||||||
|
} |
||||||
|
}, |
||||||
|
created() { |
||||||
|
this.processId = JSON.parse(uni.getStorageSync('processId')) |
||||||
|
this.busnissID = uni.getStorageSync('businessId') |
||||||
|
this.listOne()// 第一个表单 |
||||||
|
this.listTwo()// 第二个表单 |
||||||
|
}, |
||||||
|
mounted() { |
||||||
|
|
||||||
|
}, |
||||||
|
methods:{ |
||||||
|
listOne(){ |
||||||
|
let params = { |
||||||
|
businessId: this.busnissID, |
||||||
|
processId: this.processId |
||||||
|
} |
||||||
|
this.$http.get('/api-guarantee/dg-audit-process/auditProcessList',params).then(res=>{ |
||||||
|
console.log(res,'取得业务流程进程') |
||||||
|
if(res.data) this.fromData = res.data |
||||||
|
console.log(res,'当前的底部数据') |
||||||
|
}).catch(err=>{console.log(err)}) |
||||||
|
}, |
||||||
|
listTwo(){ |
||||||
|
let params = { |
||||||
|
businessId: this.busnissID, |
||||||
|
processId: '' |
||||||
|
} |
||||||
|
this.$http.get('/api-guarantee/dg-audit-process/auditProcessList',params).then(res=>{ |
||||||
|
if(res.data) this.fromData1 = res.data |
||||||
|
}) |
||||||
|
}, |
||||||
|
|
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style scoped lang="scss"> |
||||||
|
/deep/ .cu-timeline>.cu-item{ |
||||||
|
padding-top: 15rpx; |
||||||
|
padding-bottom: 15rpx; |
||||||
|
} |
||||||
|
// /deep/ |
||||||
|
</style> |
@ -0,0 +1,161 @@ |
|||||||
|
<!-- 商品列表组件 <good-list :list="xx"></good-list> --> |
||||||
|
<template> |
||||||
|
<view class="cu-list menu-avatar"> |
||||||
|
<view class="cu-item" :class="modalName=='move-box-'+ index?'move-cur':''" v-for="(item,index) in list" :key="index" |
||||||
|
@touchstart="ListTouchStart" @touchmove="ListTouchMove" @touchend="ListTouchEnd" :data-target="'move-box-' + index"> |
||||||
|
<view class="good-list"> |
||||||
|
<view class="charge" :id="'index-' + item.name" :data-index="item.name"> |
||||||
|
<view class="charge-title flex-between"> |
||||||
|
<text>{{item.name}}的贷款申请</text> |
||||||
|
</view> |
||||||
|
<view class="charge-text"> |
||||||
|
<view> |
||||||
|
<text>业务编号:</text> |
||||||
|
<text class="mgl30">{{item.IDNumber}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>所在部门:</text> |
||||||
|
<text class="mgl30">{{item.department}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>客户名称:</text> |
||||||
|
<text class="mgl30">{{item.cusName}}</text> |
||||||
|
</view> |
||||||
|
<view class="flex-between"> |
||||||
|
<text class="time-text">{{item.time}}</text> |
||||||
|
<text class="status-text" :style="'background-color:'+item.status.bgColor+';color:'+item.status.textColor">{{item.status.text}}</text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="move foot-btn"> |
||||||
|
<button class="mini-btn round def-btn mar-lr" type="primary" size="mini" @click="goto('/guaranteePages/pages/investigationSee/investigationSee')">查看</button> |
||||||
|
<button class="mini-btn round suc-btn" type="primary" size="mini" @click="goto('/guaranteePages/pages/assignAB/assignAB')">指派</button> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
props:{ |
||||||
|
list: { |
||||||
|
type: Array, |
||||||
|
default(){ |
||||||
|
return [] |
||||||
|
} |
||||||
|
}, |
||||||
|
listTouchStart: { |
||||||
|
type: Number, |
||||||
|
default(){ |
||||||
|
return 0 |
||||||
|
} |
||||||
|
}, |
||||||
|
listTouchDirection: { |
||||||
|
type: String, |
||||||
|
default(){ |
||||||
|
return '' |
||||||
|
} |
||||||
|
}, |
||||||
|
modalName: { |
||||||
|
type: String, |
||||||
|
default(){ |
||||||
|
return '' |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
// ListTouch触摸开始 |
||||||
|
ListTouchStart(e) { |
||||||
|
// this.listTouchStart = e.touches[0].pageX |
||||||
|
this.$emit('TouchStart', e); |
||||||
|
}, |
||||||
|
|
||||||
|
// ListTouch计算方向 |
||||||
|
ListTouchMove(e) { |
||||||
|
// this.listTouchDirection = e.touches[0].pageX - this.listTouchStart > 0 ? 'right' : 'left' |
||||||
|
this.$emit('TouchMove', e); |
||||||
|
}, |
||||||
|
|
||||||
|
// ListTouch计算滚动 |
||||||
|
ListTouchEnd(e) { |
||||||
|
this.$emit('TouchEnd', e); |
||||||
|
// if (this.listTouchDirection == 'left') { |
||||||
|
// this.modalName = e.currentTarget.dataset.target |
||||||
|
// } else { |
||||||
|
// this.modalName = null |
||||||
|
// } |
||||||
|
// this.listTouchDirection = null |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.good-list{ |
||||||
|
background-color: #fff; |
||||||
|
|
||||||
|
.good-li{ |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
padding: 20upx; |
||||||
|
border-bottom: 1upx solid #eee; |
||||||
|
|
||||||
|
.good-img{ |
||||||
|
width: 160upx; |
||||||
|
height: 160upx; |
||||||
|
margin-right: 20rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.flex-item{ |
||||||
|
flex: 1; |
||||||
|
|
||||||
|
.good-name{ |
||||||
|
font-size: 26upx; |
||||||
|
line-height: 40upx; |
||||||
|
height: 80upx; |
||||||
|
margin-bottom: 20upx; |
||||||
|
overflow: hidden; |
||||||
|
} |
||||||
|
.good-price{ |
||||||
|
font-size: 26upx; |
||||||
|
color: red; |
||||||
|
} |
||||||
|
.good-sold{ |
||||||
|
font-size: 24upx; |
||||||
|
margin-left: 16upx; |
||||||
|
color: gray; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
.charge{ |
||||||
|
margin: 0 50rpx; |
||||||
|
border-radius: 20rpx; |
||||||
|
padding: 0 30rpx; |
||||||
|
box-shadow: 0 0 16rpx #ccc; |
||||||
|
margin-top: 40rpx; |
||||||
|
.charge-title{ |
||||||
|
padding: 16rpx 0; |
||||||
|
border-bottom: 4rpx solid #F2F2F2; |
||||||
|
color: #000; |
||||||
|
font-weight: bold; |
||||||
|
.charge-status{ |
||||||
|
color: #ccc; |
||||||
|
} |
||||||
|
} |
||||||
|
.charge-text{ |
||||||
|
padding: 16rpx 0; |
||||||
|
color: #707070; |
||||||
|
.time-text{ |
||||||
|
color: #ccc; |
||||||
|
} |
||||||
|
.status-text{ |
||||||
|
font-size: 28rpx; |
||||||
|
padding: 8rpx 12rpx; |
||||||
|
border-radius: 10rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,202 @@ |
|||||||
|
<template> |
||||||
|
<view class="uni-combox"> |
||||||
|
<view v-if="label" class="uni-combox__label" :style="labelStyle"> |
||||||
|
<text>{{label}}</text> |
||||||
|
</view> |
||||||
|
<view class="uni-combox__input-box"> |
||||||
|
<input class="uni-combox__input" type="text" :placeholder="placeholder" v-model="inputVal" @input="onInput" |
||||||
|
@focus="onFocus" @blur="onBlur" /> |
||||||
|
<uni-icons class="uni-combox__input-arrow" type="arrowdown" size="14" @click="toggleSelector"></uni-icons> |
||||||
|
<view class="uni-combox__selector" v-if="showSelector"> |
||||||
|
<scroll-view scroll-y="true" class="uni-combox__selector-scroll"> |
||||||
|
<view class="uni-combox__selector-empty" v-if="filterCandidatesLength === 0"> |
||||||
|
<text>{{emptyTips}}</text> |
||||||
|
</view> |
||||||
|
<view class="uni-combox__selector-item" v-for="(item,index) in filterCandidates" :key="index" @click="onSelectorClick(index)"> |
||||||
|
<text>{{item}}</text> |
||||||
|
</view> |
||||||
|
</scroll-view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import uniIcons from '../uni-icons/uni-icons.vue' |
||||||
|
export default { |
||||||
|
name: 'uniCombox', |
||||||
|
components: { |
||||||
|
uniIcons |
||||||
|
}, |
||||||
|
props: { |
||||||
|
label: { |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
labelWidth: { |
||||||
|
type: String, |
||||||
|
default: 'auto' |
||||||
|
}, |
||||||
|
placeholder: { |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
candidates: { |
||||||
|
type: Array, |
||||||
|
default () { |
||||||
|
return [] |
||||||
|
} |
||||||
|
}, |
||||||
|
emptyTips: { |
||||||
|
type: String, |
||||||
|
default: '无匹配项' |
||||||
|
}, |
||||||
|
value: { |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
} |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
showSelector: false, |
||||||
|
inputVal: '' |
||||||
|
} |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
labelStyle() { |
||||||
|
if (this.labelWidth === 'auto') { |
||||||
|
return {} |
||||||
|
} |
||||||
|
return { |
||||||
|
width: this.labelWidth |
||||||
|
} |
||||||
|
}, |
||||||
|
filterCandidates() { |
||||||
|
return this.candidates.filter((item) => { |
||||||
|
return item.indexOf(this.inputVal) > -1 |
||||||
|
}) |
||||||
|
}, |
||||||
|
filterCandidatesLength() { |
||||||
|
return this.filterCandidates.length |
||||||
|
} |
||||||
|
}, |
||||||
|
watch: { |
||||||
|
value: { |
||||||
|
handler(newVal) { |
||||||
|
this.inputVal = newVal |
||||||
|
}, |
||||||
|
immediate: true |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
toggleSelector() { |
||||||
|
this.showSelector = !this.showSelector |
||||||
|
}, |
||||||
|
onFocus() { |
||||||
|
this.showSelector = true |
||||||
|
}, |
||||||
|
onBlur() { |
||||||
|
setTimeout(() => { |
||||||
|
this.showSelector = false |
||||||
|
},50) |
||||||
|
}, |
||||||
|
onSelectorClick(index) { |
||||||
|
this.inputVal = this.filterCandidates[index] |
||||||
|
this.showSelector = false |
||||||
|
this.$emit('input', this.inputVal) |
||||||
|
}, |
||||||
|
onInput() { |
||||||
|
setTimeout(() => { |
||||||
|
this.$emit('input', this.inputVal) |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.uni-combox { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
height: 80rpx; |
||||||
|
flex-direction: row; |
||||||
|
align-items: center; |
||||||
|
// border-bottom: solid 2rpx #DDDDDD; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-combox__label { |
||||||
|
font-size: 32rpx; |
||||||
|
line-height: 44rpx; |
||||||
|
padding-right: 20rpx; |
||||||
|
color: #999999; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-combox__input-box { |
||||||
|
position: relative; |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
flex: 1; |
||||||
|
flex-direction: row; |
||||||
|
align-items: center; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-combox__input { |
||||||
|
flex: 1; |
||||||
|
font-size: 32rpx; |
||||||
|
height: 44rpx; |
||||||
|
line-height: 44rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-combox__input-arrow { |
||||||
|
padding: 20rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-combox__selector { |
||||||
|
box-sizing: border-box; |
||||||
|
position: absolute; |
||||||
|
top: 84rpx; |
||||||
|
left: 0; |
||||||
|
width: 100%; |
||||||
|
background-color: #FFFFFF; |
||||||
|
border-radius: 12rpx; |
||||||
|
box-shadow: #DDDDDD 8rpx 8rpx 16rpx, #DDDDDD -8rpx -8rpx 16rpx; |
||||||
|
z-index: 2; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-combox__selector-scroll { |
||||||
|
max-height: 400rpx; |
||||||
|
box-sizing: border-box; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-combox__selector::before { |
||||||
|
content: ''; |
||||||
|
position: absolute; |
||||||
|
width: 0; |
||||||
|
height: 0; |
||||||
|
border-bottom: solid 12rpx #FFFFFF; |
||||||
|
border-right: solid 12rpx transparent; |
||||||
|
border-left: solid 12rpx transparent; |
||||||
|
left: 50%; |
||||||
|
top: -12rpx; |
||||||
|
margin-left: -12rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-combox__selector-empty, |
||||||
|
.uni-combox__selector-item { |
||||||
|
/* #ifdef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
line-height: 72rpx; |
||||||
|
font-size: 28rpx; |
||||||
|
text-align: center; |
||||||
|
border-bottom: solid 2rpx #DDDDDD; |
||||||
|
margin: 0rpx 20rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-combox__selector-empty:last-child, |
||||||
|
.uni-combox__selector-item:last-child { |
||||||
|
border-bottom: none; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,316 @@ |
|||||||
|
|
||||||
|
const events = { |
||||||
|
load: 'load', |
||||||
|
error: 'error' |
||||||
|
} |
||||||
|
const pageMode = { |
||||||
|
add: 'add', |
||||||
|
replace: 'replace' |
||||||
|
} |
||||||
|
|
||||||
|
const attrs = [ |
||||||
|
'pageCurrent', |
||||||
|
'pageSize', |
||||||
|
'collection', |
||||||
|
'action', |
||||||
|
'field', |
||||||
|
'getcount', |
||||||
|
'orderby', |
||||||
|
'where' |
||||||
|
] |
||||||
|
|
||||||
|
export default { |
||||||
|
data() { |
||||||
|
return { |
||||||
|
loading: false, |
||||||
|
listData: this.getone ? {} : [], |
||||||
|
paginationInternal: { |
||||||
|
current: this.pageCurrent, |
||||||
|
size: this.pageSize, |
||||||
|
count: 0 |
||||||
|
}, |
||||||
|
errorMessage: '' |
||||||
|
} |
||||||
|
}, |
||||||
|
created() { |
||||||
|
let db = null; |
||||||
|
let dbCmd = null; |
||||||
|
|
||||||
|
if(this.collection){ |
||||||
|
this.db = uniCloud.database(); |
||||||
|
this.dbCmd = this.db.command; |
||||||
|
} |
||||||
|
|
||||||
|
this._isEnded = false |
||||||
|
|
||||||
|
this.$watch(() => { |
||||||
|
var al = [] |
||||||
|
attrs.forEach(key => { |
||||||
|
al.push(this[key]) |
||||||
|
}) |
||||||
|
return al |
||||||
|
}, (newValue, oldValue) => { |
||||||
|
this.paginationInternal.pageSize = this.pageSize |
||||||
|
|
||||||
|
let needReset = false |
||||||
|
for (let i = 2; i < newValue.length; i++) { |
||||||
|
if (newValue[i] != oldValue[i]) { |
||||||
|
needReset = true |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
if (needReset) { |
||||||
|
this.clear() |
||||||
|
this.reset() |
||||||
|
} |
||||||
|
if (newValue[0] != oldValue[0]) { |
||||||
|
this.paginationInternal.current = this.pageCurrent |
||||||
|
} |
||||||
|
|
||||||
|
this._execLoadData() |
||||||
|
}) |
||||||
|
|
||||||
|
// #ifdef H5
|
||||||
|
if (process.env.NODE_ENV === 'development') { |
||||||
|
this._debugDataList = [] |
||||||
|
if (!window.unidev) { |
||||||
|
window.unidev = { |
||||||
|
clientDB: { |
||||||
|
data: [] |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
unidev.clientDB.data.push(this._debugDataList) |
||||||
|
} |
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifdef MP-TOUTIAO
|
||||||
|
let changeName |
||||||
|
let events = this.$scope.dataset.eventOpts |
||||||
|
for (var i = 0; i < events.length; i++) { |
||||||
|
let event = events[i] |
||||||
|
if (event[0].includes('^load')) { |
||||||
|
changeName = event[1][0][0] |
||||||
|
} |
||||||
|
} |
||||||
|
if (changeName) { |
||||||
|
let parent = this.$parent |
||||||
|
let maxDepth = 16 |
||||||
|
this._changeDataFunction = null |
||||||
|
while (parent && maxDepth > 0) { |
||||||
|
let fun = parent[changeName] |
||||||
|
if (fun && typeof fun === 'function') { |
||||||
|
this._changeDataFunction = fun |
||||||
|
maxDepth = 0 |
||||||
|
break |
||||||
|
} |
||||||
|
parent = parent.$parent |
||||||
|
maxDepth--; |
||||||
|
} |
||||||
|
} |
||||||
|
// #endif
|
||||||
|
|
||||||
|
// if (!this.manual) {
|
||||||
|
// this.loadData()
|
||||||
|
// }
|
||||||
|
}, |
||||||
|
// #ifdef H5
|
||||||
|
beforeDestroy() { |
||||||
|
if (process.env.NODE_ENV === 'development' && window.unidev) { |
||||||
|
var cd = this._debugDataList |
||||||
|
var dl = unidev.clientDB.data |
||||||
|
for (var i = dl.length - 1; i >= 0; i--) { |
||||||
|
if (dl[i] === cd) { |
||||||
|
dl.splice(i, 1) |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
// #endif
|
||||||
|
methods: { |
||||||
|
loadData(args1, args2) { |
||||||
|
let callback = null |
||||||
|
if (typeof args1 === 'object') { |
||||||
|
if (args1.clear) { |
||||||
|
this.clear() |
||||||
|
this.reset() |
||||||
|
} |
||||||
|
if (args1.current !== undefined) { |
||||||
|
this.paginationInternal.current = args1.current |
||||||
|
} |
||||||
|
if (typeof args2 === 'function') { |
||||||
|
callback = args2 |
||||||
|
} |
||||||
|
} else if (typeof args1 === 'function') { |
||||||
|
callback = args1 |
||||||
|
} |
||||||
|
|
||||||
|
this._execLoadData(callback) |
||||||
|
}, |
||||||
|
loadMore() { |
||||||
|
if (this._isEnded) { |
||||||
|
return |
||||||
|
} |
||||||
|
this._execLoadData() |
||||||
|
}, |
||||||
|
refresh() { |
||||||
|
this.clear() |
||||||
|
this._execLoadData() |
||||||
|
}, |
||||||
|
clear() { |
||||||
|
this._isEnded = false |
||||||
|
this.listData = [] |
||||||
|
}, |
||||||
|
reset() { |
||||||
|
this.paginationInternal.current = 1 |
||||||
|
}, |
||||||
|
remove(id, { |
||||||
|
action, |
||||||
|
callback, |
||||||
|
confirmTitle, |
||||||
|
confirmContent |
||||||
|
} = {}) { |
||||||
|
if (!id || !id.length) { |
||||||
|
return |
||||||
|
} |
||||||
|
uni.showModal({ |
||||||
|
title: confirmTitle || '提示', |
||||||
|
content: confirmContent || '是否删除该数据', |
||||||
|
showCancel: true, |
||||||
|
success: (res) => { |
||||||
|
if (!res.confirm) { |
||||||
|
return |
||||||
|
} |
||||||
|
this._execRemove(id, action, callback) |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
_execLoadData(callback) { |
||||||
|
if (this.loading) { |
||||||
|
return |
||||||
|
} |
||||||
|
this.loading = true |
||||||
|
this.errorMessage = '' |
||||||
|
|
||||||
|
this._getExec().then((res) => { |
||||||
|
this.loading = false |
||||||
|
const { |
||||||
|
data, |
||||||
|
count |
||||||
|
} = res.result |
||||||
|
this._isEnded = data.length < this.pageSize |
||||||
|
|
||||||
|
callback && callback(data, this._isEnded) |
||||||
|
this._dispatchEvent(events.load, data) |
||||||
|
|
||||||
|
if (this.getone) { |
||||||
|
this.listData = data.length ? data[0] : undefined |
||||||
|
} else if (this.pageData === pageMode.add) { |
||||||
|
this.listData.push(...data) |
||||||
|
if (this.listData.length) { |
||||||
|
this.paginationInternal.current++ |
||||||
|
} |
||||||
|
} else if (this.pageData === pageMode.replace) { |
||||||
|
this.listData = data |
||||||
|
this.paginationInternal.count = count |
||||||
|
} |
||||||
|
|
||||||
|
// #ifdef H5
|
||||||
|
if (process.env.NODE_ENV === 'development') { |
||||||
|
this._debugDataList.length = 0 |
||||||
|
this._debugDataList.push(...JSON.parse(JSON.stringify(this.listData))) |
||||||
|
} |
||||||
|
// #endif
|
||||||
|
}).catch((err) => { |
||||||
|
this.loading = false |
||||||
|
this.errorMessage = err |
||||||
|
callback && callback() |
||||||
|
this.$emit(events.error, err) |
||||||
|
}) |
||||||
|
}, |
||||||
|
_getExec() { |
||||||
|
let exec = this.db |
||||||
|
if (this.action) { |
||||||
|
exec = exec.action(this.action) |
||||||
|
} |
||||||
|
|
||||||
|
exec = exec.collection(this.collection) |
||||||
|
|
||||||
|
if (!(!this.where || !Object.keys(this.where).length)) { |
||||||
|
exec = exec.where(this.where) |
||||||
|
} |
||||||
|
if (this.field) { |
||||||
|
exec = exec.field(this.field) |
||||||
|
} |
||||||
|
if (this.orderby) { |
||||||
|
exec = exec.orderBy(this.orderby) |
||||||
|
} |
||||||
|
|
||||||
|
const { |
||||||
|
current, |
||||||
|
size |
||||||
|
} = this.paginationInternal |
||||||
|
exec = exec.skip(size * (current - 1)).limit(size).get({ |
||||||
|
getCount: this.getcount |
||||||
|
}) |
||||||
|
|
||||||
|
return exec |
||||||
|
}, |
||||||
|
_execRemove(id, action, callback) { |
||||||
|
if (!this.collection || !id) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
const ids = Array.isArray(id) ? id : [id] |
||||||
|
if (!ids.length) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
uni.showLoading({ |
||||||
|
mask: true |
||||||
|
}) |
||||||
|
|
||||||
|
let exec = this.db |
||||||
|
if (action) { |
||||||
|
exec = exec.action(action) |
||||||
|
} |
||||||
|
|
||||||
|
exec.collection(this.collection).where({ |
||||||
|
_id: dbCmd.in(ids) |
||||||
|
}).remove().then((res) => { |
||||||
|
callback && callback(res.result) |
||||||
|
if (this.pageData === pageMode.replace) { |
||||||
|
this.refresh() |
||||||
|
} else { |
||||||
|
this.removeData(ids) |
||||||
|
} |
||||||
|
}).catch((err) => { |
||||||
|
uni.showModal({ |
||||||
|
content: err.message, |
||||||
|
showCancel: false |
||||||
|
}) |
||||||
|
}).finally(() => { |
||||||
|
uni.hideLoading() |
||||||
|
}) |
||||||
|
}, |
||||||
|
removeData(ids) { |
||||||
|
let il = ids.slice(0) |
||||||
|
let dl = this.listData |
||||||
|
for (let i = dl.length - 1; i >= 0; i--) { |
||||||
|
let index = il.indexOf(dl[i]._id) |
||||||
|
if (index >= 0) { |
||||||
|
dl.splice(i, 1) |
||||||
|
il.splice(index, 1) |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
_dispatchEvent(type, data) { |
||||||
|
if (this._changeDataFunction) { |
||||||
|
this._changeDataFunction(data, this._isEnded) |
||||||
|
} else { |
||||||
|
this.$emit(type, data, this._isEnded) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,847 @@ |
|||||||
|
<template> |
||||||
|
<view class="uni-data-checklist"> |
||||||
|
<template v-if="loading"> |
||||||
|
<view class="uni-data-loading"> |
||||||
|
<uni-load-more status="loading" iconType="snow" :iconSize="18" :content-text="contentText"></uni-load-more> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
<template v-else> |
||||||
|
<checkbox-group v-if="multiple" class="checklist-group" :class="{'is-list':mode==='list','is-wrap':wrap}" @change="chagne"> |
||||||
|
<label class="checklist-box" :class="item.labelClass" :style="[item.styleBackgroud]" v-for="(item,index) in dataList" |
||||||
|
:key="index"> |
||||||
|
<checkbox class="hidden" hidden :disabled="!!item.disabled" :value="item.value+''" :checked="item.selected" /> |
||||||
|
<view v-if="(mode !=='tag' && mode !== 'list') || ( mode === 'list' && icon === 'left')" class="checkbox__inner" |
||||||
|
:class="item.checkboxBgClass" :style="[item.styleIcon]"> |
||||||
|
<view class="checkbox__inner-icon" :class="item.checkboxClass"></view> |
||||||
|
</view> |
||||||
|
<view class="checklist-content" :class="{'list-content':mode === 'list' && icon ==='left'}"> |
||||||
|
<text class="checklist-text" :class="item.textClass" :style="[item.styleIconText]">{{item.text}}</text> |
||||||
|
<!-- input的disable去掉,添加change事件发送值到父级 --> |
||||||
|
<uni-easyinput @input="upToFa" v-if="inputVisble" type="text" :inputBorder="true" v-model="item.value" placeholder="请输入"></uni-easyinput> |
||||||
|
<view v-if="mode === 'list' && icon === 'right'" class="checkobx__list" :class="item.listClass" :style="[item.styleBackgroud]"></view> |
||||||
|
</view> |
||||||
|
</label> |
||||||
|
</checkbox-group> |
||||||
|
<radio-group v-else class="checklist-group" :class="{'is-list':mode==='list','is-wrap':wrap}" @change="chagne"> |
||||||
|
<label class="checklist-box" :class="item.labelClass" :style="[item.styleBackgroud]" v-for="(item,index) in dataList" :key="index"> |
||||||
|
<radio hidden :disabled="item.disabled" :value="item.value+''" :checked="item.selected" /> |
||||||
|
<view v-if="(mode !=='tag' && mode !== 'list') || ( mode === 'list' && icon === 'left')" class="radio__inner" |
||||||
|
:class="item.checkboxBgClass" :style="[item.styleBackgroud]"> |
||||||
|
<view class="radio__inner-icon" :class="item.checkboxClass" :style="[item.styleIcon]"></view> |
||||||
|
</view> |
||||||
|
<view class="checklist-content" :class="{'list-content':mode === 'list' && icon ==='left'}"> |
||||||
|
<text class="checklist-text" :class="item.textClass" :style="[item.styleIconText]">{{item.text}}</text> |
||||||
|
<view v-if="mode === 'list' && icon === 'right'" class="checkobx__list" :class="item.listClass" :style="[item.styleRightIcon]"></view> |
||||||
|
</view> |
||||||
|
</label> |
||||||
|
</radio-group> |
||||||
|
</template> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
/** |
||||||
|
* DataChecklist 数据选择器 |
||||||
|
* @description 通过数据渲染 checkbox 和 radio |
||||||
|
* @tutorial https://ext.dcloud.net.cn/plugin?id=xxx |
||||||
|
* @property {String} mode = [default| list | button | tag] 显示模式 |
||||||
|
* @value default 默认横排模式 |
||||||
|
* @value list 列表模式 |
||||||
|
* @value button 按钮模式 |
||||||
|
* @value tag 标签模式 |
||||||
|
* @property {Boolean} multiple = [true|false] 是否多选 |
||||||
|
* @property {Array|String|Number} value 默认值 |
||||||
|
* @property {Array} localdata 本地数据 ,格式 [{text:'',value:''}] |
||||||
|
* @property {Number|String} min 最小选择个数 ,multiple为true时生效 |
||||||
|
* @property {Number|String} max 最大选择个数 ,multiple为true时生效 |
||||||
|
* @property {Boolean} wrap 是否换行显示 |
||||||
|
* @property {String} icon = [left|right] list 列表模式下icon显示位置 |
||||||
|
* @property {Boolean} selectedColor 选中颜色 |
||||||
|
* @property {Boolean} selectedTextColor 选中文本颜色,如不填写则自动显示 |
||||||
|
* @value left 左侧显示 |
||||||
|
* @value right 右侧显示 |
||||||
|
* @event {Function} change 选中发生变化触发 |
||||||
|
*/ |
||||||
|
|
||||||
|
import clientdb from './clientdb.js' |
||||||
|
export default { |
||||||
|
name: 'uniDataChecklist', |
||||||
|
mixins: [clientdb], |
||||||
|
props: { |
||||||
|
mode: { |
||||||
|
type: String, |
||||||
|
default: 'default' |
||||||
|
}, |
||||||
|
multiple: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
value: { |
||||||
|
type: [Array, String, Number], |
||||||
|
default () { |
||||||
|
return '' |
||||||
|
} |
||||||
|
}, |
||||||
|
localdata: { |
||||||
|
type: Array, |
||||||
|
default () { |
||||||
|
return [] |
||||||
|
} |
||||||
|
}, |
||||||
|
min: { |
||||||
|
type: [Number, String], |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
max: { |
||||||
|
type: [Number, String], |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
wrap: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
icon: { |
||||||
|
type: String, |
||||||
|
default: 'left' |
||||||
|
}, |
||||||
|
selectedColor:{ |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
selectedTextColor:{ |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
// clientDB 相关 |
||||||
|
options: { |
||||||
|
type: [Object, Array], |
||||||
|
default () { |
||||||
|
return {} |
||||||
|
} |
||||||
|
}, |
||||||
|
collection: { |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
action: { |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
field: { |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
pageData: { |
||||||
|
type: String, |
||||||
|
default: 'add' |
||||||
|
}, |
||||||
|
pageCurrent: { |
||||||
|
type: Number, |
||||||
|
default: 1 |
||||||
|
}, |
||||||
|
pageSize: { |
||||||
|
type: Number, |
||||||
|
default: 20 |
||||||
|
}, |
||||||
|
getcount: { |
||||||
|
type: [Boolean, String], |
||||||
|
default: false |
||||||
|
}, |
||||||
|
orderby: { |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
where: { |
||||||
|
type: [String, Object], |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
getone: { |
||||||
|
type: [Boolean, String], |
||||||
|
default: false |
||||||
|
}, |
||||||
|
manual: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
inputVisble: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
} |
||||||
|
}, |
||||||
|
watch: { |
||||||
|
localdata: { |
||||||
|
handler(newVal) { |
||||||
|
this.range = newVal |
||||||
|
this.dataList = this.getDataList(this.getSelectedValue(newVal)) |
||||||
|
// console.log(this.dataList[0],'是否有值',newVal) |
||||||
|
this.$forceUpdate() |
||||||
|
}, |
||||||
|
deep: true |
||||||
|
}, |
||||||
|
|
||||||
|
listData(newVal) { |
||||||
|
this.range = newVal |
||||||
|
this.dataList = this.getDataList(this.getSelectedValue(newVal)) |
||||||
|
}, |
||||||
|
value(newVal) { |
||||||
|
this.dataList = this.getDataList(newVal) |
||||||
|
this.formItem && this.formItem.setValue(newVal) |
||||||
|
} |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
dataList: [], |
||||||
|
range: [], |
||||||
|
contentText: { |
||||||
|
contentdown: '查看更多', |
||||||
|
contentrefresh: '加载中', |
||||||
|
contentnomore: '没有更多' |
||||||
|
}, |
||||||
|
styles: { |
||||||
|
selectedColor: '#007aff', |
||||||
|
selectedTextColor: '#333', |
||||||
|
}, |
||||||
|
changeData:'888' |
||||||
|
}; |
||||||
|
}, |
||||||
|
created() { |
||||||
|
// console.log(this.localdata,'获取父级传值查看') |
||||||
|
this.form = this.getForm('uniForms') |
||||||
|
this.formItem = this.getForm('uniFormsItem') |
||||||
|
this.formItem && this.formItem.setValue(this.value) |
||||||
|
this.styles = { |
||||||
|
selectedColor: this.selectedColor, |
||||||
|
selectedTextColor: this.selectedTextColor |
||||||
|
} |
||||||
|
|
||||||
|
if (this.formItem) { |
||||||
|
if (this.formItem.name) { |
||||||
|
this.rename = this.formItem.name |
||||||
|
this.form.inputChildrens.push(this) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (this.localdata && this.localdata.length !== 0) { |
||||||
|
this.range = this.localdata |
||||||
|
this.dataList = this.getDataList(this.getSelectedValue(this.range)) |
||||||
|
} else { |
||||||
|
if (this.collection) { |
||||||
|
this.loadData() |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
init(range) {}, |
||||||
|
/** |
||||||
|
* 获取父元素实例 |
||||||
|
*/ |
||||||
|
getForm(name = 'uniForms') { |
||||||
|
let parent = this.$parent; |
||||||
|
let parentName = parent.$options.name; |
||||||
|
while (parentName !== name) { |
||||||
|
parent = parent.$parent; |
||||||
|
if (!parent) return false |
||||||
|
parentName = parent.$options.name; |
||||||
|
} |
||||||
|
return parent; |
||||||
|
}, |
||||||
|
chagne(e) { |
||||||
|
const values = e.detail.value |
||||||
|
|
||||||
|
let detail = { |
||||||
|
value: [], |
||||||
|
data: [] |
||||||
|
} |
||||||
|
|
||||||
|
if (this.multiple) { |
||||||
|
this.range.forEach(item => { |
||||||
|
if (values.includes(item.value + '')) { |
||||||
|
detail.value.push(item.value) |
||||||
|
detail.data.push(item) |
||||||
|
} |
||||||
|
}) |
||||||
|
} else { |
||||||
|
const range = this.range.find(item => (item.value + '') === values) |
||||||
|
if (range) { |
||||||
|
detail = { |
||||||
|
value: range.value, |
||||||
|
data: range |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
this.formItem && this.formItem.setValue(detail.value) |
||||||
|
this.$emit('input', detail.value) |
||||||
|
this.$emit('change', { |
||||||
|
detail |
||||||
|
}) |
||||||
|
if (this.multiple) { |
||||||
|
// 如果 v-model 没有绑定 ,则走内部逻辑 |
||||||
|
// if (this.value.length === 0) { |
||||||
|
this.dataList = this.getDataList(detail.value, true) |
||||||
|
// } |
||||||
|
} else { |
||||||
|
this.dataList = this.getDataList(detail.value) |
||||||
|
} |
||||||
|
}, |
||||||
|
getLabelClass(item, index) { |
||||||
|
let classes = [] |
||||||
|
switch (this.mode) { |
||||||
|
case 'default': |
||||||
|
item.disabled && classes.push('disabled-cursor') |
||||||
|
break |
||||||
|
case 'button': |
||||||
|
classes.push(...['is-button', ...this.getClasses(item, 'button')]) |
||||||
|
break |
||||||
|
case 'list': |
||||||
|
if (this.multiple) { |
||||||
|
classes.push('is-list-multiple-box') |
||||||
|
} else { |
||||||
|
classes.push('is-list-box') |
||||||
|
} |
||||||
|
item.disabled && classes.push('is-list-disabled') |
||||||
|
index !== 0 && classes.push('is-list-border') |
||||||
|
break |
||||||
|
case 'tag': |
||||||
|
classes.push(...['is-tag', ...this.getClasses(item, 'tag')]) |
||||||
|
break |
||||||
|
} |
||||||
|
classes = classes.join(' ') |
||||||
|
return classes |
||||||
|
}, |
||||||
|
getCheckboxClass(item, type = '') { |
||||||
|
let classes = [] |
||||||
|
if (this.multiple) { |
||||||
|
classes.push(...this.getClasses(item, 'default-multiple', type)) |
||||||
|
} else { |
||||||
|
classes.push(...this.getClasses(item, 'default', type)) |
||||||
|
} |
||||||
|
classes = classes.join(' ') |
||||||
|
return classes |
||||||
|
}, |
||||||
|
getTextClass(item) { |
||||||
|
let classes = [] |
||||||
|
switch (this.mode) { |
||||||
|
case 'default': |
||||||
|
classes.push(...this.getClasses(item, 'list')) |
||||||
|
break |
||||||
|
case 'button': |
||||||
|
classes.push(...this.getClasses(item, 'list')) |
||||||
|
break |
||||||
|
case 'list': |
||||||
|
classes.push(...this.getClasses(item, 'list')) |
||||||
|
break |
||||||
|
case 'tag': |
||||||
|
classes.push(...['is-tag-text', ...this.getClasses(item, 'tag-text')]) |
||||||
|
break |
||||||
|
} |
||||||
|
classes = classes.join(' ') |
||||||
|
return classes |
||||||
|
}, |
||||||
|
|
||||||
|
/** |
||||||
|
* 获取渲染的新数组 |
||||||
|
* @param {Object} value 选中内容 |
||||||
|
*/ |
||||||
|
getDataList(value) { |
||||||
|
// 解除引用关系,破坏原引用关系,避免污染源数据 |
||||||
|
let dataList = JSON.parse(JSON.stringify(this.range)) |
||||||
|
let list = [] |
||||||
|
if (this.multiple) { |
||||||
|
if (!Array.isArray(value)) { |
||||||
|
value = [] |
||||||
|
// console.error('props 类型错误'); |
||||||
|
} |
||||||
|
} |
||||||
|
dataList.forEach((item, index) => { |
||||||
|
item.disabled = item.disable || item.disabled || false |
||||||
|
if (this.multiple) { |
||||||
|
if (value.length > 0) { |
||||||
|
let have = value.find(val => val === item.value) |
||||||
|
item.selected = have !== undefined |
||||||
|
} else { |
||||||
|
item.selected = false |
||||||
|
} |
||||||
|
} else { |
||||||
|
item.selected = value === item.value |
||||||
|
} |
||||||
|
|
||||||
|
list.push(item) |
||||||
|
}) |
||||||
|
// console.log(dataList,'看下list') |
||||||
|
return this.setRange(list) |
||||||
|
}, |
||||||
|
/** |
||||||
|
* 处理最大最小值 |
||||||
|
* @param {Object} list |
||||||
|
*/ |
||||||
|
setRange(list) { |
||||||
|
let selectList = list.filter(item => item.selected) |
||||||
|
let min = Number(this.min) || 0 |
||||||
|
let max = Number(this.max) || '' |
||||||
|
list.forEach((item, index) => { |
||||||
|
if (this.multiple) { |
||||||
|
if (selectList.length <= min) { |
||||||
|
let have = selectList.find(val => val.value === item.value) |
||||||
|
if (have !== undefined) { |
||||||
|
item.disabled = true |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (selectList.length >= max && max !== '') { |
||||||
|
let have = selectList.find(val => val.value === item.value) |
||||||
|
if (have === undefined) { |
||||||
|
item.disabled = true |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
this.setClass(item, index) |
||||||
|
list[index] = item |
||||||
|
}) |
||||||
|
// console.log(this.dataList,'处理过的值查看') |
||||||
|
return list |
||||||
|
}, |
||||||
|
/** |
||||||
|
* 设置 class |
||||||
|
* @param {Object} item |
||||||
|
* @param {Object} index |
||||||
|
*/ |
||||||
|
setClass(item, index) { |
||||||
|
// 设置 label 的 class |
||||||
|
item.labelClass = this.getLabelClass(item, index) |
||||||
|
// 设置 checkbox外层样式 |
||||||
|
item.checkboxBgClass = this.getCheckboxClass(item, '-bg') |
||||||
|
// 设置 checkbox 内层样式 |
||||||
|
item.checkboxClass = this.getCheckboxClass(item) |
||||||
|
// 设置文本样式 |
||||||
|
item.textClass = this.getTextClass(item) |
||||||
|
// 设置 list 对勾右侧样式 |
||||||
|
item.listClass = this.getCheckboxClass(item, '-list') |
||||||
|
|
||||||
|
// 设置自定义样式 |
||||||
|
item.styleBackgroud = this.setStyleBackgroud(item) |
||||||
|
item.styleIcon = this.setStyleIcon(item) |
||||||
|
item.styleIconText = this.setStyleIconText(item) |
||||||
|
item.styleRightIcon = this.setStyleRightIcon(item) |
||||||
|
}, |
||||||
|
/** |
||||||
|
* 获取 class |
||||||
|
*/ |
||||||
|
getClasses(item, name, type = '') { |
||||||
|
let classes = [] |
||||||
|
item.disabled && classes.push('is-' + name + '-disabled' + type) |
||||||
|
item.selected && classes.push('is-' + name + '-checked' + type) |
||||||
|
|
||||||
|
if (this.mode !== 'button' || name === 'button') { |
||||||
|
item.selected && item.disabled && classes.push('is-' + name + '-disabled-checked' + type) |
||||||
|
} |
||||||
|
|
||||||
|
return classes |
||||||
|
}, |
||||||
|
/** |
||||||
|
* 获取选中值 |
||||||
|
* @param {Object} range |
||||||
|
*/ |
||||||
|
getSelectedValue(range) { |
||||||
|
if (!this.multiple) return this.value |
||||||
|
let selectedArr = [] |
||||||
|
range.forEach((item) => { |
||||||
|
if (item.selected) { |
||||||
|
selectedArr.push(item.value) |
||||||
|
} |
||||||
|
}) |
||||||
|
return this.value.length > 0 ? this.value : selectedArr |
||||||
|
}, |
||||||
|
|
||||||
|
/** |
||||||
|
* 设置背景样式 |
||||||
|
*/ |
||||||
|
setStyleBackgroud(item) { |
||||||
|
let styles = {} |
||||||
|
|
||||||
|
if (item.selected) { |
||||||
|
if (this.mode !== 'list') { |
||||||
|
styles.borderColor = this.styles.selectedColor |
||||||
|
} |
||||||
|
if (this.mode === 'tag') { |
||||||
|
styles.backgroundColor = this.styles.selectedColor |
||||||
|
} |
||||||
|
} |
||||||
|
return styles |
||||||
|
}, |
||||||
|
setStyleIcon(item) { |
||||||
|
let styles = {} |
||||||
|
|
||||||
|
if (item.selected) { |
||||||
|
styles.backgroundColor = this.styles.selectedColor |
||||||
|
styles.borderColor = this.styles.selectedColor |
||||||
|
} |
||||||
|
return styles |
||||||
|
}, |
||||||
|
setStyleIconText(item) { |
||||||
|
let styles = {} |
||||||
|
if (item.selected) { |
||||||
|
if (this.styles.selectedTextColor) { |
||||||
|
styles.color = this.styles.selectedTextColor |
||||||
|
} else { |
||||||
|
if(this.mode === 'tag'){ |
||||||
|
styles.color = '#fff' |
||||||
|
}else{ |
||||||
|
styles.color = this.styles.selectedColor |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return styles |
||||||
|
}, |
||||||
|
setStyleRightIcon(item){ |
||||||
|
let styles = {} |
||||||
|
if (item.selected) { |
||||||
|
if(this.mode === 'list'){ |
||||||
|
styles.borderColor = this.styles.selectedColor |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return styles |
||||||
|
}, |
||||||
|
// 自定义发送事件--传值inpu和传值item |
||||||
|
upToFa(val,item){ |
||||||
|
console.log(val,'输入的值','item',item) |
||||||
|
this.$emit('getInput',val,item) |
||||||
|
// this.$forceUpdate() |
||||||
|
setTimeout(() => { console.log(val) }, 0) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style> |
||||||
|
.uni-data-checklist { |
||||||
|
position: relative; |
||||||
|
z-index: 0; |
||||||
|
width: 80%; |
||||||
|
margin-left: 20%; |
||||||
|
/* min-height: 36px; */ |
||||||
|
} |
||||||
|
|
||||||
|
.uni-data-loading { |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
/* justify-content: center; */ |
||||||
|
height: 72rpx; |
||||||
|
padding-left: 20rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.checklist-group { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
flex-direction: row; |
||||||
|
flex-wrap: wrap; |
||||||
|
} |
||||||
|
|
||||||
|
.checklist-box { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
flex-direction: row; |
||||||
|
align-items: center; |
||||||
|
margin: 10rpx 0; |
||||||
|
margin-right: 50rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.checklist-text { |
||||||
|
font-size: 28rpx; |
||||||
|
color: #333; |
||||||
|
margin-left: 10rpx; |
||||||
|
transition: color 0.2s; |
||||||
|
} |
||||||
|
|
||||||
|
.is-button { |
||||||
|
margin-right: 20rpx; |
||||||
|
padding: 6rpx 30rpx; |
||||||
|
border: 2rpx #DCDFE6 solid; |
||||||
|
border-radius: 6rpx; |
||||||
|
transition: border-color 0.2s; |
||||||
|
} |
||||||
|
|
||||||
|
.is-list { |
||||||
|
flex-direction: column; |
||||||
|
} |
||||||
|
|
||||||
|
.is-list-box { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
padding: 20rpx 30rpx; |
||||||
|
padding-left: 0; |
||||||
|
margin: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.checklist-content { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
flex: 1; |
||||||
|
flex-direction: row; |
||||||
|
align-items: center; |
||||||
|
justify-content: space-between; |
||||||
|
} |
||||||
|
|
||||||
|
.list-content { |
||||||
|
margin-left: 10rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.is-list-multiple-box { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
padding: 20rpx 30rpx; |
||||||
|
padding-left: 0; |
||||||
|
margin: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.is-list-border { |
||||||
|
border-top: 2rpx #eee solid; |
||||||
|
} |
||||||
|
|
||||||
|
.is-tag { |
||||||
|
margin-right: 20rpx; |
||||||
|
padding: 6rpx 20rpx; |
||||||
|
border: 2rpx #eee solid; |
||||||
|
border-radius: 6rpx; |
||||||
|
background-color: #f5f5f5; |
||||||
|
/* transition: border-color 0.1s; */ |
||||||
|
} |
||||||
|
|
||||||
|
.is-tag-text { |
||||||
|
margin: 0; |
||||||
|
color: #666; |
||||||
|
} |
||||||
|
|
||||||
|
.checkbox__inner { |
||||||
|
flex-shrink: 0; |
||||||
|
position: relative; |
||||||
|
border: 2rpx solid #DCDFE6; |
||||||
|
border-radius: 4rpx; |
||||||
|
box-sizing: border-box; |
||||||
|
width: 32rpx; |
||||||
|
height: 32rpx; |
||||||
|
background-color: #fff; |
||||||
|
z-index: 1; |
||||||
|
transition: border-color 0.1s; |
||||||
|
} |
||||||
|
|
||||||
|
.checkbox__inner-icon { |
||||||
|
border: 2rpx solid #fff; |
||||||
|
border-left: 0; |
||||||
|
border-top: 0; |
||||||
|
height: 16rpx; |
||||||
|
left: 10rpx; |
||||||
|
position: absolute; |
||||||
|
top: 2rpx; |
||||||
|
width: 6rpx; |
||||||
|
opacity: 0; |
||||||
|
transition: transform .2s; |
||||||
|
transform-origin: center; |
||||||
|
transform: rotate(40deg) scaleY(0.4); |
||||||
|
} |
||||||
|
|
||||||
|
.radio__inner { |
||||||
|
flex-shrink: 0; |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
justify-content: center; |
||||||
|
align-items: center; |
||||||
|
position: relative; |
||||||
|
border: 2rpx solid #DCDFE6; |
||||||
|
border-radius: 4rpx; |
||||||
|
box-sizing: border-box; |
||||||
|
width: 32rpx; |
||||||
|
height: 32rpx; |
||||||
|
border-radius: 32rpx; |
||||||
|
background-color: #fff; |
||||||
|
z-index: 1; |
||||||
|
transition: border-color .3s; |
||||||
|
} |
||||||
|
|
||||||
|
.radio__inner-icon { |
||||||
|
width: 16rpx; |
||||||
|
height: 16rpx; |
||||||
|
border-radius: 20rpx; |
||||||
|
opacity: 0; |
||||||
|
transition: transform .3s; |
||||||
|
} |
||||||
|
|
||||||
|
.checkobx__list { |
||||||
|
border: 2rpx solid #fff; |
||||||
|
border-left: 0; |
||||||
|
border-top: 0; |
||||||
|
height: 24rpx; |
||||||
|
width: 12rpx; |
||||||
|
transform-origin: center; |
||||||
|
opacity: 0; |
||||||
|
transition: all 0.3s; |
||||||
|
transform: rotate(45deg); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/* 禁用样式 */ |
||||||
|
.is-default-disabled-bg { |
||||||
|
background-color: #F2F6FC; |
||||||
|
border-color: #DCDFE6; |
||||||
|
/* #ifdef H5 */ |
||||||
|
cursor: not-allowed; |
||||||
|
/* #endif */ |
||||||
|
} |
||||||
|
|
||||||
|
.is-default-multiple-disabled-bg { |
||||||
|
background-color: #F2F6FC; |
||||||
|
border-color: #DCDFE6; |
||||||
|
/* #ifdef H5 */ |
||||||
|
cursor: not-allowed; |
||||||
|
/* #endif */ |
||||||
|
} |
||||||
|
|
||||||
|
.is-default-disabled { |
||||||
|
border-color: #F2F6FC; |
||||||
|
} |
||||||
|
|
||||||
|
.is-default-multiple-disabled { |
||||||
|
border-color: #F2F6FC; |
||||||
|
} |
||||||
|
|
||||||
|
.is-list-disabled { |
||||||
|
/* #ifdef H5 */ |
||||||
|
cursor: not-allowed; |
||||||
|
/* #endif */ |
||||||
|
color: #999; |
||||||
|
} |
||||||
|
|
||||||
|
.is-list-disabled-checked { |
||||||
|
color: #a1dcc1; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
.is-button-disabled { |
||||||
|
/* #ifdef H5 */ |
||||||
|
cursor: not-allowed; |
||||||
|
/* #endif */ |
||||||
|
border-color: #EBEEF5; |
||||||
|
} |
||||||
|
|
||||||
|
.is-button-text-disabled { |
||||||
|
color: #C0C4CC; |
||||||
|
} |
||||||
|
|
||||||
|
.is-button-disabled-checked { |
||||||
|
border-color: #a1dcc1; |
||||||
|
} |
||||||
|
|
||||||
|
.is-tag-disabled { |
||||||
|
/* #ifdef H5 */ |
||||||
|
cursor: not-allowed; |
||||||
|
/* #endif */ |
||||||
|
border-color: #e9e9eb; |
||||||
|
background-color: #f4f4f5; |
||||||
|
} |
||||||
|
|
||||||
|
.is-tag-text-disabled { |
||||||
|
color: #bcbec2; |
||||||
|
} |
||||||
|
|
||||||
|
/* 选中样式 */ |
||||||
|
.is-default-checked-bg { |
||||||
|
border-color: #007aff; |
||||||
|
} |
||||||
|
|
||||||
|
.is-default-multiple-checked-bg { |
||||||
|
border-color: #007aff; |
||||||
|
background-color: #007aff; |
||||||
|
} |
||||||
|
|
||||||
|
.is-default-checked { |
||||||
|
opacity: 1; |
||||||
|
background-color: #007aff; |
||||||
|
transform: rotate(45deg) scaleY(1); |
||||||
|
} |
||||||
|
|
||||||
|
.is-default-multiple-checked { |
||||||
|
opacity: 1; |
||||||
|
transform: rotate(45deg) scaleY(1); |
||||||
|
} |
||||||
|
|
||||||
|
.is-default-disabled-checked-bg { |
||||||
|
opacity: 0.4; |
||||||
|
} |
||||||
|
|
||||||
|
.is-default-multiple-disabled-checked-bg { |
||||||
|
opacity: 0.4; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
.is-default-checked-list { |
||||||
|
border-color: #007aff; |
||||||
|
opacity: 1; |
||||||
|
transform: rotate(45deg) scaleY(1); |
||||||
|
} |
||||||
|
|
||||||
|
.is-default-multiple-checked-list { |
||||||
|
border-color: #007aff; |
||||||
|
opacity: 1; |
||||||
|
transform: rotate(45deg) scaleY(1); |
||||||
|
} |
||||||
|
|
||||||
|
.is-list-disabled-checked { |
||||||
|
opacity: 0.4; |
||||||
|
} |
||||||
|
|
||||||
|
.is-default-disabled-checked-list { |
||||||
|
opacity: 0.4; |
||||||
|
} |
||||||
|
|
||||||
|
.is-default-multiple-disabled-checked-list { |
||||||
|
opacity: 0.4; |
||||||
|
} |
||||||
|
|
||||||
|
.is-button-checked { |
||||||
|
border-color: #007aff; |
||||||
|
} |
||||||
|
|
||||||
|
.is-button-disabled-checked { |
||||||
|
opacity: 0.4; |
||||||
|
} |
||||||
|
|
||||||
|
.is-list-checked { |
||||||
|
color: #007aff; |
||||||
|
} |
||||||
|
|
||||||
|
.is-tag-checked { |
||||||
|
border-color: #007aff; |
||||||
|
background-color: #007aff; |
||||||
|
} |
||||||
|
|
||||||
|
.is-tag-text-checked { |
||||||
|
color: #fff; |
||||||
|
} |
||||||
|
|
||||||
|
.is-tag-disabled-checked { |
||||||
|
opacity: 0.4; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
.disabled-cursor { |
||||||
|
/* #ifdef H5 */ |
||||||
|
cursor: not-allowed; |
||||||
|
/* #endif */ |
||||||
|
} |
||||||
|
|
||||||
|
.is-wrap { |
||||||
|
flex-direction: column; |
||||||
|
} |
||||||
|
|
||||||
|
.hidden { |
||||||
|
/* #ifdef MP-ALIPAY */ |
||||||
|
display: none; |
||||||
|
/* #endif */ |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,364 @@ |
|||||||
|
<template> |
||||||
|
<view class="uni-datetime-picker"> |
||||||
|
<view @click="tiggerTimePicker"> |
||||||
|
<slot> |
||||||
|
<view class="uni-datetime-picker-timebox uni-datetime-picker-flex"> |
||||||
|
{{time}}<view v-if="!time" class="uni-datetime-picker-time">选择日期<!-- 时间 --></view> |
||||||
|
<view class="uni-datetime-picker-down-arrow"></view> |
||||||
|
</view> |
||||||
|
</slot> |
||||||
|
</view> |
||||||
|
<view v-if="visible" class="uni-datetime-picker-mask" @click="initTimePicker"></view> |
||||||
|
<view v-if="visible" class="uni-datetime-picker-popup"> |
||||||
|
<view class="uni-title"> |
||||||
|
设置日期<!-- 和时间 --> |
||||||
|
</view> |
||||||
|
<picker-view class="uni-datetime-picker-view" :indicator-style="indicatorStyle" :value="ymd" @change="bindDateChange"> |
||||||
|
<picker-view-column class="uni-datetime-picker-hyphen"> |
||||||
|
<view class="uni-datetime-picker-item" v-for="(item,index) in years" :key="index">{{item}}</view> |
||||||
|
</picker-view-column> |
||||||
|
<picker-view-column class="uni-datetime-picker-hyphen"> |
||||||
|
<view class="uni-datetime-picker-item" v-for="(item,index) in months" :key="index">{{item < 10 ? '0' + item : item}}</view> |
||||||
|
</picker-view-column> |
||||||
|
<picker-view-column> |
||||||
|
<view class="uni-datetime-picker-item" v-for="(item,index) in days" :key="index">{{item < 10 ? '0' + item : item}}</view> |
||||||
|
</picker-view-column> |
||||||
|
</picker-view> |
||||||
|
<!-- <picker-view class="uni-datetime-picker-view" :indicator-style="indicatorStyle" :value="hms" @change="bindTimeChange"> |
||||||
|
<picker-view-column class="uni-datetime-picker-colon"> |
||||||
|
<view class="uni-datetime-picker-item" v-for="(item,index) in hours" :key="index">{{item < 10 ? '0' + item : item}}</view> |
||||||
|
</picker-view-column> |
||||||
|
<picker-view-column class="uni-datetime-picker-colon"> |
||||||
|
<view class="uni-datetime-picker-item" v-for="(item,index) in minutes" :key="index">{{item < 10 ? '0' + item : item}}</view> |
||||||
|
</picker-view-column> |
||||||
|
<picker-view-column> |
||||||
|
<view class="uni-datetime-picker-item" v-for="(item,index) in seconds" :key="index">{{item < 10 ? '0' + item : item}}</view> |
||||||
|
</picker-view-column> |
||||||
|
</picker-view> --> |
||||||
|
<view class="uni-datetime-picker-btn"> |
||||||
|
<!-- <view class="" @click="clearTime">重置</view> --> |
||||||
|
<view class="uni-datetime-picker-btn-group"> |
||||||
|
<view class="uni-datetime-picker-cancel" @click="tiggerTimePicker">取消</view> |
||||||
|
<view class="" @click="setTime">确定</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
data() { |
||||||
|
return { |
||||||
|
visible: false, |
||||||
|
time: '', |
||||||
|
years: [], |
||||||
|
months: [], |
||||||
|
days: [], |
||||||
|
hours: [], |
||||||
|
minutes: [], |
||||||
|
seconds: [], |
||||||
|
year: 1900, |
||||||
|
month: 0, |
||||||
|
day: 0, |
||||||
|
hour: 0, |
||||||
|
minute: 0, |
||||||
|
second: 0, |
||||||
|
indicatorStyle: `height: 50px;`, |
||||||
|
} |
||||||
|
}, |
||||||
|
props: { |
||||||
|
type: { |
||||||
|
type: String, |
||||||
|
default: 'datetime-local' |
||||||
|
}, |
||||||
|
timestamp: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
value: { |
||||||
|
type: [String, Number], |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
maxYear: { |
||||||
|
type: [Number, String], |
||||||
|
default: 2100 |
||||||
|
}, |
||||||
|
minYear: { |
||||||
|
type: [Number, String], |
||||||
|
default: 1900 |
||||||
|
} |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
ymd() { |
||||||
|
return [this.year - this.minYear, this.month - 1, this.day - 1] |
||||||
|
}, |
||||||
|
hms() { |
||||||
|
return [this.hour, this.minute, this.second] |
||||||
|
} |
||||||
|
}, |
||||||
|
watch: { |
||||||
|
value(newValue) { |
||||||
|
this.parseValue(this.value) |
||||||
|
this.initTime() |
||||||
|
} |
||||||
|
}, |
||||||
|
created() { |
||||||
|
this.form = this.getForm('uniForms') |
||||||
|
this.formItem = this.getForm('uniFormsItem') |
||||||
|
|
||||||
|
if (this.formItem) { |
||||||
|
if (this.formItem.name) { |
||||||
|
this.rename = this.formItem.name |
||||||
|
this.form.inputChildrens.push(this) |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
mounted() { |
||||||
|
const date = new Date() |
||||||
|
for (let i = this.minYear; i <= this.maxYear; i++) { |
||||||
|
this.years.push(i) |
||||||
|
} |
||||||
|
|
||||||
|
for (let i = 1; i <= 12; i++) { |
||||||
|
this.months.push(i) |
||||||
|
} |
||||||
|
|
||||||
|
for (let i = 1; i <= 31; i++) { |
||||||
|
this.days.push(i) |
||||||
|
} |
||||||
|
|
||||||
|
for (let i = 0; i <= 23; i++) { |
||||||
|
this.hours.push(i) |
||||||
|
} |
||||||
|
|
||||||
|
for (let i = 0; i <= 59; i++) { |
||||||
|
this.minutes.push(i) |
||||||
|
} |
||||||
|
|
||||||
|
for (let i = 0; i <= 59; i++) { |
||||||
|
this.seconds.push(i) |
||||||
|
} |
||||||
|
this.parseValue(this.value) |
||||||
|
if (this.value) { |
||||||
|
this.initTime() |
||||||
|
} |
||||||
|
|
||||||
|
}, |
||||||
|
methods: { |
||||||
|
/** |
||||||
|
* 获取父元素实例 |
||||||
|
*/ |
||||||
|
getForm(name = 'uniForms') { |
||||||
|
let parent = this.$parent; |
||||||
|
let parentName = parent.$options.name; |
||||||
|
while (parentName !== name) { |
||||||
|
parent = parent.$parent; |
||||||
|
if (!parent) return false |
||||||
|
parentName = parent.$options.name; |
||||||
|
} |
||||||
|
return parent; |
||||||
|
}, |
||||||
|
parseDateTime(datetime) { |
||||||
|
let defaultDate = null |
||||||
|
if (!datetime) { |
||||||
|
defaultDate = new Date() |
||||||
|
} else { |
||||||
|
defaultDate = new Date(datetime) |
||||||
|
} |
||||||
|
this.year = defaultDate.getFullYear() |
||||||
|
if (this.year < this.minYear || this.year > this.maxYear) { |
||||||
|
const now = Date.now() |
||||||
|
this.parseDateTime(now) |
||||||
|
return |
||||||
|
} |
||||||
|
this.month = defaultDate.getMonth() + 1 |
||||||
|
this.day = defaultDate.getDate() |
||||||
|
this.hour = defaultDate.getHours() |
||||||
|
this.minute = defaultDate.getMinutes() |
||||||
|
this.second = defaultDate.getSeconds() |
||||||
|
}, |
||||||
|
parseValue(defaultTime) { |
||||||
|
if (Number(defaultTime)) { |
||||||
|
defaultTime = parseInt(defaultTime) |
||||||
|
} |
||||||
|
this.parseDateTime(defaultTime) |
||||||
|
}, |
||||||
|
bindDateChange(e) { |
||||||
|
const val = e.detail.value |
||||||
|
this.year = this.years[val[0]] |
||||||
|
this.month = this.months[val[1]] |
||||||
|
this.day = this.days[val[2]] |
||||||
|
console.log(this.year+'-'+this.month+'-'+this.day) |
||||||
|
}, |
||||||
|
bindTimeChange(e) { |
||||||
|
const val = e.detail.value |
||||||
|
this.hour = this.hours[val[0]] |
||||||
|
this.minute = this.minutes[val[1]] |
||||||
|
this.second = this.seconds[val[2]] |
||||||
|
}, |
||||||
|
initTimePicker() { |
||||||
|
// if (!this.time) { |
||||||
|
// this.parseValue() |
||||||
|
// } |
||||||
|
this.parseValue(this.value) |
||||||
|
this.visible = !this.visible |
||||||
|
}, |
||||||
|
tiggerTimePicker() { |
||||||
|
this.visible = !this.visible |
||||||
|
}, |
||||||
|
clearTime() { |
||||||
|
this.time = '' |
||||||
|
this.tiggerTimePicker() |
||||||
|
}, |
||||||
|
initTime() { |
||||||
|
this.time = this.createDomSting() |
||||||
|
if (!this.timestamp) { |
||||||
|
this.formItem && this.formItem.setValue(this.time) |
||||||
|
this.$emit('change', this.time) |
||||||
|
} else { |
||||||
|
this.formItem && this.formItem.setValue(this.createTimeStamp(this.time)) |
||||||
|
this.$emit('change', this.createTimeStamp(this.time)) |
||||||
|
} |
||||||
|
}, |
||||||
|
setTime() { |
||||||
|
this.initTime() |
||||||
|
this.tiggerTimePicker() |
||||||
|
}, |
||||||
|
createTimeStamp(time) { |
||||||
|
return Date.parse(new Date(time)) |
||||||
|
}, |
||||||
|
createDomSting() { |
||||||
|
const yymmdd = this.year + |
||||||
|
'-' + |
||||||
|
(this.month < 10 ? '0' + this.month : this.month) + |
||||||
|
'-' + |
||||||
|
(this.day < 10 ? '0' + this.day : this.day) |
||||||
|
// + |
||||||
|
// ' ' + |
||||||
|
// (this.hour < 10 ? '0' + this.hour : this.hour) + |
||||||
|
// ':' + |
||||||
|
// (this.minute < 10 ? '0' + this.minute : this.minute) + |
||||||
|
// ':' + |
||||||
|
// (this.second < 10 ? '0' + this.second : this.second) |
||||||
|
|
||||||
|
return yymmdd |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style> |
||||||
|
.uni-datetime-picker-view { |
||||||
|
width: 100%; |
||||||
|
height: 130px; |
||||||
|
margin-top: 60rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-item { |
||||||
|
line-height: 100rpx; |
||||||
|
text-align: center; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-title{ |
||||||
|
text-align: center; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-btn { |
||||||
|
margin-top: 120rpx; |
||||||
|
display: flex; |
||||||
|
justify-content: center; |
||||||
|
align-items: center; |
||||||
|
color: #00B9FF; |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-btn-group { |
||||||
|
display: flex; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-cancel { |
||||||
|
margin-right: 60rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-mask { |
||||||
|
position: fixed; |
||||||
|
bottom: 0px; |
||||||
|
top: 0px; |
||||||
|
left: 0px; |
||||||
|
right: 0px; |
||||||
|
background-color: rgba(0, 0, 0, 0.4); |
||||||
|
transition-duration: 0.3s; |
||||||
|
z-index: 998; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-popup { |
||||||
|
border-radius: 16rpx; |
||||||
|
padding: 60rpx; |
||||||
|
width: 540rpx; |
||||||
|
background-color: #fff; |
||||||
|
position: fixed; |
||||||
|
top: 50%; |
||||||
|
left: 50%; |
||||||
|
transform: translate(-50%, -50%); |
||||||
|
transition-duration: 0.3s; |
||||||
|
z-index: 999; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-time { |
||||||
|
color: grey; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-colon::after { |
||||||
|
content: ':'; |
||||||
|
position: absolute; |
||||||
|
top: 106rpx; |
||||||
|
right: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-hyphen::after { |
||||||
|
content: '-'; |
||||||
|
position: absolute; |
||||||
|
top: 106rpx; |
||||||
|
right: -4rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-timebox { |
||||||
|
border: 2rpx solid #E5E5E5; |
||||||
|
border-radius: 40rpx; |
||||||
|
padding: 14rpx 40rpx; |
||||||
|
box-sizing: border-box; |
||||||
|
cursor: pointer; |
||||||
|
width: 80%; |
||||||
|
margin-left: 20%; |
||||||
|
} |
||||||
|
|
||||||
|
// 下箭头 |
||||||
|
.uni-datetime-picker-down-arrow { |
||||||
|
display :inline-block; |
||||||
|
position: relative; |
||||||
|
width: 40rpx; |
||||||
|
height: 30rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-down-arrow::after { |
||||||
|
display: inline-block; |
||||||
|
content: " "; |
||||||
|
height: 18rpx; |
||||||
|
width: 18rpx; |
||||||
|
border-width: 0 2rpx 2rpx 0; |
||||||
|
border-color: #E5E5E5; |
||||||
|
border-style: solid; |
||||||
|
transform: matrix(0.71, 0.71, -0.71, 0.71, 0, 0); |
||||||
|
transform-origin: center; |
||||||
|
transition: transform .3s; |
||||||
|
position: absolute; |
||||||
|
top: 50%; |
||||||
|
right: 10rpx; |
||||||
|
margin-top: -10rpx; |
||||||
|
} |
||||||
|
.uni-datetime-picker-flex { |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,56 @@ |
|||||||
|
/** |
||||||
|
* @desc 函数防抖 |
||||||
|
* @param func 目标函数 |
||||||
|
* @param wait 延迟执行毫秒数 |
||||||
|
* @param immediate true - 立即执行, false - 延迟执行 |
||||||
|
*/ |
||||||
|
export const debounce = function(func, wait = 1000, immediate = true) { |
||||||
|
let timer; |
||||||
|
console.log(1); |
||||||
|
return function() { |
||||||
|
console.log(123); |
||||||
|
let context = this, |
||||||
|
args = arguments; |
||||||
|
if (timer) clearTimeout(timer); |
||||||
|
if (immediate) { |
||||||
|
let callNow = !timer; |
||||||
|
timer = setTimeout(() => { |
||||||
|
timer = null; |
||||||
|
}, wait); |
||||||
|
if (callNow) func.apply(context, args); |
||||||
|
} else { |
||||||
|
timer = setTimeout(() => { |
||||||
|
func.apply(context, args); |
||||||
|
}, wait) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
/** |
||||||
|
* @desc 函数节流 |
||||||
|
* @param func 函数 |
||||||
|
* @param wait 延迟执行毫秒数 |
||||||
|
* @param type 1 使用表时间戳,在时间段开始的时候触发 2 使用表定时器,在时间段结束的时候触发 |
||||||
|
*/ |
||||||
|
export const throttle = (func, wait = 1000, type = 1) => { |
||||||
|
let previous = 0; |
||||||
|
let timeout; |
||||||
|
return function() { |
||||||
|
let context = this; |
||||||
|
let args = arguments; |
||||||
|
if (type === 1) { |
||||||
|
let now = Date.now(); |
||||||
|
|
||||||
|
if (now - previous > wait) { |
||||||
|
func.apply(context, args); |
||||||
|
previous = now; |
||||||
|
} |
||||||
|
} else if (type === 2) { |
||||||
|
if (!timeout) { |
||||||
|
timeout = setTimeout(() => { |
||||||
|
timeout = null; |
||||||
|
func.apply(context, args) |
||||||
|
}, wait) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,402 @@ |
|||||||
|
<template> |
||||||
|
<view class="uni-easyinput" :class="{'uni-easyinput-error':msg}" :style="{color:inputBorder && msg?'#dd524d':styles.color}"> |
||||||
|
<view class="uni-easyinput__content" :class="{'is-input-border':inputBorder ,'is-input-error-border':inputBorder && msg,'is-textarea':type==='textarea','is-disabled':disabled}" |
||||||
|
:style="{'border-color':inputBorder && msg?'#dd524d':styles.borderColor,'background-color':disabled?styles.disableColor:'#fff'}"> |
||||||
|
<uni-icons v-if="prefixIcon" class="content-clear-icon" :type="prefixIcon" color="#c0c4cc" @click="onClickIcon('prefix')"></uni-icons> |
||||||
|
<textarea v-if="type === 'textarea'" class="uni-easyinput__content-textarea" :class="{'input-padding':inputBorder}" |
||||||
|
:name="name" :value="val" :placeholder="placeholder" :placeholderStyle="placeholderStyle" :disabled="disabled" |
||||||
|
:maxlength="inputMaxlength" :focus="focused" :autoHeight="autoHeight" @input="onInput" @blur="onBlur" @focus="onFocus" |
||||||
|
@confirm="onConfirm"></textarea> |
||||||
|
<input v-else :type="type === 'password'?'text':type" class="uni-easyinput__content-input" :style="{ |
||||||
|
'padding-right':type === 'password' ||clearable || prefixIcon?'':'40rpx', |
||||||
|
'padding-left':prefixIcon?'':'40rpx' |
||||||
|
}" |
||||||
|
:name="name" :value="val" :password="!showPassword && type === 'password'" :placeholder="placeholder" |
||||||
|
:placeholderStyle="placeholderStyle" :disabled="disabled" :maxlength="inputMaxlength" :focus="focused" @focus="onFocus" |
||||||
|
@blur="onBlur" @input="onInput" @confirm="onConfirm" /> |
||||||
|
<template v-if="type === 'password'"> |
||||||
|
<uni-icons v-if="val != '' " class="content-clear-icon" :class="{'is-textarea-icon':type==='textarea'}" :type="showPassword?'eye-slash-filled':'eye-filled'" |
||||||
|
:size="18" color="#c0c4cc" @click="onEyes"></uni-icons> |
||||||
|
</template> |
||||||
|
<template v-else-if="suffixIcon"> |
||||||
|
<uni-icons v-if="suffixIcon" class="content-clear-icon" :type="suffixIcon" color="#c0c4cc" @click="onClickIcon('suffix')"></uni-icons> |
||||||
|
</template> |
||||||
|
<template v-else> |
||||||
|
<uni-icons class="content-clear-icon" :class="{'is-textarea-icon':type==='textarea'}" type="clear" :size="clearSize" |
||||||
|
v-if="clearable && focused && val " color="#c0c4cc" @click="onClear"></uni-icons> |
||||||
|
</template> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
/** |
||||||
|
* Field 输入框 |
||||||
|
* @description 此组件可以实现表单的输入与校验,包括 "text" 和 "textarea" 类型。 |
||||||
|
* @tutorial https://ext.dcloud.net.cn/plugin?id=21001 |
||||||
|
* @property {String| Number} value 输入内容 |
||||||
|
* @property {String } type 输入框的类型(默认text) password/text/textarea/.. |
||||||
|
* @value text 文本输入键盘 |
||||||
|
* @value textarea 多行文本输入键盘 |
||||||
|
* @value password 密码输入键盘 |
||||||
|
* @value number 数字输入键盘,注意iOS上app-vue弹出的数字键盘并非9宫格方式 |
||||||
|
* @value idcard 身份证输入键盘,信、支付宝、百度、QQ小程序 |
||||||
|
* @value digit 带小数点的数字键盘 ,App的nvue页面、微信、支付宝、百度、头条、QQ小程序支持 |
||||||
|
* @property {Boolean} clearable 是否显示右侧清空内容的图标控件(输入框有内容,且获得焦点时才显示),点击可清空输入框内容(默认true) |
||||||
|
* @property {Boolean} autoHeight 是否自动增高输入区域,type为textarea时有效(默认true) |
||||||
|
* @property {String } placeholder 输入框的提示文字 |
||||||
|
* @property {String } placeholderStyle placeholder的样式(内联样式,字符串),如"color: #ddd" |
||||||
|
* @property {Boolean} focus 是否自动获得焦点(默认false) |
||||||
|
* @property {Boolean} disabled 是否不可输入(默认false) |
||||||
|
* @property {Number } maxlength 最大输入长度,设置为 -1 的时候不限制最大长度(默认140) |
||||||
|
* @property {String } confirmType 设置键盘右下角按钮的文字,仅在type="text"时生效(默认done) |
||||||
|
* @property {Number } clearSize 清除图标的大小,单位px(默认15) |
||||||
|
* @property {String} prefixIcon 输入框头部图标 |
||||||
|
* @property {String} suffixIcon 输入框尾部图标 |
||||||
|
* @property {Boolean} trim 是否自动去除两端的空格 |
||||||
|
* @property {Boolean} inputBorder 是否显示input输入框的边框(默认false) |
||||||
|
* @property {Object} styles 自定义颜色 |
||||||
|
* @event {Function} input 输入框内容发生变化时触发 |
||||||
|
* @event {Function} focus 输入框获得焦点时触发 |
||||||
|
* @event {Function} blur 输入框失去焦点时触发 |
||||||
|
* @event {Function} confirm 点击完成按钮时触发 |
||||||
|
* @event {Function} iconClick 点击图标时触发 |
||||||
|
* @example <uni-easyinput v-model="mobile"></uni-easyinput> |
||||||
|
*/ |
||||||
|
|
||||||
|
import { |
||||||
|
debounce, |
||||||
|
throttle |
||||||
|
} from './common.js' |
||||||
|
|
||||||
|
export default { |
||||||
|
name: 'uni-easyinput', |
||||||
|
props: { |
||||||
|
name: String, |
||||||
|
value: [Number, String], |
||||||
|
type: { |
||||||
|
type: String, |
||||||
|
default: 'text' |
||||||
|
}, |
||||||
|
clearable: { |
||||||
|
type: Boolean, |
||||||
|
default: true |
||||||
|
}, |
||||||
|
autoHeight: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
placeholder: String, |
||||||
|
placeholderStyle: String, |
||||||
|
focus: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
disabled: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
maxlength: { |
||||||
|
type: [Number, String], |
||||||
|
default: 140 |
||||||
|
}, |
||||||
|
confirmType: { |
||||||
|
type: String, |
||||||
|
default: 'done' |
||||||
|
}, |
||||||
|
// 清除按钮的大小 |
||||||
|
clearSize: { |
||||||
|
type: [Number, String], |
||||||
|
default: 15 |
||||||
|
}, |
||||||
|
// 是否显示 input 边框 |
||||||
|
inputBorder: { |
||||||
|
type: Boolean, |
||||||
|
default: true |
||||||
|
}, |
||||||
|
prefixIcon: { |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
suffixIcon: { |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
// 是否自动去除两端的空格 |
||||||
|
trim: { |
||||||
|
type: Boolean, |
||||||
|
default: true |
||||||
|
}, |
||||||
|
// 自定义样式 |
||||||
|
styles: { |
||||||
|
type: Object, |
||||||
|
default () { |
||||||
|
return { |
||||||
|
color: '#333', |
||||||
|
disableColor: '#eee', |
||||||
|
borderColor: '#e5e5e5' |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
focused: false, |
||||||
|
errMsg: '', |
||||||
|
val: '', |
||||||
|
showMsg: '', |
||||||
|
border: false, |
||||||
|
isFirstBorder: false, |
||||||
|
showClearIcon: false, |
||||||
|
showPassword: false |
||||||
|
}; |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
msg() { |
||||||
|
return this.errorMessage || this.errMsg; |
||||||
|
}, |
||||||
|
// 因为uniapp的input组件的maxlength组件必须要数值,这里转为数值,给用户可以传入字符串数值 |
||||||
|
inputMaxlength() { |
||||||
|
return Number(this.maxlength); |
||||||
|
}, |
||||||
|
}, |
||||||
|
watch: { |
||||||
|
value(newVal) { |
||||||
|
if (this.errMsg) this.errMsg = '' |
||||||
|
this.val = newVal |
||||||
|
if (this.formItem) { |
||||||
|
this.formItem.setValue(newVal) |
||||||
|
} |
||||||
|
}, |
||||||
|
focus(newVal) { |
||||||
|
this.$nextTick(() => { |
||||||
|
this.focused = this.focus |
||||||
|
}) |
||||||
|
} |
||||||
|
}, |
||||||
|
created() { |
||||||
|
this.val = this.value |
||||||
|
this.form = this.getForm('uniForms') |
||||||
|
this.formItem = this.getForm('uniFormsItem') |
||||||
|
if (this.formItem) { |
||||||
|
if (this.formItem.name) { |
||||||
|
this.rename = this.formItem.name |
||||||
|
this.form.inputChildrens.push(this) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
}, |
||||||
|
mounted() { |
||||||
|
// this.onInput = throttle(this.input, 500) |
||||||
|
this.$nextTick(() => { |
||||||
|
this.focused = this.focus |
||||||
|
}) |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
/** |
||||||
|
* 初始化变量值 |
||||||
|
*/ |
||||||
|
init() { |
||||||
|
|
||||||
|
}, |
||||||
|
onClickIcon(type) { |
||||||
|
this.$emit('iconClick', type) |
||||||
|
}, |
||||||
|
/** |
||||||
|
* 获取父元素实例 |
||||||
|
*/ |
||||||
|
getForm(name = 'uniForms') { |
||||||
|
let parent = this.$parent; |
||||||
|
let parentName = parent.$options.name; |
||||||
|
while (parentName !== name) { |
||||||
|
parent = parent.$parent; |
||||||
|
if (!parent) return false; |
||||||
|
parentName = parent.$options.name; |
||||||
|
} |
||||||
|
return parent; |
||||||
|
}, |
||||||
|
|
||||||
|
onEyes() { |
||||||
|
this.showPassword = !this.showPassword |
||||||
|
}, |
||||||
|
onInput(event) { |
||||||
|
let value = event.detail.value; |
||||||
|
// 判断是否去除空格 |
||||||
|
if (this.trim) value = this.trimStr(value); |
||||||
|
if (this.errMsg) this.errMsg = '' |
||||||
|
this.val = value |
||||||
|
this.$emit('input', value); |
||||||
|
}, |
||||||
|
|
||||||
|
onFocus(event) { |
||||||
|
this.focused = true; |
||||||
|
this.$emit('focus', event); |
||||||
|
}, |
||||||
|
onBlur(event) { |
||||||
|
let value = event.detail.value; |
||||||
|
// 最开始使用的是监听图标@touchstart事件,自从hx2.8.4后,此方法在微信小程序出错 |
||||||
|
// 这里改为监听点击事件,手点击清除图标时,同时也发生了@blur事件,导致图标消失而无法点击,这里做一个延时 |
||||||
|
setTimeout(() => { |
||||||
|
this.focused = false; |
||||||
|
}, 100); |
||||||
|
this.$emit('blur', event); |
||||||
|
}, |
||||||
|
onConfirm(e) { |
||||||
|
this.$emit('confirm', e.detail.value); |
||||||
|
}, |
||||||
|
onClear(event) { |
||||||
|
this.val = ''; |
||||||
|
this.$emit('input', ''); |
||||||
|
}, |
||||||
|
fieldClick() { |
||||||
|
this.$emit('click'); |
||||||
|
}, |
||||||
|
trimStr(str, pos = 'both') { |
||||||
|
if (pos == 'both') { |
||||||
|
return str.replace(/^\s+|\s+$/g, ''); |
||||||
|
} else if (pos == 'left') { |
||||||
|
return str.replace(/^\s*/, ''); |
||||||
|
} else if (pos == 'right') { |
||||||
|
return str.replace(/(\s*$)/g, ''); |
||||||
|
} else if (pos == 'all') { |
||||||
|
return str.replace(/\s+/g, ''); |
||||||
|
} else { |
||||||
|
return str; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.uni-easyinput { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
width: 80%; |
||||||
|
margin-left: 20%; |
||||||
|
/* #endif */ |
||||||
|
flex: 1; |
||||||
|
position: relative; |
||||||
|
// padding: 16px 14px; |
||||||
|
text-align: left; |
||||||
|
color: #333; |
||||||
|
font-size: 28rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-easyinput__content { |
||||||
|
flex: 1; |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
width: 100%; |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
flex-direction: row; |
||||||
|
align-items: center; |
||||||
|
box-sizing: border-box; |
||||||
|
min-height: 72rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-easyinput__content-input { |
||||||
|
position: relative; |
||||||
|
overflow: hidden; |
||||||
|
flex: 1; |
||||||
|
width: auto; |
||||||
|
line-height: 2; |
||||||
|
font-size: 28rpx; |
||||||
|
// padding-right: 10px; |
||||||
|
} |
||||||
|
|
||||||
|
.is-textarea { |
||||||
|
align-items: flex-start; |
||||||
|
} |
||||||
|
|
||||||
|
.is-textarea-icon { |
||||||
|
margin-top: 10rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-easyinput__content-textarea { |
||||||
|
position: relative; |
||||||
|
overflow: hidden; |
||||||
|
flex: 1; |
||||||
|
width: auto; |
||||||
|
line-height: 1.5; |
||||||
|
font-size: 28rpx; |
||||||
|
// padding-right: 10px; |
||||||
|
padding-top: 20rpx; |
||||||
|
padding-bottom: 20rpx; |
||||||
|
// box-sizing: border-box; |
||||||
|
min-height: 160rpx; |
||||||
|
height: 160rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.input-padding { |
||||||
|
padding-left: 20rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.content-clear-icon { |
||||||
|
padding: 0 10rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.label-icon { |
||||||
|
margin-right: 10rpx; |
||||||
|
margin-top: -2rpx; |
||||||
|
} |
||||||
|
|
||||||
|
// 显示边框 |
||||||
|
.is-input-border { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
flex-direction: row; |
||||||
|
align-items: center; |
||||||
|
border: 2rpx solid $uni-border-color; |
||||||
|
border-radius: 40rpx; |
||||||
|
box-sizing: border-box; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-easyinput__right { |
||||||
|
// margin-left: 5px; |
||||||
|
} |
||||||
|
|
||||||
|
// 必填 |
||||||
|
.is-required { |
||||||
|
color: $uni-color-error; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-error-message { |
||||||
|
position: absolute; |
||||||
|
bottom: -34rpx; |
||||||
|
left: 0; |
||||||
|
line-height: 24rpx; |
||||||
|
color: $uni-color-error; |
||||||
|
font-size: 24rpx; |
||||||
|
text-align: left; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-error-msg--boeder { |
||||||
|
position: relative; |
||||||
|
bottom: 0; |
||||||
|
line-height: 44rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.is-input-error-border { |
||||||
|
border-color: $uni-color-error; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-easyinput--border { |
||||||
|
margin-bottom: 0; |
||||||
|
padding: 20rpx 30rpx; |
||||||
|
// padding-bottom: 0; |
||||||
|
border-top: 2rpx #eee solid; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-easyinput-error { |
||||||
|
padding-bottom: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.is-first-border { |
||||||
|
border: none; |
||||||
|
} |
||||||
|
|
||||||
|
.is-disabled { |
||||||
|
background-color: #eee; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,440 @@ |
|||||||
|
<template> |
||||||
|
<view class="uni-forms-item" :class="{'uni-forms-item--border':border,'is-first-border':border&&isFirstBorder,'uni-forms-item-error':msg}"> |
||||||
|
<view class="uni-forms-item__inner" :class="['is-direction-'+labelPos,]"> |
||||||
|
<view v-if="label" class="uni-forms-item__label" :style="{width:labelWid+'px',justifyContent: justifyContent}"> |
||||||
|
<slot name="left"> |
||||||
|
<uni-icons v-if="leftIcon" class="label-icon" size="16" :type="leftIcon" :color="iconColor" /> |
||||||
|
<text v-if="required" class="is-required">*</text> |
||||||
|
<text class="label-color">{{label}}</text> |
||||||
|
</slot> |
||||||
|
</view> |
||||||
|
<view class="uni-forms-item__content" :class="{'is-input-error-border': msg}"> |
||||||
|
<slot></slot> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="uni-error-message" :class="{'uni-error-msg--boeder':border}" :style="{ |
||||||
|
paddingLeft: (labelPos === 'left'? Number(labelWid)+5:5) + 'px' |
||||||
|
}">{{ showMsg === 'undertext' ? msg:'' }}</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
/** |
||||||
|
* Field 输入框 |
||||||
|
* @description 此组件可以实现表单的输入与校验,包括 "text" 和 "textarea" 类型。 |
||||||
|
* @tutorial https://ext.dcloud.net.cn/plugin?id=21001 |
||||||
|
* @property {Boolean} required 是否必填,左边显示红色"*"号(默认false) |
||||||
|
* @property {String} validateTrigger = [bind|submit] 校验触发器方式 默认 submit 可选 |
||||||
|
* @value bind 发生变化时触发 |
||||||
|
* @value submit 提交时触发 |
||||||
|
* @property {String } leftIcon label左边的图标,限 uni-ui 的图标名称 |
||||||
|
* @property {String } iconColor 左边通过icon配置的图标的颜色(默认#606266) |
||||||
|
* @property {String } label 输入框左边的文字提示 |
||||||
|
* @property {Number } labelWidth label的宽度,单位px(默认65) |
||||||
|
* @property {String } labelAlign = [left|center|right] label的文字对齐方式(默认left) |
||||||
|
* @value left label 左侧显示 |
||||||
|
* @value center label 居中 |
||||||
|
* @value right label 右侧对齐 |
||||||
|
* @property {String } labelPosition = [top|left] label的文字的位置(默认left) |
||||||
|
* @value top 顶部显示 label |
||||||
|
* @value left 左侧显示 label |
||||||
|
* @property {String } errorMessage 显示的错误提示内容,如果为空字符串或者false,则不显示错误信息 |
||||||
|
* @property {String } name 表单域的属性名,在使用校验规则时必填 |
||||||
|
*/ |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default { |
||||||
|
name: "uniFormsItem", |
||||||
|
props: { |
||||||
|
// 自定义内容 |
||||||
|
custom: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
// 是否显示报错信息 |
||||||
|
showMessage: { |
||||||
|
type: Boolean, |
||||||
|
default: true |
||||||
|
}, |
||||||
|
name: String, |
||||||
|
required: Boolean, |
||||||
|
validateTrigger: { |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
leftIcon: String, |
||||||
|
iconColor: { |
||||||
|
type: String, |
||||||
|
default: '#606266' |
||||||
|
}, |
||||||
|
label: String, |
||||||
|
// 左边标题的宽度单位px |
||||||
|
labelWidth: { |
||||||
|
type: [Number, String], |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
// 对齐方式,left|center|right |
||||||
|
labelAlign: { |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
// lable的位置,可选为 left-左边,top-上边 |
||||||
|
labelPosition: { |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
errorMessage: { |
||||||
|
type: [String, Boolean], |
||||||
|
default: '' |
||||||
|
} |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
errorTop: false, |
||||||
|
errorBottom: false, |
||||||
|
labelMarginBottom: '', |
||||||
|
errorWidth: '', |
||||||
|
errMsg: '', |
||||||
|
val: '', |
||||||
|
labelPos: '', |
||||||
|
labelWid: '', |
||||||
|
labelAli: '', |
||||||
|
showMsg: 'undertext', |
||||||
|
border: false, |
||||||
|
isFirstBorder: false |
||||||
|
}; |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
msg() { |
||||||
|
return this.errorMessage || this.errMsg; |
||||||
|
}, |
||||||
|
fieldStyle() { |
||||||
|
let style = {} |
||||||
|
if (this.labelPos == 'top') { |
||||||
|
style.padding = '0 0' |
||||||
|
this.labelMarginBottom = '6px' |
||||||
|
} |
||||||
|
if (this.labelPos == 'left' && this.msg !== false && this.msg != '') { |
||||||
|
style.paddingBottom = '0px' |
||||||
|
this.errorBottom = true |
||||||
|
this.errorTop = false |
||||||
|
} else if (this.labelPos == 'top' && this.msg !== false && this.msg != '') { |
||||||
|
this.errorBottom = false |
||||||
|
this.errorTop = true |
||||||
|
} else { |
||||||
|
// style.paddingBottom = '' |
||||||
|
this.errorTop = false |
||||||
|
this.errorBottom = false |
||||||
|
} |
||||||
|
return style |
||||||
|
}, |
||||||
|
|
||||||
|
// uni不支持在computed中写style.justifyContent = 'center'的形式,故用此方法 |
||||||
|
justifyContent() { |
||||||
|
if (this.labelAli === 'left') return 'flex-start'; |
||||||
|
if (this.labelAli === 'center') return 'center'; |
||||||
|
if (this.labelAli === 'right') return 'flex-end'; |
||||||
|
} |
||||||
|
|
||||||
|
}, |
||||||
|
watch: { |
||||||
|
validateTrigger(trigger) { |
||||||
|
this.formTrigger = trigger |
||||||
|
} |
||||||
|
}, |
||||||
|
created() { |
||||||
|
this.form = this.getForm() |
||||||
|
this.group = this.getForm('uniGroup') |
||||||
|
this.formRules = [] |
||||||
|
this.formTrigger = this.validateTrigger |
||||||
|
// if (this.form) { |
||||||
|
this.form.childrens.push(this) |
||||||
|
// } |
||||||
|
this.init() |
||||||
|
}, |
||||||
|
destroyed() { |
||||||
|
if (this.form) { |
||||||
|
this.form.childrens.forEach((item, index) => { |
||||||
|
if (item === this) { |
||||||
|
this.form.childrens.splice(index, 1) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
init() { |
||||||
|
if (this.form) { |
||||||
|
let { |
||||||
|
formRules, |
||||||
|
validator, |
||||||
|
formData, |
||||||
|
value, |
||||||
|
labelPosition, |
||||||
|
labelWidth, |
||||||
|
labelAlign, |
||||||
|
errShowType |
||||||
|
} = this.form |
||||||
|
|
||||||
|
this.labelPos = this.labelPosition ? this.labelPosition : labelPosition |
||||||
|
this.labelWid = this.label ? (this.labelWidth ? this.labelWidth : labelWidth):0 |
||||||
|
this.labelAli = this.labelAlign ? this.labelAlign : labelAlign |
||||||
|
// 判断第一个 item |
||||||
|
if (!this.form.isFirstBorder) { |
||||||
|
this.form.isFirstBorder = true |
||||||
|
this.isFirstBorder = true |
||||||
|
} |
||||||
|
// 判断 group 里的第一个 item |
||||||
|
if (this.group) { |
||||||
|
if (!this.group.isFirstBorder) { |
||||||
|
this.group.isFirstBorder = true |
||||||
|
this.isFirstBorder = true |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
this.border = this.form.border |
||||||
|
this.showMsg = errShowType |
||||||
|
|
||||||
|
if (formRules) { |
||||||
|
this.formRules = formRules[this.name] || {} |
||||||
|
} |
||||||
|
|
||||||
|
this.validator = validator |
||||||
|
|
||||||
|
// if (this.name) { |
||||||
|
// // formData[this.name] = value.hasOwnProperty(this.name) ? value[this.name] : this.form._getValue(this, '') |
||||||
|
// } |
||||||
|
} else { |
||||||
|
this.labelPos = this.labelPosition || 'left' |
||||||
|
this.labelWid = this.labelWidth || 65 |
||||||
|
this.labelAli = this.labelAlign || 'left' |
||||||
|
} |
||||||
|
}, |
||||||
|
/** |
||||||
|
* 获取父元素实例 |
||||||
|
*/ |
||||||
|
getForm(name = 'uniForms') { |
||||||
|
let parent = this.$parent; |
||||||
|
let parentName = parent.$options.name; |
||||||
|
while (parentName !== name) { |
||||||
|
parent = parent.$parent; |
||||||
|
if (!parent) return false |
||||||
|
parentName = parent.$options.name; |
||||||
|
} |
||||||
|
return parent; |
||||||
|
}, |
||||||
|
/** |
||||||
|
* 移除该表单项的校验结果 |
||||||
|
*/ |
||||||
|
clearValidate() { |
||||||
|
this.errMsg = '' |
||||||
|
}, |
||||||
|
setValue(value){ |
||||||
|
if (this.name) { |
||||||
|
if(this.errMsg) this.errMsg = '' |
||||||
|
this.form.formData[this.name] = this.form._getValue(this, value) |
||||||
|
} |
||||||
|
}, |
||||||
|
/** |
||||||
|
* 校验规则 |
||||||
|
* @param {Object} value |
||||||
|
*/ |
||||||
|
async triggerCheck(value, callback) { |
||||||
|
let promise = null; |
||||||
|
this.errMsg = '' |
||||||
|
// if no callback, return promise |
||||||
|
if (callback && typeof callback !== 'function' && Promise) { |
||||||
|
promise = new Promise((resolve, reject) => { |
||||||
|
callback = function(valid) { |
||||||
|
!valid ? resolve(valid) : reject(valid) |
||||||
|
}; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
if (!this.validator) { |
||||||
|
typeof callback === 'function' && callback(null); |
||||||
|
if (promise) return promise |
||||||
|
} |
||||||
|
|
||||||
|
const isNoField = this.isRequired(this.formRules.rules || []) |
||||||
|
|
||||||
|
|
||||||
|
let isTrigger = this.isTrigger(this.formRules.validateTrigger, this.validateTrigger, this.form.validateTrigger) |
||||||
|
|
||||||
|
let result = null |
||||||
|
|
||||||
|
if (!(!isTrigger)) { |
||||||
|
result = this.validator && (await this.validator.validateUpdate({ |
||||||
|
[this.name]: value |
||||||
|
}, this.form.formData)) |
||||||
|
} |
||||||
|
// 判断是否必填 |
||||||
|
if (!isNoField && !value) { |
||||||
|
result = null |
||||||
|
} |
||||||
|
|
||||||
|
if (isTrigger && result && result.errorMessage) { |
||||||
|
if (this.form.errShowType === 'toast') { |
||||||
|
uni.showToast({ |
||||||
|
title: result.errorMessage || '校验错误', |
||||||
|
icon: 'none' |
||||||
|
}) |
||||||
|
} |
||||||
|
if (this.form.errShowType === 'modal') { |
||||||
|
uni.showModal({ |
||||||
|
title: '提示', |
||||||
|
content: result.errorMessage || '校验错误' |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
this.errMsg = !result ? '' : result.errorMessage |
||||||
|
this.form.validateCheck(result ? result : null) |
||||||
|
typeof callback === 'function' && callback(result ? result : null); |
||||||
|
if (promise) return promise |
||||||
|
|
||||||
|
}, |
||||||
|
/** |
||||||
|
* 触发时机 |
||||||
|
* @param {Object} event |
||||||
|
*/ |
||||||
|
isTrigger(rule, itemRlue, parentRule) { |
||||||
|
let rl = true; |
||||||
|
// bind submit |
||||||
|
if (rule === 'submit' || !rule) { |
||||||
|
if (rule === undefined) { |
||||||
|
if (itemRlue !== 'bind') { |
||||||
|
if (!itemRlue) { |
||||||
|
return parentRule === 'bind' ? true : false |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
return true |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
return true; |
||||||
|
}, |
||||||
|
// 是否有必填字段 |
||||||
|
isRequired(rules) { |
||||||
|
let isNoField = false |
||||||
|
for (let i = 0; i < rules.length; i++) { |
||||||
|
const ruleData = rules[i] |
||||||
|
if (ruleData.required) { |
||||||
|
isNoField = true |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
return isNoField |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.uni-forms-item { |
||||||
|
position: relative; |
||||||
|
// padding: 16px 14px; |
||||||
|
text-align: left; |
||||||
|
color: #333; |
||||||
|
font-size: 28rpx; |
||||||
|
margin-bottom: 44rpx; |
||||||
|
background-color: #fff; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-forms-item__inner { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
// flex-direction: row; |
||||||
|
// align-items: center; |
||||||
|
} |
||||||
|
|
||||||
|
.is-direction-left { |
||||||
|
flex-direction: row; |
||||||
|
} |
||||||
|
|
||||||
|
.is-direction-top { |
||||||
|
flex-direction: column; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-forms-item__label { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
flex-shrink: 0; |
||||||
|
/* #endif */ |
||||||
|
flex-direction: row; |
||||||
|
align-items: center; |
||||||
|
font-size: 28rpx; |
||||||
|
color: #333; |
||||||
|
width: 130rpx; |
||||||
|
// line-height: 2; |
||||||
|
// margin-top: 3px; |
||||||
|
padding: 10rpx 0; |
||||||
|
box-sizing: border-box; |
||||||
|
height: 72rpx; |
||||||
|
margin-right: 10rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-forms-item__content { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
width: 100%; |
||||||
|
// display: flex; |
||||||
|
/* #endif */ |
||||||
|
// flex: 1; |
||||||
|
// flex-direction: row; |
||||||
|
// align-items: center; |
||||||
|
box-sizing: border-box; |
||||||
|
min-height: 72rpx; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
.label-icon { |
||||||
|
margin-right: 10rpx; |
||||||
|
margin-top: -2px; |
||||||
|
} |
||||||
|
|
||||||
|
// 必填 |
||||||
|
.is-required { |
||||||
|
color: $uni-color-error; |
||||||
|
} |
||||||
|
|
||||||
|
.label-color{ |
||||||
|
color: #00B9FF; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-error-message { |
||||||
|
position: absolute; |
||||||
|
bottom: -34rpx; |
||||||
|
left: 0; |
||||||
|
line-height: 24rpx; |
||||||
|
color: $uni-color-error; |
||||||
|
font-size: 24rpx; |
||||||
|
text-align: left; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-error-msg--boeder { |
||||||
|
position: relative; |
||||||
|
bottom: 0; |
||||||
|
line-height: 44rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.is-input-error-border { |
||||||
|
border-color: $uni-color-error; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-forms-item--border { |
||||||
|
margin-bottom: 0; |
||||||
|
padding: 20rpx 30rpx; |
||||||
|
// padding-bottom: 0; |
||||||
|
border-top: 2rpx #eee solid; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-forms-item-error { |
||||||
|
padding-bottom: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.is-first-border { |
||||||
|
border: none; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,420 @@ |
|||||||
|
<template> |
||||||
|
<!-- --> |
||||||
|
<view class="uni-forms" :class="{'uni-forms--top':!border}"> |
||||||
|
<form @submit.stop="submitForm" @reset="resetForm"> |
||||||
|
<slot></slot> |
||||||
|
</form> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
/** |
||||||
|
* Forms 表单 |
||||||
|
* @description 由输入框、选择器、单选框、多选框等控件组成,用以收集、校验、提交数据 |
||||||
|
* @tutorial https://ext.dcloud.net.cn/plugin?id=2773 |
||||||
|
* @property {Object} rules 表单校验规则 |
||||||
|
* @property {String} validateTrigger = [bind|submit] 校验触发器方式 默认 submit 可选 |
||||||
|
* @value bind 发生变化时触发 |
||||||
|
* @value submit 提交时触发 |
||||||
|
* @property {String} labelPosition = [top|left] label 位置 默认 left 可选 |
||||||
|
* @value top 顶部显示 label |
||||||
|
* @value left 左侧显示 label |
||||||
|
* @property {String} labelWidth label 宽度,默认 65px |
||||||
|
* @property {String} labelAlign = [left|center|right] label 居中方式 默认 left 可选 |
||||||
|
* @value left label 左侧显示 |
||||||
|
* @value center label 居中 |
||||||
|
* @value right label 右侧对齐 |
||||||
|
* @property {String} errShowType = [undertext|toast|modal] 校验错误信息提示方式 |
||||||
|
* @value undertext 错误信息在底部显示 |
||||||
|
* @value toast 错误信息toast显示 |
||||||
|
* @value modal 错误信息modal显示 |
||||||
|
* @event {Function} submit 提交时触发 |
||||||
|
*/ |
||||||
|
import Vue from 'vue' |
||||||
|
Vue.prototype.binddata = function(name, value, formName) { |
||||||
|
if (formName) { |
||||||
|
this.$refs[formName].setValue(name, value) |
||||||
|
} else { |
||||||
|
let formVm |
||||||
|
for (let i in this.$refs) { |
||||||
|
const vm = this.$refs[i] |
||||||
|
if (vm && vm.$options && vm.$options.name === 'uniForms') { |
||||||
|
formVm = vm |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
if (!formVm) return console.error('当前 uni-froms 组件缺少 ref 属性') |
||||||
|
formVm.setValue(name, value) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
import Validator from './validate.js' |
||||||
|
|
||||||
|
export default { |
||||||
|
name: 'uniForms', |
||||||
|
props: { |
||||||
|
value: { |
||||||
|
type: Object, |
||||||
|
default () { |
||||||
|
return {} |
||||||
|
} |
||||||
|
}, |
||||||
|
// 表单校验规则 |
||||||
|
rules: { |
||||||
|
type: Object, |
||||||
|
default () { |
||||||
|
return {} |
||||||
|
} |
||||||
|
}, |
||||||
|
// 校验触发器方式,默认 关闭 |
||||||
|
validateTrigger: { |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
// label 位置,可选值 top/left |
||||||
|
labelPosition: { |
||||||
|
type: String, |
||||||
|
default: 'left' |
||||||
|
}, |
||||||
|
// label 宽度,单位 px |
||||||
|
labelWidth: { |
||||||
|
type: [String, Number], |
||||||
|
default: 65 |
||||||
|
}, |
||||||
|
// label 居中方式,可选值 left/center/right |
||||||
|
labelAlign: { |
||||||
|
type: String, |
||||||
|
default: 'left' |
||||||
|
}, |
||||||
|
errShowType: { |
||||||
|
type: String, |
||||||
|
default: 'undertext' |
||||||
|
}, |
||||||
|
border: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
} |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
formData: {} |
||||||
|
}; |
||||||
|
}, |
||||||
|
watch: { |
||||||
|
rules(newVal) { |
||||||
|
this.init(newVal) |
||||||
|
}, |
||||||
|
trigger(trigger) { |
||||||
|
this.formTrigger = trigger |
||||||
|
}, |
||||||
|
value: { |
||||||
|
handler(newVal) { |
||||||
|
if (this.isChildEdit) { |
||||||
|
this.isChildEdit = false |
||||||
|
return |
||||||
|
} |
||||||
|
this.childrens.forEach((item) => { |
||||||
|
if (item.name) { |
||||||
|
const formDataValue = newVal.hasOwnProperty(item.name) ? newVal[item.name] : null |
||||||
|
this.formData[item.name] = this._getValue(item, formDataValue) |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
deep: true |
||||||
|
} |
||||||
|
}, |
||||||
|
created() { |
||||||
|
let _this = this |
||||||
|
this.childrens = [] |
||||||
|
this.inputChildrens = [] |
||||||
|
this.checkboxChildrens = [] |
||||||
|
this.formRules = [] |
||||||
|
this.init(this.rules) |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
init(formRules) { |
||||||
|
if (Object.keys(formRules).length > 0) { |
||||||
|
this.formTrigger = this.trigger |
||||||
|
this.formRules = formRules |
||||||
|
if (!this.validator) { |
||||||
|
this.validator = new Validator(formRules) |
||||||
|
} |
||||||
|
} |
||||||
|
this.childrens.forEach((item) => { |
||||||
|
item.init() |
||||||
|
}) |
||||||
|
}, |
||||||
|
/** |
||||||
|
* 设置校验规则 |
||||||
|
* @param {Object} formRules |
||||||
|
*/ |
||||||
|
setRules(formRules) { |
||||||
|
this.init(formRules) |
||||||
|
}, |
||||||
|
/** |
||||||
|
* 公开给用户使用 |
||||||
|
* 设置自定义表单组件 value 值 |
||||||
|
* @param {String} name 字段名称 |
||||||
|
* @param {String} value 字段值 |
||||||
|
*/ |
||||||
|
setValue(name, value, callback) { |
||||||
|
let example = this.childrens.find(child => child.name === name) |
||||||
|
if (!example) return null |
||||||
|
this.isChildEdit = true |
||||||
|
value = this._getValue(example, value) |
||||||
|
this.formData[name] = value |
||||||
|
example.val = value |
||||||
|
this.$emit('input', Object.assign({}, this.value, this.formData)) |
||||||
|
return example.triggerCheck(value, callback) |
||||||
|
}, |
||||||
|
|
||||||
|
/** |
||||||
|
* TODO 表单提交, 小程序暂不支持这种用法 |
||||||
|
* @param {Object} event |
||||||
|
*/ |
||||||
|
submitForm(event) { |
||||||
|
const value = event.detail.value |
||||||
|
return this.validateAll(value || this.formData, 'submit') |
||||||
|
}, |
||||||
|
/** |
||||||
|
* 表单重置 |
||||||
|
* @param {Object} event |
||||||
|
*/ |
||||||
|
resetForm(event) { |
||||||
|
this.childrens.forEach(item => { |
||||||
|
item.errMsg = '' |
||||||
|
const inputComp = this.inputChildrens.find(child => child.rename === item.name) |
||||||
|
if (inputComp) { |
||||||
|
inputComp.errMsg = '' |
||||||
|
inputComp.$emit('input', inputComp.multiple?[]:'') |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
this.isChildEdit = true |
||||||
|
this.childrens.forEach((item) => { |
||||||
|
if (item.name) { |
||||||
|
this.formData[item.name] = this._getValue(item, '') |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
this.$emit('input', this.formData) |
||||||
|
this.$emit('reset', event) |
||||||
|
}, |
||||||
|
|
||||||
|
/** |
||||||
|
* 触发表单校验,通过 @validate 获取 |
||||||
|
* @param {Object} validate |
||||||
|
*/ |
||||||
|
validateCheck(validate) { |
||||||
|
if (validate === null) validate = null |
||||||
|
this.$emit('validate', validate) |
||||||
|
}, |
||||||
|
/** |
||||||
|
* 校验所有或者部分表单 |
||||||
|
*/ |
||||||
|
async validateAll(invalidFields, type, callback) { |
||||||
|
|
||||||
|
this.childrens.forEach(item => { |
||||||
|
item.errMsg = '' |
||||||
|
}) |
||||||
|
|
||||||
|
let promise; |
||||||
|
if (!callback && typeof callback !== 'function' && Promise) { |
||||||
|
promise = new Promise((resolve, reject) => { |
||||||
|
callback = function(valid, invalidFields) { |
||||||
|
!valid ? resolve(invalidFields) : reject(valid); |
||||||
|
}; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
let fieldsValue = {} |
||||||
|
let tempInvalidFields = Object.assign({}, invalidFields) |
||||||
|
|
||||||
|
Object.keys(this.formRules).forEach(item => { |
||||||
|
const values = this.formRules[item] |
||||||
|
const rules = (values && values.rules) || [] |
||||||
|
let isNoField = false |
||||||
|
for (let i = 0; i < rules.length; i++) { |
||||||
|
const rule = rules[i] |
||||||
|
if (rule.required) { |
||||||
|
isNoField = true |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 如果存在 required 才会将内容插入校验对象 |
||||||
|
if (!isNoField && (!tempInvalidFields[item] && tempInvalidFields[item] !== false)) { |
||||||
|
delete tempInvalidFields[item] |
||||||
|
} |
||||||
|
|
||||||
|
}) |
||||||
|
// 循环字段是否存在于校验规则中 |
||||||
|
for (let i in this.formRules) { |
||||||
|
for (let j in tempInvalidFields) { |
||||||
|
if (i === j) { |
||||||
|
fieldsValue[i] = tempInvalidFields[i] |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
let result = [] |
||||||
|
let example = null |
||||||
|
if (this.validator) { |
||||||
|
for (let i in fieldsValue) { |
||||||
|
const resultData = await this.validator.validateUpdate({ |
||||||
|
[i]: fieldsValue[i] |
||||||
|
}, this.formData) |
||||||
|
if (resultData) { |
||||||
|
example = this.childrens.find(child => child.name === resultData.key) |
||||||
|
const inputComp = this.inputChildrens.find(child => child.rename === example.name) |
||||||
|
if (inputComp) { |
||||||
|
inputComp.errMsg = resultData.errorMessage |
||||||
|
} |
||||||
|
result.push(resultData) |
||||||
|
if (this.errShowType === 'undertext') { |
||||||
|
if (example) example.errMsg = resultData.errorMessage |
||||||
|
} else { |
||||||
|
if (this.errShowType === 'toast') { |
||||||
|
uni.showToast({ |
||||||
|
title: resultData.errorMessage || '校验错误', |
||||||
|
icon: 'none' |
||||||
|
}) |
||||||
|
break |
||||||
|
} else if (this.errShowType === 'modal') { |
||||||
|
uni.showModal({ |
||||||
|
title: '提示', |
||||||
|
content: resultData.errorMessage || '校验错误' |
||||||
|
}) |
||||||
|
break |
||||||
|
} else { |
||||||
|
if (example) example.errMsg = resultData.errorMessage |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (Array.isArray(result)) { |
||||||
|
if (result.length === 0) result = null |
||||||
|
} |
||||||
|
if (type === 'submit') { |
||||||
|
this.$emit('submit', { |
||||||
|
detail: { |
||||||
|
value: invalidFields, |
||||||
|
errors: result |
||||||
|
} |
||||||
|
}) |
||||||
|
} else { |
||||||
|
this.$emit('validate', result) |
||||||
|
} |
||||||
|
callback && typeof callback === 'function' && callback(result, invalidFields) |
||||||
|
if (promise && callback) { |
||||||
|
return promise |
||||||
|
} else { |
||||||
|
return null |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
/** |
||||||
|
* 外部调用方法 |
||||||
|
* 手动提交校验表单 |
||||||
|
* 对整个表单进行校验的方法,参数为一个回调函数。 |
||||||
|
*/ |
||||||
|
submit(callback) { |
||||||
|
// Object.assign(this.formData,formData) |
||||||
|
return this.validateAll(this.formData, 'submit', callback) |
||||||
|
}, |
||||||
|
|
||||||
|
/** |
||||||
|
* 外部调用方法 |
||||||
|
* 校验表单 |
||||||
|
* 对整个表单进行校验的方法,参数为一个回调函数。 |
||||||
|
*/ |
||||||
|
validate(callback) { |
||||||
|
return this.validateAll(this.formData, '', callback) |
||||||
|
}, |
||||||
|
|
||||||
|
/** |
||||||
|
* 部分表单校验 |
||||||
|
* @param {Object} props |
||||||
|
* @param {Object} cb |
||||||
|
*/ |
||||||
|
validateField(props, callback) { |
||||||
|
props = [].concat(props); |
||||||
|
let invalidFields = {} |
||||||
|
this.childrens.forEach(item => { |
||||||
|
// item.parentVal((val, name) => { |
||||||
|
if (props.indexOf(item.name) !== -1) { |
||||||
|
invalidFields = Object.assign({}, invalidFields, { |
||||||
|
[item.name]: this.formData[item.name] |
||||||
|
}) |
||||||
|
} |
||||||
|
// }) |
||||||
|
|
||||||
|
}) |
||||||
|
return this.validateAll(invalidFields, '', callback) |
||||||
|
}, |
||||||
|
|
||||||
|
/** |
||||||
|
* 对整个表单进行重置,将所有字段值重置为初始值并移除校验结果 |
||||||
|
*/ |
||||||
|
resetFields() { |
||||||
|
this.resetForm() |
||||||
|
}, |
||||||
|
|
||||||
|
/** |
||||||
|
* 移除表单项的校验结果。传入待移除的表单项的 prop 属性或者 prop 组成的数组,如不传则移除整个表单的校验结果 |
||||||
|
*/ |
||||||
|
clearValidate(props) { |
||||||
|
props = [].concat(props); |
||||||
|
this.childrens.forEach(item => { |
||||||
|
const inputComp = this.inputChildrens.find(child => child.rename === item.name) |
||||||
|
if (props.length === 0) { |
||||||
|
item.errMsg = '' |
||||||
|
if (inputComp) { |
||||||
|
inputComp.errMsg = '' |
||||||
|
} |
||||||
|
} else { |
||||||
|
if (props.indexOf(item.name) !== -1) { |
||||||
|
item.errMsg = '' |
||||||
|
if (inputComp) { |
||||||
|
inputComp.errMsg = '' |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
// 把 value 转换成指定的类型 |
||||||
|
_getValue(item, value) { |
||||||
|
const rules = item.formRules.rules || [] |
||||||
|
const isRuleNum = rules.find(val => val.format && this.type_filter(val.format)) |
||||||
|
const isRuleBool = rules.find(val => val.format && val.format === 'boolean' || val.format === 'bool') |
||||||
|
// 输入值为 number |
||||||
|
if (isRuleNum) { |
||||||
|
value = value === '' || value === null ? null : Number(value) |
||||||
|
} |
||||||
|
// 简单判断真假值 |
||||||
|
if (isRuleBool) { |
||||||
|
value = !value ? false : true |
||||||
|
} |
||||||
|
return value |
||||||
|
}, |
||||||
|
// 过滤数字类型 |
||||||
|
type_filter(format) { |
||||||
|
return format === 'int' || format === 'double' || format === 'number' |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.uni-forms { |
||||||
|
overflow: hidden; |
||||||
|
// padding: 10px 15px; |
||||||
|
// background-color: #fff; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-forms--top { |
||||||
|
padding: 20rpx 30rpx; |
||||||
|
// padding-top: 22px; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,442 @@ |
|||||||
|
|
||||||
|
var pattern = { |
||||||
|
email: /^\S+?@\S+?\.\S+?$/, |
||||||
|
url: new RegExp("^(?!mailto:)(?:(?:http|https|ftp)://|//)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-*)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-*)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?$", 'i') |
||||||
|
}; |
||||||
|
|
||||||
|
const FORMAT_MAPPING = { |
||||||
|
"int": 'number', |
||||||
|
"bool": 'boolean', |
||||||
|
"double": 'number', |
||||||
|
"long": 'number', |
||||||
|
"password": 'string' |
||||||
|
} |
||||||
|
|
||||||
|
function formatMessage(args, resources) { |
||||||
|
var defaultMessage = ['label'] |
||||||
|
defaultMessage.forEach((item) => { |
||||||
|
if (args[item] === undefined) { |
||||||
|
args[item] = '' |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
let str = resources |
||||||
|
for (let key in args) { |
||||||
|
let reg = new RegExp('{' + key + '}') |
||||||
|
str = str.replace(reg, args[key]) |
||||||
|
} |
||||||
|
return str |
||||||
|
} |
||||||
|
|
||||||
|
function isEmptyValue(value, type) { |
||||||
|
if (value === undefined || value === null) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
if (typeof value === 'string' && !value) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
if (Array.isArray(value) && !value.length) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
if (type === 'object' && !Object.keys(value).length) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
const types = { |
||||||
|
integer(value) { |
||||||
|
return types.number(value) && parseInt(value, 10) === value; |
||||||
|
}, |
||||||
|
string(value) { |
||||||
|
return typeof value === 'string'; |
||||||
|
}, |
||||||
|
number(value) { |
||||||
|
if (isNaN(value)) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
return typeof value === 'number'; |
||||||
|
}, |
||||||
|
"boolean": function (value) { |
||||||
|
return typeof value === 'boolean'; |
||||||
|
}, |
||||||
|
"float": function (value) { |
||||||
|
return types.number(value) && !types.integer(value); |
||||||
|
}, |
||||||
|
array(value) { |
||||||
|
return Array.isArray(value); |
||||||
|
}, |
||||||
|
object(value) { |
||||||
|
return typeof value === 'object' && !types.array(value); |
||||||
|
}, |
||||||
|
date(value) { |
||||||
|
var v |
||||||
|
if (value instanceof Date) { |
||||||
|
v = value; |
||||||
|
} else { |
||||||
|
v = new Date(value); |
||||||
|
} |
||||||
|
return typeof v.getTime === 'function' && typeof v.getMonth === 'function' && typeof v.getYear === 'function' && !isNaN(v.getTime()); |
||||||
|
}, |
||||||
|
timestamp(value) { |
||||||
|
if (!this.integer(value) || Math.abs(value).toString().length > 16) { |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
return this.date(value); |
||||||
|
}, |
||||||
|
email(value) { |
||||||
|
return typeof value === 'string' && !!value.match(pattern.email) && value.length < 255; |
||||||
|
}, |
||||||
|
url(value) { |
||||||
|
return typeof value === 'string' && !!value.match(pattern.url); |
||||||
|
}, |
||||||
|
pattern(reg, value) { |
||||||
|
try { |
||||||
|
return new RegExp(reg).test(value); |
||||||
|
} catch (e) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
}, |
||||||
|
method(value) { |
||||||
|
return typeof value === 'function'; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class RuleValidator { |
||||||
|
|
||||||
|
constructor(message) { |
||||||
|
this._message = message |
||||||
|
} |
||||||
|
|
||||||
|
async validateRule(key, value, data, allData) { |
||||||
|
var result = null |
||||||
|
|
||||||
|
let rules = key.rules |
||||||
|
|
||||||
|
let hasRequired = rules.findIndex((item) => { |
||||||
|
return item.required |
||||||
|
}) |
||||||
|
if (hasRequired < 0) { |
||||||
|
if (value === null || value === undefined) { |
||||||
|
return result |
||||||
|
} |
||||||
|
if (typeof value === 'string' && !value.length) { |
||||||
|
return result |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var message = this._message |
||||||
|
|
||||||
|
if (rules === undefined) { |
||||||
|
return message['default'] |
||||||
|
} |
||||||
|
|
||||||
|
for (var i = 0; i < rules.length; i++) { |
||||||
|
let rule = rules[i] |
||||||
|
let vt = this._getValidateType(rule) |
||||||
|
|
||||||
|
if (key.label !== undefined) { |
||||||
|
Object.assign(rule, { |
||||||
|
label: key.label |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
if (RuleValidatorHelper[vt]) { |
||||||
|
result = RuleValidatorHelper[vt](rule, value, message) |
||||||
|
if (result != null) { |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (rule.validateExpr) { |
||||||
|
let now = Date.now() |
||||||
|
let resultExpr = rule.validateExpr(value, allData, now) |
||||||
|
if (resultExpr === false) { |
||||||
|
result = this._getMessage(rule, rule.errorMessage || this._message['default']) |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (rule.validateFunction) { |
||||||
|
result = await this.validateFunction(rule, value, data, allData, vt) |
||||||
|
if (result !== null) { |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return result |
||||||
|
} |
||||||
|
|
||||||
|
async validateFunction(rule, value, data, allData, vt) { |
||||||
|
let result = null |
||||||
|
try { |
||||||
|
let callbackMessage = null |
||||||
|
const res = await rule.validateFunction(rule, value, allData || data, (message) => { |
||||||
|
callbackMessage = message |
||||||
|
}) |
||||||
|
if (callbackMessage || (typeof res === 'string' && res) || res === false) { |
||||||
|
result = this._getMessage(rule, callbackMessage || res, vt) |
||||||
|
} |
||||||
|
} catch (e) { |
||||||
|
result = this._getMessage(rule, e.message, vt) |
||||||
|
} |
||||||
|
return result |
||||||
|
} |
||||||
|
|
||||||
|
_getMessage(rule, message, vt) { |
||||||
|
return formatMessage(rule, message || rule.errorMessage || this._message[vt] || message['default']) |
||||||
|
} |
||||||
|
|
||||||
|
_getValidateType(rule) { |
||||||
|
// TODO
|
||||||
|
var result = '' |
||||||
|
if (rule.required) { |
||||||
|
result = 'required' |
||||||
|
} else if (rule.format) { |
||||||
|
result = 'format' |
||||||
|
} else if (rule.range) { |
||||||
|
result = 'range' |
||||||
|
} else if (rule.maximum || rule.minimum) { |
||||||
|
result = 'rangeNumber' |
||||||
|
} else if (rule.maxLength || rule.minLength) { |
||||||
|
result = 'rangeLength' |
||||||
|
} else if (rule.pattern) { |
||||||
|
result = 'pattern' |
||||||
|
} |
||||||
|
return result |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const RuleValidatorHelper = { |
||||||
|
required(rule, value, message) { |
||||||
|
if (rule.required && isEmptyValue(value, rule.format || typeof value)) { |
||||||
|
return formatMessage(rule, rule.errorMessage || message.required); |
||||||
|
} |
||||||
|
|
||||||
|
return null |
||||||
|
}, |
||||||
|
|
||||||
|
range(rule, value, message) { |
||||||
|
const { range, errorMessage } = rule; |
||||||
|
|
||||||
|
let list = new Array(range.length); |
||||||
|
for (let i = 0; i < range.length; i++) { |
||||||
|
const item = range[i]; |
||||||
|
if (types.object(item) && item.value !== undefined) { |
||||||
|
list[i] = item.value; |
||||||
|
} else { |
||||||
|
list[i] = item; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
let result = false |
||||||
|
if (Array.isArray(value)) { |
||||||
|
result = (new Set(value.concat(list)).size === list.length); |
||||||
|
} else { |
||||||
|
if (list.indexOf(value) > -1) { |
||||||
|
result = true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (!result) { |
||||||
|
return formatMessage(rule, errorMessage || message['enum']); |
||||||
|
} |
||||||
|
|
||||||
|
return null |
||||||
|
}, |
||||||
|
|
||||||
|
rangeNumber(rule, value, message) { |
||||||
|
if (!types.number(value)) { |
||||||
|
return formatMessage(rule, rule.errorMessage || message.pattern.mismatch); |
||||||
|
} |
||||||
|
|
||||||
|
let { minimum, maximum, exclusiveMinimum, exclusiveMaximum } = rule; |
||||||
|
let min = exclusiveMinimum ? value <= minimum : value < minimum; |
||||||
|
let max = exclusiveMaximum ? value >= maximum : value > maximum; |
||||||
|
|
||||||
|
if (minimum !== undefined && min) { |
||||||
|
return formatMessage(rule, rule.errorMessage || message['number'].min) |
||||||
|
} else if (maximum !== undefined && max) { |
||||||
|
return formatMessage(rule, rule.errorMessage || message['number'].max) |
||||||
|
} else if (minimum !== undefined && maximum !== undefined && (min || max)) { |
||||||
|
return formatMessage(rule, rule.errorMessage || message['number'].range) |
||||||
|
} |
||||||
|
|
||||||
|
return null |
||||||
|
}, |
||||||
|
|
||||||
|
rangeLength(rule, value, message) { |
||||||
|
if (!types.string(value) && !types.array(value)) { |
||||||
|
return formatMessage(rule, rule.errorMessage || message.pattern.mismatch); |
||||||
|
} |
||||||
|
|
||||||
|
let min = rule.minLength; |
||||||
|
let max = rule.maxLength; |
||||||
|
let val = value.length; |
||||||
|
|
||||||
|
if (min !== undefined && val < min) { |
||||||
|
return formatMessage(rule, rule.errorMessage || message['length'].min) |
||||||
|
} else if (max !== undefined && val > max) { |
||||||
|
return formatMessage(rule, rule.errorMessage || message['length'].max) |
||||||
|
} else if (min !== undefined && max !== undefined && (val < min || val > max)) { |
||||||
|
return formatMessage(rule, rule.errorMessage || message['length'].range) |
||||||
|
} |
||||||
|
|
||||||
|
return null |
||||||
|
}, |
||||||
|
|
||||||
|
pattern(rule, value, message) { |
||||||
|
if (!types['pattern'](rule.pattern, value)) { |
||||||
|
return formatMessage(rule, rule.errorMessage || message.pattern.mismatch); |
||||||
|
} |
||||||
|
|
||||||
|
return null |
||||||
|
}, |
||||||
|
|
||||||
|
format(rule, value, message) { |
||||||
|
var customTypes = Object.keys(types); |
||||||
|
var format = FORMAT_MAPPING[rule.format] ? FORMAT_MAPPING[rule.format] : rule.format; |
||||||
|
|
||||||
|
if (customTypes.indexOf(format) > -1) { |
||||||
|
if (!types[format](value)) { |
||||||
|
return formatMessage(rule, rule.errorMessage || message.types[format]); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return null |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class SchemaValidator extends RuleValidator { |
||||||
|
|
||||||
|
constructor(schema, options) { |
||||||
|
super(SchemaValidator.message); |
||||||
|
|
||||||
|
this._schema = schema |
||||||
|
this._options = options || null |
||||||
|
} |
||||||
|
|
||||||
|
updateSchema(schema) { |
||||||
|
this._schema = schema |
||||||
|
} |
||||||
|
|
||||||
|
async validate(data, allData) { |
||||||
|
let result = this._checkFieldInSchema(data) |
||||||
|
if (!result) { |
||||||
|
result = await this.invokeValidate(data, false, allData) |
||||||
|
} |
||||||
|
return result.length ? result[0] : null |
||||||
|
} |
||||||
|
|
||||||
|
async validateAll(data, allData) { |
||||||
|
let result = this._checkFieldInSchema(data) |
||||||
|
if (!result) { |
||||||
|
result = await this.invokeValidate(data, true, allData) |
||||||
|
} |
||||||
|
return result |
||||||
|
} |
||||||
|
|
||||||
|
async validateUpdate(data, allData) { |
||||||
|
let result = this._checkFieldInSchema(data) |
||||||
|
if (!result) { |
||||||
|
result = await this.invokeValidateUpdate(data, false, allData) |
||||||
|
} |
||||||
|
return result.length ? result[0] : null |
||||||
|
} |
||||||
|
|
||||||
|
async invokeValidate(data, all, allData) { |
||||||
|
let result = [] |
||||||
|
let schema = this._schema |
||||||
|
for (let key in schema) { |
||||||
|
let value = schema[key] |
||||||
|
let errorMessage = await this.validateRule(value, data[key], data, allData) |
||||||
|
if (errorMessage != null) { |
||||||
|
result.push({ |
||||||
|
key, |
||||||
|
errorMessage |
||||||
|
}) |
||||||
|
if (!all) break |
||||||
|
} |
||||||
|
} |
||||||
|
return result |
||||||
|
} |
||||||
|
|
||||||
|
async invokeValidateUpdate(data, all, allData) { |
||||||
|
let result = [] |
||||||
|
for (let key in data) { |
||||||
|
let errorMessage = await this.validateRule(this._schema[key], data[key], data, allData) |
||||||
|
if (errorMessage != null) { |
||||||
|
result.push({ |
||||||
|
key, |
||||||
|
errorMessage |
||||||
|
}) |
||||||
|
if (!all) break |
||||||
|
} |
||||||
|
} |
||||||
|
return result |
||||||
|
} |
||||||
|
|
||||||
|
_checkFieldInSchema(data) { |
||||||
|
var keys = Object.keys(data) |
||||||
|
var keys2 = Object.keys(this._schema) |
||||||
|
if (new Set(keys.concat(keys2)).size === keys2.length) { |
||||||
|
return '' |
||||||
|
} |
||||||
|
return [{ |
||||||
|
key: 'invalid', |
||||||
|
errorMessage: SchemaValidator.message['defaultInvalid'] |
||||||
|
}] |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function Message() { |
||||||
|
return { |
||||||
|
default: '验证错误', |
||||||
|
defaultInvalid: '字段超出范围', |
||||||
|
required: '{label}必填', |
||||||
|
'enum': '{label}超出范围', |
||||||
|
whitespace: '{label}不能为空', |
||||||
|
date: { |
||||||
|
format: '{label}日期{value}格式无效', |
||||||
|
parse: '{label}日期无法解析,{value}无效', |
||||||
|
invalid: '{label}日期{value}无效' |
||||||
|
}, |
||||||
|
types: { |
||||||
|
string: '{label}类型无效', |
||||||
|
array: '{label}类型无效', |
||||||
|
object: '{label}类型无效', |
||||||
|
number: '{label}类型无效', |
||||||
|
date: '{label}类型无效', |
||||||
|
boolean: '{label}类型无效', |
||||||
|
integer: '{label}类型无效', |
||||||
|
float: '{label}类型无效', |
||||||
|
regexp: '{label}无效', |
||||||
|
email: '{label}类型无效', |
||||||
|
url: '{label}类型无效' |
||||||
|
}, |
||||||
|
length: { |
||||||
|
min: '{label}长度不能少于{minLength}', |
||||||
|
max: '{label}长度不能超过{maxLength}', |
||||||
|
range: '{label}必须介于{minLength}和{maxLength}之间' |
||||||
|
}, |
||||||
|
number: { |
||||||
|
min: '{label}不能小于{minimum}', |
||||||
|
max: '{label}不能大于{maximum}', |
||||||
|
range: '{label}必须介于{minimum}and{maximum}之间' |
||||||
|
}, |
||||||
|
pattern: { |
||||||
|
mismatch: '{label}格式不匹配' |
||||||
|
} |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
SchemaValidator.message = new Message(); |
||||||
|
|
||||||
|
export default SchemaValidator |
@ -0,0 +1,127 @@ |
|||||||
|
<template> |
||||||
|
<view class="uni-group" :class="['uni-group--'+mode ,margin?'group-margin':'']" :style="{marginTop: `${top}px` }"> |
||||||
|
<slot name="title"> |
||||||
|
<view v-if="title" class="uni-group__title" :style="{'padding-left':border?'30px':'15px'}"> |
||||||
|
<text class="uni-group__title-text">{{ title }}</text> |
||||||
|
</view> |
||||||
|
</slot> |
||||||
|
<view class="uni-group__content" :class="{'group-conent-padding':border}"> |
||||||
|
<slot /> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
/** |
||||||
|
* Group 分组 |
||||||
|
* @description 表单字段分组 |
||||||
|
* @tutorial https://ext.dcloud.net.cn/plugin?id=21002 |
||||||
|
* @property {String} title 主标题 |
||||||
|
* @property {Number} top 分组间隔 |
||||||
|
*/ |
||||||
|
export default { |
||||||
|
name: 'uniGroup', |
||||||
|
props: { |
||||||
|
title: { |
||||||
|
type: String, |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
top: { |
||||||
|
type: [Number, String], |
||||||
|
default: 10 |
||||||
|
}, |
||||||
|
mode: { |
||||||
|
type: String, |
||||||
|
default: 'default' |
||||||
|
} |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
margin: false, |
||||||
|
border: false |
||||||
|
} |
||||||
|
}, |
||||||
|
watch: { |
||||||
|
title(newVal) { |
||||||
|
if (uni.report && newVal !== '') { |
||||||
|
uni.report('title', newVal) |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
created() { |
||||||
|
this.form = this.getForm() |
||||||
|
if (this.form) { |
||||||
|
this.margin = true |
||||||
|
this.border = this.form.border |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
/** |
||||||
|
* 获取父元素实例 |
||||||
|
*/ |
||||||
|
getForm() { |
||||||
|
let parent = this.$parent; |
||||||
|
let parentName = parent.$options.name; |
||||||
|
while (parentName !== 'uniForms') { |
||||||
|
parent = parent.$parent; |
||||||
|
if (!parent) return false |
||||||
|
parentName = parent.$options.name; |
||||||
|
} |
||||||
|
return parent; |
||||||
|
}, |
||||||
|
onClick() { |
||||||
|
this.$emit('click') |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
<style lang="scss" scoped> |
||||||
|
.uni-group { |
||||||
|
background: #fff; |
||||||
|
margin-top: 20rpx; |
||||||
|
// border: 1px red solid; |
||||||
|
} |
||||||
|
|
||||||
|
.group-margin { |
||||||
|
margin: 0 -30rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-group__title { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
align-items: center; |
||||||
|
padding-left: 30rpx; |
||||||
|
height: 80rpx; |
||||||
|
background-color: $uni-bg-color-grey; |
||||||
|
font-weight: normal; |
||||||
|
color: $uni-text-color; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-group__content { |
||||||
|
padding: 30rpx 0; |
||||||
|
// padding-bottom: 5px; |
||||||
|
background-color: #FFF; |
||||||
|
} |
||||||
|
|
||||||
|
.group-conent-padding { |
||||||
|
padding: 0 30rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-group__title-text { |
||||||
|
font-size: $uni-font-size-base; |
||||||
|
color: $uni-text-color; |
||||||
|
} |
||||||
|
|
||||||
|
.distraction { |
||||||
|
flex-direction: row; |
||||||
|
align-items: center; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-group--card { |
||||||
|
margin: 20rpx; |
||||||
|
border-radius: 10rpx; |
||||||
|
overflow: hidden; |
||||||
|
box-shadow: 0 0 10rpx 2rpx rgba($color: #000000, $alpha: 0.08); |
||||||
|
} |
||||||
|
</style> |
File diff suppressed because one or more lines are too long
@ -0,0 +1,132 @@ |
|||||||
|
export default { |
||||||
|
"pulldown": "\ue588", |
||||||
|
"refreshempty": "\ue461", |
||||||
|
"back": "\ue471", |
||||||
|
"forward": "\ue470", |
||||||
|
"more": "\ue507", |
||||||
|
"more-filled": "\ue537", |
||||||
|
"scan": "\ue612", |
||||||
|
"qq": "\ue264", |
||||||
|
"weibo": "\ue260", |
||||||
|
"weixin": "\ue261", |
||||||
|
"pengyouquan": "\ue262", |
||||||
|
"loop": "\ue565", |
||||||
|
"refresh": "\ue407", |
||||||
|
"refresh-filled": "\ue437", |
||||||
|
"arrowthindown": "\ue585", |
||||||
|
"arrowthinleft": "\ue586", |
||||||
|
"arrowthinright": "\ue587", |
||||||
|
"arrowthinup": "\ue584", |
||||||
|
"undo-filled": "\ue7d6", |
||||||
|
"undo": "\ue406", |
||||||
|
"redo": "\ue405", |
||||||
|
"redo-filled": "\ue7d9", |
||||||
|
"bars": "\ue563", |
||||||
|
"chatboxes": "\ue203", |
||||||
|
"camera": "\ue301", |
||||||
|
"chatboxes-filled": "\ue233", |
||||||
|
"camera-filled": "\ue7ef", |
||||||
|
"cart-filled": "\ue7f4", |
||||||
|
"cart": "\ue7f5", |
||||||
|
"checkbox-filled": "\ue442", |
||||||
|
"checkbox": "\ue7fa", |
||||||
|
"arrowleft": "\ue582", |
||||||
|
"arrowdown": "\ue581", |
||||||
|
"arrowright": "\ue583", |
||||||
|
"smallcircle-filled": "\ue801", |
||||||
|
"arrowup": "\ue580", |
||||||
|
"circle": "\ue411", |
||||||
|
"eye-filled": "\ue568", |
||||||
|
"eye-slash-filled": "\ue822", |
||||||
|
"eye-slash": "\ue823", |
||||||
|
"eye": "\ue824", |
||||||
|
"flag-filled": "\ue825", |
||||||
|
"flag": "\ue508", |
||||||
|
"gear-filled": "\ue532", |
||||||
|
"reload": "\ue462", |
||||||
|
"gear": "\ue502", |
||||||
|
"hand-thumbsdown-filled": "\ue83b", |
||||||
|
"hand-thumbsdown": "\ue83c", |
||||||
|
"hand-thumbsup-filled": "\ue83d", |
||||||
|
"heart-filled": "\ue83e", |
||||||
|
"hand-thumbsup": "\ue83f", |
||||||
|
"heart": "\ue840", |
||||||
|
"home": "\ue500", |
||||||
|
"info": "\ue504", |
||||||
|
"home-filled": "\ue530", |
||||||
|
"info-filled": "\ue534", |
||||||
|
"circle-filled": "\ue441", |
||||||
|
"chat-filled": "\ue847", |
||||||
|
"chat": "\ue263", |
||||||
|
"mail-open-filled": "\ue84d", |
||||||
|
"email-filled": "\ue231", |
||||||
|
"mail-open": "\ue84e", |
||||||
|
"email": "\ue201", |
||||||
|
"checkmarkempty": "\ue472", |
||||||
|
"list": "\ue562", |
||||||
|
"locked-filled": "\ue856", |
||||||
|
"locked": "\ue506", |
||||||
|
"map-filled": "\ue85c", |
||||||
|
"map-pin": "\ue85e", |
||||||
|
"map-pin-ellipse": "\ue864", |
||||||
|
"map": "\ue364", |
||||||
|
"minus-filled": "\ue440", |
||||||
|
"mic-filled": "\ue332", |
||||||
|
"minus": "\ue410", |
||||||
|
"micoff": "\ue360", |
||||||
|
"mic": "\ue302", |
||||||
|
"clear": "\ue434", |
||||||
|
"smallcircle": "\ue868", |
||||||
|
"close": "\ue404", |
||||||
|
"closeempty": "\ue460", |
||||||
|
"paperclip": "\ue567", |
||||||
|
"paperplane": "\ue503", |
||||||
|
"paperplane-filled": "\ue86e", |
||||||
|
"person-filled": "\ue131", |
||||||
|
"contact-filled": "\ue130", |
||||||
|
"person": "\ue101", |
||||||
|
"contact": "\ue100", |
||||||
|
"images-filled": "\ue87a", |
||||||
|
"phone": "\ue200", |
||||||
|
"images": "\ue87b", |
||||||
|
"image": "\ue363", |
||||||
|
"image-filled": "\ue877", |
||||||
|
"location-filled": "\ue333", |
||||||
|
"location": "\ue303", |
||||||
|
"plus-filled": "\ue439", |
||||||
|
"plus": "\ue409", |
||||||
|
"plusempty": "\ue468", |
||||||
|
"help-filled": "\ue535", |
||||||
|
"help": "\ue505", |
||||||
|
"navigate-filled": "\ue884", |
||||||
|
"navigate": "\ue501", |
||||||
|
"mic-slash-filled": "\ue892", |
||||||
|
"search": "\ue466", |
||||||
|
"settings": "\ue560", |
||||||
|
"sound": "\ue590", |
||||||
|
"sound-filled": "\ue8a1", |
||||||
|
"spinner-cycle": "\ue465", |
||||||
|
"download-filled": "\ue8a4", |
||||||
|
"personadd-filled": "\ue132", |
||||||
|
"videocam-filled": "\ue8af", |
||||||
|
"personadd": "\ue102", |
||||||
|
"upload": "\ue402", |
||||||
|
"upload-filled": "\ue8b1", |
||||||
|
"starhalf": "\ue463", |
||||||
|
"star-filled": "\ue438", |
||||||
|
"star": "\ue408", |
||||||
|
"trash": "\ue401", |
||||||
|
"phone-filled": "\ue230", |
||||||
|
"compose": "\ue400", |
||||||
|
"videocam": "\ue300", |
||||||
|
"trash-filled": "\ue8dc", |
||||||
|
"download": "\ue403", |
||||||
|
"chatbubble-filled": "\ue232", |
||||||
|
"chatbubble": "\ue202", |
||||||
|
"cloud-download": "\ue8e4", |
||||||
|
"cloud-upload-filled": "\ue8e5", |
||||||
|
"cloud-upload": "\ue8e6", |
||||||
|
"cloud-download-filled": "\ue8e9", |
||||||
|
"headphones":"\ue8bf", |
||||||
|
"shop":"\ue609" |
||||||
|
} |
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -0,0 +1,210 @@ |
|||||||
|
<template> |
||||||
|
<view class="uni-pagination"> |
||||||
|
<view class="uni-pagination__btns"> |
||||||
|
<view class="uni-pagination__btn" :class="currentIndex === 1 ? 'uni-pagination--disabled' : 'uni-pagination--enabled'" |
||||||
|
:hover-class="currentIndex === 1 ? '' : 'uni-pagination--hover'" :hover-start-time="20" :hover-stay-time="70" |
||||||
|
@click="clickLeft"> |
||||||
|
<template v-if="showIcon===true || showIcon === 'true'"> |
||||||
|
<uni-icons color="#000" size="20" type="arrowleft" /> |
||||||
|
</template> |
||||||
|
<template v-else><text class="uni-pagination__child-btn">{{ prevText }}</text></template> |
||||||
|
</view> |
||||||
|
<view class="uni-pagination__btn" :class="currentIndex === maxPage ? 'uni-pagination--disabled' : 'uni-pagination--enabled'" |
||||||
|
:hover-class="currentIndex === maxPage ? '' : 'uni-pagination--hover'" :hover-start-time="20" :hover-stay-time="70" |
||||||
|
@click="clickRight"> |
||||||
|
<template v-if="showIcon===true || showIcon === 'true'"> |
||||||
|
<uni-icons color="#000" size="20" type="arrowright" /> |
||||||
|
</template> |
||||||
|
<template v-else><text class="uni-pagination__child-btn">{{ nextText }}</text></template> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="uni-pagination__num"> |
||||||
|
<view class="uni-pagination__num-current"> |
||||||
|
<text class="uni-pagination__num-current-text" style="color:#007aff">{{ currentIndex }}</text><text class="uni-pagination__num-current-text">/{{ maxPage || 0 }}</text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import uniIcons from '../uni-icons/uni-icons.vue' |
||||||
|
export default { |
||||||
|
name: 'UniPagination', |
||||||
|
components: { |
||||||
|
uniIcons |
||||||
|
}, |
||||||
|
props: { |
||||||
|
prevText: { |
||||||
|
type: String, |
||||||
|
default: '上一页' |
||||||
|
}, |
||||||
|
nextText: { |
||||||
|
type: String, |
||||||
|
default: '下一页' |
||||||
|
}, |
||||||
|
current: { |
||||||
|
type: [Number, String], |
||||||
|
default: 1 |
||||||
|
}, |
||||||
|
total: { // 数据总量 |
||||||
|
type: [Number, String], |
||||||
|
default: 0 |
||||||
|
}, |
||||||
|
pageSize: { // 每页数据量 |
||||||
|
type: [Number, String], |
||||||
|
default: 10 |
||||||
|
}, |
||||||
|
showIcon: { // 是否以 icon 形式展示按钮 |
||||||
|
type: [Boolean, String], |
||||||
|
default: false |
||||||
|
} |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
currentIndex: 1 |
||||||
|
} |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
maxPage() { |
||||||
|
let maxPage = 1 |
||||||
|
let total = Number(this.total) |
||||||
|
let pageSize = Number(this.pageSize) |
||||||
|
if (total && pageSize) { |
||||||
|
maxPage = Math.ceil(total / pageSize) |
||||||
|
} |
||||||
|
return maxPage |
||||||
|
} |
||||||
|
}, |
||||||
|
watch: { |
||||||
|
current(val) { |
||||||
|
this.currentIndex = +val |
||||||
|
} |
||||||
|
}, |
||||||
|
created() { |
||||||
|
this.currentIndex = +this.current |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
clickLeft() { |
||||||
|
if (Number(this.currentIndex) === 1) { |
||||||
|
return |
||||||
|
} |
||||||
|
this.currentIndex -= 1 |
||||||
|
this.change('prev') |
||||||
|
}, |
||||||
|
clickRight() { |
||||||
|
if (Number(this.currentIndex) === this.maxPage) { |
||||||
|
return |
||||||
|
} |
||||||
|
this.currentIndex += 1 |
||||||
|
this.change('next') |
||||||
|
}, |
||||||
|
change(e) { |
||||||
|
this.$emit('change', { |
||||||
|
type: e, |
||||||
|
current: this.currentIndex |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.uni-pagination { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
/* #ifdef APP-NVUE */ |
||||||
|
padding: 0 40rpx; |
||||||
|
/* #endif */ |
||||||
|
width: 700rpx; |
||||||
|
position: relative; |
||||||
|
overflow: hidden; |
||||||
|
flex-direction: row; |
||||||
|
justify-content: center; |
||||||
|
align-items: center; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-pagination__btns { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
flex: 1; |
||||||
|
justify-content: space-between; |
||||||
|
align-items: center; |
||||||
|
flex-direction: row; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-pagination__btn { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
width: 60rpx; |
||||||
|
height: 60rpx; |
||||||
|
line-height: 60rpx; |
||||||
|
border-radius: 50%; |
||||||
|
font-size: $uni-font-size-base; |
||||||
|
position: relative; |
||||||
|
background-color: $uni-bg-color-grey; |
||||||
|
flex-direction: row; |
||||||
|
justify-content: center; |
||||||
|
align-items: center; |
||||||
|
text-align: center; |
||||||
|
border-width: 2rpx; |
||||||
|
border-style: solid; |
||||||
|
border-color: $uni-border-color; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-pagination__child-btn { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
font-size: $uni-font-size-base; |
||||||
|
position: relative; |
||||||
|
flex-direction: row; |
||||||
|
justify-content: center; |
||||||
|
align-items: center; |
||||||
|
text-align: center; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-pagination__num { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
position: absolute; |
||||||
|
left: 300rpx; |
||||||
|
top: 0; |
||||||
|
flex-direction: row; |
||||||
|
justify-content: center; |
||||||
|
align-items: center; |
||||||
|
width: 100rpx; |
||||||
|
height: 60rpx; |
||||||
|
line-height: 60rpx; |
||||||
|
font-size: $uni-font-size-base; |
||||||
|
color: $uni-text-color; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-pagination__num-current { |
||||||
|
/* #ifndef APP-NVUE */ |
||||||
|
display: flex; |
||||||
|
/* #endif */ |
||||||
|
flex-direction: row; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-pagination__num-current-text { |
||||||
|
font-size: 30rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-pagination--enabled { |
||||||
|
color: #333333; |
||||||
|
opacity: 1; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-pagination--disabled { |
||||||
|
opacity: 0.3; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-pagination--hover { |
||||||
|
color: rgba(0, 0, 0, .6); |
||||||
|
background-color: $uni-bg-color-hover; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,324 @@ |
|||||||
|
<template> |
||||||
|
<view class="uni-datetime-picker"> |
||||||
|
<view @click="tiggerTimePicker"> |
||||||
|
<slot> |
||||||
|
<view class="uni-datetime-picker-timebox uni-datetime-picker-flex"> |
||||||
|
{{pickValue}}<view v-if="!pickValue" class="uni-datetime-picker-time">请选择<!-- 时间 --></view> |
||||||
|
<template> |
||||||
|
<uni-icons class="content-clear-icon is-textarea-icon" type="clear" :size="clearSize" |
||||||
|
color="#c0c4cc" @click="onClear"></uni-icons> |
||||||
|
</template> |
||||||
|
<view class="uni-datetime-picker-down-arrow"></view> |
||||||
|
</view> |
||||||
|
</slot> |
||||||
|
</view> |
||||||
|
<view v-if="visible" class="uni-datetime-picker-mask" @click="initTimePicker"></view> |
||||||
|
<view v-if="visible" class="uni-datetime-picker-popup"> |
||||||
|
<view class="uni-title"> |
||||||
|
请选择<!-- 和时间 --> |
||||||
|
</view> |
||||||
|
|
||||||
|
<!-- <picker @change="bindPickerChange" :value="index" :range="array" :range-key="'name'" class="uni-datetime-picker-hyphen"> |
||||||
|
|
||||||
|
</picker> --> |
||||||
|
|
||||||
|
<picker-view class="uni-datetime-picker-view" :indicator-style="indicatorStyle" :value="pickValue" @change="bindPickerChange"> |
||||||
|
<picker-view-column class="uni-datetime-picker-colon"> |
||||||
|
<view class="uni-datetime-picker-item" v-for="(item,index) in pickData" :key="index">{{item.name}}</view> |
||||||
|
</picker-view-column> |
||||||
|
</picker-view> |
||||||
|
<view class="uni-datetime-picker-btn"> |
||||||
|
<!-- <view class="" @click="clearTime">重置</view> --> |
||||||
|
<view class="uni-datetime-picker-btn-group"> |
||||||
|
<view class="uni-datetime-picker-cancel" @click="tiggerTimePicker">取消</view> |
||||||
|
<view class="" @click="setTime">确定</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
data() { |
||||||
|
return { |
||||||
|
visible: false, |
||||||
|
pickName: '', |
||||||
|
years: [], |
||||||
|
year: 1900, |
||||||
|
indicatorStyle: `height: 50px;`, |
||||||
|
} |
||||||
|
}, |
||||||
|
props: { |
||||||
|
pickData: { |
||||||
|
type: Object, |
||||||
|
default: [] |
||||||
|
}, |
||||||
|
pickValue: { |
||||||
|
type: [String, Number], |
||||||
|
default: '' |
||||||
|
}, |
||||||
|
type: { |
||||||
|
type: String, |
||||||
|
default: 'datetime-local' |
||||||
|
}, |
||||||
|
timestamp: { |
||||||
|
type: Boolean, |
||||||
|
default: false |
||||||
|
}, |
||||||
|
value: { |
||||||
|
type: [String, Number], |
||||||
|
default: '' |
||||||
|
} |
||||||
|
}, |
||||||
|
watch: { |
||||||
|
value(newValue) { |
||||||
|
this.parseValue(this.value) |
||||||
|
this.initTime() |
||||||
|
} |
||||||
|
}, |
||||||
|
created() { |
||||||
|
this.form = this.getForm('uniForms') |
||||||
|
this.formItem = this.getForm('uniFormsItem') |
||||||
|
|
||||||
|
if (this.formItem) { |
||||||
|
if (this.formItem.name) { |
||||||
|
this.rename = this.formItem.name |
||||||
|
this.form.inputChildrens.push(this) |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
mounted() { |
||||||
|
this.parseValue(this.value) |
||||||
|
if (this.value) { |
||||||
|
this.initTime() |
||||||
|
} |
||||||
|
|
||||||
|
}, |
||||||
|
methods: { |
||||||
|
//切换遮罩层 |
||||||
|
tiggerTimePicker() { |
||||||
|
this.visible = !this.visible |
||||||
|
}, |
||||||
|
bindPickerChange(e){ |
||||||
|
const val = e.detail.value |
||||||
|
this.pickValue = this.pickData[val[0]].value |
||||||
|
this.pickName = this.pickData[val[0]].name |
||||||
|
console.log(this.pickValue) |
||||||
|
}, |
||||||
|
onClear(){ |
||||||
|
this.pickValue = '', |
||||||
|
this.pickName = '' |
||||||
|
}, |
||||||
|
setTime() { |
||||||
|
this.$emit('change', this.pickName) |
||||||
|
this.tiggerTimePicker() |
||||||
|
}, |
||||||
|
/** |
||||||
|
* 获取父元素实例 |
||||||
|
*/ |
||||||
|
getForm(name = 'uniForms') { |
||||||
|
let parent = this.$parent; |
||||||
|
let parentName = parent.$options.name; |
||||||
|
while (parentName !== name) { |
||||||
|
parent = parent.$parent; |
||||||
|
if (!parent) return false |
||||||
|
parentName = parent.$options.name; |
||||||
|
} |
||||||
|
return parent; |
||||||
|
}, |
||||||
|
parseDateTime(datetime) { |
||||||
|
let defaultDate = null |
||||||
|
if (!datetime) { |
||||||
|
defaultDate = new Date() |
||||||
|
} else { |
||||||
|
defaultDate = new Date(datetime) |
||||||
|
} |
||||||
|
this.year = defaultDate.getFullYear() |
||||||
|
if (this.year < this.minYear || this.year > this.maxYear) { |
||||||
|
const now = Date.now() |
||||||
|
this.parseDateTime(now) |
||||||
|
return |
||||||
|
} |
||||||
|
this.month = defaultDate.getMonth() + 1 |
||||||
|
this.day = defaultDate.getDate() |
||||||
|
this.hour = defaultDate.getHours() |
||||||
|
this.minute = defaultDate.getMinutes() |
||||||
|
this.second = defaultDate.getSeconds() |
||||||
|
}, |
||||||
|
parseValue(defaultTime) { |
||||||
|
if (Number(defaultTime)) { |
||||||
|
defaultTime = parseInt(defaultTime) |
||||||
|
} |
||||||
|
this.parseDateTime(defaultTime) |
||||||
|
}, |
||||||
|
bindDateChange(e) { |
||||||
|
const val = e.detail.value |
||||||
|
this.year = this.years[val[0]] |
||||||
|
this.month = this.months[val[1]] |
||||||
|
this.day = this.days[val[2]] |
||||||
|
}, |
||||||
|
bindTimeChange(e) { |
||||||
|
const val = e.detail.value |
||||||
|
this.hour = this.hours[val[0]] |
||||||
|
this.minute = this.minutes[val[1]] |
||||||
|
this.second = this.seconds[val[2]] |
||||||
|
}, |
||||||
|
initTimePicker() { |
||||||
|
// if (!this.time) { |
||||||
|
// this.parseValue() |
||||||
|
// } |
||||||
|
this.parseValue(this.value) |
||||||
|
this.visible = !this.visible |
||||||
|
}, |
||||||
|
clearTime() { |
||||||
|
this.time = '' |
||||||
|
this.tiggerTimePicker() |
||||||
|
}, |
||||||
|
initTime() { |
||||||
|
this.time = this.createDomSting() |
||||||
|
if (!this.timestamp) { |
||||||
|
this.formItem && this.formItem.setValue(this.time) |
||||||
|
this.$emit('change', this.time) |
||||||
|
} else { |
||||||
|
this.formItem && this.formItem.setValue(this.createTimeStamp(this.time)) |
||||||
|
this.$emit('change', this.createTimeStamp(this.time)) |
||||||
|
} |
||||||
|
}, |
||||||
|
createTimeStamp(time) { |
||||||
|
return Date.parse(new Date(time)) |
||||||
|
}, |
||||||
|
createDomSting() { |
||||||
|
const yymmdd = this.year + |
||||||
|
'-' + |
||||||
|
(this.month < 10 ? '0' + this.month : this.month) + |
||||||
|
'-' + |
||||||
|
(this.day < 10 ? '0' + this.day : this.day) |
||||||
|
// + |
||||||
|
// ' ' + |
||||||
|
// (this.hour < 10 ? '0' + this.hour : this.hour) + |
||||||
|
// ':' + |
||||||
|
// (this.minute < 10 ? '0' + this.minute : this.minute) + |
||||||
|
// ':' + |
||||||
|
// (this.second < 10 ? '0' + this.second : this.second) |
||||||
|
|
||||||
|
return yymmdd |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style> |
||||||
|
.uni-datetime-picker-view { |
||||||
|
width: 100%; |
||||||
|
height: 130px; |
||||||
|
margin-top: 60rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-item { |
||||||
|
line-height: 100rpx; |
||||||
|
text-align: center; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-title{ |
||||||
|
text-align: center; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-btn { |
||||||
|
margin-top: 120rpx; |
||||||
|
display: flex; |
||||||
|
justify-content: center; |
||||||
|
align-items: center; |
||||||
|
color: #00B9FF; |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-btn-group { |
||||||
|
display: flex; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-cancel { |
||||||
|
margin-right: 60rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-mask { |
||||||
|
position: fixed; |
||||||
|
bottom: 0px; |
||||||
|
top: 0px; |
||||||
|
left: 0px; |
||||||
|
right: 0px; |
||||||
|
background-color: rgba(0, 0, 0, 0.4); |
||||||
|
transition-duration: 0.3s; |
||||||
|
z-index: 998; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-popup { |
||||||
|
border-radius: 16rpx; |
||||||
|
padding: 60rpx; |
||||||
|
width: 540rpx; |
||||||
|
background-color: #fff; |
||||||
|
position: fixed; |
||||||
|
top: 50%; |
||||||
|
left: 50%; |
||||||
|
transform: translate(-50%, -50%); |
||||||
|
transition-duration: 0.3s; |
||||||
|
z-index: 999; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-time { |
||||||
|
color: grey; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-colon::after { |
||||||
|
content: ':'; |
||||||
|
position: absolute; |
||||||
|
top: 106rpx; |
||||||
|
right: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-hyphen::after { |
||||||
|
content: '-'; |
||||||
|
position: absolute; |
||||||
|
top: 106rpx; |
||||||
|
right: -4rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-timebox { |
||||||
|
border: 2rpx solid #E5E5E5; |
||||||
|
border-radius: 40rpx; |
||||||
|
padding: 14rpx 40rpx; |
||||||
|
box-sizing: border-box; |
||||||
|
cursor: pointer; |
||||||
|
width: 80%; |
||||||
|
margin-left: 20%; |
||||||
|
} |
||||||
|
|
||||||
|
// 下箭头 |
||||||
|
.uni-datetime-picker-down-arrow { |
||||||
|
display :inline-block; |
||||||
|
position: relative; |
||||||
|
width: 40rpx; |
||||||
|
height: 30rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.uni-datetime-picker-down-arrow::after { |
||||||
|
display: inline-block; |
||||||
|
content: " "; |
||||||
|
height: 18rpx; |
||||||
|
width: 18rpx; |
||||||
|
border-width: 0 2rpx 2rpx 0; |
||||||
|
border-color: #E5E5E5; |
||||||
|
border-style: solid; |
||||||
|
transform: matrix(0.71, 0.71, -0.71, 0.71, 0, 0); |
||||||
|
transform-origin: center; |
||||||
|
transition: transform .3s; |
||||||
|
position: absolute; |
||||||
|
top: 50%; |
||||||
|
right: 10rpx; |
||||||
|
margin-top: -10rpx; |
||||||
|
} |
||||||
|
.uni-datetime-picker-flex { |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,38 @@ |
|||||||
|
//#ifdef H5
|
||||||
|
const BASEURL = '' |
||||||
|
//#endif
|
||||||
|
|
||||||
|
//#ifndef H5
|
||||||
|
// const BASEURL = getApp().globalData.url
|
||||||
|
//#endif
|
||||||
|
|
||||||
|
function previepdf(url,ext) { |
||||||
|
uni.showLoading({ |
||||||
|
title: '加载中' |
||||||
|
}); |
||||||
|
uni.downloadFile({ |
||||||
|
url: url, |
||||||
|
|
||||||
|
success(res) { |
||||||
|
let path = res.tempFilePath; |
||||||
|
uni.openDocument({ |
||||||
|
filePath: path, |
||||||
|
fileType: ext, |
||||||
|
|
||||||
|
success() { |
||||||
|
uni.hideLoading();
|
||||||
|
}, |
||||||
|
|
||||||
|
fail() { |
||||||
|
uni.hideLoading(); |
||||||
|
} |
||||||
|
|
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
module.exports = { |
||||||
|
previepdf |
||||||
|
}; |
@ -0,0 +1,101 @@ |
|||||||
|
/** |
||||||
|
* GET请求封装 |
||||||
|
*/ |
||||||
|
function get(url, data = {}) { |
||||||
|
return request(url, data, 'GET'); |
||||||
|
} |
||||||
|
|
||||||
|
function put(url, data = {}) { |
||||||
|
return request(url, data, 'put'); |
||||||
|
} |
||||||
|
|
||||||
|
function deletes(url, data = {}) { |
||||||
|
return request(url, data, 'delete'); |
||||||
|
} |
||||||
|
/** |
||||||
|
* POST请求封装 |
||||||
|
*/ |
||||||
|
|
||||||
|
function post(url, data = {}) { |
||||||
|
return request(url, data, 'POST'); |
||||||
|
} |
||||||
|
/** |
||||||
|
* 微信的request |
||||||
|
*/ |
||||||
|
//#ifdef H5
|
||||||
|
const BASEURL = '' |
||||||
|
//#endif
|
||||||
|
|
||||||
|
//#ifndef H5
|
||||||
|
const BASEURL = getApp().globalData.url |
||||||
|
//#endif
|
||||||
|
|
||||||
|
function request(url, data = {}, method = "GET") { |
||||||
|
return new Promise(function(resolve, reject) { |
||||||
|
uni.showLoading({ |
||||||
|
title: "加载中" |
||||||
|
}); |
||||||
|
uni.request({ |
||||||
|
url: BASEURL + url, |
||||||
|
method: method, |
||||||
|
data: data, |
||||||
|
header: { |
||||||
|
'mini-session': uni.getStorageSync('session') || '', |
||||||
|
'Accept': 'application/json', |
||||||
|
'X-Requested-With': 'XMLHttpRequest', |
||||||
|
'Content-Type': 'application/json' |
||||||
|
}, |
||||||
|
|
||||||
|
success(res) { |
||||||
|
if (res.data) { |
||||||
|
if (res.data.error) { |
||||||
|
if (res.data.error.code != 404) { |
||||||
|
uni.showToast({ |
||||||
|
title: res.data.error.message, |
||||||
|
icon: 'none' |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
if (res.data.error.code == 9999) { |
||||||
|
uni.clearStorageSync('session'); |
||||||
|
uni.showToast({ |
||||||
|
title: '登录失效,请重新登录', |
||||||
|
icon: 'none' |
||||||
|
}); |
||||||
|
setTimeout(() => { |
||||||
|
uni.reLaunch({ |
||||||
|
url: '/pages/login/login' |
||||||
|
}); |
||||||
|
}, 1000); |
||||||
|
} |
||||||
|
|
||||||
|
reject(res.data.error.message); |
||||||
|
} else { |
||||||
|
resolve(res.data); |
||||||
|
} |
||||||
|
} else { |
||||||
|
resolve(null); |
||||||
|
uni.hideLoading(); |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
fail(res) { |
||||||
|
console.log(res); |
||||||
|
uni.showToast({ |
||||||
|
title: '请求超时,请重试', |
||||||
|
icon: 'none' |
||||||
|
}); // wx.hideLoading()
|
||||||
|
} |
||||||
|
|
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = { |
||||||
|
get, |
||||||
|
post, |
||||||
|
put, |
||||||
|
deletes, |
||||||
|
}; |
@ -0,0 +1,167 @@ |
|||||||
|
//#ifdef H5
|
||||||
|
const BASEURL = '' |
||||||
|
//#endif
|
||||||
|
|
||||||
|
//#ifndef H5
|
||||||
|
//#endif
|
||||||
|
import Vue from 'vue' |
||||||
|
const BASEURL = Vue.prototype.baseUrl // 取得全局变量
|
||||||
|
|
||||||
|
function uploadImage() { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
// 原本的上传图片
|
||||||
|
// uni.chooseImage({
|
||||||
|
// count: 1,
|
||||||
|
// sizeType: ['original', 'compressed'],
|
||||||
|
// sourceType: ['album', 'camera'],
|
||||||
|
|
||||||
|
// 修改为上传图片或者视频
|
||||||
|
uni.chooseMedia({ |
||||||
|
count: 1,// 待处理,多个上传需要封装循环上传
|
||||||
|
mediaType: ['image','video'], |
||||||
|
sourceType: ['album', 'camera'], |
||||||
|
maxDuration: 30, |
||||||
|
camera: 'back', |
||||||
|
success(ress) { |
||||||
|
if (ress.tempFiles.length > 0) { |
||||||
|
console.log(ress,'看下是啥格式') |
||||||
|
// 修改--改为选择图片或者视频的接口
|
||||||
|
// const tempFilePaths = ress.tempFiles[0].path
|
||||||
|
const tempFilePaths = ress.tempFiles[0].tempFilePath |
||||||
|
|
||||||
|
const size = ress.tempFiles[0].size |
||||||
|
if (size < 8388608) { |
||||||
|
uni.showLoading({ |
||||||
|
title:'上传中' |
||||||
|
}) |
||||||
|
// let imageurl = tempFilePaths
|
||||||
|
let pdfurl = "" |
||||||
|
uni.uploadFile({ |
||||||
|
url: BASEURL+'/api-guarantee/dg-apply-amount-info/uploadFile', |
||||||
|
filePath: tempFilePaths, |
||||||
|
header: { |
||||||
|
// "mini-session": uni.getStorageSync('session'),
|
||||||
|
"token": uni.getStorageSync('token'), |
||||||
|
"Content-Type": "multipart/form-data;boundary=----WebKitFormBoundaryi8lPVoSysovJLNqi", |
||||||
|
"Accept": "application/json" |
||||||
|
}, |
||||||
|
name: 'file', |
||||||
|
// formData: {
|
||||||
|
// 'user': 'test'
|
||||||
|
// },
|
||||||
|
success(res) { |
||||||
|
if (res.statusCode == 500) { |
||||||
|
// 判断图片或者视频未做
|
||||||
|
uni.showToast({ |
||||||
|
title: '上传失败', |
||||||
|
icon: 'none' |
||||||
|
}) |
||||||
|
reject('失败') |
||||||
|
} else { |
||||||
|
uni.showToast({ |
||||||
|
title: '上传成功', |
||||||
|
icon: 'success' |
||||||
|
}) |
||||||
|
if (res.data) { |
||||||
|
let imageurl = JSON.parse(res.data).data[0] |
||||||
|
let index= imageurl.lastIndexOf("."); |
||||||
|
let ext = imageurl.substr(index+1); |
||||||
|
resolve({ |
||||||
|
imageurl, // 图片地址
|
||||||
|
ext: ext |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
} else { |
||||||
|
uni.showToast({ |
||||||
|
title: '超出限制大小', |
||||||
|
icon: "none" |
||||||
|
}) |
||||||
|
} |
||||||
|
} else { |
||||||
|
uni.showToast({ |
||||||
|
title: '文件不存在', |
||||||
|
icon: "none" |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
function uploadPdf() { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
uni.chooseMessageFile({ |
||||||
|
count: 1, |
||||||
|
type: 'file', |
||||||
|
success(ress) { |
||||||
|
if (ress.tempFiles.length > 0) { |
||||||
|
if (ress.tempFiles[0].size < 8388608) { |
||||||
|
uni.showLoading({ |
||||||
|
title:'上传中' |
||||||
|
}) |
||||||
|
// let pdfurl = ress.tempFiles[0].name
|
||||||
|
uni.uploadFile({ |
||||||
|
url:BASEURL+'/api-guarantee/dg-apply-amount-info/uploadFile', |
||||||
|
filePath: ress.tempFiles[0].path, |
||||||
|
name: 'file', |
||||||
|
header: { |
||||||
|
// "mini-session": uni.getStorageSync('session'),
|
||||||
|
"token": uni.getStorageSync('token'), |
||||||
|
"Content-Type": "multipart/form-data; boundary=----WebKitFormBoundaryi8lPVoSysovJLNqi", |
||||||
|
"Accept": "application/json" |
||||||
|
}, |
||||||
|
// formData: {
|
||||||
|
// 'user': 'test'
|
||||||
|
// },
|
||||||
|
success(res) { |
||||||
|
if (res.statusCode != 500) { |
||||||
|
uni.showToast({ |
||||||
|
title: '文件上传成功', |
||||||
|
icon: 'success' |
||||||
|
}) |
||||||
|
if (res.data) { |
||||||
|
let pdfurl = JSON.parse(res.data).data[0] |
||||||
|
let index= pdfurl.lastIndexOf("."); |
||||||
|
let ext = pdfurl.substr(index+1); |
||||||
|
resolve({ |
||||||
|
pdfurl, // 文件地址
|
||||||
|
ext: ext |
||||||
|
}) |
||||||
|
} else { |
||||||
|
reject('失败') |
||||||
|
} |
||||||
|
|
||||||
|
} else { |
||||||
|
uni.showToast({ |
||||||
|
title: '服务器错误', |
||||||
|
icon: 'none' |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
} else { |
||||||
|
uni.showToast({ |
||||||
|
title: '超出限制大小', |
||||||
|
icon: "none" |
||||||
|
}) |
||||||
|
return |
||||||
|
} |
||||||
|
} else { |
||||||
|
uni.showToast({ |
||||||
|
title: '文件不存在', |
||||||
|
icon: "none" |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
module.exports = { |
||||||
|
uploadImage, |
||||||
|
uploadPdf |
||||||
|
} |
@ -0,0 +1,347 @@ |
|||||||
|
<template> |
||||||
|
<view> |
||||||
|
<!-- 菜单 (悬浮,预先隐藏)--> |
||||||
|
<me-tabs v-if="isShowSticky" v-model="tabIndex" :fixed="true" :tabs="tabs" @change="tabChange"></me-tabs> |
||||||
|
<mescroll-body ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback" :up="upOption" @scroll="scroll" @topclick="topClick"> |
||||||
|
<!-- 菜单 (在mescroll-uni中不能使用fixed,否则iOS滚动时会抖动, 所以需在mescroll-uni之外存在一个一样的菜单) --> |
||||||
|
<view id="tabInList"> |
||||||
|
<me-tabs v-model="tabIndex" :tabs="tabs" @change="tabChange"></me-tabs> |
||||||
|
</view> |
||||||
|
|
||||||
|
<!-- 搜索 --> |
||||||
|
<view class="cu-bar bg-white search" :class="scrolltop ? 'fixed' : ''" :style="[{top:CustomBar + 'px'}]"> |
||||||
|
<view class="search-form round"> |
||||||
|
<input @input="searchList" v-model="listParams.customerNumberOrName" type="text" placeholder="输入搜索的关键词" confirm-type="search"></input> |
||||||
|
<text class="cuIcon-search"></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<!-- 数据列表 --> |
||||||
|
<!-- <index-list :list="chargeList"></index-list> --> |
||||||
|
<!-- <touch-list :list="chargeList" :listTouchDirection="listTouchDirection" :listTouchStart="listTouchStart" :modalName="modalName" |
||||||
|
@TouchStart="ListTouchStart" @TouchMove="ListTouchMove" @TouchEnd="ListTouchEnd" |
||||||
|
></touch-list> --> |
||||||
|
|
||||||
|
<view class="cu-list menu-avatar"> |
||||||
|
<view class="cu-item" :class="modalName=='move-box-'+ index?'move-cur':''" v-for="(item,index) in listData" :key="index" |
||||||
|
@touchstart="ListTouchStart" @touchmove="ListTouchMove" @touchend="ListTouchEnd" :data-target="'move-box-' + index"> |
||||||
|
<view class="good-list"> |
||||||
|
<view class="charge" :id="'index-' + item.name" :data-index="item.name"> |
||||||
|
<navigator v-if="[65,66,67].includes(item.roleId)" @click="handleGe(item,'watch')" hover-class='none' :url="'/guaranteePages/pages/InformationSee/InformationSee'" |
||||||
|
class="nav-li" navigateTo :style="[{animation: 'show ' + ((index+1)*0.2+1) + 's 1'}]"> |
||||||
|
<view class="charge-title flex-between"> |
||||||
|
<text>{{item.name}}的贷款申请</text> |
||||||
|
</view> |
||||||
|
<view class="charge-text"> |
||||||
|
<view> |
||||||
|
<text>业务编号:</text> |
||||||
|
<text class="mgl30">{{item.businessCode}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>客户名称:</text> |
||||||
|
<text class="mgl30">{{item.name}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>联系电话:</text> |
||||||
|
<text class="mgl30">{{item.phone}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>业务类别:</text> |
||||||
|
<text class="mgl30">{{item.businessType}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>申请额度(万元):</text> |
||||||
|
<text class="mgl30">{{item.applyAmount}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>申请期限:</text> |
||||||
|
<text class="mgl30">{{item.applyTime}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>申请日期:</text> |
||||||
|
<text class="mgl30">{{item.createTime}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>审批状态:</text> |
||||||
|
<text class="mgl30">{{item.statusVal}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>业务状态:</text> |
||||||
|
<text class="mgl30">{{item.businessStatusVal}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>操作状态:</text> |
||||||
|
<text class="mgl30">{{item.operationType}}</text> |
||||||
|
</view> |
||||||
|
<!-- <view class="flex-between"> |
||||||
|
<text class="time-text">{{item.time}}</text> |
||||||
|
<text class="status-text" :style="'background-color:'+item.status.bgColor+';color:'+item.status.textColor">{{item.status.text}}</text> |
||||||
|
</view> --> |
||||||
|
</view> |
||||||
|
</navigator> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="move foot-btn sideBtn"> |
||||||
|
<button v-show="item.status===1&&item.operatingStatus == 1||item.status===4&&item.operatingStatus == 1" v-if="[66].includes(item.roleId)&&item.businessStatus !== 3" class="mini-btn round def-btn mar-lr" type="primary" size="mini" @click="handleGe(item,'survey')">调查</button> |
||||||
|
<button v-show="item.status===0&&item.operatingStatus == 1" v-if="[65].includes(item.roleId)&&item.businessStatus !== 3" class="mini-btn round suc-btn" type="primary" size="mini" @click="handleGe(item,'none',true)">指派</button> |
||||||
|
<view class="margin-top"> |
||||||
|
<button v-show="item.status===1&&item.operatingStatus == 1||item.status===4&&item.operatingStatus == 1" v-if="[65,67].includes(item.roleId)&&item.businessStatus !== 3" class="mini-btn round def-btn mar-lr" type="primary" size="mini" @click="handleGe(item,'audit')">审核</button> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</mescroll-body> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import MescrollMixin from "@/components/mescroll-uni/mescroll-mixins.js"; |
||||||
|
export default { |
||||||
|
mixins: [MescrollMixin], // 使用mixin |
||||||
|
data() { |
||||||
|
return { |
||||||
|
upOption: { |
||||||
|
// 如果用mescroll-uni 则需要onScroll: true, 且需要 @scroll="scroll"; 而mescroll-body最简单只需在onPageScroll处理即可 |
||||||
|
// onScroll: true // 是否监听滚动事件, 默认false (配置为true时,可@scroll="scroll"获取到滚动条位置和方向) |
||||||
|
}, |
||||||
|
tabs:[ |
||||||
|
{name:'全部', num:1, y:0, curPageLen:0, hasNext:true}, |
||||||
|
{name:'待指派', num:1, y:0, curPageLen:0, hasNext:true}, |
||||||
|
{name:'审核中', num:1, y:0, curPageLen:0, hasNext:true}, |
||||||
|
{name:'已审核', num:1, y:0, curPageLen:0, hasNext:true}, |
||||||
|
{name:'已拒绝', num:1, y:0, curPageLen:0, hasNext:true}, |
||||||
|
{name:'已驳回', num:1, y:0, curPageLen:0, hasNext:true}, |
||||||
|
{name:'已撤销', num:1, y:0, curPageLen:0, hasNext:true}, |
||||||
|
], |
||||||
|
tabIndex: 0, // 当前菜单下标 |
||||||
|
preIndex: 0, // 前一个菜单下标 |
||||||
|
navTop: null, // nav距离到顶部的距离 (如计算不准确,可直接写死某个值) |
||||||
|
isShowSticky: false, // 是否悬浮 |
||||||
|
TabCur: 0, |
||||||
|
scrollLeft: 0, |
||||||
|
CustomBar: this.CustomBar, |
||||||
|
listCurID: '', |
||||||
|
listData: [], |
||||||
|
total: 1, |
||||||
|
scrolltop: false, |
||||||
|
listTouchStart: 0, |
||||||
|
listTouchDirection: null, |
||||||
|
modalName: null, |
||||||
|
listParams:{ |
||||||
|
customerNumberOrName:"",// 搜索框 |
||||||
|
page:1, |
||||||
|
size:10 |
||||||
|
}, |
||||||
|
status:'',// 状态 |
||||||
|
}; |
||||||
|
}, |
||||||
|
onShow() { |
||||||
|
// this.getList()// 请求列表 |
||||||
|
this.upCallback({num:1,size:10,search:''}) |
||||||
|
}, |
||||||
|
mounted() { |
||||||
|
// this.getList()// 初次请求列表 |
||||||
|
// this.listData.map(e =>{ |
||||||
|
// e.status = this.core.auditStatus(e.status) |
||||||
|
// }) |
||||||
|
}, |
||||||
|
destroyed() { |
||||||
|
|
||||||
|
}, |
||||||
|
methods: { |
||||||
|
changeTab(index){ |
||||||
|
this.TabCur = index; |
||||||
|
}, |
||||||
|
/*下拉刷新的回调 */ |
||||||
|
downCallback() { |
||||||
|
// 这里加载你想下拉刷新的数据, 比如刷新轮播数据 |
||||||
|
// loadSwiper(); |
||||||
|
// 下拉刷新的回调,默认重置上拉加载列表为第一页 (自动执行 page.num=1, 再触发upCallback方法 ) |
||||||
|
this.mescroll.resetUpScroll() |
||||||
|
}, |
||||||
|
/*上拉加载的回调: 其中page.num:当前页 从1开始, page.size:每页数据条数,默认10 */ |
||||||
|
upCallback(page) { |
||||||
|
this.$http.get('/api-guarantee/dg-message-investigation/messageList',{ |
||||||
|
page: page.num?page.num:1, |
||||||
|
size: page.size?page.size:10, |
||||||
|
status:this.status?this.status:'', |
||||||
|
customerNumberOrName:page.search?page.search:'' |
||||||
|
}).then(res => { |
||||||
|
if(page.num == 1) this.listData = []; //如果是第一页需手动制空列表 |
||||||
|
res.data.list.map(e =>{ |
||||||
|
e.statusVal = this.core.statusVal(e.status) |
||||||
|
e.businessStatusVal = this.core.businessType(e.businessStatus) |
||||||
|
e.operationType = this.core.operationType(e.operatingStatus) |
||||||
|
}) |
||||||
|
this.listData = this.listData.concat(res.data.list); //追加新数据 |
||||||
|
this.mescroll.endSuccess(res.data.list); // 隐藏加载状态栏 |
||||||
|
}).catch(()=>{ |
||||||
|
//联网失败, 结束加载 |
||||||
|
this.mescroll.endErr(); |
||||||
|
}) |
||||||
|
}, |
||||||
|
// 搜索框--结合防抖 |
||||||
|
searchList(e){ |
||||||
|
let that = this |
||||||
|
if (this.timer) { |
||||||
|
clearTimeout(this.timer); |
||||||
|
this.timer = null; |
||||||
|
} |
||||||
|
this.timer = setTimeout(function () { |
||||||
|
that.upCallback({num:1,size:10,search:e.detail.value}) |
||||||
|
},800) |
||||||
|
}, |
||||||
|
// 设置nav到顶部的距离 (滚动条为0, 菜单顶部的数据加载完毕获取到的navTop数值是最精确的) |
||||||
|
setNavTop(){ |
||||||
|
let view = uni.createSelectorQuery().select('#tabInList'); |
||||||
|
view.boundingClientRect(data => { |
||||||
|
this.navTop = data.top // 到屏幕顶部的距离 |
||||||
|
}).exec(); |
||||||
|
}, |
||||||
|
// mescroll-uni的滚动事件 (需在up配置onScroll:true才生效) |
||||||
|
// 而mescroll-body最简单只需在onPageScroll处理即可 |
||||||
|
scroll(){ |
||||||
|
// 菜单悬浮的原理: 监听滚动条的位置大于某个值时,控制顶部菜单的显示和隐藏 |
||||||
|
if (this.mescroll.getScrollTop() >= this.navTop) { |
||||||
|
this.isShowSticky = true // 显示悬浮菜单 |
||||||
|
} else { |
||||||
|
this.isShowSticky = false // 隐藏悬浮菜单 |
||||||
|
} |
||||||
|
}, |
||||||
|
// 点击回到顶部按钮时,先隐藏悬浮菜单,避免闪动 |
||||||
|
topClick(){ |
||||||
|
this.isShowSticky = false |
||||||
|
}, |
||||||
|
// 切换菜单 |
||||||
|
tabChange (index) { |
||||||
|
if(index==0){ |
||||||
|
this.status = '' |
||||||
|
}else{ |
||||||
|
this.status = `${index-1}` |
||||||
|
} |
||||||
|
this.upCallback({ |
||||||
|
num:1,size:10 |
||||||
|
}) |
||||||
|
// 记录前一个菜单的数据 |
||||||
|
let preTab = this.tabs[this.preIndex] |
||||||
|
preTab.y = this.mescroll.getScrollTop(); // 滚动条位置 |
||||||
|
this.preIndex = index; |
||||||
|
// 当前菜单的数据 |
||||||
|
let curTab = this.tabs[index] |
||||||
|
if (!curTab.goods) { |
||||||
|
// 没有初始化,则初始化 |
||||||
|
this.isChangeTab = true; |
||||||
|
this.mescroll.resetUpScroll() |
||||||
|
} else{ |
||||||
|
// 初始化过,则恢复之前的列表数据 |
||||||
|
this.mescroll.setPageNum(curTab.num + 1); // 恢复当前页码 |
||||||
|
this.mescroll.endSuccess(curTab.curPageLen, curTab.hasNext); // 恢复是否有下一页或显示空布局 |
||||||
|
this.$nextTick(()=>{ |
||||||
|
this.mescroll.scrollTo(curTab.y, 0) // 恢复滚动条的位置 |
||||||
|
}) |
||||||
|
} |
||||||
|
}, |
||||||
|
// ListTouch触摸开始 |
||||||
|
ListTouchStart(e) { |
||||||
|
this.listTouchStart = e.touches[0].pageX |
||||||
|
}, |
||||||
|
|
||||||
|
// ListTouch计算方向 |
||||||
|
ListTouchMove(e) { |
||||||
|
this.listTouchDirection = e.touches[0].pageX - this.listTouchStart > 0 ? 'right' : 'left' |
||||||
|
}, |
||||||
|
|
||||||
|
// ListTouch计算滚动 |
||||||
|
ListTouchEnd(e) { |
||||||
|
if (this.listTouchDirection == 'left') { |
||||||
|
this.modalName = e.currentTarget.dataset.target |
||||||
|
} else { |
||||||
|
this.modalName = null |
||||||
|
} |
||||||
|
this.listTouchDirection = null |
||||||
|
}, |
||||||
|
// 待改造 |
||||||
|
|
||||||
|
// 列表数据 |
||||||
|
getList(val){ |
||||||
|
if(val==='search'){ |
||||||
|
this.listParams.page = 1 |
||||||
|
this.listParams.size = 10 |
||||||
|
} |
||||||
|
this.$http.get('/api-guarantee/dg-message-investigation/messageList',this.listParams).then(res=>{ |
||||||
|
if(res.data&&res.data.list&&res.data.list.length!==0){ |
||||||
|
res.data.list.map(e =>{ |
||||||
|
e.statusVal = this.core.statusVal(e.status) |
||||||
|
e.businessStatusVal = this.core.businessType(e.businessStatus) |
||||||
|
e.operationType = this.core.operationType(e.operatingStatus) |
||||||
|
}) |
||||||
|
this.listData = res.data.list |
||||||
|
}else{ |
||||||
|
this.listData = [] |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
// 传递当前操作状态和值 |
||||||
|
handleGe(item,str,assign){ |
||||||
|
if(assign){// 如果是指派-进入指派页面 |
||||||
|
this.goto('/guaranteePages/pages/assignInformation/assignInformation')// 跳转指派 |
||||||
|
uni.removeStorageSync('handleGe') |
||||||
|
}else{ |
||||||
|
uni.setStorageSync('handleGe',str)// 操作状态记录 |
||||||
|
} |
||||||
|
if(!assign&&str!=='watch'){// 非指派和查看的情况下 |
||||||
|
this.goto('/guaranteePages/pages/InformationSee/InformationSee') |
||||||
|
} |
||||||
|
uni.setStorageSync('applyMsg',JSON.stringify(item))// 所有的值存起来--业务逻辑应用需要的值不同懒得一个个传 |
||||||
|
uni.setStorageSync('businessId',item.businessId)// 储存有业务id |
||||||
|
}, |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}, |
||||||
|
// // 使用mescroll-body最简单只需在onPageScroll处理即可 |
||||||
|
// onPageScroll(e){ |
||||||
|
// if (e.scrollTop >= this.navTop) { |
||||||
|
// this.isShowSticky = true // 显示悬浮菜单 |
||||||
|
// } else { |
||||||
|
// this.isShowSticky = false // 隐藏悬浮菜单 |
||||||
|
// } |
||||||
|
// }, |
||||||
|
|
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.cu-bar .search-form{ |
||||||
|
border: 2rpx solid #00B9FF; |
||||||
|
border-radius: 20rpx; |
||||||
|
background-color: #fff; |
||||||
|
margin: 0 50rpx; |
||||||
|
height: 72rpx; |
||||||
|
line-height: 72rpx; |
||||||
|
margin-top: 20rpx; |
||||||
|
input{ |
||||||
|
height: 72rpx; |
||||||
|
line-height: 72rpx; |
||||||
|
padding: 0 40rpx; |
||||||
|
} |
||||||
|
.cuIcon-search{ |
||||||
|
margin: 0 20rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
.add-customer{ |
||||||
|
padding: 0 50rpx; |
||||||
|
button{ |
||||||
|
background-color: #00B9FF; |
||||||
|
border-radius: 30rpx; |
||||||
|
.mar-icon{ |
||||||
|
margin-right: 10rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
.sideBtn{ |
||||||
|
display: flex; |
||||||
|
flex-wrap: wrap; |
||||||
|
justify-content: space-around; |
||||||
|
} |
||||||
|
</style> |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,361 @@ |
|||||||
|
<template> |
||||||
|
<view> |
||||||
|
<!-- 菜单 (悬浮,预先隐藏)--> |
||||||
|
<!-- <me-tabs v-if="isShowSticky" v-model="tabIndex" :fixed="true" :tabs="tabs" @change="tabChange"></me-tabs> --> |
||||||
|
|
||||||
|
<mescroll-body ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback" :up="upOption" @scroll="scroll" @topclick="topClick"> |
||||||
|
<!-- 菜单 (在mescroll-uni中不能使用fixed,否则iOS滚动时会抖动, 所以需在mescroll-uni之外存在一个一样的菜单) --> |
||||||
|
<view id="tabInList"> |
||||||
|
<me-tabs v-model="tabIndex" :tabs="tabs" @change="tabChange"></me-tabs> |
||||||
|
</view> |
||||||
|
|
||||||
|
<!-- 搜索 --> |
||||||
|
<view class="cu-bar bg-white search" :class="scrolltop ? 'fixed' : ''" :style="[{top:CustomBar + 'px'}]"> |
||||||
|
<view class="search-form round"> |
||||||
|
<input @input="searchList" v-model="serach" type="text" placeholder="输入搜索的关键词" confirm-type="search"></input> |
||||||
|
<text class="cuIcon-search"></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="add-customer mat15 flex-between"> |
||||||
|
<text>共{{total}}条</text> |
||||||
|
<view v-if="roleArr.includes('57')"> |
||||||
|
<button class="mini-btn" type="primary" size="mini" @click="handleApply(item,'new')"><text class="cuIcon-add mar-icon"></text>添加</button> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<!-- 数据列表 --> |
||||||
|
<view class="cu-list menu-avatar"> |
||||||
|
<view class="cu-item" :class="modalName=='move-box-'+ index?'move-cur':''" v-for="(item,index) in businessApplyListData" :key="index" |
||||||
|
@touchstart="ListTouchStart" @touchmove="ListTouchMove" @touchend="ListTouchEnd" :data-target="'move-box-' + index"> |
||||||
|
<view class="good-list"> |
||||||
|
<view class="charge" :id="'index-' + item.name" :data-index="index"> |
||||||
|
<navigator v-if="[57,58].includes(item.roleId)" @click="handleApply(item,'watch')" hover-class='none' :url="'/guaranteePages/pages/addApplication/addApplication'" |
||||||
|
class="nav-li" navigateTo :style="[{animation: 'show ' + ((index+1)*0.2+1) + 's 1'}]"> |
||||||
|
<view class="charge-title flex-between"> |
||||||
|
<text>{{item.name}}</text> |
||||||
|
</view> |
||||||
|
<view class="charge-text"> |
||||||
|
<view> |
||||||
|
<text>业务编号:</text> |
||||||
|
<text class="mgl30">{{item.businessCode}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>客户名称:</text> |
||||||
|
<text class="mgl30">{{item.name}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>联系电话:</text> |
||||||
|
<text class="mgl30">{{item.phone}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>业务类别:</text> |
||||||
|
<text class="mgl30">{{item.businessType}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>申请额度(万元):</text> |
||||||
|
<text class="mgl30">{{item.applyAmount||''}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>申请期限:</text> |
||||||
|
<text class="mgl30">{{item.applyTime}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>申请日期:</text> |
||||||
|
<text class="mgl30">{{item.createTime}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>审批状态:</text> |
||||||
|
<text class="mgl30">{{item.statusVal}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>业务状态:</text> |
||||||
|
<text class="mgl30">{{item.businessStatusVal}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>操作状态:</text> |
||||||
|
<text class="mgl30">{{item.operationType}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>提单人:</text> |
||||||
|
<text class="mgl30">{{item.account}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>所属部门:</text> |
||||||
|
<text class="mgl30">{{item.deptName}}</text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</navigator> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="move foot-btn sideBtn"> |
||||||
|
<view class=""> |
||||||
|
<button v-show="item.status===1" v-if="[57].includes(item.roleId)&&item.businessStatus !== 3" class="mini-btn round def-btn mar-lr mgr6" type="primary" size="mini" @click="repaelPop(item)">撤销</button> |
||||||
|
<button v-show="item.status===1" v-if="[58].includes(item.roleId)&&item.businessStatus !== 3" class="mini-btn round def-btn mar-lr" type="primary" size="mini" @click="handleApply(item,'audit')">审核</button> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<button v-show="item.status===4||item.status===5||item.status == 6&&item.operatingStatus == 3&&item.businessStatus !== 2" v-if="[57].includes(item.roleId)&&item.businessStatus !== 3" class="mini-btn round def-btn mar-lr" type="primary" size="mini" @click="handleApply(item,'change')">修改</button> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</mescroll-body> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import MescrollMixin from "@/components/mescroll-uni/mescroll-mixins.js"; |
||||||
|
// import core from "@/util/core.js" |
||||||
|
export default { |
||||||
|
mixins: [MescrollMixin], // 使用mixin |
||||||
|
data() { |
||||||
|
return { |
||||||
|
upOption: { |
||||||
|
// 如果用mescroll-uni 则需要onScroll: true, 且需要 @scroll="scroll"; 而mescroll-body最简单只需在onPageScroll处理即可 |
||||||
|
// onScroll: true // 是否监听滚动事件, 默认false (配置为true时,可@scroll="scroll"获取到滚动条位置和方向) |
||||||
|
}, |
||||||
|
tabs:[ |
||||||
|
{name:'待指派', num:1, y:0, curPageLen:0, hasNext:true}, |
||||||
|
{name:'审核中', num:1, y:0, curPageLen:0, hasNext:true}, |
||||||
|
{name:'已审核', num:1, y:0, curPageLen:0, hasNext:true}, |
||||||
|
{name:'已拒绝', num:1, y:0, curPageLen:0, hasNext:true}, |
||||||
|
{name:'已驳回', num:1, y:0, curPageLen:0, hasNext:true}, |
||||||
|
{name:'已撤销', num:1, y:0, curPageLen:0, hasNext:true}, |
||||||
|
{name:'草稿', num:1, y:0, curPageLen:0, hasNext:true}, |
||||||
|
], |
||||||
|
tabIndex: 0, // 当前菜单下标 |
||||||
|
preIndex: 0, // 前一个菜单下标 |
||||||
|
navTop: null, // nav距离到顶部的距离 (如计算不准确,可直接写死某个值) |
||||||
|
isShowSticky: false, // 是否悬浮 |
||||||
|
TabCur: 0, |
||||||
|
scrollLeft: 0, |
||||||
|
CustomBar: this.CustomBar, |
||||||
|
listCurID: '', |
||||||
|
businessApplyListData: [], |
||||||
|
page: 1, |
||||||
|
pageSize: 10, |
||||||
|
total: 0, |
||||||
|
CustomerNumberOrName: '', |
||||||
|
scrolltop: false, |
||||||
|
listTouchStart: 0, |
||||||
|
listTouchDirection: null, |
||||||
|
modalName: null, |
||||||
|
roleArr:'',// 取得当前角色信息 |
||||||
|
serach:'',// 搜索值 |
||||||
|
status:'',// 筛选值 |
||||||
|
}; |
||||||
|
}, |
||||||
|
onShow() { |
||||||
|
// 返回时刷新数据 |
||||||
|
this.upCallback({num:1,size:10,search:''})// 每次返回该页面,刷新页面请求 |
||||||
|
}, |
||||||
|
created() { |
||||||
|
this.roleArr = uni.getStorageSync('roleArr') |
||||||
|
}, |
||||||
|
destroyed() {},// 离开时销毁组件 |
||||||
|
methods: { |
||||||
|
// 业务-列表 |
||||||
|
getBusinessApply(){ |
||||||
|
let params = { |
||||||
|
page: this.page?this.page:1, |
||||||
|
size: this.pageSize?this.pageSize:10, |
||||||
|
CustomerNumberOrName: this.CustomerNumberOrName |
||||||
|
} |
||||||
|
this.$http.get('/api-guarantee/dg-apply-amount-info/businessApplicationList',params).then(res => { |
||||||
|
res.data.list.map(e =>{ |
||||||
|
e.statusVal = this.core.statusVal(e.status) |
||||||
|
e.businessStatusVal = this.core.businessType(e.businessStatus) |
||||||
|
e.operationType = this.core.operationType(e.operatingStatus) |
||||||
|
}) |
||||||
|
|
||||||
|
this.businessApplyListData = res.data.list |
||||||
|
this.total = res.data.totalCount |
||||||
|
}).catch(function (error) {}); |
||||||
|
}, |
||||||
|
// 撤销业务 |
||||||
|
repaelPop(item){ |
||||||
|
let _this = this |
||||||
|
uni.showModal({ |
||||||
|
title: '提示', |
||||||
|
content: '是否撤销该申请', |
||||||
|
success: function (res) { |
||||||
|
if (res.confirm) { |
||||||
|
_this.$http.get('/api-guarantee/dg-apply-amount-info/revokeBusinessApplication',{id: item.id}).then(res => { |
||||||
|
uni.showToast({title: '撤销成功'}) |
||||||
|
_this.upCallback({num:1,size:10,search:''}) |
||||||
|
}).catch(function (error) {}); |
||||||
|
} else if (res.cancel) {} |
||||||
|
} |
||||||
|
}); |
||||||
|
}, |
||||||
|
changeTab(index){ |
||||||
|
this.TabCur = index; |
||||||
|
}, |
||||||
|
/*下拉刷新的回调 */ |
||||||
|
downCallback() { |
||||||
|
// 这里加载你想下拉刷新的数据, 比如刷新轮播数据 |
||||||
|
// loadSwiper(); |
||||||
|
// 下拉刷新的回调,默认重置上拉加载列表为第一页 (自动执行 page.num=1, 再触发upCallback方法 ) |
||||||
|
this.mescroll.resetUpScroll() |
||||||
|
// this.getBusinessApply() |
||||||
|
}, |
||||||
|
/*上拉加载的回调: 其中page.num:当前页 从1开始, page.size:每页数据条数,默认10 */ |
||||||
|
upCallback(page) { |
||||||
|
this.$http.get('/api-guarantee/dg-apply-amount-info/businessApplicationList',{ |
||||||
|
page: page.num, |
||||||
|
size: page.size, |
||||||
|
status:this.status, |
||||||
|
CustomerNumberOrName:page.search?page.search:'' |
||||||
|
}).then(res => { |
||||||
|
if(page.num == 1) this.businessApplyListData = []; //如果是第一页需手动制空列表 |
||||||
|
res.data.list.map(e =>{ |
||||||
|
e.statusVal = this.core.statusVal(e.status) |
||||||
|
e.businessStatusVal = this.core.businessType(e.businessStatus) |
||||||
|
e.operationType = this.core.operationType(e.operatingStatus) |
||||||
|
}) |
||||||
|
this.businessApplyListData = this.businessApplyListData.concat(res.data.list); //追加新数据 |
||||||
|
this.total = res.data.totalCount |
||||||
|
this.mescroll.endSuccess(res.data.list); // 隐藏加载状态栏 |
||||||
|
}).catch(()=>{ |
||||||
|
//联网失败, 结束加载 |
||||||
|
this.mescroll.endErr(); |
||||||
|
}) |
||||||
|
}, |
||||||
|
// 搜索框--结合防抖 |
||||||
|
searchList(e){ |
||||||
|
let that = this |
||||||
|
if (this.timer) { |
||||||
|
clearTimeout(this.timer); |
||||||
|
this.timer = null; |
||||||
|
} |
||||||
|
this.timer = setTimeout(function () { |
||||||
|
that.upCallback({num:1,size:10,search:e.detail.value}) |
||||||
|
},800) |
||||||
|
}, |
||||||
|
// 设置nav到顶部的距离 (滚动条为0, 菜单顶部的数据加载完毕获取到的navTop数值是最精确的) |
||||||
|
setNavTop(){ |
||||||
|
let view = uni.createSelectorQuery().select('#tabInList'); |
||||||
|
view.boundingClientRect(data => { |
||||||
|
if(data) this.navTop = data.top // 到屏幕顶部的距离 |
||||||
|
}).exec(); |
||||||
|
}, |
||||||
|
// mescroll-uni的滚动事件 (需在up配置onScroll:true才生效) |
||||||
|
// 而mescroll-body最简单只需在onPageScroll处理即可 |
||||||
|
scroll(){ |
||||||
|
// 菜单悬浮的原理: 监听滚动条的位置大于某个值时,控制顶部菜单的显示和隐藏 |
||||||
|
if (this.mescroll.getScrollTop() >= this.navTop) { |
||||||
|
this.isShowSticky = true // 显示悬浮菜单 |
||||||
|
} else { |
||||||
|
this.isShowSticky = false // 隐藏悬浮菜单 |
||||||
|
} |
||||||
|
}, |
||||||
|
// 点击回到顶部按钮时,先隐藏悬浮菜单,避免闪动 |
||||||
|
topClick(){ |
||||||
|
this.isShowSticky = false |
||||||
|
}, |
||||||
|
// 切换菜单 |
||||||
|
tabChange (index) { |
||||||
|
this.status = index+""; |
||||||
|
this.upCallback({num:1,size:10}) |
||||||
|
// 记录前一个菜单的数据 |
||||||
|
// let preTab = this.tabs[this.preIndex] |
||||||
|
// preTab.y = this.mescroll.getScrollTop(); // 滚动条位置 |
||||||
|
// // 当前菜单的数据 |
||||||
|
// let curTab = this.tabs[index] |
||||||
|
// if (!curTab.goods) { |
||||||
|
// // 没有初始化,则初始化 |
||||||
|
// this.isChangeTab = true; |
||||||
|
// this.mescroll.resetUpScroll() |
||||||
|
// } else{ |
||||||
|
// // 初始化过,则恢复之前的列表数据 |
||||||
|
// this.mescroll.setPageNum(curTab.num + 1); // 恢复当前页码 |
||||||
|
// this.mescroll.endSuccess(curTab.curPageLen, curTab.hasNext); // 恢复是否有下一页或显示空布局 |
||||||
|
// this.$nextTick(()=>{ |
||||||
|
// this.mescroll.scrollTo(curTab.y, 0) // 恢复滚动条的位置 |
||||||
|
// }) |
||||||
|
// this.getBusinessApply() |
||||||
|
// } |
||||||
|
}, |
||||||
|
// ListTouch触摸开始 |
||||||
|
ListTouchStart(e) { |
||||||
|
this.listTouchStart = e.touches[0].pageX |
||||||
|
}, |
||||||
|
// ListTouch计算方向 |
||||||
|
ListTouchMove(e) { |
||||||
|
this.listTouchDirection = e.touches[0].pageX - this.listTouchStart > 0 ? 'right' : 'left' |
||||||
|
}, |
||||||
|
// ListTouch计算滚动 |
||||||
|
ListTouchEnd(e) { |
||||||
|
if (this.listTouchDirection == 'left') { |
||||||
|
this.modalName = e.currentTarget.dataset.target |
||||||
|
} else { |
||||||
|
this.modalName = null |
||||||
|
} |
||||||
|
this.listTouchDirection = null |
||||||
|
}, |
||||||
|
// 传递当前操作状态和值 |
||||||
|
handleApply(item,str){ |
||||||
|
uni.setStorageSync('applyHandle',str)// 操作状态 |
||||||
|
if(str==='new'){// 添加的情况下跳转 |
||||||
|
this.goto('/guaranteePages/pages/addApplication/addApplication') |
||||||
|
uni.removeStorageSync('applyMsg') |
||||||
|
uni.removeStorageSync('businessId') |
||||||
|
}else{ |
||||||
|
uni.setStorageSync('applyMsg',JSON.stringify(item))// 所有的值存起来 |
||||||
|
uni.setStorageSync('businessId',item.businessId)// 储存有业务id |
||||||
|
} |
||||||
|
if(str!='new'&&str!='watch'){// 非新增和查看时的跳转 |
||||||
|
this.goto('/guaranteePages/pages/addApplication/addApplication') |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
}, |
||||||
|
// 使用mescroll-body最简单只需在onPageScroll处理即可 |
||||||
|
onPageScroll(e){ |
||||||
|
if (e.scrollTop >= this.navTop) { |
||||||
|
this.isShowSticky = true // 显示悬浮菜单 |
||||||
|
} else { |
||||||
|
this.isShowSticky = false // 隐藏悬浮菜单 |
||||||
|
} |
||||||
|
}, |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.cu-bar .search-form{ |
||||||
|
border: 2rpx solid #00B9FF; |
||||||
|
border-radius: 20rpx; |
||||||
|
background-color: #fff; |
||||||
|
margin: 0 50rpx; |
||||||
|
height: 72rpx; |
||||||
|
line-height: 72rpx; |
||||||
|
margin-top: 20rpx; |
||||||
|
input{ |
||||||
|
height: 72rpx; |
||||||
|
line-height: 72rpx; |
||||||
|
padding: 0 40rpx; |
||||||
|
} |
||||||
|
.cuIcon-search{ |
||||||
|
margin: 0 20rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
.add-customer{ |
||||||
|
padding: 0 50rpx; |
||||||
|
button{ |
||||||
|
background-color: #00B9FF; |
||||||
|
border-radius: 30rpx; |
||||||
|
.mar-icon{ |
||||||
|
margin-right: 10rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
.sideBtn{ |
||||||
|
display: flex; |
||||||
|
flex-wrap: wrap; |
||||||
|
justify-content: space-around; |
||||||
|
} |
||||||
|
/deep/ .cu-list>.cu-item .move view{ |
||||||
|
flex: none; |
||||||
|
margin-top: 10rpx; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,341 @@ |
|||||||
|
<template> |
||||||
|
<view> |
||||||
|
<!-- 菜单 (悬浮,预先隐藏)--> |
||||||
|
<me-tabs v-if="isShowSticky" v-model="tabIndex" :fixed="true" :tabs="tabs" @change="tabChange"></me-tabs> |
||||||
|
|
||||||
|
<mescroll-body ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback" :up="upOption" @scroll="scroll" @topclick="topClick"> |
||||||
|
<!-- 菜单 (在mescroll-uni中不能使用fixed,否则iOS滚动时会抖动, 所以需在mescroll-uni之外存在一个一样的菜单) --> |
||||||
|
<view id="tabInList"> |
||||||
|
<me-tabs v-model="tabIndex" :tabs="tabs" @change="tabChange"></me-tabs> |
||||||
|
</view> |
||||||
|
|
||||||
|
<!-- 搜索 --> |
||||||
|
<view class="cu-bar bg-white search" :class="scrolltop ? 'fixed' : ''" :style="[{top:CustomBar + 'px'}]"> |
||||||
|
<view class="search-form round"> |
||||||
|
<input @input="searchList" v-model="listParams.customerNumberOrName" type="text" placeholder="输入搜索的关键词" confirm-type="search"></input> |
||||||
|
<text class="cuIcon-search"></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="cu-list menu-avatar"> |
||||||
|
<view class="cu-item" :class="modalName=='move-box-'+ index?'move-cur':''" v-for="(item,index) in listData" :key="index" |
||||||
|
@touchstart="ListTouchStart" @touchmove="ListTouchMove" @touchend="ListTouchEnd" :data-target="'move-box-' + index"> |
||||||
|
<view class="good-list"> |
||||||
|
<view class="charge" :id="'index-' + item.name" :data-index="item.name"> |
||||||
|
<navigator @click="handleGe(item,'watch')" hover-class='none' :url="'/guaranteePages/pages/assetSee/assetSee'" |
||||||
|
class="nav-li" navigateTo :style="[{animation: 'show ' + ((index+1)*0.2+1) + 's 1'}]"> |
||||||
|
<view class="charge-title flex-between"> |
||||||
|
<text>{{item.name}}</text> |
||||||
|
</view> |
||||||
|
<view class="charge-text"> |
||||||
|
<view> |
||||||
|
<text>业务编号:</text> |
||||||
|
<text class="mgl30">{{item.businessCode}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>客户名称:</text> |
||||||
|
<text class="mgl30">{{item.name}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>联系电话:</text> |
||||||
|
<text class="mgl30">{{item.phone}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>业务类别:</text> |
||||||
|
<text class="mgl30">{{item.businessType}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>申请额度(万元):</text> |
||||||
|
<text class="mgl30">{{item.applyAmount}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>申请期限:</text> |
||||||
|
<text class="mgl30">{{item.applyTime}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>申请日期:</text> |
||||||
|
<text class="mgl30">{{item.createTime}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>审批状态:</text> |
||||||
|
<text class="mgl30">{{item.statusVal}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>业务状态:</text> |
||||||
|
<text class="mgl30">{{item.businessStatusVal}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>操作状态:</text> |
||||||
|
<text class="mgl30">{{item.operationType}}</text> |
||||||
|
</view> |
||||||
|
<!-- <view class="flex-between"> |
||||||
|
<text class="time-text">{{item.time}}</text> |
||||||
|
<text class="status-text" :style="'background-color:'+item.status.bgColor+';color:'+item.status.textColor">{{item.status.text}}</text> |
||||||
|
</view> --> |
||||||
|
</view> |
||||||
|
</navigator> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="move foot-btn sideBtn"> |
||||||
|
<view class=""> |
||||||
|
<button v-show="item.status===1&&item.operatingStatus == 1||item.status===4&&item.operatingStatus == 1" v-if="[63].includes(item.roleId)&&item.businessStatus !== 3" class="mini-btn round def-btn mar-lr" type="primary" size="mini" @click="handleGe(item,'survey')">调查</button> |
||||||
|
<button v-show="item.status===0&&item.operatingStatus == 1" v-if="[62].includes(item.roleId)&&item.businessStatus !== 3" class="mini-btn round suc-btn" type="primary" size="mini" @click="handleGe(item,'none',true)">指派</button> |
||||||
|
</view> |
||||||
|
<view class=""> |
||||||
|
<button v-show="item.status===1&&item.operatingStatus == 1||item.status===4&&item.operatingStatus == 1" v-if="[62,64].includes(item.roleId)&&item.businessStatus !== 3" class="mini-btn round def-btn mar-lr" type="primary" size="mini" @click="handleGe(item,'audit')">审核</button> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</mescroll-body> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import MescrollMixin from "@/components/mescroll-uni/mescroll-mixins.js"; |
||||||
|
export default { |
||||||
|
mixins: [MescrollMixin], // 使用mixin |
||||||
|
data() { |
||||||
|
return { |
||||||
|
upOption: { |
||||||
|
// 如果用mescroll-uni 则需要onScroll: true, 且需要 @scroll="scroll"; 而mescroll-body最简单只需在onPageScroll处理即可 |
||||||
|
// onScroll: true // 是否监听滚动事件, 默认false (配置为true时,可@scroll="scroll"获取到滚动条位置和方向) |
||||||
|
}, |
||||||
|
tabs:[ |
||||||
|
{name:'全部', num:1, y:0, curPageLen:0, hasNext:true}, |
||||||
|
{name:'待指派', num:1, y:0, curPageLen:0, hasNext:true}, |
||||||
|
{name:'审核中', num:1, y:0, curPageLen:0, hasNext:true}, |
||||||
|
{name:'已审核', num:1, y:0, curPageLen:0, hasNext:true}, |
||||||
|
{name:'已拒绝', num:1, y:0, curPageLen:0, hasNext:true}, |
||||||
|
{name:'已驳回', num:1, y:0, curPageLen:0, hasNext:true}, |
||||||
|
{name:'已撤销', num:1, y:0, curPageLen:0, hasNext:true}, |
||||||
|
], |
||||||
|
tabIndex: 0, // 当前菜单下标 |
||||||
|
preIndex: 0, // 前一个菜单下标 |
||||||
|
navTop: null, // nav距离到顶部的距离 (如计算不准确,可直接写死某个值) |
||||||
|
isShowSticky: false, // 是否悬浮 |
||||||
|
TabCur: 0, |
||||||
|
scrollLeft: 0, |
||||||
|
CustomBar: this.CustomBar, |
||||||
|
listCurID: '', |
||||||
|
listData: [], |
||||||
|
status:'',// 状态 |
||||||
|
total: 1, |
||||||
|
scrolltop: false, |
||||||
|
listTouchStart: 0, |
||||||
|
listTouchDirection: null, |
||||||
|
modalName: null, |
||||||
|
|
||||||
|
listParams:{ |
||||||
|
customerNumberOrName:"",// 搜索框 |
||||||
|
page:1, |
||||||
|
size:10 |
||||||
|
} |
||||||
|
}; |
||||||
|
}, |
||||||
|
onShow() { |
||||||
|
this.upCallback({ |
||||||
|
num:1,size:10,serach:'' |
||||||
|
}) |
||||||
|
}, |
||||||
|
mounted() { |
||||||
|
|
||||||
|
}, |
||||||
|
methods: { |
||||||
|
changeTab(index){ |
||||||
|
this.TabCur = index; |
||||||
|
}, |
||||||
|
/*下拉刷新的回调 */ |
||||||
|
downCallback() { |
||||||
|
// 这里加载你想下拉刷新的数据, 比如刷新轮播数据 |
||||||
|
// loadSwiper(); |
||||||
|
// 下拉刷新的回调,默认重置上拉加载列表为第一页 (自动执行 page.num=1, 再触发upCallback方法 ) |
||||||
|
this.mescroll.resetUpScroll() |
||||||
|
}, |
||||||
|
/*上拉加载的回调: 其中page.num:当前页 从1开始, page.size:每页数据条数,默认10 */ |
||||||
|
upCallback(page) { |
||||||
|
this.$http.post('/api-guarantee/dg-assets-investigation/assetsList',{ |
||||||
|
page: page.num?page.num:1, |
||||||
|
size: page.size?page.size:10, |
||||||
|
status:this.status?this.status:'', |
||||||
|
customerNumberOrName:page.search?page.search:'' |
||||||
|
}).then(res => { |
||||||
|
if(page.num == 1) this.listData = []; //如果是第一页需手动制空列表 |
||||||
|
res.data.list.map(e =>{ |
||||||
|
e.statusVal = this.core.statusVal(e.status) |
||||||
|
e.businessStatusVal = this.core.businessType(e.businessStatus) |
||||||
|
e.operationType = this.core.operationType(e.operatingStatus) |
||||||
|
}) |
||||||
|
this.listData = this.listData.concat(res.data.list); //追加新数据 |
||||||
|
this.mescroll.endSuccess(res.data.list); // 隐藏加载状态栏 |
||||||
|
}).catch(()=>{ |
||||||
|
//联网失败, 结束加载 |
||||||
|
this.mescroll.endErr(); |
||||||
|
}) |
||||||
|
}, |
||||||
|
// 搜索框--结合防抖 |
||||||
|
searchList(e){ |
||||||
|
let that = this |
||||||
|
if (this.timer) { |
||||||
|
clearTimeout(this.timer); |
||||||
|
this.timer = null; |
||||||
|
} |
||||||
|
this.timer = setTimeout(function () { |
||||||
|
that.upCallback({num:1,size:10,search:e.detail.value}) |
||||||
|
},800) |
||||||
|
}, |
||||||
|
// 设置nav到顶部的距离 (滚动条为0, 菜单顶部的数据加载完毕获取到的navTop数值是最精确的) |
||||||
|
setNavTop(){ |
||||||
|
let view = uni.createSelectorQuery().select('#tabInList'); |
||||||
|
view.boundingClientRect(data => { |
||||||
|
this.navTop = data.top // 到屏幕顶部的距离 |
||||||
|
}).exec(); |
||||||
|
}, |
||||||
|
// mescroll-uni的滚动事件 (需在up配置onScroll:true才生效) |
||||||
|
// 而mescroll-body最简单只需在onPageScroll处理即可 |
||||||
|
scroll(){ |
||||||
|
// 菜单悬浮的原理: 监听滚动条的位置大于某个值时,控制顶部菜单的显示和隐藏 |
||||||
|
if (this.mescroll.getScrollTop() >= this.navTop) { |
||||||
|
this.isShowSticky = true // 显示悬浮菜单 |
||||||
|
} else { |
||||||
|
this.isShowSticky = false // 隐藏悬浮菜单 |
||||||
|
} |
||||||
|
}, |
||||||
|
// 点击回到顶部按钮时,先隐藏悬浮菜单,避免闪动 |
||||||
|
topClick(){ |
||||||
|
this.isShowSticky = false |
||||||
|
}, |
||||||
|
// 切换菜单 |
||||||
|
tabChange (index) { |
||||||
|
// this.listParams.status = index |
||||||
|
if(index==0){ |
||||||
|
this.status = '' |
||||||
|
}else{ |
||||||
|
this.status = `${index-1}` |
||||||
|
} |
||||||
|
this.upCallback({ |
||||||
|
num:1,size:10,serach:'' |
||||||
|
}) |
||||||
|
// 记录前一个菜单的数据 |
||||||
|
let preTab = this.tabs[this.preIndex] |
||||||
|
preTab.y = this.mescroll.getScrollTop(); // 滚动条位置 |
||||||
|
this.preIndex = index; |
||||||
|
// 当前菜单的数据 |
||||||
|
let curTab = this.tabs[index] |
||||||
|
if (!curTab.goods) { |
||||||
|
// 没有初始化,则初始化 |
||||||
|
this.isChangeTab = true; |
||||||
|
this.mescroll.resetUpScroll() |
||||||
|
} else{ |
||||||
|
// 初始化过,则恢复之前的列表数据 |
||||||
|
this.mescroll.setPageNum(curTab.num + 1); // 恢复当前页码 |
||||||
|
this.mescroll.endSuccess(curTab.curPageLen, curTab.hasNext); // 恢复是否有下一页或显示空布局 |
||||||
|
this.$nextTick(()=>{ |
||||||
|
this.mescroll.scrollTo(curTab.y, 0) // 恢复滚动条的位置 |
||||||
|
}) |
||||||
|
} |
||||||
|
}, |
||||||
|
// ListTouch触摸开始 |
||||||
|
ListTouchStart(e) { |
||||||
|
this.listTouchStart = e.touches[0].pageX |
||||||
|
}, |
||||||
|
|
||||||
|
// ListTouch计算方向 |
||||||
|
ListTouchMove(e) { |
||||||
|
this.listTouchDirection = e.touches[0].pageX - this.listTouchStart > 0 ? 'right' : 'left' |
||||||
|
}, |
||||||
|
|
||||||
|
// ListTouch计算滚动 |
||||||
|
ListTouchEnd(e) { |
||||||
|
if (this.listTouchDirection == 'left') { |
||||||
|
this.modalName = e.currentTarget.dataset.target |
||||||
|
} else { |
||||||
|
this.modalName = null |
||||||
|
} |
||||||
|
this.listTouchDirection = null |
||||||
|
}, |
||||||
|
// 列表数据 |
||||||
|
getList(val){ |
||||||
|
if(val==='search'){ |
||||||
|
this.listParams.page = 1 |
||||||
|
this.listParams.size = 10 |
||||||
|
} |
||||||
|
this.$http.get('/api-guarantee/dg-assets-investigation/assetsList',this.listParams).then(res=>{ |
||||||
|
if(res.data&&res.data.list&&res.data.list.length!==0){ |
||||||
|
res.data.list.map(e =>{ |
||||||
|
e.statusVal = this.core.statusVal(e.status) |
||||||
|
e.businessStatusVal = this.core.businessType(e.businessStatus) |
||||||
|
e.operationType = this.core.operationType(e.operatingStatus) |
||||||
|
}) |
||||||
|
this.listData = res.data.list |
||||||
|
}else{ |
||||||
|
this.listData = [] |
||||||
|
} |
||||||
|
|
||||||
|
}) |
||||||
|
|
||||||
|
}, |
||||||
|
|
||||||
|
// 传递当前操作状态和值 |
||||||
|
handleGe(item,str,assign){ |
||||||
|
if(assign){// 如果是指派-进入指派页面 |
||||||
|
this.goto('/guaranteePages/pages/assignCommissioner/assignCommissioner')// 跳转指派 |
||||||
|
uni.removeStorageSync('handleGe') |
||||||
|
}else{ |
||||||
|
uni.setStorageSync('handleGe',str)// 操作状态记录 |
||||||
|
} |
||||||
|
if(!assign&&str!=='watch'){// 审核 |
||||||
|
this.goto('/guaranteePages/pages/assetSee/assetSee') |
||||||
|
} |
||||||
|
uni.setStorageSync('applyMsg',JSON.stringify(item))// 所有的值存起来 |
||||||
|
uni.setStorageSync('businessId',item.businessId)// 储存有业务id |
||||||
|
}, |
||||||
|
|
||||||
|
}, |
||||||
|
// 使用mescroll-body最简单只需在onPageScroll处理即可 |
||||||
|
onPageScroll(e){ |
||||||
|
if (e.scrollTop >= this.navTop) { |
||||||
|
this.isShowSticky = true // 显示悬浮菜单 |
||||||
|
} else { |
||||||
|
this.isShowSticky = false // 隐藏悬浮菜单 |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
|
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.cu-bar .search-form{ |
||||||
|
border: 2rpx solid #00B9FF; |
||||||
|
border-radius: 20rpx; |
||||||
|
background-color: #fff; |
||||||
|
margin: 0 50rpx; |
||||||
|
height: 72rpx; |
||||||
|
line-height: 72rpx; |
||||||
|
margin-top: 20rpx; |
||||||
|
input{ |
||||||
|
height: 72rpx; |
||||||
|
line-height: 72rpx; |
||||||
|
padding: 0 40rpx; |
||||||
|
} |
||||||
|
.cuIcon-search{ |
||||||
|
margin: 0 20rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
.add-customer{ |
||||||
|
padding: 0 50rpx; |
||||||
|
button{ |
||||||
|
background-color: #00B9FF; |
||||||
|
border-radius: 30rpx; |
||||||
|
.mar-icon{ |
||||||
|
margin-right: 10rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
.sideBtn{ |
||||||
|
display: flex; |
||||||
|
flex-wrap: wrap; |
||||||
|
justify-content: space-around; |
||||||
|
} |
||||||
|
</style> |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,289 @@ |
|||||||
|
<template> |
||||||
|
<view class="ass-content"> |
||||||
|
<view class="flex-align-start"> |
||||||
|
<view class="ass-label"> |
||||||
|
<text>A角选择</text> |
||||||
|
</view> |
||||||
|
<view class="assign-view"> |
||||||
|
<view class="cu-bar search"> |
||||||
|
<view class="search-form round"> |
||||||
|
<input type="text" v-model="aSearch" placeholder="请输入" confirm-type="search"></input> |
||||||
|
<text class="cuIcon-search"></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<mix-tree @checkedRund = "getCheckedA" :fliter="aSearch" :list="data" @treeItemClick="handleCheck"></mix-tree> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="flex-align-start mat15"> |
||||||
|
<view class="ass-label"> |
||||||
|
<text>B角选择</text> |
||||||
|
</view> |
||||||
|
<view class="assign-view"> |
||||||
|
<view class="cu-bar search"> |
||||||
|
<view class="search-form round"> |
||||||
|
<input type="text" v-model="bSearch" placeholder="请输入" confirm-type="search"></input> |
||||||
|
<text class="cuIcon-search"></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<mix-tree @checkedRund = "getCheckedB" :fliter="bSearch" :list="data" @treeItemClick="handleCheck"></mix-tree> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="flex-align-start mat15"> |
||||||
|
<view class="ass-label"> |
||||||
|
<text>已选员工</text> |
||||||
|
</view> |
||||||
|
<view class="AB-input"> |
||||||
|
<view class="flex"> |
||||||
|
<input disabled type="text" :value="'A角: '+A" placeholder="请选择A角"/> |
||||||
|
<!-- <text class="cuIcon-roundclosefill" @click="A = ''"></text> --> |
||||||
|
</view> |
||||||
|
<view class="flex mat15"> |
||||||
|
<input disabled type="text" :value="'B角: '+B" placeholder="请选择B角"/> |
||||||
|
<!-- <text class="cuIcon-roundclosefill" @click="B = ''"></text> --> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="mat40 flex-justify-center ass-foot-btn"> |
||||||
|
<button class="cu-btn block def-btn margin-tb-sm lg round mar15" @click="geDesignateAB">确定</button> |
||||||
|
<button class="cu-btn block cancel-btn margin-tb-sm lg round mal15" @click="back()">取消</button> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import LyTree from '@/components/ly-tree/ly-tree.vue' |
||||||
|
// import mix-tree from '@/components/mix-tree/mix-tree.vue' |
||||||
|
export default { |
||||||
|
components: { |
||||||
|
LyTree |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
filterText: '', |
||||||
|
A: '', |
||||||
|
B: '', |
||||||
|
data: [], |
||||||
|
defaultProps: { |
||||||
|
children: 'children', |
||||||
|
label: 'name' |
||||||
|
}, |
||||||
|
bSearch:'', |
||||||
|
aSearch:'', |
||||||
|
getItem:{},// 取得传递的item |
||||||
|
arrDatas:[],//扁平化 |
||||||
|
aID:'', |
||||||
|
bID:'', |
||||||
|
|
||||||
|
}; |
||||||
|
}, |
||||||
|
created() { |
||||||
|
if(uni.getStorageSync('decideIndex')){ |
||||||
|
let data = JSON.parse(uni.getStorageSync('decideIndex')) |
||||||
|
this.getItem.businessId = data.businessId |
||||||
|
this.getItem.companyId = data.companyId |
||||||
|
this.getItem.id = data.detailId |
||||||
|
uni.removeStorageSync('decideIndex') |
||||||
|
}else{ |
||||||
|
this.getItem = JSON.parse(uni.getStorageSync('applyMsg')) |
||||||
|
} |
||||||
|
}, |
||||||
|
onHide() { |
||||||
|
uni.removeStorageSync('applyMsg') |
||||||
|
}, |
||||||
|
onShow() { |
||||||
|
this.trees()// 每次展示调用树数据 |
||||||
|
}, |
||||||
|
onLoad() { |
||||||
|
// 如果初始化树时设置节点选择需要做适当延时,可以在下一个事件轮询中执行 |
||||||
|
// this.$nextTick(() => { |
||||||
|
// this.$refs.tree.setCheckedKeys([6]) |
||||||
|
// }) |
||||||
|
}, |
||||||
|
watch: { |
||||||
|
filterText(val) { |
||||||
|
this.$refs.tree.filter(val); |
||||||
|
}, |
||||||
|
bSearch(val){ |
||||||
|
} |
||||||
|
}, |
||||||
|
// 如果不需要不用到这些方法,需要删除相应代码,打印大量日志会造成性能损耗 |
||||||
|
methods: { |
||||||
|
// 只有在"点击"修改的指定节点会触发(也就是说这个方法只会触发一次),obj中包含当前选中 |
||||||
|
handleCheck(obj) { |
||||||
|
}, |
||||||
|
// 获取树结构数据 |
||||||
|
trees(){ |
||||||
|
this.$http.get('/api-hrms//hrms/dept/empTrees').then(res=>{ |
||||||
|
this.data = res.data |
||||||
|
this.disposeData(this.data)// 处理数据 |
||||||
|
}) |
||||||
|
}, |
||||||
|
// 处理树结构数据 |
||||||
|
disposeData(data){// children和员工分开,整合到一个children里 |
||||||
|
let len = data.length |
||||||
|
for(let i = 0;i<len;i++){ |
||||||
|
if(data[i].children){ // children有值则---进入整合,实际上可能没有children,也需要整合 |
||||||
|
if(data[i].userDept){//有员工就整合 |
||||||
|
if(data[i].userDept.length!==0){//多个员工处理 |
||||||
|
let len = data[i].userDept.length |
||||||
|
for(let a=0;a<len;a++){ |
||||||
|
data[i].userDept[a].name=data[i].userDept[a].account |
||||||
|
} |
||||||
|
} |
||||||
|
data[i].children = [...data[i].children,...data[i].userDept] |
||||||
|
data[i].children = [...new Set(data[i].children)] |
||||||
|
} |
||||||
|
if(data[i].children.length!==0){//有子级则进入子级,继续循环 |
||||||
|
this.disposeData(data[i].children) |
||||||
|
} |
||||||
|
}else if(!data[i].children&&data[i].userDept&&data[i].userDept.length!==0){//没有子级,而有员工时,创建新的children |
||||||
|
data[i].children = [] |
||||||
|
let len = data[i].userDept.length |
||||||
|
for(let a=0;a<len;a++){ |
||||||
|
data[i].userDept[a].name=data[i].userDept[a].account |
||||||
|
} |
||||||
|
data[i].children = [...data[i].children,...data[i].userDept] |
||||||
|
data[i].children =[...new Set(data[i].children)] |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
// 获取点击事件,可设置单选多选等 |
||||||
|
getCheckedA(nameList,idsList){ |
||||||
|
if(idsList.length!==0){ |
||||||
|
this.aID = idsList[0] |
||||||
|
}else{ |
||||||
|
this.aID = '' |
||||||
|
} |
||||||
|
if(nameList&&nameList.length!==0){ |
||||||
|
this.A = nameList[0] |
||||||
|
}else{ |
||||||
|
this.A = '' |
||||||
|
} |
||||||
|
|
||||||
|
if(this.aID&&this.aID===this.bID){ |
||||||
|
this.aID = '' |
||||||
|
return uni.showToast({title: '请勿选择相同的A/B角!',icon:'none'}) |
||||||
|
} |
||||||
|
|
||||||
|
}, |
||||||
|
// 获取点击事件b |
||||||
|
getCheckedB(nameList,idsList){ |
||||||
|
this.B = nameList[0] |
||||||
|
this.bID = idsList[0] |
||||||
|
if(this.aID===this.bID){ |
||||||
|
this.bID = '' |
||||||
|
return uni.showToast({title: '请勿选择相同的A/B角!',icon:'none'}) |
||||||
|
} |
||||||
|
}, |
||||||
|
// 扁平化树结构数据 |
||||||
|
renderTreeList(list=[], rank=0, parentId=[]){ |
||||||
|
list.forEach(item=>{ |
||||||
|
this.arrDatas.push({ |
||||||
|
id: item.id, |
||||||
|
name: item.name, |
||||||
|
parentId, // 父级id数组 |
||||||
|
rank, // 层级 |
||||||
|
showChild: false, //子级是否显示 |
||||||
|
show: rank === 0 ,// 自身是否显示 |
||||||
|
// 自定义 |
||||||
|
account: item.account?item.account:false,// 设置展示员工的勾选 |
||||||
|
}) |
||||||
|
if(Array.isArray(item.children) && item.children.length > 0){ |
||||||
|
let parents = [...parentId]; |
||||||
|
parents.push(item.id); |
||||||
|
this.renderTreeList(item.children, rank+1, parents); |
||||||
|
}else{ |
||||||
|
this.arrDatas[this.arrDatas.length-1].lastRank = true; |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
// // 把值过滤传递--扁平化后的处理 |
||||||
|
// async filterNode(value,data,str) { |
||||||
|
// if(!value){ |
||||||
|
// this.trees() |
||||||
|
// return |
||||||
|
// } |
||||||
|
// let arr = JSON.parse(JSON.stringify(data)) |
||||||
|
// this.renderTreeList(data)// 扁平化处理 |
||||||
|
// this.data = [] |
||||||
|
// this.arrDatas.forEach(e=>{// 处理传值 |
||||||
|
// if (e.name.includes(value)){ |
||||||
|
// this.data.push(e) |
||||||
|
// } |
||||||
|
// }) |
||||||
|
// this.data = [...new Set(this.data)] |
||||||
|
// }, |
||||||
|
|
||||||
|
// 指派ab |
||||||
|
geDesignateAB(){ |
||||||
|
let obj = { |
||||||
|
businessId:this.getItem.businessId, |
||||||
|
companyId:this.getItem.companyId, |
||||||
|
id:this.getItem.id, |
||||||
|
} |
||||||
|
|
||||||
|
obj.empAId = this.aID |
||||||
|
obj.empBId = this.bID |
||||||
|
if(!obj.empAId||!obj.empBId){ |
||||||
|
return uni.showToast({title: '请选择角色后再确定!',icon:'none'}) |
||||||
|
} |
||||||
|
this.$http.post('/api-guarantee/dg-guarantee-assign-user/assignCorners',obj).then(res=>{ |
||||||
|
uni.showToast({ |
||||||
|
title: "操作成功!" |
||||||
|
}) |
||||||
|
this.back() |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.ass-content{ |
||||||
|
padding: 40rpx 40rpx; |
||||||
|
.ass-foot-btn .cu-btn.lg{ |
||||||
|
padding: 0 100rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
.ass-label{ |
||||||
|
color: #00B9FF; |
||||||
|
font-size: 28rpx; |
||||||
|
margin-right: 40rpx; |
||||||
|
} |
||||||
|
.assign-view{ |
||||||
|
border-radius: 20rpx; |
||||||
|
background-color: #fff; |
||||||
|
box-shadow: 0 0 16rpx #ccc; |
||||||
|
height: 600rpx; |
||||||
|
overflow-x: hidden; |
||||||
|
overflow-y: scroll; |
||||||
|
} |
||||||
|
.cu-bar .search-form{ |
||||||
|
border-radius: 20rpx; |
||||||
|
background-color: #f5f5f5; |
||||||
|
height: 72rpx; |
||||||
|
line-height: 72rpx; |
||||||
|
input{ |
||||||
|
height: 72rpx; |
||||||
|
line-height: 72rpx; |
||||||
|
padding: 0 40rpx; |
||||||
|
} |
||||||
|
.cuIcon-search{ |
||||||
|
margin: 0 20rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
.AB-input{ |
||||||
|
view{ |
||||||
|
background-color: #f5f5f5; |
||||||
|
color: #666; |
||||||
|
padding: 20rpx 40rpx; |
||||||
|
border-radius: 20rpx; |
||||||
|
text{ |
||||||
|
color: #B2B2B2; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,251 @@ |
|||||||
|
<template> |
||||||
|
<view class="ass-content"> |
||||||
|
<view class="flex-align-start"> |
||||||
|
<view class="ass-label"> |
||||||
|
<text>资产部专员</text> |
||||||
|
</view> |
||||||
|
<view class="assign-view"> |
||||||
|
<view class="cu-bar search"> |
||||||
|
<view class="search-form round"> |
||||||
|
<input type="text" v-model="search" placeholder="请输入" confirm-type="search"></input> |
||||||
|
<text class="cuIcon-search"></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<mix-tree class="wd60" @checkedRund = "getChecked" :fliter="search" :list="data" @treeItemClick="handleCheck"></mix-tree> |
||||||
|
|
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="flex-align-start mat15"> |
||||||
|
<view class="ass-label"> |
||||||
|
<text>已选调查专员</text> |
||||||
|
</view> |
||||||
|
<view class="AB-input"> |
||||||
|
<view class="flex"> |
||||||
|
<input disabled type="text" :value="name" placeholder="请选择调查专员"/> |
||||||
|
<!-- <text class="cuIcon-roundclosefill" @click="A = ''"></text> --> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="mat40 flex-justify-center ass-foot-btn"> |
||||||
|
<button class="cu-btn block def-btn margin-tb-sm lg round mar15" @click="assetAssign()">确定</button> |
||||||
|
<button class="cu-btn block cancel-btn margin-tb-sm lg round mal15" @click="back()">取消</button> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import LyTree from '@/components/ly-tree/ly-tree.vue' |
||||||
|
export default { |
||||||
|
components: { |
||||||
|
LyTree |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
filterNodeData: null, |
||||||
|
filterText: '', |
||||||
|
name:'',// 选中的名称 |
||||||
|
ID:"",// 选中的id |
||||||
|
data: [],// 树结构数据 |
||||||
|
defaultProps: { |
||||||
|
children: 'children', |
||||||
|
label: 'label' |
||||||
|
}, |
||||||
|
search:'',// 搜索绑定值 |
||||||
|
getItem:{},// 读取传值 |
||||||
|
}; |
||||||
|
}, |
||||||
|
props:{}, |
||||||
|
created() { |
||||||
|
if(uni.getStorageSync('decideIndex')){ |
||||||
|
let data = JSON.parse(uni.getStorageSync('decideIndex')) |
||||||
|
this.getItem.businessId = data.businessId |
||||||
|
this.getItem.companyId = data.companyId |
||||||
|
this.getItem.id = data.detailId |
||||||
|
uni.removeStorageSync('decideIndex') |
||||||
|
}else{ |
||||||
|
this.getItem = JSON.parse(uni.getStorageSync('applyMsg')) |
||||||
|
} |
||||||
|
}, |
||||||
|
onShow() { |
||||||
|
this.trees()// 每次展示调用树数据 |
||||||
|
}, |
||||||
|
beforeDestroy() { |
||||||
|
uni.removeStorageSync('applyMsg') |
||||||
|
}, |
||||||
|
onLoad() { |
||||||
|
// 如果初始化树时设置节点选择需要做适当延时,可以在下一个事件轮询中执行 |
||||||
|
// this.$nextTick(() => { |
||||||
|
// this.$refs.tree.setCheckedKeys([6]) |
||||||
|
// }) |
||||||
|
}, |
||||||
|
watch: { |
||||||
|
// filterText(val) { |
||||||
|
// this.$refs.tree.filter(val); |
||||||
|
// } |
||||||
|
}, |
||||||
|
// 如果不需要不用到这些方法,需要删除相应代码,打印大量日志会造成性能损耗 |
||||||
|
methods: { |
||||||
|
// 只有在"点击"修改的指定节点会触发(也就是说这个方法只会触发一次),obj中包含当前选中 |
||||||
|
handleCheck(obj) { |
||||||
|
// obj: { |
||||||
|
// checkedKeys: [9, 5], // 当前选中节点的id数组 |
||||||
|
// checkedNodes: [{...}, {...}], // 当前选中节点数组 |
||||||
|
// data: {...}, // 当前节点的数据 |
||||||
|
// halfCheckedKeys: [1, 4, 2], // 半选中节点的id数组 |
||||||
|
// halfCheckedNodes: [{...}, {...}, {...}], // 半选中节点的数组 |
||||||
|
// node: Node {...} // 当前节点实例 |
||||||
|
// } |
||||||
|
|
||||||
|
}, |
||||||
|
|
||||||
|
// // 只要节点的选中状态改变就触发(包括设置默认选中,点击选中/取消选中),会被触发多次 |
||||||
|
// handleRadioChange(obj) { |
||||||
|
// }, |
||||||
|
|
||||||
|
// handleNodeClick(obj) { |
||||||
|
// }, |
||||||
|
// // 查询过滤 |
||||||
|
// filterNode(value, data) { |
||||||
|
// if (!value) return true; |
||||||
|
// return data.label.indexOf(value) !== -1; |
||||||
|
// }, |
||||||
|
|
||||||
|
// 获取树结构数据 |
||||||
|
trees(){ |
||||||
|
this.$http.get('/api-hrms//hrms/dept/empTrees').then(res=>{ |
||||||
|
this.data = res.data |
||||||
|
this.disposeData(this.data)// 处理数据 |
||||||
|
}) |
||||||
|
}, |
||||||
|
// 处理树结构数据 |
||||||
|
disposeData(data){// children和员工分开,整合到一个children里 |
||||||
|
let len = data.length |
||||||
|
for(let i = 0;i<len;i++){ |
||||||
|
if(data[i].children){ // children有值则---进入整合,实际上可能没有children,也需要整合 |
||||||
|
if(data[i].userDept){//有员工就整合 |
||||||
|
if(data[i].userDept.length!==0){//多个员工处理 |
||||||
|
let len = data[i].userDept.length |
||||||
|
for(let a=0;a<len;a++){ |
||||||
|
data[i].userDept[a].name=data[i].userDept[a].account |
||||||
|
} |
||||||
|
} |
||||||
|
data[i].children = [...data[i].children,...data[i].userDept] |
||||||
|
data[i].children = [...new Set(data[i].children)] |
||||||
|
} |
||||||
|
if(data[i].children.length!==0){//有子级则进入子级,继续循环 |
||||||
|
this.disposeData(data[i].children) |
||||||
|
} |
||||||
|
}else if(!data[i].children&&data[i].userDept&&data[i].userDept.length!==0){//没有子级,而有员工时,创建新的children |
||||||
|
data[i].children = [] |
||||||
|
let len = data[i].userDept.length |
||||||
|
for(let a=0;a<len;a++){ |
||||||
|
data[i].userDept[a].name=data[i].userDept[a].account |
||||||
|
} |
||||||
|
data[i].children = [...data[i].children,...data[i].userDept] |
||||||
|
data[i].children =[...new Set(data[i].children)] |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
// handleCheck(obj) { |
||||||
|
// if (obj.node.checked) { |
||||||
|
// this.placeholder = `过滤"${obj.data.label}"的子节点`; |
||||||
|
// this.filterNodeData = obj.data; |
||||||
|
// } else { |
||||||
|
// this.placeholder = `过滤所有节点`; |
||||||
|
// this.filterNodeData = null; |
||||||
|
// } |
||||||
|
|
||||||
|
// // filter(val, nodeData),假如要搜索A节点下面的数据,那么nodeData代表treeData中A节点的数据 |
||||||
|
// this.$refs.tree.filter(this.filterText, this.filterNodeData); |
||||||
|
// } |
||||||
|
|
||||||
|
// 获取点击事件,可设置单选多选等,分别是名称数组和id数组 |
||||||
|
getChecked(nameList,idsList){ |
||||||
|
if(idsList.length!==0){ |
||||||
|
this.ID = idsList[0] |
||||||
|
}else{ |
||||||
|
this.ID = '' |
||||||
|
} |
||||||
|
if(nameList&&nameList.length!==0){ |
||||||
|
this.name = nameList[0] |
||||||
|
}else{ |
||||||
|
this.name = '' |
||||||
|
} |
||||||
|
}, |
||||||
|
// 点击确定指派 |
||||||
|
assetAssign(){ |
||||||
|
let obj = { |
||||||
|
businessId:this.getItem.businessId, |
||||||
|
companyId:this.getItem.companyId, |
||||||
|
id:this.getItem.id, |
||||||
|
} |
||||||
|
if(!this.ID){ |
||||||
|
return uni.showToast({title: '请选择后再确定!',icon:'none'}) |
||||||
|
} |
||||||
|
// 选中的角色 |
||||||
|
obj.empId = this.ID |
||||||
|
|
||||||
|
this.$http.post('/api-guarantee/dg-assets-investigation/assignCorners',obj).then(res=>{ |
||||||
|
this.back() |
||||||
|
uni.showToast({ |
||||||
|
title:'操作成功!' |
||||||
|
}) |
||||||
|
}).catch(err=>{ |
||||||
|
uni.showToast({ |
||||||
|
title:err.data.message,icon:'none' |
||||||
|
}) |
||||||
|
}) |
||||||
|
}, |
||||||
|
} |
||||||
|
}; |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.ass-content{ |
||||||
|
padding: 40rpx 40rpx; |
||||||
|
.ass-foot-btn .cu-btn.lg{ |
||||||
|
padding: 0 100rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
.ass-label{ |
||||||
|
color: #00B9FF; |
||||||
|
font-size: 28rpx; |
||||||
|
margin-right: 40rpx; |
||||||
|
} |
||||||
|
.assign-view{ |
||||||
|
border-radius: 20rpx; |
||||||
|
background-color: #fff; |
||||||
|
box-shadow: 0 0 16rpx #ccc; |
||||||
|
height: 800rpx; |
||||||
|
overflow-x: hidden; |
||||||
|
overflow-y: scroll; |
||||||
|
} |
||||||
|
.cu-bar .search-form{ |
||||||
|
border-radius: 20rpx; |
||||||
|
background-color: #f5f5f5; |
||||||
|
height: 72rpx; |
||||||
|
line-height: 72rpx; |
||||||
|
input{ |
||||||
|
height: 72rpx; |
||||||
|
line-height: 72rpx; |
||||||
|
padding: 0 40rpx; |
||||||
|
} |
||||||
|
.cuIcon-search{ |
||||||
|
margin: 0 20rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
.AB-input{ |
||||||
|
view{ |
||||||
|
background-color: #f5f5f5; |
||||||
|
color: #666; |
||||||
|
padding: 20rpx 40rpx; |
||||||
|
border-radius: 20rpx; |
||||||
|
text{ |
||||||
|
color: #B2B2B2; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,237 @@ |
|||||||
|
<template> |
||||||
|
<view class="ass-content"> |
||||||
|
<view class="flex-align-start"> |
||||||
|
<view class="ass-label"> |
||||||
|
<text>信息部专员</text> |
||||||
|
</view> |
||||||
|
<view class="assign-view"> |
||||||
|
<view class="cu-bar search"> |
||||||
|
<view class="search-form round"> |
||||||
|
<input type="text" v-model="search" placeholder="请输入" confirm-type="search"></input> |
||||||
|
<text class="cuIcon-search"></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<mix-tree class="wd60" @checkedRund = "getChecked" :fliter="search" :list="data" @treeItemClick="handleCheck"></mix-tree> |
||||||
|
|
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="flex-align-start mat15"> |
||||||
|
<view class="ass-label"> |
||||||
|
<text>已选调查专员</text> |
||||||
|
</view> |
||||||
|
<view class="AB-input"> |
||||||
|
<view class="flex"> |
||||||
|
<input disabled type="text" :value="name" placeholder="请选择调查专员"/> |
||||||
|
<!-- <text class="cuIcon-roundclosefill" @click="A = ''"></text> --> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="mat40 flex-justify-center ass-foot-btn"> |
||||||
|
<button class="cu-btn block def-btn margin-tb-sm lg round mar15" @click="assetAssign">确定</button> |
||||||
|
<button class="cu-btn block cancel-btn margin-tb-sm lg round mal15" @click="back()">取消</button> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import LyTree from '@/components/ly-tree/ly-tree.vue' |
||||||
|
export default { |
||||||
|
components: { |
||||||
|
LyTree |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
filterNodeData: null, |
||||||
|
search: '',// 搜索文字 |
||||||
|
name:'',// 名称 |
||||||
|
data: [], |
||||||
|
defaultProps: { |
||||||
|
children: 'children', |
||||||
|
label: 'label' |
||||||
|
} |
||||||
|
}; |
||||||
|
}, |
||||||
|
onShow() { |
||||||
|
this.trees()// 每次展示调用树数据 |
||||||
|
}, |
||||||
|
onLoad() { |
||||||
|
// 如果初始化树时设置节点选择需要做适当延时,可以在下一个事件轮询中执行 |
||||||
|
// this.$nextTick(() => { |
||||||
|
// // this.$refs.tree.setCheckedKeys([6]) |
||||||
|
// }) |
||||||
|
}, |
||||||
|
created() { |
||||||
|
if(uni.getStorageSync('decideIndex')){ |
||||||
|
let data = JSON.parse(uni.getStorageSync('decideIndex')) |
||||||
|
this.getItem.businessId = data.businessId |
||||||
|
this.getItem.companyId = data.companyId |
||||||
|
this.getItem.id = data.detailId |
||||||
|
uni.removeStorageSync('decideIndex') |
||||||
|
}else{ |
||||||
|
this.getItem = JSON.parse(uni.getStorageSync('applyMsg')) |
||||||
|
} |
||||||
|
}, |
||||||
|
watch: { |
||||||
|
search(val){ |
||||||
|
} |
||||||
|
// filterText(val) { |
||||||
|
// this.$refs.tree.filter(val); |
||||||
|
// } |
||||||
|
}, |
||||||
|
// 如果不需要不用到这些方法,需要删除相应代码,打印大量日志会造成性能损耗 |
||||||
|
methods: { |
||||||
|
// 只有在"点击"修改的指定节点会触发(也就是说这个方法只会触发一次),obj中包含当前选中 |
||||||
|
handleCheck(obj) { |
||||||
|
}, |
||||||
|
|
||||||
|
// 只要节点的选中状态改变就触发(包括设置默认选中,点击选中/取消选中),会被触发多次 |
||||||
|
handleRadioChange(obj) { |
||||||
|
}, |
||||||
|
|
||||||
|
handleNodeClick(obj) { |
||||||
|
}, |
||||||
|
// 查询过滤 |
||||||
|
filterNode(value, data) { |
||||||
|
if (!value) return true; |
||||||
|
return data.label.indexOf(value) !== -1; |
||||||
|
}, |
||||||
|
// 获取树结构数据 |
||||||
|
trees(){ |
||||||
|
this.$http.get('/api-hrms//hrms/dept/empTrees').then(res=>{ |
||||||
|
this.data = res.data |
||||||
|
this.disposeData(this.data)// 处理数据 |
||||||
|
}) |
||||||
|
}, |
||||||
|
// 处理树结构数据 |
||||||
|
disposeData(data){// children和员工分开,整合到一个children里 |
||||||
|
let len = data.length |
||||||
|
for(let i = 0;i<len;i++){ |
||||||
|
if(data[i].children){ // children有值则---进入整合,实际上可能没有children,也需要整合 |
||||||
|
if(data[i].userDept){//有员工就整合 |
||||||
|
if(data[i].userDept.length!==0){//多个员工处理 |
||||||
|
let len = data[i].userDept.length |
||||||
|
for(let a=0;a<len;a++){ |
||||||
|
data[i].userDept[a].name=data[i].userDept[a].account |
||||||
|
} |
||||||
|
} |
||||||
|
data[i].children = [...data[i].children,...data[i].userDept] |
||||||
|
data[i].children = [...new Set(data[i].children)] |
||||||
|
} |
||||||
|
if(data[i].children.length!==0){//有子级则进入子级,继续循环 |
||||||
|
this.disposeData(data[i].children) |
||||||
|
} |
||||||
|
}else if(!data[i].children&&data[i].userDept&&data[i].userDept.length!==0){//没有子级,而有员工时,创建新的children |
||||||
|
data[i].children = [] |
||||||
|
let len = data[i].userDept.length |
||||||
|
for(let a=0;a<len;a++){ |
||||||
|
data[i].userDept[a].name=data[i].userDept[a].account |
||||||
|
} |
||||||
|
data[i].children = [...data[i].children,...data[i].userDept] |
||||||
|
data[i].children =[...new Set(data[i].children)] |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
// 获取点击事件,可设置单选多选等,分别是名称数组和id数组 |
||||||
|
getChecked(nameList,idsList){ |
||||||
|
if(idsList.length!==0){ |
||||||
|
this.ID = idsList[0] |
||||||
|
}else{ |
||||||
|
this.ID = '' |
||||||
|
} |
||||||
|
if(nameList&&nameList.length!==0){ |
||||||
|
this.name = nameList[0] |
||||||
|
}else{ |
||||||
|
this.name = '' |
||||||
|
} |
||||||
|
}, |
||||||
|
// 点击确定指派 |
||||||
|
assetAssign(){ |
||||||
|
let obj = { |
||||||
|
businessId:this.getItem.businessId, |
||||||
|
companyId:this.getItem.companyId, |
||||||
|
id:this.getItem.id, |
||||||
|
} |
||||||
|
if(!this.ID){ |
||||||
|
return uni.showToast({title: '请选择后再确定!',icon:'none'}) |
||||||
|
} |
||||||
|
// 选中的角色 |
||||||
|
obj.empId = this.ID |
||||||
|
uni.showLoading({ |
||||||
|
title:'提交中' |
||||||
|
}) |
||||||
|
this.$http.post('/api-guarantee/dg-message-investigation/assignCorners',obj).then(res=>{ |
||||||
|
setTimeout(()=>{ |
||||||
|
uni.hideLoading() |
||||||
|
this.back() |
||||||
|
},500) |
||||||
|
}).catch(err=>{ |
||||||
|
uni.showToast({ |
||||||
|
title:err.data.message,icon:'none' |
||||||
|
}) |
||||||
|
}) |
||||||
|
}, |
||||||
|
// handleCheck(obj) { |
||||||
|
// if (obj.node.checked) { |
||||||
|
// this.placeholder = `过滤"${obj.data.label}"的子节点`; |
||||||
|
// this.filterNodeData = obj.data; |
||||||
|
// } else { |
||||||
|
// this.placeholder = `过滤所有节点`; |
||||||
|
// this.filterNodeData = null; |
||||||
|
// } |
||||||
|
|
||||||
|
// // filter(val, nodeData),假如要搜索A节点下面的数据,那么nodeData代表treeData中A节点的数据 |
||||||
|
// this.$refs.tree.filter(this.filterText, this.filterNodeData); |
||||||
|
// } |
||||||
|
} |
||||||
|
}; |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.ass-content{ |
||||||
|
padding: 40rpx 40rpx; |
||||||
|
.ass-foot-btn .cu-btn.lg{ |
||||||
|
padding: 0 100rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
.ass-label{ |
||||||
|
color: #00B9FF; |
||||||
|
font-size: 28rpx; |
||||||
|
margin-right: 40rpx; |
||||||
|
} |
||||||
|
.assign-view{ |
||||||
|
border-radius: 20rpx; |
||||||
|
background-color: #fff; |
||||||
|
box-shadow: 0 0 16rpx #ccc; |
||||||
|
height: 800rpx; |
||||||
|
overflow-x: hidden; |
||||||
|
overflow-y: scroll; |
||||||
|
} |
||||||
|
.cu-bar .search-form{ |
||||||
|
border-radius: 20rpx; |
||||||
|
background-color: #f5f5f5; |
||||||
|
height: 72rpx; |
||||||
|
line-height: 72rpx; |
||||||
|
input{ |
||||||
|
height: 72rpx; |
||||||
|
line-height: 72rpx; |
||||||
|
padding: 0 40rpx; |
||||||
|
} |
||||||
|
.cuIcon-search{ |
||||||
|
margin: 0 20rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
.AB-input{ |
||||||
|
view{ |
||||||
|
background-color: #f5f5f5; |
||||||
|
color: #666; |
||||||
|
padding: 20rpx 40rpx; |
||||||
|
border-radius: 20rpx; |
||||||
|
text{ |
||||||
|
color: #B2B2B2; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,236 @@ |
|||||||
|
<template> |
||||||
|
<view class="ass-content"> |
||||||
|
<view class="flex-align-start"> |
||||||
|
<view class="ass-label"> |
||||||
|
<text>合规调查专员</text> |
||||||
|
</view> |
||||||
|
<view class="assign-view"> |
||||||
|
<view class="cu-bar search"> |
||||||
|
<view class="search-form round"> |
||||||
|
<input type="text" v-model="search" placeholder="请输入" confirm-type="search"></input> |
||||||
|
<text class="cuIcon-search"></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<mix-tree class="wd60" @checkedRund = "getChecked" :fliter="search" :list="data" @treeItemClick="handleCheck"></mix-tree> |
||||||
|
<!-- <ly-tree :tree-data="data" |
||||||
|
:props="defaultProps" |
||||||
|
ref="tree" |
||||||
|
node-key="id" |
||||||
|
show-radio |
||||||
|
checkOnlyLeaf |
||||||
|
@check="handleCheck" |
||||||
|
@radio-change="handleRadioChange" |
||||||
|
@node-click="handleNodeClick" |
||||||
|
:filter-node-method="filterNode" |
||||||
|
child-visible-for-filter-node |
||||||
|
/> --> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="flex-align-start mat15"> |
||||||
|
<view class="ass-label"> |
||||||
|
<text>已选调查专员</text> |
||||||
|
</view> |
||||||
|
<view class="AB-input"> |
||||||
|
<view class="flex"> |
||||||
|
<input disabled type="text" :value="name" placeholder="请选择调查专员"/> |
||||||
|
<!-- <text class="cuIcon-roundclosefill" @click="A = ''"></text> --> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="mat40 flex-justify-center ass-foot-btn"> |
||||||
|
<button class="cu-btn block def-btn margin-tb-sm lg round mar15" @click="assetAssign">确定</button> |
||||||
|
<button class="cu-btn block cancel-btn margin-tb-sm lg round mal15" @click="back()">取消</button> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import LyTree from '@/components/ly-tree/ly-tree.vue' |
||||||
|
export default { |
||||||
|
components: { |
||||||
|
LyTree |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
filterNodeData: null, |
||||||
|
search: '',// 搜索文字 |
||||||
|
name:'',// 名称 |
||||||
|
data: [], |
||||||
|
defaultProps: { |
||||||
|
children: 'children', |
||||||
|
label: 'label' |
||||||
|
} |
||||||
|
}; |
||||||
|
}, |
||||||
|
onShow() { |
||||||
|
this.trees()// 每次展示调用树数据 |
||||||
|
}, |
||||||
|
onLoad() { |
||||||
|
// 如果初始化树时设置节点选择需要做适当延时,可以在下一个事件轮询中执行 |
||||||
|
// this.$nextTick(() => { |
||||||
|
// // this.$refs.tree.setCheckedKeys([6]) |
||||||
|
// }) |
||||||
|
}, |
||||||
|
created() { |
||||||
|
if(uni.getStorageSync('decideIndex')){ |
||||||
|
let data = JSON.parse(uni.getStorageSync('decideIndex')) |
||||||
|
this.getItem.businessId = data.businessId |
||||||
|
this.getItem.companyId = data.companyId |
||||||
|
this.getItem.iid = data.detailId |
||||||
|
uni.removeStorageSync('decideIndex') |
||||||
|
}else{ |
||||||
|
this.getItem = JSON.parse(uni.getStorageSync('applyMsg')) |
||||||
|
} |
||||||
|
}, |
||||||
|
watch: { |
||||||
|
filterText(val) { |
||||||
|
this.$refs.tree.filter(val); |
||||||
|
} |
||||||
|
}, |
||||||
|
// 如果不需要不用到这些方法,需要删除相应代码,打印大量日志会造成性能损耗 |
||||||
|
methods: { |
||||||
|
// 只有在"点击"修改的指定节点会触发(也就是说这个方法只会触发一次),obj中包含当前选中 |
||||||
|
handleCheck(obj) { |
||||||
|
}, |
||||||
|
|
||||||
|
// 只要节点的选中状态改变就触发(包括设置默认选中,点击选中/取消选中),会被触发多次 |
||||||
|
handleRadioChange(obj) { |
||||||
|
}, |
||||||
|
|
||||||
|
handleNodeClick(obj) { |
||||||
|
}, |
||||||
|
// 查询过滤 |
||||||
|
filterNode(value, data) { |
||||||
|
if (!value) return true; |
||||||
|
return data.label.indexOf(value) !== -1; |
||||||
|
}, |
||||||
|
// 获取树结构数据 |
||||||
|
trees(){ |
||||||
|
this.$http.get('/api-hrms//hrms/dept/empTrees').then(res=>{ |
||||||
|
this.data = res.data |
||||||
|
this.disposeData(this.data)// 处理数据 |
||||||
|
}) |
||||||
|
}, |
||||||
|
// 处理树结构数据 |
||||||
|
disposeData(data){// children和员工分开,整合到一个children里 |
||||||
|
let len = data.length |
||||||
|
for(let i = 0;i<len;i++){ |
||||||
|
if(data[i].children){ // children有值则---进入整合,实际上可能没有children,也需要整合 |
||||||
|
if(data[i].userDept){//有员工就整合 |
||||||
|
if(data[i].userDept.length!==0){//多个员工处理 |
||||||
|
let len = data[i].userDept.length |
||||||
|
for(let a=0;a<len;a++){ |
||||||
|
data[i].userDept[a].name=data[i].userDept[a].account |
||||||
|
} |
||||||
|
} |
||||||
|
data[i].children = [...data[i].children,...data[i].userDept] |
||||||
|
data[i].children = [...new Set(data[i].children)] |
||||||
|
} |
||||||
|
if(data[i].children.length!==0){//有子级则进入子级,继续循环 |
||||||
|
this.disposeData(data[i].children) |
||||||
|
} |
||||||
|
}else if(!data[i].children&&data[i].userDept&&data[i].userDept.length!==0){//没有子级,而有员工时,创建新的children |
||||||
|
data[i].children = [] |
||||||
|
let len = data[i].userDept.length |
||||||
|
for(let a=0;a<len;a++){ |
||||||
|
data[i].userDept[a].name=data[i].userDept[a].account |
||||||
|
} |
||||||
|
data[i].children = [...data[i].children,...data[i].userDept] |
||||||
|
data[i].children =[...new Set(data[i].children)] |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
// 获取点击事件,可设置单选多选等,分别是名称数组和id数组 |
||||||
|
getChecked(nameList,idsList){ |
||||||
|
if(idsList.length!==0){ |
||||||
|
this.ID = idsList[0] |
||||||
|
}else{ |
||||||
|
this.ID = '' |
||||||
|
} |
||||||
|
if(nameList&&nameList.length!==0){ |
||||||
|
this.name = nameList[0] |
||||||
|
}else{ |
||||||
|
this.name = '' |
||||||
|
} |
||||||
|
}, |
||||||
|
// 点击确定指派 |
||||||
|
assetAssign(){ |
||||||
|
let obj = { |
||||||
|
iid:this.getItem.iid, |
||||||
|
} |
||||||
|
if(!this.ID){ |
||||||
|
return uni.showToast({title: '请选择后再确定!',icon:'none'}) |
||||||
|
} |
||||||
|
// 选中的角色 |
||||||
|
obj.userId = this.ID |
||||||
|
this.$http.get('/api-guarantee/compliance/investigation/assign',obj).then(res=>{ |
||||||
|
uni.showToast({ |
||||||
|
title:'操作成功!' |
||||||
|
}) |
||||||
|
this.back() |
||||||
|
}) |
||||||
|
}, |
||||||
|
// handleCheck(obj) { |
||||||
|
// if (obj.node.checked) { |
||||||
|
// this.placeholder = `过滤"${obj.data.label}"的子节点`; |
||||||
|
// this.filterNodeData = obj.data; |
||||||
|
// } else { |
||||||
|
// this.placeholder = `过滤所有节点`; |
||||||
|
// this.filterNodeData = null; |
||||||
|
// } |
||||||
|
|
||||||
|
// // filter(val, nodeData),假如要搜索A节点下面的数据,那么nodeData代表treeData中A节点的数据 |
||||||
|
// this.$refs.tree.filter(this.filterText, this.filterNodeData); |
||||||
|
// } |
||||||
|
} |
||||||
|
}; |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.ass-content{ |
||||||
|
padding: 40rpx 40rpx; |
||||||
|
.ass-foot-btn .cu-btn.lg{ |
||||||
|
padding: 0 100rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
.ass-label{ |
||||||
|
color: #00B9FF; |
||||||
|
font-size: 28rpx; |
||||||
|
margin-right: 40rpx; |
||||||
|
} |
||||||
|
.assign-view{ |
||||||
|
border-radius: 20rpx; |
||||||
|
background-color: #fff; |
||||||
|
box-shadow: 0 0 16rpx #ccc; |
||||||
|
height: 800rpx; |
||||||
|
overflow-x: hidden; |
||||||
|
overflow-y: scroll; |
||||||
|
} |
||||||
|
.cu-bar .search-form{ |
||||||
|
border-radius: 20rpx; |
||||||
|
background-color: #f5f5f5; |
||||||
|
height: 72rpx; |
||||||
|
line-height: 72rpx; |
||||||
|
input{ |
||||||
|
height: 72rpx; |
||||||
|
line-height: 72rpx; |
||||||
|
padding: 0 40rpx; |
||||||
|
} |
||||||
|
.cuIcon-search{ |
||||||
|
margin: 0 20rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
.AB-input{ |
||||||
|
view{ |
||||||
|
background-color: #f5f5f5; |
||||||
|
color: #666; |
||||||
|
padding: 20rpx 40rpx; |
||||||
|
border-radius: 20rpx; |
||||||
|
text{ |
||||||
|
color: #B2B2B2; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,342 @@ |
|||||||
|
<template> |
||||||
|
<view> |
||||||
|
<!-- 菜单 (悬浮,预先隐藏)--> |
||||||
|
<me-tabs v-if="isShowSticky" v-model="tabIndex" :fixed="true" :tabs="tabs" @change="tabChange"></me-tabs> |
||||||
|
|
||||||
|
<mescroll-body ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback" :up="upOption" @scroll="scroll" @topclick="topClick"> |
||||||
|
<!-- 菜单 (在mescroll-uni中不能使用fixed,否则iOS滚动时会抖动, 所以需在mescroll-uni之外存在一个一样的菜单) --> |
||||||
|
<view id="tabInList"> |
||||||
|
<me-tabs v-model="tabIndex" :tabs="tabs" @change="tabChange"></me-tabs> |
||||||
|
</view> |
||||||
|
<!-- 搜索 --> |
||||||
|
<view class="cu-bar bg-white search" :class="scrolltop ? 'fixed' : ''" :style="[{top:CustomBar + 'px'}]"> |
||||||
|
<view class="search-form round"> |
||||||
|
<input @input="searchList" type="text" v-model="listParams.CustomerNumberOrName" placeholder="输入搜索的关键词" confirm-type="search"></input> |
||||||
|
<text class="cuIcon-search"></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="cu-list menu-avatar"> |
||||||
|
<view class="cu-item" :class="modalName=='move-box-'+ index?'move-cur':''" v-for="(item,index) in listData" :key="index" |
||||||
|
@touchstart="ListTouchStart" @touchmove="ListTouchMove" @touchend="ListTouchEnd" :data-target="'move-box-' + index"> |
||||||
|
<view class="good-list"> |
||||||
|
<view class="charge" :id="'index-' + item.name" :data-index="index"> |
||||||
|
<navigator @click="handleGe(item,'watch')" hover-class='none' :url="'/guaranteePages/pages/investigationSee/investigationSee'" |
||||||
|
class="nav-li" navigateTo :style="[{animation: 'show ' + ((index+1)*0.2+1) + 's 1'}]"> |
||||||
|
<view class="charge-title flex-between"> |
||||||
|
<text>{{item.name}}</text> |
||||||
|
</view> |
||||||
|
<view class="charge-text"> |
||||||
|
<view> |
||||||
|
<text>业务编号:</text> |
||||||
|
<text class="mgl30">{{item.businessCode}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>客户名称:</text> |
||||||
|
<text class="mgl30">{{item.name}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>联系电话:</text> |
||||||
|
<text class="mgl30">{{item.phone}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>业务类别:</text> |
||||||
|
<text class="mgl30">{{item.businessType}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>申请额度(万元):</text> |
||||||
|
<text class="mgl30">{{item.applyAmount}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>申请期限:</text> |
||||||
|
<text class="mgl30">{{item.applyTime}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>申请日期:</text> |
||||||
|
<text class="mgl30">{{item.createTime}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>审批状态:</text> |
||||||
|
<text class="mgl30">{{item.statusVal}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>业务状态:</text> |
||||||
|
<text class="mgl30">{{item.businessStatusVal}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>操作状态:</text> |
||||||
|
<text class="mgl30">{{item.operationType}}</text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</navigator> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="move foot-btn sideBtn"> |
||||||
|
<view class=""> |
||||||
|
<button v-show="item.status===0&&item.operatingStatus == 1" v-if="[58].includes(item.roleId)&&item.businessStatus !== 3" class="mini-btn round def-btn mar-lr mgr6" type="primary" size="mini" @click="handleGe(item,'none',true)">指派</button> |
||||||
|
<button v-show="item.status===1&&item.operatingStatus == 1||item.status===4&&item.operatingStatus == 1" v-if="[58,61].includes(item.roleId)&&item.businessStatus !== 3" class="mini-btn round def-btn mar-lr" type="primary" size="mini" @click="handleGe(item,'audit')">审核</button> |
||||||
|
</view> |
||||||
|
<view class="margin-top"> |
||||||
|
<button v-show="item.status===1&&item.operatingStatus == 1||item.status===4&&item.operatingStatus == 1" v-if="[59,60].includes(item.roleId)&&item.businessStatus !== 3" class="mini-btn round def-btn mar-lr" type="primary" size="mini" @click="handleGe(item,'servey')">调查</button> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<!-- 数据列表 --> |
||||||
|
<!-- <index-list :list="chargeList"></index-list> --> |
||||||
|
<!-- <touch-list :list="chargeList" :listTouchDirection="listTouchDirection" :listTouchStart="listTouchStart" :modalName="modalName" |
||||||
|
@TouchStart="ListTouchStart" @TouchMove="ListTouchMove" @TouchEnd="ListTouchEnd" |
||||||
|
></touch-list> --> |
||||||
|
</mescroll-body> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import MescrollMixin from "@/components/mescroll-uni/mescroll-mixins.js"; |
||||||
|
export default { |
||||||
|
mixins: [MescrollMixin], // 使用mixin |
||||||
|
data() { |
||||||
|
return { |
||||||
|
upOption: { |
||||||
|
// 如果用mescroll-uni 则需要onScroll: true, 且需要 @scroll="scroll"; 而mescroll-body最简单只需在onPageScroll处理即可 |
||||||
|
// onScroll: true // 是否监听滚动事件, 默认false (配置为true时,可@scroll="scroll"获取到滚动条位置和方向) |
||||||
|
}, |
||||||
|
tabs:[ |
||||||
|
{name:'全部', num:1, y:0, curPageLen:0, hasNext:true}, |
||||||
|
{name:'待指派', num:1, y:0, curPageLen:0, hasNext:true}, |
||||||
|
{name:'审核中', num:1, y:0, curPageLen:0, hasNext:true}, |
||||||
|
{name:'已审核', num:1, y:0, curPageLen:0, hasNext:true}, |
||||||
|
{name:'已拒绝', num:1, y:0, curPageLen:0, hasNext:true}, |
||||||
|
{name:'已驳回', num:1, y:0, curPageLen:0, hasNext:true}, |
||||||
|
{name:'已撤销', num:1, y:0, curPageLen:0, hasNext:true}, |
||||||
|
], |
||||||
|
tabIndex: 0, // 当前菜单下标 |
||||||
|
preIndex: 0, // 前一个菜单下标 |
||||||
|
navTop: null, // nav距离到顶部的距离 (如计算不准确,可直接写死某个值) |
||||||
|
isShowSticky: false, // 是否悬浮 |
||||||
|
TabCur: 0, |
||||||
|
scrollLeft: 0, |
||||||
|
CustomBar: this.CustomBar, |
||||||
|
listCurID: '', |
||||||
|
chargeList: [], |
||||||
|
total: 1, |
||||||
|
scrolltop: false, |
||||||
|
listTouchStart: 0, |
||||||
|
listTouchDirection: null, |
||||||
|
modalName: null, |
||||||
|
listData:{},// 列表数据 |
||||||
|
listParams:{ |
||||||
|
CustomerNumberOrName:'',// 搜索框 |
||||||
|
status:'',// 状态值,0-6 |
||||||
|
size:10, |
||||||
|
page:1, |
||||||
|
}, |
||||||
|
status:'' |
||||||
|
}; |
||||||
|
}, |
||||||
|
created() { |
||||||
|
}, |
||||||
|
onShow() { |
||||||
|
this.upCallback({ |
||||||
|
num:1,size:10,search:'' |
||||||
|
})// 重新请求 |
||||||
|
}, |
||||||
|
mounted() { |
||||||
|
// this.chargeList.map(e =>{ |
||||||
|
// e.status = this.core.auditStatus(e.status) |
||||||
|
// }) |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
changeTab(index){ |
||||||
|
this.TabCur = index; |
||||||
|
}, |
||||||
|
/*下拉刷新的回调 */ |
||||||
|
downCallback() { |
||||||
|
// 这里加载你想下拉刷新的数据, 比如刷新轮播数据 |
||||||
|
// loadSwiper(); |
||||||
|
// 下拉刷新的回调,默认重置上拉加载列表为第一页 (自动执行 page.num=1, 再触发upCallback方法 ) |
||||||
|
this.mescroll.resetUpScroll() |
||||||
|
}, |
||||||
|
/*上拉加载的回调: 其中page.num:当前页 从1开始, page.size:每页数据条数,默认10 */ |
||||||
|
upCallback(page) { |
||||||
|
this.$http.get('/api-guarantee/dg-guarantee-assign-user/guaranteeList',{ |
||||||
|
page: page.num?page.num:1, |
||||||
|
size: page.size?page.size:10, |
||||||
|
status:this.status?this.status:'', |
||||||
|
CustomerNumberOrName:page.search?page.search:'' |
||||||
|
}).then(res => { |
||||||
|
if(page.num == 1) this.listData = []; //如果是第一页需手动制空列表 |
||||||
|
res.data.list.map(e =>{ |
||||||
|
e.statusVal = this.core.statusVal(e.status) |
||||||
|
e.businessStatusVal = this.core.businessType(e.businessStatus) |
||||||
|
e.operationType = this.core.operationType(e.operatingStatus) |
||||||
|
}) |
||||||
|
this.listData = this.listData.concat(res.data.list); //追加新数据 |
||||||
|
this.mescroll.endSuccess(res.data.list); // 隐藏加载状态栏 |
||||||
|
}).catch(()=>{ |
||||||
|
//联网失败, 结束加载 |
||||||
|
this.mescroll.endErr(); |
||||||
|
}) |
||||||
|
}, |
||||||
|
// 搜索框--结合防抖 |
||||||
|
searchList(e){ |
||||||
|
let that = this |
||||||
|
if (this.timer) { |
||||||
|
clearTimeout(this.timer); |
||||||
|
this.timer = null; |
||||||
|
} |
||||||
|
this.timer = setTimeout(function () { |
||||||
|
that.upCallback({num:1,size:10,search:e.detail.value}) |
||||||
|
},800) |
||||||
|
}, |
||||||
|
// 设置nav到顶部的距离 (滚动条为0, 菜单顶部的数据加载完毕获取到的navTop数值是最精确的) |
||||||
|
setNavTop(){ |
||||||
|
let view = uni.createSelectorQuery().select('#tabInList'); |
||||||
|
view.boundingClientRect(data => { |
||||||
|
if(data) this.navTop = data.top // 到屏幕顶部的距离 |
||||||
|
}).exec(); |
||||||
|
}, |
||||||
|
// mescroll-uni的滚动事件 (需在up配置onScroll:true才生效) |
||||||
|
// 而mescroll-body最简单只需在onPageScroll处理即可 |
||||||
|
scroll(){ |
||||||
|
// 菜单悬浮的原理: 监听滚动条的位置大于某个值时,控制顶部菜单的显示和隐藏 |
||||||
|
if (this.mescroll.getScrollTop() >= this.navTop) { |
||||||
|
this.isShowSticky = true // 显示悬浮菜单 |
||||||
|
} else { |
||||||
|
this.isShowSticky = false // 隐藏悬浮菜单 |
||||||
|
} |
||||||
|
}, // "path": "pages/index/index",// 待删除,调试的时候调换 |
||||||
|
// 点击回到顶部按钮时,先隐藏悬浮菜单,避免闪动 |
||||||
|
topClick(){ |
||||||
|
this.isShowSticky = false |
||||||
|
}, |
||||||
|
// 切换菜单 |
||||||
|
tabChange (index) { |
||||||
|
if(index==0){ |
||||||
|
this.status = '' |
||||||
|
}else{ |
||||||
|
this.status = `${index-1}` |
||||||
|
} |
||||||
|
this.upCallback({ |
||||||
|
num:1,size:10,serch:'' |
||||||
|
}) |
||||||
|
// 记录前一个菜单的数据 |
||||||
|
let preTab = this.tabs[this.preIndex] |
||||||
|
preTab.y = this.mescroll.getScrollTop(); // 滚动条位置 |
||||||
|
this.preIndex = index; |
||||||
|
// 当前菜单的数据 |
||||||
|
let curTab = this.tabs[index] |
||||||
|
if (!curTab.goods) { |
||||||
|
// 没有初始化,则初始化 |
||||||
|
this.isChangeTab = true; |
||||||
|
this.mescroll.resetUpScroll() |
||||||
|
} else{ |
||||||
|
// 初始化过,则恢复之前的列表数据 |
||||||
|
this.mescroll.setPageNum(curTab.num + 1); // 恢复当前页码 |
||||||
|
this.mescroll.endSuccess(curTab.curPageLen, curTab.hasNext); // 恢复是否有下一页或显示空布局 |
||||||
|
this.$nextTick(()=>{ |
||||||
|
this.mescroll.scrollTo(curTab.y, 0) // 恢复滚动条的位置 |
||||||
|
}) |
||||||
|
} |
||||||
|
}, |
||||||
|
// ListTouch触摸开始 |
||||||
|
ListTouchStart(e) { |
||||||
|
this.listTouchStart = e.touches[0].pageX |
||||||
|
}, |
||||||
|
// ListTouch计算方向 |
||||||
|
ListTouchMove(e) { |
||||||
|
this.listTouchDirection = e.touches[0].pageX - this.listTouchStart > 0 ? 'right' : 'left' |
||||||
|
}, |
||||||
|
// ListTouch计算滚动 |
||||||
|
ListTouchEnd(e) { |
||||||
|
if (this.listTouchDirection == 'left') { |
||||||
|
this.modalName = e.currentTarget.dataset.target |
||||||
|
} else { |
||||||
|
this.modalName = null |
||||||
|
} |
||||||
|
this.listTouchDirection = null |
||||||
|
}, |
||||||
|
// 列表数据 |
||||||
|
getList(val){ |
||||||
|
if(val==='search'){ |
||||||
|
this.listParams.page = 1 |
||||||
|
this.listParams.size = 10 |
||||||
|
} |
||||||
|
this.$http.get('/api-guarantee/dg-guarantee-assign-user/guaranteeList',this.listParams).then(res=>{ |
||||||
|
if(res.data&&res.data.list&&res.data.list.length!==0){ |
||||||
|
res.data.list.map(e =>{ |
||||||
|
e.statusVal = this.core.statusVal(e.status) |
||||||
|
e.businessStatusVal = this.core.businessType(e.businessStatus) |
||||||
|
e.operationType = this.core.operationType(e.operatingStatus) |
||||||
|
}) |
||||||
|
this.listData = res.data.list |
||||||
|
}else{ |
||||||
|
this.listData = [] |
||||||
|
} |
||||||
|
|
||||||
|
}) |
||||||
|
}, |
||||||
|
// 传递当前操作状态和值 |
||||||
|
handleGe(item,str,assign){ |
||||||
|
if(assign){// 如果是指派-进入指派页面 |
||||||
|
this.goto('/guaranteePages/pages/assignAB/assignAB')// 跳转指派 |
||||||
|
uni.removeStorageSync('handleGe') |
||||||
|
}else{ |
||||||
|
uni.setStorageSync('handleGe',str)// 操作状态记录 |
||||||
|
} |
||||||
|
if(!assign&&str!=='watch'){ |
||||||
|
this.goto('/guaranteePages/pages/investigationSee/investigationSee') |
||||||
|
} |
||||||
|
uni.setStorageSync('applyMsg',JSON.stringify(item))// 所有的值存起来 |
||||||
|
uni.setStorageSync('businessId',item.businessId)// 储存有业务id |
||||||
|
}, |
||||||
|
}, |
||||||
|
// 使用mescroll-body最简单只需在onPageScroll处理即可 |
||||||
|
// onPageScroll(e){ |
||||||
|
// if (e.scrollTop >= this.navTop) { |
||||||
|
// this.isShowSticky = true // 显示悬浮菜单 |
||||||
|
// } else { |
||||||
|
// this.isShowSticky = false // 隐藏悬浮菜单 |
||||||
|
// } |
||||||
|
// } |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.cu-bar .search-form{ |
||||||
|
border: 2rpx solid #00B9FF; |
||||||
|
border-radius: 20rpx; |
||||||
|
background-color: #fff; |
||||||
|
margin: 0 50rpx; |
||||||
|
height: 72rpx; |
||||||
|
line-height: 72rpx; |
||||||
|
margin-top: 20rpx; |
||||||
|
input{ |
||||||
|
height: 72rpx; |
||||||
|
line-height: 72rpx; |
||||||
|
padding: 0 40rpx; |
||||||
|
} |
||||||
|
.cuIcon-search{ |
||||||
|
margin: 0 20rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
.add-customer{ |
||||||
|
padding: 0 50rpx; |
||||||
|
button{ |
||||||
|
background-color: #00B9FF; |
||||||
|
border-radius: 30rpx; |
||||||
|
.mar-icon{ |
||||||
|
margin-right: 10rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
.sideBtn{ |
||||||
|
display: flex; |
||||||
|
flex-wrap: wrap; |
||||||
|
justify-content: space-around; |
||||||
|
} |
||||||
|
/deep/ .cu-list>.cu-item .move view{ |
||||||
|
flex: none; |
||||||
|
margin-top: 10rpx; |
||||||
|
} |
||||||
|
|
||||||
|
</style> |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,228 @@ |
|||||||
|
<template> |
||||||
|
<view> |
||||||
|
|
||||||
|
<mescroll-body :sticky="true" ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback"> |
||||||
|
<view class="sticky-tabs"> |
||||||
|
<me-tabs v-model="tabIndex" :tabs="tabs" @change="tabChange"></me-tabs> |
||||||
|
</view> |
||||||
|
|
||||||
|
<!-- 搜索 --> |
||||||
|
<view class="cu-bar bg-white search"> |
||||||
|
<view class="search-form round"> |
||||||
|
<input v-model="searchVal" @confirm="keywordSearch" type="text" placeholder="输入搜索的关键词" confirm-type="search"></input> |
||||||
|
<text class="cuIcon-search"></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="add-customer mat15"> |
||||||
|
<text>共{{total}}条</text> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="cu-list menu-avatar"> |
||||||
|
<view class="cu-item" :class="modalName=='move-box-'+ index?'move-cur':''" v-for="(item,index) in processData" :key="index" |
||||||
|
@touchstart="ListTouchStart" @touchmove="ListTouchMove" @touchend="ListTouchEnd" :data-target="'move-box-' + index"> |
||||||
|
<view class="good-list" @tap="letterData(item,2)"> |
||||||
|
<view class="charge" :id="'index-' + item.name" :data-index="item.name"> |
||||||
|
<view class="charge-title flex-between"> |
||||||
|
<text>{{item.name}}的贷款申请</text> |
||||||
|
</view> |
||||||
|
<view class="charge-text"> |
||||||
|
<view class="flex-between"> |
||||||
|
<text>业务编号:</text> |
||||||
|
<text class="mgl30">{{item.businessCode}}</text> |
||||||
|
</view> |
||||||
|
<view class="flex-between"> |
||||||
|
<text>联系电话:</text> |
||||||
|
<text class="mgl30">{{item.phone}}</text> |
||||||
|
</view> |
||||||
|
<view class="flex-between"> |
||||||
|
<text>业务类别:</text> |
||||||
|
<text class="mgl30">{{item.businessType}}</text> |
||||||
|
</view> |
||||||
|
<view class="flex-between"> |
||||||
|
<text>担保额度(万元):</text> |
||||||
|
<text class="mgl30">{{item.loanMoney}}</text> |
||||||
|
</view> |
||||||
|
<view class="flex-between"> |
||||||
|
<text>担保期限:</text> |
||||||
|
<text class="mgl30">{{item.loanTern}}</text> |
||||||
|
</view> |
||||||
|
<view class="flex-between"> |
||||||
|
<text>贷审会日期:</text> |
||||||
|
<text class="mgl30">{{item.passingTime}}</text> |
||||||
|
</view> |
||||||
|
<view class="flex-between"> |
||||||
|
<text>审批状态:</text> |
||||||
|
<text class="mgl30">{{item.statusText}}</text> |
||||||
|
</view> |
||||||
|
<view class="flex-between"> |
||||||
|
<text>业务状态:</text> |
||||||
|
<text class="mgl30">{{item.businessText}}</text> |
||||||
|
</view> |
||||||
|
<view class="flex-between"> |
||||||
|
<text>操作状态:</text> |
||||||
|
<text class="mgl30">{{item.operatingText}}</text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="move foot-btn"> |
||||||
|
<button v-if="item.isFgJl == 1 && item.operatingText !== '已处理'&&item.businessStatus !== 3" @tap="letterData(item,1)" |
||||||
|
class="mini-btn round def-btn mar-lr" type="primary" size="mini">担保函</button> |
||||||
|
<button v-if="item.isFgJl != 1 && item.operatingText !== '已处理'&&item.businessStatus !== 3" @tap="letterData(item,3)" |
||||||
|
class="mini-btn round suc-btn" type="primary" size="mini">确认</button> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</mescroll-body> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import MescrollMixin from "@/components/mescroll-uni/mescroll-mixins.js"; |
||||||
|
export default { |
||||||
|
mixins: [MescrollMixin], // 使用mixin |
||||||
|
data() { |
||||||
|
return { |
||||||
|
tabs:[ |
||||||
|
{name:'全部', value: ''}, |
||||||
|
{name:'审核中', value: 1}, |
||||||
|
{name:'已审核', value: 2}, |
||||||
|
{name:'已驳回', value: 4} |
||||||
|
], |
||||||
|
tabIndex: 0, // 当前菜单下标 |
||||||
|
processData: [], |
||||||
|
total: 0, |
||||||
|
page: { |
||||||
|
num: 1, |
||||||
|
size: 10, |
||||||
|
}, |
||||||
|
status: '',//状态的值 |
||||||
|
searchVal: '', //搜索框的值 |
||||||
|
|
||||||
|
listTouchStart: 0, |
||||||
|
listTouchDirection: null, |
||||||
|
modalName: null |
||||||
|
}; |
||||||
|
}, |
||||||
|
onShow() { |
||||||
|
this.upCallback({ |
||||||
|
num:1,size:10 |
||||||
|
}) |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
//搜索框搜索 |
||||||
|
keywordSearch(e){ |
||||||
|
this.searchVal = e.detail.value |
||||||
|
this.upCallback(this.page) |
||||||
|
}, |
||||||
|
/*下拉刷新的回调 */ |
||||||
|
downCallback() { |
||||||
|
// 下拉刷新的回调,默认重置上拉加载列表为第一页 (自动执行 page.num=1, 再触发upCallback方法 ) |
||||||
|
this.mescroll.resetUpScroll() |
||||||
|
}, |
||||||
|
/*上拉加载的回调: 其中page.num:当前页 从1开始, page.size:每页数据条数,默认10 */ |
||||||
|
upCallback(page) { |
||||||
|
this.$http.post('/api-guarantee/dg-guarantee-letter-assign-user/guaranteeLetterList',{ |
||||||
|
page: page.num, |
||||||
|
size: page.size, |
||||||
|
customerNumberOrName: this.searchVal, |
||||||
|
status: this.status |
||||||
|
}).then(res => { |
||||||
|
if(page.num == 1) this.processData = []; //如果是第一页需手动制空列表 |
||||||
|
res.data.list.map(e =>{ |
||||||
|
e.statusText = this.core.auditStatus(e.status).text |
||||||
|
e.businessText = this.core.businessType(e.businessStatus) |
||||||
|
e.operatingText = this.core.operationType(e.operatingStatus) |
||||||
|
}) |
||||||
|
this.total = res.data.totalCount |
||||||
|
|
||||||
|
this.processData = this.processData.concat(res.data.list); //追加新数据 |
||||||
|
this.mescroll.endSuccess(res.data.list); // 隐藏加载状态栏 |
||||||
|
}).catch(()=>{ |
||||||
|
//联网失败, 结束加载 |
||||||
|
this.mescroll.endErr(); |
||||||
|
}) |
||||||
|
}, |
||||||
|
// 切换菜单 |
||||||
|
tabChange (index) { |
||||||
|
this.status = this.tabs[index].value;//当前下标的值 |
||||||
|
this.searchVal = ''; |
||||||
|
this.processData = []; // 置空列表,显示加载进度条 |
||||||
|
this.mescroll.resetUpScroll() |
||||||
|
}, |
||||||
|
letterData(item,Type){ |
||||||
|
let operationType = Type //列表操作状态 |
||||||
|
//担保函数据 |
||||||
|
let letterValue = { |
||||||
|
businessId: item.businessId,//业务id |
||||||
|
bank: item.bank,//银行 |
||||||
|
name: item.name,//客户名称 |
||||||
|
passingTime: item.passingTime,//贷审会日期 |
||||||
|
loanMoney: this.core.fMoney(item.loanMoney),//担保金额 |
||||||
|
guaranteeMoney : this.core.fMoney3(item.loanMoney),//担保金额转格式 |
||||||
|
file: item.file,//会议纪要 |
||||||
|
loanTern: item.loanTern,//担保期限 |
||||||
|
auditOpinion: item.auditOpinion,//审核意见 |
||||||
|
roleId: item.roleId //角色id |
||||||
|
} |
||||||
|
let fileList = item.file.split(",") |
||||||
|
letterValue = JSON.stringify(letterValue) |
||||||
|
uni.navigateTo({ |
||||||
|
url: '../letterSee/letterSee?fileList='+fileList+'&operationType='+operationType+'&letterValue='+letterValue |
||||||
|
}); |
||||||
|
uni.setStorageSync('businessId',item.businessId)// 业务id--给底部流程图用 |
||||||
|
}, |
||||||
|
|
||||||
|
// ListTouch触摸开始 |
||||||
|
ListTouchStart(e) { |
||||||
|
this.listTouchStart = e.touches[0].pageX |
||||||
|
}, |
||||||
|
|
||||||
|
// ListTouch计算方向 |
||||||
|
ListTouchMove(e) { |
||||||
|
this.listTouchDirection = e.touches[0].pageX - this.listTouchStart > 0 ? 'right' : 'left' |
||||||
|
}, |
||||||
|
|
||||||
|
// ListTouch计算滚动 |
||||||
|
ListTouchEnd(e) { |
||||||
|
if (this.listTouchDirection == 'left') { |
||||||
|
this.modalName = e.currentTarget.dataset.target |
||||||
|
} else { |
||||||
|
this.modalName = null |
||||||
|
} |
||||||
|
this.listTouchDirection = null |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.cu-bar .search-form{ |
||||||
|
border: 2rpx solid #00B9FF; |
||||||
|
border-radius: 20rpx; |
||||||
|
background-color: #fff; |
||||||
|
margin: 0 50rpx; |
||||||
|
height: 72rpx; |
||||||
|
line-height: 72rpx; |
||||||
|
margin-top: 20rpx; |
||||||
|
input{ |
||||||
|
height: 72rpx; |
||||||
|
line-height: 72rpx; |
||||||
|
padding: 0 40rpx; |
||||||
|
} |
||||||
|
.cuIcon-search{ |
||||||
|
margin: 0 20rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
.add-customer{ |
||||||
|
padding: 0 50rpx; |
||||||
|
button{ |
||||||
|
background-color: #00B9FF; |
||||||
|
border-radius: 30rpx; |
||||||
|
.mar-icon{ |
||||||
|
margin-right: 10rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,199 @@ |
|||||||
|
<template> |
||||||
|
<view class="letter-content"> |
||||||
|
<view class="letter-header"> |
||||||
|
<text>担保承诺函</text> |
||||||
|
</view> |
||||||
|
<view class="mat15 bank"> |
||||||
|
<text>{{letterValue.bank}}</text> |
||||||
|
</view> |
||||||
|
<view class="letter-main mat15"> |
||||||
|
<text v-show="operationType !== 2"> |
||||||
|
根据<text class="letter-key">{{letterValue.name}}</text>的贷款申请,经我公司项目贷审会研究,同意为其在贵行申请的贷款提供担保,特向贵行提供承诺。本承诺系我公司同意与贵行签订保证合同的意向,一切权利义务以保证合同为准! |
||||||
|
</text> |
||||||
|
<!-- 查看担保函的文字 --> |
||||||
|
<text v-show="operationType === 2"> |
||||||
|
根据<text class="letter-key">{{letterValue.name}}</text>的贷款申请,经我公司<text class="letter-key">{{letterValue.passingTime}}</text>项目贷审会研究,同意为其在贵行申请的贷款提供<text class="letter-key">{{letterValue.loanMoney}}</text>万元担保,期限<text class="letter-key">{{letterValue.loanTern}}</text>,特向贵行提供承诺。本承诺系我公司同意与贵行签订保证合同的意向,一切权利义务以保证合同为准! |
||||||
|
</text> |
||||||
|
<view v-show="operationType !== 2"> |
||||||
|
<view class="mat15"> |
||||||
|
<text>客户名称:{{letterValue.name}}</text> |
||||||
|
</view> |
||||||
|
<view class="mat15"> |
||||||
|
<text>贷审会日期:{{letterValue.passingTime}}</text> |
||||||
|
</view> |
||||||
|
<view class="mat15"> |
||||||
|
<text>担保金额(万元):{{letterValue.loanMoney}} (大写:{{letterValue.guaranteeMoney}})</text> |
||||||
|
</view> |
||||||
|
<view class="mat15"> |
||||||
|
<text>担保期限:{{letterValue.loanTern}}</text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="mat40 letter-footer"> |
||||||
|
<view class="mat15"> |
||||||
|
<text>大庆市工商业融资担保有限公司</text> |
||||||
|
</view> |
||||||
|
<view class="mat15"> |
||||||
|
<text>法定代表人:</text> |
||||||
|
</view> |
||||||
|
<view class="mat15"> |
||||||
|
<text>或授权代理人:</text> |
||||||
|
</view> |
||||||
|
<view class="mat15"> |
||||||
|
<text>  年   月   日</text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<!-- PC端仅能在确认和担保函查看此附件 --> |
||||||
|
<view class="bottom-border" v-show="operationType != 2"> |
||||||
|
<view class="left-border"><text class="mgl10">附件</text></view> |
||||||
|
</view> |
||||||
|
<upload v-show="operationType !== 2" @upload="uploadFile" :files="filesArray" :handle="false"></upload> |
||||||
|
|
||||||
|
<uni-forms v-show="operationType !== 2" :value="letterValue" ref="letterValue" validate-trigger="bind" err-show-type="toast"> |
||||||
|
<uni-forms-item name="name" :label="operationType === 1 ? '审核' : '担保函确认'"> |
||||||
|
<uni-easyinput :disabled="operationType == 2 " type="textarea" v-model="letterValue.auditOpinion" placeholder="请输入审核意见"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
</uni-forms> |
||||||
|
<view class="foot-btn btn-rig pad-bt margin-top" v-show="operationType == 1 || operationType == 3"> |
||||||
|
<button v-show="operationType == 1" class="mini-btn round def-btn mar-lr" type="primary" size="mini" @tap="sumbitOpinion('提交',2)">提交</button> |
||||||
|
<button v-show="operationType == 3" class="mini-btn round suc-btn mar-lr" type="primary" size="mini" @tap="sumbitOpinion('确认',2)">确认</button> |
||||||
|
<button class="mini-btn round refuse-btn mar-lr" type="primary" size="mini" @tap="sumbitOpinion('驳回',4)">驳回</button> |
||||||
|
<button plain class="mini-btn round plain-btn" type="primary" size="mini" @tap="back()">返回</button> |
||||||
|
</view> |
||||||
|
|
||||||
|
<timeline></timeline> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import upload from '../../../components/pretty-uploadFile/pretty-uploadFile.vue' |
||||||
|
export default { |
||||||
|
components: { |
||||||
|
upload |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
fileList: [], |
||||||
|
operationType: '', |
||||||
|
letterValue: {}, |
||||||
|
}; |
||||||
|
}, |
||||||
|
destroyed() {// 销毁组件操作 |
||||||
|
uni.removeStorageSync('copyData') |
||||||
|
}, |
||||||
|
onLoad(option) { |
||||||
|
// 判断来自待处理否 |
||||||
|
if(uni.getStorageSync('decideIndex')){ |
||||||
|
let data = JSON.parse(uni.getStorageSync('decideIndex')) |
||||||
|
this.letterDetail(data) |
||||||
|
if(data.btn==='担保函'){ |
||||||
|
this.operationType = 1 |
||||||
|
}else{// 确认 |
||||||
|
this.operationType = 3 |
||||||
|
} |
||||||
|
uni.removeStorageSync('decideIndex') |
||||||
|
}else if(uni.getStorageSync('copyData')){// 抄送判断 |
||||||
|
let data =JSON.parse(uni.getStorageSync('copyData')) |
||||||
|
this.operationType = 2 |
||||||
|
// 非抄送的逻辑,调接口进行赋值详情 |
||||||
|
if(uni.getStorageSync('notCopy')){ |
||||||
|
this.letterDetail(data) |
||||||
|
}else{ |
||||||
|
this.letterValue = { |
||||||
|
name:data.clientName, |
||||||
|
passingTime:data.passingTime, |
||||||
|
loanMoney:data.loanMoney, |
||||||
|
loanTern:data.loanTern |
||||||
|
} |
||||||
|
} |
||||||
|
}else{ |
||||||
|
if(option){ |
||||||
|
this.fileList = option.fileList |
||||||
|
this.operationType = option.operationType |
||||||
|
// operationType 状态:1 确认担保函 , 2 查看 ,3 审核 |
||||||
|
this.letterValue = JSON.parse(option.letterValue) |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
// 详情--个人效率专用 |
||||||
|
letterDetail(data){ |
||||||
|
this.$http.get('/api-guarantee/dg-guarantee-letter-assign-user/guaranteeLetterDetail',{id:data.detailId}).then(res=>{ |
||||||
|
let that = this,data = res.data.data |
||||||
|
this.letterValue = { |
||||||
|
businessId: data.businessId,//业务id |
||||||
|
bank: data.bank,//银行 |
||||||
|
name: data.clientName,//客户名称 |
||||||
|
passingTime: data.passingTime,//贷审会日期 |
||||||
|
loanMoney: data.loanMoney,//担保金额 |
||||||
|
guaranteeMoney: that.core.fMoney3(data.loanMoney),//担保金额转格式 |
||||||
|
file: '',//会议纪要 |
||||||
|
loanTern: data.loanTern,//担保期限 |
||||||
|
auditOpinion: '',//审核意见 |
||||||
|
roleId: ''//角色id |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
|
||||||
|
// 提交审核意见 |
||||||
|
sumbitOpinion(text,type){ |
||||||
|
let _this = this |
||||||
|
uni.showModal({ |
||||||
|
title: '提示', |
||||||
|
content: `确定要${text}该审核意见吗?`, |
||||||
|
success: function (res) { |
||||||
|
if (res.confirm) { |
||||||
|
if(type == 4 && _this.letterValue.auditOpinion == ''){ |
||||||
|
return uni.showToast({title: '请先填写审核意见!',icon:'none'}) |
||||||
|
} |
||||||
|
_this.$http.post('/api-guarantee/dg-guarantee-letter-assign-user/updateStatus',{ |
||||||
|
auditOpinion: _this.letterValue.auditOpinion?_this.letterValue.auditOpinion:"", |
||||||
|
businessId: _this.letterValue.businessId, |
||||||
|
status: type |
||||||
|
}).then(res => { |
||||||
|
uni.showToast({title: `${text}成功`,icon:'none'}) |
||||||
|
_this.back() |
||||||
|
}).catch(()=>{ |
||||||
|
|
||||||
|
}) |
||||||
|
} else if (res.cancel) {} |
||||||
|
} |
||||||
|
}); |
||||||
|
}, |
||||||
|
/* 上传文件 */ |
||||||
|
uploadFile(e) { |
||||||
|
let fileList = e.map(r => r.url) |
||||||
|
this.complianceSurveyFrom.fileUrls = fileList |
||||||
|
}, |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.letter-content{ |
||||||
|
padding: 40rpx 40rpx; |
||||||
|
.letter-header{ |
||||||
|
text-align: center; |
||||||
|
font-size: 32rpx; |
||||||
|
font-weight: 500; |
||||||
|
padding-bottom: 40rpx; |
||||||
|
border-bottom: 2rpx solid #e5e5e5; |
||||||
|
} |
||||||
|
.bank{ |
||||||
|
font-weight: 700; |
||||||
|
} |
||||||
|
.letter-main{ |
||||||
|
letter-spacing: 4rpx; |
||||||
|
text-indent: 60rpx; |
||||||
|
} |
||||||
|
.letter-key{ |
||||||
|
font-weight: bold; |
||||||
|
margin: 0 10px; |
||||||
|
} |
||||||
|
.letter-footer{ |
||||||
|
text-align: right; |
||||||
|
margin-right: 20rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,223 @@ |
|||||||
|
<template> |
||||||
|
<view> |
||||||
|
|
||||||
|
<mescroll-body :sticky="true" ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback"> |
||||||
|
<view class="sticky-tabs"> |
||||||
|
<me-tabs v-model="tabIndex" :tabs="tabs" @change="tabChange"></me-tabs> |
||||||
|
</view> |
||||||
|
|
||||||
|
<!-- 搜索 --> |
||||||
|
<view class="cu-bar bg-white search"> |
||||||
|
<view class="search-form round"> |
||||||
|
<input v-model="searchVal" @confirm="keywordSearch" type="text" placeholder="输入搜索的关键词" confirm-type="search"></input> |
||||||
|
<text class="cuIcon-search"></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="add-customer mat15"> |
||||||
|
<text>共{{total}}条</text> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="cu-list menu-avatar"> |
||||||
|
<view class="cu-item" :class="modalName=='move-box-'+ index?'move-cur':''" v-for="(item,index) in processData" :key="index" |
||||||
|
@touchstart="ListTouchStart" @touchmove="ListTouchMove" @touchend="ListTouchEnd" :data-target="'move-box-' + index"> |
||||||
|
<view class="good-list" @tap="letterData(item,2)"> |
||||||
|
<view class="charge" :id="'index-' + item.name" :data-index="item.name"> |
||||||
|
<view class="charge-title flex-between"> |
||||||
|
<text>{{item.name}}的贷款申请</text> |
||||||
|
</view> |
||||||
|
<view class="charge-text"> |
||||||
|
<view class="flex-between"> |
||||||
|
<text>业务编号:</text> |
||||||
|
<text class="mgl30">{{item.businessCode}}</text> |
||||||
|
</view> |
||||||
|
<view class="flex-between"> |
||||||
|
<text>联系电话:</text> |
||||||
|
<text class="mgl30">{{item.phone}}</text> |
||||||
|
</view> |
||||||
|
<view class="flex-between"> |
||||||
|
<text>业务类别:</text> |
||||||
|
<text class="mgl30">{{item.businessType}}</text> |
||||||
|
</view> |
||||||
|
<view class="flex-between"> |
||||||
|
<text>担保额度(万元):</text> |
||||||
|
<text class="mgl30">{{item.loanMoney}}</text> |
||||||
|
</view> |
||||||
|
<view class="flex-between"> |
||||||
|
<text>担保期限:</text> |
||||||
|
<text class="mgl30">{{item.loanTern}}</text> |
||||||
|
</view> |
||||||
|
<view class="flex-between"> |
||||||
|
<text>贷审会日期:</text> |
||||||
|
<text class="mgl30">{{item.passingTime}}</text> |
||||||
|
</view> |
||||||
|
<view class="flex-between"> |
||||||
|
<text>审批状态:</text> |
||||||
|
<text class="mgl30">{{item.statusText}}</text> |
||||||
|
</view> |
||||||
|
<view class="flex-between"> |
||||||
|
<text>业务状态:</text> |
||||||
|
<text class="mgl30">{{item.businessText}}</text> |
||||||
|
</view> |
||||||
|
<view class="flex-between"> |
||||||
|
<text>操作状态:</text> |
||||||
|
<text class="mgl30">{{item.operatingText}}</text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="move foot-btn"> |
||||||
|
<button v-if="item.operatingText !== '已处理'&&item.businessStatus !== 3" @tap="letterData(item,3)" class="mini-btn round suc-btn" type="primary" size="mini">确认</button> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</mescroll-body> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import MescrollMixin from "@/components/mescroll-uni/mescroll-mixins.js"; |
||||||
|
export default { |
||||||
|
mixins: [MescrollMixin], // 使用mixin |
||||||
|
data() { |
||||||
|
return { |
||||||
|
tabs:[ |
||||||
|
{name:'全部', value: ''}, |
||||||
|
{name:'审核中', value: 1}, |
||||||
|
{name:'已审核', value: 2}, |
||||||
|
{name:'已驳回', value: 4} |
||||||
|
], |
||||||
|
tabIndex: 0, // 当前菜单下标 |
||||||
|
processData: [], |
||||||
|
total: 0, |
||||||
|
page: { |
||||||
|
num: 1, |
||||||
|
size: 10, |
||||||
|
}, |
||||||
|
status: '',//状态的值 |
||||||
|
searchVal: '', //搜索框的值 |
||||||
|
|
||||||
|
listTouchStart: 0, |
||||||
|
listTouchDirection: null, |
||||||
|
modalName: null |
||||||
|
}; |
||||||
|
}, |
||||||
|
onShow() { |
||||||
|
this.upCallback({ |
||||||
|
num:1,size:10 |
||||||
|
}) |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
//搜索框搜索 |
||||||
|
keywordSearch(e){ |
||||||
|
this.searchVal = e.detail.value |
||||||
|
this.upCallback(this.page) |
||||||
|
}, |
||||||
|
/*下拉刷新的回调 */ |
||||||
|
downCallback() { |
||||||
|
// 下拉刷新的回调,默认重置上拉加载列表为第一页 (自动执行 page.num=1, 再触发upCallback方法 ) |
||||||
|
this.mescroll.resetUpScroll() |
||||||
|
}, |
||||||
|
/*上拉加载的回调: 其中page.num:当前页 从1开始, page.size:每页数据条数,默认10 */ |
||||||
|
upCallback(page) { |
||||||
|
this.$http.post('/api-guarantee/dg-loan-notice/loanNoticeList',{ |
||||||
|
page: page.num, |
||||||
|
size: page.size, |
||||||
|
customerNumberOrName: this.searchVal, |
||||||
|
status: this.status |
||||||
|
}).then(res => { |
||||||
|
if(page.num == 1) this.processData = []; //如果是第一页需手动制空列表 |
||||||
|
res.data.list.map(e =>{ |
||||||
|
e.statusText = this.core.auditStatus(e.status).text |
||||||
|
e.businessText = this.core.businessType(e.businessStatus) |
||||||
|
e.operatingText = this.core.operationType(e.operatingStatus) |
||||||
|
}) |
||||||
|
this.total = res.data.totalCount |
||||||
|
|
||||||
|
this.processData = this.processData.concat(res.data.list); //追加新数据 |
||||||
|
this.mescroll.endSuccess(res.data.list); // 隐藏加载状态栏 |
||||||
|
}).catch(()=>{ |
||||||
|
//联网失败, 结束加载 |
||||||
|
this.mescroll.endErr(); |
||||||
|
}) |
||||||
|
}, |
||||||
|
// 切换菜单 |
||||||
|
tabChange (index) { |
||||||
|
this.status = this.tabs[index].value;//当前下标的值 |
||||||
|
this.searchVal = ''; |
||||||
|
this.processData = []; // 置空列表,显示加载进度条 |
||||||
|
this.mescroll.resetUpScroll() |
||||||
|
}, |
||||||
|
letterData(item,Type){ |
||||||
|
let operationType = Type //列表操作状态 |
||||||
|
//担保函数据 |
||||||
|
let letterValue = { |
||||||
|
businessId: item.businessId,//业务id |
||||||
|
bank: item.bank,//银行 |
||||||
|
name: item.name,//客户名称 |
||||||
|
passingTime: item.passingTime,//贷审会日期 |
||||||
|
loanMoney: this.core.fMoney(item.loanMoney),//担保金额 |
||||||
|
guaranteeMoney : this.core.fMoney3(item.loanMoney),//担保金额转格式 |
||||||
|
file: item.file,//会议纪要 |
||||||
|
loanTern: item.loanTern,//担保期限 |
||||||
|
auditOpinion: item.auditOpinion,//审核意见 |
||||||
|
roleId: item.roleId //角色id |
||||||
|
} |
||||||
|
letterValue = JSON.stringify(letterValue) |
||||||
|
uni.navigateTo({ |
||||||
|
url: '../noticeSee/noticeSee?operationType='+operationType+'&letterValue='+letterValue |
||||||
|
}); |
||||||
|
}, |
||||||
|
|
||||||
|
// ListTouch触摸开始 |
||||||
|
ListTouchStart(e) { |
||||||
|
this.listTouchStart = e.touches[0].pageX |
||||||
|
}, |
||||||
|
|
||||||
|
// ListTouch计算方向 |
||||||
|
ListTouchMove(e) { |
||||||
|
this.listTouchDirection = e.touches[0].pageX - this.listTouchStart > 0 ? 'right' : 'left' |
||||||
|
}, |
||||||
|
|
||||||
|
// ListTouch计算滚动 |
||||||
|
ListTouchEnd(e) { |
||||||
|
if (this.listTouchDirection == 'left') { |
||||||
|
this.modalName = e.currentTarget.dataset.target |
||||||
|
} else { |
||||||
|
this.modalName = null |
||||||
|
} |
||||||
|
this.listTouchDirection = null |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.cu-bar .search-form{ |
||||||
|
border: 2rpx solid #00B9FF; |
||||||
|
border-radius: 20rpx; |
||||||
|
background-color: #fff; |
||||||
|
margin: 0 50rpx; |
||||||
|
height: 72rpx; |
||||||
|
line-height: 72rpx; |
||||||
|
margin-top: 20rpx; |
||||||
|
input{ |
||||||
|
height: 72rpx; |
||||||
|
line-height: 72rpx; |
||||||
|
padding: 0 40rpx; |
||||||
|
} |
||||||
|
.cuIcon-search{ |
||||||
|
margin: 0 20rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
.add-customer{ |
||||||
|
padding: 0 50rpx; |
||||||
|
button{ |
||||||
|
background-color: #00B9FF; |
||||||
|
border-radius: 30rpx; |
||||||
|
.mar-icon{ |
||||||
|
margin-right: 10rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,184 @@ |
|||||||
|
<template> |
||||||
|
<view class="letter-content"> |
||||||
|
<view class="letter-header"> |
||||||
|
<text>放款通知</text> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="mat15 bank"> |
||||||
|
<text>{{letterValue.bank}}</text> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="letter-main mat15"> |
||||||
|
<text> |
||||||
|
我公司同意贵行为该公司放款。 |
||||||
|
</text> |
||||||
|
|
||||||
|
<view> |
||||||
|
<view class="mat15"> |
||||||
|
<text>客户名称:{{letterValue.name}}</text> |
||||||
|
</view> |
||||||
|
<view class="mat15"> |
||||||
|
<text>放款额度(万元):{{letterValue.loanMoney}} (大写:{{letterValue.guaranteeMoney}})</text> |
||||||
|
</view> |
||||||
|
<view class="mat15"> |
||||||
|
<text>担保期限:{{letterValue.loanTern}}</text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="mat15"> |
||||||
|
我公司同意贵行为<text class="letter-key">{{letterValue.name}}</text>放款人民币<text class="letter-key">{{letterValue.loanMoney}}</text>万元整(大写:<text class="letter-key">{{letterValue.guaranteeMoney}}</text>)。 |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="mat40 letter-footer"> |
||||||
|
<view class="mat15"> |
||||||
|
<text>大庆市工商业融资担保有限公司</text> |
||||||
|
</view> |
||||||
|
<view class="mat15"> |
||||||
|
<text>法定代表人:</text> |
||||||
|
</view> |
||||||
|
<view class="mat15"> |
||||||
|
<text>或授权代理人:</text> |
||||||
|
</view> |
||||||
|
<view class="mat15"> |
||||||
|
<text>  年   月   日</text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<uni-forms v-show="operationType !== 2" :value="letterValue" ref="letterValue" validate-trigger="bind" err-show-type="toast"> |
||||||
|
<uni-forms-item name="name" :label="operationType === 1 ? '审核' : '放款确认'"> |
||||||
|
<uni-easyinput :disabled="operationType == 2" type="textarea" v-model="letterValue.auditOpinion" placeholder="请输入审核意见"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
</uni-forms> |
||||||
|
<!-- v-show="operationType == 1" --> |
||||||
|
<view class="foot-btn btn-rig pad-bt margin-top"> |
||||||
|
<button v-show="operationType == 3" class="mini-btn round def-btn mar-lr" type="primary" size="mini" @tap="sumbitOpinion('提交',2)">提交</button> |
||||||
|
<button v-show="operationType == 3" class="mini-btn round refuse-btn mar-lr" type="primary" size="mini" @tap="sumbitOpinion('驳回',4)">驳回</button> |
||||||
|
<button plain class="mini-btn round plain-btn" type="primary" size="mini" @tap="back()">返回</button> |
||||||
|
</view> |
||||||
|
|
||||||
|
<timeline></timeline> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
data() { |
||||||
|
return { |
||||||
|
fileList: [], |
||||||
|
operationType: '', |
||||||
|
letterValue: {}, |
||||||
|
}; |
||||||
|
}, |
||||||
|
onLoad(option) { |
||||||
|
// 判断来自待处理否 |
||||||
|
if(uni.getStorageSync('decideIndex')){ // 仅有一个确认 |
||||||
|
let data = JSON.parse(uni.getStorageSync('decideIndex')) |
||||||
|
this.loanNoticeDetail(data) |
||||||
|
this.operationType = 3 |
||||||
|
uni.removeStorageSync('decideIndex') |
||||||
|
}else if(uni.getStorageSync('copyData')){ |
||||||
|
let data =JSON.parse(uni.getStorageSync('copyData')),that = this |
||||||
|
this.operationType = 2 |
||||||
|
if(uni.getStorageSync('notCopy','查看')){ |
||||||
|
this.loanNoticeDetail(data) |
||||||
|
}else{ |
||||||
|
this.letterValue = { |
||||||
|
name:data.clientName, |
||||||
|
passingTime:data.passingTime, |
||||||
|
loanMoney:data.loanMoney, |
||||||
|
loanTern:data.loanTern, |
||||||
|
bank:data.bank, |
||||||
|
guaranteeMoney:that.core.fMoney3(data.loanMoney) |
||||||
|
} |
||||||
|
} |
||||||
|
}else{ |
||||||
|
this.fileList = option.fileList |
||||||
|
this.operationType = option.operationType |
||||||
|
this.letterValue = JSON.parse(option.letterValue) |
||||||
|
} |
||||||
|
|
||||||
|
}, |
||||||
|
methods: { |
||||||
|
// 详情 |
||||||
|
loanNoticeDetail(data){ |
||||||
|
this.$http.get('/api-guarantee/dg-loan-notice/loanNoticeDetail',{id:data.detailId}).then(res=>{ |
||||||
|
let that = this,data = res.data.data |
||||||
|
this.letterValue = { |
||||||
|
businessId: data.businessId,//业务id |
||||||
|
bank: data.bank,//银行 |
||||||
|
name: data.name,//客户名称 |
||||||
|
passingTime: data.passingTime,//贷审会日期 |
||||||
|
loanMoney: data.loanMoney,//担保金额 |
||||||
|
guaranteeMoney: that.core.fMoney3(data.loanMoney),//担保金额转格式 |
||||||
|
file: '',//会议纪要 |
||||||
|
loanTern: data.loanTern,//担保期限 |
||||||
|
auditOpinion: '',//审核意见 |
||||||
|
roleId: ''//角色id |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
//放大图片 |
||||||
|
ViewImage(e) { |
||||||
|
uni.previewImage({ |
||||||
|
urls: this.imgList, |
||||||
|
current: e.currentTarget.dataset.url |
||||||
|
}); |
||||||
|
}, |
||||||
|
|
||||||
|
// 提交审核意见 |
||||||
|
sumbitOpinion(text,type){ |
||||||
|
let _this = this |
||||||
|
uni.showModal({ |
||||||
|
title: '提示', |
||||||
|
content: `确定要${text}该审核意见吗?`, |
||||||
|
success: function (res) { |
||||||
|
if (res.confirm) { |
||||||
|
if(type == 4 && _this.letterValue.auditOpinion == ''){ |
||||||
|
return uni.showToast({title: '请先填写审核意见!',icon:'none'}) |
||||||
|
} |
||||||
|
_this.$http.post('/api-guarantee/dg-loan-notice/updateLoanNotice',{ |
||||||
|
auditOpinion: _this.letterValue.auditOpinion, |
||||||
|
businessId: _this.letterValue.businessId, |
||||||
|
status: type |
||||||
|
}).then(res => { |
||||||
|
uni.showToast({title: `${text}成功`,icon:'none'}) |
||||||
|
_this.back() |
||||||
|
}).catch(()=>{ |
||||||
|
|
||||||
|
}) |
||||||
|
} else if (res.cancel) {} |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.letter-content{ |
||||||
|
padding: 40rpx 40rpx; |
||||||
|
.letter-header{ |
||||||
|
text-align: center; |
||||||
|
font-size: 32rpx; |
||||||
|
font-weight: 500; |
||||||
|
padding-bottom: 40rpx; |
||||||
|
border-bottom: 2rpx solid #e5e5e5; |
||||||
|
} |
||||||
|
.bank{ |
||||||
|
font-weight: 700; |
||||||
|
} |
||||||
|
.letter-main{ |
||||||
|
letter-spacing: 4rpx; |
||||||
|
text-indent: 60rpx; |
||||||
|
} |
||||||
|
.letter-key{ |
||||||
|
font-weight: bold; |
||||||
|
margin: 0 10px; |
||||||
|
} |
||||||
|
.letter-footer{ |
||||||
|
text-align: right; |
||||||
|
margin-right: 20rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,231 @@ |
|||||||
|
<template> |
||||||
|
<view> |
||||||
|
|
||||||
|
<mescroll-body :sticky="true" ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback"> |
||||||
|
<view class="sticky-tabs"> |
||||||
|
<me-tabs v-model="tabIndex" :tabs="tabs" @change="tabChange"></me-tabs> |
||||||
|
</view> |
||||||
|
|
||||||
|
<!-- 搜索 --> |
||||||
|
<view class="cu-bar bg-white search"> |
||||||
|
<view class="search-form round"> |
||||||
|
<input v-model="searchVal" @confirm="keywordSearch" type="text" placeholder="输入搜索的关键词" confirm-type="search"></input> |
||||||
|
<text class="cuIcon-search"></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="add-customer mat15"> |
||||||
|
<text>共{{total}}条</text> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="cu-list menu-avatar"> |
||||||
|
<view class="cu-item" :class="modalName=='move-box-'+ index?'move-cur':''" v-for="(item,index) in processData" :key="index" |
||||||
|
@touchstart="ListTouchStart" @touchmove="ListTouchMove" @touchend="ListTouchEnd" :data-target="'move-box-' + index"> |
||||||
|
<view class="good-list" @tap="letterData(item,2)"> |
||||||
|
<view class="charge" :id="'index-' + item.name" :data-index="item.name"> |
||||||
|
<view class="charge-title flex-between"> |
||||||
|
<text>{{item.name}}的贷款申请</text> |
||||||
|
</view> |
||||||
|
<view class="charge-text"> |
||||||
|
<view class="flex-between"> |
||||||
|
<text>业务编号:</text> |
||||||
|
<text class="mgl30">{{item.businessCode}}</text> |
||||||
|
</view> |
||||||
|
<view class="flex-between"> |
||||||
|
<text>联系电话:</text> |
||||||
|
<text class="mgl30">{{item.phone}}</text> |
||||||
|
</view> |
||||||
|
<view class="flex-between"> |
||||||
|
<text>业务类别:</text> |
||||||
|
<text class="mgl30">{{item.businessType}}</text> |
||||||
|
</view> |
||||||
|
<view class="flex-between"> |
||||||
|
<text>担保额度(万元):</text> |
||||||
|
<text class="mgl30">{{item.loanMoney}}</text> |
||||||
|
</view> |
||||||
|
<view class="flex-between"> |
||||||
|
<text>担保期限:</text> |
||||||
|
<text class="mgl30">{{item.loanTern}}</text> |
||||||
|
</view> |
||||||
|
<view class="flex-between"> |
||||||
|
<text>贷审会日期:</text> |
||||||
|
<text class="mgl30">{{item.passingTime}}</text> |
||||||
|
</view> |
||||||
|
<view class="flex-between"> |
||||||
|
<text>审批状态:</text> |
||||||
|
<text class="mgl30">{{item.statusText}}</text> |
||||||
|
</view> |
||||||
|
<view class="flex-between"> |
||||||
|
<text>业务状态:</text> |
||||||
|
<text class="mgl30">{{item.businessText}}</text> |
||||||
|
</view> |
||||||
|
<view class="flex-between"> |
||||||
|
<text>操作状态:</text> |
||||||
|
<text class="mgl30">{{item.operatingText}}</text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="move foot-btn"> |
||||||
|
<button v-if="item.operatingText !== '已处理'&&item.businessStatus !== 3" @tap="letterData(item,3)" |
||||||
|
class="mini-btn round suc-btn" type="primary" size="mini">确认</button> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</mescroll-body> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import MescrollMixin from "@/components/mescroll-uni/mescroll-mixins.js"; |
||||||
|
export default { |
||||||
|
mixins: [MescrollMixin], // 使用mixin |
||||||
|
data() { |
||||||
|
return { |
||||||
|
tabs:[ |
||||||
|
{name:'全部', value: ''}, |
||||||
|
{name:'审核中', value: 1}, |
||||||
|
{name:'已审核', value: 2}, |
||||||
|
{name:'已驳回', value: 4} |
||||||
|
], |
||||||
|
tabIndex: 0, // 当前菜单下标 |
||||||
|
processData: [], |
||||||
|
total: 0, |
||||||
|
page: { |
||||||
|
num: 1, |
||||||
|
size: 10, |
||||||
|
}, |
||||||
|
status: '',//状态的值 |
||||||
|
searchVal: '', //搜索框的值 |
||||||
|
|
||||||
|
listTouchStart: 0, |
||||||
|
listTouchDirection: null, |
||||||
|
modalName: null |
||||||
|
}; |
||||||
|
}, |
||||||
|
onShow() { |
||||||
|
this.upCallback({ |
||||||
|
num:1,size:10 |
||||||
|
}) |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
//搜索框搜索 |
||||||
|
keywordSearch(e){ |
||||||
|
this.searchVal = e.detail.value |
||||||
|
this.upCallback(this.page) |
||||||
|
}, |
||||||
|
/*下拉刷新的回调 */ |
||||||
|
downCallback() { |
||||||
|
// 下拉刷新的回调,默认重置上拉加载列表为第一页 (自动执行 page.num=1, 再触发upCallback方法 ) |
||||||
|
this.mescroll.resetUpScroll() |
||||||
|
}, |
||||||
|
/*上拉加载的回调: 其中page.num:当前页 从1开始, page.size:每页数据条数,默认10 */ |
||||||
|
upCallback(page) { |
||||||
|
this.$http.post('/api-guarantee/dg-payment-confirmation-consider/paymentConfirmationList',{ |
||||||
|
page: page.num, |
||||||
|
size: page.size, |
||||||
|
customerNumberOrName: this.searchVal, |
||||||
|
status: this.status |
||||||
|
}).then(res => { |
||||||
|
if(page.num == 1) this.processData = []; //如果是第一页需手动制空列表 |
||||||
|
res.data.list.map(e =>{ |
||||||
|
e.statusText = this.core.auditStatus(e.status).text |
||||||
|
e.businessText = this.core.businessType(e.businessStatus) |
||||||
|
e.operatingText = this.core.operationType(e.operatingStatus) |
||||||
|
}) |
||||||
|
this.total = res.data.totalCount |
||||||
|
|
||||||
|
this.processData = this.processData.concat(res.data.list); //追加新数据 |
||||||
|
this.mescroll.endSuccess(res.data.list); // 隐藏加载状态栏 |
||||||
|
}).catch(()=>{ |
||||||
|
//联网失败, 结束加载 |
||||||
|
this.mescroll.endErr(); |
||||||
|
}) |
||||||
|
}, |
||||||
|
// 切换菜单 |
||||||
|
tabChange (index) { |
||||||
|
this.status = this.tabs[index].value;//当前下标的值 |
||||||
|
this.searchVal = ''; |
||||||
|
this.processData = []; // 置空列表,显示加载进度条 |
||||||
|
this.mescroll.resetUpScroll() |
||||||
|
}, |
||||||
|
letterData(item,Type){ |
||||||
|
let operationType = Type //列表操作状态 |
||||||
|
//担保函数据 |
||||||
|
let letterValue = { |
||||||
|
businessId: item.businessId,//业务id |
||||||
|
bank: item.bank,//银行 |
||||||
|
name: item.name,//客户名称 |
||||||
|
passingTime: item.passingTime,//贷审会日期 |
||||||
|
loanMoney: this.core.fMoney(item.loanMoney),//担保金额 |
||||||
|
guaranteeMoney : this.core.fMoney3(item.loanMoney),//担保金额转格式 |
||||||
|
file: item.file,//会议纪要 |
||||||
|
loanTern: item.loanTern,//担保期限 |
||||||
|
auditOpinion: item.auditOpinion,//审核意见 |
||||||
|
imgFile: item.imgFile, //图片列表 |
||||||
|
isFgJl: item.isFgJl//上传的附件 |
||||||
|
} |
||||||
|
let imgListT = [] |
||||||
|
if(item.imgFile){ |
||||||
|
imgListT = item.imgFile |
||||||
|
} |
||||||
|
let fileList = item.file.split(",") |
||||||
|
letterValue = JSON.stringify(letterValue) |
||||||
|
uni.setStorageSync('businessId',item.businessId)// 业务id--给底部流程图用 |
||||||
|
uni.navigateTo({ |
||||||
|
url: '../paymentSee/paymentSee?fileList='+fileList+'&imgListT='+imgListT+'&operationType='+operationType+'&letterValue='+letterValue |
||||||
|
}); |
||||||
|
}, |
||||||
|
|
||||||
|
// ListTouch触摸开始 |
||||||
|
ListTouchStart(e) { |
||||||
|
this.listTouchStart = e.touches[0].pageX |
||||||
|
}, |
||||||
|
|
||||||
|
// ListTouch计算方向 |
||||||
|
ListTouchMove(e) { |
||||||
|
this.listTouchDirection = e.touches[0].pageX - this.listTouchStart > 0 ? 'right' : 'left' |
||||||
|
}, |
||||||
|
|
||||||
|
// ListTouch计算滚动 |
||||||
|
ListTouchEnd(e) { |
||||||
|
if (this.listTouchDirection == 'left') { |
||||||
|
this.modalName = e.currentTarget.dataset.target |
||||||
|
} else { |
||||||
|
this.modalName = null |
||||||
|
} |
||||||
|
this.listTouchDirection = null |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.cu-bar .search-form{ |
||||||
|
border: 2rpx solid #00B9FF; |
||||||
|
border-radius: 20rpx; |
||||||
|
background-color: #fff; |
||||||
|
margin: 0 50rpx; |
||||||
|
height: 72rpx; |
||||||
|
line-height: 72rpx; |
||||||
|
margin-top: 20rpx; |
||||||
|
input{ |
||||||
|
height: 72rpx; |
||||||
|
line-height: 72rpx; |
||||||
|
padding: 0 40rpx; |
||||||
|
} |
||||||
|
.cuIcon-search{ |
||||||
|
margin: 0 20rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
.add-customer{ |
||||||
|
padding: 0 50rpx; |
||||||
|
button{ |
||||||
|
background-color: #00B9FF; |
||||||
|
border-radius: 30rpx; |
||||||
|
.mar-icon{ |
||||||
|
margin-right: 10rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,257 @@ |
|||||||
|
<template> |
||||||
|
<view class="letter-content"> |
||||||
|
<view class="letter-header"> |
||||||
|
<text>担保承诺函</text> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="mat15 bank"> |
||||||
|
<text>{{letterValue.bank}}</text> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="letter-main mat15"> |
||||||
|
<text v-show="operationType !== 2"> |
||||||
|
根据<text class="letter-key">{{letterValue.name}}</text>的贷款申请,经我公司项目贷审会研究,同意为其在贵行申请的贷款提供担保,特向贵行提供承诺。本承诺系我公司同意与贵行签订保证合同的意向,一切权利义务以保证合同为准! |
||||||
|
</text> |
||||||
|
|
||||||
|
<!-- 查看担保函的文字 --> |
||||||
|
<text v-show="operationType === 2"> |
||||||
|
根据<text class="letter-key">{{letterValue.name}}</text>的贷款申请,经我公司<text class="letter-key">{{letterValue.passingTime}}</text>项目贷审会研究,同意为其在贵行申请的贷款提供<text class="letter-key">{{letterValue.loanMoney}}</text>万元担保,期限<text class="letter-key">{{letterValue.loanTern}}</text>,特向贵行提供承诺。本承诺系我公司同意与贵行签订保证合同的意向,一切权利义务以保证合同为准! |
||||||
|
</text> |
||||||
|
|
||||||
|
<view v-show="operationType !== 2"> |
||||||
|
<view class="mat15"> |
||||||
|
<text>客户名称:{{letterValue.name}}</text> |
||||||
|
</view> |
||||||
|
<view class="mat15"> |
||||||
|
<text>贷审会日期:{{letterValue.passingTime}}</text> |
||||||
|
</view> |
||||||
|
<view class="mat15"> |
||||||
|
<text>担保金额(万元):{{letterValue.loanMoney}} (大写:{{letterValue.guaranteeMoney}})</text> |
||||||
|
</view> |
||||||
|
<view class="mat15"> |
||||||
|
<text>担保期限:{{letterValue.loanTern}}</text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="mat40 letter-footer"> |
||||||
|
<view class="mat15"> |
||||||
|
<text>大庆市工商业融资担保有限公司</text> |
||||||
|
</view> |
||||||
|
<view class="mat15"> |
||||||
|
<text>法定代表人:</text> |
||||||
|
</view> |
||||||
|
<view class="mat15"> |
||||||
|
<text>或授权代理人:</text> |
||||||
|
</view> |
||||||
|
<view class="mat15"> |
||||||
|
<text>  年   月   日</text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<view class="bottom-border"> |
||||||
|
<view class="left-border"><text class="mgl10">附件</text></view> |
||||||
|
</view> |
||||||
|
<upload :files="filesArray" :handle="false"></upload> |
||||||
|
<!-- operationType !== 2 --> |
||||||
|
<view class="bottom-border" v-show="operationType !== 2"> |
||||||
|
<view class="left-border"><text class="mgl10">上传银行回单</text></view> |
||||||
|
</view> |
||||||
|
<!-- 仅财务部可以上传 --> |
||||||
|
<upload @upload="uploadFile" :files="imgArray" :handle="operationType != 2&&letterValue.isFgJl !== 1"></upload> |
||||||
|
|
||||||
|
<uni-forms v-show="operationType != 2" :value="letterValue" ref="letterValue" validate-trigger="bind" err-show-type="toast"> |
||||||
|
<uni-forms-item name="name" :label="operationType === 1 ? '审核' : '财务确认'"> |
||||||
|
<uni-easyinput type="textarea" v-model="letterValue.auditOpinion" placeholder="请输入审核意见"></uni-easyinput> |
||||||
|
</uni-forms-item> |
||||||
|
</uni-forms> |
||||||
|
<view class="foot-btn btn-rig pad-bt margin-top"> |
||||||
|
<button v-show="operationType == 3" class="mini-btn round def-btn mar-lr" type="primary" size="mini" @tap="sumbitOpinion('提交',2)">提交</button> |
||||||
|
<button v-show="operationType == 3" class="mini-btn round refuse-btn mar-lr" type="primary" size="mini" @tap="sumbitOpinion('驳回',4)">驳回</button> |
||||||
|
<button plain class="mini-btn round plain-btn" type="primary" size="mini" @tap="back()">返回</button> |
||||||
|
</view> |
||||||
|
|
||||||
|
<timeline></timeline> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import upload from '../../../components/pretty-uploadFile/pretty-uploadFile.vue' |
||||||
|
export default { |
||||||
|
components: { |
||||||
|
upload |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
// fileList: [], |
||||||
|
// imgListT: [], |
||||||
|
operationType: '', |
||||||
|
letterValue: {}, |
||||||
|
|
||||||
|
imgArray:[],// 展示用图片 |
||||||
|
filesArray:[],// 展示用附件 |
||||||
|
|
||||||
|
roleArr:[], |
||||||
|
|
||||||
|
getImg:[],// 获取上传图片 |
||||||
|
isFgJl:'' |
||||||
|
|
||||||
|
}; |
||||||
|
}, |
||||||
|
destroyed() {// 销毁组件操作 |
||||||
|
uni.removeStorageSync('copyData') |
||||||
|
}, |
||||||
|
onLoad(option) { |
||||||
|
// 判断来自待处理否 |
||||||
|
if(uni.getStorageSync('decideIndex')){ // 仅有一个确认 |
||||||
|
let data = JSON.parse(uni.getStorageSync('decideIndex')) |
||||||
|
this.paymentDetail(data) |
||||||
|
this.operationType = 3 |
||||||
|
uni.removeStorageSync('decideIndex') |
||||||
|
}else if(uni.getStorageSync('copyData')){ |
||||||
|
let data =JSON.parse(uni.getStorageSync('copyData')) |
||||||
|
this.operationType = 2 |
||||||
|
if(uni.getStorageSync('notCopy','查看')){ |
||||||
|
this.paymentDetail(data) |
||||||
|
}else{ |
||||||
|
this.letterValue = { |
||||||
|
name:data.clientName, |
||||||
|
passingTime:data.passingTime, |
||||||
|
loanMoney:data.loanMoney, |
||||||
|
loanTern:data.loanTern |
||||||
|
} |
||||||
|
if(data.file) this.filesArray = this.handleFiles(data.file.split(',')) |
||||||
|
if(data.imgFile) this.imgArray = this.handleFiles(data.imgFile.split(',')) |
||||||
|
} |
||||||
|
}else{ |
||||||
|
this.roleArr = uni.getStorageSync('roleArr').split(",") |
||||||
|
// 附件信息 |
||||||
|
if(option.fileList)this.filesArray = this.handleFiles(option.fileList.split(',')) |
||||||
|
// 图片信息 |
||||||
|
if(option.imgListT) this.imgArray = this.handleFiles(option.imgListT.split(',')) |
||||||
|
if(option.imgListT) this.getImg = option.imgListT.split(',') |
||||||
|
// operationType 2查看,3确定 |
||||||
|
this.operationType = option.operationType |
||||||
|
this.letterValue = JSON.parse(option.letterValue) |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
// 详情接口--个人效率专用 |
||||||
|
paymentDetail(data){ |
||||||
|
this.$http.get('/api-guarantee/dg-payment-confirmation-consider/paymentConfirmationDetail',{id:data.detailId}).then(res=>{ |
||||||
|
let that = this,data = res.data.data |
||||||
|
this.letterValue = { |
||||||
|
businessId: data.businessId,//业务id |
||||||
|
bank: data.bank,//银行 |
||||||
|
name: data.clientName,//客户名称 |
||||||
|
passingTime: data.passingTime,//贷审会日期 |
||||||
|
loanMoney: data.loanMoney,//担保金额 |
||||||
|
guaranteeMoney: that.core.fMoney3(data.loanMoney),//担保金额转格式 |
||||||
|
file: '',//会议纪要 |
||||||
|
loanTern: data.loanTern,//担保期限 |
||||||
|
auditOpinion: '',//审核意见 |
||||||
|
roleId: ''//角色id |
||||||
|
} |
||||||
|
if(data.file) this.filesArray = this.handleFiles(data.file.split(',')) |
||||||
|
if(data.imgFile) this.imgArray = this.handleFiles(data.imgFile.split(',')) |
||||||
|
}) |
||||||
|
}, |
||||||
|
//放大图片 |
||||||
|
ViewImage(e) { |
||||||
|
uni.previewImage({ |
||||||
|
urls: this.imgList, |
||||||
|
current: e.currentTarget.dataset.url |
||||||
|
}); |
||||||
|
}, |
||||||
|
|
||||||
|
// 提交审核意见 |
||||||
|
sumbitOpinion(text,type){ |
||||||
|
let _this = this |
||||||
|
uni.showModal({ |
||||||
|
title: '提示', |
||||||
|
content: `确定要${text}该审核意见吗?`, |
||||||
|
success: function (res) { |
||||||
|
if (res.confirm) { |
||||||
|
if(type == 4 && _this.letterValue.auditOpinion == ''){ |
||||||
|
return uni.showToast({title: '请先填写审核意见!',icon:'none'}) |
||||||
|
} |
||||||
|
// let imgFile = _this.imgArray.map(e =>e.url).join() |
||||||
|
_this.$http.post('/api-guarantee/dg-payment-confirmation-consider/updatePaymentConfirmation',{ |
||||||
|
auditOpinion: _this.letterValue.auditOpinion, |
||||||
|
businessId: _this.letterValue.businessId, |
||||||
|
status: type, |
||||||
|
imgFile: _this.getImg.join() |
||||||
|
}).then(res => { |
||||||
|
uni.showToast({title: `${text}成功`,icon:'none'}) |
||||||
|
_this.back() |
||||||
|
}).catch(()=>{ |
||||||
|
|
||||||
|
}) |
||||||
|
} else if (res.cancel) {} |
||||||
|
} |
||||||
|
}); |
||||||
|
}, |
||||||
|
// 传入 data 要处理的数据,格式['url1','url2'] |
||||||
|
// 传入对应的index |
||||||
|
handleFiles(data,num){ |
||||||
|
if(!data||!data.length) return [] |
||||||
|
let arr = [] |
||||||
|
data.map(e=>{ |
||||||
|
if(e=='') return |
||||||
|
let obj = {}, |
||||||
|
num= e.lastIndexOf("."), |
||||||
|
ext = e.substr(num+1); |
||||||
|
// 判断格式--图片或者其他 |
||||||
|
if(e.toLowerCase().includes('jpg','jpeg','png','bmp','gif')){ |
||||||
|
obj.book = 'img' |
||||||
|
}else{ |
||||||
|
obj.book = "file" |
||||||
|
if(ext=='png'){ |
||||||
|
obj.book = 'img' |
||||||
|
} |
||||||
|
} |
||||||
|
// 确定文件格式 |
||||||
|
obj.ext = ext |
||||||
|
obj.url = e |
||||||
|
arr.push(obj) |
||||||
|
// // 接收的数组,1附件2图片 |
||||||
|
// num==1?this.filesArray.push(obj):this.imgArray.push(obj) |
||||||
|
}) |
||||||
|
return arr |
||||||
|
}, |
||||||
|
/* 上传文件 */ |
||||||
|
uploadFile(e) { |
||||||
|
let fileList = e.map(r => r.url) |
||||||
|
this.getImg = fileList |
||||||
|
}, |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.letter-content{ |
||||||
|
padding: 40rpx 40rpx; |
||||||
|
.letter-header{ |
||||||
|
text-align: center; |
||||||
|
font-size: 32rpx; |
||||||
|
font-weight: 500; |
||||||
|
padding-bottom: 40rpx; |
||||||
|
border-bottom: 2rpx solid #e5e5e5; |
||||||
|
} |
||||||
|
.bank{ |
||||||
|
font-weight: 700; |
||||||
|
} |
||||||
|
.letter-main{ |
||||||
|
letter-spacing: 4rpx; |
||||||
|
text-indent: 60rpx; |
||||||
|
} |
||||||
|
.letter-key{ |
||||||
|
font-weight: bold; |
||||||
|
margin: 0 10px; |
||||||
|
} |
||||||
|
.letter-footer{ |
||||||
|
text-align: right; |
||||||
|
margin-right: 20rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,338 @@ |
|||||||
|
<template> |
||||||
|
<view> |
||||||
|
<!-- 菜单 (悬浮,预先隐藏)--> |
||||||
|
<me-tabs v-if="isShowSticky" v-model="tabIndex" :fixed="true" :tabs="tabs" @change="tabChange"></me-tabs> |
||||||
|
|
||||||
|
<mescroll-body ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback" :up="upOption" @scroll="scroll" @topclick="topClick"> |
||||||
|
<!-- 菜单 (在mescroll-uni中不能使用fixed,否则iOS滚动时会抖动, 所以需在mescroll-uni之外存在一个一样的菜单) --> |
||||||
|
<view id="tabInList"> |
||||||
|
<me-tabs v-model="tabIndex" :tabs="tabs" @change="tabChange"></me-tabs> |
||||||
|
</view> |
||||||
|
|
||||||
|
<!-- 搜索 --> |
||||||
|
<view class="cu-bar bg-white search" :class="scrolltop ? 'fixed' : ''" :style="[{top:CustomBar + 'px'}]"> |
||||||
|
<view class="search-form round"> |
||||||
|
<input @input="searchList" type="text" placeholder="输入搜索的关键词" v-model="listParams.codeOrName" confirm-type="search"></input> |
||||||
|
<text class="cuIcon-search"></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<!-- 数据列表 --> |
||||||
|
<!-- <index-list :list="chargeList"></index-list> --> |
||||||
|
<!-- <touch-list :list="chargeList" :listTouchDirection="listTouchDirection" :listTouchStart="listTouchStart" :modalName="modalName" |
||||||
|
@TouchStart="ListTouchStart" @TouchMove="ListTouchMove" @TouchEnd="ListTouchEnd" |
||||||
|
></touch-list> --> |
||||||
|
|
||||||
|
<view class="cu-list menu-avatar"> |
||||||
|
<view class="cu-item" :class="modalName=='move-box-'+ index?'move-cur':''" v-for="(item,index) in listData" :key="index" |
||||||
|
@touchstart="ListTouchStart" @touchmove="ListTouchMove" @touchend="ListTouchEnd" :data-target="'move-box-' + index"> |
||||||
|
<view class="good-list"> |
||||||
|
<view class="charge" :id="'index-' + item.name" :data-index="item.name"> |
||||||
|
<navigator v-if="[59,60,68,69,70].includes(item.roleId)" @click="handleGe(item,'watch')" hover-class='none' :url="'/guaranteePages/pages/regulationSee/regulationSee'" |
||||||
|
class="nav-li" navigateTo :style="[{animation: 'show ' + ((index+1)*0.2+1) + 's 1'}]"> |
||||||
|
<view class="charge-title flex-between"> |
||||||
|
<text>{{item.name}}的贷款申请</text> |
||||||
|
</view> |
||||||
|
<view class="charge-text"> |
||||||
|
<view> |
||||||
|
<text>业务编号:</text> |
||||||
|
<text class="mgl30">{{item.businessCode}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>客户名称:</text> |
||||||
|
<text class="mgl30">{{item.name}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>联系电话:</text> |
||||||
|
<text class="mgl30">{{item.phone}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>业务类别:</text> |
||||||
|
<text class="mgl30">{{item.businessType}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>申请额度(万元):</text> |
||||||
|
<text class="mgl30">{{item.applyAmount}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>申请期限:</text> |
||||||
|
<text class="mgl30">{{item.applyTime}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>申请日期:</text> |
||||||
|
<text class="mgl30">{{item.createTime}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>审批状态:</text> |
||||||
|
<text class="mgl30">{{item.statusVal}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>业务状态:</text> |
||||||
|
<text class="mgl30">{{item.businessStatusVal}}</text> |
||||||
|
</view> |
||||||
|
<view> |
||||||
|
<text>操作状态:</text> |
||||||
|
<text class="mgl30">{{item.operationType}}</text> |
||||||
|
</view> |
||||||
|
<!-- <view class="flex-between"> |
||||||
|
<text class="time-text">{{item.time}}</text> |
||||||
|
<text class="status-text" :style="'background-color:'+item.status.bgColor+';color:'+item.status.textColor">{{item.status.text}}</text> |
||||||
|
</view> --> |
||||||
|
</view> |
||||||
|
</navigator> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="move foot-btn sideBtn"> |
||||||
|
<button v-if="[68].includes(item.roleId)&&item.businessStatus !== 3" v-show="item.auditStatus===0&&item.operationStatus == 1" class="mini-btn round def-btn mar-lr" type="primary" size="mini" @click="handleGe(item,'none',true)">指派</button> |
||||||
|
<button v-if="[69].includes(item.roleId)&&item.businessStatus !== 3" v-show="item.auditStatus===1&&item.operationStatus == 1||item.auditStatus===4&&item.operationStatus == 1" class="mini-btn round suc-btn mar-lr" type="primary" size="mini" @click="handleGe(item,'survey')">调查</button> |
||||||
|
<view class="margin-top"> |
||||||
|
<button v-if="[68,70].includes(item.roleId)&&item.businessStatus !== 3" v-show="item.auditStatus===1&&item.operationStatus == 1||item.auditStatus===4&&item.operationStatus == 1" class="mini-btn round def-btn" type="primary" size="mini" @click="handleGe(item,'audit')">审核</button> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</mescroll-body> |
||||||
|
</view> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import MescrollMixin from "@/components/mescroll-uni/mescroll-mixins.js"; |
||||||
|
export default { |
||||||
|
mixins: [MescrollMixin], // 使用mixin |
||||||
|
data() { |
||||||
|
return { |
||||||
|
upOption: { |
||||||
|
// 如果用mescroll-uni 则需要onScroll: true, 且需要 @scroll="scroll"; 而mescroll-body最简单只需在onPageScroll处理即可 |
||||||
|
// onScroll: true // 是否监听滚动事件, 默认false (配置为true时,可@scroll="scroll"获取到滚动条位置和方向) |
||||||
|
}, |
||||||
|
tabs:[ |
||||||
|
{name:'全部', num:1, y:0, curPageLen:0, hasNext:true}, |
||||||
|
{name:'待指派', num:1, y:0, curPageLen:0, hasNext:true}, |
||||||
|
{name:'审核中', num:1, y:0, curPageLen:0, hasNext:true}, |
||||||
|
{name:'已审核', num:1, y:0, curPageLen:0, hasNext:true}, |
||||||
|
{name:'已拒绝', num:1, y:0, curPageLen:0, hasNext:true}, |
||||||
|
{name:'已驳回', num:1, y:0, curPageLen:0, hasNext:true}, |
||||||
|
{name:'已撤销', num:1, y:0, curPageLen:0, hasNext:true}, |
||||||
|
], |
||||||
|
tabIndex: 0, // 当前菜单下标 |
||||||
|
preIndex: 0, // 前一个菜单下标 |
||||||
|
navTop: null, // nav距离到顶部的距离 (如计算不准确,可直接写死某个值) |
||||||
|
isShowSticky: false, // 是否悬浮 |
||||||
|
TabCur: 0, |
||||||
|
scrollLeft: 0, |
||||||
|
CustomBar: this.CustomBar, |
||||||
|
listCurID: '', |
||||||
|
listData: [], |
||||||
|
total: 1, |
||||||
|
scrolltop: false, |
||||||
|
listTouchStart: 0, |
||||||
|
listTouchDirection: null, |
||||||
|
modalName: null, |
||||||
|
listParams:{ |
||||||
|
customerNumberOrName:"",// 搜索框 |
||||||
|
page:1, |
||||||
|
size:10 |
||||||
|
}, |
||||||
|
status:'',// 顶部状态 |
||||||
|
}; |
||||||
|
}, |
||||||
|
onShow() { |
||||||
|
this.upCallback({num:1,size:10,search:''})// 每次返回该页面,刷新页面请求 |
||||||
|
}, |
||||||
|
mounted() { |
||||||
|
// this.listData.map(e =>{ |
||||||
|
// e.status = this.core.auditStatus(e.status) |
||||||
|
// }) |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
changeTab(index){ |
||||||
|
this.TabCur = index; |
||||||
|
}, |
||||||
|
/*下拉刷新的回调 */ |
||||||
|
downCallback() { |
||||||
|
// 这里加载你想下拉刷新的数据, 比如刷新轮播数据 |
||||||
|
// loadSwiper(); |
||||||
|
// 下拉刷新的回调,默认重置上拉加载列表为第一页 (自动执行 page.num=1, 再触发upCallback方法 ) |
||||||
|
this.mescroll.resetUpScroll() |
||||||
|
}, |
||||||
|
/*上拉加载的回调: 其中page.num:当前页 从1开始, page.size:每页数据条数,默认10 */ |
||||||
|
upCallback(page) { |
||||||
|
this.$http.get('/api-guarantee/compliance/investigation/list',{ |
||||||
|
page: page.num, |
||||||
|
size: page.size, |
||||||
|
status:this.status?this.status:'', |
||||||
|
codeOrName:page.search?page.search:'' |
||||||
|
}).then(res => { |
||||||
|
if(page.num == 1) this.listData = []; //如果是第一页需手动制空列表 |
||||||
|
res.data.list.map(e =>{ |
||||||
|
e.statusVal = this.core.statusVal(e.auditStatus) |
||||||
|
e.businessStatusVal = this.core.businessType(e.businessStatus) |
||||||
|
e.operationType = this.core.operationType(e.operationStatus) |
||||||
|
}) |
||||||
|
this.listData = this.listData.concat(res.data.list); //追加新数据 |
||||||
|
this.total = res.data.totalCount |
||||||
|
this.mescroll.endSuccess(res.data.list); // 隐藏加载状态栏 |
||||||
|
}).catch(()=>{ |
||||||
|
//联网失败, 结束加载 |
||||||
|
this.mescroll.endErr(); |
||||||
|
}) |
||||||
|
}, |
||||||
|
// 搜索框--结合防抖 |
||||||
|
searchList(e){ |
||||||
|
let that = this |
||||||
|
if (this.timer) { |
||||||
|
clearTimeout(this.timer); |
||||||
|
this.timer = null; |
||||||
|
} |
||||||
|
this.timer = setTimeout(function () { |
||||||
|
that.upCallback({num:1,size:10,search:e.detail.value}) |
||||||
|
},800) |
||||||
|
}, |
||||||
|
// 设置nav到顶部的距离 (滚动条为0, 菜单顶部的数据加载完毕获取到的navTop数值是最精确的) |
||||||
|
setNavTop(){ |
||||||
|
let view = uni.createSelectorQuery().select('#tabInList'); |
||||||
|
view.boundingClientRect(data => { |
||||||
|
this.navTop = data.top // 到屏幕顶部的距离 |
||||||
|
}).exec(); |
||||||
|
}, |
||||||
|
// mescroll-uni的滚动事件 (需在up配置onScroll:true才生效) |
||||||
|
// 而mescroll-body最简单只需在onPageScroll处理即可 |
||||||
|
scroll(){ |
||||||
|
// 菜单悬浮的原理: 监听滚动条的位置大于某个值时,控制顶部菜单的显示和隐藏 |
||||||
|
if (this.mescroll.getScrollTop() >= this.navTop) { |
||||||
|
this.isShowSticky = true // 显示悬浮菜单 |
||||||
|
} else { |
||||||
|
this.isShowSticky = false // 隐藏悬浮菜单 |
||||||
|
} |
||||||
|
}, |
||||||
|
// 点击回到顶部按钮时,先隐藏悬浮菜单,避免闪动 |
||||||
|
topClick(){ |
||||||
|
this.isShowSticky = false |
||||||
|
}, |
||||||
|
// 切换菜单 |
||||||
|
tabChange (index) { |
||||||
|
if(index==0){ |
||||||
|
this.status = '' |
||||||
|
}else{ |
||||||
|
this.status = `${index-1}` |
||||||
|
} |
||||||
|
this.upCallback({ |
||||||
|
num:1,size:10,search:'' |
||||||
|
}) |
||||||
|
// 记录前一个菜单的数据 |
||||||
|
let preTab = this.tabs[this.preIndex] |
||||||
|
preTab.y = this.mescroll.getScrollTop(); // 滚动条位置 |
||||||
|
this.preIndex = index; |
||||||
|
// 当前菜单的数据 |
||||||
|
let curTab = this.tabs[index] |
||||||
|
if (!curTab.goods) { |
||||||
|
// 没有初始化,则初始化 |
||||||
|
this.isChangeTab = true; |
||||||
|
this.mescroll.resetUpScroll() |
||||||
|
} else{ |
||||||
|
// 初始化过,则恢复之前的列表数据 |
||||||
|
this.mescroll.setPageNum(curTab.num + 1); // 恢复当前页码 |
||||||
|
this.mescroll.endSuccess(curTab.curPageLen, curTab.hasNext); // 恢复是否有下一页或显示空布局 |
||||||
|
this.$nextTick(()=>{ |
||||||
|
this.mescroll.scrollTo(curTab.y, 0) // 恢复滚动条的位置 |
||||||
|
}) |
||||||
|
} |
||||||
|
}, |
||||||
|
// ListTouch触摸开始 |
||||||
|
ListTouchStart(e) { |
||||||
|
this.listTouchStart = e.touches[0].pageX |
||||||
|
}, |
||||||
|
|
||||||
|
// ListTouch计算方向 |
||||||
|
ListTouchMove(e) { |
||||||
|
this.listTouchDirection = e.touches[0].pageX - this.listTouchStart > 0 ? 'right' : 'left' |
||||||
|
}, |
||||||
|
|
||||||
|
// ListTouch计算滚动 |
||||||
|
ListTouchEnd(e) { |
||||||
|
if (this.listTouchDirection == 'left') { |
||||||
|
this.modalName = e.currentTarget.dataset.target |
||||||
|
} else { |
||||||
|
this.modalName = null |
||||||
|
} |
||||||
|
this.listTouchDirection = null |
||||||
|
}, |
||||||
|
// // 列表数据 |
||||||
|
// getList(val){ |
||||||
|
// if(val==='search'){ |
||||||
|
// this.listParams.page = 1 |
||||||
|
// this.listParams.size = 10 |
||||||
|
// } |
||||||
|
// this.$http.get('/api-guarantee/compliance/investigation/list',this.listParams).then(res=>{ |
||||||
|
// if(res.data&&res.data.list&&res.data.list.length!==0){ |
||||||
|
// res.data.list.map(e =>{ |
||||||
|
// e.statusVal = this.core.statusVal(e.status) |
||||||
|
// e.businessStatusVal = this.core.businessType(e.businessStatus) |
||||||
|
// e.operationType = this.core.operationType(e.operatingStatus) |
||||||
|
// }) |
||||||
|
// this.listData = res.data.list |
||||||
|
// }else{ |
||||||
|
// this.listData = [] |
||||||
|
// } |
||||||
|
// }) |
||||||
|
// }, |
||||||
|
// 传递当前操作状态和值 |
||||||
|
handleGe(item,str,assign){ |
||||||
|
if(assign){// 如果是指派-进入指派页面 |
||||||
|
this.goto('/guaranteePages/pages/assignRegulation/assignRegulation')// 跳转指派 |
||||||
|
uni.removeStorageSync('handleGe') |
||||||
|
}else{ |
||||||
|
uni.setStorageSync('handleGe',str)// 操作状态记录 |
||||||
|
} |
||||||
|
if(!assign&&str!=='watch'){// 非指派和查看的情况下 |
||||||
|
this.goto('/guaranteePages/pages/regulationSee/regulationSee') |
||||||
|
} |
||||||
|
uni.setStorageSync('applyMsg',JSON.stringify(item))// 所有的值存起来--业务逻辑应用需要的值不同懒得一个个传 |
||||||
|
uni.setStorageSync('businessId',item.businessId)// 储存有业务id |
||||||
|
}, |
||||||
|
}, |
||||||
|
// 使用mescroll-body最简单只需在onPageScroll处理即可 |
||||||
|
onPageScroll(e){ |
||||||
|
if (e.scrollTop >= this.navTop) { |
||||||
|
this.isShowSticky = true // 显示悬浮菜单 |
||||||
|
} else { |
||||||
|
this.isShowSticky = false // 隐藏悬浮菜单 |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.cu-bar .search-form{ |
||||||
|
border: 2rpx solid #00B9FF; |
||||||
|
border-radius: 20rpx; |
||||||
|
background-color: #fff; |
||||||
|
margin: 0 50rpx; |
||||||
|
height: 72rpx; |
||||||
|
line-height: 72rpx; |
||||||
|
margin-top: 20rpx; |
||||||
|
input{ |
||||||
|
height: 72rpx; |
||||||
|
line-height: 72rpx; |
||||||
|
padding: 0 40rpx; |
||||||
|
} |
||||||
|
.cuIcon-search{ |
||||||
|
margin: 0 20rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
.add-customer{ |
||||||
|
padding: 0 50rpx; |
||||||
|
button{ |
||||||
|
background-color: #00B9FF; |
||||||
|
border-radius: 30rpx; |
||||||
|
.mar-icon{ |
||||||
|
margin-right: 10rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
.sideBtn{ |
||||||
|
display: flex; |
||||||
|
flex-wrap: wrap; |
||||||
|
justify-content: space-around; |
||||||
|
} |
||||||
|
</style> |
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue