Commit 4a3a32b8 authored by beiqiao's avatar beiqiao

语法提示,文档说明,进一步更新。

parent da09bd81
......@@ -32,7 +32,7 @@
* @property {String Number} speed 水平滚动时的滚动速度,即每秒移动多少距离,只对水平衔接方式有效,单位rpx(默认160)
* @property {String Number} font-size 字体大小,单位rpx(默认28)
* @property {Boolean} is-circular mode为horizontal时,指明是否水平衔接滚动(默认true)
* @property {String} play-state 播放状态,paly - 播放,paused - 暂停(默认paly)
* @property {String} play-state 播放状态,play - 播放,paused - 暂停(默认play)
* @property {Boolean} disable-touch 是否禁止通过手动滑动切换通知,只有mode = vertical,或者mode = horizontal且is-circular = false时有效(默认true)
* @event {Function} click 点击通告文字触发,只有mode = vertical,或者mode = horizontal且is-circular = false时有效
* @event {Function} close 点击右侧关闭图标触发
......@@ -116,7 +116,7 @@
type: Boolean,
default: true
},
// 播放状态,paly-播放,paused-暂停
// 播放状态,play-播放,paused-暂停
playState: {
type: String,
default: 'play'
......
......@@ -40,16 +40,16 @@
</template>
<script>
/**
* alertTips 提示
* @description 骨架屏一般用于页面在请求远程数据尚未完成时,页面用灰色块预显示本来的页面结构,给用户更好的体验。
* @tutorial https://www.uviewui.com/components/skeleton.html
* @property {String} el-color 骨架块状元素的背景颜色(默认#e5e5e5)
* @property {String} bg-color 骨架组件背景颜色(默认#ffffff)
* @property {Boolean} animation 骨架块是否显示动画效果(默认false)
* @property {String Number} border-radius u-skeleton-fillet类名元素,对应的骨架块的圆角大小,单位rpx(默认10)
* @property {Boolean} loading 是否显示骨架组件,请求完成后,将此值设置为false(默认true)
* @example <u-skeleton :loading="true" :animation="true"></u-skeleton>
/**
* alertTips 提示
* @description 骨架屏一般用于页面在请求远程数据尚未完成时,页面用灰色块预显示本来的页面结构,给用户更好的体验。
* @tutorial https://www.uviewui.com/components/skeleton.html
* @property {String} el-color 骨架块状元素的背景颜色(默认#e5e5e5)
* @property {String} bg-color 骨架组件背景颜色(默认#ffffff)
* @property {Boolean} animation 骨架块是否显示动画效果(默认false)
* @property {String Number} border-radius u-skeleton-fillet类名元素,对应的骨架块的圆角大小,单位rpx(默认10)
* @property {Boolean} loading 是否显示骨架组件,请求完成后,将此值设置为false(默认true)
* @example <u-skeleton :loading="true" :animation="true"></u-skeleton>
*/
export default {
name: "u-skeleton",
......
<template>
<view class="">
<view class="u-steps">
<view class="u-steps-item" v-for="(item,index) in list" :key="index">
<view class="u-steps-item-num" v-if="mode == 'number' && current < index">{{index+1}}</view>
<view class="u-steps-item-dot" v-if="mode == 'dot'" :style="{backgroundColor: index <= current ? innerActiveColor : unActiveColor}"></view>
<u-icon size="22" class="u-steps-item-checked" :style="{backgroundColor: index <= current ? innerActiveColor : unActiveColor}"
v-if="mode == 'number' && current >= index" name="checkmark"></u-icon>
<text :style="{color: index <= current ? innerActiveColor : unActiveColor}">{{item.name}}</text>
<view class="u-steps-item-line" :style="{backgroundColor: index <= current ? innerActiveColor : unActiveColor, top: mode == 'dot' ? '24rpx' : '36rpx'}">
</view>
</view>
</view>
</view>
</template>
<script>
export default {
props: {
// 步骤条的类型,dot|number
mode: {
type: String,
default: 'dot'
},
// 步骤条的数据
list: {
type: Array,
default () {
return []
}
},
// 主题类型, primary|success|info|warning|error
type: {
type: String,
default: 'primary'
},
// 当前哪一步是激活的
current: {
type: [Number, String],
default: 0
},
// 激活步骤的颜色
activeColor: {
type: String,
default: ''
},
// 未激活的颜色
unActiveColor: {
type: String,
default: '#606266'
}
},
data() {
return {
}
},
computed: {
innerActiveColor() {
if (this.activeColor) return this.activeColor;
else if (this.type) return this.$u.color[this.type];
else return "#2979ff";
},
}
}
</script>
<style lang="scss" scoped>
.u-steps {
display: flex;
}
.u-steps-item {
flex: 1;
text-align: center;
position: relative;
min-width: 100rpx;
font-size: 26rpx;
color: #8799a3;
}
.u-steps-item .u-steps-item-line {
content: "";
position: absolute;
height: 2rpx;
width: calc(100% - 80rpx);
left: calc(0rpx - (100% - 80rpx) / 2);
top: 36rpx;
z-index: 0;
}
.u-steps-item:first-child .u-steps-item-line {
display: none;
}
.u-steps-item-num {
display: flex;
align-items: center;
justify-content: center;
width: 44rpx;
height: 44rpx;
border: 1px solid #8799a3;
border-radius: 50%;
margin: 14rpx auto;
overflow: hidden;
}
.u-steps-item-dot {
width: 20rpx;
height: 20rpx;
display: flex;
border-radius: 50%;
margin: 14rpx auto;
}
.u-steps-item-checked {
display: flex;
align-items: center;
justify-content: center;
width: 44rpx;
color: #fff !important;
height: 44rpx;
border-radius: 50%;
margin: 14rpx auto;
overflow: hidden;
}
<template>
<view class="">
<view class="u-steps">
<view class="u-steps-item" v-for="(item,index) in list" :key="index">
<view class="u-steps-item-num" v-if="mode == 'number' && current < index">{{index+1}}</view>
<view class="u-steps-item-dot" v-if="mode == 'dot'" :style="{backgroundColor: index <= current ? innerActiveColor : unActiveColor}"></view>
<u-icon size="22" class="u-steps-item-checked" :style="{backgroundColor: index <= current ? innerActiveColor : unActiveColor}"
v-if="mode == 'number' && current >= index" name="checkmark"></u-icon>
<text :style="{color: index <= current ? innerActiveColor : unActiveColor}">{{item.name}}</text>
<view class="u-steps-item-line" :style="{backgroundColor: index <= current ? innerActiveColor : unActiveColor, top: mode == 'dot' ? '24rpx' : '36rpx'}">
</view>
</view>
</view>
</view>
</template>
<script>
/**
* alertTips 提示
* @description 该组件一般用于完成一个任务要分几个步骤,标识目前处于第几步的场景。
* @tutorial https://www.uviewui.com/components/steps.html
* @property {String} mode 设置模式(默认dot)
* @property {Array} list 数轴条数据,数组。具体见上方示例
* @property {String} type type主题(默认primary)
* @property {Number String} current 设置当前处于第几步
* @property {String} active-color 已完成步骤的激活颜色,如设置,type值会失效
* @property {String} un-active-color 未激活的颜色,用于表示未完成步骤的颜色(默认#606266)
* @example <u-steps :list="numList" active-color="#fa3534"></u-steps>
*/
export default {
name: "u-steps",
props: {
// 步骤条的类型,dot|number
mode: {
type: String,
default: 'dot'
},
// 步骤条的数据
list: {
type: Array,
default () {
return []
}
},
// 主题类型, primary|success|info|warning|error
type: {
type: String,
default: 'primary'
},
// 当前哪一步是激活的
current: {
type: [Number, String],
default: 0
},
// 激活步骤的颜色
activeColor: {
type: String,
default: ''
},
// 未激活的颜色
unActiveColor: {
type: String,
default: '#606266'
}
},
data() {
return {
}
},
computed: {
innerActiveColor() {
if (this.activeColor) return this.activeColor;
else if (this.type) return this.$u.color[this.type];
else return "#2979ff";
},
}
}
</script>
<style lang="scss" scoped>
.u-steps {
display: flex;
}
.u-steps-item {
flex: 1;
text-align: center;
position: relative;
min-width: 100rpx;
font-size: 26rpx;
color: #8799a3;
}
.u-steps-item .u-steps-item-line {
content: "";
position: absolute;
height: 2rpx;
width: calc(100% - 80rpx);
left: calc(0rpx - (100% - 80rpx) / 2);
top: 36rpx;
z-index: 0;
}
.u-steps-item:first-child .u-steps-item-line {
display: none;
}
.u-steps-item-num {
display: flex;
align-items: center;
justify-content: center;
width: 44rpx;
height: 44rpx;
border: 1px solid #8799a3;
border-radius: 50%;
margin: 14rpx auto;
overflow: hidden;
}
.u-steps-item-dot {
width: 20rpx;
height: 20rpx;
display: flex;
border-radius: 50%;
margin: 14rpx auto;
}
.u-steps-item-checked {
display: flex;
align-items: center;
justify-content: center;
width: 44rpx;
color: #fff !important;
height: 44rpx;
border-radius: 50%;
margin: 14rpx auto;
overflow: hidden;
}
</style>
<template>
<view class="">
<view class="u-sticky-wrap" :class="[elClass]" :style="{
height: fixed ? height + 'px' : 'auto',
backgroundColor: bgColor
}">
<view class="u-sticky" :style="{
position: fixed ? 'fixed' : 'static',
top: stickyTop + 'px',
left: left + 'px',
width: width == 'auto' ? 'auto' : width + 'px',
zIndex: uZIndex
}">
<slot></slot>
</view>
</view>
</view>
</template>
<script>
export default {
props: {
// 吸顶容器到顶部某个距离的时候,进行吸顶,在H5平台,NavigationBar为44px
offsetTop: {
type: [Number, String],
default: 0
},
//列表中的索引值
index: {
type: [Number, String],
default: ''
},
// 是否开启吸顶功能
enable: {
type: Boolean,
default: true
},
// h5顶部导航栏的高度
h5NavHeight: {
type: [Number, String],
default: 44
},
// 吸顶区域的背景颜色
bgColor: {
type: String,
default: '#ffffff'
},
// z-index值
zIndex: {
type: [Number, String],
default: ''
}
},
data() {
return {
fixed: false,
height: 'auto',
stickyTop: 0,
elClass: this.$u.guid(),
left: 0,
width: 'auto',
};
},
watch: {
offsetTop(val) {
this.initObserver();
},
enable(val) {
if(val == false) {
this.fixed = false;
this.disconnectObserver('contentObserver');
} else {
this.initObserver();
}
}
},
computed: {
uZIndex() {
return this.zIndex ? this.zIndex : this.$u.zIndex.sticky;
}
},
mounted() {
this.initObserver();
},
methods: {
initObserver() {
if(!this.enable) return ;
// #ifdef H5
this.stickyTop = this.offsetTop != 0 ? uni.upx2px(this.offsetTop) + this.h5NavHeight : this.h5NavHeight;
// #endif
// #ifndef H5
this.stickyTop = this.offsetTop != 0 ? uni.upx2px(this.offsetTop) : 0;
// #endif
this.disconnectObserver('contentObserver');
this.$uGetRect('.' + this.elClass).then((res) => {
this.height = res.height;
this.left = res.left;
this.width = res.width;
this.$nextTick(() => {
this.observeContent();
});
});
},
observeContent() {
this.disconnectObserver('contentObserver');
const contentObserver = this.createIntersectionObserver({
thresholds: [0.95, 0.98, 1]
});
contentObserver.relativeToViewport({
top: -this.stickyTop
});
contentObserver.observe('.' + this.elClass, res => {
if (!this.enable) return ;
this.setFixed(res.boundingClientRect.top);
});
this.contentObserver = contentObserver;
},
setFixed(top) {
const fixed = top < this.stickyTop;
this.fixed = fixed;
if(fixed) this.$emit('fixed', this.index);
},
disconnectObserver(observerName) {
const observer = this[observerName];
observer && observer.disconnect();
},
}
};
</script>
<style scoped lang="scss">
.u-sticky {
z-index: 9999999999;
}
</style>
\ No newline at end of file
<template>
<view class="">
<view class="u-sticky-wrap" :class="[elClass]" :style="{
height: fixed ? height + 'px' : 'auto',
backgroundColor: bgColor
}">
<view class="u-sticky" :style="{
position: fixed ? 'fixed' : 'static',
top: stickyTop + 'px',
left: left + 'px',
width: width == 'auto' ? 'auto' : width + 'px',
zIndex: uZIndex
}">
<slot></slot>
</view>
</view>
</view>
</template>
<script>
/**
* alertTips 提示
* @description 该组件与CSS中position: sticky属性实现的效果一致,当组件达到预设的到顶部距离时, 就会固定在指定位置,组件位置大于预设的顶部距离时,会重新按照正常的布局排列。
* @tutorial https://www.uviewui.com/components/sticky.html
* @property {String Number} offset-top 吸顶时与顶部的距离,单位rpx(默认0)
* @property {String Number} index 自定义标识,用于区分是哪一个组件
* @property {Boolean} enable 是否开启吸顶功能(默认true)
* @property {String} bg-color 组件背景颜色(默认#ffffff)
* @property {String Number} z-index 吸顶时的z-index值(默认970)
* @property {String Number} h5-nav-height 导航栏高度,自定义导航栏时(无导航栏时需设置为0),需要传入此值,单位px(默认44)
* @event {Function} fixed 组件吸顶时触发
* @example <u-sticky offset-top="200"><view>塞下秋来风景异,衡阳雁去无留意</view></u-sticky>
*/
export default {
name: "u-sticky",
props: {
// 吸顶容器到顶部某个距离的时候,进行吸顶,在H5平台,NavigationBar为44px
offsetTop: {
type: [Number, String],
default: 0
},
//列表中的索引值
index: {
type: [Number, String],
default: ''
},
// 是否开启吸顶功能
enable: {
type: Boolean,
default: true
},
// h5顶部导航栏的高度
h5NavHeight: {
type: [Number, String],
default: 44
},
// 吸顶区域的背景颜色
bgColor: {
type: String,
default: '#ffffff'
},
// z-index值
zIndex: {
type: [Number, String],
default: ''
}
},
data() {
return {
fixed: false,
height: 'auto',
stickyTop: 0,
elClass: this.$u.guid(),
left: 0,
width: 'auto',
};
},
watch: {
offsetTop(val) {
this.initObserver();
},
enable(val) {
if (val == false) {
this.fixed = false;
this.disconnectObserver('contentObserver');
} else {
this.initObserver();
}
}
},
computed: {
uZIndex() {
return this.zIndex ? this.zIndex : this.$u.zIndex.sticky;
}
},
mounted() {
this.initObserver();
},
methods: {
initObserver() {
if (!this.enable) return;
// #ifdef H5
this.stickyTop = this.offsetTop != 0 ? uni.upx2px(this.offsetTop) + this.h5NavHeight : this.h5NavHeight;
// #endif
// #ifndef H5
this.stickyTop = this.offsetTop != 0 ? uni.upx2px(this.offsetTop) : 0;
// #endif
this.disconnectObserver('contentObserver');
this.$uGetRect('.' + this.elClass).then((res) => {
this.height = res.height;
this.left = res.left;
this.width = res.width;
this.$nextTick(() => {
this.observeContent();
});
});
},
observeContent() {
this.disconnectObserver('contentObserver');
const contentObserver = this.createIntersectionObserver({
thresholds: [0.95, 0.98, 1]
});
contentObserver.relativeToViewport({
top: -this.stickyTop
});
contentObserver.observe('.' + this.elClass, res => {
if (!this.enable) return;
this.setFixed(res.boundingClientRect.top);
});
this.contentObserver = contentObserver;
},
setFixed(top) {
const fixed = top < this.stickyTop;
this.fixed = fixed;
if (fixed) this.$emit('fixed', this.index);
},
disconnectObserver(observerName) {
const observer = this[observerName];
observer && observer.disconnect();
},
}
};
</script>
<style scoped lang="scss">
.u-sticky {
z-index: 9999999999;
}
</style>
<template>
<view class="u-subsection" :style="[subsectionStyle]">
<view
class="u-item u-line-1"
:style="[itemStyle(index)]"
@tap="click(index)"
:class="[noBorderRight(index), 'u-item-' + index]"
v-for="(item, index) in listInfo"
:key="index"
>
<view :style="[textStyle(index)]" class="u-item-text u-line-1">{{ item.name }}</view>
</view>
<view class="u-item-bg" :style="[itemBarStyle]"></view>
</view>
</template>
<script>
export default {
props: {
// tab的数据
list: {
type: Array,
default() {
return [];
}
},
// 当前活动的tab的index
current: {
type: [Number, String],
default: 0
},
// 激活的颜色
activeColor: {
type: String,
default: '#303133'
},
// 模式选择,mode=button为按钮形式,mode=subsection时为分段模式
mode: {
type: String,
default: 'button'
},
// 字体大小,单位rpx
fontSize: {
type: [Number, String],
default: 28
},
// 是否开启动画效果
animation: {
type: Boolean,
default: true
},
// 组件的高度,单位rpx
height: {
type: [Number, String],
default: 70
},
// 激活tab的字体是否加粗
bold: {
type: Boolean,
default: true
},
// mode=button时,组件背景颜色
bgColor: {
type: String,
default: '#eeeeef'
},
// mode = button时,滑块背景颜色
buttonColor: {
type: String,
default: '#ffffff'
},
// 在切换分段器的时候,是否让设备震一下
vibrateShort: {
type: Boolean,
default: false
}
},
data() {
return {
listInfo: [],
itemBgStyle: {
width: 0,
left: 0,
backgroundColor: '#ffffff',
height: '100%',
transition: ''
},
currentIndex: this.current,
buttonPadding: 3, // mode = button 时,组件的内边距
borderRadius: 5, // 圆角值
firstTimeVibrateShort: true // 组件初始化时,会触发current变化,此时不应震动
};
},
watch: {
current: {
immediate: true,
handler(nVal) {
this.currentIndex = nVal;
this.changeSectionStatus(nVal);
}
}
},
created() {
// 将list的数据,传入listInfo数组,因为不能修改props传递的list值
// 可以接受直接数组形式,或者数组元素为对象的形式,如:['简介', '评论'],或者[{name: '简介'}, {name: '评论'}]
this.listInfo = this.list.map((val, index) => {
if (typeof val != 'object') {
let obj = {
width: 0,
name: val
};
return obj;
} else {
val.width = 0;
return val;
}
});
},
computed: {
// 设置mode=subsection时,滑块特有的样式
noBorderRight() {
return index => {
if (this.mode != 'subsection') return;
let classs = '';
// 不显示右边的边框
if (index < this.list.length - 1) classs += ' u-none-border-right';
// 显示整个组件的左右边圆角
if (index == 0) classs += ' u-item-first';
if (index == this.list.length - 1) classs += ' u-item-last';
return classs;
};
},
// 文字的样式
textStyle() {
return index => {
let style = {};
// 设置字体颜色
if (this.mode == 'subsection') {
if (index == this.currentIndex) {
style.color = '#ffffff';
} else {
style.color = this.activeColor;
}
} else {
if (index == this.currentIndex) {
style.color = this.activeColor;
} else {
style.color = this.inactiveColor;
}
}
// 字体加粗
if (index == this.currentIndex && this.bold) style.fontWeight = 'bold';
// 文字大小
style.fontSize = this.fontSize + 'rpx';
return style;
};
},
// 每个分段器item的样式
itemStyle() {
return index => {
let style = {};
if (this.mode == 'subsection') {
// 设置border的样式
style.borderColor = this.activeColor;
style.borderWidth = '1px';
style.borderStyle = 'solid';
}
return style;
};
},
// mode=button时,外层view的样式
subsectionStyle() {
let style = {};
style.height = uni.upx2px(this.height) + 'px';
if (this.mode == 'button') {
style.backgroundColor = this.bgColor;
style.padding = `${this.buttonPadding}px`;
style.borderRadius = `${this.borderRadius}px`;
}
return style;
},
// 滑块的样式
itemBarStyle() {
let style = {};
style.backgroundColor = this.activeColor;
style.zIndex = 1;
if (this.mode == 'button') {
style.backgroundColor = this.buttonColor;
style.borderRadius = `${this.borderRadius}px`;
style.bottom = `${this.buttonPadding}px`;
style.height = uni.upx2px(this.height) - this.buttonPadding * 2 + 'px';
style.zIndex = 0;
}
return Object.assign(this.itemBgStyle, style);
}
},
mounted() {
setTimeout(() => {
this.getTabsInfo();
}, 10);
},
methods: {
// 改变滑块的样式
changeSectionStatus(nVal) {
if (this.mode == 'subsection') {
// 根据滑块在最左边和最右边时,显示左边和右边的圆角
if (nVal == this.list.length - 1) {
this.itemBgStyle.borderRadius = `0 ${this.buttonPadding}px ${this.buttonPadding}px 0`;
}
if (nVal == 0) {
this.itemBgStyle.borderRadius = `${this.buttonPadding}px 0 0 ${this.buttonPadding}px`;
}
if (nVal > 0 && nVal < this.list.length - 1) {
this.itemBgStyle.borderRadius = '0';
}
}
// 更新滑块的位置
setTimeout(() => {
this.itemBgLeft();
}, 10);
if (this.vibrateShort && !this.firstTimeVibrateShort) {
// 使手机产生短促震动,微信小程序有效,APP(HX 2.6.8)和H5无效
// #ifndef H5
uni.vibrateShort();
// #endif
}
// 第一次过后,设置firstTimeVibrateShort为false,让其下一次可以震动(如果允许震动的话)
this.firstTimeVibrateShort = false;
},
click(index) {
// 不允许点击当前激活选项
if (index == this.currentIndex) return;
this.currentIndex = index;
this.changeSectionStatus(index);
this.$emit('change', Number(index));
},
// 获取各个tab的节点信息
getTabsInfo() {
let view = uni.createSelectorQuery().in(this);
for (let i = 0; i < this.list.length; i++) {
view.select('.u-item-' + i).boundingClientRect();
}
view.exec(res => {
if (!res.length) {
setTimeout(() => {
this.getTabsInfo();
return;
}, 10);
}
// 将分段器每个item的宽度,放入listInfo数组
res.map((val, index) => {
this.listInfo[index].width = val.width;
});
// 初始化滑块的宽度
if (this.mode == 'subsection') {
this.itemBgStyle.width = this.listInfo[0].width + 'px';
} else if (this.mode == 'button') {
this.itemBgStyle.width = this.listInfo[0].width + 'px';
}
// 初始化滑块的位置
this.itemBgLeft();
});
},
itemBgLeft() {
// 根据是否开启动画效果,
if (this.animation) {
this.itemBgStyle.transition = 'all 0.35s';
} else {
this.itemBgStyle.transition = 'all 0s';
}
let left = 0;
// 计算当前活跃item到组件左边的距离
this.listInfo.map((val, index) => {
if (index < this.currentIndex) left += val.width;
});
// 根据mode不同模式,计算滑块需要移动的距离
if (this.mode == 'subsection') {
this.itemBgStyle.left = left + 'px';
} else if (this.mode == 'button') {
this.itemBgStyle.left = left + this.buttonPadding + 'px';
}
}
}
};
</script>
<style lang="scss" scoped>
.u-subsection {
display: flex;
align-items: center;
overflow: hidden;
position: relative;
}
.u-item {
flex: 1;
text-align: center;
font-size: 26rpx;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: $u-main-color;
display: inline-flex;
padding: 0 6rpx;
}
.u-item-bg {
background-color: $u-type-primary;
position: absolute;
z-index: -1;
}
.u-none-border-right {
border-right: none !important;
}
.u-item-first {
border-top-left-radius: 8rpx;
border-bottom-left-radius: 8rpx;
}
.u-item-last {
border-top-right-radius: 8rpx;
border-bottom-right-radius: 8rpx;
}
.u-item-text {
transition: all 0.35s;
color: $u-main-color;
display: flex;
align-items: center;
position: relative;
z-index: 99;
}
<template>
<view class="u-subsection" :style="[subsectionStyle]">
<view class="u-item u-line-1" :style="[itemStyle(index)]" @tap="click(index)" :class="[noBorderRight(index), 'u-item-' + index]"
v-for="(item, index) in listInfo" :key="index">
<view :style="[textStyle(index)]" class="u-item-text u-line-1">{{ item.name }}</view>
</view>
<view class="u-item-bg" :style="[itemBarStyle]"></view>
</view>
</template>
<script>
/**
* alertTips 提示
* @description 该分段器一般用于用户从几个选项中选择某一个的场景
* @tutorial https://www.uviewui.com/components/subsection.html
* @property {Array} list 选项的数组,形式见上方"基本使用"
* @property {String Number} current 初始化时默认选中的选项索引值(默认0)
* @property {String} active-color 激活时的颜色,mode为subsection时固定为白色(默认#ff9900)
* @property {String} inactive-color 未激活时字体的颜色,mode为subsection时无效(默认#303133)
* @property {String} mode 模式选择,见官网"模式选择"说明(默认button)
* @property {String Number} font-size 字体大小,单位rpx(默认28)
* @property {Boolean} animation 是否开启动画效果,见上方说明(默认true)
* @property {Boolean} bold 激活选项的字体是否加粗(默认true)
* @property {String} bg-color 组件背景颜色,mode为button时有效(默认#eeeeef)
* @property {String} button-color 按钮背景颜色,mode为button时有效(默认#ffffff)
* @event {Function} change 分段器选项发生改变时触发
* @example <u-subsection active-color="#ff9900"></u-subsection>
*/
export default {
name: "u-subsection",
props: {
// tab的数据
list: {
type: Array,
default () {
return [];
}
},
// 当前活动的tab的index
current: {
type: [Number, String],
default: 0
},
// 激活的颜色
activeColor: {
type: String,
default: '#303133'
},
// 模式选择,mode=button为按钮形式,mode=subsection时为分段模式
mode: {
type: String,
default: 'button'
},
// 字体大小,单位rpx
fontSize: {
type: [Number, String],
default: 28
},
// 是否开启动画效果
animation: {
type: Boolean,
default: true
},
// 组件的高度,单位rpx
height: {
type: [Number, String],
default: 70
},
// 激活tab的字体是否加粗
bold: {
type: Boolean,
default: true
},
// mode=button时,组件背景颜色
bgColor: {
type: String,
default: '#eeeeef'
},
// mode = button时,滑块背景颜色
buttonColor: {
type: String,
default: '#ffffff'
},
// 在切换分段器的时候,是否让设备震一下
vibrateShort: {
type: Boolean,
default: false
}
},
data() {
return {
listInfo: [],
itemBgStyle: {
width: 0,
left: 0,
backgroundColor: '#ffffff',
height: '100%',
transition: ''
},
currentIndex: this.current,
buttonPadding: 3, // mode = button 时,组件的内边距
borderRadius: 5, // 圆角值
firstTimeVibrateShort: true // 组件初始化时,会触发current变化,此时不应震动
};
},
watch: {
current: {
immediate: true,
handler(nVal) {
this.currentIndex = nVal;
this.changeSectionStatus(nVal);
}
}
},
created() {
// 将list的数据,传入listInfo数组,因为不能修改props传递的list值
// 可以接受直接数组形式,或者数组元素为对象的形式,如:['简介', '评论'],或者[{name: '简介'}, {name: '评论'}]
this.listInfo = this.list.map((val, index) => {
if (typeof val != 'object') {
let obj = {
width: 0,
name: val
};
return obj;
} else {
val.width = 0;
return val;
}
});
},
computed: {
// 设置mode=subsection时,滑块特有的样式
noBorderRight() {
return index => {
if (this.mode != 'subsection') return;
let classs = '';
// 不显示右边的边框
if (index < this.list.length - 1) classs += ' u-none-border-right';
// 显示整个组件的左右边圆角
if (index == 0) classs += ' u-item-first';
if (index == this.list.length - 1) classs += ' u-item-last';
return classs;
};
},
// 文字的样式
textStyle() {
return index => {
let style = {};
// 设置字体颜色
if (this.mode == 'subsection') {
if (index == this.currentIndex) {
style.color = '#ffffff';
} else {
style.color = this.activeColor;
}
} else {
if (index == this.currentIndex) {
style.color = this.activeColor;
} else {
style.color = this.inactiveColor;
}
}
// 字体加粗
if (index == this.currentIndex && this.bold) style.fontWeight = 'bold';
// 文字大小
style.fontSize = this.fontSize + 'rpx';
return style;
};
},
// 每个分段器item的样式
itemStyle() {
return index => {
let style = {};
if (this.mode == 'subsection') {
// 设置border的样式
style.borderColor = this.activeColor;
style.borderWidth = '1px';
style.borderStyle = 'solid';
}
return style;
};
},
// mode=button时,外层view的样式
subsectionStyle() {
let style = {};
style.height = uni.upx2px(this.height) + 'px';
if (this.mode == 'button') {
style.backgroundColor = this.bgColor;
style.padding = `${this.buttonPadding}px`;
style.borderRadius = `${this.borderRadius}px`;
}
return style;
},
// 滑块的样式
itemBarStyle() {
let style = {};
style.backgroundColor = this.activeColor;
style.zIndex = 1;
if (this.mode == 'button') {
style.backgroundColor = this.buttonColor;
style.borderRadius = `${this.borderRadius}px`;
style.bottom = `${this.buttonPadding}px`;
style.height = uni.upx2px(this.height) - this.buttonPadding * 2 + 'px';
style.zIndex = 0;
}
return Object.assign(this.itemBgStyle, style);
}
},
mounted() {
setTimeout(() => {
this.getTabsInfo();
}, 10);
},
methods: {
// 改变滑块的样式
changeSectionStatus(nVal) {
if (this.mode == 'subsection') {
// 根据滑块在最左边和最右边时,显示左边和右边的圆角
if (nVal == this.list.length - 1) {
this.itemBgStyle.borderRadius = `0 ${this.buttonPadding}px ${this.buttonPadding}px 0`;
}
if (nVal == 0) {
this.itemBgStyle.borderRadius = `${this.buttonPadding}px 0 0 ${this.buttonPadding}px`;
}
if (nVal > 0 && nVal < this.list.length - 1) {
this.itemBgStyle.borderRadius = '0';
}
}
// 更新滑块的位置
setTimeout(() => {
this.itemBgLeft();
}, 10);
if (this.vibrateShort && !this.firstTimeVibrateShort) {
// 使手机产生短促震动,微信小程序有效,APP(HX 2.6.8)和H5无效
// #ifndef H5
uni.vibrateShort();
// #endif
}
// 第一次过后,设置firstTimeVibrateShort为false,让其下一次可以震动(如果允许震动的话)
this.firstTimeVibrateShort = false;
},
click(index) {
// 不允许点击当前激活选项
if (index == this.currentIndex) return;
this.currentIndex = index;
this.changeSectionStatus(index);
this.$emit('change', Number(index));
},
// 获取各个tab的节点信息
getTabsInfo() {
let view = uni.createSelectorQuery().in(this);
for (let i = 0; i < this.list.length; i++) {
view.select('.u-item-' + i).boundingClientRect();
}
view.exec(res => {
if (!res.length) {
setTimeout(() => {
this.getTabsInfo();
return;
}, 10);
}
// 将分段器每个item的宽度,放入listInfo数组
res.map((val, index) => {
this.listInfo[index].width = val.width;
});
// 初始化滑块的宽度
if (this.mode == 'subsection') {
this.itemBgStyle.width = this.listInfo[0].width + 'px';
} else if (this.mode == 'button') {
this.itemBgStyle.width = this.listInfo[0].width + 'px';
}
// 初始化滑块的位置
this.itemBgLeft();
});
},
itemBgLeft() {
// 根据是否开启动画效果,
if (this.animation) {
this.itemBgStyle.transition = 'all 0.35s';
} else {
this.itemBgStyle.transition = 'all 0s';
}
let left = 0;
// 计算当前活跃item到组件左边的距离
this.listInfo.map((val, index) => {
if (index < this.currentIndex) left += val.width;
});
// 根据mode不同模式,计算滑块需要移动的距离
if (this.mode == 'subsection') {
this.itemBgStyle.left = left + 'px';
} else if (this.mode == 'button') {
this.itemBgStyle.left = left + this.buttonPadding + 'px';
}
}
}
};
</script>
<style lang="scss" scoped>
.u-subsection {
display: flex;
align-items: center;
overflow: hidden;
position: relative;
}
.u-item {
flex: 1;
text-align: center;
font-size: 26rpx;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: $u-main-color;
display: inline-flex;
padding: 0 6rpx;
}
.u-item-bg {
background-color: $u-type-primary;
position: absolute;
z-index: -1;
}
.u-none-border-right {
border-right: none !important;
}
.u-item-first {
border-top-left-radius: 8rpx;
border-bottom-left-radius: 8rpx;
}
.u-item-last {
border-top-right-radius: 8rpx;
border-bottom-right-radius: 8rpx;
}
.u-item-text {
transition: all 0.35s;
color: $u-main-color;
display: flex;
align-items: center;
position: relative;
z-index: 99;
}
</style>
<template>
<movable-area class="u-swipe-action" :style="{backgroundColor: bgColor}">
<movable-view
class="u-swipe-view"
@change="change"
@touchend="touchend"
@touchstart="touchstart"
direction="horizontal"
:disabled="disabled"
:x="moveX"
:style="{
width: movableViewWidth
}"
>
<view class="u-swipe-content"><slot></slot></view>
<view
class="u-swipe-del"
@tap.stop="del"
:style="{
width: innerBtnWidth + 'px',
backgroundColor: btnBgColor
}"
>
<view class="u-btn-text">{{ btnText }}</view>
</view>
</movable-view>
</movable-area>
</template>
<script>
export default {
props: {
// 左边滑动出来按钮的文字
btnText: {
type: String,
default: '删除'
},
// 滑动出来的按钮的背景颜色
btnBgColor: {
type: String,
default: '#ff0033'
},
// index值,用于得知点击删除的是哪个按钮
index: {
type: [Number, String],
default: ''
},
// 滑动按钮的宽度,单位为rpx
btnWidth: {
type: [String, Number],
default: 180
},
// 是否禁止某个action滑动
disabled: {
type: Boolean,
default: false
},
// 打开或着关闭组件
show: {
type: Boolean,
default: false
},
// 组件背景颜色
bgColor: {
type: String,
default: '#ffffff'
}
},
watch: {
show: {
immediate: true,
handler(nVal, oVal) {
if(nVal) {
this.open();
} else {
this.close();
}
}
}
},
data() {
return {
moveX: 0, // movable-view元素在x轴上需要移动的目标移动距离,用于展开或收起滑动的按钮
scrollX: 0, // movable-view移动过程中产生的change事件中的x轴移动值
status: false, // 滑动的状态,表示当前是展开还是关闭按钮的状态
movableAreaWidth: 0, // 滑动区域
elId: this.$u.guid(), // id,用于通知另外组件关闭时的识别
}
},
computed: {
movableViewWidth() {
return this.movableAreaWidth + this.innerBtnWidth + 'px';
},
innerBtnWidth() {
return uni.upx2px(this.btnWidth);
},
},
mounted() {
this.getActionRect();
},
methods: {
// 点击删除按钮
del() {
this.status = false
this.moveX = 0
this.$emit('click', this.index);
},
// movable-view元素移动事件
change(e) {
this.scrollX = e.detail.x;
},
// 关闭按钮状态
close() {
this.moveX = 0;
this.status = false;
},
// 打开按钮的状态
open() {
if(this.disabled) return ;
this.moveX = - this.btnWidth;
this.status = true;
},
// 用户手指离开movable-view元素,停止触摸
touchend() {
this.moveX = this.scrollX;
// 停止触摸时候,判断当前是展开还是关闭状态
// 关闭状态
// 这一步很重要,需要先给this.moveX一个变化的随机值,否则因为前后设置的为同一个值
// props单向数据流的原因,导致movable-view元素不会发生变化,切记,详见文档:
// https://uniapp.dcloud.io/use?id=%e5%b8%b8%e8%a7%81%e9%97%ae%e9%a2%98
this.$nextTick(function(){
if(this.status == false) {
// 关闭状态左滑,产生的x轴位移为负值,也就是说滑动的距离大于按钮的三分之一宽度,自动展开按钮
if(this.scrollX <= -this.innerBtnWidth / 3) {
this.moveX = -this.innerBtnWidth; // 按钮宽度的负值,即为展开状态movable-view元素左滑的距离
this.status = true; // 标志当前为展开状态
this.emitOpenEvent();
// 产生震动效果
uni.vibrateShort();
} else {
this.moveX = 0; // 如果距离没有按钮宽度的三分之一,自动收起
this.status = false;
this.emitCloseEvent();
}
} else {
// 如果在打开的状态下,右滑动的距离X轴偏移超过按钮的三分之一(负值反过来的三分之二),自动收起按钮
if(this.scrollX > -this.innerBtnWidth * 2 / 3) {
this.moveX = 0;
this.$nextTick(() => {
this.moveX = 101;
})
this.status = false;
this.emitCloseEvent();
} else {
this.moveX = -this.innerBtnWidth;
this.status = true;
this.emitOpenEvent();
}
}
})
},
emitOpenEvent() {
this.$emit('open', this.index);
},
emitCloseEvent() {
this.$emit('close', this.index);
},
// 开始触摸
touchstart() {
},
getActionRect() {
this.$uGetRect('.u-swipe-action').then(res => {
this.movableAreaWidth = res.width;
})
}
}
}
</script>
<style scoped lang="scss">
.u-swipe-action {
width: auto;
height: initial;
position: relative;
overflow: hidden;
}
.u-swipe-view {
display: flex;
height: initial;
position: relative; /* 这一句很关键,覆盖默认的绝对定位 */
}
.u-swipe-content {
flex: 1;
}
.u-swipe-del {
position: relative;
font-size: 30rpx;
color: #ffffff;
}
.u-btn-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
<template>
<movable-area class="u-swipe-action" :style="{backgroundColor: bgColor}">
<movable-view class="u-swipe-view" @change="change" @touchend="touchend" @touchstart="touchstart" direction="horizontal"
:disabled="disabled" :x="moveX" :style="{
width: movableViewWidth
}">
<view class="u-swipe-content">
<slot></slot>
</view>
<view class="u-swipe-del" @tap.stop="del" :style="{
width: innerBtnWidth + 'px',
backgroundColor: btnBgColor
}">
<view class="u-btn-text">{{ btnText }}</view>
</view>
</movable-view>
</movable-area>
</template>
<script>
/**
* alertTips 提示
* @description 该组件一般用于左滑唤出操作菜单的场景,用的最多的是左滑删除操作。
* @tutorial https://www.uviewui.com/components/swipeAction.html
* @property {String} btn-text 按钮文字(默认“删除”)
* @property {String} btn-bg-color 按钮背景颜色(默认#ff0033)
* @property {String} bg-color 整个组件背景颜色(默认#ffffff)
* @property {String Number} index 标识符,点击时候用于区分点击了哪一个,用v-for循环时的index即可
* @property {String Number} btn-width 按钮宽度,单位rpx(默认180)
* @property {Boolean} disabled 是否禁止某个swipeAction滑动(默认false)
* @event {Function} click 点击组件时触发
* @event {Function} close 组件触发关闭状态时
* @event {Function} open 组件触发打开状态时
* @example <u-swipe-action btn-text="收藏">...</u-swipe-action>
*/
export default {
name: "u-swipe-action",
props: {
// 左边滑动出来按钮的文字
btnText: {
type: String,
default: '删除'
},
// 滑动出来的按钮的背景颜色
btnBgColor: {
type: String,
default: '#ff0033'
},
// index值,用于得知点击删除的是哪个按钮
index: {
type: [Number, String],
default: ''
},
// 滑动按钮的宽度,单位为rpx
btnWidth: {
type: [String, Number],
default: 180
},
// 是否禁止某个action滑动
disabled: {
type: Boolean,
default: false
},
// 打开或着关闭组件
show: {
type: Boolean,
default: false
},
// 组件背景颜色
bgColor: {
type: String,
default: '#ffffff'
}
},
watch: {
show: {
immediate: true,
handler(nVal, oVal) {
if (nVal) {
this.open();
} else {
this.close();
}
}
}
},
data() {
return {
moveX: 0, // movable-view元素在x轴上需要移动的目标移动距离,用于展开或收起滑动的按钮
scrollX: 0, // movable-view移动过程中产生的change事件中的x轴移动值
status: false, // 滑动的状态,表示当前是展开还是关闭按钮的状态
movableAreaWidth: 0, // 滑动区域
elId: this.$u.guid(), // id,用于通知另外组件关闭时的识别
}
},
computed: {
movableViewWidth() {
return this.movableAreaWidth + this.innerBtnWidth + 'px';
},
innerBtnWidth() {
return uni.upx2px(this.btnWidth);
},
},
mounted() {
this.getActionRect();
},
methods: {
// 点击删除按钮
del() {
this.status = false
this.moveX = 0
this.$emit('click', this.index);
},
// movable-view元素移动事件
change(e) {
this.scrollX = e.detail.x;
},
// 关闭按钮状态
close() {
this.moveX = 0;
this.status = false;
},
// 打开按钮的状态
open() {
if (this.disabled) return;
this.moveX = -this.btnWidth;
this.status = true;
},
// 用户手指离开movable-view元素,停止触摸
touchend() {
this.moveX = this.scrollX;
// 停止触摸时候,判断当前是展开还是关闭状态
// 关闭状态
// 这一步很重要,需要先给this.moveX一个变化的随机值,否则因为前后设置的为同一个值
// props单向数据流的原因,导致movable-view元素不会发生变化,切记,详见文档:
// https://uniapp.dcloud.io/use?id=%e5%b8%b8%e8%a7%81%e9%97%ae%e9%a2%98
this.$nextTick(function() {
if (this.status == false) {
// 关闭状态左滑,产生的x轴位移为负值,也就是说滑动的距离大于按钮的三分之一宽度,自动展开按钮
if (this.scrollX <= -this.innerBtnWidth / 3) {
this.moveX = -this.innerBtnWidth; // 按钮宽度的负值,即为展开状态movable-view元素左滑的距离
this.status = true; // 标志当前为展开状态
this.emitOpenEvent();
// 产生震动效果
uni.vibrateShort();
} else {
this.moveX = 0; // 如果距离没有按钮宽度的三分之一,自动收起
this.status = false;
this.emitCloseEvent();
}
} else {
// 如果在打开的状态下,右滑动的距离X轴偏移超过按钮的三分之一(负值反过来的三分之二),自动收起按钮
if (this.scrollX > -this.innerBtnWidth * 2 / 3) {
this.moveX = 0;
this.$nextTick(() => {
this.moveX = 101;
})
this.status = false;
this.emitCloseEvent();
} else {
this.moveX = -this.innerBtnWidth;
this.status = true;
this.emitOpenEvent();
}
}
})
},
emitOpenEvent() {
this.$emit('open', this.index);
},
emitCloseEvent() {
this.$emit('close', this.index);
},
// 开始触摸
touchstart() {
},
getActionRect() {
this.$uGetRect('.u-swipe-action').then(res => {
this.movableAreaWidth = res.width;
})
}
}
}
</script>
<style scoped lang="scss">
.u-swipe-action {
width: auto;
height: initial;
position: relative;
overflow: hidden;
}
.u-swipe-view {
display: flex;
height: initial;
position: relative;
/* 这一句很关键,覆盖默认的绝对定位 */
}
.u-swipe-content {
flex: 1;
}
.u-swipe-del {
position: relative;
font-size: 30rpx;
color: #ffffff;
}
.u-btn-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
</style>
<template>
<view class="u-swiper-wrap"
:style="{
borderRadius: `${borderRadius}rpx`,
}">
<swiper
@change="change"
:current="current"
:interval="interval"
:circular="circular"
:duration="duration"
:autoplay="autoplay"
:previous-margin="effect3d ? effect3dPreviousMargin + 'rpx' : '0'"
:next-margin="effect3d ? effect3dPreviousMargin + 'rpx' : '0'"
:style="{
height: height + 'rpx'
}"
>
<swiper-item
class="u-swiper-item"
v-for="(item, index) in list"
:key="index"
@tap="listClick(index)"
>
<view
class="u-list-image-wrap"
:class="[current != index ? 'u-list-scale' : '']"
:style="{
borderRadius: `${borderRadius}rpx`,
transform: effect3d && current != index ? 'scaleY(0.9)' : 'scaleY(1)',
margin: effect3d && current != index ? '0 20rpx' : 0
}"
>
<image class="u-swiper-image" :src="item[name]" :mode="imgMode"></image>
<view v-if="title"
class="u-swiper-title u-line-1"
:style="{
'padding-bottom': titlePaddingBottom
}"
>
{{ item.title }}
</view>
</view>
</swiper-item>
</swiper>
<view
class="u-swiper-indicator"
:style="{
top: indicatorPos == 'topLeft' || indicatorPos == 'topCenter' || indicatorPos == 'topRight' ? '12rpx' : 'auto',
bottom: indicatorPos == 'bottomLeft' || indicatorPos == 'bottomCenter' || indicatorPos == 'bottomRight' ? '12rpx' : 'auto',
justifyContent: justifyContent,
padding: `0 ${effect3d ? '74rpx' : '24rpx'}`
}"
>
<block v-if="mode == 'rect'">
<view class="u-indicator-item-rect" :class="{ 'u-indicator-item-rect-active': index == current }" v-for="(item, index) in list" :key="index"></view>
</block>
<block v-if="mode == 'dot'">
<view class="u-indicator-item-dot" :class="{ 'u-indicator-item-dot-active': index == current }" v-for="(item, index) in list" :key="index"></view>
</block>
<block v-if="mode == 'round'">
<view class="u-indicator-item-round" :class="{ 'u-indicator-item-round-active': index == current }" v-for="(item, index) in list" :key="index"></view>
</block>
<block v-if="mode == 'number'">
<view class="u-indicator-item-number">{{ current + 1 }}/{{ list.length }}</view>
</block>
</view>
</view>
</template>
<script>
export default {
props: {
// 轮播图的数据,格式如:[{image: 'xxxx', title: 'xxxx'},{image: 'yyyy', title: 'yyyy'}],其中title字段可选
list: {
type: Array,
default() {
return [];
}
},
// 是否显示title标题
title: {
type: Boolean,
default: false
},
// 用户自定义的指示器的样式
indicator: {
type: Object,
default() {
return {};
}
},
// 圆角值
borderRadius: {
type: [Number, String],
default: 8
},
// 隔多久自动切换
interval: {
type: [String, Number],
default: 3000
},
// 指示器的模式,rect|dot|number|round
mode: {
type: String,
default: 'round'
},
// list的高度,单位rpx
height: {
type: [Number, String],
default: 250
},
// 指示器的位置,topLeft|topCenter|topRight|bottomLeft|bottomCenter|bottomRight
indicatorPos: {
type: String,
default: 'bottomCenter'
},
// 是否开启缩放效果
effect3d: {
type: Boolean,
default: false
},
// 3D模式的情况下,激活item与前后item之间的距离,单位rpx
effect3dPreviousMargin: {
type: [Number, String],
default: 50
},
// 是否自动播放
autoplay: {
type: Boolean,
default: true
},
// 自动轮播时间间隔,单位ms
duration: {
type: [Number, String],
default: 500
},
// 是否衔接滑动,即到最后一张时接着滑动,是佛自动切换到第一张
circular: {
type: Boolean,
default: true
},
// 图片的形式模式
imgMode: {
type: String,
default: 'aspectFill'
},
// 从list数组中读取的图片的属性名
name: {
type: String,
default: 'image'
}
},
data() {
return {
current: 0 // 当前活跃的swiper-item的index
};
},
computed: {
justifyContent() {
if(this.indicatorPos == 'topLeft' || this.indicatorPos == 'bottomLeft') return 'flex-start';
if(this.indicatorPos == 'topCenter' || this.indicatorPos == 'bottomCenter') return 'center';
if(this.indicatorPos == 'topRight' || this.indicatorPos == 'bottomRight') return 'flex-end';
},
titlePaddingBottom() {
let tmp = 0;
if(this.mode == 'none') return '12rpx';
if(['bottomLeft', 'bottomCenter', 'bottomRight'].indexOf(this.indicatorPos) >= 0 && this.mode == 'number') {
tmp = '60rpx';
} else if(['bottomLeft', 'bottomCenter', 'bottomRight'].indexOf(this.indicatorPos) >= 0 && this.mode != 'number') {
tmp = '40rpx';
} else {
tmp = '12rpx';
}
return tmp;
}
},
methods: {
listClick(index) {
this.$emit('click', index);
},
change(e) {
this.current = e.detail.current;
}
}
};
</script>
<style lang="scss" scoped>
.u-swiper-wrap {
position: relative;
overflow: hidden;
}
.u-swiper-image {
width: 100%;
will-change: transform;
height: 100%;
display: block;
/* #ifdef H5 */
pointer-events: none;
/* #endif */
}
.u-swiper-indicator {
padding: 0 24rpx;
position: absolute;
display: flex;
width: 100%;
z-index: 1;
}
.u-indicator-item-rect {
width: 26rpx;
height: 8rpx;
margin: 0 6rpx;
transition: all 0.5s;
background-color: rgba(0, 0, 0, 0.3);
}
.u-indicator-item-rect-active {
background-color: rgba(255, 255, 255, 0.8);
}
.u-indicator-item-dot {
width: 14rpx;
height: 14rpx;
margin: 0 6rpx;
border-radius: 20rpx;
transition: all 0.5s;
background-color: rgba(0, 0, 0, 0.3);
}
.u-indicator-item-dot-active {
background-color: rgba(255, 255, 255, 0.8);
}
.u-indicator-item-round {
width: 14rpx;
height: 14rpx;
margin: 0 6rpx;
border-radius: 20rpx;
transition: all 0.5s;
background-color: rgba(0, 0, 0, 0.3);
}
.u-indicator-item-round-active {
width: 34rpx;
background-color: rgba(255, 255, 255, 0.8);
}
.u-indicator-item-number {
padding: 6rpx 16rpx;
line-height: 1;
background-color: rgba(0, 0, 0, 0.3);
border-radius: 100rpx;
font-size: 26rpx;
color: rgba(255, 255, 255, 0.8);
}
.u-list-scale {
transform-origin: center center;
}
.u-list-image-wrap {
width: 100%;
height: 100%;
flex: 1;
transition: all 0.5s;
overflow: hidden;
box-sizing: content-box;
position: relative;
}
.u-swiper-title {
position: absolute;
background-color: rgba(0, 0, 0, 0.3);
bottom: 0;
left: 0;
width: 100%;
font-size: 28rpx;
padding: 12rpx 24rpx;
color: rgba(255, 255, 255, 0.9);
}
.u-swiper-item {
display: flex;
overflow: hidden;
align-items: center;
}
<template>
<view class="u-swiper-wrap" :style="{
borderRadius: `${borderRadius}rpx`,
}">
<swiper @change="change" :current="current" :interval="interval" :circular="circular" :duration="duration" :autoplay="autoplay"
:previous-margin="effect3d ? effect3dPreviousMargin + 'rpx' : '0'" :next-margin="effect3d ? effect3dPreviousMargin + 'rpx' : '0'"
:style="{
height: height + 'rpx'
}">
<swiper-item class="u-swiper-item" v-for="(item, index) in list" :key="index" @tap="listClick(index)">
<view class="u-list-image-wrap" :class="[current != index ? 'u-list-scale' : '']" :style="{
borderRadius: `${borderRadius}rpx`,
transform: effect3d && current != index ? 'scaleY(0.9)' : 'scaleY(1)',
margin: effect3d && current != index ? '0 20rpx' : 0
}">
<image class="u-swiper-image" :src="item[name]" :mode="imgMode"></image>
<view v-if="title" class="u-swiper-title u-line-1" :style="{
'padding-bottom': titlePaddingBottom
}">
{{ item.title }}
</view>
</view>
</swiper-item>
</swiper>
<view class="u-swiper-indicator" :style="{
top: indicatorPos == 'topLeft' || indicatorPos == 'topCenter' || indicatorPos == 'topRight' ? '12rpx' : 'auto',
bottom: indicatorPos == 'bottomLeft' || indicatorPos == 'bottomCenter' || indicatorPos == 'bottomRight' ? '12rpx' : 'auto',
justifyContent: justifyContent,
padding: `0 ${effect3d ? '74rpx' : '24rpx'}`
}">
<block v-if="mode == 'rect'">
<view class="u-indicator-item-rect" :class="{ 'u-indicator-item-rect-active': index == current }" v-for="(item, index) in list"
:key="index"></view>
</block>
<block v-if="mode == 'dot'">
<view class="u-indicator-item-dot" :class="{ 'u-indicator-item-dot-active': index == current }" v-for="(item, index) in list"
:key="index"></view>
</block>
<block v-if="mode == 'round'">
<view class="u-indicator-item-round" :class="{ 'u-indicator-item-round-active': index == current }" v-for="(item, index) in list"
:key="index"></view>
</block>
<block v-if="mode == 'number'">
<view class="u-indicator-item-number">{{ current + 1 }}/{{ list.length }}</view>
</block>
</view>
</view>
</template>
<script>
/**
* alertTips 提示
* @description 该组件一般用于导航轮播,广告展示等场景,可开箱即用
* @tutorial https://www.uviewui.com/components/swiper.html
* @property {Array} list 轮播图数据,见官网"基本使用"说明
* @property {Boolean} title 是否显示标题文字,需要配合list参数,见官网说明(默认false)
* @property {String} mode 指示器模式,见官网说明(默认round)
* @property {String Number} height 轮播图组件高度,单位rpx(默认250)
* @property {String} indicator-pos 指示器的位置(默认bottomCenter)
* @property {Boolean} effect3d 是否开启3D效果(默认false)
* @property {Boolean} autoplay 是否自动播放(默认true)
* @property {String Number} interval 自动轮播时间间隔,单位ms(默认2500)
* @property {Boolean} circular 是否衔接播放,见官网说明(默认true)
* @property {String Number} border-radius 轮播图圆角值,单位rpx(默认8)
* @property {Object} title-style 自定义标题样式
* @property {String Number} effect3d-previous-margin mode = true模式的情况下,激活项与前后项之间的距离,单位rpx(默认50)
* @property {String} img-mode 图片的裁剪模式,详见image组件裁剪模式(默认aspectFill)
* @event {Function} click 点击轮播图时触发
* @example <u-swiper :list="list" mode="dot" indicator-pos="bottomRight"></u-swiper>
*/
export default {
name: "u-swiper",
props: {
// 轮播图的数据,格式如:[{image: 'xxxx', title: 'xxxx'},{image: 'yyyy', title: 'yyyy'}],其中title字段可选
list: {
type: Array,
default () {
return [];
}
},
// 是否显示title标题
title: {
type: Boolean,
default: false
},
// 用户自定义的指示器的样式
indicator: {
type: Object,
default () {
return {};
}
},
// 圆角值
borderRadius: {
type: [Number, String],
default: 8
},
// 隔多久自动切换
interval: {
type: [String, Number],
default: 3000
},
// 指示器的模式,rect|dot|number|round
mode: {
type: String,
default: 'round'
},
// list的高度,单位rpx
height: {
type: [Number, String],
default: 250
},
// 指示器的位置,topLeft|topCenter|topRight|bottomLeft|bottomCenter|bottomRight
indicatorPos: {
type: String,
default: 'bottomCenter'
},
// 是否开启缩放效果
effect3d: {
type: Boolean,
default: false
},
// 3D模式的情况下,激活item与前后item之间的距离,单位rpx
effect3dPreviousMargin: {
type: [Number, String],
default: 50
},
// 是否自动播放
autoplay: {
type: Boolean,
default: true
},
// 自动轮播时间间隔,单位ms
duration: {
type: [Number, String],
default: 500
},
// 是否衔接滑动,即到最后一张时接着滑动,是佛自动切换到第一张
circular: {
type: Boolean,
default: true
},
// 图片的形式模式
imgMode: {
type: String,
default: 'aspectFill'
},
// 从list数组中读取的图片的属性名
name: {
type: String,
default: 'image'
}
},
data() {
return {
current: 0 // 当前活跃的swiper-item的index
};
},
computed: {
justifyContent() {
if (this.indicatorPos == 'topLeft' || this.indicatorPos == 'bottomLeft') return 'flex-start';
if (this.indicatorPos == 'topCenter' || this.indicatorPos == 'bottomCenter') return 'center';
if (this.indicatorPos == 'topRight' || this.indicatorPos == 'bottomRight') return 'flex-end';
},
titlePaddingBottom() {
let tmp = 0;
if (this.mode == 'none') return '12rpx';
if (['bottomLeft', 'bottomCenter', 'bottomRight'].indexOf(this.indicatorPos) >= 0 && this.mode == 'number') {
tmp = '60rpx';
} else if (['bottomLeft', 'bottomCenter', 'bottomRight'].indexOf(this.indicatorPos) >= 0 && this.mode != 'number') {
tmp = '40rpx';
} else {
tmp = '12rpx';
}
return tmp;
}
},
methods: {
listClick(index) {
this.$emit('click', index);
},
change(e) {
this.current = e.detail.current;
}
}
};
</script>
<style lang="scss" scoped>
.u-swiper-wrap {
position: relative;
overflow: hidden;
}
.u-swiper-image {
width: 100%;
will-change: transform;
height: 100%;
display: block;
/* #ifdef H5 */
pointer-events: none;
/* #endif */
}
.u-swiper-indicator {
padding: 0 24rpx;
position: absolute;
display: flex;
width: 100%;
z-index: 1;
}
.u-indicator-item-rect {
width: 26rpx;
height: 8rpx;
margin: 0 6rpx;
transition: all 0.5s;
background-color: rgba(0, 0, 0, 0.3);
}
.u-indicator-item-rect-active {
background-color: rgba(255, 255, 255, 0.8);
}
.u-indicator-item-dot {
width: 14rpx;
height: 14rpx;
margin: 0 6rpx;
border-radius: 20rpx;
transition: all 0.5s;
background-color: rgba(0, 0, 0, 0.3);
}
.u-indicator-item-dot-active {
background-color: rgba(255, 255, 255, 0.8);
}
.u-indicator-item-round {
width: 14rpx;
height: 14rpx;
margin: 0 6rpx;
border-radius: 20rpx;
transition: all 0.5s;
background-color: rgba(0, 0, 0, 0.3);
}
.u-indicator-item-round-active {
width: 34rpx;
background-color: rgba(255, 255, 255, 0.8);
}
.u-indicator-item-number {
padding: 6rpx 16rpx;
line-height: 1;
background-color: rgba(0, 0, 0, 0.3);
border-radius: 100rpx;
font-size: 26rpx;
color: rgba(255, 255, 255, 0.8);
}
.u-list-scale {
transform-origin: center center;
}
.u-list-image-wrap {
width: 100%;
height: 100%;
flex: 1;
transition: all 0.5s;
overflow: hidden;
box-sizing: content-box;
position: relative;
}
.u-swiper-title {
position: absolute;
background-color: rgba(0, 0, 0, 0.3);
bottom: 0;
left: 0;
width: 100%;
font-size: 28rpx;
padding: 12rpx 24rpx;
color: rgba(255, 255, 255, 0.9);
}
.u-swiper-item {
display: flex;
overflow: hidden;
align-items: center;
}
</style>
<template>
<view
class="u-switch"
:class="[value == true ? 'u-switch--on' : '', disabled ? 'u-switch--disabled' : '']"
@tap="onClick"
:style="[switchStyle]"
>
<view class="u-switch__node node-class">
<u-loading :show="loading" class="u-switch__loading" :size="size * 0.6" :color="loadingColor"/>
</view>
</view>
</template>
<script>
export default {
props: {
// 是否为加载中状态
loading: {
type: Boolean,
default: false
},
// 是否为禁用装填
disabled: {
type: Boolean,
default: false
},
// 开关尺寸,单位rpx
size: {
type: [Number, String],
default: 50
},
// 打开时的背景颜色
activeColor: {
type: String,
default: '#2979ff'
},
// 关闭时的背景颜色
unactiveColor: {
type: String,
default: '#ffffff'
},
// 通过v-model双向绑定的值
value: {
type: Boolean,
default: false
}
},
data() {
return {
}
},
computed: {
switchStyle() {
let style = {};
style.fontSize = this.size + 'rpx';
style.backgroundColor = this.value ? this.activeColor : this.unactiveColor;
return style;
},
loadingColor() {
return this.value ? this.activeColor : null;
}
},
methods: {
onClick() {
if (!this.disabled && !this.loading) {
// 使手机产生短促震动,微信小程序有效,APP(HX 2.6.8)和H5无效
uni.vibrateShort();
this.$emit('input', !this.value);
// 放到下一个生命周期,因为双向绑定的value修改父组件状态需要时间,且是异步的
this.$nextTick(function(){
this.$emit('change', this.value);
})
}
}
}
};
</script>
<style lang="scss" scoped>
.u-switch {
position: relative;
display: inline-block;
box-sizing: initial;
width: 2em;
width: var(--switch-width, 2em);
height: 1em;
height: var(--switch-height, 1em);
background-color: #fff;
background-color: var(--switch-background-color, #fff);
border: 1px solid rgba(0, 0, 0, 0.1);
border: var(--switch-border, 1px solid rgba(0, 0, 0, 0.1));
border-radius: 1em;
border-radius: var(--switch-node-size, 1em);
transition: background-color 0.3s;
transition: background-color var(--switch-transition-duration, 0.3s);
}
.u-switch__node {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
top: 0;
left: 0;
border-radius: 100%;
z-index: 1;
z-index: var(--switch-node-z-index, 1);
width: 1em;
width: var(--switch-node-size, 1em);
height: 1em;
height: var(--switch-node-size, 1em);
background-color: #fff;
background-color: var(--switch-node-background-color, #fff);
box-shadow: 0 3px 1px 0 rgba(0, 0, 0, 0.05), 0 2px 2px 0 rgba(0, 0, 0, 0.1), 0 3px 3px 0 rgba(0, 0, 0, 0.05);
box-shadow: var(--switch-node-box-shadow, 0 3px 1px 0 rgba(0, 0, 0, 0.05), 0 2px 2px 0 rgba(0, 0, 0, 0.1), 0 3px 3px 0 rgba(0, 0, 0, 0.05));
transition: -webkit-transform 0.3s cubic-bezier(0.3, 1.05, 0.4, 1.05);
transition: transform 0.3s cubic-bezier(0.3, 1.05, 0.4, 1.05);
transition: transform 0.3s cubic-bezier(0.3, 1.05, 0.4, 1.05), -webkit-transform 0.3s cubic-bezier(0.3, 1.05, 0.4, 1.05);
transition: -webkit-transform var(--switch-transition-duration, 0.3s) cubic-bezier(0.3, 1.05, 0.4, 1.05);
transition: transform var(--switch-transition-duration, 0.3s) cubic-bezier(0.3, 1.05, 0.4, 1.05);
transition: transform var(--switch-transition-duration, 0.3s) cubic-bezier(0.3, 1.05, 0.4, 1.05),
-webkit-transform var(--switch-transition-duration, 0.3s) cubic-bezier(0.3, 1.05, 0.4, 1.05);
}
.u-switch__loading {
display: flex;
align-items: center;
justify-content: center;
}
.u-switch--on {
background-color: #1989fa;
background-color: var(--switch-on-background-color, #1989fa);
}
.u-switch--on .u-switch__node {
-webkit-transform: translateX(1em);
transform: translateX(1em);
-webkit-transform: translateX(calc(var(--switch-width, 2em) - var(--switch-node-size, 1em)));
transform: translateX(calc(var(--switch-width, 2em) - var(--switch-node-size, 1em)));
}
.u-switch--disabled {
opacity: 0.4;
opacity: var(--switch-disabled-opacity, 0.4);
}
<template>
<view class="u-switch" :class="[value == true ? 'u-switch--on' : '', disabled ? 'u-switch--disabled' : '']" @tap="onClick"
:style="[switchStyle]">
<view class="u-switch__node node-class">
<u-loading :show="loading" class="u-switch__loading" :size="size * 0.6" :color="loadingColor" />
</view>
</view>
</template>
<script>
/**
* alertTips 提示
* @description 选择开关一般用于只有两个选择,且只能选其一的场景。
* @tutorial https://www.uviewui.com/components/switch.html
* @property {Boolean} loading 是否处于加载中(默认false)
* @property {Boolean} disabled 是否禁用(默认false)
* @property {String Number} size 开关尺寸,单位rpx(默认50)
* @property {String} active-color 打开时的背景色(默认#2979ff)
* @property {Boolean} inactive-color 关闭时的背景色(默认#ffffff)
* @event {Function} change 在switch打开或关闭时触发
* @example <u-switch v-model="checked" active-color="red" inactive-color="#eee"></u-switch>
*/
export default {
name: "u-switch",
props: {
// 是否为加载中状态
loading: {
type: Boolean,
default: false
},
// 是否为禁用装填
disabled: {
type: Boolean,
default: false
},
// 开关尺寸,单位rpx
size: {
type: [Number, String],
default: 50
},
// 打开时的背景颜色
activeColor: {
type: String,
default: '#2979ff'
},
// 关闭时的背景颜色
unactiveColor: {
type: String,
default: '#ffffff'
},
// 通过v-model双向绑定的值
value: {
type: Boolean,
default: false
}
},
data() {
return {
}
},
computed: {
switchStyle() {
let style = {};
style.fontSize = this.size + 'rpx';
style.backgroundColor = this.value ? this.activeColor : this.unactiveColor;
return style;
},
loadingColor() {
return this.value ? this.activeColor : null;
}
},
methods: {
onClick() {
if (!this.disabled && !this.loading) {
// 使手机产生短促震动,微信小程序有效,APP(HX 2.6.8)和H5无效
uni.vibrateShort();
this.$emit('input', !this.value);
// 放到下一个生命周期,因为双向绑定的value修改父组件状态需要时间,且是异步的
this.$nextTick(function() {
this.$emit('change', this.value);
})
}
}
}
};
</script>
<style lang="scss" scoped>
.u-switch {
position: relative;
display: inline-block;
box-sizing: initial;
width: 2em;
width: var(--switch-width, 2em);
height: 1em;
height: var(--switch-height, 1em);
background-color: #fff;
background-color: var(--switch-background-color, #fff);
border: 1px solid rgba(0, 0, 0, 0.1);
border: var(--switch-border, 1px solid rgba(0, 0, 0, 0.1));
border-radius: 1em;
border-radius: var(--switch-node-size, 1em);
transition: background-color 0.3s;
transition: background-color var(--switch-transition-duration, 0.3s);
}
.u-switch__node {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
top: 0;
left: 0;
border-radius: 100%;
z-index: 1;
z-index: var(--switch-node-z-index, 1);
width: 1em;
width: var(--switch-node-size, 1em);
height: 1em;
height: var(--switch-node-size, 1em);
background-color: #fff;
background-color: var(--switch-node-background-color, #fff);
box-shadow: 0 3px 1px 0 rgba(0, 0, 0, 0.05), 0 2px 2px 0 rgba(0, 0, 0, 0.1), 0 3px 3px 0 rgba(0, 0, 0, 0.05);
box-shadow: var(--switch-node-box-shadow, 0 3px 1px 0 rgba(0, 0, 0, 0.05), 0 2px 2px 0 rgba(0, 0, 0, 0.1), 0 3px 3px 0 rgba(0, 0, 0, 0.05));
transition: -webkit-transform 0.3s cubic-bezier(0.3, 1.05, 0.4, 1.05);
transition: transform 0.3s cubic-bezier(0.3, 1.05, 0.4, 1.05);
transition: transform 0.3s cubic-bezier(0.3, 1.05, 0.4, 1.05), -webkit-transform 0.3s cubic-bezier(0.3, 1.05, 0.4, 1.05);
transition: -webkit-transform var(--switch-transition-duration, 0.3s) cubic-bezier(0.3, 1.05, 0.4, 1.05);
transition: transform var(--switch-transition-duration, 0.3s) cubic-bezier(0.3, 1.05, 0.4, 1.05);
transition: transform var(--switch-transition-duration, 0.3s) cubic-bezier(0.3, 1.05, 0.4, 1.05),
-webkit-transform var(--switch-transition-duration, 0.3s) cubic-bezier(0.3, 1.05, 0.4, 1.05);
}
.u-switch__loading {
display: flex;
align-items: center;
justify-content: center;
}
.u-switch--on {
background-color: #1989fa;
background-color: var(--switch-on-background-color, #1989fa);
}
.u-switch--on .u-switch__node {
-webkit-transform: translateX(1em);
transform: translateX(1em);
-webkit-transform: translateX(calc(var(--switch-width, 2em) - var(--switch-node-size, 1em)));
transform: translateX(calc(var(--switch-width, 2em) - var(--switch-node-size, 1em)));
}
.u-switch--disabled {
opacity: 0.4;
opacity: var(--switch-disabled-opacity, 0.4);
}
</style>
<template>
<view class="u-table" :style="[tableStyle]">
<slot />
</view>
</template>
<script>
export default {
props: {
borderColor: {
type: String,
default: '#e4e7ed'
},
align: {
type: String,
default: 'center'
},
// td的内边距
padding: {
type: String,
default: '10rpx 6rpx'
},
// 字体大小
fontSize: {
type: [String, Number],
default: 28
},
// 字体颜色
color: {
type: String,
default: '#606266'
},
// th的自定义样式
thStyle: {
type: Object,
default() {
return {}
}
},
// table的背景颜色
bgColor: {
type: String,
default: '#ffffff'
}
},
provide() {
return {
uTable: this,
uTd: this
};
},
data() {
return {
}
},
computed: {
tableStyle() {
let style = {};
style.borderLeft = `solid 1px ${this.borderColor}`;
style.borderTop = `solid 1px ${this.borderColor}`;
style.backgroundColor = this.bgColor;;
return style;
}
}
}
</script>
<style lang="scss" scoped>
.u-table {
width: 100%;
box-sizing: border-box;
}
.u-table /deep/ t-tr {
display: flex;
}
<template>
<view class="u-table" :style="[tableStyle]">
<slot />
</view>
</template>
<script>
/**
* alertTips 提示
* @description 表格组件一般用于展示大量结构化数据的场景
* @tutorial https://www.uviewui.com/components/table.html
* @property {String} border-color 表格边框的颜色(默认#e4e7ed)
* @property {String} bg-color 表格的背景颜色(默认#ffffff)
* @property {String} align 单元格的内容对齐方式,作用类似css的text-align(默认center)
* @property {String} padding 单元格的内边距,同css的padding写法(默认10rpx 0)
* @property {String Number} font-size 单元格字体大小,单位rpx(默认28)
* @property {String} color 单元格字体颜色(默认#606266)
* @property {Object} th-style th单元格的样式,对象形式(将th所需参数放在table组件,是为了避免每一个th组件要写一遍)
* @event {Function} click 点击组件时触发
* @event {Function} close 点击关闭按钮时触发
* @example <u-table></u-table>
*/
export default {
name: "u-table",
props: {
borderColor: {
type: String,
default: '#e4e7ed'
},
align: {
type: String,
default: 'center'
},
// td的内边距
padding: {
type: String,
default: '10rpx 6rpx'
},
// 字体大小
fontSize: {
type: [String, Number],
default: 28
},
// 字体颜色
color: {
type: String,
default: '#606266'
},
// th的自定义样式
thStyle: {
type: Object,
default () {
return {}
}
},
// table的背景颜色
bgColor: {
type: String,
default: '#ffffff'
}
},
provide() {
return {
uTable: this,
uTd: this
};
},
data() {
return {}
},
computed: {
tableStyle() {
let style = {};
style.borderLeft = `solid 1px ${this.borderColor}`;
style.borderTop = `solid 1px ${this.borderColor}`;
style.backgroundColor = this.bgColor;;
return style;
}
}
}
</script>
<style lang="scss" scoped>
.u-table {
width: 100%;
box-sizing: border-box;
}
.u-table /deep/ t-tr {
display: flex;
}
</style>
<template>
<view
class="u-tabs"
:style="{
zIndex: zIndex,
background: bgColor
}"
>
<scroll-view scroll-x class="u-scroll-view" :scroll-left="scrollLeft" scroll-with-animation :style="{ zIndex: zIndex + 1 }">
<view class="u-tabs-scroll-box" :class="{'u-tabs-scorll-flex': !isScroll}">
<view
class="u-tabs-item"
:style="{
height: height + 'rpx',
lineHeight: height + 'rpx',
padding: `0 ${gutter / 2}rpx`,
color: tabsInfo.length > 0 ? (tabsInfo[index] ? tabsInfo[index].color : activeColor) : inactiveColor,
fontSize: fontSize + 'rpx',
zIndex: zIndex + 2,
fontWeight: (index == getCurrent && bold) ? 'bold' : 'normal'
}"
v-for="(item, index) in getTabs"
:key="index"
:class="[preId + index]"
@tap="emit(index)"
>
{{ item[name] || item['name']}}
</view>
<view
class="u-scroll-bar"
:style="{
width: barWidthPx + 'px',
height: barHeight + 'rpx',
borderRadius: '100px',
backgroundColor: activeColor,
left: scrollBarLeft + 'px'
}"
></view>
</view>
</scroll-view>
</view>
</template>
<script>
import colorGradient from '@/uview/libs/function/colorGradient';
let color = colorGradient;
const { windowWidth } = uni.getSystemInfoSync();
const preId = 'UEl_';
export default {
props: {
// 导航菜单是否需要滚动,如只有2或者3个的时候,就不需要滚动了,此时使用flex平分tab的宽度
isScroll: {
type: Boolean,
default: true
},
//需循环的标签列表
list: {
type: Array,
default() {
return [];
}
},
// 当前活动tab的索引
current: {
type: [Number, String],
default: 0
},
// 导航栏的高度和行高,单位rpx
height: {
type: [Number, String],
default: 80
},
// 字体大小,单位rpx
fontSize: {
type: [Number, String],
default: 30
},
// 过渡动画时长, 单位s
// duration: {
// type: [Number, String],
// default: 0.5
// },
swiperWidth: {
//line3生效, 外部swiper的宽度, 单位rpx
type: [String, Number],
default: 750
},
// 选中项的主题颜色
activeColor: {
type: String,
default: '#2979ff'
},
// 未选中项的颜色
inactiveColor: {
type: String,
default: '#303133'
},
// 菜单底部移动的bar的宽度,单位rpx
barWidth: {
type: [Number, String],
default: 40
},
// 移动bar的高度
barHeight: {
type: [Number, String],
default: 6
},
// 单个tab的左或右内边距(各占一半),单位rpx
gutter: {
type: [Number, String],
default: 40
},
// 如果是绝对定位,添加z-index值
zIndex: {
type: [Number, String],
default: 1
},
// 导航栏的背景颜色
bgColor: {
type: String,
default: '#ffffff'
},
//滚动至中心目标类型
autoCenterMode: {
type: String,
default: 'window'
},
// 读取传入的数组对象的属性
name: {
type: String,
default: 'name'
},
// 活动tab字体是否加粗
bold: {
type: Boolean,
default: true
}
},
data() {
return {
scrollLeft: 0, // 滚动scroll-view的左边滚动距离
tabQueryInfo: [], // 存放对tab菜单查询后的节点信息
windowWidth: 0, // 屏幕宽度,单位为px
//scrollBarLeft: 0, // 移动bar需要通过translateX()移动的距离
animationFinishCurrent: this.current,
componentsWidth: 0,
line3AddDx: 0,
line3Dx: 0,
preId,
sW: 0,
tabsInfo: [],
colorGradientArr: [],
colorStep: 100 // 两个颜色之间的渐变等分
};
},
computed: {
// 获取当前活跃的current值
getCurrent() {
const current = Number(this.current);
// 判断是否超出边界
if (current > this.getTabs.length - 1) {
return this.getTabs.length - 1;
}
if (current < 0) return 0;
return current;
},
getTabs() {
return this.list;
},
// 滑块需要移动的距离
scrollBarLeft() {
return Number(this.line3Dx) + Number(this.line3AddDx);
},
// 滑块的宽度转为px单位
barWidthPx() {
return uni.upx2px(this.barWidth);
}
},
watch: {
current(n, o) {
this.change(n);
this.setFinishCurrent(n);
},
list() {
this.$nextTick(() => {
this.init();
})
}
},
mounted() {
this.init();
},
methods: {
async init() {
this.countPx();
await this.getTabsInfo();
this.countLine3Dx();
this.getQuery(() => {
this.setScrollViewToCenter();
});
// 颜色渐变过程数组
this.colorGradientArr = color.colorGradient(this.inactiveColor, this.activeColor, this.colorStep);
},
// 获取各个tab的节点信息
getTabsInfo() {
return new Promise((resolve, reject) => {
let view = uni.createSelectorQuery().in(this);
for (let i = 0; i < this.list.length; i++) {
view.select('.' + preId + i).boundingClientRect();
}
view.exec(res => {
const arr = [];
for (let i = 0; i < res.length; i++) {
// 给每个tab添加其文字颜色属性
res[i].color = this.inactiveColor;
// 当前tab直接赋予activeColor
if (i == this.getCurrent) res[i].color = this.activeColor;
arr.push(res[i]);
}
this.tabsInfo = arr;
resolve();
});
})
},
// 当swiper滑动结束,计算滑块最终要停留的位置
countLine3Dx() {
const tab = this.tabsInfo[this.animationFinishCurrent];
// 让滑块中心点和当前tab中心重合
if (tab) this.line3Dx = tab.left + tab.width / 2 - this.barWidthPx / 2;
},
countPx() {
// swiper宽度由rpx转为px单位,因为dx等,都是px单位
this.sW = uni.upx2px(Number(this.swiperWidth));
},
emit(index) {
this.$emit('change', index);
},
change() {
this.setScrollViewToCenter();
},
getQuery(cb) {
try {
let view = uni.createSelectorQuery().in(this).select('.u-tabs');
view.fields(
{
size: true
},
data => {
if (data) {
this.componentsWidth = data.width;
if (cb && typeof cb === 'function') cb(data);
} else {
this.getQuery(cb);
}
}
).exec();
} catch (e) {
this.componentsWidth = windowWidth;
}
},
// 把活动tab移动到屏幕中心点
setScrollViewToCenter() {
let tab;
tab = this.tabsInfo[this.animationFinishCurrent];
if (tab) {
let tabCenter = tab.left + tab.width / 2;
let fatherWidth;
// 活动tab移动到中心时,以屏幕还是tab组件为宽度为基准
if (this.autoCenterMode === 'window') {
fatherWidth = windowWidth;
} else {
fatherWidth = this.componentsWidth;
}
this.scrollLeft = tabCenter - fatherWidth / 2;
}
},
setDx(dx) {
let nextTabIndex = dx > 0 ? this.animationFinishCurrent + 1 : this.animationFinishCurrent - 1;
// 判断索引是否超出边界
nextTabIndex = nextTabIndex <= 0 ? 0 : nextTabIndex;
nextTabIndex = nextTabIndex >= this.list.length ? this.list.length - 1 : nextTabIndex;
const tab = this.tabsInfo[nextTabIndex];
// 当前tab中心点x轴坐标
let nowTab = this.tabsInfo[this.animationFinishCurrent];
let nowTabX = nowTab.left + nowTab.width / 2;
// 下一个tab
let nextTab = this.tabsInfo[nextTabIndex];
let nextTabX = nextTab.left + nextTab.width / 2;
// 两个tab之间的距离,因为下一个tab可能在当前tab的左边或者右边,取绝对值即可
let distanceX = Math.abs(nextTabX - nowTabX);
this.line3AddDx = (dx / this.sW) * distanceX;
this.setTabColor(this.animationFinishCurrent, nextTabIndex, dx);
},
// 设置tab的颜色
setTabColor(nowTabIndex, nextTabIndex, dx) {
let colorIndex = Math.abs(Math.ceil((dx / this.sW) * 100));
let colorLength = this.colorGradientArr.length;
// 处理超出索引边界的情况
colorIndex = colorIndex >= colorLength ? colorLength - 1 : colorIndex <= 0 ? 0 : colorIndex;
// 设置下一个tab的颜色
this.tabsInfo[nextTabIndex].color = this.colorGradientArr[colorIndex];
// 设置当前tab的颜色
this.tabsInfo[nowTabIndex].color = this.colorGradientArr[colorLength - 1 - colorIndex];
},
// swiper结束滑动
setFinishCurrent(current) {
// 如果滑动的索引不一致,修改tab颜色变化,因为可能会有直接点击tab的情况
if (current != this.animationFinishCurrent) {
this.tabsInfo.map((val, index) => {
if (current == index) val.color = this.activeColor;
else val.color = this.inactiveColor;
return val;
});
}
this.line3AddDx = 0;
this.animationFinishCurrent = current;
this.countLine3Dx();
}
}
};
</script>
<style scoped lang="scss">
view,
scroll-view {
box-sizing: border-box;
}
.u-tabs {
width: 100%;
transition-property: background-color, color;
}
::-webkit-scrollbar,
::-webkit-scrollbar,
::-webkit-scrollbar {
display: none;
width: 0 !important;
height: 0 !important;
-webkit-appearance: none;
background: transparent;
}
/* #ifdef H5 */
// 通过样式穿透,隐藏H5下,scroll-view下的滚动条
scroll-view /deep/ ::-webkit-scrollbar {
display: none;
width: 0 !important;
height: 0 !important;
-webkit-appearance: none;
background: transparent;
}
/* #endif */
.u-scroll-view {
width: 100%;
white-space: nowrap;
position: relative;
}
.u-tabs-scroll-box {
position: relative;
}
.u-tabs-scorll-flex {
display: flex;
justify-content: space-between;
}
.u-tabs-scorll-flex .u-tabs-item {
flex: 1;
}
.u-tabs-item {
position: relative;
display: inline-block;
text-align: center;
transition-property: background-color, color, font-weight;
}
.content {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.boxStyle {
pointer-events: none;
position: absolute;
transition-property: all;
}
.boxStyle2 {
pointer-events: none;
position: absolute;
bottom: 0;
transition-property: all;
transform: translateY(-100%);
}
.itemBackgroundBox {
pointer-events: none;
position: absolute;
top: 0;
transition-property: left, background-color;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.itemBackground {
height: 100%;
width: 100%;
transition-property: all;
}
.u-scroll-bar {
position: absolute;
bottom: 4rpx;
}
<template>
<view class="u-tabs" :style="{
zIndex: zIndex,
background: bgColor
}">
<scroll-view scroll-x class="u-scroll-view" :scroll-left="scrollLeft" scroll-with-animation :style="{ zIndex: zIndex + 1 }">
<view class="u-tabs-scroll-box" :class="{'u-tabs-scorll-flex': !isScroll}">
<view class="u-tabs-item" :style="{
height: height + 'rpx',
lineHeight: height + 'rpx',
padding: `0 ${gutter / 2}rpx`,
color: tabsInfo.length > 0 ? (tabsInfo[index] ? tabsInfo[index].color : activeColor) : inactiveColor,
fontSize: fontSize + 'rpx',
zIndex: zIndex + 2,
fontWeight: (index == getCurrent && bold) ? 'bold' : 'normal'
}"
v-for="(item, index) in getTabs" :key="index" :class="[preId + index]" @tap="emit(index)">
{{ item[name] || item['name']}}
</view>
<view class="u-scroll-bar" :style="{
width: barWidthPx + 'px',
height: barHeight + 'rpx',
borderRadius: '100px',
backgroundColor: activeColor,
left: scrollBarLeft + 'px'
}"></view>
</view>
</scroll-view>
</view>
</template>
<script>
/**
* alertTips 提示
* @description 该组件内部实现主要依托于uniapp的scroll-view和swiper组件,主要特色是切换过程中,tabsSwiper文字的颜色可以渐变,底部滑块可以 跟随式滑动,活动tab滚动居中等。应用场景可以用于需要左右切换页面,比如商城的订单中心(待收货-待付款-待评价-已退货)等应用场景。
* @tutorial https://www.uviewui.com/components/tabsSwiper.html
* @property {Boolean} is-scroll tabs是否可以左右拖动(默认true)
* @property {Array} list 标签数组,元素为对象,如[{name: '推荐'}]
* @property {String Number} current 指定哪个tab为激活状态(默认0)
* @property {String Number} height 导航栏的高度,单位rpx(默认80)
* @property {String Number} font-size tab文字大小,单位rpx(默认30)
* @property {String Number} swiper-width tabs组件外部swiper的宽度,默认为屏幕宽度,单位rpx(默认750)
* @property {String} active-color 滑块和激活tab文字的颜色(默认#2979ff)
* @property {String} inactive-color tabs文字颜色(默认#303133)
* @property {String Number} bar-width 滑块宽度,单位rpx(默认40)
* @property {String Number} bar-height 滑块高度,单位rpx(默认6)
* @property {String Number} gutter 单个tab标签的左右内边距之和,单位rpx(默认40)
* @property {String} bg-color tabs导航栏的背景颜色(默认#ffffff)
* @property {String} name 组件内部读取的list参数中的属性名,见官网说明(默认name)
* @property {Boolean} bold 激活选项的字体是否加粗(默认true)
* @event {Function} change 点击标签时触发
* @example <u-tabs-swiper ref="tabs" :list="list" :is-scroll="false"></u-tabs-swiper>
*/
import colorGradient from '@/uview/libs/function/colorGradient';
let color = colorGradient;
const {
windowWidth
} = uni.getSystemInfoSync();
const preId = 'UEl_';
export default {
name: "u-tabs-swiper",
props: {
// 导航菜单是否需要滚动,如只有2或者3个的时候,就不需要滚动了,此时使用flex平分tab的宽度
isScroll: {
type: Boolean,
default: true
},
//需循环的标签列表
list: {
type: Array,
default () {
return [];
}
},
// 当前活动tab的索引
current: {
type: [Number, String],
default: 0
},
// 导航栏的高度和行高,单位rpx
height: {
type: [Number, String],
default: 80
},
// 字体大小,单位rpx
fontSize: {
type: [Number, String],
default: 30
},
// 过渡动画时长, 单位s
// duration: {
// type: [Number, String],
// default: 0.5
// },
swiperWidth: {
//line3生效, 外部swiper的宽度, 单位rpx
type: [String, Number],
default: 750
},
// 选中项的主题颜色
activeColor: {
type: String,
default: '#2979ff'
},
// 未选中项的颜色
inactiveColor: {
type: String,
default: '#303133'
},
// 菜单底部移动的bar的宽度,单位rpx
barWidth: {
type: [Number, String],
default: 40
},
// 移动bar的高度
barHeight: {
type: [Number, String],
default: 6
},
// 单个tab的左或右内边距(各占一半),单位rpx
gutter: {
type: [Number, String],
default: 40
},
// 如果是绝对定位,添加z-index值
zIndex: {
type: [Number, String],
default: 1
},
// 导航栏的背景颜色
bgColor: {
type: String,
default: '#ffffff'
},
//滚动至中心目标类型
autoCenterMode: {
type: String,
default: 'window'
},
// 读取传入的数组对象的属性
name: {
type: String,
default: 'name'
},
// 活动tab字体是否加粗
bold: {
type: Boolean,
default: true
}
},
data() {
return {
scrollLeft: 0, // 滚动scroll-view的左边滚动距离
tabQueryInfo: [], // 存放对tab菜单查询后的节点信息
windowWidth: 0, // 屏幕宽度,单位为px
//scrollBarLeft: 0, // 移动bar需要通过translateX()移动的距离
animationFinishCurrent: this.current,
componentsWidth: 0,
line3AddDx: 0,
line3Dx: 0,
preId,
sW: 0,
tabsInfo: [],
colorGradientArr: [],
colorStep: 100 // 两个颜色之间的渐变等分
};
},
computed: {
// 获取当前活跃的current值
getCurrent() {
const current = Number(this.current);
// 判断是否超出边界
if (current > this.getTabs.length - 1) {
return this.getTabs.length - 1;
}
if (current < 0) return 0;
return current;
},
getTabs() {
return this.list;
},
// 滑块需要移动的距离
scrollBarLeft() {
return Number(this.line3Dx) + Number(this.line3AddDx);
},
// 滑块的宽度转为px单位
barWidthPx() {
return uni.upx2px(this.barWidth);
}
},
watch: {
current(n, o) {
this.change(n);
this.setFinishCurrent(n);
},
list() {
this.$nextTick(() => {
this.init();
})
}
},
mounted() {
this.init();
},
methods: {
async init() {
this.countPx();
await this.getTabsInfo();
this.countLine3Dx();
this.getQuery(() => {
this.setScrollViewToCenter();
});
// 颜色渐变过程数组
this.colorGradientArr = color.colorGradient(this.inactiveColor, this.activeColor, this.colorStep);
},
// 获取各个tab的节点信息
getTabsInfo() {
return new Promise((resolve, reject) => {
let view = uni.createSelectorQuery().in(this);
for (let i = 0; i < this.list.length; i++) {
view.select('.' + preId + i).boundingClientRect();
}
view.exec(res => {
const arr = [];
for (let i = 0; i < res.length; i++) {
// 给每个tab添加其文字颜色属性
res[i].color = this.inactiveColor;
// 当前tab直接赋予activeColor
if (i == this.getCurrent) res[i].color = this.activeColor;
arr.push(res[i]);
}
this.tabsInfo = arr;
resolve();
});
})
},
// 当swiper滑动结束,计算滑块最终要停留的位置
countLine3Dx() {
const tab = this.tabsInfo[this.animationFinishCurrent];
// 让滑块中心点和当前tab中心重合
if (tab) this.line3Dx = tab.left + tab.width / 2 - this.barWidthPx / 2;
},
countPx() {
// swiper宽度由rpx转为px单位,因为dx等,都是px单位
this.sW = uni.upx2px(Number(this.swiperWidth));
},
emit(index) {
this.$emit('change', index);
},
change() {
this.setScrollViewToCenter();
},
getQuery(cb) {
try {
let view = uni.createSelectorQuery().in(this).select('.u-tabs');
view.fields({
size: true
},
data => {
if (data) {
this.componentsWidth = data.width;
if (cb && typeof cb === 'function') cb(data);
} else {
this.getQuery(cb);
}
}
).exec();
} catch (e) {
this.componentsWidth = windowWidth;
}
},
// 把活动tab移动到屏幕中心点
setScrollViewToCenter() {
let tab;
tab = this.tabsInfo[this.animationFinishCurrent];
if (tab) {
let tabCenter = tab.left + tab.width / 2;
let fatherWidth;
// 活动tab移动到中心时,以屏幕还是tab组件为宽度为基准
if (this.autoCenterMode === 'window') {
fatherWidth = windowWidth;
} else {
fatherWidth = this.componentsWidth;
}
this.scrollLeft = tabCenter - fatherWidth / 2;
}
},
setDx(dx) {
let nextTabIndex = dx > 0 ? this.animationFinishCurrent + 1 : this.animationFinishCurrent - 1;
// 判断索引是否超出边界
nextTabIndex = nextTabIndex <= 0 ? 0 : nextTabIndex;
nextTabIndex = nextTabIndex >= this.list.length ? this.list.length - 1 : nextTabIndex;
const tab = this.tabsInfo[nextTabIndex];
// 当前tab中心点x轴坐标
let nowTab = this.tabsInfo[this.animationFinishCurrent];
let nowTabX = nowTab.left + nowTab.width / 2;
// 下一个tab
let nextTab = this.tabsInfo[nextTabIndex];
let nextTabX = nextTab.left + nextTab.width / 2;
// 两个tab之间的距离,因为下一个tab可能在当前tab的左边或者右边,取绝对值即可
let distanceX = Math.abs(nextTabX - nowTabX);
this.line3AddDx = (dx / this.sW) * distanceX;
this.setTabColor(this.animationFinishCurrent, nextTabIndex, dx);
},
// 设置tab的颜色
setTabColor(nowTabIndex, nextTabIndex, dx) {
let colorIndex = Math.abs(Math.ceil((dx / this.sW) * 100));
let colorLength = this.colorGradientArr.length;
// 处理超出索引边界的情况
colorIndex = colorIndex >= colorLength ? colorLength - 1 : colorIndex <= 0 ? 0 : colorIndex;
// 设置下一个tab的颜色
this.tabsInfo[nextTabIndex].color = this.colorGradientArr[colorIndex];
// 设置当前tab的颜色
this.tabsInfo[nowTabIndex].color = this.colorGradientArr[colorLength - 1 - colorIndex];
},
// swiper结束滑动
setFinishCurrent(current) {
// 如果滑动的索引不一致,修改tab颜色变化,因为可能会有直接点击tab的情况
if (current != this.animationFinishCurrent) {
this.tabsInfo.map((val, index) => {
if (current == index) val.color = this.activeColor;
else val.color = this.inactiveColor;
return val;
});
}
this.line3AddDx = 0;
this.animationFinishCurrent = current;
this.countLine3Dx();
}
}
};
</script>
<style scoped lang="scss">
view,
scroll-view {
box-sizing: border-box;
}
.u-tabs {
width: 100%;
transition-property: background-color, color;
}
::-webkit-scrollbar,
::-webkit-scrollbar,
::-webkit-scrollbar {
display: none;
width: 0 !important;
height: 0 !important;
-webkit-appearance: none;
background: transparent;
}
/* #ifdef H5 */
// 通过样式穿透,隐藏H5下,scroll-view下的滚动条
scroll-view /deep/ ::-webkit-scrollbar {
display: none;
width: 0 !important;
height: 0 !important;
-webkit-appearance: none;
background: transparent;
}
/* #endif */
.u-scroll-view {
width: 100%;
white-space: nowrap;
position: relative;
}
.u-tabs-scroll-box {
position: relative;
}
.u-tabs-scorll-flex {
display: flex;
justify-content: space-between;
}
.u-tabs-scorll-flex .u-tabs-item {
flex: 1;
}
.u-tabs-item {
position: relative;
display: inline-block;
text-align: center;
transition-property: background-color, color, font-weight;
}
.content {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.boxStyle {
pointer-events: none;
position: absolute;
transition-property: all;
}
.boxStyle2 {
pointer-events: none;
position: absolute;
bottom: 0;
transition-property: all;
transform: translateY(-100%);
}
.itemBackgroundBox {
pointer-events: none;
position: absolute;
top: 0;
transition-property: left, background-color;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.itemBackground {
height: 100%;
width: 100%;
transition-property: all;
}
.u-scroll-bar {
position: absolute;
bottom: 4rpx;
}
</style>
<template>
<view class="u-tabs" :id="id" :style="{
background: bgColor
}">
<scroll-view scroll-x class="u-scroll-view" :scroll-left="scrollLeft" scroll-with-animation>
<view class="u-scroll-box" :class="{'u-tabs-scorll-flex': !isScroll}">
<view
class="u-tab-item"
:id="'u-tab-item-' + index"
v-for="(item, index) in list"
:key="index"
@tap="clickTab(index)"
:style="[tabItemStyle(index)]"
>
{{ item[name] || item['name']}}
</view>
<view class="u-tab-bar" :style="[tabBarStyle]"></view>
</view>
</scroll-view>
</view>
</template>
<script>
export default {
props: {
// 导航菜单是否需要滚动,如只有2或者3个的时候,就不需要滚动了,此时使用flex平分tab的宽度
isScroll: {
type: Boolean,
default: true
},
//需循环的标签列表
list: {
type: Array,
default() {
return [];
}
},
// 当前活动tab的索引
current: {
type: Number,
default: 0
},
// 导航栏的高度和行高
height: {
type: [String, Number],
default: 80
},
// 字体大小
fontSize: {
type: [String, Number],
default: 30
},
// 过渡动画时长, 单位ms
duration: {
type: [String, Number],
default: 0.5
},
// 选中项的主题颜色
activeColor: {
type: String,
default: '#2979ff'
},
// 未选中项的颜色
inactiveColor: {
type: String,
default: '#303133'
},
// 菜单底部移动的bar的宽度,单位rpx
barWidth: {
type: [String, Number],
default: 40
},
// 移动bar的高度
barHeight: {
type: [String, Number],
default: 6
},
// 单个tab的左或有内边距(左右相同)
gutter: {
type: [String, Number],
default: 30
},
// 导航栏的背景颜色
bgColor: {
type: String,
default: '#ffffff'
},
// 读取传入的数组对象的属性
name: {
type: String,
default: 'name'
},
// 活动tab字体是否加粗
bold: {
type: Boolean,
default: true
}
},
data() {
return {
scrollLeft: 0, // 滚动scroll-view的左边滚动距离
tabQueryInfo: [], // 存放对tab菜单查询后的节点信息
componentWidth: 0, // 屏幕宽度,单位为px
scrollBarLeft: 0 ,// 移动bar需要通过translateX()移动的距离
parentLeft: 0, // 父元素(tabs组件)到屏幕左边的距离
id: this.$u.guid(), // id值
currentIndex: this.current,
};
},
watch: {
// 监听tab的变化,重新计算tab菜单的布局信息,因为实际使用中菜单可能是通过
// 后台获取的(如新闻app顶部的菜单),获取返回需要一定时间,所以list变化时,重新获取布局信息
list(n, o) {
// 用$nextTick等待视图更新完毕后再计算tab的局部信息,否则可能因为tab还没生成就获取,就会有问题
this.$nextTick(() => {
this.init();
});
},
current: {
immediate: true,
handler(nVal) {
// 视图更新后再执行移动操作
this.$nextTick(() => {
this.currentIndex = nVal;
this.scrollByIndex();
});
}
},
},
computed: {
// 移动bar的样式
tabBarStyle() {
return {
width: this.barWidth + 'rpx',
transform: `translate(${this.scrollBarLeft}px, -100%)`,
'transition-duration': `${this.duration}s`,
'background-color': this.activeColor,
height: this.barHeight + 'rpx',
// 设置一个很大的值,它会自动取能用的最大值,不用高度的一半,是因为高度可能是单数,会有小数出现
'border-radius': `${this.barHeight / 2}px`
};
},
// tab的样式
tabItemStyle() {
return (index) => {
let style = {
height: this.height + 'rpx',
'line-height': this.height + 'rpx',
'font-size': this.fontSize + 'rpx',
'transition-duration': `${this.duration}s`,
padding: this.isScroll ? `0 ${this.gutter}rpx` : '',
};
// 字体加粗
if (index == this.currentIndex && this.bold) style.fontWeight = 'bold';
if(index == this.currentIndex) {
style.color = this.activeColor;
} else {
style.color = this.inactiveColor;
}
return style;
}
}
},
methods: {
// 设置一个init方法,方便多处调用
async init() {
// 获取tabs组件的尺寸信息
let tabRect = await this.$uGetRect('#' + this.id);
// tabs组件距离屏幕左边的宽度
this.parentLeft = tabRect.left;
// tabs组件的宽度
this.componentWidth = tabRect.width;
this.getTabRect();
},
// 点击某一个tab菜单
clickTab(index) {
// 发送事件给父组件
this.$emit('change', index);
},
// 查询tab的布局信息
getTabRect() {
// 创建节点查询
let query = uni.createSelectorQuery().in(this);
// 历遍所有tab,这里是执行了查询,最终使用exec()会一次性返回查询的数组结果
for (let i = 0; i < this.list.length; i++) {
// 只要size和rect两个参数
query.select(`#u-tab-item-${i}`).fields({
size: true,
rect: true
});
}
// 执行查询,一次性获取多个结果
query.exec(
function(res) {
this.tabQueryInfo = res;
// 初始化滚动条和移动bar的位置
this.scrollByIndex();
}.bind(this)
);
},
// 滚动scroll-view,让活动的tab处于屏幕的中间位置
scrollByIndex() {
// 当前活动tab的布局信息,有tab菜单的width和left(为元素左边界到父元素左边界的距离)等信息
let tabInfo = this.tabQueryInfo[this.currentIndex];
if (!tabInfo) return;
// 活动tab的宽度
let tabWidth = tabInfo.width;
// 活动item的左边到tabs组件左边的距离,用item的left减去tabs的left
let offsetLeft = tabInfo.left - this.parentLeft;
// 将活动的tabs-item移动到屏幕正中间,实际上是对scroll-view的移动
let scrollLeft = offsetLeft - (this.componentWidth - tabWidth) / 2;
this.scrollLeft = scrollLeft < 0 ? 0 : scrollLeft;
// 当前活动item的中点点到左边的距离减去滑块宽度的一半,即可得到滑块所需的移动距离
let left = tabInfo.left + tabInfo.width / 2 - this.parentLeft;
// 计算当前活跃item到组件左边的距离
this.scrollBarLeft = left - uni.upx2px(this.barWidth) / 2;
}
},
mounted() {
this.init();
}
};
</script>
<style lang="scss">
view,
scroll-view {
box-sizing: border-box;
}
::-webkit-scrollbar,
::-webkit-scrollbar,
::-webkit-scrollbar {
display: none;
width: 0 !important;
height: 0 !important;
-webkit-appearance: none;
background: transparent;
}
.u-scroll-box {
position: relative;
}
/* #ifdef H5 */
// 通过样式穿透,隐藏H5下,scroll-view下的滚动条
scroll-view /deep/ ::-webkit-scrollbar {
display: none;
width: 0 !important;
height: 0 !important;
-webkit-appearance: none;
background: transparent;
}
/* #endif */
.u-scroll-view {
width: 100%;
white-space: nowrap;
position: relative;
}
.u-tab-item {
position: relative;
display: inline-block;
text-align: center;
transition-property: background-color, color;
}
.u-tab-bar {
position: absolute;
bottom: 0;
}
.u-tabs-scorll-flex {
display: flex;
justify-content: space-between;
}
<template>
<view class="u-tabs" :id="id" :style="{
background: bgColor
}">
<scroll-view scroll-x class="u-scroll-view" :scroll-left="scrollLeft" scroll-with-animation>
<view class="u-scroll-box" :class="{'u-tabs-scorll-flex': !isScroll}">
<view class="u-tab-item" :id="'u-tab-item-' + index" v-for="(item, index) in list" :key="index" @tap="clickTab(index)"
:style="[tabItemStyle(index)]">
{{ item[name] || item['name']}}
</view>
<view class="u-tab-bar" :style="[tabBarStyle]"></view>
</view>
</scroll-view>
</view>
</template>
<script>
/**
* alertTips 提示
* @description 该组件,是一个tabs标签组件,在标签多的时候,可以配置为左右滑动,标签少的时候,可以禁止滑动。 该组件的一个特点是配置为滚动模式时,激活的tab会自动移动到组件的中间位置。
* @tutorial https://www.uviewui.com/components/tabs.html
* @property {Boolean} is-scroll tabs是否可以左右拖动(默认true)
* @property {Array} list 标签数组,元素为对象,如[{name: '推荐'}]
* @property {String Number} current 指定哪个tab为激活状态(默认0)
* @property {String Number} height 导航栏的高度,单位rpx(默认80)
* @property {String Number} font-size tab文字大小,单位rpx(默认30)
* @property {String Number} duration 滑块移动一次所需的时间,单位秒(默认0.5)
* @property {String} active-color 滑块和激活tab文字的颜色(默认#2979ff)
* @property {String} inactive-color tabs文字颜色(默认#303133)
* @property {String Number} bar-width 滑块宽度,单位rpx(默认40)
* @property {String Number} bar-height 滑块高度,单位rpx(默认6)
* @property {String Number} gutter 单个tab标签的左右内边距之和,单位rpx(默认40)
* @property {String} bg-color tabs导航栏的背景颜色(默认#ffffff)
* @property {String} name 组件内部读取的list参数中的属性名,见官网说明(默认name)
* @property {Boolean} bold 激活选项的字体是否加粗(默认true)
* @event {Function} change 点击标签时触发
* @example <u-tabs ref="tabs" :list="list" :is-scroll="false"></u-tabs>
*/
export default {
name: "u-tabs",
props: {
// 导航菜单是否需要滚动,如只有2或者3个的时候,就不需要滚动了,此时使用flex平分tab的宽度
isScroll: {
type: Boolean,
default: true
},
//需循环的标签列表
list: {
type: Array,
default () {
return [];
}
},
// 当前活动tab的索引
current: {
type: Number,
default: 0
},
// 导航栏的高度和行高
height: {
type: [String, Number],
default: 80
},
// 字体大小
fontSize: {
type: [String, Number],
default: 30
},
// 过渡动画时长, 单位ms
duration: {
type: [String, Number],
default: 0.5
},
// 选中项的主题颜色
activeColor: {
type: String,
default: '#2979ff'
},
// 未选中项的颜色
inactiveColor: {
type: String,
default: '#303133'
},
// 菜单底部移动的bar的宽度,单位rpx
barWidth: {
type: [String, Number],
default: 40
},
// 移动bar的高度
barHeight: {
type: [String, Number],
default: 6
},
// 单个tab的左或有内边距(左右相同)
gutter: {
type: [String, Number],
default: 30
},
// 导航栏的背景颜色
bgColor: {
type: String,
default: '#ffffff'
},
// 读取传入的数组对象的属性
name: {
type: String,
default: 'name'
},
// 活动tab字体是否加粗
bold: {
type: Boolean,
default: true
}
},
data() {
return {
scrollLeft: 0, // 滚动scroll-view的左边滚动距离
tabQueryInfo: [], // 存放对tab菜单查询后的节点信息
componentWidth: 0, // 屏幕宽度,单位为px
scrollBarLeft: 0, // 移动bar需要通过translateX()移动的距离
parentLeft: 0, // 父元素(tabs组件)到屏幕左边的距离
id: this.$u.guid(), // id值
currentIndex: this.current,
};
},
watch: {
// 监听tab的变化,重新计算tab菜单的布局信息,因为实际使用中菜单可能是通过
// 后台获取的(如新闻app顶部的菜单),获取返回需要一定时间,所以list变化时,重新获取布局信息
list(n, o) {
// 用$nextTick等待视图更新完毕后再计算tab的局部信息,否则可能因为tab还没生成就获取,就会有问题
this.$nextTick(() => {
this.init();
});
},
current: {
immediate: true,
handler(nVal) {
// 视图更新后再执行移动操作
this.$nextTick(() => {
this.currentIndex = nVal;
this.scrollByIndex();
});
}
},
},
computed: {
// 移动bar的样式
tabBarStyle() {
return {
width: this.barWidth + 'rpx',
transform: `translate(${this.scrollBarLeft}px, -100%)`,
'transition-duration': `${this.duration}s`,
'background-color': this.activeColor,
height: this.barHeight + 'rpx',
// 设置一个很大的值,它会自动取能用的最大值,不用高度的一半,是因为高度可能是单数,会有小数出现
'border-radius': `${this.barHeight / 2}px`
};
},
// tab的样式
tabItemStyle() {
return (index) => {
let style = {
height: this.height + 'rpx',
'line-height': this.height + 'rpx',
'font-size': this.fontSize + 'rpx',
'transition-duration': `${this.duration}s`,
padding: this.isScroll ? `0 ${this.gutter}rpx` : '',
};
// 字体加粗
if (index == this.currentIndex && this.bold) style.fontWeight = 'bold';
if (index == this.currentIndex) {
style.color = this.activeColor;
} else {
style.color = this.inactiveColor;
}
return style;
}
}
},
methods: {
// 设置一个init方法,方便多处调用
async init() {
// 获取tabs组件的尺寸信息
let tabRect = await this.$uGetRect('#' + this.id);
// tabs组件距离屏幕左边的宽度
this.parentLeft = tabRect.left;
// tabs组件的宽度
this.componentWidth = tabRect.width;
this.getTabRect();
},
// 点击某一个tab菜单
clickTab(index) {
// 发送事件给父组件
this.$emit('change', index);
},
// 查询tab的布局信息
getTabRect() {
// 创建节点查询
let query = uni.createSelectorQuery().in(this);
// 历遍所有tab,这里是执行了查询,最终使用exec()会一次性返回查询的数组结果
for (let i = 0; i < this.list.length; i++) {
// 只要size和rect两个参数
query.select(`#u-tab-item-${i}`).fields({
size: true,
rect: true
});
}
// 执行查询,一次性获取多个结果
query.exec(
function(res) {
this.tabQueryInfo = res;
// 初始化滚动条和移动bar的位置
this.scrollByIndex();
}.bind(this)
);
},
// 滚动scroll-view,让活动的tab处于屏幕的中间位置
scrollByIndex() {
// 当前活动tab的布局信息,有tab菜单的width和left(为元素左边界到父元素左边界的距离)等信息
let tabInfo = this.tabQueryInfo[this.currentIndex];
if (!tabInfo) return;
// 活动tab的宽度
let tabWidth = tabInfo.width;
// 活动item的左边到tabs组件左边的距离,用item的left减去tabs的left
let offsetLeft = tabInfo.left - this.parentLeft;
// 将活动的tabs-item移动到屏幕正中间,实际上是对scroll-view的移动
let scrollLeft = offsetLeft - (this.componentWidth - tabWidth) / 2;
this.scrollLeft = scrollLeft < 0 ? 0 : scrollLeft;
// 当前活动item的中点点到左边的距离减去滑块宽度的一半,即可得到滑块所需的移动距离
let left = tabInfo.left + tabInfo.width / 2 - this.parentLeft;
// 计算当前活跃item到组件左边的距离
this.scrollBarLeft = left - uni.upx2px(this.barWidth) / 2;
}
},
mounted() {
this.init();
}
};
</script>
<style lang="scss">
view,
scroll-view {
box-sizing: border-box;
}
::-webkit-scrollbar,
::-webkit-scrollbar,
::-webkit-scrollbar {
display: none;
width: 0 !important;
height: 0 !important;
-webkit-appearance: none;
background: transparent;
}
.u-scroll-box {
position: relative;
}
/* #ifdef H5 */
// 通过样式穿透,隐藏H5下,scroll-view下的滚动条
scroll-view /deep/ ::-webkit-scrollbar {
display: none;
width: 0 !important;
height: 0 !important;
-webkit-appearance: none;
background: transparent;
}
/* #endif */
.u-scroll-view {
width: 100%;
white-space: nowrap;
position: relative;
}
.u-tab-item {
position: relative;
display: inline-block;
text-align: center;
transition-property: background-color, color;
}
.u-tab-bar {
position: absolute;
bottom: 0;
}
.u-tabs-scorll-flex {
display: flex;
justify-content: space-between;
}
</style>
<template>
<view v-if="show" :class="[
disabled ? 'u-disabled' : '',
'u-size-' + size,
'u-shape-' + shape,
'u-mode-' + mode + '-' + type
]"
class="u-tag" :style="[customStyle]" @tap="clickTag">
{{text}}
<view class="u-icon-wrap" @tap.stop>
<u-icon @click="close" size="22" v-if="closeable" name="close" class="u-close-icon" :style="[iconStyle]"></u-icon>
</view>
</view>
</template>
<script>
export default {
// 是否禁用这个标签,禁用的话,会屏蔽点击事件
props: {
// 标签类型info、primary、success、warning、error
type: {
type: String,
default: 'primary'
},
disabled: {
type: [Boolean, String],
default: false
},
// 标签的大小,分为default(默认),mini(较小)
size: {
type: String,
default: 'default'
},
// tag的形状,circle(两边半圆形), square(方形,带圆角),circleLeft(左边是半圆),circleRight(右边是半圆)
shape: {
type: String,
default: 'square'
},
// 标签文字
text: {
type: String,
default: ''
},
// 背景颜色,默认为空字符串,即不处理
bgColor: {
type: String,
default: ''
},
// 标签字体颜色,默认为空字符串,即不处理
color: {
type: String,
default: ''
},
// 镂空形式标签的边框颜色
borderColor: {
type: String,
default: ''
},
// 关闭按钮图标的颜色
closeColor: {
type: String,
default: ''
},
// 点击时返回的索引值,用于区分例遍的数组哪个元素被点击了
index: {
type: [Number, String],
default: ''
},
// 模式选择,dark|light|plain
mode: {
type: String,
default: 'light'
},
// 是否可关闭
closeable: {
type: Boolean,
default: false
},
// 是否显示
show: {
type: Boolean,
default: true
}
},
data() {
return {
}
},
computed: {
customStyle() {
let style = {};
// 文字颜色(如果有此值,会覆盖type值的颜色)
if(this.color) style.color = this.color+"!important";
// tag的背景颜色(如果有此值,会覆盖type值的颜色)
if(this.bgColor) style.backgroundColor = this.bgColor+"!important";
// 如果是镂空型tag,没有传递边框颜色(borderColor)的话,使用文字的颜色(color属性)
if(this.plain && this.color && !this.borderColor) style.borderColor = this.color;
else style.borderColor = this.borderColor;
return style;
},
iconStyle() {
if(!this.closeable) return ;
let style = {};
if(this.size == 'mini') style.fontSize = '20rpx';
else style.fontSize = '22rpx';
if(this.mode == 'plain' || this.mode == 'light') style.color = this.$u.color[this.type];
else if(this.mode == 'dark') style.color = "#ffffff";
if(this.closeColor) style.color = this.closeColor;
return style;
}
},
methods: {
// 标签被点击
clickTag() {
// 如果是disabled状态,不发送点击事件
if(this.disabled) return ;
this.$emit('click', this.index);
},
// 点击标签关闭按钮
close() {
this.$emit('close', this.index);
}
}
}
</script>
<style lang="scss" scoped>
.u-tag {
box-sizing: border-box;
align-items: center;
border-radius: 6rpx;
display: inline-block;
line-height: 1;
}
.u-size-default {
font-size: 22rpx;
padding: 12rpx 22rpx;
}
.u-size-mini {
font-size: 20rpx;
padding: 6rpx 12rpx;
}
.u-mode-light-primary {
background-color: $u-type-primary-light;
color: $u-type-primary;
border: 1px solid rgb(215, 234, 254);
}
.u-mode-light-success {
background-color: $u-type-success-light;
color: $u-type-success;
border: 1px solid #BEF5C8;
}
.u-mode-light-error {
background-color: $u-type-error-light;
color: $u-type-error;
border: 1px solid #fde2e2;
}
.u-mode-light-warning {
background-color: $u-type-warning-light;
color: $u-type-warning;
border: 1px solid #faecd8;
}
.u-mode-light-info {
background-color: $u-type-info-light;
color: $u-type-info;
border: 1px solid #ebeef5;
}
.u-mode-dark-primary {
background-color: $u-type-primary;
color: #FFFFFF;
}
.u-mode-dark-success {
background-color: $u-type-success;
color: #FFFFFF;
}
.u-mode-dark-error {
background-color: $u-type-error;
color: #FFFFFF;
}
.u-mode-dark-warning {
background-color: $u-type-warning;
color: #FFFFFF;
}
.u-mode-dark-info {
background-color: $u-type-info;
color: #FFFFFF;
}
.u-mode-plain-primary {
background-color: #FFFFFF;
color: $u-type-primary;
border: 1px solid $u-type-primary;
}
.u-mode-plain-success {
background-color: #FFFFFF;
color: $u-type-success;
border: 1px solid $u-type-success;
}
.u-mode-plain-error {
background-color: #FFFFFF;
color: $u-type-error;
border: 1px solid $u-type-error;
}
.u-mode-plain-warning {
background-color: #FFFFFF;
color: $u-type-warning;
border: 1px solid $u-type-warning;
}
.u-mode-plain-info {
background-color: #FFFFFF;
color: $u-type-info;
border: 1px solid $u-type-info;
}
.u-disabled {
opacity: 0.55;
}
.u-shape-circle {
border-radius: 100rpx;
}
.u-shape-circleRight {
border-radius: 0 100rpx 100rpx 0;
}
.u-shape-circleLeft {
border-radius: 100rpx 0 0 100rpx;
}
.u-close-icon {
margin-left: 14rpx;
font-size: 22rpx;
color: $u-type-success;
}
.u-icon-wrap {
display: inline-flex;
transform: scale(0.86);
}
<template>
<view v-if="show" :class="[
disabled ? 'u-disabled' : '',
'u-size-' + size,
'u-shape-' + shape,
'u-mode-' + mode + '-' + type
]"
class="u-tag" :style="[customStyle]" @tap="clickTag">
{{text}}
<view class="u-icon-wrap" @tap.stop>
<u-icon @click="close" size="22" v-if="closeable" name="close" class="u-close-icon" :style="[iconStyle]"></u-icon>
</view>
</view>
</template>
<script>
/**
* alertTips 提示
* @description 该组件一般用于标记和选择
* @tutorial https://www.uviewui.com/components/tag.html
* @property {String} type 主题类型(默认primary)
* @property {String} size 标签大小(默认default)
* @property {String} shape 标签形状(默认square)
* @property {String} text 标签的文字内容
* @property {String} bg-color 自定义标签的背景颜色
* @property {String} border-color 标签的边框颜色
* @property {String} close-color 关闭按钮的颜色
* @property {String Number} index 点击标签时,会通过click事件返回该值
* @property {String} mode 模式选择,见官网说明(默认light)
* @property {Boolean} closeable 是否可关闭,设置为true,文字右边会出现一个关闭图标(默认false)
* @property {Boolean} show 标签显示与否(默认true)
* @event {Function} click 点击标签触发
* @event {Function} close closeable为true时,点击标签关闭按钮触发
* @example <u-tag text="雪月夜" type="success" />
*/
export default {
name: "u-tag",
// 是否禁用这个标签,禁用的话,会屏蔽点击事件
props: {
// 标签类型info、primary、success、warning、error
type: {
type: String,
default: 'primary'
},
disabled: {
type: [Boolean, String],
default: false
},
// 标签的大小,分为default(默认),mini(较小)
size: {
type: String,
default: 'default'
},
// tag的形状,circle(两边半圆形), square(方形,带圆角),circleLeft(左边是半圆),circleRight(右边是半圆)
shape: {
type: String,
default: 'square'
},
// 标签文字
text: {
type: String,
default: ''
},
// 背景颜色,默认为空字符串,即不处理
bgColor: {
type: String,
default: ''
},
// 标签字体颜色,默认为空字符串,即不处理
color: {
type: String,
default: ''
},
// 镂空形式标签的边框颜色
borderColor: {
type: String,
default: ''
},
// 关闭按钮图标的颜色
closeColor: {
type: String,
default: ''
},
// 点击时返回的索引值,用于区分例遍的数组哪个元素被点击了
index: {
type: [Number, String],
default: ''
},
// 模式选择,dark|light|plain
mode: {
type: String,
default: 'light'
},
// 是否可关闭
closeable: {
type: Boolean,
default: false
},
// 是否显示
show: {
type: Boolean,
default: true
}
},
data() {
return {
}
},
computed: {
customStyle() {
let style = {};
// 文字颜色(如果有此值,会覆盖type值的颜色)
if (this.color) style.color = this.color + "!important";
// tag的背景颜色(如果有此值,会覆盖type值的颜色)
if (this.bgColor) style.backgroundColor = this.bgColor + "!important";
// 如果是镂空型tag,没有传递边框颜色(borderColor)的话,使用文字的颜色(color属性)
if (this.plain && this.color && !this.borderColor) style.borderColor = this.color;
else style.borderColor = this.borderColor;
return style;
},
iconStyle() {
if (!this.closeable) return;
let style = {};
if (this.size == 'mini') style.fontSize = '20rpx';
else style.fontSize = '22rpx';
if (this.mode == 'plain' || this.mode == 'light') style.color = this.$u.color[this.type];
else if (this.mode == 'dark') style.color = "#ffffff";
if (this.closeColor) style.color = this.closeColor;
return style;
}
},
methods: {
// 标签被点击
clickTag() {
// 如果是disabled状态,不发送点击事件
if (this.disabled) return;
this.$emit('click', this.index);
},
// 点击标签关闭按钮
close() {
this.$emit('close', this.index);
}
}
}
</script>
<style lang="scss" scoped>
.u-tag {
box-sizing: border-box;
align-items: center;
border-radius: 6rpx;
display: inline-block;
line-height: 1;
}
.u-size-default {
font-size: 22rpx;
padding: 12rpx 22rpx;
}
.u-size-mini {
font-size: 20rpx;
padding: 6rpx 12rpx;
}
.u-mode-light-primary {
background-color: $u-type-primary-light;
color: $u-type-primary;
border: 1px solid rgb(215, 234, 254);
}
.u-mode-light-success {
background-color: $u-type-success-light;
color: $u-type-success;
border: 1px solid #BEF5C8;
}
.u-mode-light-error {
background-color: $u-type-error-light;
color: $u-type-error;
border: 1px solid #fde2e2;
}
.u-mode-light-warning {
background-color: $u-type-warning-light;
color: $u-type-warning;
border: 1px solid #faecd8;
}
.u-mode-light-info {
background-color: $u-type-info-light;
color: $u-type-info;
border: 1px solid #ebeef5;
}
.u-mode-dark-primary {
background-color: $u-type-primary;
color: #FFFFFF;
}
.u-mode-dark-success {
background-color: $u-type-success;
color: #FFFFFF;
}
.u-mode-dark-error {
background-color: $u-type-error;
color: #FFFFFF;
}
.u-mode-dark-warning {
background-color: $u-type-warning;
color: #FFFFFF;
}
.u-mode-dark-info {
background-color: $u-type-info;
color: #FFFFFF;
}
.u-mode-plain-primary {
background-color: #FFFFFF;
color: $u-type-primary;
border: 1px solid $u-type-primary;
}
.u-mode-plain-success {
background-color: #FFFFFF;
color: $u-type-success;
border: 1px solid $u-type-success;
}
.u-mode-plain-error {
background-color: #FFFFFF;
color: $u-type-error;
border: 1px solid $u-type-error;
}
.u-mode-plain-warning {
background-color: #FFFFFF;
color: $u-type-warning;
border: 1px solid $u-type-warning;
}
.u-mode-plain-info {
background-color: #FFFFFF;
color: $u-type-info;
border: 1px solid $u-type-info;
}
.u-disabled {
opacity: 0.55;
}
.u-shape-circle {
border-radius: 100rpx;
}
.u-shape-circleRight {
border-radius: 0 100rpx 100rpx 0;
}
.u-shape-circleLeft {
border-radius: 100rpx 0 0 100rpx;
}
.u-close-icon {
margin-left: 14rpx;
font-size: 22rpx;
color: $u-type-success;
}
.u-icon-wrap {
display: inline-flex;
transform: scale(0.86);
}
</style>
<template>
<view class="u-td" :style="[tdStyle]">
<slot></slot>
</view>
</template>
<script>
export default {
props: {
// 宽度,百分比或者具体带单位的值,如30%, 200rpx等,一般使用百分比
width: {
type: [Number, String],
default: 'auto'
}
},
data() {
return {
tr: []
};
},
inject: ['uTable', 'uTr'],
provide() {
return {
uTd: this
}
},
created() {
},
computed: {
tdStyle() {
let style = {};
if(this.width != "auto") style.flex = `0 0 ${this.width}`;
style.textAlign = this.uTable.align;
style.padding = this.tr.length == 0 ? this.uTable.padding : 0;
style.borderBottom = this.tr.length == 0 ? `solid 1px ${this.uTable.borderColor}` : 0;
style.borderRight = this.tr.length == 0 ? `solid 1px ${this.uTable.borderColor}` : 0;
style.fontSize = this.uTable.fontSize + 'rpx';
style.color = this.uTable.color;
return style;
}
}
};
</script>
<style lang="scss" scoped>
.u-td {
display: flex;
flex-direction: column;
flex: 1;
justify-content: center;
font-size: 28rpx;
color: $u-content-color;
align-self: stretch;
box-sizing: border-box;
}
.u-col-1 {
flex: 0 0 calc(100%/12);
}
.u-col-2 {
flex: 0 0 calc(100%/12 * 2);
}
.u-col-3 {
flex: 0 0 calc(100%/12 * 3);
}
.u-col-4 {
flex: 0 0 calc(100%/12 * 4);
}
.u-col-5 {
flex: 0 0 calc(100%/12 * 5);
}
.u-col-6 {
flex: 0 0 calc(100%/12 * 6);
}
.u-col-7 {
flex: 0 0 calc(100%/12 * 7);
}
.u-col-8 {
flex: 0 0 calc(100%/12 * 8);
}
.u-col-9 {
flex: 0 0 calc(100%/12 * 9);
}
.u-col-10 {
flex: 0 0 calc(100%/12 * 10);
}
.u-col-11 {
flex: 0 0 calc(100%/12 * 11);
}
.u-col-12 {
flex: 0 0 calc(100%/12 * 12);
}
<template>
<view class="u-td" :style="[tdStyle]">
<slot></slot>
</view>
</template>
<script>
/**
* alertTips 提示
* @description 表格组件一般用于展示大量结构化数据的场景(搭配<u-table>使用)
* @tutorial https://www.uviewui.com/components/table.html#td-props
* @property {String Number} width 单元格宽度百分比或者具体带单位的值,如30%, 200rpx等,一般使用百分比,单元格宽度默认为均分tr的长度(默认auto)
* @example <u-td>二年级</u-td>
*/
export default {
name: "u-td",
props: {
// 宽度,百分比或者具体带单位的值,如30%, 200rpx等,一般使用百分比
width: {
type: [Number, String],
default: 'auto'
}
},
data() {
return {
tr: []
};
},
inject: ['uTable', 'uTr'],
provide() {
return {
uTd: this
}
},
created() {
},
computed: {
tdStyle() {
let style = {};
if (this.width != "auto") style.flex = `0 0 ${this.width}`;
style.textAlign = this.uTable.align;
style.padding = this.tr.length == 0 ? this.uTable.padding : 0;
style.borderBottom = this.tr.length == 0 ? `solid 1px ${this.uTable.borderColor}` : 0;
style.borderRight = this.tr.length == 0 ? `solid 1px ${this.uTable.borderColor}` : 0;
style.fontSize = this.uTable.fontSize + 'rpx';
style.color = this.uTable.color;
return style;
}
}
};
</script>
<style lang="scss" scoped>
.u-td {
display: flex;
flex-direction: column;
flex: 1;
justify-content: center;
font-size: 28rpx;
color: $u-content-color;
align-self: stretch;
box-sizing: border-box;
}
.u-col-1 {
flex: 0 0 calc(100%/12);
}
.u-col-2 {
flex: 0 0 calc(100%/12 * 2);
}
.u-col-3 {
flex: 0 0 calc(100%/12 * 3);
}
.u-col-4 {
flex: 0 0 calc(100%/12 * 4);
}
.u-col-5 {
flex: 0 0 calc(100%/12 * 5);
}
.u-col-6 {
flex: 0 0 calc(100%/12 * 6);
}
.u-col-7 {
flex: 0 0 calc(100%/12 * 7);
}
.u-col-8 {
flex: 0 0 calc(100%/12 * 8);
}
.u-col-9 {
flex: 0 0 calc(100%/12 * 9);
}
.u-col-10 {
flex: 0 0 calc(100%/12 * 10);
}
.u-col-11 {
flex: 0 0 calc(100%/12 * 11);
}
.u-col-12 {
flex: 0 0 calc(100%/12 * 12);
}
</style>
<template>
<view class="u-th" :style="[thStyle]">
<slot></slot>
</view>
</template>
<script>
export default {
props: {
// 宽度,百分比或者具体带单位的值,如30%, 200rpx等,一般使用百分比
width: {
type: [Number, String],
default: ''
}
},
data() {
return {
};
},
inject: ['uTable'],
computed: {
thStyle() {
let style = {};
if(this.width) style.flex = `0 0 ${this.width}%`;
style.textAlign = this.uTable.align;
style.padding = this.uTable.padding;
style.borderBottom = `solid 1px ${this.uTable.borderColor}`;
style.borderRight = `solid 1px ${this.uTable.borderColor}`;
Object.assign(style, this.uTable.thStyle);
return style;
}
}
};
</script>
<style lang="scss" scoped>
.u-th {
display: flex;
flex-direction: column;
flex: 1;
justify-content: center;
font-size: 28rpx;
color: $u-main-color;
font-weight: bold;
background-color: rgb(245, 246, 248);
}
<template>
<view class="u-th" :style="[thStyle]">
<slot></slot>
</view>
</template>
<script>
/**
* alertTips 提示
* @description 表格组件一般用于展示大量结构化数据的场景(搭配<u-table>使用)
* @tutorial https://www.uviewui.com/components/table.html#td-props
* @property {String Number} width 标题单元格宽度百分比或者具体带单位的值,如30%, 200rpx等,一般使用百分比,单元格宽度默认为均分tr的长度
* @example
*/
export default {
name: "u-th",
props: {
// 宽度,百分比或者具体带单位的值,如30%, 200rpx等,一般使用百分比
width: {
type: [Number, String],
default: ''
}
},
data() {
return {
};
},
inject: ['uTable'],
computed: {
thStyle() {
let style = {};
if (this.width) style.flex = `0 0 ${this.width}%`;
style.textAlign = this.uTable.align;
style.padding = this.uTable.padding;
style.borderBottom = `solid 1px ${this.uTable.borderColor}`;
style.borderRight = `solid 1px ${this.uTable.borderColor}`;
Object.assign(style, this.uTable.thStyle);
return style;
}
}
};
</script>
<style lang="scss" scoped>
.u-th {
display: flex;
flex-direction: column;
flex: 1;
justify-content: center;
font-size: 28rpx;
color: $u-main-color;
font-weight: bold;
background-color: rgb(245, 246, 248);
}
</style>
<template>
<view class="u-time-axis-item">
<slot name="content" />
<view class="u-time-axis-node" :style="[nodeStyle]">
<slot name="node">
<view class="u-dot">
</view>
</slot>
</view>
</view>
</template>
<script>
export default {
props: {
// 节点的背景颜色
bgColor: {
type: String,
default: "#ffffff"
},
// 节点左边图标绝对定位的top值
nodeTop: {
type: [String, Number],
default: ""
}
},
data() {
return {
}
},
computed: {
nodeStyle() {
let style = {
backgroundColor: this.bgColor,
};
if(this.nodeTop != "") style.top = this.nodeTop + 'rpx';
return style;
}
}
}
</script>
<style lang="scss" scoped>
.u-time-axis-item {
display: flex;
flex-direction: column;
width: 100%;
position: relative;
margin-bottom: 32rpx;
}
.u-time-axis-node {
position: absolute;
top: 12rpx;
left: -40rpx;
transform-origin: 0;
transform: translateX(-50%);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
font-size: 24rpx;
}
.u-dot {
height: 16rpx;
width: 16rpx;
border-radius: 100rpx;
background: #ddd;
}
<template>
<view class="u-time-axis-item">
<slot name="content" />
<view class="u-time-axis-node" :style="[nodeStyle]">
<slot name="node">
<view class="u-dot">
</view>
</slot>
</view>
</view>
</template>
<script>
/**
* alertTips 提示
* @description 时间轴组件一般用于物流信息展示,各种跟时间相关的记录等场景。(搭配<u-time-line>使用)
* @tutorial https://www.uviewui.com/components/timeLine.html
* @property {String} bg-color 左边节点的背景颜色,一般通过slot内容自定义背景颜色即可(默认#ffffff)
* @property {String Number} node-top 节点左边图标绝对定位的top值,单位rpx
* @example <u-time-line-item node-top="2">...</u-time-line-item>
*/
export default {
name: "u-time-line-item",
props: {
// 节点的背景颜色
bgColor: {
type: String,
default: "#ffffff"
},
// 节点左边图标绝对定位的top值
nodeTop: {
type: [String, Number],
default: ""
}
},
data() {
return {
}
},
computed: {
nodeStyle() {
let style = {
backgroundColor: this.bgColor,
};
if (this.nodeTop != "") style.top = this.nodeTop + 'rpx';
return style;
}
}
}
</script>
<style lang="scss" scoped>
.u-time-axis-item {
display: flex;
flex-direction: column;
width: 100%;
position: relative;
margin-bottom: 32rpx;
}
.u-time-axis-node {
position: absolute;
top: 12rpx;
left: -40rpx;
transform-origin: 0;
transform: translateX(-50%);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
font-size: 24rpx;
}
.u-dot {
height: 16rpx;
width: 16rpx;
border-radius: 100rpx;
background: #ddd;
}
</style>
<template>
<view class="u-time-axis">
<slot />
</view>
</template>
<script>
export default {
data() {
return {
}
}
}
</script>
<style lang="scss" scoped>
.u-time-axis {
padding-left: 40rpx;
position: relative;
}
.u-time-axis::before {
content: " ";
position: absolute;
left: 0;
top: 12rpx;
width: 1px;
bottom: 0;
border-left: 1px solid #ddd;
transform-origin: 0 0;
transform: scaleX(0.5);
}
<template>
<view class="u-time-axis">
<slot />
</view>
</template>
<script>
/**
* alertTips 提示
* @description 时间轴组件一般用于物流信息展示,各种跟时间相关的记录等场景。
* @tutorial https://www.uviewui.com/components/timeLine.html
* @example <u-time-line></u-time-line>
*/
export default {
name: "u-time-line",
data() {
return {
}
}
}
</script>
<style lang="scss" scoped>
.u-time-axis {
padding-left: 40rpx;
position: relative;
}
.u-time-axis::before {
content: " ";
position: absolute;
left: 0;
top: 12rpx;
width: 1px;
bottom: 0;
border-left: 1px solid #ddd;
transform-origin: 0 0;
transform: scaleX(0.5);
}
</style>
<template>
<view class="u-toast" :class="[isShow ? 'u-show' : '', 'u-type-' + config.type, 'u-postion-' + config.postion]" :style="{
padding: isShow ? '0 40rpx' : 0,
zIndex: uZIndex
}">
<view class="u-icon-wrap">
<u-icon v-if="config.icon" class="u-icon" :name="iconName" :size="30" :color="$u.color[config.type]"></u-icon>
</view>
<text class="u-text">{{config.title}}</text>
</view>
</template>
<script>
export default {
props: {
// z-index值
zIndex: {
type: [Number, String],
default: ''
},
},
data() {
return {
isShow: false,
timer: null, // 定时器
config: {
params: {}, // URL跳转的参数,对象
title: '', // 显示文本
type: '', // 主题类型,primary,success,error,warning,black
duration: 2000, // 显示的时间,毫秒
isTab: false, // 是否跳转tab页面
url: '', // toast消失后是否跳转页面,有则跳转
icon: true, // 显示的图标
postion: 'center', // toast出现的位置
}
};
},
computed: {
iconName() {
// 只有不为none,并且type为error|warning|succes|info时候,才显示图标
if (['error', 'warning', 'success', 'info'].indexOf(this.config.type) >= 0 && this.config.icon) {
let icon = this.$u.type2icon(this.config.type);
return icon ;
}
},
uZIndex() {
// 显示toast时候,如果用户有传递z-index值,有限使用
return this.isShow ? (this.zIndex ? this.zIndex : this.$u.zIndex.toast) : '-1';
}
},
methods: {
// 显示toast组件,由父组件通过this.$refs.xxx.show(options)形式调用
show(options) {
this.config = Object.assign(this.config, options);
if (this.timer) {
// 清除定时器
clearTimeout(this.timer);
this.timer = null;
}
this.isShow = true;
this.timer = setTimeout(() => {
// 倒计时结束,清除定时器,隐藏toast组件
this.isShow = false;
clearTimeout(this.timer);
this.timer = null;
this.timeEnd();
}, this.config.duration);
},
// 隐藏toast组件,由父组件通过this.$refs.xxx.hide()形式调用
hide() {
this.isShow = false;
if (this.timer) {
// 清除定时器
clearTimeout(this.timer);
this.timer = null;
}
},
// 倒计时结束之后,进行的一些操作
timeEnd() {
// 如果带有url值,根据isTab为true或者false进行跳转
if (this.config.url) {
// 如果url没有"/"开头,添加上,因为uni的路由跳转需要"/"开头
if (this.config.url[0] != '/') this.config.url = '/' + this.config.url;
// 判断是否有传递显式的参数
if (Object.keys(this.config.params).length) {
// 判断用户传递的url中,是否带有参数
// 使用正则匹配,主要依据是判断是否有"/","?","="等,如“/page/index/index?name=mary"
// 如果有params参数,转换后无需带上"?"
let query = '';
if (/.*\/.*\?.*=.*/.test(this.config.url)) {
// object对象转为get类型的参数
query = this.$u.queryParams(this.config.params, false);
this.config.url = this.config.url + "&" + query;
} else {
query = this.$u.queryParams(this.config.params);
this.config.url += query;
}
}
// 如果是跳转tab页面,就使用uni.switchTab
if (this.config.isTab) {
uni.switchTab({
url: this.config.url
});
} else {
uni.navigateTo({
url: this.config.url
});
}
}
}
}
};
</script>
<style lang="scss" scoped>
.u-toast {
position: fixed;
z-index: -1;
transition: opacity 0.3s;
text-align: center;
color: #fff;
border-radius: 8rpx;
background: #585858;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
opacity: 0;
}
.u-toast.u-show {
opacity: 1;
z-index: 9999999;
}
.u-text {
word-break: keep-all;
white-space:nowrap;
line-height: normal;
}
.u-icon {
margin-right: 10rpx;
display: flex;
align-items: center;
line-height: normal;
}
.u-postion-center {
left: 50%;
top: 50%;
transform: translateX(-50%) translateY(-50%);
}
.u-postion-top {
left: 50%;
top: 20%;
transform: translateX(-50%) translateY(-50%);
}
.u-postion-bottom {
left: 50%;
bottom: 20%;
transform: translateX(-50%) translateY(-50%);
}
.u-type-primary {
color: $u-type-primary;
background-color: $u-type-primary-light;
border: 1px solid rgb(215, 234, 254);
}
.u-type-success {
color: $u-type-success;
background-color: $u-type-success-light;
border: 1px solid #BEF5C8;
}
.u-type-error {
color: $u-type-error;
background-color: $u-type-error-light;
border: 1px solid #fde2e2;
}
.u-type-warning {
color: $u-type-warning;
background-color: $u-type-warning-light;
border: 1px solid #faecd8;
}
.u-type-info {
color: $u-type-info;
background-color: $u-type-info-light;
border: 1px solid #ebeef5;
}
.u-type-default {
color: #fff;
background-color: #585858;
}
<template>
<view class="u-toast" :class="[isShow ? 'u-show' : '', 'u-type-' + config.type, 'u-postion-' + config.postion]" :style="{
padding: isShow ? '0 40rpx' : 0,
zIndex: uZIndex
}">
<view class="u-icon-wrap">
<u-icon v-if="config.icon" class="u-icon" :name="iconName" :size="30" :color="$u.color[config.type]"></u-icon>
</view>
<text class="u-text">{{config.title}}</text>
</view>
</template>
<script>
/**
* alertTips 提示
* @description 此组件表现形式类似uni的uni.showToastAPI,但也有不同的地方。
* @tutorial https://www.uviewui.com/components/toast.html
* @property {String} z-index toast展示时的z-index值
* @event {Function} show 显示toast,如需一进入页面就显示toast,请在onReady生命周期调用
* @example <u-toast ref="uToast" />
*/
export default {
name: "u-toast",
props: {
// z-index值
zIndex: {
type: [Number, String],
default: ''
},
},
data() {
return {
isShow: false,
timer: null, // 定时器
config: {
params: {}, // URL跳转的参数,对象
title: '', // 显示文本
type: '', // 主题类型,primary,success,error,warning,black
duration: 2000, // 显示的时间,毫秒
isTab: false, // 是否跳转tab页面
url: '', // toast消失后是否跳转页面,有则跳转
icon: true, // 显示的图标
postion: 'center', // toast出现的位置
}
};
},
computed: {
iconName() {
// 只有不为none,并且type为error|warning|succes|info时候,才显示图标
if (['error', 'warning', 'success', 'info'].indexOf(this.config.type) >= 0 && this.config.icon) {
let icon = this.$u.type2icon(this.config.type);
return icon;
}
},
uZIndex() {
// 显示toast时候,如果用户有传递z-index值,有限使用
return this.isShow ? (this.zIndex ? this.zIndex : this.$u.zIndex.toast) : '-1';
}
},
methods: {
// 显示toast组件,由父组件通过this.$refs.xxx.show(options)形式调用
show(options) {
this.config = Object.assign(this.config, options);
if (this.timer) {
// 清除定时器
clearTimeout(this.timer);
this.timer = null;
}
this.isShow = true;
this.timer = setTimeout(() => {
// 倒计时结束,清除定时器,隐藏toast组件
this.isShow = false;
clearTimeout(this.timer);
this.timer = null;
this.timeEnd();
}, this.config.duration);
},
// 隐藏toast组件,由父组件通过this.$refs.xxx.hide()形式调用
hide() {
this.isShow = false;
if (this.timer) {
// 清除定时器
clearTimeout(this.timer);
this.timer = null;
}
},
// 倒计时结束之后,进行的一些操作
timeEnd() {
// 如果带有url值,根据isTab为true或者false进行跳转
if (this.config.url) {
// 如果url没有"/"开头,添加上,因为uni的路由跳转需要"/"开头
if (this.config.url[0] != '/') this.config.url = '/' + this.config.url;
// 判断是否有传递显式的参数
if (Object.keys(this.config.params).length) {
// 判断用户传递的url中,是否带有参数
// 使用正则匹配,主要依据是判断是否有"/","?","="等,如“/page/index/index?name=mary"
// 如果有params参数,转换后无需带上"?"
let query = '';
if (/.*\/.*\?.*=.*/.test(this.config.url)) {
// object对象转为get类型的参数
query = this.$u.queryParams(this.config.params, false);
this.config.url = this.config.url + "&" + query;
} else {
query = this.$u.queryParams(this.config.params);
this.config.url += query;
}
}
// 如果是跳转tab页面,就使用uni.switchTab
if (this.config.isTab) {
uni.switchTab({
url: this.config.url
});
} else {
uni.navigateTo({
url: this.config.url
});
}
}
}
}
};
</script>
<style lang="scss" scoped>
.u-toast {
position: fixed;
z-index: -1;
transition: opacity 0.3s;
text-align: center;
color: #fff;
border-radius: 8rpx;
background: #585858;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
opacity: 0;
}
.u-toast.u-show {
opacity: 1;
z-index: 9999999;
}
.u-text {
word-break: keep-all;
white-space: nowrap;
line-height: normal;
}
.u-icon {
margin-right: 10rpx;
display: flex;
align-items: center;
line-height: normal;
}
.u-postion-center {
left: 50%;
top: 50%;
transform: translateX(-50%) translateY(-50%);
}
.u-postion-top {
left: 50%;
top: 20%;
transform: translateX(-50%) translateY(-50%);
}
.u-postion-bottom {
left: 50%;
bottom: 20%;
transform: translateX(-50%) translateY(-50%);
}
.u-type-primary {
color: $u-type-primary;
background-color: $u-type-primary-light;
border: 1px solid rgb(215, 234, 254);
}
.u-type-success {
color: $u-type-success;
background-color: $u-type-success-light;
border: 1px solid #BEF5C8;
}
.u-type-error {
color: $u-type-error;
background-color: $u-type-error-light;
border: 1px solid #fde2e2;
}
.u-type-warning {
color: $u-type-warning;
background-color: $u-type-warning-light;
border: 1px solid #faecd8;
}
.u-type-info {
color: $u-type-info;
background-color: $u-type-info-light;
border: 1px solid #ebeef5;
}
.u-type-default {
color: #fff;
background-color: #585858;
}
</style>
<template>
<view class="u-tips" :class="['u-' + type, isShow ? 'u-tip-show' : '']" :style="{
top: navbarHeight + 'px',
zIndex: uZIndex
}">{{ title }}</view>
</template>
<script>
export default {
props: {
// 导航栏高度,用于提示的初始化
navbarHeight: {
type: [Number, String],
// #ifndef H5
default: 0,
// #endif
// #ifdef H5
default: 44,
// #endif
},
// z-index值
zIndex: {
type: [Number, String],
default: ''
}
},
data() {
return {
timer: null, // 定时器
isShow: false, // 是否显示消息组件
title: '', // 组件中显示的消息内容
type: 'primary', // 消息的类型(颜色不同),primary,success,error,warning,info
duration: 2000, // 组件显示的时间,单位为毫秒
};
},
computed: {
uZIndex() {
return this.zIndex ? this.zIndex : this.$u.zIndex.topTips;
}
},
methods: {
show(config = {}) {
// 先清除定时器(可能是上一次定义的,需要清除了再开始新的)
clearTimeout(this.timer);
// 时间,内容,类型主题(type)等参数
if (config.duration) this.duration = config.duration;
if (config.type) this.type = config.type;
this.title = config.title;
this.isShow = true;
// 倒计时
this.timer = setTimeout(() => {
this.isShow = false;
clearTimeout(this.timer);
this.timer = null;
}, this.duration);
}
}
};
</script>
<style lang="scss" scoped>
view {
box-sizing: border-box;
}
// 顶部弹出类型样式
.u-tips {
width: 100%;
position: fixed;
z-index: 1;
padding: 20rpx 30rpx;
color: #FFFFFF;
font-size: 28rpx;
left: 0;
right: 0;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
// 此处为最核心点,translateY(-100%)意味着将其从Y轴隐藏(隐藏到顶部(h5)或者说导航栏(app)下面)
transform: translateY(-100%);
transition: all 0.35s linear;
}
.u-tip-show {
transform: translateY(0);
opacity: 1;
z-index: 99;
}
.u-primary {
background: $u-type-primary;
}
.u-success {
background: $u-type-success;
}
.u-warning {
background: $u-type-warning;
}
.u-error {
background: $u-type-error;
}
.u-info {
background: $u-type-info;
}
<template>
<view class="u-tips" :class="['u-' + type, isShow ? 'u-tip-show' : '']" :style="{
top: navbarHeight + 'px',
zIndex: uZIndex
}">{{ title }}</view>
</template>
<script>
/**
* alertTips 提示
* @description 该组件一般用于页面顶部向下滑出一个提示,尔后自动收起的场景。
* @tutorial https://www.uviewui.com/components/topTips.html
* @property {String Number} navbar-height 导航栏高度(包含状态栏高度在内),单位PX
* @property {String Number} z-index z-index值(默认975)
* @example <u-top-tips ref="uTips" type="success" duration="1500"></u-top-tips>
*/
export default {
name: "u-top-tips",
props: {
// 导航栏高度,用于提示的初始化
navbarHeight: {
type: [Number, String],
// #ifndef H5
default: 0,
// #endif
// #ifdef H5
default: 44,
// #endif
},
// z-index值
zIndex: {
type: [Number, String],
default: ''
}
},
data() {
return {
timer: null, // 定时器
isShow: false, // 是否显示消息组件
title: '', // 组件中显示的消息内容
type: 'primary', // 消息的类型(颜色不同),primary,success,error,warning,info
duration: 2000, // 组件显示的时间,单位为毫秒
};
},
computed: {
uZIndex() {
return this.zIndex ? this.zIndex : this.$u.zIndex.topTips;
}
},
methods: {
show(config = {}) {
// 先清除定时器(可能是上一次定义的,需要清除了再开始新的)
clearTimeout(this.timer);
// 时间,内容,类型主题(type)等参数
if (config.duration) this.duration = config.duration;
if (config.type) this.type = config.type;
this.title = config.title;
this.isShow = true;
// 倒计时
this.timer = setTimeout(() => {
this.isShow = false;
clearTimeout(this.timer);
this.timer = null;
}, this.duration);
}
}
};
</script>
<style lang="scss" scoped>
view {
box-sizing: border-box;
}
// 顶部弹出类型样式
.u-tips {
width: 100%;
position: fixed;
z-index: 1;
padding: 20rpx 30rpx;
color: #FFFFFF;
font-size: 28rpx;
left: 0;
right: 0;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
// 此处为最核心点,translateY(-100%)意味着将其从Y轴隐藏(隐藏到顶部(h5)或者说导航栏(app)下面)
transform: translateY(-100%);
transition: all 0.35s linear;
}
.u-tip-show {
transform: translateY(0);
opacity: 1;
z-index: 99;
}
.u-primary {
background: $u-type-primary;
}
.u-success {
background: $u-type-success;
}
.u-warning {
background: $u-type-warning;
}
.u-error {
background: $u-type-error;
}
.u-info {
background: $u-type-info;
}
</style>
<template>
<view class="u-tr">
<slot></slot>
</view>
</template>
<script>
export default {
inject: ['uTable', 'uTd'],
provide() {
return {
uTr: this,
};
},
created() {
if(this.uTd && this.uTd.tr) {
this.uTd.tr.push(this);
}
}
}
</script>
<style lang="scss" scoped>
.u-tr {
display: flex;
}
<template>
<view class="u-tr">
<slot></slot>
</view>
</template>
<script>
/**
* alertTips 提示
* @description 表格组件一般用于展示大量结构化数据的场景(搭配<u-table>使用)
* @tutorial https://www.uviewui.com/components/table.html
* @example <u-tr></u-tr>
*/
export default {
name: "u-tr",
inject: ['uTable', 'uTd'],
provide() {
return {
uTr: this,
};
},
created() {
if (this.uTd && this.uTd.tr) {
this.uTd.tr.push(this);
}
}
}
</script>
<style lang="scss" scoped>
.u-tr {
display: flex;
}
</style>
<template>
<view class="u-upload" v-if="!disabled">
<view v-if="showUploadList" class="u-list-item u-preview-wrap" v-for="(item, index) in lists" :key="index">
<view v-if="deletable" class="u-delete-icon" @tap.stop="deleteItem(index)">
<u-icon class="u-icon" name="close" size="20" color="#ffffff"></u-icon>
</view>
<u-line-progress v-if="showProgress && item.progress > 0 && !item.error" :show-percent="false" height="16" class="u-progress" :percent="item.progress"></u-line-progress>
<view @tap.stop="retry(index)" v-if="item.error" class="u-error-btn">点击重试</view>
<image @tap.stop="doPreviewImage(item.url || item.path, index)" class="u-preview-image" v-if="!item.isImage" :src=" item.url || item.path "
:mode="imageMode"></image>
</view>
<slot name="file" :file="lists"></slot>
<view style="display: inline-block;" @tap="selectFile" v-if="maxCount > lists.length">
<slot name="addBtn"></slot>
<view v-if="!customBtn" class="u-list-item u-add-wrap" hover-class="u-add-wrap__hover" hover-stay-time="150">
<u-icon name="plus" class="u-add-btn" size="40"></u-icon>
<view class="u-add-tips">{{uploadText}}</view>
</view>
</view>
</view>
</template>
<script>
export default {
props: {
//是否显示组件自带的图片预览功能
showUploadList: {
type: Boolean,
default: true
},
// 后端地址
action: {
type: String,
default: ''
},
// 最大上传数量
maxCount: {
type: [String, Number],
default: 52
},
// 是否显示进度条
showProgress: {
type: Boolean,
default: true
},
// 是否启用
disabled: {
type: Boolean,
default: false
},
// 预览上传的图片时的裁剪模式,和image组件mode属性一致
imageMode: {
type: String,
default: 'aspectFill'
},
// 头部信息
header: {
type: Object,
default () {
return {}
}
},
// 额外携带的参数
formData: {
type: Object,
default () {
return {}
}
},
// 上传的文件字段名
name: {
type: String,
default: 'file'
},
// 所选的图片的尺寸, 可选值为original compressed
sizeType: {
type: Array,
default () {
return ['original', 'compressed']
}
},
sourceType: {
type: Array,
default () {
return ['album', 'camera']
}
},
// 是否在点击预览图后展示全屏图片预览
previewFullImage: {
type: Boolean,
default: true
},
// 是否开启图片多选,部分安卓机型不支持
multiple: {
type: Boolean,
default: true
},
// 是否展示删除按钮
deletable: {
type: Boolean,
default: true
},
// 文件大小限制,单位为byte
maxSize: {
type: [String, Number],
default: Number.MAX_VALUE
},
// 显示已上传的文件列表
fileList: {
type: Array,
default () {
return []
}
},
// 上传区域的提示文字
uploadText: {
type: String,
default: '选择图片'
},
// 是否自动上传
autoUpload: {
type: Boolean,
default: true
},
// 是否显示toast消息提示
showTips: {
type: Boolean,
default: true
},
// 是否通过slot自定义传入选择图标的按钮
customBtn: {
type: Boolean,
default: false
}
},
mounted() {
},
data() {
return {
lists: [],
isInCount: true,
uploading: false
}
},
watch: {
fileList: {
immediate: true,
handler(val) {
val.map(value => {
this.lists.push({url: value.url, error: false, progress: 100});
})
}
}
},
methods: {
// 选择图片
selectFile() {
if (this.disabled) return;
const {
name = '', maxCount, multiple, maxSize, sizeType, lists, camera, compressed, maxDuration, sourceType
} = this;
let chooseFile = null;
const newMaxCount = maxCount - lists.length;
// 设置为只选择图片的时候使用 chooseImage 来实现
chooseFile = new Promise((resolve, reject) => {
wx.chooseImage({
count: multiple ? (newMaxCount > 9 ? 9 : newMaxCount) : 1,
sourceType: sourceType,
sizeType,
success: resolve,
fail: reject
});
});
chooseFile
.then((res) => {
let file = null;
let listOldLength = this.lists.length;
res.tempFiles.map((val, index) => {
// 如果是非多选,index大于等于1或者超出最大限制数量时,不处理
if (!multiple && index >= 1) return;
if (val.size > maxSize) {
this.$emit('on-oversize', val, this.lists);
this.showToast('超出允许的文件大小');
} else {
if(maxCount <= lists.length) {
this.$emit('on-exceed', val, this.lists);
this.showToast('超出最大允许的文件个数');
return ;
}
lists.push({
url: val.path,
progress: 0,
error: false
});
}
})
// 每次图片选择完,抛出一个事件,并将当前内部选择的图片数组抛出去
this.$emit('on-choose-complete', this.lists);
if(this.autoUpload) this.uploadFile(listOldLength);
})
.catch(error => {
// this.$emit('on-error', error);
});
},
// 提示用户消息
showToast(message, force = false) {
if(this.showTips || force) {
uni.showToast({
title: message,
icon: "none"
});
}
},
// 该方法供用户通过ref调用,手动上传
upload() {
this.uploadFile();
},
// 对失败的图片重新上传
retry(index) {
this.lists[index].progress = 0;
this.lists[index].error = false;
this.lists[index].response = null;
uni.showLoading({
title: '重新上传'
});
this.uploadFile(index);
},
// 上传图片
uploadFile(index = 0) {
if (this.disabled) return;
if(this.uploading) return ;
// 全部上传完成
if (index >= this.lists.length) {
this.$emit('on-uploaded', this.lists);
return ;
}
// 检查上传地址
if (!this.action) {
this.showToast('请配置上传地址', true);
return;
}
// 检查是否是已上传或者正在上传中
if (this.lists[index].progress == 100) {
if(this.autoUpload == false) this.uploadFile(index + 1);
return;
}
this.lists[index].error = false;
this.uploading = true;
// 创建上传对象
const task = uni.uploadFile({
url: this.action,
filePath: this.lists[index].url,
name: this.name,
formData: this.formData,
header: this.header,
success: (res) => {
if (res.statusCode != 200) {
this.uploadError(index, res.data);
} else {
// 上传成功
this.lists[index].response = res.data;
this.lists[index].progress = 100;
this.lists[index].error = false;
this.$emit('on-success', res.data, index, this.lists);
}
},
fail: (e) => {
this.uploadError(index, e);
},
complete: (res) => {
uni.hideLoading();
this.uploading = false;
this.uploadFile(index + 1);
this.$emit('on-change', res, index, this.lists);
}
});
task.onProgressUpdate((res) => {
if (res.progress > 0) {
this.lists[index].progress = res.progress;
this.$emit('on-progress', res, index, this.lists);
}
});
},
// 上传失败
uploadError(index, err) {
this.lists[index].progress = 0;
this.lists[index].error = true;
this.lists[index].response = null;
this.$emit('on-error', err, index, this.lists);
this.showToast('上传失败,请重试');
},
// 删除一个图片
deleteItem(index) {
uni.showModal({
title: '提示',
content: '您确定要删除此项吗?',
success: res => {
if (res.confirm) {
if (this.lists[index].process < 100 && this.lists[index].process > 0) {
typeof this.lists[index].uploadTask != 'undefined' && this.lists[index].uploadTask.abort();
}
this.lists.splice(index, 1);
this.$forceUpdate();
this.$emit('on-remove', index, this.lists);
this.showToast('移除成功');
}
}
});
},
// 预览图片
doPreviewImage(url, index) {
if (!this.previewFullImage)
return;
const images = this.lists.map(item => item.url || item.path);
uni.previewImage({
urls: images,
current: url,
success: () => {
this.$emit('on-preview', url, this.lists);
},
fail: () => {
uni.showToast({
title: '预览图片失败',
icon: 'none'
});
}
});
}
}
}
</script>
<style lang="scss" scoped>
.u-upload {
display: flex;
flex-wrap: wrap;
align-items: center;
}
.u-list-item {
width: 200rpx;
height: 200rpx;
overflow: hidden;
margin: 10rpx;
background: rgb(244, 245, 246);
position: relative;
border-radius: 10rpx;
display: inline-flex;
align-items: center;
justify-content: center;
}
.u-preview-wrap {
border: 1px solid rgb(235, 236, 238);
}
.u-add-wrap {
flex-direction: column;
color: $u-content-color;
font-size: 28rpx;
}
.u-add-tips {
margin-top: 20rpx;
}
.u-add-wrap__hover {
background-color: rgb(235, 236, 238);
}
.u-preview-image {
display: block;
width: 100%;
height: 100%;
}
.u-delete-icon {
position: absolute;
top: 10rpx;
right: 10rpx;
z-index: 10;
background-color: $u-type-error;
border-radius: 100rpx;
width: 44rpx;
height: 44rpx;
display: flex;
align-items: center;
justify-content: center;
}
.u-icon {
display: flex;
align-items: center;
justify-content: center;
}
.u-progress {
position: absolute;
bottom: 10rpx;
left: 8rpx;
right: 8rpx;
z-index: 9;
width: auto;
}
.u-error-btn {
color: #FFFFFF;
background-color: $u-type-error;
font-size: 20rpx;
padding: 4px 0;
text-align: center;
position: absolute;
bottom: 0;
left: 0;
right: 0;
z-index: 9;
line-height: 1;
}
<template>
<view class="u-upload" v-if="!disabled">
<view v-if="showUploadList" class="u-list-item u-preview-wrap" v-for="(item, index) in lists" :key="index">
<view v-if="deletable" class="u-delete-icon" @tap.stop="deleteItem(index)">
<u-icon class="u-icon" name="close" size="20" color="#ffffff"></u-icon>
</view>
<u-line-progress v-if="showProgress && item.progress > 0 && !item.error" :show-percent="false" height="16" class="u-progress"
:percent="item.progress"></u-line-progress>
<view @tap.stop="retry(index)" v-if="item.error" class="u-error-btn">点击重试</view>
<image @tap.stop="doPreviewImage(item.url || item.path, index)" class="u-preview-image" v-if="!item.isImage" :src=" item.url || item.path "
:mode="imageMode"></image>
</view>
<slot name="file" :file="lists"></slot>
<view style="display: inline-block;" @tap="selectFile" v-if="maxCount > lists.length">
<slot name="addBtn"></slot>
<view v-if="!customBtn" class="u-list-item u-add-wrap" hover-class="u-add-wrap__hover" hover-stay-time="150">
<u-icon name="plus" class="u-add-btn" size="40"></u-icon>
<view class="u-add-tips">{{uploadText}}</view>
</view>
</view>
</view>
</template>
<script>
/**
* alertTips 提示
* @description 该组件用于上传图片场景
* @tutorial https://www.uviewui.com/components/upload.html
* @property {String} action 服务器上传地址
* @property {String Number} max-count 最大选择图片的数量(默认99)
* @property {Boolean} custom-btn 如果需要自定义选择图片的按钮,设置为true(默认false)
* @property {Boolean} show-progress 是否显示进度条(默认true)
* @property {Boolean} disabled 是否启用(显示/移仓)组件(默认false)
* @property {String} image-mode 预览图片等显示模式,可选值为uni的image的mode属性值(默认aspectFill)
* @property {Object} header 上传携带的头信息,对象形式
* @property {Object} form-data 上传额外携带的参数
* @property {String} name 上传文件的字段名,供后端获取使用(默认file)
* @property {Array<String>} size-type original 原图,compressed 压缩图,默认二者都有(默认['original', 'compressed'])
* @property {Array<String>} source-type 选择图片的来源,album-从相册选图,camera-使用相机,默认二者都有(默认['album', 'camera'])
* @property {Boolean} preview-full-image 是否可以通过uni.previewImage预览已选择的图片(默认true)
* @property {Boolean} multiple 是否开启图片多选,部分安卓机型不支持(默认true)
* @property {Boolean} deletable 是否显示删除图片的按钮(默认true)
* @property {String Number} max-size 选择单个文件的最大大小,单位B(byte),默认不限制(默认Number.MAX_VALUE)
* @property {Array<Object>} file-list 默认显示的图片列表,数组元素为对象,必须提供url属性
* @property {Boolean} upload-text 选择图片按钮的提示文字(默认“选择图片”)
* @property {Boolean} auto-upload 选择完图片是否自动上传,见上方说明(默认true)
* @property {Boolean} show-tips 特殊情况下是否自动提示toast,见上方说明(默认true)
* @property {Boolean} show-upload-list 是否显示组件内部的图片预览(默认true)
* @event {Function} on-oversize 图片大小超出最大允许大小
* @event {Function} on-preview 全屏预览图片时触发
* @event {Function} on-remove 移除图片时触发
* @event {Function} on-success 图片上传成功时触发
* @event {Function} on-change 图片上传后,无论成功或者失败都会触发
* @event {Function} on-error 图片上传失败时触发
* @event {Function} on-progress 图片上传过程中的进度变化过程触发
* @event {Function} on-uploaded 所有图片上传完毕触发
* @event {Function} on-choose-complete 每次选择图片后触发,只是让外部可以得知每次选择后,内部的文件列表
* @example <u-upload :action="action" :file-list="fileList" ></u-upload>
*/
export default {
name: "u-upload",
props: {
//是否显示组件自带的图片预览功能
showUploadList: {
type: Boolean,
default: true
},
// 后端地址
action: {
type: String,
default: ''
},
// 最大上传数量
maxCount: {
type: [String, Number],
default: 52
},
// 是否显示进度条
showProgress: {
type: Boolean,
default: true
},
// 是否启用
disabled: {
type: Boolean,
default: false
},
// 预览上传的图片时的裁剪模式,和image组件mode属性一致
imageMode: {
type: String,
default: 'aspectFill'
},
// 头部信息
header: {
type: Object,
default () {
return {}
}
},
// 额外携带的参数
formData: {
type: Object,
default () {
return {}
}
},
// 上传的文件字段名
name: {
type: String,
default: 'file'
},
// 所选的图片的尺寸, 可选值为original compressed
sizeType: {
type: Array,
default () {
return ['original', 'compressed']
}
},
sourceType: {
type: Array,
default () {
return ['album', 'camera']
}
},
// 是否在点击预览图后展示全屏图片预览
previewFullImage: {
type: Boolean,
default: true
},
// 是否开启图片多选,部分安卓机型不支持
multiple: {
type: Boolean,
default: true
},
// 是否展示删除按钮
deletable: {
type: Boolean,
default: true
},
// 文件大小限制,单位为byte
maxSize: {
type: [String, Number],
default: Number.MAX_VALUE
},
// 显示已上传的文件列表
fileList: {
type: Array,
default () {
return []
}
},
// 上传区域的提示文字
uploadText: {
type: String,
default: '选择图片'
},
// 是否自动上传
autoUpload: {
type: Boolean,
default: true
},
// 是否显示toast消息提示
showTips: {
type: Boolean,
default: true
},
// 是否通过slot自定义传入选择图标的按钮
customBtn: {
type: Boolean,
default: false
}
},
mounted() {
},
data() {
return {
lists: [],
isInCount: true,
uploading: false
}
},
watch: {
fileList: {
immediate: true,
handler(val) {
val.map(value => {
this.lists.push({
url: value.url,
error: false,
progress: 100
});
})
}
}
},
methods: {
// 选择图片
selectFile() {
if (this.disabled) return;
const {
name = '', maxCount, multiple, maxSize, sizeType, lists, camera, compressed, maxDuration, sourceType
} = this;
let chooseFile = null;
const newMaxCount = maxCount - lists.length;
// 设置为只选择图片的时候使用 chooseImage 来实现
chooseFile = new Promise((resolve, reject) => {
wx.chooseImage({
count: multiple ? (newMaxCount > 9 ? 9 : newMaxCount) : 1,
sourceType: sourceType,
sizeType,
success: resolve,
fail: reject
});
});
chooseFile
.then((res) => {
let file = null;
let listOldLength = this.lists.length;
res.tempFiles.map((val, index) => {
// 如果是非多选,index大于等于1或者超出最大限制数量时,不处理
if (!multiple && index >= 1) return;
if (val.size > maxSize) {
this.$emit('on-oversize', val, this.lists);
this.showToast('超出允许的文件大小');
} else {
if (maxCount <= lists.length) {
this.$emit('on-exceed', val, this.lists);
this.showToast('超出最大允许的文件个数');
return;
}
lists.push({
url: val.path,
progress: 0,
error: false
});
}
})
// 每次图片选择完,抛出一个事件,并将当前内部选择的图片数组抛出去
this.$emit('on-choose-complete', this.lists);
if (this.autoUpload) this.uploadFile(listOldLength);
})
.catch(error => {
// this.$emit('on-error', error);
});
},
// 提示用户消息
showToast(message, force = false) {
if (this.showTips || force) {
uni.showToast({
title: message,
icon: "none"
});
}
},
// 该方法供用户通过ref调用,手动上传
upload() {
this.uploadFile();
},
// 对失败的图片重新上传
retry(index) {
this.lists[index].progress = 0;
this.lists[index].error = false;
this.lists[index].response = null;
uni.showLoading({
title: '重新上传'
});
this.uploadFile(index);
},
// 上传图片
uploadFile(index = 0) {
if (this.disabled) return;
if (this.uploading) return;
// 全部上传完成
if (index >= this.lists.length) {
this.$emit('on-uploaded', this.lists);
return;
}
// 检查上传地址
if (!this.action) {
this.showToast('请配置上传地址', true);
return;
}
// 检查是否是已上传或者正在上传中
if (this.lists[index].progress == 100) {
if (this.autoUpload == false) this.uploadFile(index + 1);
return;
}
this.lists[index].error = false;
this.uploading = true;
// 创建上传对象
const task = uni.uploadFile({
url: this.action,
filePath: this.lists[index].url,
name: this.name,
formData: this.formData,
header: this.header,
success: (res) => {
if (res.statusCode != 200) {
this.uploadError(index, res.data);
} else {
// 上传成功
this.lists[index].response = res.data;
this.lists[index].progress = 100;
this.lists[index].error = false;
this.$emit('on-success', res.data, index, this.lists);
}
},
fail: (e) => {
this.uploadError(index, e);
},
complete: (res) => {
uni.hideLoading();
this.uploading = false;
this.uploadFile(index + 1);
this.$emit('on-change', res, index, this.lists);
}
});
task.onProgressUpdate((res) => {
if (res.progress > 0) {
this.lists[index].progress = res.progress;
this.$emit('on-progress', res, index, this.lists);
}
});
},
// 上传失败
uploadError(index, err) {
this.lists[index].progress = 0;
this.lists[index].error = true;
this.lists[index].response = null;
this.$emit('on-error', err, index, this.lists);
this.showToast('上传失败,请重试');
},
// 删除一个图片
deleteItem(index) {
uni.showModal({
title: '提示',
content: '您确定要删除此项吗?',
success: res => {
if (res.confirm) {
if (this.lists[index].process < 100 && this.lists[index].process > 0) {
typeof this.lists[index].uploadTask != 'undefined' && this.lists[index].uploadTask.abort();
}
this.lists.splice(index, 1);
this.$forceUpdate();
this.$emit('on-remove', index, this.lists);
this.showToast('移除成功');
}
}
});
},
// 预览图片
doPreviewImage(url, index) {
if (!this.previewFullImage)
return;
const images = this.lists.map(item => item.url || item.path);
uni.previewImage({
urls: images,
current: url,
success: () => {
this.$emit('on-preview', url, this.lists);
},
fail: () => {
uni.showToast({
title: '预览图片失败',
icon: 'none'
});
}
});
}
}
}
</script>
<style lang="scss" scoped>
.u-upload {
display: flex;
flex-wrap: wrap;
align-items: center;
}
.u-list-item {
width: 200rpx;
height: 200rpx;
overflow: hidden;
margin: 10rpx;
background: rgb(244, 245, 246);
position: relative;
border-radius: 10rpx;
display: inline-flex;
align-items: center;
justify-content: center;
}
.u-preview-wrap {
border: 1px solid rgb(235, 236, 238);
}
.u-add-wrap {
flex-direction: column;
color: $u-content-color;
font-size: 28rpx;
}
.u-add-tips {
margin-top: 20rpx;
}
.u-add-wrap__hover {
background-color: rgb(235, 236, 238);
}
.u-preview-image {
display: block;
width: 100%;
height: 100%;
}
.u-delete-icon {
position: absolute;
top: 10rpx;
right: 10rpx;
z-index: 10;
background-color: $u-type-error;
border-radius: 100rpx;
width: 44rpx;
height: 44rpx;
display: flex;
align-items: center;
justify-content: center;
}
.u-icon {
display: flex;
align-items: center;
justify-content: center;
}
.u-progress {
position: absolute;
bottom: 10rpx;
left: 8rpx;
right: 8rpx;
z-index: 9;
width: auto;
}
.u-error-btn {
color: #FFFFFF;
background-color: $u-type-error;
font-size: 20rpx;
padding: 4px 0;
text-align: center;
position: absolute;
bottom: 0;
left: 0;
right: 0;
z-index: 9;
line-height: 1;
}
</style>
<template>
<view class="u-code-wrap">
</view>
</template>
<script>
export default {
props: {
// 倒计时总秒数
seconds: {
type: [String, Number],
default: 60
},
// 尚未开始时提示
startText: {
type: String,
default: '获取验证码'
},
// 正在倒计时中的提示
changeText: {
type: String,
default: 'X秒重新获取'
},
// 倒计时结束时的提示
endText: {
type: String,
default: '重新获取'
},
},
data() {
return {
secNum: this.seconds,
timer: null,
canGetCode: true, // 是否可以执行验证码操作
}
},
mounted() {
this.changeEvent(this.startText);
},
methods: {
// 开始倒计时
start() {
this.secNum = this.seconds;
this.$emit('start');
this.canGetCode = false;
// 这里放这句,是为了一开始时就提示,否则要等setInterval的1秒后才会有提示
this.changeEvent(this.changeText.replace(/x|X/, this.secNum));
this.timer = setInterval(() => {
if(--this.secNum) {
// 用当前倒计时的秒数替换提示字符串中的"x"字母
this.changeEvent(this.changeText.replace(/x|X/, this.secNum));
} else {
clearInterval(this.timer);
this.changeEvent(this.endText);
this.secNum = this.seconds;
this.$emit('end');
this.canGetCode = true;
}
}, 1000);
},
// 重置,可以让用户再次获取验证码
reset() {
this.canGetCode = true;
clearInterval(this.timer);
this.secNum = this.seconds;
this.changeEvent(this.endText);
},
changeEvent(text) {
this.$emit('change', text);
}
},
beforeDestroy() {
clearTimeout(this.timer)
},
}
</script>
<style lang="scss" scoped>
.u-code-wrap {
width: 0;
height: 0;
position: fixed;
z-index: -1;
}
<template>
<view class="u-code-wrap">
</view>
</template>
<script>
/**
* alertTips 提示
* @description 考虑到用户实际发送验证码的场景,可能是一个按钮,也可能是一段文字,提示语各有不同,所以本组件 不提供界面显示,只提供提示语,由用户将提示语嵌入到具体的场景
* @tutorial https://www.uviewui.com/components/verificationCode.html
* @property {Number String} seconds 倒计时所需的秒数(默认60)
* @property {String} start-text 开始前的提示语,见官网说明(默认获取验证码)
* @property {String} change-text 倒计时期间的提示语,必须带有字母"x",见官网说明(默认X秒重新获取)
* @property {String} end-text 倒计结束的提示语,见官网说明(默认重新获取)
* @event {Function} change 倒计时期间,每秒触发一次
* @event {Function} start 开始倒计时触发
* @event {Function} end 结束倒计时触发
* @example <u-verification-code :seconds="seconds" @end="end" @start="start" ref="uCode"
*/
export default {
name: "u-verification-code",
props: {
// 倒计时总秒数
seconds: {
type: [String, Number],
default: 60
},
// 尚未开始时提示
startText: {
type: String,
default: '获取验证码'
},
// 正在倒计时中的提示
changeText: {
type: String,
default: 'X秒重新获取'
},
// 倒计时结束时的提示
endText: {
type: String,
default: '重新获取'
},
},
data() {
return {
secNum: this.seconds,
timer: null,
canGetCode: true, // 是否可以执行验证码操作
}
},
mounted() {
this.changeEvent(this.startText);
},
methods: {
// 开始倒计时
start() {
this.secNum = this.seconds;
this.$emit('start');
this.canGetCode = false;
// 这里放这句,是为了一开始时就提示,否则要等setInterval的1秒后才会有提示
this.changeEvent(this.changeText.replace(/x|X/, this.secNum));
this.timer = setInterval(() => {
if (--this.secNum) {
// 用当前倒计时的秒数替换提示字符串中的"x"字母
this.changeEvent(this.changeText.replace(/x|X/, this.secNum));
} else {
clearInterval(this.timer);
this.changeEvent(this.endText);
this.secNum = this.seconds;
this.$emit('end');
this.canGetCode = true;
}
}, 1000);
},
// 重置,可以让用户再次获取验证码
reset() {
this.canGetCode = true;
clearInterval(this.timer);
this.secNum = this.seconds;
this.changeEvent(this.endText);
},
changeEvent(text) {
this.$emit('change', text);
}
},
beforeDestroy() {
clearTimeout(this.timer)
},
}
</script>
<style lang="scss" scoped>
.u-code-wrap {
width: 0;
height: 0;
position: fixed;
z-index: -1;
}
</style>
<template>
<view class="u-waterfall">
<view id="u-left-cloumn" class="u-cloumn">
<slot name="left" :list="leftList"></slot>
</view>
<view id="u-right-cloumn" class="u-cloumn">
<slot name="right" :list="rightList"></slot>
</view>
</view>
</template>
<script>
export default {
props: {
flowList: {
// 瀑布流数据
type: Array,
required: true,
default: function() {
return [];
}
},
// 每次向结构插入数据的时间间隔,间隔越长,越能保证两列高度相近,但是对用户体验越不好
// 单位ms
addTime: {
type: [Number, String],
default: 200
}
},
provide() {
return {
uWaterfall: this
}
},
data() {
return {
leftList: [],
rightList: [],
tempList: [],
children: []
}
},
watch: {
copyFlowList(nVal, oVal) {
// 取差值,即这一次数组变化新增的部分
let startIndex = Array.isArray(oVal) && oVal.length > 0 ? oVal.length : 0;
this.tempList = this.cloneData(nVal.slice(startIndex));
this.splitData();
},
},
mounted() {
this.tempList = this.cloneData(this.copyFlowList);
this.splitData();
},
computed: {
// 破坏flowList变量的引用,否则watch的结果新旧值是一样的
copyFlowList() {
return this.cloneData(this.flowList);
}
},
methods: {
async splitData() {
if (!this.tempList.length) return;
let leftRect = await this.$uGetRect('#u-left-cloumn');
let rightRect = await this.$uGetRect('#u-right-cloumn');
// 如果左边小于或等于右边,就添加到左边,否则添加到右边
let item = this.tempList[0];
if (leftRect.height < rightRect.height) {
this.leftList.push(item);
} else if(leftRect.height > rightRect.height) {
this.rightList.push(item);
} else {
// 这里是为了保证第一和第二张添加时,左右都能有内容
// 因为添加第一张,实际队列的高度可能还是0,这时需要根据队列元素长度判断下一个该放哪边
if(this.leftList.length <= this.rightList.length) {
this.leftList.push(item);
} else {
this.rightList.push(item);
}
}
// 移除临时列表的第一项
this.tempList.splice(0, 1);
// 如果临时数组还有数据,继续循环
if (this.tempList.length) {
setTimeout(() => {
this.splitData();
}, this.addTime)
}
},
// 复制而不是引用对象和数组
cloneData(data) {
return JSON.parse(JSON.stringify(data));
}
}
}
</script>
<style lang="scss" scoped>
.u-waterfall {
display: flex;
flex-direction: row;
align-items: flex-start;
}
.u-cloumn {
display: flex;
flex: 1;
flex-direction: column;
height: auto;
}
.u-image {
width: 100%;
}
<template>
<view class="u-waterfall">
<view id="u-left-cloumn" class="u-cloumn">
<slot name="left" :list="leftList"></slot>
</view>
<view id="u-right-cloumn" class="u-cloumn">
<slot name="right" :list="rightList"></slot>
</view>
</view>
</template>
<script>
/**
* alertTips 提示
* @description 这是一个瀑布流形式的组件,内容分为左右两列,结合uView的懒加载组件效果更佳。相较于某些只是奇偶数左右分别,或者没有利用vue作用域插槽的做法,uView的瀑布流实现了真正的 组件化,搭配LazyLoad 懒加载和loadMore 加载更多组件,让您开箱即用,眼前一亮。
* @tutorial https://www.uviewui.com/components/waterfall.html
* @property {Array} flow-list 用于渲染的数据
* @property {String Number} add-time 单条数据添加到队列的时间间隔,单位ms,见上方注意事项说明(默认200)
* @example <u-waterfall :flowList="flowList"></u-waterfall>
*/
export default {
name: "u-waterfall",
props: {
flowList: {
// 瀑布流数据
type: Array,
required: true,
default: function() {
return [];
}
},
// 每次向结构插入数据的时间间隔,间隔越长,越能保证两列高度相近,但是对用户体验越不好
// 单位ms
addTime: {
type: [Number, String],
default: 200
}
},
provide() {
return {
uWaterfall: this
}
},
data() {
return {
leftList: [],
rightList: [],
tempList: [],
children: []
}
},
watch: {
copyFlowList(nVal, oVal) {
// 取差值,即这一次数组变化新增的部分
let startIndex = Array.isArray(oVal) && oVal.length > 0 ? oVal.length : 0;
this.tempList = this.cloneData(nVal.slice(startIndex));
this.splitData();
},
},
mounted() {
this.tempList = this.cloneData(this.copyFlowList);
this.splitData();
},
computed: {
// 破坏flowList变量的引用,否则watch的结果新旧值是一样的
copyFlowList() {
return this.cloneData(this.flowList);
}
},
methods: {
async splitData() {
if (!this.tempList.length) return;
let leftRect = await this.$uGetRect('#u-left-cloumn');
let rightRect = await this.$uGetRect('#u-right-cloumn');
// 如果左边小于或等于右边,就添加到左边,否则添加到右边
let item = this.tempList[0];
if (leftRect.height < rightRect.height) {
this.leftList.push(item);
} else if (leftRect.height > rightRect.height) {
this.rightList.push(item);
} else {
// 这里是为了保证第一和第二张添加时,左右都能有内容
// 因为添加第一张,实际队列的高度可能还是0,这时需要根据队列元素长度判断下一个该放哪边
if (this.leftList.length <= this.rightList.length) {
this.leftList.push(item);
} else {
this.rightList.push(item);
}
}
// 移除临时列表的第一项
this.tempList.splice(0, 1);
// 如果临时数组还有数据,继续循环
if (this.tempList.length) {
setTimeout(() => {
this.splitData();
}, this.addTime)
}
},
// 复制而不是引用对象和数组
cloneData(data) {
return JSON.parse(JSON.stringify(data));
}
}
}
</script>
<style lang="scss" scoped>
.u-waterfall {
display: flex;
flex-direction: row;
align-items: flex-start;
}
.u-cloumn {
display: flex;
flex: 1;
flex-direction: column;
height: auto;
}
.u-image {
width: 100%;
}
</style>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment