增加other分包页面

我的页面里增加跳转other分包跳转(仅在ios不是浏览器审核时展示)
This commit is contained in:
YeMingfei666 2024-12-20 18:01:37 +08:00
parent 908205200b
commit b2fd3ba347
154 changed files with 43228 additions and 84 deletions

44
App.vue
View File

@ -1,6 +1,6 @@
<script>
import config from '@/common/config.js'
import store from './store/index.js'
export default {
onLaunch: function() {
let that = this
@ -469,7 +469,8 @@
content: res.des,
success: res => {
if (res.confirm) {
plus.runtime.openURL(config.APIHOST2+'/pages/login/appEq')
plus.runtime.openURL(config.APIHOST2 +
'/pages/login/appEq')
return
uni.showLoading({
title: '下载中...',
@ -480,10 +481,18 @@
uni.downloadFile({
url: androidLink,
success: downloadResult => {
console.log(downloadResult)
if (downloadResult.statusCode === 200) {
console.log(
downloadResult
)
if (downloadResult
.statusCode ===
200) {
plus.runtime
.install(downloadResult.tempFilePath, { force: false },
.install(
downloadResult
.tempFilePath, {
force: false
},
d => {
console
.log(
@ -493,7 +502,10 @@
.restart();
},
e => {
console.log(e)
console
.log(
e
)
console
.error(
'install fail...'
@ -531,7 +543,8 @@
content: res.des,
success: res => {
if (res.confirm) {
plus.runtime.openURL(config.APIHOST2+'/pages/login/appEq')
plus.runtime.openURL(config.APIHOST2 +
'/pages/login/appEq')
return
uni.showLoading({
title: '下载中...',
@ -542,9 +555,15 @@
uni.downloadFile({
url: androidLink,
success: downloadResult => {
if (downloadResult.statusCode === 200) {
if (downloadResult
.statusCode ===
200) {
plus.runtime
.install(downloadResult.tempFilePath, { force: false },
.install(
downloadResult
.tempFilePath, {
force: false
},
d => {
console
.log(
@ -554,7 +573,10 @@
.restart();
},
e => {
console.log(e)
console
.log(
e
)
console
.error(
'install fail...'
@ -840,4 +862,6 @@
@import 'components/colorui/main.css';
@import 'components/colorui/icon.css';
@import '@/common/style/common.scss';
@import './tuniao-ui/index.scss';
@import './tuniao-ui/iconfont.css';
</style>

8
common/config/config.js Normal file
View File

@ -0,0 +1,8 @@
export default {
baseUrl: 'http://127.0.0.1/',
baseApi: 'http://127.0.0.1:7001/',
color:{
main:'#FFC428',
red:'#fa3534'
}
}

458
components/other-xuafu.vue Normal file
View File

@ -0,0 +1,458 @@
<template>
<button @click="navThanks" v-if="show">
<view class="dong">
<view class="monster">
<view class="monster__face">
<view class="monster__eye avatar-eye avatar-eye--green eye--left">
<view class="avatar-eye-pupil pupil--green"><span class="avatar-eye-pupil-blackThing"><span
class="avatar-eye-pupil-lightReflection"></span></span></view>
</view>
<view class="monster__eye avatar-eye avatar-eye--violet eye--right">
<view class="avatar-eye-pupil pupil--pink"><span class="avatar-eye-pupil-blackThing"><span
class="avatar-eye-pupil-lightReflection"></span></span></view>
</view>
<view class="monster__noses">
<view class="monster__nose"></view>
<view class="monster__nose"></view>
</view>
<view class="monster__mouth">
<view class="monster__top"></view>
<view class="monster__bottom"></view>
</view>
</view>
</view>
</view>
</button>
</template>
<script>
import {returnIsShenhe} from '@/utils/api.js'
import {isIos} from '@/utils/app.js'
export default {
name: "other-xuanu",
data() {
return {
show:false
};
},
methods:{
navThanks(){
uni.navigateTo({
url:'/other/index/index'
})
},
async init(){
const isShehe=await returnIsShenhe()
this.show=isShehe
}
},
mounted() {
// #ifdef APP
if(isIos()){
this.init()
}
// #endif
}
}
</script>
<style scoped>
/* 大嘴鸟*/
.dong {
z-index: 9999;
position: fixed;
bottom: 0;
right: -80px;
transform: scale(0.24);
-webkit-transform: scale(0.24);
-moz-transform: scale(0.24);
}
.monster {
transform: rotate(-50deg);
-ms-transform: rotate(-50deg);
/* IE 9 */
-moz-transform: rotate(-50deg);
/* Firefox */
-webkit-transform: rotate(-50deg);
/* Safari 和 Chrome */
-o-transform: rotate(-50deg);
/* Opera */
display: flex;
justify-content: center;
position: relative;
width: 170px;
height: 400px;
border-top-left-radius: 200px;
border-top-right-radius: 200px;
background-color: rgb(255, 117, 129);
box-shadow: 20px 20px 60px rgba(255, 117, 129,.7);
}
.monster__face {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
position: absolute;
top: 14%;
width: 75%;
height: 160px;
}
.monster__noses {
top: 50%;
display: flex;
justify-content: space-between;
width: 28%;
height: auto;
margin-bottom: 10px;
}
.monster__nose {
width: 8px;
height: 12px;
border-radius: 20px;
background: rgba(0, 0, 0, 0.5);
box-shadow: 4px 8px 5px rgba(0, 0, 0, 0.1);
}
.monster__mouth {
display: flex;
justify-content: center;
align-items: center;
position: relative;
width: 100%;
height: 0%;
overflow: hidden;
border: 25px solid #FFC333;
border-radius: 100px;
background-color: #810332;
animation: mouth 1.75s infinite;
box-shadow: 4px 8px 5px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
}
.monster__mouth::before {
content: '';
position: absolute;
width: 150px;
height: 80px;
border-radius: 100px;
background-color: #400018;
}
.monster__mouth::after {
content: '';
position: absolute;
bottom: -80px;
width: 160px;
height: 80px;
border-top-left-radius: 50%;
border-top-right-radius: 50%;
background-color: #DC1B50;
animation: tongue 1.75s infinite;
}
.monster__top {
position: absolute;
top: -30px;
width: 170px;
height: 30px;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
background-color: #ffffff;
z-index: 100;
animation: t 1.75s infinite;
}
.monster__bottom {
position: absolute;
bottom: 0;
width: 100px;
height: 30px;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
background-color: #ffffff;
z-index: 100;
animation: b 1.75s infinite;
}
.avatar-eye {
position: absolute;
top: -10%;
width: 65px;
height: 65px;
background: linear-gradient(105deg, white, #cb87f4);
border-radius: 100%;
box-shadow: 4px 8px 5px rgba(0, 0, 0, 0.2);
margin: 3px;
-webkit-transform: translateX(-50%);
transform: translateX(-50%);
}
.avatar-eye--green {
background: linear-gradient(to bottom, #fdfdfd, #c3efea);
}
.avatar-eye--violet {
background: linear-gradient(to bottom, #fdfdfd, #e6d6f6);
}
.eye--left {
left: 10%;
}
.eye--right {
left: 85%;
}
.eye--center {
left: 45%;
top: 10%;
}
.avatar-eye-pupil {
position: absolute;
width: 55%;
height: 55%;
left: 50%;
top: 25%;
-webkit-transform: translate(-50%);
transform: translate(-50%);
z-index: 100;
border-radius: 100%;
}
.pupil--green {
background: linear-gradient(135deg, rgba(188, 248, 177, 0.7), #2fa38c 75%);
}
.pupil--pink {
background: linear-gradient(135deg, #f1a183, #8535cd);
}
.avatar-eye-pupil-blackThing {
position: absolute;
width: 55%;
height: 55%;
left: 50%;
top: 25%;
background: #2c2f32;
-webkit-transform: translate(-50%);
transform: translate(-50%);
border-radius: 100%;
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.2);
}
.avatar-eye-pupil-lightReflection {
position: absolute;
width: 7px;
height: 7px;
left: 25%;
top: 10%;
background: #ebebeb;
-webkit-transform: translate(-50%);
transform: translate(-50%);
border-radius: 100%;
box-shadow: 10px 10px 10px rgba(255, 255, 255, 0.2);
}
/*大嘴动起来*/
@keyframes t {
0%,
10%,
80%,
100% {
top: -30px;
}
20% {
top: 0px;
}
30% {
top: -20px;
}
40% {
top: -0px;
}
50% {
top: -25px;
}
70% {
top: 0px;
}
}
@keyframes b {
0%,
10%,
80%,
100% {
bottom: -30px;
}
20% {
bottom: 0px;
}
30% {
bottom: -20px;
}
40% {
bottom: -0px;
}
50% {
bottom: -25px;
}
70% {
bottom: 0px;
}
}
@keyframes mouth {
0%,
10%,
100% {
width: 100%;
height: 0%;
}
15% {
width: 90%;
height: 30%;
}
20% {
width: 50%;
height: 70%;
}
25% {
width: 70%;
height: 70%;
}
30% {
width: 80%;
height: 60%;
}
35% {
width: 60%;
height: 70%;
}
40% {
width: 55%;
height: 75%;
}
45% {
width: 50%;
height: 90%;
}
50% {
width: 40%;
height: 70%;
}
55% {
width: 70%;
height: 95%;
}
60% {
width: 40%;
height: 50%;
}
65% {
width: 100%;
height: 60%;
}
70% {
width: 100%;
height: 70%;
}
75% {
width: 90%;
height: 70%;
}
80% {
width: 50%;
height: 70%;
}
85% {
width: 90%;
height: 50%;
}
85% {
width: 40%;
height: 70%;
}
90% {
width: 90%;
height: 30%;
}
95% {
width: 100%;
height: 10%;
}
}
@keyframes tongue {
0%,
20%,
100% {
bottom: -80px;
}
30%,
90% {
bottom: -40px;
}
40% {
bottom: -45px;
}
50% {
bottom: -50px;
}
70% {
bottom: -80px;
}
90% {
bottom: -40px;
}
}
</style>

View File

@ -0,0 +1,94 @@
<template>
<view class="demo-title">
<view>
<view v-if="type === 'first'" class="main_title">
<view v-if="leftIcon" class="main_title__icon main_title__icon--left" :class="[`tn-icon-${leftIcon}`]"></view>
<view class="main_title__content">{{ title }}</view>
<view v-if="rightIcon" class="main_title__icon main_title__icon--right" :class="[`tn-icon-${rightIcon}`]"></view>
</view>
<view v-if="type === 'second'" class="second_title">
<view class="second_title__content">{{ title }}</view>
</view>
</view>
<view class="content" :class="[{
'content--padding': contentPadding
}]">
<slot></slot>
</view>
</view>
</template>
<script>
export default {
name: 'demo-title',
options: {
// Vue(shadow)
virtualHost: true
},
props: {
//
type: {
type: String,
default: 'first'
},
//
title: {
type: String,
default: ''
},
//
leftIcon: {
type: String,
default: 'star'
},
//
rightIcon: {
type: String,
default: 'star'
},
//
contentPadding: {
type: Boolean,
default: true
}
}
}
</script>
<style lang="scss" scoped>
.main_title {
display: flex;
align-items: center;
justify-content: center;
margin-top: 50rpx;
font-size: 36rpx;
font-weight: bold;
&__content {
padding: 0 18rpx;
}
&__icon {
font-size: 34rpx;
}
}
.second_title {
margin: 24rpx 0;
margin-left: 30rpx;
&__content {
font-size: 32rpx;
font-weight: bold;
}
}
.content {
margin-top: 30rpx;
&--padding {
margin-left: 30rpx;
margin-right: 30rpx;
}
}
</style>

View File

@ -0,0 +1,689 @@
<template>
<view class="dynamic-demo">
<!-- 效果预览窗口 -->
<view v-if="!noDemo" class="demo-container" :class="{'demo-container--full': full}">
<view class="demo">
<slot></slot>
</view>
<!-- 提示信息 -->
<view v-if="haveTips">
<view class="demo__tips__icon" @click="demoTipsClick">
<view class="icon tn-icon-help"></view>
</view>
<view class="demo__tips__content"
:class="[showContentTips ? 'demo__tips__content--show' : 'demo__tips__content--hide']">
<view v-for="(item,index) in tipsData" :key="index" class="demo__tips__content--item">{{ item }}</view>
</view>
</view>
</view>
<!-- 模式切换 -->
<view v-if="multiMode" class="mode-switch">
<view class="mode-switch__container">
<view v-for="(item, index) in sectionModeListInfos" :key="index" class="mode-switch__item"
:class="[`mode-switch-item-${index}`,{'mode-switch__item--active': modeIndex === index}]"
@click="switchMode(index)">{{ item.name }}</view>
<!-- 滑块样式 -->
<view class="mode-switch__slider" :style="[modeSwitchSliderStyle]"></view>
</view>
</view>
<!-- 组件对应可选项容器 -->
<view class="section-container">
<scroll-view
class="section__scroll-view"
:class="{'section__scroll-view--auto': sectionScrollViewStyle.height === 'auto'}"
:style="[sectionScrollViewStyle]"
:scroll-y="sectionScrollViewStyle.height !== 'auto'"
>
<block v-for="(item,index) in btnsList" :key="index">
<view class="section__content" :class="{'section__content--visible': item.show}">
<view class="section__content__title">
<view class="section__content__title__left-line" :class="[`tn-main-gradient-${tuniaoColorList[index]}`]"></view>
<view class="section__content__title--text tn-text-ellipsis" :class="[`tn-main-gradient-${tuniaoColorList[index]}`]">{{ item.title }}</view>
<view class="section__content__title__right-line" :class="[`tn-main-gradient-${tuniaoColorList[index]}`]"></view>
</view>
<view class="section__content__btns">
<view v-for="(section_btn,section_index) in item.optional" :key="section_index"
class="section__content__btns__item" :class="[`tn-main-gradient-${tuniaoColorList[index]}--light`]" @click="sectionBtnClick(index, section_index)">
<view class="section__content__btns__item__bg"
:class="[`tn-main-gradient-${tuniaoColorList[index]}`, {'section__content__btns__item__bg--active':sectionIndex[modeIndex][index]['value'] === section_index}]"></view>
<view class="section__content__btns__item--text tn-text-ellipsis"
:class="[sectionIndex[modeIndex][index]['value'] === section_index ? 'section__content__btns__item--text--active' : `tn-color-${tuniaoColorList[index]}`]">{{ section_btn }}</view>
</view>
</view>
</view>
</block>
</scroll-view>
</view>
</view>
</template>
<script>
export default {
name: 'dynamic-demo-template',
props: {
//
sectionList: {
type: Array,
default() {
return []
}
},
//
tips: {
type: [String, Array],
default: ''
},
//
full: {
type: Boolean,
default: false
},
// 使
customBar: {
type: Boolean,
default: true
},
//
fullWindowsScroll: {
type: Boolean,
default: false
},
//
noDemo: {
type: Boolean,
default: false
}
},
computed: {
tipsData() {
if (typeof this.tips === 'string') {
return [this.tips]
}
return this.tips
},
haveTips() {
return this.tips && this.tips.length > 0
},
multiMode() {
return this.sectionList.length > 1
},
sectionModeList() {
return this.sectionList.map((item) => {
return item.name
})
}
},
data() {
return {
//
tuniaoColorList: this.$t.color.getTuniaoColorList(),
// prop
_sectionList: [],
//
sectionModeListInfos: [],
//
modeIndex: 0,
//
modeSwitchSliderStyle: {
width: 0,
left: 0
},
//
showContentTips: false,
//
sectionScrollViewStyle: {
height: 0
},
//
btnsList: [],
//
sectionIndex: [],
// 使scroll-view
sectionScrollFlag: true
}
},
watch: {
sectionList: {
handler(value) {
// sectionList
this.initSectionBtns()
},
deep: true
},
sectionScrollFlag(value) {
if (!value) {
this.sectionScrollViewStyle.height = 'auto'
}
},
fullWindowsScroll: {
handler(value) {
if (value) {
this.sectionScrollViewStyle.height = 'auto'
}
},
immediate: true
}
},
created() {
//
this.sectionModeListInfos = this.sectionModeList.map((item) => {
return {
name: item
}
})
//
this.initSectionBtns()
},
mounted() {
//
// setTimeout(() => {
// // scroll-view
// this.initSectionScrollView()
// if (this.multiMode) {
// //
// this.getModeTabsInfo()
// }
// }, 10)
this.$nextTick(() => {
// scroll-view
this.initSectionScrollView()
if (this.multiMode) {
//
this.getModeTabsInfo()
}
})
},
methods: {
//
initSectionScrollView() {
//
if (this.fullWindowsScroll) {
return
}
//
uni.getSystemInfo({
success: (systemInfo) => {
//
const navBarHeight = this.customBar ? 0 : this.vuex_custom_bar_height
if (this.multiMode) {
uni.createSelectorQuery().in(this).select('.mode-switch').boundingClientRect(data => {
if (data.bottom >= systemInfo.safeArea.height) {
this.sectionScrollFlag = false
} else {
this.sectionScrollFlag = true
const containerBaseHeight = systemInfo.safeArea.height - data.bottom
this.sectionScrollViewStyle.height = (containerBaseHeight - navBarHeight) + systemInfo.statusBarHeight - uni.upx2px(75) + 'px'
}
}).exec()
} else {
if (!this.noDemo) {
uni.createSelectorQuery().in(this).select('.demo-container').boundingClientRect(data => {
if (data.bottom >= systemInfo.safeArea.height) {
this.sectionScrollFlag = false
} else {
this.sectionScrollFlag = true
const containerBaseHeight = systemInfo.safeArea.height - data.bottom
this.sectionScrollViewStyle.height = (containerBaseHeight - navBarHeight) + systemInfo.statusBarHeight - uni.upx2px(75) + 'px'
}
}).exec()
} else {
this.sectionScrollFlag = false
}
}
}
})
},
//
updateSectionScrollView() {
this.$nextTick(() => {
this.initSectionScrollView()
})
},
// tab
getModeTabsInfo() {
let view = uni.createSelectorQuery().in(this)
for (let i = 0; i < this.sectionModeListInfos.length; i++) {
view.select('.mode-switch-item-' + i).boundingClientRect()
}
view.exec(res => {
//
if (!res.length) {
setTimeout(() => {
this.getModeTabsInfo()
}, 10)
return
}
// list
res.map((item, index) => {
this.sectionModeListInfos[index].width = item.width
})
//
this.modeSwitchSliderStyle.width = this.sectionModeListInfos[0].width + 'px'
//
this.modeSliderPosition()
})
},
//
modeSliderPosition() {
let left = 0
//
this.sectionModeListInfos.map((item, index) => {
if (index < this.modeIndex) left += item.width
})
this.modeSwitchSliderStyle.left = left + 'px'
},
//
switchMode(index) {
//
if (index === this.modeIndex) return
this.modeIndex = index
this.modeSliderPosition()
this.updateSectionBtns()
this.$emit('modeClick', {
index: index
})
},
//
demoTipsClick() {
this.showContentTips = !this.showContentTips
},
//
initSectionBtns() {
this.sectionIndex = []
this.sectionIndex = this.sectionList.map((item) => {
if (item.hasOwnProperty('section') && item.section.length > 0) {
return Array(item.section.length).fill({
value: 0,
change: false
})
} else {
return []
}
})
this._sectionList = this.$t.deepClone(this.sectionList)
// show
this._sectionList.map((item) => {
const section = item.section.map((section_item) => {
if (!section_item.hasOwnProperty('show')) {
section_item.show = true
}
return section_item
})
item.section = section
return item
})
//
this.updateSectionBtns()
},
//
updateSectionBtns(sectionIndex = -1, showState = true) {
let sectionOptional = this._sectionList[this.modeIndex]['section']
this.btnsList = sectionOptional.map((item, index) => {
//
let changeValue = this.sectionIndex[this.modeIndex][index]['change'] || false
let currentSectionIndexValue = this.sectionIndex[this.modeIndex][index]['value'] || 0
// (使)
let indexValue = changeValue ? currentSectionIndexValue : item.hasOwnProperty('current') ? item.current : 0
//
let show = (sectionIndex !== -1 && sectionIndex === index) ? showState : item.hasOwnProperty('show') ? item.show : true
//
if (indexValue < 0) {
indexValue = 0
}
if (indexValue >= item.optional.length) {
indexValue = item.optional.length
}
// this.sectionIndex[this.modeIndex][index]['value'] = indexValue
this.$set(this.sectionIndex[this.modeIndex], index, {value: indexValue, change: changeValue})
item.show = show
return item
})
},
//
updateSectionBtnsState(sectionIndex = -1, showState = true) {
// sectionIndex
if (this.$t.array.isArray(sectionIndex)) {
if (sectionIndex.length === 0) {
return
}
sectionIndex = sectionIndex.filter((item) => item >= 0 && item < this.sectionList[this.modeIndex]['section'].length)
sectionIndex.map((item) => {
this.btnsList[item]['show'] = showState
this._sectionList[this.modeIndex]['section'][item]['show'] = showState
})
} else {
if (sectionIndex < 0 || sectionIndex >= this.sectionList[this.modeIndex]['section'].length) {
return
}
//
this.btnsList[sectionIndex]['show'] = showState
this._sectionList[this.modeIndex]['section'][sectionIndex]['show'] = showState
}
},
//
updateSectionBtnsValue(modeIndex = 0, sectionIndex = -1, value = 0) {
if (sectionIndex < 0 || sectionIndex >= this.sectionList[modeIndex]['section'].length) {
return
}
// showStatefalse
this.sectionIndex[modeIndex][sectionIndex] = {
value,
change: true
}
},
//
sectionBtnClick(index, sectionIndex) {
// if (this.sectionIndex[this.modeIndex][index] === sectionIndex) {
// return
// }
this.$set(this.sectionIndex[this.modeIndex], index, {value: sectionIndex, change: true})
this.$emit('click', {
methods: this.btnsList[index]['methods'],
index: sectionIndex,
name: this.btnsList[index]['optional'][sectionIndex]
})
}
}
}
</script>
<style lang="scss" scoped>
.dynamic-demo {
padding-top: 78rpx;
/* 顶部模式切换start */
.mode-switch {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
margin-top: 75rpx;
padding: 0 30rpx;
&__container {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
width: 476rpx;
height: 62rpx;
background-color: #FFFFFF;
box-shadow: 0rpx 10rpx 50rpx 0rpx rgba(0, 3, 72, 0.1);
border-radius: 31rpx;
}
&__item {
flex: 1;
height: 62rpx;
width: 100%;
line-height: 62rpx;
text-align: center;
font-size: 28rpx;
color: $tn-font-sub-color;
z-index: 2;
transition: all 0.3s;
&--active {
color: #FFFFFF;
font-weight: bold;
}
}
&__slider {
position: absolute;
height: 62rpx;
border-radius: 31rpx;
// background-image: linear-gradient(-86deg, #FF8359 0%, #FFDF40 100%);
background-image: linear-gradient(-86deg, #00C3FF 0%, #58FFF5 100%);
box-shadow: 1rpx 10rpx 24rpx 0rpx #00C3FF77;
z-index: 1;
transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
}
/* 顶部模式切换end */
/* 演示内容展示start */
.demo-container {
min-height: 327rpx;
width: calc(100% - 60rpx);
background-color: #FFFFFF;
box-shadow: 0rpx 10rpx 50rpx 0rpx rgba(0, 3, 72, 0.1);
margin: 0 30rpx 5rpx 30rpx;
border-radius: 20rpx;
position: relative;
display: flex;
justify-content: center;
align-items: center;
&--full {
display: inline-block;
padding-bottom: 20rpx;
min-height: 0rpx;
padding: 10rpx 20rpx 30rpx;
}
.demo {
padding-top: 70rpx;
&__tips {
&__icon {
position: absolute;
top: 20rpx;
right: 16rpx;
width: 39rpx;
height: 39rpx;
line-height: 39rpx;
font-size: 39rpx;
.icon {
background: linear-gradient(-45deg, #FF8359 0%, #FFDF40 100%);
-webkit-background-clip: text;
color: transparent;
text-shadow: 0rpx 10rpx 10rpx rgba(255, 156, 82, 0.2);
}
}
&__content {
position: absolute;
top: 65rpx;
right: 16rpx;
font-size: 20rpx;
margin-left: 20rpx;
word-wrap: normal;
display: flex;
flex-direction: column;
background-color: #E6E6E6;
padding: 20rpx;
border-radius: 10rpx;
transition: transform 0.3s cubic-bezier(0.68, -0.55, 0.265, 1);
transform-origin: 0 0;
z-index: 999999;
&--hide {
transform: scaleY(0);
}
&--show {
transform: scaleY(100%);
&::after {
content: "";
width: 0px;
height: 0px;
border-width: 4px;
border-style: solid;
border-color: transparent transparent rgba(149, 149, 149, 0.1) transparent;
position: absolute;
top: -8px;
right: 6px;
}
}
}
}
}
}
/* 演示内容展示end */
/* 可选项start */
.section-container {
width: 100%;
height: auto;
margin-top: 70rpx;
.section {
&__content {
margin-top: 70rpx;
display: none;
&--visible {
display: block;
&:last-child {
padding-bottom: calc(70rpx + env(safe-area-inset-bottom));
}
}
&:nth-child(1) {
margin-top: 0rpx;
}
&__title {
display: flex;
justify-content: center;
align-items: center;
margin: 0 30rpx;
text-align: center;
&__left-line,
&__right-line {
width: 100rpx;
height: 2rpx;
position: relative;
}
&__left-line {
&::after {
content: '';
background: inherit;
width: 12rpx;
height: 12rpx;
position: absolute;
top: -12rpx;
right: 0rpx;
border-radius: 50%;
transform: translateY(50%);
}
}
&__right-line {
&::after {
content: '';
background: inherit;
width: 12rpx;
height: 12rpx;
position: absolute;
top: -12rpx;
left: 0rpx;
border-radius: 50%;
transform: translateY(50%);
}
}
&--text {
-webkit-background-clip: text;
color: transparent;
min-width: 124rpx;
height: 30rpx;
font-size: 32rpx;
line-height: 1;
margin: 0 35rpx;
}
}
&__btns {
width: calc(100% - 60rpx);
margin: 0 30rpx;
margin-top: 29rpx;
padding: 50rpx 30rpx 0rpx 0rpx;
background-color: #FFFFFF;
box-shadow: 0rpx 10rpx 50rpx 0rpx rgba(0, 3, 72, 0.1);
border-radius: 20rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
flex-wrap: wrap;
&__item {
max-width: 30%;
padding: 17rpx 36rpx;
border-radius: 10rpx;
margin-bottom: 40rpx;
margin-left: 40rpx;
position: relative;
z-index: 1;
// &::before {
// content: " ";
// position: absolute;
// top: 10rpx;
// left: 1rpx;
// width: 100%;
// height: 100%;
// background: inherit;
// filter: blur(24rpx);
// opacity: 1;
// z-index: -1;
// }
&__bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: inherit;
z-index: -1;
opacity: 0;
transform: scale(0);
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
&--active {
opacity: 1;
transform: scale(1);
}
}
&--text {
font-size: 24rpx;
line-height: 1.2em;
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
&--active {
color: #FFFFFF;
}
}
}
}
}
}
}
/* 可选项end */
}
</style>

View File

@ -0,0 +1,147 @@
<template>
<view class="multiple-options">
<view class="list">
<block v-for="(item, index) in listData" :key="index">
<view
class="list__item"
:class="[`tn-main-gradient-${tuniaoColorList[item.bgColorIndex]}--light`]"
@tap="navOptionsPage(item.url)"
>
<view class="list__content">
<view class="list__content__title">{{ item.title }}</view>
<view class="list__content__desc">{{ item.desc }}</view>
</view>
<view class="list__icon">
<view class="list__icon__main" :class="[`tn-icon-${item.mainIcon}`, `tn-main-gradient-${tuniaoColorList[item.bgColorIndex]}`]"></view>
<view class="list__icon__sub" :class="[`tn-icon-${item.subIcon}`, `tn-main-gradient-${tuniaoColorList[item.bgColorIndex]}`]"></view>
</view>
</view>
</block>
</view>
</view>
</template>
<script>
export default {
name: 'multiple-options-demo',
props: {
//
list: {
type: Array,
default() {
return []
}
}
},
data() {
return {
//
tuniaoColorList: [
'red',
'purplered',
'purple',
'bluepurple',
'aquablue',
'blue',
'indigo',
'cyan',
'teal',
'green',
'orange',
'orangered'
],
listData: []
}
},
watch: {
list(val) {
this.initList()
}
},
created() {
this.initList()
},
methods: {
//
initList() {
//
this.listData = this.list.map((item, index) => {
item.bgColorIndex = this.getBgNum()
item.mainIcon = item?.mainIcon || 'computer-fill'
item.subIcon = item?.subIcon || 'share'
return item
})
},
//
navOptionsPage(url) {
uni.navigateTo({
url: url
})
},
//
getBgNum() {
return Math.floor((Math.random() * this.tuniaoColorList.length))
}
}
}
</script>
<style lang="scss" scoped>
.list {
&__item {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
width: calc(100% - 60rpx);
margin: 108rpx 30rpx 0rpx 30rpx;
box-shadow: 0rpx 10rpx 50rpx 0rpx rgba(0, 3, 72, 0.1);
border-radius: 20rpx;
}
&__content {
flex: 1;
// color: $tn-font-color;
margin: 34rpx 0rpx 27rpx 37rpx;
&__title {
font-size: 36rpx;
font-weight: bold;
margin-bottom: 12rpx;
}
&__desc {
font-size: 28rpx;
}
}
&__icon {
flex: 1;
margin-right: 26rpx;
position: relative;
&__main, &__sub {
-webkit-background-clip: text;
color: transparent;
position: absolute;
transition: transform 0.25s ease;
}
&__main {
font-size: 200rpx;
width: 190rpx;
line-height: 200rpx;
top: 0;
right: 0rpx;
transform: translateY(-60%);
}
&__sub {
font-size: 70rpx;
top: 0;
right: 175rpx;
transform: translateY(-5rpx);
}
}
}
</style>

View File

@ -0,0 +1,169 @@
<template>
<view class="nav-index-button" :style="{bottom: `${bottom}rpx`, right: `${right}rpx`}" @tap.stop="navIndex">
<view class="nav-index-button__content">
<view class="nav-index-button__content--icon tn-flex tn-flex-row-center tn-flex-col-center tn-shadow-blur tn-cool-bg-color-5">
<view class="tn-icon-home-fill"></view>
</view>
</view>
<view class="nav-index-button__meteor">
<view class="nav-index-button__meteor__wrapper">
<view v-for="(item,index) in 6" :key="index" class="nav-index-button__meteor__item" :style="{transform: `rotateX(${-60 + (30 * index)}deg) rotateZ(${-60 + (30 * index)}deg)`}">
<view class="nav-index-button__meteor__item--pic"></view>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'nav-index-button',
props: {
//
bottom: {
type: [Number, String],
default: 300
},
//
right: {
type: [Number, String],
default: 75
},
//
indexPath: {
type: String,
default: '/pages/index'
}
},
methods: {
//
navIndex() {
//
const pages = getCurrentPages()
if (pages && pages.length > 0) {
const indexPath = this.indexPath || '/pages/index'
const firstPage = pages[0]
if (pages.length == 1 && (!firstPage.route || firstPage.route != indexPath.substring(1, indexPath.length))) {
uni.reLaunch({
url: indexPath
})
} else {
uni.navigateBack({
delta: 1
})
}
} else {
uni.reLaunch({
url: indexPath
})
}
}
}
}
</script>
<style lang="scss" scoped>
.nav-index-button {
position: fixed;
animation: suspension 3s ease-in-out infinite;
z-index: 999999;
&__content {
position: absolute;
width: 100rpx;
height: 100rpx;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
&--icon {
width: 100rpx;
height: 100rpx;
font-size: 60rpx;
border-radius: 50%;
margin-bottom: 18rpx;
position: relative;
z-index: 1;
transform: scale(0.85);
&::after {
content: " ";
position: absolute;
z-index: -1;
width: 100%;
height: 100%;
left: 0;
bottom: 0;
border-radius: inherit;
opacity: 1;
transform: scale(1, 1);
background-size: 100% 100%;
background-image: url(https://resource.tuniaokj.com/images/cool_bg_image/icon_bg6.png);
}
}
}
&__meteor {
position: absolute;
top: 50%;
left: 50%;
width: 100rpx;
height: 100rpx;
transform-style: preserve-3d;
transform: translate(-50%, -50%) rotateY(75deg) rotateZ(10deg);
&__wrapper {
width: 100rpx;
height: 100rpx;
transform-style: preserve-3d;
animation: spin 20s linear infinite;
}
&__item {
position: absolute;
width: 100rpx;
height: 100rpx;
border-radius: 1000rpx;
left: 0;
top: 0;
&--pic {
display: block;
width: 100%;
height: 100%;
background: url(https://resource.tuniaokj.com/images/cool_bg_image/arc3.png) no-repeat center center;
background-size: 100% 100%;
animation: arc 4s linear infinite;
}
}
}
}
@keyframes suspension {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-0.8rem);
}
}
@keyframes spin {
0% {
transform: rotateX(0deg);
}
100% {
transform: rotateX(-360deg);
}
}
@keyframes arc {
to {
transform: rotate(360deg);
}
}
</style>

View File

@ -0,0 +1,52 @@
/**
* 动态参数演示mixin
*/
module.exports = {
data() {
return {
// 效果显示框top的值
contentContainerTop: '0px',
contentContainerIsTop: false,
// 参数显示框top的值
sectionContainerTop: '0px'
}
},
onReady() {
this.updateSectionContainerTop()
},
methods: {
// 处理演示效果框的位置
async _handleContentConatinerPosition() {
// 获取效果演示框的节点信息
const contentContainer = await this._tGetRect('#content_container')
// 获取参数框的节点信息
this._tGetRect('#section_container').then((res) => {
// 判断参数框是否在移动,如果是则更新效果框的位置
// 如果效果框的顶部已经触控到顶部导航栏就停止跟随
if (res.top - contentContainer.bottom != 15) {
const newTop = res.top - (contentContainer.height + uni.upx2px(20))
const minTop = this.vuex_custom_bar_height + 1
if (newTop < minTop) {
this.contentContainerTop = minTop + 'px'
this.contentContainerIsTop = true
} else {
this.contentContainerTop = newTop + 'px'
this.contentContainerIsTop = false
}
}
})
},
// 更新状态切换栏位置信息
updateSectionContainerTop() {
this._tGetRect('#content_container').then((res) => {
this.contentContainerTop = (this.vuex_custom_bar_height + 148) + 'px'
this.sectionContainerTop = (res.height + 20) + 'px'
})
}
},
// 监听页面滚动
onPageScroll() {
this._handleContentConatinerPosition()
}
}

View File

@ -0,0 +1,60 @@
/**
* 演示页面mixin
*/
module.exports = {
data() {
return {
}
},
onLoad() {
// 更新顶部导航栏信息
this.updateCustomBarInfo()
},
methods: {
// 点击左上角返回按钮时触发事件
goBack() {
// 通过判断当前页面的页面栈信息,是否有上一页进行返回,如果没有则跳转到首页
const pages = getCurrentPages()
if (pages && pages.length > 0) {
const firstPage = pages[0]
if (pages.length == 1 && (!firstPage.route || firstPage.route != 'pages/index')) {
uni.reLaunch({
url: '/pages/index'
})
} else {
uni.navigateBack({
delta: 1
})
}
} else {
uni.reLaunch({
url: '/pages/index'
})
}
},
// 更新顶部导航栏信息
async updateCustomBarInfo() {
// 获取vuex中的自定义顶栏的高度
let customBarHeight = this.vuex_custom_bar_height
let statusBarHeight = this.vuex_status_bar_height
// 如果获取失败则重新获取
if (!customBarHeight) {
try {
const navBarInfo = await this.$t.updateCustomBar()
customBarHeight = navBarInfo.customBarHeight
statusBarHeight = navBarInfo.statusBarHeight
} catch(e) {
setTimeout(() => {
this.updateCustomBarInfo()
}, 10)
return
}
}
// 更新vuex中的导航栏信息
this.$t.vuex('vuex_status_bar_height', statusBarHeight)
this.$t.vuex('vuex_custom_bar_height', customBarHeight)
}
}
}

View File

@ -0,0 +1,330 @@
/**
* 页面展示列表数据
*/
export default {
data: [{
title: '图鸟首页',
backgroundColor: 'tn-cool-bg-color-1',
list: [{
icon: 'code',
title: '关于我们',
url: '/homePages/about',
author: '图鸟北北'
},{
icon: 'code',
title: '全局搜索',
url: '/homePages/search',
author: '图鸟北北'
},
{
icon: 'code',
title: '今日热榜',
url: '/homePages/hot',
author: '图鸟北北'
},
{
icon: 'code',
title: '前端业务',
url: '/homePages/profession',
author: '图鸟北北'
},
{
icon: 'code',
title: '加载效果',
url: '/homePages/loading',
author: '图鸟北北'
}
]
},
{
title: '酷炫圈子',
backgroundColor: 'tn-cool-bg-color-1',
list: [{
icon: 'code',
title: '博主_Me',
url: '/circlePages/blogger',
author: '图鸟北北'
},
{
icon: 'code',
title: '博主_Ta',
url: '/circlePages/blogger_other',
author: '图鸟北北'
},
{
icon: 'code',
title: '编辑发布',
url: '/circlePages/edit',
author: '图鸟北北'
},
{
icon: 'code',
title: '广告页',
url: '/circlePages/advertise',
author: '图鸟北北'
},
{
icon: 'code',
title: '资讯详情',
url: '/circlePages/news',
author: '图鸟北北'
},
{
icon: 'code',
title: '名片王者',
url: '/circlePages/king',
author: '图鸟北北'
},
{
icon: 'code',
title: '智能名片',
url: '/circlePages/business',
author: '图鸟北北'
},
{
icon: 'code',
title: '精选圈子',
url: '/circlePages/group',
author: '图鸟北北'
},
{
icon: 'code',
title: '积分排行',
url: '/circlePages/ranking',
author: '图鸟北北'
},
{
icon: 'code',
title: '圈子详情',
url: '/circlePages/details',
author: '图鸟北北'
},
{
icon: 'code',
title: '预约接龙',
url: '/circlePages/reserve',
author: '图鸟北北'
},
{
icon: 'code',
title: '活动创建',
url: '/circlePages/create',
author: '图鸟北北'
},
{
icon: 'code',
title: '打造圈子',
url: '/circlePages/build',
author: '图鸟北北'
},
{
icon: 'code',
title: '一起群聊',
url: '/circlePages/chat',
author: '图鸟北北'
},
{
icon: 'code',
title: '对话聊天',
url: '/circlePages/chatting',
author: '图鸟北北'
}
]
},
{
title: '活动广场',
backgroundColor: 'tn-cool-bg-color-1',
list: [{
icon: 'code',
title: '地图打卡',
url: '/activityPages/map',
author: '图鸟北北'
},
{
icon: 'code',
title: '快速答题',
url: '/activityPages/topic',
author: '图鸟北北'
},
{
icon: 'code',
title: '课程学习',
url: '/activityPages/study',
author: '图鸟北北'
},
{
icon: 'code',
title: '开源项目',
url: '/activityPages/project',
author: '图鸟北北'
},
{
icon: 'code',
title: '活动星球',
url: '/activityPages/planet',
author: '图鸟北北'
}
]
},
{
title: '商品优选',
backgroundColor: 'tn-cool-bg-color-1',
list: [{
icon: 'code',
title: '优质商家',
url: '/preferredPages/shop',
author: '图鸟北北'
},
{
icon: 'code',
title: '商品详情',
url: '/preferredPages/product',
author: '图鸟北北'
},
{
icon: 'code',
title: '历史订单',
url: '/preferredPages/order',
author: '图鸟北北'
},
{
icon: 'code',
title: '商品分类',
url: '/preferredPages/classify',
author: '图鸟北北'
},
{
icon: 'code',
title: '商家相册',
url: '/preferredPages/photo',
author: '图鸟北北'
},
{
icon: 'code',
title: '品牌官网',
url: '/preferredPages/website',
author: '图鸟北北'
},
{
icon: 'code',
title: '积分兑换',
url: '/preferredPages/redeem',
author: '图鸟北北'
},
{
icon: 'code',
title: '免单活动',
url: '/preferredPages/award',
author: '图鸟北北'
},
{
icon: 'code',
title: '免单获取',
url: '/preferredPages/awardget',
author: '图鸟北北'
}
]
},
{
title: '关于我的',
backgroundColor: 'tn-cool-bg-color-1',
list: [{
icon: 'code',
title: '使用协议',
url: '/minePages/protocol',
author: '图鸟北北'
},
{
icon: 'code',
title: '授权登录',
url: '/minePages/login',
author: '图鸟北北'
},
{
icon: 'code',
title: '消息通知',
url: '/minePages/message',
author: '图鸟北北'
},
{
icon: 'code',
title: '全局设置',
url: '/minePages/set',
author: '图鸟北北'
},
{
icon: 'code',
title: '立即体验',
url: '/minePages/start',
author: '图鸟北北'
},
{
icon: 'code',
title: '感谢名单',
url: '/minePages/thanks',
author: '图鸟北北'
},
{
icon: 'code',
title: '版本更新',
url: '/minePages/version',
author: '图鸟北北'
},
{
icon: 'code',
title: '帮助中心',
url: '/minePages/help',
author: '图鸟北北'
},
{
icon: 'code',
title: '头像上传',
url: '/minePages/avatar',
author: '图鸟北北'
},
{
icon: 'code',
title: '积分明细',
url: '/minePages/integral',
author: '图鸟北北'
},
{
icon: 'code',
title: '积分签到',
url: '/minePages/signed',
author: '图鸟北北'
},
{
icon: 'code',
title: '好物收藏',
url: '/minePages/collect',
author: '图鸟北北'
},
{
icon: 'code',
title: '账号安全',
url: '/minePages/safety',
author: '图鸟北北'
},
{
icon: 'code',
title: '赞赏支持',
url: '/minePages/reward',
author: '图鸟北北'
},
{
icon: 'code',
title: '缺省页',
url: '/minePages/default',
author: '图鸟北北'
},
{
icon: 'code',
title: '富文本',
url: '/minePages/content',
author: '图鸟北北'
}
]
}
]
}

13
main.js
View File

@ -1,10 +1,10 @@
import Vue from 'vue'
import App from './App'
import store from './store'
import Vue from 'vue'
import HttpRequest from './common/httpRequest'
import HttpCache from './common/cache'
import queue from './common/queue'
import TuniaoUI from 'tuniao-ui'
Vue.config.productionTip = false
Vue.prototype.$Request = HttpRequest;
@ -12,7 +12,6 @@ Vue.prototype.$queue = queue;
Vue.prototype.$Sysconf = HttpRequest.config;
Vue.prototype.$SysCache = HttpCache;
App.mpType = 'app'
// 引入全局uView
@ -21,13 +20,17 @@ Vue.use(uView);
const app = new Vue({
store,
...App
})
// http拦截器将此部分放在new Vue()和app.$mount()之间才能App.vue中正常使用
import httpInterceptor from '@/common/http.interceptor.js'
Vue.use(httpInterceptor, app)
Vue.use(TuniaoUI)
// 引入TuniaoUI提供的vuex简写方法
let vuexStore = require('@/store/$t.mixin.js')
Vue.mixin(vuexStore)
// http接口API集中管理引入部分
import httpApi from '@/common/http.api.js'
Vue.use(httpApi, app)

1206
other/blogger/blogger.vue Normal file

File diff suppressed because it is too large Load Diff

800
other/blogger/details.vue Normal file
View File

@ -0,0 +1,800 @@
<template>
<view class="template-details tn-safe-area-inset-bottom">
<!-- 顶部自定义导航 -->
<tn-nav-bar fixed alpha customBack>
<view slot="back" class='tn-custom-nav-bar__back'
@click="goBack">
<text class='icon tn-icon-left'></text>
<text class='icon tn-icon-home-capsule-fill'></text>
</view>
</tn-nav-bar>
<view class="" :style="{paddingTop: vuex_custom_bar_height + 'px'}">
<!-- 图文信息 -->
<block v-for="(item,index) in content" :key="index">
<view class="blogger__item">
<view class="blogger__author tn-flex tn-flex-row-between tn-flex-col-center">
<view class="justify__author__info" @click="tn('/circlePages/blogger_other')">
<view class="tn-flex tn-flex-row-center">
<view class="tn-flex tn-flex-row-center tn-flex-col-center">
<view class="">
<tn-avatar
class=""
shape="circle"
:src="item.userAvatar"
size="lg">
</tn-avatar>
</view>
<view class="tn-padding-right tn-text-ellipsis">
<view class="tn-padding-right tn-padding-left-sm tn-text-bold tn-text-lg">{{ item.userName }}</view>
<view class="tn-padding-right tn-padding-left-sm tn-padding-top-xs tn-color-gray">{{ item.date }}</view>
</view>
</view>
</view>
</view>
<view class="blogger__author__btn justify-content-item tn-flex-col-center tn-flex-row-center">
<!-- 既然都点到详情里面了加个关注按钮呗 -->
<text class="tn-bg-brown--light tn-round tn-text-df tn-text-bold tn-color-brown" style="padding: 10rpx 24rpx;">+ 关注</text>
</view>
</view>
<view class="blogger__desc tn-margin-top-sm tn-margin-bottom-sm tn-text-justify tn-flex-col-center tn-flex-row-left">
<view v-for="(label_item,label_index) in item.label" :key="label_index" class="blogger__desc__label tn-float-left tn-margin-right tn-bg-gray--light tn-round tn-text-sm tn-text-bold">
<text class="blogger__desc__label--prefix">#</text>
<text class="tn-text-df">{{ label_item }}</text>
</view>
<!-- 不用限制长度了因为发布的时候限制长度了-->
<text v-if="!item.label || item.label.length < 4" class="blogger__desc__content tn-flex-1 tn-text-justify tn-text-df">{{ item.desc }}</text>
</view>
<!-- 内容太多疲劳了-->
<!-- <view
v-if="item.content"
class="blogger__content"
:id="`blogger__content--${index}`"
>
<view
class="blogger__content__data clamp-text-2">
{{ item.content }}
</view>
</view> -->
<block v-if="item.mainImage">
<view v-if="[1,2,4].indexOf(item.mainImage.length) != -1" class="tn-padding-top-xs">
<image v-for="(image_item,image_index) in item.mainImage" :key="image_index"
class="blogger__main-image"
:class="{
'blogger__main-image--1 tn-margin-bottom-sm': item.mainImage.length === 1,
'blogger__main-image--2 tn-margin-right-sm tn-margin-bottom-sm': item.mainImage.length === 2 || item.mainImage.length === 4
}"
:src="image_item"
mode="aspectFill"
></image>
</view>
<view v-else class="tn-padding-top-xs">
<tn-grid hoverClass="none" :col="3">
<block v-for="(image_item,image_index) in item.mainImage" :key="image_index">
<!-- #ifndef MP-WEIXIN -->
<tn-grid-item style="width: 30%;margin: 10rpx;">
<image
class="blogger__main-image blogger__main-image--3"
:src="image_item"
mode="aspectFill"
></image>
</tn-grid-item>
<!-- #endif-->
<!-- #ifdef MP-WEIXIN -->
<tn-grid-item style="width: 30%;margin: 10rpx;">
<image
class="blogger__main-image blogger__main-image--3"
:src="image_item"
mode="aspectFill"
></image>
</tn-grid-item>
<!-- #endif-->
</block>
</tn-grid>
</view>
</block>
<view class="tn-flex tn-flex-row-between tn-flex-col-center tn-margin-top-xs">
<view class="justify-content-item tn-flex tn-flex-col-center">
<view style="margin-right: 10rpx;">
<tn-avatar-group :lists="item.viewUser.latestUserAvatar" size="sm"></tn-avatar-group>
</view>
<text class="tn-color-gray">{{ item.viewUser.viewUserCount }}</text>
</view>
<view class="justify-content-item tn-color-gray tn-text-center">
<view class="">
<text class="blogger__count-icon tn-icon-footprint"></text>
<text class="tn-padding-right">{{ item.collectionCount }}</text>
<text class="blogger__count-icon tn-icon-message"></text>
<text class="tn-padding-right">{{ item.commentCount }}</text>
<text class="blogger__count-icon tn-icon-like"></text>
<text class="">{{ item.likeCount }}</text>
</view>
</view>
</view>
</view>
<!-- 边距间隔 -->
<!-- <view class="tn-strip-bottom" v-if="index != content.length - 1"></view> -->
</block>
<!-- 按钮-->
<view class="tn-flex tn-flex-row-between" style="margin: 40rpx 0 60rpx 0;">
<view class="tn-flex-1 justify-content-item tn-margin-xs tn-text-center">
<tn-button backgroundColor="#00FFC6" padding="40rpx 0" width="90%" shadow fontBold>
<text class="tn-icon-like-lack tn-padding-right-xs tn-color-black"></text>
<text class="tn-color-black"> </text>
</tn-button>
</view>
<view class="tn-flex-1 justify-content-item tn-margin-xs tn-text-center">
<tn-button backgroundColor="#FFF00D" padding="40rpx 0" width="90%" shadow fontBold open-type="share">
<text class="tn-icon-share-triangle tn-padding-right-xs tn-color-black"></text>
<text class="tn-color-black"> </text>
</tn-button>
</view>
</view>
</view>
<!-- 评论 -->
<view class="tn-margin" style="padding-bottom: 120rpx;">
<!-- 图标logo/头像 -->
<view class="tn-flex tn-flex-row-between tn-flex-col-center tn-margin-top-xl" @click="tn('/circlePages/blogger_other')">
<view class="justify-content-item">
<view class="tn-flex tn-flex-col-center tn-flex-row-left">
<view class="logo-pic tn-shadow">
<view class="logo-image">
<view class="tn-shadow-blur" style="background-image:url('https://resource.tuniaokj.com/images/blogger/avatar_4.jpeg');width: 60rpx;height: 60rpx;background-size: cover;">
</view>
</view>
</view>
<view class="tn-padding-right tn-padding-left-sm">
<view class="tn-padding-right tn-text-df tn-text-bold tn-color-black">
抓住那只高产母猪
</view>
<view class="tn-padding-right tn-text-ellipsis tn-text-xs tn-color-gray" style="padding-top: 5rpx;">
2024年5月26日
</view>
</view>
</view>
</view>
<view class="justify-content-item tn-flex-row-center tn-flex-col-center tn-color-gray">
<view class="tn-text-center">
<text class="tn-icon-like-lack tn-padding-xs"></text>
</view>
<view class="tn-text-center">
<text class="tn-text-xs">26</text>
</view>
</view>
</view>
<view class="" style="margin: 20rpx 30rpx 30rpx 80rpx;">
求打卡地点
</view>
<view class="tn-bg-gray--light tn-padding-sm" style="margin: 20rpx 30rpx 30rpx 80rpx;border-radius: 10rpx;">
<text class="tn-text-bold tn-padding-right-xs">博主回复: </text>
<text style="line-height: 40rpx;">保密</text>
<view class="tn-flex tn-flex-row-between tn-margin-top-xs">
<view class="justify-content-item tn-text-xs tn-color-gray" style="padding-top: 5rpx;">
2024年5月26日
</view>
<view class="justify-content-item tn-text-xs tn-color-gray">
<text class="tn-padding-xs">16</text>
<text class="tn-icon-like-lack"></text>
</view>
</view>
</view>
<!-- 评论2-->
<!-- 图标logo/头像 -->
<view class="tn-flex tn-flex-row-between tn-flex-col-center tn-margin-top-xl" @click="tn('/circlePages/blogger_other')">
<view class="justify-content-item">
<view class="tn-flex tn-flex-col-center tn-flex-row-left">
<view class="logo-pic tn-shadow">
<view class="logo-image">
<view class="tn-shadow-blur" style="background-image:url('https://resource.tuniaokj.com/images/blogger/avatar_3.jpeg');width: 60rpx;height: 60rpx;background-size: cover;">
</view>
</view>
</view>
<view class="tn-padding-right tn-padding-left-sm">
<view class="tn-padding-right tn-text-df tn-text-bold tn-color-black">
北染陌人
</view>
<view class="tn-padding-right tn-text-ellipsis tn-text-xs tn-color-gray" style="padding-top: 5rpx;">
2024年5月25日
</view>
</view>
</view>
</view>
<view class="justify-content-item tn-flex-row-center tn-flex-col-center tn-color-gray">
<view class="tn-text-center">
<text class="tn-icon-like-lack tn-padding-xs"></text>
</view>
<view class="tn-text-center">
<text class="tn-text-xs">68</text>
</view>
</view>
</view>
<view class="" style="margin: 20rpx 30rpx 30rpx 80rpx;">
求摄影师微信谢谢
</view>
<!-- 评论3-->
<!-- 图标logo/头像 -->
<view class="tn-flex tn-flex-row-between tn-flex-col-center tn-margin-top-xl" @click="tn('/circlePages/blogger_other')">
<view class="justify-content-item">
<view class="tn-flex tn-flex-col-center tn-flex-row-left">
<view class="logo-pic tn-shadow">
<view class="logo-image">
<view class="tn-shadow-blur" style="background-image:url('https://resource.tuniaokj.com/images/blogger/avatar_2.jpeg');width: 60rpx;height: 60rpx;background-size: cover;">
</view>
</view>
</view>
<view class="tn-padding-right tn-padding-left-sm">
<view class="tn-padding-right tn-text-df tn-text-bold tn-color-black">
原来是吖释鸭
</view>
<view class="tn-padding-right tn-text-ellipsis tn-text-xs tn-color-gray">
2024年5月25日
</view>
</view>
</view>
</view>
<view class="justify-content-item tn-flex-row-center tn-flex-col-center tn-color-gray">
<view class="tn-text-center">
<text class="tn-icon-like-lack tn-padding-xs"></text>
</view>
<view class="tn-text-center">
<text class="tn-text-xs">43</text>
</view>
</view>
</view>
<view class="" style="margin: 20rpx 30rpx 30rpx 80rpx;">
吃瓜群众到此一游阿巴阿巴
</view>
</view>
<view class="tabbar footerfixed dd-glass">
<view class="tn-flex tn-flex-row-between tn-flex-col-center">
<view class="justify-content-item tn-margin-top">
<view class="tn-flex tn-flex-row-center tn-flex-col-center">
<!-- <view class="tn-flex tn-flex-row-center tn-padding-right tn-padding-left">
<text class="tn-icon-emoji-good tn-text-xxl"></text>
</view> -->
<view class="tn-flex tn-flex-row-center tn-flex-col-center tn-padding-right tn-padding-left-sm">
<view class="avatar-all">
<view class="tn-shadow-blur" style="background-image:url('https://resource.tuniaokj.com/images/blogger/onepiece-1.jpg');width: 60rpx;height: 60rpx;background-size: cover;">
</view>
</view>
</view>
<view class="topic__info__item__input tn-flex tn-flex-direction-row tn-flex-nowrap tn-flex-col-center tn-flex-row-left">
<view class="topic__info__item__input__left-icon">
<view class="tn-icon-emoji-good"></view>
</view>
<view class="topic__info__item__input__content">
<input maxlength="20" placeholder-class="input-placeholder" :cursor-spacing="18" placeholder="不说点啥子吗?" />
</view>
</view>
</view>
</view>
<view class="justify-content-item tn-flex-row-center tn-flex-col-center tn-margin-top tn-margin-right">
<view class="topic__info__item__sure">
<view class="tn-flex-1 justify-content-item tn-text-center">
<tn-button shape="round" backgroundColor="tn-cool-bg-color-15--reverse" width="100%" shadow>
<text class="tn-color-white" hover-class="tn-hover" :hover-stay-time="150">
</text>
</tn-button>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import template_page_mixin from '@/libs/mixin/template_page_mixin.js'
export default {
name: 'TemplateDetails',
mixins: [template_page_mixin],
data(){
return {
content: [
/* {
userAvatar: 'https://resource.tuniaokj.com/images/blogger/avatar_4.jpeg',
userName: '可我会像',
date: '2024年5月20日',
label: ['追剧达人','重生粉','UI框架'],
desc: '追剧比追人要轻松多了助你开发酷炫UI一臂之力',
content: '基础常用的布局元素,酷炫完善的配色体系,统一可增的图标 icon ,简便调用的功能组件,酷炫的前端页面,吖,编不下去了',
viewUser: {
latestUserAvatar: [
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_1.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_2.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_3.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_4.jpeg'},
],
viewUserCount: 62
},
collectionCount: 439,
commentCount: 46,
likeCount: 83
},
{
userAvatar: 'https://resource.tuniaokj.com/images/blogger/avatar_1.jpeg',
userName: '可我会像',
date: '2024年5月20日',
label: ['追剧达人','重生粉','UI框架'],
desc: '追剧比追人要轻松多了助你开发酷炫UI一臂之力',
content: '基础常用的布局元素,酷炫完善的配色体系,统一可增的图标 icon ,简便调用的功能组件,酷炫的前端页面,吖,编不下去了',
mainImage:[
'https://resource.tuniaokj.com/images/blogger/content_1.jpeg'
],
viewUser: {
latestUserAvatar: [
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_1.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_3.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_2.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_4.jpeg'},
],
viewUserCount: 12
},
collectionCount: 902,
commentCount: 64,
likeCount: 83
},
{
userAvatar: 'https://resource.tuniaokj.com/images/blogger/avatar_2.jpeg',
userName: '可我会像',
date: '2024年5月20日',
label: [],
desc: '',
content: '',
mainImage:[
'https://resource.tuniaokj.com/images/shop/computer2.jpg',
'https://resource.tuniaokj.com/images/shop/prototype2.jpg',
],
viewUser: {
latestUserAvatar: [
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_4.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_2.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_3.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_1.jpeg'},
],
viewUserCount: 56
},
collectionCount: 431,
commentCount: 26,
likeCount: 84
},
{
userAvatar: 'https://resource.tuniaokj.com/images/blogger/avatar_3.jpeg',
userName: '可我会像',
date: '2024年5月20日',
label: ['追剧达人','重生粉'],
desc: '追剧比追人要轻松多了',
content: '基础常用的布局元素,酷炫完善的配色体系,统一可增的图标 icon ,简便调用的功能组件,酷炫的前端页面,吖,编不下去了 基础常用的布局元素,酷炫完善的配色体系,统一可增的图标 icon ,简便调用的功能组件,酷炫的前端页面,吖,编不下去了',
mainImage:[
'https://resource.tuniaokj.com/images/swiper/swiper2.jpg',
'https://resource.tuniaokj.com/images/swiper/swiper3.jpg',
'https://resource.tuniaokj.com/images/swiper/swiper4.jpg',
],
viewUser: {
latestUserAvatar: [
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_1.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_4.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_2.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_3.jpeg'},
],
viewUserCount: 231
},
collectionCount: 780,
commentCount: 89,
likeCount: 82
},
{
userAvatar: 'https://resource.tuniaokj.com/images/blogger/content_1.jpeg',
userName: '可我会像',
date: '2024年5月20日',
label: ['追剧达人','链接'],
desc: 'https://www.yuque.com/tuniao',
mainImage:[
'https://resource.tuniaokj.com/images/shop/watch1.jpg',
'https://resource.tuniaokj.com/images/shop/watch2.jpg',
'https://resource.tuniaokj.com/images/shop/pillow2.jpg',
'https://resource.tuniaokj.com/images/shop/pillow.jpg',
],
viewUser: {
latestUserAvatar: [
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_2.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_1.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_4.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_3.jpeg'},
],
viewUserCount: 28
},
collectionCount: 432,
commentCount: 33,
likeCount: 12
}, */
{
userAvatar: 'https://resource.tuniaokj.com/images/blogger/blogger_beibei.jpg',
userName: '可我会像',
date: '2024年5月20日',
label: ['追剧达人','重生粉'],
desc: '追剧比追人要轻松多了',
mainImage:[
'https://resource.tuniaokj.com/images/blogger/y11.jpg',
'https://resource.tuniaokj.com/images/blogger/y33.jpg',
'https://resource.tuniaokj.com/images/blogger/y22.jpg',
'https://resource.tuniaokj.com/images/blogger/y44.jpg',
'https://resource.tuniaokj.com/images/blogger/y55.jpg',
],
viewUser: {
latestUserAvatar: [
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_4.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_3.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_2.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_1.jpeg'},
],
viewUserCount: 65
},
collectionCount: 265,
commentCount: 22,
likeCount: 102
}
],
}
},
methods: {
//
tn(e) {
uni.navigateTo({
url: e,
});
},
}
}
</script>
<style lang="scss" scoped>
/* 胶囊*/
.tn-custom-nav-bar__back {
width: 100%;
height: 100%;
position: relative;
display: flex;
justify-content: space-evenly;
align-items: center;
box-sizing: border-box;
background-color: rgba(0, 0, 0, 0.15);
border-radius: 1000rpx;
border: 1rpx solid rgba(255, 255, 255, 0.5);
color: #FFFFFF;
font-size: 18px;
.icon {
display: block;
flex: 1;
margin: auto;
text-align: center;
}
&:before {
content: " ";
width: 1rpx;
height: 110%;
position: absolute;
top: 22.5%;
left: 0;
right: 0;
margin: auto;
transform: scale(0.5);
transform-origin: 0 0;
pointer-events: none;
box-sizing: border-box;
opacity: 0.7;
background-color: #FFFFFF;
}
}
/* 文章内容 start*/
.blogger {
&__item {
padding: 30rpx;
}
&__author {
&__btn {
margin-right: -12rpx;
padding: 0 20rpx;
}
}
&__desc {
line-height: 55rpx;
&__label {
padding: 0 20rpx;
margin: 0rpx 18rpx 0 0;
&--prefix {
color: #00FFC8;
padding-right: 10rpx;
}
}
&__content {
}
}
&__content {
margin-top: 18rpx;
padding-right: 18rpx;
&__data {
line-height: 46rpx;
text-align: justify;
overflow: hidden;
transition: all 0.25s ease-in-out;
}
&__status {
margin-top: 10rpx;
font-size: 26rpx;
color: #82B2FF;
}
}
&__main-image {
border-radius: 16rpx;
&--1 {
max-width: 80%;
max-height: 300rpx;
}
&--2 {
max-width: 260rpx;
max-height: 260rpx;
}
&--3 {
height: 212rpx;
width: 100%;
}
}
&__count-icon {
font-size: 40rpx;
padding-right: 5rpx;
}
&__ad {
width: 100%;
height: 500rpx;
transform: translate3d(0px, 0px, 0px) !important;
::v-deep .uni-swiper-slide-frame {
transform: translate3d(0px, 0px, 0px) !important;
}
.uni-swiper-slide-frame {
transform: translate3d(0px, 0px, 0px) !important;
}
&__item {
position: absolute;
width: 100%;
height: 100%;
transform-origin: left center;
transform: translate3d(100%, 0px, 0px) scale(1) !important;
transition: transform 0.25s ease-in-out;
z-index: 1;
&--0 {
transform: translate3d(0%, 0px, 0px) scale(1) !important;
z-index: 4;
}
&--1 {
transform: translate3d(13%, 0px, 0px) scale(0.9) !important;
z-index: 3;
}
&--2 {
transform: translate3d(26%, 0px, 0px) scale(0.8) !important;
z-index: 2;
}
}
&__content {
border-radius: 40rpx;
width: 640rpx;
height: 500rpx;
overflow: hidden;
}
&__image {
width: 100%;
height: 100%;
}
}
}
/* 文章内容 end*/
/* 间隔线 start*/
.tn-strip-bottom {
width: 100%;
border-bottom: 20rpx solid rgba(241, 241, 241, 0.8);
}
/* 间隔线 end*/
/* 头像 start */
.logo-image {
width: 60rpx;
height: 60rpx;
position: relative;
}
.logo-pic {
background-size: cover;
background-repeat: no-repeat;
// background-attachment:fixed;
background-position: top;
box-shadow: 0rpx 0rpx 80rpx 0rpx rgba(0, 0, 0, 0.15);
border-radius: 50%;
overflow: hidden;
// background-color: #FFFFFF;
}
/* 底部 start*/
.footerfixed{
position: fixed;
width: 100%;
bottom: 0;
z-index: 999;
background-color: rgba(255,255,255,0.5);
box-shadow: 0rpx 0rpx 30rpx 0rpx rgba(0, 0, 0, 0.07);
}
.tabbar {
align-items: center;
min-height: 130rpx;
padding: 0;
height: calc(130rpx + env(safe-area-inset-bottom) / 2);
padding-bottom: calc(30rpx + env(safe-area-inset-bottom) / 2);
padding-left: 10rpx;
padding-right: 10rpx;
}
/* 毛玻璃*/
.dd-glass {
width: 100%;
backdrop-filter: blur(20rpx);
-webkit-backdrop-filter: blur(20rpx);
}
/* 头像*/
.avatar-all {
width: 60rpx;
height: 60rpx;
border: 4rpx solid rgba(255,255,255,0.05);
border-radius: 50%;
overflow: hidden;
box-shadow: 0rpx 0rpx 80rpx 0rpx rgba(0, 0, 0, 0.15);
}
/* 内容*/
.topic {
position: relative;
height: 100%;
z-index: 1;
margin-bottom: 120rpx;
/* 表单信息 start */
&__info {
margin: 0 50rpx;
margin-top: 105rpx;
padding: 30rpx 51rpx;
border-radius: 20rpx;
background-color: rgba(255,255,255,1);
border: 2rpx solid rgba(255, 255, 255, 0.1);
box-shadow: 0rpx 10rpx 50rpx 0rpx rgba(0, 3, 72, 0.1);
&__item {
&__input {
width: 400rpx;
height: 60rpx;
border: 1rpx solid #C6D1D8;
border-radius: 39rpx;
&__left-icon {
width: 10%;
font-size: 44rpx;
margin-left: 20rpx;
margin-right: 5rpx;
color: #C6D1D8;
}
&__content {
width: 80%;
padding-left: 10rpx;
&--verify-code {
width: 56%;
}
input {
font-size: 30rpx;
color: #78909C;
// letter-spacing: 0.1em;
}
}
&__right-icon {
width: 10%;
font-size: 34rpx;
margin-right: 20rpx;
color: #78909C;
}
&__right-verify-code {
width: 34%;
margin-right: 20rpx;
}
}
&__button {
width: 100%;
height: 60rpx;
text-align: center;
font-size: 31rpx;
font-weight: bold;
line-height: 77rpx;
// text-indent: 1em;
border-radius: 100rpx;
color: #FFFFFF;
background-color: rgba(255,255,255,0.2);
// border: 2rpx solid #FFFFFF;
}
&__sure {
height: 60rpx;
width: 140rpx;
}
}
}
/* 表单信息 end */
/* 内容 end */
}
/deep/.input-placeholder {
font-size: 30rpx;
color: #C6D1D8;
}
</style>

1042
other/index/index.vue Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,163 @@
<template>
<view>
<u-popup v-model="show" mode="center" @close="close">
<view class="bg">
<view class="title">恭喜您获得</view>
<view class="goods">
<template v-if="result&&result.type==3">
<view class="u-flex u-col-center u-row-center">
<image style="height: 100px;" :src="result.img" mode="heightFix"></image>
</view>
<view class="u-flex u-row-center u-m-t-30">
<view class="type">{{result.name}}</view>
</view>
</template>
<template v-if="result&&result.type==2">
<view class="u-flex color-money u-col-center u-row-center">
<view class="money">{{result.number}}</view>
<view class="font-bold " style="margin-top: 20rpx;font-size: 36rpx;"></view>
</view>
<view class="u-flex u-m-t-24 u-row-center">
<view class="type">现金红包</view>
</view>
</template>
</view>
<view class="u-flex close u-row-center">
<u-icon name="close-circle" :size="54" @click="close" color="#fff"></u-icon>
</view>
</view>
</u-popup>
</view>
</template>
<script>
export default {
data() {
return {
show: false,
result: ''
}
},
methods: {
open(data) {
console.log(data);
this.result = data
this.show = true
},
close() {
console.log('抽奖弹窗关闭');
this.show = false
// if(!this.result){
// return
// }
// const {
// orderId,
// id
// } = this.result
// this.$Request.postJson('app/discSpinning/receive', this.result).then(res => {
// this.result = ''
// console.log(res)
// if (res.code == 0) {
// console.log('');
// const key=res.data==0?'isBindAliPay':undefined
// this.$emit('close',key)
// if(key&&key=='isBindAliPay'){
// uni.navigateTo({
// url:'/me/invite/zhifubao'
// })
// }
// } else {
// }
// })
}
}
}
</script>
<style lang="scss" scoped>
::v-deep .u-mode-center-box {
background-color: transparent;
}
.color-money {
color: #E42F00;
}
.money {
font-weight: 700;
font-size: 96rpx;
letter-spacing: 2px;
}
.bg {
width: 628rpx;
height: 770rpx;
margin-right: 10rpx;
background-color: transparent;
background-repeat: no-repeat;
background-position: center center;
background-size: cover;
background-image: url("~static/images/zhuanpan/gift.png");
position: relative;
@media (-webkit-min-device-pixel-ratio: 2),
(min-device-pixel-ratio: 2) {
background-image: url("~static/images/zhuanpan/gift@2x.png");
}
.title {
position: absolute;
top: 218rpx;
text-align: center;
left: 0;
right: 0;
font-weight: 700;
font-size: 60rpx;
color: #AF6920;
letter-spacing: 4rpx;
}
.goods {
position: absolute;
top: 336rpx;
text-align: center;
left: 0;
right: 0;
text-align: center;
}
.type {
padding: 6rpx 28rpx;
border-radius: 100rpx;
background: #E25B41;
font-size: 28rpx;
color: #fff;
font-weight: bold;
}
.close{
position: absolute;
bottom: 0;
left: 0;
right: 0;
}
.btn-box {
position: absolute;
top: 574rpx;
left: 0;
right: 0;
.btn {
padding: 10rpx 60rpx 10rpx 64rpx;
text-align: center;
font-weight: bold;
font-size: 44rpx;
color: #AF6920;
letter-spacing: 2px;
}
}
}
</style>

View File

@ -1,28 +1,68 @@
<template>
<view class="content">
<u-navbar title="抽奖" back-icon-color="#fff" :background="{background:'transparent'}" immersive :border-bottom="false"
title-color="#fff"></u-navbar>
<u-navbar title="抽奖" back-icon-color="#fff" :background="{background:'transparent'}" immersive
:border-bottom="false" title-color="#fff"></u-navbar>
<view class="sm">
<HM-slotMachine ref="HMslotMachine"></HM-slotMachine>
</view>
<view class="start" @tap="start">
<text> </text>
</view>
<ling-qu ref="refLingqu"></ling-qu>
</view>
</template>
<script>
import lingQu from './components/pop-ling-qu.vue'
function isAllElementssame(array, key) {
for (let index = 0; index < array.length; index++) {
if (array[index][key] != array[0][key]) {
return false
}
}
return true;
}
export default {
components: {
lingQu
},
data() {
return {
prizeList:[
{name:'iPhone13',value:'iPhone',img:require('@/other/static/1.png')},
{name:'airPods3',value:'airPods',img:require('@/other/static/2.png')},
{name:'行李箱',value:'luggage',img:require('@/other/static/3.png')},
{name:'风筒',value:'dryer',img:require('@/other/static/4.png')},
{name:'平行车',value:'balanceCar',img:require('@/other/static/5.png')},
{name:'iPad5',value:'iPad',img:require('@/other/static/6.png')}
prizeList: [{
name: 'iPhone13',
value: 'iPhone',
img: require('@/other/static/1.png')
},
{
name: 'airPods3',
value: 'airPods',
img: require('@/other/static/2.png')
},
{
name: '行李箱',
value: 'luggage',
img: require('@/other/static/3.png')
},
{
name: '风筒',
value: 'dryer',
img: require('@/other/static/4.png')
},
{
name: '平行车',
value: 'balanceCar',
img: require('@/other/static/5.png')
},
{
name: 'iPad5',
value: 'iPad',
img: require('@/other/static/6.png')
}
]
}
},
@ -37,30 +77,45 @@
// duration
// direction up|down
this.$refs.HMslotMachine.init({
prizeList:this.prizeList,
defaultResults:['iPhone','iPhone','iPhone'],
duration:4000,
direction:'up'
prizeList: this.prizeList,
defaultResults: ['iPhone', 'iPhone', 'iPhone'],
duration: 4000,
direction: 'up'
})
},
methods: {
start(){
showLingPop(data) {
this.$refs.refLingqu.open(data)
},
start() {
// roll(options)
//
// results [value,value,value] valuevalue
// success e={results} results
const that = this;
this.$refs.HMslotMachine.roll({
results:this.getResults(),
success:(e)=>{
results: this.getResults(),
success: (e) => {
console.log("success e: ", e);
const item = isAllElementssame(e.results, 'value') ? e.results[0] : undefined
if (item) {
that.showLingPop({...item,type:3})
} else {
uni.showToast({
title: '很遗憾,未中奖',
icon: 'none'
})
}
console.log("success e: ",e);
}
})
},
getResults(){
getResults() {
// ajax
let max = this.prizeList.length-1;
let arr = [Math.floor(Math.random() * (max - 1 + 1) + 1),Math.floor(Math.random() * (max - 1 + 1) + 1),Math.floor(Math.random() * (max - 1 + 1) + 1)];
let max = this.prizeList.length - 1;
let arr = [Math.floor(Math.random() * (max - 1 + 1) + 1), Math.floor(Math.random() * (max - 1 + 1) + 1),
Math.floor(Math.random() * (max - 1 + 1) + 1)
];
return [
this.prizeList[arr[0]].value,
this.prizeList[arr[1]].value,
@ -72,18 +127,21 @@
</script>
<style lang="scss">
page{
background-image: linear-gradient(to top,#8F1E70, #51279A);
min-height: calc(100vh - var(--window-bottom) - var(--window-top));
}
page {
background-image: linear-gradient(to top, #8F1E70, #51279A);
min-height: calc(100vh - var(--window-bottom) - var(--window-top));
}
.content {
display: flex;
flex-direction: column;
align-items: center;
.sm{
.sm {
margin-top: 200rpx;
}
.start{
.start {
width: 70%;
height: 80rpx;
display: flex;
@ -95,7 +153,8 @@ page{
box-shadow: 0 1px 2px rgba($color: #51279A, $alpha: 1);
border-bottom: solid 3px #8d5805;
box-sizing: border-box;
text{
text {
font-size: 20px;
font-weight: bold;
color: #b51c06;
@ -103,6 +162,4 @@ page{
}
}
}
</style>

BIN
other/static/ad.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

BIN
other/static/avatar/5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
other/static/avatar/6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
other/static/avatar/7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

779
other/tools/tools.vue Normal file
View File

@ -0,0 +1,779 @@
<template>
<view class="template-activity tn-safe-area-inset-bottom">
<!-- 顶部自定义导航 -->
<tn-nav-bar fixed alpha customBack>
<view slot="back" class='tn-custom-nav-bar__back' @click="goBack">
<text class='icon tn-icon-left'></text>
<text class='icon tn-icon-home-capsule-fill'></text>
</view>
</tn-nav-bar>
<!-- 方式4 start-->
<!-- <view class="tn-flex">
<view class="tn-flex-1 tn-padding-sm tn-radius" @click="tn('/activityPages/planet')">
<view class="tn-flex tn-flex-direction-column tn-flex-row-center tn-flex-col-center">
<view class="icon4__item--icon tn-flex tn-flex-row-center tn-flex-col-center tn-shadow-blur">
<view class="tn-icon-discover-planet-fill tn-cool-color-icon4 tn-cool-bg-color-5"></view>
</view>
<view class="tn-color-gray--dark tn-text-center">
<text class="tn-text-ellipsis">知识星球</text>
</view>
</view>
</view>
<view class="tn-flex-1 tn-padding-sm tn-radius" @click="tn('/activityPages/project')">
<view class="tn-flex tn-flex-direction-column tn-flex-row-center tn-flex-col-center">
<view class="icon4__item--icon tn-flex tn-flex-row-center tn-flex-col-center tn-shadow-blur">
<view class="tn-icon-trophy-fill tn-cool-color-icon4 tn-cool-bg-color-15"></view>
</view>
<view class="tn-color-gray--dark tn-text-center">
<text class="tn-text-ellipsis">开源项目</text>
</view>
</view>
</view>
<view class="tn-flex-1 tn-padding-sm tn-radius" @click="tn('/activityPages/map')">
<view class="tn-flex tn-flex-direction-column tn-flex-row-center tn-flex-col-center">
<view class="icon4__item--icon tn-flex tn-flex-row-center tn-flex-col-center tn-shadow-blur">
<view class="tn-icon-map-fill tn-cool-color-icon4 tn-cool-bg-color-8"></view>
</view>
<view class="tn-color-gray--dark tn-text-center">
<text class="tn-text-ellipsis">地图打卡</text>
</view>
</view>
</view>
<view class="tn-flex-1 tn-padding-sm tn-radius" @click="tn('/activityPages/study')">
<view class="tn-flex tn-flex-direction-column tn-flex-row-center tn-flex-col-center">
<view class="icon4__item--icon tn-flex tn-flex-row-center tn-flex-col-center tn-shadow-blur">
<view class="tn-icon-creative-fill tn-cool-color-icon4 tn-cool-bg-color-3"></view>
</view>
<view class="tn-color-gray--dark tn-text-center">
<text class="tn-text-ellipsis">课程学习</text>
</view>
</view>
</view>
</view> -->
<!-- 方式4 end-->
<view class="tn-margin-top-lg">
<view class="nav_title--wrap">
<view class="nav_title tn-cool-bg-color-15">
<text class="tn-icon-star tn-padding-right-sm"></text>
/ / /
<text class="tn-icon-star tn-padding-left-sm"></text>
</view>
</view>
</view>
<view class='nav-list tn-margin-bottom tn-margin-top'>
<block v-for="(item, index) in list1" :key="index">
<view class="nav-list-item tn-shadow-blur tn-cool-bg-image"
:class="['tn-main-gradient-' + item.color + '--light']">
<view class="nav-link">
<view class='title tn-text-bold' style="color: #080808;">{{ item.title }}</view>
<view class='join tn-color-grey tn-text-sm'>{{ item.join }} 人参与</view>
</view>
<view class="icon tn-shadow-blur" :class="['tn-bg-' + item.color]">
<view class="" :class="['tn-icon-' + item.icon]"></view>
</view>
</view>
</block>
</view>
<view class="tn-margin-top-lg">
<view class="nav_title--wrap">
<view class="nav_title tn-cool-bg-color-15">
<text class="tn-icon-star tn-padding-right-sm"></text>
/ / /
<text class="tn-icon-star tn-padding-left-sm"></text>
</view>
</view>
</view>
<view class='nav-list tn-margin-bottom tn-margin-top'>
<navigator class="nav-list-item tn-shadow-blur tn-cool-bg-image"
:class="['tn-main-gradient-' + item.color + '--light']" target="miniProgram" :app-id="item.appid"
:path="item.path" version="release" delta="1" hover-class="none" v-for="(item, index) in linksData"
:key="index">
<view class="nav-link">
<view class='title tn-text-bold' style="color: #080808;">{{ item.title }}</view>
<view class='join tn-color-grey tn-text-sm'>{{ item.join }} 人查看</view>
</view>
<view class="icon tn-shadow-blur" :class="['tn-bg-' + item.color]">
<view class="" :class="['tn-icon-' + item.icon]"></view>
</view>
</navigator>
</view>
<view class='tn-tabbar-height'></view>
</view>
</template>
<script>
export default {
name: 'Discovery',
data() {
return {
cardCur: 0,
swiperList: [{
id: 0,
type: 'image',
url: 'https://resource.tuniaokj.com/images/index_bg/pro1.jpg',
}, {
id: 1,
type: 'image',
url: 'https://resource.tuniaokj.com/images/index_bg/pro2.jpg',
}, {
id: 2,
type: 'image',
url: 'https://resource.tuniaokj.com/images/index_bg/pro3.jpg',
}, {
id: 3,
type: 'image',
url: 'https://resource.tuniaokj.com/images/index_bg/pro4.jpg',
}, {
id: 4,
type: 'image',
url: 'https://resource.tuniaokj.com/images/index_bg/pro5.jpg',
}, {
id: 5,
type: 'image',
url: 'https://resource.tuniaokj.com/images/index_bg/pro6.jpg',
}],
list1: [{
icon: 'honor-fill',
title: '称呼计算器',
join: '629',
color: 'blue'
},
{
icon: 'count-fill',
title: '支付宝语音生成',
join: '268',
color: 'purplered'
},
{
icon: 'gloves-fill',
title: '一周天气预报',
join: '332',
color: 'cyan'
},
{
icon: 'trusty-fill',
title: '今日星座运势',
join: '106',
color: 'orangeyellow'
},
{
icon: 'hardware-fill',
title: '来碗毒鸡汤',
join: '98',
color: 'indigo'
},
{
icon: 'baby-fill',
title: '垃圾分一分',
join: '57',
color: 'red'
},
{
icon: 'safe-fill',
title: '手持弹幕',
join: '76',
color: 'green'
},
{
icon: 'flag-fill',
title: '孩子取名',
join: '225',
color: 'orange'
},
{
icon: 'topics-fill',
title: '午餐吃什么',
join: '422',
color: 'teal'
},
{
icon: 'light-fill',
title: '朋友圈文案',
join: '983',
color: 'orangered'
}
],
linksData: [{
icon: 'honor-fill',
join: '629',
color: 'purplered',
url: '',
title: '司命',
appid: 'wx734d93edc5b48019',
path: 'pages/index/index'
},
{
icon: 'count-fill',
join: '268',
color: 'blue',
url: '',
title: '爱小睡眠',
appid: 'wx65880bde88b32037',
path: 'pages/index/index'
},
{
icon: 'gloves-fill',
join: '332',
color: 'orangeyellow',
url: '',
title: '群友作品',
appid: '',
path: 'pages/index/index'
},
{
icon: 'trusty-fill',
join: '106',
color: 'cyan',
url: '',
title: '群友作品',
appid: '',
path: 'pages/index/index'
},
{
icon: 'hardware-fill',
join: '98',
color: 'red',
url: '',
title: '群友作品',
appid: '',
path: 'pages/index/index'
},
{
icon: 'baby-fill',
join: '57',
color: 'indigo',
url: '',
title: '群友作品',
appid: '',
path: 'pages/index/index'
},
{
icon: 'safe-fill',
join: '76',
color: 'orange',
url: '',
title: '群友作品',
appid: '',
path: 'pages/index/index'
},
{
icon: 'flag-fill',
join: '225',
color: 'green',
url: '',
title: '群友作品',
appid: '',
path: 'pages/index/index'
},
{
icon: 'topics-fill',
join: '422',
color: 'orangered',
url: '',
title: '群友作品',
appid: '',
path: 'pages/index/index'
},
{
icon: 'light-fill',
join: '983',
color: 'teal',
url: '',
title: '表情包制作',
appid: 'wx096589e82af2ffa5',
path: 'pages/index/index'
}
],
}
},
methods: {
goBack(){
uni.navigateBack()
},
// cardSwiper
cardSwiper(e) {
this.cardCur = e.detail.current
},
//
tn(e) {
uni.navigateTo({
url: e,
});
},
}
}
</script>
<style lang="scss" scoped>
.template-activity {
max-height: 100vh;
}
.tn-tabbar-height {
min-height: 120rpx;
height: calc(140rpx + env(safe-area-inset-bottom) / 2);
}
/* 胶囊*/
.tn-custom-nav-bar__back {
width: 100%;
height: 100%;
position: relative;
display: flex;
justify-content: space-evenly;
align-items: center;
box-sizing: border-box;
background-color: rgba(0, 0, 0, 0.15);
border-radius: 1000rpx;
border: 1rpx solid rgba(255, 255, 255, 0.5);
color: #FFFFFF;
font-size: 18px;
.icon {
display: block;
flex: 1;
margin: auto;
text-align: center;
}
&:before {
content: " ";
width: 1rpx;
height: 110%;
position: absolute;
top: 22.5%;
left: 0;
right: 0;
margin: auto;
transform: scale(0.5);
transform-origin: 0 0;
pointer-events: none;
box-sizing: border-box;
opacity: 0.7;
background-color: #FFFFFF;
}
}
/* .tnphone-white-min 细边框*/
.tnphone-white-min {
width: 380rpx;
height: 800rpx;
border-radius: 40rpx;
background: #E9E5F3;
padding: 7rpx;
display: table;
color: #333;
box-sizing: border-box;
box-shadow: 0rpx 10rpx 50rpx 0rpx rgba(0, 0, 0, 0.15);
margin: 70rpx auto;
cursor: default;
position: relative
}
.tnphone-white-min .skin {
width: 100%;
height: 100%;
border-radius: 40rpx;
background: #E9E5F3;
padding: 10rpx;
}
.tnphone-white-min .screen {
width: 100%;
height: 100%;
border-radius: 30rpx;
background: #E9E5F3;
position: relative;
overflow: hidden
}
.tnphone-white-min .head {
width: 100%;
height: 90rpx;
text-align: center;
position: absolute;
padding: 45rpx 15rpx 10rpx 15rpx;
}
.tnphone-white-min .peak {
left: 22%;
width: 56%;
height: 27rpx;
margin: -2rpx auto 0rpx;
border-radius: 0 0 20rpx 20rpx;
background: #E9E5F3;
position: absolute
}
.tnphone-white-min .sound {
width: 48rpx;
height: 6rpx;
border-radius: 15rpx;
background: #555;
position: absolute;
left: 50%;
top: 50%;
margin-left: -24rpx;
margin-top: -10rpx;
box-shadow: 0rpx 4rpx 4rpx 0rpx #444 inset
}
.tnphone-white-min .lens {
width: 6rpx;
height: 6rpx;
border-radius: 50%;
background: #2c5487;
position: absolute;
left: 50%;
top: 50%;
margin-left: 34rpx;
margin-top: -10rpx
}
.tnphone-white-min .talk {
width: 50%;
height: 6rpx;
border-radius: 15rpx;
background: rgba(0, 0, 0, .3);
position: absolute;
bottom: 8rpx;
left: 50%;
margin-left: -25%
}
.tnphone-white-min .area-l,
.tnphone-white-min .area-r {
width: 70rpx;
height: 16rpx;
position: absolute;
top: 6rpx
}
.tnphone-white-min .area-l {
left: 0;
text-align: center;
font-size: 12rpx;
line-height: 22rpx;
text-indent: 10rpx;
font-weight: 600;
padding-left: 20rpx;
}
.tnphone-white-min .area-r {
right: 0;
text-align: center;
font-size: 12rpx;
line-height: 22rpx;
text-indent: 10rpx;
font-weight: 600;
padding-right: 20rpx;
}
.tnphone-white-min .fa-feed {
float: left;
font-size: 12rpx !important;
transform: rotate(-45deg);
margin-top: 4rpx;
margin-right: 8rpx
}
.tnphone-white-min .fa-battery-full {
float: left;
font-size: 12rpx !important;
margin-top: 6rpx
}
.tnphone-white-min .fa-chevron-left {
float: left;
margin-top: 4rpx
}
.tnphone-white-min .fa-cog {
float: right;
margin-top: 4rpx
}
.tnphone-white-min .btn01 {
width: 3rpx;
height: 28rpx;
border-radius: 3rpx 0 0 3rpx;
background: #222;
position: absolute;
top: 105rpx;
left: -3rpx
}
.tnphone-white-min .btn02 {
width: 3rpx;
height: 54rpx;
border-radius: 3rpx 0 0 3rpx;
background: #222;
position: absolute;
top: 160rpx;
left: -3rpx
}
.tnphone-white-min .btn03 {
width: 3rpx;
height: 54rpx;
border-radius: 3rpx 0 0 3rpx;
background: #222;
position: absolute;
top: 230rpx;
left: -3rpx
}
.tnphone-white-min .btn04 {
width: 3rpx;
height: 86rpx;
border-radius: 0 3rpx 3rpx 0;
background: #222;
position: absolute;
top: 180rpx;
right: -3rpx
}
/* 顶部背景图 start */
.top-backgroup {
height: 450rpx;
z-index: -1;
.backgroud-image {
width: 100%;
height: 667rpx;
// z-index: -1;
}
}
/* 顶部背景图 end */
/* 轮播样机样式 start*/
.card-swiper {
height: 830rpx !important;
}
.card-swiper swiper-item {
width: 260rpx !important;
// left: 170rpx;
// width: 380rpx !important;
// left: 185rpx;
box-sizing: border-box;
padding: 0rpx 15rpx 90rpx 15rpx;
overflow: initial;
}
.card-swiper swiper-item .swiper-item {
display: block;
transform: scale(0.45);
transition: all 0.2s ease-in 0s;
overflow: hidden;
}
.card-swiper swiper-item.cur .swiper-item {
transform: scale(0.65);
transition: all 0.2s ease-in 0s;
}
.image-banner {
display: flex;
align-items: center;
justify-content: center;
}
.image-banner image {
width: 100%;
height: 770rpx;
// border: 1rpx solid red;
}
/* 轮播指示点 start*/
.indication {
z-index: 9999;
width: 100%;
height: 36rpx;
position: absolute;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.spot {
background-color: #000;
opacity: 0;
width: 10rpx;
height: 10rpx;
border-radius: 20rpx;
margin: 0 8rpx !important;
top: -80rpx;
position: relative;
}
.spot.active {
opacity: 0;
width: 30rpx;
background-color: #000;
}
/* 图标容器4 start */
.tn-cool-color-icon4 {
// background-image: -webkit-linear-gradient(135deg, #ED1C24, #FECE12); 16
// background-image: linear-gradient(135deg, #ED1C24, #FECE12);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
text-fill-color: transparent;
}
.icon4 {
&__item {
width: 30%;
background-color: #FFFFFF;
border-radius: 10rpx;
padding: 30rpx;
margin: 20rpx 10rpx;
transform: scale(1);
transition: transform 0.3s linear;
transform-origin: center center;
&--icon {
width: 110rpx;
height: 110rpx;
font-size: 55rpx;
border-radius: 50%;
margin-bottom: 18rpx;
position: relative;
z-index: 1;
box-shadow: 0px 10px 30px rgba(70, 23, 129, 0.12),
0px -8px 40px rgba(255, 255, 255, 1),
inset 0px -10px 10px rgba(70, 23, 129, 0.05),
inset 0px 10px 20px rgba(255, 255, 255, 1);
}
}
}
/* 标题 start */
.nav_title {
-webkit-background-clip: text;
color: transparent;
&--wrap {
position: relative;
display: flex;
height: 120rpx;
font-size: 42rpx;
align-items: center;
justify-content: center;
font-weight: bold;
background-image: url(https://resource.tuniaokj.com/images/title_bg/title44.png);
background-size: cover;
}
}
/* 标题 end */
/* 组件导航列表 start*/
.nav-list {
display: flex;
flex-wrap: wrap;
padding: 0rpx 12rpx 0rpx;
justify-content: space-between;
/* 列表元素 start */
.nav-list-item {
padding: 95rpx 30rpx 5rpx 30rpx;
border-radius: 12rpx;
width: 45%;
margin: 0 18rpx 40rpx;
background-size: cover;
background-position: center;
position: relative;
z-index: 99;
/* 元素标题 start */
.nav-link {
font-size: 32rpx;
text-transform: capitalize;
padding: 0 0 10rpx 0;
position: relative;
.title {
color: #FFFFFF;
margin-top: 100rpx;
text-align: center;
}
.join {
color: #FFFFFF;
margin-top: 20rpx;
margin-bottom: 40rpx;
text-align: center;
}
}
/* 元素标题 end */
/* 元素图标 start */
.icon {
font-variant: small-caps;
position: absolute;
top: 60rpx;
right: 50rpx;
left: 37%;
width: 90rpx;
height: 90rpx;
line-height: 90rpx;
margin: 0;
padding: 0;
display: inline-flex;
text-align: center;
justify-content: center;
vertical-align: middle;
font-size: 50rpx;
color: #FFFFFF;
white-space: nowrap;
opacity: 0.9;
background-color: rgba(0, 0, 0, 0.05);
background-size: cover;
background-position: 50%;
border-radius: 5000rpx;
&::after {
content: " ";
position: absolute;
z-index: -1;
width: 100%;
height: 100%;
left: 0;
bottom: 0;
border-radius: inherit;
opacity: 1;
transform: scale(1, 1);
background-size: 100% 100%;
background-image: url(https://resource.tuniaokj.com/images/cool_bg_image/icon_bg2.png);
}
}
/* 元素图标 end */
}
/* 列表元素 end */
}
/* 组件导航列表 end*/
</style>

570
other/topic/reserve.vue Normal file
View File

@ -0,0 +1,570 @@
<template>
<view class="template-reserve tn-safe-area-inset-bottom">
<!-- 顶部自定义导航 -->
<tn-nav-bar fixed alpha customBack>
<view slot="back" class='tn-custom-nav-bar__back'
@click="goBack">
<text class='icon tn-icon-left'></text>
<text class='icon tn-icon-home-capsule-fill'></text>
</view>
</tn-nav-bar>
<!-- 页面内容 -->
<view class="slideshow">
<view class="slideshow-image" style="background-image: url('https://resource.tuniaokj.com/images/blogger/avatar_1.jpeg')"></view>
<view class="slideshow-image" style="background-image: url('https://resource.tuniaokj.com/images/blogger/avatar_2.jpeg')"></view>
<view class="slideshow-image" style="background-image: url('https://resource.tuniaokj.com/images/blogger/avatar_3.jpeg')"></view>
<view class="slideshow-image" style="background-image: url('https://resource.tuniaokj.com/images/blogger/avatar_4.jpeg')"></view>
</view>
<view class="reserve tn-safe-area-inset-bottom" :style="{paddingTop: vuex_custom_bar_height + 'px'}">
<view class="reserve-content tn-padding tn-color-black tn-text-lg dd-glass2" style="margin:71vh 30rpx 20vh 30rpx">
<view class="tn-text-center tn-text-bold tn-padding-top tn-padding-bottom">
活动详情
</view>
<view class="">
<view class="blogger__desc tn-margin-top-sm tn-margin-bottom-sm tn-text-justify tn-flex-col-center tn-flex-row-left">
<view class="blogger__desc__label tn-float-left tn-margin-right tn-bg-gray--light tn-round tn-text-sm tn-text-bold">
<text class="blogger__desc__label--prefix">#</text>
<text class="tn-text-df">常回家看看</text>
</view>
<!-- 不用限制长度了因为发布的时候限制长度了-->
<text class="blogger__desc__content tn-flex-1 tn-text-justify tn-text-df">
可爱的校友让我们相约华软来一场说看就看的木棉雨通知将于活动前一晚送达请查收
</text>
</view>
</view>
<view class="tn-padding-top-lg">
集合时间2024年12月20日 12:00:00
</view>
<view class="">
集合地点喷泉广场
</view>
<!-- <view class="tn-margin-top tn-color-black tn-bg-white tn-radius" >
<view class="tn-flex tn-flex-row-between tn-flex-col-center">
<view class="justify-content-item">
<view class="tn-flex tn-flex-col-center tn-flex-row-left">
<view class="tn-padding tn-color-black">
<view class="tn-padding-right-sm tn-text-lg tn-text-bold clamp-text-1">
广东省广州市番禺祈福新村129号
</view>
</view>
</view>
</view>
<view class="tn-flex justify-content-item tn-flex-row-center tn-padding-right-xs">
<view class="tn-bg-gray--light tn-padding-xs tn-margin-sm tn-color-black tn-round">
<text class="tn-icon-location-fill" style="font-size: 50rpx;"></text>
</view>
</view>
</view>
</view> -->
<view class="tn-text-center tn-text-bold tn-padding-top-xl">
活动参与者
</view>
<tn-read-more :closeBtn="true" openText="查看更多参与者" openIcon="more-circle" closeText="折叠起来" closeIcon="close" :showHeight="300">
<view class="tn-flex tn-flex-wrap tn-margin-top-xl">
<block v-for="(item, index) in groupList" :key="index">
<view class="tn-padding-bottom tn-padding-left" style="width: 20%;">
<view class="tn-flex tn-flex-row-left tn-flex-col-center" style="">
<view class="user-pic">
<view class="user-image">
<view class="tn-shadow-blur" :style="'background-image:url('+ item.src +');width: 70rpx;height: 70rpx;background-size: cover;'">
</view>
</view>
</view>
</view>
</view>
</block>
</view>
</tn-read-more>
</view>
<view class="tn-footerfixed tn-flex tn-flex-row-between tn-flex-col-center tn-padding tn-safe-area-inset-bottom dd-glass">
<view class="justify-content-item tn-padding-bottom">
<view class="tn-flex tn-flex-col-center tn-flex-row-left">
<view class="user-pic">
<view class="user-image">
<view class="tn-shadow-blur" style="background-image:url('https://resource.tuniaokj.com/images/blogger/blogger_beibei.jpg');width: 70rpx;height: 70rpx;background-size: cover;">
</view>
</view>
</view>
</view>
</view>
<!-- 按钮-->
<view class="justify-content-item tn-flex-col-center tn-flex-row-center tn-text-center tn-padding-bottom">
<view class="tn-flex tn-flex-row-between">
<view class="justify-content-item tn-margin-xs tn-text-center" style="width: 300rpx;">
<tn-button shape="round" backgroundColor="#00FFC6" padding="40rpx 0" width="90%" shadow fontBold>
<text class="tn-icon-topic tn-padding-right-xs tn-color-black"></text>
<text class="tn-color-black">参与活动</text>
</tn-button>
</view>
</view>
</view>
<view class="justify-content-item tn-flex-col-center tn-flex-row-center tn-text-center tn-padding-bottom" @click="openLocation">
<view class="tn-bg-gray--light tn-padding-xs tn-margin-sm tn-color-black tn-round">
<text class="tn-icon-location-fill" style="font-size: 50rpx;"></text>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import template_page_mixin from '@/libs/mixin/template_page_mixin.js'
export default {
name: 'TemplateReserve',
mixins: [template_page_mixin],
data(){
return {
groupList: [
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_1.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_2.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_1.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_4.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_3.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/blogger_beibei.jpg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_4.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_1.jpeg'},
{src: 'https://resource.tuniaokj.com/images/shop/phonecase1.jpg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_3.jpeg'},
{src: 'https://resource.tuniaokj.com/images/shop/cup1.jpg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_4.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_3.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/blogger_beibei.jpg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_1.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_2.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_3.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_4.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/blogger_beibei.jpg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_1.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_2.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_3.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_4.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_3.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/blogger_beibei.jpg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_1.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_2.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_3.jpeg'},
{src: 'https://resource.tuniaokj.com/images/shop/watch1.jpg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_4.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_2.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/blogger_beibei.jpg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_1.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_2.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_3.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_4.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/blogger_beibei.jpg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_2.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/blogger_beibei.jpg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_4.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/blogger_beibei.jpg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_1.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_2.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_4.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/blogger_beibei.jpg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_1.jpeg'},
{src: 'https://resource.tuniaokj.com/images/blogger/avatar_3.jpeg'},
]
}
},
methods: {
openLocation() {
uni.openLocation({
longitude: 113.3298396012573,
latitude: 22.961803525530176,
name: '祈福新村',
address: '祈福新村'
})
},
}
}
</script>
<style lang="scss" scoped>
.template-reserve {
}
/* 胶囊*/
.tn-custom-nav-bar__back {
width: 100%;
height: 100%;
position: relative;
display: flex;
justify-content: space-evenly;
align-items: center;
box-sizing: border-box;
background-color: rgba(0, 0, 0, 0.15);
border-radius: 1000rpx;
border: 1rpx solid rgba(255, 255, 255, 0.5);
color: #FFFFFF;
font-size: 18px;
.icon {
display: block;
flex: 1;
margin: auto;
text-align: center;
}
&:before {
content: " ";
width: 1rpx;
height: 110%;
position: absolute;
top: 22.5%;
left: 0;
right: 0;
margin: auto;
transform: scale(0.5);
transform-origin: 0 0;
pointer-events: none;
box-sizing: border-box;
opacity: 0.7;
background-color: #FFFFFF;
}
}
/* 内容*/
.reserve{
position: relative;
z-index: 99;
}
.reserve-content{
background-color: rgba(255,255,255,0.7);
border-radius: 30rpx;
}
/* 标签 */
.blogger {
&__item {
padding: 30rpx;
}
&__desc {
line-height: 55rpx;
&__label {
padding: 0 20rpx;
margin: 0rpx 18rpx 0 0;
&--prefix {
color: #00FFC8;
padding-right: 10rpx;
}
}
}
}
/* 文字截取*/
.clamp-text-1 {
-webkit-line-clamp: 1;
display: -webkit-box;
-webkit-box-orient: vertical;
text-overflow: ellipsis;
overflow: hidden;
}
.clamp-text-2 {
-webkit-line-clamp: 2;
display: -webkit-box;
-webkit-box-orient: vertical;
text-overflow: ellipsis;
overflow: hidden;
}
/* 毛玻璃*/
.dd-glass {
width: 100%;
backdrop-filter: blur(20rpx);
-webkit-backdrop-filter: blur(20rpx);
}
/* 毛玻璃*/
.dd-glass2 {
// width: 100%;
backdrop-filter: blur(8rpx);
-webkit-backdrop-filter: blur(8rpx);
}
/* 用户头像 start */
.user-image {
width: 70rpx;
height: 70rpx;
position: relative;
}
.user-pic {
background-size: cover;
background-repeat: no-repeat;
// background-attachment:fixed;
background-position: top;
border-radius: 100rpx;
overflow: hidden;
background-color: #FFFFFF;
}
/* 底部悬浮按钮 start*/
.tn-tabbar-height {
min-height: 120rpx;
height: calc(140rpx + env(safe-area-inset-bottom) / 2);
}
.tn-footerfixed {
position: fixed;
background-color: rgba(255,255,255,0.5);
box-shadow: 0rpx 0rpx 30rpx 0rpx rgba(0, 0, 0, 0.07);
bottom: 0;
width: 100%;
transition: all 0.25s ease-out;
z-index: 100;
}
/* 相册 start*/
.slideshow {
top: 0;
position: fixed;
width: 100vw;
height: 100vh;
overflow: hidden;
z-index: 0;
}
.slideshow-image {
position: absolute;
width: 100%;
height: 100%;
background: no-repeat 50% 50%;
background-size: cover;
-webkit-animation-name: kenburns;
animation-name: kenburns;
-webkit-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
animation-iteration-count: infinite;
-webkit-animation-duration: 16s;
animation-duration: 16s;
opacity: 1;
transform: scale(1.2);
}
.slideshow-image:nth-child(1) {
-webkit-animation-name: kenburns-1;
animation-name: kenburns-1;
z-index: 3;
}
.slideshow-image:nth-child(2) {
-webkit-animation-name: kenburns-2;
animation-name: kenburns-2;
z-index: 2;
}
.slideshow-image:nth-child(3) {
-webkit-animation-name: kenburns-3;
animation-name: kenburns-3;
z-index: 1;
}
.slideshow-image:nth-child(4) {
-webkit-animation-name: kenburns-4;
animation-name: kenburns-4;
z-index: 0;
}
@-webkit-keyframes kenburns-1 {
0% {
opacity: 1;
transform: scale(1.2);
}
1.5625% {
opacity: 1;
}
23.4375% {
opacity: 1;
}
26.5625% {
opacity: 0;
transform: scale(1);
}
100% {
opacity: 0;
transform: scale(1.2);
}
98.4375% {
opacity: 0;
transform: scale(1.2117647059);
}
100% {
opacity: 1;
}
}
@keyframes kenburns-1 {
0% {
opacity: 1;
transform: scale(1.2);
}
1.5625% {
opacity: 1;
}
23.4375% {
opacity: 1;
}
26.5625% {
opacity: 0;
transform: scale(1);
}
100% {
opacity: 0;
transform: scale(1.2);
}
98.4375% {
opacity: 0;
transform: scale(1.2117647059);
}
100% {
opacity: 1;
}
}
@-webkit-keyframes kenburns-2 {
23.4375% {
opacity: 1;
transform: scale(1.2);
}
26.5625% {
opacity: 1;
}
48.4375% {
opacity: 1;
}
51.5625% {
opacity: 0;
transform: scale(1);
}
100% {
opacity: 0;
transform: scale(1.2);
}
}
@keyframes kenburns-2 {
23.4375% {
opacity: 1;
transform: scale(1.2);
}
26.5625% {
opacity: 1;
}
48.4375% {
opacity: 1;
}
51.5625% {
opacity: 0;
transform: scale(1);
}
100% {
opacity: 0;
transform: scale(1.2);
}
}
@-webkit-keyframes kenburns-3 {
48.4375% {
opacity: 1;
transform: scale(1.2);
}
51.5625% {
opacity: 1;
}
73.4375% {
opacity: 1;
}
76.5625% {
opacity: 0;
transform: scale(1);
}
100% {
opacity: 0;
transform: scale(1.2);
}
}
@keyframes kenburns-3 {
48.4375% {
opacity: 1;
transform: scale(1.2);
}
51.5625% {
opacity: 1;
}
73.4375% {
opacity: 1;
}
76.5625% {
opacity: 0;
transform: scale(1);
}
100% {
opacity: 0;
transform: scale(1.2);
}
}
@-webkit-keyframes kenburns-4 {
73.4375% {
opacity: 1;
transform: scale(1.2);
}
76.5625% {
opacity: 1;
}
98.4375% {
opacity: 1;
}
100% {
opacity: 0;
transform: scale(1);
}
}
@keyframes kenburns-4 {
73.4375% {
opacity: 1;
transform: scale(1.2);
}
76.5625% {
opacity: 1;
}
98.4375% {
opacity: 1;
}
100% {
opacity: 0;
transform: scale(1);
}
}
/* 相册 end*/
</style>

1287
other/topic/topic.vue Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,7 @@
{
"easycom": {
"^u-(.*)": "@/uview-ui/components/u-$1/u-$1.vue"
"^u-(.*)": "@/uview-ui/components/u-$1/u-$1.vue",
"^tn-(.*)": "@/tuniao-ui/components/tn-$1/tn-$1.vue"
},
"pages": [ //pageshttps://uniapp.dcloud.io/collocation/pages
{
@ -688,7 +689,13 @@
},
{
"root": "other",
"pages": [
"pages": [{
"path": "index/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "更多"
}
},
{
"path": "about/about",
"style": {
@ -696,34 +703,69 @@
}
},
{
"path" : "coup/coup",
"style" :
{
"navigationBarTitleText" : "卡包"
"path": "coup/coup",
"style": {
"navigationBarTitleText": "卡包"
}
},
{
"path" : "address/address",
"style" :
{
"navigationBarTitleText" : "收货地址"
"path": "address/address",
"style": {
"navigationBarTitleText": "收货地址"
}
},
{
"path" : "pay/pay",
"style" :
{
"navigationBarTitleText" : "发红包"
"path": "pay/pay",
"style": {
"navigationBarTitleText": "发红包"
}
},
{
"path" : "slotMachine/slotMachine",
"style" :
{
"navigationBarTitleText" : "抽奖",
"path": "slotMachine/slotMachine",
"style": {
"navigationBarTitleText": "抽奖",
"navigationStyle": "custom"
}
},
{
"path" : "blogger/blogger",
"style" :
{
"navigationBarTitleText" : "",
"navigationStyle": "custom"
}
},
{
"path" : "blogger/details",
"style" :
{
"navigationBarTitleText" : "",
"navigationStyle": "custom"
}
},
{
"path" : "topic/topic",
"style" :
{
"navigationBarTitleText" : "话题"
}
},
{
"path" : "topic/reserve",
"style" :
{
"navigationBarTitleText" : "",
"navigationStyle": "custom"
}
},
{
"path" : "tools/tools",
"style" :
{
"navigationBarTitleText" : "",
"navigationStyle": "custom"
}
}
]

View File

@ -252,15 +252,20 @@
<!-- 抖音im客服 -->
<ttMsg />
<other-xuanfu></other-xuanfu>
</view>
</template>
<script>
import ttMsg from '../../components/ttMsg/ttMsg.vue'
import httpsRequest from '../../common/httpRequest.js'
import otherXuanfu from '@/components/other-xuafu.vue'
export default {
components: {
ttMsg
ttMsg,otherXuanfu
},
data() {
return {

28
store/$t.mixin.js Normal file
View File

@ -0,0 +1,28 @@
import { mapState } from 'vuex'
import store from '@/store/index.js'
// 尝试将用户在根目录中的store/index.js的vuex的state变量加载到全局变量中
let $tStoreKey = []
try {
$tStoreKey = store.state ? Object.keys(store.state) : []
} catch(e) {
}
module.exports = {
beforeCreate() {
// 将vuex方法挂在在$t中
// 使用方法:
// 修改vuex的state中的user.name变量为图鸟小菜 => this.$t.vuex('user.name', '图鸟小菜')
// 修改vuexde state中的version变量为1.0.1 => this.$t.vuex('version', 1.0.1)
this.$t.vuex = (name, value) => {
this.$store.commit('$tStore', {
name, value
})
}
},
computed: {
// 将vuex的state中的变量结构到全局混入mixin中
...mapState($tStoreKey)
}
}

75
store/index.js Normal file
View File

@ -0,0 +1,75 @@
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
let lifeData = {}
// 尝试获取本地是否存在lifeData变量第一次启动时不存在
try {
lifeData = uni.getStorageSync('lifeData')
} catch(e) {
}
// 标记需要永久存储的变量在每次启动时取出在state中的变量名
let saveStateKeys = ['vuex_user']
// 保存变量到本地存储
const saveLifeData = function(key, value) {
// 判断变量是否在存储数组中
if (saveStateKeys.indexOf(key) != -1) {
// 获取本地存储的lifeData对象将变量添加到对象中
let tmpLifeData = uni.getStorageSync('lifeData')
// 第一次启动时不存在,则放一个空对象
tmpLifeData = tmpLifeData ? tmpLifeData : {},
tmpLifeData[key] = value
// 将变量再次放回本地存储中
uni.setStorageSync('lifeData', tmpLifeData)
}
}
const store = new Vuex.Store({
state: {
// 如果上面从本地获取的lifeData对象下有对应的属性就赋值给state中对应的变量
// 加上vuex_前缀是防止变量名冲突也让人一目了然
vuex_user: lifeData.vuex_user ? lifeData.vuex_user : {name: '图鸟'},
// 如果vuex_version无需保存到本地永久存储无需lifeData.vuex_version方式
// app版本
vuex_version: "1.0.0",
// 是否使用自定义导航栏
vuex_custom_nav_bar: true,
// 状态栏高度
vuex_status_bar_height: 0,
// 自定义导航栏的高度
vuex_custom_bar_height: 0
},
mutations: {
$tStore(state, payload) {
// 判断是否多层调用state中为对象存在的情况例如user.info.score = 1
let nameArr = payload.name.split('.')
let saveKey = ''
let len = nameArr.length
if (len >= 2) {
let obj = state[nameArr[0]]
for (let i= 1; i < len - 1; i++) {
obj = obj[nameArr[i]]
}
obj[nameArr[len - 1]] = payload.value
saveKey = nameArr[0]
} else {
// 单层级变量
state[payload.name] = payload.value
saveKey = payload.name
}
// 保存变量到本地中
saveLifeData(saveKey, state[saveKey])
}
},
actions: {
}
})
export default store

4
tuniao-ui/README.md Normal file
View File

@ -0,0 +1,4 @@
TuniaoUi for uniApp v1.0.0 | by 图鸟 2021-09-01
仅供开发,如作它用所承受的法律责任一概与作者无关
*使用TuniaoUi开发扩展与插件时请注明基于tuniao字眼

View File

@ -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() {
// inputpropsvalue
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>

View File

@ -0,0 +1,104 @@
<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(index)]">
<tn-avatar
:src="item.src || ''"
:text="item.text || ''"
:icon="item.icon || ''"
:size="size"
:shape="shape"
:imgMode="imgMode"
:border="true"
backgroundColor="rgba(255, 255, 255, 0.4)"
: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() {
return (index) => {
let style = {}
if (this._checkSizeIsInline()) {
switch(this.size) {
case 'sm':
style.marginLeft = index != 0 ? `${-48 * this.gap}rpx` : ''
break
case 'lg':
style.marginLeft = index != 0 ? `${-96 * this.gap}rpx` : ''
break
case 'xl':
style.marginLeft = index != 0 ? `${-128 * this.gap}rpx` : ''
break
}
} else {
const size = Number(this.size.replace(/(px|rpx)/g, '')) || 64
style.marginLeft = index != 0 ? `-${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>

View File

@ -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: 0
},
//
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>

View File

@ -0,0 +1,173 @@
<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: ''
},
//
dot: {
type: Boolean,
default: false
},
// 使
absolute: {
type: Boolean,
default: false
},
// top
top: {
type: [String, Number],
default: ''
},
// right
right: {
type: [String, Number],
default: ''
},
//
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.lineHeight = 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;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
font-size: 20rpx;
background-color: #FFFFFF;
// color: #FFFFFF;
border-radius: 100rpx;
padding: 4rpx 8rpx;
line-height: initial;
&--dot {
width: 8rpx;
height: 8rpx;
border-radius: 50%;
padding: 0;
}
&--absolute {
position: absolute;
top: 0;
right: 0;
}
&--center-position {
transform: translate(50%, -50%);
}
}
</style>

View File

@ -0,0 +1,302 @@
<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'
},
// cssmargin
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 !== '' && this.backgroundColorClass.indexOf('tn-bg') != -1) {
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 || '#080808'
} else {
style.backgroundColor = this.backgroundColorStyle || '#FFFFFF'
}
}
//
if (this.shadow && !this.backgroundColorClass) {
if (this.backgroundColorStyle.indexOf('#') != -1) {
style.boxShadow = `6rpx 6rpx 8rpx ${(this.backgroundColorStyle || '#000000')}10`
} else if (this.backgroundColorStyle.indexOf('rgb') != -1 || this.backgroundColorStyle.indexOf('rgba') != -1 || !this.backgroundColorStyle) {
style.boxShadow = `6rpx 6rpx 8rpx ${(this.backgroundColorStyle || 'rgba(0, 0, 0, 0.1)')}`
}
}
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;
transform: translate(0rpx, 0rpx);
// background-color: $tn-mai
border-radius: 12rpx;
// color: $tn-font-color;
margin: 0;
&--plain {
background-color: transparent !important;
background-image: none;
&.tn-round {
border-radius: 1000rpx !important;
}
}
}
</style>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,654 @@
<template>
<view class="tn-cascade-selection tn-cascade-selection-class">
<scroll-view
class="selection__scroll-view"
:class="[{'tn-border-solid-bottom': headerLine}]"
:style="[scrollViewStyle]"
scroll-x
scroll-with-animation
:scroll-into-view="scrollViewId"
>
<view class="selection__header" :class="[backgroundColorClass]" :style="[headerStyle]">
<view
v-for="(item, index) in selectedArr"
:key="index"
:id="`select__${index}`"
class="selection__header__item"
:class="[headerItemClass(index)]"
:style="[headerItemStyle(index)]"
@tap.stop="clickNav(index)"
>
{{ item.text }}
<view
v-if="index===currentTab && showActiveLine"
class="selection__header__line"
:style="{backgroundColor: activeLineColor}"
></view>
</view>
</view>
</scroll-view>
<swiper
class="selection__list"
:class="[backgroundColorClass]"
:style="[listStyle]"
:current="currentTab"
:duration="300"
@change="switchTab"
>
<swiper-item
v-for="(item, index) in selectedArr"
:key="index"
>
<scroll-view
class="selection__list__item"
:style="{height: selectionContainerHeight + 'rpx'}"
scroll-y
:scroll-into-view="item.scrollViewId"
>
<view class="selection__list__item--first"></view>
<view
v-for="(subItem, subIndex) in item.list"
:key="subIndex"
:id="`select__${subIndex}`"
class="selection__list__item__cell"
:style="[itemStyle]"
@tap="change(index, subIndex, subItem)"
>
<view
v-if="item.index === subIndex"
class="selection__list__item__icon tn-icon-success"
:style="[itemIconStyle]"
></view>
<image
v-if="subItem.src"
class="selection__list__item__image"
:style="[itemImageStyle]"
:src="subItem.src"
></image>
<view
class="selection__list__item__title"
:class="[{'tn-text-bold': item.index === subIndex && itemActiveBold}]"
:style="[itemTitleStyle(index, subIndex)]"
>
{{ subItem.text }}
</view>
<view
v-if="subItem.subText"
class="selection__list__item__title--sub"
:style="[itemSubTitleStyle]"
>
{{ subItem.subText }}
</view>
</view>
</scroll-view>
</swiper-item>
</swiper>
</view>
</template>
<script>
import componentsColorMixin from '../../libs/mixin/components_color.js'
export default {
name: 'tn-cascade-selection',
mixins: [ componentsColorMixin ],
props: {
//
/* {
text: '', //
subText: '', //
src: '', //
value: 0, //
children: [
{
text: '',
subText: '',
value: 0,
children: []
}
]
} */
list: {
type: Array,
default() {
return []
}
},
//
// ['value1','value2','value3']
defaultValue: {
type: Array,
default() {
return []
}
},
//
request: {
type: Boolean,
default: false
},
// requesttrue,
receiveData: {
type: Array,
default() {
return []
}
},
// header线
headerLine: {
type: Boolean,
default: true
},
// header
headerBgColor: {
type: String,
default: ''
},
// ,rpx
tabsHeight: {
type: Number,
default: 88
},
//
text: {
type: String,
default: '请选择'
},
//
activeColor: {
type: String,
default: '#01BEFF'
},
//
activeBold: {
type: Boolean,
default: true
},
// 线
showActiveLine: {
type: Boolean,
default: true
},
// 线
activeLineColor: {
type: String,
default: '#01BEFF'
},
// icon,rpx
activeIconSize: {
type: Number,
default: 0
},
// icon
activeIconColor: {
type: String,
default: '#01BEFF'
},
// item, rpx
itemImgWidth: {
type: Number,
default: 0
},
// item, rpx
itemImgHeight: {
type: Number,
default: 0
},
// item
itemImgRadius: {
type: String,
default: '50%'
},
// item text
itemTextColor: {
type: String,
default: ''
},
// item text
itemActiveTextColor: {
type: String,
default: ''
},
// item text
itemActiveBold: {
type: Boolean,
default: true
},
// item text, rpx
itemTextSize: {
type: Number,
default: 0
},
// item subText
itemSubTextColor: {
type: String,
default: ''
},
// item subText, rpx
itemSubTextSize: {
type: Number,
default: 0
},
// item
itemStyle: {
type: Object,
default() {
return {}
}
},
// selection, rpx
selectionContainerHeight: {
type: Number,
default: 300
}
},
computed: {
scrollViewStyle() {
let style = {}
if (this.headerBgColor) {
style.backgroundColor = this.headerBgColor
}
return style
},
headerStyle() {
let style = {}
style.height = `${this.tabsHeight}rpx`
if (this.backgroundColorStyle) {
style.backgroundColor = this.backgroundColorStyle
}
return style
},
headerItemClass() {
return (index) => {
let clazz = ''
if (index !== this.currentTab) {
clazz += ` ${this.fontColorClass}`
} else {
if (this.activeBold) {
clazz += ' tn-text-bold'
}
}
return clazz
}
},
headerItemStyle() {
return (index) => {
let style = {}
style.color = index === this.currentTab ? this.activeColor : (this.fontColorStyle ? this.fontColorStyle : '')
if (this.fontSizeStyle) {
style.fontSize = this.fontSizeStyle
}
return style
}
},
listStyle() {
let style = {}
style.height = `${this.selectionContainerHeight}rpx`
if (this.backgroundColorStyle) {
style.color = this.backgroundColorStyle
}
return style
},
itemIconStyle() {
let style = {}
if (this.activeIconColor) {
style.color = this.activeIconColor
}
if (this.activeIconSize) {
style.fontSize = this.activeIconSize + 'rpx'
}
return style
},
itemImageStyle() {
let style = {}
if (this.itemImgWidth) {
style.width = this.itemImgWidth + 'rpx'
}
if (this.itemImgHeight) {
style.height = this.itemImgHeight + 'rpx'
}
if (this.itemImgRadius) {
style.borderRadius = this.itemImgRadius
}
return style
},
itemTitleStyle() {
return (index, subIndex) => {
let style = {}
if (index === subIndex) {
if (this.itemActiveTextColor) {
style.color = this.itemActiveTextColor
}
} else {
if (this.itemTextColor) {
style.color = this.itemTextColor
}
}
if (this.itemTextSize) {
style.fontSize = this.itemTextSize + 'rpx'
}
return style
}
},
itemSubTitleStyle() {
let style = {}
if (this.itemSubTextColor) {
style.color = this.itemSubTextColor
}
if (this.itemSubTextSize) {
style.fontSize = this.itemSubTextSize + 'rpx'
}
return {}
}
},
watch: {
list(val) {
this.initData(val, -1)
},
defaultValue(val) {
this.setDefaultValue(val)
},
receiveData(val) {
this.addSubData(val, this.currentTab)
},
},
data() {
return {
//
currentTab: 0,
// tabsscrollView
scrollViewId: 'select__0',
//
selectedArr: []
}
},
created() {
this.setDefaultValue(this.defaultValue)
},
methods: {
//
initData(data, index) {
if (!data || data.length === 0) return
if (this.request) {
//
this.addSubData(data, index)
} else {
this.addSubData(this.getItemList(index, -1), index)
}
},
//
reset() {
this.initData(this.list, -1)
},
//
switchTab(e) {
this.currentTab = e.detail.current
this.checkSelectPosition()
},
//
clickNav(index) {
if (this.currentTab !== index) {
this.currentTab = index
}
},
//
change(index, subIndex, subItem) {
let item = this.selectedArr[index]
if (item.index === subIndex) return
item.index = subIndex
item.text = subItem.text
item.subText = subItem.subText || ''
item.value = subItem.value
item.src = subItem.src || ''
this.$emit('change', {
index: index,
subIndex: subIndex,
...subItem
})
//
if (!this.request) {
let data = this.getItemList(index, subIndex)
this.addSubData(data, index)
}
},
//
setDefaultValue(val) {
let defaultValues = val || []
if (defaultValues.length > 0) {
this.selectedArr = this.getItemListWithValues(JSON.parse(JSON.stringify(this.list)), defaultValues)
if (!this.selectedArr) return
this.currentTab = this.selectedArr.length - 1
this.$nextTick(() => {
this.checkSelectPosition()
})
// defaultItemList.map((item) => {
// item.scrollViewId = `select__${item.index}`
// })
// this.selectedArr = defaultItemList
// this.currentTab = defaultItemList.length - 1
// this.$nextTick(() => {
// this.checkSelectPosition()
// })
} else {
this.initData(this.list, -1)
}
},
// item
getItemList(index, subIndex) {
let list = []
let arr = JSON.parse(JSON.stringify(this.list))
//
if (index === -1) {
list = this.removeChildren(arr)
} else {
//
let value = this.selectedArr[0].index
value = value === -1 ? subIndex : value
list = arr[value].children || []
if (index > 0) {
for (let i = 1; i < index + 1; i++) {
//
let val = index === i ? subIndex : this.selectedArr[i].index
list = list[val].children || []
if (list.length === 0) break
}
}
list = this.removeChildren(list)
}
return list
},
// item
getItemListWithValues(data, values) {
const defaultValues = JSON.parse(JSON.stringify(values))
if (!defaultValues || defaultValues.length === 0) return
// item
const itemIndex = data.findIndex((item) => {
return item.value === defaultValues[0]
})
if (itemIndex === -1) return
const item = data[itemIndex]
item.index = itemIndex
item.scrollViewId = `select__${itemIndex}`
item.list = this.removeChildren(JSON.parse(JSON.stringify(data)))
// 1
if (defaultValues.length === 1 || (!item.hasOwnProperty('children') || item.children.length === 0)) {
return this.removeChildren([item])
} else {
let selectItemList = []
const children = item.children
selectItemList.push(item)
//
defaultValues.splice(0, 1)
const childrenValue = this.getItemListWithValues(children, defaultValues)
selectItemList = selectItemList.concat(childrenValue)
return this.removeChildren(selectItemList)
}
},
//
removeChildren(data) {
let list = data.map((item) => {
if (item.hasOwnProperty('children')) {
delete item['children']
}
return item
})
return list
},
//
addSubData(data, index) {
//
if (!data || data.length === 0) {
if (index == -1) return
//
let arr = this.selectedArr
//
if (index < arr.length - 1) {
let newArr = arr.slice(0, index + 1)
this.selectedArr = newArr
}
let result = JSON.parse(JSON.stringify(this.selectedArr))
let lastItem = result[result.length - 1] || {}
let text = ''
result.map(item => {
text += item.text
delete item['list']
delete item['scrollViewId']
return item
})
this.$emit('complete', {
result: result,
value: lastItem.value,
text: text,
subText: lastItem.subText,
src: lastItem.src
})
} else {
//
let item = [{
text: this.text,
subText: '',
value: '',
src: '',
index: -1,
scrollViewId: 'select__0',
list: data
}]
//
if (index === -1) {
this.selectedArr = item
} else {
//
let retainArr = this.selectedArr.slice(0, index + 1)
this.selectedArr = retainArr.concat(item)
}
this.$nextTick(() => {
this.currentTab = this.selectedArr.length - 1
})
}
},
//
checkSelectPosition() {
let item = this.selectedArr[this.currentTab]
item.scrollViewId = 'select__0'
this.$nextTick(() => {
setTimeout(() => {
//
let val = item.index < 2 ? 0 : Number(item.index - 2)
item.scrollViewId = `select__${val}`
}, 10)
})
//
if (this.currentTab > 1) {
this.scrollViewId = `select__${this.currentTab - 1}`
} else {
this.scrollViewId = `select__0`
}
}
}
}
</script>
<style lang="scss" scoped>
.tn-cascade-selection {
width: 100%;
}
.selection {
&__scroll-view {
background-color: #FFFFFF;
}
&__header {
width: 100%;
display: flex;
align-items: center;
position: relative;
&__item {
max-width: 240rpx;
padding: 15rpx 30rpx;
flex-shrink: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
position: relative;
}
&__line {
width: 60rpx;
height: 6rpx;
border-radius: 4rpx;
position: absolute;
bottom: 0;
right: 0;
left: 50%;
transform: translateX(-50%);
}
}
&__list {
background-color: #FFFFFF;
&__item {
&--first {
width: 100%;
height: 20rpx;
}
&__cell {
width: 100%;
display: flex;
align-items: center;
padding: 20rpx 30rpx;
}
&__icon {
margin-right: 12rpx;
font-size: 24rpx;
}
&__image {
width: 40rpx;
height: 40rpx;
margin-right: 12rpx;
flex-shrink: 0;
}
&__title {
word-break: break-all;
color: #333333;
font-size: 28rpx;
&--sub {
margin-left: 20rpx;
word-break: break-all;
color: $tn-font-sub-color;
font-size: 24rpx;
}
}
}
}
}
</style>

View File

@ -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: {
// computedtn-checkbox
// parentDatawatch(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>

View File

@ -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
// H5APP使flex
style.flex = `0 0 ${this.parentData.width}`;
// #endif
}
if(this.parent && this.parentData.wrap) {
style.width = '100%';
// #ifndef MP
// H5APP使flex100%
style.flex = '0 0 100%';
// #endif
}
return style
},
labelClass() {
let clazz = ''
if (this.isDisabled) {
clazz += ' tn-checkbox__label--disabled'
}
return clazz
}
},
data() {
return {
// checkboxvalue
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-groupthischildren
// 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)
}
},
// inputv-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.message.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>

View File

@ -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>

View File

@ -0,0 +1,236 @@
<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;
padding-left: 24rpx;
text-align: left;
background-color: #FFFFFF;
&__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;
background-color: #FFFFFF;
padding-left: 24rpx;
}
}
}
</style>

View File

@ -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()
})
},
// collapseItemcollapseItem
onChange() {
let activeItem = []
this.childrens.forEach((child, index) => {
if (child.isShow) {
activeItem.push(child.nameSync)
}
})
// activeItem1
if (this.accordion) activeItem = activeItem.join(',')
this.$emit('change', activeItem)
}
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -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>

View File

@ -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 : ''
style.fontSize = this.fontSizeStyle ? this.fontSizeStyle : ''
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>

View File

@ -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="showHours && (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="showMinutes"
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="showSeconds"
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)
// 00
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>

View File

@ -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>

View File

@ -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()
// 使setTimteout60
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>

View File

@ -0,0 +1,328 @@
var cropper = {
// 画布x轴起点
cutX: 0,
// 画布y轴起点
cutY: 0,
// 触摸点信息(手指与图片中心点的相对位置)
touchRelactive: [{
x: 0,
y: 0
}],
// 双指触摸时斜边的长度
hypotenuseLength:0,
// 是否结束触摸
touchEndFlag: false,
// 画布宽高
canvasWidth: 0,
canvasHeight: 0,
// 图片宽高
imgWidth: 0,
imgHeight: 0,
// 图片缩放比例
scale: 1,
// 图片旋转角度
angle: 0,
// 图片上边距
imgTop: 0,
// 图片左边距
imgLeft: 0,
// 窗口宽高
windowWidth: 0,
windowHeight: 0,
init: true
}
function bool(str) {
return str === 'true' || str === true
}
function propsChange(prop, oldProp, ownerInstance, instance) {
if (prop && prop !== 'null') {
var params = prop.split(',')
var type = +params[0]
var dataset = instance.getDataset()
if (cropper.init || type == 4) {
cropper.canvasWidth = +dataset.width
cropper.canvasHeight = +dataset.height
cropper.imgTop = +dataset.windowheight / 2
cropper.imgLeft = +dataset.windowwidth / 2
cropper.imgWidth = +dataset.imgwidth
cropper.imgHeight = +dataset.imgheight
cropper.windowHeight = +dataset.windowheight
cropper.windowWidth = +dataset.windowwidth
cropper.init = false
} else if (type == 2 || type == 3) {
cropper.imgWidth = +dataset.imgwidth
cropper.imgHeight = +dataset.imgheight
}
cropper.angle = +dataset.angle
if (type == 3) {
imgTransform(ownerInstance)
}
switch(type) {
case 1:
setCutCenter(ownerInstance)
// 设置裁剪框大小
computeCutSize(ownerInstance)
// 检查裁剪框是否在范围内
cutDetectionPosition(ownerInstance)
break
case 2:
setCutCenter(ownerInstance)
break
case 3:
imgMarginDetectionScale(ownerInstance)
break
case 4:
imageReset(ownerInstance)
break
case 5:
setCutCenter(ownerInstance)
break
default:
break
}
}
}
function touchStart(event, ownerInstance) {
var touch = event.touches || event.changedTouches
cropper.touchEndFlag = false
if (touch.length === 1) {
cropper.touchRelactive[0] = {
x: touch[0].pageX - cropper.imgLeft,
y: touch[0].pageY - cropper.imgTop
}
} else {
var width = Math.abs(touch[0].pageX - touch[1].pageX)
var height = Math.abs(touch[0].pageY - touch[1].pageY)
cropper.touchRelactive = [{
x: touch[0].pageX - cropper.imgLeft,
y: touch[0].pageY - cropper.imgTop
},{
x: touch[1].pageX - cropper.imgLeft,
y: touch[1].pageY - cropper.imgTop
}]
cropper.hypotenuseLength = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2))
}
}
function touchMove(event, ownerInstance) {
var touch = event.touches || event.changedTouches
if (cropper.touchEndFlag) return
moveDuring(ownerInstance)
if (event.touches.length === 1) {
var left = touch[0].pageX - cropper.touchRelactive[0].x,
top = touch[0].pageY - cropper.touchRelactive[0].y;
cropper.imgLeft = left
cropper.imgTop = top
imgTransform(ownerInstance)
imgMarginDetectionPosition(ownerInstance)
} else {
var dataset = event.instance.getDataset()
var minScale = +dataset.minscale
var maxScale = +dataset.maxscale
var width = Math.abs(touch[0].pageX - touch[1].pageX),
height = Math.abs(touch[0].pageY - touch[1].pageY),
hypotenuse = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2)),
scale = cropper.scale * (hypotenuse / cropper.hypotenuseLength),
current_deg = 0;
scale = scale <= minScale ? minScale : scale
scale = scale >= maxScale ? maxScale : scale
cropper.scale = scale
imgMarginDetectionScale(ownerInstance, true)
var touchRelative = [{
x: touch[0].pageX - cropper.imgLeft,
y: touch[0].pageY - cropper.imgTop
}, {
x: touch[1].pageX - cropper.imgLeft,
y: touch[1].pageY - cropper.imgTop
}]
cropper.touchRelactive = touchRelative
cropper.hypotenuseLength = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2))
// 更新视图
cropper.angle = cropper.angle + current_deg
imgTransform(ownerInstance)
}
}
function touchEnd(event, ownerInstance) {
cropper.touchEndFlag = true
moveStop(ownerInstance)
updateData(ownerInstance)
}
function moveDuring(ownerInstance) {
if (!ownerInstance) return
ownerInstance.callMethod('moveDuring')
}
function moveStop(ownerInstance) {
if (!ownerInstance) return
ownerInstance.callMethod('moveStop')
}
function setCutCenter(ownerInstance) {
var cutX = (cropper.windowWidth - cropper.canvasWidth) * 0.5
var cutY = (cropper.windowHeight - cropper.canvasHeight) * 0.5
cropper.imgTop = cropper.imgTop - cropper.cutY + cutY
cropper.cutY = cutY
cropper.imgLeft = cropper.imgLeft - cropper.cutX + cutX
cropper.cutX = cutX
cutDetectionPosition(ownerInstance)
imgTransform(ownerInstance)
updateData(ownerInstance)
}
// 检测剪裁框位置是否在允许的范围内(屏幕内)
function cutDetectionPosition(ownerInstance) {
var windowHeight = cropper.windowHeight,
windowWidth = cropper.windowWidth;
// 检测上边距是否在范围内
var cutDetectionPositionTop = function() {
if (cropper.cutY < 0) {
cropper.cutY = 0
}
if (cropper.cutY > windowHeight - cropper.canvasHeight) {
cropper.cutY = windowHeight - cropper.canvasHeight
}
}
// 检测左边距是否在范围内
var cutDetectionPositionLeft = function() {
if (cropper.cutX < 0) {
cropper.cutX = 0
}
if (cropper.cutX > windowWidth - cropper.canvasWidth) {
cropper.cutX = windowWidth - cropper.canvasWidth
}
}
// 裁剪框坐标处理如果只写一个参数则另一个默认为0都不写默认为居中
if (cropper.cutX === null && cropper.cutY === null) {
var cutX = (windowWidth - cropper.canvasWidth) * 0.5,
cutY = (windowHeight - cropper.canvasHeight) * 0.5;
cropper.cutX = cutX
cropper.cutY = cutY
} else if (cropper.cutX !== null && cropper.cutX !== null) {
cutDetectionPositionTop()
cutDetectionPositionLeft()
} else if (cropper.cutX !== null && cropper.cutY === null) {
cutDetectionPositionLeft()
cropper.cutY = (windowHeight - cropper.canvasHeight) / 2
} else if (cropper.cutX === null && cropper.cutY !== null) {
cutDetectionPositionTop()
cropper.cutX = (windowWidth - cropper.canvasWidth) / 2
}
}
// 图片边缘检测-缩放
function imgMarginDetectionScale(ownerInstance, delay) {
var scale = cropper.scale,
imgWidth = cropper.imgWidth,
imgHeight = cropper.imgHeight;
if ((cropper.angle / 90) % 2) {
imgWidth = cropper.imgHeight
imgHeight = cropper.imgWidth
}
if (imgWidth * scale < cropper.canvasWidth) {
scale = cropper.canvasWidth / imgWidth
}
if (imgHeight * scale < cropper.canvasHeight) {
scale = Math.max(scale, cropper.canvasHeight / imgHeight)
}
imgMarginDetectionPosition(ownerInstance, scale, delay)
}
// 图片边缘检测-位置
function imgMarginDetectionPosition(ownerInstance, scale, delay) {
var left = cropper.imgLeft,
top = cropper.imgTop,
imgWidth = cropper.imgWidth,
imgHeight = cropper.imgHeight;
scale = scale || cropper.scale
if ((cropper.angle / 90) % 2) {
imgWidth = cropper.imgHeight
imgHeight = cropper.imgWidth
}
left = cropper.cutX + (imgWidth * scale) / 2 >= left ? left : cropper.cutX + (imgWidth * scale) / 2
left = cropper.cutX + cropper.canvasWidth - (imgWidth * scale) / 2 <= left ? left : cropper.cutX + cropper.canvasWidth - (imgWidth * scale) / 2
top = cropper.cutY + (imgHeight * scale) / 2 >= top ? top : cropper.cutY + (imgHeight * scale) / 2
top = cropper.cutY + cropper.canvasHeight - (imgHeight * scale) / 2 <= top ? top : cropper.cutY + cropper.canvasHeight - (imgHeight * scale) / 2
cropper.imgLeft = left
cropper.imgTop = top
cropper.scale = scale
if (!delay || delay === 'null') {
imgTransform(ownerInstance)
}
}
// 改变截取值大小
function computeCutSize(ownerInstance) {
if (cropper.canvasWidth > cropper.windowWidth) {
cropper.canvasWidth = cropper.windowWidth
} else if (cropper.canvasWidth + cropper.cutX > cropper.windowWidth) {
cropper.cutX = cropper.windowWidth - cropper.cutX
}
if (cropper.canvasHeight > cropper.windowHeight) {
cropper.canvasHeight = cropper.windowHeight
} else if (cropper.canvasHeight + cropper.cutY > cropper.windowHeight) {
cropper.cutY = cropper.windowHeight - cropper.cutY
}
}
// 图片动画
function imgTransform(ownerInstance) {
var image = ownerInstance.selectComponent('.tn-cropper__image')
if (!image) return
var x = cropper.imgLeft - cropper.imgWidth / 2,
y = cropper.imgTop - cropper.imgHeight / 2;
image.setStyle({
'transform': 'translate3d('+ x + 'px,' + y + 'px,0) scale(' + cropper.scale +') rotate(' + cropper.angle + 'deg)'
})
}
// 图片重置
function imageReset(ownerInstance) {
cropper.scale = 1
cropper.angle = 0
imgTransform(ownerInstance)
}
// 高度变化
function canvasHeight(ownerInstance) {
if (!ownerInstance) return
computeCutSize(ownerInstance)
}
// 宽度变化
function canvasWidth(ownerInstance) {
if (!ownerInstance) return
computeCutSize(ownerInstance)
}
// 更新数据
function updateData(ownerInstance) {
if (!ownerInstance) return
ownerInstance.callMethod('change', {
cutX: cropper.cutX,
cutY: cropper.cutY,
imgWidth: cropper.imgWidth,
imgHeight: cropper.imgHeight,
scale: cropper.scale,
angle: cropper.angle,
imgTop: cropper.imgTop,
imgLeft: cropper.imgLeft
})
}
module.exports = {
touchStart: touchStart,
touchMove: touchMove,
touchEnd: touchEnd,
propsChange: propsChange
}

View File

@ -0,0 +1,570 @@
<template>
<view class="tn-cropper-class tn-cropper" @touchmove.stop.prevent="stop">
<image
v-if="imageUrl"
:src="imageUrl"
class="tn-cropper__image"
:style="{
width: (imgWidth ? imgWidth : width) + 'px',
height: (imgHeight ? imgHeight : height) + 'px',
transitionDuration: (animation ? 0.3 : 0) + 's'
}"
mode="widthFix"
:data-minScale="minScale"
:data-maxScale="maxScale"
@load="imageLoad"
@error="imageLoad"
@touchstart="wxs.touchStart"
@touchmove="wxs.touchMove"
@touchend="wxs.touchEnd"
></image>
<view
class="tn-cropper__wrapper"
:style="{
width: width + 'px',
height: height + 'px',
borderRadius: isRound ? '50%' : '0'
}"
>
<view
class="tn-cropper__border"
:style="{
border: borderStyle,
borderRadius: isRound ? '50%' : '0',
}"
:prop="props"
:change:prop="wxs.propsChange"
:data-width="width"
:data-height="height"
:data-windowHeight="systemInfo.windowHeight || 600"
:data-windowWidth="systemInfo.windowWidth || 400"
:data-imgTop="imgTop"
:data-imgWidth="imgWidth"
:data-imgHeight="imgHeight"
:data-angle="angle"
></view>
</view>
<canvas
class="tn-cropper__canvas"
:style="{
width: width * scaleRatio + 'px',
height: height * scaleRatio + 'px'
}"
:canvas-id="CANVAS_ID"
:id="CANVAS_ID"
:disable-scroll="true"
></canvas>
<view
v-if="!custom"
class="tn-cropper__tabbar"
>
<view class="tn-cropper__tabbar__btn tn-cropper__tabber__cancel" @tap.stop="back">取消</view>
<view class="tn-cropper__tabbar__rotate" :class="[`tn-icon-${rotateIcon}`]" @tap.stop="setAngle"></view>
<view class="tn-cropper__tabbar__btn tn-cropper__tabber__confirm" @tap.stop="getCutImage">完成</view>
</view>
</view>
</template>
<script src="./index.wxs" lang="wxs" module="wxs"></script>
<script>
export default {
name: 'tn-cropper',
props: {
//
imageUrl: {
type: String,
default: ''
},
// px
height: {
type: Number,
default: 280
},
// px
width: {
type: Number,
default: 280
},
//
isRound: {
type: Boolean,
default: false
},
//
borderStyle: {
type: String,
default: '1rpx solid #FFF'
},
//
scaleRatio: {
type: Number,
default: 1
},
//
// (0, 1]
quality: {
type: Number,
default: 0.8
},
// base64(H5base64)
returnBase64: {
type: Boolean,
default: false
},
//
rotateAngle: {
type: Number,
default: 0
},
//
minScale: {
type: Number,
default: 0.5
},
//
maxScale: {
type: Number,
default: 2
},
// ()
custom: {
type: Boolean,
default: false
},
//
// customtrue
startCutting: {
type: Boolean,
default: false
},
// loading
loading: {
type: Boolean,
default: true
},
//
rotateIcon: {
type: String,
default: 'circle-arrow'
}
},
data() {
return {
// canvasid
CANVAS_ID: 'tn-cropper-canvas',
//
TIME_CUT_CENTER: null,
// canvas
ctx: null,
// x
cutX: 0,
// y
cutY: 0,
//
imgWidth: 0,
//
imgHeight: 0,
//
imgTop: 0,
//
imgLeft: 0,
//
scale: 1,
//
angle: 0,
//
animation: false,
//
animationTime: null,
//
systemInfo: {},
//
props: '',
//
sizeChange: 0,
angleChange: 0,
resetChange: 0,
centerChange: 0
}
},
watch: {
imageUrl(val) {
this.imageReset()
this.showLoading()
uni.getImageInfo({
src: val,
success: (res) => {
//
this.imgComputeSize(res.width, res.height)
this.angleChange++
this.props = `3,${this.angleChange}`
},
fail: (err) => {
console.log(err);
this.imgComputeSize()
this.angleChange++
this.props = `3,${this.angleChange}`
}
})
},
rotateAngle(val) {
this.animation = true
this.angle = val
this.angleChanged(val)
},
animation(val) {
clearTimeout(this.animationTime)
if (val) {
this.animationTime = setTimeout(() => {
this.animation = false
}, 200)
}
},
startCutting(val) {
if (this.custom && val) {
this.getCutImage()
}
}
},
mounted() {
this.systemInfo = uni.getSystemInfoSync()
this.imgTop = this.systemInfo.windowHeight / 2
this.imgLeft = this.systemInfo.windowWidth / 2
this.ctx = uni.createCanvasContext(this.CANVAS_ID, this)
//
this.$nextTick(() => {
this.props = '1,1'
})
setTimeout(() => {
this.$emit('ready', {})
}, 200)
},
methods: {
//
async getLocalImage(url) {
return await new Promise((resolve, reject) => {
uni.downloadFile({
url: url,
success: (res) => {
resolve(res.tempFilePath)
},
fail: (err) => {
reject(false)
}
})
})
},
//
getCutImage() {
if (!this.imageUrl) {
uni.showToast({
title: '请选择图片',
icon: 'none'
})
return
}
this.loading && this.showLoading()
const draw = async () => {
//
let imgWidth = this.imgWidth * this.scale * this.scaleRatio
let imgHeight = this.imgHeight * this.scale * this.scaleRatio
// canvas
let xpos = this.imgLeft - this.cutX
let ypos = this.imgTop - this.cutY
let imgUrl = this.imageUrl
// #ifdef APP-PLUS || MP-WEIXIN
if (~this.imageUrl.indexOf('https:')) {
imgUrl = await this.getLocalImage(this.imageUrl)
}
// #endif
//
this.ctx.translate(xpos * this.scaleRatio, ypos * this.scaleRatio)
//
if (this.isRound) {
const r = this.width > this.height ? Math.floor(this.height / 2) : Math.floor(this.width / 2)
let translateX = Math.floor(this.width / 2)
let translateY = Math.floor(this.height / 2)
this.ctx.beginPath()
this.ctx.arc(translateX - (xpos * this.scaleRatio), translateY - (ypos * this.scaleRatio), r, 0, (360 * Math.PI) / 180)
this.ctx.closePath()
this.ctx.stroke()
this.ctx.clip()
}
this.ctx.rotate((this.angle * Math.PI) / 180)
this.ctx.drawImage(imgUrl, -imgWidth / 2, -imgHeight / 2, imgWidth, imgHeight)
//
this.ctx.draw(false, () => {
let params = {
width: this.width * this.scaleRatio,
height: Math.round(this.height * this.scaleRatio),
destWidth: this.width * this.scaleRatio,
destHeight: Math.round(this.height) * this.scaleRatio,
fileType: 'png',
quality: this.quality
}
let data = {
url: '',
base64: '',
width: this.width * this.scaleRatio,
height: this.height * this.scaleRatio
}
// #ifdef MP-ALIPAY
if (this.returnBase64) {
this.ctx.toDataURL(params).then((urlData) => {
data.base64 = urlData
this.loading && uni.hideLoading()
this.$emit('cropper', data)
})
} else {
this.ctx.toTempFilePath({
...params,
success: (res) => {
data.url = res.apFilePath
this.loading && uni.hideLoading()
this.$emit('cropper', data)
}
})
}
// #endif
let base64Flag = this.returnBase64
// #ifndef MP-ALIPAY
// #ifdef MP-BAIDU || MP-TOUTIAO || H5
base64Flag = false
// #endif
if (base64Flag) {
uni.canvasGetImageData({
canvasId: this.CANVAS_ID,
x: 0,
y: 0,
width: this.width * this.scaleRatio,
height: Math.round(this.height * this.scaleRatio),
success: (res) => {
const arrayBuffer = new Uint8Array(res.data)
const base64 = uni.arrayBufferToBase64(arrayBuffer)
data.base64 = base64
this.loading && uni.hideLoading()
this.$emit('cropper', data)
}
}, this)
} else {
uni.canvasToTempFilePath({
...params,
canvasId: this.CANVAS_ID,
success: (res) => {
data.url = res.tempFilePath
// #ifdef H5
data.base64 = res.tempFilePath
// #endif
this.loading && uni.hideLoading()
this.$emit('cropper', data)
}
}, this)
}
// #endif
})
}
draw()
},
//
change(e) {
this.cutX = e.cutX || 0
this.cutY = e.cutY || 0
this.imgWidth = e.imgWidth || this.imgWidth
this.imgHeight = e.imgHeight || this.imgHeight
this.scale = e.scale || 1
this.angle = e.angle || 0
this.imgTop = e.imgTop || 0
this.imgLeft = e.imgLeft || 0
},
//
imageReset() {
this.scale = 1
this.angle = 0
let systemInfo = this.systemInfo.windowHeight ? this.systemInfo : uni.getSystemInfoSync()
this.imgTop = systemInfo.windowHeight / 2
this.imgLeft = systemInfo.windowWidth / 2
this.resetChange++
this.props = `4,${this.resetChange}`
//
this.$emit('initAngle', {})
},
//
imgComputeSize(width, height) {
// =
let imgWidth = width,
imgHeight = height;
if (imgWidth && imgHeight) {
if (imgWidth / imgHeight > this.width / this.height) {
imgHeight = this.height
imgWidth = (width / height) * imgHeight
} else {
imgWidth = this.width
imgHeight = (height / width) * imgWidth
}
} else {
let systemInfo = this.systemInfo.windowHeight ? this.systemInfo : uni.getSystemInfoSync()
imgWidth = systemInfo.windowWidth
imgHeight = 0
}
this.imgWidth = imgWidth
this.imgHeight = imgHeight
this.sizeChange++
this.props = `2,${this.sizeChange}`
},
//
imageLoad(e) {
this.imageReset()
uni.hideLoading()
this.$emit('imageLoad', {})
},
//
moveStop() {
clearTimeout(this.TIME_CUT_CENTER)
this.TIME_CUT_CENTER = setTimeout(() => {
this.centerChange++
this.props = `5,${this.centerChange}`
}, 688)
},
//
moveDuring() {
clearTimeout(this.TIME_CUT_CENTER)
},
//
showLoading() {
uni.showLoading({
title: '请稍等......',
mask: true
})
},
//
stop() {},
// /
back() {
uni.navigateBack()
},
//
angleChanged(val) {
this.moveStop()
if (val % 90) {
this.angle = Math.round(val / 90) * 90
}
this.angleChange++
this.props = `3,${this.angleChange}`
},
//
setAngle() {
this.animation = true
this.angle = this.angle + 90
this.angleChanged(this.angle)
}
}
}
</script>
<style lang="scss" scoped>
.tn-cropper {
width: 100vw;
height: 100vh;
background: linear-gradient(-120deg, #F15BB5, #9A5CE5, #01BEFF, #00F5D4);
// background: linear-gradient(-120deg, #9A5CE5, #01BEFF, #00F5D4, #43e97b);
// background: linear-gradient(-120deg,#c471f5, #ec008c, #ff4e50,#f9d423);
// background: linear-gradient(-120deg, #0976ea, #c471f5, #f956b6, #ea7e0a);
position: fixed;
top: 0;
left: 0;
z-index: 1;
&__image {
width: 100%;
border-style: none;
position: absolute;
top: 0;
left: 0;
z-index: 2;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
transform-origin: center;
}
&__canvas {
position: fixed;
z-index: 10;
left: -2000px;
top: -2000px;
pointer-events: none;
}
&__wrapper {
position: fixed;
z-index: 4;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
border: 3000px solid rgba(0, 0, 0, 0.55);
pointer-events: none;
box-sizing: content-box;
}
&__border {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
pointer-events: none;
}
&__tabbar {
width: 100%;
height: 120rpx;
padding: 0 40rpx;
box-sizing: border-box;
position: fixed;
left: 0;
bottom: 0;
z-index: 99;
display: flex;
align-items: center;
justify-content: space-between;
color: #FFFFFF;
font-size: 32rpx;
&::after {
content: '';
position: absolute;
top: 0;
right: 0;
left: 0;
border-top: 1rpx solid rgba(255, 255, 255, 0.2);
-webkit-transform: scaleY(0.5) translateZ(0);
transform: scaleY(0.5) translateZ(0);
transform-origin: 0 100%;
}
&__btn {
height: 80rpx;
display: flex;
align-items: center;
}
&__rotate {
width: 44rpx;
height: 44rpx;
font-size: 40rpx;
text-align: center;
}
}
}
</style>

View File

@ -0,0 +1,288 @@
function setTimeout(instance, cb, time) {
if (time > 0) {
var s = getDate().getTime()
var fn = function () {
if (getDate().getTime() - s > time) {
cb && cb()
} else
instance.requestAnimationFrame(fn)
}
fn()
}
else
cb && cb()
}
// 判断触摸的移动方向
function decideSwiperDirection(startTouches, currentTouches, vertical) {
// 震动偏移容差
var toleranceShake = 150
// 移动容差
var toleranceTranslate = 10
if (!vertical) {
// 水平方向移动
if (Math.abs(currentTouches.y - startTouches.y) <= toleranceShake) {
// console.log(currentTouches.x, startTouches.x);
if (Math.abs(currentTouches.x - startTouches.x) > toleranceTranslate) {
if (currentTouches.x - startTouches.x > 0) {
return 'right'
} else if (currentTouches.x - startTouches.x < 0) {
return 'left'
}
}
}
} else {
// 垂直方向移动
if (Math.abs(currentTouches.x - startTouches.x) <= toleranceShake) {
// console.log(currentTouches.x, startTouches.x);
if (Math.abs(currentTouches.y - startTouches.y) > toleranceTranslate) {
if (currentTouches.y - startTouches.y > 0) {
return 'down'
} else if (currentTouches.y - startTouches.y < 0) {
return 'up'
}
}
}
}
return ''
}
// swiperItem参数数据更新
var itemDataObserver = function(newVal, oldVal, ownerInstance, instance) {
if (!newVal || newVal === 'undefined') return
var state = ownerInstance.getState()
state.itemData = newVal
}
// swiperIndex数据更新
var currentIndexObserver = function(newVal, oldVal, ownerInstance, instance) {
if ((!newVal && newVal != 0) || newVal === 'undefined') return
var state = ownerInstance.getState()
state.currentIndex = newVal
}
// containerData数据更新
var containerDataObserver = function(newVal, oldVal, ownerInstance, instance) {
if (!newVal || newVal === 'undefined') return
var state = ownerInstance.getState()
state.containerData = newVal
}
// 开始触摸
var touchStart = function(event, ownerInstance) {
console.log('touchStart');
var instance = event.instance
var dataset = instance.getDataset()
var state = ownerInstance.getState()
var itemData = state.itemData
var containerData = state.containerData
// 由于当前SwiperIndex初始为0可能会导致swiperIndex数据没有更新
if (!state.currentIndex || state.currentIndex === 'undefined') {
state.currentIndex = 0
}
if (!containerData || containerData.circular === 'undefined') {
containerData.circular = false
}
state.containerData = containerData
// 如果当前切换动画还没执行结束再次触摸会重新加载对应的swiperContainer的信息
// console.log(containerData.animationFinish);
if (!containerData.animationFinish) {
ownerInstance.callMethod('changeParentSwiperContainerStyleStatus',{
status: 'reload'
})
}
// 判断是否为为当前显示的SwiperItem
if (itemData.index != state.currentIndex) return
var touches = event.changedTouches[0]
if (!touches) return
// 标记滑动开始时间
state.touchStartTime = getDate().getTime()
// 记录当前滑动开始的xy坐标
state.touchRelactive = {
x: touches.pageX,
y: touches.pageY
}
// 记录触摸id用于处理多指的情况
state.touchId = touches.identifier
// 标记开始触摸
state.touching = true
ownerInstance.callMethod('updateTouchingStatus', {
status: true
})
}
// 正在移动
var touchMove = function(event, ownerInstance) {
console.log('touchMove');
var instance = event.instance
var dataset = instance.getDataset()
var state = ownerInstance.getState()
var itemData = state.itemData
var containerData = state.containerData
// 判断是否为为当前显示的SwiperItem
if (itemData.index != state.currentIndex) return
// 判断是否开始触摸
if (!state.touching) return
var touches = event.changedTouches[0]
if (!touches) return
// 判断是否为同一个触摸点
if (state.touchId != touches.identifier) return
var currentTouchRelactive = {
x: touches.pageX,
y: touches.pageY
}
// 计算相对位移比例
if (containerData.vertical) {
var touchDistance = currentTouchRelactive.y - state.touchRelactive.y
var itemHeight = itemData.itemHeight
var distanceRate = touchDistance / itemHeight
// console.log(currentTouchRelactive.y, touchDistance, itemHeight, distanceRate);
// 判断是否为衔接轮播如果不是衔接轮播如果当前为第一个swiperItem并且向下滑、当前为最后一个swiperItem并且向上滑时不进行操作
if (!containerData.circular &&
((state.currentIndex === 0 && touchDistance > 0) || (state.currentIndex === containerData.swiperItemLength - 1 && touchDistance < 0))
) {
return
}
// 如果超出了距离则不进行操作
if((Math.abs(touchDistance) > (itemData.itemTop + itemData.itemHeight))) {
ownerInstance.callMethod('updateParentSwiperContainerStyle', {
value: distanceRate < 0 ? -1 : 1
})
return
}
} else {
var touchDistance = currentTouchRelactive.x - state.touchRelactive.x
var itemWidth = itemData.itemWidth
var distanceRate = touchDistance / itemWidth
// console.log(currentTouchRelactive.x, touchDistance, itemWidth, distanceRate);
// 判断是否为衔接轮播如果不是衔接轮播如果当前为第一个swiperItem并且向右滑、当前为最后一个swiperItem并且向左滑时不进行操作
if (!containerData.circular &&
((state.currentIndex === 0 && touchDistance > 0) || (state.currentIndex === containerData.swiperItemLength - 1 && touchDistance < 0))
) {
return
}
// 如果超出了距离则不进行操作
if((Math.abs(touchDistance) > (itemData.itemLeft + itemData.itemWidth))) {
ownerInstance.callMethod('updateParentSwiperContainerStyle', {
value: distanceRate < 0 ? -1 : 1
})
return
}
}
ownerInstance.callMethod('updateParentSwiperContainerStyle', {
value: distanceRate
})
}
// 移动结束
var touchEnd = function(event, ownerInstance) {
console.log('touchEnd');
var instance = event.instance
var dataset = instance.getDataset()
var state = ownerInstance.getState()
var itemData = state.itemData
var containerData = state.containerData
// 判断是否为为当前显示的SwiperItem
if (itemData.index != state.currentIndex) return
// 判断是否开始触摸
if (!state.touching) return
var touches = event.changedTouches[0]
if (!touches) return
// 判断是否为同一个触摸点
if (state.touchId != touches.identifier) return
var currentTime = getDate().getTime()
var currentTouchRelactive = {
x: touches.pageX,
y: touches.pageY
}
if (containerData.vertical) {
// 判断触摸移动方向
var direction = decideSwiperDirection(state.touchRelactive, currentTouchRelactive, true)
// 判断是否为衔接轮播如果不是衔接轮播如果当前为第一个swiperItem并且向下滑、当前为最后一个swiperItem并且向上滑时不进行操作
if (containerData.circular ||
!((state.currentIndex === 0 && direction === 'down') || (state.currentIndex === containerData.swiperItemLength - 1 && direction === 'up'))
) {
// 判断触摸的时间和移动的距离是否超过了当前itemHeight的一半如果是则执行切换操作
// console.log(currentTime - state.touchStartTime, Math.abs(currentTouchRelactive.y - state.touchRelactive.y));
if ((currentTime - state.touchStartTime) > 200 && Math.abs(currentTouchRelactive.y - state.touchRelactive.y) < itemData.itemHeight / 2) {
ownerInstance.callMethod('changeParentSwiperContainerStyleStatus',{
status: 'reset'
})
} else {
// console.log(direction, state.touchRelactive.y, currentTouchRelactive.y);
ownerInstance.callMethod('updateParentSwiperContainerStyleWithDirection', {
direction: direction
})
}
}
} else {
// 判断触摸移动方向
var direction = decideSwiperDirection(state.touchRelactive, currentTouchRelactive, false)
// 判断是否为衔接轮播如果不是衔接轮播如果当前为第一个swiperItem并且向右滑、当前为最后一个swiperItem并且向左滑时不进行操作
if (containerData.circular ||
!((state.currentIndex === 0 && direction === 'right') || (state.currentIndex === containerData.swiperItemLength - 1 && direction === 'left'))
) {
// 判断触摸的时间和移动的距离是否超过了当前itemWidth的一半如果是则执行切换操作
// console.log(currentTime - state.touchStartTime, Math.abs(currentTouchRelactive.x - state.touchRelactive.x));
if ((currentTime - state.touchStartTime) > 200 && Math.abs(currentTouchRelactive.x - state.touchRelactive.x) < itemData.itemWidth / 2) {
ownerInstance.callMethod('changeParentSwiperContainerStyleStatus',{
status: 'reset'
})
} else {
// console.log(direction, state.touchRelactive.x, currentTouchRelactive.x);
ownerInstance.callMethod('updateParentSwiperContainerStyleWithDirection', {
direction: direction
})
}
}
}
// 清除标记
state.touchId = null
state.touchRelactive = null
state.touchStartTime = 0
// 标记停止触摸
state.touching = true
ownerInstance.callMethod('updateTouchingStatus', {
status: false
})
}
module.exports = {
itemDataObserver: itemDataObserver,
currentIndexObserver: currentIndexObserver,
containerDataObserver: containerDataObserver,
touchStart: touchStart,
touchMove: touchMove,
touchEnd: touchEnd
}

View File

@ -0,0 +1,277 @@
<template>
<!-- #ifdef MP-WEIXIN -->
<view
class="tn-c-swiper-item"
:style="[swiperStyle]"
:itemData="itemData"
:currentIndex="currentIndex"
:containerData="containerData"
:change:itemData="wxs.itemDataObserver"
:change:currentIndex="wxs.currentIndexObserver"
:change:containerData="wxs.containerDataObserver"
@touchstart="wxs.touchStart"
:catch:touchmove="touching?wxs.touchMove:''"
:catch:touchend="touching?wxs.touchEnd:''"
>
<view class="item__container tn-c-swiper-item__container" :style="[containerStyle]">
<slot></slot>
</view>
</view>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<view
class="tn-c-swiper-item"
:style="[swiperStyle]"
:itemData="itemData"
:currentIndex="currentIndex"
:containerData="containerData"
:change:itemData="wxs.itemDataObserver"
:change:currentIndex="wxs.currentIndexObserver"
:change:containerData="wxs.containerDataObserver"
@touchstart="wxs.touchStart"
@touchmove="wxs.touchMove"
@touchend="wxs.touchEnd"
>
<view class="item__container tn-c-swiper-item__container" :style="[containerStyle]">
<slot></slot>
</view>
</view>
<!-- #endif -->
</template>
<script src="./index.wxs" lang="wxs" module="wxs"></script>
<script>
export default {
name: 'tn-custom-swiper-item',
props: {
},
computed: {
// swiperItem
itemData() {
return {
index: this.index,
itemWidth: this.itemWidth,
itemHeight: this.itemHeight,
itemTop: this.itemTop,
itemLeft: this.itemLeft
}
},
currentIndex() {
return this.parentData.currentIndex
},
containerData() {
return {
duration: this.parentData.duration,
animationFinish: this.parentData.swiperContainerAnimationFinish,
circular: this.parentData.circular,
swiperItemLength: this.swiperItemLength,
vertical: this.parentData.vertical
}
},
swiperStyle() {
let style = {}
style.transform = `translate3d(${this.translateX}%, ${this.translateY}%, 0px)`
return style
},
containerStyle() {
let style = {}
if (this.parentData.customSwiperStyle && Object.keys(this.parentData.customSwiperStyle).length > 0) {
style = this.parentData.customSwiperStyle
}
if ((this.currentIndex === 0 && this.index === this.swiperItemLength - 1) || (this.index === this.currentIndex - 1) &&
(this.parentData.prevSwiperStyle && Object.keys(this.parentData.prevSwiperStyle).length > 0)
) {
// swiperItem
const copyStyle = JSON.parse(JSON.stringify(style))
style = Object.assign(copyStyle, this.parentData.prevSwiperStyle)
}
if ((this.currentIndex === this.swiperItemLength - 1 && this.index === 0) || (this.index === this.currentIndex + 1) &&
(this.parentData.nextSwiperStyle && Object.keys(this.parentData.nextSwiperStyle).length > 0)
) {
// swiperItem
const copyStyle = JSON.parse(JSON.stringify(style))
style = Object.assign(copyStyle, this.parentData.nextSwiperStyle)
}
return style
}
},
data() {
return {
//
parentData: {
duration: 500,
currentIndex: 0,
swiperContainerAnimationFinish: false,
circular: false,
vertical: false,
prevSwiperStyle: {},
customSwiperStyle: {},
nextSwiperStyle: {}
},
//
touching: true,
// swiperItem
translateX: 0,
translateY: 0,
// swiperItem
itemWidth: 0,
itemHeight: 0,
// swiperItem
itemTop: 0,
itemLeft: 0,
// swiperItem prev current next
status: 'current',
// swiperItemindex
index: 0,
// swiperItem
swiperItemLength: 0
}
},
created() {
this.parent = false
this.updateParentData()
// childrenswiperItem
this.index = this.parent.children.length
this.parent && this.parent.children.push(this)
},
mounted() {
this.$nextTick(() => {
this.initSwiperItem()
})
},
methods: {
// swiperItem
initSwiperItem() {
this.getSwiperItemRect(() => {
this.parent.updateAllSwiperItemStyle()
this.parentData.swiperContainerAnimationFinish = true
})
},
// swiperItem
async getSwiperItemRect(callback) {
const swiperItemRes = await this._tGetRect('.tn-c-swiper-item')
if (!swiperItemRes.height || !swiperItemRes.width) {
setTimeout(() => {
this.getSwiperItemRect()
}, 30)
return
}
this.itemWidth = swiperItemRes.width
this.itemHeight = swiperItemRes.height
this.itemTop = swiperItemRes.top
this.itemLeft = swiperItemRes.left
callback && callback()
},
// swiperItem
updateSwiperItemStyle(swiperItemLength, currentIndex = undefined) {
currentIndex = currentIndex != undefined ? currentIndex : this.parentData.currentIndex
this.swiperItemLength = swiperItemLength
// swiperItem
// swiperItemswiperItem
if (currentIndex === 0 && this.index === swiperItemLength - 1) {
if (this.parentData.vertical) {
this.translateX = 0
this.translateY = -100
} else {
this.translateX = -100
this.translateY = 0
}
}
// swiperItemswiperItem
else if (currentIndex === swiperItemLength - 1 && this.index === 0) {
if (this.parentData.vertical) {
this.translateX = 0
this.translateY = swiperItemLength * 100
} else {
this.translateX = swiperItemLength * 100
this.translateY = 0
}
}
//
else {
if (this.parentData.vertical) {
this.translateX = 0
this.translateY = this.index * 100
} else {
this.translateX = this.index * 100
this.translateY = 0
}
}
},
//
updateParentSwiperContainerStyle(e) {
this.parent.updateSwiperContainerStyleWithValue(e.value)
},
//
updateParentSwiperContainerStyleWithDirection(e) {
this.parent.updateSwiperContainerStyleWithDirection(e.direction)
},
//
changeParentSwiperContainerStyleStatus(e) {
// reset -> reload ->
this.parent.updateSwiperContainerStyleWithDirection(e.status)
},
//
updateParentData() {
this.getParentData('tn-custom-swiper')
},
//
updateTouchingStatus(e) {
this.touching = e.status
if (e.status) {
this.parent.stopAutoPlay()
} else {
this.parent.startAutoPlay()
}
},
//
extractCustomStyle(customStyle) {
let data = {
transform: {},
style: {}
}
if (!customStyle) return data
// transform
const allowTransformProps = ['scale','scaleX','scaleY','scaleZ','rotate','rotateX','rotateY','rotateZ']
for (let prop in customStyle) {
if (prop.startsWith('transformProp')) {
// transform
let transformProp = prop.substring('transformProp'.length)
const index = allowTransformProps.findIndex((item) => {
return item.toLowerCase() === transformProp.toLowerCase()
})
if (index !== -1) {
transformProp = allowTransformProps[index]
data.transform[transformProp] = customStyle[prop]
}
} else {
//
data.style[prop] = customStyle[prop]
}
}
return data
}
}
}
</script>
<style lang="scss" scoped>
.tn-c-swiper-item {
width: 100%;
height: 100%;
position: absolute;
display: block;
will-change: transform;
cursor: none;
transform: translate3d(0px, 0px, 0px);
.item__container {
width: 100%;
height: 100%;
display: block;
position: absolute;
}
}
</style>

View File

@ -0,0 +1,535 @@
<template>
<view
class="tn-c-swiper-class tn-c-swiper"
>
<!-- 轮播item容器-->
<view class="tn-swiper__container" :style="[swiperContainerStyle]" :animation="containerAnimation">
<slot></slot>
</view>
<!-- 轮播指示器-->
<view v-if="indicator" class="tn-swiper__indicator" :class="[`tn-swiper__indicator--${vertical ? 'vertical' : 'horizontal'}`]" :style="[indicatorStyle]">
<!-- 方形 -->
<block v-if="indicatorType === 'rect'">
<view
v-for="(item, index) in children.length"
:key="index"
class="tn-swiper__indicator__rect"
:class="[
`tn-swiper__indicator__rect--${vertical ? 'vertical' : 'horizontal'}`,
currentIndex === index ? `tn-swiper__indicator__rect--active tn-swiper__indicator__rect--active--${vertical ? 'vertical' : 'horizontal'}` : ''
]"
:style="[indicatorPointStyle(index)]"
></view>
</block>
<!-- -->
<block v-if="indicatorType === 'dot'">
<view
v-for="(item, index) in children.length"
:key="index"
class="tn-swiper__indicator__dot"
:class="[
`tn-swiper__indicator__dot--${vertical ? 'vertical' : 'horizontal'}`,
currentIndex === index ? `tn-swiper__indicator__dot--active tn-swiper__indicator__dot--active--${vertical ? 'vertical' : 'horizontal'}` : ''
]"
:style="[indicatorPointStyle(index)]"
></view>
</block>
<!-- 圆角方形 -->
<block v-if="indicatorType === 'round'">
<view
v-for="(item, index) in children.length"
:key="index"
class="tn-swiper__indicator__round"
:class="[
`tn-swiper__indicator__round--${vertical ? 'vertical' : 'horizontal'}`,
currentIndex === index ? `tn-swiper__indicator__round--active tn-swiper__indicator__round--active--${vertical ? 'vertical' : 'horizontal'}` : ''
]"
:style="[indicatorPointStyle(index)]"
></view>
</block>
<!-- 序号 -->
<block v-if="indicatorType === 'number' && !vertical">
<view class="tn-swiper__indicator__number">{{ currentIndex + 1 }}/{{ children.length }}</view>
</block>
</view>
</view>
</template>
<script>
export default {
name: 'tn-custom-swiper',
props: {
//
current: {
type: Number,
default: 0
},
//
autoplay: {
type: Boolean,
default: false
},
//
interval: {
type: Number,
default: 5000
},
//
duration: {
type: Number,
default: 500
},
//
circular: {
type: Boolean,
default: false
},
//
vertical: {
type: Boolean,
default: false
},
//
indicator: {
type: Boolean,
default: false
},
//
// rect -> round -> dot -> number ->
indicatorType: {
type: String,
default: 'dot'
},
//
// topLeft \ topCenter \ topRight \ bottomLeft \ bottomCenter \ bottomRight
indicatorPosition: {
type: String,
default: 'bottomCenter'
},
//
indicatorActiveColor: {
type: String,
default: ''
},
//
indicatorInactiveColor: {
type: String,
default: ''
},
//
prevSwiperStyle: {
type: Object,
default() {
return {}
}
},
//
customSwiperStyle: {
type: Object,
default() {
return {}
}
},
//
nextSwiperStyle: {
type: Object,
default() {
return {}
}
}
},
computed: {
parentData() {
return [
this.duration,
this.currentIndex,
this.swiperContainerAnimationFinish,
this.circular,
this.vertical,
this.prevSwiperStyle,
this.customSwiperStyle,
this.nextSwiperStyle
]
},
indicatorStyle() {
let style = {}
if (this.vertical) {
if (this.indicatorPosition === 'topLeft' || this.indicatorPosition === 'bottomLeft') style.justifyContent = 'flex-start'
if (this.indicatorPosition === 'topCenter' || this.indicatorPosition === 'bottomCenter') style.justifyContent = 'center'
if (this.indicatorPosition === 'topRight' || this.indicatorPosition === 'bottomRight') style.justifyContent = 'flex-end'
if (['topLeft','topCenter','topRight'].indexOf(this.indicatorPosition) >= 0) {
if (this.vertical) {
style.right = '12rpx'
style.left = 'auto'
} else {
style.top = '12rpx'
style.bottom = 'auto'
}
} else {
if (this.vertical) {
style.right = 'auto'
style.left = '12rpx'
} else {
style.top = 'auto'
style.bottom = '12rpx'
}
}
} else {
if (this.indicatorPosition === 'topLeft' || this.indicatorPosition === 'bottomLeft') style.justifyContent = 'flex-start'
if (this.indicatorPosition === 'topCenter' || this.indicatorPosition === 'bottomCenter') style.justifyContent = 'center'
if (this.indicatorPosition === 'topRight' || this.indicatorPosition === 'bottomRight') style.justifyContent = 'flex-end'
if (['topLeft','topCenter','topRight'].indexOf(this.indicatorPosition) >= 0) {
style.top = '12rpx'
style.bottom = 'auto'
} else {
style.top = 'auto'
style.bottom = '12rpx'
}
}
return style
},
indicatorPointStyle() {
return (index) => {
let style = {}
if (index === this.currentIndex && this.indicatorActiveColor !== '') {
style.backgroundColor = this.indicatorActiveColor
} else if (this.indicatorInactiveColor !== '') {
style.backgroundColor = this.indicatorInactiveColor
}
return style
}
}
},
watch: {
parentData() {
if (this.children.length) {
this.children.forEach((item) => {
// updateParentData()
typeof(item.updateParentData) === 'function' && item.updateParentData()
})
}
},
current(nVal, oVal) {
if (this.currentIndex === nVal) return
this.currentIndex = nVal > this.children.length ? this.children.length - 1 : nVal
this.swiperContainerAnimationFinish = false
//
this.swiperContainerStyle.transitionDuration = `${this.duration + 90}ms`
this.updateSwiperContainerItem(oVal)
}
},
data() {
return {
//
clearAnimationTimer: null,
//
convergeTimer: null,
// Timer
autoPlayTimer: null,
//
currentIndex: this.current,
// swiperContainer
swiperContainerStyle: {
transform: 'translate3d(0px, 0px, 0px)',
transitionDuration: '0ms'
},
// swiperContainer
containerAnimation: {},
//
swiperContainerAnimationFinish: false
}
},
created() {
this.children = []
},
mounted() {
this.$nextTick(() => {
const index = this.currentIndex > this.children.length ? this.children.length - 1 : this.currentIndex
this.updateSwiperContainerStyle(index)
this.startAutoPlay()
})
},
methods: {
// swiperItem
updateAllSwiperItemStyle() {
this.children.forEach((item, index) => {
typeof(item.updateSwiperItemStyle) === 'function' && item.updateSwiperItemStyle(this.children.length)
})
},
// swiperIndexswiperItemContainer
updateSwiperContainerStyle(index) {
if (this.vertical) {
this.swiperContainerStyle.transform = `translate3d(0px, ${-index * 100}%, 0px)`
} else {
this.swiperContainerStyle.transform = `translate3d(${-index * 100}%, 0px, 0px)`
}
},
// swiperItemContainer
updateSwiperContainerStyleWithValue(value) {
if (this.vertical) {
this.swiperContainerStyle.transform = `translate3d(0px, ${(-this.currentIndex * 100) + value * 100}%, 0px)`
} else {
this.swiperContainerStyle.transform = `translate3d(${(-this.currentIndex * 100) + value * 100}%, 0px, 0px)`
}
},
// swiperItemContainer
updateSwiperContainerStyleWithDirection(direction) {
const oldCurrent = this.currentIndex
const childrenLength = this.children.length
const lastSwiperItemIndex = childrenLength - 1
this.swiperContainerAnimationFinish = false
// SwiperItem
if (direction === 'reset') {
//
this.swiperContainerStyle.transitionDuration = `${this.duration}ms`
this.updateSwiperContainerStyle(this.currentIndex)
this.clearAnimationTimer = setTimeout(() => {
this.clearSwiperContainerAnimation()
}, this.duration)
} else if (direction === 'reload') {
this.clearConvergeSwiperItemTimer()
this.clearSwiperContainerAnimation()
this.updateSwiperItemStyle(0)
this.updateSwiperItemStyle(lastSwiperItemIndex)
} else {
if (direction === 'left' || direction === 'up') {
if (oldCurrent === childrenLength - 1 && !this.circular) {
this.clearSwiperContainerAnimation()
this.clearConvergeSwiperItemTimer()
return
}
this.currentIndex = oldCurrent + 1 >= childrenLength ? 0 : oldCurrent + 1
} else if (direction === 'right' || direction === 'down') {
if (oldCurrent === 0 && !this.circular) {
this.clearSwiperContainerAnimation()
this.clearConvergeSwiperItemTimer()
return
}
this.currentIndex = oldCurrent - 1 < 0 ? childrenLength - 1 : oldCurrent - 1
}
//
this.swiperContainerStyle.transitionDuration = `${this.duration + 90}ms`
// this.updateSwiperItemContainerRect(this.currentIndex)
}
// console.log(direction, oldCurrent, this.currentIndex);
this.updateSwiperContainerItem(oldCurrent)
//
this.$emit('change', {
current: this.currentIndex
})
},
//
startAutoPlay() {
if (this.autoplay && !this.autoPlayTimer && this.circular) {
this.autoPlayTimer = setInterval(() => {
this.updateSwiperContainerStyleWithDirection('left')
}, this.interval)
}
},
//
stopAutoPlay() {
if (this.autoPlayTimer) {
clearInterval(this.autoPlayTimer)
this.autoPlayTimer = null
}
},
// swiperContainerswiperItem
updateSwiperContainerItem(oldCurrent) {
const childrenLength = this.children.length
const lastSwiperItemIndex = childrenLength - 1
// SwiperItem
// swiperItemContainer
if (oldCurrent === 0 && this.currentIndex === lastSwiperItemIndex) {
//
// this.swiperContainerStyle.transform = `translate3d(100%, 0px, 0px)`
this.updateSwiperContainerStyle(-1)
this.clearSwiperContainerAnimationTimer()
this.clearAnimationTimer = setTimeout(() => {
this.convergeSwiperItem()
}, this.duration)
} else if (oldCurrent === lastSwiperItemIndex && this.currentIndex === 0) {
//
// this.swiperContainerStyle.transform = `translate3d(${-childrenLength * 100}%, 0px, 0px)`
this.updateSwiperContainerStyle(childrenLength)
this.clearSwiperContainerAnimationTimer()
this.clearAnimationTimer = setTimeout(() => {
this.convergeSwiperItem()
}, this.duration)
} else {
this.updateSwiperContainerStyle(this.currentIndex)
this.updateSwiperItemStyle(0)
this.updateSwiperItemStyle(lastSwiperItemIndex)
this.clearAnimationTimer = setTimeout(() => {
this.clearSwiperContainerAnimation()
}, this.duration)
}
},
// swiperItem
updateSwiperItemStyle(index) {
const childrenLength = this.children.length
if (index < 0) index = 0
if (index > childrenLength - 1) index = childrenLength - 1
typeof(this.children[index].updateSwiperItemStyle) === 'function' && this.children[index].updateSwiperItemStyle(childrenLength, this.currentIndex)
},
// swiperItem
updateSwiperItemContainerRect(index) {
const childrenLength = this.children.length
if (index < 0) index = 0
if (index > childrenLength - 1) index = childrenLength - 1
typeof(this.children[index].getSwiperItemRect) === 'function' && this.children[index].getSwiperItemRect()
},
//
convergeSwiperItem() {
const lastSwiperItemIndex = this.children.length - 1
this.clearSwiperContainerAnimation()
this.clearConvergeSwiperItemTimer()
this.convergeTimer = setTimeout(() => {
this.updateSwiperItemStyle(0)
this.updateSwiperItemStyle(lastSwiperItemIndex)
this.updateSwiperContainerStyle(this.currentIndex)
this.clearConvergeSwiperItemTimer()
}, 30)
},
// /
clearSwiperContainerAnimation() {
this.swiperContainerStyle.transitionDuration = `0ms`
this.swiperContainerAnimationFinish = true
this.clearSwiperContainerAnimationTimer()
},
// /
clearConvergeSwiperItemTimer() {
if (this.convergeTimer) {
clearTimeout(this.convergeTimer)
this.convergeTimer = null
}
},
// /
clearSwiperContainerAnimationTimer() {
if (this.clearAnimationTimer) {
clearTimeout(this.clearAnimationTimer)
this.clearAnimationTimer = null
}
}
}
}
</script>
<style lang="scss" scoped>
.tn-c-swiper {
position: relative;
overflow: hidden;
width: 100%;
height: 100%;
.tn-swiper {
&__container {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
will-change: transform;
transition-property: all;
transition-timing-function: ease-out;
}
&__indicator {
position: absolute;
display: flex;
z-index: 1;
&--horizontal {
padding: 0 24rpx;
flex-direction: row;
width: 100%;
}
&--vertical {
padding: 24rpx 0;
flex-direction: column;
height: 100%;
}
&__rect {
background-color: rgba(0, 0, 0, 0.3);
transition: all 0.5s;
&--horizontal {
width: 26rpx;
height: 8rpx;
}
&--vertical {
width: 8rpx;
height: 26rpx;
}
&--active {
background-color: rgba(255, 255, 255, 0.8);
}
}
&__dot {
width: 14rpx;
height: 14rpx;
border-radius: 20rpx;
background-color: rgba(0, 0, 0, 0.3);
transition: all 0.5s;
&--horizontal {
margin: 0 6rpx;
}
&--vertical {
margin: 6rpx 0;
}
&--active {
background-color: rgba(255, 255, 255, 0.8);
}
}
&__round {
width: 14rpx;
height: 14rpx;
border-radius: 20rpx;
background-color: rgba(0, 0, 0, 0.3);
transition: all 0.5s;
&--horizontal {
margin: 0 6rpx;
}
&--vertical {
margin: 6rpx 0;
}
&--active {
background-color: rgba(255, 255, 255, 0.8);
&--horizontal {
width: 34rpx;
}
&--vertical {
height: 34rpx;
}
}
}
&__number {
padding: 6rpx 16rpx;
line-height: 1;
background-color: rgba(0, 0, 0, 0.3);
color: rgba(255, 255, 255, 0.8);
border-radius: 100rpx;
font-size: 26rpx;
}
}
}
}
</style>

View File

@ -0,0 +1,265 @@
// 判断是否出界
var isOutRange = function(x1, y1, x2, y2, x3, y3) {
return x1 < 0 || x1 >= y1 || x2 < 0 || x2 >= y2 || x3 < 0 || x3 >= y3
}
var edit = false
function bool(str) {
return str === 'true' || str === true
}
/**
* 排序核心
* @param {Object} startKey 开始时位置
* @param {Object} endKey 结束时位置
* @param {Object} instance wxs内的局部变量快照
*/
var sortCore = function(startKey, endKey, state) {
var basedata = state.basedata
var excludeFix = function(sortKey, type) {
// fixed 元素位置不会变化, 这里直接用 sortKey 获取,更加便捷
if (state.list[sortKey].fixed) {
var _sortKey = type ? --sortKey : ++sortKey
return excludeFix(sortKey, type)
}
return sortKey
}
// 先获取到 endKey 对应的 realKey, 防止下面排序过程中该 realKey 被修改
var endRealKey = -1
state.list.forEach(function(item) {
if (item.sortKey === endKey) endRealKey = item.realKey
})
return state.list.map(function(item) {
if (item.fixed) return item
var sortKey = item.sortKey
var realKey = item.realKey
if (startKey < endKey) {
// 正序拖动
if (sortKey > startKey && sortKey <= endKey) {
--realKey
sortKey = excludeFix(--sortKey, true)
} else if (sortKey === startKey) {
realKey = endRealKey
sortKey = endKey
}
} else if (startKey > endKey) {
// 倒序拖动
if (sortKey >= endKey && sortKey < startKey) {
++realKey
sortKey = excludeFix(++sortKey, false)
} else if (sortKey === startKey) {
realKey = endRealKey
sortKey = endKey
}
}
if (item.sortKey != sortKey) {
item.translateX = (sortKey % basedata.columns) * 100 + '%'
item.translateY = Math.floor(sortKey / basedata.columns) * 100 + '%'
item.sortKey = sortKey
item.realKey = realKey
}
return item
})
}
var triggerCustomEvent = function(list, type, instance) {
if (!instance) return
var _list = [],
listData = [];
list.forEach(function(item) {
_list[item.sortKey] = item
})
_list.forEach(function(item) {
listData.push(item.data)
})
// 编译到小程序 funcName作为参数传递导致事件不执行
switch(type) {
case 'change':
instance.callMethod('change', {data: listData})
break
case 'sortEnd':
instance.callMethod('sortEnd', {data: listData})
break
}
}
var listObserver = function(newVal, oldVal, ownerInstance, instance) {
var state = ownerInstance.getState()
state.itemsInstance = ownerInstance.selectAllComponents('.tn-drag__item')
state.list = newVal || []
state.list.forEach(function(item, index) {
var itemInstance = state.itemsInstance[index]
if (item && itemInstance) {
itemInstance.setStyle({
'transform': 'translate3d('+ item.translateX + ',' + item.translateY +', 0)'
})
if (item.fixed) itemInstance.addClass('tn-drag__fixed')
}
})
}
var baseDataObserver = function(newVal, oldVal, ownerInstance, instance) {
var state = ownerInstance.getState()
state.basedata = newVal
}
var longPress = function(event, ownerInstance) {
var instance = event.instance
var dataset = instance.getDataset()
var state = ownerInstance.getState()
edit = bool(dataset.edit)
if (!edit) return
if (!state.basedata || state.basedata === 'undefined') {
state.basedata = JSON.parse(dataset.basedata)
}
var basedata = state.basedata
var touches = event.changedTouches[0]
if (!touches) return
state.current = +dataset.index
// 初始项是固定项则返回
var item = state.list[state.current]
if (item && item.fixed) return
// 如果已经在 drag 中则返回, 防止多指触发 drag 动作, touchstart 事件中有效果
if (state.dragging) return
ownerInstance.callMethod("drag", {
dragging: true
})
// 计算X, Y轴初始位移使item中心移动到点击处单列的时候X轴初始不做位移
state.translateX = basedata.columns === 1 ? 0 : touches.pageX - (basedata.itemWidth / 2 + basedata.left)
state.translateY = touches.pageY - (basedata.itemHeight / 2 + basedata.top)
state.touchId = touches.identifier
instance.setStyle({
'transform': 'translate3d(' + state.translateX + 'px,' + state.translateY +'px, 0)'
})
state.itemsInstance.forEach(function(item, index) {
item.removeClass("tn-drag__transition").removeClass("tn-drag__current")
item.addClass(index === state.current ? "tn-drag__current" : "tn-drag__transition")
})
ownerInstance.callMethod("vibrate")
state.dragging = true
}
var touchStart = function(event, ownerInstance) {
var instance = event.instance
var dataset = instance.getDataset()
edit = bool(dataset.edit)
}
var touchMove = function(event, ownerInstance) {
var instance = event.instance
var dataset = instance.getDataset()
var state = ownerInstance.getState()
var basedata = state.basedata
if (!state.dragging || !edit) return
var touches = event.changedTouches[0]
if (!touches) return
// 如果不是同一个触发点则返回
if (state.touchId !== touches.identifier) return
// 计算X,Y轴位移, 单列时候X轴初始不做位移
var translateX = basedata.columns === 1 ? 0 : touches.pageX - (basedata.itemWidth / 2 + basedata.left)
var translateY = touches.pageY - (basedata.itemHeight / 2 + basedata.top)
// 到顶到低自动滑动
if (touches.clientY > basedata.windowHeight - basedata.itemHeight - basedata.realBottomSize) {
// 当前触摸点pageY + item高度 - (屏幕高度 - 底部固定区域高度)
ownerInstance.callMethod('pageScroll', {
scrollTop: touches.pageY + basedata.itemHeight - (basedata.windowHeight - basedata.realBottomSize)
})
} else if (touches.clientY < basedata.itemHeight + basedata.realTopSize) {
// 当前触摸点pageY - item高度 - 顶部固定区域高
ownerInstance.callMethod('pageScroll', {
scrollTop: touches.pageY - basedata.itemHeight - basedata.realTopSize
})
}
// 设置当前激活元素的偏移量
instance.setStyle({
'transform': 'translate3d('+ translateX + 'px,' + translateY + 'px, 0)'
})
var startKey = state.list[state.current].sortKey
var currentX = Math.round(translateX / basedata.itemWidth)
var currentY = Math.round(translateY / basedata.itemHeight)
var endKey = currentX + basedata.columns * currentY
// 目标项时固定项则返回
var item = state.list[endKey]
if (item && item.fixed) return
// X轴或者Y轴超出范围则返回
if (isOutRange(currentX, basedata.columns, currentY, basedata.rows, endKey, state.list.length)) return
// 防止拖拽过程中发生乱序问题
if (startKey === endKey || startKey === state.preStartKey) return
state.preStartKey = startKey
var list = sortCore(startKey, endKey, state)
state.itemsInstance.forEach(function(itemInstance, index) {
var item = list[index]
if (index !== state.current) {
itemInstance.setStyle({
'transform': 'translate3d('+ item.translateX + ',' + item.translateY +', 0)'
})
}
})
// ownerInstance.callMethod('vibrate')
ownerInstance.callMethod('listDataChange', {
data: list
})
triggerCustomEvent(list, "change", ownerInstance)
}
var touchEnd = function(event, ownerInstance) {
var instance = event.instance
var dataset = instance.getDataset()
var state = ownerInstance.getState()
var basedata = state.basedata
if (!state.dragging || !edit) return
triggerCustomEvent(state.list, "sortEnd", ownerInstance)
instance.addClass('tn-drag__transition')
instance.setStyle({
'transform': 'translate3d('+ state.list[state.current].translateX + ',' + state.list[state.current].translateY + ', 0)'
})
state.itemsInstance.forEach(function(item, index) {
item.removeClass('tn-drag__transition')
})
state.preStartKey = -1
state.dragging = false
ownerInstance.callMethod('drag', {
dragging: false
})
state.current = -1
state.translateX = 0
state.translateY = 0
}
module.exports = {
longPress: longPress,
touchStart: touchStart,
touchMove: touchMove,
touchEnd: touchEnd,
baseDataObserver: baseDataObserver,
listObserver: listObserver
}

View File

@ -0,0 +1,278 @@
<template>
<view
class="tn-drag-class tn-drag"
:style="{
height: wrapHeight + 'rpx'
}"
:list="listData"
:basedata="baseData"
:change:list="wxs.listObserver"
:change:basedata="wxs.baseDataObserver"
>
<!-- #ifdef MP-WEIXIN -->
<view
v-for="(item, index) in listData"
:key="item.id"
class="tn-drag__item"
:style="{
width: 100 / columns + '%',
height: itemHeight + 'rpx'
}"
:data-index="index"
:data-basedata="baseData"
:data-edit="edit"
@longpress="wxs.longPress"
@touchstart="wxs.touchStart"
:catch:touchmove="dragging?wxs.touchMove:''"
:catch:touchend="dragging?wxs.touchEnd:''"
>
<slot :entity="item.data" :fixed="item.fixed" :index="index" :height="itemHeight" :isEdit="edit"></slot>
</view>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<view
v-for="(item, index) in listData"
:key="item.id"
class="tn-drag__item"
:style="{
width: 100 / columns + '%',
height: itemHeight + 'rpx'
}"
@longpress="wxs.longPress"
:data-index="index"
:data-basedata="baseData"
:data-edit="edit"
@touchstart="wxs.touchStart"
@touchmove="wxs.touchMove"
@touchend="wxs.touchEnd"
>
<slot :entity="item.data" :fixed="item.fixed" :index="index" :height="itemHeight" :isEdit="edit"></slot>
</view>
<!-- #endif -->
</view>
</template>
<script src="./index.wxs" lang="wxs" module="wxs"></script>
<script>
export default {
name: 'tn-drag',
props: {
//
// fixed
list: {
type: Array,
default () {
return []
}
},
//
edit: {
type: Boolean,
default: true
},
//
columns: {
type: Number,
default: 3
},
// item, rpx
itemHeight: {
type: Number,
default: 0
},
//
scrollTop: {
type: Number,
default: 0
}
},
computed: {
wrapHeight() {
return this.rows * this.itemHeight
}
},
data() {
return {
//
baseData: {},
//
dragData: [],
//
rows: 0,
//
listData: [],
//
dragging: false
}
},
watch: {
list(val) {
this.listData = []
this.$nextTick(() => {
this.init()
})
},
columns(val) {
this.listData = []
this.$nextTick(() => {
this.init()
})
}
},
mounted() {
this.$nextTick(() => {
this.init()
})
},
methods: {
//
init() {
this.dragging = true
const initDragItem = item => {
const obj = {
...item
}
const fixed = obj?.fixed || false
delete obj["fixed"]
return {
id: this.unique(),
fixed,
data: {
...obj
}
}
}
let i = 0
const listData = (this.list || []).map((item, index) => {
let listItem = initDragItem(item)
//
listItem.realKey = i++
//
listItem.sortKey = index
listItem.translateX = `${(listItem.sortKey % this.columns) * 100}%`
listItem.translateY = `${Math.floor(listItem.sortKey / this.columns) * 100}%`
return listItem
})
this.rows = Math.ceil(listData.length / this.columns)
this.listData = listData
this.dragData = listData
if (listData.length === 0) return
// console.log(listData);
// dom
this.$nextTick(() => {
this.initRect()
})
},
// dom
initRect() {
const {
windowWidth,
windowHeight
} = uni.getSystemInfoSync()
let baseData = {}
baseData.windowHeight = windowHeight
baseData.realTopSize = 0
baseData.realBottomSize = 0
baseData.columns = this.columns
baseData.rows = this.rows
const query = uni.createSelectorQuery().in(this)
query.select('.tn-drag').boundingClientRect()
query.select('.tn-drag__item').boundingClientRect()
query.exec(res => {
if (!res) {
setTimeout(() => {
this.initRect()
}, 10)
return
}
baseData.itemWidth = res[1].width
baseData.itemHeight = res[1].height
baseData.left = res[0].left
baseData.top = res[0].top + this.scrollTop
this.dragging = false
this.baseData = baseData
})
},
//
vibrate() {
uni.vibrateShort()
},
//
pageScroll(e) {
uni.pageScrollTo({
scrollTop: e.scrollTop,
duration: 0
})
},
//
drag(e) {
this.dragging = e.dragging
},
//
listDataChange(e) {
this.dragData = e.data
},
// item
itemClick(index) {
const item = this.dragData[index]
this.$emit('click', {
key: item.realKey,
data: item.data
})
},
//
sortEnd(e) {
this.$emit('end', {
data: e.data
})
},
//
change(e) {
this.$emit('change', {
data: e.data
})
},
// id
unique(n = 6) {
let id = ''
for (let i = 0; i < n; i++) id += Math.floor(Math.random() * 10)
return 'tn_' + new Date().getTime() + id
}
}
}
</script>
<style lang="scss" scoped>
.tn-drag {
position: relative;
&__item {
position: absolute;
z-index: 2;
top: 0;
left: 0;
}
&__transition {
transition: transform 0.25s !important;
}
&__current {
z-index: 10 !important;
}
&__fixed {
z-index: 1 !important;
}
}
</style>

View File

@ -0,0 +1,190 @@
<template>
<view v-if="show" class="tn-empty-class tn-empty" :style="[emptyStyle]">
<view
v-if="!isImage"
class="tn-empty__icon"
:class="[icon ? `tn-icon-${icon}` : `tn-icon-empty-${mode}`]"
:style="[iconStyle]"
></view>
<image
v-else
class="tn-empty__image"
:style="[imageStyle]"
:src="icon"
mode="widthFix"
></image>
<view
class="tn-empty__text"
:style="[textStyle]"
>{{ text ? text : icons[mode]}}</view>
<view v-if="$slots.default || $slots.$default" class="tn-empty__slot">
<slot/>
</view>
</view>
</template>
<script>
export default {
name: 'tn-empty',
props: {
//
show: {
type: Boolean,
default: true
},
// icon
// 使
icon: {
type: String,
default: ''
},
//
mode: {
type: String,
default: 'data'
},
//
text: {
type: String,
default: ''
},
//
textColor: {
type: String,
default: ''
},
// rpx
textSize: {
type: Number,
default: 0
},
//
iconColor: {
type: String,
default: ''
},
// rpx
iconSize: {
type: Number,
default: 0
},
// rpx
imgWidth: {
type: Number,
default: 0
},
// rpx
imgHeight: {
type: Number,
default: 0
},
//
customStyle: {
type: Object,
default() {
return {}
}
}
},
computed: {
emptyStyle() {
let style = {}
Object.assign(style, this.customStyle)
return style
},
iconStyle() {
let style = {}
if (this.iconSize) {
style.fontSize = this.iconSize + 'rpx'
}
if (this.iconColor) {
style.color = this.iconColor
}
return style
},
imageStyle() {
let style = {}
if (this.imgWidth) {
style.width = this.imgWidth + 'rpx'
}
if (this.imgHeight) {
style.height = this.imgHeight + 'rpx'
}
return style
},
textStyle() {
let style = {}
if (this.textColor) {
style.color = this.textColor
}
if (this.textSize) {
style.fontSize = this.textSize + 'rpx'
}
return style
},
// icon
isImage() {
return this.icon.indexOf('/') >= 0
}
},
data() {
return {
icons: {
cart: '购物车为空',
page: '页面不存在',
search: '搜索结果为空',
address: '地址为空',
network: '网络不通',
order: '订单为空',
coupon: '优惠券为空',
favor: '暂无收藏',
permission: '无权限',
history: '历史记录为空',
message: '暂无消息',
list: '列表为空',
data: '暂无数据',
comment: '暂无评论'
}
}
}
}
</script>
<style lang="scss" scoped>
.tn-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
&__icon {
margin-top: 14rpx;
color: #AAAAAA;
font-size: 90rpx;
}
&__image {
width: 160rpx;
height: 160rpx;
}
&__text {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-top: 20rpx;
color: #AAAAAA;
font-size: 30rpx;
}
&__slot {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-top: 20rpx;
}
}
</style>

View File

@ -0,0 +1,523 @@
<template>
<view class="tn-fab-class tn-fab" @touchmove.stop.prevent>
<view
class="tn-fab__box"
:class="{'tn-fab--right': left === 'auto'}"
:style="{
left: $t.string.getLengthUnitValue(fabLeft || left),
right: $t.string.getLengthUnitValue(fabRight || right),
bottom: $t.string.getLengthUnitValue(fabBottom || bottom),
zIndex: elZIndex
}"
>
<view
v-if="visibleSync"
class="tn-fab__btns"
:class="[`tn-fab__btns__animation--${animationType}`,
showFab ? `tn-fab__btns--visible--${animationType}` : ''
]"
>
<view
v-for="(item, index) in btnList"
:key="index"
class="tn-fab__item"
:class="[
`tn-fab__item__animation--${animationType}`,
{'tn-fab__item--left': right === 'auto'}
]"
:style="[fabItemStyle(index)]"
@tap.stop="handleClick(index)"
>
<!-- 带图标或者图片时显示的文字信息 -->
<view
v-if="animationType !== 'around' && (item.imgUrl || item.icon)"
:class="[left === 'auto' ? 'tn-fab__item__text--right' : 'tn-fab__item__text--left']"
:style="{
color: item.textColor || '#FFF',
fontSize: $t.string.getLengthUnitValue(item.textSize || 28)
}"
>{{ item.text || '' }}</view>
<!-- 带图片或者图标时的图片图标信息 -->
<view
class="tn-fab__item__btn"
:class="[!item.bgColor ? backgroundColorClass : '']"
:style="{
width: $t.string.getLengthUnitValue(width),
height: $t.string.getLengthUnitValue(height),
lineHeight: $t.string.getLengthUnitValue(height),
backgroundColor: item.bgColor || backgroundColorStyle || '#01BEFF',
borderRadius: $t.string.getLengthUnitValue(radius)
}"
>
<!-- 无图片和无图标时只显示文字 -->
<view
v-if="!item.imgUrl && !item.icon"
class="tn-fab__item__btn__title"
:style="{
color: item.textColor || '#fff',
fontSize: $t.string.getLengthUnitValue(item.textSize || 28)
}"
>{{ item.text || '' }}</view>
<!-- 图标 -->
<view
v-if="item.icon"
class="tn-fab__item__btn__icon"
:class="[`tn-icon-${item.icon}`]"
:style="{
color: item.iconColor || '#fff',
fontSize: $t.string.getLengthUnitValue(item.iconSize || iconSize || 64)
}"
></view>
<!-- 图片 -->
<image
v-else-if="!item.icon && item.imgUrl"
class="tn-fab__item__btn__image"
:style="{
width: $t.string.getLengthUnitValue(item.imgWidth || 64),
height: $t.string.getLengthUnitValue(item.imgHeight || 64),
}"
:src="item.imgUrl"
></image>
</view>
</view>
</view>
<view
class="tn-fab__item__btn tn-fab__item__btn--fab"
:class="[backgroundColorClass, fontColorClass, {'tn-fab__item__btn--active': showFab}]"
:style="{
width: $t.string.getLengthUnitValue(width),
height: $t.string.getLengthUnitValue(height),
backgroundColor: backgroundColorStyle || !backgroundColorClass ? '#01BEFF' : '',
color: fontColorStyle || '#fff',
borderRadius: $t.string.getLengthUnitValue(radius),
zIndex: elZIndex - 1
}"
@tap.stop="fabClick"
>
<slot>
<view class="tn-fab__item__btn__icon" :class="[`tn-icon-${icon}`]" :style="{fontSize: $t.string.getLengthUnitValue(iconSize || 64)}"></view>
</slot>
</view>
</view>
<view v-if="visibleSync && showMask" class="tn-fab__mask" :class="{'tn-fab__mask--visible': showFab}" :style="{zIndex: elZIndex - 3}" @tap="clickMask()"></view>
</view>
</template>
<script>
import componentsColorMixin from '../../libs/mixin/components_color.js'
export default {
name: 'tn-fab',
mixins: [componentsColorMixin],
props: {
//
// {
// //
// bgColor: '#fff',
// //
// imgUrl: '',
// //
// imgWidth: 60,
// //
// imgHeight: 60,
// //
// icon: '',
// //
// iconSize: 60,
// //
// iconColor: '#fff',
// //
// text: '',
// //
// textSize: 30,
// //
// textColor: '#fff'
// }
btnList: {
type: Array,
default() {
return []
}
},
//
customBtn: {
type: Boolean,
default: false
},
//
width: {
type: [String, Number],
default: 88
},
//
height: {
type: [String, Number],
default: 88
},
//
iconSize: {
type: [String, Number],
default: 64
},
//
icon: {
type: String,
default: 'open'
},
//
radius: {
type: [String, Number],
default: '50%'
},
//
left: {
type: [String, Number],
default: 'auto'
},
//
right: {
type: [String, Number],
default: 'auto'
},
//
bottom: {
type: [String, Number],
default: 100
},
// up around
animationType: {
type: String,
default: 'up'
},
// , px
aroundBtnDistance: {
type: Number,
default: 10
},
zIndex: {
type: Number,
default: 0
},
//
showMask: {
type: Boolean,
default: true
},
//
maskCloseable: {
type: Boolean,
default: true
}
},
data() {
return {
showFab: false,
visibleSync: false,
timer: null,
fabLeft: 0,
fabRight: 0,
fabBottom: 0,
fabBtnInfo: {
width: 0,
height: 0,
left: 0,
right: 0,
bottom: 0
},
systemInfo: {
width: 0,
height: 0
},
updateProps: false
}
},
computed: {
elZIndex() {
return this.zIndex || this.$t.zIndex.fab
},
propsData() {
return [this.width, this.height, this.left, this.right, this.bottom]
},
fabItemStyle() {
return (index) => {
let style = {
zIndex: this.elZIndex - 2
}
if (this.animationType === 'up' || !this.showFab) {
return style
}
let base = 1
if (this.left === 'auto') {
base = 1
} else if (this.right === 'auto') {
base = -1
}
style.transform = `rotate(${base * index * 60}deg) translateX(${(this.aroundBtnDistance + this.fabBtnInfo.width) * (-(base))}px)`
return style
}
}
},
watch: {
propsData() {
//
this.updateProps = true
}
},
mounted() {
this.$nextTick(() => {
this.getFabBtnRectInfo()
})
},
beforeDestroy() {
if (this.timer) {
clearTimeout(this.timer)
}
},
methods: {
//
handleClick(index) {
this.close()
this.$emit('click', {index: index})
},
//
fabClick() {
if (this.showFab) {
this.close()
} else {
// console.log(this.visibleSync);
if (this.visibleSync) {
this.visibleSync = false
return
}
this.open()
}
},
//
clickMask() {
if (!this.showMask || !this.maskCloseable) return
this.close()
},
open() {
this.change('visibleSync', 'showFab', true)
this.translateFabPosition()
},
close() {
this.change('showFab', 'visibleSync', false)
this.fabLeft = 0
this.fabRight = 0
this.fabBottom = 0
},
//
//
change(param1, param2, status) {
this[param1] = status
if (status) {
// #ifdef H5 || MP
this.timer = setTimeout(() => {
this[param2] = status
this.$emit(status ? 'open' : 'close')
clearTimeout(this.timer)
}, 10)
// #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')
clearTimeout(this.timer)
}, 250)
}
},
/******************** 旋转动画相关函数 ********************/
//
async getFabBtnRectInfo() {
const systemInfo = uni.getSystemInfoSync()
const btnRectInfo = await this._tGetRect('.tn-fab__item__btn--fab')
if (!btnRectInfo) {
setTimeout(() => {
this.getFabBtnRectInfo()
}, 10)
return
}
console.log(btnRectInfo);
this.systemInfo = {
width: systemInfo.windowWidth,
height: systemInfo.windowHeight
}
this.fabBtnInfo.width = btnRectInfo.width
this.fabBtnInfo.height = btnRectInfo.height
this.fabBtnInfo.left = btnRectInfo.left
this.fabBtnInfo.right = btnRectInfo.right
this.fabBtnInfo.bottom = btnRectInfo.bottom
},
//
translateFabPosition() {
if (this.updateProps) {
this.getFabBtnRectInfo()
this.updateProps = false
}
if (this.animationType === 'up') return
//
const btnGroupWidth = this.fabBtnInfo.width + this.aroundBtnDistance + 10
//
if (this.left === 'auto' && btnGroupWidth > this.systemInfo.width - this.fabBtnInfo.right) {
//
this.fabRight = btnGroupWidth + 'px'
} else if (this.right === 'auto' && btnGroupWidth > this.fabBtnInfo.left) {
this.fabLeft = btnGroupWidth + 'px'
}
if (btnGroupWidth > this.systemInfo.height - this.fabBtnInfo.bottom) {
this.fabBottom = btnGroupWidth + 'px'
}
}
}
}
</script>
<style lang="scss" scoped>
.tn-fab {
&__box {
display: flex;
justify-content: center;
align-items: flex-start;
flex-direction: column;
position: fixed;
transition: all 0.25s ease-in-out;
}
&--right {
align-items: flex-end;
}
&__btns {
transition: all 0.25s cubic-bezier(0,.13,0,1.43);
transform-origin: 80% bottom;
&__animation--up {
opacity: 0;
transform: translateY(100%);
}
&__animation--around {
position: absolute;
top: 0;
left: 0;
}
&--visible--up {
// visibility: visible;
opacity: 1;
transform: translateY(0);
}
&--visible--around {
// visibility: visible;
// opacity: 1;
}
}
&__item {
display: flex;
justify-content: flex-end;
align-items: center;
padding-bottom: 20rpx;
&__animation--around {
position: absolute;
top: 0;
left: 0;
transition: transform 0.25s ease-in-out;
transform-origin: 50% 50%;
padding-bottom: 0 !important;
}
&--left {
flex-flow: row-reverse;
}
&__text {
&--left {
padding-left: 14rpx;
}
&--right {
padding-right: 14rpx;
}
}
&__btn {
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 5rpx 2rpx rgba(0, 0, 0, 0.07);
transition: all 0.2s linear;
&--active {
animation-name: fab-button-animation;
animation-duration: 0.2s;
animation-timing-function: cubic-bezier(0,.13,0,1.43);
}
&__title {
width: 90%;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&__icon {
text-align: center;
font-size: 64rpx;
}
&__image {
display: block;
}
}
}
&__mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: $tn-mask-bg-color;
transition: all 0.2s ease-in-out;
opacity: 0;
&--visible {
opacity: 1;
}
}
}
@keyframes fab-button-animation {
0% {
transform: scale(0.6);
}
// 20% {
// transform: scale(1.8);
// }
// 40% {
// transform: scale(0.4);
// }
// 50% {
// transform: scale(1.4);
// }
// 80% {
// transform: scale(0.8);
// }
100% {
transform: scale(1);
}
}
</style>

View File

@ -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')labelauto
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: '',
//
// computedthis.parentdata
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) {
// parentDataparentparentData
Object.keys(this.parentData).map(key => {
this.parentData[key] = this.parent[key]
})
// proptnForm使form-item
if (this.prop) {
//
this.parent.fields.push(this)
this.errorType = this.parent.errorType
//
this.initialValue = this.fieldValue
// $nextTicktn-formrulesref
// $nextTickreftn-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-formtn-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)
},
// formrulesform-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
// blurchange
// 使indexOftrigger['blur','change']
return rules.filter(rule => rule.trigger && rule.trigger.indexOf(triggerType) !== -1)
},
//
validation(trigger, callback = ()=>{}) {
//
this.fieldValue = this.parent.model[this.prop]
// blurchange
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>

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() {
// formform-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-itemvalidation
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.message.toast(errors[0])
}
//
if (typeof callback == 'function') callback(valid)
}
})
})
})
}
}
}
</script>
<style>
</style>

View File

@ -0,0 +1,382 @@
<template>
<view
class="tn-goods-nav-class tn-goods-nav"
:class="[
backgroundColorClass,
{
'tn-goods-nav--fixed': fixed,
'tn-safe-area-inset-bottom': safeAreaInsetBottom,
'tn-goods-nav--shadow': shadow
}
]"
:style="[backgroundColorStyle, navStyle]"
>
<view class="options">
<view
v-for="(item, index) in optionsData"
:key="index"
class="options__item"
:class="[{'options__item--avatar': item.showAvatar}]"
@tap="handleOptionClick(index)"
>
<block v-if="item.showAvatar">
<tn-avatar
:src="item.avatar"
size="sm"
:badge="item.showBadge"
:badgeText="item.count"
:badgeBgColor="item.countBackgroundColor"
:badgeColor="item.countFontColor"
:badgeSize="26"
></tn-avatar>
</block>
<block v-else>
<view class="options__item__icon" :class="[`tn-icon-${item.icon}`]" :style="[optionStyle(index, 'icon')]">
<tn-badge v-if="item.showBadge" :absolute="true" :backgroundColor="item.countBackgroundColor" :fontColor="item.countFontColor" :fontSize="16" padding="2rpx 5rpx">{{ item.count }}</tn-badge>
</view>
<view class="options__item__text" :style="[optionStyle(index, 'text')]">{{ item.text }}</view>
</block>
</view>
</view>
<view class="buttons">
<view
v-for="(item, index) in buttonGroupsData"
:key="index"
class="buttons__item"
:class="[buttonClass(index)]"
:style="[buttonStyle(index)]"
@tap="handleButtonClick(index)"
>
<view class="buttons__item__text">{{ item.text }}</view>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'tn-goods-nav',
props: {
//
// 3
// {
// icon: '', //
// text: '', //
// count: '', //
// countBackgroundColor: '', //
// countFontColor: '', //
// iconColor: '', //
// textColor: '', //
// avatar: '', //
// }
options: {
type: Array,
default() {
return [{
icon: 'shop',
text: '店铺'
},{
icon: 'service',
text: '客服'
},{
icon: 'star',
text: '收藏'
}]
}
},
//
// 2
// {
// text: '', //
// backgroundColor: '', //
// color: '' //
// }
buttonGroups: {
type: Array,
default() {
return [{
text: '加入购物车',
backgroundColor: '#FFA726',
color: '#FFFFFF'
},{
text: '结算',
backgroundColor: '#FF7043',
color: '#FFFFFF'
}]
}
},
//
backgroundColor: {
type: String,
default: ''
},
// rpx
height: {
type: Number,
default: 0
},
//
shadow: {
type: Boolean,
default: false
},
//
zIndex: {
type: Number,
default: 0
},
//
// rect -> paddingRect -> round ->
buttonType: {
type: String,
default: 'rect'
},
//
fixed: {
type: Boolean,
default: false
},
// iPhoneX
safeAreaInsetBottom: {
type: Boolean,
default: false
}
},
computed: {
backgroundColorStyle() {
return this.$t.color.getBackgroundColorStyle(this.backgroundColor)
},
backgroundColorClass() {
return this.$t.color.getBackgroundColorInternalClass(this.backgroundColor)
},
navStyle() {
let style = {}
if (this.height) {
style.height = this.height + 'rpx'
}
style.zIndex = this.zIndex ? this.zIndex : this.$t.zIndex.goodsNav
return style
},
// style
optionStyle() {
return (index, type) => {
let style = {}
const item = this.optionsData[index]
if (type === 'icon' && item.iconColor) {
style.color = item.iconColor
} else if (type === 'text' && item.fontColor) {
style.color = item.fontColor
}
return style
}
},
// class
buttonClass() {
return (index) => {
let clazz = ''
const item = this.buttonGroupsData[index]
if (item.backgroundColorClass) {
clazz += ` ${item.backgroundColorClass}`
}
if (item.colorClass) {
clazz += ` ${item.colorClass}`
}
clazz += ` buttons__item--${this.$t.string.humpConvertChar(this.buttonType, '-')}`
return clazz
}
},
// style
buttonStyle() {
return (index) => {
let style = {}
const item = this.buttonGroupsData[index]
if (item.backgroundColorStyle) {
style.backgroundColor = item.backgroundColorStyle
}
if (item.colorStyle) {
style.color = item.colorStyle
}
return style
}
}
},
watch: {
options(val) {
this.initData()
},
buttonGroups(val) {
this.initData()
}
},
data() {
return {
//
optionsData: [],
//
buttonGroupsData: []
}
},
created() {
this.initData()
},
methods: {
//
initData() {
this.handleOptionsData()
this.handleButtonGroupsData()
},
//
handleOptionClick(index) {
this.$emit('optionClick', {
index: index
})
},
//
handleButtonClick(index) {
this.$emit('buttonClick', {
index: index
})
},
//
handleOptionsData() {
this.optionsData = this.options.map((item) => {
let option = {...item}
option.showAvatar = item.hasOwnProperty('avatar')
if (item.hasOwnProperty('count')) {
const count = this.$t.number.formatNumberString(item.count, 2)
option.showBadge = true
option.count = typeof count === 'number' ? String(count) : count
option.countBackgroundColor = item.countBackgroundColor ? item.countBackgroundColor : '#01BEFF'
option.countFontColor = item.countFontColor ? item.countFontColor : '#FFFFFF'
}
return option
})
},
//
handleButtonGroupsData() {
this.buttonGroupsData = this.buttonGroups.map((item) => {
let button = {...item}
if (item.hasOwnProperty('backgroundColor')) {
button.backgroundColorClass = this.$t.color.getBackgroundColorInternalClass(item.backgroundColor)
button.backgroundColorStyle = this.$t.color.getBackgroundColorStyle(item.backgroundColor)
}
if (item.hasOwnProperty('color')) {
button.colorClass = this.$t.color.getBackgroundColorInternalClass(item.color)
button.colorStyle = this.$t.color.getBackgroundColorStyle(item.color)
}
return button
})
}
}
}
</script>
<style lang="scss" scoped>
.tn-goods-nav {
background-color: #FFFFFF;
display: flex;
flex-direction: row;
align-items: center;
height: 88rpx;
width: 100%;
box-sizing: content-box;
&--shadow {
box-shadow: 0rpx -10rpx 30rpx 0rpx rgba(0, 0, 0, 0.05);
&::before {
content: " ";
position: absolute;
width: 100%;
height: 100%;
bottom: 0;
left: 0;
right: 0;
margin: auto;
background-color: transparent;
z-index: -1;
}
}
&--fixed {
position: fixed;
bottom: 0;
left: 0;
right: 0;
}
.options {
display: flex;
flex-direction: row;
align-items: center;
height: 100%;
color: #AAAAAA;
&__item {
padding: 0 26rpx;
&--avatar {
padding: 0rpx 0rpx 0rpx 26rpx !important;
}
&__icon {
position: relative;
font-size: 36rpx;
margin-bottom: 6rpx;
}
&__text {
font-size: 22rpx;
}
}
}
.buttons {
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
height: 100%;
&__item {
flex: 1;
padding: 0 10rpx;
display: flex;
align-items: center;
justify-content: center;
&--rect {
height: 100%;
}
&--padding-rect {
height: 80%;
}
&--round {
height: 75%;
&:first-child {
border-top-left-radius: 100rpx;
border-bottom-left-radius: 100rpx;
}
&:last-child {
border-top-right-radius: 100rpx;
border-bottom-right-radius: 100rpx;
}
}
&__text {
display: inline-block;
font-weight: bold;
font-size: 30rpx;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
}
}
}
</style>

View File

@ -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>

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() {
// childrendata
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>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,645 @@
<template>
<view v-if="!disabled" class="tn-image-upload-class tn-image-upload">
<block v-if="showUploadList">
<view
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>
</block>
<!-- <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
// watchthis.lists
// sometrueeverytrue
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, lists, 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.message.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.message.loading('重新上传')
this.uploadFile(index)
},
//
async uploadFile(index = 0) {
if (this.disabled) return
if (this.uploading) return
//
if (index >= this.lists.length) {
this.$emit('on-uploaded', this.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)thisthis
// bind()thisthisthis
// uploadtn-formthis.$parenttn-formthis
// this$parentthis.$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 => {
// jsonjson
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.message.closeLoading()
this.uploading = false
this.uploadFile(index + 1)
this.$emit('on-change', res, index, this.lists, this.index)
}
})
this.lists[index].uploadTask = task
task.onProgressUpdate(res => {
if (res.progress > 0) {
this.lists[index].progress = res.progress
this.$emit('on-progress', res, index, this.lists, this.index)
}
})
},
//
uploadError(index, err) {
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.message.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>

View File

@ -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>

View File

@ -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() {
// createdchildrensdata
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>

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 && showRightIcon && rightIcon !== ''"
class="tn-input__right-icon__item tn-input__right-icon__clear"
>
<view class="icon" :class="[`tn-icon-${rightIcon}`]"></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
},
// typepassword
passwordIcon: {
type: Boolean,
default: true
},
// type inputtextarea
border: {
type: Boolean,
default: false
},
//
borderColor: {
type: String,
default: '#dcdfe6'
},
// typeselectselect
selectOpen: {
type: Boolean,
default: false
},
//
clearable: {
type: Boolean,
default: true
},
//
cursorSpacing: {
type: Number,
default: 0
},
// selectionStartselectionEnd使
//
selectionStart: {
type: Number,
default: -1
},
//
selectionEnd: {
type: Number,
default: -1
},
//
trim: {
type: Boolean,
default: true
},
//
showConfirmBar: {
type: Boolean,
default: true
},
//
showRightIcon: {
type: Boolean,
default: false
},
//
rightIcon: {
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
// typeselectinput
// 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-itemthis.$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-itemthis.$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>

View File

@ -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>

View File

@ -0,0 +1,225 @@
<template>
<view class="tn-landscape-class tn-landscape">
<view v-if="showValue" class="tn-landscape__container" :style="[containerStyle]">
<slot></slot>
<view
v-if="closeBtn"
class="tn-landscape__icon tn-icon-close-fill"
:class="[{
'tn-landscape__icon--left-top': closePosition === 'leftTop',
'tn-landscape__icon--right-top': closePosition === 'rightTop',
'tn-landscape__icon--bottom': closePosition === 'bottom'
}]"
:style="[closeBtnStyle]"
@tap="close"
></view>
</view>
<view
v-if="mask"
class="tn-landscape__mask"
:class="[{'tn-landscape__mask--show': showValue}]"
:style="[maskStyle]"
@tap="closeMask"
></view>
</view>
</template>
<script>
export default {
name: 'tn-landscape',
props: {
//
show: {
type: Boolean,
default: false
},
//
closeBtn: {
type: Boolean,
default: true
},
//
closeColor: {
type: String,
default: ''
},
// rpx
closeSize: {
type: Number,
default: 0
},
//
// leftTop -> rightTop -> bottom ->
closePosition: {
type: String,
default: 'rightTop'
},
// toprpx
// leftToprightTop
closeTop: {
type: Number,
default: 0
},
// rightrpx
// RightTop
closeRight: {
type: Number,
default: 0
},
// bottomrpx
// bottom
closeBottom: {
type: Number,
default: 0
},
// leftrpx
// leftTop
closeLeft: {
type: Number,
default: 0
},
//
mask: {
type: Boolean,
default: true
},
//
maskCloseable: {
type: Boolean,
default: true
},
// zIndex
zIndex: {
type: Number,
default: 0
}
},
computed: {
containerStyle() {
let style = {}
style.zIndex = this.zIndex ? this.zIndex : this.$t.zIndex.landsacpe
return style
},
closeBtnStyle() {
let style = {}
if (this.closePosition === 'leftTop') {
if (this.closeTop) {
style.top = this.closeTop + 'rpx'
}
if (this.closeLeft) {
style.left = this.closeLeft + 'rpx'
}
} else if (this.closePosition === 'rightTop') {
if (this.closeTop) {
style.top = this.closeTop + 'rpx'
}
if (this.closeRight) {
style.right = this.closeRight + 'rpx'
}
} else if (this.closePosition === 'bottom') {
if (this.closeBottom) {
style.bottom = this.closeBottom + 'rpx'
}
}
if (this.closeSize) {
style.fontSize = this.closeSize + 'rpx'
}
if (this.closeColor) {
style.color = this.closeColor
}
return style
},
maskStyle() {
let style = {}
style.zIndex = this.zIndex ? this.zIndex - 1 : this.$t.zIndex.landsacpe - 1
return style
}
},
watch: {
show: {
handler(val) {
this.showValue = val
},
immediate: true
}
},
data() {
return {
showValue: false
}
},
methods: {
//
close() {
this.showValue = false
this.$emit('close')
},
//
closeMask() {
if (!this.maskCloseable) return
this.close()
}
}
}
</script>
<style lang="scss" scoped>
.tn-landscape {
width: 100%;
overflow: hidden;
&__container {
max-width: 100%;
position: fixed;
display: inline-flex;
flex-direction: column;
align-items: center;
justify-content: center;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
&__icon {
position: absolute;
text-align: center;
font-size: 50rpx;
color: #FFFFFF;
&--left-top {
top: -40rpx;
left: 20rpx;
}
&--right-top {
top: -40rpx;
right: 40rpx;
}
&--bottom {
left: 50%;
bottom: -40rpx;
transform: translateX(-50%);
}
}
&__mask {
position: fixed;
width: 100%;
height: 100%;
background-color: $tn-mask-bg-color;
top: 0;
right: 0;
bottom: 0;
left: 0;
opacity: 0;
transform: scale3d(1, 1, 0);
transition: all 0.25s ease-in;
&--show {
opacity: 1 !important;
transform: scale3d(1, 1, 1);
}
}
}
</style>

View File

@ -0,0 +1,254 @@
<template>
<view class="tn-lazy-load-class tn-lazy-load">
<view
class="tn-lazy-load__item"
:class="[`tn-lazy-load__item--${elIndex}`]"
:style="[lazyLoadItemStyle]"
>
<view class="tn-lazy-load__item__content">
<image
v-if="!error"
class="tn-lazy-load__item__image"
:style="[imageStyle]"
:src="show ? image : loadingImg"
:mode="imgMode"
@load="handleImgLoaded"
@error="handleImgError"
@tap="handleImgClick"
></image>
<image
v-else
class="tn-lazy-load__item__image tn-lazy-load__item__image--error"
:style="[imageStyle]"
:src="errorImg"
:mode="imgMode"
@load="handleErrorImgLoaded"
@tap="handleImgClick"
></image>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'tn-lazy-load',
props: {
//
index: {
type: [String, Number],
default: ''
},
//
image: {
type: String,
default: ''
},
//
imgMode: {
type: String,
default: 'scaleToFill'
},
//
loadingImg: {
type: String,
// default: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAGQCAMAAAC3Ycb+AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OUM0QjNBQjkyQUQ2MTFFQTlCNUQ4RTIzNDE5RUIxNjciIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OUM0QjNBQkEyQUQ2MTFFQTlCNUQ4RTIzNDE5RUIxNjciPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo5QzRCM0FCNzJBRDYxMUVBOUI1RDhFMjM0MTlFQjE2NyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo5QzRCM0FCODJBRDYxMUVBOUI1RDhFMjM0MTlFQjE2NyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PtRHfPcAAAAzUExURZWVldfX18PDw62trZubm9zc3Li4uKGhoebm5tLS0uHh4c3Nzaenp729vcjIyLKysuvr6141L40AAAcXSURBVHja7NzZlqpGAEBR5lG0//9rIw7IJKJi4or7PGTdtN10wr5SVAEGf/qqArsAiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAIiIAAERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAgAgJEQIAICBABERAg+nmQFMi5Jis+sIniED23jSzIgLTtg2D//iYme/8QBM/9lQ+CAEhbNLM3N9hEHAThX7GPCiBfAxK1b51kD+R7QMLjXg7iCsgWIPUh7pfVozG791oeBPngm48G583uW5GkBvI+SBaM2xXDn1oqum423bX/mgF5FySc2cv93Voug9TdZotsggnkBZB2NzbhrSY5HnoG07jei8dvzsJB/c3W60SALILE46+WCztsbhPR7R2VJq0ukEcT49nyy8QhaKcRa3fYHZD4+ufqOJAcgDz8/59vtw1I3QP5K6JsOG0vm3hce4I8LQp/BaRZGJC3AAn7IKOKXbC+7EdA5vdmmVwOLksgRThqOqiH4XEGsht+peoPUE8U/jJIO5OLH4GEwUslV5G0PTBG5Uiw/Y2jyigO3l9HAHKv9PYb82LloH74dZBoBUgar+l48NsNvtD0fkez9iwrAvIYZDRCl+Xs149Hm/KZmQ+QjUCiO1ei4ru7EsgnQYrkznlQb7thCuRfAzlOAPN72427P4VA/i2Q/DKT/Ls/VR8fvIBsDZIuz7TPF6TCbnk4GJkB2RokejTjuE7/unlgCuSTIO0Cy+Plp6vDfnQlBchy8QtjSHVd3EgmK1bHLm+H6+nXYbz2DuQRSPnqoL7vvq0u70on4zvxgCyWD3b9UyDVdW24PaWaiGTnFZJwPIQAebDpIKheBIm7n124ZthMJipAlkqHO+IZkP1tbfzOJark/A7MgKyvvl60fRqkvXfhuow+t9+q00+0/yyBrK8ZngOtBzldhw2X9tvpNGty0gvkmbPeJ0Cy/r09s/stbmfo0yMWkEdjevgKyOn2t2pxv7UXoibTdCDLje9/Ww1ymqzn87dbp92242ZmMRjI8hASvwKSLq4udqN6ksw8nxXN3tszD9L8Gkg+2mFrQYql5az4tvFj5xOx4VwnSdeBtGdyPwUytxK77pBVlNHdO7OK3rh/eTPUvdutT3fO52tuHMqD4N7llv8pyOQQ//w19YVDfX27+Sfuby9/6nau4pdA8vEdOZuChEH/quHt0Jg+IRJ/5+PrHwKZXfjbDiS73Zo7mu5UkzX7uTsXe0e/7nC3ePf1O69+BUg2XDfZCqSqOu7rGVf8cHBe8zhC2b61dtUHXv0OkGo6ZL4JkpbRYXdUaFevivx2M/1GIOctNh949TtAoumQ+TpIHMX54CJu+8BDd8FkE5BqcZh/59XvAClmTvKfB0nDqIlHo3T70SftyW1eX9dXtgQJqs1f/Q6QaOa/7wmQKtxH8eiGoCRuovODIO3VxOMmruZbHrLyD7z6DSDtGyT7ew1kf9hNn07c986JTovzzem0Id9wUG+Vk/IDr34DSNR7huZJkMFT6vEhqrPx/j5cnlZML8N6/PAzh9Y99Flm5Yde/c9BquDOkvkKkMP58dA4qi9vivE8JOvGz/j8FokfPpr288+pH2ZPOZrLmeGD+7KOh6dqYWJ48ki7yUg0tz0go/fv/LLddfV3sgOLJyaGPY/zrSlh1a36Arkzoue9CyG35ze6E6/dzO2Ga0EGHqdRJIkfn9/8OEjTW8Vq91ZWh39FeehWA7Nu9ft8CpUEk1WWOyDF0OPyEU2Pnzf/bZC0P6IPzmAvu7KauQBVrgKpJ0tG2arHzX8e5Pb3PezNs/PrX+3JMyCLn9XXf37tPFHvt09WfCDDjx+yyn1/p1V11j7GnB/q3leLuVva79S/tzed+db08YpF4uOZtmz/9oXWMq6BCAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAERECACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiAALELvqt/BBgACqVeUBXxcCkAAAAASUVORK5CYII='
},
//
errorImg: {
type: String,
default: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAGQCAMAAAC3Ycb+AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ODdDMjhENDYyQUQ2MTFFQTlDQ0VBODgxQjFFOEEyMEMiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6ODdDMjhENDcyQUQ2MTFFQTlDQ0VBODgxQjFFOEEyMEMiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo4N0MyOEQ0NDJBRDYxMUVBOUNDRUE4ODFCMUU4QTIwQyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo4N0MyOEQ0NTJBRDYxMUVBOUNDRUE4ODFCMUU4QTIwQyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PhLwhikAAAAzUExURZWVldfX162trcPDw5ubm7i4uNzc3Obm5s3NzaGhoeHh4cjIyKenp9LS0r29vbKysuvr67sDMEkAAAlpSURBVHja7NzpYqMgAIVRUVHc8/5PO66R1WAbOzX97q+ZtDEpR0AWTR7kVyWhCAAhgABCAAGEAAIIAQQQAggBBBACCCAEEEAIIIAQQAgggBBAACGAAEIAAYQAQgABhAACCAEEEAIIIAQQAgggBBBACCCAEEAAIYAQQAAhgABCAAGEAAIIAYQAAggBBBACCCAEEEAIIAQQQAgggBBAACGAAEIAIYAAQgABhAACCAEEEAIIAQQQAgggBBBACCCAEEAAIYAQQAAhgABCAAGEAAIIAYQAAggBBBACCCAEEEAIIAQQQAgggBBAACGAAEIAIYAAQgABhAACCAEEEAIIAQQQAgggBBBACCCAEEAIIIAQQAAhgABCAAGEAEIAAYQAAggBBBACCCAEEAIIIAQQQAgggBBAACGAEEAAIYAAsqeX5QWHKIcs/Ptl03lfL4zDFPWfBGmSpPn+IZzSH5KkCL5B+n+oklwz6Iz//R2QzFOabzhEmiRirAmZt/bl0w/dpMbLqeeo4wEdpC7zR5WAPKziHKtO7ql+ReKvIa9BxgNaL5ZtEkpeAGIVp5jKJa09xVo9vgSSzQcszdYvmOqjQNSQ6pHK6rO1n1Xj32788miwHLaZz1Tl9i/yayDlYJ/60/+lp8GSY7OY1B8E4p55bWmfquFk22GLuUUxi78cX+m+BjL2GLkhMrV+/muS6Sfic0CEp5T1Yu2OQdTzsKV0MJV73KVjroyTffxfuv5Tf3fd6iLT9wz8YdVHgUzF2Is9/Xhi5sYJqP1w/GUpjOiHVbaI0w2L+pg3GZzvtokcgHxWDXHaiy78l3sPke01qphamT5c+dqyeAGSumdL/mkggauTam0e3L/mPEiqtzKDbl0Z1Wn8xOa4ySo8X/7TQIJnY/seEKWf12UmC72CKP9xYjr19RPT7NNA+oMO+R0gwmlotAry+C6I0f59ch8yXVQOr0BKYcXt1IUYRyCt+Ur9HGsrQKI79WY9sY9ARPKlzFOFdb41ioD8b5Bp+mqeeRKAxINkESBFGpOpKhgv9OuYpH8A8l4Qa3qp60Kl2/k+rG2sWafuuyCBafb2j4JkgZUob3nWcmicpkxEgmTLLGejTxnWSWCi8lPmsk6DlIHFJv24ojiYyYoGacwL8zXTLEAVaDI/Ybb3NIgKDSv2oXpmHkvNs+PTpMASEdlk7fOZeRk37fwJ6yGnQarQsGIfqqcvx43rTOXY6jf7uKXdRzdLDRPbjIrx1cIj3Kr4KyBFezzgUGuR5893qkOQ19fR2uVBaU+r16LphJNOiatK7PeBZK/Kb+tUn71rcQjSvARpghfH/yG/D2RetTuI3N5QrMWdP46brP7FmKZ//CGQ9At9SL01DLkzY/Vs8Z97fQZ7gelw7jHqCz+/Wile5J4g3Vc79eb5a6oLSue+Ve83gaSv2jp5PxCzjzwFUm9zw9MllSMil1kS4d2E9SaQ1xNo9wMxx0+nQNLnew/WDHvveMAHYm08mofl3TFI/8pD3Q6kMAv6DIi2jTCwRJUvNdDYrrJum9oHhusCbWALonwxBRk1vXMnEGWuT5wAmfYuVGUYpJ7fUZujCN92hvzwWlrFgxSfANKb10DxIMbShnfrynyZZV30imA7P43ArXXHbvBVkTCIuGy25AdBrHmNeBCpL214QdLp9LZarG3IMWrmW0ehtuO7F2PS09UcgqS3B7FKPhpknrStD0HGF/vQRne37LwLG8EbHT4WxN7/Fg0yD9Yr/3br4nnstA+0Il6QxzdBmg8A6a2/IRbkcK9h/uzV8zywF/oSkOyageCPglRWgcWClHnEzs9q/t/SENVXgFijlsq3VtXdCsRp4qObrLLLgjuzSq3fX89ZZW6AfxNIzF6X9FYgThN/fk093KkvHX/hbWd+DqS/FUhlf+G3gohEXzVs3g9iDluWoaW8fL73QhB34u+tIHIf19nLuF4Q98a09Eynnl56q+ePgEhnX+dbQOp6H5XnJ0ACd8dFgkwf12nTOTcEqd2pom+CFF02TIPw6dKmrLS5qOtBpo8b5quUtrwrSGbuqPkeSJqllTFHO02NPxdMrm+y5LKdWyWXjw4vA5nGEtnjuyCFyHqNYvEolzmASm3zK1Eg5zr13lhqV1tlksnVw8Pkwgri7O07AVKLJkutRYw87bPlRpBpNXE8xGb+fhBlvEGrGPLqViu5sILIx9dAmqF1705sxF4M8+R8P5dOdQwi12fMnATpjJ2JSt/POIvU9wPJEs/jduJAjLvU0yFT0i64Yb1bsVi79dA4pEy3TzoHMq2O7Re4vXm5O9+l290NpE4CU+YRIMNye2iaqbVS2AUnn2fsekthYKReVNutVedA5juttyIXrT38mOds+ps9DWhwL7GWc61/DVKPzVN9UHDarf1icU98IOU8tm6L031Iq63t1tKzj3fe/FCpO4F0/i0Z2+yvA1KeGBjqj1qYx8/zoxpKZ1Yl367I1k+sfcft/QPy9csXy/32qX1qLZsrryG5BGQaRj0vc/b7N54XXq293TCLB5HO42Fy517obW19b+qjl3CHp0fdLJcWvmdy1etESi/uAdJrs1hTaUklHuW8qSDdC3UfXVR5cnD3rAFSSqtFb7z7eapErx7rC739jCXfbK3aWiipjXo8UbmxXPa7QQq9R289j2Gr88N7Ag5AlHPRKc37pNZv0CZtX1tVMG6rm8qW1/KlCgQvcMss933ybwXZz3dReW5yce4ByZtHFIhwT9kmjxg8BzbKDUe1PB9edBJqSN7/KM1LmqyuMZ5BpeTUw1aD/uDI0relPfSHa/Wn8Pxq1BNfxy/h3IdwOJqIKumb9CHvTqMefyY82RoQAgggBBBACCCAEEAAIYAQQAAhgABCAAGEAAIIAYQAAggBBBACCCAEEEAIIAQQQAgggBBAACGAAEIAIYAAQgABhAACCAEEEAIIAQQQAgggBBBACCCAEEAIIIAQQAAhgABCAAGEAEIAAYQAAggBBBACCCAEEAIIIAQQQAgggBBAACGAEEAAIYAAQgABhAACCAEEEAIIAQQQAgggBBBACCCAEEAIIIAQQAAhgABCAAGEAEIAAYQAAggBBBACCCAEEAIIIAQQQAgggBBAACGAEEAAIYAAQgABhAACCAGEAAIIAQQQAgggBBBACCAEEEAIIIAQQAAhgABCACGAAEIAAYQAAggBBBACCAEEEAIIIAQQQAggfyL/BBgA8PgLdH0TBtkAAAAASUVORK5CYII='
},
// rpx
//
threshold: {
type: [Number, String],
default: 100
},
//
isEffect: {
type: Boolean,
default: true
},
//
duration: {
type: [String, Number],
default: 500
},
// 线
// linear|ease|ease-in|ease-out|ease-in-out|cubic-bezier(n,n,n,n);
effect: {
type: String,
default: 'ease-in-out'
},
// rpx
height: {
type: [String, Number],
default: 450
},
//
borderRadius: {
type: String,
default: ''
}
},
computed: {
thresholdValue() {
// thresholdthis.threshold
let threshold = uni.upx2px(Math.abs(this.threshold))
return this.threshold < 0 ? -threshold : threshold
},
lazyLoadItemStyle() {
let style = {}
style.opacity = Number(this.opacity)
if (this.borderRadius) {
style.borderRadius = this.borderRadius
}
// time,duration(prop)
style.transition = `opacity ${this.time / 1000}s ${this.effect}`
style.height = this.$t.string.getLengthUnitValue(this.height)
return style
},
imageStyle() {
let style = {}
if (typeof this.height === 'string' && this.height.indexOf('%') === -1) {
style.height = this.$t.string.getLengthUnitValue(this.height)
}
return style
}
},
watch: {
show(val) {
//
if (!this.effect) return
this.time = 0
// opacity1()0()1
this.opacity = 0
setTimeout(() => {
this.time = this.duration
this.opacity = 1
}, 30)
},
image(val) {
//
if (!val) {
// null''undefined
this.error = true
} else {
this.init()
this.error = false
}
}
},
data() {
return {
elIndex: this.$t.uuid(),
//
show: false,
//
opacity: 1,
//
time: this.duration,
//
// loadlazy-loading-loaded-
loadStatus: '',
//
error: false
}
},
created() {
// data
this.observer = {}
this.observerName = 'lazyLoadContentObserver'
},
mounted() {
// tOnLazyLoadReachBottom
this.$nextTick(() => {
uni.$once('tOnLazyLoadReachBottom', () => {
if (!this.show) this.show = true
})
})
// mounted30ms
setTimeout(() => {
this.disconnectObserver(this.observerName)
const contentObserver = uni.createIntersectionObserver(this)
contentObserver.relativeToViewport({
bottom: this.thresholdValue
}).observe(`.tn-lazy-load__item--${this.elIndex}`, (res) => {
if (res.intersectionRatio > 0) {
//
this.show = true
//
this.disconnectObserver(this.observerName)
}
})
this[this.observerName] = contentObserver
}, 50)
},
methods: {
//
init() {
this.error = false
this.loadStatus = ''
},
//
handleImgClick() {
let whichImg = ''
// showfalse
if (this.show === false) whichImg = 'lazyImg'
// errortrue
else if (this.error === true) whichImg = 'errorImg'
//
else whichImg = 'realImg'
this.$emit('click', {
index: this.index,
whichImg: whichImg
})
},
// show
handleImgLoaded() {
if (this.loadStatus = '') {
//
this.loadStatus = 'lazyed'
}
else if (this.loadStatus == 'lazyed') {
//
this.loadStatus = 'loaded'
this.$emit('loaded', this.index)
}
},
//
handleErrorImgLoaded() {
this.$emit('error', this.index)
},
//
handleImgError() {
this.error = true
},
disconnectObserver(observerName) {
const observer = this[observerName]
observer && observer.disconnect()
}
}
}
</script>
<style lang="scss" scoped>
.tn-lazy-load {
&__item {
background-color: $tn-bg-gray-color;
overflow: hidden;
&__image {
// 3px
display: block;
width: 100%;
//
transform: transition3d(0, 0, 0);
//
will-change: transform;
}
}
}
</style>

View File

@ -0,0 +1,143 @@
<template>
<view
class="tn-line-progress-class tn-line-progress"
:style="[progressStyle]"
>
<view
class="tn-line-progress--active"
:class="[
$t.color.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.color.getBackgroundColorStyle(this.activeColor)) {
style.backgroundColor = this.$t.color.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>

View File

@ -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>

View File

@ -0,0 +1,184 @@
<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: #FFFFFF;
&__title {
width: 100%;
padding: 30rpx;
font-size: 30rpx;
line-height: 30rpx;
box-sizing: border-box;
&--card {
// margin: 0rpx 30rpx;
}
}
&__content {
width: 100%;
position: relative;
border-radius: 0;
&--card {
// width: auto;
// overflow: hidden;
// margin-right: 30rpx;
// margin-left: 30rpx;
// border-radius: 20rpx
}
}
&--card {
// padding-bottom: 30rpx;
border-radius: 20rpx;
overflow: hidden;
}
}
</style>

View File

@ -0,0 +1,188 @@
<template>
<view class="tn-load-more-class tn-load-more">
<view
class="tn-load-more__wrap"
:class="[backgroundColorClass]"
:style="[loadStyle]"
>
<view class="tn-load-more__line"></view>
<view
class="tn-load-more__content"
:class="[{'tn-load-more__content--more': (status === 'loadmore' || status === 'nomore')}]"
>
<view class="tn-load-more__loading">
<tn-loading
class="tn-load-more__loading__icon"
:mode="loadingIconType"
:show="status === 'loading' && loadingIcon"
:color="loadingIconColor"
></tn-loading>
</view>
<view
class="tn-load-more__text"
:class="[fontColorClass, {'tn-load-more__text--dot': (status === 'nomore' && dot)}]"
:style="[loadTextStyle]"
>{{ showText }}</view>
</view>
<view class="tn-load-more__line"></view>
</view>
</view>
</template>
<script>
import componentsColorMixin from '../../libs/mixin/components_color.js'
export default {
name: 'tn-load-more',
mixins: [componentsColorMixin],
props: {
//
// loadmore ->
// loading ->
// nomore ->
status: {
type: String,
default: 'loadmore'
},
//
loadingIcon: {
type: Boolean,
default: true
},
// tn-loading
loadingIconType: {
type: String,
default: 'circle'
},
//
loadingIconColor: {
type: String,
default: ''
},
//
loadText: {
type: Object,
default() {
return {
loadmore: '加载更多',
loading: '正在加载...',
nomore: '没有更多了'
}
}
},
// nomore
dot: {
type: Boolean,
default: false
},
//
customStyle: {
type: Object,
default() {
return {}
}
}
},
computed: {
loadStyle() {
let style = {}
if (this.backgroundColorStyle) {
style.backgroundColor = this.backgroundColorStyle
}
//
if (Object.keys(this.customStyle).length > 0) {
Object.assign(style, this.customStyle)
}
return style
},
loadTextStyle() {
let style = {}
if (this.fontColorStyle) {
style.color = this.fontColorStyle
}
if (this.fontSizeStyle) {
style.fontSize = this.fontSizeStyle
style.lineHeight = this.$t.string.getLengthUnitValue(this.fontSize + 2, this.fontUnit)
}
return style
},
//
showText() {
let text = ''
if (this.status === 'loadmore') text = this.loadText.loadmore || '加载更多'
else if (this.status === 'loading') text = this.loadText.loading || '正在加载...'
else if (this.status === 'nomore' && this.dot) text = this.dotText
else text = this.loadText.nomore || '没有更多了'
return text
}
},
data() {
return {
//
dotText: '●'
}
},
methods: {
//
loadMore() {
// loadmore
if (this.status === 'loadmore') this.$emit('loadmore')
}
}
}
</script>
<style lang="scss" scoped>
.tn-load-more {
&__wrap {
background-color: transparent;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
color: $tn-content-color;
}
&__line {
vertical-align: middle;
border: 1px solid $tn-content-color;
width: 50rpx;
transform: scaleY(0.5);
}
&__content {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
padding: 0 12rpx;
&--more {
position: relative;
}
}
&__loading {
margin-right: 8rpx;
&__icon {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
}
&__text {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
line-height: 30rpx;
&--dot {
font-size: 28rpx;
}
}
}
</style>

View File

@ -0,0 +1,114 @@
<template>
<view
v-if="show"
class="tn-loading-class tn-loading"
:class="[
`tn-loading-${mode}`,
animation ? `tn-loading-${mode}--animation` : ''
]"
:style="[loadStyle]"
></view>
</template>
<script>
export default {
name: 'tn-loading',
props: {
//
// circle flower
mode: {
type: String,
default: 'circle'
},
//
show: {
type: Boolean,
default: true
},
//
animation: {
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 {
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 {
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>

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>

View File

@ -0,0 +1,355 @@
<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" :class="[`tn-icon-${backIcon}`]"></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
},
//
backIcon: {
type: String,
default: 'left'
},
//
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.fontColorClass) {
clazz += `${this.fontColorClass}`
}
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'
}
if (!this.fontColorClass && this.fontColor) {
style.color= this.fontColor
}
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
this.updateNavBarInfo()
},
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 updateNavBarInfo() {
// vuex
let customBarHeight = this.vuex_custom_bar_height
let statusBarHeight = this.vuex_status_bar_height
//
if (!customBarHeight) {
try {
const navBarInfo = await this.$t.updateCustomBar()
customBarHeight = navBarInfo.customBarHeight
statusBarHeight = navBarInfo.statusBarHeight
} catch(e) {
setTimeout(() => {
this.updateNavBarInfo()
}, 10)
return
}
}
// vuex
this && this.$t.vuex('vuex_status_bar_height', statusBarHeight)
this && this.$t.vuex('vuex_custom_bar_height', customBarHeight)
this.customBarHeight = customBarHeight
this.statusBarHeight = statusBarHeight
},
//
async handlerBack() {
if (this.beforeBack && typeof(this.beforeBack) === 'function') {
//
// (H5)thisthis
// bind()thisthisthis
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 (pages.length == 1 && (!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;
padding: 20rpx;
font-size: 38rpx;
line-height: 100%;
// 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 {
padding: 20rpx 20rpx 20rpx 0rpx;
}
}
&__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>

View File

@ -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: {
// showfalseautoHiddentruelist
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>

View File

@ -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) {
// valueinputValue
if (!this.changeFromInner) {
this.updateInputValue()
// inputValuethis.handleChange()changeFromInnertrue
// this.$nextTick
// changeFromInnerfalse
this.$nextTick(() => {
this.changeFromInner = false
})
}
},
inputValue(newVal, oldVal) {
//
if (newVal === '') return
let value = 0
// minmax使
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-90min
// props min0
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>

View File

@ -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>

View File

@ -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=selectormode=multiSelector
range: {
type: Array,
default() {
return []
}
},
// mode=selectormode=multiSelector
defaultSelector: {
type: Array,
default() {
return [0]
}
},
// rangeArray<Object>rangeKeyObjectkey
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"]
// defaultReginareaCode使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() {
// pickerthis.citysthis.areas
this.citys = citys[this.province]
this.areas = areas[this.province][this.city]
},
yearAndMonth() {
//
// 3031229228
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-yearend+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.valueArrpicker
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
//
// 331229day3129(picker-column1)
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
// defaultRegionareaCode使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
// defaultRegionareaCode使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
// defaultRegionareaCode使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-ddiOS使"/"
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>

View File

@ -0,0 +1,491 @@
<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-topmode=center
negativeTop: {
type: Number,
default: 0
},
// marginTopmode = top,left,right使
marginTop: {
type: Number,
default: 0
},
// 使Pickerkeyboard
// v-modelprops
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) {
// console.log(this.visibleSync);
if (this.visibleSync) {
this.visibleSync = false
return
}
this.open()
} else if (!this.closeFromInner) {
this.close()
}
this.closeFromInner = false
}
},
mounted() {
// valuetruepopup
this.value && this.open()
},
methods: {
//
maskClick() {
if (!this.maskCloseable) return
this.close()
},
open() {
this.change('visibleSync', 'showPopup', true)
},
//
close() {
// valuewatchvalueclose
// @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.popupfalsepickeractionsheetpopup
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')
clearTimeout(this.timer)
}, 10)
// #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')
clearTimeout(this.timer)
}, 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 linear;
transition-property: opacity;
opacity: 0;
&--show {
opacity: 1;
}
}
}
</style>

View File

@ -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: {
// computedtn-radio
// parentDatawatch(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() {
// childrendata
this.children = []
},
methods: {
// tn-radiotn-radiovaluetn-radio
setValue(value) {
// value(parentValuevalue)tn-radio
this.children.map(child => {
if (child.parentData.value !== value) child.parentData.value = ''
})
// emitv-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>

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() {
// radionameparentvalue
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
// H5APP使flex
style.flex = `0 0 ${this.parentData.width}`;
// #endif
}
if(this.parent && this.parentData.wrap) {
style.width = '100%';
// #ifndef MP
// H5APP使flex100%
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-radionamev-model()
if (this.parentData.value !== this.name) this.$emit('change', this.name)
},
//
// parentData.valuenameradio
// radioparentData.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>

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
// icons324
// 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
// icons324
// 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(5colors32
// 45)
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>

View File

@ -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>

View File

@ -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 : ''
style.fontSize = this.fontSizeStyle ? this.fontSizeStyle : ''
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__contentpadding-left: 100%
this.animationDuration = `${this.textWidth / uni.upx2px(this.speed)}s`
// APP(HX2.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>

View File

@ -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>

View File

@ -0,0 +1,401 @@
<template>
<view class="tn-scroll-view-class tn-scroll-view">
<scroll-view
class="scroll-view"
:style="[scrollViewStyle]"
scroll-y
scroll-anchoring
enable-back-to-top
:throttle="false"
:scroll-top="scrollTop"
:lower-threshold="lowerThreshold"
@scroll="handleScroll"
@touchend="handleTouchEnd"
@touchmove.prevent.stop="handleTouchMove"
@touchstart="handleTouchStart"
@scrolltolower="handleScrollTolower"
>
<view class="scroll__content" :style="[scrollContentStyle]">
<view class="scroll__pull-down">
<slot name="pulldown">
<view class="scroll__refresh" :style="[refreshStyle]">
<view><tn-loading :animation="refreshing"></tn-loading></view>
<view class="scroll__refresh--text" :style="[refreshTextStyle]">{{ refreshStateText }}</view>
</view>
</slot>
</view>
<view :id="elScrollDataId" class="scroll__data">
<slot></slot>
</view>
</view>
</scroll-view>
</view>
</template>
<script>
import componentsColor from '../../libs/mixin/components_color.js'
export default {
name: 'tn-scroll-view',
mixins: [ componentsColor ],
props: {
// H5
h5NavHeight: {
type: Number,
default: 45
},
//
customNavHeight: {
type: Number,
default: 0
},
//
offsetTop: {
type: Number,
default: 0
},
//
offsetBottom: {
type: Number,
default: 0
},
// ()
height: {
type: Number,
default: null
},
//
disabled: {
type: Boolean,
default: false
},
//
pullDownDisabled: {
type: Boolean,
default: false
},
//
pullDownSpeed: {
type: Number,
default: 0.5
},
//
refreshDelayed: {
type: Number,
default: 800
},
//
refreshFinishDelayed: {
type: Number,
default: 800
},
//
refresherThreshold: {
type: Number,
default: 70
},
//
lowerThreshold: {
type: Number,
default: 40
},
//
refreshState: {
type: Boolean,
default: false
},
//
refreshingText: {
type: String,
default: '正在刷新'
},
//
refreshSuccessText: {
type: String,
default: '刷新成功'
},
//
pulldownText: {
type: String,
default: '下拉刷新'
},
//
pulldownFinishText: {
type: String,
default: '松开刷新'
}
},
data() {
return {
// id
elScrollDataId: '',
//
systemInfo: {
height: 0,
statusBarHeight: 0
},
//
scrollTop: 0,
//
scrollDataTop: -1,
//
scrollDataOffsetTop: -1,
//
scrollViewHeight: 0,
//
currentScrollTop: 0,
// Y
currentTouchStartY: 0,
//
refreshStateText: '下拉刷新',
//
refreshing: false,
//
refreshFinish: false,
//
pulldowning: false,
//
pullDownHeight: 0,
//
showPullDown: false
}
},
computed: {
scrollViewStyle() {
let style = {}
style.height = this.scrollViewHeight + 'px'
if (!this.backgroundColorClass) {
style.backgroundColor = this.backgroundColorStyle
}
return style
},
scrollContentStyle() {
let style = {}
style.transform = this.showPullDown ? `translateY(${this.pullDownHeight}px)` : `translateY(0px)`
style.transition = this.pulldowning ? `transform 100ms ease-out` : `transform 500ms cubic-bezier(0.19,1.64,0.42,0.72)`
return style
},
refreshStyle() {
let style = {}
style.opacity = this.showPullDown ? 1 : 0
return style
},
refreshTextStyle() {
let style = {}
if (!this.fontColorClass) {
style.color = this.fontColorStyle
}
return style
},
loadTextStyle() {
let style = {}
if (!this.fontColorClass) {
style.color = this.fontColorStyle
}
return style
}
},
watch: {
refreshState(nVal, oVal) {
if (!nVal) {
if (this.showPullDown) {
//
this.pulldowning = false
//
this.showPullDown = false
//
this.refreshing = false
}
}
}
},
created() {
this.elScrollDataId = this.$t.uuid()
this.getSystemInfo()
},
mounted() {
this.$nextTick(() => {
this.init()
})
},
methods: {
//
init() {
this.refreshStateText = this.pulldownText
// scrollView
this.updateScrollViewInfo()
},
//
getSystemInfo() {
const systemInfo = uni.getSystemInfoSync()
this.systemInfo.height = systemInfo.safeArea.height
this.systemInfo.statusBarHeight = systemInfo.statusBarHeight
},
// scrollView
updateScrollViewInfo() {
if (this.height) {
this.scrollViewHeight = this.height
} else {
// scrollView
// console.log(this.systemInfo, this.offsetTop, this.customNavHeight);
// #ifdef H5
this.scrollViewHeight = this.systemInfo.height - (
this.offsetTop +
(this.customNavHeight ? this.customNavHeight : this.h5NavHeight) +
this.offsetBottom)
this.scrollDataOffsetTop = this.offsetTop + (this.customNavHeight ? this.customNavHeight : this.h5NavHeight)
// #endif
// #ifndef H5
this.scrollViewHeight = this.systemInfo.height - (
this.offsetTop +
this.systemInfo.statusBarHeight +
this.offsetBottom)
this.scrollDataOffsetTop = this.offsetTop + this.systemInfo.statusBarHeight
// #endif
}
},
// scrollView
async getScrollDataInfo() {
const scrollInfo = await this._tGetRect(`#${this.elScrollDataId}`)
this.scrollDataTop = scrollInfo.top
},
//
handleScrollTolower(e) {
if (this.pullUpDisabled) return
this.$emit('scrolltolower', e)
},
//
handleScroll(e) {
this.currentScrollTop = e.detail.scrollTop
this.$emit('scroll', e.detail)
},
//
handleTouchStart(e) {
if (this.disabled) return
this.currentTouchStartY = e.touches[0].clientY
this.getScrollDataInfo()
this.$emit('touchStart', e)
},
//
handleTouchMove(e) {
if (this.disabled) return
if (this.currentScrollTop == 0 && e.touches[0].clientY >= this.currentTouchStartY) {
//
const moveOffset = this.scrollDataTop > 0 ?
(this.scrollDataOffsetTop - this.scrollDataTop) :
(Math.abs(this.scrollDataTop) + this.scrollDataOffsetTop)
this.pulldowning = true
this.showPullDown = true
let pullDownDistance = ((e.touches[0].clientY - this.currentTouchStartY) - moveOffset) * this.pullDownSpeed
this.pullDownHeight = pullDownDistance
// this.pullDownHeight = pullDownDistance > this.refresherThreshold ? this.refresherThreshold : pullDownDistance
this.refreshStateText = this.pullDownHeight >= this.refresherThreshold ? this.pulldownFinishText : this.pulldownText
if (pullDownDistance > this.refresherThreshold) {
this.$emit('refreshReady')
}
}
this.$emit('touchMove', e)
},
//
handleTouchEnd(e) {
if (this.disabled) return
//
if (this.showPullDown) {
//
if (this.pullDownHeight < this.refresherThreshold) {
//
this.pulldowning = false
//
this.pullDownHeight = 0
//
this.showPullDown = false
//
this.$emit('refreshStop')
} else {
this.pullDownHeight = this.pullDownHeight > this.refresherThreshold ? this.refresherThreshold : this.pullDownHeight
this.refresh()
}
}
//
this.$emit('touchEnd', e)
},
//
refresh() {
//
this.refreshFinish = false
//
this.refreshing = true
//
this.refreshStateText = this.refreshingText
// refresh
setTimeout(() => {
this.$emit('refresh')
}, this.refreshDelayed)
},
}
}
</script>
<style lang="scss" scoped>
.tn-scroll-view {
.scroll-view {
position: relative;
touch-action: none;
.scroll__content {
display: flex;
will-change: transform;
flex-direction: column;
.scroll {
&__pull-down {
position: absolute;
left: 0;
width: 100%;
display: flex;
padding: 30rpx 0;
align-items: center;
justify-content: center;
transform: translateY(-100%);
}
&__refresh {
display: flex;
align-items: center;
justify-content: center;
&--text {
margin-left: 10rpx;
}
}
&__data {
}
&__pull-up {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
transform: translateY(100%);
}
&__load {
padding: 20rpx 0;
display: flex;
align-items: center;
justify-content: center;
&--text {
margin-left: 10rpx;
}
}
}
}
}
}
</style>

View File

@ -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-autochildren
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() {
// 0columnNum
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.columnDatacolumnChange
// 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>

View File

@ -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.message.toast('旋转图片失败')
})
} else {
this.$emit('save', res.tempFilePath)
}
},
fail: () => {
this.$t.message.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.message.toast('旋转图片失败')
})
} else {
uni.previewImage({
urls: [res.tempFilePath]
})
}
},
fail: (e) => {
this.$t.message.toast('预览失败')
}
}, this)
},
//
closeBoard() {
this.$t.message.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>

View File

@ -0,0 +1,254 @@
<template>
<view
v-if="show"
class="tn-skeleton-class tn-skeleton"
:class="[backgroundColorClass]"
:style="[skeletonStyle]"
@touchmove.stop.prevent
>
<view
v-for="(item, index) in rectNodes"
:key="$t.uuid()"
class="tn-skeleton__item tn-skeleton__item--rect"
:class="[elBackgroundColorClass, {'tn-skeleton__item--fade': animation}]"
:style="[itemStyle('rect', item)]"
></view>
<view
v-for="(item, index) in circleNodes"
:key="$t.uuid()"
class="tn-skeleton__item tn-skeleton__item--circle"
:class="[elBackgroundColorClass, {'tn-skeleton__item--fade': animation}]"
:style="[itemStyle('circle', item)]"
></view>
<view
v-for="(item, index) in filletNodes"
:key="$t.uuid()"
class="tn-skeleton__item tn-skeleton__item--fillet"
:class="[elBackgroundColorClass, {'tn-skeleton__item--fade': animation}]"
:style="[itemStyle('fillet', item)]"
></view>
</view>
</template>
<script>
import componentsColorMixin from '../../libs/mixin/components_color.js'
export default {
name: 'tn-skeleton',
mixins: [ componentsColorMixin ],
props: {
//
show: {
type: Boolean,
default: false
},
//
elBackgroundColor: {
type: String,
default: ''
},
//
animation: {
type: Boolean,
default: true
},
//
rectCustomStyle: {
type: Object,
default() {
return {}
}
},
//
circleCustomStyle: {
type: Object,
default() {
return {}
}
},
//
filletCustomStyle: {
type: Object,
default() {
return {}
}
}
},
computed: {
elBackgroundColorStyle() {
return this.$t.color.getBackgroundColorStyle(this.elBackgroundColor)
},
elBackgroundColorClass() {
return this.$t.color.getBackgroundColorInternalClass(this.elBackgroundColor)
},
//
skeletonStyle() {
let style = {}
style.width = this.skeletonWidth + 'px'
style.height = this.skeletonHeight + 'px'
if (this.backgroundColorStyle) {
style.backgroundColor = this.backgroundColorStyle
}
style.left = this.left + 'px'
style.top = this.top + 'px'
return style
},
//
itemStyle() {
return (type, item) => {
let style = {}
style.width = item.width + 'px'
style.height = item.height + 'px'
if (this.elBackgroundColorStyle) {
style.backgroundColor = this.elBackgroundColorStyle
}
style.left = (item.left - this.left) + 'px'
style.top = (item.top - this.top) + 'px'
if (type === 'rect') {
Object.assign(style, this.rectCustomStyle)
} else if (type === 'circle') {
style.borderRadius = (item.width / 2) + 'px'
Object.assign(style, this.circleCustomStyle)
} else if (type === 'fillet') {
Object.assign(style, this.filletCustomStyle)
}
return style
}
}
},
data() {
return {
//
skeletonWidth: 750,
//
skeletonHeight: 1500,
//
filletNodes: [],
//
circleNodes: [],
//
rectNodes: [],
//
top: 0,
left: 0
}
},
mounted() {
this.$nextTick(() => {
//
const systemInfo = uni.getSystemInfoSync()
this.skeletonWidth = systemInfo.safeArea.width
this.skeletonHeight = systemInfo.safeArea.height
this.selectQueryInfo()
})
},
methods: {
//
selectQueryInfo() {
//
// 使in(this)
let query = null
// 使in(this)
// #ifdef MP-WEIXIN
query = uni.createSelectorQuery().in(this.$parent)
// #endif
// #ifndef MP-WEIXIN
query = uni.createSelectorQuery()
// #endif
query.selectAll('.tn-skeleton').boundingClientRect().exec((res) => {
console.log(res);
this.skeletonWidth = res[0][0].width
this.skeletonHeight = res[0][0].height
this.top = res[0][0].bottom - res[0][0].height
this.left = res[0][0].left
})
//
this.getRectElements()
this.getCircleElements()
this.getFillteElements()
},
//
getRectElements() {
let query = null
// 使in(this)
// #ifdef MP-WEIXIN
query = uni.createSelectorQuery().in(this.$parent)
// #endif
// #ifndef MP-WEIXIN
query = uni.createSelectorQuery()
// #endif
query.selectAll('.tn-skeleton-rect').boundingClientRect().exec((res) => {
this.rectNodes = res[0]
})
},
//
getCircleElements() {
let query = null
// 使in(this)
// #ifdef MP-WEIXIN
query = uni.createSelectorQuery().in(this.$parent)
// #endif
// #ifndef MP-WEIXIN
query = uni.createSelectorQuery()
// #endif
query.selectAll('.tn-skeleton-circle').boundingClientRect().exec((res) => {
this.circleNodes = res[0]
})
},
//
getFillteElements() {
let query = null
// 使in(this)
// #ifdef MP-WEIXIN
query = uni.createSelectorQuery().in(this.$parent)
// #endif
// #ifndef MP-WEIXIN
query = uni.createSelectorQuery()
// #endif
query.selectAll('.tn-skeleton-fillet').boundingClientRect().exec((res) => {
this.filletNodes = res[0]
})
}
}
}
</script>
<style lang="scss" scoped>
.tn-skeleton {
position: absolute;
z-index: 9998;
overflow: hidden;
background-color: #FFFFFF;
&__item {
position: absolute;
background-color: #F0F0F0;
&--fillet {
border-radius: 10rpx;
}
&--fade {
width: 100%;
height: 100%;
background-color: #E6E6E6;
animation-duration: 1.5s;
animation-name: blink;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
}
}
}
@keyframes blink {
0% {
opacity: 1;
}
50% {
opacity: 0.4;
}
100% {
opacity: 1;
}
}
</style>

View File

@ -0,0 +1,255 @@
<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
//
// statusmoving
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)
// 0100
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 {
width: 100%;
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>

View File

@ -0,0 +1,657 @@
function setTimeout(instance, cb, time) {
if (time > 0) {
var s = getDate().getTime()
var fn = function () {
if (getDate().getTime() - s > time) {
cb && cb()
} else
instance.requestAnimationFrame(fn)
}
fn()
}
else
cb && cb()
}
// 判断触摸的移动方向
function decideSwiperDirection(startTouches, currentTouches, direction) {
// 震动偏移容差
var toleranceShake = 30
// 移动容差
var toleranceTranslate = 10
if (direction === 'horizontal') {
// 水平方向移动
if (Math.abs(currentTouches.y - startTouches.y) <= toleranceShake) {
// console.log(currentTouches.x, startTouches.x);
if (Math.abs(currentTouches.x - startTouches.x) > toleranceTranslate) {
if (currentTouches.x - startTouches.x > 0) {
return 'right'
} else if (currentTouches.x - startTouches.x < 0) {
return 'left'
}
}
}
} else if (direction === 'vertical') {
// 垂直方向移动
if (Math.abs(currentTouches.x - startTouches.x) <= toleranceShake) {
// console.log(currentTouches.x, startTouches.x);
if (Math.abs(currentTouches.y - startTouches.y) > toleranceTranslate) {
if (currentTouches.y - startTouches.y > 0) {
return 'down'
} else if (currentTouches.y - startTouches.y < 0) {
return 'up'
}
}
}
}
return ''
}
// 更新轮播样式信息
function updateSwiperStyle(currentTouches, instance, state) {
var itemData = state.itemData
var itemsInstance = state.itemsInstance
var list = state.list
var currentIndex = state.currentIndex
var touchRelactive = state.touchRelactive
// console.log(itemAnimationWidth);
if (itemData.direction === 'horizontal') {
// 水平方向
var itemAnimationWidth = state.itemAnimationWidth
// 偏移的x轴距离
var translateX = currentTouches.x - touchRelactive.x
if (currentTouches.x > itemData.windowWidth || currentTouches.x < 0) return
// console.log(translateX);
// 更新其他轮播样式
if (state.direction == 'left') {
// 设置当前激活元素的偏移量
instance.setStyle({
'transform': 'translate3d('+ translateX + 'px, 0px, 0px)',
'z-index': list[currentIndex].zIndex + 1,
'opacity': list[currentIndex].opacity
})
// 移动距离是否超过了指定的容器宽度
if (Math.abs(translateX) > itemAnimationWidth) {
state.itemsInstance.forEach( function(itemInstance, index) {
if (index != currentIndex) {
var preIndex = (index == 0) ? list.length - 1 : index - 1
var distanceRate = (Math.abs(translateX) - itemAnimationWidth) / (itemData.itemWidth - itemAnimationWidth)
var itemTranslateX = list[index].translateX - (list[index].translateX - list[preIndex].translateX) * distanceRate
var itemScale = list[index].scale + (list[preIndex].scale - list[index].scale) * distanceRate
var itemOpacity = list[index].opacity + (list[preIndex].opacity - list[index].opacity) * distanceRate
// console.log(preIndex);
// console.log(list[index]);
// console.log(distanceRate);
// console.log(itemTranslateX);
// console.log(itemScale);
// console.log(itemOpacity);
// console.log('-----------------------------------------------------------');
itemInstance.setStyle({
'transform': 'translate3d(' + itemTranslateX + 'px, 0px, 0px) scale(' + itemScale + ')',
'z-index': list[index].zIndex,
'opacity': itemOpacity
})
}
})
}
} else if (state.direction == 'right') {
var preIndex = (currentIndex == 0) ? list.length - 1 : currentIndex - 1
// 右滑的时候把最底部的取出,并放到最高层级
state.itemsInstance[preIndex].setStyle({
'transform': 'translate3d(-' + (itemData.itemWidth - translateX) + 'px, 0px, 0px) scale(1)',
'z-index': list[currentIndex].zIndex + 1,
'opacity': list[currentIndex].opacity
})
// 当前轮播逐渐缩小
if (Math.abs(translateX) < itemAnimationWidth) {
state.itemsInstance.forEach( function(itemInstance, index) {
if (index != preIndex) {
var replaceIndex = index == list.length - 1 ? 0 : index + 1
var distanceRate = Math.abs(translateX) / itemAnimationWidth
var itemTranslateX = list[index].translateX + (list[replaceIndex].translateX - list[index].translateX) * distanceRate
var itemScale = list[index].scale - (list[index].scale - list[replaceIndex].scale) * distanceRate
var itemOpacity = list[index].opacity - (list[index].opacity - list[replaceIndex].opacity) * distanceRate
// console.log(preIndex);
// console.log(index);
// console.log(replaceIndex);
// console.log(list[index]);
// console.log(list[replaceIndex].translateX - list[index].translateX);
// console.log(distanceRate);
// console.log(itemTranslateX);
// console.log(itemScale);
// console.log('-----------------------------------------------------------');
itemInstance.setStyle({
'transform': 'translate3d(' + itemTranslateX + 'px, 0px, 0px) scale(' + itemScale + ')',
'z-index': list[index].zIndex,
'opacity': itemOpacity
})
}
})
}
}
} else if (itemData.direction === 'vertical') {
// 垂直方向
var itemAnimationHeight = state.itemAnimationHeight
// 偏移的y轴距离
var translateY = currentTouches.y - touchRelactive.y
if (currentTouches.y > itemData.windowHeight || currentTouches.y < 0) return
// console.log(translateX);
// 更新其他轮播样式
if (state.direction == 'up') {
// 设置当前激活元素的偏移量
instance.setStyle({
'transform': 'translate3d(0px, '+ translateY + 'px, 0px)',
'z-index': list[currentIndex].zIndex + 1,
'opacity': list[currentIndex].opacity
})
// 移动距离是否超过了指定的容器宽度
if (Math.abs(translateY) > itemAnimationHeight) {
state.itemsInstance.forEach( function(itemInstance, index) {
if (index != currentIndex) {
var preIndex = (index == 0) ? list.length - 1 : index - 1
var distanceRate = (Math.abs(translateY) - itemAnimationHeight) / (itemData.itemHeight - itemAnimationHeight)
var itemTranslateY = list[index].translateY - (list[index].translateY - list[preIndex].translateY) * distanceRate
var itemScale = list[index].scale + (list[preIndex].scale - list[index].scale) * distanceRate
var itemOpacity = list[index].opacity + (list[preIndex].opacity - list[index].opacity) * distanceRate
// console.log(preIndex);
// console.log(list[index]);
// console.log(distanceRate);
// console.log(itemTranslateX);
// console.log(itemScale);
// console.log('-----------------------------------------------------------');
itemInstance.setStyle({
'transform': 'translate3d(0px, ' + itemTranslateY + 'px, 0px) scale(' + itemScale + ')',
'z-index': list[index].zIndex,
'opacity': itemOpacity
})
}
})
}
} else if (state.direction == 'down') {
var preIndex = (currentIndex == 0) ? list.length - 1 : currentIndex - 1
// 下滑的时候把最底部的取出,并放到最高层级
state.itemsInstance[preIndex].setStyle({
'transform': 'translate3d(0px, -' + (itemData.itemHeight - translateY) + 'px, 0px) scale(1)',
'z-index': list[currentIndex].zIndex + 1,
'opacity': list[currentIndex].opacity
})
// 当前轮播逐渐缩小
if (Math.abs(translateY) < itemAnimationHeight) {
state.itemsInstance.forEach( function(itemInstance, index) {
if (index != preIndex) {
var replaceIndex = index == list.length - 1 ? 0 : index + 1
var distanceRate = Math.abs(translateY) / itemAnimationHeight
var itemTranslateY = list[index].translateY + (list[replaceIndex].translateY - list[index].translateY) * distanceRate
var itemScale = list[index].scale - (list[index].scale - list[replaceIndex].scale) * distanceRate
var itemOpacity = list[index].opacity - (list[index].opacity - list[replaceIndex].opacity) * distanceRate
// console.log(preIndex);
// console.log(index);
// console.log(replaceIndex);
// console.log(list[index]);
// console.log(list[replaceIndex].translateX - list[index].translateX);
// console.log(distanceRate);
// console.log(itemTranslateX);
// console.log(itemScale);
// console.log('-----------------------------------------------------------');
itemInstance.setStyle({
'transform': 'translate3d(0px, ' + itemTranslateY + 'px, 0px) scale(' + itemScale + ')',
'z-index': list[index].zIndex,
'opacity': itemOpacity
})
}
})
}
}
}
}
// 更新当前轮播序号
function updateCurrentSwiperIndex(index, ownerInstance, state) {
state.currentIndex = index
ownerInstance.callMethod('changeSwiperIndex', {
index: index
})
}
// 切换到下一个轮播
function switchNextSwiper(newIndex, touches, instance, state) {
var currentIndex = state.currentIndex
var list = state.list
var direction = state.itemData.direction
var touchRelactive = state.touchRelactive || {x: 0, y: 0}
// 已经完成轮播切换
var currentListItemData = JSON.parse(JSON.stringify(list))
if (direction === 'horizontal') {
// 水平方向移动
var itemWidth = state.itemData.itemWidth
// 当前轮播移动到最左边
instance.setStyle({
'transform': 'translate3d(-'+ itemWidth + 'px, 0px, 0px) scale(1)',
'z-index': list[currentIndex].zIndex + 1,
'opacity': list[currentIndex].opacity
})
// 计算当前移动需要的剩余时间
var time = Math.floor((itemWidth - Math.abs(touches.pageX - touchRelactive.x)) / itemWidth * 250)
setTimeout(instance, function() {
for (var i = list.length - 1; i >= 0; i--) {
var replaceIndex = i - 1 < 0 ? list.length - 1 : i - 1
// console.log(i);
// console.log(replaceIndex);
state.itemsInstance[i].setStyle({
'transform': 'translate3d('+ currentListItemData[replaceIndex].translateX + 'px, 0px, 0px) scale(' + currentListItemData[replaceIndex].scale + ')',
'z-index': currentListItemData[replaceIndex].zIndex,
'opacity': currentListItemData[replaceIndex].opacity
})
state.list[i] = currentListItemData[replaceIndex]
}
}, time)
} else if (direction === 'vertical') {
// 垂直方向移动
var itemHeight = state.itemData.itemHeight
// 当前轮播移动到最上边
instance.setStyle({
'transform': 'translate3d(0px, -'+ itemHeight + 'px, 0px) scale(1)',
'z-index': list[currentIndex].zIndex + 1,
'opacity': list[currentIndex].opacity
})
// 计算当前移动需要的剩余时间
var time = Math.floor((itemHeight - Math.abs(touches.pageY - touchRelactive.y)) / itemHeight * 250)
setTimeout(instance, function() {
for (var i = list.length - 1; i >= 0; i--) {
var replaceIndex = i - 1 < 0 ? list.length - 1 : i - 1
// console.log(i);
// console.log(replaceIndex);
state.itemsInstance[i].setStyle({
'transform': 'translate3d(0px, '+ currentListItemData[replaceIndex].translateY + 'px, 0px) scale(' + currentListItemData[replaceIndex].scale + ')',
'z-index': currentListItemData[replaceIndex].zIndex,
'opacity': currentListItemData[replaceIndex].opacity
})
state.list[i] = currentListItemData[replaceIndex]
}
}, time)
}
}
// 切换到上一个轮播
function switchPrevSwiper(newIndex, touches, instance, state) {
var currentIndex = state.currentIndex
var list = state.list
var direction = state.itemData.direction
var touchRelactive = state.touchRelactive || {x: 0, y: 0}
var currentListItemData = JSON.parse(JSON.stringify(list))
if (direction === 'horizontal') {
// 水平方向移动
var itemWidth = state.itemData.itemWidth
// 当前上一个轮播移动到正常位置
state.itemsInstance[newIndex].setStyle({
'transform': 'translate3d(0px, 0px, 0px) scale(1)',
'z-index': list[currentIndex].zIndex + 1,
'opacity': list[currentIndex].opacity
})
// 计算当前移动需要的剩余时间
var time = Math.floor((itemWidth - Math.abs(touches.pageX - touchRelactive.x)) / itemWidth * 250)
// 更新除当前上一个轮播外的其他轮播,向后移动一个层级
// 更新列表位置相关数据
setTimeout(instance, function() {
for (var i = 0; i < list.length; i++) {
var replaceIndex = (i + 1 > list.length - 1) ? 0 : i + 1
state.itemsInstance[i].setStyle({
'transform': 'translate3d('+ currentListItemData[replaceIndex].translateX + 'px, 0px, 0px) scale(' + currentListItemData[replaceIndex].scale + ')',
'z-index': currentListItemData[replaceIndex].zIndex,
'opacity': currentListItemData[replaceIndex].opacity
})
state.list[i] = currentListItemData[replaceIndex]
}
}, time)
} else if (direction === 'vertical') {
// 垂直方向移动
var itemHeight = state.itemData.itemHeight
// 当前上一个轮播移动到正常位置
state.itemsInstance[newIndex].setStyle({
'transform': 'translate3d(0px, 0px, 0px) scale(1)',
'z-index': list[currentIndex].zIndex + 1,
'opacity': list[currentIndex].opacity
})
// 计算当前移动需要的剩余时间
var time = Math.floor((itemHeight - Math.abs(touches.pageY - touchRelactive.y)) / itemHeight * 250)
// 更新除当前上一个轮播外的其他轮播,向后移动一个层级
// 更新列表位置相关数据
setTimeout(instance, function() {
for (var i = 0; i < list.length; i++) {
var replaceIndex = (i + 1 > list.length - 1) ? 0 : i + 1
state.itemsInstance[i].setStyle({
'transform': 'translate3d(0px, '+ currentListItemData[replaceIndex].translateY + 'px, 0px) scale(' + currentListItemData[replaceIndex].scale + ')',
'z-index': currentListItemData[replaceIndex].zIndex,
'opacity': currentListItemData[replaceIndex].opacity
})
state.list[i] = currentListItemData[replaceIndex]
}
}, time)
}
}
// 反转动画
function toggleSwiperAnimation(state, add) {
if (!state.itemsInstance) return
if (add === true) {
state.itemsInstance.forEach(function(item, index) {
if (!item.hasClass('tn-stack-swiper__item__transition')) {
item.addClass('tn-stack-swiper__item__transition')
}
})
} else {
state.itemsInstance.forEach(function(item, index) {
if (item.hasClass('tn-stack-swiper__item__transition')) {
item.removeClass('tn-stack-swiper__item__transition')
}
})
}
}
// 更新数据
var itemDataObserver = function (newVal, oldVal, ownerInstance, instance) {
var state = ownerInstance.getState()
state.itemData = newVal
}
// 列表初始化
var listObserver = function(newVal, oldVal, ownerInstance, instance) {
var state = ownerInstance.getState()
var itemData = state.itemData
state.itemsInstance = ownerInstance.selectAllComponents('.tn-stack-swiper__item')
state.list = newVal || []
state.list.forEach(function(item, index) {
var itemInstance = state.itemsInstance[index]
if (item && itemInstance) {
if (itemData.direction === 'horizontal') {
itemInstance.setStyle({
'transform': 'translate3d('+ item.translateX + 'px, 0px, 0px) scale(' + item.scale + ')',
'z-index': item.zIndex,
'opacity': item.opacity
})
} else if (itemData.direction === 'vertical') {
itemInstance.setStyle({
'transform': 'translate3d(0px, '+ item.translateY + 'px, 0px) scale(' + item.scale + ')',
'z-index': item.zIndex,
'opacity': item.opacity
})
}
}
})
}
// 切换轮播位置
var swiperIndexChange = function(newVal, oldVal, ownerInstance, instance) {
var state = ownerInstance.getState()
// console.log(newVal);
// ownerInstance.callMethod('printLog', newVal)
// console.log(oldVal);
// ownerInstance.callMethod('printLog', oldVal)
// 排除第一次初始化和手动切换的情况
if (oldVal < 0 || typeof oldVal == 'undefined' || state.currentIndex == newVal) {
if (oldVal < 0 || typeof oldVal == 'undefined') {
state.currentIndex = 0
}
return
}
state.currentIndex = newVal
// console.log(state.currentIndex);
if (newVal > oldVal || (oldVal == state.list.length - 1 && newVal == 0)) {
// console.log("next");
// state.itemsInstance.forEach(function(item, index) {
// item.addClass("tn-stack-swiper__item__transition")
// })
switchNextSwiper(newVal, {
pageX: 0
}, state.itemsInstance[oldVal], state)
} else if (newVal < oldVal || (oldVal == 0 && newVal == state.list.length - 1)) {
// console.log("prev");
}
}
// 自动轮播切换状态
var autoplayFlagChange = function(newVal, oldVal, ownerInstance, instance) {
var state = ownerInstance.getState()
if (newVal === true) {
toggleSwiperAnimation(state, true)
} else {
toggleSwiperAnimation(state, false)
}
}
// 开始触摸
var touchStart = function (event, ownerInstance) {
// console.log('touchStart');
var instance = event.instance
var dataset = instance.getDataset()
var state = ownerInstance.getState()
var itemData = state.itemData
// 判断是否为为当前显示的轮播
if (dataset.index != state.currentIndex) return
var touches = event.changedTouches[0]
if (!touches) return
// 记录当前滑动开始的xy坐标
state.touchRelactive = {
x: touches.pageX,
y: touches.pageY
}
// 记录触摸id用于处理多指的情况
state.touchId = touches.identifier
if (itemData.direction === 'horizontal') {
// 水平方向移动
// 设置左右滑动时相对偏移距离
state.itemAnimationWidth = itemData.itemWidth * (dataset.switchrate / 100)
} else if (itemData.direction === 'vertical') {
// 垂直方向移动
// 设置上下滑动时相对偏移距离
state.itemAnimationHeight = itemData.itemHeight * (dataset.switchrate / 100)
}
// 移除运动动画时间
toggleSwiperAnimation(state, false)
// 标记开始触摸
state.touching = true
ownerInstance.callMethod('changeTouchState', {
touching: true
})
// 停止执行自动轮播
ownerInstance.callMethod('clearAutoPlayTimer')
}
// 开始移动
var touchMove = function (event, ownerInstance) {
// console.log('touchMove');
var instance = event.instance
var dataset = instance.getDataset()
var state = ownerInstance.getState()
var itemData = state.itemData
// 判断是否为为当前显示的轮播
if (dataset.index != state.currentIndex) return
// 还没开始触摸直接返回
if (!state.touching) return
var touches = event.changedTouches[0]
if (!touches) return
// 判断是否为同一个触摸点
if (state.touchId != touches.identifier) return
var currentTouchRelactive = {
x: touches.pageX,
y: touches.pageY
}
// 是否已经确定了移动方向
if (!state.direction) {
state.direction = decideSwiperDirection(state.touchRelactive, currentTouchRelactive, itemData.direction)
}
// console.log(decideSwiperDirection(state.touchRelactive, currentTouchRelactive));
updateSwiperStyle(currentTouchRelactive, instance, state)
}
// 移动结束
var touchEnd = function (event, ownerInstance) {
// console.log('touchEnd');
var instance = event.instance
var dataset = instance.getDataset()
var state = ownerInstance.getState()
var itemData = state.itemData
var list = state.list
var touchRelactive = state.touchRelactive
// 判断是否为为当前显示的轮播
if (dataset.index != state.currentIndex) return
// 还没开始触摸直接返回
if (!state.touching) return
var touches = event.changedTouches[0]
if (!touches) return
// 判断是否为同一个触摸点
if (state.touchId != touches.identifier) return
// 添加运动动画时间
toggleSwiperAnimation(state, true)
if (itemData.direction === 'horizontal') {
// 水平方向移动
var itemAnimationWidth = state.itemAnimationWidth
// 判断时左滑还是右滑
// 判断是否超过自动滚动到下一页还是回滚
if (state.direction == 'left') {
if (Math.abs(touches.pageX - touchRelactive.x) < itemAnimationWidth) {
list.forEach(function(item, index) {
var itemInstance = state.itemsInstance[index]
if (item && itemInstance) {
itemInstance.setStyle({
'transform': 'translate3d('+ item.translateX + 'px, 0px, 0px) scale(' + item.scale + ')',
'z-index': item.zIndex
})
}
})
} else {
var newIndex = state.currentIndex + 1 > list.length - 1 ? 0 : state.currentIndex + 1
switchNextSwiper(newIndex, touches, instance, state)
updateCurrentSwiperIndex(newIndex, ownerInstance, state)
}
} else if (state.direction == 'right') {
if (Math.abs(touches.pageX - touchRelactive.x) < itemAnimationWidth) {
// 滑动显示图片回滚
var preIndex = (state.currentIndex == 0) ? list.length - 1 : state.currentIndex - 1
state.itemsInstance[preIndex].setStyle({
'transform': 'translate3d(-' + itemData.itemWidth + 'px, 0px, 0px) scale(1)',
'z-index': list[state.currentIndex].zIndex + 1,
'opacity': list[state.currentIndex].opacity
})
list.forEach(function(item, index) {
var itemInstance = state.itemsInstance[index]
if (item && itemInstance) {
itemInstance.setStyle({
'transform': 'translate3d('+ item.translateX + 'px, 0px, 0px) scale(' + item.scale + ')',
'z-index': item.zIndex,
'opacity': item.opacity
})
}
})
} else {
var newIndex = (state.currentIndex - 1 < 0) ? list.length - 1 : state.currentIndex - 1
switchPrevSwiper(newIndex, touches, instance, state)
updateCurrentSwiperIndex(newIndex, ownerInstance, state)
}
}
} else if (itemData.direction === 'vertical') {
// 垂直方向移动
var itemAnimationHeight = state.itemAnimationHeight
// 判断时上滑还是下滑
// 判断是否超过自动滚动到下一页还是回滚
if (state.direction == 'up') {
if (Math.abs(touches.pageY - touchRelactive.y) < itemAnimationHeight) {
list.forEach(function(item, index) {
var itemInstance = state.itemsInstance[index]
if (item && itemInstance) {
itemInstance.setStyle({
'transform': 'translate3d(0px, '+ item.translateY + 'px, 0px) scale(' + item.scale + ')',
'z-index': item.zIndex,
'opacity': item.opacity
})
}
})
} else {
var newIndex = state.currentIndex + 1 > list.length - 1 ? 0 : state.currentIndex + 1
switchNextSwiper(newIndex, touches, instance, state)
updateCurrentSwiperIndex(newIndex, ownerInstance, state)
}
} else if (state.direction == 'down') {
if (Math.abs(touches.pageY - touchRelactive.y) < itemAnimationHeight) {
// 滑动显示图片回滚
var preIndex = (state.currentIndex == 0) ? list.length - 1 : state.currentIndex - 1
state.itemsInstance[preIndex].setStyle({
'transform': 'translate3d(0px, -' + itemData.itemHeight + 'px, 0px) scale(1)',
'z-index': list[state.currentIndex].zIndex + 1,
'opacity': list[state.currentIndex].opacity
})
list.forEach(function(item, index) {
var itemInstance = state.itemsInstance[index]
if (item && itemInstance) {
itemInstance.setStyle({
'transform': 'translate3d(0px, '+ item.translateY + 'px, 0px) scale(' + item.scale + ')',
'z-index': item.zIndex,
'opacity': item.opacity
})
}
})
} else {
var newIndex = (state.currentIndex - 1 < 0) ? list.length - 1 : state.currentIndex - 1
switchPrevSwiper(newIndex, touches, instance, state)
updateCurrentSwiperIndex(newIndex, ownerInstance, state)
}
}
}
// 清除对应的标志位
state.touchRelactive = null
state.touching = false
state.direction = null
state.touchId = null
ownerInstance.callMethod('changeTouchState', {
touching: false
})
// 重新开始执行自动轮播
ownerInstance.callMethod('setAutoPlay')
}
module.exports = {
itemDataObserver: itemDataObserver,
listObserver: listObserver,
swiperIndexChange: swiperIndexChange,
autoplayFlagChange: autoplayFlagChange,
touchStart: touchStart,
touchMove: touchMove,
touchEnd: touchEnd
}

View File

@ -0,0 +1,657 @@
function setTimeout(instance, cb, time) {
if (time > 0) {
var s = getDate().getTime()
var fn = function () {
if (getDate().getTime() - s > time) {
cb && cb()
} else
instance.requestAnimationFrame(fn)
}
fn()
}
else
cb && cb()
}
// 判断触摸的移动方向
function decideSwiperDirection(startTouches, currentTouches, direction) {
// 震动偏移容差
var toleranceShake = 30
// 移动容差
var toleranceTranslate = 10
if (direction === 'horizontal') {
// 水平方向移动
if (Math.abs(currentTouches.y - startTouches.y) <= toleranceShake) {
// console.log(currentTouches.x, startTouches.x);
if (Math.abs(currentTouches.x - startTouches.x) > toleranceTranslate) {
if (currentTouches.x - startTouches.x > 0) {
return 'right'
} else if (currentTouches.x - startTouches.x < 0) {
return 'left'
}
}
}
} else if (direction === 'vertical') {
// 垂直方向移动
if (Math.abs(currentTouches.x - startTouches.x) <= toleranceShake) {
// console.log(currentTouches.x, startTouches.x);
if (Math.abs(currentTouches.y - startTouches.y) > toleranceTranslate) {
if (currentTouches.y - startTouches.y > 0) {
return 'down'
} else if (currentTouches.y - startTouches.y < 0) {
return 'up'
}
}
}
}
return ''
}
// 更新轮播样式信息
function updateSwiperStyle(currentTouches, instance, state) {
var itemData = state.itemData
var itemsInstance = state.itemsInstance
var list = state.list
var currentIndex = state.currentIndex
var touchRelactive = state.touchRelactive
// console.log(itemAnimationWidth);
if (itemData.direction === 'horizontal') {
// 水平方向
var itemAnimationWidth = state.itemAnimationWidth
// 偏移的x轴距离
var translateX = currentTouches.x - touchRelactive.x
if (currentTouches.x > itemData.windowWidth || currentTouches.x < 0) return
// console.log(translateX);
// 更新其他轮播样式
if (state.direction == 'left') {
// 设置当前激活元素的偏移量
instance.setStyle({
'transform': 'translate3d('+ translateX + 'px, 0px, 0px)',
'z-index': list[currentIndex].zIndex + 1,
'opacity': list[currentIndex].opacity
})
// 移动距离是否超过了指定的容器宽度
if (Math.abs(translateX) > itemAnimationWidth) {
state.itemsInstance.forEach( function(itemInstance, index) {
if (index != currentIndex) {
var preIndex = (index == 0) ? list.length - 1 : index - 1
var distanceRate = (Math.abs(translateX) - itemAnimationWidth) / (itemData.itemWidth - itemAnimationWidth)
var itemTranslateX = list[index].translateX - (list[index].translateX - list[preIndex].translateX) * distanceRate
var itemScale = list[index].scale + (list[preIndex].scale - list[index].scale) * distanceRate
var itemOpacity = list[index].opacity + (list[preIndex].opacity - list[index].opacity) * distanceRate
// console.log(preIndex);
// console.log(list[index]);
// console.log(distanceRate);
// console.log(itemTranslateX);
// console.log(itemScale);
// console.log(itemOpacity);
// console.log('-----------------------------------------------------------');
itemInstance.setStyle({
'transform': 'translate3d(' + itemTranslateX + 'px, 0px, 0px) scale(' + itemScale + ')',
'z-index': list[index].zIndex,
'opacity': itemOpacity
})
}
})
}
} else if (state.direction == 'right') {
var preIndex = (currentIndex == 0) ? list.length - 1 : currentIndex - 1
// 右滑的时候把最底部的取出,并放到最高层级
state.itemsInstance[preIndex].setStyle({
'transform': 'translate3d(-' + (itemData.itemWidth - translateX) + 'px, 0px, 0px) scale(1)',
'z-index': list[currentIndex].zIndex + 1,
'opacity': list[currentIndex].opacity
})
// 当前轮播逐渐缩小
if (Math.abs(translateX) < itemAnimationWidth) {
state.itemsInstance.forEach( function(itemInstance, index) {
if (index != preIndex) {
var replaceIndex = index == list.length - 1 ? 0 : index + 1
var distanceRate = Math.abs(translateX) / itemAnimationWidth
var itemTranslateX = list[index].translateX + (list[replaceIndex].translateX - list[index].translateX) * distanceRate
var itemScale = list[index].scale - (list[index].scale - list[replaceIndex].scale) * distanceRate
var itemOpacity = list[index].opacity - (list[index].opacity - list[replaceIndex].opacity) * distanceRate
// console.log(preIndex);
// console.log(index);
// console.log(replaceIndex);
// console.log(list[index]);
// console.log(list[replaceIndex].translateX - list[index].translateX);
// console.log(distanceRate);
// console.log(itemTranslateX);
// console.log(itemScale);
// console.log('-----------------------------------------------------------');
itemInstance.setStyle({
'transform': 'translate3d(' + itemTranslateX + 'px, 0px, 0px) scale(' + itemScale + ')',
'z-index': list[index].zIndex,
'opacity': itemOpacity
})
}
})
}
}
} else if (itemData.direction === 'vertical') {
// 垂直方向
var itemAnimationHeight = state.itemAnimationHeight
// 偏移的y轴距离
var translateY = currentTouches.y - touchRelactive.y
if (currentTouches.y > itemData.windowHeight || currentTouches.y < 0) return
// console.log(translateX);
// 更新其他轮播样式
if (state.direction == 'up') {
// 设置当前激活元素的偏移量
instance.setStyle({
'transform': 'translate3d(0px, '+ translateY + 'px, 0px)',
'z-index': list[currentIndex].zIndex + 1,
'opacity': list[currentIndex].opacity
})
// 移动距离是否超过了指定的容器宽度
if (Math.abs(translateY) > itemAnimationHeight) {
state.itemsInstance.forEach( function(itemInstance, index) {
if (index != currentIndex) {
var preIndex = (index == 0) ? list.length - 1 : index - 1
var distanceRate = (Math.abs(translateY) - itemAnimationHeight) / (itemData.itemHeight - itemAnimationHeight)
var itemTranslateY = list[index].translateY - (list[index].translateY - list[preIndex].translateY) * distanceRate
var itemScale = list[index].scale + (list[preIndex].scale - list[index].scale) * distanceRate
var itemOpacity = list[index].opacity + (list[preIndex].opacity - list[index].opacity) * distanceRate
// console.log(preIndex);
// console.log(list[index]);
// console.log(distanceRate);
// console.log(itemTranslateX);
// console.log(itemScale);
// console.log('-----------------------------------------------------------');
itemInstance.setStyle({
'transform': 'translate3d(0px, ' + itemTranslateY + 'px, 0px) scale(' + itemScale + ')',
'z-index': list[index].zIndex,
'opacity': itemOpacity
})
}
})
}
} else if (state.direction == 'down') {
var preIndex = (currentIndex == 0) ? list.length - 1 : currentIndex - 1
// 下滑的时候把最底部的取出,并放到最高层级
state.itemsInstance[preIndex].setStyle({
'transform': 'translate3d(0px, -' + (itemData.itemHeight - translateY) + 'px, 0px) scale(1)',
'z-index': list[currentIndex].zIndex + 1,
'opacity': list[currentIndex].opacity
})
// 当前轮播逐渐缩小
if (Math.abs(translateY) < itemAnimationHeight) {
state.itemsInstance.forEach( function(itemInstance, index) {
if (index != preIndex) {
var replaceIndex = index == list.length - 1 ? 0 : index + 1
var distanceRate = Math.abs(translateY) / itemAnimationHeight
var itemTranslateY = list[index].translateY + (list[replaceIndex].translateY - list[index].translateY) * distanceRate
var itemScale = list[index].scale - (list[index].scale - list[replaceIndex].scale) * distanceRate
var itemOpacity = list[index].opacity - (list[index].opacity - list[replaceIndex].opacity) * distanceRate
// console.log(preIndex);
// console.log(index);
// console.log(replaceIndex);
// console.log(list[index]);
// console.log(list[replaceIndex].translateX - list[index].translateX);
// console.log(distanceRate);
// console.log(itemTranslateX);
// console.log(itemScale);
// console.log('-----------------------------------------------------------');
itemInstance.setStyle({
'transform': 'translate3d(0px, ' + itemTranslateY + 'px, 0px) scale(' + itemScale + ')',
'z-index': list[index].zIndex,
'opacity': itemOpacity
})
}
})
}
}
}
}
// 更新当前轮播序号
function updateCurrentSwiperIndex(index, ownerInstance, state) {
state.currentIndex = index
ownerInstance.callMethod('changeSwiperIndex', {
index: index
})
}
// 切换到下一个轮播
function switchNextSwiper(newIndex, touches, instance, state) {
var currentIndex = state.currentIndex
var list = state.list
var direction = state.itemData.direction
var touchRelactive = state.touchRelactive || {x: 0, y: 0}
// 已经完成轮播切换
var currentListItemData = JSON.parse(JSON.stringify(list))
if (direction === 'horizontal') {
// 水平方向移动
var itemWidth = state.itemData.itemWidth
// 当前轮播移动到最左边
instance.setStyle({
'transform': 'translate3d(-'+ itemWidth + 'px, 0px, 0px) scale(1)',
'z-index': list[currentIndex].zIndex + 1,
'opacity': list[currentIndex].opacity
})
// 计算当前移动需要的剩余时间
var time = Math.floor((itemWidth - Math.abs(touches.pageX - touchRelactive.x)) / itemWidth * 250)
setTimeout(instance, function() {
for (var i = list.length - 1; i >= 0; i--) {
var replaceIndex = i - 1 < 0 ? list.length - 1 : i - 1
// console.log(i);
// console.log(replaceIndex);
state.itemsInstance[i].setStyle({
'transform': 'translate3d('+ currentListItemData[replaceIndex].translateX + 'px, 0px, 0px) scale(' + currentListItemData[replaceIndex].scale + ')',
'z-index': currentListItemData[replaceIndex].zIndex,
'opacity': currentListItemData[replaceIndex].opacity
})
state.list[i] = currentListItemData[replaceIndex]
}
}, time)
} else if (direction === 'vertical') {
// 垂直方向移动
var itemHeight = state.itemData.itemHeight
// 当前轮播移动到最上边
instance.setStyle({
'transform': 'translate3d(0px, -'+ itemHeight + 'px, 0px) scale(1)',
'z-index': list[currentIndex].zIndex + 1,
'opacity': list[currentIndex].opacity
})
// 计算当前移动需要的剩余时间
var time = Math.floor((itemHeight - Math.abs(touches.pageY - touchRelactive.y)) / itemHeight * 250)
setTimeout(instance, function() {
for (var i = list.length - 1; i >= 0; i--) {
var replaceIndex = i - 1 < 0 ? list.length - 1 : i - 1
// console.log(i);
// console.log(replaceIndex);
state.itemsInstance[i].setStyle({
'transform': 'translate3d(0px, '+ currentListItemData[replaceIndex].translateY + 'px, 0px) scale(' + currentListItemData[replaceIndex].scale + ')',
'z-index': currentListItemData[replaceIndex].zIndex,
'opacity': currentListItemData[replaceIndex].opacity
})
state.list[i] = currentListItemData[replaceIndex]
}
}, time)
}
}
// 切换到上一个轮播
function switchPrevSwiper(newIndex, touches, instance, state) {
var currentIndex = state.currentIndex
var list = state.list
var direction = state.itemData.direction
var touchRelactive = state.touchRelactive || {x: 0, y: 0}
var currentListItemData = JSON.parse(JSON.stringify(list))
if (direction === 'horizontal') {
// 水平方向移动
var itemWidth = state.itemData.itemWidth
// 当前上一个轮播移动到正常位置
state.itemsInstance[newIndex].setStyle({
'transform': 'translate3d(0px, 0px, 0px) scale(1)',
'z-index': list[currentIndex].zIndex + 1,
'opacity': list[currentIndex].opacity
})
// 计算当前移动需要的剩余时间
var time = Math.floor((itemWidth - Math.abs(touches.pageX - touchRelactive.x)) / itemWidth * 250)
// 更新除当前上一个轮播外的其他轮播,向后移动一个层级
// 更新列表位置相关数据
setTimeout(instance, function() {
for (var i = 0; i < list.length; i++) {
var replaceIndex = (i + 1 > list.length - 1) ? 0 : i + 1
state.itemsInstance[i].setStyle({
'transform': 'translate3d('+ currentListItemData[replaceIndex].translateX + 'px, 0px, 0px) scale(' + currentListItemData[replaceIndex].scale + ')',
'z-index': currentListItemData[replaceIndex].zIndex,
'opacity': currentListItemData[replaceIndex].opacity
})
state.list[i] = currentListItemData[replaceIndex]
}
}, time)
} else if (direction === 'vertical') {
// 垂直方向移动
var itemHeight = state.itemData.itemHeight
// 当前上一个轮播移动到正常位置
state.itemsInstance[newIndex].setStyle({
'transform': 'translate3d(0px, 0px, 0px) scale(1)',
'z-index': list[currentIndex].zIndex + 1,
'opacity': list[currentIndex].opacity
})
// 计算当前移动需要的剩余时间
var time = Math.floor((itemHeight - Math.abs(touches.pageY - touchRelactive.y)) / itemHeight * 250)
// 更新除当前上一个轮播外的其他轮播,向后移动一个层级
// 更新列表位置相关数据
setTimeout(instance, function() {
for (var i = 0; i < list.length; i++) {
var replaceIndex = (i + 1 > list.length - 1) ? 0 : i + 1
state.itemsInstance[i].setStyle({
'transform': 'translate3d(0px, '+ currentListItemData[replaceIndex].translateY + 'px, 0px) scale(' + currentListItemData[replaceIndex].scale + ')',
'z-index': currentListItemData[replaceIndex].zIndex,
'opacity': currentListItemData[replaceIndex].opacity
})
state.list[i] = currentListItemData[replaceIndex]
}
}, time)
}
}
// 反转动画
function toggleSwiperAnimation(state, add) {
if (!state.itemsInstance) return
if (add === true) {
state.itemsInstance.forEach(function(item, index) {
if (!item.hasClass('tn-stack-swiper__item__transition')) {
item.addClass('tn-stack-swiper__item__transition')
}
})
} else {
state.itemsInstance.forEach(function(item, index) {
if (item.hasClass('tn-stack-swiper__item__transition')) {
item.removeClass('tn-stack-swiper__item__transition')
}
})
}
}
// 更新数据
var itemDataObserver = function (newVal, oldVal, ownerInstance, instance) {
var state = ownerInstance.getState()
state.itemData = newVal
}
// 列表初始化
var listObserver = function(newVal, oldVal, ownerInstance, instance) {
var state = ownerInstance.getState()
var itemData = state.itemData
state.itemsInstance = ownerInstance.selectAllComponents('.tn-stack-swiper__item')
state.list = newVal || []
state.list.forEach(function(item, index) {
var itemInstance = state.itemsInstance[index]
if (item && itemInstance) {
if (itemData.direction === 'horizontal') {
itemInstance.setStyle({
'transform': 'translate3d('+ item.translateX + 'px, 0px, 0px) scale(' + item.scale + ')',
'z-index': item.zIndex,
'opacity': item.opacity
})
} else if (itemData.direction === 'vertical') {
itemInstance.setStyle({
'transform': 'translate3d(0px, '+ item.translateY + 'px, 0px) scale(' + item.scale + ')',
'z-index': item.zIndex,
'opacity': item.opacity
})
}
}
})
}
// 切换轮播位置
var swiperIndexChange = function(newVal, oldVal, ownerInstance, instance) {
var state = ownerInstance.getState()
// console.log(newVal);
// ownerInstance.callMethod('printLog', newVal)
// console.log(oldVal);
// ownerInstance.callMethod('printLog', oldVal)
// 排除第一次初始化和手动切换的情况
if (oldVal < 0 || typeof oldVal == 'undefined' || state.currentIndex == newVal) {
if (oldVal < 0 || typeof oldVal == 'undefined') {
state.currentIndex = 0
}
return
}
state.currentIndex = newVal
// console.log(state.currentIndex);
if (newVal > oldVal || (oldVal == state.list.length - 1 && newVal == 0)) {
// console.log("next");
// state.itemsInstance.forEach(function(item, index) {
// item.addClass("tn-stack-swiper__item__transition")
// })
switchNextSwiper(newVal, {
pageX: 0
}, state.itemsInstance[oldVal], state)
} else if (newVal < oldVal || (oldVal == 0 && newVal == state.list.length - 1)) {
// console.log("prev");
}
}
// 自动轮播切换状态
var autoplayFlagChange = function(newVal, oldVal, ownerInstance, instance) {
var state = ownerInstance.getState()
if (newVal === true) {
toggleSwiperAnimation(state, true)
} else {
toggleSwiperAnimation(state, false)
}
}
// 开始触摸
var touchStart = function (event, ownerInstance) {
// console.log('touchStart');
var instance = event.instance
var dataset = instance.getDataset()
var state = ownerInstance.getState()
var itemData = state.itemData
// 判断是否为为当前显示的轮播
if (dataset.index != state.currentIndex) return
var touches = event.changedTouches[0]
if (!touches) return
// 记录当前滑动开始的xy坐标
state.touchRelactive = {
x: touches.pageX,
y: touches.pageY
}
// 记录触摸id用于处理多指的情况
state.touchId = touches.identifier
if (itemData.direction === 'horizontal') {
// 水平方向移动
// 设置左右滑动时相对偏移距离
state.itemAnimationWidth = itemData.itemWidth * (dataset.switchrate / 100)
} else if (itemData.direction === 'vertical') {
// 垂直方向移动
// 设置上下滑动时相对偏移距离
state.itemAnimationHeight = itemData.itemHeight * (dataset.switchrate / 100)
}
// 移除运动动画时间
toggleSwiperAnimation(state, false)
// 标记开始触摸
state.touching = true
ownerInstance.callMethod('changeTouchState', {
touching: true
})
// 停止执行自动轮播
ownerInstance.callMethod('clearAutoPlayTimer')
}
// 开始移动
var touchMove = function (event, ownerInstance) {
// console.log('touchMove');
var instance = event.instance
var dataset = instance.getDataset()
var state = ownerInstance.getState()
var itemData = state.itemData
// 判断是否为为当前显示的轮播
if (dataset.index != state.currentIndex) return
// 还没开始触摸直接返回
if (!state.touching) return
var touches = event.changedTouches[0]
if (!touches) return
// 判断是否为同一个触摸点
if (state.touchId != touches.identifier) return
var currentTouchRelactive = {
x: touches.pageX,
y: touches.pageY
}
// 是否已经确定了移动方向
if (!state.direction) {
state.direction = decideSwiperDirection(state.touchRelactive, currentTouchRelactive, itemData.direction)
}
// console.log(decideSwiperDirection(state.touchRelactive, currentTouchRelactive));
updateSwiperStyle(currentTouchRelactive, instance, state)
}
// 移动结束
var touchEnd = function (event, ownerInstance) {
// console.log('touchEnd');
var instance = event.instance
var dataset = instance.getDataset()
var state = ownerInstance.getState()
var itemData = state.itemData
var list = state.list
var touchRelactive = state.touchRelactive
// 判断是否为为当前显示的轮播
if (dataset.index != state.currentIndex) return
// 还没开始触摸直接返回
if (!state.touching) return
var touches = event.changedTouches[0]
if (!touches) return
// 判断是否为同一个触摸点
if (state.touchId != touches.identifier) return
// 添加运动动画时间
toggleSwiperAnimation(state, true)
if (itemData.direction === 'horizontal') {
// 水平方向移动
var itemAnimationWidth = state.itemAnimationWidth
// 判断时左滑还是右滑
// 判断是否超过自动滚动到下一页还是回滚
if (state.direction == 'left') {
if (Math.abs(touches.pageX - touchRelactive.x) < itemAnimationWidth) {
list.forEach(function(item, index) {
var itemInstance = state.itemsInstance[index]
if (item && itemInstance) {
itemInstance.setStyle({
'transform': 'translate3d('+ item.translateX + 'px, 0px, 0px) scale(' + item.scale + ')',
'z-index': item.zIndex
})
}
})
} else {
var newIndex = state.currentIndex + 1 > list.length - 1 ? 0 : state.currentIndex + 1
switchNextSwiper(newIndex, touches, instance, state)
updateCurrentSwiperIndex(newIndex, ownerInstance, state)
}
} else if (state.direction == 'right') {
if (Math.abs(touches.pageX - touchRelactive.x) < itemAnimationWidth) {
// 滑动显示图片回滚
var preIndex = (state.currentIndex == 0) ? list.length - 1 : state.currentIndex - 1
state.itemsInstance[preIndex].setStyle({
'transform': 'translate3d(-' + itemData.itemWidth + 'px, 0px, 0px) scale(1)',
'z-index': list[state.currentIndex].zIndex + 1,
'opacity': list[state.currentIndex].opacity
})
list.forEach(function(item, index) {
var itemInstance = state.itemsInstance[index]
if (item && itemInstance) {
itemInstance.setStyle({
'transform': 'translate3d('+ item.translateX + 'px, 0px, 0px) scale(' + item.scale + ')',
'z-index': item.zIndex,
'opacity': item.opacity
})
}
})
} else {
var newIndex = (state.currentIndex - 1 < 0) ? list.length - 1 : state.currentIndex - 1
switchPrevSwiper(newIndex, touches, instance, state)
updateCurrentSwiperIndex(newIndex, ownerInstance, state)
}
}
} else if (itemData.direction === 'vertical') {
// 垂直方向移动
var itemAnimationHeight = state.itemAnimationHeight
// 判断时上滑还是下滑
// 判断是否超过自动滚动到下一页还是回滚
if (state.direction == 'up') {
if (Math.abs(touches.pageY - touchRelactive.y) < itemAnimationHeight) {
list.forEach(function(item, index) {
var itemInstance = state.itemsInstance[index]
if (item && itemInstance) {
itemInstance.setStyle({
'transform': 'translate3d(0px, '+ item.translateY + 'px, 0px) scale(' + item.scale + ')',
'z-index': item.zIndex,
'opacity': item.opacity
})
}
})
} else {
var newIndex = state.currentIndex + 1 > list.length - 1 ? 0 : state.currentIndex + 1
switchNextSwiper(newIndex, touches, instance, state)
updateCurrentSwiperIndex(newIndex, ownerInstance, state)
}
} else if (state.direction == 'down') {
if (Math.abs(touches.pageY - touchRelactive.y) < itemAnimationHeight) {
// 滑动显示图片回滚
var preIndex = (state.currentIndex == 0) ? list.length - 1 : state.currentIndex - 1
state.itemsInstance[preIndex].setStyle({
'transform': 'translate3d(0px, -' + itemData.itemHeight + 'px, 0px) scale(1)',
'z-index': list[state.currentIndex].zIndex + 1,
'opacity': list[state.currentIndex].opacity
})
list.forEach(function(item, index) {
var itemInstance = state.itemsInstance[index]
if (item && itemInstance) {
itemInstance.setStyle({
'transform': 'translate3d(0px, '+ item.translateY + 'px, 0px) scale(' + item.scale + ')',
'z-index': item.zIndex,
'opacity': item.opacity
})
}
})
} else {
var newIndex = (state.currentIndex - 1 < 0) ? list.length - 1 : state.currentIndex - 1
switchPrevSwiper(newIndex, touches, instance, state)
updateCurrentSwiperIndex(newIndex, ownerInstance, state)
}
}
}
// 清除对应的标志位
state.touchRelactive = null
state.touching = false
state.direction = null
state.touchId = null
ownerInstance.callMethod('changeTouchState', {
touching: false
})
// 重新开始执行自动轮播
ownerInstance.callMethod('setAutoPlay')
}
module.exports = {
itemDataObserver: itemDataObserver,
listObserver: listObserver,
swiperIndexChange: swiperIndexChange,
autoplayFlagChange: autoplayFlagChange,
touchStart: touchStart,
touchMove: touchMove,
touchEnd: touchEnd
}

View File

@ -0,0 +1,284 @@
<template>
<view
class="tn-stack-swiper-class tn-stack-swiper"
:style="{
width: $t.string.getLengthUnitValue(width),
height: $t.string.getLengthUnitValue(height)
}"
:list="swiperList"
:itemData="itemData"
:currentIndex="swiperIndex"
:autoplayFlag="autoplayFlag"
:change:list="wxs.listObserver"
:change:itemData="wxs.itemDataObserver"
:change:currentIndex="wxs.swiperIndexChange"
:change:autoplayFlag="wxs.autoplayFlagChange"
>
<block v-for="(item, index) in list" :key="index">
<!-- #ifdef MP-WEIXIN -->
<view
class="tn-stack-swiper__item tn-stack-swiper__item__transition"
:class="[`tn-stack-swiper__item--${direction}`]"
:data-index="index"
:data-switchRate="switchRate"
@touchstart="wxs.touchStart"
:catch:touchmove="touching?wxs.touchMove:''"
:catch:touchend="touching?wxs.touchEnd:''"
>
<image class="tn-stack-swiper__image" :src="item.image"></image>
</view>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<view
class="tn-stack-swiper__item"
:class="[`tn-stack-swiper__item--${direction}`]"
:data-index="index"
:data-switchRate="switchRate"
@touchstart="wxs.touchStart"
@touchmove="wxs.touchMove"
@touchend="wxs.touchEnd"
>
<image class="tn-stack-swiper__image" :src="item.image"></image>
</view>
<!-- #endif -->
</block>
</view>
</template>
<!-- #ifdef MP-WEIXIN -->
<script src="./index.wxs" lang="wxs" module="wxs"></script>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<script src="./index-h5.wxs" lang="wxs" module="wxs"></script>
<!-- #endif -->
<script>
export default {
name: 'tn-stack-swiper',
props: {
//
// {
// //
// image: 'xxx'
// }
list: {
type: Array,
default() {
return []
}
},
// rpx
width: {
type: [String, Number],
default: '100%'
},
// rpx
height: {
type: [String, Number],
default: 500
},
//
autoplay: {
type: Boolean,
default: false
},
// ms
interval: {
type: Number,
default: 5000
},
// , [0 - 100]
// item
switchRate: {
type: Number,
default: 30
},
// [0-1]
scaleRate: {
type: Number,
default: 0.1
},
//
translateRate: {
type: Number,
default: 16
},
//
opacityRate: {
type: Number,
default:10
},
//
// horizontal -> vertical ->
direction: {
type: String,
default: 'horizontal'
}
},
data() {
return {
autoplayTimer: null,
// window
windowWidth: 0,
// item
swiperItemWidth: 0,
// item
swiperItemHeight: 0,
// item
swiperIndex: -1,
//
touching: true,
//
swiperList: [],
//
autoplayFlag: false
}
},
computed: {
itemData() {
return {
windowWidth: this.windowWidth,
itemWidth: this.swiperItemWidth,
itemHeight: this.swiperItemHeight,
direction: this.direction,
autoplaying: this.autoplayFlag
}
}
},
watch: {
list(val) {
this.swiperList = []
this.$nextTick(() => {
this.initSwiperRectInfo()
})
},
autoplay(val) {
if (!val) {
this.clearAutoPlayTimer()
} else {
this.setAutoPlay()
}
}
},
created() {
this.autoplayFlag = this.autoplay
},
mounted() {
this.$nextTick(() => {
this.initSwiperRectInfo()
})
},
destroyed() {
this.clearAutoPlayTimer()
},
methods: {
//
async initSwiperRectInfo() {
//
// this.touching = true
// item
const swiperItemRect = await this._tGetRect('.tn-stack-swiper__item')
if (!swiperItemRect || !swiperItemRect.width || !swiperItemRect.height) {
setTimeout(() => {
this.initSwiperRectInfo()
}, 50)
return
}
this.swiperItemWidth = swiperItemRect.width
this.swiperItemHeight = swiperItemRect.height
// this.touching = false
//
const systemInfo = uni.getSystemInfoSync()
this.windowWidth = systemInfo.windowWidth
this.swiperIndex = 0
// swiper
this.swiperList = this.list.map((item, index) => {
const scale = 1 - (this.scaleRate * index)
if (this.direction === 'horizontal') {
item.translateX = ((index * this.translateRate) * 0.01 * this.swiperItemWidth)
} else if (this.direction === 'vertical') {
item.translateY = ((index * this.translateRate) * 0.01 * this.swiperItemHeight)
}
item.opacity = (1 - ((index * this.opacityRate) * 0.01))
item.zIndex = this.list.length - index
item.scale = scale <= 0 ? 0 : scale
return item
})
this.setAutoPlay()
},
//
setAutoPlay() {
if (this.autoplay) {
this.clearAutoPlayTimer()
this.autoplayFlag = true
this.autoplayTimer = setInterval(() => {
this.swiperIndex = this.swiperIndex + 1 > this.swiperList.length - 1 ? 0 : this.swiperIndex + 1
}, this.interval)
}
},
//
clearAutoPlayTimer() {
if (this.autoplayTimer != null) {
this.autoplayFlag = false
clearInterval(this.autoplayTimer)
}
},
// index
changeSwiperIndex(e) {
// console.log(e.index);
this.swiperIndex = e.index
this.$emit('change', { index: e.index })
},
//
changeTouchState(e) {
this.touching = e.touching
},
//
printLog(data) {
console.log("log", data);
}
}
}
</script>
<style lang="scss" scoped>
.tn-stack-swiper {
position: relative;
&__item {
position: absolute;
border-radius: 20rpx;
overflow: hidden;
&--horizontal {
width: 88%;
height: 100%;
transform-origin: left center;
}
&--vertical {
width: 100%;
height: 88%;
transform-origin: top center;
}
&__transition {
transition-property: transform,opacity;
transition-duration: 0.25s;
transition-timing-function: ease-out;
// transition: transform, opacity 0.25s ease-in-out !important;
}
}
&__image {
width: 100%;
height: 100%;
}
}
</style>

View File

@ -0,0 +1,346 @@
<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}`]"
@tap="clickStepItem(index)"
>
<!-- 数值模式 -->
<view
v-if="mode === 'number'"
class="tn-steps__item__number"
:style="{
backgroundColor: currentIndex <= index ? 'transparent' : activeColor,
borderColor: currentIndex <= index ? inActiveColor : activeColor
}"
>
<text
class="tn-steps__item__number__text"
:class="[{'tn-steps__item__number__text--visible': currentIndex <= index}]"
:style="{
color: currentIndex <= 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': currentIndex > index}]"></view>
</view>
<!-- 点模式 -->
<view
v-if="mode === 'dot'"
class="tn-steps__item__dot"
:style="{
backgroundColor: currentIndex <= index ? inActiveColor : activeColor
}"
></view>
<!-- 图标模式 -->
<view
v-if="mode === 'icon'"
class="tn-steps__item__icon"
:class="[iconModeClass(index)]"
:style="{
color: currentIndex <= index ? inActiveColor : activeColor
}"
></view>
<!-- 点图标模式 -->
<view v-if="mode === 'dotIcon'" class="tn-steps__item__dot-icon">
<view v-if="currentIndex <= 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: currentIndex <= 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: currentIndex <= index + 1 ? inActiveColor : activeColor
}"
></view>
</view>
</view>
</template>
<script>
export default {
name: 'tn-steps',
props: {
//
// dot -> number -> icon -> dotIcon ->
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.currentIndex > index && item.hasOwnProperty('selectIcon')) {
return `tn-icon-${item.selectIcon}`
} else {
//
return `tn-icon-${item.icon || this.icon}`
}
}
}
},
data() {
return {
currentIndex: 0
}
},
watch: {
current: {
handler(val) {
this.currentIndex = val
},
immediate: true
}
},
methods: {
//
clickStepItem(index) {
const chooseIndex = index + 1
this.currentIndex = chooseIndex
this.$emit('click', { index: chooseIndex })
}
}
}
</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>

View File

@ -0,0 +1,186 @@
<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: {
//
// H5customNavBar45px
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.color.getBackgroundColorStyle(this.backgroundColor)
},
backgroundColorClass() {
return this.$t.color.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()
}
},
customNavHeight(val) {
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.createIntersectionObserverthis.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>

View File

@ -0,0 +1,410 @@
<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"
:class="[itemBarClass]"
: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: 'subsection'
},
//
height: {
type: Number,
default: 60
},
// tab
list: {
type: Array,
default() {
return []
}
},
// tabindex
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'
},
// modebuttonrpx
borderRadius: {
type: Number,
default: 10
},
//
animation: {
type: Boolean,
default: true
},
//
// cubic-bezier -> 线
animationType: {
type: String,
default: ''
},
//
vibrateShort: {
type: Boolean,
default: false
}
},
data() {
return {
//
listInfo: [],
//
itemBgStyle: {
width: 0,
left: 0,
backgroundColor: '#ffffff',
height: '100%'
},
//
currentIndex: this.current,
buttonPadding: 3,
// current
firstVibrateShort: true
}
},
watch: {
current: {
handler(val) {
this.currentIndex = val
this.changeSectionStatus(val)
},
immediate: true
}
},
created() {
// listlistInfo
// ['', ''],[{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 (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.fontSizeStyle) {
style.fontSize = this.fontSizeStyle
}
if (this.mode === 'subsection') {
// border
style.borderColor = this.buttonColor
style.borderWidth = '1rpx'
style.borderStyle = 'solid'
}
return style
}
},
// mode = buttonview
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}rpx`
}
return style
},
// mode = buttonviewclass
subsectionBackgroundColorClass() {
let clazz = ''
if (this.mode === 'button' && this.backgroundColorClass) {
clazz = this.backgroundColorClass
}
return clazz
},
itemBarClass() {
let clazz = ''
const buttonBgClass = this.$t.color.getBackgroundColorInternalClass(this.buttonColor)
if (this.animation) {
clazz += ' tn-subsection__bg__animation'
if (this.animationType) {
clazz += ` tn-subsection__bg__animation--${this.animationType}`
}
}
if (buttonBgClass) {
clazz += ` ${buttonBgClass}`
}
return clazz
},
//
itemBarStyle() {
let style = {}
const buttonBgStyle = this.$t.color.getBackgroundColorStyle(this.buttonColor)
if (buttonBgStyle) {
style.backgroundColor = this.buttonColor
}
style.zIndex = 1
if (this.mode === 'button') {
style.borderRadius = `${this.borderRadius}rpx`
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() {
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 {
transition: 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;
transition-property: all;
transition-duration: 0s;
transition-timing-function: linear;
&__animation {
transition-duration: 0.25s !important;
&--cubic-bezier {
transition-timing-function: cubic-bezier(0.68, -0.55, 0.265, 1.55) !important;
}
}
}
}
</style>

View File

@ -0,0 +1,230 @@
/**
* 此为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
// 记录开始触摸的时间
state.touchStartTime = getDate().getTime()
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 {
// 获取手指离开的时间
var touchEndTime = getDate().getTime()
// 判断是否点击了
if (Math.abs(pageX - state.startX) < 5 && Math.abs(pageY - state.startY) < 5 && touchEndTime - state.touchStartTime < 100) {
ownerInstance.callMethod('handlerItemClick')
}
// 在关闭状态下,忽略右滑动
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
}

View File

@ -0,0 +1,236 @@
<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-forindex
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: 60
},
// 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', {
type: 'button',
index
})
},
// item
handlerItemClick() {
this.$emit('click', {
type: 'item',
name: this.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>

View File

@ -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>

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