图鸟UI V1.0.0 版本提交

This commit is contained in:
JaylenTech
2021-12-29 11:14:34 +08:00
commit cb0af8c384
203 changed files with 44944 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
TuniaoUi for uniApp v1.0.0 | by 图鸟 2021-09-01
仅供开发,如作它用所承受的法律责任一概与作者无关
*使用TuniaoUi开发扩展与插件时,请注明基于tuniao字眼
@@ -0,0 +1,202 @@
<template>
<view v-if="value" class="tn-action-sheet-class tn-action-sheet">
<tn-popup
v-model="value"
mode="bottom"
length="auto"
:popup="false"
:borderRadius="borderRadius"
:maskCloseable="maskCloseable"
:safeAreaInsetBottom="safeAreaInsetBottom"
:zIndex="elZIndex"
@close="close"
>
<!-- 提示信息 -->
<view
v-if="tips.text"
class="tn-action-sheet__tips tn-border-solid-bottom"
:style="[tipsStyle]"
>
{{tips.text}}
</view>
<!-- 按钮列表 -->
<block v-for="(item, index) in list" :key="index">
<view
class="tn-action-sheet__item tn-text-ellipsis"
:class="[ index < list.length - 1 ? 'tn-border-solid-bottom' : '']"
:style="[itemStyle(index)]"
hover-class="tn-hover-class"
:hover-stay-time="150"
@tap="itemClick(index)"
@touchmove.stop.prevent
>
<text>{{item.text}}</text>
<text v-if="item.subText" class="tn-action-sheet__item__subtext tn-text-ellipsis">{{item.subText}}</text>
</view>
</block>
<!-- 取消按钮 -->
<block v-if="cancelBtn">
<view class="tn-action-sheet__cancel--gab"></view>
<view
class="tn-action-sheet__cancel tn-action-sheet__item"
hover-class="tn-hover-class"
:hover-stay-time="150"
@tap="close"
>{{cancelText}}</view>
</block>
</tn-popup>
</view>
</template>
<script>
export default {
name: 'tn-action-sheet',
props: {
// 通过v-model控制弹出和收起
value: {
type: Boolean,
default: false
},
// 按钮文字数组,可以自定义颜色和字体大小
// return [{
// text: '确定',
// subText: '这是一个确定按钮',
// color: '',
// fontSize: '',
// disabled: true
// }]
list: {
type: Array,
default() {
return []
}
},
// 顶部提示文字
tips: {
type: Object,
default() {
return {
text: '',
color: '',
fontSize: 26
}
}
},
// 弹出的顶部圆角值
borderRadius: {
type: Number,
default: 0
},
// 点击遮罩可以关闭
maskCloseable: {
type: Boolean,
default: true
},
// 底部取消按钮
cancelBtn: {
type: Boolean,
default: true
},
// 底部取消按钮的文字
cancelText: {
type: String,
default: '取消'
},
// 开启底部安全区域
// 在iPhoneX机型底部添加一定的内边距
safeAreaInsetBottom: {
type: Boolean,
default: false
},
// z-index值
zIndex: {
type: Number,
default: 0
}
},
computed: {
// 顶部提示样式
tipsStyle() {
let style = {}
if (this.tips.color) style.color = this.tips.color
if (this.tips.fontSize) style.fontSize = this.tips.fontSize + 'rpx'
return style
},
// 操作项目的样式
itemStyle() {
return (index) => {
let style = {}
if (this.list[index].color) style.color = this.list[index].color
if (this.list[index].fontSize) style.fontSize = this.list[index].fontSize + 'rpx'
// 选项被禁用的样式
if (this.list[index].disabled) style.color = '#AAAAAA'
return style
}
},
elZIndex() {
return this.zIndex ? this.zIndex : this.$t.zIndex.popup
}
},
methods: {
// 点击取消按钮
close() {
// 发送input事件,并不会作用于父组件,而是要设置组件内部通过props传递的value参数
this.popupClose();
this.$emit('close');
},
// 关闭弹窗
popupClose() {
this.$emit('input', false)
},
// 点击对应的item
itemClick(index) {
// 如果是禁用项则不进行操作
if (this.list[index].disabled) return
this.$emit('click', index)
this.popupClose()
}
}
}
</script>
<style lang="scss" scoped>
.tn-action-sheet {
&__tips {
font-size: 26rpx;
text-align: center;
padding: 34rpx 0;
line-height: 1;
color: $tn-content-color;
}
&__item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 32rpx;
padding: 34rpx 0;
&__subtext {
font-size: 24rpx;
color: $tn-content-color;
margin-top: 20rpx;
}
}
&__cancel {
color: $tn-font-color;
&--gab {
height: 12rpx;
background-color: #eaeaec;
}
}
}
</style>
@@ -0,0 +1,101 @@
<template>
<view class="tn-avatar-group-class tn-avatar-group">
<view v-for="(item, index) in lists" :key="index" class="tn-avatar-group__item" :style="[itemStyle]">
<tn-avatar
:src="item.src || ''"
:text="item.text || ''"
:icon="item.icon || ''"
:size="size"
:shape="shape"
:imgMode="imgMode"
:border="true"
:borderSize="4"
></tn-avatar>
</view>
</view>
</template>
<script>
export default {
name: 'tn-avatar-group',
props: {
// 头像列表
lists: {
type: Array,
default() {
return []
}
},
// 头像类型
// square 带圆角正方形 circle 圆形
shape: {
type: String,
default: 'circle'
},
// 大小
// sm 小头像 lg 大头像 xl 加大头像
// 如果为其他则认为是直接设置大小
size: {
type: [Number, String],
default: ''
},
// 当设置为显示头像信息时,
// 图片的裁剪模式
imgMode: {
type: String,
default: 'aspectFill'
},
// 头像之间的遮挡比例
// 0.4 代表 40%
gap: {
type: Number,
default: 0.4
}
},
computed: {
itemStyle() {
let style = {}
if (this._checkSizeIsInline()) {
switch(this.size) {
case 'sm':
style.marginLeft = `${-48 * this.gap}rpx`
break
case 'lg':
style.marginLeft = `${-96 * this.gap}rpx`
break
case 'xl':
style.marginLeft = `${-128 * this.gap}rpx`
break
}
} else {
const size = Number(this.size.replace(/(px|rpx)/g, '')) || 64
style.marginLeft = `-${size * this.gap}rpx`
}
return style
}
},
data() {
return {
}
},
methods: {
// 检查是否使用内置的大小进行设置
_checkSizeIsInline() {
if (/(xs|sm|md|lg|xl|xxl)/.test(this.size)) return true
else return false
}
}
}
</script>
<style lang="scss" scoped>
.tn-avatar-group {
display: flex;
flex-direction: row;
&__item {
position: relative;
}
}
</style>
@@ -0,0 +1,298 @@
<template>
<view
class="tn-avatar-class tn-avatar"
:class="[backgroundColorClass,avatarClass]"
:style="[avatarStyle]"
@tap="click"
>
<image
v-if="showImg"
class="tn-avatar__img"
:class="[imgClass]"
:src="src"
:mode="imgMode || 'aspectFill'"
@error="loadImageError"
></image>
<view v-else class="tn-avatar__text" >
<view v-if="text">{{ text }}</view>
<view v-else :class="[`tn-icon-${icon}`]"></view>
</view>
<!-- 角标 -->
<tn-badge
v-if="badge && (badgeIcon || badgeText)"
:radius="badgeSize"
:backgroundColor="badgeBgColor"
:fontColor="badgeColor"
:fontSize="badgeSize - 8"
:absolute="true"
:top="badgePosition[0]"
:right="badgePosition[1]"
>
<view v-if="badgeIcon && badgeText === ''">
<view :class="[`tn-icon-${badgeIcon}`]"></view>
</view>
<view v-else>
{{ badgeText }}
</view>
</tn-badge>
</view>
</template>
<script>
import componentsColorMixin from '../../libs/mixin/components_color.js'
export default {
mixins: [componentsColorMixin],
name: 'tn-avatar',
props: {
// 序号
index: {
type: [Number, String],
default: 0
},
// 头像类型
// square 带圆角正方形 circle 圆形
shape: {
type: String,
default: 'circle'
},
// 大小
// sm 小头像 lg 大头像 xl 加大头像
// 如果为其他则认为是直接设置大小
size: {
type: [Number, String],
default: ''
},
// 是否显示阴影
shadow: {
type: Boolean,
default: false
},
// 是否显示边框
border: {
type: Boolean,
default: false
},
// 边框颜色
borderColor: {
type: String,
default: 'rgba(0, 0, 0, 0.1)'
},
// 边框大小, rpx
borderSize: {
type: Number,
default: 2
},
// 头像路径
src: {
type: String,
default: ''
},
// 文字
text: {
type: String,
default: ''
},
// 图标
icon: {
type: String,
default: ''
},
// 当设置为显示头像信息时,
// 图片的裁剪模式
imgMode: {
type: String,
default: 'aspectFill'
},
// 是否显示角标
badge: {
type: Boolean,
default: false
},
// 设置显示角标后,角标大小
badgeSize: {
type: Number,
default: 28
},
// 角标背景颜色
badgeBgColor: {
type: String,
default: '#AAAAAA'
},
// 角标字体颜色
badgeColor: {
type: String,
default: '#FFFFFF'
},
// 角标图标
badgeIcon: {
type: String,
default: ''
},
// 角标文字,优先级比icon高
badgeText: {
type: String,
default: ''
},
// 角标坐标
// [top, right]
badgePosition: {
type: Array,
default() {
return [0, 0]
}
}
},
data() {
return {
// 图片显示是否发生错误
imgLoadError: false
}
},
computed: {
showImg() {
// 如果设置了图片地址,则为显示图片,否则为显示文本
return this.text === '' && this.icon === ''
},
avatarClass() {
let clazz = ''
clazz += ` tn-avatar--${this.shape}`
if (this._checkSizeIsInline()) {
clazz += ` tn-avatar--${this.size}`
}
if (this.shadow) {
clazz += ' tn-avatar--shadow'
}
return clazz
},
avatarStyle() {
let style = {}
if (this.backgroundColorStyle) {
style.background = this.backgroundColorStyle
} else if (this.shadow && this.showImg) {
style.backgroundImage = `url(${this.src})`
}
if (this.border) {
style.border = `${this.borderSize}rpx solid ${this.borderColor}`
}
if (!this._checkSizeIsInline()) {
style.width = this.size
style.height = this.size
}
return style
},
imgClass() {
let clazz = ''
clazz += ` tn-avatar__img--${this.shape}`
return clazz
}
},
methods: {
// 加载图片失败
loadImageError() {
this.imgLoadError = true
},
// 点击事件
click() {
this.$emit("click", this.index)
},
// 检查是否使用内置的大小进行设置
_checkSizeIsInline() {
if (/^(xs|sm|md|lg|xl|xxl)$/.test(this.size)) return true
else return false
}
}
}
</script>
<style lang="scss" scoped>
.tn-avatar {
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
margin: 0;
padding: 0;
text-align: center;
align-items: center;
justify-content: center;
background-color: $tn-font-holder-color;
color: #FFFFFF;
white-space: nowrap;
position: relative;
width: 64rpx;
height: 64rpx;
z-index: 1;
&--sm {
width: 48rpx;
height: 48rpx;
}
&--lg {
width: 96rpx;
height: 96rpx;
}
&--xl {
width: 128rpx;
height: 128rpx;
}
&--square {
border-radius: 10rpx;
}
&--circle {
border-radius: 5000rpx;
}
&--shadow {
position: relative;
&::after {
content: " ";
display: block;
background: inherit;
filter: blur(10rpx);
position: absolute;
width: 100%;
height: 100%;
top: 10rpx;
left: 10rpx;
z-index: -1;
opacity: 0.4;
transform-origin: 0 0;
border-radius: inherit;
transform: scale(1, 1);
}
}
&__img {
width: 100%;
height: 100%;
&--square {
border-radius: 10rpx;
}
&--circle {
border-radius: 5000rpx;
}
}
&__text {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
}
</style>
+172
View File
@@ -0,0 +1,172 @@
<template>
<view
class="tn-badge-class tn-badge"
:class="[
backgroundColorClass,
fontColorClass,
badgeClass
]"
:style="[badgeStyle]"
@click="handleClick"
>
<slot v-if="!dot"></slot>
</view>
</template>
<script>
import componentsColorMixin from '../../libs/mixin/components_color.js'
export default {
mixins: [componentsColorMixin],
name: 'tn-badge',
props: {
// 序号
index: {
type: [Number, String],
default: '0'
},
// 徽章的大小 rpx
radius: {
type: Number,
default: 0
},
// 内边距
padding: {
type: String,
default: ''
},
// 外边距
margin: {
type: String,
default: '0'
},
// 是否为一个点
dot: {
type: Boolean,
default: false
},
// 是否使用绝对定位
absolute: {
type: Boolean,
default: false
},
// top
top: {
type: [String, Number],
default: '0'
},
// right
right: {
type: [String, Number],
default: '0'
},
// 居中 对齐右上角
translateCenter: {
type: Boolean,
default: true
}
},
computed: {
badgeClass() {
let clazz = ''
if (this.dot) {
clazz += ' tn-badge--dot'
}
if (this.absolute) {
clazz += ' tn-badge--absolute'
if (this.translateCenter) {
clazz += ' tn-badge--center-position'
}
}
return clazz
},
badgeStyle() {
let style = {}
if (this.radius !== 0) {
style.width = this.radius + 'rpx'
style.height = this.radius + 'rpx'
// style.borderRadius = (this.radius * 8) + 'rpx'
}
if (this.padding) {
style.padding = this.padding
}
if (this.margin) {
style.margin = this.margin
}
if (this.fontColorStyle) {
style.color = this.fontColorStyle
}
if (this.fontSize) {
style.fontSize = this.fontSize + this.fontUnit
}
if (this.backgroundColorStyle) {
style.backgroundColor = this.backgroundColorStyle
}
if (this.top) {
style.top = this.$t.string.getLengthUnitValue(this.top)
}
if (this.right) {
style.right = this.$t.string.getLengthUnitValue(this.right)
}
return style
},
},
data() {
return {
}
},
methods: {
// 处理点击事件
handleClick() {
this.$emit('click', {
index: Number(this.index)
})
this.$emit('tap', {
index: Number(this.index)
})
},
}
}
</script>
<style lang="scss" scoped>
.tn-badge {
width: auto;
height: auto;
line-height: 1;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
font-size: 18rpx;
background-color: $tn-main-color;
color: #FFFFFF;
border-radius: 100rpx;
padding: 4rpx 8rpx;
&--dot {
width: 8rpx;
height: 8rpx;
border-radius: 50%;
padding: 0;
}
&--absolute {
position: absolute;
top: 0;
right: 0;
}
&--center-position {
transform: translate(50%, -50%);
}
}
</style>
@@ -0,0 +1,298 @@
<template>
<button
class="tn-btn-class tn-btn"
:class="[
buttonClass,
backgroundColorClass,
fontColorClass
]"
:style="[buttonStyle]"
hover-class="tn-hover"
:loading="loading"
:disabled="disabled"
:form-type="formType"
:open-type="openType"
@getuserinfo="handleGetUserInfo"
@getphonenumber="handleGetPhoneNumber"
@contact="handleContact"
@error="handleError"
@tap="handleClick"
>
<slot></slot>
</button>
</template>
<script>
import componentsColorMixin from '../../libs/mixin/components_color.js'
export default {
mixins: [componentsColorMixin],
name: "tn-button",
// 解决再微信小程序种,自定义按钮无法触发bindsubmit
behaviors: ['wx://form-field-button'],
props: {
// 按钮索引,用于区分多个按钮
index: {
type: [Number, String],
default: 0
},
// 按钮形状 default 默认 round 圆角 icon 图标按钮
shape: {
type: String,
default: 'default'
},
// 是否加阴影
shadow: {
type: Boolean,
default: false
},
// 宽度 rpx或%
width: {
type: String,
default: 'auto'
},
// 高度 rpx或%
height: {
type: String,
default: ''
},
// 按钮的尺寸 sm lg
size: {
type: String,
default: ''
},
// 字体是否加粗
fontBold: {
type: Boolean,
default: false
},
padding: {
type: String,
default: '0 30rpx'
},
// 外边距 与css的margin参数用法相同
margin: {
type: String,
default: ''
},
// 是否镂空
plain: {
type: Boolean,
default: false
},
// 当plain=true时,是否显示边框
border: {
type: Boolean,
default: true
},
// 当plain=true时,是否加粗显示边框
borderBold: {
type: Boolean,
default: false
},
// 是否禁用
disabled: {
type: Boolean,
default: false
},
// 是否显示加载图标
loading: {
type: Boolean,
default: false
},
// 触发form表单的事件类型
formType: {
type: String,
default: ''
},
// 开放能力
openType: {
type: String,
default: ''
},
// 是否阻止重复点击(默认间隔是200ms)
blockRepeatClick: {
type: Boolean,
default: false
}
},
computed: {
// 根据不同的参数动态生成class
buttonClass() {
let clazz = ''
// 按钮形状
switch (this.shape) {
case 'icon':
case 'round':
clazz += ' tn-round'
break
}
// 阴影
if (this.shadow) {
if (this.backgroundColorClass !== '') {
const color = this.backgroundColor.slice(this.backgroundColor.lastIndexOf('-') + 1)
clazz += ` tn-shadow-${color}`
} else {
clazz += ' tn-shadow-blur'
}
}
// 字体加粗
if (this.fontBold) {
clazz += ' tn-text-bold'
}
// 设置为镂空并且设置镂空便可才进行设置
if (this.plain) {
clazz += ' tn-btn--plain'
if (this.border) {
clazz += ' tn-border-solid'
if (this.borderBold) {
clazz += ' tn-bold-border'
}
if (this.backgroundColor !== '' && this.backgroundColor.includes('tn-bg')) {
const color = this.backgroundColor.slice(this.backgroundColor.lastIndexOf('-') + 1)
clazz += ` tn-border-${color}`
}
}
}
return clazz
},
// 按钮的样式
buttonStyle() {
let style = {}
switch(this.size) {
case 'sm':
style.padding = '0 20rpx'
style.fontSize = '22rpx'
style.height = this.height || '48rpx'
break
case 'lg':
style.padding = '0 40rpx'
style.fontSize = '32rpx'
style.height = this.height || '80rpx'
break
default :
style.padding = '0 30rpx'
style.fontSize = '28rpx'
style.height = this.height || '64rpx'
}
// 是否手动设置了内边距
if (this.padding) {
style.padding = this.padding
}
// 是否手动设置外边距
if (this.margin) {
style.margin = this.margin
}
// 是否手动设置了字体大小
if (this.fontSize) {
style.fontSize = this.fontSize + this.fontUnit
}
style.width = this.shape === 'icon' ? style.height : this.width
style.padding = this.shape === 'icon' ? '0' : style.padding
if (this.fontColorStyle) {
style.color = this.fontColorStyle
}
if (!this.backgroundColorClass) {
if (this.plain) {
style.borderColor = this.backgroundColorStyle || '#01BEFF'
} else {
style.backgroundColor = this.backgroundColorStyle || '#01BEFF'
}
}
// 设置阴影
if (this.shadow && !this.backgroundColorClass) {
style.boxShadow = `6rpx 6rpx 8rpx ${(this.backgroundColorStyle || '#01BEFF')}10`
}
return style
},
},
data() {
return {
// 上次点击的时间
clickTime: 0,
// 两次点击防抖的间隔时间
clickIntervalTime: 200
}
},
methods: {
// 按钮点击事件
handleClick() {
if (this.disabled) {
return
}
if (this.blockRepeatClick) {
const nowTime = new Date().getTime()
if (nowTime - this.clickTime <= this.clickIntervalTime) {
return
}
this.clickTime = nowTime
setTimeout(() => {
this.clickTime = 0
}, this.clickIntervalTime)
}
this.$emit('click', {
index: Number(this.index)
})
// 兼容tap事件
this.$emit('tap', {
index: Number(this.index)
})
},
handleGetUserInfo({ detail = {} } = {}) {
this.$emit('getuserinfo', detail);
},
handleContact({ detail = {} } = {}) {
this.$emit('contact', detail);
},
handleGetPhoneNumber({ detail = {} } = {}) {
this.$emit('getphonenumber', detail);
},
handleError({ detail = {} } = {}) {
this.$emit('error', detail);
},
}
}
</script>
<style lang="scss" scoped>
.tn-btn {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
line-height: 1;
text-align: center;
text-decoration: none;
overflow: visible;
margin-left: inherit;
transform: translate(0rpx, 0rpx);
margin-right: inherit;
// background-color: $tn-mai
border-radius: 12rpx;
color: #FFFFFF;
&--plain {
background-color: transparent !important;
background-image: none;
&.tn-round {
border-radius: 1000rpx !important;
}
}
}
</style>
@@ -0,0 +1,707 @@
<template>
<tn-popup
v-model="value"
mode="bottom"
:popup="false"
length="auto"
:borderRadius="borderRadius"
:safeAreaInsetBottom="safeAreaInsetBottom"
:maskCloseable="maskCloseable"
:closeBtn="closeBtn"
:zIndex="elIndex"
@close="close"
>
<view class="tn-calendar-class tn-calendar">
<!-- 头部 -->
<view class="tn-calendar__header">
<view v-if="!$slots.tooltip || !$slots.$tooltip" class="tn-calendar__header__text">
{{ toolTips }}
</view>
<view v-else>
<slot name="tooltip"></slot>
</view>
</view>
<!-- 操作提示信息 -->
<view class="tn-calendar__action">
<view v-if="changeYear" class="tn-calendar__action__icon" :style="{backgroundColor: yearArrowColor}" @tap.stop="changeYearHandler(false)">
<view><text class="tn-icon-left"></text></view>
</view>
<view v-if="changeMonth" class="tn-calendar__action__icon" :style="{backgroundColor: monthArrowColor}" @tap.stop="changeMonthHandler(false)">
<view><text class="tn-icon-left"></text></view>
</view>
<view class="tn-calendar__action__text">{{ dateTitle }}</view>
<view v-if="changeMonth" class="tn-calendar__action__icon" :style="{backgroundColor: monthArrowColor}" @tap.stop="changeMonthHandler(true)">
<view><text class="tn-icon-right"></text></view>
</view>
<view v-if="changeYear" class="tn-calendar__action__icon" :style="{backgroundColor: yearArrowColor}" @tap.stop="changeYearHandler(true)">
<view><text class="tn-icon-right"></text></view>
</view>
</view>
<!-- 星期中文标识 -->
<view class="tn-calendar__week-day-zh">
<view v-for="(item,index) in weekDayZh" :key="index" class="tn-calendar__week-day-zh__text">{{ item }}</view>
</view>
<!-- 日历主体 -->
<view class="tn-calendar__content">
<!-- 前置空白部分 -->
<block v-for="(item, index) in weekdayArr" :key="index">
<view class="tn-calendar__content__item"></view>
</block>
<view
v-for="(item, index) in daysArr"
:key="index"
class="tn-calendar__content__item"
:class="{
'tn-hover': disabledChoose(year, month, index + 1),
'tn-calendar__content--start-date': (mode === 'range' && startDate == `${year}-${month}-${index+1}`) || mode === 'date',
'tn-calendar__content--end-date': (mode === 'range' && endDate == `${year}-${month}-${index+1}`) || mode === 'date'
}"
:style="{
backgroundColor: colorValue(index, 'bg')
}"
@tap.stop="dateClick(index)"
>
<view class="tn-calendar__content__item__text" :style="{color: colorValue(index, 'text')}">
<view>{{ item.day }}</view>
</view>
<view class="tn-calendar__content__item__tips" :style="{color: item.color}">
{{ item.bottomInfo }}
</view>
</view>
<view class="tn-calendar__content__month--bg">{{ month }}</view>
</view>
<!-- 底部 -->
<view class="tn-calendar__bottom">
<view class="tn-calendar__bottom__choose">
<text>{{ mode === 'date' ? activeDate : startDate }}</text>
<text v-if="endDate">{{ endDate }}</text>
</view>
<view class="tn-calendar__bottom__btn" :style="{backgroundColor: btnColor}" @click="handleBtnClick(false)">
<view class="tn-calendar__bottom__btn--text">确定</view>
</view>
</view>
</view>
</tn-popup>
</template>
<script>
import Calendar from '../../libs/utils/calendar.js'
export default {
name: 'tn-calendar',
props: {
// 双向绑定控制组件弹出与收起
value: {
type: Boolean,
default: false
},
// 模式
// date -> 单日期 range -> 日期范围
mode: {
type: String,
default: 'date'
},
// 是否允许切换年份
changeYear: {
type: Boolean,
default: true
},
// 是否允许切换月份
changeMonth: {
type: Boolean,
default: true
},
// 可切换的最大年份
maxYear: {
type: [Number, String],
default: 2100
},
// 可切换的最小年份
minYear: {
type: [Number, String],
default: 1970
},
// 最小日期(不在范围被不允许选择)
minDate: {
type: String,
default: '1970-01-01'
},
// 最大日期,如果为空则默认为今天
maxDate: {
type: String,
default: ''
},
// 切换月份按钮的颜色
monthArrowColor: {
type: String,
default: '#AAAAAA'
},
// 切换年份按钮的颜色
yearArrowColor: {
type: String,
default: '#C8C8C8'
},
// 默认字体颜色
color: {
type: String,
default: '#080808'
},
// 选中|起始结束日期背景颜色
activeBgColor: {
type: String,
default: '#01BEFF'
},
// 选中|起始结束日期文字颜色
activeColor: {
type: String,
default: '#FFFFFF'
},
// 范围日期内的背景颜色
rangeBgColor: {
type: String,
default: '#E6E6E655'
},
// 范围日期内的文字颜色
rangeColor: {
type: String,
default: '#01BEFF'
},
// 起始日期显示的文字,mode=range时生效
startText: {
type: String,
default: '开始'
},
// 结束日期显示的文字,mode=range时生效
endText: {
type: String,
default: '结束'
},
// 按钮背景颜色
btnColor: {
type: String,
default: '#01BEFF'
},
// 农历文字的颜色
lunarColor: {
type: String,
default: '#AAAAAA'
},
// 选中日期是否有选中效果
isActiveCurrent: {
type: Boolean,
default: true
},
// 切换年月是否触发事件,mode=date时生效
isChange: {
type: Boolean,
default: false
},
// 是否显示农历
showLunar: {
type: Boolean,
default: true
},
// 顶部提示文字
toolTips: {
type: String,
default: '请选择日期'
},
// 显示圆角的大小
borderRadius: {
type: Number,
default: 8
},
// 是否开启底部安全区适配,开启的话,会在iPhoneX机型底部添加一定的内边距
safeAreaInsetBottom: {
type: Boolean,
default: false
},
// 是否可以通过点击遮罩进行关闭
maskCloseable: {
type: Boolean,
default: true
},
// zIndex
zIndex: {
type: Number,
default: 0
},
// 是否显示关闭按钮
closeBtn: {
type: Boolean,
default: false
},
},
computed: {
dateChange() {
return `${this.mode}-${this.minDate}-${this.maxDate}`
},
elIndex() {
return this.zIndex ? this.zIndex : this.$t.zIndex.popup
},
colorValue() {
return (index, type) => {
let color = type === 'bg' ? '' : this.color
let day = index + 1
let date = `${this.year}-${this.month}-${day}`
let timestamp = new Date(date.replace(/\-/g,'/')).getTime()
let start = this.startDate.replace(/\-/g,'/')
let end = this.endDate.replace(/\-/g,'/')
if ((this.mode === 'date' && this.isActiveCurrent && this.activeDate == date) || this.startDate == date || this.endDate == date) {
color = type === 'bg' ? this.activeBgColor : this.activeColor
} else if (this.endDate && timestamp > new Date(start).getTime() && timestamp < new Date(end).getTime()) {
color = type === 'bg' ? this.rangeBgColor : this.rangeColor
}
return color
}
}
},
data() {
return {
// 星期几,1-7
weekday: 1,
weekdayArr: [],
// 星期对应的中文
weekDayZh: ['日','一','二','三','四','五','六'],
// 当前月有多少天
days: 0,
daysArr: [],
year: 2021,
month: 0,
day: 0,
startYear: 0,
startMonth: 0,
startDay: 0,
endYear: 0,
endMonth: 0,
endDay: 0,
today: '',
activeDate: '',
startDate: '',
endDate: '',
min: null,
max: null,
// 日期标题
dateTitle: '',
// 标记是否已经选择了开始日期
chooseStart: false
}
},
watch: {
dateChange() {
this.init()
}
},
created() {
this.init()
},
methods: {
// 初始化
init() {
let now = new Date()
this.year = now.getFullYear()
this.month = now.getMonth() + 1
this.day = now.getDate()
this.today = `${this.year}-${this.month}-${this.day}`
this.activeDate = this.today
this.min = this.initDate(this.minDate)
this.max = this.initDate(this.maxDate || this.today)
this.startDate = ''
this.startYear = 0
this.startMonth = 0
this.startDay = 0
this.endDate = ''
this.endYear = 0
this.endMonth = 0
this.endDay = 0
this.chooseStart = false
this.changeData()
},
// 切换月份
changeMonthHandler(add) {
if (add) {
let month = this.month + 1
let year = month > 12 ? this.year + 1 : this.year
if (!this.checkRange(year)) {
this.month = month > 12 ? 1 : month
this.year = year
this.changeData()
}
} else {
let month = this.month - 1
let year = month < 1 ? this.year - 1 : this.year
if (!this.checkRange(year)) {
this.month = month < 1 ? 12 : month
this.year = year
this.changeData()
}
}
},
// 切换年份
changeYearHandler(add) {
let year = add ? this.year + 1 : this.year - 1
if (!this.checkRange(year)) {
this.year = year
this.changeData()
}
},
// 日期点击事件
dateClick(day) {
day += 1
if (!this.disabledChoose(this.year, this.month, day)) {
this.day = day
let date = `${this.year}-${this.month}-${day}`
if (this.mode === 'date') {
this.activeDate = date
} else {
let startTimeCompare = new Date(date.replace(/\-/g,'/')).getTime() < new Date(this.startDate.replace(/\-/g,'/')).getTime()
if (!this.chooseStart || startTimeCompare) {
this.startDate = date
this.startYear = this.year
this.startMonth = this.month
this.startDay = this.day
this.endYear = 0
this.endMonth = 0
this.endDay = 0
this.endDate = ''
this.activeDate = ''
this.chooseStart = true
} else {
this.endDate = date
this.endYear = this.year
this.endMonth = this.month
this.endDay = this.day
this.chooseStart = false
}
}
this.daysArr = this.handleDaysArr()
}
},
// 修改日期数据
changeData() {
this.days = this.getMonthDay(this.year, this.month)
this.daysArr = this.handleDaysArr()
this.weekday = this.getMonthFirstWeekDay(this.year, this.month)
this.weekdayArr = this.generateArray(1, this.weekday)
this.dateTitle = `${this.year}${this.month}`
if (this.isChange && this.mode === 'date') {
this.handleBtnClick(true)
}
},
// 处理按钮点击
handleBtnClick(show) {
if (!show) {
this.close()
}
if (this.mode === 'date') {
let arr = this.activeDate.split('-')
let year = this.isChange ? this.year : Number(arr[0])
let month = this.isChange ? this.month : Number(arr[1])
let day = this.isChange ? this.day : Number(arr[2])
let days = this.getMonthDay(year, month)
let result = `${year}-${this.formatNumber(month)}-${this.formatNumber(day)}`
let weekText = this.getWeekText(result)
let isToday = false
if (`${year}-${month}-${day}` === this.today) {
isToday = true
}
this.$emit('change', {
year,
month,
day,
days,
week: weekText,
isToday,
date: result,
// 是否为切换年月操作
switch: show
})
} else {
if (!this.startDate || !this.endDate) return
let startMonth = this.formatNumber(this.startMonth)
let startDay = this.formatNumber(this.startDay)
let startDate = `${this.startYear}-${startMonth}-${startDay}`
let startWeek = this.getWeekText(startDate)
let endMonth = this.formatNumber(this.endMonth)
let endDay = this.formatNumber(this.endDay)
let endDate = `${this.endYear}-${endMonth}-${endDay}`
let endWeek = this.getWeekText(endDate)
this.$emit('change', {
startYear: this.startYear,
startMonth: this.startMonth,
startDay: this.startDay,
startDate,
startWeek,
endYear: this.endYear,
endMonth: this.endMonth,
endDay: this.endDay,
endDate,
endWeek
})
}
},
// 判断是否允许选择
disabledChoose(year, month, day) {
let flag = true
let date = `${year}/${month}/${day}`
let min = `${this.min.year}/${this.min.month}/${this.min.day}`
let max = `${this.max.year}/${this.max.month}/${this.max.day}`
let timestamp = new Date(date).getTime()
if (timestamp >= new Date(min).getTime() && timestamp <= new Date(max).getTime()) {
flag = false
}
return flag
},
// 检查是否在日期范围内
checkRange(year) {
let overstep = false
if (year < this.minYear || year > this.maxYear) {
uni.showToast({
title: '所选日期超出范围',
icon: 'none'
})
overstep = true
}
return overstep
},
// 处理日期
initDate(date) {
let fdate = date.split('-')
return {
year: Number(fdate[0] || 1970),
month: Number(fdate[1] || 1),
day: Number(fdate[2] || 1)
}
},
// 处理日期数组
handleDaysArr() {
let days = this.generateArray(1, this.days)
let daysArr = days.map((item) => {
let bottomInfo = this.showLunar ? Calendar.solar2lunar(this.year, this.month, item).IDayCn : ''
let color = this.showLunar ? this.lunarColor : this.activeColor
if (
(this.mode === 'date' && this.day == item) ||
(this.mode === 'range' && (this.startDay == item || this.endDay == item))
) {
color = this.activeColor
}
if (this.mode === 'range') {
if (this.startDay == item && this.startDay != this.endDay) {
bottomInfo = this.startText
}
if (this.endDay == item) {
bottomInfo = this.endText
}
}
return {
day: item,
color: color,
bottomInfo: bottomInfo
}
})
return daysArr
},
// 获取对应月有多少天
getMonthDay(year, month) {
return new Date(year, month, 0).getDate()
},
// 获取对应月的第一天时星期几
getMonthFirstWeekDay(year, month) {
return new Date(`${year}/${month}/01 00:00:00`).getDay()
},
// 获取对应星期的文本
getWeekText(date) {
date = new Date(`${date.replace(/\-/g, '/')} 00:00:00`)
let week = date.getDay()
return '星期' + this.weekDayZh[week]
},
// 生成日期天数数组
generateArray(start, end) {
return Array.from(new Array(end + 1).keys()).slice(start)
},
// 格式化数字
formatNumber(num) {
return num < 10 ? '0' + num : num + ''
},
// 关闭窗口
close() {
this.$emit('input', false)
}
}
}
</script>
<style lang="scss" scoped>
.tn-calendar {
color: $tn-font-color;
&__header {
width: 100%;
box-sizing: border-box;
font-size: 30rpx;
background-color: #FFFFFF;
color: $tn-main-color;
&__text {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
margin-top: 30rpx;
padding: 0 60rpx;
}
}
&__action {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
padding: 40rpx 0 40rpx 0;
&__icon {
display: flex;
align-items: center;
justify-content: center;
margin: 0 16rpx;
width: 32rpx;
height: 32rpx;
font-size: 20rpx;
// line-height: 32rpx;
border-radius: 50%;
color: #FFFFFF;
}
&__text {
padding: 0 16rpx;
color: $tn-font-color;
font-size: 32rpx;
font-weight: bold;
}
}
&__week-day-zh {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 12rpx 0;
overflow: hidden;
box-shadow: 16rpx 6rpx 8rpx 0 #E6E6E6;
margin-bottom: 2rpx;
&__text {
flex: 1;
text-align: center;
}
}
&__content {
display: flex;
flex-direction: row;
flex-wrap: wrap;
width: 100%;
padding: 12rpx 0;
box-sizing: border-box;
background-color: #F7F7F7;
position: relative;
&__item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 14.2857%;
padding: 12rpx 0;
margin: 6rpx 0;
overflow: hidden;
position: relative;
z-index: 2;
// box-shadow: inset 0rpx 0rpx 22rpx 4rpx rgba(255,255,255, 0.52);
&__text {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 80rpx;
font-size: 32rpx;
position: relative;
}
&__tips {
position: absolute;
width: 100%;
line-height: 24rpx;
left: 0;
bottom: 8rpx;
text-align: center;
z-index: 2;
transform-origin: center center;
transform: scale(0.8);
}
}
&--start-date {
border-top-left-radius: 8rpx;
border-bottom-left-radius: 8rpx;
}
&--end-date {
border-top-right-radius: 8rpx;
border-bottom-right-radius: 8rpx;
}
&__month {
&--bg {
position: absolute;
font-size: 200rpx;
line-height: 200rpx;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
color: $tn-font-holder-color;
z-index: 1;
}
}
}
&__bottom {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
background-color: #F7F7F7;
padding: 0 40rpx 30rpx;
box-sizing: border-box;
font-size: 24rpx;
color: $tn-font-sub-color;
&__choose {
height: 50rpx;
}
&__btn {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 60rpx;
border-radius: 40rpx;
color: #FFFFFF;
font-size: 28rpx;
}
}
}
</style>
@@ -0,0 +1,320 @@
<template>
<view class="tn-car-keyboard-class tn-car-keyboard" @touchmove.stop.prevent="() => {}">
<view class="tn-car-keyboard__grids">
<view
v-for="(data, index) in inputCarNumber ? endKeyBoardList : areaList"
:key="index"
class="tn-car-keyboard__grids__item"
>
<view
v-for="(sub_data, sub_index) in data"
:key="sub_index"
class="tn-car-keyboard__grids__btn"
:class="{'tn-car-keyboard__grids__btn--disabled': sub_data === 'I'}"
:hover-class="sub_data !== 'I' ? 'tn-car-keyboard--hover' : ''"
:hover-stay-time="100"
@tap="click(index, sub_index)"
>
{{ sub_data }}
</view>
</view>
<view
class="tn-car-keyboard__back"
hover-class="tn-hover-class"
:hover-stay-time="150"
@touchstart.stop="backspaceClick"
@touchend="clearTimer"
>
<view class="tn-icon-left-arrow tn-car-keyboard__back__icon"></view>
</view>
<view
class="tn-car-keyboard__change"
hover-class="tn-car-keyboard--hover"
:hover-stay-time="150"
@tap="changeMode"
>
<text class="tn-car-keyboard__mode--zh" :class="[`tn-car-keyboard__mode--${!inputCarNumber ? 'active' : 'inactive'}`]"></text>
/
<text class="tn-car-keyboard__mode--en" :class="[`tn-car-keyboard__mode--${inputCarNumber ? 'active' : 'inactive'}`]"></text>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'tn-car-keyboard',
props: {
// 是否打乱键盘顺序
randomEnabled: {
type: Boolean,
default: false
},
// 切换中英文输入
switchEnMode: {
type: Boolean,
default: false
}
},
computed: {
areaList() {
let data = [
'京',
'沪',
'粤',
'津',
'冀',
'豫',
'云',
'辽',
'黑',
'湘',
'皖',
'鲁',
'苏',
'浙',
'赣',
'鄂',
'桂',
'甘',
'晋',
'陕',
'蒙',
'吉',
'闽',
'贵',
'渝',
'川',
'青',
'琼',
'宁',
'藏',
'港',
'澳',
'新',
'使',
'学',
'临',
'警'
]
// 打乱顺序
if (this.randomEnabled) data = this.$t.array.random(data)
// 切割二维数组
let showData = []
showData[0] = data.slice(0, 10)
showData[1] = data.slice(10, 20)
showData[2] = data.slice(20, 30)
showData[3] = data.slice(30, 37)
return showData
},
endKeyBoardList() {
let data = [
1,
2,
3,
4,
5,
6,
7,
8,
9,
0,
'Q',
'W',
'E',
'R',
'T',
'Y',
'U',
'I',
'O',
'P',
'A',
'S',
'D',
'F',
'G',
'H',
'J',
'K',
'L',
'Z',
'X',
'C',
'V',
'B',
'N',
'M'
]
// 打乱顺序
if (this.randomEnabled) data = this.$t.array.random(data)
// 切割二维数组
let showData = []
showData[0] = data.slice(0, 10)
showData[1] = data.slice(10, 20)
showData[2] = data.slice(20, 29)
showData[3] = data.slice(29, 36)
return showData
}
},
data() {
return {
// 标记是否输入车牌号码
inputCarNumber: false,
// 长按多次删除事件监听
longPressDeleteTimer: null
}
},
watch:{
switchEnMode: {
handler(value) {
if (value) {
this.inputCarNumber = true
} else {
this.inputCarNumber = false
}
},
immediate: true
}
},
methods: {
// 点击键盘按钮
click(i, j) {
let value = ''
// 根据不同模式获取不同数组的值
if (this.inputCarNumber) value = this.endKeyBoardList[i][j]
else value = this.areaList[i][j]
// 车牌里不包含I
if (value === 'I') return
this.$emit('change', value)
},
// 修改输入模式
// 中文/英文
changeMode() {
this.inputCarNumber = !this.inputCarNumber
},
// 点击退格
backspaceClick() {
this.$emit('backspace')
this.clearTimer()
this.longPressDeleteTimer = setInterval(() => {
this.$emit('backspace')
}, 250)
},
// 清空定时器
clearTimer() {
if (this.longPressDeleteTimer) {
clearInterval(this.longPressDeleteTimer)
this.longPressDeleteTimer = null
}
}
}
}
</script>
<style lang="scss" scoped>
.tn-car-keyboard {
position: relative;
padding: 24rpx 0;
background-color: #E6E6E6;
&__grids {
&__item {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
&__btn {
display: inline-flex;
justify-content: center;
flex: 0 0 64rpx;
width: 62rpx;
height: 80rpx;
font-size: 38rpx;
line-height: 80rpx;
font-weight: 500;
text-decoration: none;
text-align: center;
background-color: #FFFFFF;
margin: 8rpx 5rpx;
border-radius: 8rpx;
box-shadow: 0 2rpx 0rpx $tn-box-shadow-color;
&--disabled {
opacity: 0.6;
}
}
}
&__back {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
position: absolute;
width: 96rpx;
height: 80rpx;
right: 22rpx;
bottom: 32rpx;
background-color: #E6E6E6;
border-radius: 8rpx;
box-shadow: 0 2rpx 0rpx $tn-box-shadow-color;
}
&__change {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
position: absolute;
width: 96rpx;
height: 80rpx;
left: 22rpx;
bottom: 32rpx;
line-height: 1;
background-color: #FFFFFF;
border-radius: 8rpx;
box-shadow: 0 2rpx 0rpx $tn-box-shadow-color;
}
&__mode {
&--zh {
transform: translateY(-10rpx);
}
&--en {
transform: translateY(10rpx);
}
&--active {
color: $tn-main-color;
font-size: 30rpx;
}
&--inactive {
&.tn-car-keyboard__mode--zh {
transform: scale(0.85) translateY(-10rpx);
}
}
&--inactive {
&.tn-car-keyboard__mode--en {
transform: scale(0.85) translateY(10rpx);
}
}
}
&--hover {
background-color: #E6E6E6 !important;
}
}
</style>
@@ -0,0 +1,134 @@
<template>
<view class="tn-checkbox-group-class tn-checkbox-group">
<slot></slot>
</view>
</template>
<script>
import Emitter from '../../libs/utils/emitter.js'
export default {
mixins: [ Emitter ],
name: 'tn-checkbox-group',
props: {
value: {
type: Array,
default() {
return []
}
},
// 可以选中多少个checkbox
max: {
type: Number,
default: 999
},
// 表单提交时的标识符
name: {
type: String,
default: ''
},
// 禁用选择
disabled: {
type: Boolean,
default: false
},
// 禁用点击标签进行选择
disabledLabel: {
type: Boolean,
default: false
},
// 选择框的形状 square 方形 circle 圆形
shape: {
type: String,
default: 'square'
},
// 选中时的颜色
activeColor: {
type: String,
default: '#01BEFF'
},
// 组件大小
size: {
type: Number,
default: 34
},
// 每个checkbox占的宽度
width: {
type: String,
default: 'auto'
},
// 是否换行
wrap: {
type: Boolean,
default: false
},
// 图标大小
iconSize: {
type: Number,
default: 20
}
},
computed: {
// 这里computed的变量,都是子组件tn-checkbox需要用到的,由于头条小程序的兼容性差异,子组件无法实时监听父组件参数的变化
// 所以需要手动通知子组件,这里返回一个parentData变量,供watch监听,在其中去通知每一个子组件重新从父组件(tn-checkbox-group)
// 拉取父组件新的变化后的参数
parentData() {
return [this.value, this.disabled, this.disabledLabel, this.shape, this.activeColor, this.size, this.width, this.wrap, this.iconSize]
}
},
data() {
return {
}
},
watch: {
// 当父组件中的子组件需要共享的参数发生了变化,手动通知子组件
parentData() {
if (this.children.length) {
this.children.map(child => {
// 判断子组件(tn-checkbox)如果有updateParentData方法的话,子组件重新从父组件拉取了最新的值
typeof(child.updateParentData) === 'function' && child.updateParentData()
})
}
}
},
created() {
this.children = []
},
methods: {
initValue(values) {
this.$emit('input', values)
},
// 触发事件
emitEvent() {
let values = []
this.children.map(child => {
if (child.checkValue) values.push(child.name)
})
this.$emit('change', values)
this.$emit('input', values)
// 发出事件,用于在表单组件中嵌入checkbox的情况,进行验证
// 由于头条小程序执行迟钝,故需要用几十毫秒的延时
setTimeout(() => {
// 将当前的值发送到 tn-form-item 进行校验
this.dispatch('tn-form-item', 'on-form-change', values)
}, 60)
}
}
}
</script>
<style lang="scss" scoped>
.tn-checkbox-group {
/* #ifndef MP || APP-NVUE */
display: inline-flex;
flex-wrap: wrap;
/* #endif */
&::after {
content: " ";
display: table;
clear: both;
}
}
</style>
@@ -0,0 +1,328 @@
<template>
<view class="tn-checkbox-class tn-checkbox" :style="[checkboxStyle]">
<view
class="tn-checkbox__icon-wrap"
:class="[iconClass]"
:style="[iconStyle]"
@tap="toggle"
>
<view class="tn-checkbox__icon-wrap__icon" :class="[`tn-icon-${iconName}`]"></view>
</view>
<view
class="tn-checkbox__label"
:class="[labelClass]"
:style="{
fontSize: labelSize ? labelSize + 'rpx' : ''
}"
@tap="onClickLabel"
>
<slot></slot>
</view>
</view>
</template>
<script>
export default {
name: 'tn-checkbox',
props: {
// checkbox名称
name: {
type: [String, Number],
default: ''
},
// 是否为选中状态
value: {
type: Boolean,
default: false
},
// 禁用选择
disabled: {
type: Boolean,
default: false
},
// 禁用点击标签进行选择
disabledLabel: {
type: Boolean,
default: false
},
// 选择框的形状 square 方形 circle 圆形
shape: {
type: String,
default: ''
},
// 选中时的颜色
activeColor: {
type: String,
default: ''
},
// 组件大小
size: {
type: Number,
default: 0
},
// 图标名称
iconName: {
type: String,
default: 'success'
},
// 图标大小
iconSize: {
type: Number,
default: 0
},
// label的字体大小
labelSize: {
type: Number,
default: 0
}
},
computed: {
// 是否禁用选中,父组件的禁用会覆盖当前的禁用状态
isDisabled() {
return this.disabled ? this.disabled : (this.parent ? this.parentData.disabled : false)
},
// 是否禁用点击label选中,父组件的禁用会覆盖当前的禁用状态
isDisabledLabel() {
return this.disabledLabel ? this.disabledLabel : (this.parent ? this.parentData.disabledLabel : false)
},
// 尺寸
checkboxSize() {
return this.size ? this.size : (this.parent ? this.parentData.size : 34)
},
// 激活时的颜色
elAvtiveColor() {
return this.activeColor ? this.activeColor : (this.parent ? this.parentData.activeColor : '#01BEFF')
},
// 形状
elShape() {
return this.shape ? this.shape : (this.parent ? this.parentData.shape : 'square')
},
iconClass() {
let clazz = ''
clazz += (' tn-checkbox__icon-wrap--' + this.elShape)
if (this.checkValue) clazz += ' tn-checkbox__icon-wrap--checked'
if (this.isDisabled) clazz += ' tn-checkbox__icon-wrap--disabled'
if (this.value && this.isDisabled) clazz += ' tn-checkbox__icon-wrap--disabled--checked'
return clazz
},
iconStyle() {
let style = {}
// 判断是否用户手动禁用和传递的值
if (this.elAvtiveColor && this.checkValue && !this.isDisabled) {
style.borderColor = this.elAvtiveColor
style.backgroundColor = this.elAvtiveColor
}
// checkbox内部的勾选图标,如果选中状态,为白色,否则为透明色即可
style.color = this.checkValue ? '#FFFFFF' : 'transparent'
style.width = this.checkboxSize + 'rpx'
style.height = style.width
style.fontSize = (this.iconSize ? this.iconSize : (this.parent ? this.parentData.iconSize : 20)) + 'rpx'
return style
},
checkboxStyle() {
let style = {}
if (this.parent && this.parentData.width) {
// #ifdef MP
// 各家小程序因为它们特殊的编译结构,使用float布局
style.float = 'left';
// #endif
// #ifndef MP
// H5和APP使用flex布局
style.flex = `0 0 ${this.parentData.width}`;
// #endif
}
if(this.parent && this.parentData.wrap) {
style.width = '100%';
// #ifndef MP
// H5和APP使用flex布局,将宽度设置100%,即可自动换行
style.flex = '0 0 100%';
// #endif
}
return style
},
labelClass() {
let clazz = ''
if (this.isDisabled) {
clazz += ' tn-checkbox__label--disabled'
}
return clazz
}
},
data() {
return {
// 当前checkbox的value值
checkValue: false,
parentData: {
value: null,
max: null,
disabled: null,
disabledLabel: null,
shape: null,
activeColor: null,
size: null,
width: null,
wrap: null,
iconSize: null
}
}
},
watch: {
value(val) {
this.checkValue = val
}
},
created() {
// 支付宝小程序不支持provide/inject,所以使用这个方法获取整个父组件,在created定义,避免循环应用
// this.parent = this.$t.$parent.call(this, 'tn-checkbox-group')
// // 如果存在u-checkbox-group,将本组件的this塞进父组件的children中
// this.parent && this.parent.children.push(this)
// // 初始化父组件的value值
// this.parent && this.parent.emitEvent()
this.updateParentData()
this.parent && this.parent.children.push(this)
},
methods: {
updateCheckValue() {
// 更新当前checkbox的选中状态
this.checkValue = (this.parent && this.parentData.value.includes(this.name)) || this.value === true
if (this.parent) {
if (this.value && !this.parentData.value.includes(this.name)) {
this.parentData.value.push(this.name)
this.parent.initValue(this.parentData.value)
}
}
},
updateParentData() {
this.getParentData('tn-checkbox-group')
this.updateCheckValue()
},
onClickLabel() {
if (!this.isDisabled && !this.isDisabledLabel) {
this.setValue()
}
},
toggle() {
if (!this.isDisabled) {
this.setValue()
}
},
emitEvent() {
this.$emit('change', {
name: this.name,
value: !this.checkValue
})
if (this.parent) {
this.checkValue = !this.checkValue
// 执行父组件tn-checkbox-group的事件方法
// 等待下一个周期再执行,因为this.$emit('input')作用于父组件,再反馈到子组件内部,需要时间
setTimeout(() => {
if(this.parent.emitEvent) this.parent.emitEvent();
}, 80)
}
},
// 设置input的值,通过v-modal绑定组件的值
setValue() {
// 判断是否为可选项组
if (this.parent) {
// 反转状态
if (this.checkValue === true) {
this.emitEvent()
// this.$emit('input', !this.checkValue)
} else {
// 超出最大可选项,弹出提示
if (this.parentData.value.length >= this.parentData.max) {
return this.$t.messageUtils.toast(`最多可选${this.parent.max}`)
}
// 如果原来为未选中状态,需要选中的数量少于父组件中设置的max值,才可以选中
this.emitEvent();
// this.$emit('input', !this.checkValue);
}
} else {
// 只有一个可选项
this.emitEvent()
this.$emit('input', !this.checkValue)
}
}
}
}
</script>
<style lang="scss" scoped>
.tn-checkbox {
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
align-items: center;
overflow: hidden;
user-select: none;
line-height: 1.8;
&__icon-wrap {
color: $tn-font-color;
flex: none;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: 42rpx;
height: 42rpx;
color: transparent;
text-align: center;
transition-property: color, border-color, background-color;
border: 1px solid $tn-font-sub-color;
transition-duration: 0.2s;
/* #ifdef MP-TOUTIAO */
// 头条小程序兼容性问题,需要设置行高为0,否则图标偏下
&__icon {
line-height: 0;
}
/* #endif */
&--circle {
border-radius: 100%;
}
&--square {
border-radius: 6rpx;
}
&--checked {
color: #FFFFFF;
background-color: $tn-main-color;
border-color: $tn-main-color;
}
&--disabled {
background-color: $tn-font-holder-color;
border-color: $tn-font-sub-color;
}
&--disabled--checked {
color: $tn-font-sub-color !important;
}
}
&__label {
word-wrap: break-word;
margin-left: 10rpx;
margin-right: 24rpx;
color: $tn-font-color;
font-size: 30rpx;
&--disabled {
color: $tn-font-sub-color;
}
}
}
</style>
@@ -0,0 +1,223 @@
<template>
<view
class="tn-circle-progress-class tn-circle-progress"
:style="{
width: widthPx + 'px',
height: widthPx + 'px'
}"
>
<!-- 支付宝小程序不支持canvas-id属性必须用id属性 -->
<!-- 默认圆环 -->
<canvas
class="tn-circle-progress__canvas-bg"
:canvas-id="elBgId"
:id="elBgId"
:style="{
width: widthPx + 'px',
height: widthPx + 'px'
}"
></canvas>
<!-- 进度圆环 -->
<canvas
class="tn-circle-progress__canvas"
:canvas-id="elId"
:id="elId"
:style="{
width: widthPx + 'px',
height: widthPx + 'px'
}"
></canvas>
<view class="tn-circle-progress__content">
<slot v-if="$slots.default || $slots.$default"></slot>
<view v-else-if="showPercent" class="tn-circle-progress__content__percent">{{ percent + '%' }}</view>
</view>
</view>
</template>
<script>
export default {
name: 'tn-circle-progress',
props: {
// 进度(百分比)
percent: {
type: Number,
default: 0,
validator: val => {
return val >= 0 && val <= 100
}
},
// 圆环线宽
borderWidth: {
type: Number,
default: 14
},
// 整体圆的宽度
width: {
type: Number,
default: 200
},
// 是否显示条纹
striped: {
type: Boolean,
default: false
},
// 条纹是否运动
stripedActive: {
type: Boolean,
default: true
},
// 激活部分颜色
activeColor: {
type: String,
default: '#01BEFF'
},
// 非激活部分颜色
inactiveColor: {
type: String,
default: '#f0f0f0'
},
// 是否显示进度条内部百分比值
showPercent: {
type: Boolean,
default: false
},
// 圆环执行动画的时间,ms
duration: {
type: Number,
default: 1500
}
},
data() {
return {
// 微信小程序中不能使用this.$t.uuid()形式动态生成id值,否则会报错
// #ifdef MP-WEIXIN
elBgId: 'tCircleProgressBgId',
elId: 'tCircleProgressElId',
// #endif
// #ifndef MP-WEIXIN
elBgId: this.$t.uuid(),
elId: this.$t.uuid(),
// #endif
// 活动圆上下文
progressContext: null,
// 转换成px为单位的背景宽度
widthPx: uni.upx2px(this.width || 200),
// 转换成px为单位的圆环宽度
borderWidthPx: uni.upx2px(this.borderWidth || 14),
// canvas画圆的起始角度,默认为-90度,顺时针
startAngle: -90 * Math.PI / 180,
// 动态修改进度值的时候,保存进度值的变化前后值
newPercent: 0,
oldPercent: 0
}
},
watch: {
percent(newVal, oldVal = 0) {
if (newVal > 100) newVal = 100
if (oldVal < 0) oldVal = 0
this.newPercent = newVal
this.oldPercent = oldVal
setTimeout(() => {
// 无论是百分比值增加还是减少,需要操作还是原来的旧的百分比值
// 将此值减少或者新增到新的百分比值
this.drawCircleByProgress(oldVal)
}, 50)
}
},
created() {
// 赋值,用于加载后第一个画圆使用
this.newPercent = this.percent;
this.oldPercent = 0;
},
mounted() {
setTimeout(() => {
this.drawProgressBg()
this.drawCircleByProgress(this.oldPercent)
}, 50)
},
methods: {
// 绘制进度条背景
drawProgressBg() {
let ctx = uni.createCanvasContext(this.elBgId, this)
// 设置线宽
ctx.setLineWidth(this.borderWidthPx)
// 设置颜色
ctx.setStrokeStyle(this.inactiveColor)
ctx.beginPath()
let radius = this.widthPx / 2
ctx.arc(radius, radius, radius - this.borderWidthPx, 0, 360 * Math.PI / 180, false)
ctx.stroke()
ctx.draw()
},
// 绘制圆弧的进度
drawCircleByProgress(progress) {
// 如果已经存在则拿来使用
let ctx = this.progressContext
if (!ctx) {
ctx =uni.createCanvasContext(this.elId, this)
this.progressContext = ctx
}
ctx.setLineCap('round')
// 设置线条宽度和颜色
ctx.setLineWidth(this.borderWidthPx)
ctx.setStrokeStyle(this.activeColor)
// 将总过渡时间除以100,得出每修改百分之一进度所需的时间
let preSecondTime = Math.floor(this.duration / 100)
// 结束角的计算依据为:将2π分为100份,乘以当前的进度值,得出终止点的弧度值,加起始角,为整个圆从默认的
let endAngle = ((360 * Math.PI / 180) / 100) * progress + this.startAngle
let radius = this.widthPx / 2
ctx.beginPath()
ctx.arc(radius, radius, radius - this.borderWidthPx, this.startAngle, endAngle, false)
ctx.stroke()
ctx.draw()
// 如果变更后新值大于旧值,意味着增大了百分比
if (this.newPercent > this.oldPercent) {
// 每次递增百分之一
progress++
// 如果新增后的值,大于需要设置的值百分比值,停止继续增加
if (progress > this.newPercent) return
} else {
progress--
if (progress < this.newPercent) return
}
setTimeout(() => {
// 定时器,每次操作间隔为time值,为了让进度条有动画效果
this.drawCircleByProgress(progress)
}, preSecondTime)
}
}
}
</script>
<style lang="scss" scoped>
.tn-circle-progress {
position: relative;
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
align-items: center;
justify-content: center;
background-color: transparent;
&__canvas {
position: absolute;
&-bg {
position: absolute;
}
}
&__content {
display: flex;
align-items: center;
justify-content: center;
&__percent {
font-size: 28rpx;
}
}
}
</style>
@@ -0,0 +1,232 @@
<template>
<view class="tn-collapse-item-class tn-collapse-item" :style="[itemStyle]">
<!-- 头部 -->
<view
class="tn-collapse-item__head"
:style="[headStyle]"
:hover-stay-time="200"
:hover-class="hoverClass"
@tap.stop="headClick"
>
<block v-if="!$slots['title-all'] || !$slots['$title-all']">
<view
v-if="!$slots.title || !$slots.$title"
class="tn-collapse-item__head__title tn-text-ellipsis"
:style="[
{ textAlign: align ? align : 'left'},
isShow && activeStyle && !arrow ? activeStyle : ''
]"
>{{ title }}</view>
<view v-else>
<slot name="title"></slot>
</view>
<view class="tn-collapse-item__head__icon__wrap">
<view
v-if="arrow"
class="tn-icon-down tn-collapse-item__head__icon__arrow"
:class="{'tn-collapse-item__head__icon__arrow--active': isShow}"
:style="[arrowIconStyle]"
></view>
</view>
</block>
<view v-else>
<slot name="title-all"></slot>
</view>
</view>
<!-- 内容 -->
<view
class="tn-collapse-item__body"
:style="[{
height: isShow ? height + 'px' : '0'
}]"
>
<view class="tn-collapse-item__body__content" :id="elId" :style="[bodyStyle]">
<slot></slot>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'tn-collapse-item',
props: {
// 展开
open: {
type: Boolean,
default: false
},
// 唯一标识
name: {
type: String,
default: ''
},
// 标题
title: {
type: String,
default: ''
},
// 标题对齐方式
align: {
type: String,
default: 'left'
},
// 点击不收起
disabled: {
type: Boolean,
default: false
},
// 活动时样式
activeStyle: {
type: Object,
default() {
return {}
}
},
// 标识
index: {
type: [Number, String],
default: ''
}
},
computed: {
arrowIconStyle() {
let style = {}
if (this.arrowColor) {
style.color = this.arrowColor
}
return style
}
},
data() {
return {
isShow: false,
elId: this.$t.uuid(),
// body高度
height: 0,
// 头部样式
headStyle: {},
// 主体样式
bodyStyle: {},
// item样式
itemStyle: {},
// 显示右边箭头
arrow: true,
// 箭头颜色
arrowColor: '',
// 点击头部时的效果样式
hoverClass: ''
}
},
watch: {
open(value) {
this.isShow = value
}
},
created() {
this.parent = false
this.isShow = this.open
},
mounted() {
this.init()
},
methods: {
// 异步获取内容或者修改了内容时重新获取内容的信息
init() {
this.parent = this.$t.$parent.call(this, 'tn-collapse')
if (this.parent) {
this.nameSync = this.name ? this.name : this.parent.childrens.length
// 不存在才添加对应实例
!this.parent.childrens.includes(this) && this.parent.childrens.push(this)
this.headStyle = this.parent.headStyle
this.bodyStyle = this.parent.bodyStyle
this.itemStyle = this.parent.itemStyle
this.arrow = this.parent.arrow
this.arrowColor = this.parent.arrowColor
this.hoverClass = this.parent.hoverClass
}
this.$nextTick(() => {
this.queryRect()
})
},
// 点击头部
headClick() {
if (this.disabled) return
if (this.parent && this.parent.accordion) {
this.parent.childrens.map(child => {
// 如果是手风琴模式,将其他的item关闭
if (this !== child) {
child.isShow = false
}
})
}
this.isShow = !this.isShow
// 触发修改事件
this.$emit('change', {
index: this.index,
show: this.isShow
})
// 只有在打开时才触发父元素的change
if (this.isShow) this.parent && this.parent.onChange()
this.$forceUpdate()
},
// 查询内容高度
queryRect() {
this._tGetRect('#'+this.elId).then(res => {
this.height = res.height
})
}
}
}
</script>
<style lang="scss" scoped>
.tn-collapse-item {
&__head {
position: relative;
display: flex;
flex-direction: row;
justify-content: space-around;
align-items: center;
color: $tn-font-color;
font-size: 30rpx;
line-height: 1;
padding: 24rpx 0;
text-align: left;
&__title {
flex: 1;
overflow: hidden;
}
&__icon {
&__arrow {
transition: all 0.3s;
margin-right: 20rpx;
margin-left: 14rpx;
font-size: inherit;
&--active {
transform: rotate(180deg);
transform-origin: center center;
}
}
}
}
&__body {
transition: all 0.3s;
overflow: hidden;
&__content {
overflow: hidden;
font-size: 28rpx;
color: $tn-font-color;
text-align: left;
}
}
}
</style>
@@ -0,0 +1,98 @@
<template>
<view class="tn-collapse-class tn-collapse">
<slot></slot>
</view>
</template>
<script>
export default {
name: 'tn-collapse',
props: {
// 是否为手风琴
accordion: {
type: Boolean,
default: true
},
// 头部样式
headStyle: {
type: Object,
default() {
return {}
}
},
// 主题样式
bodyStyle: {
type: Object,
default() {
return {}
}
},
// 每一个item的样式
itemStyle: {
type: Object,
default() {
return {}
}
},
// 显示箭头
arrow: {
type: Boolean,
default: true
},
// 箭头颜色
arrowColor: {
type: String,
default: '#AAAAAA'
},
// 点击标题栏时的按压样式
hoverClass: {
type: String,
default: 'tn-hover'
}
},
computed: {
parentData() {
return [this.headStyle, this.bodyStyle, this.itemStyle, this.arrow, this.arrowColor, this.hoverClass]
}
},
data() {
return {
}
},
watch: {
parentData() {
// 如果父组件的参数发生变化重新初始化子组件的信息
if (this.childrens.length > 0) {
this.init()
}
}
},
created() {
this.childrens = []
},
methods: {
// 重新初始化内部所有子元素计算高度,异步获取数据时重新渲染
init() {
this.childrens.forEach((child, index) => {
child.init()
})
},
// collapseItem被点击时由collapseItem调用父组件
onChange() {
let activeItem = []
this.childrens.forEach((child, index) => {
if (child.isShow) {
activeItem.push(child.nameSync)
}
})
// 如果时手风琴模式,只有一个匹配结果,即activeItem长度为1
if (this.accordion) activeItem = activeItem.join(',')
this.$emit('change', activeItem)
}
}
}
</script>
<style lang="scss" scoped>
</style>
@@ -0,0 +1,318 @@
<template>
<text
class="tn-color-icon-class tn-color-icon"
:class="[
'tn-color-icon-' + name
]"
:style="{
fontSize: size + unit,
margin: margin
}"
@tap="handleClick"
></text>
</template>
<script>
export default {
name: 'tn-color-icon',
props: {
// 索引
index: {
type: [Number, String],
default: '0'
},
// 图标名称
name: {
type: String,
default: ''
},
// 图标大小
size: {
type: Number,
default:32
},
// 大小单位
unit: {
type: String,
default: 'px'
},
// 外边距
margin: {
type: String,
default: '0'
}
},
computed: {
},
data() {
return {
}
},
methods: {
// 处理点击事件
handleClick() {
this.$emit("click", {
index: Number(this.index)
})
this.$emit("tap", {
index: Number(this.index)
})
}
}
}
</script>
<style scoped>
@charset "UTF-8";
@font-face {
font-family: "tuniaoColorFont"; /* Project id 2445412 */
/* Color fonts */
src: url('iconfont.woff2?t=1632654518618') format('woff2');
}
.tn-color-icon {
font-family: "tuniaoColorFont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
text-decoration: none;
}
.tn-color-icon-logo-github:before {
content: "\e601";
}
.tn-color-icon-logo-qq:before {
content: "\e602";
}
.tn-color-icon-logo-weixin:before {
content: "\e603";
}
.tn-color-icon-logo-alipay:before {
content: "\e604";
}
.tn-color-icon-logo-weibo:before {
content: "\e605";
}
.tn-color-icon-logo-dingtalk:before {
content: "\e606";
}
.tn-color-icon-safe:before {
content: "\e607";
}
.tn-color-icon-wifi:before {
content: "\e608";
}
.tn-color-icon-help:before {
content: "\e609";
}
.tn-color-icon-tag:before {
content: "\e60a";
}
.tn-color-icon-play:before {
content: "\e60b";
}
.tn-color-icon-stopwatch:before {
content: "\e60c";
}
.tn-color-icon-home:before {
content: "\e60d";
}
.tn-color-icon-map:before {
content: "\e60e";
}
.tn-color-icon-book:before {
content: "\e60f";
}
.tn-color-icon-qrcode:before {
content: "\e610";
}
.tn-color-icon-discover:before {
content: "\e611";
}
.tn-color-icon-visitor:before {
content: "\e612";
}
.tn-color-icon-menu:before {
content: "\e613";
}
.tn-color-icon-renew:before {
content: "\e614";
}
.tn-color-icon-business:before {
content: "\e615";
}
.tn-color-icon-telephone:before {
content: "\e616";
}
.tn-color-icon-medicine:before {
content: "\e617";
}
.tn-color-icon-chicken:before {
content: "\e618";
}
.tn-color-icon-clock:before {
content: "\e619";
}
.tn-color-icon-download:before {
content: "\e61a";
}
.tn-color-icon-lamp:before {
content: "\e61b";
}
.tn-color-icon-hourglass:before {
content: "\e61c";
}
.tn-color-icon-calendar:before {
content: "\e61d";
}
.tn-color-icon-bluetooth:before {
content: "\e61e";
}
.tn-color-icon-fish:before {
content: "\e61f";
}
.tn-color-icon-seal:before {
content: "\e620";
}
.tn-color-icon-remind:before {
content: "\e621";
}
.tn-color-icon-music:before {
content: "\e622";
}
.tn-color-icon-email:before {
content: "\e623";
}
.tn-color-icon-medal:before {
content: "\e624";
}
.tn-color-icon-image:before {
content: "\e625";
}
.tn-color-icon-network:before {
content: "\e626";
}
.tn-color-icon-wallet:before {
content: "\e627";
}
.tn-color-icon-program:before {
content: "\e628";
}
.tn-color-icon-shrimp:before {
content: "\e629";
}
.tn-color-icon-collect:before {
content: "\e62a";
}
.tn-color-icon-screw:before {
content: "\e62b";
}
.tn-color-icon-set:before {
content: "\e62c";
}
.tn-color-icon-userfavorite:before {
content: "\e62d";
}
.tn-color-icon-useradd:before {
content: "\e62e";
}
.tn-color-icon-honor:before {
content: "\e62f";
}
.tn-color-icon-shop:before {
content: "\e630";
}
.tn-color-icon-usercard:before {
content: "\e631";
}
.tn-color-icon-school:before {
content: "\e632";
}
.tn-color-icon-user:before {
content: "\e633";
}
.tn-color-icon-internet:before {
content: "\e634";
}
.tn-color-icon-time:before {
content: "\e635";
}
.tn-color-icon-topic:before {
content: "\e636";
}
.tn-color-icon-phone:before {
content: "\e637";
}
.tn-color-icon-usertable:before {
content: "\e638";
}
.tn-color-icon-userset:before {
content: "\e639";
}
.tn-color-icon-game:before {
content: "\e63a";
}
</style>
@@ -0,0 +1,251 @@
<template>
<view
class="tn-column-notice-class tn-column-notice"
:class="[backgroundColorClass]"
:style="[noticeStyle]"
>
<!-- 左图标 -->
<view class="tn-column-notice__icon">
<view
v-if="leftIcon"
class="tn-column-notice__icon--left"
:class="[`tn-icon-${leftIconName}`,fontColorClass]"
:style="[fontStyle('leftIcon')]"
@tap="clickLeftIcon"></view>
</view>
<!-- 滚动显示内容 -->
<swiper class="tn-column-notice__swiper" :style="[swiperStyle]" :vertical="vertical" circular :autoplay="autoplay && playStatus === 'play'" :interval="duration" @change="change">
<swiper-item v-for="(item, index) in list" :key="index" class="tn-column-notice__swiper--item">
<view
class="tn-column-notice__swiper--content tn-text-ellipsis"
:class="[fontColorClass]"
:style="[fontStyle()]"
@tap="click(index)"
>{{ item }}</view>
</swiper-item>
</swiper>
<!-- 右图标 -->
<view class="tn-column-notice__icon">
<view
v-if="rightIcon"
class="tn-column-notice__icon--right"
:class="[`tn-icon-${rightIconName}`,fontColorClass]"
:style="[fontStyle('rightIcon')]"
@tap="clickRightIcon"></view>
<view
v-if="closeBtn"
class="tn-column-notice__icon--right"
:class="[`tn-icon-close`,fontColorClass]"
:style="[fontStyle('close')]"
@tap="close"></view>
</view>
</view>
</template>
<script>
import componentsColorMixin from '../../libs/mixin/components_color.js'
export default {
name: 'tn-column-notice',
mixins: [componentsColorMixin],
props: {
// 显示的内容
list: {
type: Array,
default() {
return []
}
},
// 是否显示
show: {
type: Boolean,
default: true
},
// 播放状态
// play -> 播放 paused -> 暂停
playStatus: {
type: String,
default: 'play'
},
// 滚动方向
// horizontal -> 水平滚动 vertical -> 垂直滚动
mode: {
type: String,
default: 'horizontal'
},
// 是否显示左边图标
leftIcon: {
type: Boolean,
default: true
},
// 左边图标的名称
leftIconName: {
type: String,
default: 'sound'
},
// 左边图标的大小
leftIconSize: {
type: Number,
default: 34
},
// 是否显示右边的图标
rightIcon: {
type: Boolean,
default: false
},
// 右边图标的名称
rightIconName: {
type: String,
default: 'right'
},
// 右边图标的大小
rightIconSize: {
type: Number,
default: 26
},
// 是否显示关闭按钮
closeBtn: {
type: Boolean,
default: false
},
// 圆角
radius: {
type: Number,
default: 0
},
// 内边距
padding: {
type: String,
default: '18rpx 24rpx'
},
// 自动播放
autoplay: {
type: Boolean,
default: true
},
// 滚动周期
duration: {
type: Number,
default: 2000
}
},
computed: {
fontStyle() {
return (type) => {
let style = {}
style.color = this.fontColorStyle ? this.fontColorStyle : '#080808'
style.fontSize = this.fontSizeStyle ? this.fontSizeStyle : '26rpx'
if (type === 'leftIcon' && this.leftIconSize) {
style.fontSize = this.leftIconSize + 'rpx'
}
if (type === 'rightIcon' && this.rightIconSize) {
style.fontSize = this.rightIconSize + 'rpx'
}
if (type === 'close') {
style.fontSize = '24rpx'
}
return style
}
},
noticeStyle() {
let style = {}
style.backgroundColor = this.backgroundColorStyle ? this.backgroundColorStyle : 'transparent'
if (this.padding) style.padding = this.padding
return style
},
swiperStyle() {
let style = {}
style.height = this.fontSize ? this.fontSize + 6 + this.fontUnit : '32rpx'
style.lineHeight = style.height
return style
},
// 标记是否为垂直
vertical() {
if (this.mode === 'horizontal') return false
else return true
}
},
data() {
return {
}
},
watch: {
},
methods: {
// 点击了通知栏
click(index) {
this.$emit('click', index)
},
// 点击了关闭按钮
close() {
this.$emit('close')
},
// 点击了左边图标
clickLeftIcon() {
this.$emit('clickLeft')
},
// 点击了右边图标
clickRightIcon() {
this.$emit('clickRight')
},
// 切换消息时间
change(event) {
let index = event.detail.current
if (index === this.list.length - 1) {
this.$emit('end')
}
}
}
}
</script>
<style lang="scss" scoped>
.tn-column-notice {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
flex-wrap: nowrap;
overflow: hidden;
&__swiper {
height: auto;
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
margin-left: 12rpx;
&--item {
display: flex;
flex-direction: row;
align-items: center;
overflow: hidden;
}
&--content {
overflow: hidden;
}
}
&__icon {
&--left {
display: inline-flex;
align-items: center;
}
&--right {
margin-left: 12rpx;
display: inline-flex;
align-items: center;
}
}
}
</style>
@@ -0,0 +1,314 @@
<template>
<view class="tn-countdown-class tn-countdown">
<view
v-if="showDays && (hideZeroDay || (!hideZeroDay && d != '00'))"
class="tn-countdown__item"
:class="[backgroundColorClass]"
:style="[itemStyle]"
>
<view class="tn-countdown__item__time" :class="[fontColorClass]" :style="[letterStyle]">
{{ d }}
</view>
</view>
<view
v-if="showDays && (hideZeroDay || (!hideZeroDay && d != '00'))"
class="tn-countdown__separator"
:style="{
fontSize: separatorSize + 'rpx',
color: separatorColor,
paddingBottom: separator === 'en' ? '4rpx' : 0
}"
>
{{ separator === 'en' ? ':' : '天'}}
</view>
<view
v-if="showHours"
class="tn-countdown__item"
:class="[backgroundColorClass]"
:style="[itemStyle]"
>
<view class="tn-countdown__item__time" :class="[fontColorClass]" :style="[letterStyle]">
{{ h }}
</view>
</view>
<view
v-if="showHours"
class="tn-countdown__separator"
:style="{
fontSize: separatorSize + 'rpx',
color: separatorColor,
paddingBottom: separator === 'en' ? '4rpx' : 0
}"
>
{{ separator === 'en' ? ':' : '时'}}
</view>
<view
v-if="showMinutes"
class="tn-countdown__item"
:class="[backgroundColorClass]"
:style="[itemStyle]"
>
<view class="tn-countdown__item__time" :class="[fontColorClass]" :style="[letterStyle]">
{{ m }}
</view>
</view>
<view
v-if="showMinutes"
class="tn-countdown__separator"
:style="{
fontSize: separatorSize + 'rpx',
color: separatorColor,
paddingBottom: separator === 'en' ? '4rpx' : 0
}"
>
{{ separator === 'en' ? ':' : '分'}}
</view>
<view
v-if="showSeconds"
class="tn-countdown__item"
:class="[backgroundColorClass]"
:style="[itemStyle]"
>
<view class="tn-countdown__item__time" :class="[fontColorClass]" :style="[letterStyle]">
{{ s }}
</view>
</view>
<view
v-if="showSeconds && separator === 'cn'"
class="tn-countdown__separator"
:style="{
fontSize: separatorSize + 'rpx',
color: separatorColor,
paddingBottom: separator === 'en' ? '4rpx' : 0
}"
>
</view>
</view>
</template>
<script>
import componentsColorMixin from '../../libs/mixin/components_color.js'
export default {
name: 'tn-count-down',
mixins: [componentsColorMixin],
props: {
// 倒计时时间,秒作为单位
timestamp: {
type: Number,
default: 0
},
// 是否自动开始
autoplay: {
type: Boolean,
default: true
},
// 数字框高度
height: {
type: [String, Number],
default: 'auto'
},
// 分隔符类型
// en -> 使用英文的冒号 cn -> 使用中文进行分割
separator: {
type: String,
default: 'en'
},
// 分割符大小
separatorSize: {
type: Number,
default: 30
},
// 分隔符颜色
separatorColor: {
type: String,
default: '#080808'
},
// 是否显示边框
showBorder: {
type: Boolean,
default: false
},
// 边框颜色
borderColor: {
type: String,
default: '#080808'
},
// 是否显示秒
showSeconds: {
type: Boolean,
default: true
},
// 是否显示分
showMinutes: {
type: Boolean,
default: true
},
// 是否显示时
showHours: {
type: Boolean,
default: true
},
// 是否显示天
showDays: {
type: Boolean,
default: true
},
// 如果当天的部分为0时,是否隐藏不显示
hideZeroDay: {
type: Boolean,
default: false
}
},
computed: {
// 倒计时item的样式
itemStyle() {
let style = {}
if (this.height) {
style.height = this.$t.string.getLengthUnitValue(this.height)
style.width = style.height
}
if (this.showBorder) {
style.borderStyle = 'solid'
style.borderColor = this.borderColor
style.borderWidth = '1rpx'
}
style.backgroundColor = this.backgroundColorStyle || '#FFFFFF'
return style
},
// 倒计时数字样式
letterStyle() {
let style = {}
style.fontSize = this.fontSizeStyle || '30rpx'
style.color = this.fontColorStyle || '#080808'
return style
}
},
data() {
return {
d: '00',
h: '00',
m: '00',
s: '00',
// 定时器
timer: null,
// 记录倒计过程中变化的秒数
seconds: 0
}
},
watch: {
// 监听时间戳变化
timestamp(value) {
this.clearTimer()
this.start()
}
},
mounted() {
// 如果时自动倒计时,加载完成开始计时
this.autoplay && this.timestamp && this.start()
},
beforeDestroy() {
this.clearTimer()
},
methods: {
// 开始倒计时
start() {
// 避免可能出现的倒计时重叠情况
this.clearTimer()
if (this.timestamp <= 0) return
this.seconds = Number(this.timestamp)
this.formatTime(this.seconds)
this.timer = setInterval(() => {
this.seconds--
// 发出change事件
this.$emit('change', this.seconds)
if (this.seconds < 0) {
return this.end()
}
this.formatTime(this.seconds)
}, 1000)
},
// 格式化时间
formatTime(seconds) {
// 小于等于0的话,结束倒计时
seconds <= 0 && this.end()
let [day, hour, minute, second] = [0, 0, 0, 0]
day = Math.floor(seconds / (60 * 60 * 24))
// 如果不显示天,则将天对应的小时计入到小时中
// 先把当前的hour计算出来供分和秒使用
hour = Math.floor(seconds / (60 * 60)) - (day * 24)
let showHour = null
if (this.showDays) {
showHour = hour
} else {
// 将天数对应的小时加入到时中进行显示
showHour = Math.floor(seconds / (60 * 60))
}
minute = Math.floor(seconds / 60) - (hour * 60) - (day * 24 * 60)
second = Math.floor(seconds) - (minute * 60) - (hour * 60 * 60) - (day * 24 * 60 * 60)
// 如果小于0在前面进行补0操作
showHour = this.$t.number.formatNumberAddZero(showHour)
minute = this.$t.number.formatNumberAddZero(minute)
second = this.$t.number.formatNumberAddZero(second)
day = this.$t.number.formatNumberAddZero(day)
this.d = day
this.h = showHour
this.m = minute
this.s = second
},
// 倒计时结束
end() {
this.clearTimer()
this.$emit('end')
},
// 清除倒计时
clearTimer() {
if (this.timer !== null) {
clearInterval(this.timer)
this.timer = null
}
}
}
}
</script>
<style lang="scss" scoped>
.tn-countdown {
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
align-items: center;
&__item {
box-sizing: content-box;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 2rpx;
border-radius: 6rpx;
white-space: nowrap;
transform: translateZ(0);
&__time {
margin: 0;
padding: 0;
line-height: 1;
}
}
&__separator {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 0 5rpx;
line-height: 1;
}
}
</style>
@@ -0,0 +1,171 @@
<template>
<view class="tn-count-scroll-class tn-count-scroll">
<view
v-for="(item, index) in columns"
:key="index"
class="tn-count-scroll__box"
:style="{
width: $t.string.getLengthUnitValue(width),
height: heightPxValue + 'px'
}"
>
<view
class="tn-count-scroll__column"
:style="{
transform: `translate3d(0, -${keys[index] * heightPxValue}px, 0)`,
transitionDuration: `${duration}s`
}"
>
<view
v-for="(value, value_index) in item"
:key="value_index"
class="tn-count-scroll__column__item"
:class="[fontColorClass]"
:style="{
height: heightPxValue + 'px',
lineHeight: heightPxValue + 'px',
fontSize: fontSizeStyle || '32rpx',
fontWeight: bold ? 'bold': 'normal',
color: fontColorStyle || '#080808'
}"
>
{{ value }}
</view>
</view>
</view>
</view>
</template>
<script>
import componentsColorMixin from '../../libs/mixin/components_color.js'
export default {
name: 'tn-count-scroll',
mixins: [componentsColorMixin],
props: {
value: {
type: Number,
default: 0
},
// 行高
height: {
type: Number,
default: 32
},
// 单个字的宽度
width: {
type: [String, Number],
default: 'auto'
},
// 是否加粗
bold: {
type: Boolean,
default: false
},
// 持续时间
duration: {
type: Number,
default: 1.2
},
// 十分位分割符
decimalSeparator: {
type: String,
default: '.'
},
// 千分位分割符
thousandthsSeparator: {
type: String,
default: ''
}
},
computed: {
heightPxValue() {
return uni.upx2px(this.height || 0)
}
},
data() {
return {
// 每列的数据
columns: [],
// 每列对应值所在的滚动位置
keys: []
}
},
watch: {
value(val) {
this.initColumn(val)
}
},
created() {
// 为了达到一进入就有滚动效果,延迟执行初始化
this.initColumn()
setTimeout(() => {
this.initColumn(this.value)
}, 20)
},
methods: {
// 初始化每一列的数据
initColumn(val) {
val = val + ''
let digit = val.length,
columnArray = [],
rows = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
for (let i = 0; i < digit; i++) {
if (val[i] === this.decimalSeparator || val[i] === this.thousandthsSeparator) {
columnArray.push(val[i])
} else {
columnArray.push(rows)
}
}
this.columns = columnArray
this.roll(val)
},
// 滚动处理
roll(value) {
let valueArray = value.toString().split(''),
lengths = this.columns.length,
indexs = [];
while (valueArray.length) {
let figure = valueArray.pop()
if (figure === this.decimalSeparator || figure === this.thousandthsSeparator) {
indexs.unshift(0)
} else {
indexs.unshift(Number(figure))
}
}
while(indexs.length < lengths) {
indexs.unshift(0)
}
this.keys = indexs
}
}
}
</script>
<style lang="scss" scoped>
.tn-count-scroll {
display: inline-flex;
align-items: center;
justify-content: space-between;
&__box {
overflow: hidden;
}
&__column {
transform: translate3d(0, 0, 0);
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
transition-timing-function: cubic-bezier(0, 1, 0, 1);
&__item {
display: flex;
align-items: center;
justify-content: center;
}
}
}
</style>
@@ -0,0 +1,231 @@
<template>
<view
class="tn-count-num-class tn-count-num"
:class="[fontColorClass]"
:style="{
fontSize: fontSizeStyle || '50rpx',
fontWeight: bold ? 'bold' : 'normal',
color: fontColorStyle || '#080808'
}"
>
{{ displayValue }}
</view>
</template>
<script>
import componentsColorMixin from '../../libs/mixin/components_color.js'
export default {
name: 'tn-count-to',
mixins: [componentsColorMixin],
props: {
// 开始的数值,默认为0
startVal: {
type: Number,
default: 0
},
// 结束目标数值
endVal: {
type: Number,
default: 0,
required: true
},
// 是否自动开始
autoplay: {
type: Boolean,
default: true
},
// 滚动到目标值的持续时间,单位为毫秒
duration: {
type: Number,
default: 2000
},
// 是否在即将结束的时候使用缓慢滚动的效果
useEasing: {
type: Boolean,
default: true
},
// 显示的小数位数
decimals: {
type: Number,
default: 0
},
// 十进制的分割符
decimalSeparator: {
type: String,
default: '.'
},
// 千分位的分隔符
// 类似金额的分割(¥23,321.05中的",")
thousandthsSeparator: {
type: String,
default: ''
},
// 是否显示加粗字体
bold: {
type: Boolean,
default: false
}
},
computed: {
countDown() {
return this.startVal > this.endVal
}
},
data() {
return {
localStartVal: this.startVal,
localDuration: this.duration,
// 显示的数值
displayValue: this.formatNumber(this.startVal),
// 打印的数值
printValue: null,
// 是否暂停
paused: false,
// 开始时间戳
startTime: null,
// 停留时间戳
remainingTime: null,
// 当前时间戳
timestamp: null,
// 上一次的时间戳
lastTime: 0,
rAF: null
}
},
watch: {
startVal() {
this.autoplay && this.start()
},
endVal() {
this.autoplay && this.start()
}
},
mounted() {
this.autoplay && this.start()
},
methods: {
// 开始滚动
start() {
this.localStartVal = this.startVal
this.startTime = null
this.localDuration = this.duration
this.paused = false
this.rAF = this.requestAnimationFrame(this.count)
},
// 重新开始
reStart() {
if (this.paused) {
this.resume()
this.paused = false
} else {
this.stop()
this.paused = true
}
},
// 停止
stop() {
this.cancelAnimationFrame(this.rAF)
},
// 恢复
resume() {
this.startTime = null
this.localDuration = this.remainingTime
this.localStartVal = this.printValue
this.requestAnimationFrame(this.count)
},
// 重置
reset() {
this.startTime = null
this.cnacelAnimationFrame(this.rAF)
this.displayValue = this.formatNumber(this.startVal)
},
// 销毁组件
destroyed() {
this.cancelAnimationFrame(this.rAF)
},
// 累加时间
count(timestamp) {
if (!this.startTime) this.startTime = timestamp
this.timestamp = timestamp
const progress = timestamp - this.startTime
this.remainingTime = this.localDuration - progress
if (this.useEasing) {
if (this.countDown) {
this.printValue = this.localStartVal - this.easingFn(progress, 0, this.localStartVal - this.endVal, this.localDuration)
} {
this.printValue = this.easingFn(progress, this.localStartVal, this.endVal - this.localStartVal, this.localDuration)
}
} else {
if (this.countDown) {
this.printValue = this.localStartVal - (this.localStartVal - this.endVal) * (progress / this.localDuration)
} else {
this.printValue = this.localStartVal + (this.endVal - this.localStartVal) * (progress / this.localDuration)
}
}
if (this.countDown) {
this.printValue = this.printValue < this.endVal ? this.endVal : this.printValue
} else {
this.printValue = this.printValue > this.endVal ? this.endVal : this.printValue
}
this.displayValue = this.formatNumber(this.printValue)
if (progress < this.localDuration) {
this.rAF = this.requestAnimationFrame(this.count)
} else {
this.$emit('end')
}
},
// 缓动时间计算
easingFn(t, b, c, d) {
return (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b
},
// 请求帧动画
requestAnimationFrame(cb) {
const currentTime = new Date().getTime()
// 为了使setTimteout的尽可能的接近每秒60帧的效果
const timeToCall = Math.max(0, 16 - (currentTime - this.lastTime))
const timerId = setTimeout(() => {
cb && cb(currentTime + timeToCall)
}, timeToCall)
this.lastTime = currentTime + timeToCall
return timerId
},
// 清除帧动画
clearAnimationFrame(timerId) {
clearTimeout(timerId)
},
// 格式化数值
formatNumber(number) {
const reg = /(\d+)(\d{3})/
number = Number(number)
number = number.toFixed(Number(this.decimals))
number += ''
const numberArray = number.split('.')
let num1 = numberArray[0]
const num2 = numberArray.length > 1 ? this.decimalSeparator + numberArray[1] : ''
if (this.thousandthsSeparator && !this.isNumber(this.thousandthsSeparator)) {
while(reg.test(num1)) {
num1 = num1.replace(reg, '$1' + this.thousandthsSeparator + '$2')
}
}
return num1 + num2
},
// 判断是否为数字
isNumber(val) {
return !isNaN(parseFloat(val))
}
}
}
</script>
<style lang="scss" scoped>
.tn-count-num {
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
text-align: center;
line-height: 1;
}
</style>
@@ -0,0 +1,457 @@
<template>
<view
class="tn-form-item-class tn-form-item"
:class="{
'tn-border-solid-bottom': elBorderBottom,
'tn-form-item__border-bottom--error': validateState === 'error' && showError('border-bottom')
}"
>
<view
class="tn-form-item__body"
:style="{
flexDirection: elLabelPosition == 'left' ? 'row' : 'column'
}"
>
<!-- 处理微信小程序中设置属性的问题不设置值的时候会变成true -->
<view
class="tn-form-item--left"
:style="{
width: wLabelWidth,
flex: `0 0 ${wLabelWidth}`,
marginBottom: elLabelPosition == 'left' ? 0 : '10rpx'
}"
>
<!-- 块对齐 -->
<view v-if="required || leftIcon || label" class="tn-form-item--left__content"
:style="[leftContentStyle]"
>
<!-- nvue不支持伪元素before -->
<view v-if="leftIcon" class="tn-form-item--left__content__icon">
<view :class="[`tn-icon-${leftIcon}`]" :style="leftIconStyle"></view>
</view>
<!-- <view
class="tn-form-item--left__content__label"
:style="[elLabelStyle, {
'justify-content': elLabelAlign === 'left' ? 'flex-satrt' : elLabelAlign === 'center' ? 'center' : 'flex-end'
}]"
>
{{label}}
</view> -->
<view
class="tn-form-item--left__content__label"
:style="[elLabelStyle]"
>
{{label}}
</view>
<text v-if="required" class="tn-form-item--left__content--required">*</text>
</view>
</view>
<view class="tn-form-item--right tn-flex">
<view class="tn-form-item--right__content">
<view class="tn-form-item--right__content__slot">
<slot></slot>
</view>
<view v-if="$slots.right || rightIcon" class="tn-form-item--right__content__icon tn-flex">
<view v-if="rightIcon" :class="[`tn-icon-${rightIcon}`]" :style="rightIconStyle"></view>
<slot name="right"></slot>
</view>
</view>
</view>
</view>
<view
v-if="validateState === 'error' && showError('message')"
class="tn-form-item__message"
:style="{
paddingLeft: elLabelPosition === 'left' ? elLabelWidth + 'rpx' : '0'
}"
>
{{validateMessage}}
</view>
</view>
</template>
<script>
import Emitter from '../../libs/utils/emitter.js'
import schema from '../../libs/utils/async-validator.js'
// 去除警告信息
schema.warning = function() {}
export default {
mixins: [Emitter],
name: 'tn-form-item',
inject: {
tnForm: {
default() {
return null
}
}
},
props: {
// label提示语
label: {
type: String,
default: ''
},
// 绑定的值
prop: {
type: String,
default: ''
},
// 是否显示表单域的下划线边框
borderBottom: {
type:Boolean,
default: true
},
// label(标签名称)的位置
// left - 左边
// top - 上边
labelPosition: {
type: String,
default: ''
},
// label的宽度
labelWidth: {
type: Number,
default: 0
},
// label的对齐方式
// left - 左对齐
// top - 上对齐
// right - 右对齐
// bottom - 下对齐
labelAlign: {
type: String,
default: ''
},
// label 的样式
labelStyle: {
type: Object,
default() {
return {}
}
},
// 左侧图标
leftIcon: {
type: String,
default: ''
},
// 右侧图标
rightIcon: {
type: String,
default: ''
},
// 左侧图标样式
leftIconStyle: {
type: Object,
default() {
return {}
}
},
// 右侧图标样式
rightIconStyle: {
type: Object,
default() {
return {}
}
},
// 是否显示必填项的*,不做校验用途
required: {
type: Boolean,
default: false
}
},
computed: {
// 处理微信小程序label的宽度
wLabelWidth() {
// 如果用户设置label为空字符串(微信小程序空字符串最终会变成字符串的'true'),意味着要将label的位置宽度设置为auto
return this.elLabelPosition === 'left' ? (this.label === 'true' || this.label === '' ? 'auto' : this.elLabelWidth + 'rpx') : '100%'
},
// 是否显示错误提示
showError() {
return type => {
if (this.errorType.indexOf('none') >= 0) return false
else if (this.errorType.indexOf(type) >= 0) return true
else return false
}
},
// label的宽度(默认值为90)
elLabelWidth() {
return this.labelWidth != 0 ? this.labelWidth : (this.parentData.labelWidth != 0 ? this.parentData.labelWidth : 90)
},
// label的样式
elLabelStyle() {
return Object.keys(this.labelStyle).length ? this.labelStyle : (Object.keys(this.parentData.labelStyle).length ? this.parentData.labelStyle : {})
},
// label显示位置
elLabelPosition() {
return this.labelPosition ? this.labelPosition : (this.parentData.labelPosition ? this.parentData.labelPosition : 'left')
},
// label对齐方式
elLabelAlign() {
return this.labelAlign ? this.labelAlign : (this.parentData.labelAlign ? this.parentData.labelAlign : 'left')
},
// label下划线
elBorderBottom() {
return this.borderBottom !== '' ? this.borderBottom : (this.parentData.borderBottom !== '' ? this.parentData.borderBottom : true)
},
leftContentStyle() {
let style = {}
if (this.elLabelPosition === 'left') {
switch(this.elLabelAlign) {
case 'left':
style.justifyContent = 'flex-start'
break
case 'center':
style.justifyContent = 'center'
break
default:
style.justifyContent = 'flex-end'
break
}
}
return style
}
},
data() {
return {
// 默认值
initialValue: '',
// 是否校验成功
validateState: '',
// 校验失败提示信息
validateMessage: '',
// 错误的提示方式(参考form组件)
errorType: ['message'],
// 当前子组件输入的值
fieldValue: '',
// 父组件的参数
// 由于再computed中无法得知this.parent的变化,所以放在data中
parentData: {
borderBottom: true,
labelWidth: 90,
labelPosition: 'left',
labelAlign: 'left',
labelStyle: {},
}
}
},
watch: {
validateState(val) {
this.broadcastInputError()
},
"tnForm.errorType"(val) {
this.errorType = val
this.broadcastInputError()
}
},
mounted() {
// 组件创建完成后,保存当前实例到form组件中
// 支付宝、头条小程序不支持provide/inject,所以使用这个方法获取整个父组件,在created定义,避免循环应用\
this.parent = this.$t.$parent.call(this, 'tn-form')
if (this.parent) {
// 遍历parentData属性,将parent中同名的属性赋值给parentData
Object.keys(this.parentData).map(key => {
this.parentData[key] = this.parent[key]
})
// 如果没有传入prop或者tnForm为空(单独使用form-item组件的时候),就不进行校验
if (this.prop) {
// 将本实例添加到父组件中
this.parent.fields.push(this)
this.errorType = this.parent.errorType
// 设置初始值
this.initialValue = this.fieldValue
// 添加表单校验,这里必须要写在$nextTick中,因为tn-form的rules是通过ref手动传入的
// 不在$nextTick中的话,可能会造成执行此处代码时,父组件还没通过ref把规则给tn-form,导致规则为空
this.$nextTick(() => {
this.setRules()
})
}
}
},
beforeDestroy() {
// 组件销毁前,将实例从tn-form的缓存中移除
// 如果当前没有prop的话表示当前不进行删除
if (this.parent && this.prop) {
this.parent.fields.map((item, index) => {
if (item === this) this.parent.fields.splice(index, 1)
})
}
},
methods: {
// 向input组件发出错误事件
broadcastInputError() {
this.broadcast('tn-input', 'on-form-item-error', this.validateState === 'error' && this.showError('border'))
},
// 设置校验规则
setRules() {
let that = this
// 从父组件tn-form拿到当前tn-form-item需要验证 的规则
// let rules = this.getRules()
// if (rules.length) {
// this.isRequired = rules.some(rule => {
// // 如果有必填项,就返回,没有的话,就是undefined
// return rule.required
// })
// }
// blur事件
this.$on('on-form-blur', that.onFieldBlur)
// change事件
this.$on('on-form-change', that.onFieldChange)
},
// 从form的rules属性中取出当前form-item的校验规则
getRules() {
let rules = this.parent.rules
rules = rules ? rules[this.prop] : []
// 返回数值形式的值
return [].concat(rules || [])
},
// blur事件时进行表单认证
onFieldBlur() {
this.validation('blur')
},
// change事件时进行表单认证
onFieldChange() {
this.validation('change')
},
// 过滤出符合要求的rule规则
getFilterRule(triggerType = '') {
let rules = this.getRules()
// 整体验证表单时,triggerType为空字符串,此时返回所有规则进行验证
if (!triggerType) return rules
// 某些场景可能的判断规则,可能不存在trigger属性,故先判断是否存在此属性
// 历遍判断规则是否有对应的事件,比如blur,change触发等的事件
// 使用indexOf判断,是因为某些时候设置的验证规则的trigger属性可能为多个,比如['blur','change']
return rules.filter(rule => rule.trigger && rule.trigger.indexOf(triggerType) !== -1)
},
// 校验数据
validation(trigger, callback = ()=>{}) {
// 校验之前先获取需要校验的值
this.fieldValue = this.parent.model[this.prop]
// blur和唱歌是否有当前方式的校验规则
let rules = this.getFilterRule(trigger)
// 判断是否有验证规则,如果没有规则,也调用回调方法,否则父组件tn-form会因为
// 对count变量的统计错误而无法进入上一层的回调
if (!rules || rules.length === 0) {
return callback('')
}
// 设置当前为校验中
this.validateState = 'validating'
// 调用async-validator的方法
let validator = new schema({
[this.prop]: rules
})
validator.validate({
[this.prop]: this.fieldValue
}, {
firstFields: true
}, (errors, fields) => {
// 记录状态和报错信息
this.validateState = !errors ? 'success' : 'error'
this.validateMessage = errors ? errors[0].message : ''
callback(this.validateMessage)
})
},
// 清空当前item信息
resetField() {
this.parent.model[this.prop] = this.initialValue
// 清空错误标记
this.validateState = 'success'
}
}
}
</script>
<style lang="scss" scoped>
.tn-form-item {
display: flex;
flex-direction: column;
padding: 20rpx 0;
font-size: 28rpx;
color: $tn-font-color;
box-sizing: border-box;
line-height: $tn-form-item-height;
&__border-bottom--error:after {
border-color: $tn-color-red;
}
&__body {
display: flex;
flex-direction: row;
}
&--left {
display: flex;
flex-direction: row;
align-items: center;
&__content {
display: flex;
flex-direction: row;
position: relative;
align-items: center;
padding-right: 18rpx;
flex: 1;
&--required {
position: relative;
right: 0;
vertical-align: middle;
color: $tn-color-red;
}
&__icon {
color: $tn-font-sub-color;
margin-right: 8rpx;
}
&__label {
// display: flex;
// flex-direction: row;
// align-items: center;
// flex: 1;
}
}
}
&--right {
flex: 1;
&__content {
display: flex;
flex-direction: row;
align-items: center;
flex: 1;
&__slot {
flex: 1;
/* #ifndef MP */
display: flex;
flex-direction: row;
align-items: center;
/* #endif */
}
&__icon {
margin-left: 10rpx;
color: $tn-font-sub-color;
font-size: 30rpx;
}
}
}
&__message {
font-size: 24rpx;
line-height: 24rpx;
color: $tn-color-red;
margin-top: 12rpx;
}
}
</style>
+139
View File
@@ -0,0 +1,139 @@
<template>
<view class="tn-form-class tn-form">
<slot></slot>
</view>
</template>
<script>
export default {
name: 'tn-form',
props: {
// 表单数据对象(需要验证的表单数据)
model: {
type: Object,
default() {
return {}
}
},
// 发生错误时的提示方式
// toast - 弹出toast框
// message - 提示信息
// border - 如果设置了边框,边框会变成红色
// border-bottom - 下边框会呈现红色
// none - 无提示
errorType: {
type: Array,
default() {
return ['message', 'toast']
}
},
// 是否显示表单域的下划线边框
borderBottom: {
type:Boolean,
default: true
},
// label(标签名称)的位置
// left - 左边
// top - 上边
labelPosition: {
type: String,
default: 'left'
},
// label的宽度
labelWidth: {
type: Number,
default: 90
},
// label的对齐方式
// left - 左对齐
// center - 居中对齐
// right - 右对齐
labelAlign: {
type: String,
default: 'left'
},
// label 的样式
labelStyle: {
type: Object,
default() {
return {}
}
}
},
// 向子孙传递数据
provide() {
return {
tnForm: this
}
},
data() {
return {
rules: {}
}
},
created() {
// 存储当前form下的所有form-item的实例
// 不能定义再data中,否则小程序会循环引用而报错
this.fields = []
},
methods: {
/**
* 设置规则
*
* @param {Object} rules
*/
setRules(rules) {
this.rules = rules
},
/**
* 清空form-item组件
*/
resetFields() {
this.fields.map(field => {
field.resetField()
})
},
/**
* 校验数据
* @param {Object} callback 校验回调方法
*/
validate(callback) {
return new Promise(resolve => {
// 标记校验是否通过
let valid = true
// 标记是否检查完毕
let count = 0
// 存放错误信息
let errors = []
// 对所有form-item进行校验
this.fields.map(field => {
// 调用对应form-item实例的validation校验方法
field.validation('', error => {
// 如果有一个form-item校验不通过,则整个表单校验不通过
if (error) {
valid = false
errors.push(error)
}
// 当遍历完所有的form-item的校验规则,返回信息
if (++count === this.fields.length) {
resolve(valid)
// 判断是否设置了toast的提示方式,只提示表单域中最前面的一条错误信息
if (this.errorType.indexOf('none') === -1 &&
this.errorType.indexOf('toast') >= 0 &&
errors.length > 0) {
this.$t.messageUtils.toast(errors[0])
}
// 调用回调方法
if (typeof callback == 'function') callback(valid)
}
})
})
})
}
}
}
</script>
<style>
</style>
@@ -0,0 +1,114 @@
<template>
<view
class="tn-grid-item-class tn-grid-item"
:class="[
backgroundColorClass
]"
:hover-class="hoverClass"
:hover-stay-time="150"
:style="{
backgroundColor: backgroundColorStyle,
width: gridWidth
}"
@tap="click"
>
<view
class="tn-grid-item__box"
>
<slot></slot>
</view>
</view>
</template>
<script>
import componentsColorMixin from '../../libs/mixin/components_color.js'
export default {
mixins: [ componentsColorMixin ],
name: 'tn-grid-item',
props: {
// 序号
index: {
type: [Number, String],
default: ''
}
},
data() {
return {
// 父组件数据
parentData: {
// 按下去的样式
hoverClass: '',
col: 3
}
}
},
created() {
// 父组件实例
this.updateParentData()
this.parent.children.push(this)
},
computed: {
// 计算每个宫格的宽度
gridWidth() {
// #ifdef MP-WEIXIN
return '100%'
// #endif
// #ifndef MP-WEIXIN
return 100 / Number(this.parentData.col) + '%'
// #endif
},
// 点击效果
hoverClass() {
return this.parentData.hoverClass !== 'none'
? this.parentData.hoverClass + ' tn-grid-item--hover'
: this.parentData.hoverClass
}
},
methods: {
// 获取父组件参数
updateParentData() {
this.getParentData('tn-grid')
},
click() {
this.$emit('click', this.index)
this.parent && this.parent.click(this.index)
}
}
}
</script>
<style lang="scss" scoped>
.tn-grid-item {
box-sizing: border-box;
background-color: #FFFFFF;
/* #ifndef APP-NVUE */
display: flex;
flex-direction: row;
/* #endif */
align-items: center;
justify-content: center;
position: relative;
flex-direction: column;
/* #ifdef MP */
// float: left;
/* #endif */
&__box {
/* #ifndef APP-NVUE */
display: flex;
flex-direction: row;
/* #endif */
align-items: center;
justify-content: center;
flex-direction: column;
flex: 1;
width: 100%;
height: 100%;
}
&--hover {
background: $tn-space-color !important;
}
}
</style>
+111
View File
@@ -0,0 +1,111 @@
<template>
<view
class="tn-grid-class tn-grid"
:style="{
justifyContent: gridAlignStyle
}"
>
<slot></slot>
</view>
</template>
<script>
export default {
name: 'tn-grid',
props: {
// 分成几列
col: {
type: [Number, String],
default: 3
},
// 宫格对齐方式
// left 左对齐 center 居中对齐 right 右对齐
align: {
type: String,
default: 'left'
},
// 点击时的效果,none没有效果
hoverClass: {
type: String,
default: 'tn-hover'
}
},
data() {
return {
}
},
watch: {
// 当父组件和子组件需要共享参数变化,通知子组件
parentData() {
if (this.children.length) {
this.children.map(child => {
// 判断子组件是否有updateParentData方式,有才执行
typeof(child.updateParentData) === 'function' && child.updateParentData()
})
}
}
},
created() {
// 如果将children定义在data中,在微信小程序会造成循环引用而报错
this.children = []
},
computed: {
// 计算父组件的值是否发生变化
parentData() {
return [this.hoverClass, this.col, this.border]
},
// 宫格对齐方式
gridAlignStyle() {
switch(this.align) {
case 'left':
return 'flex-start'
case 'center':
return 'center'
case 'right':
return 'flex-end'
default:
return 'flex-start'
}
}
},
methods: {
click(index) {
this.$emit('click', index)
}
}
}
</script>
<style lang="scss" scoped>
// 组件中兼容小程序的方式,不过不能使用对齐方式
// .tn-grid {
// width: 100%;
// /* #ifdef MP */
// position: relative;
// box-sizing: border-box;
// overflow: hidden;
// /* #endif */
// /* #ifndef MP */
// /* #ifndef APP-NVUE */
// display: flex;
// flex-direction: row;
// /* #endif */
// flex-wrap: wrap;
// align-items: center;
// /* #endif */
// }
// 在使用组件时兼容小程序
.tn-grid {
width: 100%;
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
}
</style>
@@ -0,0 +1,644 @@
<template>
<view v-if="!disabled" class="tn-image-upload-class tn-image-upload">
<view
v-if="showUploadList"
v-for="(item, index) in lists"
:key="index"
class="tn-image-upload__item tn-image-upload__item-preview"
:style="{
width: $t.string.getLengthUnitValue(width),
height: $t.string.getLengthUnitValue(height)
}"
>
<!-- 删除按钮 -->
<view
v-if="deleteable"
class="tn-image-upload__item-preview__delete"
@tap.stop="deleteItem(index)"
:style="{
borderTopColor: deleteBackgroundColor
}"
>
<view
class="tn-image-upload__item-preview__delete--icon"
:class="[`tn-icon-${deleteIcon}`]"
:style="{
color: deleteColor
}"
></view>
</view>
<!-- 进度条 -->
<tn-line-progress
v-if="showProgress && item.progress > 0 && !item.error"
class="tn-image-upload__item-preview__progress"
:percent="item.progress"
:showPercent="false"
:round="false"
:height="8"
></tn-line-progress>
<!-- 重试按钮 -->
<view v-if="item.error" class="tn-image-upload__item-preview__error-btn" @tap.stop="retry(index)">点击重试</view>
<!-- 图片信息 -->
<image
class="tn-image-upload__item-preview__image"
:src="item.url || item.path"
:mode="imageMode"
@tap.stop="doPreviewImage(item.url || item.path, index)"
></image>
</view>
<!-- <view v-if="$slots.file || $slots.$file" style="width: 100%;">
</view> -->
<!-- 自定义图片展示列表 -->
<slot name="file" :file="lists"></slot>
<!-- 添加按钮 -->
<view v-if="maxCount > lists.length" class="tn-image-upload__add" :class="{'tn-image-upload__add--custom': customBtn}" @tap="selectFile">
<!-- 添加按钮 -->
<view
v-if="!customBtn"
class="tn-image-upload__item tn-image-upload__item-add"
hover-class="tn-hover-class"
hover-stay-time="150"
:style="{
width: $t.string.getLengthUnitValue(width),
height: $t.string.getLengthUnitValue(height)
}"
>
<view class="tn-image-upload__item-add--icon tn-icon-add"></view>
<view class="tn-image-upload__item-add__tips">{{ uploadText }}</view>
</view>
<!-- 自定义添加按钮 -->
<view>
<slot name="addBtn"></slot>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'tn-image-upload',
props: {
// 已上传的文件列表
fileList: {
type: Array,
default() {
return []
}
},
// 上传图片地址
action: {
type: String,
default: ''
},
// 上传文件的字段名称
name: {
type: String,
default: 'file'
},
// 头部信息
header: {
type: Object,
default() {
return {}
}
},
// 携带的参数
formData: {
type: Object,
default() {
return {}
}
},
// 是否禁用
disabled: {
type: Boolean,
default: false
},
// 是否自动上传
autoUpload: {
type: Boolean,
default: true
},
// 最大上传数量
maxCount: {
type: Number,
default: 9
},
// 是否显示组件自带的图片预览
showUploadList: {
type: Boolean,
default: true
},
// 预览上传图片的裁剪模式
imageMode: {
type: String,
default: 'aspectFill'
},
// 点击图片是否全屏预览
previewFullImage: {
type: Boolean,
default: true
},
// 是否显示进度条
showProgress: {
type: Boolean,
default: true
},
// 是否显示删除按钮
deleteable: {
type: Boolean,
default: true
},
// 删除按钮图标
deleteIcon: {
type: String,
default: 'close'
},
// 删除按钮的背景颜色
deleteBackgroundColor: {
type: String,
default: ''
},
// 删除按钮的颜色
deleteColor: {
type: String,
default: ''
},
// 上传区域提示文字
uploadText: {
type: String,
default: '选择图片'
},
// 显示toast提示
showTips: {
type: Boolean,
default: true
},
// 自定义选择图标按钮
customBtn: {
type: Boolean,
default: false
},
// 预览图片和选择图片区域的宽度
width: {
type: Number,
default: 200
},
// 预览图片和选择图片区域的高度
height: {
type: Number,
default: 200
},
// 选择图片的尺寸
// 参考上传文档 https://uniapp.dcloud.io/api/media/image
sizeType: {
type: Array,
default() {
return ['original', 'compressed']
}
},
// 图片来源
sourceType: {
type: Array,
default() {
return ['album', 'camera']
}
},
// 是否可以多选
multiple: {
type: Boolean,
default: true
},
// 文件大小(byte)
maxSize: {
type: Number,
default: 10 * 1024 * 1024
},
// 允许上传的类型
limitType: {
type: Array,
default() {
return ['png','jpg','jpeg','webp','gif','image']
}
},
// 是否自定转换为json
toJson: {
type: Boolean,
default: true
},
// 上传前钩子函数,每个文件上传前都会执行
beforeUpload: {
type: Function,
default: null
},
// 删除文件前钩子函数
beforeRemove: {
type: Function,
default: null
},
index: {
type: [Number, String],
default: ''
}
},
computed: {
},
data() {
return {
lists: [],
uploading: false
}
},
watch: {
fileList: {
handler(val) {
val.map(value => {
// 首先检查内部是否已经添加过这张图片,因为外部绑定了一个对象给fileList的话(对象引用),进行修改外部fileList时,
// 会触发watch,导致重新把原来的图片再次添加到this.lists
// 数组的some方法意思是,只要数组元素有任意一个元素条件符合,就返回true,而另一个数组的every方法的意思是数组所有元素都符合条件才返回true
let tmp = this.lists.some(listVal => {
return listVal.url === value.url
})
// 如果内部没有这张图片,则添加到内部
!tmp && this.lists.push({ url: value.url, error: false, progress: 100 })
})
},
immediate: true
},
lists(val) {
this.$emit('on-list-change', val, this.index)
}
},
methods: {
// 清除列表
clear() {
this.lists = []
},
// 重新上传队列中上传失败所有文件
reUpload() {
this.uploadFile()
},
// 选择图片
selectFile() {
if (this.disabled) return
const {
name = '',
maxCount,
multiple,
maxSize,
sizeType,
lists,
camera,
compressed,
sourceType
} = this
let chooseFile = null
const newMaxCount = maxCount - lists.length
// 只选择图片的时候使用 chooseImage 来实现
chooseFile = new Promise((resolve, reject) => {
uni.chooseImage({
count: multiple ? (newMaxCount > 9 ? 9 : newMaxCount) : 1,
sourceType,
sizeType,
success: resolve,
fail: reject
})
})
chooseFile.then(res => {
let file = null
let listOldLength = lists.length
res.tempFiles.map((val, index) => {
if (!this.checkFileExt(val)) return
// 是否超出最大限制数量
if (!multiple && index >= 1) return
if (val.size > maxSize) {
this.$emit('on-oversize', val, lists, this.index)
this.showToast('超出可允许文件大小')
} else {
if (maxCount <= lists.length) {
this.$emit('on-exceed', val, list, this.index)
this.showToast('超出最大允许的文件数')
return
}
lists.push({
url: val.path,
progress: 0,
error: false,
file: val
})
}
})
this.$emit('on-choose-complete', this.lists, this.index)
if (this.autoUpload) this.uploadFile(listOldLength)
}).catch(err => {
this.$emit('on-choose-fail', err)
})
},
// 提示用户信息
showToast(message, force = false) {
if (this.showTips || force) {
this.$t.messageUtils.toast(message)
}
},
// 手动上传,通过ref进行调用
upload() {
this.uploadFile()
},
// 对失败图片进行再次上传
retry(index) {
this.lists[index].progress = 0
this.lists[index].error = false
this.lists[index].response = null
this.$t.messageUtils.loading('重新上传')
this.uploadFile(index)
},
// 上传文件
async uploadFile(index = 0) {
if (this.disabled) return
if (this.uploading) return
// 全部上传完成
if (index >= this.lists.length) {
this.$emit('on-uploaded', this.lists, this.index)
return
}
// 检查是否已经全部上传或者上传中
if (this.lists[index].progress === 100) {
this.lists[index].uploadTask = null
if (this.autoUpload) this.uploadFile(index + 1)
return
}
// 执行before-upload钩子
if (this.beforeUpload && typeof(this.beforeUpload) === 'function') {
// 在微信,支付宝等环境(H5正常),会导致父组件定义的函数体中的this变成子组件的this
// 通过bind()方法,绑定父组件的this,让this的this为父组件的上下文
// 因为upload组件可能会被嵌套在其他组件内,比如tn-form,这时this.$parent其实为tn-form的this
// 非页面的this,所以这里需要往上历遍,一直寻找到最顶端的$parent,这里用了this.$u.$parent.call(this)
let beforeResponse = this.beforeUpload.bind(this.$t.$parent.call(this))(index, this.lists)
// 判断是否返回了Promise
if (!!beforeResponse && typeof beforeResponse.then === 'function') {
await beforeResponse.then(res => {
// promise返回成功,不进行操作继续
}).catch(err => {
// 进入catch回调的话,继续下一张
return this.uploadFile(index + 1)
})
} else if (beforeResponse === false) {
// 如果返回flase,继续下一张图片上传
return this.uploadFile(index + 1)
} else {
// 为true的情况,不进行操作
}
}
// 检查上传地址
if (!this.action) {
this.showToast('请配置上传地址', true)
return
}
this.lists[index].error = false
this.uploading = true
// 创建上传对象
const task = uni.uploadFile({
url: this.action,
filePath: this.lists[index].url,
name: this.name,
formData: this.formData,
header: this.header,
success: res => {
// 判断啊是否为json字符串,将其转换为json格式
let data = this.toJson && this.$t.test.jsonString(res.data) ? JSON.parse(res.data) : res.data
if (![200, 201, 204].includes(res.statusCode)) {
this.uploadError(index, data)
} else {
this.lists[index].response = data
this.lists[index].progress = 100
this.lists[index].error = false
this.$emit('on-success', data, index, this.lists, this.index)
}
},
fail: err => {
this.uploadError(index, err)
},
complete: res => {
this.$t.messageUtils.closeLoading()
this.uploading = false
this.uploadFile(index + 1)
this.$emit('on-change', res, index, this.list, this.index)
}
})
this.lists[index].uploadTask = task
task.onProgressUpdate(res => {
if (res.progress > 0) {
this.lists[index].progress = res.progress
this.$emit('on-progress', res, index, this.lists, this.index)
}
})
},
// 上传失败
uploadError(index, err) {
this.lists[index].progress = 0
this.lists[index].error = true
this.lists[index].response = null
this.showToast('上传失败,请重试')
this.$emit('on-error', err, index, this.lists, this.index)
},
// 删除一个图片
deleteItem(index) {
if (!this.deleteable) return
this.$t.messageUtils.modal(
'提示',
'您确定要删除吗?',
async () => {
// 先检查是否有定义before-remove移除前钩子
// 执行before-remove钩子
if (this.beforeRemove && typeof(this.beforeRemove) === 'function') {
let beforeResponse = this.beforeRemove.bind(this.$t.$parent.call(this))(index, this.lists)
// 判断是否返回promise
if (!!beforeResponse && typeof beforeResponse.then === 'function') {
await beforeResponse.then(res => {
// promise返回成功不进行操作
this.handlerDeleteItem(index)
}).catch(err => {
this.showToast('删除操作被中断')
})
} else if (beforeResponse === false) {
this.showToast('删除操作被中断')
} else {
this.handlerDeleteItem(index)
}
} else {
this.handlerDeleteItem(index)
}
}, true)
},
// 移除文件操作
handlerDeleteItem(index) {
// 如果文件正在上传中,终止上传任务
if (this.lists[index].progress < 100 && this.lists[index].progress > 0) {
typeof this.lists[index].uploadTask !== 'undefined' && this.lists[index].uploadTask.abort()
}
this.lists.splice(index, 1)
this.$forceUpdate()
this.$emit('on-remove', index, this.lists, this.index)
this.showToast('删除成功')
},
// 移除文件,通过ref手动形式进行调用
remove(index) {
if (!this.deleteable) return
// 判断索引合法
if (index >= 0 && index < this.lists.length) {
this.lists.splice(index, 1)
}
},
// 预览图片
doPreviewImage(url, index) {
if (!this.previewFullImage) return
const images = this.lists.map(item => item.url || item.path)
uni.previewImage({
urls: images,
current: url,
success: () => {
this.$emit('on-preview', url, this.lists, this.index)
},
fail: () => {
this.showToast('预览图片失败')
}
})
},
// 检查文件后缀是否合法
checkFileExt(file) {
// 是否为合法后缀
let noArrowExt = false
// 后缀名
let fileExt = ''
const reg = /.+\./
// #ifdef H5
fileExt = file.name.replace(reg, '').toLowerCase()
// #endif
// #ifndef H5
fileExt = file.path.replace(reg, '').toLowerCase()
// #endif
noArrowExt = this.limitType.some(ext => {
return ext.toLowerCase() === fileExt
})
if (!noArrowExt) this.showToast(`不支持${fileExt}格式的文件`)
return noArrowExt
}
}
}
</script>
<style lang="scss" scoped>
.tn-image-upload {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
&__item {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
align-items: center;
justify-content: center;
width: 200rpx;
height: 200rpx;
overflow: hidden;
margin: 12rpx;
margin-left: 0;
background-color: $tn-font-holder-color;
position: relative;
border-radius: 10rpx;
&-preview {
border: 1rpx solid $tn-border-solid-color;
&__delete {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
top: 0;
right: 0;
z-index: 10;
border-top: 60rpx solid;
border-left: 60rpx solid transparent;
border-top-color: $tn-color-red;
width: 0rpx;
height: 0rpx;
&--icon {
position: absolute;
top: -50rpx;
right: 6rpx;
color: #FFFFFF;
font-size: 24rpx;
line-height: 1;
}
}
&__progress {
position: absolute;
width: auto;
bottom: 0rpx;
left: 0rpx;
right: 0rpx;
z-index: 9;
/* #ifdef MP-WEIXIN */
display: inline-flex;
/* #endif */
}
&__error-btn {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background-color: $tn-color-red;
color: #FFFFFF;
font-size: 20rpx;
padding: 8rpx 0;
text-align: center;
z-index: 9;
line-height: 1;
}
&__image {
display: block;
width: 100%;
height: 100%;
border-radius: 10rpx;
}
}
&-add {
flex-direction: column;
color: $tn-content-color;
font-size: 26rpx;
&--icon {
font-size: 40rpx;
}
&__tips {
margin-top: 20rpx;
line-height: 40rpx;
}
}
}
&__add {
width: auto;
display: inline-block;
&--custom {
width: 100%;
}
}
}
</style>
@@ -0,0 +1,90 @@
<template>
<!-- 支付宝小程序使用_tGetRect()获取组件的根元素尺寸所以在外面套一个"壳" -->
<view>
<view :id="elId" class="tn-index-anchor__wrap" :style="[wrapperStyle]">
<view class="tn-index-anchor" :class="[active ? 'tn-index-anchor--active' : '']" :style="[customAnchorStyle]">
<view v-if="useSlot">
<slot></slot>
</view>
<block v-else>
<text>{{ index }}</text>
</block>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'tn-index-anchor',
props: {
// 使用自定义内容
useSlot: {
type: Boolean,
default: false
},
// 索引字符
index: {
type: String,
default: ''
},
// 自定义样式
customStyle: {
type: Object,
default() {
return {}
}
}
},
computed: {
customAnchorStyle() {
return Object.assign(this.anchorStyle, this.customStyle)
}
},
data() {
return {
elId: this.$t.uuid(),
// 内容的高度
height: 0,
// 内容的top
top: 0,
// 是否被激活
active: false,
// 样式(父组件外部提供)
wrapperStyle: {},
anchorStyle: {}
}
},
created() {
this.parent = false
},
mounted() {
this.parent = this.$t.$parent.call(this, 'tn-index-list')
if (this.parent) {
this.parent.childrens.push(this)
this.parent.updateData()
}
}
}
</script>
<style lang="scss" scoped>
.tn-index-anchor {
width: 100%;
box-sizing: border-box;
padding: 8rpx 24rpx;
color: $tn-font-color;
font-size: 28rpx;
font-weight: 500;
line-height: 1.2;
background-color: rgb(245, 245, 245);
&--active {
right: 0;
left: 0;
color: $tn-main-color;
background-color: #FFFFFF;
}
}
</style>
@@ -0,0 +1,361 @@
<template>
<!-- 支付宝小程序使用_tGetRect()获取组件的根元素尺寸所以在外面套一个"壳" -->
<view>
<view class="tn-index-list-class tn-index-list">
<slot></slot>
<!-- 侧边栏 -->
<view
v-if="showSidebar"
class="tn-index-list__sidebar"
@touchstart.stop.prevent="onTouchMove"
@touchmove.stop.prevent="onTouchMove"
@touchend.stop.prevent="onTouchStop"
@touchcancel.stop.prevent="onTouchStop"
>
<view
v-for="(item, index) in indexList"
:key="index"
class="tn-index-list__sidebar__item"
:style="{
zIndex: zIndex + 1,
color: activeAnchorIndex === index ? activeColor : ''
}"
>
{{ item }}
</view>
</view>
<!-- 选中弹出框 -->
<view
v-if="touchMove && indexList[touchMoveIndex]"
class="tn-index-list__alert"
:style="{
zIndex: selectAlertZIndex
}"
>
<text>{{ indexList[touchMoveIndex] }}</text>
</view>
</view>
</view>
</template>
<script>
// 生成 A-Z的字母列表
let indexList = function() {
let indexList = []
let charCodeOfA = 'A'.charCodeAt(0)
for (var i = 0; i < 26; i++) {
indexList.push(String.fromCharCode(charCodeOfA + i))
}
return indexList
}
export default {
name: 'tn-index-list',
props: {
// 索引列表
indexList: {
type: Array,
default() {
return indexList()
}
},
// 是否自动吸顶
sticky: {
type: Boolean,
default: true
},
// 自动吸顶时距离顶部的距离,单位px
stickyTop: {
type: Number,
default: 0
},
// 自定义顶栏的高度,单位px
customBarHeight: {
type: Number,
default: 0
},
// 当前滚动的高度
// 由于自定义组件无法获取滚动高度,所以依赖传入
scrollTop: {
type: Number,
default: 0
},
// 选中索引时的颜色
activeColor: {
type: String,
default: '#01BEFF'
},
// 吸顶时的z-index
zIndex: {
type: Number,
default: 0
}
},
computed: {
// 选中索引列表弹出提示框的z-index
selectAlertZIndex() {
return this.$t.zIndex.toast
},
// 吸顶的偏移高度
stickyOffsetTop() {
// #ifdef H5
return this.stickyTop !== '' ? this.stickyTop : 44
// #endif
// #ifndef H5
return this.stickyTop !== '' ? this.stickyTop : 0
// #endif
}
},
data() {
return {
// 当前激活的列表锚点的序号
activeAnchorIndex: 0,
// 显示侧边索引栏
showSidebar: true,
// 标记是否开始触摸移动
touchMove: false,
// 当前触摸移动到对应索引的序号
touchMoveIndex: 0,
// 滚动到对应锚点的序号
scrollToAnchorIndex: 0,
// 侧边栏的信息
sidebar: {
height: 0,
top: 0
},
// 内容区域高度
height: 0,
// 内容区域top
top: 0
}
},
watch: {
scrollTop() {
this.updateData()
}
},
created() {
// 只能在created生命周期定义childrens,如果在data定义,会因为循环引用而报错
this.childrens = []
},
methods: {
// 更新数据
updateData() {
this.timer && clearTimeout(this.timer)
this.timer = setTimeout(() => {
this.showSidebar = !!this.childrens.length
this.getRect().then(() => {
this.onScroll()
})
}, 0)
},
// 获取对应的信息
getRect() {
return Promise.all([
this.getAnchorRect(),
this.getListRect(),
this.getSidebarRect()
])
},
// 获取列表内容子元素信息
getAnchorRect() {
return Promise.all(this.childrens.map((child, index) => {
child._tGetRect('.tn-index-anchor__wrap').then((rect) => {
Object.assign(child, {
height: rect.height,
top: rect.top - this.customBarHeight
})
})
}))
},
// 获取列表信息
getListRect() {
return this._tGetRect('.tn-index-list').then(rect => {
Object.assign(this, {
height: rect.height,
top: rect.top + this.scrollTop
})
})
},
// 获取侧边滚动栏信息
getSidebarRect() {
return this._tGetRect('.tn-index-list__sidebar').then(rect => {
this.sidebar = {
height: rect.height,
top: rect.top
}
})
},
// 滚动事件
onScroll() {
const {
childrens = []
} = this
if (!childrens.length) {
return
}
const {
sticky,
stickyOffsetTop,
zIndex,
scrollTop,
activeColor
} = this
const active = this.getActiveAnchorIndex()
this.activeAnchorIndex = active
if (sticky) {
let isActiveAnchorSticky = false
if (active !== -1) {
isActiveAnchorSticky = childrens[active].top <= 0
}
childrens.forEach((item, index) => {
if (index === active) {
let wrapperStyle = ''
let anchorStyle = {
color: `${activeColor}`
}
if (isActiveAnchorSticky) {
wrapperStyle = {
height: `${childrens[index].height}px`
}
anchorStyle = {
position: 'fixed',
top: `${stickyOffsetTop}px`,
zIndex: `${zIndex ? zIndex : this.$t.zIndex.indexListSticky}`,
color: `${activeColor}`
}
}
item.active = true
item.wrapperStyle = wrapperStyle
item.anchorStyle = anchorStyle
} else if (index === active - 1) {
const currentAnchor = childrens[index]
const currentOffsetTop = currentAnchor.top
const targetOffsetTop = index === childrens.length - 1 ? this.top : childrens[index + 1].top
const parentOffsetHeight = targetOffsetTop - currentOffsetTop
const translateY = parentOffsetHeight - currentAnchor.height
const anchorStyle = {
position: 'relative',
transform: `translate3d(0, ${translateY}px, 0)`,
zIndex: `${zIndex ? zIndex : this.$t.zIndex.indexListSticky}`,
color: `${activeColor}`
}
item.active = false
item.anchorStyle = anchorStyle
} else {
item.active = false
item.wrapperStyle = ''
item.anchorStyle = ''
}
})
}
},
// 触摸移动
onTouchMove(event) {
this.touchMove = true
const sidebarLength = this.childrens.length
const touch = event.touches[0]
const itemHeight = this.sidebar.height / sidebarLength
let clientY = touch.clientY
let index = Math.floor((clientY - this.sidebar.top) / itemHeight)
if (index < 0) {
index = 0
} else if (index > sidebarLength - 1) {
index = sidebarLength - 1
}
this.touchMoveIndex = index
this.scrollToAnchor(index)
},
// 触摸停止
onTouchStop() {
this.touchMove = false
this.scrollToAnchorIndex = null
},
// 获取当前的锚点序号
getActiveAnchorIndex() {
const {
childrens,
sticky
} = this
for (let i = this.childrens.length - 1; i >= 0; i--) {
const preAnchorHeight = i > 0 ? childrens[i - 1].height : 0
const reachTop = sticky ? preAnchorHeight : 0
if (reachTop >= childrens[i].top) {
return i
}
}
return -1
},
// 滚动到对应的锚点
scrollToAnchor(index) {
if (this.scrollToAnchorIndex === index) {
return
}
this.scrollToAnchorIndex = index
const anchor = this.childrens.find(item => item.index === this.indexList[index])
if (anchor) {
const scrollTop = anchor.top + this.scrollTop
this.$emit('select', {
index: anchor.index,
scrollTop: scrollTop
})
uni.pageScrollTo({
duration:0,
scrollTop: scrollTop
})
}
}
}
}
</script>
<style lang="scss" scoped>
.tn-index-list {
position: relative;
&__sidebar {
display: flex;
flex-direction: column;
position: fixed;
top: 50%;
right: 0;
text-align: center;
transform: translateY(-50%);
user-select: none;
z-index: 99;
&__item {
font-weight: 500;
padding: 8rpx 18rpx;
font-size: 22rpx;
line-height: 1;
}
}
&__alert {
display: flex;
flex-direction: row;
position: fixed;
width: 120rpx;
height: 120rpx;
top: 50%;
right: 90rpx;
align-items: center;
justify-content: center;
margin-top: -60rpx;
border-radius: 24rpx;
font-size: 50rpx;
color: #FFFFFF;
background-color: $tn-font-sub-color;
padding: 0;
z-index: 9999999;
text {
line-height: 50rpx;
}
}
}
</style>
+427
View File
@@ -0,0 +1,427 @@
<template>
<view
class="tn-input-class tn-input"
:class="{
'tn-input--border': border,
'tn-input--error': validateState
}"
:style="{
padding: `0 ${border ? 20 : 0}rpx`,
borderColor: borderColor,
textAlign: inputAlign
}"
@tap.stop="inputClick"
>
<textarea
v-if="type === 'textarea'"
class="tn-input__input tn-input__textarea"
:style="[inputStyle]"
:value="defaultValue"
:placeholder="placeholder"
:placeholderStyle="placeholderStyle"
:disabled="disabled || type === 'select'"
:maxlength="maxLength"
:fixed="fixed"
:focus="focus"
:autoHeight="autoHeight"
:selectionStart="elSelectionStart"
:selectionEnd="elSelectionEnd"
:cursorSpacing="cursorSpacing"
:showConfirmBar="showConfirmBar"
@input="handleInput"
@blur="handleBlur"
@focus="onFocus"
@confirm="onConfirm"
/>
<input
v-else
class="tn-input__input"
:type="type === 'password' ? 'text' : type"
:style="[inputStyle]"
:value="defaultValue"
:password="type === 'password' && !showPassword"
:placeholder="placeholder"
:placeholderStyle="placeholderStyle"
:disabled="disabled || type === 'select'"
:maxlength="maxLength"
:focus="focus"
:confirmType="confirmType"
:selectionStart="elSelectionStart"
:selectionEnd="elSelectionEnd"
:cursorSpacing="cursorSpacing"
:showConfirmBar="showConfirmBar"
@input="handleInput"
@blur="handleBlur"
@focus="onFocus"
@confirm="onConfirm"
/>
<!-- 右边的icon -->
<view class="tn-input__right-icon tn-flex tn-flex-col-center">
<!-- 清除按钮 -->
<view
v-if="clearable && value !== '' && focused"
class="tn-input__right-icon__item tn-input__right-icon__clear"
@tap="onClear"
>
<view class="icon tn-icon-close"></view>
</view>
<view
v-else-if="type === 'text' && !focused && showLeftIcon && leftIcon !== ''"
class="tn-input__right-icon__item tn-input__right-icon__clear"
>
<view class="icon" :class="[`tn-icon-${leftIcon}`]"></view>
</view>
<!-- 显示密码按钮 -->
<view
v-if="passwordIcon && type === 'password'"
class="tn-input__right-icon__item tn-input__right-icon__clear"
@tap="showPassword = !showPassword"
>
<view v-if="!showPassword" class="tn-icon-eye-hide"></view>
<view v-else class="icon tn-icon-eye"></view>
</view>
<!-- 可选项箭头 -->
<view
v-if="type === 'select'"
class="tn-input__right-icon__item tn-input__right-icon__select"
:class="{
'tn-input__right-icon__select--reverse': selectOpen
}"
>
<view class="icon tn-icon-up-triangle"></view>
</view>
</view>
</view>
</template>
<script>
import Emitter from '../../libs/utils/emitter.js'
export default {
mixins: [Emitter],
name: 'tn-input',
props: {
value: {
type: [String, Number],
default: ''
},
// 输入框的类型
type: {
type: String,
default: 'text'
},
// 输入框文字对齐方式
inputAlign: {
type: String,
default: 'left'
},
// 文本框为空时显示的信息
placeholder: {
type: String,
default: ''
},
placeholderStyle: {
type: String,
default: 'color: #AAAAAA'
},
// 是否禁用输入框
disabled: {
type: Boolean,
default: false
},
// 可输入文字的最大长度
maxLength: {
type: Number,
default: 255
},
// 输入框高度
height: {
type: Number,
default: 0
},
// 根据内容自动调整高度
autoHeight: {
type: Boolean,
default: true
},
// 键盘右下角显示的文字,仅在text时生效
confirmType: {
type: String,
default: 'done'
},
// 输入框自定义样式
customStyle: {
type: Object,
default() {
return {}
}
},
// 是否固定输入框
fixed: {
type: Boolean,
default: false
},
// 是否自动获取焦点
focus: {
type: Boolean,
default: false
},
// 当type为password时,是否显示右侧密码图标
passwordIcon: {
type: Boolean,
default: true
},
// 当type为 input或者textarea时是否显示边框
border: {
type: Boolean,
default: false
},
// 边框的颜色
borderColor: {
type: String,
default: '#dcdfe6'
},
// 当type为select时,旋转右侧图标,标记当时select是打开还是关闭
selectOpen: {
type: Boolean,
default: false
},
// 是否可清空
clearable: {
type: Boolean,
default: true
},
// 光标与键盘的距离
cursorSpacing: {
type: Number,
default: 0
},
// selectionStart和selectionEnd需要搭配使用,自动聚焦时生效
// 光标起始位置
selectionStart: {
type: Number,
default: -1
},
// 光标结束位置
selectionEnd: {
type: Number,
default: -1
},
// 自动去除两端空格
trim: {
type: Boolean,
default: true
},
// 是否显示键盘上方的完成按钮
showConfirmBar: {
type: Boolean,
default: true
},
// 是否在输入框内最右边显示图标
showLeftIcon: {
type: Boolean,
default: false
},
// 最右边图标的名称
leftIcon: {
type: String,
default: ''
}
},
computed: {
// 输入框样式
inputStyle() {
let style = {}
// 如果没有设置高度,根据不同的类型设置一个默认值
style.minHeight = this.height ? this.height + 'rpx' :
this.type === 'textarea' ? this.textareaHeight + 'rpx' : this.inputHeight + 'rpx'
style = Object.assign(style, this.customStyle)
return style
},
// 光标起始位置
elSelectionStart() {
return String(this.selectionStart)
},
// 光标结束位置
elSelectionEnd() {
return String(this.selectionEnd)
}
},
data() {
return {
// 默认值
defaultValue: this.value,
// 输入框高度
inputHeight: 70,
// textarea的高度
textareaHeight: 100,
// 标记验证的状态
validateState: false,
// 标记是否获取到焦点
focused: false,
// 是否预览密码
showPassword: false,
// 用于头条小程序,判断@input中,前后的值是否发生了变化,因为头条中文下,按下键没有输入内容,也会触发@input事件
lastValue: '',
}
},
watch: {
value(newVal, oldVal) {
this.defaultValue = newVal
// 当值发生变化时,并且type为select时,不会触发input事件
// 模拟input事件
if (newVal !== oldVal && this.type === 'select') {
this.handleInput({
detail: {
value: newVal
}
})
}
}
},
created() {
// 监听form-item发出的错误事件,将输入框变成红色
this.$on("on-form-item-error", this.onFormItemError)
},
methods: {
/**
* input事件
*/
handleInput(event) {
let value = event.detail.value
// 是否需要去掉空格
if (this.trim) value = this.$t.string.trim(value)
// 原生事件
this.$emit('input', value)
// model赋值
this.defaultValue = value
// 过一个生命周期再发送事件给tn-form-item,否则this.$emit('input')更新了父组件的值,但是微信小程序上
// 尚未更新到tn-form-item,导致获取的值为空,从而校验混论
// 这里不能延时时间太短,或者使用this.$nextTick,否则在头条上,会造成混乱
setTimeout(() => {
// 头条小程序由于自身bug,导致中文下,每按下一个键(尚未完成输入),都会触发一次@input,导致错误,这里进行判断处理
// #ifdef MP-TOUTIAO
if (this.$t.string.trim(value) === this.lastValue) return
this.lastValue = value
// #endif
// 发送当前的值到form-item进行校验
this.dispatch('tn-form-item','on-form-change', value)
}, 40)
},
/**
* blur事件
*/
handleBlur(event) {
let value = event.detail.value
// 由于点击清除图标也会触发blur事件,导致图标消失从而无法点击
setTimeout(() => {
this.focused = false
}, 100)
// 原生事件
this.$emit('blur', value)
// 过一个生命周期再发送事件给tn-form-item,否则this.$emit('blur')更新了父组件的值,但是微信小程序上
// 尚未更新到tn-form-item,导致获取的值为空,从而校验混论
// 这里不能延时时间太短,或者使用this.$nextTick,否则在头条上,会造成混乱
setTimeout(() => {
// 头条小程序由于自身bug,导致中文下,每按下一个键(尚未完成输入),都会触发一次@input,导致错误,这里进行判断处理
// #ifdef MP-TOUTIAO
if (this.$t.string.trim(value) === this.lastValue) return
this.lastValue = value
// #endif
// 发送当前的值到form-item进行校验
this.dispatch('tn-form-item','on-form-blur', value)
}, 40)
},
// 处理校验错误
onFormItemError(status) {
this.validateState = status
},
// 聚焦事件
onFocus(event) {
this.focused = true
this.$emit('focus')
},
// 点击确认按钮事件
onConfirm(event) {
this.$emit('confirm', event.detail.value)
},
// 清除事件
onClear(event) {
this.$emit('input', '')
},
// 点击事件
inputClick() {
this.$emit('click')
}
}
}
</script>
<style lang="scss" scoped>
.tn-input {
display: flex;
flex-direction: row;
position: relative;
flex: 1;
&__input {
font-size: 28rpx;
color: $tn-font-color;
flex: 1;
}
&__textarea {
width: auto;
font-size: 28rpx;
color: $tn-font-color;
padding: 10rpx 0;
line-height: normal;
flex: 1;
}
&--border {
border-radius: 6rpx;
border: 2rpx solid $tn-border-solid-color;
}
&--error {
border-color: $tn-color-red !important;
}
&__right-icon {
line-height: 1;
.icon {
color: $tn-font-sub-color;
}
&__item {
margin-left: 10rpx;
}
&__clear {
.icon {
font-size: 32rpx;
}
}
&__select {
transition: transform .4s;
.icon {
font-size: 26rpx;
}
&--reverse {
transform: rotate(-180deg);
}
}
}
}
</style>
@@ -0,0 +1,220 @@
<template>
<view v-if="value" class="tn-keyboard-class tn-keyboard">
<tn-popup
v-model="value"
mode="bottom"
:popup="false"
length="auto"
:mask="mask"
:maskCloseable="maskCloseable"
:safeAreaInsetBottom="safeAreaInsetBottom"
:zIndex="elZIndex"
@close="popupClose"
>
<view>
<slot></slot>
</view>
<!-- 提示信息 -->
<view v-if="tooltip" class="tn-keyboard__tooltip">
<view
v-if="cancelBtn"
class="tn-keyboard__tooltip__item tn-keyboard__tooltip__cancel"
hover-class="tn-keyboard__tooltip__cancel--hover"
:hover-stay-time="150"
@tap="onCancel"
>
{{ cancelBtn ? cancelText : ''}}
</view>
<view v-if="showTips" class="tn-keyboard__tooltip__item tn-keyboard__tooltip__tips">
{{ tips ? tips : mode === 'number' ? '数字键盘' : mode === 'card' ? '身份证键盘' : '车牌号码键盘'}}
</view>
<view
v-if="confirmBtn"
class="tn-keyboard__tooltip__item tn-keyboard__tooltip__confirm"
hover-class="tn-keybord__tooltip__confirm--hover"
:hover-stay-time="150"
@tap="onConfirm"
>
{{ confirmBtn ? confirmText : ''}}
</view>
</view>
<!-- 键盘内容 -->
<block v-if="mode === 'number' || mode === 'card'">
<tn-number-keyboard :mode="mode" :dotEnabled="dotEnabled" :randomEnabled="randomEnabled" @change="change" @backspace="backspaceClick"></tn-number-keyboard>
</block>
<block v-if="mode === 'car'">
<tn-car-keyboard :randomEnabled="randomEnabled" :switchEnMode="switchEnMode" @change="change" @backspace="backspaceClick"></tn-car-keyboard>
</block>
</tn-popup>
</view>
</template>
<script>
export default {
name: 'tn-keyboard',
props: {
// 控制键盘弹出收回
value: {
type: Boolean,
default: false
},
// 键盘类型
// number - 数字键盘 card - 身份证键盘 car - 车牌号码
mode: {
type: String,
default: 'number'
},
// 当mode = number时,是否显示'.'符号
dotEnabled: {
type: Boolean,
default: true
},
// 是否打乱顺序
randomEnabled: {
type: Boolean,
default: false
},
// 当mode = car,设置键盘中英文状态
switchEnMode: {
type: Boolean,
default: false
},
// 显示顶部工具条
tooltip: {
type: Boolean,
default: true
},
// 是否显示提示信息
showTips: {
type: Boolean,
default: true
},
// 提示文字
tips: {
type: String,
default: ''
},
// 是否显示取消按钮
cancelBtn: {
type: Boolean,
default: true
},
// 是否显示确认按钮
confirmBtn: {
type: Boolean,
default: true
},
// 取消按钮文字
cancelText: {
type: String,
default: '取消'
},
// 确认按钮文字
confirmText: {
type: String,
default: '确认'
},
// 是否开启底部安全区适配,开启的话,会在iPhoneX机型底部添加一定的内边距
safeAreaInsetBottom: {
type: Boolean,
default: false
},
// 是否可以通过点击遮罩进行关闭
maskCloseable: {
type: Boolean,
default: true
},
// 是否显示遮罩
mask: {
type: Boolean,
default: true
},
// z-index
zIndex: {
type: Number,
default: 0
}
},
computed: {
elZIndex() {
return this.zIndex ? this.zIndex : this.$t.zIndex.popup
}
},
data() {
return {
}
},
methods: {
change(e) {
this.$emit('change', e)
},
// 关闭键盘
popupClose() {
// 修改value的值
this.$emit('input', false)
},
// 输入完成
onConfirm() {
this.popupClose()
this.$emit('confirm')
},
// 输入取消
onCancel() {
this.popupClose()
this.$emit('cancel')
},
// 点击退格
backspaceClick() {
this.$emit('backspace')
}
}
}
</script>
<style lang="scss" scoped>
.tn-keyboard {
position: relative;
&__tooltip {
display: flex;
flex-direction: row;
justify-content: space-between;
&__item {
color: $tn-font-color;
flex: 0 0 33.3333333333%;
text-align: center;
font-size: 28rpx;
padding: 20rpx 10rpx;
}
&__cancel {
text-align: left;
flex-grow: 1;
flex-wrap: 0;
padding-left: 40rpx;
color: $tn-content-color;
&--hover {
color: $tn-font-color;
}
}
&__confirm {
text-align: right;
flex-grow: 1;
flex-wrap: 0;
padding-right: 40rpx;
color: $tn-main-color;
&--hover {
color: $tn-color-blue;
}
}
}
}
</style>
@@ -0,0 +1,143 @@
<template>
<view
class="tn-line-progress-class tn-line-progress"
:style="[progressStyle]"
>
<view
class="tn-line-progress--active"
:class="[
$t.colorUtils.getBackgroundColorInternalClass(activeColor),
striped ? stripedAnimation ? 'tn-line-progress__striped tn-line-progress__striped--active' : 'tn-line-progress__striped' : '',
]"
:style="[progressActiveStyle]"
>
<slot v-if="$slots.default || $slots.$default"></slot>
<block v-else-if="showPercent">{{ percent + '%' }}</block>
</view>
</view>
</template>
<script>
export default {
name: 'tn-line-progress',
props: {
// 进度(百分比)
percent: {
type: Number,
default: 0,
validator: val => {
return val >= 0 && val <= 100
}
},
// 高度
height: {
type: Number,
default: 0
},
// 是否显示为圆角
round: {
type: Boolean,
default: true
},
// 是否显示条纹
striped: {
type: Boolean,
default: false
},
// 条纹是否运动
stripedAnimation: {
type: Boolean,
default: true
},
// 激活部分颜色
activeColor: {
type: String,
default: ''
},
// 非激活部分颜色
inactiveColor: {
type: String,
default: ''
},
// 是否显示进度条内部百分比值
showPercent: {
type: Boolean,
default: false
}
},
computed: {
progressStyle() {
let style = {}
style.borderRadius = this.round ? '100rpx' : 0
if (this.height) {
style.height = this.$t.string.getLengthUnitValue(this.height)
}
if (this.inactiveColor) {
style.backgroundColor = this.inactiveColor
}
return style
},
progressActiveStyle() {
let style = {}
style.width = this.percent + '%'
if (this.$t.colorUtils.getBackgroundColorStyle(this.activeColor)) {
style.backgroundColor = this.$t.colorUtils.getBackgroundColorStyle(this.activeColor)
}
return style
}
},
data() {
return {
}
},
}
</script>
<style lang="scss" scoped>
.tn-line-progress {
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
align-items: center;
width: 100%;
height: 28rpx;
overflow: hidden;
border-radius: 100rpx;
background-color: $tn-progress-bg-color;
&--active {
display: flex;
flex-direction: row;
align-items: center;
justify-items: flex-end;
justify-content: space-around;
width: 0;
height: 100%;
font-size: 20rpx;
color: #FFFFFF;
background-color: $tn-main-color;
transition: all 0.3s ease;
}
&__striped {
background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
background-size: 80rpx 80rpx;
&--active {
animation: progress-striped 2s linear infinite;
}
}
}
@keyframes progress-striped {
0% {
background-position: 0 0;
}
100% {
background-position: 80rpx 0;
}
}
</style>
@@ -0,0 +1,209 @@
<template>
<view
class="tn-list-cell-class tn-list-cell"
:class="[
backgroundColorClass,
fontColorClass,
cellClass
]"
:hover-class="hover ? 'tn-hover' : ''"
:hover-stay-time="150"
:style="[cellStyle]"
@tap="handleClick"
>
<slot></slot>
</view>
</template>
<script>
import componentsColorMixin from '../../libs/mixin/components_color.js'
export default {
mixins: [ componentsColorMixin ],
name: 'tn-list-cell',
props: {
// 列表序号
index: {
type: [Number, String],
default: '0'
},
// 内边距
padding: {
type: String,
default: ''
},
// 是否有箭头
arrow: {
type: Boolean,
default: false
},
//箭头是否有偏移距离
arrowRight: {
type: Boolean,
default: true
},
// 是否有点击效果
hover: {
type: Boolean,
default: false
},
// 隐藏线条
unlined: {
type: Boolean,
default: false
},
//线条是否有左偏移距离
lineLeft: {
type: Boolean,
default: true
},
//线条是否有右偏移距离
lineRight: {
type: Boolean,
default: true
},
//是否加圆角
radius: {
type: Boolean,
default: false
}
},
computed: {
cellClass() {
let clazz = ''
if (this.arrow) {
clazz += ' tn-list-cell--arrow'
if (!this.arrowRight) {
clazz += ' tn-list-cell--arrow--none-right'
}
}
if (this.unlined) {
clazz += ' tn-list-cell--unlined'
} else {
if (this.lineLeft) {
clazz += ' tn-list-cell--line-left'
}
if (this.lineRight) {
clazz += ' tn-list-cell--line-right'
}
}
if (this.radius) {
clazz += ' tn-list-cell--radius'
}
return clazz
},
cellStyle() {
let style = {}
if (this.backgroundColorStyle) {
style.backgroundColor = this.backgroundColorStyle
}
if (this.fontColorStyle) {
style.color = this.fontColorStyle
}
if (this.fontSize) {
style.fontSize = this.fontSize + this.fontUnit
}
if (this.padding) {
style.padding = this.padding
}
return style
},
},
data() {
return {
}
},
methods: {
// 处理点击事件
handleClick() {
this.$emit("click", {
index: Number(this.index)
})
this.$emit("tap", {
index: Number(this.index)
})
}
}
}
</script>
<style lang="scss" scoped>
.tn-list-cell {
position: relative;
width: 100%;
box-sizing: border-box;
background-color: #FFFFFF;
color: $tn-font-color;
font-size: 28rpx;
padding: 26rpx 30rpx;
&--radius {
border-radius: 12rpx;
overflow: hidden;
}
&--arrow {
&::before {
content: " ";
position: absolute;
top: 50%;
right: 30rpx;
width: 20rpx;
height: 20rpx;
margin-top: -12rpx;
border-width: 4rpx 4rpx 0 0;
border-color: $tn-font-holder-color;
border-style: solid;
transform: matrix(0.5, 0.5, -0.5, 0.5, 0, 0);
}
&--none-right {
&::before {
right: 0 !important;
}
}
}
&::after {
content: " ";
position: absolute;
bottom: 0;
right: 0;
left: 0;
pointer-events: none;
border-bottom: 1px solid $tn-border-solid-color;
transform: scaleY(0.5) translateZ(0);
transform-origin: 0 100%;
}
&--line-left {
&::after {
left: 30rpx !important;
}
}
&--line-right {
&::after {
right: 30rpx !important;
}
}
&--unlined {
&::after {
border-bottom: 0 !important;
}
}
}
</style>
@@ -0,0 +1,181 @@
<template>
<view
class="tn-list-view-class tn-list-view"
:class="[
backgroundColorClass,
viewClass
]"
:style="[viewStyle]"
>
<view
v-if="showTitle"
class="tn-list-view__title"
:class="[
fontColorClass
]"
:style="[titleStyle]"
@tap="handleClickTitle"
>{{ title }}</view>
<view
v-else
:class="[{'tn-list-view__title--card': card}]"
@tap="handleClickTitle"
>
<slot name="title"></slot>
</view>
<view
class="tn-list-view__content tn-border-solid-top tn-border-solid-bottom"
:class="[contentClass]"
>
<slot></slot>
</view>
</view>
</template>
<script>
import componentsColorMixin from '../../libs/mixin/components_color.js'
export default {
mixins: [ componentsColorMixin ],
name: 'tn-list-view',
props: {
// 标题
title: {
type: String,
default: ''
},
// 去掉边框 上边框 top, 下边框 bottom, 所有边框 all
unlined: {
type: String,
default: 'all'
},
// 上外边距
marginTop: {
type: String,
default: ''
},
// 内容是否显示为卡片模式
card: {
type: Boolean,
default: false
},
// 是否自定义标题
customTitle: {
type: Boolean,
default: false
}
},
computed: {
showTitle() {
return !this.customTitle && this.title
},
viewClass() {
let clazz = ''
if (this.card) {
clazz += ' tn-list-view--card'
}
return clazz
},
viewStyle() {
let style = {}
if (this.backgroundColorStyle) {
style.backgroundColor = this.backgroundColorStyle
}
if (this.marginTop) {
style.marginTop = this.marginTop
}
return style
},
titleStyle() {
let style = {}
if (this.fontColorStyle) {
style.color = this.fontColorStyle
}
if (this.fontSize) {
style.fontSize = this.fontSize + this.fontUnit
}
return style
},
contentClass() {
let clazz = ''
if (this.card) {
clazz += ' tn-list-view__content--card'
}
switch(this.unlined) {
case 'top':
clazz += ' tn-none-border-top'
break
case 'bottom':
clazz += ' tn-none-border-bottom'
break
case 'all':
clazz += ' tn-none-border'
break
}
return clazz
}
},
data () {
return {
kindShowFlag: this.showKind
}
},
methods: {
// 处理标题点击事件
handleClickTitle() {
if (!this.kindList) return
this.kindShowFlag = !this.kindShowFlag
this.$emit("clickTitle", {})
}
}
}
</script>
<style lang="scss" scoped>
.tn-list-view {
background-color: transparent;
&__title {
width: 100%;
padding: 30rpx;
font-size: 30rpx;
line-height: 30rpx;
box-sizing: border-box;
&--card {
margin: 0rpx 30rpx;
}
}
&__content {
width: 100%;
position: relative;
&--card {
width: auto;
overflow: hidden;
margin-right: 30rpx;
margin-left: 30rpx;
border-radius: 20rpx
}
}
&--card {
padding-bottom: 30rpx;
}
}
</style>
@@ -0,0 +1,100 @@
<template>
<view
v-if="show"
class="tn-loading-class tn-loading"
:class="mode === 'circle' ? 'tn-loading-circle' : 'tn-loading-flower'"
:style="[loadStyle]"
></view>
</template>
<script>
export default {
name: 'tn-loading',
props: {
// 动画类型
// circle 圆圈 flower 花朵形状
mode: {
type: String,
default: 'circle'
},
// 是否显示动画
show: {
type: Boolean,
default: true
},
// 圆圈颜色
color: {
type: String,
default: ''
},
// 图标大小
size: {
type: Number,
default: 34
}
},
computed: {
// 加载动画圆圈的样式
loadStyle() {
let style = {}
style.width = this.size + 'rpx'
style.height = style.width
if (this.mode === 'circle') style.borderColor = `#E6E6E6 #E6E6E6 #E6E6E6 ${this.color ? this.color : '#AAAAAA'}`
return style
}
}
}
</script>
<style lang="scss" scoped>
.tn-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: #E6E6E6 #E6E6E6 #E6E6E6 #AAAAAA;
animation: tn-circle 1s linear infinite;
-webkit-animation: tn-circle 1s linear infinite;
}
.tn-loading-flower {
display: inline-block;
vertical-align: middle;
width: 28rpx;
height: 28rpx;
background: transparent url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjAiIGhlaWdodD0iMTIwIiB2aWV3Qm94PSIwIDAgMTAwIDEwMCI+PHBhdGggZmlsbD0ibm9uZSIgZD0iTTAgMGgxMDB2MTAwSDB6Ii8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjRTlFOUU5IiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAgLTMwKSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iIzk4OTY5NyIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSgzMCAxMDUuOTggNjUpIi8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjOUI5OTlBIiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0icm90YXRlKDYwIDc1Ljk4IDY1KSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iI0EzQTFBMiIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSg5MCA2NSA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNBQkE5QUEiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoMTIwIDU4LjY2IDY1KSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iI0IyQjJCMiIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSgxNTAgNTQuMDIgNjUpIi8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjQkFCOEI5IiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0icm90YXRlKDE4MCA1MCA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNDMkMwQzEiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoLTE1MCA0NS45OCA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNDQkNCQ0IiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoLTEyMCA0MS4zNCA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNEMkQyRDIiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoLTkwIDM1IDY1KSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iI0RBREFEQSIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSgtNjAgMjQuMDIgNjUpIi8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjRTJFMkUyIiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0icm90YXRlKC0zMCAtNS45OCA2NSkiLz48L3N2Zz4=) no-repeat;;
background-size: 100%;
animation: tn-flower 1s steps(12) infinite;
-webkit-animation: tn-flower 1s steps(12) infinite;
}
@keyframes tn-flower {
0% {
transform: rotate(0deg);
-webkit-transform: rotate(0deg);
}
to {
transform: rotate(360deg);
-webkit-transform: rotate(360deg);
}
}
@keyframes tn-circle {
0% {
transform: rotate(0);
-webkit-transform: rotate(0);
}
100% {
transform: rotate(360deg);
-webkit-transform: rotate(360deg);
}
}
</style>
+246
View File
@@ -0,0 +1,246 @@
<template>
<view v-if="value" class="tn-modal-class tn-modal">
<tn-popup
v-model="value"
mode="center"
:popup="false"
:borderRadius="radius"
:width="width"
:zoom="zoom"
:safeAreaInsetBottom="safeAreaInsetBottom"
:maskCloseable="maskCloseable"
:zIndex="zIndex"
:closeBtn="showCloseBtn"
@close="close"
>
<!-- 内容 -->
<view
class="tn-modal__box"
:class="[
backgroundColorClass
]"
:style="[boxStyle]"
>
<!-- 不是自定义弹框内容 -->
<view v-if="!custom">
<view class="tn-modal__box__title" v-if="title && title !== ''">{{ title }}</view>
<view
class="tn-modal__box__content"
:class="[
fontColorClass,
contentClass
]"
:style="contentStyle"
>{{ content }}</view>
<view v-if="button && button.length" class="tn-modal__box__btn-box" :class="[button.length != 2 ? 'tn-flex-direction-column' : '']">
<block v-for="(item, index) in button" :key="index">
<tn-button
width="100%"
height="68rpx"
:fontSize="26"
:backgroundColor="item.backgroundColor || ''"
:fontColor="item.fontColor || ''"
:plain="item.plain || false"
:shape="item.shape || 'round'"
:class="[
button.length > 2 ? 'tn-margin-bottom' : ''
]"
@click="handleClick(index)"
:style="{
width: button.length != 2 ? '80%' : '46%'
}"
>
{{ item.text }}
</tn-button>
</block>
</view>
</view>
<view v-else>
<slot></slot>
</view>
</view>
</tn-popup>
</view>
</template>
<script>
import componentsColorMixin from '../../libs/mixin/components_color.js'
export default {
mixins: [componentsColorMixin],
name: 'tn-modal',
props: {
// 显示控制
value: {
type: Boolean,
default: false
},
// 弹框宽度
width: {
type: String,
default: '84%'
},
// 内边距
padding: {
type: String,
default: ''
},
// 圆角
radius: {
type: Number,
default: 12
},
// 标题
title: {
type: String,
default: ''
},
// 内容
content: {
type: String,
default: ''
},
// 按钮内容 设置参数与button组件的参数一致
// {
// text: '确定',
// backgroundColor: 'red',
// fontColor: 'white',
// plain: true,
// shape: ''
// }
button: {
type: Array,
default: () => {
return []
}
},
safeAreaInsetBottom: {
type: Boolean,
default: false
},
// 点击遮罩是否可以关闭
maskCloseable: {
type: Boolean,
default: true
},
// 是否显示右上角关闭按钮
showCloseBtn: {
type: Boolean,
default: false
},
// 放大动画
zoom: {
type: Boolean,
default: true
},
// 自定义弹框内容
custom: {
type: Boolean,
default: false
},
// 弹框的z-index
zIndex: {
type: Number,
default: 0
}
},
computed: {
boxStyle() {
let style = {}
if (this.padding) {
style.padding = this.padding
}
if (this.backgroundColorStyle) {
style.backgroundColor = this.backgroundColorStyle
}
return style
},
contentClass() {
let clazz = ''
if (this.title) {
clazz += ' tn-margin-top'
} else {
clazz += ' tn-modal__box__content--no-title'
}
return clazz
},
contentStyle() {
let style = {}
if (this.fontSize) {
style.fontSize = this.fontSize + this.fontUnit
}
if (this.fontColorStyle) {
style.color = this.fontColorStyle
}
return style
},
},
data() {
return {
}
},
methods: {
// 处理按钮点击事件
handleClick(index) {
if (!this.value) return
this.$emit("click", {
index: Number(index)
})
},
// 处理关闭事件
close() {
this.$emit("cancel")
this.$emit('input', false)
}
}
}
</script>
<style lang="scss" scoped>
.tn-modal {
&__box {
position: relative;
box-sizing: border-box;
background-color: #FFFFFF;
padding: 40rpx 64rpx;
&__title {
text-align: center;
font-size: 34rpx;
color: #333;
padding-top: 20rpx;
font-weight: bold;
}
&__content {
text-align: center;
padding-bottom: 30rpx;
color: $tn-font-color;
font-size: 28rpx;
&--no-title {
padding-bottom: 0rpx !important;
}
}
&__btn-box {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
}
&__content ~ &__btn-box {
margin-top: 30rpx;
}
}
}
</style>
@@ -0,0 +1,325 @@
<template>
<view
class="tn-custom-nav-bar-class tn-custom-nav-bar"
:style="[navBarStyle]"
>
<view
class="tn-custom-nav-bar__bar"
:class="[barClass]"
:style="[barStyle]"
>
<view v-if="isBack">
<view v-if="customBack">
<view
:style="{
width: customBackStyleInfo.width + 'px',
height: customBackStyleInfo.height + 'px',
marginLeft: customBackStyleInfo.left + 'px'
}"
>
<slot name="back"></slot>
</view>
</view>
<view v-else class="tn-custom-nav-bar__bar__action" @tap="handlerBack">
<text class="tn-custom-nav-bar__bar__action--nav-back"></text>
<text class="tn-custom-nav-bar__bar__action--nav-back-text" v-if="backTitle">{{ backTitle }}</text>
</view>
</view>
<view class="tn-custom-nav-bar__bar__content" :style="[contentStyle]">
<slot></slot>
</view>
<view>
<slot name="right"></slot>
</view>
</view>
</view>
</template>
<script>
import componentsColorMixin from '../../libs/mixin/components_color.js'
export default {
name: 'tn-nav-bar',
mixins: [componentsColorMixin],
props: {
// 层级
zIndex: {
type: Number,
default: 0
},
// 导航栏的高度
height: {
type: Number,
default: 0
},
// 高度单位
unit: {
type: String,
default: 'px'
},
// 是否显示返回按钮
isBack: {
type: Boolean,
default: true
},
// 返回按钮旁显示的文字
backTitle: {
type: String,
default: '返回'
},
// 透明状态栏
alpha: {
type: Boolean,
default: false
},
// 是否固定在顶部
fixed: {
type: Boolean,
default: true
},
// 是否显示底部阴影
bottomShadow: {
type: Boolean,
default: true
},
// 是否自定义返回按钮
customBack: {
type: Boolean,
default: false
},
// 返回前回调
beforeBack: {
type: Function,
default: null
}
},
computed: {
navBarStyle() {
let style = {}
style.height = this.height === 0 ? this.customBarHeight + this.unit : this.height + this.unit
if (this.fixed) {
style.position = 'fixed'
}
style.zIndex = this.elZIndex
return style
},
barClass() {
let clazz = ''
if (this.backgroundColorClass) {
clazz += ` ${this.backgroundColorClass}`
}
if (this.fixed) {
clazz += ' tn-custom-nav-bar__bar--fixed'
}
if (this.alpha) {
clazz += ' tn-custom-nav-bar__bar--alpha'
}
if (this.bottomShadow) {
clazz += ' tn-custom-nav-bar__bar--bottom-shadow'
}
return clazz
},
barStyle() {
let style = {}
style.height = this.height === 0 ? this.customBarHeight + this.unit : this.height + this.unit
if (this.fixed) {
style.paddingTop = this.statusBarHeight + 'px'
}
if(!this.backgroundColorClass) {
style.backgroundColor = this.backgroundColor !== '' ? this.backgroundColor : '#FFFFFF'
}
style.zIndex = this.elZIndex
return style
},
contentStyle() {
let style = {}
style.top = this.fixed ? this.statusBarHeight + 'px' : '0px'
style.height = this.height === 0 ? (this.customBarHeight - this.statusBarHeight) + this.unit : this.height + this.unit
style.lineHeight = style.height
if (this.isBack) {
if (this.customBack) {
const width = (this.customBackStyleInfo.width + this.customBackStyleInfo.left) * 2
style.width = `calc(100% - ${width}px)`
} else {
style.width = 'calc(100% - 340rpx)'
}
} else {
style.width = '100%'
}
return style
},
elZIndex() {
return this.zIndex ? this.zIndex : this.$t.zIndex.navbar
}
},
data() {
return {
// 状态栏的高度
statusBarHeight: 0,
// 自定义导航栏的高度
customBarHeight: 0,
// 自定义返回按钮时,返回容器的宽高边距信息
customBackStyleInfo: {
width: 86,
height: 32,
left: 15
}
}
},
mounted() {
// 获取vuex中的自定义顶栏的高度
let customBarHeight = this.vuex_custom_bar_height
let statusBarHeight = this.vuex_status_bar_height
// 如果获取失败则重新获取
if (!customBarHeight) {
this.$t.updateCustomBar()
customBarHeight = this.vuex_custom_bar_height
statusBarHeight = this.vuex_status_bar_height
}
this.customBarHeight = customBarHeight
this.statusBarHeight = statusBarHeight
},
created() {
// 获取胶囊信息
// #ifdef MP-WEIXIN
let custom = wx.getMenuButtonBoundingClientRect()
this.customBackStyleInfo.width = custom.width
this.customBackStyleInfo.height = custom.height
this.customBackStyleInfo.left = uni.upx2px(750) - custom.right
// #endif
},
methods: {
// 处理返回事件
async handlerBack() {
if (this.beforeBack && typeof(this.beforeBack) === 'function') {
// 执行回调,同时传入索引当作参数
// 在微信,支付宝等环境(H5正常),会导致父组件定义的函数体中的this变成子组件的this
// 通过bind()方法,绑定父组件的this,让this的this为父组件的上下文
let beforeBack = this.beforeBack.bind(this.$t.$parent.call(this))()
// 判断是否返回了Promise
if (!!beforeBack && typeof beforeBack.then === 'function') {
await beforeBack.then(res => {
// Promise返回成功
this.navBack()
}).catch(err => {})
} else if (beforeBack === true) {
this.navBack()
}
} else {
this.navBack()
}
},
// 返回上一页
navBack() {
// 通过判断当前页面的页面栈信息,是否有上一页进行返回,如果没有则跳转到首页
const pages = getCurrentPages()
if (pages && pages.length > 0) {
const firstPage = pages[0]
if (!firstPage.route || firstPage.route != 'pages/index/index') {
uni.reLaunch({
url: '/pages/index/index'
})
} else {
uni.navigateBack({
delta: 1
})
}
} else {
uni.reLaunch({
url: '/pages/index/index'
})
}
}
}
}
</script>
<style lang="scss" scoped>
.tn-custom-nav-bar {
display: block;
position: relative;
&__bar {
display: flex;
position: relative;
align-items: center;
min-height: 100rpx;
justify-content: space-between;
min-height: 0px;
/* #ifdef MP-WEIXIN */
padding-right: 220rpx;
/* #endif */
/* #ifdef MP-ALIPAY */
padding-right: 150rpx;
/* #endif */
box-shadow: 0rpx 0rpx 0rpx;
z-index: 9999;
&--fixed {
position: fixed;
width: 100%;
top: 0;
}
&--alpha {
background: transparent !important;
box-shadow: none !important;
}
&--bottom-shadow {
box-shadow: 0rpx 0rpx 80rpx 0rpx rgba(0, 0, 0, 0.05);
}
&__action {
display: flex;
align-items: center;
height: 100%;
justify-content: center;
max-width: 100%;
&--nav-back {
/* position: absolute; */
/* top: 50%; */
/* left: 20rpx; */
/* margin-top: -15rpx; */
width: 25rpx;
height: 25rpx;
margin-left: 25rpx;
border-width: 0 0 4rpx 4rpx;
border-color: #000000;
border-style: solid;
transform: matrix(0.5, 0.5, -0.5, 0.5, 0, 0);
}
&--nav-back-text {
margin-left: 10rpx;
}
}
&__content {
position: absolute;
text-align: center;
left: 0;
right: 0;
bottom: 0;
margin: auto;
font-size: 32rpx;
cursor: none;
// pointer-events: none;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
}
}
</style>
@@ -0,0 +1,209 @@
<template>
<view
v-if="showNotice"
class="tn-notice-bar-class tn-notice-bar"
:style="{
borderRadius: radius + 'rpx'
}"
>
<block v-if="mode === 'horizontal' && circular">
<tn-row-notice
:backgroundColor="backgroundColor"
:fontColor="fontColor"
:fontSize="fontSize"
:fontUnit="fontUnit"
:list="list"
:show="show"
:playStatus="playStatus"
:leftIcon="leftIcon"
:leftIconName="leftIconName"
:leftIconSize="leftIconSize"
:rightIcon="rightIcon"
:rightIconName="rightIconName"
:rightIconSize="rightIconSize"
:closeBtn="closeBtn"
:autoplay="autoplay"
:radius="radius"
:padding="padding"
:speed="speed"
@click="click"
@close="close"
@clickLeft="clickLeftIcon"
@clickRight="clickRightIcon"
></tn-row-notice>
</block>
<block v-if="mode === 'vertical' || (mode === 'horizontal' && !circular)">
<tn-column-notice
:backgroundColor="backgroundColor"
:fontColor="fontColor"
:fontSize="fontSize"
:fontUnit="fontUnit"
:list="list"
:show="show"
:mode="mode"
:playStatus="playStatus"
:leftIcon="leftIcon"
:leftIconName="leftIconName"
:leftIconSize="leftIconSize"
:rightIcon="rightIcon"
:rightIconName="rightIconName"
:rightIconSize="rightIconSize"
:closeBtn="closeBtn"
:autoplay="autoplay"
:radius="radius"
:padding="padding"
:duration="duration"
@click="click"
@close="close"
@clickLeft="clickLeftIcon"
@clickRight="clickRightIcon"
@end="end"
></tn-column-notice>
</block>
</view>
</template>
<script>
import componentsColorMixin from '../../libs/mixin/components_color.js'
export default {
name: 'tn-notice-bar',
mixins: [componentsColorMixin],
props: {
// 显示的内容
list: {
type: Array,
default() {
return []
}
},
// 是否显示
show: {
type: Boolean,
default: true
},
// 播放状态
// play -> 播放 paused -> 暂停
playStatus: {
type: String,
default: 'play'
},
// 滚动方向
// horizontal -> 水平滚动 vertical -> 垂直滚动
mode: {
type: String,
default: 'horizontal'
},
// 是否显示左边图标
leftIcon: {
type: Boolean,
default: true
},
// 左边图标的名称
leftIconName: {
type: String,
default: 'sound'
},
// 左边图标的大小
leftIconSize: {
type: Number,
default: 34
},
// 是否显示右边的图标
rightIcon: {
type: Boolean,
default: false
},
// 右边图标的名称
rightIconName: {
type: String,
default: 'right'
},
// 右边图标的大小
rightIconSize: {
type: Number,
default: 26
},
// 是否显示关闭按钮
closeBtn: {
type: Boolean,
default: false
},
// 圆角
radius: {
type: Number,
default: 0
},
// 内边距
padding: {
type: String,
default: '18rpx 24rpx'
},
// 自动播放
autoplay: {
type: Boolean,
default: true
},
// 滚动周期
duration: {
type: Number,
default: 2000
},
// 水平滚动时的速度,即每秒滚动多少rpx
speed: {
type: Number,
default: 160
},
// 水平滚动的时候是否采用衔接的模式
circular: {
type: Boolean,
default: true
},
// 没有数据时是否显示通知
autoHidden: {
type: Boolean,
default: true
}
},
computed: {
// 当设置了show为false,或者autoHidden为true且list为空时,不显示通知
showNotice() {
if (this.show === false || (this.autoHidden && this.list.length === 0)) return false
else return true
}
},
data() {
return {
}
},
methods: {
// 点击了通知栏
click(index) {
this.$emit('click', index)
},
// 点击了关闭按钮
close() {
this.$emit('close')
},
// 点击了左边图标
clickLeftIcon() {
this.$emit('clickLeft')
},
// 点击了右边图标
clickRightIcon() {
this.$emit('clickRight')
},
// 一个周期滚动结束
end() {
this.$emit('end')
}
}
}
</script>
<style lang="scss" scoped>
.tn-notice-bar {
overflow: hidden;
}
</style>
@@ -0,0 +1,401 @@
<template>
<view class="tn-number-box-class tn-number-box">
<!-- -->
<view
class="tn-number-box__btn__minus"
:class="[
backgroundColorClass,
fontColorClass,
{'tn-number-box__btn--disabled': disabled || inputValue <= min}
]"
:style="{
backgroundColor: backgroundColorStyle,
height: $t.string.getLengthUnitValue(inputHeight),
color: fontColorStyle,
fontSize: fontSizeStyle
}"
@touchstart.stop.prevent="touchStart('minus')"
@touchend.stop.prevent="clearTimer"
>
<view class="tn-icon-reduce"></view>
</view>
<!-- 输入框 -->
<input
v-model="inputValue"
:disabled="disabledInput || disabled"
:cursor-spacing="getCursorSpacing"
class="tn-number-box__input"
:class="[
fontColorClass,
{'tn-number-box__input--disabled': disabledInput || disabled}
]"
:style="{
width: $t.string.getLengthUnitValue(inputWidth),
height: $t.string.getLengthUnitValue(inputHeight),
color: fontColorStyle,
fontSize: fontSizeStyle,
backgroundColor: backgroundColorStyle
}"
@blur="blurInput"
@focus="focusInput"
/>
<!-- -->
<view
class="tn-number-box__btn__plus"
:class="[
backgroundColorClass,
fontColorClass,
{'tn-number-box__btn--disabled': disabled || inputValue >= max}
]"
:style="{
backgroundColor: backgroundColorStyle,
height: $t.string.getLengthUnitValue(inputHeight),
color: fontColorStyle,
fontSize: fontSizeStyle
}"
@touchstart.stop.prevent="touchStart('plus')"
@touchend.stop.prevent="clearTimer"
>
<view class="tn-icon-add"></view>
</view>
</view>
</template>
<script>
import componentsColor from '../../libs/mixin/components_color.js'
export default {
mixins: [componentsColor],
name: 'tn-number-box',
props: {
value: {
type: Number,
default: 1
},
// 索引
index: {
type: [Number, String],
default: ''
},
// 最小值
min: {
type: Number,
default: 0
},
// 最大值
max: {
type: Number,
default: 99999
},
// 步进值
step: {
type: Number,
default: 1
},
// 禁用
disabled: {
type: Boolean,
default: false
},
// 是否禁用输入
disabledInput: {
type: Boolean,
default: false
},
// 输入框的宽度
inputWidth: {
type: Number,
default: 88
},
// 输入框的高度
inputHeight: {
type: Number,
default: 50
},
// 输入框和键盘之间的距离
cursorSpacing: {
type: Number,
default: 100
},
// 是否开启长按进行连续递增减
longPress: {
type: Boolean,
default: true
},
// 长按触发间隔
longPressTime: {
type: Number,
default: 250
},
// 是否只能输入正整数
positiveInteger: {
type: Boolean,
default: true
}
},
computed: {
getCursorSpacing() {
return Number(uni.upx2px(this.cursorSpacing))
}
},
data() {
return {
// 输入框的值
inputValue: 1,
// 长按定时器
longPressTimer: null,
// 标记值的改变是来自外部还是内部
changeFromInner: false,
// 内部定时器
innerChangeTimer: null
}
},
watch: {
value(val) {
// 只有value的改变是来自外部的时候,才去同步inputValue的值,否则会造成循环错误
if (!this.changeFromInner) {
this.updateInputValue()
// 因为inputValue变化后,会触发this.handleChange(),在其中changeFromInner会再次被设置为true
// 造成外面修改值,也导致被认为是内部修改的混乱,这里进行this.$nextTick延时,保证在运行周期的最后处
// 将changeFromInner设置为false
this.$nextTick(() => {
this.changeFromInner = false
})
}
},
inputValue(newVal, oldVal) {
// 为了让用户能够删除所有输入值,重新输入内容,删除所有值后,内容为空字符串
if (newVal === '') return
let value = 0
// 首先判断是否数值,并且在min和max之间,如果不是,使用原来值
let isNumber = this.$t.test.number(newVal)
if (isNumber && newVal >= this.min && newVal <= this.max) value = newVal
else value = oldVal
// 判断是否只能输入大于等于0的整数
if (this.positiveInteger) {
// 小于0或者带有小数点
if (newVal < 0 || String(newVal).indexOf('.') !== -1) {
value = Math.floor(newVal)
// 双向绑定input的值,必须要使用$nextTick修改显示的值
this.$nextTick(() => {
this.inputValue = value
})
}
}
this.handleChange(value, 'change')
},
min() {
this.updateInputValue()
},
max() {
this.updateInputValue()
}
},
created() {
this.updateInputValue()
},
methods: {
// 开始点击按钮
touchStart(func) {
// 先执行一遍方法,否则会造成松开手时,就执行了clearTimer,导致无法实现功能
this[func]()
// 如果没有开启长按功能,直接返回
if (!this.longPress) return
// 清空长按定时器,防止重复注册
if (this.longPressTimer) {
clearInterval(this.longPressTimer)
this.longPressTimer = null
}
this.longPressTimer = setInterval(() => {
// 执行加减操作
this[func]()
}, this.longPressTime)
},
// 清除定时器
clearTimer() {
this.$nextTick(() => {
if (this.longPressTimer) {
clearInterval(this.longPressTimer)
this.longPressTimer = null
}
})
},
// 减
minus() {
this.computeValue('minus')
},
// 加
plus() {
this.computeValue('plus')
},
// 处理小数相加减出现溢出问题
calcPlus(num1, num2) {
let baseNum = 0, baseNum1 = 0, baseNum2 = 0
try {
baseNum1 = num1.toString().split('.')[1].length
} catch(e) {
baseNum1 = 0
}
try {
baseNum2 = num2.toString().split('.')[1].length
} catch(e) {
baseNum2 = 0
}
baseNum = Math.pow(10, Math.max(baseNum1, baseNum2))
// 精度
let precision = baseNum1 >= baseNum2 ? baseNum1 : baseNum2
return ((num1 * baseNum + num2 * baseNum) / baseNum).toFixed(precision)
},
calcMinus(num1, num2) {
let baseNum = 0, baseNum1 = 0, baseNum2 = 0
try {
baseNum1 = num1.toString().split('.')[1].length
} catch(e) {
baseNum1 = 0
}
try {
baseNum2 = num2.toString().split('.')[1].length
} catch(e) {
baseNum2 = 0
}
baseNum = Math.pow(10, Math.max(baseNum1, baseNum2))
// 精度
let precision = baseNum1 >= baseNum2 ? baseNum1 : baseNum2
return ((num1 * baseNum - num2 * baseNum) / baseNum).toFixed(precision)
},
// 处理操作后的值
computeValue(type) {
uni.hideKeyboard()
if (this.disabled) return
let value = 0
if (type === 'minus') {
// 减
value = this.calcMinus(this.inputValue, this.step)
} else if (type === 'plus') {
// 加
value = this.calcPlus(this.inputValue, this.step)
}
// 判断是否比最小值小和操作最大值
if (value < this.min || value > this.max) return
this.inputValue = value
this.handleChange(value, type)
},
// 处理用户手动输入
blurInput(event) {
let val = 0,
value = event.detail.value
// 如果为非0-9数字组成,或者其第一位数值为0,直接让其等于min值
// 这里不直接判断是否正整数,是因为用户传递的props min值可能为0
if (!/(^\d+$)/.test(value) || value[0] == 0) {
val = this.min
} else {
val = +value
}
if (val > this.max) {
val = this.max
} else if (val < this.min) {
val = this.min
}
this.$nextTick(() => {
this.inputValue = val
})
this.handleChange(val, 'blur')
},
// 获取焦点
focusInput() {
this.$emit('focus')
},
// 初始化inputValue
updateInputValue() {
let value = this.value
if (value <= this.min) {
value = this.min
} else if (value >= this.max) {
value = this.max
}
this.inputValue = Number(value)
},
// 处理值改变状态
handleChange(value, type) {
if (this.disabled) return
// 清除定时器,防止混乱
if (this.innerChangeTimer) {
clearTimeout(this.innerChangeTimer)
this.innerChangeTimer = null
}
// 内部修改值
this.changeFromInner = true
// 一定时间内,清除changeFromInner标记,否则内部值改变后
// 外部通过程序修改value值,将会无效
this.innerChangeTimer = setTimeout(() => {
this.changeFromInner = false
}, 150)
this.$emit('input', Number(value))
this.$emit(type, {
value: Number(value),
index: this.index
})
}
}
}
</script>
<style lang="scss" scoped>
.tn-number-box {
display: inline-flex;
align-items: center;
&__btn {
&__plus,&__minus {
width: 60rpx;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
background-color: $tn-font-holder-color;
}
&__plus {
border-radius: 0 8rpx 8rpx 0;
}
&__minus {
border-radius: 8rpx 0 0 8rpx;
}
&--disabled {
color: $tn-font-sub-color !important;
background: $tn-font-holder-color !important;
}
}
&__input {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
position: relative;
text-align: center;
box-sizing: border-box;
padding: 0 4rpx;
margin: 0 6rpx;
background-color: $tn-font-holder-color;
&--disabled {
color: $tn-font-sub-color !important;
background: $tn-font-holder-color !important;
}
}
}
</style>
@@ -0,0 +1,182 @@
<template>
<view class="tn-number-keyboard-class tn-number-keyboard" @touchmove.stop.prevent="() => {}">
<view class="tn-number-keyboard__grids">
<view
v-for="(item, index) in dataList"
:key="index"
class="tn-number-keyboard__grids__item"
:class="{
'tn-bg-gray--light': showGaryBg(index),
'tn-border-solid-top': index <= 2,
'tn-border-solid-bottom': index < 9,
'tn-border-solid-right': (index + 1) % 3 != 0
}"
:hover-class="hoverClass(index)"
:hover-stay-time="150"
@tap="keyboardClick(item)"
>
<view class="tn-number-keyboard__grids__btn">{{ item }}</view>
</view>
<view
class="tn-number-keyboard__grids__item tn-bg-gray--light"
hover-class="tn-hover"
:hover-stay-time="150"
@touchstart.stop="backspaceClick"
@touchend="clearTimer"
>
<view class="tn-number-keyboard__grids__btn tn-number-keyboard__back">
<view class="tn-icon-left-arrow tn-number-keyboard__back__icon"></view>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'tn-number-keyboard',
props: {
// 键盘类型
// number -> 数字键盘 card -> 身份证键盘
mode: {
type: String,
default: 'number'
},
// 是否显示键盘的'.'符号
dotEnabled: {
type: Boolean,
default: true
},
// 是否为乱序键盘
randomEnabled: {
type: Boolean,
default: false
}
},
computed: {
// 键盘显示的内容
dataList() {
let tmp = []
if (!this.dotEnabled && this.mode === 'number') {
if (!this.randomEnabled) {
return [1, 2, 3, 4, 5, 6, 7, 8, 9, '', 0]
} else {
let data = this.$t.array.random([1, 2, 3, 4, 5, 6, 7, 8, 9, 0])
data.splice(-1, 0, '')
return data
}
} else if (this.dotEnabled && this.mode === 'number') {
if (!this.randomEnabled) {
return [1, 2, 3, 4, 5, 6, 7, 8, 9, this.dot, 0]
} else {
let data = this.$t.array.random([1, 2, 3, 4, 5, 6, 7, 8, 9, 0])
data.splice(-1, 0, this.dot)
return data
}
} else if (this.mode === 'card') {
if (!this.randomEnabled) {
return [1, 2, 3, 4, 5, 6, 7, 8, 9, this.cardX, 0]
} else {
let data = this.$t.array.random([1, 2, 3, 4, 5, 6, 7, 8, 9, 0])
data.splice(-1, 0, this.cardX)
return data
}
}
},
// 按键的样式
keyStyle() {
return index => {
let style = {}
if (this.mode === 'number' && !this.dotEnabled && index === 9) style.flex = '0 0 66.6666666666%'
return style
}
},
// 是否让按键显示灰色,只在数字键盘和非乱序且在点击时
showGaryBg() {
return index => {
if (!this.randomEnabled && index === 9 && (this.mode !== 'number' || (this.mode === 'number' && this.dotEnabled))) return true
else return false
}
},
// 手指停留的class
hoverClass() {
return index => {
if (this.mode === 'number' && !this.dotEnabled && index === 9) return ''
if (!this.randomEnabled && index === 9 && (this.mode === 'number' && this.dotEnabled || this.mode === 'card')) return 'tn-hover'
else return 'tn-number-keyboard--hover'
}
}
},
data() {
return {
// 退格键内容
backspace: 'backspace',
// 点内容
dot: '.',
// 长按多次删除事件监听
longPressDeleteTimer: null,
// 身份证的X符号
cardX: 'X'
}
},
methods: {
// 点击退格键
backspaceClick() {
this.$emit('backspace')
this.clearTimer()
this.longPressDeleteTimer = setInterval(() => {
this.$emit('backspace')
}, 250)
},
// 获取键盘显示的内容
keyboardClick(value) {
if (this.mode === 'number' && !this.dotEnabled && value === '') return
// 允许键盘显示点模式和触发非点按键时,将内容转换为数字类型
if (this.dotEnabled && value != this.dot && value != this.cardX) value = Number(value)
this.$emit('change', value)
},
// 清除定时器
clearTimer() {
if (this.longPressDeleteTimer) {
clearInterval(this.longPressDeleteTimer)
this.longPressDeleteTimer = null
}
}
}
}
</script>
<style lang="scss" scoped>
.tn-number-keyboard {
position: relative;
&__grids {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-end;
&__item {
display: flex;
flex-direction: row;
flex: 0 0 33.3333333333%;
align-items: center;
justify-content: center;
height: 110rpx;
text-align: center;
font-size: 50rpx;
color: $tn-font-color;
font-weight: 500;
}
}
&__back {
font-size: 38rpx;
}
&--hover {
background-color: $tn-font-holder-color;
}
}
</style>
@@ -0,0 +1,723 @@
<template>
<view v-if="value" class="tn-picker-class tn-picker">
<tn-popup
v-model="value"
mode="bottom"
:popup="false"
length="auto"
:safeAreaInsetBottom="safeAreaInsetBottom"
:maskCloseable="maskCloseable"
:zIndex="elZIndex"
@close="close"
>
<view class="tn-picker__content" :style="{ zIndex: elZIndex }">
<!-- 顶部 -->
<view class="tn-picker__content__header tn-border-solid-bottom" @touchmove.stop.prevent>
<!-- 取消按钮 -->
<view
class="tn-picker__content__header__btn tn-picker__content__header--cancel"
:style="{ color: cancelColor }"
hover-class="tn-hover-class"
:hover-stay-time="150"
@tap="getResult('cancel')"
>{{cancelText}}</view>
<!-- 标题 -->
<view class="tn-picker__content__header__title">{{ title }}</view>
<!-- 确认按钮 -->
<view
class="tn-picker__content__header__btn tn-picker__content__header--confirm"
:style="{ color: moving ? cancelColor : confirmColor}"
hover-class="tn-hover-class"
:hover-stay-time="150"
@touchmove.stop
@tap.stop="getResult('confirm')"
>{{confirmText}}</view>
</view>
<!-- 主体 -->
<view class="tn-picker__content__body">
<!-- 地区选择 -->
<picker-view
v-if="mode === 'region'"
class="tn-picker__content__body__view"
:value="valueArr"
@change="change"
@pickstart="pickStart"
@pickend="pickEnd"
>
<picker-view-column v-if="!reset && params.province">
<view class="tn-picker__content__body__item" v-for="(item, index) in provinces" :key="index">
<view class="tn-text-ellipsis">{{ item.label }}</view>
</view>
</picker-view-column>
<picker-view-column v-if="!reset && params.city">
<view class="tn-picker__content__body__item" v-for="(item, index) in citys" :key="index">
<view class="tn-text-ellipsis">{{ item.label }}</view>
</view>
</picker-view-column>
<picker-view-column v-if="!reset && params.area">
<view class="tn-picker__content__body__item" v-for="(item, index) in areas" :key="index">
<view class="tn-text-ellipsis">{{ item.label }}</view>
</view>
</picker-view-column>
</picker-view>
<!-- 时间选择 -->
<picker-view
v-else-if="mode === 'time'"
class="tn-picker__content__body__view"
:value="valueArr"
@change="change"
@pickstart="pickStart"
@pickend="pickEnd"
>
<picker-view-column v-if="!reset && params.year">
<view class="tn-picker__content__body__item" v-for="(item, index) in years" :key="index">
{{ item }}
<text v-if="showTimeTag" class="tn-picker__content__body__item__text"></text>
</view>
</picker-view-column>
<picker-view-column v-if="!reset && params.month">
<view class="tn-picker__content__body__item" v-for="(item, index) in months" :key="index">
{{ formatNumber(item) }}
<text v-if="showTimeTag" class="tn-picker__content__body__item--text"></text>
</view>
</picker-view-column>
<picker-view-column v-if="!reset && params.day">
<view class="tn-picker__content__body__item" v-for="(item, index) in days" :key="index">
{{ formatNumber(item) }}
<text v-if="showTimeTag" class="tn-picker__content__body__item--text"></text>
</view>
</picker-view-column>
<picker-view-column v-if="!reset && params.hour">
<view class="tn-picker__content__body__item" v-for="(item, index) in hours" :key="index">
{{ formatNumber(item) }}
<text v-if="showTimeTag" class="tn-picker__content__body__item--text"></text>
</view>
</picker-view-column>
<picker-view-column v-if="!reset && params.minute">
<view class="tn-picker__content__body__item" v-for="(item, index) in minutes" :key="index">
{{ formatNumber(item) }}
<text v-if="showTimeTag" class="tn-picker__content__body__item--text"></text>
</view>
</picker-view-column>
<picker-view-column v-if="!reset && params.second">
<view class="tn-picker__content__body__item" v-for="(item, index) in seconds" :key="index">
{{ formatNumber(item) }}
<text v-if="showTimeTag" class="tn-picker__content__body__item--text"></text>
</view>
</picker-view-column>
</picker-view>
<!-- 单列选择 -->
<picker-view
v-else-if="mode === 'selector'"
class="tn-picker__content__body__view"
:value="valueArr"
@change="change"
@pickstart="pickStart"
@pickend="pickEnd"
>
<picker-view-column v-if="!reset">
<view class="tn-picker__content__body__item" v-for="(item, index) in range" :key="index">
<view class="tn-text-ellipsis">{{ getItemValue(item, 'selector') }}</view>
</view>
</picker-view-column>
</picker-view>
<!-- 多列选择 -->
<picker-view
v-else-if="mode === 'multiSelector'"
class="tn-picker__content__body__view"
:value="valueArr"
@change="change"
@pickstart="pickStart"
@pickend="pickEnd"
>
<picker-view-column v-if="!reset" v-for="(item, index) in range" :key="index">
<view class="tn-picker__content__body__item" v-for="(sub_item, sub_index) in item" :key="sub_index">
<view class="tn-text-ellipsis">{{ getItemValue(sub_item, 'multiSelector') }}</view>
</view>
</picker-view-column>
</picker-view>
</view>
</view>
</tn-popup>
</view>
</template>
<script>
import provinces from '../../libs/utils/province.js'
import citys from '../../libs/utils/city.js'
import areas from '../../libs/utils/area.js'
export default {
name: 'tn-picker',
props: {
value: {
type: Boolean,
default: false,
},
// 顶部标题
title: {
type: String,
default: ''
},
// picker中显示的参数
params: {
type: Object,
default() {
return {
year: true,
month: true,
day: true,
hour: false,
minute: false,
second: false,
province: true,
city: true,
area: true,
timestamp: true
}
}
},
// 模式选择,region-地区类型,time-时间类型,selector-单列模式,multiSelector-多列模式
mode: {
type: String,
default: 'time'
},
// 当mode=selector或者mode=multiSelector时,提供的数组
range: {
type: Array,
default() {
return []
}
},
// 当mode=selector或者mode=multiSelector时,提供的默认项下标
defaultSelector: {
type: Array,
default() {
return [0]
}
},
// 当range是一个Array<Object>时,通过rangeKey来指定Object中key的值作为显示的内容
rangeKey: {
type: String,
default: ''
},
// 时间模式 ,是否显示时间后的单位
showTimeTag: {
type: Boolean,
default: true
},
// 开始年份
startYear: {
type: [String, Number],
default: 1950
},
// 结束年份
endYear: {
type: [String, Number],
default: 2050
},
// 默认显示的时间
// 2021-09-01 || 2021-09-01 13:00:23 || 2021/09/01
defaultTime: {
type: String,
default: ''
},
// 默认显示的地区
// 可传类似["广东省", "广州市", "天河区"]
defaultRegin: {
type: Array,
default() {
return []
}
},
// 默认显示的地区编码
// 可传类似["11", "1101", "110101"]
// 如果defaultRegin和areaCode同时存在,优先使用areaCode的值
areaCode: {
type: Array,
default() {
return []
}
},
// 取消按钮的文字
cancelText: {
type: String,
default: '取消'
},
// 取消按钮的颜色
cancelColor: {
type: String,
default: ''
},
// 确认按钮的文字
confirmText: {
type: String,
default: '确认'
},
// 确认按钮的演示
confirmColor: {
type: String,
default: ''
},
safeAreaInsetBottom: {
type: Boolean,
default: false
},
// 是否允许通过点击遮罩关闭
maskCloseable: {
type: Boolean,
default: true
},
zIndex: {
type: Number,
default: 0
}
},
computed: {
// 监听参数变化
propsChange() {
return [this.mode, this.defaultTime, this.startYear, this.endYear, this.defaultRegin, this.areaCode]
},
// 监听地区发生变化
regionChange() {
return [this.province, this.city]
},
// 监听年月发生变化
yearAndMonth() {
return [this.year, this.month]
},
elZIndex() {
return this.zIndex ? this.zIndex : this.$t.zIndex.popup
}
},
data() {
return {
years: [],
months: [],
days: [],
hours: [],
minutes: [],
seconds: [],
year: 0,
month: 0,
day: 0,
hour: 0,
minute: 0,
second: 0,
reset: false,
startDate: '',
endDate: '',
valueArr: [],
provinces: provinces,
citys: citys[0],
areas: areas[0][0],
province: 0,
city: 0,
area: 0,
// 列是否还在滑动中,微信小程序如果在滑动中就点确定,结果可能不准确
moving: false
}
},
watch: {
propsChange() {
this.reset = true
setTimeout(() => this.init(), 10)
},
regionChange() {
// 如果地区发生变化,为了让picker联动起来,必须重置this.citys和this.areas
this.citys = citys[this.province]
this.areas = areas[this.province][this.city]
},
yearAndMonth() {
// 月份的变化,实时变更日的天数,因为不同月份,天数不一样
// 一个月可能有30,31天,甚至闰年2月的29天,平年2月28天
if (this.params.year) this.setDays()
},
value(val) {
// 微信和QQ小程序由于一些奇怪的原因(故同时对所有平台均初始化一遍),需要重新初始化才能显示正确的值
if (val) {
this.reset = true
setTimeout(() => this.init(), 10)
}
}
},
mounted() {
this.init()
},
methods: {
// 记录开始滑动
pickStart() {
// #ifdef MP-WEIXIN
this.moving = true
// #endif
},
// 记录滚动结束
pickEnd() {
// #ifdef MP-WEIXIN
this.moving = false
// #endif
},
// 根据传递的列表的数据获取显示的数据
getItemValue(item, mode) {
// 单列模式或者多列模式中的getItemValue同时被执行,故在这里再加一层判断
if (this.mode === mode) {
return typeof item === 'object' ? item[this.rangeKey] : item
}
},
// 往数字前面补0
formatNumber(num) {
return this.$t.number.formatNumberAddZero(num)
},
// 生成递进的数组
generateArray(start, end) {
// 转为数值格式,否则用户给end-year等传递字符串值时,下面的end+1会导致字符串拼接,而不是相加
start = Number(start)
end = Number(end)
end = end > start ? end : start
// 生成数组并获取其中索引然后提取出来(获取开始和结束之间的数据)
return [...Array(end+1).keys()].slice(start)
},
getIndex(arr, val) {
let index = arr.indexOf(val)
// 如果index为-1着找不到元素
// ~(-1)=(-1)-1=0
return ~index ? index : 0
},
// 日期时间处理
initTimeValue() {
// 格式化时间,在IE浏览器(uni不存在此情况),无法识别日期间的"-"间隔符号
let fdate = this.defaultTime.replace(/\-/g, '/')
fdate = fdate && fdate.indexOf('/') == -1 ? `2021/01/01 ${fdate}` : fdate
let time = null
if (fdate) time = new Date(fdate)
else time = new Date()
// 获取年月日时分秒
this.year = time.getFullYear()
this.month = time.getMonth() + 1
this.day = time.getDate()
this.hour = time.getHours()
this.minute = time.getMinutes()
this.second = time.getSeconds()
},
// 初始化数据
init() {
this.valueArr = []
this.reset = false
if (this.mode === 'time') {
this.initTimeValue()
if (this.params.year) {
this.valueArr.push(0)
this.setYears()
}
if (this.params.month) {
this.valueArr.push(0)
this.setMonths()
}
if (this.params.day) {
this.valueArr.push(0)
this.setDays()
}
if (this.params.hour) {
this.valueArr.push(0)
this.setHours()
}
if (this.params.minute) {
this.valueArr.push(0)
this.setMinutes()
}
if (this.params.second) {
this.valueArr.push(0)
this.setSeconds()
}
} else if (this.mode === 'region') {
if (this.params.province) {
this.valueArr.push(0)
this.setProvinces()
}
if (this.params.city) {
this.valueArr.push(0)
this.setCitys()
}
if (this.params.area) {
this.valueArr.push(0)
this.setAreas()
}
} else if (this.mode === 'selector') {
this.valueArr = this.defaultSelector
} else if (this.mode === 'multiSelector') {
this.valueArr = this.defaultSelector
this.multiSelectorValue = this.defaultSelector
}
this.$forceUpdate()
},
// 设置picker某一列的值
setYears() {
this.years = this.generateArray(this.startYear, this.endYear)
// 设置this.valueArr某一项的值,是为了让picker预选中某一个值
this.valueArr.splice(this.valueArr.length - 1, 1, this.getIndex(this.years, this.year))
},
setMonths() {
this.months = this.generateArray(1, 12)
this.valueArr.splice(this.valueArr.length - 1, 1, this.getIndex(this.months, this.month))
},
setDays() {
let totalDays = new Date(this.year, this.month, 0).getDate()
this.days = this.generateArray(1, totalDays)
let index = 0
// 避免多次触发导致值数组计算错误
if (this.params.year && this.params.month) index = 2
else if (this.params.month) index = 1
else if (this.params.year) index = 1
else index = 0
// 当月份变化时,会导致日期的天数也会变化,如果原来选的天数大于变化后的天数,则重置为变化后的最大值
// 比如原来选中3月31日,调整为2月后,日期变为最大29,这时如果day值继续为31显然不合理,于是将其置为29(picker-column从1开始)
if (this.day > this.days.length) this.day = this.days.length
this.valueArr.splice(index, 1, this.getIndex(this.days, this.day))
},
setHours() {
this.hours = this.generateArray(0, 23)
this.valueArr.splice(this.valueArr.length - 1, 1, this.getIndex(this.hours, this.hour))
},
setMinutes() {
this.minutes = this.generateArray(0, 59)
this.valueArr.splice(this.valueArr.length - 1, 1, this.getIndex(this.minutes, this.minute))
},
setSeconds() {
this.seconds = this.generateArray(0, 59)
this.valueArr.splice(this.valueArr.length - 1, 1, this.getIndex(this.seconds, this.second))
},
setProvinces() {
if (!this.params.province) return
let tmp = ''
let useCode = false
// 如果同时配置了defaultRegion和areaCode,优先使用areaCode参数
if (this.areaCode.length) {
tmp = this.areaCode[0]
useCode = true
} else if (this.defaultRegin.length) {
tmp = this.defaultRegin[0]
} else {
tmp = 0
}
// 遍历省份数组
provinces.map((v, k) => {
if (useCode ? v.value == tmp : v.label == tmp) {
this.province = k
return
}
})
this.provinces = provinces
this.valueArr.splice(0, 1, this.province)
},
setCitys() {
if (!this.params.city) return
let tmp = ''
let useCode = false
// 如果同时配置了defaultRegion和areaCode,优先使用areaCode参数
if (this.areaCode.length) {
tmp = this.areaCode[1]
useCode = true
} else if (this.defaultRegin.length) {
tmp = this.defaultRegin[1]
} else {
tmp = 0
}
// 遍历省份数组
citys[this.province].map((v, k) => {
if (useCode ? v.value == tmp : v.label == tmp) {
this.city = k
return
}
})
this.citys = citys[this.province]
this.valueArr.splice(1, 1, this.city)
},
setAreas() {
if (!this.params.area) return
let tmp = ''
let useCode = false
// 如果同时配置了defaultRegion和areaCode,优先使用areaCode参数
if (this.areaCode.length) {
tmp = this.areaCode[2]
useCode = true
} else if (this.defaultRegin.length) {
tmp = this.defaultRegin[2]
} else {
tmp = 0
}
// 遍历省份数组
areas[this.province][this.city].map((v, k) => {
if (useCode ? v.value == tmp : v.label == tmp) {
this.area = k
return
}
})
this.areas = areas[this.province][this.city]
this.valueArr.splice(2, 1, this.area)
},
close() {
this.$emit('input', false)
},
// 监听用户修改了picker列选项
change(event) {
this.valueArr = event.detail.value
let i = 0
if (this.mode === 'time') {
// 使用i++是因为不知道数组的长度
if (this.params.year) this.year = this.years[this.valueArr[i++]]
if (this.params.month) this.month = this.months[this.valueArr[i++]]
if (this.params.day) this.day = this.days[this.valueArr[i++]]
if (this.params.hour) this.hour = this.hours[this.valueArr[i++]]
if (this.params.minute) this.minute = this.minutes[this.valueArr[i++]]
if (this.params.second) this.second = this.seconds[this.valueArr[i++]]
} else if (this.mode === 'region') {
// 标记省市是否发生了变化
let provinceChange = false,
cityChange = false
if (this.params.province) {
let value = this.valueArr[i++]
if (this.province != value) {
// 如果省份发生了变化,则重置市区的索引为0
this.city = 0
this.area = 0
provinceChange = true
}
this.province = value
}
if (this.params.city && !provinceChange) {
let value = this.valueArr[i++]
if (this.city != value) {
// 如果市发生了变化,则重置区的索引为0
this.area = 0
cityChange = true
}
this.city = value
}
if (this.params.area && !provinceChange && !cityChange) this.area = this.valueArr[i++]
// 如果有省市进行了改变,重新设置列表的值
if (provinceChange || cityChange) {
this.valueArr = [this.province, this.city, this.area]
}
} else if (this.mode === 'multiSelector') {
let index = null
// 对比前后两个数组,寻找变更的是哪一列,如果某一个元素不同,即可判定该列发生了变化
this.defaultSelector.map((v, idx) => {
if (v != event.detail.value[idx]) index = idx
})
// 为了让用户对多列变化时,动态设置其他列
if (index != null) {
this.$emit('columnchange', {
column: index,
index: event.detail.value[index]
})
}
}
},
// 用户点击确定按钮
getResult(event = null) {
// #ifdef MP-WEIXIN
if (this.moving) return
// #endif
let result = {}
// 只返回用户需要的数据
if (this.mode === 'time') {
if (this.params.year) result.year = this.formatNumber(this.year || 0)
if (this.params.month) result.month = this.formatNumber(this.month || 0)
if (this.params.day) result.day = this.formatNumber(this.day || 0)
if (this.params.hour) result.hour = this.formatNumber(this.hour || 0)
if (this.params.minute) result.minute = this.formatNumber(this.minute || 0)
if (this.params.second) result.second = this.formatNumber(this.second || 0)
if (this.params.timestamp) result.timestamp = this.getTimestamp()
} else if (this.mode === 'region') {
if (this.params.province) result.province = provinces[this.province]
if (this.params.city) result.city = citys[this.province][this.city]
if (this.params.area) result.area = areas[this.province][this.city][this.area]
} else if (this.mode === 'multiSelector') {
result = this.valueArr
} else if (this.mode === 'selector') {
result = this.valueArr
}
if (event) this.$emit(event, result)
this.close()
},
// 获取时间戳
getTimestamp() {
// yyyy-mm-dd为安卓写法,不支持iOS,需要使用"/"分隔,才能二者兼容
let time = this.year + '/' + this.month + '/' + this.day + ' ' + this.hour + ':' + this.minute + ':' + this.second;
return new Date(time).getTime() / 1000;
}
}
}
</script>
<style lang="scss" scoped>
.tn-picker {
&__content {
position: relative;
&__header {
position: relative;
display: flex;
flex-direction: row;
width: 100%;
height: 90rpx;
padding: 0 40rpx;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
font-size: 30rpx;
background-color: #FFFFFF;
&__btn {
padding: 16rpx;
box-sizing: border-box;
text-align: center;
text-decoration: none;
}
&__title {
color: $tn-font-color;
}
&--cancel {
color: $tn-font-sub-color;
}
&--confirm {
color: $tn-main-color;
}
}
&__body {
width: 100%;
height: 500rpx;
overflow: hidden;
background-color: #FFFFFF;
&__view {
height: 100%;
box-sizing: border-box;
}
&__item {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
font-size: 32rpx;
color: $tn-font-color;
padding: 0 8rpx;
&--text {
font-size: 24rpx;
padding-left: 8rpx;
}
}
}
}
}
</style>
+484
View File
@@ -0,0 +1,484 @@
<template>
<view
v-if="visibleSync"
class="tn-popup-class tn-popup"
:style="[customStyle, popupStyle, { zIndex: elZIndex - 1}]"
hover-stop-propagation
>
<!-- mask -->
<view
class="tn-popup__mask"
:class="[{'tn-popup__mask--show': showPopup && mask}]"
:style="{zIndex: elZIndex - 2}"
@tap="maskClick"
@touchmove.stop.prevent = "() => {}"
hover-stop-propagation
></view>
<!-- 弹框内容 -->
<view
class="tn-popup__content"
:class="[
mode !== 'center' ? backgroundColorClass : '',
safeAreaInsetBottom ? 'tn-safe-area-inset-bottom' : '',
'tn-popup--' + mode,
showPopup ? 'tn-popup__content--visible' : '',
zoom && mode === 'center' ? 'tn-popup__content__center--animation-zoom' : ''
]"
:style="[contentStyle]"
@tap="modeCenterClose"
@touchmove.stop.prevent
@tap.stop.prevent
>
<!-- 居中时候的内容 -->
<view
v-if="mode === 'center'"
class="tn-popup__content__center_box"
:class="[backgroundColorClass]"
:style="[centerStyle]"
@touchmove.stop.prevent
@tap.stop.prevent
>
<!-- 关闭按钮 -->
<view
v-if="closeBtn"
class="tn-popup__close"
:class="[`tn-icon-${closeBtnIcon}`, `tn-popup__close--${closeBtnPosition}`]"
:style="[closeBtnStyle, {zIndex: elZIndex}]"
@tap="close"
></view>
<scroll-view class="tn-popup__content__scroll-view">
<slot></slot>
</scroll-view>
</view>
<!-- 除居中外的其他情况 -->
<scroll-view v-else class="tn-popup__content__scroll-view">
<slot></slot>
</scroll-view>
<!-- 关闭按钮 -->
<view
v-if="mode !== 'center' && closeBtn"
class="tn-popup__close"
:class="[`tn-popup__close--${closeBtnPosition}`]"
:style="{zIndex: elZIndex}"
@tap="close"
>
<view :class="[`tn-icon-${closeBtnIcon}`]" :style="[closeBtnStyle]"></view>
</view>
</view>
</view>
</template>
<script>
import componentsColorMixin from '../../libs/mixin/components_color.js'
export default {
mixins: [componentsColorMixin],
name: 'tn-popup',
props: {
value: {
type: Boolean,
default: false
},
// 弹出方向
// left/right/top/bottom/center
mode: {
type: String,
default: 'left'
},
// 是否显示遮罩
mask: {
type: Boolean,
default: true
},
// 抽屉的宽度(mode=left/right,高度(mode=top/bottom
length: {
type: [Number, String],
default: 'auto'
},
// 宽度,只对左,右,中部弹出时起作用,单位rpx,或者"auto"
// 或者百分比"50%",表示由内容撑开高度或者宽度,优先级高于length参数
width: {
type: String,
default: ''
},
// 高度,只对上,下,中部弹出时起作用,单位rpx,或者"auto"
// 或者百分比"50%",表示由内容撑开高度或者宽度,优先级高于length参数
height: {
type: String,
default: ''
},
// 是否开启动画,只在mode=center有效
zoom: {
type: Boolean,
default: true
},
// 是否开启底部安全区适配,开启的话,会在iPhoneX机型底部添加一定的内边距
safeAreaInsetBottom: {
type: Boolean,
default: false
},
// 是否可以通过点击遮罩进行关闭
maskCloseable: {
type: Boolean,
default: true
},
// 用户自定义样式
customStyle: {
type: Object,
default() {
return {}
}
},
// 显示圆角的大小
borderRadius: {
type: Number,
default: 0
},
// zIndex
zIndex: {
type: Number,
default: 0
},
// 是否显示关闭按钮
closeBtn: {
type: Boolean,
default: false
},
// 关闭按钮的图标
closeBtnIcon: {
type: String,
default: 'close'
},
// 关闭按钮显示的位置
// top-left/top-right/bottom-left/bottom-right
closeBtnPosition: {
type: String,
default: 'top-right'
},
// 关闭按钮图标颜色
closeIconColor: {
type: String,
default: '#AAAAAA'
},
// 关闭按钮图标的大小
closeIconSize: {
type: Number,
default: 30
},
// 给一个负的margin-top,往上偏移,避免和键盘重合的情况,仅在mode=center时有效
negativeTop: {
type: Number,
default: 0
},
// marginTop,在mode = top,left,right时生效,避免用户使用了自定义导航栏,组件把导航栏遮挡了
marginTop: {
type: Number,
default: 0
},
// 此为内部参数,不在文档对外使用,为了解决Picker和keyboard等融合了弹窗的组件
// 对v-model双向绑定多层调用造成报错不能修改props值的问题
popup: {
type: Boolean,
default: true
},
},
computed: {
// 处理使用了自定义导航栏时被遮挡的问题
popupStyle() {
let style = {}
if ((this.mode === 'top' || this.mode === 'left' || this.mode === 'right') && this.marginTop) {
style.marginTop = this.$t.string.getLengthUnitValue(this.marginTop, 'px')
}
return style
},
// 根据mode的位置,设定其弹窗的宽度(mode = left|right),或者高度(mode = top|bottom)
contentStyle() {
let style = {}
// 如果是左边或者上边弹出时,需要给translate设置为负值,用于隐藏
if (this.mode === 'left' || this.mode === 'right') {
style = {
width: this.width ? this.$t.string.getLengthUnitValue(this.width) : this.$t.string.getLengthUnitValue(this.length),
height: '100%',
transform: `translate3D(${this.mode === 'left' ? '-100%' : '100%'}, 0px, 0px)`
}
} else if (this.mode === 'top' || this.mode === 'bottom') {
style = {
width: '100%',
height: this.height ? this.$t.string.getLengthUnitValue(this.height) : this.$t.string.getLengthUnitValue(this.length),
transform: `translate3D(0px, ${this.mode === 'top' ? '-100%': '100%'}, 0px)`
}
}
style.zIndex = this.elZIndex
// 如果设置了圆角的值,添加弹窗的圆角
if (this.borderRadius) {
switch(this.mode) {
case 'left':
style.borderRadius = `0 ${this.borderRadius}rpx ${this.borderRadius}rpx 0`
break
case 'top':
style.borderRadius = `0 0 ${this.borderRadius}rpx ${this.borderRadius}rpx`
break
case 'right':
style.borderRadius = `${this.borderRadius}rpx 0 0 ${this.borderRadius}rpx`
break
case 'bottom':
style.borderRadius = `${this.borderRadius}rpx ${this.borderRadius}rpx 0 0`
break
}
style.overflow = 'hidden'
}
if (this.backgroundColorStyle && this.mode !== 'center') {
style.backgroundColor = this.backgroundColorStyle
}
return style
},
// 中部弹窗的样式
centerStyle() {
let style = {}
style.width = this.width ? this.$t.string.getLengthUnitValue(this.width) : this.$t.string.getLengthUnitValue(this.length)
// 中部弹出的模式,如果没有设置高度,就用auto值,由内容撑开
style.height = this.height ? this.$t.string.getLengthUnitValue(this.height) : 'auto'
style.zIndex = this.elZIndex
if (this.negativeTop) {
style.marginTop = `-${this.$t.string.getLengthUnitValue(this.negativeTop)}`
}
if (this.borderRadius) {
style.borderRadius = `${this.borderRadius}rpx`
style.overflow='hidden'
}
if (this.backgroundColorStyle) {
style.backgroundColor = this.backgroundColorStyle
}
return style
},
// 关闭按钮样式
closeBtnStyle() {
let style = {}
if (this.closeIconColor) {
style.color = this.closeIconColor
}
if (this.closeIconSize) {
style.fontSize = this.closeIconSize + 'rpx'
}
return style
},
elZIndex() {
return this.zIndex ? this.zIndex : this.$t.zIndex.popup
}
},
data() {
return {
timer: null,
visibleSync: false,
showPopup: false,
closeFromInner: false
}
},
watch: {
value(val) {
if (val) {
this.open()
} else if (!this.closeFromInner) {
this.close()
}
this.closeFromInner = false
}
},
mounted() {
// 组件渲染完成时,检查value是否为true,如果是,弹出popup
this.value && this.open()
},
methods: {
// 点击遮罩
maskClick() {
if (!this.maskCloseable) return
this.close()
},
open() {
this.change('visibleSync', 'showPopup', true)
},
// 关闭弹框
close() {
// 标记关闭是内部发生的,否则修改了value值,导致watch中对value检测,导致再执行一遍close
// 造成@close事件触发两次
this.closeFromInner = true
this.change('showPopup', 'visibleSync', false)
},
// 中部弹出时,需要.tn-drawer-content将内容居中,此元素会铺满屏幕,点击需要关闭弹窗
// 让其只在mode=center时起作用
modeCenterClose() {
if (this.mode != 'center' || !this.maskCloseable) return
this.close()
},
// 关闭时先通过动画隐藏弹窗和遮罩,再移除整个组件
// 打开时,先渲染组件,延时一定时间再让遮罩和弹窗的动画起作用
change(param1, param2, status) {
// 如果this.popup为false,意味着为pickeractionsheet等组件调用了popup组件
if (this.popup === true) {
this.$emit('input', status)
}
this[param1] = status
if (status) {
// #ifdef H5 || MP
this.timer = setTimeout(() => {
this[param2] = status
this.$emit(status ? 'open' : 'close')
}, 50)
// #endif
// #ifndef H5 || MP
this.$nextTick(() => {
this[param2] = status
this.$emit(status ? 'open' : 'close')
})
// #endif
} else {
this.timer = setTimeout(() => {
this[param2] = status
this.$emit(status ? 'open' : 'close')
}, 250)
}
}
}
}
</script>
<style lang="scss" scoped>
.tn-popup {
/* #ifndef APP-NVUE */
display: block;
/* #endif */
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
&__content {
/* #ifndef APP-NVUE */
display: block;
/* #endif */
position: absolute;
transition: all 0.25s linear;
&--visible {
transform: translate3D(0px, 0px, 0px) !important;
&.tn-popup--center {
transform: scale(1);
opacity: 1;
}
}
&__center_box {
min-width: 100rpx;
min-height: 100rpx;
/* #ifndef APP-NVUE */
display: block;
/* #endif */
position: relative;
background-color: #FFFFFF;
}
&__scroll-view {
width: 100%;
height: 100%;
}
&__center--animation-zoom {
transform: scale(1.15);
}
}
&__scroll_view {
width: 100%;
height: 100%;
}
&--left {
top: 0;
bottom: 0;
left: 0;
background-color: #FFFFFF;
}
&--right {
top: 0;
bottom: 0;
right: 0;
background-color: #FFFFFF;
}
&--top {
left: 0;
right: 0;
top: 0;
background-color: #FFFFFF;
}
&--bottom {
left: 0;
right: 0;
bottom: 0;
background-color: #FFFFFF;
}
&--center {
display: flex;
flex-direction: column;
bottom: 0;
top: 0;
left: 0;
right: 0;
justify-content: center;
align-items: center;
opacity: 0;
}
&__close {
position: absolute;
&--top-left {
top: 30rpx;
left: 30rpx;
}
&--top-right {
top: 30rpx;
right: 30rpx;
}
&--bottom-left {
bottom: 30rpx;
left: 30rpx;
}
&--bottom-right {
bottom: 30rpx;
right: 30rpx;
}
}
&__mask {
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
right: 0;
border: 0;
background-color: $tn-mask-bg-color;
transition: 0.25s ease-in-out;
transition-property: opacity;
opacity: 0;
&--show {
opacity: 1;
}
}
}
</style>
@@ -0,0 +1,124 @@
<template>
<view class="tn-radio-group-class tn-radio-group">
<slot></slot>
</view>
</template>
<script>
import Emitter from '../../libs/utils/emitter.js'
export default {
mixins: [ Emitter ],
name: 'tn-radio-group',
props: {
// 单选组的值,会选中值相同的选项
value: {
type: String,
default: ''
},
// 是否禁用
disabled: {
type: Boolean,
default: false
},
// 禁用点击标签进行选择
disabledLabel: {
type: Boolean,
default: false
},
// 选择框的形状 square 方形 circle 圆形
shape: {
type: String,
default: 'circle'
},
// 选中时的颜色
activeColor: {
type: String,
default: '#01BEFF'
},
// 组件大小
size: {
type: Number,
default: 34
},
// 每个radio占的宽度
width: {
type: String,
default: 'auto'
},
// 是否换行
wrap: {
type: Boolean,
default: false
},
// 图标大小
iconSize: {
type: Number,
default: 20
}
},
computed: {
// 这里computed的变量,都是子组件tn-radio需要用到的,由于头条小程序的兼容性差异,子组件无法实时监听父组件参数的变化
// 所以需要手动通知子组件,这里返回一个parentData变量,供watch监听,在其中去通知每一个子组件重新从父组件(tn-radio-group)
// 拉取父组件新的变化后的参数
parentData() {
return [this.value, this.disabled, this.activeColor, this.size, this.disabledLabel, this.shape, this.iconSize, this.width, this.wrap]
}
},
data() {
return {
}
},
watch: {
// 当父组件中的子组件需要共享的参数发生了变化,手动通知子组件
parentData() {
if (this.children.length) {
this.children.map(child => {
// 判断子组件(tn-radio)如果有updateParentData方法的话,子组件重新从父组件拉取了最新的值
typeof(child.updateParentData) === 'function' && child.updateParentData()
})
}
}
},
created() {
// 如果将children定义在data中,在微信小程序会造成循环引用而报错
this.children = []
},
methods: {
// 改方法由子组件tn-radio调用,当一个tn-radio被选中的时候,给父组件设置value值(通知其他tn-radio组件)
setValue(value) {
// 通过子组件传递过来的value值(此被选中的子组件内部已将parentValue设置等于value的值),将其他tn-radio设置未选中的状态
this.children.map(child => {
if (child.parentData.value !== value) child.parentData.value = ''
})
// 通过emit事件,设置父组件通过v-model双向绑定的值
this.$emit('input', value)
this.$emit('change', value)
// 等待下一个周期再执行,因为this.$emit('input')作用于父组件,再反馈到子组件内部,需要时间
// 由于头条小程序执行迟钝,故需要用几十毫秒的延时
setTimeout(() => {
// 将当前的值发送到 tn-form-item 进行校验
this.dispatch('tn-form-item', 'on-form-change', value);
}, 60)
}
}
}
</script>
<style lang="scss" scoped>
.tn-radio-group {
/* #ifndef MP || APP-NVUE */
display: inline-flex;
flex-wrap: wrap;
/* #endif */
&::after {
content: " ";
display: table;
clear: both;
}
}
</style>
+265
View File
@@ -0,0 +1,265 @@
<template>
<view class="tn-radio-class tn-radio" :style="[radioStyle]">
<view
class="tn-radio__icon-wrap"
:class="[iconClass]"
:style=[iconStyle]
@tap="toggle"
>
<view class="tn-icon-success tn-radio__icon-wrap__icon"></view>
</view>
<view
class="tn-radio__label"
:style="{
fontSize: labelSize ? labelSize + 'rpx' : ''
}"
@tap="onClickLabel"
>
<slot></slot>
</view>
</view>
</template>
<script>
export default {
name: 'tn-radio',
props: {
// radio名称
name: {
type: [String, Number],
default: ''
},
// 是否禁用
disabled: {
type: Boolean,
default: false
},
// 禁用点击标签进行选择
disabledLabel: {
type: Boolean,
default: false
},
// 选择框的形状 square 方形 circle 圆形
shape: {
type: String,
default: ''
},
// 选中时的颜色
activeColor: {
type: String,
default: ''
},
// 组件尺寸
size: {
type: Number,
default: 0
},
// 图标大小
iconSize: {
type: Number,
default: 0
},
// label字体大小
labelSize: {
type: Number,
default: 0
}
},
computed: {
// 禁用,父组件会覆盖子组件的状态
isDisabled() {
return this.disabled ? this.disabled : (this.parentData.disabled ? this.parentData.disabled : false)
},
// 禁用label点击,父组件会覆盖子组件的状态
isDisabledLabel() {
return this.disabledLabel ? this.disabledLabel : (this.parentData.disabledLabel ? this.parentData.disabledLabel : false)
},
// 组件尺寸
elSize() {
return this.size ? this.size : (this.parentData.size ? this.parentData.size : 34)
},
// 组件选中时的颜色
elActiveColor() {
return this.activeColor ? this.activeColor : (this.parentData.activeColor ? this.parentData.activeColor : '#01BEFF')
},
// 组件形状
elShape() {
return this.shape ? this.shape : (this.parentData.shape ? this.parentData.shape : 'circle')
},
iconClass() {
let clazz = ''
clazz += (' tn-radio__icon-wrap--' + this.elShape)
if (this.parentData.value === this.name) clazz += ' tn-radio__icon-wrap--checked'
if (this.isDisabled) clazz += ' tn-radio__icon-wrap--disabled'
if (this.parentData.value === this.name && this.isDisabled) clazz += ' tn-radio__icon-wrap--disabled--checked'
return clazz
},
iconStyle() {
// 当前radio的name等于parent的value才认为时选中
let style = {}
if (this.elActiveColor && this.parentData.value === this.name && !this.isDisabled) {
style.borderColor = this.elActiveColor
style.backgroundColor = this.elActiveColor
}
style.color = this.parentData.value === this.name ? '#FFFFFF' : 'transparent'
style.width = this.elSize + 'rpx'
style.height = style.width
style.fontSize = (this.iconSize ? this.iconSize : (this.parentData.iconSize ? this.parentData.iconSize : 20)) + 'rpx'
return style
},
radioStyle() {
let style = {}
if (this.parent && this.parentData.width) {
// #ifdef MP
// 各家小程序因为它们特殊的编译结构,使用float布局
style.float = 'left';
// #endif
// #ifndef MP
// H5和APP使用flex布局
style.flex = `0 0 ${this.parentData.width}`;
// #endif
}
if(this.parent && this.parentData.wrap) {
style.width = '100%';
// #ifndef MP
// H5和APP使用flex布局,将宽度设置100%,即可自动换行
style.flex = '0 0 100%';
// #endif
}
return style
}
},
data() {
return {
// 父组件的默认值,因为头条小程序不支持在computed中使用this.parent.xxx的形式
// 故只能使用如此方法
parentData: {
value: null,
disabled: null,
disabledLabel: null,
shape: null,
activeColor: null,
size: null,
width: null,
wrap: null,
iconSize: null,
}
}
},
created() {
// 支付宝小程序不支持provide/inject,所以使用这个方法获取整个父组件,在created定义,避免循环引用
this.updateParentData()
this.parent.children.push(this)
},
methods: {
updateParentData() {
this.getParentData('tn-radio-group')
},
onClickLabel() {
if (!this.isDisabledLabel && !this.isDisabled) {
this.setRadioCheckedStatus()
}
},
toggle() {
if (!this.isDisabled) {
this.setRadioCheckedStatus()
}
},
emitEvent() {
// tn-radio的name不等于父组件的v-model的值时(意味着未选中),才发出事件,避免多次点击触发事件
if (this.parentData.value !== this.name) this.$emit('change', this.name)
},
// 改变选中的状态
// 更改本组件的parentData.value的值为本组件的name值,同时通过父组件遍历所有的radio实例
// 将本组件外的其他radio的parentData.value都设置为空
setRadioCheckedStatus() {
this.emitEvent()
if (this.parent) {
this.parent.setValue(this.name)
this.parentData.value = this.name
}
}
}
}
</script>
<style lang="scss" scoped>
.tn-radio {
/* #ifndef APP-NVUE */
display: inline-flex;
/* #endif */
align-items: center;
overflow: hidden;
user-select: none;
line-height: 1.8;
&__icon-wrap {
color: $tn-font-color;
display: flex;
flex-direction: row;
flex: none;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: 42rpx;
height: 42rpx;
color: transparent;
text-align: center;
transition-property: color, border-color, background-color;
font-size: 20rpx;
border: 1rpx solid $tn-font-sub-color;
transition-duration: 0.2s;
/* #ifdef MP-TOUTIAO */
// 头条小程序兼容性问题,需要设置行高为0,否则图标偏下
&__icon {
line-height: 0;
}
/* #endif */
&--circle {
border-radius: 100%;
}
&--square {
border-radius: 6rpx;
}
&--checked {
color: #FFFFFF;
background-color: $tn-main-color;
border-color: $tn-main-color;
}
&--disabled {
background-color: $tn-font-holder-color;
border-color: $tn-font-sub-color;
}
&--disabled--checked {
color: $tn-font-sub-color !important;
}
}
&__label {
word-wrap: break-word;
margin-left: 10rpx;
margin-right: 24rpx;
color: $tn-font-color;
font-size: 30rpx;
&--disabled {
color: $tn-font-sub-color;
}
}
}
</style>
+334
View File
@@ -0,0 +1,334 @@
<template>
<view
:id="elId"
class="tn-rate-class tn-rate"
@touchmove.stop.prevent="touchMove"
>
<view class="tn-rate__wrap" :class="[elClass]" v-for="(item, index) in count" :key="index">
<view
class="tn-rate__wrap__icon"
:class="[`tn-icon-${(allowHalf && halfIcon ? activeIndex > index + 1 : activeIndex > index) ? elActionIcon : elInactionIcon}`]"
:style="[iconStyle(index)]"
@tap="click(index + 1, $event)"
>
<!-- 半图标 -->
<view
v-if="showHalfIcon(index)"
class="tn-rate__wrap__icon--half"
:class="[`tn-icon-${elActionIcon}`]"
:style="[halfIconStyle]"
></view>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'tn-rate',
props: {
// 选中星星的数量
value: {
type: Number,
default: 0
},
// 显示的星星数
count: {
type: Number,
default: 5
},
// 最小能选择的星星数
minCount: {
type: Number,
default: 0
},
// 禁用状态
disabled: {
type: Boolean,
default: false
},
// 是否可以选择半星
allowHalf: {
type: Boolean,
default: false
},
// 星星大小
size: {
type: Number,
default: 32
},
// 被选中的图标
activeIcon: {
type: String,
default: 'star-fill'
},
// 未被选中的图标
inactiveIcon: {
type: String,
default: 'star'
},
// 被选中的颜色
activeColor: {
type: String,
default: '#01BEFF'
},
// 默认颜色
inactiveColor: {
type: String,
default: '#AAAAAA'
},
// 星星之间的距离
gutter: {
type: Number,
default: 10
},
// 自定义颜色
colors: {
type: Array,
default() {
return []
}
},
// 自定义图标
icons: {
type: Array,
default() {
return []
}
}
},
computed: {
// 图标显示的比例
showHalfIcon(index) {
return index => {
return this.allowHalf && Math.ceil(this.activeIndex) === index + 1 && this.halfIcon
}
},
// 被激活的图标
elActionIcon() {
const len = this.icons.length
// icons参数传递了3个图标,当选中2个时,用第一个图标,4个时,用第二个图标
// 5个时,用第三个图标作为激活的图标
if (len && len <= this.count) {
const step = Math.round(Math.ceil(this.activeIndex) / Math.round(this.count / len))
if (step < 1) return this.icons[0]
if (step > len) return this.icons[len - 1]
return this.icons[step - 1]
}
return this.activeIcon
},
// 未被激活的图标
elInactionIcon() {
const len = this.icons.length
// icons参数传递了3个图标,当选中2个时,用第一个图标,4个时,用第二个图标
// 5个时,用第三个图标作为激活的图标
if (len && len <= this.count) {
const step = Math.round(Math.ceil(this.activeIndex) / Math.round(this.count / len))
if (step < 1) return this.icons[0]
if (step > len) return this.icons[len - 1]
return this.icons[step - 1]
}
return this.inactiveIcon
},
// 被激活的颜色
elActionColor() {
const len = this.colors.length
// 如果有设置colors参数(此参数用于将图标分段,比如一共5颗星,colors传3个颜色值,那么根据一定的规则,2颗星可能为第一个颜色
// 4颗星为第二个颜色值,5颗星为第三个颜色值)
if (len && len <= this.count) {
const step = Math.round(Math.ceil(this.activeIndex) / Math.round(this.count / len))
if (step < 1) return this.colors[0]
if (step > len) return this.colors[len - 1]
return this.colors[step - 1]
}
return this.activeColor
},
// 图标的样式
iconStyle() {
return index => {
let style = {}
style.fontSize = this.$t.string.getLengthUnitValue(this.size)
style.padding = `0 ${this.$t.string.getLengthUnitValue(this.gutter / 2)}`
// 当前图标的颜色
if (this.allowHalf && this.halfIcon) {
style.color = this.activeIndex > index + 1 ? this.elActionColor : this.inactiveColor
} else {
style.color = this.activeIndex > index ? this.elActionColor : this.inactiveColor
}
return style
}
},
// 半图标样式
halfIconStyle() {
let style = {}
style.fontSize = this.$t.string.getLengthUnitValue(this.size)
style.padding = `0 ${this.$t.string.getLengthUnitValue(this.gutter / 2)}`
style.color = this.elActionColor
return style
}
},
data() {
return {
// 保证控件的唯一性
elId: this.$t.uuid(),
elClass: this.$t.uuid(),
// 评分盒子左边到屏幕左边的距离,用于滑动选择时计算距离
starBoxLeft: 0,
// 当前激活的星星的序号
activeIndex: this.value,
// 每个星星的宽度
starWidth: 0,
// 每个星星最右边到盒子组件最左边的距离
starWidthArr: [],
// 标记是否为半图标
halfIcon: false,
}
},
watch: {
value(val) {
this.activeIndex = val
if (this.allowHalf && (val % 1 === 0.5)) {
this.halfIcon = true
} else {
this.halfIcon = false
}
},
size() {
// 当尺寸修改的时候重新获取布局尺寸信息
this.$nextTick(() => {
this.getElRectById()
this.getElRectByClass()
})
}
},
mounted() {
this.getElRectById()
this.getElRectByClass()
},
methods: {
// 获取评分组件盒子的布局信息
getElRectById() {
this._tGetRect('#'+this.elId).then(res => {
this.starBoxLeft = res.left
})
},
// 获取单个星星的尺寸
getElRectByClass() {
this._tGetRect('.'+this.elClass).then(res => {
this.starWidth = res.width
// 把每个星星最右边到盒子最左边的距离
for (let i = 0; i < this.count; i++) {
this.starWidthArr[i] = (i + 1) * this.starWidth
}
})
},
// 手指滑动
touchMove(e) {
if (this.disabled) return
if (!e.changedTouches[0]) return
const movePageX = e.changedTouches[0].pageX
// 滑动点相对于评分盒子左边的距离
const distance = movePageX - this.starBoxLeft
// 如果滑动到了评分盒子的左边界,设置为0星
if (distance <= 0) {
this.activeIndex = 0
}
// 计算滑动的距离相当于点击多少颗星星
let index = Math.ceil(distance / this.starWidth)
if (this.allowHalf) {
const iconHalfWidth = this.starWidthArr[index - 1] - (this.starWidth / 2)
if (distance < iconHalfWidth) {
this.halfIcon = true
index -= 0.5
} else {
this.halfIcon = false
}
}
this.activeIndex = index > this.count ? this.count : index
if (this.activeIndex < this.minCount) this.activeIndex = this.minCount
this.emitEvent()
},
// 通过点击直接选中
click(index, e) {
if (this.disabled) return
// 半星选择
if (this.allowHalf) {
if (!e.changedTouches[0]) return
const movePageX = e.changedTouches[0].pageX
// 点击点相对于当前图标左边的距离
const distance = movePageX - this.starBoxLeft
const iconHalfWidth = this.starWidthArr[index - 1] - (this.starWidth / 2)
if (distance < iconHalfWidth) {
this.halfIcon = true
} else {
this.halfIcon = false
}
}
// 对第一个星星特殊处理,只有一个的时候,点击可以取消,否则无法作0星评价
if (index == 1) {
if (this.allowHalf && this.allowHalf) {
if ((this.activeIndex === 0.5 && this.halfIcon) ||
(this.activeIndex === 1 && !this.halfIcon)) {
this.activeIndex = 0
} else {
this.activeIndex = this.halfIcon ? 0.5 : 1
}
} else {
if (this.activeIndex == 1) {
this.activeIndex = 0
} else {
this.activeIndex = 1
}
}
} else {
this.activeIndex = (this.allowHalf && this.halfIcon) ? index - 0.5 : index
}
if (this.activeIndex < this.minCount) this.activeIndex = this.minCount
this.emitEvent()
},
// 发送事件
emitEvent() {
this.$emit('change', this.activeIndex)
// 修改v-model的值
this.$emit('input', this.activeIndex)
}
}
}
</script>
<style lang="scss" scoped>
.tn-rate {
display: inline-flex;
align-items: center;
margin: 0;
padding: 0;
&__wrap {
&__icon {
position: relative;
box-sizing: border-box;
&--half {
position: absolute;
top: 0;
left: 0;
display: inline-block;
overflow: hidden;
width: 50%;
}
}
}
}
</style>
@@ -0,0 +1,222 @@
<template>
<view>
<view class="tn-read-more-class tn-read-more">
<!-- 内容 -->
<view
:id="elId"
class="tn-read-more__content"
:style="[contentStyle]"
>
<slot></slot>
</view>
<!-- 展开收起按钮 -->
<view
v-if="isLongContent"
class="tn-read-more__show-more__wrap"
:class="{'tn-read-more__show-more': showMore}"
:style="[innerShadowStyle]"
@tap="toggleReadMore">
<text class="tn-read-more__show-more--text" :style="[fontStyle]">{{ showMore ? closeText : openText }}</text>
<view class="tn-read-more__show-more--icon">
<view :class="[tipIconClass]" :style="[fontStyle]"></view>
</view>
</view>
</view>
</view>
</template>
<script>
import componentsColorMixin from '../../libs/mixin/components_color.js'
export default {
name: 'tn-read-more',
mixins: [componentsColorMixin],
props: {
// 默认占位高度
showHeight: {
type: Number,
default: 400
},
// 显示收起按钮
closeBtn: {
type: Boolean,
default: false
},
// 展开提示文字
openText: {
type: String,
default: '展开阅读全文'
},
// 收起提示文字
closeText: {
type: String,
default: '收起'
},
// 展开提示图标
openIcon: {
type: String,
default: 'down'
},
// 收起提示图标
closeIcon: {
type: String,
default: 'up'
},
// 阴影样式
shadowStyle: {
type: Object,
default () {
return {
backgroundImage: "linear-gradient(-180deg, rgba(255, 255, 255, 0) 0%, #fff 80%)",
paddingTop: "300rpx",
marginTop: "-300rpx"
}
}
},
// 组件标识
index: {
type: [Number, String],
default: ''
}
},
computed: {
paramsChange() {
return `${this.open}-${this.showHeight}`
},
contentStyle() {
let style = {}
if (this.isLongContent && !this.showMore) {
style.height = `${this.showHeight}rpx`
} else {
if (!this.initHeight) {
style.height = 'auto'
} else {
style.height = `${this.contentHeight}px`
}
}
return style
},
innerShadowStyle() {
let style = {}
// 折叠时才需要阴影样式
if (!this.showMore) {
style = Object.assign(style, this.shadowStyle)
}
return style
},
fontStyle() {
let style = {}
style.color = this.fontColorStyle ? this.fontColorStyle : '#01BEFF'
style.fontSize = this.fontSizeStyle ? this.fontSizeStyle : '28rpx'
return style
},
tipIconClass() {
if (this.showMore) {
if (this.closeIcon) {
return `tn-icon-${this.closeIcon}`
}
} else {
if (this.openIcon) {
return `tn-icon-${this.openIcon}`
}
}
}
},
data() {
return {
elId: this.$t.uuid(),
// 标记是否已经初始化高度完成
initHeight: false,
// 是否需要隐藏一部分内容
isLongContent: false,
// 当前展开的打开、关闭状态
showMore: false,
// 内容的高度
contentHeight: 0
}
},
watch: {
paramsChange(value) {
this.initHeight = false
this.isLongContent = false
this.showMore = true
this.$nextTick(() => {
this.init()
})
}
},
mounted() {
this.$nextTick(() => {
this.init()
})
},
methods: {
// 初始化组件
init() {
// 判断高度,如果真实内容的高度大于占位高度,则显示展开与收起的控制按钮
this._tGetRect('#' + this.elId).then(res => {
this.contentHeight = res.height
this.initHeight = true
if (res.height > uni.upx2px(this.showHeight)) {
this.isLongContent = true
this.showMore = false
}
})
},
// 展开或者收起内容
toggleReadMore() {
this.showMore = !this.showMore
// 是否显示收起按钮
if (!this.closeBtn) this.isLongContent = false
this.$emit(this.showMore ? 'open' : 'closed', this.index)
}
}
}
</script>
<style lang="scss" scoped>
.tn-read-more {
&__content {
font-size: 28rpx;
color: $tn-font-color;
line-height: 1.8;
text-align: left;
overflow: hidden;
transition: all 0.3s linear;
}
&__show-more {
padding-top: 0;
background: none;
margin-top: 20rpx;
&__wrap {
position: relative;
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding-bottom: 26rpx;
}
&--text {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
line-height: 1;
}
&--icon {
margin-left: 14rpx;
}
}
}
</style>
@@ -0,0 +1,301 @@
<template>
<view
v-if="show"
class="tn-row-notice-class tn-row-notice"
:class="[backgroundColorClass]"
:style="[noticeStyle]"
>
<view class="tn-row-notice__wrap">
<!-- 左图标 -->
<view class="tn-row-notice__icon">
<view
v-if="leftIcon"
class="tn-row-notice__icon--left"
:class="[`tn-icon-${leftIconName}`,fontColorClass]"
:style="[fontStyle('leftIcon')]"
@tap="clickLeftIcon"></view>
</view>
<!-- 消息体 -->
<view class="tn-row-notice__content-box" id="tn-row-notice__content-box">
<view
class="tn-row-notice__content"
id="tn-row-notice__content"
:style="{
animationDuration: animationDuration,
animationPlayState: animationPlayState
}"
>
<text
class="tn-row-notice__content--text"
:class="[fontColorClass]"
:style="[fontStyle()]"
@tap="click"
>{{ showText }}</text>
</view>
</view>
<!-- 右图标 -->
<view class="tn-row-notice__icon">
<view
v-if="rightIcon"
class="tn-row-notice__icon--right"
:class="[`tn-icon-${rightIconName}`,fontColorClass]"
:style="[fontStyle('rightIcon')]"
@tap="clickRightIcon"></view>
<view
v-if="closeBtn"
class="tn-row-notice__icon--right"
:class="[`tn-icon-close`,fontColorClass]"
:style="[fontStyle('close')]"
@tap="close"></view>
</view>
</view>
</view>
</template>
<script>
import componentsColorMixin from '../../libs/mixin/components_color.js'
export default {
name: 'tn-row-notice',
mixins: [componentsColorMixin],
props: {
// 显示的内容
list: {
type: Array,
default() {
return []
}
},
// 是否显示
show: {
type: Boolean,
default: true
},
// 播放状态
// play -> 播放 paused -> 暂停
playStatus: {
type: String,
default: 'play'
},
// 是否显示左边图标
leftIcon: {
type: Boolean,
default: true
},
// 左边图标的名称
leftIconName: {
type: String,
default: 'sound'
},
// 左边图标的大小
leftIconSize: {
type: Number,
default: 34
},
// 是否显示右边的图标
rightIcon: {
type: Boolean,
default: false
},
// 右边图标的名称
rightIconName: {
type: String,
default: 'right'
},
// 右边图标的大小
rightIconSize: {
type: Number,
default: 26
},
// 是否显示关闭按钮
closeBtn: {
type: Boolean,
default: false
},
// 圆角
radius: {
type: Number,
default: 0
},
// 内边距
padding: {
type: String,
default: '18rpx 24rpx'
},
// 自动播放
autoplay: {
type: Boolean,
default: true
},
// 水平滚动时的速度,即每秒滚动多少rpx
speed: {
type: Number,
default: 160
}
},
computed: {
fontStyle() {
return (type) => {
let style = {}
style.color = this.fontColorStyle ? this.fontColorStyle : '#080808'
style.fontSize = this.fontSizeStyle ? this.fontSizeStyle : '26rpx'
if (type === 'leftIcon' && this.leftIconSize) {
style.fontSize = this.leftIconSize + 'rpx'
}
if (type === 'rightIcon' && this.rightIconSize) {
style.fontSize = this.rightIconSize + 'rpx'
}
if (type === 'close') {
style.fontSize = '24rpx'
}
return style
}
},
noticeStyle() {
let style = {}
style.backgroundColor = this.backgroundColorStyle ? this.backgroundColorStyle : 'transparent'
if (this.padding) style.padding = this.padding
return style
}
},
data() {
return {
// 滚动文字的宽度
textWidth: 0,
// 存放滚动文字的盒子的宽度
textBoxWidth: 0,
// 动画执行的时间
animationDuration: '10s',
// 动画执行状态
animationPlayState: 'paused',
// 当前显示的文本
showText: ''
}
},
watch: {
list: {
handler(value) {
this.showText = value.join('')
this.$nextTick(() => {
this.initNotice()
})
},
immediate: true
},
playStatus(value) {
if (value === 'play') this.animationPlayState = 'running'
else this.animationPlayState = 'paused'
},
speed(value) {
this.initNotice()
}
},
methods: {
// 初始化通知栏
initNotice() {
let query = [],
textBoxWidth = 0,
textWidth = 0;
let textQuery = new Promise((resolve, reject) => {
uni.createSelectorQuery()
.in(this)
.select(`#tn-row-notice__content`)
.boundingClientRect()
.exec(ret => {
this.textWidth = ret[0].width
resolve()
})
})
query.push(textQuery)
Promise.all(query).then(() => {
// 根据t=s/v(时间=路程/速度),这里为何不需要加上#tn-row-notice__content-box的宽度,因为设置了.tn-row-notice__content样式中设置了padding-left: 100%
this.animationDuration = `${this.textWidth / uni.upx2px(this.speed)}s`
// 这里必须这样开始动画,否则在APP上动画速度不会改变(HX版本2.4.6IOS13)
this.animationPlayState = 'paused'
setTimeout(() => {
if (this.autoplay && this.playStatus === 'play') this.animationPlayState = 'running'
}, 10)
})
},
// 点击了通知栏
click() {
this.$emit('click')
},
// 点击了关闭按钮
close() {
this.$emit('close')
},
// 点击了左边图标
clickLeftIcon() {
this.$emit('clickLeft')
},
// 点击了右边图标
clickRightIcon() {
this.$emit('clickRight')
}
}
}
</script>
<style lang="scss" scoped>
.tn-row-notice {
width: 100%;
overflow: hidden;
&__wrap {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
&__content {
&-box {
flex: 1;
display: flex;
flex-direction: row;
overflow: hidden;
margin-left: 12rpx;
}
display: flex;
flex-direction: row;
flex-wrap: nowrap;
text-align: right;
// 为了能滚动起来
padding-left: 100%;
animation: tn-notice-loop-animation 10s linear infinite both;
&--text {
word-break: keep-all;
white-space: nowrap;
}
}
&__icon {
&--left {
display: inline-flex;
align-items: center;
}
&--right {
margin-left: 12rpx;
display: inline-flex;
align-items: center;
}
}
}
@keyframes tn-notice-loop-animation {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-100%);
}
}
</style>
@@ -0,0 +1,177 @@
<template>
<view class="tn-scroll-list-class tn-scroll-list">
<scroll-view
class="tn-scroll-list__scroll-view"
scroll-x
:upper-threshold="0"
:lower-threshold="0"
@scroll="scrollHandler"
@scrolltoupper="scrollToUpperHandler"
@scrolltolower="scrollToLowerHandler"
>
<view class="tn-scroll-list__scroll-view__content">
<slot></slot>
</view>
</scroll-view>
<!-- 指示器-->
<view
v-if="indicator"
class="tn-scroll-list__indicator"
:style="[indicatorStyle]"
>
<view class="tn-scroll-list__indicator__line" :style="[lineStyle]">
<view class="tn-scroll-list__indicator__line__bar" :style="[barStyle]"></view>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'tn-scroll-list',
props: {
// 是否显示指示器
indicator: {
type: Boolean,
default: true
},
// 指示器整体宽度
indicatorWidth: {
type: [String, Number],
default: 50
},
// 指示器滑块的宽度
indicatorBarWidth: {
type: [String, Number],
default: 20
},
// 指示器颜色
indicatorColor: {
type: String,
default: '#E6E6E6'
},
// 指示器激活时颜色
indicatorActiveColor: {
type: String,
default: '#01BEFF'
},
// 自定义指示器样式
indicatorStyle: {
type: Object,
default() {
return {}
}
}
},
computed: {
// 指示器滑块样式
barStyle() {
let style = {}
// 获取滑动距离的比值
// 滑块当前移动距离与总需滑动距离(指示器的总宽度减去滑块宽度)的比值 = scroll-view的滚动距离与目标滚动距离(scroll-view的实际宽度减去包裹元素的宽度)之比
const scrollLeft = this.scrollInfo.scrollLeft,
scrollWidth = this.scrollInfo.scrollWidth,
barAllMoveWidth = uni.upx2px(this.indicatorWidth) - uni.upx2px(this.indicatorBarWidth);
const x = scrollLeft / (scrollWidth - this.scrollWidth) * barAllMoveWidth
style.transform = `translateX(${x}px)`
// 设置滑块的宽度和背景颜色
style.width = `${this.indicatorBarWidth}rpx`
style.backgroundColor = this.indicatorActiveColor
return style
},
// 指示器整体样式
lineStyle() {
let style = {}
style.width = `${this.indicatorWidth}rpx`
style.backgroundColor = this.indicatorColor
return style
}
},
data() {
return {
// 滑动时滑块信息
scrollInfo: {
scrollLeft: 0,
scrollWidth: 0
},
// 滑动区域的宽度
scrollWidth: 0
}
},
mounted() {
this.$nextTick(() => {
this.init()
})
},
methods: {
// 初始化
init() {
this.getComponentWidth()
},
// 处理滚动事件
scrollHandler(event) {
this.scrollInfo = event.detail
},
// 滚动到最左边触发事件
scrollToUpperHandler() {
this.$emit('left')
this.scrollInfo.scrollLeft = 0
},
// 滚动到最右边触发事件
scrollToLowerHandler() {
this.$emit('right')
// this.scrollInfo.scrollLeft = uni.upx2px(this.indicatorWidth) - uni.upx2px(this.indicatorBarWidth)
},
// 获取组件的宽度
getComponentWidth() {
this._tGetRect('.tn-scroll-list').then(res => {
if (!res) {
setTimeout(() => {
this.getComponentWidth()
}, 10)
return
}
this.scrollWidth = res.width
})
}
}
}
</script>
<style lang="scss" scoped>
.tn-scroll-list {
padding-bottom: 20rpx;
&__scroll-view {
display: flex;
flex-direction: row;
&__content {
display: flex;
flex-direction: row;
}
}
&__indicator {
display: flex;
flex-direction: row;
justify-content: center;
margin-top: 30rpx;
&__line {
width: 120rpx;
height: 8rpx;
border-radius: 200rpx;
overflow: hidden;
&__bar {
width: 40rpx;
height: 8rpx;
border-radius: 200rpx;
}
}
}
}
</style>
@@ -0,0 +1,380 @@
<template>
<view v-if="value" class="tn-select-class tn-select">
<tn-popup
v-model="value"
mode="bottom"
:popup="false"
length="auto"
:safeAreaInsetBottom="safeAreaInsetBottom"
:maskCloseable="maskCloseable"
:zIndex="elZIndex"
@close="close"
>
<view class="tn-select__content">
<!-- 头部 -->
<view class="tn-select__content__header" @touchmove.stop.prevent>
<view
class="tn-select__content__header__btn tn-select__content__header--cancel"
:style="{ color: cancelColor }"
hover-class="tn-hover-class"
hover-stay-time="150"
@tap="getResult('cancel')"
>{{ cancelText }}</view>
<view class="tn-select__content__header__title">{{ title }}</view>
<view
class="tn-select__content__header__btn tn-select__content__header--confirm"
:style="{ color: confirmColor }"
hover-class="tn-hover-class"
hover-stay-time="150"
@tap="getResult('confirm')"
>{{ confirmText }}</view>
</view>
<!-- 列表内容 -->
<view class="tn-select__content__body">
<picker-view
class="tn-select__content__body__view"
:value="defaultSelector"
@pickstart="pickStart"
@pickend="pickEnd"
@change="columnChange"
>
<picker-view-column v-for="(item, index) in columnData" :key="index">
<view class="tn-select__content__body__item" v-for="(sub_item, sub_index) in item" :key="sub_index">
<view class="tn-text-ellipsis">
{{ sub_item[labelName] }}
</view>
</view>
</picker-view-column>
</picker-view>
</view>
</view>
</tn-popup>
</view>
</template>
<script>
export default {
name: 'tn-select',
props: {
value: {
type: Boolean,
default: false
},
// 列表模式
// single 单列 multi 多列 multi-auto 多列联动
mode: {
type: String,
default: 'single'
},
// 列数据
list: {
type: Array,
default() {
return []
}
},
// value属性名
valueName: {
type: String,
default: 'value'
},
// label属性名
labelName: {
type: String,
default: 'label'
},
// 当mode=multi-auto时,children的属性名
childName: {
type: String,
default: 'children'
},
// 默认值
defaultValue: {
type: Array,
default() {
return [0]
}
},
// 顶部标题
title: {
type: String,
default: ''
},
// 取消按钮文字
cancelText: {
type: String,
default: '取消'
},
// 取消按钮文字颜色
cancelColor: {
type: String,
default: ''
},
// 确认按钮文字
confirmText: {
type: String,
default: '确认'
},
// 确认按钮文字颜色
confirmColor: {
type: String,
default: ''
},
// 点击遮罩关闭
maskCloseable: {
type: Boolean,
default: true
},
// 预留安全区域
safeAreaInsetBottom: {
type: Boolean,
default: false
},
// zIndex
zIndex: {
type: Number,
default: 0
}
},
computed: {
elZIndex() {
return this.zIndex ? this.zIndex : this.$t.zIndex.popup
}
},
data() {
return {
// 列是否还在滑动中,微信小程序如果在滑动中就点确定,结果可能不准确
moving: false,
// 用户保存当前列的索引,用于判断下一次变化时改变的列
defaultSelector: [0],
// picker-view数据
columnData: [],
// 保存用户选择的结果
selectValue: [],
// 上一次改变时的index
lastSelectIndex: [],
// 列数
columnNum: 0
}
},
watch: {
// 在select弹起的时候,重新初始化所有数据
value: {
handler(val) {
if (val) setTimeout(() => this.init(), 10)
},
immediate: true
}
},
methods: {
// 标识滑动开始,只有微信小程序才有这样的事件
pickStart() {
// #ifdef MP-WEIXIN
this.moving = true;
// #endif
},
// 标识滑动结束
pickEnd() {
// #ifdef MP-WEIXIN
this.moving = false;
// #endif
},
init() {
this.setColumnNum()
this.setDefaultSelector()
this.setColumnData()
this.setSelectValue()
},
// 获取默认选中列下标
setDefaultSelector() {
// 如果没有传入默认值,生成用0填充长度为columnNum的数组
this.defaultSelector = this.defaultValue.length === this.columnNum ? this.defaultValue : Array(this.columnNum).fill(0)
this.lastSelectIndex = this.$t.deepClone(this.defaultSelector)
},
// 计算列数
setColumnNum() {
// 单列的数量为1
if (this.mode === 'single') this.columnNum = 1
// 多列时取list的长度
else if (this.mode === 'multi') this.columnNum = this.list.length
// 多列联动时,通过遍历list的第一个元素,得出有多少列
else if (this.mode === 'multi-auto') {
let num = 1
let column = this.list
// 如果存在children属性,再次遍历
while (column[0][this.childName]) {
column = column[0] ? column[0][this.childName] : {},
num++
}
this.columnNum = num
}
},
// 获取需要展示在picker中的列数据
setColumnData() {
let data = []
this.selectValue = []
if (this.mode === 'multi-auto') {
// 获取所有数据中的第一个元素
let column = this.list[this.defaultSelector.length ? this.defaultSelector[0] : 0]
// 通过循环所有列数,再根据设定列的数组,得出当前需要渲染的整个列数组
for (let i = 0; i < this.columnNum; i++) {
// 第一列默认为整个list数组
if (i === 0) {
data[i] = this.list
column = column[this.childName]
} else {
// 大于第一列时,判断是否有默认选中的,如果没有就用该列的第一项
data[i] = column
column = column[this.defaultSelector[i]][this.childName]
}
}
} else if (this.mode === 'single') {
data[0] = this.list
} else {
data = this.list
}
this.columnData = data
},
// 获取默认选中的值,如果没有设置,则默认选中第一项
setSelectValue() {
let tmp = null
for (let i = 0; i < this.columnNum; i++) {
tmp = this.columnData[i][this.defaultSelector[i]]
let data = {
value: tmp ? tmp[this.valueName] : null,
label: tmp ? tmp[this.labelName] : null
}
// 判断是否存在额外的参数
if (tmp && tmp.extra) data.extra = tmp.extra
this.selectValue.push(data)
}
},
// 列选项
columnChange(event) {
let index = null
let columnIndex = event.detail.value
this.selectValue = []
if (this.mode === 'multi-auto') {
// 对比前后两个数组,判断变更的是那一列
this.lastSelectIndex.map((v, idx) => {
if (v != columnIndex[idx]) index = idx
})
this.defaultSelector = columnIndex
// 当前变化列的下一列的数据,需要获取上一列的数据,同时需要指定是上一列的第几个的children,再往后的
// 默认是队列的第一个为默认选项
for (let i = index + 1; i < this.columnNum; i++) {
this.columnData[i] = this.columnData[i - 1][i - 1 == index ? columnIndex[index] : 0][this.childName]
this.defaultSelector[i] = 0
}
// 在历遍的过程中,可能由于上一步修改this.columnData,导致产生连锁反应,程序触发columnChange,会有多次调用
// 只有在最后一次数据稳定后的结果是正确的,此前的历遍中,可能会产生undefined,故需要判断
columnIndex.map((item, index) => {
let data = this.columnData[index][columnIndex[index]]
let tmp = {
value: data ? data[this.valueName] : null,
label: data ? data[this.labelName] : null
}
if (data && data.extra !== undefined) tmp.extra = data.extra
this.selectValue.push(tmp)
})
this.lastSelectIndex = columnIndex
} else if (this.mode === 'single') {
let data = this.columnData[0][columnIndex[0]]
let tmp = {
value: data ? data[this.valueName] : null,
label: data ? data[this.labelName] : null
}
if (data && data.extra !== undefined) tmp.extra = data.extra
this.selectValue.push(tmp)
} else if (this.mode === 'multi') {
columnIndex.map((item, index) => {
let data = this.columnData[index][columnIndex[index]]
let tmp = {
value: data ? data[this.valueName] : null,
label: data ? data[this.labelName] : null
}
if (data && data.extra !== undefined) tmp.extra = data.extra
this.selectValue.push(tmp)
})
}
},
close() {
this.$emit('input', false)
},
getResult(event = null) {
// #ifdef MP-WEIXIN
if (this.moving) return;
// #endif
if (event) this.$emit(event, this.selectValue)
this.close()
}
}
}
</script>
<style lang="scss" scoped>
.tn-select {
&__content {
position: relative;
&__header {
position: relative;
display: flex;
flex-direction: row;
width: 100%;
height: 90rpx;
padding: 0 40rpx;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
font-size: 30rpx;
background-color: #FFFFFF;
&__btn {
padding: 16rpx;
box-sizing: border-box;
text-align: center;
text-decoration: none;
}
&__title {
color: $tn-font-color;
}
&--cancel {
color: $tn-font-sub-color;
}
&--confirm {
color: $tn-main-color;
}
}
&__body {
width: 100%;
height: 500rpx;
overflow: hidden;
background-color: #FFFFFF;
&__view {
height: 100%;
box-sizing: border-box;
}
&__item {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
font-size: 32rpx;
color: $tn-font-color;
padding: 0 8rpx;
}
}
}
}
</style>
@@ -0,0 +1,690 @@
<template>
<view v-if="show" class="tn-sign-board-class tn-sign-board" :style="{top: `${customBarHeight}px`, height: `calc(100% - ${customBarHeight}px)`}">
<!-- 签名canvas -->
<view class="tn-sign-board__content">
<view class="tn-sign-board__content__wrapper">
<canvas class="tn-sign-board__content__canvas" :canvas-id="canvasName" :disableScroll="true" @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd"></canvas>
</view>
</view>
<!-- 底部工具栏 -->
<view class="tn-sign-board__tools">
<!-- 可选颜色 -->
<view class="tn-sign-board__tools__color">
<view
v-for="(item, index) in signSelectColor"
:key="index"
class="tn-sign-board__tools__color__item"
:class="[{'tn-sign-board__tools__color__item--active': currentSelectColor === item}]"
:style="{backgroundColor: item}"
@tap="colorSwitch(item)"
></view>
</view>
<!-- 按钮 -->
<view class="tn-sign-board__tools__button">
<view class="tn-sign-board__tools__button__item tn-bg-red" @tap="reDraw">清除</view>
<view class="tn-sign-board__tools__button__item tn-bg-blue" @tap="save">保存</view>
<view class="tn-sign-board__tools__button__item tn-bg-indigo" @tap="previewImage">预览</view>
<view class="tn-sign-board__tools__button__item tn-bg-orange" @tap="closeBoard">关闭</view>
</view>
</view>
<!-- 伪全屏生成旋转图片canvas容器不在页面上展示 -->
<view style="position: fixed; left: -2000px;width: 0;height: 0;overflow: hidden;">
<canvas canvas-id="temp-tn-sign-canvas" :style="{width: `${canvasHeight}px`, height: `${canvasHeight}px`}"></canvas>
</view>
</view>
</template>
<script>
export default {
name: 'tn-sign-board',
props: {
// 是否显示
show: {
type: Boolean,
default: false
},
// 可选签名颜色
signSelectColor: {
type: Array,
default() {
return ['#080808', '#E83A30']
}
},
// 是否旋转输出图片
rotate: {
type: Boolean,
default: true
},
// 自定义顶栏的高度
customBarHeight: {
type: [String, Number],
default: 0
}
},
data() {
return {
canvasName: 'tn-sign-canvas',
ctx: null,
canvasWidth: 0,
canvasHeight: 0,
currentSelectColor: this.signSelectColor[0],
// 第一次触摸
firstTouch: false,
// 透明度
transparent: 1,
// 笔迹倍数
lineSize: 1.5,
// 最小画笔半径
minLine: 0.5,
// 最大画笔半径
maxLine: 4,
// 画笔压力
pressure: 1,
// 顺滑度,用60的距离来计算速度
smoothness: 60,
// 当前触摸的点
currentPoint: {},
// 当前线条
currentLine: [],
// 画笔圆半径
radius: 1,
// 裁剪区域
cutArea: {
top: 0,
right: 0,
bottom: 0,
left: 0
},
// 所有线条, 生成贝塞尔点
// bethelPoint: [],
// 上一个点
lastPoint: 0,
// 笔迹
chirography: [],
// 当前笔迹
// currentChirography: {},
// 画线轨迹,生成线条的实际点
linePrack: []
}
},
watch: {
show(value) {
if (value && this.canvasWidth === 0 && this.canvasHeight === 0) {
this.$nextTick(() => {
this.getCanvasInfo()
})
}
},
signSelectColor(value) {
if (value.length > 0) {
this.currentSelectColor = value[0]
}
}
},
created() {
// 创建canvas
this.ctx = uni.createCanvasContext(this.canvasName, this)
},
mounted() {
// 获取画板的相关信息
// this.$nextTick(() => {
// this.getCanvasInfo()
// })
},
methods: {
// 获取画板的相关信息
getCanvasInfo() {
this._tGetRect('.tn-sign-board__content__canvas').then(res => {
this.canvasWidth = res.width
this.canvasHeight = res.height
// 初始化Canvas
this.$nextTick(() => {
this.initCanvas('#FFFFFF')
})
})
},
// 初始化Canvas
initCanvas(color) {
/* 将canvas背景设置为 白底,不设置 导出的canvas的背景为透明 */
// rect() 参数说明 矩形路径左上角的横坐标,左上角的纵坐标, 矩形路径的宽度, 矩形路径的高度
// 矩形的宽高需要减去边框的宽度
this.ctx.rect(0, 0, this.canvasWidth - uni.upx2px(4), this.canvasHeight - uni.upx2px(4))
this.ctx.setFillStyle(color)
this.ctx.fill()
this.ctx.draw()
},
// 开始画
onTouchStart(e) {
if (e.type != 'touchstart') return false
// 设置线条颜色
this.ctx.setFillStyle(this.currentSelectColor)
// 设置透明度
this.ctx.setGlobalAlpha(this.transparent)
let currentPoint = {
x: e.touches[0].x,
y: e.touches[0].y
}
let currentLine = this.currentLine
currentLine.unshift({
time: new Date().getTime(),
dis: 0,
x: currentPoint.x,
y: currentPoint.y
})
this.currentPoint = currentPoint
if (this.firstTouch) {
this.cutArea = {
top: currentPoint.y,
right: currentPoint.x,
bottom: currentPoint.y,
left: currentPoint.x
}
this.firstTouch = false
}
this.pointToLine(currentLine)
},
// 正在画
onTouchMove(e) {
if (e.type != 'touchmove') return false
if (e.cancelable) {
// 判断默认行为是否已经被禁用
if (!e.defaultPrevented) {
e.preventDefault()
}
}
let point = {
x: e.touches[0].x,
y: e.touches[0].y
}
if (point.y < this.cutArea.top) {
this.cutArea.top = point.y
}
if (point.y < 0) this.cutArea.top = 0
if (point.x < this.cutArea.right) {
this.cutArea.right = point.x
}
if (this.canvasWidth - point.x <= 0) {
this.cutArea.right = this.canvasWidth
}
if (point.y > this.cutArea.bottom) {
this.cutArea.bottom = this.canvasHeight
}
if (this.canvasHeight - point.y <= 0) {
this.cutArea.bottom = this.canvasHeight
}
if (point.x < this.cutArea.left) {
this.cutArea.left = point.x
}
if (point.x < 0) this.cutArea.left = 0
this.lastPoint = this.currentPoint
this.currentPoint = point
let currentLine = this.currentLine
currentLine.unshift({
time: new Date().getTime(),
dis: this.distance(this.currentPoint, this.lastPoint),
x: point.x,
y: point.y
})
this.pointToLine(currentLine)
},
// 移动结束
onTouchEnd(e) {
if (e.type != 'touchend') return false
let point = {
x: e.changedTouches[0].x,
y: e.changedTouches[0].y
}
this.lastPoint = this.currentPoint
this.currentPoint = point
let currentLine = this.currentLine
currentLine.unshift({
time: new Date().getTime(),
dis: this.distance(this.currentPoint, this.lastPoint),
x: point.x,
y: point.y
})
//一笔结束,保存笔迹的坐标点,清空,当前笔迹
//增加判断是否在手写区域
this.pointToLine(currentLine)
let currentChirography = {
lineSize: this.lineSize,
lineColor: this.currentSelectColor
}
let chirography = this.chirography
chirography.unshift(currentChirography)
this.chirography = chirography
let linePrack = this.linePrack
linePrack.unshift(this.currentLine)
this.linePrack = linePrack
this.currentLine = []
},
// 重置绘画板
reDraw() {
this.initCanvas('#FFFFFF')
},
// 保存
save() {
// 在组件内使用需要第二个参数this
uni.canvasToTempFilePath({
canvasId: this.canvasName,
fileType: 'png',
quality: 1,
success: (res) => {
if (this.rotate) {
this.getRotateImage(res.tempFilePath).then((res) => {
this.$emit('save', res)
}).catch(err => {
this.$t.messageUtils.toast('旋转图片失败')
})
} else {
this.$emit('save', res.tempFilePath)
}
},
fail: () => {
this.$t.messageUtils.toast('保存失败')
}
}, this)
},
// 预览图片
previewImage() {
// 在组件内使用需要第二个参数this
uni.canvasToTempFilePath({
canvasId: this.canvasName,
fileType: 'png',
quality: 1,
success: (res) => {
if (this.rotate) {
this.getRotateImage(res.tempFilePath).then((res) => {
uni.previewImage({
urls: [res]
})
}).catch(err => {
this.$t.messageUtils.toast('旋转图片失败')
})
} else {
uni.previewImage({
urls: [res.tempFilePath]
})
}
},
fail: (e) => {
this.$t.messageUtils.toast('预览失败')
}
}, this)
},
// 关闭签名板
closeBoard() {
this.$t.messageUtils.modal('提示信息','关闭后内容将被清除,是否确认关闭',() => {
this.$emit('closed')
}, true)
},
// 切换画笔颜色
colorSwitch(color) {
this.currentSelectColor = color
},
// 绘制两点之间的线条
pointToLine(line) {
this.calcBethelLine(line)
},
// 计算插值,让线条更加圆滑
calcBethelLine(line) {
if (line.length <= 1) {
line[0].r = this.radius
return
}
let x0,
x1,
x2,
y0,
y1,
y2,
r0,
r1,
r2,
len,
lastRadius,
dis = 0,
time = 0,
curveValue = 0.5;
if (line.length <= 2) {
x0 = line[1].x
y0 = line[1].y
x2 = line[1].x + (line[0].x - line[1].x) * curveValue
y2 = line[1].y + (line[0].y - line[1].y) * curveValue
x1 = x0 + (x2 - x0) * curveValue
y1 = y0 + (y2 - y0) * curveValue
} else {
x0 = line[2].x + (line[1].x - line[2].x) * curveValue
y0 = line[2].y + (line[1].y - line[2].y) * curveValue
x1 = line[1].x
y1 = line[1].y
x2 = x1 + (line[0].x - x1) * curveValue
y2 = y1 + (line[0].y - y1) * curveValue
}
// 三个点分别是(x0,y0),(x1,y1),(x2,y2) (x1,y1)这个是控制点,控制点不会落在曲线上;实际上,这个点还会手写获取的实际点,却落在曲线上
len = this.distance({
x: x2,
y: y2
}, {
x: x0,
y: y0
})
lastRadius = this.radius
for (let i = 0; i < line.length - 1; i++) {
dis += line[i].dis
time += line[i].time - line[i + 1].time
if (dis > this.smoothness) break
}
this.radius = Math.min((time / len) * this.pressure + this.minLine, this.maxLine) * this.lineSize
line[0].r = this.radius
// 计算笔迹半径
if (line.length <= 2) {
r0 = (lastRadius + this.radius) / 2
r1 = r0
r2 = r1
} else {
r0 = (line[2].r + line[1].r) / 2
r1 = line[1].r
r2 = (line[1].r + line[0].r) / 2
}
let n = 5
let point = []
for (let i = 0; i < n; i++) {
let t = i / (n - 1)
let x = (1 - t) * (1 - t) * x0 + 2 * t * (1 - t) * x1 + t * t * x2
let y = (1 - t) * (1 - t) * y0 + 2 * t * (1 - t) * y1 + t * t * y2
let r = lastRadius + ((this.radius - lastRadius) / n) * i
point.push({
x,
y,
r
})
if (point.length === 3) {
let a = this.ctaCalc(point[0].x, point[0].y, point[0].r, point[1].x, point[1].y, point[1].r, point[2].x, point[2].y, point[2].r)
a[0].color = this.currentSelectColor
this.drawBethel(a, true)
point = [{
x,
y,
r
}]
}
}
this.currentLine = line
},
// 求两点之间的距离
distance(a, b) {
let x = b.x - a.x
let y = b.y - a.y
return Math.sqrt(x * x + y * y)
},
// 计算点信息
ctaCalc(x0, y0, r0, x1, y1, r1, x2, y2, r2) {
let a = [],
vx01,
vy01,
norm,
n_x0,
n_y0,
vx21,
vy21,
n_x2,
n_y2;
vx01 = x1 - x0
vy01 = y1 - y0
norm = Math.sqrt(vx01 * vx01 + vy01 * vy01 + 0.0001) * 2
vx01 = (vx01 / norm) * r0
vy01 = (vy01 / norm) * r0
n_x0 = vy01
n_y0 = -vx01
vx21 = x1 - x2
vy21 = y1 - y2
norm = Math.sqrt(vx21 * vx21 + vy21 * vy21 + 0.0001) * 2
vx21 = (vx21 / norm) * r2
vy21 = (vy21 / norm) * r2
n_x2 = -vy21
n_y2 = vx21
a.push({
mx: x0 + n_x0,
my: y0 + n_y0,
color: '#080808'
})
a.push({
c1x: x1 + n_x0,
c1y: y1 + n_y0,
c2x: x1 + n_x2,
c2y: y1 + n_y2,
ex: x2 + n_x2,
ey: y2 + n_y2
})
a.push({
c1x: x2 + n_x2 - vx21,
c1y: y2 + n_y2 - vy21,
c2x: x2 - n_x2 - vx21,
c2y: y2 - n_y2 - vy21,
ex: x2 - n_x2,
ey: y2 - n_y2
})
a.push({
c1x: x1 - n_x2,
c1y: y1 - n_y2,
c2x: x1 - n_x0,
c2y: y1 - n_y0,
ex: x0 - n_x0,
ey: y0 - n_y0
})
a.push({
c1x: x0 - n_x0 - vx01,
c1y: y0 - n_y0 - vy01,
c2x: x0 + n_x0 - vx01,
c2y: y0 + n_y0 - vy01,
ex: x0 + n_x0,
ey: y0 + n_y0
})
a[0].mx = a[0].mx.toFixed(1)
a[0].mx = parseFloat(a[0].mx)
a[0].my = a[0].my.toFixed(1)
a[0].my = parseFloat(a[0].my)
for (let i = 1; i < a.length; i++) {
a[i].c1x = a[i].c1x.toFixed(1)
a[i].c1x = parseFloat(a[i].c1x)
a[i].c1y = a[i].c1y.toFixed(1)
a[i].c1y = parseFloat(a[i].c1y)
a[i].c2x = a[i].c2x.toFixed(1)
a[i].c2x = parseFloat(a[i].c2x)
a[i].c2y = a[i].c2y.toFixed(1)
a[i].c2y = parseFloat(a[i].c2y)
a[i].ex = a[i].ex.toFixed(1)
a[i].ex = parseFloat(a[i].ex)
a[i].ey = a[i].ey.toFixed(1)
a[i].ey = parseFloat(a[i].ey)
}
return a
},
// 绘制贝塞尔曲线
drawBethel(point, is_fill, color) {
this.ctx.beginPath()
this.ctx.moveTo(point[0].mx, point[0].my)
if (color != undefined) {
this.ctx.setFillStyle(color)
this.ctx.setStrokeStyle(color)
} else {
this.ctx.setFillStyle(point[0].color)
this.ctx.setStrokeStyle(point[0].color)
}
for (let i = 1; i < point.length; i++) {
this.ctx.bezierCurveTo(point[i].c1x, point[i].c1y, point[i].c2x, point[i].c2y, point[i].ex, point[i].ey)
}
this.ctx.stroke()
if (is_fill != undefined) {
//填充图形 ( 后绘制的图形会覆盖前面的图形, 绘制时注意先后顺序 )
this.ctx.fill()
}
this.ctx.draw(true)
},
// 旋转图片
async getRotateImage(dataUrl) {
// const url = await this.base64ToPath(dataUrl)
const url = dataUrl
// 创建新画布
const tempCtx = uni.createCanvasContext('temp-tn-sign-canvas', this)
const width = this.canvasWidth
const height = this.canvasHeight
tempCtx.restore()
tempCtx.save()
tempCtx.translate(0, height)
tempCtx.rotate(270 * Math.PI / 180)
tempCtx.drawImage(url, 0, 0, width, height)
tempCtx.draw()
return new Promise((resolve, reject) => {
setTimeout(() => {
uni.canvasToTempFilePath({
canvasId: 'temp-tn-sign-canvas',
fileType: 'png',
x: 0,
y: height - width,
width: height,
height: width,
success: res => resolve(res.tempFilePath),
fail: reject
}, this)
}, 50)
})
},
// 将base64转换为本地
base64ToPath(dataUrl) {
return new Promise((resolve, reject) => {
// 判断地址是否包含bas64字样,不包含直接返回
if (dataUrl.indexOf('base64') !== -1) {
const data = uni.base64ToArrayBuffer(dataUrl.replace(/^data:image\/\w+;base64,/, ''))
// #ifdef MP-WEIXIN
const filePath = `${wx.env.USER_DATA_PATH}/${new Date().getTime()}-${Math.random().toString(32).slice(2)}.png`
// #endif
// #ifndef MP-WEIXIN
const filePath = `${new Date().getTime()}-${Math.random().toString(32).slice(2)}.png`
// #endif
uni.getFileSystemManager().writeFile({
filePath,
data,
encoding: 'base64',
success: () => resolve(filePath),
fail: reject
})
} else {
resolve(dataUrl)
}
})
}
}
}
</script>
<style lang="scss" scoped>
.tn-sign-board {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
background-color: #E6E6E6;
z-index: 997;
display: flex;
flex-direction: row-reverse;
&__content {
width: 84%;
height: 100%;
&__wrapper {
width: calc(100% - 60rpx);
height: calc(100% - 60rpx);
margin: 30rpx;
border-radius: 20rpx;
border: 2rpx dotted #AAAAAA;
overflow: hidden;
}
&__canvas {
width: 100%;
height: 100%;
background-color: #FFFFFF;
}
}
&__tools {
width: 16%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
&__color {
margin-top: 30rpx;
&__item {
width: 70rpx;
height: 70rpx;
border-radius: 100rpx;
margin: 20rpx auto;
&--active {
position: relative;
&::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 40%;
height: 40%;
border-radius: 100rpx;
background-color: #FFFFFF;
transform: translate(-50%, -50%);
}
}
}
}
&__button {
margin-bottom: 30rpx;
display: flex;
flex-direction: column;
&__item {
width: 130rpx;
height: 60rpx;
line-height: 60rpx;
text-align: center;
margin: 60rpx auto;
border-radius: 10rpx;
color: #FFFFFF;
transform-origin: center center;
transform: rotateZ(90deg);
}
}
}
}
</style>
@@ -0,0 +1,254 @@
<template>
<view
class="tn-slider-class tn-slider"
:class="{'tn-slider--disabled': disabled}"
:style="{
backgroundColor: inactiveColor
}"
@tap="click"
>
<!-- slider滑动线 -->
<view
class="tn-slider__gap"
:style="[
barStyle,
{
height: this.$t.string.getLengthUnitValue(lineHeight),
backgroundColor: activeColor
}
]"
>
<!-- slider滑块 -->
<view
class="tn-slider__button-wrap"
@touchstart="touchStart"
@touchmove="touchMove"
@touchend="touchEnd"
@touchcancel="touchEnd"
>
<view v-if="$slots.default || $slots.$default">
<slot></slot>
</view>
<view
v-else
class="tn-slider__button"
:style="[blockStyle, {
height: this.$t.string.getLengthUnitValue(blockWidth),
width: this.$t.string.getLengthUnitValue(blockWidth),
backgroundColor: blockColor
}]"
></view>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'tn-slider',
props: {
// 进度值
value: {
type: [Number, String],
default: 0
},
// 最小值
min: {
type: Number,
default: 0
},
// 最大值
max: {
type: Number,
default: 100
},
// 步进值
step: {
type: Number,
default: 1
},
// 禁用
disabled: {
type: Boolean,
default: false
},
// 滑块宽度
blockWidth: {
type: Number,
default: 30
},
// 滑动条高度
lineHeight: {
type: Number,
default: 8
},
// 滑动条激活的颜色
activeColor: {
type: String,
default: '#01BEFF'
},
// 滑动条未被激活的颜色
inactiveColor: {
type: String,
default: '#E6E6E6'
},
// 滑块的颜色
blockColor: {
type: String,
default: '#FFFFFF'
},
// 自定义滑块的样式
blockStyle: {
type: Object,
default() {
return {}
}
}
},
data() {
return {
startX: 0,
status: 'end',
newValue: 0,
distanceX: 0,
startValue: 0,
barStyle: {},
sliderRect: {
left: 0,
width: 0
}
}
},
watch: {
value(val) {
// 只有在非滑动状态时,才可以通过value更新滑块值,这里监听,是为了让用户触发
if (this.status === 'end') this.updateValue(val, false)
}
},
created() {
this.updateValue(this.value, false)
},
mounted() {
this._tGetRect('.tn-slider').then(res => {
this.sliderRect = res
})
},
methods: {
// 开始滑动
touchStart(event) {
if (this.disabled) return
if (!event.changedTouches[0]) return
this.startX = 0
// 触摸点
this.startX = event.changedTouches[0].pageX
this.startValue = this.format(this.value)
// 标识当前开始触摸
this.status = 'start'
},
// 滑动移动中
touchMove(event) {
if (this.disabled) return
if (!event.changedTouches[0]) return
// 连续触摸的过程会一直触发本方法,但只有手指触发且移动了才被认为是拖动了,才发出事件
// 触摸后第一次移动已经将status设置为moving状态,故触摸第二次移动不会触发本事件
if (this.status === 'start') this.$emit('start')
let movePageX = event.changedTouches[0].pageX
// 滑块的左边不一定跟屏幕左边接壤,所以需要减去最外层父元素的左边值
this.distanceX = movePageX - this.sliderRect.left
// 获得移动距离对整个滑块的百分比值,此为带有多位小数的值,不能用此更新视图
// 否则造成通信阻塞,需要每改变一个step值时修改一次视图
this.newValue = ((this.distanceX / this.sliderRect.width) * (this.max - this.min)) + this.min
this.status = 'moving'
this.$emit('moving')
this.updateValue(this.newValue, true)
},
// 滑动结束
touchEnd() {
if(this.disabled) return
if (this.status === 'moving') {
this.updateValue(this.newValue, false)
this.$emit('end')
}
this.status = 'end'
},
// 更新数值
updateValue(value, drag) {
// 去掉小数部分,对step进行步进处理
value = this.format(value)
const width = Math.round((value - this.min) / (this.max - this.min) * 100)
// 不允许滑动的距离小于0和超过100
if (width < 0 || width > 100) return
// 设置移动的百分比
let barStyle = {
width: width + '%'
}
// 移动期间取消动画
if (drag === true) {
barStyle.transition = 'none'
} else {
// 非移动期间,删掉对过渡为空的声明,让css中的声明起效
delete barStyle.transition
}
// 修改value值
this.$emit('input', value)
this.barStyle = barStyle
},
// 点击事件
click(event) {
if (this.disabled) return
// 直接点击的情况,计算方式和touchMove方法一致
const value = (((event.detail.x - this.sliderRect.left) / this.sliderRect.width) * (this.max - this.min)) + this.min
this.updateValue(value, false)
},
// 格式化滑动的值
format(value) {
return Math.round(Math.max(this.min, Math.min(value, this.max)) / this.step) * this.step
}
}
}
</script>
<style lang="scss" scoped>
.tn-slider {
position: relative;
border-radius: 1000rpx;
// 增加点击的范围
border-width: 20rpx;
border-style: solid;
border-color: transparent;
background-color: $tn-font-holder-color;
background-clip: content-box;
&__gap {
position: relative;
border-radius: inherit;
transition: width 0.2s;
background-color: #01BEFF;
}
&__button {
width: 30rpx;
height: 30rpx;
border-radius: 50%;
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.6);
background-color: #FFFFFF;
cursor: pointer;
&-wrap {
position: absolute;
top: 50%;
right: 0;
transform: translate3d(50%, -50%, 0);
}
}
&--disabled {
opacity: 0.6;
}
}
</style>
+330
View File
@@ -0,0 +1,330 @@
<template>
<view
class="tn-steps-class tn-steps"
:style="{
flexDirection: direction
}"
>
<view
v-for="(item, index) in list"
:key="index"
class="tn-steps__item"
:class="[`tn-steps__item--${direction}`]"
>
<!-- 数值模式 -->
<view
v-if="mode === 'number'"
class="tn-steps__item__number"
:style="{
backgroundColor: current <= index ? 'transparent' : activeColor,
borderColor: current <= index ? inActiveColor : activeColor
}"
>
<text
class="tn-steps__item__number__text"
:class="[{'tn-steps__item__number__text--visible': current <= index}]"
:style="{
color: current <= index ? inActiveColor : activeColor
}"
>
{{ index + 1}}
</text>
<view class="tn-steps__item__number__icon" :class="[`tn-icon-${item.icon || icon}`,{'tn-steps__item__number__icon--visible': current > index}]"></view>
</view>
<!-- 点模式 -->
<view
v-if="mode === 'dot'"
class="tn-steps__item__dot"
:style="{
backgroundColor: current <= index ? inActiveColor : activeColor
}"
></view>
<!-- 图标模式 -->
<view
v-if="mode === 'icon'"
class="tn-steps__item__icon"
:class="[iconModeClass(index)]"
:style="{
color: current <= index ? inActiveColor : activeColor
}"
></view>
<!-- 点图标模式 -->
<view v-if="mode === 'dotIcon'" class="tn-steps__item__dot-icon">
<view v-if="current <= index" class="tn-steps__item__dot-icon--dot" :style="{backgroundColor: inActiveColor}"></view>
<view v-else class="tn-steps__item__dot-icon--icon" :class="[iconModeClass(index)]" :style="{color: activeColor}"></view>
</view>
<!-- 步骤下的文字 -->
<text
v-if="showTitle"
class="tn-steps__item__text tn-text-ellipsis"
:class="[`tn-steps__item__text--${direction}`]"
:style="{
color: current <= index ? inActiveColor : activeColor
}"
>
{{ item.name }}
</text>
<!-- 连接的横线 -->
<view
v-if="index < list.length - 1"
class="tn-steps__item__line"
:class="[`tn-steps__item__line--${mode}`]"
:style="{
borderColor: current <= index + 1 ? inActiveColor : activeColor
}"
></view>
</view>
</view>
</template>
<script>
export default {
name: 'tn-steps',
props: {
// 模式类型
// dot -> 点 number -> 数字 icon -> 图标 dot_icon -> 点图标
mode: {
type: String,
default: 'dot'
},
// 步骤条的方向
// row -> 横向 column -> 竖向
direction: {
type: String,
default: 'row'
},
// 步骤条数据
list: {
type: Array,
default() {
return []
}
},
// 当前激活的步数
current: {
type: Number,
default: 0
},
// 激活步骤的颜色
activeColor: {
type: String,
default: '#01BEFF'
},
// 未激活步骤的颜色
inActiveColor: {
type: String,
default: '#AAAAAA'
},
// 激活后显示的图标,在数字模式下有效
icon: {
type: String,
default: 'success'
},
// 是否显示标题
showTitle: {
type: Boolean,
default: true
}
},
computed: {
// icon模式下图标的值
iconModeClass() {
return (index) => {
const item = this.list[index]
// 状态被选中并且对应数据下存在selectIcon属性
if (this.current > index && item.hasOwnProperty('selectIcon')) {
return `tn-icon-${item.selectIcon}`
} else {
// 未选中
return `tn-icon-${item.icon || this.icon}`
}
}
}
},
data() {
return {}
},
methods: {
}
}
</script>
<style lang="scss" scoped>
$tn-steps-item-number-width: 44rpx;
$tn-steps-item-dot-width: 20rpx;
.tn-steps {
display: flex;
flex-direction: row;
&__item {
flex: 1;
position: relative;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
min-width: 100rpx;
font-size: 28rpx;
text-align: center;
&__number {
// display: flex;
// flex-wrap: wrap;
// align-items: center;
// justify-content: center;
position: relative;
width: $tn-steps-item-number-width;
height: $tn-steps-item-number-width;
line-height: calc(#{$tn-steps-item-number-width} - 2rpx);
border: 1px solid #AAAAAA;
border-radius: 50%;
overflow: hidden;
transition: all 0.3s linear;
&__text {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
transition: inherit;
transform: translateY(-#{$tn-steps-item-number-width});
&--visible {
transform: translateY(0);
}
}
&__icon {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
color: #FFFFFF;
transition: all 0.3s linear;
transform: translateY(#{$tn-steps-item-number-width});
&--visible {
transform: translateY(0);
}
}
}
&__dot {
width: $tn-steps-item-dot-width;
height: $tn-steps-item-dot-width;
display: flex;
flex-direction: row;
border-radius: 50%;
transition: all 0.3s linear;
&--icon {
width: $tn-steps-item-number-width;
height: $tn-steps-item-number-width;
}
}
&__icon {
width: $tn-steps-item-number-width;
height: $tn-steps-item-number-width;
font-size: $tn-steps-item-number-width;
transition: all 0.3s linear;
}
&__dot-icon {
width: $tn-steps-item-number-width;
height: $tn-steps-item-number-width;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
transition: all 0.3s linear;
&--dot {
width: $tn-steps-item-dot-width;
height: $tn-steps-item-dot-width;
border-radius: 50%;
transition: inherit;
}
&--icon {
width: $tn-steps-item-number-width;
height: $tn-steps-item-number-width;
font-size: $tn-steps-item-number-width;
transition: inherit;
}
}
&__text {
transition: all 0.3s linear;
&--row {
margin-top: 14rpx;
}
&--column {
margin-left: 14rpx;
}
}
&__line {
position: absolute;
z-index: 0;
vertical-align: middle;
transition: all 0.3s linear;
}
&--row {
display: flex;
flex-direction: column;
.tn-steps__item__line {
border-bottom-width: 1px;
border-bottom-style: solid;
width: 50%;
left: 75%;
&--dot {
top: calc(#{$tn-steps-item-dot-width} / 2);
}
&--number, &--icon, &--dotIcon {
top: calc(#{$tn-steps-item-number-width} / 2);
}
}
}
&--column {
display: flex;
flex-direction: row;
justify-content: flex-start;
min-height: 120rpx;
.tn-steps__item__line {
border-left-width: 1px;
border-left-style: solid;
height: 50%;
top: 75%;
&--dot {
left: calc(#{$tn-steps-item-dot-width} / 2);
}
&--number, &--icon, &--dotIcon {
left: calc(#{$tn-steps-item-number-width} / 2);
}
}
}
}
}
</style>
@@ -0,0 +1,183 @@
<template>
<view class="tn-sticky-class">
<view
class="tn-sticky__wrap"
:class="[stickyClass]"
:style="[stickyStyle]"
>
<view
class="tn-sticky__item"
:style="{
position: fixed ? 'fixed' : 'static',
top: stickyTop + 'px',
left: left + 'px',
width: width === 'auto' ? 'auto' : width + 'px',
zIndex: elZIndex
}"
>
<slot></slot>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'tn-sticky',
props: {
// 吸顶容器到顶部某个距离的时候进行吸顶
// 在H5中,customNavBar的高度为45px
offsetTop: {
type: Number,
default: 0
},
// H5顶部导航栏的高度
h5NavHeight: {
type: Number,
default: 45
},
// 自定义顶部导航栏高度
customNavHeight: {
type: Number,
default: 0
},
// 是否开启吸顶
enabled: {
type: Boolean,
default: true
},
// 吸顶容器的背景颜色
backgroundColor: {
type: String,
default: '#FFFFFF'
},
// z-index
zIndex: {
type: Number,
default: 0
},
// 索引值,区分不同的吸顶组件
index: {
type: [String, Number],
default: ''
}
},
computed: {
elZIndex() {
return this.zIndex ? this.zIndex : this.$t.zIndex.sticky
},
backgroundColorStyle() {
return this.$t.colorUtils.getBackgroundColorStyle(this.backgroundColor)
},
backgroundColorClass() {
return this.$t.colorUtils.getBackgroundColorInternalClass(this.backgroundColor)
},
stickyClass() {
let clazz = ''
clazz += this.elClass
if (this.backgroundColorClass) {
clazz += ` ${this.backgroundColorClass}`
}
return clazz
},
stickyStyle() {
let style = {}
style.height = this.fixed ? this.height + 'px' : 'auto'
if (this.backgroundColorStyle) {
style.color = this.backgroundColorStyle
}
if (this.elZIndex) {
style.zIndex = this.elZIndex
}
return style
}
},
data() {
return {
// 监听组件别名
stickyObserverName: 'tnStickyObserver',
// 组件的唯一编号
elClass: this.$t.uuid(),
// 是否固定
fixed: false,
// 高度
height: 'auto',
// 宽度
width: 'auto',
// 距离顶部的距离
stickyTop: 0,
// 左边距离
left: 0
}
},
watch: {
offsetTop(val) {
this.initObserver()
},
enabled(val) {
if (val === false) {
this.fixed = false
this.disconnectObserver(this.stickyObserverName)
} else {
this.initObserver()
}
}
},
mounted() {
this.initObserver()
},
methods: {
// 初始化监听组件的布局状态
initObserver() {
if (!this.enabled) return
// #ifdef H5
this.stickyTop = this.offsetTop != 0 ? uni.upx2px(this.offsetTop) + this.h5NavHeight : this.h5NavHeight
// #endif
// #ifndef H5
this.stickyTop = this.offsetTop != 0 ? uni.upx2px(this.offsetTop) + this.customNavHeight : this.customNavHeight
// #endif
this.disconnectObserver(this.stickyObserverName)
this._tGetRect('.' + this.elClass).then((res) => {
this.height = res.height
this.left = res.left
this.width = res.width
this.$nextTick(() => {
this.connectObserver()
})
})
},
// 监听组件的布局状态
connectObserver() {
this.disconnectObserver(this.stickyObserverName)
// 组件内获取布局状态,不能用uni.createIntersectionObserver,而必须用this.createIntersectionObserver
const contentObserver = this.createIntersectionObserver({
thresholds: [0.95, 0.98, 1]
})
contentObserver.relativeToViewport({
top: -this.stickyTop
})
contentObserver.observe('.' + this.elClass, res => {
if (!this.enabled) return
this.setFixed(res.boundingClientRect.top)
})
this[this.stickyObserverName] = contentObserver
},
// 设置是否固定
setFixed(top) {
const fixed = top < this.stickyTop
if (fixed) this.$emit('fixed', this.index)
else if (this.fixed) this.$emit('unfixed', this.index)
this.fixed = fixed
},
// 停止监听组件的布局状态
disconnectObserver(observerName) {
const observer = this[observerName]
observer && observer.disconnect()
}
}
}
</script>
<style>
</style>
@@ -0,0 +1,381 @@
<template>
<view
class="tn-subsection-class tn-subsection"
:class="[subsectionBackgroundColorClass]"
:style="[subsectionStyle]"
>
<!-- 滑块 -->
<block v-for="(item, index) in listInfo" :key="index">
<view
class="tn-subsection__item tn-text-ellipsis"
:class="[
'section-item-' + index,
noBorderRight(index)
]"
:style="[itemStyle(index)]"
@tap="click(index)"
>
<view class="tn-subsection__item--text tn-text-ellipsis" :style="[textStyle(index)]">
{{ item.name }}
</view>
</view>
</block>
<!-- 背景 -->
<view class="tn-subsection__bg" :style="[itemBarStyle]"></view>
</view>
</template>
<script>
import componentsColorMixin from '../../libs/mixin/components_color.js'
export default {
mixins: [componentsColorMixin],
name: 'tn-subsection',
props: {
// 模式选择
// button 按钮模式 subsection 分段模式
mode: {
type: String,
default: 'button'
},
// 组件高度
height: {
type: Number,
default: 60
},
// tab的数据
list: {
type: Array,
default() {
return []
}
},
// 当前活动tab的index
current: {
type: [Number, String],
default: 0
},
// 激活时的字体颜色
activeColor: {
type: String,
default: '#FFFFFF'
},
// 未激活时的字体颜色
inactiveColor: {
type: String,
default: '#AAAAAA'
},
// 激活tab的字体是否加粗
bold: {
type: Boolean,
default: false
},
backgroundColor: {
type: String,
default: '#F4F4F4'
},
// 滑块的颜色
buttonColor: {
type: String,
default: '#01BEFF'
},
// 是否开启动画
animation: {
type: Boolean,
default: true
},
// 滑动滑块的是否,是否触发震动
vibrateShort: {
type: Boolean,
default: false
}
},
data() {
return {
// 列表数据
listInfo: [],
// 子元素的背景样式
itemBgStyle: {
width: 0,
left: 0,
backgroundColor: '#ffffff',
height: '100%',
transition: ''
},
// 当前选中的滑块
currentIndex: this.current,
buttonPadding: 3,
borderRadius: 5,
// 组件初始化的是否current变换不应该震动
firstVibrateShort: true
}
},
watch: {
current: {
handler(val) {
this.currentIndex = val
this.changeSectionStatus(val)
},
immediate: true
}
},
created() {
// 将list的数据,传入listInfo数组
// 接受直接数组形式,或者数组元素为对象的形式,如:['开启', '关闭'],或者[{name: '开启'}, {name: '关闭'}]
this.listInfo = this.list.map((item, index) => {
if (typeof item !== 'object') {
let obj = {
width: 0,
name: item
}
return obj
} else {
item.width = 0
return obj
}
})
},
computed: {
// 设置mode=subsection时,滑块没有样式
noBorderRight() {
return index => {
if (this.mode !== 'subsection') return
let clazz = ''
// 不显示右边的边距
if (index < this.list.length - 1) clazz += ' tn-subsection__item--none-border-right'
// 显示整个组件的左右边圆角
if (index === 0) clazz += ' tn-subsection__item--first'
if (index === this.list.length - 1) clazz += ' tn-subsection__item--last'
return clazz
}
},
// 文字的样式
textStyle() {
return index => {
let style = {}
// 设置字体颜色
if (this.mode === 'subsection') {
if (index === this.currentIndex) {
style.color = '#FFFFFF'
} else {
style.color = this.inactiveColor
}
} else {
if (index === this.currentIndex) {
style.color = this.activeColor
} else {
style.color = this.inactiveColor
}
}
// 字体加粗
if (index === this.currentIndex && this.bold) style.fontWeight = 'bold'
// 文字大小
style.fontSize = (this.fontSize || 26) + this.fontUnit
return style
}
},
// 每个分段器item的样式
itemStyle() {
return index => {
let style = {}
if (this.mode === 'subsection') {
// 设置border的样式
style.borderColor = this.inactiveColor
style.borderWidth = '1rpx'
style.borderStyle = 'solid'
}
return style
}
},
// mode = button时,设置外层view的样式
subsectionStyle() {
let style = {}
style.height = this.height + 'rpx'
if (this.mode === 'button') {
style.backgroundColor = this.backgroundColorStyle
style.padding = `${this.buttonPadding}px`
style.borderRadius = `${this.borderRadius}px`
}
return style
},
// mode = button时,设置外层view的背景class
subsectionBackgroundColorClass() {
let clazz = ''
if (this.mode === 'button' && this.backgroundColorClass) {
clazz = this.backgroundColorClass
}
return clazz
},
// 滑块样式
itemBarStyle() {
let style = {}
style.backgroundColor = this.buttonColor
style.zIndex = 1
if (this.mode === 'button') {
style.backgroundColor = this.buttonColor
style.borderRadius = `${this.borderRadius}px`
style.bottom = `${this.buttonPadding}px`
style.height = (this.height - (this.buttonPadding * 4)) + 'rpx'
style.zIndex = 0
}
return Object.assign(this.itemBgStyle, style)
}
},
mounted() {
// 等待加载组件完成
setTimeout(() => {
this.getTabsInfo()
}, 10)
},
methods: {
// 改变滑块样式
changeSectionStatus(val) {
if (this.mode === 'subsection') {
// 根据滑块在最左和最右时,显示对应的圆角
if (val === this.list.length - 1) {
this.itemBgStyle.borderRadius = `0 ${this.buttonPadding}px ${this.buttonPadding}px 0`
}
if (val === 0) {
this.itemBgStyle.borderRadius = `${this.buttonPadding}px 0 0 ${this.buttonPadding}px`
}
if (val > 0 && val < this.list.length - 1) {
this.itemBgStyle.borderRadius = '0'
}
}
// 更新滑块的位置
setTimeout(() => {
this.itemBgLeft()
}, 10)
if (this.vibrateShort && !this.firstVibrateShort) {
// 使手机产生短促震动,微信小程序有效,APP(HX 2.6.8)和H5无效
// #ifndef H5
uni.vibrateShort();
// #endif
}
this.firstVibrateShort = false
},
// 获取各个tab的节点信息
getTabsInfo() {
let view = uni.createSelectorQuery().in(this)
for (let i = 0; i < this.list.length; i++) {
view.select('.section-item-' + i).boundingClientRect()
}
view.exec(res => {
// 如果没有获取到,则重新获取
if (!res.length) {
setTimeout(() => {
this.getTabsInfo()
return
}, 10)
}
// 将每个分段器的宽度放入listInfo中
res.map((item, index) => {
this.listInfo[index].width = item.width
})
// 初始化滑块的宽度
if (this.mode === 'subsection') {
this.itemBgStyle.width = this.listInfo[0].width + 'px'
} else if (this.mode === 'button') {
this.itemBgStyle.width = this.listInfo[0].width + 'px'
}
// 初始化滑块的位置
this.itemBgLeft()
})
},
// 设置滑块的位置
itemBgLeft() {
// 是否开启动画
if(this.animation) {
this.itemBgStyle.transition = 'all 0.3s'
} else {
this.itemBgStyle.transition = 'all 0s'
}
let left = 0
// 计算当前活跃item到组件左边的距离
this.listInfo.map((item, index) => {
if (index < this.currentIndex) left += item.width
})
// 根据不同的模式,计算滑块的位置
if (this.mode === 'subsection') {
this.itemBgStyle.left = left + 'px'
} else if (this.mode === 'button') {
this.itemBgStyle.left = left + this.buttonPadding + 'px'
}
},
// 点击事件
click(index) {
// 不允许点击当前激活的选项
if (index === this.currentIndex) return
this.currentIndex = index
this.changeSectionStatus(index)
this.$emit('change', {
index: Number(index),
name: this.listInfo[index]['name']
})
}
}
}
</script>
<style lang="scss" scoped>
.tn-subsection {
/* #ifndef APP-PLUS */
display: flex;
flex-direction: row;
/* #endif */
align-items: center;
overflow: hidden;
position: relative;
&__item {
/* #ifndef APP-PLUS */
display: flex;
flex-direction: row;
/* #endif */
flex: 1;
text-align: center;
font-size: 26rpx;
height: 100%;
align-items: center;
justify-content: center;
color: #FFFFFF;
padding: 0 6rpx;
&--text {
transform: all 0.3s;
color: #FFFFFF;
/* #ifndef APP-PLUS */
display: flex;
flex-direction: row;
/* #endif */
align-items: center;
position: relative;
z-index: 3;
}
&--first {
border-top-left-radius: 8rpx;
border-bottom-left-radius: 8rpx;
}
&--last {
border-top-right-radius: 8rpx;
border-bottom-right-radius: 8rpx;
}
&--none-border-right {
border-right: none !important;
}
}
&__bg {
background-color: $tn-main-color;
position: absolute;
z-index: -1;
}
}
</style>
@@ -0,0 +1,220 @@
/**
* 此为wxs模块,只支持APP-VUE,微信和QQ小程序以及H5平台
* wxs内部不支持es6语法,变量只能使用var定义,无法使用解构,箭头函数等特性
*/
// 开始触摸
function touchStart(event, ownerInstance) {
// 触发事件的组件的ComponentDescriptor实例
var instance = event.instance
// wxs内的局部变量快照,此快照是属于整个组件,在touchstart和touchmove事件中都能获取到相同的结果
var state = instance.getState()
if (state.disabled) return
var touches = event.touches
// 如果进行的是多指触控,不允许操作
if (touches && touches.length > 1) return
// 标识当前为滑动中状态
state.moving = true
// 记录触摸开始点的坐标点
state.startX = touches[0].pageX
state.startY = touches[0].pageY
ownerInstance.callMethod('closeOther')
}
// 触摸滑动
function touchMove(event, ownerInstance) {
// 触发事件的组件的ComponentDescriptor实例
var instance = event.instance
// wxs内的局部变量快照,此快照是属于整个组件,在touchstart和touchmove事件中都能获取到相同的结果
var state = instance.getState()
if (state.disabled || !state.moving) return
var touches = event.touches
var pageX = touches[0].pageX
var pageY = touches[0].pageY
var moveX = pageX - state.startX
var moveY = pageY - state.startY
var buttonsWidth = state.buttonsWidth
// 移动的X轴距离大于Y轴距离,也即终点与起点位置连线,与X轴夹角小于45度时,禁止页面滚动
if (Math.abs(moveX) > Math.abs(moveY) || Math.abs(moveX) > state.threshold) {
event.preventDefault && event.preventDefault()
event.stopPropagation && event.stopPropagation()
}
// 移动的Y轴距离大于X轴距离,也即终点与起点位置连线,与Y轴夹角小于45度时,认为页面时上下滑动而不是左右滑动单元格
if (Math.abs(moveX) < Math.abs(moveY)) return
// 限制右滑的距离,不允许内容部分往右偏移,右滑会导致X轴偏移值大于0,以此做判断
// 此处不能直接return,因为滑动过程中会缺失某些关键点坐标,会导致错乱,所以处理的方法是在超出后设置为0
if (state.status === 'open') {
// 在开启状态下,忽略左滑动
if (moveX < 0) moveX = 0
// 要收起菜单,最大能移动的距离为按钮的总宽度
if (moveX > buttonsWidth) moveX = buttonsWidth
// 如果是已经打开的状态,向左滑动时,移动收起菜单
moveSwipeAction(-buttonsWidth + moveX, instance, ownerInstance)
} else {
// 关闭状态下,忽略右滑
if (moveX > 0) return
// 滑动的距离不允许超过所有按钮的总宽度,此时只能左滑
// 滑动距离设置为按钮的宽度(负数)
if (Math.abs(moveX) > buttonsWidth) moveX = -buttonsWidth
// 在滑动过程中不断移动单元格内容,使其不断显示出来
moveSwipeAction(moveX, instance, ownerInstance)
}
}
// 触摸结束
function touchEnd(event, ownerInstance) {
// 触发事件的组件的ComponentDescriptor实例
var instance = event.instance
// wxs内的局部变量快照,此快照是属于整个组件,在touchstart和touchmove事件中都能获取到相同的结果
var state = instance.getState()
if (!state.moving || state.disabled) return
var touches = event.changedTouches ? event.changedTouches[0] : {}
var pageX = touches.pageX
var pageY = touches.pageY
var moveX = pageX - state.startX
if (state.status === 'open') {
// 在开启状态下,忽略左滑动
if (moveX < 0) moveX = 0
// 在开启状态下,点击一下内容区域,moveX为0,也即没有移动,这是执行收起操作
if (moveX === 0) {
return closeSwipeAction(instance, ownerInstance)
}
// 在开启状态下,滑动距离小于阈值,则默认不关闭同时恢复原来的打开状态
if (Math.abs(moveX) < state.threshold) {
openSwipeAction(instance, ownerInstance)
} else {
// 如果滑动距离大于阈值则执行收起逻辑
closeSwipeAction(instance, ownerInstance)
}
} else {
// 在关闭状态下,忽略右滑动
if (moveX > 0) return
if (Math.abs(moveX) < state.threshold) {
closeSwipeAction(instance, ownerInstance)
} else {
openSwipeAction(instance, ownerInstance)
}
}
}
// 获取过渡时间
function getDuration(value) {
if (value.toString().indexOf('s') >= 0) return value
return value > 30 ? value + 'ms' : value + 's'
}
// 移动滑动选择器内容区域,同时显示出其隐藏的菜单
function moveSwipeAction(moveX, instance, ownerInstance) {
var state = instance.getState()
// 获取所有按钮的实例,需要通过它去设置按钮的位移
var buttons = ownerInstance.selectAllComponents('.tn-swipe-action-item__right__button')
// 设置菜单内容部分的偏移
instance.requestAnimationFrame(function () {
instance.setStyle({
// 设置translateX的值
'transition': 'none',
transform: 'translateX('+ moveX +'px)',
'-webkit-transform': 'translateX('+ moveX +'px)'
})
})
}
// 一次性展开滑动菜单
function openSwipeAction(instance, ownerInstance) {
var state = instance.getState()
// 获取所有按钮的实例,需要通过它去设置按钮的位移
var buttons = ownerInstance.selectAllComponents('.tn-swipe-action-item__right__button')
// 处理duration单位问题
var duration = getDuration(state.duration)
// 展开过程中,是向左移动,所以x的偏移应该是负值
var buttonsWidth = -state.buttonsWidth
instance.requestAnimationFrame(function () {
// 设置菜单主体内容
instance.setStyle({
'transition': 'transform ' + duration,
'transform': 'translateX('+ buttonsWidth +'px)',
'-webkit-transform': 'translateX('+ buttonsWidth +'px)'
})
})
setStatus('open', instance, ownerInstance)
}
// 一次性收起滑动菜单
function closeSwipeAction(instance, ownerInstance) {
var state = instance.getState()
// 获取所有按钮的实例,需要通过它去设置按钮的位移
var buttons = ownerInstance.selectAllComponents('.tn-swipe-action-item__right__button')
var len = buttons.length
// 处理duration单位问题
var duration = getDuration(state.duration)
instance.requestAnimationFrame(function () {
// 设置菜单主体内容
instance.setStyle({
'transition': 'transform ' + duration,
'transform': 'translateX(0px)',
'-webkit-transform': 'translateX(0px)'
})
// 设置各个隐藏按钮的收起状态
for (var i = len - 1; i >= 0; i--) {
buttons[i].setStyle({
'transition': 'transform ' + duration,
'transform': 'translateX(0px)',
'-webkit-transform': 'translateX(0px)'
})
}
})
setStatus('close', instance, ownerInstance)
}
// 标记菜单的当前状态,open - 打开 close - 关闭
function setStatus(status, instance, ownerInstance) {
var state = instance.getState()
state.status = status
ownerInstance.callMethod('setStatus', status)
}
// status的状态发生变化
function statusChange(newValue, oldValue, ownerInstance, instance) {
var state = instance.getState()
if (state.disabled) return
// 打开或关闭单元格
if (newValue === 'close' && state.status === 'open') {
closeSwipeAction(instance, ownerInstance)
} else if (newValue === 'open' && state.status === 'close') {
openSwipeAction(instance, ownerInstance)
}
}
// 菜单尺寸发生变化
function sizeChange(newValue, oldValue, ownerInstance, instance) {
// wxs内的局部变量快照
var state = instance.getState()
state.disabled = newValue.disabled
state.duration = newValue.duration
state.show = newValue.show
state.threshold = newValue.threshold
state.buttons = newValue.buttons
if (state.buttons) {
var len = state.buttons.length
var buttonsWidth = 0
var buttons = newValue.buttons
for (var i = 0; i < len; i++) {
buttonsWidth += buttons[i].width
}
}
state.buttonsWidth = buttonsWidth
}
module.exports = {
touchStart: touchStart,
touchMove: touchMove,
touchEnd: touchEnd,
sizeChange: sizeChange,
statusChange: statusChange
}
@@ -0,0 +1,229 @@
<template>
<view class="tn-swipe-action-item-class tn-swipe-action-item">
<view class="tn-swipe-action-item__right">
<slot name="button">
<view
v-for="(item, index) in options"
:key="index"
class="tn-swipe-action-item__right__button"
:style="[{
alignItems: item.style && item.style.borderRadius ? 'center' : 'stretch'
}]"
@tap="buttonClickHandler(item, index)"
>
<view
class="tn-swipe-action-item__right__button__wrapper"
:style="[{
backgroundColor: item.style && item.style.backgroundColor ? item.style.backgroundColor : '#AAAAAA',
borderRadius: item.style && item.style.borderRadius ? item.style.borderRadius : '0',
padding: item.style && item.style.borderRadius ? '0' : '0 30rpx'
}, item.style]"
>
<view
v-if="item.icon"
:class="[`tn-icon-${item.icon}`]"
:style="[{
color: item.style && item.style.color ? item.style.color : '#FFFFFF',
fontSize: item.iconSize ? item.iconSize + 'rpx' : item.style && item.style.fontSize ? (item.style.fontsize * 1.2) + 'rpx' : '34rpx',
marginRight: item.text ? '4rpx' : 0
}]"
></view>
<text
v-if="item.text"
class="tn-swipe-action-item__right__button__text tn-text-ellipsis"
:style="[{
color: item.style && item.style.color ? item.style.color : '#FFFFFF',
fontSize: item.style && item.style.fontSize ? item.style.fontSize + 'rpx' : '32rpx',
lineHeight: item.style && item.style.fontSize ? item.style.fontSize + 'rpx' : '32rpx'
}]"
>{{ item.text }}</text>
</view>
</view>
</slot>
</view>
<view
class="tn-swipe-action-item__content"
:status="status"
:size="size"
:change:status="wxs.statusChange"
:change:size="wxs.sizeChange"
@touchstart="wxs.touchStart"
@touchmove="wxs.touchMove"
@touchend="wxs.touchEnd"
>
<slot></slot>
</view>
</view>
</template>
<!-- #ifdef APP-VUE || MP-WEIXIN || H5 || MP-QQ -->
<script src="./index.wxs" module="wxs" lang="wxs"></script>
<!-- #endif -->
<script>
export default {
name: 'tn-swipe-action-item',
props: {
// 是否显示滑动菜单
show: {
type: Boolean,
default: false
},
// 标识符,如果是v-for可用index的索引值
name: {
type: [String, Number],
default: ''
},
// 右侧按钮内容
options: {
type: Array,
default() {
return []
}
},
// 是否禁用
disabled: {
type: Boolean,
default: false
},
// 是否自动关闭其他swipe按钮组
autoClose: {
type: Boolean,
default: true
},
// 滑动距离阈值,大于此值才会打开菜单
threshold: {
type: Number,
default: 20
},
// 动画过渡时间,单位ms
duration: {
type: Number,
default: 300
}
},
computed: {
// 由于wxs无法直接读取外部的值,需要在外部值变化时,重新执行赋值逻辑
itemData() {
return [this.disabled, this.autoClose, this.threshold, this.options, this.duration]
}
},
data() {
return {
// 按钮尺寸信息
size: {},
// 父组件参数
parentData: {
autoClose: true
},
// 当前状态
status: 'close'
}
},
watch: {
itemData() {
this.queryRect()
}
},
created() {
this.parent = false
this.updateParentData()
this.parent && this.parent.children.push(this)
},
mounted() {
this.$nextTick(() => {
this.init()
})
},
methods: {
init() {
this.queryRect()
},
// 更新父组件信息
updateParentData() {
this.getParentData('tn-swipe-action')
},
// 查询节点
queryRect() {
this._tGetRect('.tn-swipe-action-item__right__button', true).then(res => {
this.size = {
buttons: res,
show: this.show,
disabled: this.disabled,
threshold: this.threshold,
duration: this.duration
}
})
},
// 按钮点击
buttonClickHandler(item, index) {
this.$emit('click', {
index,
name: item.name
})
},
// 关闭时执行
closeHandler() {
this.status = 'close'
},
// 设置状态
setStatus(status) {
this.status = status
},
// 关闭其他单元格
closeOther() {
this.parent && this.parent.closeOther(this)
}
}
}
</script>
<style lang="scss" scoped>
.tn-swipe-action-item {
position: relative;
overflow: hidden;
touch-action: none;
&__right {
display: flex;
flex-direction: row;
position: absolute;
top: 0;
bottom: 0;
right: 0;
&__button {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
overflow: hidden;
&__wrapper {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 0 30rpx;
}
&__text {
display: flex;
flex-direction: row;
align-content: center;
justify-content: center;
text-align: center;
color: #FFFFFF;
font-size: 30rpx;
}
}
}
&__content {
background-color: #FFFFFF;
transform: translateX(0px);
}
}
</style>
@@ -0,0 +1,61 @@
<template>
<view class="tn-swipe-action-class tn-swipe-action">
<slot></slot>
</view>
</template>
<script>
export default {
name: 'tn-swipe-action',
props: {
// 是否自动关闭其他swipe按钮组
autoClose: {
type: Boolean,
default: true
}
},
provide() {
return {
swipeAction: this
}
},
computed: {
// 用于监听父组件参数变化
parentData() {
return [this.autoClose]
}
},
data() {
return {}
},
watch: {
parentData() {
if (this.children.length) {
this.children.map(child => {
// 判断子组件(tn-swipe-action-item)如果有updateParentData方法的话,就就执行(执行的结果是子组件重新从父组件拉取了最新的值)
typeof(child.updateParentData) === 'function' && child.updateParentData()
})
}
}
},
created() {
this.children = []
},
methods: {
// 关闭其他单元格
closeOther(child) {
if (this.autoClose) {
// 历遍所有的单元格,找出非当前操作中的单元格,进行关闭
this.children.map((item, index) => {
if (child !== item) {
item.closeHandler()
}
})
}
}
}
}
</script>
<style>
</style>
@@ -0,0 +1,363 @@
<template>
<view class="tn-swiper__wrap-class tn-swiper__wrap" :style="{borderRadius: `${radius}rpx`}">
<!-- 轮播图 -->
<swiper
class="tn-swiper"
:class="[backgroundColorClass]"
:style="[swiperStyle]"
:current="current"
:interval="interval"
:circular="circular"
:autoplay="autoplay"
:duration="duration"
:previous-margin="effect3d ? effect3dPreviousSpacing + 'rpx' : '0'"
:next-margin="effect3d ? effect3dPreviousSpacing + 'rpx' : '0'"
@change="change"
>
<swiper-item
v-for="(item, index) in list"
:key="index"
class="tn-swiper__item"
>
<view
class="tn-swiper__item__image__wrap"
:class="[swiperIndex !== index ? 'tn-swiper__item__image--scale' : '']"
:style="{
borderRadius: `${radius}rpx`,
transform: effect3d && swiperIndex !== index ? 'scaleY(0.9)' : 'scaleY(1)',
margin: effect3d && swiperIndex !== index ? '0 20rpx' : 0
}"
>
<image class="tn-swiper__item__image" :src="item[name] || item" :mode="imageMode"></image>
<view
v-if="title && item[titleName]"
class="tn-swiper__item__title tn-text-ellipsis"
:style="[titleStyle]">
{{ item[titleName] }}
</view>
</view>
</swiper-item>
</swiper>
<!-- 指示点 -->
<view class="tn-swiper__indicator" :style="[indicatorStyle]">
<block v-if="mode === 'rect'">
<view
v-for="(item, index) in list"
:key="index"
class="tn-swiper__indicator__rect"
:class="{'tn-swiper__indicator__rect--active': swiperIndex === index}"
></view>
</block>
<block v-if="mode === 'dot'">
<view
v-for="(item, index) in list"
:key="index"
class="tn-swiper__indicator__dot"
:class="{'tn-swiper__indicator__dot--active': swiperIndex === index}"
></view>
</block>
<block v-if="mode === 'round'">
<view
v-for="(item, index) in list"
:key="index"
class="tn-swiper__indicator__round"
:class="{'tn-swiper__indicator__round--active': swiperIndex === index}"
></view>
</block>
<block v-if="mode === 'number'">
<view class="tn-swiper__indicator__number">{{ swiperIndex + 1 }}/{{ list.length }}</view>
</block>
</view>
</view>
</template>
<script>
export default {
name: 'tn-swiper',
props: {
// 轮播图列表数据
// [{image: xxx.jpg, title: 'xxxx'}]
list: {
type: Array,
default() {
return []
}
},
// 初始化时,默认显示第几项
current: {
type: Number,
default: 0
},
// 高度
height: {
type: Number,
default: 250
},
// 背景颜色
backgroundColor: {
type: String,
default: 'transparent'
},
// 图片的属性名
name: {
type: String,
default: 'image'
},
// 是否显示标题
title: {
type: Boolean,
default: false
},
// 标题的属性名
titleName: {
type: String,
default: 'title'
},
// 用户自定义标题样式
titleStyle: {
type: Object,
default() {
return {}
}
},
// 圆角的值
radius: {
type: Number,
default: 8
},
// 指示器模式
// rect -> 方形 round -> 圆角方形 dot -> 点 number -> 轮播图下标
mode: {
type: String,
default: 'round'
},
// 指示器位置
// topLeft \ topCenter \ topRight \ bottomLeft \ bottomCenter \ bottomRight
indicatorPosition: {
type: String,
default: 'bottomCenter'
},
// 开启3D缩放效果
effect3d: {
type: Boolean,
default: false
},
// 在3D缩放模式下,item之间的间隔
effect3dPreviousSpacing: {
type: Number,
default: 50
},
// 自定播放
autoplay: {
type: Boolean,
default: true
},
// 图片之间播放间隔多久
interval: {
type: Number,
default: 3000
},
// 轮播间隔时间
duration: {
type: Number,
default: 500
},
// 是否衔接滑动
circular: {
type: Boolean,
default: true
},
// 图片裁剪模式
imageMode: {
type: String,
default: 'aspectFill'
}
},
computed: {
backgroundColorStyle() {
return this.$t.colorUtils.getBackgroundColorStyle(this.backgroundColor)
},
backgroundColorClass() {
return this.$t.colorUtils.getBackgroundColorInternalClass(this.backgroundColor)
},
swiperStyle() {
let style = {}
if (this.backgroundColorStyle) {
style.backgroundColor = this.backgroundColorStyle
}
if (this.height) {
style.height = this.height + 'rpx'
}
return style
},
indicatorStyle() {
let style = {}
if (this.indicatorPosition === 'topLeft' || this.indicatorPosition === 'bottomLeft') style.justifyContent = 'flex-start'
if (this.indicatorPosition === 'topCenter' || this.indicatorPosition === 'bottomCenter') style.justifyContent = 'center'
if (this.indicatorPosition === 'topRight' || this.indicatorPosition === 'bottomRight') style.justifyContent = 'flex-end'
if (['topLeft','topCenter','topRight'].indexOf(this.indicatorPosition) >= 0) {
style.top = '12rpx'
style.bottom = 'auto'
} else {
style.top = 'auto'
style.bottom = '12rpx'
}
style.padding = `0 ${this.effect3d ? '74rpx' : '24rpx'}`
return style
},
swiperTitleStyle() {
let style = {}
if (this.mode === 'none' || this.mode === '') style.paddingBottom = '12rpx'
if (['bottomLeft','bottomCenter','bottomRight'].indexOf(this.indicatorPosition) >= 0 && this.mode === 'number') {
style.paddingBottom = '60rpx'
} else if (['bottomLeft','bottomCenter','bottomRight'].indexOf(this.indicatorPosition) >= 0 && this.mode !== 'number') {
style.paddingBottom = '40rpx'
} else {
style.paddingBottom = '12rpx'
}
style = Object.assign(style, this.titleStyle)
return style
}
},
data() {
return {
// 当前显示的item的index
swiperIndex: this.current
}
},
watch: {
list(newVal, oldVal) {
// 如果修改了list的数据,重置current的值
if (newVal.length !== oldVal.length) this.swiperIndex = 0
},
current(value) {
// 监听外部current的变化,实时修改内部依赖于此测swiperIndex值,如果更新了current,而不是更新swiperIndex,就会错乱,因为指示器是依赖于swiperIndex的
this.swiperIndex = value
}
},
methods: {
click(index) {
this.$emit('click', index)
},
// 图片自动切换时触发
change(event) {
const current = event.detail.current
this.swiperIndex = current
this.$emit('change', current)
}
}
}
</script>
<style lang="scss" scoped>
.tn-swiper {
&__wrap {
position: relative;
overflow: hidden;
transform: translateY(0);
}
&__item {
display: flex;
flex-direction: row;
align-items: center;
overflow: hidden;
&__image {
width: 100%;
height: 100%;
will-change: transform;
display: block;
/* #ifdef H5 */
pointer-events: none;
/* #endif */
&__wrap {
width: 100%;
height: 100%;
flex: 1;
transition: all 0.5s;
overflow: hidden;
box-sizing: content-box;
position: relative;
}
&--scale {
transform-origin: center center;
}
}
&__title {
width: 100%;
position: absolute;
background-color: rgba(0, 0, 0, 0.3);
bottom: 0;
left: 0;
font-size: 28rpx;
padding: 12rpx 24rpx;
color: rgba(255, 255, 255, 0.8);
}
}
&__indicator {
padding: 0 24rpx;
position: absolute;
display: flex;
flex-direction: row;
width: 100%;
z-index: 1;
&__rect {
width: 26rpx;
height: 8rpx;
background-color: rgba(0, 0, 0, 0.3);
transition: all 0.5s;
&--active {
background-color: rgba(255, 255, 255, 0.8);
}
}
&__dot {
width: 14rpx;
height: 14rpx;
margin: 0 6rpx;
border-radius: 20rpx;
background-color: rgba(0, 0, 0, 0.3);
transition: all 0.5s;
&--active {
background-color: rgba(255, 255, 255, 0.8);
}
}
&__round {
width: 14rpx;
height: 14rpx;
margin: 0 6rpx;
border-radius: 20rpx;
background-color: rgba(0, 0, 0, 0.3);
transition: all 0.5s;
&--active {
width: 34rpx;
background-color: rgba(255, 255, 255, 0.8);
}
}
&__number {
padding: 6rpx 16rpx;
line-height: 1;
background-color: rgba(0, 0, 0, 0.3);
color: rgba(255, 255, 255, 0.8);
border-radius: 100rpx;
font-size: 26rpx;
}
}
}
</style>
@@ -0,0 +1,241 @@
<template>
<view
class="tn-switch-class tn-switch"
:class="[
value ? 'tn-switch--on' : '',
disabled ? 'tn-switch--disabled' : '',
`tn-switch--${shape}`
]"
:style="[switchStyle]"
@tap="click"
>
<view
class="tn-switch__node"
:class="[`tn-switch__node--${shape}`]"
:style="[switchNodeStyle]"
>
<tn-loading class="tn-switch__node__loading" :show="loading" mode="flower" :size="size * 0.6" :color="loadingColor"></tn-loading>
</view>
<!-- 左图标 -->
<view
v-if="leftIcon !== ''"
class="tn-switch__icon tn-switch__icon--left"
:class="[
`tn-icon-${leftIcon}`,
value ? 'tn-switch__icon--show' : ''
]"
:style="[iconStyle]"></view>
<!-- 右图标 -->
<view
v-if="rightIcon !== ''"
class="tn-switch__icon tn-switch__icon--right"
:class="[
`tn-icon-${rightIcon}`,
!value ? 'tn-switch__icon--show' : ''
]"
:style="[iconStyle]"></view>
</view>
</template>
<script>
export default {
name: 'tn-switch',
props: {
value: {
type: Boolean,
default: false
},
// 按钮的样式
// circle 圆角 square 方形
shape: {
type: String,
default: 'circle'
},
// 是否禁用
disabled: {
type: Boolean,
default: false
},
// 尺寸
size: {
type: Number,
default: 50
},
// 打开时的背景颜色
activeColor: {
type: String,
default: ''
},
// 关闭时的背景颜色
inactiveColor: {
type: String,
default: ''
},
// 激活时的值
activeValue: {
type: [Number, String, Boolean],
default: true
},
// 关闭时的值
inactiveValue: {
type: [Number, String, Boolean],
default: false
},
// 左图标
leftIcon: {
type: String,
default: ''
},
// 右图标
rightIcon: {
type: String,
default: ''
},
// 是否为加载状态
loading: {
type: Boolean,
default: false
},
// 点击手机是否震动
vibrateShort: {
type: Boolean,
default: false
}
},
computed: {
switchStyle() {
let style = {}
style.fontSize = this.$t.string.getLengthUnitValue(this.size)
style.backgroundColor = this.value ?
this.activeColor ? this.activeColor : '#01BEFF' :
this.inactiveColor ? this.inactiveColor : '#AAAAAA'
return style
},
switchNodeStyle() {
let style = {}
style.width = this.$t.string.getLengthUnitValue(this.size)
style.height = style.width
return style
},
iconStyle() {
let style = {}
style.fontSize = this.$t.string.getLengthUnitValue(this.size - 20)
style.lineHeight = this.$t.string.getLengthUnitValue(this.size)
return style
},
loadingColor() {
return this.value ? this.activeColor : null
}
},
data() {
return {
}
},
methods: {
click() {
if (!this.disabled && !this.loading) {
if (this.vibrateShort) uni.vibrateShort()
this.$emit('input', !this.value)
// 放到下一个生命周期,因为双向绑定的value修改父组件状态需要时间,且是异步的
this.$nextTick(() => {
this.$emit('change', this.value ? this.activeValue : this.inactiveValue);
})
}
}
}
}
</script>
<style lang="scss" scoped>
.tn-switch {
/* #ifndef APP-NVUE */
display: inline-block;
/* #endif */
position: relative;
box-sizing: initial;
width: 2em;
height: 1em;
font-size: 50rpx;
background-color: #AAAAAA;
transition: background-color 0.3s;
&__node {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
position: absolute;
top: 0;
left: 0;
z-index: 1;
background-color: #FFFFFF;
transform: scale(0.9);
box-shadow: 0 6rpx 2rpx 0 rgba(0, 0, 0, 0.05), 0 4rpx 4rpx 0 rgba(0, 0, 0, 0.1), 0 6rpx 6rpx 0 rgba(0, 0, 0, 0.05);
transition: transform 0.3s cubic-bezier(0.3, 1.05, 0.4, 1.05);
-webkit-transition: transform 0.3s cubic-bezier(0.3, 1.05, 0.4, 1.05);
&__loading {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
&--circle {
border-radius: 100%;
}
&--square {
border-radius: 15%;
}
}
&__icon {
color: #FFFFFF;
font-size: 30rpx;
line-height: 50rpx;
height: 100%;
vertical-align: middle;
position: absolute;
transform: scale(0);
transform-origin: 50% 50%;
transition: transform 0.3s ease-in-out;
&--left {
top: 0;
left: 10rpx;
}
&--right {
top: 0;
right: 10rpx;
}
&--show {
transform: scale(1);
}
}
&--circle {
border-radius: 1em;
}
&--square {
border-radius: 0.1em;
}
&--on {
background-color: $tn-main-color;
.tn-switch__node {
transform: translateX(100%) scale(0.9);
}
}
&--disabled {
opacity: 0.4;
}
}
</style>
@@ -0,0 +1,575 @@
<template>
<view v-if="show" class="tn-tabbar-class tn-tabbar" @touchmove.stop.prevent="() => {}">
<!-- tabbar 内容-->
<view
class="tn-tabbar__content"
:class="{
'tn-tabbar--fixed': fixed,
'tn-safe-area-inset-bottom': safeAreaInsetBottom,
'tn-tabbar--shadow': shadow
}"
:style="{
height: height + 'rpx',
backgroundColor: bgColor
}"
>
<!-- tabbar item -->
<view
v-for="(item, index) in list"
:key="index"
class="tn-tabbar__content__item"
:id="`tabbar_item_${index}`"
:class="{'tn-tabbar__content__item--out': item.out}"
:style="{
backgroundColor: bgColor
}"
@tap.stop="clickItemHandler(index)"
>
<!-- tabbar item的图片或者icon-->
<view :class="[itemButtonClass(index)]"
:style="[itemButtonStyle(index)]"
>
<image
v-if="isImage(index)"
:src="elIcon(index)"
mode="scaleToFill"
class="tn-tabbar__content__item__image"
:style="{
width: `${item.iconSize || iconSize}rpx`,
height: `${item.iconSize || iconSize}rpx`
}"
></image>
<view
v-else
class="tn-tabbar__content__item__icon"
:class="[`tn-icon-${elIcon(index)}`,elIconColor(index, false)]"
:style="{
fontSize: `${item.iconSize || iconSize}rpx`,
color: elIconColor(index)
}"
></view>
<!-- 角标-->
<tn-badge
v-if="!item.out && (item.count || item.dot)"
:dot="item.dot || false"
backgroundColor="tn-bg-red"
:radius="item.dot ? 14 : 0"
:fontSize="14"
padding="2rpx 4rpx"
:absolute="true"
:top="2"
>
{{ $t.number.formatNumberString(item.count) }}
</tn-badge>
</view>
<!-- tabbar item的文字-->
<view
class="tn-tabbar__content__item__text"
:class="[elColor(index, false)]"
:style="{
color: elColor(index),
fontSize: `${fontSize}rpx`
}"
>
<text class="tn-text-ellipsis">{{ item.title }}</text>
</view>
</view>
<!-- item 突起部分 -->
<view
v-if="outItemIndex !== -1"
class="tn-tabbar__content__out"
:class="[{
'tn-tabbar__content__out--shadow': shadow
}, animation && value === outItemIndex ? `tn-tabbar__content__out--animation--${animationMode}` : '']"
:style="{
backgroundColor: bgColor,
left: outItemLeft,
width: `${outHeight}rpx`,
height: `${outHeight}rpx`,
top: `-${outHeight * 0.3}rpx`
}"
@tap.stop="clickItemHandler(outItemIndex)"
></view>
</view>
<!-- 防止tabbar塌陷 -->
<view class="tn-tabbar__placeholder" :class="{'tn-safe-area-inset-bottom': safeAreaInsetBottom}" :style="{
height: `calc(${height}rpx)`
}"></view>
</view>
</template>
<script>
export default {
name: 'tn-tabbar',
props: {
// 绑定当前被选中的current值
value: {
type: [String, Number],
default: 0
},
// 是否显示
show: {
type: Boolean,
default: true
},
// 图标列表
list: {
type: Array,
default() {
return []
}
},
// 高度,单位rpx
height: {
type: Number,
default: 100
},
// 突起的高度
outHeight: {
type: Number,
default: 100
},
// 背景颜色
bgColor: {
type: String,
default: '#FFFFFF'
},
// 图标大小
iconSize: {
type: Number,
default: 40
},
// 字体大小
fontSize: {
type: Number,
default: 24
},
// 激活时的颜色
activeColor: {
type: String,
default: '#01BEFF'
},
// 非激活时的颜色
inactiveColor: {
type: String,
default: '#AAAAAA'
},
// 激活时图标的颜色
activeIconColor: {
type: String,
default: '#01BEFF'
},
// 非激活时图标的颜色
inactiveIconColor: {
type: String,
default: '#AAAAAA'
},
// 激活时的自定义样式
activeStyle: {
type: Object,
default() {
return {}
}
},
// 是否显示阴影
shadow: {
type: Boolean,
default: true
},
// 点击时是否有动画
animation: {
type: Boolean,
default: false
},
// 点击时的动画模式
animationMode: {
type: String,
default: 'scale'
},
// 是否固定在底部
fixed: {
type: Boolean,
default: true
},
// 是否开启底部安全区适配,开启的话,会在iPhoneX机型底部添加一定的内边距
safeAreaInsetBottom: {
type: Boolean,
default: false
},
// 切换前回调
beforeSwitch: {
type: Function,
default: null
}
},
computed: {
// 当前字体的颜色
elColor() {
return (index, style = true) => {
let currentItem = this.list[index]
let color = ''
if (index === this.value) {
color = currentItem['activeColor'] || this.activeColor
} else {
color = currentItem['inactiveColor'] || this.inactiveColor
}
// 判断是否获取内部样式
if (style) {
if (this.$t.colorUtils.getFontColorStyle(color) !== '') {
return color
} else {
return ''
}
} else {
if (this.$t.colorUtils.getFontColorStyle(color) === '') {
return color
} else {
return ''
}
}
}
},
// 当前图标的颜色
elIconColor() {
return (index, style = true) => {
let currentItem = this.list[index]
let color = ''
if (index === this.value) {
color = currentItem['activeIconColor'] || this.activeIconColor
} else {
color = currentItem['inactiveIconColor'] || this.inactiveIconColor
}
// 判断是否获取内部样式
if (style) {
if (this.$t.colorUtils.getFontColorStyle(color) !== '') {
return color
} else {
return ''
}
} else {
if (this.$t.colorUtils.getFontColorStyle(color) === '') {
return color + ' tn-tabbar__content__item__icon--clip'
} else {
return ''
}
}
}
},
// 当前的图标
elIcon() {
return (index) => {
let currentItem = this.list[index]
if (index === this.value) {
return currentItem['activeIcon']
} else {
return currentItem['inactiveIcon']
}
}
},
// 突起部分item button对应的类
itemButtonClass() {
return (index) => {
let clazz = ''
if (this.list[index]['out']) {
clazz += 'tn-tabbar__content__item__button--out'
if (this.$t.colorUtils.getFontColorStyle(this.activeIconColor) === '') {
clazz += ` ${this.activeIconColor}`
}
if (this.value === index) {
clazz += ` tn-tabbar__content__item__button--out--animation--${this.animationMode}`
}
} else {
clazz += 'tn-tabbar__content__item__button'
if (this.value === index) {
clazz += ` tn-tabbar__content__item__button--animation--${this.animationMode}`
}
}
return clazz
}
},
// 突起部分item button样式
itemButtonStyle() {
return (index) => {
let style = {}
if (this.list[index]['out']) {
if (this.$t.colorUtils.getFontColorStyle(this.activeIconColor) !== '') {
style.backgroundColor = this.activeIconColor
}
style.width = `${this.outHeight - 35}rpx`
style.height = `${this.outHeight - 35}rpx`
style.top = `-${this.outHeight * 0.15}rpx`
return style
}
return style
}
},
// 判断图标是否为图片
isImage() {
return (index) => {
const icon = this.list[index]['activeIcon']
// 只有包含了'/'就认为是图片
return icon.indexOf('/') !== -1
}
}
},
data() {
return {
// 当前突起的位置
outItemLeft: '50%',
// 当前设置了突起按钮的index
outItemIndex: -1,
// 每一个item的信息
tabbatItemInfo: []
}
},
watch: {
},
created() {
this.getOutItemIndex()
},
mounted() {
this.$nextTick(() => {
this.getTabbarItem()
})
},
methods: {
// 获取每一个item的信息
getTabbarItem() {
let query = uni.createSelectorQuery().in(this)
// 遍历获取信息
for (let i = 0; i < this.list.length; i++) {
query.select(`#tabbar_item_${i}`).fields({
size: true,
rect: true
})
}
query.exec(res => {
if (!res) {
setTimeout(() => {
this.getTabbarItem()
}, 10)
return
}
this.tabbatItemInfo = res.map((item) => {
return {
left: item.left,
width: item.width
}
})
this.updateOutItemLeft()
})
},
// 获取突起Item所在的index(如果存在)
getOutItemIndex() {
this.outItemIndex = this.list.findIndex((item) => {
return item.hasOwnProperty('out') && item.out
})
},
// 点击底部菜单时触发
async clickItemHandler(index) {
if (this.beforeSwitch && typeof(this.beforeSwitch) === 'function') {
// 执行回调,同时传入索引当作参数
// 在微信,支付宝等环境(H5正常),会导致父组件定义的函数体中的this变成子组件的this
// 通过bind()方法,绑定父组件的this,让this的this为父组件的上下文
let beforeSwitch = this.beforeSwitch.bind(this.$t.$parent.call(this))(index)
// 判断是否返回了Promise
if (!!beforeSwitch && typeof beforeSwitch.then === 'function') {
await beforeSwitch.then(res => {
// Promise返回成功
this.switchTab(index)
}).catch(err => {
})
} else if (beforeSwitch === true) {
this.switchTab(index)
}
} else {
this.switchTab(index)
}
},
// 切换tab
switchTab(index) {
// 发出事件和修改v-model绑定的值
this.$emit('change', index)
this.$emit('input', index)
},
// 设置突起的位置
updateOutItemLeft() {
// 查找出需要突起的元素
const index = this.list.findIndex((item) => {
return item.out
})
if (index !== -1) {
this.outItemLeft = this.tabbatItemInfo[index].left + (this.tabbatItemInfo[index].width / 2) + 'px'
}
}
}
}
</script>
<style lang="scss" scoped>
.tn-tabbar {
&__content {
box-sizing: content-box;
display: flex;
flex-direction: row;
align-items: center;
position: relative;
width: 100%;
z-index: 1024;
&__out {
position: absolute;
z-index: 4;
border-radius: 100%;
left: 50%;
transform: translateX(-50%);
&--shadow {
box-shadow: 0rpx -10rpx 30rpx 0rpx rgba(0, 0, 0, 0.05);
&::before {
content: " ";
position: absolute;
width: 100%;
height: 50rpx;
bottom: 0;
left: 0;
right: 0;
margin: auto;
background-color: inherit;
}
}
&--animation {
&--scale {
transform-origin: 50% 100%;
animation:tabbar-content-out-click 0.2s forwards 1 ease-in-out;
}
}
}
&__item {
flex: 1;
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: center;
height: 100%;
position: relative;
&__button {
margin-bottom: 10rpx;
display: flex;
align-items: center;
justify-content: center;
position: relative;
&--out {
margin-bottom: 10rpx;
border-radius: 50%;
position: absolute;
display: flex;
justify-content: center;
align-items: center;
z-index: 6;
&--animation {
&--scale {
transform-origin: 50% 100%;
animation:tabbar-item-button-out-click 0.2s forwards 1;
}
}
}
&--animation {
&--scale {
.tn-tabbar__content__item__icon, .tn-tabbar__content__item__image {
transform-origin: 50% 100%;
animation:tabbar-item-button-click 0.2s forwards 1;
}
}
}
}
&__icon {
&--clip {
-webkit-background-clip: text;
color: transparent !important;
}
}
&__text {
width: 100%;
font-size: 26rpx;
line-height: 28rpx;
text-align: center;
margin-bottom: 10rpx;
z-index: 10;
transition: all 0.2s ease-in-out;
}
&--out {
height: calc(100% - 1px);
}
}
}
&--fixed {
position: fixed;
bottom: 0;
left: 0;
right: 0;
}
&--shadow {
box-shadow: 0rpx 0rpx 30rpx 0rpx rgba(0, 0, 0, 0.07);
}
}
/* 点击动画 start */
@keyframes tabbar-item-button-click{
from{
transform: scale(0.8);
}
to{
transform: scale(1);
}
}
@keyframes tabbar-item-button-out-click {
0%{
transform: translateY(0) scale(1);
}
50%{
transform: translateY(-10rpx) scale(1.2);
}
100%{
transform: translateY(0) scale(1);
}
}
@keyframes tabbar-content-out-click {
0%{
transform: translateX(-50%) translateY(0) scale(1);
}
50% {
transform: translateX(-50%) translateY(-10rpx) scale(1.1);
}
100% {
transform: translateX(-50%) translateY(0) scale(1);
}
}
/* 点击动画 end */
</style>
@@ -0,0 +1,444 @@
<template>
<view class="tn-tabs-swiper-class tn-tabs-swiper" :class="[backgroundColorClass]" :style="{backgroundColor: backgroundColorStyle, marginTop: $t.string.getLengthUnitValue(top, 'px'), zIndex: zIndex}">
<scroll-view scroll-x class="tn-tabs-swiper__scroll-view" :scroll-left="scrollLeft" scroll-with-animation :style="{zIndex: zIndex + 1}">
<view class="tn-tabs-swiper__scroll-view__box" :class="{'tn-tabs-swiper__scroll-view--flex': !isScroll}">
<!-- item -->
<view
v-for="(item, index) in list"
:key="index"
:id="'tn-tabs-swiper__scroll-view__item-' + index"
class="tn-tabs-swiper__scroll-view__item tn-text-ellipsis"
:style="[tabItemStyle(index)]"
@tap="emit(index)"
>
<tn-badge v-if="item[count] || item['count']" backgroundColor="tn-bg-red" :absolute="true" :top="badgeOffset[0] || 0" :right="badgeOffset[1] || 0">{{ item[count] || item['count']}}</tn-badge>
{{ item[name] || item['name'] }}
</view>
<!-- 底部滑块 -->
<view v-if="showBar" class="tn-tabs-swiper__bar" :style="[tabBarStyle]"></view>
</view>
</scroll-view>
</view>
</template>
<script>
import componentsColor from '../../libs/mixin/components_color.js'
const { windowWidth } = uni.getSystemInfoSync()
export default {
mixins: [componentsColor],
name: 'tn-tabs-swiper',
props: {
// 标签列表
list: {
type: Array,
default() {
return []
}
},
// 列表数据tab名称的属性
name: {
type: String,
default: 'name'
},
// 列表数据微标数量的属性
count: {
type: String,
default: 'count'
},
// 当前活动的tab索引
current: {
type: Number,
default: 0
},
// 菜单是否可以滑动
isScroll: {
type: Boolean,
default: true
},
// 高度
height: {
type: Number,
default: 80
},
// 距离顶部的距离(px)
top: {
type: Number,
default: 0
},
// item的高度
itemWidth: {
type: [String, Number],
default: 'auto'
},
// swiper的宽度
swiperWidth: {
type: Number,
default: 750
},
// 选中时的颜色
activeColor: {
type: String,
default: '#01BEFF'
},
// 未被选中时的颜色
inactiveColor: {
type: String,
default: '#080808'
},
// 选中的item样式
activeItemStyle: {
type: Object,
default() {
return {}
}
},
// 是否显示底部滑块
showBar: {
type: Boolean,
default: true
},
// 底部滑块的宽度
barWidth: {
type: Number,
default: 40
},
// 底部滑块的高度
barHeight: {
type: Number,
default: 6
},
// 自定义底部滑块的样式
barStyle: {
type: Object,
default() {
return {}
}
},
// 单个tab的左右内边距
gutter: {
type: Number,
default: 30
},
// 微标的偏移数[top, right]
badgeOffset: {
type: Array,
default() {
return [20, 22]
}
},
// 是否加粗字体
bold: {
type: Boolean,
default: false
},
// 滚动至中心目标类型
autoCenterMode: {
type: String,
default: 'window'
},
zIndex: {
type: Number,
default: 1
}
},
computed: {
currentIndex() {
const current = Number(this.current)
// 判断是否超出
if (current > this.list.length - 1) {
return this.list.length - 1
}
if (current < 0) return 0
return current
},
// 滑块需要移动的距离
scrollBarLeft() {
return Number(this.tabLineDx) + Number(this.tabLineAddDx)
},
// 滑块宽度转换为px
barWidthPx() {
return uni.upx2px(this.barWidth)
},
// 将swiper宽度转换为px
swiperWidthPx() {
return uni.upx2px(this.swiperWidth)
},
// tab样式
tabItemStyle() {
return index => {
let style = {
height: this.$t.string.getLengthUnitValue(this.height),
lineHeight: this.$t.string.getLengthUnitValue(this.height),
fontSize: this.fontSizeStyle || '28rpx',
color: this.tabsInfo.length > 0 ? (this.tabsInfo[index] ? this.tabsInfo[index].color : this.activeColor) : this.inactiveColor,
padding: this.isScroll ? `0 ${this.gutter}rpx` : '',
flex: this.isScroll ? 'auto' : '1',
zIndex: this.zIndex + 2
}
if (index === this.currentIndex) {
if (this.bold) {
style.fontWeight = 'bold'
}
Object.assign(style, this.activeItemStyle)
}
return style
}
},
// 底部滑块样式
tabBarStyle() {
let style = {
width: this.$t.string.getLengthUnitValue(this.barWidth),
height: this.$t.string.getLengthUnitValue(this.barHeight),
borderRadius: `${this.barHeight / 2}rpx`,
backgroundColor: this.activeColor,
left: this.scrollBarLeft + 'px'
}
Object.assign(style, this.barStyle)
return style
},
},
data() {
return {
// 滚动scroll-view的左边滚动距离
scrollLeft: 0,
// 存放tab菜单节点信息
tabsInfo: [],
// 屏幕宽度
windowWidth: 0,
// 滑动动画结束后对应的标签Index
animationFinishCurrent: this.current,
// 组件的宽度
componentsWidth: 0,
// 移动距离
tabLineAddDx: 0,
tabLineDx: 0,
// 颜色渐变数组
colorGradientArr: [],
// 两个颜色之间的渐变等分
colorStep: 100,
}
},
watch: {
current(value) {
this.change(value)
this.setFinishCurrent(value)
},
list() {
this.$nextTick(() => {
this.init()
})
}
},
mounted() {
this.init()
},
methods: {
// 初始化
async init() {
await this.getTabsInfo()
this.countLine3Dx()
this.getQuery(() => {
this.setScrollViewToCenter()
})
// 获取渐变颜色数组
this.colorGradientArr = this.$t.colorUtils.colorGradient(this.inactiveColor, this.activeColor, this.colorStep)
},
// 发送事件
emit(index) {
this.$emit('change', index)
},
// tabs发生变化
change() {
this.setScrollViewToCenter()
},
// 获取各个tab的节点信息
getTabsInfo() {
return new Promise((resolve, reject) => {
let view = uni.createSelectorQuery().in(this)
for (let i = 0; i < this.list.length; i++) {
view.select('#tn-tabs-swiper__scroll-view__item-'+i).boundingClientRect()
}
view.exec(res => {
const arr = []
for (let i = 0; i < res.length; i++) {
// 添加颜色属性
res[i].color = this.inactiveColor
if (i === this.currentIndex) res[i].color = this.activeColor
arr.push(res[i])
}
this.tabsInfo = arr
resolve()
})
})
},
// 查询components信息
getQuery(cb) {
try {
let view = uni.createSelectorQuery().in(this).select('.tn-tabs-swiper')
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
}
},
// 当swiper滑动结束的时候,计算滑块最终停留的位置
countLine3Dx() {
const tab = this.tabsInfo[this.animationFinishCurrent]
// 让滑块中心点和当前tab中心重合
if (tab) this.tabLineDx = tab.left + tab.width / 2 - this.barWidthPx / 2 - this.tabsInfo[0].left
},
// 把活动的tab移动到屏幕中心
setScrollViewToCenter() {
let tab = this.tabsInfo[this.animationFinishCurrent]
if (tab) {
let tabCenter = tab.left + tab.width / 2
let parentWidth
// 活动tab移动到中心时,以屏幕还是tab组件宽度为基准
if (this.autoCenterMode === 'window') {
parentWidth = windowWidth
} else {
parentWidth = this.componentsWidth
}
this.scrollLeft = tabCenter - parentWidth / 2
}
},
// 设置偏移位置
setDx(dx) {
// 计算下一个标签的步进值
let nextIndexStep = Math.ceil(Math.abs(dx / this.swiperWidthPx))
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
// 当前tab中心点x轴坐标
let currentTab = this.tabsInfo[this.animationFinishCurrent]
let currentTabX = currentTab.left + currentTab.width / 2
// 下一个tab中心点x轴坐标
let nextTab = this.tabsInfo[nextTabIndex]
let nextTabX = nextTab.left + nextTab.width / 2
// 两个tab之间的距离
let distanceX = Math.abs(nextTabX - currentTabX)
this.tabLineAddDx = (dx / this.swiperWidthPx) * distanceX
this.setTabColor(this.animationFinishCurrent, nextTabIndex, dx)
},
// 设置tab的颜色
setTabColor(currentTabIndex, nextTabIndex, dx) {
let nextIndexStep = Math.ceil(Math.abs(dx / this.swiperWidthPx))
if (Math.abs(dx) > this.swiperWidthPx) {
dx = dx > 0 ? dx - (this.swiperWidthPx * (nextIndexStep - 1)) : dx + (this.swiperWidthPx * (nextIndexStep - 1))
}
let colorIndex = Math.abs(Math.ceil((dx / this.swiperWidthPx) * 100))
let colorLength = this.colorGradientArr.length
// 处理超出索引边界
colorIndex = colorIndex >= colorLength ? colorLength - 1 : colorIndex <= 0 ? 0 : colorIndex
if (nextIndexStep > 1) {
// 设置下一个tab的颜色
// 设置之前tab的颜色为默认颜色
if (dx > 0) {
this.tabsInfo[nextTabIndex + (nextIndexStep - 1) > this.tabsInfo.length - 1 ? this.tabsInfo.length - 1 : nextTabIndex + (nextIndexStep - 1)].color = this.colorGradientArr[colorIndex]
this.tabsInfo[nextTabIndex + (nextIndexStep - 2) > this.tabsInfo.length - 1 ? this.tabsInfo.length - 1 : nextTabIndex + (nextIndexStep - 2)].color = this.colorGradientArr[colorLength - 1 - colorIndex]
} else {
this.tabsInfo[nextTabIndex - (nextIndexStep - 1) < 0 ? 0 : nextTabIndex - (nextIndexStep - 1)].color = this.colorGradientArr[colorIndex]
this.tabsInfo[nextTabIndex - (nextIndexStep - 2) < 0 ? 0 : nextTabIndex - (nextIndexStep - 2)].color = this.colorGradientArr[colorLength - 1 - colorIndex]
}
} else {
// 设置下一个tab的颜色
this.tabsInfo[nextTabIndex].color = this.colorGradientArr[colorIndex]
// 设置当前tab的颜色
this.tabsInfo[currentTabIndex].color = this.colorGradientArr[colorLength - 1 - colorIndex]
}
},
// swiper滑动结束
setFinishCurrent(current) {
// 如果滑动的索引不一致,修改tab颜色变化,因为可能会有直接点击tab的情况
this.tabsInfo.map((item, index) => {
if (current == index) item.color = this.activeColor
else item.color = this.inactiveColor
return item
})
this.tabLineAddDx = 0
this.animationFinishCurrent = current
this.countLine3Dx()
}
}
}
</script>
<style lang="scss" scoped>
/* #ifndef APP-NVUE */
::-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 */
.tn-tabs-swiper {
&__scroll-view {
position: relative;
width: 100%;
white-space: nowrap;
&__box {
position: relative;
/* #ifdef MP-TOUTIAO */
white-space: nowrap;
/* #endif */
}
&__item {
position: relative;
/* #ifndef APP-NVUE */
display: inline-block;
/* #endif */
text-align: center;
transition-property: background-color, color;
}
&--flex {
display: flex;
flex-direction: row;
justify-content: space-between;
}
}
&__bar {
position: absolute;
bottom: 0;
}
}
</style>
+340
View File
@@ -0,0 +1,340 @@
<template>
<view class="tn-tabs-class tn-tabs" :class="[backgroundColorClass]" :style="{backgroundColor: backgroundColorStyle, marginTop: $t.string.getLengthUnitValue(top, 'px')}">
<!-- _tgetRect()对组件根节点无效因为写了.in(this)故这里获取内层接点尺寸 -->
<view :id="id">
<scroll-view scroll-x class="tn-tabs__scroll-view" :scroll-left="scrollLeft" scroll-with-animation>
<view class="tn-tabs__scroll-view__box" :class="{'tn-tabs__scroll-view--flex': !isScroll}">
<!-- item -->
<view
v-for="(item, index) in list"
:key="index"
:id="'tn-tabs__scroll-view__item-' + index"
class="tn-tabs__scroll-view__item tn-text-ellipsis"
:style="[tabItemStyle(index)]"
@tap="clickTab(index)"
>
<tn-badge v-if="item[count] || item['count']" backgroundColor="tn-bg-red" :absolute="true" :top="badgeOffset[0] || 0" :right="badgeOffset[1] || 0">{{ item[count] || item['count']}}</tn-badge>
{{ item[name] || item['name'] }}
</view>
<!-- 底部滑块 -->
<view v-if="showBar" class="tn-tabs__bar" :style="[tabBarStyle]"></view>
</view>
</scroll-view>
</view>
</view>
</template>
<script>
import componentsColor from '../../libs/mixin/components_color.js'
export default {
mixins: [componentsColor],
name: 'tn-tabs',
props: {
// 标签列表
list: {
type: Array,
default() {
return []
}
},
// 列表数据tab名称的属性
name: {
type: String,
default: 'name'
},
// 列表数据微标数量的属性
count: {
type: String,
default: 'count'
},
// 当前活动的tab索引
current: {
type: Number,
default: 0
},
// 菜单是否可以滑动
isScroll: {
type: Boolean,
default: true
},
// 高度
height: {
type: Number,
default: 80
},
// 距离顶部的距离(px)
top: {
type: Number,
default: 0
},
// item的宽度
itemWidth: {
type: [String, Number],
default: 'auto'
},
// 过渡动画时长
duration: {
type: Number,
default: 0.3
},
// 选中时的颜色
activeColor: {
type: String,
default: '#01BEFF'
},
// 未被选中时的颜色
inactiveColor: {
type: String,
default: '#080808'
},
// 选中的item样式
activeItemStyle: {
type: Object,
default() {
return {}
}
},
// 是否显示底部滑块
showBar: {
type: Boolean,
default: true
},
// 底部滑块的宽度
barWidth: {
type: Number,
default: 40
},
// 底部滑块的高度
barHeight: {
type: Number,
default: 6
},
// 自定义底部滑块的样式
barStyle: {
type: Object,
default() {
return {}
}
},
// 单个tab的左右内边距
gutter: {
type: Number,
default: 30
},
// 微标的偏移数[top, right]
badgeOffset: {
type: Array,
default() {
return [20, 22]
}
},
// 是否加粗字体
bold: {
type: Boolean,
default: false
}
},
computed: {
// 底部滑块样式
tabBarStyle() {
let style = {
width: this.$t.string.getLengthUnitValue(this.barWidth),
height: this.$t.string.getLengthUnitValue(this.barHeight),
borderRadius: `${this.barHeight / 2}rpx`,
backgroundColor: this.activeColor,
opacity: this.barMoveFirst ? 0 : 1,
transform: `translate(${this.scrollBarLeft}px, -100%)`,
transitionDuration: this.barMoveFirst ? '0s' : `${this.duration}s`
}
Object.assign(style, this.barStyle)
return style
},
// tabItem样式
tabItemStyle() {
return index => {
let style = {
width: this.$t.string.getLengthUnitValue(this.itemWidth),
height: this.$t.string.getLengthUnitValue(this.height),
lineHeight: this.$t.string.getLengthUnitValue(this.height),
fontSize: this.fontSizeStyle || '28rpx',
padding: this.isScroll ? `0 ${this.gutter}rpx` : '',
flex: this.isScroll ? 'auto' : '1',
transitionDuration: `${this.duration}s`
}
if (index === this.currentIndex) {
if (this.bold) {
style.fontWeight = 'bold'
}
style.color = this.activeColor
Object.assign(style, this.activeItemStyle)
} else {
style.color = this.inactiveColor
}
return style
}
}
},
data() {
return {
// id值
id: this.$t.uuid(),
// 滚动scroll-view的左边距离
scrollLeft: 0,
// 存放查询后tab菜单的节点信息
tabQueryInfo: [],
// 组件宽度
componentWidth: 0,
// 底部滑块的移动距离
scrollBarLeft: 0,
// 组件到屏幕左边的巨鹿
componentLeft: 0,
// 当前选中的itemIndex
currentIndex: this.current,
// 标记底部滑块是否第一次移动,第一次移动的时候不触发动画
barMoveFirst: true
}
},
watch: {
// 监听tab的变化,重新计算tab菜单信息
list(newValue, oldValue) {
// list变化时,重置内部索引,防止出现超过数据边界的问题
if (newValue.length !== oldValue.length) this.currentIndex = 0
this.$nextTick(() => {
this.init()
})
},
current: {
handler(val) {
this.$nextTick(() => {
this.currentIndex = val
this.scrollByIndex()
})
},
immediate: true
}
},
mounted() {
this.init()
},
methods: {
// 初始化变量
async init() {
// 获取tabs组件的信息
let tabRect = await this._tGetRect('#' + this.id)
// 计算组件的宽度
this.componentLeft = tabRect.left
this.componentWidth = tabRect.width
this.getTabRect()
},
// 点击tab菜单
clickTab(index) {
if (index === this.currentIndex) return
this.$emit('change', index)
},
// 查询tab的布局信息
getTabRect() {
let query = uni.createSelectorQuery().in(this)
// 遍历所有的tab
for (let i = 0; i < this.list.length; i++) {
query.select(`#tn-tabs__scroll-view__item-${i}`).fields({
size: true,
rect: true
})
}
query.exec((res) => {
this.tabQueryInfo = res
// 初始滚动条和底部滑块的位置
this.scrollByIndex()
})
},
// 滚动scrollView,让活动的tab处于屏幕中间
scrollByIndex() {
// 当前获取tab的布局信息
let tabInfo = this.tabQueryInfo[this.currentIndex]
if (!tabInfo) return
// 活动tab的宽度
let tabWidth = tabInfo.width
// 活动item的左边到组件左边的距离
let offsetLeft = tabInfo.left - this.componentLeft
// 计算scroll-view移动的距离
let scrollLeft = offsetLeft - (this.componentWidth - tabWidth) / 2
this.scrollLeft = scrollLeft < 0 ? 0 : scrollLeft
// 计算当前滑块需要移动的距离,当前活动item的中点到左边的距离减去滑块宽度的一半
let left = tabInfo.left + tabInfo.width / 2 - this.componentLeft
// 计算当前活跃item到组件左边的距离
this.scrollBarLeft = left - uni.upx2px(this.barWidth) / 2
// 防止在计算时出错,所以延迟执行标记不是第一次移动
if (this.barMoveFirst) {
setTimeout(() => {
this.barMoveFirst = false
}, 100)
}
}
}
}
</script>
<style lang="scss" scoped>
/* #ifndef APP-NVUE */
::-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 */
.tn-tabs {
&__scroll-view {
position: relative;
width: 100%;
white-space: nowrap;
&__box {
position: relative;
/* #ifdef MP-TOUTIAO */
white-space: nowrap;
/* #endif */
}
&__item {
position: relative;
/* #ifndef APP-NVUE */
display: inline-block;
/* #endif */
text-align: center;
transition-property: background-color, color;
}
&--flex {
display: flex;
flex-direction: row;
justify-content: space-between;
}
}
&__bar {
position: absolute;
bottom: 0;
}
}
</style>
+214
View File
@@ -0,0 +1,214 @@
<template>
<view
class="tn-tag-class tn-tag"
:class="[
tagClass,
backgroundColorClass,
fontColorClass
]"
:style="[tagStyle]"
@tap="handleClick"
>
<slot></slot>
</view>
</template>
<script>
import componentsColorMixin from '../../libs/mixin/components_color.js'
export default {
mixins: [ componentsColorMixin ],
name: 'tn-tag',
props: {
// 序号,用于区分多个标签
index: {
type: [Number, String],
default: '0'
},
// 形状 圆角 radius 椭圆 circle 左半圆 circleLeft 右半圆 circleRight
shape: {
type: String,
default: ''
},
// 标签大小 sm lg
size: {
type: String,
default: ''
},
// 宽度
width: {
type: String,
default: ''
},
// 高度
height: {
type: String,
default: ''
},
// 内边距
padding: {
type: String,
default: ''
},
// 外边距
margin: {
type: String,
default: '0'
},
// 是否镂空
plain: {
type: Boolean,
default: false
},
// 是否将元素基点设置在左边
originLeft: {
type: Boolean,
default: false
},
// 是否将元素基点设置在右边
originRight: {
type: Boolean,
default: false
}
},
computed: {
tagClass() {
let clazz = ''
// 设置标签的形状
switch(this.shape) {
case 'radius':
clazz += ' tn-radius'
break
case 'circle':
clazz += ' tn-round'
break
case 'circleLeft':
clazz += ' tn-tag--fillet-left'
break
case 'circleRight':
clazz += ' tn-tag--fillet-right'
break
}
// 设置为镂空并且设置镂空便可才进行设置
if (this.plain) {
clazz += ' tn-tag--plain tn-border-solid'
if (this.backgroundColor !== '' && this.backgroundColor.includes('tn-bg')) {
const color = this.backgroundColor.slice(this.backgroundColor.lastIndexOf('-') + 1)
clazz += ` tn-border-${color}`
}
}
// 设置基准点
if (this.originLeft) {
clazz += ' tn-tag--origin-left'
}
if (this.originRight) {
clazz += ' tn-tag--origin-right'
}
return clazz
},
tagStyle() {
let style = {}
switch(this.size) {
case 'sm':
style.padding = '0 12rpx'
style.fontSize = '20rpx'
style.height = '32rpx'
break
case 'lg':
style.padding = '0 20rpx'
style.fontSize = '28rpx'
style.height = '62rpx'
break
default:
style.padding = '0 16rpx'
style.fontSize = '24rpx'
style.height = '48rpx'
break
}
style.width = this.width || '100%'
style.height = this.height || style.height
style.padding = this.padding || style.padding
if (this.margin) {
style.margin = this.margin
}
if (this.fontColorStyle) {
style.color = this.fontColorStyle
}
if (this.fontSize !== 0) {
style.fontSize = this.fontSize + this.fontUnit
}
if (!this.backgroundColorClass) {
style.backgroundColor = !this.plain ? (this.backgroundColorStyle || '#01BEFF') : ''
}
return style
},
},
data() {
return {
}
},
methods: {
// 处理点击事件
handleClick() {
this.$emit('click', {
index: Number(this.index)
})
this.$emit('tap', {
index: Number(this.index)
})
},
}
}
</script>
<style lang="scss" scoped>
.tn-tag {
vertical-align: middle;
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
font-family: Helvetica Neue, Helvetica, sans-serif;
white-space: nowrap;
color: #FFFFFF;
&--fillet-left {
border-radius: 50rpx 0 0 50rpx;
}
&--fillet-right {
border-radius: 0 50rpx 50rpx 0;
}
&--plain {
background-color: transparent !important;
background-image: none;
&.tn-round {
border-radius: 1000rpx !important;
}
&.tn-radius {
border-radius: 12rpx !important;
}
}
&--origin-left {
transform-origin: 0 center;
}
&--origin-right {
transform-origin: 100% center;
}
}
</style>
@@ -0,0 +1,71 @@
<template>
<view class="tn-time-line-item-class tn-time-line-item">
<view>
<slot name="content"></slot>
</view>
<view class="tn-time-line-item__node" :style="[nodeStyle]">
<slot name="node">
<view class="tn-time-line-item__node--dot"></view>
</slot>
</view>
</view>
</template>
<script>
export default {
name: 'tn-time-line-item',
props: {
// 节点左边图标的绝对定位top值
top: {
type: [String, Number],
default: ''
}
},
computed: {
nodeStyle() {
let style = {}
if (this.top !== '') style.top = this.top + 'rpx'
return style
}
},
data() {
return {
}
}
}
</script>
<style lang="scss" scoped>
.tn-time-line-item {
display: flex;
flex-direction: column;
width: 100%;
position: relative;
margin-bottom: 32rpx;
&__node {
display: flex;
flex-direction: row;
position: absolute;
top: 12rpx;
left: -40rpx;
align-items: center;
justify-content: center;
font-size: 24rpx;
transform-origin: 0;
transform: translateX(-50%);
z-index: 1;
background-color: transparent;
&--dot {
width: 16rpx;
height: 16rpx;
border-radius: 100rpx;
background-color: #AAAAAA;
}
}
}
</style>
@@ -0,0 +1,71 @@
<template>
<view class="tn-time-line-item-class tn-time-line-item">
<view>
<slot name="content"></slot>
</view>
<view class="tn-time-line-item__node" :style="[nodeStyle]">
<slot name="node">
<view class="tn-time-line-item__node--dot"></view>
</slot>
</view>
</view>
</template>
<script>
export default {
name: 'tn-time-line-item',
props: {
// 节点左边图标的绝对定位top值
top: {
type: [String, Number],
default: ''
}
},
computed: {
nodeStyle() {
let style = {}
if (this.top !== '') style.top = this.top + 'rpx'
return style
}
},
data() {
return {
}
}
}
</script>
<style lang="scss" scoped>
.tn-time-line-item {
display: flex;
flex-direction: column;
width: 100%;
position: relative;
margin-bottom: 32rpx;
&__node {
display: flex;
flex-direction: row;
position: absolute;
top: 12rpx;
left: -40rpx;
align-items: center;
justify-content: center;
font-size: 24rpx;
transform-origin: 0;
transform: translateX(-50%);
z-index: 1;
background-color: transparent;
&--dot {
width: 16rpx;
height: 16rpx;
border-radius: 100rpx;
background-color: #AAAAAA;
}
}
}
</style>
@@ -0,0 +1,39 @@
<template>
<view class="tn-time-line-class tn-time-line">
<slot></slot>
</view>
</template>
<script>
export default {
name: 'tn-time-line',
props: {
},
data() {
return {
}
}
}
</script>
<style lang="scss" scoped>
.tn-time-line {
padding-left: 40rpx;
position: relative;
&::before {
content: '';
position: absolute;
width: 1px;
left: 0;
top: 12rpx;
bottom: 0;
border-left: 1px solid #AAAAAA;
transform-origin: 0 0;
transform: scaleX(0.5);
}
}
</style>
@@ -0,0 +1,39 @@
<template>
<view class="tn-time-line-class tn-time-line">
<slot></slot>
</view>
</template>
<script>
export default {
name: 'tn-time-line',
props: {
},
data() {
return {
}
}
}
</script>
<style lang="scss" scoped>
.tn-time-line {
padding-left: 40rpx;
position: relative;
&::before {
content: '';
position: absolute;
width: 1px;
left: 0;
top: 12rpx;
bottom: 0;
border-left: 1px solid #AAAAAA;
transform-origin: 0 0;
transform: scaleX(0.5);
}
}
</style>
+240
View File
@@ -0,0 +1,240 @@
<template>
<view
v-if="visibleSync"
class="tn-tips-class tn-tips"
:class="[tipsClass]"
:style="[tipsStyle]"
>
<view
class="tn-tips__content"
:class="[
backgroundColorClass,
fontColorClass
]"
:style="{
backgroundColor: backgroundColorStyle,
color: fontColorStyle
}"
>{{ msg }}</view>
</view>
</template>
<script>
export default {
name: 'tn-tips',
props: {
// 层级
zIndex: {
type: Number,
default: 0
},
// 提示框显示位置 top center bottom
position: {
type: String,
default: 'top'
},
// 当位置设置为top的时候,设置距离顶部的距离
top: {
type: Number,
default: 0
}
},
computed: {
tipsClass() {
let clazz = ''
switch (this.position) {
case 'top':
clazz += ' tn-tips--top'
break
case 'center':
clazz += ' tn-tips--center'
break
case 'bottom':
clazz += ' tn-tips--bottom'
break
default:
clazz += ' tn-tips--top'
}
if (this.showTips) {
clazz += ' tn-tips--show'
}
return clazz
},
tipsStyle() {
let style = {}
if ((this.position === 'top' || this.position === '') && this.top) {
style.top = this.top + 'px'
}
style.zIndex = (this.zIndex ? this.zIndex : this.$t.zIndex.tips) + 1
return style
},
backgroundColorStyle() {
return this.$t.colorUtils.getBackgroundColorStyle(this.backgroundColor)
},
backgroundColorClass() {
return this.$t.colorUtils.getBackgroundColorInternalClass(this.backgroundColor)
},
fontColorStyle() {
return this.$t.colorUtils.getFontColorStyle(this.fontColor)
},
fontColorClass() {
return this.$t.colorUtils.getFontColorInternalClass(this.fontColor)
},
},
data() {
return {
//关闭提示框定时器
timer: null,
// 是否渲染组件
visibleSync: false,
// 是否显示内容
showTips: false,
// 提示信息
msg: '',
// 背景颜色
backgroundColor: '',
// 字体颜色
fontColor: ''
}
},
methods: {
show(options = {}) {
const {
duration = 2000,
msg = '',
backgroundColor = '',
fontColor = ''
} = options
if (this.timer !== null) clearTimeout(this.timer)
// 如果没有设置内容则不弹出
if (!msg) {
this._clearOptions()
this.$emit('close')
return
}
this.msg = msg
this.backgroundColor = backgroundColor || '#01BEFF'
this.fontColor = fontColor || '#FFFFFF'
this.change('visibleSync', 'showTips', true)
this.timer = setTimeout(() => {
clearTimeout(this.timer)
this.timer = null
this.change('showTips', 'visibleSync', false)
}, duration)
},
// 关闭时先通过动画隐藏弹窗和遮罩,再移除整个组件
// 打开时,先渲染组件,延时一定时间再让遮罩和弹窗的动画起作用
change(param1, param2, status) {
this[param1] = status
if (status) {
// #ifdef H5 || MP
this.timer = setTimeout(() => {
this[param2] = status
this.$emit(status ? 'open' : 'close')
}, 50)
// #endif
// #ifndef H5 || MP
this.$nextTick(() => {
this[param2] = status
this.$emit(status ? 'open' : 'close')
})
// #endif
} else {
this.timer = setTimeout(() => {
this[param2] = status
this.$emit(status ? 'open' : 'close')
this._clearOptions()
}, 300)
}
},
// 清除传递的参数
_clearOptions() {
this.msg = ''
this.backgroundColor = ''
this.fontColor = ''
},
}
}
</script>
<style lang="scss" scoped>
/*注意问题:
1、fixed 元素宽度无法自适应,所以增加了子元素
2、fixed 和 display冲突导致动画效果消失,暂时使用visibility替代
*/
.tn-tips {
height: auto;
position: fixed;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease-in-out;
opacity: 0;
&__content {
word-wrap: break-word;
word-break: break-all;
width: 100%;
height: auto;
text-align: center;
background-color: rgba(0, 0, 0, 0.7);
color: #FFFFFF;
}
&--top {
width: 100% !important;
/* padding: 18rpx 30rpx; */
top: 0;
left: 0;
transform: translateY(-100%) translateZ(0);
word-break: break-all;
}
&--center {
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
&--bottom {
bottom: 120rpx;
left: 50%;
transform: translateX(-50%);
}
&--center, &--bottom {
.content {
border-radius: 8rpx;
padding: 0;
}
}
&--center, &--bottom {
.tn-tips__content {
padding: 18rpx 30rpx !important;
}
}
&--show {
opacity: 1;
&.tn-tips--top {
transform: translateY(0) translateZ(0) !important;
}
}
}
</style>
+227
View File
@@ -0,0 +1,227 @@
<template>
<view v-if="visible">
<view
class="tn-toast-class tn-toast"
:class="[toastClass]"
:style="[toastStyle]"
>
<image v-if="image" :src="image" class="tn-toast__img" :class="{'tn-margin-bottom-sm': title || content}"></image>
<view v-if="icon" class="tn-toast__icon">
<view :class="['tn-icon-' + icon]"></view>
</view>
<view
v-if="title"
class="tn-toast__text"
:class="[haveIcon || haveContent ? '' : 'tn-toast--unicon']"
>{{ title }}</view>
<view
v-if="haveContent"
class="tn-toast__text tn-toast__content"
>{{ content }}</view>
</view>
<view class="tn-toast__mask" :class="[visible ? 'tn-toast__mask--show' : '']" :style="[maskStyle]"></view>
</view>
</template>
<script>
export default {
name: 'tn-toast',
props: {
// 层级
zIndex: {
type: Number,
default: 0
}
},
computed: {
toastClass() {
let clazz = ''
if (this.visible) {
clazz += ' tn-toast--show'
}
if (this.content) {
clazz += ' tn-toast--padding'
}
if (this.icon || this.image) {
clazz += ' tn-toast--unicon'
}
return clazz
},
toastStyle() {
let style = {}
style.width = 'auto'
if (this.icon || this.image) {
// style.width = this.content ? '420rpx' : '360rpx'
}
style.zIndex = this.zIndex ? this.zIndex : this.$t.zIndex.toast
return style
},
maskStyle() {
let style = {}
const zIndex = this.zIndex ? this.zIndex : this.$t.zIndex.toast
style.zIndex = zIndex - 1
return style
},
haveIcon() {
return this.icon || this.image
},
haveContent() {
return this.content
}
},
data() {
return {
// 自动关闭定时器
timer: null,
// 是否显示
visible: false,
// 显示的标题
title: '操作成功',
// 显示的内容
content: "",
// 是否显示icon (icon库的图标)
icon: '',
// 是否显示图片 (图片地址)
image: ''
}
},
methods: {
// 显示弹框
show(options = {}) {
const {
duration = 2000,
title = '',
content = '',
icon = '',
image = ''
} = options
if (this.timer !== null ){
clearTimeout(this.timer)
}
// 如果没有设置任何内容就不弹出
if (!icon && !image && !title && !content) {
this._clearOptions()
this.$emit('closed')
return
}
this.visible = true
this.title = title
this.content = content
this.icon = icon
if (!icon) {
this.image = image
}
this.timer = setTimeout(() => {
this.visible = false
clearTimeout(this.timer)
this.timer = null
this._clearOptions()
this.$emit('closed')
}, duration)
},
// 清除传递的参数
_clearOptions() {
this.title = ''
this.content = ''
this.icon = ''
this.image = ''
}
}
}
</script>
<style lang="scss" scoped>
.tn-toast {
height: auto;
background-color: rgba(0, 0, 0, 0.4);
border-radius: 10rpx;
opacity: 0;
position: fixed;
left: 50%;
top: 48%;
transform: translate(-50%, -50%);
transition: 0.3 ease-in-out;
transition-property: opacity, visibility;
display: flex;
align-items: center;
flex-direction: column;
padding: 20rpx 20rpx 20rpx 20rpx;
box-sizing: border-box;
&--show {
opacity: 1;
&.tn-toast--padding {
padding-top: 50rpx !important;
padding-bottom: 50rpx !important;
}
&.tn-toast--unicon {
padding: 20rpx 20rpx 20rpx 20rpx !important;
}
}
&__img {
width: 120rpx;
height: 120rpx;
display: block;
}
&__text {
font-size: 28rpx;
line-height: 28rpx;
color: #ffffff;
text-align: center;
}
&__icon {
color: #FFFFFF;
font-size: 64rpx;
}
&__content {
padding-top: 10rpx;
font-size: 24rpx !important;
}
&--unicon {
padding: 0;
word-break: break-all;
}
&--padding {
padding: 10rpx;
}
&__mask {
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
right: 0;
border: 0;
background-color: rgba(0, 0, 0, 0);
transition: 0.3s ease-in-out;
transition-property: opacity;
opacity: 0;
&--show {
height: 100%;
opacity: 1;
}
}
}
</style>
@@ -0,0 +1,149 @@
<template>
<view class="tn-code-class tn-code">
</view>
</template>
<script>
export default {
name: 'tn-verification-code',
props: {
// 倒计时总秒数
seconds: {
type: Number,
default: 60
},
// 开始时提示文字
startText: {
type: String,
default: '获取验证码'
},
// 倒计时提示文字
countDownText: {
type: String,
default: 's秒后重新获取'
},
// 结束时提示文字
endText: {
type: String,
default: '重新获取'
},
// 是否在H5刷新或各端返回再进入时继续倒计时
keepRunning: {
type: Boolean,
default: false
},
// 为了区分多个页面,或者一个页面多个倒计时组件本地存储的继续倒计时变了
uniqueKey: {
type: String,
default: ''
}
},
data() {
return {
timer: null,
secNum: this.seconds,
// 是否可以执行验证码操作
canGetCode: true
}
},
watch: {
seconds: {
handler(n) {
this.secNum = n
},
immediate: true
}
},
mounted() {
this.checkKeepRunning()
},
beforeDestroy() {
this.setTimeToStorage()
if (this.timer) {
clearInterval(this.timer)
this.timer = null
}
},
methods: {
// 检查是否继续运行
checkKeepRunning() {
// 获取上一次退出页面时的时间戳,如果没有上次保存,该值为空
let lastTimestamp = Number(uni.getStorageSync(this.uniqueKey + '_$tCountDownTimestamp'))
if (!lastTimestamp) return this.changeEvent(this.startText)
// 当前秒的时间戳
// + new Date() 相当于 new Date().getTime()
let nowTimestamp = Math.floor((+ new Date()) / 1000)
// 判断当前的时间戳,是否小于上一次的设定结束的时间,提前于结束的时间戳
if (this.keepRunning && lastTimestamp && lastTimestamp > nowTimestamp) {
// 剩余尚未执行完倒计时秒数
this.secNum = lastTimestamp - nowTimestamp
// 清除本地保存的变量
uni.removeStorageSync(this.uniqueKey + '_$tCountDownTimestamp')
// 开始倒计时
this.start()
} else {
// 如果不存在需要继续上一次的倒计时,执行正常的逻辑
this.changeEvent(this.startText);
}
},
// 开始倒计时
start() {
// 防止快速点击获取验证码按钮导致产生多个定时器导致混乱
if (this.timer) {
clearInterval(this.timer)
this.timer = null
}
this.$emit('start')
this.canGetCode = false
this.changeEvent(this.countDownText.replace(/s|S/, this.secNum))
this.setTimeToStorage()
this.timer = setInterval(() => {
if (--this.secNum) {
this.changeEvent(this.countDownText.replace(/s|S/, this.secNum))
} else {
// 倒计时结束,清空定时器、重置提示信息
this.reset()
this.$emit('end')
}
}, 1000)
},
// 重置倒计时
reset() {
this.canGetCode = true
if (this.timer) {
clearInterval(this.timer)
this.timer = null
}
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) {
let nowTimestamp = Math.floor((+ new Date()) / 1000)
// 保存本次倒计时结束时候的时间戳
uni.setStorageSync(this.uniqueKey + '_$tCountDownTimestamp', nowTimestamp + this.secNum)
}
}
}
}
</script>
<style lang="scss" scoped>
.tn-code {
width: 0;
height: 0;
position: fixed;
z-index: -1;
}
</style>
File diff suppressed because one or more lines are too long
+73
View File
@@ -0,0 +1,73 @@
// 引入全局mixin
import mixin from './libs/mixin/mixin.js'
// 全局挂载引入http相关请求拦截插件
import Request from './libs/luch-request'
// 调试输出信息
function wranning(str) {
// 开发环境进行信息输出,主要是一些报错信息
// 这个环境的来由是在程序编写时候,点击hx编辑器运行调试代码的时候,详见:
// https://uniapp.dcloud.io/frame?id=%e5%bc%80%e5%8f%91%e7%8e%af%e5%a2%83%e5%92%8c%e7%94%9f%e4%ba%a7%e7%8e%af%e5%a2%83
if (process.env.NODE_ENV === 'development') {
console.warn(str)
}
}
// 更新自定义状态栏的信息
import updateCustomBarInfo from './libs/function/updateCustomBarInfo.js'
// 获取颜色工具
import colorUtils from './libs/function/colorUtils.js'
// 消息工具
import messageUtils from './libs/function/messageUtils.js'
// 获取唯一id
import uuid from './libs/function/uuid.js'
// 数组工具
import array from './libs/function/array.js'
// 规则检验
import test from './libs/function/test.js'
// 获取整个父组件
import $parent from './libs/function/$parent.js'
// 格式化字符串工具
import string from './libs/function/string.js'
// 格式化数值工具
import number from './libs/function/number.js'
// 深度复制
import deepClone from './libs/function/deepClone.js'
// z-index配置信息
import zIndex from './libs/config/zIndex.js'
// 主题颜色信息
import color from './libs/config/color.js'
const $t = {
http: new Request(),
updateCustomBar: updateCustomBarInfo,
colorUtils,
messageUtils,
uuid,
array,
test,
$parent,
string,
number,
deepClone,
zIndex,
color,
}
// 挂载到uni对象上
uni.$t = $t
const install = Vue => {
// 全局混入
Vue.mixin(mixin)
// Filter格式化
Vue.prototype.$t = $t
}
export default {
install
}
+13
View File
@@ -0,0 +1,13 @@
// 引入公共基础类
@import './libs/css/main.scss';
@import './libs/css/color.scss';
// 小程序特有的样式
/* #ifdef MP */
@import "./libs/css/style.mp.scss";
/* #endif */
// H5特有的样式
/* #ifdef H5 */
@import "./libs/css/style.h5.scss";
/* #endif */
+15
View File
@@ -0,0 +1,15 @@
// TuniaoUI颜色值
export default {
mainColor: '#01BEFF',
reverseMainColor: '#FFF00D',
femaleColor: '#FF71D2',
maleColor: '#82B2FF',
mainOrange: '#FBBD12',
bgColor: '#FFFFFF',
spaceColor: '#F8F7F8',
fontColor: '#080808',
fontSubColor: '#AAAAAA',
contentColor: '#838383',
fontHolderColor: '#E6E6E6',
maskBgColor: 'rgba(0, 0, 0, 0.4)',
}
+12
View File
@@ -0,0 +1,12 @@
// 各组件的z-index值
export default {
navbar: 29090,
toast: 20090,
alert: 20085,
modal: 20080,
popup: 20075,
tips: 19080,
sticky: 19075,
indexListSticky: 19070,
mask: 9999,
}
+563
View File
@@ -0,0 +1,563 @@
@mixin getColor($color: #FFFFFF, $light-color: #FFFFFF, $dark-color: #FFFFFF, $disabled-color: #FFFFFF) {
color: $color !important;
@if $color != #FFFFFF and $color != #000000 {
&--light {
color: $light-color !important;
}
&--dark {
color: $dark-color !important;
}
&--disabled {
color: $disabled-color !important;
}
}
}
@mixin getBorderColor($color: #FFFFFF, $light-color: #FFFFFF, $dark-color: #FFFFFF, $disabled-color: #FFFFFF) {
@if $color != #FFFFFF and $color != #000000 {
&--light {
border-color: $light-color !important;
}
&--dark {
border-color: $dark-color !important;
}
&--disabled {
border-color: $disabled-color !important;
}
}
border-color: $color !important;
}
@mixin getBackgroundColor($color: #FFFFFF, $light-color: #FFFFFF, $dark-color: #FFFFFF, $disabled-color: #FFFFFF) {
background-color: $color !important;
@if $color != #FFFFFF and $color != #000000 {
color: $tn-font-color;
&--light {
background-color: $light-color !important;
}
&--dark {
background-color: $dark-color !important;
}
&--disabled {
background-color: $disabled-color !important;
}
}
@else {
color: $tn-font-color;
}
}
@mixin getShadowColor($type: box, $color: #FFFFFF) {
@if $type == box {
box-shadow: 6rpx 6rpx 8rpx #{$color};
} @else if $type == text {
text-shadow: 6rpx 6rpx 8rpx #{$color};
}
}
@mixin getGradientColor($start-color, $end-color, $font-color: #FFFFFF) {
background-image: repeating-linear-gradient(45deg, $start-color, $end-color);
color: $font-color;
&--reverse {
background-image: repeating-linear-gradient(-45deg, $start-color, $end-color);
color: $font-color;
}
}
@mixin getMainColorGradient($start-color, $start-color-light, $start-color-disabled, $end-color, $end-color-light) {
@include getGradientColor($start-color, $end-color);
&--light {
@include getGradientColor($start-color-light, $end-color-light, $start-color);
}
&--single {
@include getGradientColor($start-color, $start-color-disabled);
}
}
/* 颜色 start */
.tn-color-red {
@include getColor($tn-color-red, $tn-color-red-light, $tn-color-red-dark, $tn-color-red-disabled);
}
.tn-color-purplered {
@include getColor($tn-color-purplered, $tn-color-purplered-light, $tn-color-purplered-dark, $tn-color-purplered-disabled);
}
.tn-color-purple {
@include getColor($tn-color-purple, $tn-color-purple-light, $tn-color-purple-dark, $tn-color-purple-disabled);
}
.tn-color-bluepurple {
@include getColor($tn-color-bluepurple, $tn-color-bluepurple-light, $tn-color-bluepurple-dark, $tn-color-bluepurple-disabled);
}
.tn-color-aquablue {
@include getColor($tn-color-aquablue, $tn-color-aquablue-light, $tn-color-aquablue-dark, $tn-color-aquablue-disabled);
}
.tn-color-blue {
@include getColor($tn-color-blue, $tn-color-blue-light, $tn-color-blue-dark, $tn-color-blue-disabled);
}
.tn-color-indigo {
@include getColor($tn-color-indigo, $tn-color-indigo-light, $tn-color-indigo-dark, $tn-color-indigo-disabled);
}
.tn-color-cyan {
@include getColor($tn-color-cyan, $tn-color-cyan-light, $tn-color-cyan-dark, $tn-color-cyan-disabled);
}
.tn-color-teal {
@include getColor($tn-color-teal, $tn-color-teal-light, $tn-color-teal-dark, $tn-color-teal-disabled);
}
.tn-color-green {
@include getColor($tn-color-green, $tn-color-green-light, $tn-color-green-dark, $tn-color-green-disabled);
}
.tn-color-yellowgreen {
@include getColor($tn-color-yellowgreen, $tn-color-yellowgreen-light, $tn-color-yellowgreen-dark, $tn-color-yellowgreen-disabled);
}
.tn-color-lime {
@include getColor($tn-color-lime, $tn-color-lime-light, $tn-color-lime-dark, $tn-color-lime-disabled);
}
.tn-color-yellow {
@include getColor($tn-color-yellow, $tn-color-yellow-light, $tn-color-yellow-dark, $tn-color-yellow-disabled);
}
.tn-color-orangeyellow {
@include getColor($tn-color-orangeyellow, $tn-color-orangeyellow-light, $tn-color-orangeyellow-dark, $tn-color-orangeyellow-disabled);
}
.tn-color-orange {
@include getColor($tn-color-orange, $tn-color-orange-light, $tn-color-orange-dark, $tn-color-orange-disabled);
}
.tn-color-orangered {
@include getColor($tn-color-orangered, $tn-color-orangered-light, $tn-color-orangered-dark, $tn-color-orangered-disabled);
}
.tn-color-brown {
@include getColor($tn-color-brown, $tn-color-brown-light, $tn-color-brown-dark, $tn-color-brown-disabled);
}
.tn-color-grey {
@include getColor($tn-color-grey, $tn-color-grey-light, $tn-color-grey-dark, $tn-color-grey-disabled);
}
.tn-color-gray {
@include getColor($tn-color-gray, $tn-color-gray-light, $tn-color-gray-dark, $tn-color-gray-disabled);
}
.tn-color-white {
@include getColor();
}
.tn-color-black {
@include getColor(#000000);
}
/* 颜色 end */
/* 边框颜色 start */
.tn-border-red {
@include getBorderColor($tn-color-red, $tn-color-red-light, $tn-color-red-dark, $tn-color-red-disabled);
}
.tn-border-purplered {
@include getBorderColor($tn-color-purplered, $tn-color-purplered-light, $tn-color-purplered-dark, $tn-color-purplered-disabled);
}
.tn-border-purple {
@include getBorderColor($tn-color-purple, $tn-color-purple-light, $tn-color-purple-dark, $tn-color-purple-disabled);
}
.tn-border-bluepurple {
@include getBorderColor($tn-color-bluepurple, $tn-color-bluepurple-light, $tn-color-bluepurple-dark, $tn-color-bluepurple-disabled);
}
.tn-border-aquablue {
@include getBorderColor($tn-color-aquablue, $tn-color-aquablue-light, $tn-color-aquablue-dark, $tn-color-aquablue-disabled);
}
.tn-border-blue {
@include getBorderColor($tn-color-blue, $tn-color-blue-light, $tn-color-blue-dark, $tn-color-blue-disabled);
}
.tn-border-indigo {
@include getBorderColor($tn-color-indigo, $tn-color-indigo-light, $tn-color-indigo-dark, $tn-color-indigo-disabled);
}
.tn-border-cyan {
@include getBorderColor($tn-color-cyan, $tn-color-cyan-light, $tn-color-cyan-dark, $tn-color-cyan-disabled);
}
.tn-border-teal {
@include getBorderColor($tn-color-teal, $tn-color-teal-light, $tn-color-teal-dark, $tn-color-teal-disabled);
}
.tn-border-green {
@include getBorderColor($tn-color-green, $tn-color-green-light, $tn-color-green-dark, $tn-color-green-disabled);
}
.tn-border-yellowgreen {
@include getBorderColor($tn-color-yellowgreen, $tn-color-yellowgreen-light, $tn-color-yellowgreen-dark, $tn-color-yellowgreen-disabled);
}
.tn-border-lime {
@include getBorderColor($tn-color-lime, $tn-color-lime-light, $tn-color-lime-dark, $tn-color-lime-disabled);
}
.tn-border-yellow {
@include getBorderColor($tn-color-yellow, $tn-color-yellow-light, $tn-color-yellow-dark, $tn-color-yellow-disabled);
}
.tn-border-orangeyellow {
@include getBorderColor($tn-color-orangeyellow, $tn-color-orangeyellow-light, $tn-color-orangeyellow-dark, $tn-color-orangeyellow-disabled);
}
.tn-border-orange {
@include getBorderColor($tn-color-orange, $tn-color-orange-light, $tn-color-orange-dark, $tn-color-orange-disabled);
}
.tn-border-orangered {
@include getBorderColor($tn-color-orangered, $tn-color-orangered-light, $tn-color-orangered-dark, $tn-color-orangered-disabled);
}
.tn-border-brown {
@include getBorderColor($tn-color-brown, $tn-color-brown-light, $tn-color-brown-dark, $tn-color-brown-disabled);
}
.tn-border-grey {
@include getBorderColor($tn-color-grey, $tn-color-grey-light, $tn-color-grey-dark, $tn-color-grey-disabled);
}
.tn-border-gray {
@include getBorderColor($tn-color-gray, $tn-color-gray-light, $tn-color-gray-dark, $tn-color-gray-disabled);
}
.tn-border-white {
@include getBorderColor();
}
.tn-border-black {
@include getBorderColor(#000000);
}
/* 边框颜色 end */
/* 背景颜色 start */
.tn-bg-red {
@include getBackgroundColor($tn-color-red, $tn-color-red-light, $tn-color-red-dark, $tn-color-red-disabled);
}
.tn-bg-purplered {
@include getBackgroundColor($tn-color-purplered, $tn-color-purplered-light, $tn-color-purplered-dark, $tn-color-purplered-disabled);
}
.tn-bg-purple {
@include getBackgroundColor($tn-color-purple, $tn-color-purple-light, $tn-color-purple-dark, $tn-color-purple-disabled);
}
.tn-bg-bluepurple {
@include getBackgroundColor($tn-color-bluepurple, $tn-color-bluepurple-light, $tn-color-bluepurple-dark, $tn-color-bluepurple-disabled);
}
.tn-bg-aquablue {
@include getBackgroundColor($tn-color-aquablue, $tn-color-aquablue-light, $tn-color-aquablue-dark, $tn-color-aquablue-disabled);
}
.tn-bg-blue {
@include getBackgroundColor($tn-color-blue, $tn-color-blue-light, $tn-color-blue-dark, $tn-color-blue-disabled);
}
.tn-bg-indigo {
@include getBackgroundColor($tn-color-indigo, $tn-color-indigo-light, $tn-color-indigo-dark, $tn-color-indigo-disabled);
}
.tn-bg-cyan {
@include getBackgroundColor($tn-color-cyan, $tn-color-cyan-light, $tn-color-cyan-dark, $tn-color-cyan-disabled);
}
.tn-bg-teal {
@include getBackgroundColor($tn-color-teal, $tn-color-teal-light, $tn-color-teal-dark, $tn-color-teal-disabled);
}
.tn-bg-green {
@include getBackgroundColor($tn-color-green, $tn-color-green-light, $tn-color-green-dark, $tn-color-green-disabled);
}
.tn-bg-yellowgreen {
@include getBackgroundColor($tn-color-yellowgreen, $tn-color-yellowgreen-light, $tn-color-yellowgreen-dark, $tn-color-yellowgreen-disabled);
}
.tn-bg-lime {
@include getBackgroundColor($tn-color-lime, $tn-color-lime-light, $tn-color-lime-dark, $tn-color-lime-disabled);
}
.tn-bg-yellow {
@include getBackgroundColor($tn-color-yellow, $tn-color-yellow-light, $tn-color-yellow-dark, $tn-color-yellow-disabled);
}
.tn-bg-orangeyellow {
@include getBackgroundColor($tn-color-orangeyellow, $tn-color-orangeyellow-light, $tn-color-orangeyellow-dark, $tn-color-orangeyellow-disabled);
}
.tn-bg-orange {
@include getBackgroundColor($tn-color-orange, $tn-color-orange-light, $tn-color-orange-dark, $tn-color-orange-disabled);
}
.tn-bg-orangered {
@include getBackgroundColor($tn-color-orangered, $tn-color-orangered-light, $tn-color-orangered-dark, $tn-color-orangered-disabled);
}
.tn-bg-brown {
@include getBackgroundColor($tn-color-brown, $tn-color-brown-light, $tn-color-brown-dark, $tn-color-brown-disabled);
}
.tn-bg-grey {
@include getBackgroundColor($tn-color-grey, $tn-color-grey-light, $tn-color-grey-dark, $tn-color-grey-disabled);
}
.tn-bg-gray {
@include getBackgroundColor($tn-color-gray, $tn-color-gray-light, $tn-color-gray-dark, $tn-color-gray-disabled);
}
.tn-bg-white {
@include getBackgroundColor();
}
.tn-bg-black {
@include getBackgroundColor(#000000);
}
/* 背景颜色 end */
/* 阴影颜色 start */
.tn-shadow-red {
@include getShadowColor(box, $tn-color-red-light);
}
.tn-shadow-purplered {
@include getShadowColor(box, $tn-color-purplered-light);
}
.tn-shadow-purple {
@include getShadowColor(box, $tn-color-purple-light);
}
.tn-shadow-bluepurple {
@include getShadowColor(box, $tn-color-bluepurple-light);
}
.tn-shadow-aquablue {
@include getShadowColor(box, $tn-color-aquablue-light);
}
.tn-shadow-blue {
@include getShadowColor(box, $tn-color-blue-light);
}
.tn-shadow-indigo {
@include getShadowColor(box, $tn-color-indigo-light);
}
.tn-shadow-cyan {
@include getShadowColor(box, $tn-color-cyan-light);
}
.tn-shadow-teal {
@include getShadowColor(box, $tn-color-teal-light);
}
.tn-shadow-green {
@include getShadowColor(box, $tn-color-green-light);
}
.tn-shadow-yellowgreen {
@include getShadowColor(box, $tn-color-yellowgreen-light);
}
.tn-shadow-lime {
@include getShadowColor(box, $tn-color-lime-light);
}
.tn-shadow-yellow {
@include getShadowColor(box, $tn-color-yellow-light);
}
.tn-shadow-orangeyellow {
@include getShadowColor(box, $tn-color-orangeyellow-light);
}
.tn-shadow-orange {
@include getShadowColor(box, $tn-color-orange-light);
}
.tn-shadow-orangered {
@include getShadowColor(box, $tn-color-orangered-light);
}
.tn-shadow-brown {
@include getShadowColor(box, $tn-color-brown-light);
}
.tn-shadow-grey {
@include getShadowColor(box, $tn-color-grey-light);
}
.tn-shadow-gray {
@include getShadowColor(box, $tn-color-gray-light);
}
.tn-text-shadow-red {
@include getShadowColor(text, $tn-color-red-light);
}
.tn-text-shadow-purplered {
@include getShadowColor(text, $tn-color-purplered-light);
}
.tn-text-shadow-purple {
@include getShadowColor(text, $tn-color-purple-light);
}
.tn-text-shadow-bluepurple {
@include getShadowColor(text, $tn-color-bluepurple-light);
}
.tn-text-shadow-aquablue {
@include getShadowColor(text, $tn-color-aquablue-light);
}
.tn-text-shadow-blue {
@include getShadowColor(text, $tn-color-blue-light);
}
.tn-text-shadow-indigo {
@include getShadowColor(text, $tn-color-indigo-light);
}
.tn-text-shadow-cyan {
@include getShadowColor(text, $tn-color-cyan-light);
}
.tn-text-shadow-teal {
@include getShadowColor(text, $tn-color-teal-light);
}
.tn-text-shadow-green {
@include getShadowColor(text, $tn-color-green-light);
}
.tn-text-shadow-yellowgreen {
@include getShadowColor(text, $tn-color-yellowgreen-light);
}
.tn-text-shadow-lime {
@include getShadowColor(text, $tn-color-lime-light);
}
.tn-text-shadow-yellow {
@include getShadowColor(text, $tn-color-yellow-light);
}
.tn-text-shadow-orangeyellow {
@include getShadowColor(text, $tn-color-orangeyellow-light);
}
.tn-text-shadow-orange {
@include getShadowColor(text, $tn-color-orange-light);
}
.tn-text-shadow-orangered {
@include getShadowColor(text, $tn-color-orangered-light);
}
.tn-text-shadow-brown {
@include getShadowColor(text, $tn-color-brown-light);
}
.tn-text-shadow-grey {
@include getShadowColor(text, $tn-color-grey-light);
}
.tn-text-shadow-gray {
@include getShadowColor(text, $tn-color-gray-light);
}
/* 阴影颜色 end */
/* 主色渐变色 start */
.tn-main-gradient-red {
@include getMainColorGradient($tn-color-red, $tn-color-red-light, $tn-color-red-disabled, $tn-color-purplered, $tn-color-purplered-light);
}
.tn-main-gradient-purplered {
@include getMainColorGradient($tn-color-purplered, $tn-color-purplered-light, $tn-color-purplered-disabled, $tn-color-purple, $tn-color-purple-light);
}
.tn-main-gradient-purple {
@include getMainColorGradient($tn-color-purple, $tn-color-purple-light, $tn-color-purple-disabled, $tn-color-bluepurple, $tn-color-bluepurple-light);
}
.tn-main-gradient-bluepurple {
@include getMainColorGradient($tn-color-bluepurple, $tn-color-bluepurple-light, $tn-color-bluepurple-disabled, $tn-color-aquablue, $tn-color-aquablue-light);
}
.tn-main-gradient-aquablue {
@include getMainColorGradient($tn-color-aquablue, $tn-color-aquablue-light, $tn-color-aquablue-disabled, $tn-color-blue, $tn-color-blue-light);
}
.tn-main-gradient-blue {
@include getMainColorGradient($tn-color-blue, $tn-color-blue-light, $tn-color-blue-disabled, $tn-color-indigo, $tn-color-indigo-light);
}
.tn-main-gradient-indigo {
@include getMainColorGradient($tn-color-indigo, $tn-color-indigo-light, $tn-color-indigo-disabled, $tn-color-cyan, $tn-color-cyan-light);
}
.tn-main-gradient-cyan {
@include getMainColorGradient($tn-color-cyan, $tn-color-cyan-light, $tn-color-cyan-disabled, $tn-color-teal, $tn-color-teal-light);
}
.tn-main-gradient-teal {
@include getMainColorGradient($tn-color-teal, $tn-color-teal-light, $tn-color-teal-disabled, $tn-color-green, $tn-color-green-light);
}
.tn-main-gradient-green {
@include getMainColorGradient($tn-color-green, $tn-color-green-light, $tn-color-green-disabled, $tn-color-yellowgreen, $tn-color-yellowgreen-light);
}
.tn-main-gradient-yellowgreen {
@include getMainColorGradient($tn-color-yellowgreen, $tn-color-yellowgreen-light, $tn-color-yellowgreen-disabled, $tn-color-lime, $tn-color-lime-light);
}
.tn-main-gradient-lime {
@include getMainColorGradient($tn-color-lime, $tn-color-lime-light, $tn-color-lime-disabled, $tn-color-yellow, $tn-color-yellow-light);
}
.tn-main-gradient-yellow {
@include getMainColorGradient($tn-color-yellow, $tn-color-yellow-light, $tn-color-yellow-disabled, $tn-color-orangeyellow, $tn-color-orangeyellow-light);
}
.tn-main-gradient-orangeyellow {
@include getMainColorGradient($tn-color-orangeyellow, $tn-color-orangeyellow-light, $tn-color-orangeyellow-disabled, $tn-color-orange, $tn-color-orange-light);
}
.tn-main-gradient-orange {
@include getMainColorGradient($tn-color-orange, $tn-color-orange-light, $tn-color-orange-disabled, $tn-color-orangered, $tn-color-orangered-light);
}
.tn-main-gradient-orangered {
@include getMainColorGradient($tn-color-orangered, $tn-color-orangered-light, $tn-color-orangered-disabled, $tn-color-red, $tn-color-red-light);
}
/* 主色渐变色 end */
/* 动态背景颜色 start */
.tn-dynamic-bg-1 {
color: #fff;
background: linear-gradient(45deg, #F15BB5, #9A5CE5, #01BEFF, #00F5D4);
background-size: 500% 500%;
animation: dynamicBg 15s ease infinite;
}
@keyframes dynamicBg {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
/* 动态背景颜色 end */
/* 酷炫背景颜色图片 start */
.tn-cool-bg-color-1 {
@include getGradientColor($tn-cool-bg-color-1-start, $tn-cool-bg-color-1-end);
}
.tn-cool-bg-color-2 {
@include getGradientColor($tn-cool-bg-color-2-start, $tn-cool-bg-color-2-end);
}
.tn-cool-bg-color-3 {
@include getGradientColor($tn-cool-bg-color-3-start, $tn-cool-bg-color-3-end);
}
.tn-cool-bg-color-4 {
@include getGradientColor($tn-cool-bg-color-4-start, $tn-cool-bg-color-4-end);
}
.tn-cool-bg-color-5 {
@include getGradientColor($tn-cool-bg-color-5-start, $tn-cool-bg-color-5-end);
}
.tn-cool-bg-color-6 {
@include getGradientColor($tn-cool-bg-color-6-start, $tn-cool-bg-color-6-end);
}
.tn-cool-bg-color-7 {
@include getGradientColor($tn-cool-bg-color-7-start, $tn-cool-bg-color-7-end);
}
.tn-cool-bg-color-8 {
@include getGradientColor($tn-cool-bg-color-8-start, $tn-cool-bg-color-8-end);
}
.tn-cool-bg-color-9 {
@include getGradientColor($tn-cool-bg-color-9-start, $tn-cool-bg-color-9-end);
}
.tn-cool-bg-color-10 {
@include getGradientColor($tn-cool-bg-color-10-start, $tn-cool-bg-color-10-end);
}
.tn-cool-bg-color-11 {
@include getGradientColor($tn-cool-bg-color-11-start, $tn-cool-bg-color-11-end);
}
.tn-cool-bg-color-12 {
@include getGradientColor($tn-cool-bg-color-12-start, $tn-cool-bg-color-12-end);
}
.tn-cool-bg-color-13 {
@include getGradientColor($tn-cool-bg-color-13-start, $tn-cool-bg-color-13-end);
}
.tn-cool-bg-color-14 {
@include getGradientColor($tn-cool-bg-color-14-start, $tn-cool-bg-color-14-end);
}
.tn-cool-bg-color-15 {
@include getGradientColor($tn-cool-bg-color-15-start, $tn-cool-bg-color-15-end);
}
.tn-cool-bg-color-16 {
@include getGradientColor($tn-cool-bg-color-16-start, $tn-cool-bg-color-16-end);
}
.tn-cool-bg-image::after {
content: " ";
position: absolute;
z-index: -1;
width: 100%;
height: 100%;
left: 0;
bottom: 0;
border-radius: 10rpx;
opacity: 1;
transform: scale(1, 1);
background-size: 100% 100%;
background-image: inherit;
}
.tn-cool-bg-image:nth-of-type(1n)::after {
background-image: url(https://tnuiimage.tnkjapp.com/cool_bg_image/1.png);
}
.tn-cool-bg-image:nth-of-type(2n)::after {
background-image: url(https://tnuiimage.tnkjapp.com/cool_bg_image/2.png);
}
.tn-cool-bg-image:nth-of-type(3n)::after {
background-image: url(https://tnuiimage.tnkjapp.com/cool_bg_image/3.png);
}
.tn-cool-bg-image:nth-of-type(4n)::after {
background-image: url(https://tnuiimage.tnkjapp.com/cool_bg_image/4.png);
}
.tn-cool-bg-image:nth-of-type(5n)::after {
background-image: url(https://tnuiimage.tnkjapp.com/cool_bg_image/5.png);
}
.tn-cool-bg-image:nth-of-type(6n)::after {
background-image: url(https://tnuiimage.tnkjapp.com/cool_bg_image/6.png);
}
/* 酷炫背景颜色图片 end */
+713
View File
@@ -0,0 +1,713 @@
$direction: top, right, bottom, left;
body {
// 全局灰白效果
/* filter: grayscale(100%);
-webkit-filter: grayscale(100%); */
background-color: $tn-bg-color;
/* background-color: #ffffff; */
font-size: 28rpx;
color: $tn-font-color;
font-family: Helvetica Neue, Helvetica, sans-serif;
// 修复点击view标签的时候会有蓝色遮罩
-webkit-tap-highlight-color: rgba(0,0,0,0);
}
view,
scroll-view,
swiper,
button,
input,
textarea,
label,
navigator,
image {
box-sizing: border-box;
}
button::after {
border: none;
}
.tn-round {
border-radius: 5000rpx !important;
}
.tn-radius {
border-radius: 6rpx;
}
/* 基本样式 start */
.tn-width-full {
width: 100%;
}
.tn-height-full {
height: 100%;
}
/* 基本样式 end */
/* 边框 start */
.tn-border-solid,
.tn-border-solid-top,
.tn-border-solid-right,
.tn-border-solid-bottom,
.tn-border-solid-left,
.tn-border-solids,
.tn-border-solids-top,
.tn-border-solids-right,
.tn-border-solids-bottom,
.tn-border-solids-left,
.tn-border-dashed,
.tn-border-dashed-top,
.tn-border-dashed-right,
.tn-border-dashed-bottom,
.tn-border-dashed-left {
border-radius: inherit;
box-sizing: border-box;
}
@for $i from 0 to length($direction) + 1 {
@if $i == 0 {
.tn-border-solid {
border-width: 1rpx !important;
border-style: solid;
border-color: $tn-border-solid-color;
&.tn-bold-border {
border-width: 6rpx !important;
}
}
.tn-border-solids {
border-width: 1rpx !important;
border-style: solid;
border-color: $tn-border-solids-color;
&.tn-bold-border {
border-width: 6rpx !important;
}
}
.tn-border-dashed {
border-width: 1rpx !important;
border-style: dashed;
border-color: $tn-border-dashed-color;
&.tn-bold-border {
border-width: 6rpx !important;
}
}
} @else {
.tn-border-solid-#{nth($direction, $i)} {
border: 0rpx;
border-#{nth($direction, $i)}-width: 1rpx !important;
border-style: solid;
border-color: $tn-border-solid-color;
&.tn-bold-border {
border-#{nth($direction, $i)}-width: 6rpx !important;
}
}
.tn-border-solids-#{nth($direction, $i)} {
border: 0rpx;
border-#{nth($direction, $i)}-width: 1rpx !important;
border-style: solid;
border-color: $tn-border-solids-color;
&.tn-bold-border {
border-#{nth($direction, $i)}-width: 6rpx !important;
}
}
.tn-border-dashed-#{nth($direction, $i)} {
border: 0rpx;
border-#{nth($direction, $i)}-width: 1rpx !important;
border-style: dashed;
border-color: $tn-border-dashed-color;
&.tn-bold-border {
border-#{nth($direction, $i)}-width: 6rpx !important;
}
}
}
}
.tn-none-border.tn-border-solid,
.tn-none-border.tn-border-solid-top,
.tn-none-border.tn-border-solid-right,
.tn-none-border.tn-border-solid-bottom,
.tn-none-border.tn-border-solid-left,
.tn-none-border.tn-border-solids,
.tn-none-border.tn-border-solids-top,
.tn-none-border.tn-border-solids-right,
.tn-none-border.tn-border-solids-bottom,
.tn-none-border.tn-border-solids-left,
.tn-none-border.tn-border-dashed,
.tn-none-border.tn-border-dashed-top,
.tn-none-border.tn-border-dashed-right,
.tn-none-border.tn-border-dashed-bottom,
.tn-none-border.tn-border-dashed-left {
border: 0 !important;
}
.tn-none-border-top.tn-border-solid,
.tn-none-border-top.tn-border-solid-top,
.tn-none-border-top.tn-border-solid-right,
.tn-none-border-top.tn-border-solid-bottom,
.tn-none-border-top.tn-border-solid-left,
.tn-none-border-top.tn-border-solids,
.tn-none-border-top.tn-border-solids-top,
.tn-none-border-top.tn-border-solids-right,
.tn-none-border-top.tn-border-solids-bottom,
.tn-none-border-top.tn-border-solids-left,
.tn-none-border-top.tn-border-dashed,
.tn-none-border-top.tn-border-dashed-top,
.tn-none-border-top.tn-border-dashed-right,
.tn-none-border-top.tn-border-dashed-bottom,
.tn-none-border-top.tn-border-dashed-left {
/* height: 0 !important; */
border-top: 0 !important;
}
.tn-none-border-right.tn-border-solid,
.tn-none-border-right.tn-border-solid-top,
.tn-none-border-right.tn-border-solid-right,
.tn-none-border-right.tn-border-solid-bottom,
.tn-none-border-right.tn-border-solid-left,
.tn-none-border-right.tn-border-solids,
.tn-none-border-right.tn-border-solids-top,
.tn-none-border-right.tn-border-solids-right,
.tn-none-border-right.tn-border-solids-bottom,
.tn-none-border-right.tn-border-solids-left,
.tn-none-border-right.tn-border-dashed,
.tn-none-border-right.tn-border-dashed-top,
.tn-none-border-right.tn-border-dashed-right,
.tn-none-border-right.tn-border-dashed-bottom,
.tn-none-border-right.tn-border-dashed-left {
/* width: 0 !important; */
border-right: 0 !important;
}
.tn-none-border-bottom.tn-border-solid,
.tn-none-border-bottom.tn-border-solid-top,
.tn-none-border-bottom.tn-border-solid-right,
.tn-none-border-bottom.tn-border-solid-bottom,
.tn-none-border-bottom.tn-border-solid-left,
.tn-none-border-bottom.tn-border-solids,
.tn-none-border-bottom.tn-border-solids-top,
.tn-none-border-bottom.tn-border-solids-right,
.tn-none-border-bottom.tn-border-solids-bottom,
.tn-none-border-bottom.tn-border-solids-left,
.tn-none-border-bottom.tn-border-dashed,
.tn-none-border-bottom.tn-border-dashed-top,
.tn-none-border-bottom.tn-border-dashed-right,
.tn-none-border-bottom.tn-border-dashed-bottom,
.tn-none-border-bottom.tn-border-dashed-left {
/* height: 0 !important; */
border-bottom: 0 !important;
}
.tn-none-border-left.tn-border-solid,
.tn-none-border-left.tn-border-solid-top,
.tn-none-border-left.tn-border-solid-right,
.tn-none-border-left.tn-border-solid-bottom,
.tn-none-border-left.tn-border-solid-left,
.tn-none-border-left.tn-border-solids,
.tn-none-border-left.tn-border-solids-top,
.tn-none-border-left.tn-border-solids-right,
.tn-none-border-left.tn-border-solids-bottom,
.tn-none-border-left.tn-border-solids-left,
.tn-none-border-left.tn-border-dashed,
.tn-none-border-left.tn-border-dashed-top,
.tn-none-border-left.tn-border-dashed-right,
.tn-none-border-left.tn-border-dashed-bottom,
.tn-none-border-left.tn-border-dashed-left {
/* width: 0 !important; */
border-left: 0 !important;
}
/* 边框 end */
/* 阴影 start */
.tn-shadow {
box-shadow: 6rpx 6rpx 8rpx $tn-shadow-color;
}
.tn-shadow-warp {
position: relative;
box-shadow: 0 0 10rpx $tn-shadow-color;
}
.tn-shadow-warp::before,
.tn-shadow-warp::after {
content: " ";
position: absolute;
top: 20rpx;
bottom: 30rpx;
left: 20rpx;
width: 50%;
box-shadow: 0 30rpx 20rpx $tn-box-shadow-color;
transform: rotate(-3deg);
z-index: -1;
}
.tn-shadow-warp::after {
right: 20rpx;
left: auto;
transform: rotate(3deg);
}
.tn-shadow-blur {
position: relative;
}
.tn-shadow-blur::before {
content: " ";
display: block;
background: inherit;
filter: blur(10rpx);
position: absolute;
width: 100%;
height: 100%;
top: 10rpx;
left: 10rpx;
z-index: -1;
opacity: 0.4;
transform-origin: 0 0;
border-radius: inherit;
transform: scale(1, 1);
}
/* 阴影 end */
/* flex start */
.tn-flex {
display: -webkit-flex;
display: flex;
}
/* flex伸缩基准值 */
.tn-flex-basic-xs {
flex-basis: 20%;
}
.tn-flex-basic-sm {
flex-basis: 40%;
}
.tn-flex-basic-md {
flex-basis: 50%;
}
.tn-flex-basic-lg {
flex-basis: 60%;
}
.tn-flex-basic-xl {
flex-basis: 80%;
}
.tn-flex-basic-full {
flex-basis: 100%;
}
/* flex布局的方向 */
.tn-flex-direction-column {
flex-direction: column;
}
.tn-flex-direction-row {
flex-direction: row;
}
.tn-flex-direction-column-reverse {
flex-direction: column-reverse;
}
.tn-flex-direction-row-reverse {
flex-direction: row-reverse;
}
/* flex容器设置换行 */
.tn-flex-wrap {
flex-wrap: wrap;
}
.tn-flex-nowrap {
flex-wrap: nowrap;
}
/* flex容器自身垂直方向对齐方式 */
.tn-flex-center {
align-self: center;
}
.tn-flex-top {
align-self: flex-start;
}
.tn-flex-end {
align-self: flex-end;
}
.tn-flex-stretch {
align-self: stretch;
}
/* flex子元素垂直方向对齐方式 */
.tn-flex-col-center {
align-items: center;
}
.tn-flex-col-top {
align-items: flex-start;
}
.tn-flex-col-bottom {
align-items: flex-end;
}
/* flex子元素水平方向对齐方式 */
.tn-flex-row-center {
justify-content: center;
}
.tn-flex-row-left {
justify-content: flex-start;
}
.tn-flex-row-right {
justify-content: flex-end;
}
.tn-flex-row-between {
justify-content: space-between;
}
.tn-flex-row-around {
justify-content: space-around;
}
/* flex子元素空间分配 */
@for $i from 0 to 12 {
.tn-flex-#{$i} {
flex: $i;
}
}
.tn-col-12 {
width: 100%;
}
.tn-col-11 {
width: 91.66666667%;
}
.tn-col-10 {
width: 83.33333333%;
}
.tn-col-9 {
width: 75%;
}
.tn-col-8 {
width: 66.66666667%;
}
.tn-col-7 {
width: 58.33333333%;
}
.tn-col-6 {
width: 50%;
}
.tn-col-5 {
width: 41.66666667%;
}
.tn-col-4 {
width: 33.33333333%;
}
.tn-col-3 {
width: 25%;
}
.tn-col-2 {
width: 16.66666667%;
}
.tn-col-1 {
width: 8.33333333%;
}
/* flex end */
/* 内边距 start */
@for $i from 0 to length($direction) + 1 {
@if $i == 0 {
.tn-no-margin {
margin: 0;
}
.tn-margin-xs {
margin: 10rpx;
}
.tn-margin-sm {
margin: 20rpx;
}
.tn-margin {
margin: 30rpx;
}
.tn-margin-lg {
margin: 40rpx;
}
.tn-margin-xl {
margin: 50rpx;
}
} @else {
.tn-no-margin-#{nth($direction, $i)} {
margin-#{nth($direction, $i)}: 0;
}
.tn-margin-#{nth($direction, $i)}-xs {
margin-#{nth($direction, $i)}: 10rpx;
}
.tn-margin-#{nth($direction, $i)}-sm {
margin-#{nth($direction, $i)}: 20rpx;
}
.tn-margin-#{nth($direction, $i)} {
margin-#{nth($direction, $i)}: 30rpx;
}
.tn-margin-#{nth($direction, $i)}-lg {
margin-#{nth($direction, $i)}: 40rpx;
}
.tn-margin-#{nth($direction, $i)}-xl {
margin-#{nth($direction, $i)}: 50rpx;
}
}
}
/* 内边距 end */
/* 外边距 start */
@for $i from 0 to length($direction) + 1 {
@if $i == 0 {
.tn-no-padding {
padding: 0;
}
.tn-padding-xs {
padding: 10rpx;
}
.tn-padding-sm {
padding: 20rpx;
}
.tn-padding {
padding: 30rpx;
}
.tn-padding-lg {
padding: 40rpx;
}
.tn-padding-xl {
padding: 50rpx;
}
} @else {
.tn-no-padding-#{nth($direction, $i)} {
padding-#{nth($direction, $i)}: 0;
}
.tn-padding-#{nth($direction, $i)}-xs {
padding-#{nth($direction, $i)}: 10rpx;
}
.tn-padding-#{nth($direction, $i)}-sm {
padding-#{nth($direction, $i)}: 20rpx;
}
.tn-padding-#{nth($direction, $i)} {
padding-#{nth($direction, $i)}: 30rpx;
}
.tn-padding-#{nth($direction, $i)}-lg {
padding-#{nth($direction, $i)}: 40rpx;
}
.tn-padding-#{nth($direction, $i)}-xl {
padding-#{nth($direction, $i)}: 50rpx;
}
}
}
/* 外边距 end */
/* float start */
.tn-float-left {
float: left;
}
.tn-float-right {
float: right;
}
.tn-clear-float {
clear: both;
}
.tn-clear-float::after,
.tn-clear-float::before {
content: " ";
display: table;
clear: both;
}
/* float end */
/* 文本 start */
.tn-text-xs {
font-size: 20rpx;
}
.tn-text-sm {
font-size: 24rpx;
}
.tn-text-md {
font-size: 28rpx;
}
.tn-text-lg {
font-size: 32rpx;
}
.tn-text-xl {
font-size: 36rpx;
}
.tn-text-xxl {
font-size: 40rpx;
}
.tn-text-xl-xxl {
font-size: 80rpx;
}
.tn-text-xxl-xxl {
font-size: 120rpx;
}
.tn-text-upper {
text-transform: uppercase;
}
.tn-text-cap {
text-transform: capitalize;
}
.tn-text-lower {
text-transform: lowercase;
}
.tn-text-bold {
font-weight: bold;
}
.tn-text-center {
text-align: center;
}
.tn-text-left {
text-align: left;
}
.tn-text-right {
text-align: right;
}
.tn-text-justify {
text-align: justify;
}
.tn-text-content {
line-height: 1.6;
}
.tn-text-ellipsis {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.tn-text-ellipsis-2 {
display: -webkit-box;
overflow: hidden;
white-space: normal !important;
text-overflow: ellipsis;
word-wrap: break-word;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
/* 文本 end */
/* hover 点击效果 start */
.tn-hover {
opacity: 0.6;
}
/* hover 点击效果 end */
/* 去除原生button样式 start */
.tn-button--clear-style {
background-color: transparent;
padding: 0;
margin: 0;
font-size: inherit;
line-height: inherit;
border-radius: inherit;
color: inherit;
}
/* 去除原生button样式 end */
/* 头像组 start */
// .tn-avatar-group {
// direction: rtl;
// unicode-bidi: bidi-override;
// padding: 0 10rpx 0 40rpx;
// display: inline-block;
// .tn-avatar {
// margin-left: -30rpx !important;
// border: 4rpx solid $tn-border-solid-color;
// vertical-align: middle;
// &--sm {
// margin-left: -20rpx !important;
// border: 1rpx solid $tn-border-solid-color;
// }
// }
// }
/* 头像组 end */
/* 提升H5端uni.toast()的层级,避免被tn-modal等遮盖 start */
/* #ifdef H5 */
uni-toast {
z-index: 10090;
}
uni-toast .uni-toast {
z-index: 10090;
}
/* #endif */
/* 提升H5端uni.toast()的层级,避免被tn-modal等遮盖 end */
/* iPhoneX底部安全区定义 start */
.tn-safe-area-inset-bottom {
padding-bottom: 0;
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
/* iPhoneX底部安全区定义 end */
+38
View File
@@ -0,0 +1,38 @@
/* H5的时候,隐藏滚动条 */
::-webkit-scrollbar {
display: none;
width: 0 !important;
height: 0 !important;
-webkit-appearance: none;
background: transparent;
}
/* 双标签 start*/
.capsule {
display: inline-flex;
vertical-align: middle;
width: 20%;
min-width: 136rpx;
height: 45rpx;
.capsule-tag {
margin: 0;
&:first-child {
border-top-right-radius: 0rpx;
border-bottom-right-radius: 0rpx;
color: #FFFFFF;
background-color: $tn-main-color !important;
}
&:last-child {
&::after {
border-color: $tn-main-color !important;
border-top-left-radius: 0rpx;
border-bottom-left-radius: 0rpx;
}
}
}
}
/* 双标签 end*/
+55
View File
@@ -0,0 +1,55 @@
::-webkit-scrollbar {
display: none;
width: 0 !important;
height: 0 !important;
-webkit-appearance: none;
background: transparent;
}
/* 微信小程序编译后页面有组件名的元素,特别处理 start */
/* #ifdef MP-WEIXIN || MP-QQ */
// 各家小程序宫格组件外层设置为100%,避免受到父元素display: flex;的影响
/* 双标签 start*/
.capsule {
display: inline-flex;
vertical-align: middle;
width: 20%;
min-width: 136rpx;
height: 45rpx;
tn-tag {
margin: 0;
width: 100%;
&:first-child {
.tn-tag {
border-top-right-radius: 0rpx;
border-bottom-right-radius: 0rpx;
color: #FFFFFF !important;
background-color: $tn-main-color !important;
}
}
&:last-child {
.tn-tag {
&::after {
border-color: $tn-main-color !important;
border-top-left-radius: 0rpx;
border-bottom-left-radius: 0rpx;
}
}
}
}
}
/* 双标签 end*/
/* #endif */
/* 微信小程序编译后页面有组件名的元素,特别处理 end */
/* 头条小程序编译后页面有组件名的元素,特别处理 start */
/* #ifdef MP-TOUTIAO */
// 各家小程序宫格组件外层设置为100%,避免受到父元素display: flex;的影响
/* #endif */
/* 头条小程序编译后页面有组件名的元素,特别处理 end */
+18
View File
@@ -0,0 +1,18 @@
// 获取父组件的参数,在支付宝小程序中不支持provide/inject的写法
// 在非H5中this.$parent可以获取到父组件,但是在H5中需要多次调用this.$parent.$parent.xxx
// 传递默认值undefined表示查找最顶层的$parent
export default function $parent(name = undefined) {
let parent = this.$parent
// 通过whle遍历,这里主要是为了H5需要多层解析
while(parent) {
// 父组件
if (parent.$options && parent.$options.name !== name) {
// 如果组件的name不相等,则继续查找
parent = parent.$parent
} else {
return parent
}
}
return false
}
+22
View File
@@ -0,0 +1,22 @@
/**
* 打乱传入的数组
*
* @param {Array} array 待打乱的数组
*/
function random(array = []) {
return array.sort(() => Math.random() - 0.5)
}
/**
* 判断是否为数组
*
* @param {Object} arr
*/
function isArray(arr) {
return Object.prototype.toString.call(arr) === '[object Array]'
}
export default {
random,
isArray
}
+266
View File
@@ -0,0 +1,266 @@
let color = [
'red',
'purplered',
'purple',
'bluepurple',
'aquablue',
'blue',
'indigo',
'cyan',
'teal',
'green',
'yellowgreen',
'lime',
'yellow',
'orangeyellow',
'orange',
'orangered',
'brown',
'grey',
'gray'
]
/**
* 获取图鸟配色颜色列表
*/
function getTuniaoColorList() {
return color
}
/**
* 获取指定类型的随机颜色对应的类
* @param {String} type 颜色类型
*/
function getRandomColorClass(type = 'bg') {
const index = Math.floor(Math.random() * color.length)
const colorValue = color[index]
return 'tn-' + type + '-' + colorValue
}
/**
* 随机获取酷炫背景对应的类
*/
function getRandomCoolBgClass() {
const index = (Math.random() * 16) + 1
return 'tn-cool-bg-color-' + Math.floor(index)
}
/**
* 根据传入的值获取内部背景颜色类
*
* @param {String} backgroundColor 背景颜色信息
*/
function getBackgroundColorInternalClass(backgroundColor = '') {
if (!backgroundColor) return ''
if (['tn-bg', 'tn-dynamic-bg'].some(item => {
return backgroundColor.includes(item)
})) {
return backgroundColor
}
return ''
}
/**
* 根据传入的值获取背景颜色样式
*
* @param {String} backgroundColor 背景颜色信息
*/
function getBackgroundColorStyle(backgroundColor = '') {
if (!backgroundColor) return ''
if (!backgroundColor.startsWith('tn-') || ['#', 'rgb', 'rgba'].some(item => {
return backgroundColor.includes(item)
})) {
return backgroundColor
}
return ''
}
/**
* 根据传入的值获取内部字体颜色类
*
* @param {String} fontColor 背景颜色信息
*/
function getFontColorInternalClass(fontColor = '') {
if (!fontColor) return ''
if (['tn-color'].some(item => {
return fontColor.includes(item)
})) {
return fontColor
}
return ''
}
/**
* 根据传入的值获取字体颜色样式
*
* @param {String} fontColor 背景颜色信息
*/
function getFontColorStyle(fontColor = '') {
if (!fontColor) return ''
if (!fontColor.startsWith('tn-') || ['#', 'rgb', 'rgba'].some(item => {
return fontColor.includes(item)
})) {
return fontColor
}
return ''
}
/**
* 求两个颜色之间的渐变值
*
* @param {String} startColor 开始颜色
* @param {String} endColor 结束颜色
* @param {Number} step 颜色等分的份额
*/
function colorGradient(startColor = 'rgb(0, 0, 0)', endColor='rgb(255, 255, 255)', step = 10) {
let startRGB = hexToRGB(startColor, false)
let startR = startRGB[0]
let startG = startRGB[1]
let startB = startRGB[2]
let endRGB = hexToRGB(endColor, false)
let endR = endRGB[0]
let endG = endRGB[1]
let endB = endRGB[2]
// 求差值
let R = (endR - startR) / step
let G = (endG - startG) / step
let B = (endB - startB) / step
let colorArr = []
for (let i = 0; i < step; i++) {
// 计算每一步的hex值
let hex = rgbToHex(`rgb(${Math.round(R * i + startR)}, ${Math.round(G * i + startG)}, ${Math.round(B * i + startB)})`)
colorArr.push(hex)
}
return colorArr
}
/**
* 将hex的颜色表示方式转换为rgb表示方式
*
* @param {String} color 颜色
* @param {Boolean} str 是否返回字符串
* @return {Array|String} rgb的值
*/
function hexToRGB(color, str = true) {
let reg = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/
color = color.toLowerCase()
if (color && reg.test(color)) {
// #000 => #000000
if (color.length === 4) {
let colorNew = '#'
for (let i = 1; i < 4; i++) {
colorNew += color.slice(i, i + 1).concat(color.slice(i, i + 1))
}
color = colorNew
}
// 处理六位的颜色值
let colorChange = []
for (let i = 1; i < 7; i += 2) {
colorChange.push(parseInt("0x" + color.slice(i, i + 2)))
}
if (!str) {
return colorChange
} else {
return `rgb(${colorChange[0]}, ${colorChange[1]}, ${colorChange[2]})`
}
} else if (/^(rgb|RGB)/.test(color)) {
let arr = color.replace(/(?:\(|\)|rgb|RGB)*/g, "").split(',')
return arr.map(item => Number(item))
} else {
return color
}
}
/**
* 将rgb的颜色表示方式转换成hex表示方式
*
* @param {Object} rgb rgb颜色值
*/
function rgbToHex(rgb) {
let reg = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/
if (/^(rgb|RGB)/.test(rgb)) {
let color = rgb.replace(/(?:\(|\)|rgb|GRB)*/g, "").split(',')
let strHex = '#'
for (let i = 0; i < color.length; i++) {
let hex = Number(color[i]).toString(16)
// 保证每个值否是两位数
hex = String(hex).length === 1 ? 0 + '' + hex: hex
if (hex === '0') {
hex += hex
}
strHex += hex
}
if (strHex.length !== 7) {
strHex = rgb
}
return strHex
} else if (reg.test(rgb)) {
let num = rgb.replace(/#/, '').split('')
if (num.length === 6) {
return rgb
} else if (num.length === 3) {
let numHex = '#'
for (let i = 0; i < num.length; i++) {
numHex += (num[i] + num[i])
}
return numHex
}
} else {
return rgb
}
}
/**
* 将传入的颜色值转换为rgba字符串
*
* @param {String} color 颜色
* @param {Number} alpha 透明度
*/
function colorToRGBA(color, alpha = 0.3) {
color = rgbToHex(color)
// 十六进制颜色值的正则表达式
let reg = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/
color = color.toLowerCase()
if (color && reg.test(color)) {
// #000 => #000000
if (color.length === 4) {
let colorNew = '#'
for (let i = 1; i < 4; i++) {
colorNew += color.slice(i, i + 1).concat(color.slice(i, i + 1))
}
color = colorNew
}
// 处理六位的颜色值
let colorChange = []
for (let i = 1; i < 7; i += 2) {
colorChange.push(parseInt("0x" + color.slice(i, i + 2)))
}
return `rgba(${colorChange[0]}, ${colorChange[1]}, ${colorChange[2]}, ${alpha})`
} else {
return color
}
}
export default {
getTuniaoColorList,
getRandomColorClass,
getRandomCoolBgClass,
getBackgroundColorInternalClass,
getBackgroundColorStyle,
getFontColorInternalClass,
getFontColorStyle,
colorGradient,
hexToRGB,
rgbToHex,
colorToRGBA
}
+29
View File
@@ -0,0 +1,29 @@
/**
* 判断是否为数组
*
* @param {Object} arr
*/
function isArray(arr) {
return Object.prototype.toString.call(arr) === '[object Array]'
}
/**
* 深度复制数据
*
* @param {Object} obj
*/
function deepClone(obj) {
if ([null, undefined, NaN, false].includes(obj)) return obj
if (typeof obj !== 'object' && typeof obj !== 'function') {
return obj
}
var o = isArray(obj) ? [] : {}
for (let i in obj) {
if (obj.hasOwnProperty(i)) {
o[i] = typeof obj[i] === 'object' ? deepClone(obj[i]) : obj[i]
}
}
return o
}
export default deepClone
+74
View File
@@ -0,0 +1,74 @@
/**
* 弹出系统内置的toast
*/
function toast(title, mask = false, cb = null, icon = 'none', duration = 1500) {
uni.showToast({
title: title,
icon: icon,
mask: mask,
duration: duration,
success: () => {
setTimeout(() => {
cb && cb()
}, duration)
}
})
}
/**
* 弹出内置的加载框
*/
function loading(title) {
uni.showLoading({
title: title,
mask: true
})
}
/**
* 弹出系统内置的modal
*/
function modal(title,
content,
confirmCb,
showCancel = false,
cancelCb = null,
confirmText = "确定",
cancelText = "取消") {
uni.showModal({
title: title,
content: content,
showCancel: showCancel,
cancelText: cancelText,
confirmText: confirmText,
success: (res) => {
if (res.cancel) {
cancelCb && cancelCb()
} else if (res.confirm) {
confirmCb && confirmCb()
}
}
})
}
/**
* 关闭系统内置toast
*/
function closeToast() {
uni.hideToast()
}
/**
* 关闭系统内置的加载框
*/
function closeLoading() {
uni.hideLoading()
}
export default {
toast,
loading,
modal,
closeToast,
closeLoading
}
+96
View File
@@ -0,0 +1,96 @@
/**
* 格式化数字字符串
* @param {String, Number} value 待格式化的字符串
* @param {Number} digits 保留位数
*/
function formatNumberString(value, digits = 2) {
let number = 0
// 判断是什么类型
if (typeof value === 'string') {
number = Number(value)
} else if (typeof value === 'number') {
number = value
}
if (isNaN(number) || number === 0) {
return 0
}
let maxNumber = Math.pow(10, digits) - 1
if (number > maxNumber) {
return `${maxNumber}+`
}
return number
}
/**
* 格式化数字字符串,往数字前添加0
*
* @param {Object} num 待格式化的数值
*/
function formatNumberAddZero(value) {
let number = 0
// 判断是什么类型
if (typeof value === 'string') {
number = Number(value)
} else if (typeof value === 'number') {
number = value
}
if (isNaN(number) || +number < 10) {
return '0' + number
} else {
return String(number)
}
}
/**
* 格式化数字,往数值后添加单位
*
* @param {Object} value 待格式化的数值
* @param {Object} digits 保留位数
*/
function formatNumberAddUnit(value, digits = 2) {
// 数值分割点
const unitSplit = [
{ value: 1, symbol: ''},
{ value: 1E3, symbol: 'K'},
{ value: 1E4, symbol: 'W'},
]
const reg = /\.0+$|(\.[0=9]*[1-9])0+$/
let number = 0
// 判断是什么类型
if (typeof value === 'string') {
number = Number(value)
} else if (typeof value === 'number') {
number = value
}
let i
for (i = unitSplit.length - 1; i > 0; i--) {
if (number >= unitSplit[i].value) break
}
return (number / unitSplit[i].value).toFixed(digits).replace(reg, "$1") + unitSplit[i].symbol
}
/**
* 获取数值的整数位数
*
* @param {Object} number 数值
*/
function getDigit(number) {
let digit = -1
while (number >= 1) {
digit++
number = number / 10
}
return digit
}
export default {
formatNumberString,
formatNumberAddZero,
formatNumberAddUnit
}
+38
View File
@@ -0,0 +1,38 @@
/**
* 去掉字符串中空格
*
* @param {String} str 待处理的字符串
* @param {String} type 处理类型
*/
function trim(str, type = 'both') {
if (type === 'both') {
return str.replace(/^\s+|\s+$/g, "")
} else if (type === 'left') {
return str.replace(/^\s*/g, "")
} else if (type === 'right') {
return str.replace(/(\s*$)/g, "")
} else if (type === 'all') {
return str.replace(/\s+/g, "")
} else {
return str
}
}
/**
* 获取带单位的长度值
*
* @param {String} value 待处理的值
* @param {String} unit 单位
*/
function getLengthUnitValue(value, unit = 'rpx') {
if (!value) {
return ''
}
if (/(%|px|rpx|auto)$/.test(value)) return value
else return value + unit
}
export default {
trim,
getLengthUnitValue
}
+232
View File
@@ -0,0 +1,232 @@
/**
* 验证电子邮箱格式
*/
function email(value) {
return /[\w!#$%&'*+/=?^_`{|}~-]+(?:\.[\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\w](?:[\w-]*[\w])?\.)+[\w](?:[\w-]*[\w])?/.test(value);
}
/**
* 验证手机格式
*/
function mobile(value) {
return /^1[3-9]\d{9}$/.test(value)
}
/**
* 验证URL格式
*/
function url(value) {
return /http(s)?:\/\/([\w-]+\.)+[\w-]+(\/[\w-.\/?%&=]*)?/.test(value)
}
/**
* 验证日期格式
*/
function date(value) {
return !/Invalid|NaN/.test(new Date(value).toString())
}
/**
* 验证ISO类型的日期格式
*/
function dateISO(value) {
return /^\d{4}[\/\-](0?[1-9]|1[012])[\/\-](0?[1-9]|[12][0-9]|3[01])$/.test(value)
}
/**
* 验证十进制数字
*/
function number(value) {
return /^[\+-]?(\d+\.?\d*|\.\d+|\d\.\d+e\+\d+)$/.test(value)
}
/**
* 验证整数
*/
function digits(value) {
return /^\d+$/.test(value)
}
/**
* 验证身份证号码
*/
function idCard(value) {
return /^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}([0-9]|X)$/.test(
value)
}
/**
* 是否车牌号
*/
function carNo(value) {
// 新能源车牌
const xreg = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}(([0-9]{5}[DF]$)|([DF][A-HJ-NP-Z0-9][0-9]{4}$))/;
// 旧车牌
const creg = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳]{1}$/;
if (value.length === 7) {
return creg.test(value);
} else if (value.length === 8) {
return xreg.test(value);
} else {
return false;
}
}
/**
* 金额,只允许2位小数
*/
function amount(value) {
//金额,只允许保留两位小数
return /^[1-9]\d*(,\d{3})*(\.\d{1,2})?$|^0\.\d{1,2}$/.test(value);
}
/**
* 中文
*/
function chinese(value) {
let reg = /^[\u4e00-\u9fa5]+$/gi;
return reg.test(value);
}
/**
* 只能输入字母
*/
function letter(value) {
return /^[a-zA-Z]*$/.test(value);
}
/**
* 只能是字母或者数字
*/
function enOrNum(value) {
//英文或者数字
let reg = /^[0-9a-zA-Z]*$/g;
return reg.test(value);
}
/**
* 验证是否包含某个值
*/
function contains(value, param) {
return value.indexOf(param) >= 0
}
/**
* 验证一个值范围[min, max]
*/
function range(value, param) {
return value >= param[0] && value <= param[1]
}
/**
* 验证一个长度范围[min, max]
*/
function rangeLength(value, param) {
return value.length >= param[0] && value.length <= param[1]
}
/**
* 是否固定电话
*/
function landline(value) {
let reg = /^\d{3,4}-\d{7,8}(-\d{3,4})?$/;
return reg.test(value);
}
/**
* 判断是否为空
*/
function empty(value) {
switch (typeof value) {
case 'undefined':
return true;
case 'string':
if (value.replace(/(^[ \t\n\r]*)|([ \t\n\r]*$)/g, '').length == 0) return true;
break;
case 'boolean':
if (!value) return true;
break;
case 'number':
if (0 === value || isNaN(value)) return true;
break;
case 'object':
if (null === value || value.length === 0) return true;
for (var i in value) {
return false;
}
return true;
}
return false;
}
/**
* 是否json字符串
*/
function jsonString(value) {
if (typeof value == 'string') {
try {
var obj = JSON.parse(value);
if (typeof obj == 'object' && obj) {
return true;
} else {
return false;
}
} catch (e) {
return false;
}
}
return false;
}
/**
* 是否数组
*/
function array(value) {
if (typeof Array.isArray === "function") {
return Array.isArray(value);
} else {
return Object.prototype.toString.call(value) === "[object Array]";
}
}
/**
* 是否对象
*/
function object(value) {
return Object.prototype.toString.call(value) === '[object Object]';
}
/**
* 是否短信验证码
*/
function code(value, len = 6) {
return new RegExp(`^\\d{${len}}$`).test(value);
}
export default {
email,
mobile,
url,
date,
dateISO,
number,
digits,
idCard,
carNo,
amount,
chinese,
letter,
enOrNum,
contains,
range,
rangeLength,
empty,
isEmpty: empty,
jsonString,
landline,
object,
array,
code
}
@@ -0,0 +1,48 @@
/**
* 更新自定义顶部导航栏的高度
*/
async function updateCustomBarInfo () {
return await new Promise((resolve, reject) => {
uni.getSystemInfo({
success: (e) => {
let statusBarHeight = 0
let customBarHeight = 0
// #ifndef MP
statusBarHeight = e.statusBarHeight
if (e.platform == 'android') {
customBarHeight = e.statusBarHeight + 50
} else {
customBarHeight = e.statusBarHeight + 45
};
// #endif
// #ifdef MP-WEIXIN
statusBarHeight = e.statusBarHeight
let custom = wx.getMenuButtonBoundingClientRect()
customBarHeight = custom.bottom + ((custom.top - e.statusBarHeight) <= 4 ? (custom.top - e
.statusBarHeight) + 4 : (custom.top - e.statusBarHeight))
// #endif
// #ifdef MP-ALIPAY
statusBarHeight = e.statusBarHeight
customBarHeight = e.statusBarHeight + e.titleBarHeight
// #endif
this && this.$t.vuex('vuex_status_bar_height', statusBarHeight)
this && this.$t.vuex('vuex_custom_bar_height', customBarHeight)
resolve({
statusBarHeight,
customBarHeight
})
},
fail: (err) => {
console.log("获取设备信息失败", err);
this && this.$t.vuex('vuex_status_bar_height', 0)
this && this.$t.vuex('vuex_custom_bar_height', 0)
reject()
}
})
})
}
export default updateCustomBarInfo
+41
View File
@@ -0,0 +1,41 @@
/**
* 本算法来源于简书开源代码,详见:https://www.jianshu.com/p/fdbf293d0a85
* 全局唯一标识符(uuidGlobally Unique Identifier,也称作 uuid(Universally Unique IDentifier)
* 一般用于多个组件之间,给它一个唯一的标识符,或者v-for循环的时候,如果使用数组的index可能会导致更新列表出现问题
* 最可能的情况是左滑删除item或者对某条信息流"不喜欢"并去掉它的时候,会导致组件内的数据可能出现错乱
* v-for的时候,推荐使用后端返回的id而不是循环的index
* @param {Number} len uuid的长度
* @param {Boolean} firstT 将返回的首字母置为"t"
* @param {Nubmer} radix 生成uuid的基数(意味着返回的字符串都是这个基数),2-二进制,8-八进制,10-十进制,16-十六进制
*/
function uuid(len = 32, firstT = true, radix = null) {
let chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('')
let uuid = []
radix = radix || chars.length
if (len) {
// 如果指定uuid长度,只是取随机的字符,0|x为位运算,能去掉x的小数位,返回整数位
for (let i = 0; i < len; i++) uuid[i] = chars[0 | Math.random() * radix]
} else {
let r;
// rfc4122标准要求返回的uuid中,某些位为固定的字符
uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-'
uuid[14] = '4'
for (let i = 0; i < 36; i++) {
if (!uuid[i]) {
r = 0 | Math.random() * 16
uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]
}
}
}
// 移除第一个字符,并用t替代,因为第一个字符为数值时,该uuid不能用作id或者class
if (firstT) {
uuid.shift()
return 't' + uuid.join('')
} else {
return uuid.join('')
}
}
export default uuid
@@ -0,0 +1,99 @@
import buildURL from '../helpers/buildURL'
import buildFullPath from '../core/buildFullPath'
import settle from '../core/settle'
import { isUndefined } from "../utils"
/**
* 返回可选值存在的配置
* @param {Array} keys - 可选值数组
* @param {Object} config2 - 配置
* @return {{}} - 存在的配置项
*/
const mergeKeys = (keys, config2) => {
let config = {}
keys.forEach(prop => {
if (!isUndefined(config2[prop])) {
config[prop] = config2[prop]
}
})
return config
}
export default (config) => {
return new Promise((resolve, reject) => {
let fullPath = buildURL(buildFullPath(config.baseURL, config.url), config.params)
const _config = {
url: fullPath,
header: config.header,
complete: (response) => {
config.fullPath = fullPath
response.config = config
try {
// 对可能字符串不是json 的情况容错
if (typeof response.data === 'string') {
response.data = JSON.parse(response.data)
}
// eslint-disable-next-line no-empty
} catch (e) {
}
settle(resolve, reject, response)
}
}
let requestTask
if (config.method === 'UPLOAD') {
delete _config.header['content-type']
delete _config.header['Content-Type']
let otherConfig = {
// #ifdef MP-ALIPAY
fileType: config.fileType,
// #endif
filePath: config.filePath,
name: config.name
}
const optionalKeys = [
// #ifdef APP-PLUS || H5
'files',
// #endif
// #ifdef H5
'file',
// #endif
// #ifdef H5 || APP-PLUS
'timeout',
// #endif
'formData'
]
requestTask = uni.uploadFile({..._config, ...otherConfig, ...mergeKeys(optionalKeys, config)})
} else if (config.method === 'DOWNLOAD') {
// #ifdef H5 || APP-PLUS
if (!isUndefined(config['timeout'])) {
_config['timeout'] = config['timeout']
}
// #endif
requestTask = uni.downloadFile(_config)
} else {
const optionalKeys = [
'data',
'method',
// #ifdef H5 || APP-PLUS || MP-ALIPAY || MP-WEIXIN
'timeout',
// #endif
'dataType',
// #ifndef MP-ALIPAY
'responseType',
// #endif
// #ifdef APP-PLUS
'sslVerify',
// #endif
// #ifdef H5
'withCredentials',
// #endif
// #ifdef APP-PLUS
'firstIpv4',
// #endif
]
requestTask = uni.request({..._config,...mergeKeys(optionalKeys, config)})
}
if (config.getTask) {
config.getTask(requestTask, config)
}
})
}
@@ -0,0 +1,51 @@
'use strict'
function InterceptorManager() {
this.handlers = []
}
/**
* Add a new interceptor to the stack
*
* @param {Function} fulfilled The function to handle `then` for a `Promise`
* @param {Function} rejected The function to handle `reject` for a `Promise`
*
* @return {Number} An ID used to remove interceptor later
*/
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected
})
return this.handlers.length - 1
}
/**
* Remove an interceptor from the stack
*
* @param {Number} id The ID that was returned by `use`
*/
InterceptorManager.prototype.eject = function eject(id) {
if (this.handlers[id]) {
this.handlers[id] = null
}
}
/**
* Iterate over all the registered interceptors
*
* This method is particularly useful for skipping over any
* interceptors that may have become `null` calling `eject`.
*
* @param {Function} fn The function to call for each interceptor
*/
InterceptorManager.prototype.forEach = function forEach(fn) {
this.handlers.forEach(h => {
if (h !== null) {
fn(h)
}
})
}
export default InterceptorManager
+200
View File
@@ -0,0 +1,200 @@
/**
* @Class Request
* @description luch-request http请求插件
* @version 3.0.7
* @Author lu-ch
* @Date 2021-09-04
* @Email webwork.s@qq.com
* 文档: https://www.quanzhan.co/luch-request/
* github: https://github.com/lei-mu/luch-request
* DCloud: http://ext.dcloud.net.cn/plugin?id=392
* HBuilderX: beat-3.0.4 alpha-3.0.4
*/
import dispatchRequest from './dispatchRequest'
import InterceptorManager from './InterceptorManager'
import mergeConfig from './mergeConfig'
import defaults from './defaults'
import { isPlainObject } from '../utils'
import clone from '../utils/clone'
export default class Request {
/**
* @param {Object} arg - 全局配置
* @param {String} arg.baseURL - 全局根路径
* @param {Object} arg.header - 全局header
* @param {String} arg.method = [GET|POST|PUT|DELETE|CONNECT|HEAD|OPTIONS|TRACE] - 全局默认请求方式
* @param {String} arg.dataType = [json] - 全局默认的dataType
* @param {String} arg.responseType = [text|arraybuffer] - 全局默认的responseType。支付宝小程序不支持
* @param {Object} arg.custom - 全局默认的自定义参数
* @param {Number} arg.timeout - 全局默认的超时时间,单位 ms。默认60000。H5(HBuilderX 2.9.9+)、APP(HBuilderX 2.9.9+)、微信小程序(2.10.0)、支付宝小程序
* @param {Boolean} arg.sslVerify - 全局默认的是否验证 ssl 证书。默认true.仅App安卓端支持(HBuilderX 2.3.3+
* @param {Boolean} arg.withCredentials - 全局默认的跨域请求时是否携带凭证(cookies)。默认false。仅H5支持(HBuilderX 2.6.15+
* @param {Boolean} arg.firstIpv4 - 全DNS解析时优先使用ipv4。默认false。仅 App-Android 支持 (HBuilderX 2.8.0+)
* @param {Function(statusCode):Boolean} arg.validateStatus - 全局默认的自定义验证器。默认statusCode >= 200 && statusCode < 300
*/
constructor(arg = {}) {
if (!isPlainObject(arg)) {
arg = {}
console.warn('设置全局参数必须接收一个Object')
}
this.config = clone({...defaults, ...arg})
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
}
}
/**
* @Function
* @param {Request~setConfigCallback} f - 设置全局默认配置
*/
setConfig(f) {
this.config = f(this.config)
}
middleware(config) {
config = mergeConfig(this.config, config)
let chain = [dispatchRequest, undefined]
let promise = Promise.resolve(config)
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected)
})
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected)
})
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift())
}
return promise
}
/**
* @Function
* @param {Object} config - 请求配置项
* @prop {String} options.url - 请求路径
* @prop {Object} options.data - 请求参数
* @prop {Object} [options.responseType = config.responseType] [text|arraybuffer] - 响应的数据类型
* @prop {Object} [options.dataType = config.dataType] - 如果设为 json,会尝试对返回的数据做一次 JSON.parse
* @prop {Object} [options.header = config.header] - 请求header
* @prop {Object} [options.method = config.method] - 请求方法
* @returns {Promise<unknown>}
*/
request(config = {}) {
return this.middleware(config)
}
get(url, options = {}) {
return this.middleware({
url,
method: 'GET',
...options
})
}
post(url, data, options = {}) {
return this.middleware({
url,
data,
method: 'POST',
...options
})
}
// #ifndef MP-ALIPAY
put(url, data, options = {}) {
return this.middleware({
url,
data,
method: 'PUT',
...options
})
}
// #endif
// #ifdef APP-PLUS || H5 || MP-WEIXIN || MP-BAIDU
delete(url, data, options = {}) {
return this.middleware({
url,
data,
method: 'DELETE',
...options
})
}
// #endif
// #ifdef H5 || MP-WEIXIN
connect(url, data, options = {}) {
return this.middleware({
url,
data,
method: 'CONNECT',
...options
})
}
// #endif
// #ifdef H5 || MP-WEIXIN || MP-BAIDU
head(url, data, options = {}) {
return this.middleware({
url,
data,
method: 'HEAD',
...options
})
}
// #endif
// #ifdef APP-PLUS || H5 || MP-WEIXIN || MP-BAIDU
options(url, data, options = {}) {
return this.middleware({
url,
data,
method: 'OPTIONS',
...options
})
}
// #endif
// #ifdef H5 || MP-WEIXIN
trace(url, data, options = {}) {
return this.middleware({
url,
data,
method: 'TRACE',
...options
})
}
// #endif
upload(url, config = {}) {
config.url = url
config.method = 'UPLOAD'
return this.middleware(config)
}
download(url, config = {}) {
config.url = url
config.method = 'DOWNLOAD'
return this.middleware(config)
}
}
/**
* setConfig回调
* @return {Object} - 返回操作后的config
* @callback Request~setConfigCallback
* @param {Object} config - 全局默认config
*/
@@ -0,0 +1,20 @@
'use strict'
import isAbsoluteURL from '../helpers/isAbsoluteURL'
import combineURLs from '../helpers/combineURLs'
/**
* Creates a new URL by combining the baseURL with the requestedURL,
* only when the requestedURL is not already an absolute URL.
* If the requestURL is absolute, this function returns the requestedURL untouched.
*
* @param {string} baseURL The base URL
* @param {string} requestedURL Absolute or relative URL to combine
* @returns {string} The combined full path
*/
export default function buildFullPath(baseURL, requestedURL) {
if (baseURL && !isAbsoluteURL(requestedURL)) {
return combineURLs(baseURL, requestedURL)
}
return requestedURL
}
@@ -0,0 +1,30 @@
/**
* 默认的全局配置
*/
export default {
baseURL: '',
header: {},
method: 'GET',
dataType: 'json',
// #ifndef MP-ALIPAY
responseType: 'text',
// #endif
custom: {},
// #ifdef H5 || APP-PLUS || MP-ALIPAY || MP-WEIXIN
timeout: 60000,
// #endif
// #ifdef APP-PLUS
sslVerify: true,
// #endif
// #ifdef H5
withCredentials: false,
// #endif
// #ifdef APP-PLUS
firstIpv4: false,
// #endif
validateStatus: function validateStatus(status) {
return status >= 200 && status < 300
}
}
@@ -0,0 +1,6 @@
import adapter from '../adapters/index'
export default (config) => {
return adapter(config)
}
@@ -0,0 +1,103 @@
import {deepMerge, isUndefined} from '../utils'
/**
* 合并局部配置优先的配置,如果局部有该配置项则用局部,如果全局有该配置项则用全局
* @param {Array} keys - 配置项
* @param {Object} globalsConfig - 当前的全局配置
* @param {Object} config2 - 局部配置
* @return {{}}
*/
const mergeKeys = (keys, globalsConfig, config2) => {
let config = {}
keys.forEach(prop => {
if (!isUndefined(config2[prop])) {
config[prop] = config2[prop]
} else if (!isUndefined(globalsConfig[prop])) {
config[prop] = globalsConfig[prop]
}
})
return config
}
/**
*
* @param globalsConfig - 当前实例的全局配置
* @param config2 - 当前的局部配置
* @return - 合并后的配置
*/
export default (globalsConfig, config2 = {}) => {
const method = config2.method || globalsConfig.method || 'GET'
let config = {
baseURL: globalsConfig.baseURL || '',
method: method,
url: config2.url || '',
params: config2.params || {},
custom: {...(globalsConfig.custom || {}), ...(config2.custom || {})},
header: deepMerge(globalsConfig.header || {}, config2.header || {})
}
const defaultToConfig2Keys = ['getTask', 'validateStatus']
config = {...config, ...mergeKeys(defaultToConfig2Keys, globalsConfig, config2)}
// eslint-disable-next-line no-empty
if (method === 'DOWNLOAD') {
// #ifdef H5 || APP-PLUS
if (!isUndefined(config2.timeout)) {
config['timeout'] = config2['timeout']
} else if (!isUndefined(globalsConfig.timeout)) {
config['timeout'] = globalsConfig['timeout']
}
// #endif
} else if (method === 'UPLOAD') {
delete config.header['content-type']
delete config.header['Content-Type']
const uploadKeys = [
// #ifdef APP-PLUS || H5
'files',
// #endif
// #ifdef MP-ALIPAY
'fileType',
// #endif
// #ifdef H5
'file',
// #endif
'filePath',
'name',
// #ifdef H5 || APP-PLUS
'timeout',
// #endif
'formData',
]
uploadKeys.forEach(prop => {
if (!isUndefined(config2[prop])) {
config[prop] = config2[prop]
}
})
// #ifdef H5 || APP-PLUS
if (isUndefined(config.timeout) && !isUndefined(globalsConfig.timeout)) {
config['timeout'] = globalsConfig['timeout']
}
// #endif
} else {
const defaultsKeys = [
'data',
// #ifdef H5 || APP-PLUS || MP-ALIPAY || MP-WEIXIN
'timeout',
// #endif
'dataType',
// #ifndef MP-ALIPAY
'responseType',
// #endif
// #ifdef APP-PLUS
'sslVerify',
// #endif
// #ifdef H5
'withCredentials',
// #endif
// #ifdef APP-PLUS
'firstIpv4',
// #endif
]
config = {...config, ...mergeKeys(defaultsKeys, globalsConfig, config2)}
}
return config
}
@@ -0,0 +1,16 @@
/**
* Resolve or reject a Promise based on response status.
*
* @param {Function} resolve A function that resolves the promise.
* @param {Function} reject A function that rejects the promise.
* @param {object} response The response.
*/
export default function settle(resolve, reject, response) {
const validateStatus = response.config.validateStatus
const status = response.statusCode
if (status && (!validateStatus || validateStatus(status))) {
resolve(response)
} else {
reject(response)
}
}
@@ -0,0 +1,69 @@
'use strict'
import * as utils from './../utils'
function encode(val) {
return encodeURIComponent(val).
replace(/%40/gi, '@').
replace(/%3A/gi, ':').
replace(/%24/g, '$').
replace(/%2C/gi, ',').
replace(/%20/g, '+').
replace(/%5B/gi, '[').
replace(/%5D/gi, ']')
}
/**
* Build a URL by appending params to the end
*
* @param {string} url The base of the url (e.g., http://www.google.com)
* @param {object} [params] The params to be appended
* @returns {string} The formatted url
*/
export default function buildURL(url, params) {
/*eslint no-param-reassign:0*/
if (!params) {
return url
}
var serializedParams
if (utils.isURLSearchParams(params)) {
serializedParams = params.toString()
} else {
var parts = []
utils.forEach(params, function serialize(val, key) {
if (val === null || typeof val === 'undefined') {
return
}
if (utils.isArray(val)) {
key = key + '[]'
} else {
val = [val]
}
utils.forEach(val, function parseValue(v) {
if (utils.isDate(v)) {
v = v.toISOString()
} else if (utils.isObject(v)) {
v = JSON.stringify(v)
}
parts.push(encode(key) + '=' + encode(v))
})
})
serializedParams = parts.join('&')
}
if (serializedParams) {
var hashmarkIndex = url.indexOf('#')
if (hashmarkIndex !== -1) {
url = url.slice(0, hashmarkIndex)
}
url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams
}
return url
}
@@ -0,0 +1,14 @@
'use strict'
/**
* Creates a new URL by combining the specified URLs
*
* @param {string} baseURL The base URL
* @param {string} relativeURL The relative URL
* @returns {string} The combined URL
*/
export default function combineURLs(baseURL, relativeURL) {
return relativeURL
? baseURL.replace(/\/+$/, '') + '/' + relativeURL.replace(/^\/+/, '')
: baseURL
}
@@ -0,0 +1,14 @@
'use strict'
/**
* Determines whether the specified URL is absolute
*
* @param {string} url The URL to test
* @returns {boolean} True if the specified URL is absolute, otherwise false
*/
export default function isAbsoluteURL(url) {
// A URL is considered absolute if it begins with "<scheme>://" or "//" (protocol-relative URL).
// RFC 3986 defines scheme name as a sequence of characters beginning with a letter and followed
// by any combination of letters, digits, plus, period, or hyphen.
return /^([a-z][a-z\d+\-.]*:)?\/\//i.test(url)
}
+116
View File
@@ -0,0 +1,116 @@
type AnyObject = Record<string | number | symbol, any>
type HttpPromise<T> = Promise<HttpResponse<T>>;
type Tasks = UniApp.RequestTask | UniApp.UploadTask | UniApp.DownloadTask
export interface RequestTask {
abort: () => void;
offHeadersReceived: () => void;
onHeadersReceived: () => void;
}
export interface HttpRequestConfig<T = Tasks> {
/** 请求基地址 */
baseURL?: string;
/** 请求服务器接口地址 */
url?: string;
/** 请求查询参数,自动拼接为查询字符串 */
params?: AnyObject;
/** 请求体参数 */
data?: AnyObject;
/** 文件对应的 key */
name?: string;
/** HTTP 请求中其他额外的 form data */
formData?: AnyObject;
/** 要上传文件资源的路径。 */
filePath?: string;
/** 需要上传的文件列表。使用 files 时,filePath 和 name 不生效,App、H5 2.6.15+ */
files?: Array<{
name?: string;
file?: File;
uri: string;
}>;
/** 要上传的文件对象,仅H5(2.6.15+)支持 */
file?: File;
/** 请求头信息 */
header?: AnyObject;
/** 请求方式 */
method?: "GET" | "POST" | "PUT" | "DELETE" | "CONNECT" | "HEAD" | "OPTIONS" | "TRACE" | "UPLOAD" | "DOWNLOAD";
/** 如果设为 json,会尝试对返回的数据做一次 JSON.parse */
dataType?: string;
/** 设置响应的数据类型,支付宝小程序不支持 */
responseType?: "text" | "arraybuffer";
/** 自定义参数 */
custom?: AnyObject;
/** 超时时间,仅微信小程序(2.10.0)、支付宝小程序支持 */
timeout?: number;
/** DNS解析时优先使用ipv4,仅 App-Android 支持 (HBuilderX 2.8.0+) */
firstIpv4?: boolean;
/** 验证 ssl 证书 仅5+App安卓端支持(HBuilderX 2.3.3+ */
sslVerify?: boolean;
/** 跨域请求时是否携带凭证(cookies)仅H5支持(HBuilderX 2.6.15+ */
withCredentials?: boolean;
/** 返回当前请求的task, options。请勿在此处修改options。 */
getTask?: (task: T, options: HttpRequestConfig<T>) => void;
/** 全局自定义验证器 */
validateStatus?: (statusCode: number) => boolean | void;
}
export interface HttpResponse<T = any> {
config: HttpRequestConfig;
statusCode: number;
cookies: Array<string>;
data: T;
errMsg: string;
header: AnyObject;
}
export interface HttpUploadResponse<T = any> {
config: HttpRequestConfig;
statusCode: number;
data: T;
errMsg: string;
}
export interface HttpDownloadResponse extends HttpResponse {
tempFilePath: string;
}
export interface HttpError {
config: HttpRequestConfig;
statusCode?: number;
cookies?: Array<string>;
data?: any;
errMsg: string;
header?: AnyObject;
}
export interface HttpInterceptorManager<V, E = V> {
use(
onFulfilled?: (config: V) => Promise<V> | V,
onRejected?: (config: E) => Promise<E> | E
): void;
eject(id: number): void;
}
export abstract class HttpRequestAbstract {
constructor(config?: HttpRequestConfig);
config: HttpRequestConfig;
interceptors: {
request: HttpInterceptorManager<HttpRequestConfig, HttpRequestConfig>;
response: HttpInterceptorManager<HttpResponse, HttpError>;
}
middleware<T = any>(config: HttpRequestConfig): HttpPromise<T>;
request<T = any>(config: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
get<T = any>(url: string, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
upload<T = any>(url: string, config?: HttpRequestConfig<UniApp.UploadTask>): HttpPromise<T>;
delete<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
head<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
post<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
put<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
connect<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
options<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
trace<T = any>(url: string, data?: AnyObject, config?: HttpRequestConfig<UniApp.RequestTask>): HttpPromise<T>;
download(url: string, config?: HttpRequestConfig<UniApp.DownloadTask>): Promise<HttpDownloadResponse>;
setConfig(onSend: (config: HttpRequestConfig) => HttpRequestConfig): void;
}
declare class HttpRequest extends HttpRequestAbstract { }
export default HttpRequest;
+2
View File
@@ -0,0 +1,2 @@
import Request from './core/Request'
export default Request
+135
View File
@@ -0,0 +1,135 @@
'use strict'
// utils is a library of generic helper functions non-specific to axios
var toString = Object.prototype.toString
/**
* Determine if a value is an Array
*
* @param {Object} val The value to test
* @returns {boolean} True if value is an Array, otherwise false
*/
export function isArray (val) {
return toString.call(val) === '[object Array]'
}
/**
* Determine if a value is an Object
*
* @param {Object} val The value to test
* @returns {boolean} True if value is an Object, otherwise false
*/
export function isObject (val) {
return val !== null && typeof val === 'object'
}
/**
* Determine if a value is a Date
*
* @param {Object} val The value to test
* @returns {boolean} True if value is a Date, otherwise false
*/
export function isDate (val) {
return toString.call(val) === '[object Date]'
}
/**
* Determine if a value is a URLSearchParams object
*
* @param {Object} val The value to test
* @returns {boolean} True if value is a URLSearchParams object, otherwise false
*/
export function isURLSearchParams (val) {
return typeof URLSearchParams !== 'undefined' && val instanceof URLSearchParams
}
/**
* Iterate over an Array or an Object invoking a function for each item.
*
* If `obj` is an Array callback will be called passing
* the value, index, and complete array for each item.
*
* If 'obj' is an Object callback will be called passing
* the value, key, and complete object for each property.
*
* @param {Object|Array} obj The object to iterate
* @param {Function} fn The callback to invoke for each item
*/
export function forEach (obj, fn) {
// Don't bother if no value provided
if (obj === null || typeof obj === 'undefined') {
return
}
// Force an array if not already something iterable
if (typeof obj !== 'object') {
/*eslint no-param-reassign:0*/
obj = [obj]
}
if (isArray(obj)) {
// Iterate over array values
for (var i = 0, l = obj.length; i < l; i++) {
fn.call(null, obj[i], i, obj)
}
} else {
// Iterate over object keys
for (var key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
fn.call(null, obj[key], key, obj)
}
}
}
}
/**
* 是否为boolean 值
* @param val
* @returns {boolean}
*/
export function isBoolean(val) {
return typeof val === 'boolean'
}
/**
* 是否为真正的对象{} new Object
* @param {any} obj - 检测的对象
* @returns {boolean}
*/
export function isPlainObject(obj) {
return Object.prototype.toString.call(obj) === '[object Object]'
}
/**
* Function equal to merge with the difference being that no reference
* to original objects is kept.
*
* @see merge
* @param {Object} obj1 Object to merge
* @returns {Object} Result of all merge properties
*/
export function deepMerge(/* obj1, obj2, obj3, ... */) {
let result = {}
function assignValue(val, key) {
if (typeof result[key] === 'object' && typeof val === 'object') {
result[key] = deepMerge(result[key], val)
} else if (typeof val === 'object') {
result[key] = deepMerge({}, val)
} else {
result[key] = val
}
}
for (let i = 0, l = arguments.length; i < l; i++) {
forEach(arguments[i], assignValue)
}
return result
}
export function isUndefined (val) {
return typeof val === 'undefined'
}
+264
View File
@@ -0,0 +1,264 @@
/* eslint-disable */
var clone = (function() {
'use strict';
function _instanceof(obj, type) {
return type != null && obj instanceof type;
}
var nativeMap;
try {
nativeMap = Map;
} catch(_) {
// maybe a reference error because no `Map`. Give it a dummy value that no
// value will ever be an instanceof.
nativeMap = function() {};
}
var nativeSet;
try {
nativeSet = Set;
} catch(_) {
nativeSet = function() {};
}
var nativePromise;
try {
nativePromise = Promise;
} catch(_) {
nativePromise = function() {};
}
/**
* Clones (copies) an Object using deep copying.
*
* This function supports circular references by default, but if you are certain
* there are no circular references in your object, you can save some CPU time
* by calling clone(obj, false).
*
* Caution: if `circular` is false and `parent` contains circular references,
* your program may enter an infinite loop and crash.
*
* @param `parent` - the object to be cloned
* @param `circular` - set to true if the object to be cloned may contain
* circular references. (optional - true by default)
* @param `depth` - set to a number if the object is only to be cloned to
* a particular depth. (optional - defaults to Infinity)
* @param `prototype` - sets the prototype to be used when cloning an object.
* (optional - defaults to parent prototype).
* @param `includeNonEnumerable` - set to true if the non-enumerable properties
* should be cloned as well. Non-enumerable properties on the prototype
* chain will be ignored. (optional - false by default)
*/
function clone(parent, circular, depth, prototype, includeNonEnumerable) {
if (typeof circular === 'object') {
depth = circular.depth;
prototype = circular.prototype;
includeNonEnumerable = circular.includeNonEnumerable;
circular = circular.circular;
}
// maintain two arrays for circular references, where corresponding parents
// and children have the same index
var allParents = [];
var allChildren = [];
var useBuffer = typeof Buffer != 'undefined';
if (typeof circular == 'undefined')
circular = true;
if (typeof depth == 'undefined')
depth = Infinity;
// recurse this function so we don't reset allParents and allChildren
function _clone(parent, depth) {
// cloning null always returns null
if (parent === null)
return null;
if (depth === 0)
return parent;
var child;
var proto;
if (typeof parent != 'object') {
return parent;
}
if (_instanceof(parent, nativeMap)) {
child = new nativeMap();
} else if (_instanceof(parent, nativeSet)) {
child = new nativeSet();
} else if (_instanceof(parent, nativePromise)) {
child = new nativePromise(function (resolve, reject) {
parent.then(function(value) {
resolve(_clone(value, depth - 1));
}, function(err) {
reject(_clone(err, depth - 1));
});
});
} else if (clone.__isArray(parent)) {
child = [];
} else if (clone.__isRegExp(parent)) {
child = new RegExp(parent.source, __getRegExpFlags(parent));
if (parent.lastIndex) child.lastIndex = parent.lastIndex;
} else if (clone.__isDate(parent)) {
child = new Date(parent.getTime());
} else if (useBuffer && Buffer.isBuffer(parent)) {
if (Buffer.from) {
// Node.js >= 5.10.0
child = Buffer.from(parent);
} else {
// Older Node.js versions
child = new Buffer(parent.length);
parent.copy(child);
}
return child;
} else if (_instanceof(parent, Error)) {
child = Object.create(parent);
} else {
if (typeof prototype == 'undefined') {
proto = Object.getPrototypeOf(parent);
child = Object.create(proto);
}
else {
child = Object.create(prototype);
proto = prototype;
}
}
if (circular) {
var index = allParents.indexOf(parent);
if (index != -1) {
return allChildren[index];
}
allParents.push(parent);
allChildren.push(child);
}
if (_instanceof(parent, nativeMap)) {
parent.forEach(function(value, key) {
var keyChild = _clone(key, depth - 1);
var valueChild = _clone(value, depth - 1);
child.set(keyChild, valueChild);
});
}
if (_instanceof(parent, nativeSet)) {
parent.forEach(function(value) {
var entryChild = _clone(value, depth - 1);
child.add(entryChild);
});
}
for (var i in parent) {
var attrs = Object.getOwnPropertyDescriptor(parent, i);
if (attrs) {
child[i] = _clone(parent[i], depth - 1);
}
try {
var objProperty = Object.getOwnPropertyDescriptor(parent, i);
if (objProperty.set === 'undefined') {
// no setter defined. Skip cloning this property
continue;
}
child[i] = _clone(parent[i], depth - 1);
} catch(e){
if (e instanceof TypeError) {
// when in strict mode, TypeError will be thrown if child[i] property only has a getter
// we can't do anything about this, other than inform the user that this property cannot be set.
continue
} else if (e instanceof ReferenceError) {
//this may happen in non strict mode
continue
}
}
}
if (Object.getOwnPropertySymbols) {
var symbols = Object.getOwnPropertySymbols(parent);
for (var i = 0; i < symbols.length; i++) {
// Don't need to worry about cloning a symbol because it is a primitive,
// like a number or string.
var symbol = symbols[i];
var descriptor = Object.getOwnPropertyDescriptor(parent, symbol);
if (descriptor && !descriptor.enumerable && !includeNonEnumerable) {
continue;
}
child[symbol] = _clone(parent[symbol], depth - 1);
Object.defineProperty(child, symbol, descriptor);
}
}
if (includeNonEnumerable) {
var allPropertyNames = Object.getOwnPropertyNames(parent);
for (var i = 0; i < allPropertyNames.length; i++) {
var propertyName = allPropertyNames[i];
var descriptor = Object.getOwnPropertyDescriptor(parent, propertyName);
if (descriptor && descriptor.enumerable) {
continue;
}
child[propertyName] = _clone(parent[propertyName], depth - 1);
Object.defineProperty(child, propertyName, descriptor);
}
}
return child;
}
return _clone(parent, depth);
}
/**
* Simple flat clone using prototype, accepts only objects, usefull for property
* override on FLAT configuration object (no nested props).
*
* USE WITH CAUTION! This may not behave as you wish if you do not know how this
* works.
*/
clone.clonePrototype = function clonePrototype(parent) {
if (parent === null)
return null;
var c = function () {};
c.prototype = parent;
return new c();
};
// private utility functions
function __objToStr(o) {
return Object.prototype.toString.call(o);
}
clone.__objToStr = __objToStr;
function __isDate(o) {
return typeof o === 'object' && __objToStr(o) === '[object Date]';
}
clone.__isDate = __isDate;
function __isArray(o) {
return typeof o === 'object' && __objToStr(o) === '[object Array]';
}
clone.__isArray = __isArray;
function __isRegExp(o) {
return typeof o === 'object' && __objToStr(o) === '[object RegExp]';
}
clone.__isRegExp = __isRegExp;
function __getRegExpFlags(re) {
var flags = '';
if (re.global) flags += 'g';
if (re.ignoreCase) flags += 'i';
if (re.multiline) flags += 'm';
return flags;
}
clone.__getRegExpFlags = __getRegExpFlags;
return clone;
})();
export default clone

Some files were not shown because too many files have changed in this diff Show More