当前位置: 首页 > news >正文

uni-app app移动端实现纵向滑块功能,并伴随自动播放

需求 :uni-app实现纵向滑动的时间轴,添加标记点,支持自动播放;自动播放默认直接滑动到最近的标记点;

一开始用的uni官方自带滑块和uView的滑块组件, 但是两者都不支持纵向,虽然样式可以实现,但是滑动有问题;最后自定义实现纵向滑块功能,以下是代码部分:(自动滑动方向, 目前我只用了down, up没有测试)

组件实现:verticalSlider.vue

<template><view class="vertical-slider-container"><!-- 当前值显示 --><view class="value-display" v-if="showValue">{{ currentValue }}</view><!-- 滑轨区域 --><view class="slider-track" @tap="onTrackTap" ref="track"><!-- 滑轨背景 --><view class="track-background"></view><!-- 已填充区域 --><view class="track-filled" :style="{ height: filledHeight + '%' }"></view><!-- 标记点,从上往下滑动设置top值,如果从下往上需要改为bottom,滑块同理 --><view v-for="(mark, index) in marks" :key="index" class="mark-point":style="{ top: calculateMarkPosition(mark.value) + '%' }" @tap.stop="jumpToMark(mark.value)"><view class="mark-dot"></view><view v-if="showMarkLabel" class="mark-label">{{ mark.label }}</view></view><!-- 滑块 --><view class="slider-thumb" :style="{ top: thumbPosition + '%' }" @touchstart="onTouchStart"@touchmove="onTouchMove" @touchend="onTouchEnd"><view class="thumb-label" :class="{ 'label-visible': isDragging || showThumbLabel }">{{currentTime}}</view></view></view></view>
</template>
<script>import moment from 'moment';export default {name: "VerticalSlider",props: {// 最小值min: {type: Number,default: 0},// 最大值max: {type: Number,default: 100},// 当前值value: {type: Number,default: 0},// 是否显示当前值showValue: {type: Boolean,default: true},// 是否显示滑块labelshowThumbLabel: {type: Boolean,default: true},// 步长step: {type: Number,default: 1},// 标记点数组marks: {type: Array,default: () => []},// 是否显示标记点showMarks: {type: Boolean,default: true},// 是否显示标记点labelshowMarkLabel: {type: Boolean,default: true},// 是否自动滑动autoPlay: {type: Boolean,default: false},// 自动滑动方向, down或upautoPlayDirection: {type: String,default: 'down'},// 自动滑动速度 (毫秒)autoPlaySpeed: {type: Number,default: 1000},},data() {return {currentValue: this.value,isDragging: false,trackHeight: 0,trackTop: 0,autoPlayTimer: null, // 自动播放定时器animationTimer: null, // 动画定时器isAutoPlaying: false,};},computed: {// 滑块位置百分比(从上到下)thumbPosition() {return ((this.currentValue - this.min) / (this.max - this.min)) * 100;},// 已填充区域高度百分比(从上到下)filledHeight() {return this.thumbPosition;},currentTime() {return moment.unix(this.currentValue).format('YYYY-MM-DD HH:mm:ss');},sortedMarks() {return [...this.marks].sort((a, b) => a.value - b.value)}},watch: {// 监听 autoPlay 属性变化autoPlay(newVal) {if (newVal) {this.startAutoPlay();} else {this.stopAutoPlay();}}, // 监听 value 属性变化value(newVal) {if (newVal !== this.currentValue) {this.currentValue = newVal;}},// 监听当前值变化,到达边界时停止自动播放currentValue(newVal) {if (this.isAutoPlaying) {if ((this.autoPlayDirection === 'down' && newVal >= this.max) ||(this.autoPlayDirection === 'up' && newVal <= this.min)) {this.stopAutoPlay();this.$emit('autoPlayEnd');}}}},mounted() {this.$nextTick(() => {this.initTrackSize();});// 如果初始设置为自动播放,则开始if (this.autoPlay) {this.startAutoPlay();}},methods: {// 初始化滑轨尺寸initTrackSize() {const query = uni.createSelectorQuery().in(this);query.select('.slider-track').boundingClientRect(data => {if (data) {this.trackHeight = data.height;this.trackTop = data.top;}}).exec();},// 触摸开始onTouchStart(e) {this.isDragging = true;this.updateValueFromEvent(e);},// 触摸移动onTouchMove(e) {if (!this.isDragging) return;this.updateValueFromEvent(e);e.preventDefault();},// 触摸结束onTouchEnd() {this.isDragging = false;// 如果有标记点,尝试吸附到最近的标记点if (this.marks.length > 0) {this.snapToNearestMark();}},// 点击滑轨跳转onTrackTap(e) {this.updateValueFromEvent(e);// 如果有标记点,尝试吸附到最近的标记点if (this.marks.length > 0) {this.snapToNearestMark();}},// 根据事件更新值(从上到下)	updateValueFromEvent(e) {let clientY;if (e.type === 'tap') {clientY = e.detail.y;} else {clientY = e.touches[0].clientY;}// 计算点击位置在滑轨中的百分比(从上到下)const position = (clientY - this.trackTop) / this.trackHeight;let newValue = this.min + position * (this.max - this.min);// 限制在[min, max]范围内newValue = Math.max(this.min, Math.min(this.max, newValue));// 应用步长if (this.step > 0) {newValue = Math.round(newValue / this.step) * this.step;}this.updateValue(newValue);},// 更新值updateValue(newValue) {if (newValue !== this.currentValue) {this.currentValue = newValue;this.$emit('change', newValue);this.$emit('input', newValue);}},// 计算标记点位置(从上到下)calculateMarkPosition(value) {return ((value - this.min) / (this.max - this.min)) * 100;},// 跳转到标记点jumpToMark(value) {this.updateValue(value);},// 吸附到最近的标记点snapToNearestMark() {if (this.marks.length === 0) return;let nearestMark = this.marks[0];let minDiff = Math.abs(this.currentValue - nearestMark.value);for (let i = 1; i < this.marks.length; i++) {const diff = Math.abs(this.currentValue - this.marks[i].value);if (diff < minDiff) {minDiff = diff;nearestMark = this.marks[i];}}// 如果距离足够近,则吸附if (minDiff <= (this.max - this.min) * 0.05) {this.updateValue(nearestMark.value);}},// 开始自动播放startAutoPlay() {if (this.isAutoPlaying) return;this.isAutoPlaying = true;this.$emit('autoPlayStart');// this.autoPlayBySeacond();this.jumpToNextTarget();},// 按秒滑动autoPlayBySecond() {this.autoPlayTimer = setInterval(() => {let newValue;if (this.autoPlayDirection === 'down') {newValue = this.currentValue + this.step;if (newValue > this.max) newValue = this.max;} else {newValue = this.currentValue - this.step;if (newValue < this.min) newValue = this.min;}this.updateValue(newValue);// 检查是否到达边界if ((this.autoPlayDirection === 'down' && newValue >= this.max) ||(this.autoPlayDirection === 'up' && newValue <= this.min)) {this.stopAutoPlay();this.$emit('autoPlayEnd');}}, this.autoPlaySpeed);},// 跳转到下一个目标(标记点或终点)jumpToNextTarget() {const nextTarget = this.getNextTarget();if (nextTarget !== null) {// 使用动画平滑过渡到下一个目标this.animateToValue(nextTarget, 1000, () => {// 动画完成后,检查是否到达终点if (this.isAutoPlaying && nextTarget !== this.getEndValue()) {// 设置定时器进行下一次跳转this.autoPlayTimer = setTimeout(() => {this.jumpToNextTarget();}, this.autoPlaySpeed);} else {// 到达终点,停止自动播放this.stopAutoPlay();this.$emit('autoPlayEnd');}});} else {// 没有下一个目标,停止自动播放this.stopAutoPlay();this.$emit('autoPlayEnd');}},// 获取下一个目标值getNextTarget() {if (this.autoPlayDirection === 'down') {return this.getNextTargetDown();} else {return this.getNextTargetUp();}},// 向下播放时获取下一个目标getNextTargetDown() {// 如果当前值已经到达终点if (this.currentValue >= this.max) {return null;}// 查找当前值之后的下一个标记点const nextMark = this.sortedMarks.find(mark => mark.value > this.currentValue);if (nextMark) {return nextMark.value;} else {// 没有标记点,直接跳到终点return this.max;}},// 向上播放时获取下一个目标getNextTargetUp() {// 如果当前值已经到达起点if (this.currentValue <= this.min) {return null;}// 查找当前值之前的上一个标记点(按值降序查找)const prevMark = [...this.sortedMarks].reverse().find(mark => mark.value < this.currentValue);if (prevMark) {return prevMark.value;} else {// 没有标记点,直接跳到起点return this.min;}},// 获取终点值(根据方向)getEndValue() {return this.autoPlayDirection === 'down' ? this.max : this.min;},// 平滑动画到指定值animateToValue(targetValue, duration = 1000, onComplete = null) {const startValue = this.currentValue;const startTime = Date.now();const frameRate = 16; // 大约 60fps// 停止之前的动画if (this.animationTimer) {clearTimeout(this.animationTimer);}const animate = () => {const elapsed = Date.now() - startTime;const progress = Math.min(elapsed / duration, 1);// 使用缓动函数让动画更自然const easeOutCubic = 1 - Math.pow(1 - progress, 3);const newValue = startValue + (targetValue - startValue) * easeOutCubic;this.updateValue(newValue);if (progress < 1) {// 使用 setTimeout 替代 requestAnimationFramethis.animationTimer = setTimeout(animate, frameRate);} else {// 确保最终值准确this.updateValue(targetValue);if (onComplete) onComplete();}};this.animationTimer = setTimeout(animate, frameRate);},// 停止自动播放stopAutoPlay() {if (this.autoPlayTimer) {clearInterval(this.autoPlayTimer);this.autoPlayTimer = null;}if (this.isAutoPlaying) {this.isAutoPlaying = false;this.$emit('autoPlayStop');}},},beforeDestroy() {if (this.autoPlayTimer) {clearInterval(this.autoPlayTimer);}if (this.animationTimer) {clearInterval(this.animationTimer);}}};
</script>
<style lang="scss" scoped>.vertical-slider-container {display: flex;flex-direction: column;align-items: center;height: 100%;.value-display {font-size: 32rpx;font-weight: bold;margin-bottom: 20rpx;color: #333;}.slider-track {position: relative;width: 60rpx;height: 100%;background-color: transparent;touch-action: pan-y;.track-background {position: absolute;top: 0;left: 50%;transform: translateX(-50%);width: 12rpx;height: 100%;background-color: #e0e0e0;border-radius: 6rpx;}.track-filled {position: absolute;top: 0;left: 50%;transform: translateX(-50%);width: 12rpx;background: linear-gradient(0deg, #3281F4 2%, #7DD8F2 100%);border-radius: 6rpx;transition: height 0.1s ease;}.slider-thumb {position: absolute;left: 50%;transform: translateX(-50%);width: 30rpx;height: 30rpx;border-radius: 50%;background-color: #007AFF;display: flex;justify-content: center;align-items: center;z-index: 10;border: 6rpx solid #fff;/* 滑块label样式 */.thumb-label {position: absolute;left: 100%;top: 50%;transform: translateY(-50%);background: rgba(194, 59, 59, 0.8);border-radius: 12rpx;border: 1px solid #D98D8D;color: #fff;padding: 0 5rpx;font-size: 20rpx;white-space: nowrap;margin-left: 10rpx;opacity: 0;transition: opacity 0.3s ease;pointer-events: none;&.label-visible {opacity: 1;}}}.mark-point {position: absolute;left: 0;display: flex;align-items: center;flex-direction: column;z-index: 5;position: absolute;left: 50%;transform: translateX(-50%);.mark-dot {width: 12rpx;height: 12rpx;border-radius: 50%;background-color: #C23B3B;border: 2rpx solid #fff;}.mark-label {font-size: 20rpx;color: #fff;margin-left: 10rpx;white-space: nowrap;background: rgba(194, 59, 59, 0.8);border-radius: 12rpx;border: 2rpx solid #D98D8D;padding: 0 5rpx;position: absolute;left: 50%;top: -50%;box-sizing: border-box;}}}}
</style>

父组件使用:

<template><view class="slider-box"><!-- 控制按钮 --><view class="play-btn" @click="hanldeAutoPlay"><u-icon v-show="!autoPlay" name="play-right-fill" size="13" color="#fff"></u-icon><u-icon v-show="autoPlay" name="pause" size="13" color="#fff"></u-icon></view><!-- 垂直滑块 --><view class="vertical-slider-container"><VerticalSlider :value="sliderValue" :min="min" :max="max" :step="1" :showValue="false" :marks="marks":autoPlay="autoPlay" :showMarkLabel="false" @change="onChange" @autoPlayStop="onAutoPlayStop"@autoPlayEnd="onAutoPlayEnd" /></view></view>
</template>
<script>import VerticalSlider from '@/components/verticalSlider.vue';import moment from 'moment';export default {components: {VerticalSlider},props: {startTime: {type: String,default: "1970-01-01 08:00:00"},endTime: {type: String,default: "1970-01-01 08:10:00"},markList: {type: Array,default: []},},data() {return {sliderValue: 0,min: 0,max: 100,marks: [],autoPlay: false};},mounted() {this.handleTime();this.marks = this.markList;},methods: {// 最大最小值转换为秒时间戳,方便添加标记点handleTime() {this.min = moment(this.startTime).unix(); // 转为时间戳秒this.max = moment(this.endTime).unix();// 滑块默认在最高点,从上往下滑动this.sliderValue = this.min;},// 自动播放hanldeAutoPlay() {this.autoPlay = !this.autoPlay;if (this.sliderValue == this.max) {this.sliderValue = this.min;}},// 改变触发onChange(val) {this.sliderValue = val;// 整数执行if (Number.isInteger(val)) {this.$emit("onChange", val)}},// 自动播放事件onAutoPlayStop() {console.log('自动播放停止');this.autoPlay = false;},onAutoPlayEnd() {console.log('自动播放结束');this.autoPlay = false;}}};
</script>
<style lang="scss" scoped>.slider-box { height: 60vh;position: absolute;top: 300rpx;left: 40rpx;z-index: 9999;display: flex;flex-direction: column;align-items: center;justify-content: space-between;.play-btn {width: 44rpx;height: 44rpx;background: #3281F4;border-radius: 50%;border: 2px solid #FFFFFF;position: absolute;top: -60rpx;display: flex;cursor: pointer;box-sizing: border-box;.u-icon {margin: 0 auto;}}.vertical-slider-container {height: 100%;}}
</style>

实现效果:
在这里插入图片描述

如有错误或不足,请大佬们评论指正

http://www.dtcms.com/a/582754.html

相关文章:

  • Nacos-服务发现
  • 西安网站建设有限公司上海网站建设的意义
  • 网站推广新手入门h5自己制作模板
  • 广西网站建设定制阿里云备案网站负责人
  • 做网站长沙如何去掉wordpress
  • Netty详解-01
  • 我公司让别人做网站了怎么办个人微信公共号可以做微网站么
  • 做网站 发现对方传销兴义 网站建设
  • 节点小宝免费版流量机制解析:点对点直连技术与备用流量设计
  • 扁平化网站源码企业网站的建立费用 作什么科目
  • 卖货网站平台互联网o2o是什么意思
  • 网站建设需要通过哪些审批大同住房和城乡和建设网站
  • 做个企业网站要多少钱网络的推广
  • 一套随访系统源码,医院随访管理系统源码,三级随访平台源码,技术框架:Java+Spring boot,Vue,Ant-Design+MySQL5
  • 响应式网站开发现状宁波高端网站建设推广
  • 摄影网站网页设计网络营销的特征包括
  • 潍坊模板建站定制网站优惠做网站
  • 共筑网络安全,守护绿色家园
  • 一枚指纹,开启工业IoT设备安全与权限分级实践
  • 设计电子商务网站建设方案住建房官网查询
  • 响应式网站做mip小程序什么样才能移到微信上
  • GIS-gdal-java.lang.NoSuchMethodError
  • 注册安全工程师考试科目南京seo顾问
  • 省品牌建设联合会网站关键词查询
  • PsSuspend(7.23):无损挂起与恢复指定进程——精准“冻住”故障现场
  • 台州网站设计公司网站推广公司官网
  • 【LLaVA-NeXT】请问,为什么“Stderr显示是N/A”的信息呢
  • 二级域名做网站好不好2024房地产彻底结束
  • 网站建设动态页面修改删除广州网站优化地址
  • “十五五规划”智慧养老新图景:科技如何让晚年更温暖