尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
小程序刻度尺组件(抖动问题待优化,需scrollend之后再做吸附处理)
前言最近在开发一个小程序项目时需要实现一个身高测量功能调研了市面上常见的刻度尺组件后决定自己动手写一个。由于项目同时需要支持纵向身高和横向体重选择两种模式索性做了一个通用组件组件特性- 双向模式支持纵向和横向两种刻度尺- 密度可调通过 density 属性控制刻度线疏密- 高度可配每个刻度的高度/宽度可通过 itemHeight 灵活配置- 主题自定义支持自定义颜色、大小等样式属性- 交互友好滚动停止后自动吸附到刻度线核心实现结尾附完整代码核心原理利用 scroll-view 的 scrollTop/scrollLeft 控制滚动位置通过数学计算实现滚动位置与刻度值的双向转换。一、数据结构type ScaleItem { val: number // 刻度值 isBig: boolean // 是否大刻度显示数字和粗线 } //每个刻度线对应一个刻度值isBig 用于判断是否是大刻度。二、 刻度线生成buildScaleListV() { const { minValue, maxValue, step, density } this.properties const list: ScaleItem[] [] for (let i minValue; i maxValue; i density) { list.push({ val: i, isBig: i % step 0 }) } this.setData({ scaleList: list }) }关键点- density 控制循环步长实现刻度线疏密- isBig 基于刻度值判断不依赖 index三、滚动位置计算// 值 → 滚动位置 calcScrollTop(value: number) { const { minValue, density } this.properties const { itemHeight } this.data // snap 到最近的刻度线值 const validValue Math.round((value - minValue) / density) * density minValue const offset (validValue - minValue) * itemHeight this.setData({ scrollTop: Math.max(0, offset) }) } // 滚动位置 → 值 onScrollV(e: WechatMiniprogram.ScrollViewScroll) { const { minValue, maxValue } this.properties const { itemHeight } this.data const value Math.round(e.detail.scrollTop / itemHeight) minValue // ... }关键点- itemHeight 代表两个刻度值之间的物理距离- 滚动计算始终基于刻度值不考虑 density- calcScrollTop 会自动将值吸附到最近的刻度线四、布局结构!-- 纵向刻度尺 -- view classruler-vertical !-- 固定标记圆球指示线 -- view classscroll-marker-vertical.../view !-- 可滚动区域 -- scroll-view scroll-y bindscrollonScrollV view classspacer-top / view classscale-item-vertical styleheight: {{density * itemHeight}}px; wx:for{{scaleList}} !-- 大刻度/小刻度内容 -- /view view classspacer-bottom / /scroll-view /view关键点- 刻度线高度 density × itemHeight- 首尾 spacer 确保初始位置居中使用示例默认刻度尺!-- 纵向刻度尺身高测量 -- ruler directionvertical width{{120}} height{{450}} item-height{{13}} density{{3}} step{{10}} big-line-color#1890ff marker-color#1890ff min-value{{100}} max-value{{220}} default-value{{170}} bind:changeonRulerChange /完整代码ruler: 小程序刻度尺组件包含示例页面https://gitee.com/signup_xiuxiuxiu/ruler1. ruler.wxml!-- ruler 组件 - 纵向/横向刻度尺 -- !-- 纵向刻度尺 -- view classruler-vertical wx:if{{direction vertical}} stylewidth: {{width}}px; height: {{height}}px; background-color: {{bgColor}}; !-- 滚动标志圆球在左指示线在右 -- view classscroll-marker-vertical styleheight: {{ballSize}}px; top: {{ballTop}}px; view classmarker-ball stylewidth: {{ballSize}}px; height: {{ballSize}}px; background-color: {{markerColor}}; box-shadow: 0 1px 4px {{markerColor}}40; wx:if{{showBall}}/view view classmarker-line stylewidth: {{lineWidth}}px; height: 2px; background-color: {{markerColor}};/view /view !-- 刻度列表 -- scroll-view classruler-scroll-vertical scroll-y enhanced{{true}} show-scrollbar{{false}} bindscrollonScrollV scroll-top{{scrollTop}} view classspacer-top styleheight: {{height / 2}}px;/view view classscale-item-vertical {{item.isBig ? big : small}} styleheight: {{density * itemHeight}}px; wx:for{{scaleList}} wx:keyindex block wx:if{{item.isBig}} text classscale-text-vertical wx:if{{showScaleText}} stylefont-size: {{scaleFontSize}}px; color: {{scaleTextColor}}; {{item.val}}/text view classscale-line-big-vertical styleheight: {{bigLineHeight}}px; width: {{lineWidth}}px; background-color: {{bigLineColor}};/view /block block wx:else view classscale-line-vertical styleheight: {{smallLineHeight}}px; width: {{smallLineWidth}}px; background-color: {{smallLineColor}};/view /block /view view classspacer-bottom styleheight: {{height / 2}}px;/view /scroll-view /view !-- 横向刻度尺 -- view classruler-horizontal wx:if{{direction horizontal}} stylewidth: {{width}}px; height: {{height}}px; background-color: {{bgColor}}; !-- 滚动标志指示线在上圆球在下 -- view classscroll-marker-horizontal stylewidth: {{ballSize}}px;left: {{ballLeft}}px; view classmarker-line-h stylewidth: 2px; height: {{lineWidth}}px; background-color: {{markerColor}};/view view classmarker-ball-h stylewidth: {{ballSize}}px; height: {{ballSize}}px; background-color: {{markerColor}}; box-shadow: 0 1px 4px {{markerColor}}40; wx:if{{showBall}}/view /view !-- 刻度列表 -- scroll-view classruler-scroll-horizontal scroll-x enhanced{{true}} show-scrollbar{{false}} bindscrollonScrollH scroll-left{{scrollLeft}} view classscroll-content-horizontal view classspacer-left stylewidth: {{width / 2}}px;/view view classscale-item-horizontal {{item.isBig ? big : small}} stylewidth: {{density * itemHeight}}px; wx:for{{scaleList}} wx:keyindex block wx:if{{item.isBig}} view classscale-line-big-horizontal stylewidth: {{bigLineHeight}}px; height: {{lineWidth}}px; background-color: {{bigLineColor}};/view text classscale-text-horizontal wx:if{{showScaleText}} stylefont-size: {{scaleFontSize}}px; color: {{scaleTextColor}};{{item.val}}/text /block block wx:else view classscale-line-horizontal stylewidth: {{smallLineHeight}}px; height: {{smallLineWidth}}px; background-color: {{smallLineColor}};/view /block /view view classspacer-right stylewidth: {{width / 2}}px;/view /view /scroll-view /view2. ruler.scss/* 纵向刻度尺 */ .ruler-vertical { position: relative; overflow: hidden; } .scroll-marker-vertical { position: absolute; right: 0; z-index: 10; display: flex; align-items: center; transition: top 0.1s ease-out; } .marker-ball { border-radius: 50%; } .ruler-scroll-vertical { width: 100%; height: 100%; } .scale-item-vertical { display: flex; align-items: flex-start; box-sizing: border-box; .big { justify-content: flex-end; } .small { justify-content: flex-end; } } .scale-text-vertical { transform: translateY(-10px); text-align: right; box-sizing: border-box; padding-right: 15px; } .scale-line-big-vertical { height: 2px; } .scale-line-vertical { height: 1px; } /* 横向刻度尺 */ .ruler-horizontal { position: relative; overflow: hidden; } .scroll-marker-horizontal { position: absolute; top: 0; z-index: 10; display: flex; flex-direction: column; align-items: center; transition: left 0.1s ease-out; } .marker-ball-h { border-radius: 50%; } .ruler-scroll-horizontal { width: 100%; height: 100%; } .scroll-content-horizontal { display: flex; flex-direction: row; align-items: flex-start; height: 100%; width: max-content; } .spacer-left, .spacer-right { flex-shrink: 0; } .scale-item-horizontal { display: flex; flex-direction: column; flex-shrink: 0; box-sizing: border-box; .big { justify-content: flex-end; } .small { justify-content: flex-start; } } .scale-text-horizontal { text-align: center; box-sizing: border-box; padding-top: 15px; transform: translateX(-10px); }3. ruler.ts/** * ruler 组件 - 纵向/横向刻度尺 * * 实现思路 * 1. 通过 direction 切换纵向/横向 * 2. 纵向scroll-y 滚动scrollTop 控制 * 3. 横向scroll-x 滚动scrollLeft 控制 * 4. 滚动标志圆点指示线在固定位置 */ type ScaleItem { val: number // 刻度值 isBig: boolean // 是否大刻度 } Component({ properties: { // 方向配置 direction: { type: String, value: vertical // vertical | horizontal }, // 尺寸配置 width: { type: Number, value: 90 }, height: { type: Number, value: 312 }, itemHeight: { type: Number, value: 13 }, density: { type: Number, value: 1 }, // 刻度样式配置 step: { type: Number, value: 10 }, showScaleText: { type: Boolean, value: true }, scaleFontSize: { type: Number, value: 14 }, scaleTextColor: { type: String, value: #000 }, bigLineHeight: { type: Number, value: 2 }, smallLineHeight: { type: Number, value: 1 }, smallLineWidth: { type: Number, value: 20 }, lineWidth: { type: Number, value: 40 }, ballSize: { type: Number, value: 10 }, // 颜色配置 bigLineColor: { type: String, value: #FD6321 }, smallLineColor: { type: String, value: rgba(253, 99, 33, 0.3) }, markerColor: { type: String, value: #FD6321 }, bgColor: { type: String, value: rgba(253, 99, 33, 0.06) }, showBall: { type: Boolean, value: true }, // 刻度尺范围配置 minValue: { type: Number, value: 100 }, maxValue: { type: Number, value: 220 }, defaultValue: { type: Number, value: 170 } }, data: { scaleList: [] as ScaleItem[], // 纵向 scrollTop: 0, ballTop: 0, containerHeight: 0, // 横向 scrollLeft: 0, ballLeft: 0, containerWidth: 0, // 当前值 currentValue: 170, itemHeight: 13, debounceTimerV: null as number | null, debounceTimerH: null as number | null, }, lifetimes: { attached() { this.initRuler() } }, observers: { defaultValue: function (val: number) { if (val ! this.data.currentValue) { this.setData({ currentValue: val }) if (this.properties.direction horizontal) { this.calcScrollLeft(val) } else { this.calcScrollTop(val) } } } }, methods: { initRuler() { const { defaultValue, itemHeight, direction } this.properties as any this.setData({ itemHeight }) if (direction horizontal) { this.buildScaleListH() this.updateContainerWidth(() { this.calcScrollLeft(defaultValue) }) } else { this.buildScaleListV() this.updateContainerHeight(() { this.calcScrollTop(defaultValue) }) } }, // 纵向 buildScaleListV() { const { minValue, maxValue, step, density } this.properties as any const list: ScaleItem[] [] for (let i minValue; i maxValue; i density) { list.push({ val: i, isBig: i % step 0 }) } this.setData({ scaleList: list }) }, calcScrollTop(value: number) { const { minValue, density } this.properties as any const { itemHeight } this.data // snap 到最近的刻度线值 const validValue Math.round((value - minValue) / density) * density minValue const offset (validValue - minValue) * itemHeight this.setData({ scrollTop: Math.max(0, offset), currentValue: validValue }) }, updateContainerHeight(callback?: () void) { const { ballSize } this.properties as any const query wx.createSelectorQuery().in(this) query.select(.ruler-vertical).boundingClientRect((rect) { if (rect) { const containerHeight rect.height const ballTop containerHeight / 2 - ballSize / 2 this.setData({ containerHeight, ballTop }) if (callback) callback() } }).exec() }, onScrollV(e: WechatMiniprogram.ScrollViewScroll) { const { minValue, maxValue } this.properties as any const { itemHeight, debounceTimerV } this.data const value Math.round(e.detail.scrollTop / itemHeight) minValue const clampedValue Math.max(minValue, Math.min(maxValue, value)) this.setData({ currentValue: clampedValue }) this.triggerEvent(change, { value: clampedValue }) if (debounceTimerV) { clearTimeout(debounceTimerV) } const timer setTimeout(() { this.calcScrollTop(clampedValue) }, 100) this.setData({ debounceTimerV: timer }) }, // 横向 buildScaleListH() { const { minValue, maxValue, step, density } this.properties as any const list: ScaleItem[] [] for (let i minValue; i maxValue; i density) { list.push({ val: i, isBig: i % step 0 }) } this.setData({ scaleList: list }) }, calcScrollLeft(value: number) { const { minValue, density } this.properties as any const { itemHeight } this.data // snap 到最近的刻度线值 const validValue Math.round((value - minValue) / density) * density minValue const offset (validValue - minValue) * itemHeight this.setData({ scrollLeft: Math.max(0, offset), currentValue: validValue }) }, updateContainerWidth(callback?: () void) { const { ballSize } this.properties as any const query wx.createSelectorQuery().in(this) query.select(.ruler-horizontal).boundingClientRect((rect) { if (rect) { const containerWidth rect.width const ballLeft containerWidth / 2 - ballSize / 2 this.setData({ containerWidth, ballLeft }) if (callback) callback() } }).exec() }, onScrollH(e: WechatMiniprogram.ScrollViewScroll) { const { minValue, maxValue } this.properties as any const { itemHeight, debounceTimerH } this.data const value Math.round(e.detail.scrollLeft / itemHeight) minValue const clampedValue Math.max(minValue, Math.min(maxValue, value)) this.setData({ currentValue: clampedValue }) this.triggerEvent(change, { value: clampedValue }) if (debounceTimerH) { clearTimeout(debounceTimerH) } const timer setTimeout(() { this.calcScrollLeft(clampedValue) }, 100) this.setData({ debounceTimerH: timer }) }, } })4. ruler.json{ component: true, usingComponents: {} }配置介绍仓库内的README.md)## 属性说明 ### 方向配置 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | direction | String | vertical | 刻度尺方向vertical 纵向horizontal 横向 | ### 尺寸配置 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | width | Number | 90 | 刻度尺宽度px | | height | Number | 312 | 刻度尺高度px | | itemHeight | Number | 13 | 两个刻度值之间的物理距离px每条刻度线的实际高度 density × itemHeight | ### 刻度样式 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | density | Number | 1 | 刻度线密度每隔 density 个刻度值显示一条刻度线 | | step | Number | 10 | 大刻度间隔每隔多少显示数字和大刻度线 | | showScaleText | Boolean | true | 是否显示刻度数字 | | scaleFontSize | Number | 14 | 刻度数字字号px | | scaleTextColor | String | #000 | 刻度数字颜色 | | bigLineHeight | Number | 2 | 大刻度线高度px | | smallLineHeight | Number | 1 | 小刻度线高度px | | smallLineWidth | Number | 20 | 小刻度线宽度px | | lineWidth | Number | 40 | 刻度线总宽度px | ### 颜色配置 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | bigLineColor | String | #FD6321 | 大刻度线颜色 | | smallLineColor | String | rgba(253,99,33,0.3) | 小刻度线颜色 | | markerColor | String | #FD6321 | 圆球和指示线颜色 | | bgColor | String | rgba(253,99,33,0.06) | 刻度尺背景色 | ### 功能配置 | 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | ballSize | Number | 10 | 圆球尺寸px | | showBall | Boolean | true | 是否显示圆球 | | minValue | Number | 100 | 刻度尺最小值 | | maxValue | Number | 220 | 刻度尺最大值 | | defaultValue | Number | 170 | 默认值 | ## 事件 ### change 滚动停止后触发返回当前选中值。 xml ruler bind:changeonRulerChange / js onRulerChange(e) { const { value } e.detail console.log(选中值:, value) } ## 使用示例 ### 纵向刻度尺身高测量 xml ruler directionvertical width{{120}} height{{450}} item-height{{13}} density{{3}} step{{10}} show-scale-text{{true}} scale-font-size{{14}} scale-text-color#000 big-line-height{{2}} small-line-height{{1}} small-line-width{{20}} line-width{{40}} ball-size{{10}} big-line-color#1890ff small-line-colorrgba(24, 144, 255, 0.3) marker-color#1890ff bg-colorrgba(24, 144, 255, 0.06) show-ball{{true}} min-value{{100}} max-value{{220}} default-value{{170}} bind:changeonRulerChange / ## 注意事项 1. **itemHeight**代表两个刻度值之间的物理距离px刻度线的实际高度 density × itemHeight 2. **density**刻度线密度1每个刻度值都有刻度线2每隔一个刻度值有刻度线以此类推 3. **滚动范围**刻度范围从 minValue 到 maxValue首尾有半个容器宽/高度的空白区域 4. **默认值**组件初始化时会自动滚动到 defaultValue 指定的位置会自动对齐到刻度线 5. **响应式**修改 defaultValue 会触发组件重新定位到指定值总结这个组件的实现思路比较简单核心就是利用 scroll-view 的 scrollTop/scrollLeft控制滚动位置通过计算实现值与滚动位置的相互转换。density属性的引入让刻度线的疏密变得灵活可调希望能给有类似需求的同学一些参考因为考虑不同项目色调不同尺子的背景颜色刻度线的颜色都可以配置有需要的可以参考实现哈有更好的优化建议也欢迎各位大佬指出共同进步๑ᴗ๑
RELATED

相关推荐

CMO环境模型-地表覆盖模型详解

CMO环境模型-地表覆盖模型详解

/// <summary> /// 地面单元隐蔽能力计算器&#xff08;对应 ActiveUnit.LandCoverMaskingCapability&#xff09;。 /// </summary> public static class LandCoverMaskingCalculator {/// <summary>/// 根据地表覆盖类型查表得到地面单元的隐蔽能力值。///…

📅 2026/9/25 18:16:45
文档欠着、线上 Bug 又急:用 Caravel 把两条线并行跑起来

文档欠着、线上 Bug 又急:用 Caravel 把两条线并行跑起来

摘要&#xff1a; 文档和修 Bug 常常撞车。串行会浪费一小时&#xff0c;开两个终端又容易丢上下文。本文用一次完整演示说明&#xff1a;在 Caravel 工作台里先让补文档任务跑着&#xff0c;再新建修 Bug 任务并选择另一个 Agent&#xff0c;最后在列表里并排看两条线的状态。…

📅 2026/9/25 18:16:45
「不碰权重,只改策略」:Dream-RSI 如何用「做梦」实现递归自我改进

「不碰权重,只改策略」:Dream-RSI 如何用「做梦」实现递归自我改进

2026 年&#xff0c;Anthropic、OpenAI 相继把「递归自我改进」&#xff08;Recursive Self-Improvement, RSI&#xff09;从论文概念推到了公众视野&#xff0c;也把它最刺耳的那面亮了出来&#xff1a;如果一个 AI 能不断地改进自己的改进能力&#xff0c;它的能力曲线会不会…

📅 2026/9/25 18:16:45
MORE NEWS

更多资讯

📰

Atlas 300V 24G部署YOLO实战:从环境配置到推理调优全流程

1. 先搞清楚Atlas 300V到底是什么拿到“atlas”这个标题时&#xff0c;我一开始也愣了一下。因为Atlas这个词在技术圈里指代的东西太多了——华为的昇腾AI计算产品线叫Atlas&#xff0c;NVIDIA的老款显卡有ATLAS系列&#xff0c;甚至还有各种叫Atlas的开源项目。直到配合上热搜…

📰

Axure Chrome扩展V0.6.3:解决HTML原型白屏与交互调试

简介&#xff1a;这是一款专为Axure原型设计工具打造的Chrome浏览器本地HTML预览插件&#xff0c;版本V0.6.3&#xff0c;主要面向需要在真实浏览器环境中验证界面与交互效果的UI/UX设计师。插件可直接加载本地HTML文件&#xff0c;无需上传服务器&#xff0c;也无需额外复杂配…

📰

电气元件选型工具:基于IEC/GB标准的闭环校验系统

简介&#xff1a;这是一款面向电气工程师、自动化设计人员及高校相关专业师生的实用型选型辅助工具&#xff0c;聚焦电缆线径与载流量的快速计算&#xff0c;解决传统人工查表选型效率低、易出错的问题&#xff0c;适用于工业配电、建筑电气设计等实际工程场景。资源为3.37MB的…

📰

大模型工程化落地:算力调度、数据治理与推理优化实战

1. 项目概述&#xff1a;一场看不见硝烟的“算力数据工程”三重博弈最近刷到“AI大模型军备竞赛”这个说法&#xff0c;朋友圈和行业群几乎天天刷屏。但说实话&#xff0c;很多人一听到“军备竞赛”&#xff0c;下意识想到的是堆卡、烧钱、抢人才——这确实是一部分事实&#x…

📰

ArcGIS要素编号小工具全解析:字段计算器与ArcPy脚本实现流水号自动化

简介&#xff1a;面向地理信息系统领域的数据管理人员、制图人员以及需要批量要素标识的ArcGIS用户&#xff0c;这款编号小工具可针对mdb、gdb与shp三种常见地理数据格式自动添加顺序编号&#xff0c;替代逐条手动录入的重复劳动。mdb适合轻量级个人数据库存储&#xff0c;gdb在…

📰

A2A + CrewAI + OpenRouter 图表生成 Agent 教程:用 TaoToken 统一 Key 打通多智能体协作链路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬