项目初始化

This commit is contained in:
jerry
2025-01-21 01:46:34 +08:00
parent 364021b042
commit 48153e7761
962 changed files with 172070 additions and 0 deletions

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,175 @@
<template>
<view class="u-count-down">
<slot>
<text class="u-count-down__text" :style="customStyle">{{ formattedTime }}</text>
</slot>
</view>
</template>
<script>
import { isSameSecond, parseFormat, parseTimeData } from "./utils";
/**
* u-count-down 倒计时
* @description 该组件一般使用于某个活动的截止时间上,通过数字的变化,给用户明确的时间感受,提示用户进行某一个行为操作。
* @tutorial https://uviewui.com/components/countDown.html
* @property {String | Number} timestamp 倒计时时长单位ms (默认 0
* @property {String} format 时间格式DD-日HH-时mm-分ss-秒SSS-毫秒 (默认 'HH:mm:ss'
* @property {Boolean} autoStart 是否自动开始倒计时 (默认 true
* @event {Function} end 倒计时结束时触发
* @event {Function} change 倒计时变化时触发
* @event {Function} start 开始倒计时
* @event {Function} pause 暂停倒计时
* @event {Function} reset 重设倒计时,若 auto-start 为 true重设后会自动开始倒计时
* @example <u-count-down :timestamp="timestamp"></u-count-down>
*/
export default {
name: "u-count-down",
emits: ["change", "end", "finish"],
props: {
// 倒计时时长单位ms
timestamp: {
type: [String, Number],
default: 0
},
// 时间格式DD-日HH-时mm-分ss-秒SSS-毫秒
format: {
type: String,
default: "DD:HH:mm:ss"
},
// 是否自动开始倒计时
autoStart: {
type: Boolean,
default: true
},
customStyle: {
type: [String, Object],
default: ""
}
},
data() {
return {
timer: null,
// 各单位(天,时,分等)剩余时间
timeData: parseTimeData(0),
// 格式化后的时间,如"03:23:21"
formattedTime: "0",
// 倒计时是否正在进行中
runing: false,
endTime: 0, // 结束的毫秒时间戳
remainTime: 0 // 剩余的毫秒时间
};
},
watch: {
timestamp(n) {
this.reset();
},
format(newVal, oldVal) {
this.pause();
this.start();
}
},
mounted() {
this.init();
},
methods: {
init() {
this.reset();
},
// 开始倒计时
start() {
if (this.runing) return;
// 标识为进行中
this.runing = true;
// 结束时间戳 = 此刻时间戳 + 剩余的时间
this.endTime = Date.now() + this.remainTime;
this.toTick();
},
// 根据是否展示毫秒,执行不同操作函数
toTick() {
if (this.format.indexOf("SSS") > -1) {
this.microTick();
} else {
this.macroTick();
}
},
macroTick() {
this.clearTimeout();
// 每隔一定时间,更新一遍定时器的值
// 同时此定时器的作用也能带来毫秒级的更新
this.timer = setTimeout(() => {
// 获取剩余时间
const remain = this.getRemainTime();
// 重设剩余时间
if (!isSameSecond(remain, this.remainTime) || remain === 0) {
this.setRemainTime(remain);
}
// 如果剩余时间不为0则继续检查更新倒计时
if (this.remainTime !== 0) {
this.macroTick();
}
}, 30);
},
microTick() {
this.clearTimeout();
this.timer = setTimeout(() => {
this.setRemainTime(this.getRemainTime());
if (this.remainTime !== 0) {
this.microTick();
}
}, 30);
},
// 获取剩余的时间
getRemainTime() {
// 取最大值防止出现小于0的剩余时间值
return Math.max(this.endTime - Date.now(), 0);
},
// 设置剩余的时间
setRemainTime(remain) {
this.remainTime = remain;
// 根据剩余的毫秒时间,得出该有天,小时,分钟等的值,返回一个对象
const timeData = parseTimeData(remain);
this.$emit("change", timeData);
// 得出格式化后的时间
this.formattedTime = parseFormat(this.format, timeData);
// 如果时间已到,停止倒计时
if (remain <= 0) {
this.pause();
this.$emit("end");
this.$emit("finish");
}
},
// 重置倒计时
reset() {
this.pause();
this.remainTime = this.timestamp;
this.setRemainTime(this.remainTime);
if (this.autoStart) {
this.start();
}
},
// 暂停倒计时
pause() {
this.runing = false;
this.clearTimeout();
},
// 清空定时器
clearTimeout() {
clearTimeout(this.timer);
this.timer = null;
}
},
// #ifndef VUE3
beforeDestroy() {
this.clearTimeout();
},
// #endif
// #ifdef VUE3
beforeUnmount() {
this.clearTimeout();
},
// #endif
};
</script>
<style lang="scss"></style>

View File

@@ -0,0 +1,62 @@
// 补0如1 -> 01
function padZero(num, targetLength = 2) {
let str = `${num}`
while (str.length < targetLength) {
str = `0${str}`
}
return str
}
const SECOND = 1000
const MINUTE = 60 * SECOND
const HOUR = 60 * MINUTE
const DAY = 24 * HOUR
export function parseTimeData(time) {
const days = Math.floor(time / DAY)
const hours = Math.floor((time % DAY) / HOUR)
const minutes = Math.floor((time % HOUR) / MINUTE)
const seconds = Math.floor((time % MINUTE) / SECOND)
const milliseconds = Math.floor(time % SECOND)
return {
days,
hours,
minutes,
seconds,
milliseconds
}
}
export function parseFormat(format, timeData) {
let {
days,
hours,
minutes,
seconds,
milliseconds
} = timeData
// 如果格式化字符串中不存在DD(天),则将天的时间转为小时中去
if (format.indexOf('DD') === -1) {
hours += days * 24
} else {
// 对天补0
format = format.replace('DD', padZero(days))
}
// 其他同理于DD的格式化处理方式
if (format.indexOf('HH') === -1) {
minutes += hours * 60
} else {
format = format.replace('HH', padZero(hours))
}
if (format.indexOf('mm') === -1) {
seconds += minutes * 60
} else {
format = format.replace('mm', padZero(minutes))
}
if (format.indexOf('ss') === -1) {
milliseconds += seconds * 1000
} else {
format = format.replace('ss', padZero(seconds))
}
return format.replace('SSS', padZero(milliseconds, 3))
}
export function isSameSecond(time1, time2) {
return Math.floor(time1 / 1000) === Math.floor(time2 / 1000)
}

View File

@@ -0,0 +1,369 @@
<template>
<view :style="[customStyle]" class="u-icon" @tap="click" :class="['u-icon--' + labelPos]">
<image class="u-icon__img" v-if="isImg" :src="name" :mode="imgMode" :style="[imgStyle]"></image>
<view
v-else
class="u-icon__icon"
:class="customClass"
:style="[iconStyle]"
:hover-class="hoverClass"
@touchstart="touchstart"
>
<text
v-if="showDecimalIcon"
:style="[decimalIconStyle]"
:class="decimalIconClass"
:hover-class="hoverClass"
class="u-icon__decimal"
></text>
</view>
<!-- 这里进行空字符串判断如果仅仅是v-if="label"可能会出现传递0的时候结果也无法显示微信小程序不传值默认为null故需要增加null的判断 -->
<text
v-if="label !== '' && label !== null"
class="u-icon__label"
:style="{
color: labelColor,
fontSize: $u.addUnit(labelSize),
marginLeft: labelPos == 'right' ? $u.addUnit(marginLeft) : 0,
marginTop: labelPos == 'bottom' ? $u.addUnit(marginTop) : 0,
marginRight: labelPos == 'left' ? $u.addUnit(marginRight) : 0,
marginBottom: labelPos == 'top' ? $u.addUnit(marginBottom) : 0
}"
>
{{ label }}
</text>
</view>
</template>
<script>
/**
* icon 图标
* @description 基于字体的图标集,包含了大多数常见场景的图标。
* @tutorial https://www.uviewui.com/components/icon.html
* @property {String} name 图标名称,见示例图标集
* @property {String} color 图标颜色默认inherit
* @property {String | Number} size 图标字体大小单位rpx默认32
* @property {String | Number} label-size label字体大小单位rpx默认28
* @property {String} label 图标右侧的label文字默认28
* @property {String} label-pos label文字相对于图标的位置只能right或bottom默认right
* @property {String} label-color label字体颜色默认#606266
* @property {Object} custom-style icon的样式对象形式
* @property {String} custom-prefix 自定义字体图标库时,需要写上此值
* @property {String | Number} margin-left label在右侧时与图标的距离单位rpx默认6
* @property {String | Number} margin-top label在下方时与图标的距离单位rpx默认6
* @property {String | Number} margin-bottom label在上方时与图标的距离单位rpx默认6
* @property {String | Number} margin-right label在左侧时与图标的距离单位rpx默认6
* @property {String} label-pos label相对于图标的位置只能right或bottom默认right
* @property {String} index 一个用于区分多个图标的值点击图标时通过click事件传出
* @property {String} hover-class 图标按下去的样式类用法同uni的view组件的hover-class参数详情见官网
* @property {String} width 显示图片小图标时的宽度
* @property {String} height 显示图片小图标时的高度
* @property {String} top 图标在垂直方向上的定位
* @property {String} top 图标在垂直方向上的定位
* @property {String} top 图标在垂直方向上的定位
* @property {Boolean} show-decimal-icon 是否为DecimalIcon
* @property {String} inactive-color 背景颜色可接受主题色仅Decimal时有效
* @property {String | Number} percent 显示的百分比仅Decimal时有效
* @event {Function} click 点击图标时触发
* @example <u-icon name="photo" color="#2979ff" size="28"></u-icon>
*/
export default {
name: "u-icon",
emits: ["click", "touchstart"],
props: {
// 图标类名
name: {
type: String,
default: ""
},
// 图标颜色,可接受主题色
color: {
type: String,
default: ""
},
// 字体大小单位rpx
size: {
type: [Number, String],
default: "inherit"
},
// 是否显示粗体
bold: {
type: Boolean,
default: false
},
// 点击图标的时候传递事件出去的index用于区分点击了哪一个
index: {
type: [Number, String],
default: ""
},
// 触摸图标时的类名
hoverClass: {
type: String,
default: ""
},
// 自定义扩展前缀,方便用户扩展自己的图标库
customPrefix: {
type: String,
default: "uicon"
},
// 图标右边或者下面的文字
label: {
type: [String, Number],
default: ""
},
// label的位置只能右边或者下边
labelPos: {
type: String,
default: "right"
},
// label的大小
labelSize: {
type: [String, Number],
default: "28"
},
// label的颜色
labelColor: {
type: String,
default: "#606266"
},
// label与图标的距离(横向排列)
marginLeft: {
type: [String, Number],
default: "6"
},
// label与图标的距离(竖向排列)
marginTop: {
type: [String, Number],
default: "6"
},
// label与图标的距离(竖向排列)
marginRight: {
type: [String, Number],
default: "6"
},
// label与图标的距离(竖向排列)
marginBottom: {
type: [String, Number],
default: "6"
},
// 图片的mode
imgMode: {
type: String,
default: "widthFix"
},
// 自定义样式
customStyle: {
type: Object,
default() {
return {};
}
},
// 用于显示图片小图标时,图片的宽度
width: {
type: [String, Number],
default: ""
},
// 用于显示图片小图标时,图片的高度
height: {
type: [String, Number],
default: ""
},
// 用于解决某些情况下,让图标垂直居中的用途
top: {
type: [String, Number],
default: 0
},
// 是否为DecimalIcon
showDecimalIcon: {
type: Boolean,
default: false
},
// 背景颜色可接受主题色仅Decimal时有效
inactiveColor: {
type: String,
default: "#ececec"
},
// 显示的百分比仅Decimal时有效
percent: {
type: [Number, String],
default: "50"
}
},
computed: {
customClass() {
let classes = [];
let { customPrefix, name } = this;
let index = name.indexOf("-icon-");
if (index > -1) {
customPrefix = name.substring(0, index + 5);
classes.push(name);
} else {
classes.push(`${customPrefix}-${name}`);
}
// uView的自定义图标类名为u-iconfont
if (customPrefix === "uicon") {
classes.push("u-iconfont");
} else {
classes.push(customPrefix);
}
// 主题色,通过类配置
if (
this.showDecimalIcon &&
this.inactiveColor &&
this.$u.config.type.includes(this.inactiveColor)
) {
classes.push("u-icon__icon--" + this.inactiveColor);
} else if (this.color && this.$u.config.type.includes(this.color))
classes.push("u-icon__icon--" + this.color);
// 阿里,头条,百度小程序通过数组绑定类名时,无法直接使用[a, b, c]的形式,否则无法识别
// 故需将其拆成一个字符串的形式,通过空格隔开各个类名
//#ifdef MP-ALIPAY || MP-TOUTIAO || MP-BAIDU
classes = classes.join(" ");
//#endif
return classes;
},
iconStyle() {
let style = {};
style = {
fontSize: this.size == "inherit" ? "inherit" : this.$u.addUnit(this.size),
fontWeight: this.bold ? "bold" : "normal",
// 某些特殊情况需要设置一个到顶部的距离,才能更好的垂直居中
top: this.$u.addUnit(this.top)
};
// 非主题色值时,才当作颜色值
if (
this.showDecimalIcon &&
this.inactiveColor &&
!this.$u.config.type.includes(this.inactiveColor)
) {
style.color = this.inactiveColor;
} else if (this.color && !this.$u.config.type.includes(this.color)) style.color = this.color;
return style;
},
// 判断传入的name属性是否图片路径只要带有"/"均认为是图片形式
isImg() {
return this.name.indexOf("/") !== -1;
},
imgStyle() {
let style = {};
// 如果设置width和height属性则优先使用否则使用size属性
style.width = this.width ? this.$u.addUnit(this.width) : this.$u.addUnit(this.size);
style.height = this.height ? this.$u.addUnit(this.height) : this.$u.addUnit(this.size);
return style;
},
decimalIconStyle() {
let style = {};
style = {
fontSize: this.size == "inherit" ? "inherit" : this.$u.addUnit(this.size),
fontWeight: this.bold ? "bold" : "normal",
// 某些特殊情况需要设置一个到顶部的距离,才能更好的垂直居中
top: this.$u.addUnit(this.top),
width: this.percent + "%"
};
// 非主题色值时,才当作颜色值
if (this.color && !this.$u.config.type.includes(this.color)) style.color = this.color;
return style;
},
decimalIconClass() {
let classes = [];
classes.push(this.customPrefix + "-" + this.name);
// uView的自定义图标类名为u-iconfont
if (this.customPrefix == "uicon") {
classes.push("u-iconfont");
} else {
classes.push(this.customPrefix);
}
// 主题色,通过类配置
if (this.color && this.$u.config.type.includes(this.color))
classes.push("u-icon__icon--" + this.color);
else classes.push("u-icon__icon--primary");
// 阿里,头条,百度小程序通过数组绑定类名时,无法直接使用[a, b, c]的形式,否则无法识别
// 故需将其拆成一个字符串的形式,通过空格隔开各个类名
//#ifdef MP-ALIPAY || MP-TOUTIAO || MP-BAIDU
classes = classes.join(" ");
//#endif
return classes;
}
},
methods: {
click() {
this.$emit("click", this.index);
},
touchstart() {
this.$emit("touchstart", this.index);
}
}
};
</script>
<style scoped lang="scss">
@import "../../libs/css/style.components.scss";
@import "../../iconfont.css";
.u-icon {
display: inline-flex;
align-items: center;
&--left {
flex-direction: row-reverse;
align-items: center;
}
&--right {
flex-direction: row;
align-items: center;
}
&--top {
flex-direction: column-reverse;
justify-content: center;
}
&--bottom {
flex-direction: column;
justify-content: center;
}
&__icon {
position: relative;
&--primary {
color: $u-type-primary;
}
&--success {
color: $u-type-success;
}
&--error {
color: $u-type-error;
}
&--warning {
color: $u-type-warning;
}
&--info {
color: $u-type-info;
}
}
&__decimal {
position: absolute;
top: 0;
left: 0;
display: inline-block;
overflow: hidden;
}
&__img {
height: auto;
will-change: transform;
}
&__label {
line-height: 1;
}
}
</style>

View File

@@ -0,0 +1,283 @@
<template>
<view class="u-image" @tap="onClick" :style="[wrapStyle, backgroundStyle]">
<image
v-if="!isError"
:src="src"
:mode="mode"
@error="onErrorHandler"
@load="onLoadHandler"
:lazy-load="lazyLoad"
class="u-image__image"
:show-menu-by-longpress="showMenuByLongpress"
:style="{
borderRadius: shape == 'circle' ? '50%' : $u.addUnit(borderRadius)
}"
></image>
<view
v-if="showLoading && loading"
class="u-image__loading"
:style="{
borderRadius: shape == 'circle' ? '50%' : $u.addUnit(borderRadius),
backgroundColor: bgColor
}"
>
<slot v-if="$slots.loading" name="loading" />
<u-icon v-else :name="loadingIcon" :width="width" :height="height"></u-icon>
</view>
<view
v-if="showError && isError && !loading"
class="u-image__error"
:style="{
borderRadius: shape == 'circle' ? '50%' : $u.addUnit(borderRadius)
}"
>
<slot v-if="$slots.error" name="error" />
<u-icon v-else :name="errorIcon" :width="width" :height="height"></u-icon>
</view>
</view>
</template>
<script>
/**
* Image 图片
* @description 此组件为uni-app的image组件的加强版在继承了原有功能外还支持淡入动画、加载中、加载失败提示、圆角值和形状等。
* @tutorial https://uviewui.com/components/image.html
* @property {String} src 图片地址
* @property {String} mode 裁剪模式,见官网说明
* @value scaleToFill 不保持纵横比缩放图片,使图片的宽高完全拉伸至填满 image 元素
* @value aspectFit 保持纵横比缩放图片,使图片的长边能完全显示出来。也就是说,可以完整地将图片显示出来。
* @value aspectFill 保持纵横比缩放图片,只保证图片的短边能完全显示出来。也就是说,图片通常只在水平或垂直方向是完整的,另一个方向将会发生截取。
* @value widthFix 宽度不变,高度自动变化,保持原图宽高比不变
* @value heightFix 高度不变,宽度自动变化,保持原图宽高比不变 App 和 H5 平台 HBuilderX 2.9.3+ 支持、微信小程序需要基础库 2.10.3
* @value top 不缩放图片,只显示图片的顶部区域
* @value bottom 不缩放图片,只显示图片的底部区域
* @value center 不缩放图片,只显示图片的中间区域
* @value left 不缩放图片,只显示图片的左边区域
* @value right 不缩放图片,只显示图片的右边区域
* @value top left 不缩放图片,只显示图片的左上边区域
* @value top right 不缩放图片,只显示图片的右上边区域
* @value bottom left 不缩放图片,只显示图片的左下边区域
* @value bottom right 不缩放图片,只显示图片的右下边区域
* @property {String | Number} width 宽度单位任意如果为数值则为rpx单位默认100%
* @property {String | Number} height 高度单位任意如果为数值则为rpx单位默认 auto
* @property {String} shape 图片形状circle-圆形square-方形默认square
* @property {String | Number} border-radius 圆角值单位任意如果为数值则为rpx单位默认 0
* @property {Boolean} lazy-load 是否懒加载仅微信小程序、App、百度小程序、字节跳动小程序有效默认 true
* @property {Boolean} show-menu-by-longpress 是否开启长按图片显示识别小程序码菜单,仅微信小程序有效(默认 false
* @property {String} loading-icon 加载中的图标,或者小图片(默认 photo
* @property {String} error-icon 加载失败的图标,或者小图片(默认 error-circle
* @property {Boolean} show-loading 是否显示加载中的图标或者自定义的slot默认 true
* @property {Boolean} show-error 是否显示加载错误的图标或者自定义的slot默认 true
* @property {Boolean} fade 是否需要淡入效果(默认 true
* @property {String Number} width 传入图片路径时图片的宽度
* @property {String Number} height 传入图片路径时图片的高度
* @property {Boolean} webp 只支持网络资源,只对微信小程序有效(默认 false
* @property {String | Number} duration 搭配fade参数的过渡时间单位ms默认 500
* @event {Function} click 点击图片时触发
* @event {Function} error 图片加载失败时触发
* @event {Function} load 图片加载成功时触发
* @example <u-image width="100%" height="300rpx" :src="src"></u-image>
*/
export default {
name: 'u-image',
emits: ["click", "error", "load"],
props: {
// 图片地址
src: {
type: String,
default: ''
},
// 裁剪模式
mode: {
type: String,
default: 'aspectFill'
},
// 宽度,单位任意
width: {
type: [String, Number],
default: '100%'
},
// 高度,单位任意
height: {
type: [String, Number],
default: 'auto'
},
// 图片形状circle-圆形square-方形
shape: {
type: String,
default: 'square'
},
// 圆角,单位任意
borderRadius: {
type: [String, Number],
default: 0
},
// 是否懒加载微信小程序、App、百度小程序、字节跳动小程序
lazyLoad: {
type: Boolean,
default: true
},
// 开启长按图片显示识别微信小程序码菜单
showMenuByLongpress: {
type: Boolean,
default: true
},
// 加载中的图标,或者小图片
loadingIcon: {
type: String,
default: 'photo'
},
// 加载失败的图标,或者小图片
errorIcon: {
type: String,
default: 'error-circle'
},
// 是否显示加载中的图标或者自定义的slot
showLoading: {
type: Boolean,
default: true
},
// 是否显示加载错误的图标或者自定义的slot
showError: {
type: Boolean,
default: true
},
// 是否需要淡入效果
fade: {
type: Boolean,
default: true
},
// 只支持网络资源,只对微信小程序有效
webp: {
type: Boolean,
default: false
},
// 过渡时间单位ms
duration: {
type: [String, Number],
default: 500
},
// 背景颜色,用于深色页面加载图片时,为了和背景色融合
bgColor: {
type: String,
default: '#f3f4f6'
}
},
data() {
return {
// 图片是否加载错误,如果是,则显示错误占位图
isError: false,
// 初始化组件时,默认为加载中状态
loading: true,
// 不透明度,为了实现淡入淡出的效果
opacity: 1,
// 过渡时间因为props的值无法修改故需要一个中间值
durationTime: this.duration,
// 图片加载完成时去掉背景颜色因为如果是png图片就会显示灰色的背景
backgroundStyle: {}
};
},
watch: {
src: {
immediate: true,
handler (n) {
if(!n) {
// 如果传入null或者''或者false或者undefined标记为错误状态
this.isError = true;
this.loading = false;
} else {
this.isError = false;
this.loading = true;
}
}
}
},
computed: {
wrapStyle() {
let style = {};
// 通过调用addUnit()方法如果有单位如百分比px单位等直接返回如果是纯粹的数值则加上rpx单位
style.width = this.$u.addUnit(this.width);
style.height = this.$u.addUnit(this.height);
// 如果是配置了圆形设置50%的圆角,否则按照默认的配置值
style.borderRadius = this.shape == 'circle' ? '50%' : this.$u.addUnit(this.borderRadius);
// 如果设置圆角必须要有hidden否则可能圆角无效
style.overflow = this.borderRadius > 0 ? 'hidden' : 'visible';
if (this.fade) {
style.opacity = this.opacity;
style.transition = `opacity ${Number(this.durationTime) / 1000}s ease-in-out`;
}
return style;
}
},
methods: {
// 点击图片
onClick() {
this.$emit('click');
},
// 图片加载失败
onErrorHandler(err) {
this.loading = false;
this.isError = true;
this.$emit('error', err);
},
// 图片加载完成标记loading结束
onLoadHandler() {
this.loading = false;
this.isError = false;
this.$emit('load');
// 如果不需要动画效果,就不执行下方代码,同时移除加载时的背景颜色
// 否则无需fade效果时png图片依然能看到下方的背景色
if (!this.fade) return this.removeBgColor();
// 原来opacity为1(不透明,是为了显示占位图)改成0(透明,意味着该元素显示的是背景颜色,默认的灰色)再改成1是为了获得过渡效果
this.opacity = 0;
// 这里设置为0是为了图片展示到背景全透明这个过程时间为0延时之后延时之后重新设置为duration是为了获得背景透明(灰色)
// 到图片展示的过程中的淡入效果
this.durationTime = 0;
// 延时50ms否则在浏览器H5过渡效果无效
setTimeout(() => {
this.durationTime = this.duration;
this.opacity = 1;
setTimeout(() => {
this.removeBgColor();
}, this.durationTime);
}, 50);
},
// 移除图片的背景色
removeBgColor() {
// 淡入动画过渡完成后将背景设置为透明色否则png图片会看到灰色的背景
this.backgroundStyle = {
backgroundColor: 'transparent'
};
}
}
};
</script>
<style scoped lang="scss">
@import '../../libs/css/style.components.scss';
.u-image {
position: relative;
transition: opacity 0.5s ease-in-out;
&__image {
width: 100%;
height: 100%;
}
&__loading,
&__error {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
@include vue-flex;
align-items: center;
justify-content: center;
background-color: $u-bg-color;
color: $u-tips-color;
font-size: 46rpx;
}
}
</style>

View File

@@ -0,0 +1,466 @@
<template>
<view
class="u-input"
:class="{
'u-input--border': border,
'u-input--error': validateState
}"
:style="{
padding: padding ? padding : `0 ${border ? 20 : 0}rpx`,
borderColor: borderColor,
textAlign: inputAlignCom,
backgroundColor: backgroundColor,
}"
@tap.stop="inputClick"
>
<textarea
v-if="type == 'textarea'"
class="u-input__input u-input__textarea"
:style="[getStyle]"
:value="defaultValue"
:placeholder="placeholder"
:placeholderStyle="placeholderStyle"
:disabled="disabled"
:maxlength="inputMaxlength"
:fixed="fixed"
:focus="focus"
:autoHeight="autoHeight"
:selection-end="uSelectionEnd"
:selection-start="uSelectionStart"
:cursor-spacing="getCursorSpacing"
:show-confirm-bar="showConfirmbar"
:adjust-position="adjustPosition"
@input="handleInput"
@blur="handleBlur"
@focus="onFocus"
@confirm="onConfirm"
/>
<input
v-else
class="u-input__input"
:type="type == 'password' ? 'text' : type"
:style="[getStyle]"
:value="defaultValue"
:password="type == 'password' && !showPassword"
:placeholder="placeholder"
:placeholderStyle="placeholderStyle"
:disabled="disabled || type === 'select'"
:maxlength="inputMaxlength"
:focus="focus"
:confirmType="confirmType"
:cursor-spacing="getCursorSpacing"
:selection-end="uSelectionEnd"
:selection-start="uSelectionStart"
:show-confirm-bar="showConfirmbar"
:adjust-position="adjustPosition"
@focus="onFocus"
@blur="handleBlur"
@input="handleInput"
@confirm="onConfirm"
/>
<view class="u-input__right-icon u-flex">
<view
class="u-input__right-icon__clear u-input__right-icon__item"
@tap="onClear"
v-if="clearableCom && valueCom != '' && focused"
>
<u-icon size="32" name="close-circle-fill" color="#c0c4cc" />
</view>
<view
class="u-input__right-icon__clear u-input__right-icon__item"
v-if="passwordIcon && type == 'password'"
>
<u-icon
size="32"
:name="!showPassword ? 'eye' : 'eye-fill'"
color="#c0c4cc"
@click="showPassword = !showPassword"
/>
</view>
<view
class="u-input__right-icon--select u-input__right-icon__item"
v-if="type == 'select'"
:class="{
'u-input__right-icon--select--reverse': selectOpen
}"
>
<u-icon name="arrow-down-fill" size="26" color="#c0c4cc"></u-icon>
</view>
</view>
</view>
</template>
<script>
import Emitter from "../../libs/util/emitter.js";
/**
* input 输入框
* @description 此组件为一个输入框默认没有边框和样式是专门为配合表单组件u-form而设计的利用它可以快速实现表单验证输入内容下拉选择等功能。
* @tutorial https://vkuviewdoc.fsq.pub/components/input.html
* @property {String} type 模式选择,见官网说明
* @value text 文本输入键盘
* @value number 数字输入键盘
* @value idcard 身份证输入键盘
* @value digit 带小数点的数字键盘
* @value password 密码输入键盘
* @property {Boolean} clearable 是否显示右侧的清除图标(默认true)
* @property {} v-model 用于双向绑定输入框的值
* @property {String} input-align 输入框文字的对齐方式(默认left)
* @property {String} placeholder placeholder显示值(默认 '请输入内容')
* @property {Boolean} disabled 是否禁用输入框(默认false)
* @property {String Number} maxlength 输入框的最大可输入长度(默认140)
* @property {String Number} selection-start 光标起始位置自动聚焦时有效需与selection-end搭配使用默认-1
* @property {String Number} maxlength 光标结束位置自动聚焦时有效需与selection-start搭配使用默认-1
* @property {String Number} cursor-spacing 指定光标与键盘的距离单位px(默认0)
* @property {String} placeholderStyle placeholder的样式字符串形式如"color: red;"(默认 "color: #c0c4cc;")
* @property {String} confirm-type 设置键盘右下角按钮的文字仅在type为text时生效(默认done)
* @property {Object} custom-style 自定义输入框的样式,对象形式
* @property {Boolean} focus 是否自动获得焦点(默认false)
* @property {Boolean} fixed 如果type为textarea且在一个"position:fixed"的区域需要指明为true(默认false)
* @property {Boolean} password-icon type为password时是否显示右侧的密码查看图标(默认true)
* @property {Boolean} border 是否显示边框(默认false)
* @property {String} border-color 输入框的边框颜色(默认#dcdfe6)
* @property {Boolean} auto-height 是否自动增高输入区域type为textarea时有效(默认true)
* @property {String Number} height 高度单位rpx(text类型时为70textarea时为100)
* @example <u-input v-model="value" :type="type" :border="border" />
*/
export default {
name: "u-input",
emits: ["update:modelValue", "input", "change", "confirm", "clear", "blur", "focus", "click", "touchstart"],
mixins: [Emitter],
props: {
value: {
type: [String, Number],
default: ""
},
modelValue: {
type: [String, Number],
default: ""
},
// 输入框的类型textareatextnumber
type: {
type: String,
default: "text"
},
inputAlign: {
type: String,
default: ""
},
placeholder: {
type: String,
default: "请输入内容"
},
disabled: {
type: Boolean,
default: false
},
maxlength: {
type: [Number, String],
default: 140
},
placeholderStyle: {
type: String,
default: "color: #c0c4cc;"
},
confirmType: {
type: String,
default: "done"
},
// 输入框的自定义样式
customStyle: {
type: Object,
default() {
return {};
}
},
// 如果 textarea 是在一个 position:fixed 的区域,需要显示指定属性 fixed 为 true
fixed: {
type: Boolean,
default: false
},
// 是否自动获得焦点
focus: {
type: Boolean,
default: false
},
// 密码类型时,是否显示右侧的密码图标
passwordIcon: {
type: Boolean,
default: true
},
// input|textarea是否显示边框
border: {
type: Boolean,
default: false
},
// 输入框的边框颜色
borderColor: {
type: String,
default: "#dcdfe6"
},
autoHeight: {
type: Boolean,
default: true
},
// type=select时旋转右侧的图标标识当前处于打开还是关闭select的状态
// open-打开close-关闭
selectOpen: {
type: Boolean,
default: false
},
// 高度单位rpx
height: {
type: [Number, String],
default: ""
},
// 是否可清空
clearable: {
type: [Boolean, String],
},
// 指定光标与键盘的距离,单位 px
cursorSpacing: {
type: [Number, String],
default: 0
},
// 光标起始位置自动聚焦时有效需与selection-end搭配使用
selectionStart: {
type: [Number, String],
default: -1
},
// 光标结束位置自动聚焦时有效需与selection-start搭配使用
selectionEnd: {
type: [Number, String],
default: -1
},
// 是否自动去除两端的空格
trim: {
type: Boolean,
default: true
},
// 是否显示键盘上方带有”完成“按钮那一栏
showConfirmbar: {
type: Boolean,
default: true
},
// 弹出键盘时是否自动调节高度uni-app默认值是true
adjustPosition: {
type: Boolean,
default: true
},
// input的背景色
backgroundColor: {
type: String,
},
// input的padding
padding: {
type: String,
},
},
data() {
return {
defaultValue: "",
inputHeight: 70, // input的高度
textareaHeight: 100, // textarea的高度
validateState: false, // 当前input的验证状态用于错误时边框是否改为红色
focused: false, // 当前是否处于获得焦点的状态
showPassword: false, // 是否预览密码
lastValue: "" ,// 用于头条小程序,判断@input中前后的值是否发生了变化因为头条中文下按下键没有输入内容也会触发@input时间
uForm:{
inputAlign: "",
clearable: ""
}
};
},
watch: {
valueCom(nVal, oVal) {
this.defaultValue = nVal;
// 当值发生变化且为select类型时(此时input被设置为disabled不会触发@input事件),模拟触发@input事件
if (nVal != oVal && this.type == "select")
this.handleInput({
detail: {
value: nVal
}
});
}
},
computed: {
valueCom() {
// #ifndef VUE3
return this.value;
// #endif
// #ifdef VUE3
return this.modelValue;
// #endif
},
inputAlignCom(){
return this.inputAlign || this.uForm.inputAlign || "left";
},
clearableCom(){
if (typeof this.clearable == "boolean") return this.clearable;
if (typeof this.uForm.clearable == "boolean") return this.uForm.clearable;
return true;
},
// 因为uniapp的input组件的maxlength组件必须要数值这里转为数值给用户可以传入字符串数值
inputMaxlength() {
return Number(this.maxlength);
},
getStyle() {
let style = {};
// 如果没有自定义高度就根据type为input还是textare来分配一个默认的高度
style.minHeight = this.height
? this.height + "rpx"
: this.type == "textarea"
? this.textareaHeight + "rpx"
: this.inputHeight + "rpx";
style = Object.assign(style, this.customStyle);
return style;
},
//
getCursorSpacing() {
return Number(this.cursorSpacing);
},
// 光标起始位置
uSelectionStart() {
return String(this.selectionStart);
},
// 光标结束位置
uSelectionEnd() {
return String(this.selectionEnd);
}
},
created() {
// 监听u-form-item发出的错误事件将输入框边框变红色
// #ifndef VUE3
this.$on("onFormItemError", this.onFormItemError);
// #endif
this.defaultValue = this.valueCom;
},
mounted() {
let parent = this.$u.$parent.call(this, 'u-form');
if (parent) {
Object.keys(this.uForm).map(key => {
this.uForm[key] = parent[key];
});
}
},
methods: {
/**
* change 事件
* @param event
*/
handleInput(event) {
let value = event.detail.value;
// 判断是否去除空格
if (this.trim) value = this.$u.trim(value);
// vue 原生的方法 return 出去
this.$emit("input", value);
this.$emit("update:modelValue", value);
// 当前model 赋值
this.defaultValue = value;
// 过一个生命周期再发送事件给u-form-item否则this.$emit('input')更新了父组件的值,但是微信小程序上
// 尚未更新到u-form-item导致获取的值为空从而校验混论
// 这里不能延时时间太短或者使用this.$nextTick否则在头条上会造成混乱
setTimeout(() => {
// 头条小程序由于自身bug导致中文下每按下一个键(尚未完成输入),都会触发一次@input导致错误这里进行判断处理
// #ifdef MP-TOUTIAO
if (this.$u.trim(value) == this.lastValue) return;
this.lastValue = value;
// #endif
// 将当前的值发送到 u-form-item 进行校验
this.dispatch("u-form-item", "onFieldChange", value);
}, 40);
},
/**
* blur 事件
* @param event
*/
handleBlur(event) {
// 最开始使用的是监听图标@touchstart事件自从hx2.8.4后,此方法在微信小程序出错
// 这里改为监听点击事件,手点击清除图标时,同时也发生了@blur事件导致图标消失而无法点击这里做一个延时
setTimeout(() => {
this.focused = false;
}, 100);
// vue 原生的方法 return 出去
this.$emit("blur", event.detail.value);
setTimeout(() => {
// 头条小程序由于自身bug导致中文下每按下一个键(尚未完成输入),都会触发一次@input导致错误这里进行判断处理
// #ifdef MP-TOUTIAO
if (this.$u.trim(value) == this.lastValue) return;
this.lastValue = value;
// #endif
// 将当前的值发送到 u-form-item 进行校验
this.dispatch("u-form-item", "onFieldBlur", event.detail.value);
}, 40);
},
onFormItemError(status) {
this.validateState = status;
},
onFocus(event) {
this.focused = true;
this.$emit("focus");
},
onConfirm(e) {
this.$emit("confirm", e.detail.value);
},
onClear(event) {
this.$emit("input", "");
this.$emit("update:modelValue", "");
this.$emit("clear");
},
inputClick() {
this.$emit("click");
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-input {
position: relative;
flex: 1;
@include vue-flex;
&__input {
//height: $u-form-item-height;
font-size: 28rpx;
color: $u-main-color;
flex: 1;
}
&__textarea {
width: auto;
font-size: 28rpx;
color: $u-main-color;
padding: 10rpx 0;
line-height: normal;
flex: 1;
}
&--border {
border-radius: 6rpx;
border-radius: 4px;
border: 1px solid $u-form-item-border-color;
}
&--error {
border-color: $u-type-error !important;
}
&__right-icon {
&__item {
margin-left: 10rpx;
}
&--select {
transition: transform 0.4s;
&--reverse {
transform: rotate(-180deg);
}
}
}
}
</style>

View File

@@ -0,0 +1,254 @@
<template>
<view class="u-wrap" :style="{
opacity: Number(opacity),
borderRadius: borderRadius + 'rpx',
// 因为time值需要改变,所以不直接用duration值(不能改变父组件prop传过来的值)
transition: `opacity ${time / 1000}s ease-in-out`
}"
:class="'u-lazy-item-' + elIndex">
<view :class="'u-lazy-item-' + elIndex">
<image :style="{borderRadius: borderRadius + 'rpx', height: imgHeight}" v-if="!isError" class="u-lazy-item"
:src="isShow ? image : loadingImg" :mode="imgMode" @load="imgLoaded" @error="loadError" @tap="clickImg"></image>
<image :style="{borderRadius: borderRadius + 'rpx', height: imgHeight}" class="u-lazy-item error" v-else :src="errorImg"
:mode="imgMode" @load="errorImgLoaded" @tap="clickImg"></image>
</view>
</view>
</template>
<script>
/**
* lazyLoad 懒加载
* @description 懒加载使用的场景为页面有很多图片时APP会同时加载所有的图片导致页面卡顿各个位置的图片出现前后不一致等.
* @tutorial https://www.uviewui.com/components/lazyLoad.html
* @property {String Number} index 用户自定义值,在事件触发时回调,用以区分是哪个图片
* @property {String} image 图片路径
* @property {String} loading-img 预加载时的占位图
* @property {String} error-img 图片加载出错时的占位图
* @property {String} threshold 触发加载时的位置,见上方说明,单位 rpx默认300
* @property {String Number} duration 图片加载成功时淡入淡出时间单位ms默认
* @property {String} effect 图片加载成功时淡入淡出的css动画效果默认ease-in-out
* @property {Boolean} is-effect 图片加载成功时是否启用淡入淡出效果默认true
* @property {String Number} border-radius 图片圆角值单位rpx默认0
* @property {String Number} height 图片高度注意实际高度可能受img-mode参数影响默认450
* @property {String Number} mg-mode 图片的裁剪模式详见image组件裁剪模式默认widthFix
* @event {Function} click 点击图片时触发
* @event {Function} load 图片加载成功时触发
* @event {Function} error 图片加载失败时触发
* @example <u-lazy-load :image="image" :loading-img="loadingImg" :error-img="errorImg"></u-lazy-load>
*/
export default {
name: 'u-lazy-load',
emits: ["click", "load", "error"],
props: {
index: {
type: [Number, String]
},
// 要显示的图片
image: {
type: String,
default: ''
},
// 图片裁剪模式
imgMode: {
type: String,
default: 'widthFix'
},
// 占位图片路径
loadingImg: {
type: String,
default: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAGQCAMAAAC3Ycb+AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OUM0QjNBQjkyQUQ2MTFFQTlCNUQ4RTIzNDE5RUIxNjciIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OUM0QjNBQkEyQUQ2MTFFQTlCNUQ4RTIzNDE5RUIxNjciPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo5QzRCM0FCNzJBRDYxMUVBOUI1RDhFMjM0MTlFQjE2NyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo5QzRCM0FCODJBRDYxMUVBOUI1RDhFMjM0MTlFQjE2NyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PtRHfPcAAAAzUExURZWVldfX18PDw62trZubm9zc3Li4uKGhoebm5tLS0uHh4c3Nzaenp729vcjIyLKysuvr6141L40AAAcXSURBVHja7NzZlqpGAEBR5lG0//9rIw7IJKJi4or7PGTdtN10wr5SVAEGf/qqArsAiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAg+nmQFMi5Jis+sIniED23jSzIgLTtg2D//iYme/8QBM/9lQ+CAEhbNLM3N9hEHAThX7GPCiBfAxK1b51kD+R7QMLjXg7iCsgWIPUh7pfVozG791oeBPngm48G583uW5GkBvI+SBaM2xXDn1oqum423bX/mgF5FySc2cv93Voug9TdZotsggnkBZB2NzbhrSY5HnoG07jei8dvzsJB/c3W60SALILE46+WCztsbhPR7R2VJq0ukEcT49nyy8QhaKcRa3fYHZD4+ufqOJAcgDz8/59vtw1I3QP5K6JsOG0vm3hce4I8LQp/BaRZGJC3AAn7IKOKXbC+7EdA5vdmmVwOLksgRThqOqiH4XEGsht+peoPUE8U/jJIO5OLH4GEwUslV5G0PTBG5Uiw/Y2jyigO3l9HAHKv9PYb82LloH74dZBoBUgar+l48NsNvtD0fkez9iwrAvIYZDRCl+Xs149Hm/KZmQ+QjUCiO1ei4ru7EsgnQYrkznlQb7thCuRfAzlOAPN72427P4VA/i2Q/DKT/Ls/VR8fvIBsDZIuz7TPF6TCbnk4GJkB2RokejTjuE7/unlgCuSTIO0Cy+Plp6vDfnQlBchy8QtjSHVd3EgmK1bHLm+H6+nXYbz2DuQRSPnqoL7vvq0u70on4zvxgCyWD3b9UyDVdW24PaWaiGTnFZJwPIQAebDpIKheBIm7n124ZthMJipAlkqHO+IZkP1tbfzOJark/A7MgKyvvl60fRqkvXfhuow+t9+q00+0/yyBrK8ZngOtBzldhw2X9tvpNGty0gvkmbPeJ0Cy/r09s/stbmfo0yMWkEdjevgKyOn2t2pxv7UXoibTdCDLje9/Ww1ymqzn87dbp92242ZmMRjI8hASvwKSLq4udqN6ksw8nxXN3tszD9L8Gkg+2mFrQYql5az4tvFj5xOx4VwnSdeBtGdyPwUytxK77pBVlNHdO7OK3rh/eTPUvdutT3fO52tuHMqD4N7llv8pyOQQ//w19YVDfX27+Sfuby9/6nau4pdA8vEdOZuChEH/quHt0Jg+IRJ/5+PrHwKZXfjbDiS73Zo7mu5UkzX7uTsXe0e/7nC3ePf1O69+BUg2XDfZCqSqOu7rGVf8cHBe8zhC2b61dtUHXv0OkGo6ZL4JkpbRYXdUaFevivx2M/1GIOctNh949TtAoumQ+TpIHMX54CJu+8BDd8FkE5BqcZh/59XvAClmTvKfB0nDqIlHo3T70SftyW1eX9dXtgQJqs1f/Q6QaOa/7wmQKtxH8eiGoCRuovODIO3VxOMmruZbHrLyD7z6DSDtGyT7ew1kf9hNn07c986JTovzzem0Id9wUG+Vk/IDr34DSNR7huZJkMFT6vEhqrPx/j5cnlZML8N6/PAzh9Y99Flm5Yde/c9BquDOkvkKkMP58dA4qi9vivE8JOvGz/j8FokfPpr288+pH2ZPOZrLmeGD+7KOh6dqYWJ48ki7yUg0tz0go/fv/LLddfV3sgOLJyaGPY/zrSlh1a36Arkzoue9CyG35ze6E6/dzO2Ga0EGHqdRJIkfn9/8OEjTW8Vq91ZWh39FeehWA7Nu9ft8CpUEk1WWOyDF0OPyEU2Pnzf/bZC0P6IPzmAvu7KauQBVrgKpJ0tG2arHzX8e5Pb3PezNs/PrX+3JMyCLn9XXf37tPFHvt09WfCDDjx+yyn1/p1V11j7GnB/q3leLuVva79S/tzed+db08YpF4uOZtmz/9oXWMq6BCAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiAALELvqt/BBgACqVeUBXxcCkAAAAASUVORK5CYII='
},
// 加载失败的错误占位图
errorImg: {
type: String,
default: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAGQCAMAAAC3Ycb+AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ODdDMjhENDYyQUQ2MTFFQTlDQ0VBODgxQjFFOEEyMEMiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6ODdDMjhENDcyQUQ2MTFFQTlDQ0VBODgxQjFFOEEyMEMiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo4N0MyOEQ0NDJBRDYxMUVBOUNDRUE4ODFCMUU4QTIwQyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo4N0MyOEQ0NTJBRDYxMUVBOUNDRUE4ODFCMUU4QTIwQyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PhLwhikAAAAzUExURZWVldfX162trcPDw5ubm7i4uNzc3Obm5s3NzaGhoeHh4cjIyKenp9LS0r29vbKysuvr67sDMEkAAAlpSURBVHja7NzpYqMgAIVRUVHc8/5PO66R1WAbOzX97q+ZtDEpR0AWTR7kVyWhCAAhgABCAAGEAAIIAQQQAggBBBACCCAEEEAIIIAQQAgggBBAACGAAEIAAYQAQgABhAACCAEEEAIIIAQQAgggBBBACCCAEEAAIYAQQAAhgABCAAGEAAIIAYQAAggBBBACCCAEEEAIIAQQQAgggBBAACGAAEIAIYAAQgABhAACCAEEEAIIAQQQAgggBBBACCCAEEAAIYAQQAAhgABCAAGEAAIIAYQAAggBBBACCCAEEEAIIAQQQAgggBBAACGAAEIAIYAAQgABhAACCAEEEAIIAQQQAgggBBBACCCAEEAIIIAQQAAhgABCAAGEAEIAAYQAAggBBBACCCAEEAIIIAQQQAgggBBAACGAEEAAIYAAsqeX5QWHKIcs/Ptl03lfL4zDFPWfBGmSpPn+IZzSH5KkCL5B+n+oklwz6Iz//R2QzFOabzhEmiRirAmZt/bl0w/dpMbLqeeo4wEdpC7zR5WAPKziHKtO7ql+ReKvIa9BxgNaL5ZtEkpeAGIVp5jKJa09xVo9vgSSzQcszdYvmOqjQNSQ6pHK6rO1n1Xj32788miwHLaZz1Tl9i/yayDlYJ/60/+lp8GSY7OY1B8E4p55bWmfquFk22GLuUUxi78cX+m+BjL2GLkhMrV+/muS6Sfic0CEp5T1Yu2OQdTzsKV0MJV73KVjroyTffxfuv5Tf3fd6iLT9wz8YdVHgUzF2Is9/Xhi5sYJqP1w/GUpjOiHVbaI0w2L+pg3GZzvtokcgHxWDXHaiy78l3sPke01qphamT5c+dqyeAGSumdL/mkggauTam0e3L/mPEiqtzKDbl0Z1Wn8xOa4ySo8X/7TQIJnY/seEKWf12UmC72CKP9xYjr19RPT7NNA+oMO+R0gwmlotAry+C6I0f59ch8yXVQOr0BKYcXt1IUYRyCt+Ur9HGsrQKI79WY9sY9ARPKlzFOFdb41ioD8b5Bp+mqeeRKAxINkESBFGpOpKhgv9OuYpH8A8l4Qa3qp60Kl2/k+rG2sWafuuyCBafb2j4JkgZUob3nWcmicpkxEgmTLLGejTxnWSWCi8lPmsk6DlIHFJv24ojiYyYoGacwL8zXTLEAVaDI/Ybb3NIgKDSv2oXpmHkvNs+PTpMASEdlk7fOZeRk37fwJ6yGnQarQsGIfqqcvx43rTOXY6jf7uKXdRzdLDRPbjIrx1cIj3Kr4KyBFezzgUGuR5893qkOQ19fR2uVBaU+r16LphJNOiatK7PeBZK/Kb+tUn71rcQjSvARpghfH/yG/D2RetTuI3N5QrMWdP46brP7FmKZ//CGQ9At9SL01DLkzY/Vs8Z97fQZ7gelw7jHqCz+/Wile5J4g3Vc79eb5a6oLSue+Ve83gaSv2jp5PxCzjzwFUm9zw9MllSMil1kS4d2E9SaQ1xNo9wMxx0+nQNLnew/WDHvveMAHYm08mofl3TFI/8pD3Q6kMAv6DIi2jTCwRJUvNdDYrrJum9oHhusCbWALonwxBRk1vXMnEGWuT5wAmfYuVGUYpJ7fUZujCN92hvzwWlrFgxSfANKb10DxIMbShnfrynyZZV30imA7P43ArXXHbvBVkTCIuGy25AdBrHmNeBCpL214QdLp9LZarG3IMWrmW0ehtuO7F2PS09UcgqS3B7FKPhpknrStD0HGF/vQRne37LwLG8EbHT4WxN7/Fg0yD9Yr/3br4nnstA+0Il6QxzdBmg8A6a2/IRbkcK9h/uzV8zywF/oSkOyageCPglRWgcWClHnEzs9q/t/SENVXgFijlsq3VtXdCsRp4qObrLLLgjuzSq3fX89ZZW6AfxNIzF6X9FYgThN/fk093KkvHX/hbWd+DqS/FUhlf+G3gohEXzVs3g9iDluWoaW8fL73QhB34u+tIHIf19nLuF4Q98a09Eynnl56q+ePgEhnX+dbQOp6H5XnJ0ACd8dFgkwf12nTOTcEqd2pom+CFF02TIPw6dKmrLS5qOtBpo8b5quUtrwrSGbuqPkeSJqllTFHO02NPxdMrm+y5LKdWyWXjw4vA5nGEtnjuyCFyHqNYvEolzmASm3zK1Eg5zr13lhqV1tlksnVw8Pkwgri7O07AVKLJkutRYw87bPlRpBpNXE8xGb+fhBlvEGrGPLqViu5sILIx9dAmqF1705sxF4M8+R8P5dOdQwi12fMnATpjJ2JSt/POIvU9wPJEs/jduJAjLvU0yFT0i64Yb1bsVi79dA4pEy3TzoHMq2O7Re4vXm5O9+l290NpE4CU+YRIMNye2iaqbVS2AUnn2fsekthYKReVNutVedA5juttyIXrT38mOds+ps9DWhwL7GWc61/DVKPzVN9UHDarf1icU98IOU8tm6L031Iq63t1tKzj3fe/FCpO4F0/i0Z2+yvA1KeGBjqj1qYx8/zoxpKZ1Yl367I1k+sfcft/QPy9csXy/32qX1qLZsrryG5BGQaRj0vc/b7N54XXq293TCLB5HO42Fy517obW19b+qjl3CHp0fdLJcWvmdy1etESi/uAdJrs1hTaUklHuW8qSDdC3UfXVR5cnD3rAFSSqtFb7z7eapErx7rC739jCXfbK3aWiipjXo8UbmxXPa7QQq9R289j2Gr88N7Ag5AlHPRKc37pNZv0CZtX1tVMG6rm8qW1/KlCgQvcMss933ybwXZz3dReW5yce4ByZtHFIhwT9kmjxg8BzbKDUe1PB9edBJqSN7/KM1LmqyuMZ5BpeTUw1aD/uDI0relPfSHa/Wn8Pxq1BNfxy/h3IdwOJqIKumb9CHvTqMefyY82RoQAgggBBBACCCAEEAAIYAQQAAhgABCAAGEAAIIAYQAAggBBBACCCAEEEAIIAQQQAgggBBAACGAAEIAIYAAQgABhAACCAEEEAIIAQQQAgggBBBACCCAEEAIIIAQQAAhgABCAAGEAEIAAYQAAggBBBACCCAEEAIIIAQQQAgggBBAACGAEEAAIYAAQgABhAACCAEEEAIIAQQQAgggBBBACCCAEEAIIIAQQAAhgABCAAGEAEIAAYQAAggBBBACCCAEEAIIIAQQQAgggBBAACGAEEAAIYAAQgABhAACCAGEAAIIAQQQAgggBBBACCAEEEAIIIAQQAAhgABCACGAAEIAAYQAAggBBBACCAEEEAIIIAQQQAggfyL/BBgA8PgLdH0TBtkAAAAASUVORK5CYII='
},
// 图片进入可见区域前多少像素时单位rpx开始加载图片
// 负数为图片超出屏幕底部多少距离后触发懒加载,正数为图片顶部距离屏幕底部多少距离时触发(图片还没出现在屏幕上)
threshold: {
type: [Number, String],
default: 100
},
// 淡入淡出动画的过渡时间
duration: {
type: [Number, String],
default: 500
},
// 渡效果的速度曲线,各个之间差别不大,因为这是淡入淡出,且时间很短,不是那些变形或者移动的情况,会明显
// linear|ease|ease-in|ease-out|ease-in-out|cubic-bezier(n,n,n,n);
effect: {
type: String,
default: 'ease-in-out'
},
// 是否使用过渡效果
isEffect: {
type: Boolean,
default: true
},
// 圆角值
borderRadius: {
type: [Number, String],
default: 0
},
// 图片高度单位rpx
height: {
type: [Number, String],
default: '450'
}
},
data() {
return {
isShow: false,
opacity: 1,
time: this.duration,
loadStatus: '', // 默认是懒加载中的状态
isError: false, // 图片加载失败
elIndex: this.$u.guid()
}
},
computed: {
// 将threshold从rpx转为px
getThreshold() {
// 先取绝对值因为threshold可能是负数最后根据this.threshold是正数或者负数重新还原
let thresholdPx = uni.upx2px(Math.abs(this.threshold));
return this.threshold < 0 ? -thresholdPx : thresholdPx;
},
// 计算图片的高度可能为auto带%,或者直接数值
imgHeight() {
return this.$u.addUnit(this.height);
}
},
created() {
// 由于一些特殊原因不能将此变量放到data中定义
this.observer = {};
},
watch: {
isShow(nVal) {
// 如果是不开启过渡效果,直接返回
if (!this.isEffect) return;
this.time = 0;
// 原来opacity为1(不透明,是为了显示占位图)改成0(透明,意味着该元素显示的是背景颜色,默认的白色)再改成1是为了获得过渡效果
this.opacity = 0;
// 延时30ms否则在浏览器H5过渡效果无效
setTimeout(() => {
this.time = this.duration;
this.opacity = 1;
}, 30)
},
// 图片路径发生变化时需要重新标记一些变量否则会一直卡在某一个状态比如isError
image(n) {
if(!n) {
// 如果传入null或者''或者undefined标记为错误状态
this.isError = true;
} else {
this.init();
this.isError = false;
}
}
},
methods: {
// 用于重新初始化
init() {
this.isError = false;
this.loadStatus = '';
},
// 点击图片触发的事件,loadlazy-还是懒加载中状态loading-图片正在加载loaded-图片加加载完成
clickImg() {
let whichImg = '';
// 如果isShow为false意味着图片还没开始加载点击的只能是最开始的占位图
if (this.isShow == false) whichImg = 'lazyImg';
// 如果isError为true意味着图片加载失败这是只剩下错误的占位图所以点击的只能是错误占位图
// 当然,也可以给错误的占位图元素绑定点击事件,看你喜欢~
else if (this.isError == true) whichImg = 'errorImg';
// 总共三张图片,除了两个占位图,剩下的只能是正常的那张图片了
else whichImg = 'realImg';
// 只通知当前图片的index
this.$emit('click', this.index);
},
// 图片加载完成事件可能是加载占位图时触发也可能是加载真正的图片完成时触发通过isShow区分
imgLoaded() {
// 占位图加载完成
if (this.loadStatus == '') {
this.loadStatus = 'lazyed';
}
// 真正的图片加载完成
else if (this.loadStatus == 'lazyed') {
this.loadStatus = 'loaded';
this.$emit('load', this.index);
}
},
// 错误的图片加载完成
errorImgLoaded() {
this.$emit('error', this.index);
},
// 图片加载失败
loadError() {
this.isError = true;
},
disconnectObserver(observerName) {
const observer = this[observerName];
observer && observer.disconnect();
},
},
// #ifndef VUE3
// 组件销毁前将实例从u-form的缓存中移除
beforeDestroy() {
// 销毁页面时,可能还没触发某张很底部的懒加载图片,所以把这个事件给去掉
//observer.disconnect();
},
// #endif
// #ifdef VUE3
beforeUnmount() {
},
// #endif
mounted() {
// 此uOnReachBottom事件由mixin.js发出目的是让页面到底时保证所有图片都进行加载做到绝对稳定且可靠
this.$nextTick(() => {
uni.$once('uOnReachBottom', () => {
if (!this.isShow) this.isShow = true;
});
})
// mounted的时候不一定挂载了这个元素延时30ms否则会报错或者不报错但是也没有效果
setTimeout(() => {
// 这里是组件内获取布局状态不能用uni.createIntersectionObserver而必须用this.createIntersectionObserver
this.disconnectObserver('contentObserver');
const contentObserver = uni.createIntersectionObserver(this);
// 要理解这里怎么计算的,请看这个:
// https://blog.csdn.net/qq_25324335/article/details/83687695
contentObserver.relativeToViewport({
bottom: this.getThreshold,
}).observe('.u-lazy-item-' + this.elIndex, (res) => {
if (res.intersectionRatio > 0) {
// 懒加载状态改变
this.isShow = true;
// 如果图片已经加载,去掉监听,减少性能的消耗
this.disconnectObserver('contentObserver');
}
})
this.contentObserver = contentObserver;
}, 30)
}
}
</script>
<style scoped lang="scss">
@import "../../libs/css/style.components.scss";
.u-wrap {
background-color: #eee;
overflow: hidden;
}
.u-lazy-item {
width: 100%;
// 骗系统开启硬件加速
transform: transition3d(0, 0, 0);
// 防止图片加载“闪一下”
will-change: transform;
/* #ifndef APP-NVUE */
display: block;
/* #endif */
}
</style>

View File

@@ -0,0 +1,108 @@
<template>
<view v-if="show" class="u-loading" :class="mode == 'circle' ? 'u-loading-circle' : 'u-loading-flower'" :style="[cricleStyle]">
</view>
</template>
<script>
/**
* loading 加载动画
* @description 警此组件为一个小动画目前用在uView的loadmore加载更多和switch开关等组件的正在加载状态场景。
* @tutorial https://www.uviewui.com/components/loading.html
* @property {String} mode 模式选择见官网说明默认circle
* @value circle 圆型
* @value flower 花型
* @property {String} color 动画活动区域的颜色,只对 mode = flower 模式有效(默认#c7c7c7
* @property {String Number} size 加载图标的大小单位rpx默认34
* @property {Boolean} show 是否显示动画默认true
* @example <u-loading mode="circle"></u-loading>
*/
export default {
name: "u-loading",
props: {
// 动画的类型
mode: {
type: String,
default: 'circle'
},
// 动画的颜色
color: {
type: String,
default: '#c7c7c7'
},
// 加载图标的大小单位rpx
size: {
type: [String, Number],
default: '34'
},
// 是否显示动画
show: {
type: Boolean,
default: true
}
},
computed: {
// 加载中圆圈动画的样式
cricleStyle() {
let style = {};
style.width = this.size + 'rpx';
style.height = this.size + 'rpx';
if (this.mode == 'circle') style.borderColor = `#e4e4e4 #e4e4e4 #e4e4e4 ${this.color ? this.color : '#c7c7c7'}`;
return style;
},
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-loading-circle {
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
vertical-align: middle;
width: 28rpx;
height: 28rpx;
background: 0 0;
border-radius: 50%;
border: 2px solid;
border-color: #e5e5e5 #e5e5e5 #e5e5e5 #8f8d8e;
animation: u-circle 1s linear infinite;
}
.u-loading-flower {
width: 20px;
height: 20px;
display: inline-block;
vertical-align: middle;
-webkit-animation: u-flower 1s steps(12) infinite;
animation: u-flower 1s steps(12) infinite;
background: transparent url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjAiIGhlaWdodD0iMTIwIiB2aWV3Qm94PSIwIDAgMTAwIDEwMCI+PHBhdGggZmlsbD0ibm9uZSIgZD0iTTAgMGgxMDB2MTAwSDB6Ii8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjRTlFOUU5IiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAgLTMwKSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iIzk4OTY5NyIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSgzMCAxMDUuOTggNjUpIi8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjOUI5OTlBIiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0icm90YXRlKDYwIDc1Ljk4IDY1KSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iI0EzQTFBMiIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSg5MCA2NSA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNBQkE5QUEiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoMTIwIDU4LjY2IDY1KSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iI0IyQjJCMiIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSgxNTAgNTQuMDIgNjUpIi8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjQkFCOEI5IiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0icm90YXRlKDE4MCA1MCA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNDMkMwQzEiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoLTE1MCA0NS45OCA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNDQkNCQ0IiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoLTEyMCA0MS4zNCA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNEMkQyRDIiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoLTkwIDM1IDY1KSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iI0RBREFEQSIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSgtNjAgMjQuMDIgNjUpIi8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjRTJFMkUyIiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0icm90YXRlKC0zMCAtNS45OCA2NSkiLz48L3N2Zz4=) no-repeat;
background-size: 100%;
}
@keyframes u-flower {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
to {
-webkit-transform: rotate(1turn);
transform: rotate(1turn);
}
}
@keyframes u-circle {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
to {
-webkit-transform: rotate(1turn);
transform: rotate(1turn);
}
}
</style>

View File

@@ -0,0 +1,204 @@
<template>
<view class="u-load-more-wrap" :style="{
backgroundColor: bgColor,
marginBottom: marginBottom + 'rpx',
marginTop: marginTop + 'rpx',
height: $u.addUnit(height)
}">
<u-line color="#d4d4d4" length="50"></u-line>
<!-- 加载中和没有更多的状态才显示两边的横线 -->
<view :class="status == 'loadmore' || status == 'nomore' ? 'u-more' : ''" class="u-load-more-inner">
<view class="u-loadmore-icon-wrap">
<u-loading class="u-loadmore-icon" :color="iconColor" :mode="iconType == 'circle' ? 'circle' : 'flower'" :show="status == 'loading' && icon"></u-loading>
</view>
<!-- 如果没有更多的状态下显示内容为dot粗点加载特定样式 -->
<view class="u-line-1" :style="[loadTextStyle]" :class="[(status == 'nomore' && isDot == true) ? 'u-dot-text' : 'u-more-text']" @tap="loadMore">
{{ showText }}
</view>
</view>
<u-line color="#d4d4d4" length="50"></u-line>
</view>
</template>
<script>
/**
* loadmore 加载更多
* @description 此组件一般用于标识页面底部加载数据时的状态。
* @tutorial https://www.uviewui.com/components/loadMore.html
* @property {String} status 组件状态默认loadmore
* @property {String} bg-color 组件背景颜色,在页面是非白色时会用到(默认#ffffff
* @property {Boolean} icon 加载中时是否显示图标默认true
* @property {String} icon-type 加载中时的图标类型默认circle
* @property {String} icon-color icon-type为circle时有效加载中的动画图标的颜色默认#b7b7b7
* @property {Boolean} is-dot status为nomore时内容显示为一个"●"默认false
* @property {String} color 字体颜色(默认#606266
* @property {String Number} margin-top 到上一个相邻元素的距离
* @property {String Number} margin-bottom 到下一个相邻元素的距离
* @property {Object} load-text 自定义显示的文字,见上方说明示例
* @event {Function} loadmore status为loadmore时点击组件会发出此事件
* @example <u-loadmore :status="status" icon-type="iconType" load-text="loadText" />
*/
export default {
name: "u-loadmore",
emits: ["loadmore"],
props: {
// 组件背景色
bgColor: {
type: String,
default: 'transparent'
},
// 是否显示加载中的图标
icon: {
type: Boolean,
default: true
},
// 字体大小
fontSize: {
type: String,
default: '28'
},
// 字体颜色
color: {
type: String,
default: '#606266'
},
// 组件状态loadmore-加载前的状态loading-加载中的状态nomore-没有更多的状态
status: {
type: String,
default: 'loadmore'
},
// 加载中状态的图标flower-花朵状图标circle-圆圈状图标
iconType: {
type: String,
default: 'circle'
},
// 显示的文字
loadText: {
type: Object,
default () {
return {
loadmore: '加载更多',
loading: '正在加载...',
nomore: '没有更多了'
}
}
},
// 在“没有更多”状态下,是否显示粗点
isDot: {
type: Boolean,
default: false
},
// 加载中显示圆圈动画时,动画的颜色
iconColor: {
type: String,
default: '#b7b7b7'
},
// 上边距
marginTop: {
type: [String, Number],
default: 0
},
// 下边距
marginBottom: {
type: [String, Number],
default: 0
},
// 高度单位rpx
height: {
type: [String, Number],
default: 'auto'
}
},
data() {
return {
// 粗点
dotText: "●"
}
},
computed: {
// 加载的文字显示的样式
loadTextStyle() {
return {
color: this.color,
fontSize: this.fontSize + 'rpx',
position: 'relative',
zIndex: 1,
backgroundColor: this.bgColor,
// 如果是加载中状态,动画和文字需要距离近一点
}
},
// 加载中圆圈动画的样式
cricleStyle() {
return {
borderColor: `#e5e5e5 #e5e5e5 #e5e5e5 ${this.circleColor}`
}
},
// 加载中花朵动画形式
// 动画由base64图片生成暂不支持修改
flowerStyle() {
return {
}
},
// 显示的提示文字
showText() {
let text = '';
if(this.status == 'loadmore') text = this.loadText.loadmore;
else if(this.status == 'loading') text = this.loadText.loading;
else if(this.status == 'nomore' && this.isDot) text = this.dotText;
else text = this.loadText.nomore;
return text;
}
},
methods: {
loadMore() {
// 只有在“加载更多”的状态下才发送点击事件,内容不满一屏时无法触发底部上拉事件,所以需要点击来触发
if(this.status == 'loadmore') this.$emit('loadmore');
}
}
}
</script>
<style scoped lang="scss">
@import "../../libs/css/style.components.scss";
/* #ifdef MP */
// 在mp.scss中赋予了u-line为flex: 1这里需要一个明确的长度所以重置掉它
// 在组件内部,把组件名(u-line)当做选择器,在微信开发工具会提示不合法,但不影响使用
u-line {
flex: none;
}
/* #endif */
.u-load-more-wrap {
@include vue-flex;
justify-content: center;
align-items: center;
}
.u-load-more-inner {
@include vue-flex;
justify-content: center;
align-items: center;
padding: 0 12rpx;
}
.u-more {
position: relative;
@include vue-flex;
justify-content: center;
}
.u-dot-text {
font-size: 28rpx;
}
.u-loadmore-icon-wrap {
margin-right: 8rpx;
}
.u-loadmore-icon {
@include vue-flex;
align-items: center;
justify-content: center;
}
</style>

View File

@@ -0,0 +1,137 @@
<template>
<view class="u-mask" hover-stop-propagation :style="[maskStyle, zoomStyle, filterStyle]" @tap="click" @touchmove.stop.prevent="() => {}" :class="{
'u-mask-zoom': zoom,
'u-mask-show': show
}">
<slot />
</view>
</template>
<script>
/**
* mask 遮罩
* @description 创建一个遮罩层,用于强调特定的页面元素,并阻止用户对遮罩下层的内容进行操作,一般用于弹窗场景
* @tutorial https://www.uviewui.com/components/mask.html
* @property {Boolean} show 是否显示遮罩默认false
* @property {String Number} z-index z-index 层级默认1070
* @property {Object} custom-style 自定义样式对象,见上方说明
* @property {String Number} duration 动画时长单位毫秒默认300
* @property {Boolean} zoom 是否使用scale对遮罩进行缩放默认true
* @property {Boolean} mask-click-able 遮罩是否可点击为false时点击不会发送click事件默认true
* @event {Function} click mask-click-able为true时点击遮罩发送此事件
* @example <u-mask :show="show" @click="show = false"></u-mask>
*/
export default {
name: "u-mask",
emits: ["click"],
props: {
// 是否显示遮罩
show: {
type: Boolean,
default: false
},
// 层级z-index
zIndex: {
type: [Number, String],
default: ''
},
// 用户自定义样式
customStyle: {
type: Object,
default () {
return {}
}
},
// 遮罩的动画样式, 是否使用使用zoom进行scale进行缩放
zoom: {
type: Boolean,
default: true
},
// 遮罩的过渡时间单位为ms
duration: {
type: [Number, String],
default: 300
},
// 是否可以通过点击遮罩进行关闭
maskClickAble: {
type: Boolean,
default: true
},
// 遮罩的模糊度
blur: {
type: [Number, String],
default: 0
},
},
data() {
return {
zoomStyle: {
transform: ''
},
scale: 'scale(1.2, 1.2)'
}
},
watch: {
show(n) {
if(n && this.zoom) {
// 当展示遮罩的时候设置scale为1达到缩小(原来为1.2)的效果
this.zoomStyle.transform = 'scale(1, 1)';
} else if(!n && this.zoom) {
// 当隐藏遮罩的时候设置scale为1.2,达到放大(因为显示遮罩时已重置为1)的效果
this.zoomStyle.transform = this.scale;
}
}
},
computed: {
maskStyle() {
let style = {};
style.backgroundColor = "rgba(0, 0, 0, 0.6)";
if(this.show) style.zIndex = this.zIndex ? this.zIndex : this.$u.zIndex.mask;
else style.zIndex = -1;
style.transition = `all ${this.duration / 1000}s ease-in-out`;
// 判断用户传递的对象是否为空,不为空就进行合并
if (Object.keys(this.customStyle).length) style = {
...style,
...this.customStyle
};
return style;
},
filterStyle() {
let { blur } = this;
let style = {};
if (blur) {
style.backdropFilter = `blur(${blur}rpx)`;
}
return style;
}
},
methods: {
click() {
if (!this.maskClickAble) return;
this.$emit('click');
}
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
opacity: 0;
transition: transform 0.3s;
}
.u-mask-show {
opacity: 1;
}
.u-mask-zoom {
transform: scale(1.2, 1.2);
}
</style>

View File

@@ -0,0 +1,339 @@
<template>
<view>
<u-popup
:blur="blur"
:zoom="zoom"
mode="center"
:popup="false"
:z-index="uZIndex"
v-model="popupValue"
:length="width"
:mask-close-able="maskCloseAble"
:border-radius="borderRadius"
@close="popupClose"
:negative-top="negativeTop"
>
<view class="u-model">
<view v-if="showTitle" class="u-model__title u-line-1" :style="[titleStyle]">
{{ title }}
</view>
<view class="u-model__content">
<view :style="[contentStyle]" v-if="$slots.default || $slots.$default"><slot /></view>
<view v-else class="u-model__content__message" :style="[contentStyle]">
{{ content }}
</view>
</view>
<view class="u-model__footer u-border-top" v-if="showCancelButton || showConfirmButton">
<view
v-if="showCancelButton"
:hover-stay-time="100"
hover-class="u-model__btn--hover"
class="u-model__footer__button"
:style="[cancelBtnStyle]"
@tap="cancel"
>
{{ cancelText }}
</view>
<view
v-if="showConfirmButton || $slots['confirm-button']"
:hover-stay-time="100"
:hover-class="asyncClose ? 'none' : 'u-model__btn--hover'"
class="u-model__footer__button hairline-left"
:style="[confirmBtnStyle]"
@tap="confirm"
>
<slot v-if="$slots['confirm-button']" name="confirm-button"></slot>
<block v-else>
<u-loading mode="circle" :color="confirmColor" v-if="loading"></u-loading>
<block v-else>{{ confirmText }}</block>
</block>
</view>
</view>
</view>
</u-popup>
</view>
</template>
<script>
/**
* modal 模态框
* @description 弹出模态框,常用于消息提示、消息确认、在当前页面内完成特定的交互操作
* @tutorial https://www.uviewui.com/components/modal.html
* @property {Boolean} value 是否显示模态框
* @property {String | Number} z-index 层级
* @property {String} title 模态框标题(默认"提示"
* @property {String | Number} width 模态框宽度默认600
* @property {String} content 模态框内容(默认"内容"
* @property {Boolean} show-title 是否显示标题默认true
* @property {Boolean} async-close 是否异步关闭只对确定按钮有效默认false
* @property {Boolean} show-confirm-button 是否显示确认按钮默认true
* @property {Stringr | Number} negative-top modal往上偏移的值
* @property {Boolean} show-cancel-button 是否显示取消按钮默认false
* @property {Boolean} mask-close-able 是否允许点击遮罩关闭modal默认false
* @property {String} confirm-text 确认按钮的文字内容(默认"确认"
* @property {String} cancel-text 取消按钮的文字内容(默认"取消"
* @property {String} cancel-color 取消按钮的颜色(默认"#606266"
* @property {String} confirm-color 确认按钮的文字内容(默认"#2979ff"
* @property {String | Number} border-radius 模态框圆角值单位rpx默认16
* @property {Object} title-style 自定义标题样式,对象形式
* @property {Object} content-style 自定义内容样式,对象形式
* @property {Object} cancel-style 自定义取消按钮样式,对象形式
* @property {Object} confirm-style 自定义确认按钮样式,对象形式
* @property {Boolean} zoom 是否开启缩放模式默认true
* @event {Function} confirm 确认按钮被点击
* @event {Function} cancel 取消按钮被点击
* @example <u-modal :src="title" :content="content"></u-modal>
*/
export default {
name: "u-modal",
emits: ["update:modelValue", "input", "confirm", "cancel"],
props: {
// 是否显示Modal
value: {
type: Boolean,
default: false
},
modelValue: {
type: Boolean,
default: false
},
// 层级z-index
zIndex: {
type: [Number, String],
default: ""
},
// 标题
title: {
type: [String],
default: "提示"
},
// 弹窗宽度,可以是数值(rpx)百分比auto等
width: {
type: [Number, String],
default: 600
},
// 弹窗内容
content: {
type: String,
default: "内容"
},
// 是否显示标题
showTitle: {
type: Boolean,
default: true
},
// 是否显示确认按钮
showConfirmButton: {
type: Boolean,
default: true
},
// 是否显示取消按钮
showCancelButton: {
type: Boolean,
default: false
},
// 确认文案
confirmText: {
type: String,
default: "确认"
},
// 取消文案
cancelText: {
type: String,
default: "取消"
},
// 确认按钮颜色
confirmColor: {
type: String,
default: "#2979ff"
},
// 取消文字颜色
cancelColor: {
type: String,
default: "#606266"
},
// 圆角值
borderRadius: {
type: [Number, String],
default: 16
},
// 标题的样式
titleStyle: {
type: Object,
default() {
return {};
}
},
// 内容的样式
contentStyle: {
type: Object,
default() {
return {};
}
},
// 取消按钮的样式
cancelStyle: {
type: Object,
default() {
return {};
}
},
// 确定按钮的样式
confirmStyle: {
type: Object,
default() {
return {};
}
},
// 是否开启缩放效果
zoom: {
type: Boolean,
default: true
},
// 是否异步关闭,只对确定按钮有效
asyncClose: {
type: Boolean,
default: false
},
// 是否允许点击遮罩关闭modal
maskCloseAble: {
type: Boolean,
default: false
},
// 给一个负的margin-top往上偏移避免和键盘重合的情况
negativeTop: {
type: [String, Number],
default: 0
},
// 遮罩的模糊度
blur: {
type: [Number, String],
default: 0
}
},
data() {
return {
loading: false, // 确认按钮是否正在加载中
popupValue: false
};
},
computed: {
valueCom() {
// #ifndef VUE3
return this.value;
// #endif
// #ifdef VUE3
return this.modelValue;
// #endif
},
cancelBtnStyle() {
return Object.assign(
{
color: this.cancelColor
},
this.cancelStyle
);
},
confirmBtnStyle() {
return Object.assign(
{
color: this.confirmColor
},
this.confirmStyle
);
},
uZIndex() {
return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
}
},
watch: {
// 如果是异步关闭时外部修改v-model的值为false时重置内部的loading状态
// 避免下次打开的时候,状态混乱
valueCom:{
immediate: true,
handler(n){
if (n === true) this.loading = false;
this.popupValue = n;
}
}
},
methods: {
confirm() {
// 异步关闭
if (this.asyncClose) {
this.loading = true;
} else {
this.$emit("input", false);
this.$emit("update:modelValue", false);
}
this.$emit("confirm");
},
cancel() {
this.$emit("cancel");
this.$emit("input", false);
this.$emit("update:modelValue", false);
// 目前popup弹窗关闭有一个延时操作此处做一个延时
// 避免确认按钮文字变成了"确定"字样modal还没消失造成视觉不好的效果
setTimeout(() => {
this.loading = false;
}, 300);
},
// 点击遮罩关闭modal设置v-model的值为false否则无法第二次弹起modal
popupClose() {
this.$emit("input", false);
this.$emit("update:modelValue", false);
},
// 清除加载中的状态
clearLoading() {
this.loading = false;
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-model {
height: auto;
overflow: hidden;
font-size: 32rpx;
background-color: #fff;
&__btn--hover {
background-color: rgb(230, 230, 230);
}
&__title {
padding-top: 48rpx;
font-weight: 500;
text-align: center;
color: $u-main-color;
}
&__content {
&__message {
padding: 48rpx;
font-size: 30rpx;
text-align: center;
color: $u-content-color;
}
}
&__footer {
@include vue-flex;
&__button {
flex: 1;
height: 100rpx;
line-height: 100rpx;
font-size: 32rpx;
box-sizing: border-box;
cursor: pointer;
text-align: center;
border-radius: 4rpx;
}
}
}
</style>

View File

@@ -0,0 +1,315 @@
<template>
<view class="">
<view class="u-navbar" :style="[navbarStyle]" :class="{ 'u-navbar-fixed': isFixed, 'u-border-bottom': borderBottom }">
<view class="u-status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
<view class="u-navbar-inner" :style="[navbarInnerStyle]">
<view class="u-back-wrap" v-if="isBack" @tap="goBack">
<view class="u-icon-wrap">
<u-icon :name="backIconName" :color="backIconColor" :size="backIconSize"></u-icon>
</view>
<view class="u-icon-wrap u-back-text u-line-1" v-if="backText" :style="[backTextStyle]">{{ backText }}</view>
</view>
<view class="u-navbar-content-title" v-if="title" :style="[titleStyle]">
<view
class="u-title u-line-1"
:style="{
color: titleColor,
fontSize: titleSize + 'rpx',
fontWeight: titleBold ? 'bold' : 'normal'
}">
{{ title }}
</view>
</view>
<view class="u-slot-content">
<slot></slot>
</view>
<view class="u-slot-right">
<slot name="right"></slot>
</view>
</view>
</view>
<!-- 解决fixed定位后导航栏塌陷的问题 -->
<view class="u-navbar-placeholder" v-if="isFixed && !immersive" :style="{ width: '100%', height: Number(navbarHeight) + statusBarHeight + 'px' }"></view>
</view>
</template>
<script>
// 获取系统状态栏的高度
let systemInfo = uni.getSystemInfoSync();
let menuButtonInfo = {};
// 如果是小程序,获取右上角胶囊的尺寸信息,避免导航栏右侧内容与胶囊重叠(支付宝小程序非本API尚未兼容)
// #ifdef MP-WEIXIN || MP-BAIDU || MP-TOUTIAO || MP-QQ
menuButtonInfo = uni.getMenuButtonBoundingClientRect();
// #endif
/**
* navbar 自定义导航栏
* @description 此组件一般用于在特殊情况下需要自定义导航栏的时候用到一般建议使用uniapp自带的导航栏。
* @tutorial https://www.uviewui.com/components/navbar.html
* @property {String Number} height 导航栏高度(不包括状态栏高度在内,内部自动加上)注意这里的单位是px默认44
* @property {String} back-icon-color 左边返回图标的颜色(默认#606266
* @property {String} back-icon-name 左边返回图标的名称只能为uView自带的图标默认arrow-left
* @property {String Number} back-icon-size 左边返回图标的大小单位rpx默认30
* @property {String} back-text 返回图标右边的辅助提示文字
* @property {Object} back-text-style 返回图标右边的辅助提示文字的样式,对象形式(默认{ color: '#606266' }
* @property {String} title 导航栏标题,如设置为空字符,将会隐藏标题占位区域
* @property {String Number} title-width 导航栏标题的最大宽度内容超出会以省略号隐藏单位rpx默认250
* @property {String} title-color 标题的颜色(默认#606266
* @property {String Number} title-size 导航栏标题字体大小单位rpx默认32
* @property {Function} custom-back 自定义返回逻辑方法
* @property {String Number} z-index 固定在顶部时的z-index值默认980
* @property {Boolean} is-back 是否显示导航栏左边返回图标和辅助文字默认true
* @property {Object} background 导航栏背景设置,见官网说明(默认{ background: '#ffffff' }
* @property {Boolean} is-fixed 导航栏是否固定在顶部默认true
* @property {Boolean} immersive 沉浸式允许fixed定位后导航栏塌陷仅fixed定位下生效默认false
* @property {Boolean} border-bottom 导航栏底部是否显示下边框如定义了较深的背景颜色可取消此值默认true
* @example <u-navbar back-text="返回" title="剑未配妥,出门已是江湖"></u-navbar>
*/
export default {
name: "u-navbar",
props: {
// 导航栏高度单位px非rpx
height: {
type: [String, Number],
default: ''
},
// 返回箭头的颜色
backIconColor: {
type: String,
default: '#606266'
},
// 左边返回的图标
backIconName: {
type: String,
default: 'nav-back'
},
// 左边返回图标的大小rpx
backIconSize: {
type: [String, Number],
default: '44'
},
// 返回的文字提示
backText: {
type: String,
default: ''
},
// 返回的文字的 样式
backTextStyle: {
type: Object,
default () {
return {
color: '#606266'
}
}
},
// 导航栏标题
title: {
type: String,
default: ''
},
// 标题的宽度如果需要自定义右侧内容且右侧内容很多时可能需要减少这个宽度单位rpx
titleWidth: {
type: [String, Number],
default: '250'
},
// 标题的颜色
titleColor: {
type: String,
default: '#606266'
},
// 标题字体是否加粗
titleBold: {
type: Boolean,
default: false
},
// 标题的字体大小
titleSize: {
type: [String, Number],
default: 32
},
isBack: {
type: [Boolean, String],
default: true
},
// 对象形式,因为用户可能定义一个纯色,或者线性渐变的颜色
background: {
type: Object,
default () {
return {
background: '#ffffff'
}
}
},
// 导航栏是否固定在顶部
isFixed: {
type: Boolean,
default: true
},
// 是否沉浸式允许fixed定位后导航栏塌陷仅fixed定位下生效
immersive: {
type: Boolean,
default: false
},
// 是否显示导航栏的下边框
borderBottom: {
type: Boolean,
default: true
},
zIndex: {
type: [String, Number],
default: ''
},
// 自定义返回逻辑
customBack: {
type: Function,
default: null
}
},
data() {
return {
menuButtonInfo: menuButtonInfo,
statusBarHeight: systemInfo.statusBarHeight
};
},
computed: {
// 导航栏内部盒子的样式
navbarInnerStyle() {
let style = {};
// 导航栏宽度,如果在小程序下,导航栏宽度为胶囊的左边到屏幕左边的距离
style.height = this.navbarHeight + 'px';
// // 如果是各家小程序,导航栏内部的宽度需要减少右边胶囊的宽度
// #ifdef MP
let rightButtonWidth = systemInfo.windowWidth - menuButtonInfo.left;
style.marginRight = rightButtonWidth + 'px';
// #endif
return style;
},
// 整个导航栏的样式
navbarStyle() {
let style = {};
style.zIndex = this.zIndex ? this.zIndex : this.$u.zIndex.navbar;
// 合并用户传递的背景色对象
Object.assign(style, this.background);
return style;
},
// 导航中间的标题的样式
titleStyle() {
let style = {};
// #ifndef MP
style.left = (systemInfo.windowWidth - uni.upx2px(this.titleWidth)) / 2 + 'px';
style.right = (systemInfo.windowWidth - uni.upx2px(this.titleWidth)) / 2 + 'px';
// #endif
// #ifdef MP
// 此处是为了让标题显示区域即使在小程序有右侧胶囊的情况下也能处于屏幕的中间,是通过绝对定位实现的
let rightButtonWidth = systemInfo.windowWidth - menuButtonInfo.left;
style.left = (systemInfo.windowWidth - uni.upx2px(this.titleWidth)) / 2 + 'px';
style.right = rightButtonWidth - (systemInfo.windowWidth - uni.upx2px(this.titleWidth)) / 2 + rightButtonWidth +
'px';
// #endif
style.width = uni.upx2px(this.titleWidth) + 'px';
return style;
},
// 转换字符数值为真正的数值
navbarHeight() {
// #ifdef APP-PLUS || H5
return this.height ? this.height : 44;
// #endif
// #ifdef MP
// 小程序特别处理,让导航栏高度 = 胶囊高度 + 两倍胶囊顶部与状态栏底部的距离之差(相当于同时获得了导航栏底部与胶囊底部的距离)
// 此方法有缺陷,暂不用(会导致少了几个px),采用直接固定值的方式
// return menuButtonInfo.height + (menuButtonInfo.top - this.statusBarHeight) * 2;//导航高度
let height = systemInfo.platform == 'ios' ? 44 : 48;
return this.height ? this.height : height;
// #endif
}
},
created() {},
methods: {
goBack() {
// 如果自定义了点击返回按钮的函数,则执行,否则执行返回逻辑
if (typeof this.customBack === 'function') {
// 在微信,支付宝等环境(H5正常)会导致父组件定义的customBack()函数体中的this变成子组件的this
// 通过bind()方法绑定父组件的this让this.customBack()的this为父组件的上下文
this.customBack.bind(this.$u.$parent.call(this))();
} else {
uni.navigateBack();
}
}
}
};
</script>
<style scoped lang="scss">
@import "../../libs/css/style.components.scss";
.u-navbar {
width: 100%;
}
.u-navbar-fixed {
position: fixed;
left: 0;
right: 0;
top: 0;
z-index: 991;
}
.u-status-bar {
width: 100%;
}
.u-navbar-inner {
@include vue-flex;
justify-content: space-between;
position: relative;
align-items: center;
}
.u-back-wrap {
@include vue-flex;
align-items: center;
flex: 1;
flex-grow: 0;
padding: 14rpx 14rpx 14rpx 24rpx;
}
.u-back-text {
padding-left: 4rpx;
font-size: 30rpx;
}
.u-navbar-content-title {
@include vue-flex;
align-items: center;
justify-content: center;
flex: 1;
position: absolute;
left: 0;
right: 0;
height: 60rpx;
text-align: center;
flex-shrink: 0;
}
.u-navbar-centent-slot {
flex: 1;
}
.u-title {
line-height: 60rpx;
font-size: 32rpx;
flex: 1;
}
.u-navbar-right {
flex: 1;
@include vue-flex;
align-items: center;
justify-content: flex-end;
}
.u-slot-content {
flex: 1;
@include vue-flex;
align-items: center;
}
</style>

View File

@@ -0,0 +1,273 @@
<template>
<view class="u-notice-bar-wrap" v-if="isShow" :style="{
borderRadius: borderRadius + 'rpx',
}">
<block v-if="mode == 'horizontal' && isCircular">
<u-row-notice
:type="type"
:color="color"
:bgColor="bgColor"
:list="list"
:volumeIcon="volumeIcon"
:moreIcon="moreIcon"
:volumeSize="volumeSize"
:closeIcon="closeIcon"
:mode="mode"
:fontSize="fontSize"
:speed="speed"
:playState="playState"
:padding="padding"
@getMore="getMore"
@close="close"
@click="click"
></u-row-notice>
</block>
<block v-if="mode == 'vertical' || (mode == 'horizontal' && !isCircular)">
<u-column-notice
:type="type"
:color="color"
:bgColor="bgColor"
:list="list"
:volumeIcon="volumeIcon"
:moreIcon="moreIcon"
:closeIcon="closeIcon"
:mode="mode"
:volumeSize="volumeSize"
:disable-touch="disableTouch"
:fontSize="fontSize"
:duration="duration"
:playState="playState"
:padding="padding"
@getMore="getMore"
@close="close"
@click="click"
@end="end"
></u-column-notice>
</block>
</view>
</template>
<script>
/**
* noticeBar 滚动通知
* @description 该组件用于滚动通告场景,有多种模式可供选择
* @tutorial https://www.uviewui.com/components/noticeBar.html
* @property {Array} list 滚动内容,数组形式,见上方说明
* @property {String} type 显示的主题默认warning
* @property {Boolean} volume-icon 是否显示小喇叭图标默认true
* @property {Boolean} more-icon 是否显示右边的向右箭头默认false
* @property {Boolean} close-icon 是否显示关闭图标默认false
* @property {Boolean} autoplay 是否自动播放默认true
* @property {String} color 文字颜色
* @property {String Number} bg-color 背景颜色
* @property {String} mode 滚动模式默认horizontal
* @property {Boolean} show 是否显示默认true
* @property {String Number} font-size 字体大小单位rpx默认28
* @property {String Number} volume-size 左边喇叭的大小默认34
* @property {String Number} duration 滚动周期时长只对步进模式有效横向衔接模式无效单位ms默认2000
* @property {String Number} speed 水平滚动时的滚动速度即每秒移动多少距离只对水平衔接方式有效单位rpx默认160
* @property {String Number} font-size 字体大小单位rpx默认28
* @property {Boolean} is-circular mode为horizontal时指明是否水平衔接滚动默认true
* @property {String} play-state 播放状态play - 播放paused - 暂停默认play
* @property {String Number} border-radius 通知栏圆角默认为0
* @property {String Number} padding 内边距字符串与普通的内边距css写法一直默认"18rpx 24rpx"
* @property {Boolean} no-list-hidden 列表为空时是否显示组件默认false
* @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 点击右侧关闭图标触发
* @event {Function} getMore 点击右侧向右图标触发
* @event {Function} end 列表的消息每次被播放一个周期时触发只有mode = vertical或者mode = horizontal且is-circular = false时有效
* @example <u-notice-bar :more-icon="true" :list="list"></u-notice-bar>
*/
export default {
name: "u-notice-bar",
emits: ["click", "close", "getMore", "end"],
props: {
// 显示的内容,数组
list: {
type: Array,
default() {
return [];
}
},
// 显示的主题success|error|primary|info|warning
type: {
type: String,
default: 'warning'
},
// 是否显示左侧的音量图标
volumeIcon: {
type: Boolean,
default: true
},
// 音量喇叭的大小
volumeSize: {
type: [Number, String],
default: 34
},
// 是否显示右侧的右箭头图标
moreIcon: {
type: Boolean,
default: false
},
// 是否显示右侧的关闭图标
closeIcon: {
type: Boolean,
default: false
},
// 是否自动播放
autoplay: {
type: Boolean,
default: true
},
// 文字颜色,各图标也会使用文字颜色
color: {
type: String,
default: ''
},
// 背景颜色
bgColor: {
type: String,
default: ''
},
// 滚动方向horizontal-水平滚动vertical-垂直滚动
mode: {
type: String,
default: 'horizontal'
},
// 是否显示
show: {
type: Boolean,
default: true
},
// 字体大小单位rpx
fontSize: {
type: [Number, String],
default: 28
},
// 滚动一个周期的时间长单位ms
duration: {
type: [Number, String],
default: 2000
},
// 水平滚动时的滚动速度即每秒滚动多少rpx这有利于控制文字无论多少时都能有一个恒定的速度
speed: {
type: [Number, String],
default: 160
},
// 水平滚动时,是否采用衔接形式滚动
// 水平衔接模式采用的是swiper组件水平滚动
isCircular: {
type: Boolean,
default: true
},
// 播放状态play-播放paused-暂停
playState: {
type: String,
default: 'play'
},
// 是否禁止用手滑动切换
// 目前HX2.6.11只支持App 2.5.5+、H5 2.5.5+、支付宝小程序、字节跳动小程序
disableTouch: {
type: Boolean,
default: true
},
// 滚动通知设置圆角
borderRadius: {
type: [Number, String],
default: 0
},
// 通知的边距
padding: {
type: [Number, String],
default: '18rpx 24rpx'
},
// list列表为空时是否显示组件
noListHidden: {
type: Boolean,
default: true
}
},
computed: {
// 如果设置show为false或者设置了noListHidden为true且list长度又为零的话隐藏组件
isShow() {
if(this.show == false || (this.noListHidden == true && this.list.length == 0)) return false;
else return true;
}
},
methods: {
// 点击通告栏
click(index) {
this.$emit('click', index);
},
// 点击关闭按钮
close() {
this.$emit('close');
},
// 点击更多箭头按钮
getMore() {
this.$emit('getMore');
},
// 滚动一个周期结束,只对垂直,或者水平步进形式有效
end() {
this.$emit('end');
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-notice-bar-wrap {
overflow: hidden;
}
.u-notice-bar {
padding: 18rpx 24rpx;
overflow: hidden;
}
.u-direction-row {
@include vue-flex;
align-items: center;
justify-content: space-between;
}
.u-left-icon {
@include vue-flex;
align-items: center;
}
.u-notice-box {
flex: 1;
@include vue-flex;
overflow: hidden;
margin-left: 12rpx;
}
.u-right-icon {
margin-left: 12rpx;
@include vue-flex;
align-items: center;
}
.u-notice-content {
line-height: 1;
white-space: nowrap;
font-size: 26rpx;
animation: u-loop-animation 10s linear infinite both;
text-align: right;
// 这一句很重要,为了能让滚动左右连接起来
padding-left: 100%;
}
@keyframes u-loop-animation {
0% {
transform: translate3d(0, 0, 0);
}
100% {
transform: translate3d(-100%, 0, 0);
}
}
</style>

View File

@@ -0,0 +1,357 @@
<template>
<view class="u-search" @tap="clickHandler" :style="{
margin: margin,
}">
<view
class="u-content"
:style="{
backgroundColor: bgColor,
borderRadius: shape == 'round' ? '100rpx' : '10rpx',
border: borderStyle,
height: height + 'rpx'
}"
>
<view class="u-icon-wrap">
<u-icon class="u-clear-icon" :size="30" :name="searchIcon" :color="searchIconColor ? searchIconColor : color"></u-icon>
</view>
<input
confirm-type="search"
@blur="blur"
:value="valueCom"
@confirm="search"
@input="inputChange"
:disabled="disabled"
@focus="getFocus"
:focus="focus"
:maxlength="maxlength"
placeholder-class="u-placeholder-class"
:placeholder="placeholder"
:placeholder-style="`color: ${placeholderColor}`"
class="u-input"
type="text"
:style="[{
textAlign: inputAlign,
color: color,
backgroundColor: bgColor,
}, inputStyle]"
/>
<view class="u-close-wrap" v-if="keyword && clearabled && focused" @tap="clear">
<u-icon class="u-clear-icon" name="close-circle-fill" size="34" color="#c0c4cc"></u-icon>
</view>
</view>
<view :style="[actionStyle]" class="u-action"
:class="[showActionBtn || show ? 'u-action-active' : '']"
@tap.stop.prevent="custom"
>{{ actionText }}</view>
</view>
</template>
<script>
/**
* search 搜索框
* @description 搜索组件,集成了常见搜索框所需功能,用户可以一键引入,开箱即用。
* @tutorial https://www.uviewui.com/components/search.html
* @property {String} shape 搜索框形状round-圆形square-方形默认round
* @property {String} bg-color 搜索框背景颜色(默认#f2f2f2
* @property {String} border-color 边框颜色,配置了颜色,才会有边框
* @property {String} placeholder 占位文字内容(默认“请输入关键字”)
* @property {Boolean} clearabled 是否启用清除控件默认true
* @property {Boolean} focus 是否自动获得焦点默认false
* @property {Boolean} show-action 是否显示右侧控件默认true
* @property {String} action-text 右侧控件文字(默认“搜索”)
* @property {Object} action-style 右侧控件的样式,对象形式
* @property {String} input-align 输入框内容水平对齐方式默认left
* @property {Object} input-style 自定义输入框样式,对象形式
* @property {Boolean} disabled 是否启用输入框默认false
* @property {String} search-icon-color 搜索图标的颜色,默认同输入框字体颜色
* @property {String} color 输入框字体颜色(默认#606266
* @property {String} placeholder-color placeholder的颜色默认#909399
* @property {String} search-icon 输入框左边的图标可以为uView图标名称或图片路径
* @property {String} margin 组件与其他上下左右元素之间的距离,带单位的字符串形式,如"30rpx"
* @property {Boolean} animation 是否开启动画见上方说明默认false
* @property {String} value 输入框初始值
* @property {String | Number} maxlength 输入框最大能输入的长度,-1为不限制长度
* @property {Boolean} input-style input输入框的样式可以定义文字颜色大小等对象形式
* @property {String | Number} height 输入框高度单位rpx默认64
* @event {Function} change 输入框内容发生变化时触发
* @event {Function} search 用户确定搜索时触发,用户按回车键,或者手机键盘右下角的"搜索"键时触发
* @event {Function} custom 用户点击右侧控件时触发
* @event {Function} clear 用户点击清除按钮时触发
* @example <u-search placeholder="日照香炉生紫烟" v-model="keyword"></u-search>
*/
export default {
name: "u-search",
emits: ["update:modelValue", "input", "change", "search", "custom", "clear", "focus", "blur"],
props: {
// 输入框的初始化内容
value: {
type: String,
default: ''
},
modelValue: {
type: String,
default: ''
},
// 搜索框形状round-圆形square-方形
shape: {
type: String,
default: 'round'
},
// 搜索框背景色,默认值#f2f2f2
bgColor: {
type: String,
default: '#f2f2f2'
},
// 占位提示文字
placeholder: {
type: String,
default: '请输入关键字'
},
// 是否启用清除控件
clearabled: {
type: Boolean,
default: true
},
// 是否自动聚焦
focus: {
type: Boolean,
default: false
},
// 是否在搜索框右侧显示取消按钮
showAction: {
type: Boolean,
default: true
},
// 右边控件的样式
actionStyle: {
type: Object,
default() {
return {};
}
},
// 取消按钮文字
actionText: {
type: String,
default: '搜索'
},
// 输入框内容对齐方式,可选值为 left|center|right
inputAlign: {
type: String,
default: 'left'
},
// 是否启用输入框
disabled: {
type: Boolean,
default: false
},
// 开启showAction时是否在input获取焦点时才显示
animation: {
type: Boolean,
default: false
},
// 边框颜色,只要配置了颜色,才会有边框
borderColor: {
type: String,
default: 'none'
},
// 搜索框高度单位rpx
height: {
type: [Number, String],
default: 64
},
// input输入框的样式可以定义文字颜色大小等对象形式
inputStyle: {
type: Object,
default() {
return {}
}
},
// 输入框最大能输入的长度,-1为不限制长度(来自uniapp文档)
maxlength: {
type: [Number, String],
default: '-1'
},
// 搜索图标的颜色,默认同输入框字体颜色
searchIconColor: {
type: String,
default: ''
},
// 输入框字体颜色
color: {
type: String,
default: '#606266'
},
// placeholder的颜色
placeholderColor: {
type: String,
default: '#909399'
},
// 组件与其他上下左右元素之间的距离,带单位的字符串形式,如"30rpx"、"30rpx 20rpx"等写法
margin: {
type: String,
default: '0'
},
// 左边输入框的图标可以为uView图标名称或图片路径
searchIcon: {
type: String,
default: 'search'
}
},
data() {
return {
keyword: '',
showClear: false, // 是否显示右边的清除图标
show: false,
// 标记input当前状态是否处于聚焦中如果是才会显示右侧的清除控件
focused: this.focus
// 绑定输入框的值
// inputValue: this.value
};
},
watch: {
keyword(nVal) {
// 双向绑定值让v-model绑定的值双向变化
this.$emit('input', nVal);
this.$emit("update:modelValue", nVal);
// 触发change事件事件效果和v-model双向绑定的效果一样让用户多一个选择
this.$emit('change', nVal);
},
valueCom: {
immediate: true,
handler(nVal) {
this.keyword = nVal;
}
}
},
computed: {
valueCom() {
// #ifndef VUE3
return this.value;
// #endif
// #ifdef VUE3
return this.modelValue;
// #endif
},
showActionBtn() {
if (!this.animation && this.showAction) return true;
else return false;
},
// 样式根据用户传入的颜色值生成如果不传入默认为none
borderStyle() {
if (this.borderColor) return `1px solid ${this.borderColor}`;
else return 'none';
},
},
methods: {
// 目前HX2.6.9 v-model双向绑定无效故监听input事件获取输入框内容的变化
inputChange(e) {
this.keyword = e.detail.value;
},
// 清空输入
// 也可以作为用户通过this.$refs形式调用清空输入框内容
clear() {
this.keyword = '';
// 延后发出事件避免在父组件监听clear事件时value为更新前的值(不为空)
this.$nextTick(() => {
this.$emit('clear');
})
},
// 确定搜索
search(e) {
this.$emit('search', e.detail.value);
try{
// 收起键盘
uni.hideKeyboard();
}catch(e){}
},
// 点击右边自定义按钮的事件
custom() {
this.$emit('custom', this.keyword);
try{
// 收起键盘
uni.hideKeyboard();
}catch(e){}
},
// 获取焦点
getFocus() {
this.focused = true;
// 开启右侧搜索按钮展开的动画效果
if (this.animation && this.showAction) this.show = true;
this.$emit('focus', this.keyword);
},
// 失去焦点
blur() {
// 最开始使用的是监听图标@touchstart事件自从hx2.8.4后,此方法在微信小程序出错
// 这里改为监听点击事件,手点击清除图标时,同时也发生了@blur事件导致图标消失而无法点击这里做一个延时
setTimeout(() => {
this.focused = false;
}, 100)
this.show = false;
this.$emit('blur', this.keyword);
},
// 点击搜索框只有disabled=true时才发出事件因为禁止了输入意味着是想跳转真正的搜索页
clickHandler() {
if(this.disabled) this.$emit('click');
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-search {
@include vue-flex;
align-items: center;
flex: 1;
}
.u-content {
@include vue-flex;
align-items: center;
padding: 0 18rpx;
flex: 1;
}
.u-clear-icon {
@include vue-flex;
align-items: center;
}
.u-input {
flex: 1;
font-size: 28rpx;
line-height: 1;
margin: 0 10rpx;
color: $u-tips-color;
}
.u-close-wrap {
width: 40rpx;
height: 100%;
@include vue-flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
.u-placeholder-class {
color: $u-tips-color;
}
.u-action {
font-size: 28rpx;
color: $u-main-color;
width: 0;
overflow: hidden;
transition: all 0.3s;
white-space: nowrap;
text-align: center;
}
.u-action-active {
width: 80rpx;
margin-left: 10rpx;
}
</style>

View File

@@ -0,0 +1,135 @@
<template>
<view class="u-swiper-indicator">
<view
class="u-swiper-indicator__wrapper"
v-if="indicatorMode === 'line'"
:class="[`u-swiper-indicator__wrapper--${indicatorMode}`]"
:style="{
width: $u.addUnit(lineWidth * length),
backgroundColor: indicatorInactiveColor
}"
>
<view
class="u-swiper-indicator__wrapper--line__bar"
:style="[lineStyle]"
></view>
</view>
<view
class="u-swiper-indicator__wrapper"
v-if="indicatorMode === 'dot'"
>
<view
class="u-swiper-indicator__wrapper__dot"
v-for="(item, index) in length"
:key="index"
:class="[index === current && 'u-swiper-indicator__wrapper__dot--active']"
:style="[dotStyle(index)]"
>
</view>
</view>
</view>
</template>
<script>
/**
* SwiperIndicator 轮播图指示器
* @description 该组件一般用于导航轮播,广告展示等场景,可开箱即用,
* @tutorial https://www.uviewui.com/components/swiper.html
* @property {String | Number} length 轮播的长度(默认 0
* @property {String | Number} current 当前处于活动状态的轮播的索引(默认 0
* @property {String} indicatorActiveColor 指示器非激活颜色
* @property {String} indicatorInactiveColor 指示器的激活颜色
* @property {String} indicatorMode 指示器模式(默认 'line'
* @example <u-swiper :list="list4" indicator keyName="url" :autoplay="false"></u-swiper>
*/
export default {
name: 'u-swiper-indicator',
props:{
// 轮播的长度
length: {
type: [String, Number],
default: 0
},
// 当前处于活动状态的轮播的索引
current: {
type: [String, Number],
default: 0
},
// 指示器非激活颜色
indicatorActiveColor: {
type: String,
default: ''
},
// 指示器的激活颜色
indicatorInactiveColor: {
type: String,
default: ''
},
// 指示器模式line-线型dot-点型
indicatorMode: {
type: String,
default: 'line'
}
},
data() {
return {
lineWidth: 22
}
},
computed: {
// 指示器为线型的样式
lineStyle() {
let style = {}
style.width = uni.$u.addUnit(this.lineWidth)
style.transform = `translateX(${ uni.$u.addUnit(this.current * this.lineWidth) })`
style.backgroundColor = this.indicatorActiveColor
return style
},
// 指示器为点型的样式
dotStyle() {
return index => {
let style = {}
style.backgroundColor = index === this.current ? this.indicatorActiveColor : this.indicatorInactiveColor
return style
}
}
},
}
</script>
<style lang="scss" scoped>
@import "../../../libs/css/style.components.scss";
.u-swiper-indicator {
&__wrapper {
@include vue-flex;
&--line {
border-radius: 100px;
height: 4px;
&__bar {
width: 22px;
height: 4px;
border-radius: 100px;
background-color: #FFFFFF;
transition: transform 0.3s;
}
}
&__dot {
width: 5px;
height: 5px;
border-radius: 100px;
margin: 0 4px;
&--active {
width: 12px;
}
}
}
}
</style>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,341 @@
<template>
<view class="u-swiper-wrap" :style="{
borderRadius: `${borderRadius}rpx`
}">
<swiper :current="elCurrent" @change="change" @animationfinish="animationfinish" :interval="interval" :circular="circular" :duration="duration" :autoplay="autoplay"
:previous-margin="effect3d ? effect3dPreviousMargin + 'rpx' : '0'" :next-margin="effect3d ? effect3dPreviousMargin + 'rpx' : '0'"
:style="{
height: height + 'rpx',
backgroundColor: bgColor
}">
<swiper-item class="u-swiper-item" v-for="(item, index) in list" :key="index">
<view class="u-list-image-wrap" @tap.stop.prevent="listClick(index)" :class="[uCurrent != index ? 'u-list-scale' : '']" :style="{
borderRadius: `${borderRadius}rpx`,
transform: effect3d && uCurrent != index ? 'scaleY(0.9)' : 'scaleY(1)',
margin: effect3d && uCurrent != index ? '0 20rpx' : 0,
}">
<image class="u-swiper-image" :src="item[name] || item" :mode="imgMode"></image>
<view v-if="title && item.title" class="u-swiper-title u-line-1" :style="[{
'padding-bottom': titlePaddingBottom
}, titleStyle]">
{{ 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 == uCurrent }" 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 == uCurrent }" 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 == uCurrent }" v-for="(item, index) in list"
:key="index"></view>
</block>
<block v-if="mode == 'number'">
<view class="u-indicator-item-number">{{ uCurrent + 1 }}/{{ list.length }}</view>
</block>
</view>
</view>
</template>
<script>
/**
* swiper 轮播图
* @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} bg-color 背景颜色(默认#f3f4f6
* @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",
emits: ["click", "change"],
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'
},
// 背景颜色
bgColor: {
type: String,
default: '#f3f4f6'
},
// 初始化时,默认显示第几项
current: {
type: [Number, String],
default: 0
},
// 标题的样式,对象形式
titleStyle: {
type: Object,
default() {
return {}
}
}
},
watch: {
// 如果外部的list发生变化判断长度是否被修改如果前后长度不一致重置uCurrent值避免溢出
list(nVal, oVal) {
if(nVal.length !== oVal.length) this.uCurrent = 0;
},
// 监听外部current的变化实时修改内部依赖于此测uCurrent值如果更新了current而不是更新uCurrent
// 就会错乱因为指示器是依赖于uCurrent的
current(n) {
this.uCurrent = n;
}
},
data() {
return {
uCurrent: this.current // 当前活跃的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;
},
// 因为uni的swiper组件的current参数只接受Number类型这里做一个转换
elCurrent() {
return Number(this.current);
}
},
methods: {
listClick(index) {
this.$emit('click', index);
},
change(e) {
let current = e.detail.current;
this.uCurrent = current;
// 发出change事件表示当前自动切换的index从0开始
this.$emit('change', current);
},
// 头条小程序不支持animationfinish事件改由change事件
// 暂不监听此事件因为不再给swiper绑定uCurrent属性
animationfinish(e) {
// #ifndef MP-TOUTIAO
// this.uCurrent = e.detail.current;
// #endif
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-swiper-wrap {
position: relative;
overflow: hidden;
transform: translateY(0);
}
.u-swiper-image {
width: 100%;
will-change: transform;
height: 100%;
/* #ifndef APP-NVUE */
display: block;
/* #endif */
/* #ifdef H5 */
pointer-events: none;
/* #endif */
}
.u-swiper-indicator {
padding: 0 24rpx;
position: absolute;
@include vue-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 {
@include vue-flex;
overflow: hidden;
align-items: center;
}
</style>

View File

@@ -0,0 +1,191 @@
<template>
<view
class="u-switch"
:class="[valueCom == true ? 'u-switch--on' : '', disabled ? 'u-switch--disabled' : '']"
@tap="onClick"
:style="[switchStyle]"
>
<view
class="u-switch__node node-class"
:style="{
width: $u.addUnit(size),
height: $u.addUnit(size)
}"
>
<u-loading
:show="loading"
class="u-switch__loading"
:size="size * 0.6"
:color="loadingColor"
/>
</view>
</view>
</template>
<script>
/**
* switch 开关选择器
* @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
* @property {Boolean | Number | String} active-value 打开选择器时通过change事件发出的值默认true
* @property {Boolean | Number | String} inactive-value 关闭选择器时通过change事件发出的值默认false
* @event {Function} change 在switch打开或关闭时触发
* @example <u-switch v-model="checked" active-color="red" inactive-color="#eee"></u-switch>
*/
export default {
name: "u-switch",
emits: ["update:modelValue", "input", "change"],
props: {
// 通过v-model双向绑定的值
value: {
type: Boolean,
default: false
},
modelValue: {
type: Boolean,
default: false
},
// 是否为加载中状态
loading: {
type: Boolean,
default: false
},
// 是否为禁用装填
disabled: {
type: Boolean,
default: false
},
// 开关尺寸单位rpx
size: {
type: [Number, String],
default: 50
},
// 打开时的背景颜色
activeColor: {
type: String,
default: "#2979ff"
},
// 关闭时的背景颜色
inactiveColor: {
type: String,
default: "#ffffff"
},
// 是否使手机发生短促震动目前只在iOS的微信小程序有效(2020-05-06)
vibrateShort: {
type: Boolean,
default: false
},
// 打开选择器时的值
activeValue: {
type: [Number, String, Boolean],
default: true
},
// 关闭选择器时的值
inactiveValue: {
type: [Number, String, Boolean],
default: false
}
},
data() {
return {};
},
computed: {
valueCom() {
// #ifndef VUE3
return this.value;
// #endif
// #ifdef VUE3
return this.modelValue;
// #endif
},
switchStyle() {
let style = {};
style.fontSize = this.size + "rpx";
style.backgroundColor = this.valueCom ? this.activeColor : this.inactiveColor;
return style;
},
loadingColor() {
return this.valueCom ? this.activeColor : null;
}
},
methods: {
onClick() {
if (!this.disabled && !this.loading) {
// 使手机产生短促震动微信小程序有效APP(HX 2.6.8)和H5无效
if (this.vibrateShort) uni.vibrateShort();
this.$emit("input", !this.valueCom);
this.$emit("update:modelValue", !this.valueCom);
// 放到下一个生命周期因为双向绑定的value修改父组件状态需要时间且是异步的
this.$nextTick(() => {
this.$emit("change", this.valueCom ? this.activeValue : this.inactiveValue);
});
}
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-switch {
position: relative;
/* #ifndef APP-NVUE */
display: inline-block;
/* #endif */
box-sizing: initial;
width: 2em;
height: 1em;
background-color: #fff;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 1em;
transition: background-color 0.3s;
font-size: 50rpx;
}
.u-switch__node {
@include vue-flex;
align-items: center;
justify-content: center;
position: absolute;
top: 0;
left: 0;
border-radius: 100%;
z-index: 1;
background-color: #fff;
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: 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: 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: transform cubic-bezier(0.3, 1.05, 0.4, 1.05);
transition: transform 0.3s cubic-bezier(0.3, 1.05, 0.4, 1.05);
}
.u-switch__loading {
@include vue-flex;
align-items: center;
justify-content: center;
}
.u-switch--on {
background-color: #1989fa;
}
.u-switch--on .u-switch__node {
transform: translateX(100%);
}
.u-switch--disabled {
opacity: 0.4;
}
</style>

View File

@@ -0,0 +1,85 @@
<template>
<view class="u-table" :style="[tableStyle]">
<slot />
</view>
</template>
<script>
/**
* table 表格
* @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",
emits: ["click", "close"],
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'
}
},
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>
@import "../../libs/css/style.components.scss";
.u-table {
width: 100%;
box-sizing: border-box;
}
</style>

View File

@@ -0,0 +1,490 @@
<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-scroll-flex': !isScroll}">
<view class="u-tabs-item" :style="[tabItemStyle(index)]"
v-for="(item, index) in getTabs" :key="index" :class="[preId + index]" @tap="emit(index)">
<u-badge :count="item[count] || item['count'] || 0" :offset="offset" size="mini"></u-badge>
{{ item[name] || item['name']}}
</view>
<view v-if="showBar" class="u-scroll-bar" :style="[tabBarStyle]"></view>
</view>
</scroll-view>
</view>
</template>
<script>
import colorGradient from '../../libs/function/colorGradient';
let color = colorGradient;
const { windowWidth } = uni.getSystemInfoSync();
const preId = 'UEl_';
/**
* tabsSwiper 全屏选项卡
* @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 {Object} bar-style 底部滑块的样式,对象形式
* @property {Object} active-item-style 活动tabs item的样式对象形式
* @property {Boolean} show-bar 是否显示底部的滑块默认true
* @property {String Number} gutter 单个tab标签的左右内边距之和单位rpx默认40
* @property {String} bg-color tabs导航栏的背景颜色默认#ffffff
* @property {String} name 组件内部读取的list参数中的属性名见官网说明默认name
* @property {String} count 组件内部读取的list参数中的属性名badge徽标数同name属性的使用见官网说明默认count
* @property {Array} offset 设置badge徽标数的位置偏移格式为 [x, y]也即设置的为top和right的值单位rpx默认[5, 20]
* @property {Boolean} bold 激活选项的字体是否加粗默认true
* @event {Function} change 点击标签时触发
* @example <u-tabs-swiper ref="tabs" :list="list" :is-scroll="false"></u-tabs-swiper>
*/
export default {
name: "u-tabs-swiper",
emits: ["update:modelValue", "input", "change"],
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: '#303133'
},
// 未选中项的颜色
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'
},
// 读取传入的数组对象的属性(tab名称)
name: {
type: String,
default: 'name'
},
// 读取传入的数组对象的属性(徽标数)
count: {
type: String,
default: 'count'
},
// 徽标数位置偏移
offset: {
type: Array,
default: () => {
return [5, 20]
}
},
// 活动tab字体是否加粗
bold: {
type: Boolean,
default: true
},
// 当前活动tab item的样式
activeItemStyle: {
type: Object,
default() {
return {}
}
},
// 是否显示底部的滑块
showBar: {
type: Boolean,
default: true
},
// 底部滑块的自定义样式
barStyle: {
type: Object,
default() {
return {}
}
}
},
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() {
var barleft = Number(this.line3Dx) + Number(this.line3AddDx);
return barleft > 0 ? barleft : 21;
},
// 滑块的宽度转为px单位
barWidthPx() {
return uni.upx2px(this.barWidth);
},
// tab的样式
tabItemStyle() {
return (index) => {
let style = {
height: this.height + 'rpx',
lineHeight: this.height + 'rpx',
padding: `0 ${this.gutter / 2}rpx`,
color: this.tabsInfo.length > 0 ? (this.tabsInfo[index] ? this.tabsInfo[index].color : this.activeColor) : this.inactiveColor,
fontSize: this.fontSize + 'rpx',
zIndex: this.zIndex + 2,
fontWeight: (index == this.getCurrent && this.bold) ? 'bold' : 'bold'
};
if(index == this.getCurrent) {
// 给选中的tab item添加外部自定义的样式
style = Object.assign(style, this.activeItemStyle);
}
return style;
}
},
// 底部滑块的样式
tabBarStyle() {
let style = {
width: this.barWidthPx + 'px',
height: this.barHeight + 'rpx',
borderRadius: '100px',
backgroundColor: '#3cc9a4',
left: this.scrollBarLeft + 'px'
};
return Object.assign(style, this.barStyle);
}
},
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 - this.tabsInfo[0].left;
},
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的情况
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">
@import "../../libs/css/style.components.scss";
view,
scroll-view {
box-sizing: border-box;
}
.u-tabs {
width: 100%;
transition-property: background-color, color;
}
/* #ifndef APP-NVUE */
::-webkit-scrollbar,
::-webkit-scrollbar,
::-webkit-scrollbar {
display: none;
width: 0 !important;
height: 0 !important;
-webkit-appearance: none;
background: transparent;
}
/* #endif */
/* #ifdef H5 */
// 通过样式穿透隐藏H5下scroll-view下的滚动条
scroll-view ::v-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-scroll-flex {
@include vue-flex;
justify-content: space-between;
}
.u-tabs-scroll-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;
@include vue-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>

View File

@@ -0,0 +1,399 @@
<template>
<view class="u-tabs" :style="{
background: bgColor
}">
<!-- $u.getRect()对组件根节点无效因为写了.in(this)故这里获取内层接点尺寸 -->
<view :id="id">
<scroll-view scroll-x class="u-scroll-view" :scroll-left="scrollLeft" scroll-with-animation>
<view class="u-scroll-box" :id="id" :class="{'u-tabs-scroll-flex': !isScroll}">
<view class="u-tab-item u-line-1" :id="'u-tab-item-' + index" v-for="(item, index) in list" :key="index" @tap="clickTab(index)"
:style="[tabItemStyle(index)]">
<u-badge :count="item[count] || item['count'] || 0" :offset="offset" size="mini"></u-badge>
{{ item[name] || item['name']}}
</view>
<view v-if="showBar" class="u-tab-bar" :style="[tabBarStyle]"></view>
</view>
</scroll-view>
</view>
</view>
</template>
<script>
/**
* tabs 标签
* @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 {Object} active-item-style 活动tabs item的样式对象形式
* @property {Object} bar-style 底部滑块的样式,对象形式
* @property {Boolean} show-bar 是否显示底部的滑块默认true
* @property {String Number} bar-height 滑块高度单位rpx默认6
* @property {String Number} item-width 标签的宽度默认auto
* @property {String Number} gutter 单个tab标签的左右内边距之和单位rpx默认40
* @property {String} bg-color tabs导航栏的背景颜色默认#ffffff
* @property {String} name 组件内部读取的list参数中的属性名tab名称见官网说明默认name
* @property {String} count 组件内部读取的list参数中的属性名badge徽标数同name属性的使用见官网说明默认count
* @property {Array} offset 设置badge徽标数的位置偏移格式为 [x, y]也即设置的为top和right的值单位rpx默认[5, 20]
* @property {Boolean} bold 激活选项的字体是否加粗默认true
* @event {Function} change 点击标签时触发
* @example <u-tabs ref="tabs" :list="list" :is-scroll="false"></u-tabs>
*/
export default {
name: "u-tabs",
emits: ["update:modelValue", "input", "change"],
props: {
// 通过双向绑定控制组件的弹出与收起
value: {
type: [Number, String],
default: 0
},
modelValue: {
type: [Number, String],
default: 0
},
// 当前活动tab的索引请使用 v-model="current" 代替 :current="current" @change="change" 其他不变)
current: {
type: [Number, String],
default: 0
},
// 导航菜单是否需要滚动如只有2或者3个的时候就不需要滚动了此时使用flex平分tab的宽度
isScroll: {
type: Boolean,
default: true
},
//需循环的标签列表
list: {
type: Array,
default () {
return [];
}
},
// 导航栏的高度和行高
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'
},
// 读取传入的数组对象的属性(tab名称)
name: {
type: String,
default: 'name'
},
// 读取传入的数组对象的属性(徽标数)
count: {
type: String,
default: 'count'
},
// 徽标数位置偏移
offset: {
type: Array,
default: () => {
return [5, 20]
}
},
// 活动tab字体是否加粗
bold: {
type: Boolean,
default: true
},
// 当前活动tab item的样式
activeItemStyle: {
type: Object,
default() {
return {}
}
},
// 是否显示底部的滑块
showBar: {
type: Boolean,
default: true
},
// 底部滑块的自定义样式
barStyle: {
type: Object,
default() {
return {}
}
},
// 标签的宽度
itemWidth: {
type: [Number, String],
default: 'auto'
}
},
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,
barFirstTimeMove: true, // 滑块第一次移动时(页面刚生成时),无需动画,否则给人怪异的感觉
};
},
watch: {
// 监听tab的变化重新计算tab菜单的布局信息因为实际使用中菜单可能是通过
// 后台获取的如新闻app顶部的菜单获取返回需要一定时间所以list变化时重新获取布局信息
list(n, o) {
// list变动时重制内部索引否则可能导致超出数组边界的情况
if(n.length !== o.length) this.currentIndex = 0;
// 用$nextTick等待视图更新完毕后再计算tab的局部信息否则可能因为tab还没生成就获取就会有问题
this.$nextTick(() => {
this.init();
});
},
current: {
immediate: true,
handler(nVal, oVal) {
// 视图更新后再执行移动操作
this.$nextTick(() => {
this.currentIndex = nVal;
this.scrollByIndex();
});
}
},
valueCom: {
immediate: true,
handler(nVal, oVal) {
// 视图更新后再执行移动操作
this.$nextTick(() => {
this.currentIndex = nVal;
this.scrollByIndex();
});
}
}
},
computed: {
valueCom() {
// #ifndef VUE3
return this.value;
// #endif
// #ifdef VUE3
return this.modelValue;
// #endif
},
// 移动bar的样式
tabBarStyle() {
let style = {
width: this.barWidth + 'rpx',
transform: `translate(${this.scrollBarLeft}px, -100%)`,
// 滑块在页面渲染后第一次滑动时,无需动画效果
'transition-duration': `${this.barFirstTimeMove ? 0 : this.duration }s`,
'background-color': this.activeColor,
height: this.barHeight + 'rpx',
// 设置一个很大的值,它会自动取能用的最大值,不用高度的一半,是因为高度可能是单数,会有小数出现
'border-radius': `${this.barHeight / 2}px`
};
Object.assign(style, this.barStyle);
return style;
},
// 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` : '',
flex: this.isScroll ? 'auto' : '1',
width: this.$u.addUnit(this.itemWidth)
};
// 字体加粗
if (index == this.currentIndex && this.bold) style.fontWeight = 'bold';
if (index == this.currentIndex) {
style.color = this.activeColor;
// 给选中的tab item添加外部自定义的样式
style = Object.assign(style, this.activeItemStyle);
} 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) {
// 点击当前活动tab不触发事件
if(index == this.currentIndex) return ;
// 发送事件给父组件
this.$emit('change', index);
this.$emit('input', index);
this.$emit("update:modelValue", 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;
// 第一次移动滑块的时候barFirstTimeMove为true放到延时中将其设置false
// 延时是因为scrollBarLeft作用于computed计算时需要一个过程需否则导致出错
if(this.barFirstTimeMove == true) {
setTimeout(() => {
this.barFirstTimeMove = false;
}, 100)
}
}
},
mounted() {
this.init();
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
view,
scroll-view {
box-sizing: border-box;
}
/* #ifndef APP-NVUE */
::-webkit-scrollbar,
::-webkit-scrollbar,
::-webkit-scrollbar {
display: none;
width: 0 !important;
height: 0 !important;
-webkit-appearance: none;
background: transparent;
}
/* #endif */
.u-scroll-box {
position: relative;
/* #ifdef MP-TOUTIAO */
white-space: nowrap;
/* #endif */
}
/* #ifdef H5 */
// 通过样式穿透隐藏H5下scroll-view下的滚动条
scroll-view ::v-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;
/* #ifndef APP-NVUE */
display: inline-block;
/* #endif */
text-align: center;
transition-property: background-color, color;
}
.u-tab-bar {
position: absolute;
bottom: 0;
}
.u-tabs-scroll-flex {
@include vue-flex;
justify-content: space-between;
}
</style>

View File

@@ -0,0 +1,295 @@
<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" :color="closeIconColor"
name="close" class="u-close-icon" :style="[iconStyle]"></u-icon>
</view>
</view>
</template>
<script>
/**
* tag 提示
* @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',
emits: ["click", "close"],
// 是否禁用这个标签,禁用的话,会屏蔽点击事件
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, Number],
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;
// tag的背景颜色如果有此值会覆盖type值的颜色
if(this.bgColor) style.backgroundColor = this.bgColor;
// 如果是镂空型tag没有传递边框颜色borderColor的话使用文字的颜色color属性
if(this.mode == '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.type;
else if(this.mode == 'dark') style.color = "#ffffff";
if(this.closeColor) style.color = this.closeColor;
return style;
},
// 关闭图标的颜色
closeIconColor() {
// 如果定义了关闭图标的颜色,就用此值,否则用字体颜色的值
// 如果上面的二者都没有如果是dark深色模式图标就为白色
// 最后如果上面的三者都不合适就返回type值给图标获取颜色
let color = '';
if(this.closeColor) return this.closeColor;
else if(this.color) return this.color;
else if(this.mode == 'dark') return '#ffffff';
else return this.type;
}
},
methods: {
// 标签被点击
clickTag() {
// 如果是disabled状态不发送点击事件
if(this.disabled) return ;
this.$emit('click', this.index);
},
// 点击标签关闭按钮
close() {
this.$emit('close', this.index);
}
}
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-tag {
box-sizing: border-box;
align-items: center;
border-radius: 6rpx;
/* #ifndef APP-NVUE */
display: inline-block;
/* #endif */
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 $u-type-primary-disabled;
}
.u-mode-light-success {
background-color: $u-type-success-light;
color: $u-type-success;
border: 1px solid $u-type-success-disabled;
}
.u-mode-light-error {
background-color: $u-type-error-light;
color: $u-type-error;
border: 1px solid $u-type-error-disabled;
}
.u-mode-light-warning {
background-color: $u-type-warning-light;
color: $u-type-warning;
border: 1px solid $u-type-warning-disabled;
}
.u-mode-light-info {
background-color: $u-type-info-light;
color: $u-type-info;
border: 1px solid $u-type-info-disabled;
}
.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>

View File

@@ -0,0 +1,66 @@
<template>
<view class="u-td" :style="[tdStyle]">
<slot></slot>
</view>
</template>
<script>
/**
* td td单元格
* @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 {
tdStyle: {
}
}
},
created() {
this.parent = false;
},
mounted() {
this.parent = this.$u.$parent.call(this, 'u-table');
if (this.parent) {
// 将父组件的相关参数,合并到本组件
let style = {};
if (this.width != "auto") style.flex = `0 0 ${this.width}`;
style.textAlign = this.parent.align;
style.fontSize = this.parent.fontSize + 'rpx';
style.padding = this.parent.padding;
style.borderBottom = `solid 1px ${this.parent.borderColor}`;
style.borderRight = `solid 1px ${this.parent.borderColor}`;
style.color = this.parent.color;
this.tdStyle = style;
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-td {
@include vue-flex;
flex-direction: column;
flex: 1;
justify-content: center;
font-size: 28rpx;
color: $u-content-color;
align-self: stretch;
box-sizing: border-box;
height: 100%;
}
</style>

View File

@@ -0,0 +1,62 @@
<template>
<view class="u-th" :style="[thStyle]">
<slot></slot>
</view>
</template>
<script>
/**
* th th单元格
* @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 {
thStyle: {}
}
},
created() {
this.parent = false;
},
mounted() {
this.parent = this.$u.$parent.call(this, 'u-table');
if (this.parent) {
// 将父组件的相关参数,合并到本组件
let style = {};
if (this.width) style.flex = `0 0 ${this.width}`;
style.textAlign = this.parent.align;
style.padding = this.parent.padding;
style.borderBottom = `solid 1px ${this.parent.borderColor}`;
style.borderRight = `solid 1px ${this.parent.borderColor}`;
Object.assign(style, this.parent.thStyle);
this.thStyle = style;
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-th {
@include vue-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>

View File

@@ -0,0 +1,83 @@
<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>
/**
* timeLineItem 时间轴Item
* @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>
@import "../../libs/css/style.components.scss";
.u-time-axis-item {
@include vue-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%);
@include vue-flex;
align-items: center;
justify-content: center;
z-index: 1;
font-size: 24rpx;
}
.u-dot {
height: 16rpx;
width: 16rpx;
border-radius: 100rpx;
background: #ddd;
}
</style>

View File

@@ -0,0 +1,43 @@
<template>
<view class="u-time-axis">
<slot />
</view>
</template>
<script>
/**
* timeLine 时间轴
* @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>
@import "../../libs/css/style.components.scss";
.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>

View File

@@ -0,0 +1,220 @@
<template>
<view class="u-toast" :class="[isShow ? 'u-show' : '', 'u-type-' + tmpConfig.type, 'u-position-' + tmpConfig.position]" :style="{
zIndex: uZIndex
}">
<view class="u-icon-wrap">
<u-icon v-if="tmpConfig.icon" class="u-icon" :name="iconName" :size="30" :color="tmpConfig.type"></u-icon>
</view>
<text class="u-text">{{tmpConfig.title}}</text>
</view>
</template>
<script>
/**
* toast 消息提示
* @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: '', // 主题类型primarysuccesserrorwarningblack
duration: 2000, // 显示的时间,毫秒
isTab: false, // 是否跳转tab页面
url: '', // toast消失后是否跳转页面有则跳转优先级高于back参数
icon: true, // 显示的图标
position: 'center', // toast出现的位置
callback: null, // 执行完后的回调函数
back: false, // 结束toast是否自动返回上一页
},
tmpConfig: {}, // 将用户配置和内置配置合并后的临时配置变量
};
},
computed: {
iconName() {
// 只有不为none并且type为error|warning|success|info时候才显示图标
if (['error', 'warning', 'success', 'info'].indexOf(this.tmpConfig.type) >= 0 && this.tmpConfig.icon) {
let icon = this.$u.type2icon(this.tmpConfig.type);
return icon;
}
},
uZIndex() {
// 显示toast时候如果用户有传递z-index值有限使用
return this.isShow ? (this.zIndex ? this.zIndex : this.$u.zIndex.toast) : '999999';
}
},
methods: {
// 显示toast组件由父组件通过this.$refs.xxx.show(options)形式调用
show(options) {
// 不降结果合并到this.config变量避免多次条用u-toast前后的配置造成混论
this.tmpConfig = this.$u.deepMerge(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;
// 判断是否存在callback方法如果存在就执行
typeof(this.tmpConfig.callback) === 'function' && this.tmpConfig.callback();
this.timeEnd();
}, this.tmpConfig.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.tmpConfig.url) {
// 如果url没有"/"开头添加上因为uni的路由跳转需要"/"开头
if (this.tmpConfig.url[0] != '/') this.tmpConfig.url = '/' + this.tmpConfig.url;
// 判断是否有传递显式的参数
if (Object.keys(this.tmpConfig.params).length) {
// 判断用户传递的url中是否带有参数
// 使用正则匹配,主要依据是判断是否有"/","?","="等,如“/page/index/index?name=mary"
// 如果有params参数转换后无需带上"?"
let query = '';
if (/.*\/.*\?.*=.*/.test(this.tmpConfig.url)) {
// object对象转为get类型的参数
query = this.$u.queryParams(this.tmpConfig.params, false);
this.tmpConfig.url = this.tmpConfig.url + "&" + query;
} else {
query = this.$u.queryParams(this.tmpConfig.params);
this.tmpConfig.url += query;
}
}
// 如果是跳转tab页面就使用uni.switchTab
if (this.tmpConfig.isTab) {
uni.switchTab({
url: this.tmpConfig.url
});
} else {
uni.navigateTo({
url: this.tmpConfig.url
});
}
} else if(this.tmpConfig.back) {
// 回退到上一页
this.$u.route({
type: 'back'
})
}
}
}
};
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-toast {
position: fixed;
z-index: -1;
transition: opacity 0.3s;
text-align: center;
color: #fff;
border-radius: 8rpx;
background: #585858;
@include vue-flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
opacity: 0;
pointer-events: none;
padding: 18rpx 40rpx;
}
.u-toast.u-show {
opacity: 1;
}
.u-icon {
margin-right: 10rpx;
@include vue-flex;
align-items: center;
line-height: normal;
}
.u-position-center {
left: 50%;
top: 50%;
transform: translate(-50%,-50%);
/* #ifndef APP-NVUE */
max-width: 70%;
/* #endif */
}
.u-position-top {
left: 50%;
top: 20%;
transform: translate(-50%,-50%);
}
.u-position-bottom {
left: 50%;
bottom: 20%;
transform: translate(-50%,-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>

View File

@@ -0,0 +1,25 @@
<template>
<view class="u-tr">
<slot></slot>
</view>
</template>
<script>
/**
* tr 表格行标签
* @description 表格组件一般用于展示大量结构化数据的场景(搭配<u-table>使用)
* @tutorial https://www.uviewui.com/components/table.html
* @example <u-tr></u-tr>
*/
export default {
name: "u-tr",
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-tr {
@include vue-flex;
}
</style>

View File

@@ -0,0 +1,175 @@
<template>
<view class="u-code-wrap">
<!-- 此组件功能由js完成无需写html逻辑 -->
</view>
</template>
<script>
/**
* verificationCode 验证码输入框
* @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 倒计结束的提示语,见官网说明(默认重新获取)
* @property {Boolean} keep-running 是否在H5刷新或各端返回再进入时继续倒计时默认false
* @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",
emits: ["start", "end", "change"],
props: {
// 倒计时总秒数
seconds: {
type: [String, Number],
default: 60
},
// 尚未开始时提示
startText: {
type: String,
default: '获取验证码'
},
// 正在倒计时中的提示
changeText: {
type: String,
default: 'X秒重新获取'
},
// 倒计时结束时的提示
endText: {
type: String,
default: '重新获取'
},
// 是否在H5刷新或各端返回再进入时继续倒计时
keepRunning: {
type: Boolean,
default: false
},
// 为了区分多个页面,或者一个页面多个倒计时组件本地存储的继续倒计时变了
uniqueKey: {
type: String,
default: ''
}
},
data() {
return {
secNum: this.seconds,
timer: null,
canGetCode: true, // 是否可以执行验证码操作
}
},
mounted() {
this.checkKeepRunning();
},
watch: {
seconds: {
immediate: true,
handler(n) {
this.secNum = n;
}
}
},
methods: {
checkKeepRunning() {
// 获取上一次退出页面(H5还包括刷新)时的时间戳,如果没有上次的保存,此值可能为空
let lastTimestamp = Number(uni.getStorageSync(this.uniqueKey + '_$uCountDownTimestamp'));
if(!lastTimestamp) return this.changeEvent(this.startText);
// 当前秒的时间戳
let nowTimestamp = Math.floor((+ new Date()) / 1000);
// 判断当前的时间戳,是否小于上一次的本该按设定结束,却提前结束的时间戳
if(this.keepRunning && lastTimestamp && lastTimestamp > nowTimestamp) {
// 剩余尚未执行完的倒计秒数
this.secNum = lastTimestamp - nowTimestamp;
// 清除本地保存的变量
uni.removeStorageSync(this.uniqueKey + '_$uCountDownTimestamp');
// 开始倒计时
this.start();
} else {
// 如果不存在需要继续上一次的倒计时,执行正常的逻辑
this.changeEvent(this.startText);
}
},
// 开始倒计时
start() {
// 防止快速点击获取验证码的按钮而导致内部产生多个定时器导致混乱
if(this.timer) {
clearInterval(this.timer);
this.timer = null;
}
this.$emit('start');
this.canGetCode = false;
// 这里放这句是为了一开始时就提示否则要等setInterval的1秒后才会有提示
this.changeEvent(this.changeText.replace(/x|X/, this.secNum));
this.setTimeToStorage();
this.timer = setInterval(() => {
if (--this.secNum) {
// 用当前倒计时的秒数替换提示字符串中的"x"字母
this.changeEvent(this.changeText.replace(/x|X/, this.secNum));
} else {
clearInterval(this.timer);
this.timer = null;
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);
},
// 保存时间戳为了防止倒计时尚未结束H5刷新或者各端的右上角返回上一页再进来
setTimeToStorage() {
if(!this.keepRunning || !this.timer) return;
// 记录当前的时间戳,为了下次进入页面,如果还在倒计时内的话,继续倒计时
// 倒计时尚未结束结果大于0倒计时已经开始就会小于初始值如果等于初始值说明没有开始倒计时无需处理
if(this.secNum > 0 && this.secNum <= this.seconds) {
// 获取当前时间戳(+ new Date()为特殊写法)除以1000变成秒再去除小数部分
let nowTimestamp = Math.floor((+ new Date()) / 1000);
// 将本该结束时候的时间戳保存起来 => 当前时间戳 + 剩余的秒数
uni.setStorage({
key: this.uniqueKey + '_$uCountDownTimestamp',
data: nowTimestamp + Number(this.secNum)
})
}
}
},
// 组件销毁的时候,清除定时器,否则定时器会继续存在,系统不会自动清除
// #ifndef VUE3
beforeDestroy() {
this.setTimeToStorage();
clearTimeout(this.timer);
this.timer = null;
},
// #endif
// #ifdef VUE3
beforeUnmount() {
this.setTimeToStorage();
clearTimeout(this.timer);
this.timer = null;
},
// #endif
}
</script>
<style lang="scss" scoped>
@import "../../libs/css/style.components.scss";
.u-code-wrap {
width: 0;
height: 0;
position: fixed;
z-index: -1;
}
</style>

View File

@@ -0,0 +1,15 @@
<template>
</template>
<template>
<view></view>
</template>
<script>
export default {
}
</script>
<style>
</style>