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

Uniapp ECG心电图组件

基于uniapp Vue2心电图绘制组件,支持自动滑动展示和实时绘制刷新两种模式。

功能特性

  • ✅ 自动滑动展示模式 - 模拟心电图数据自动滚动
  • ✅ 实时绘制刷新模式 - 模拟实时数据添加和刷新
  • ✅ 网格背景 - 专业的医疗级网格显示
  • ✅ 响应式设计 - 适配不同屏幕尺寸
  • ✅ 触摸控制 - 支持移动端触摸操作

组件代码

<template><view class="ecg-container"><view class="ecg-header"><text class="ecg-title">{{ currentModeText }}</text></view><view class="ecg-canvas-container"><canvas canvas-id="ecgCanvas" class="ecg-canvas":style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"></canvas></view><view class="ecg-controls"><button class="control-btn" @tap="startScrollMode":disabled="isScrolling">开始自动滑动</button><button class="control-btn" @tap="startRefreshMode":disabled="isRefreshing">开始实时绘制</button><button class="control-btn stop-btn" @tap="stopAll">停止</button></view></view>
</template><script>
export default {name: 'EcgChart',data() {return {canvasWidth: 350,canvasHeight: 200,maxDataPoints: 100,displayPoints: 50,dataList: [],refreshList: [],displayData: [],scrollIndex: 0,showIndex: 0,isScrolling: false,isRefreshing: false,scrollTimer: null,refreshTimer: null,ctx: null}},computed: {currentModeText() {if (this.isScrolling) return '自动滑动展示模式'if (this.isRefreshing) return '实时绘制刷新模式'return '静态心电图'}},mounted() {this.initData()this.initCanvas()},beforeDestroy() {this.stopAll()},methods: {initData() {// 生成模拟心电图数据for (let i = 0; i < this.maxDataPoints; i++) {const value = Math.sin(i * 0.1) * 50 + Math.sin(i * 0.3) * 20 + (Math.random() - 0.5) * 10this.dataList.push(value)this.refreshList.push(value)}// 初始化显示数据this.displayData = new Array(this.displayPoints).fill(0)},initCanvas() {const query = uni.createSelectorQuery().in(this)query.select('#ecgCanvas').boundingClientRect(data => {if (data) {this.canvasWidth = data.widththis.canvasHeight = data.height}}).exec()this.ctx = uni.createCanvasContext('ecgCanvas', this)this.drawECG()},startScrollMode() {this.stopAll()this.isScrolling = truethis.scrollIndex = 0this.scrollTimer = setInterval(() => {if (this.scrollIndex < this.dataList.length) {this.scrollIndex++} else {this.scrollIndex = 0}this.drawECG()}, 50)},startRefreshMode() {this.stopAll()this.isRefreshing = truethis.refreshList = []this.showIndex = 0// 添加初始数据for (let i = 0; i < 10; i++) {const value = Math.sin(i * 0.1) * 50 + Math.sin(i * 0.3) * 20 + (Math.random() - 0.5) * 10this.refreshList.push(value)}this.refreshTimer = setInterval(() => {// 模拟实时数据添加const newValue = Math.sin(this.refreshList.length * 0.1) * 50 + Math.sin(this.refreshList.length * 0.3) * 20 + (Math.random() - 0.5) * 10this.refreshList.push(newValue)// 更新显示数据this.updateDisplayData()this.drawECG()}, 200)},stopAll() {if (this.scrollTimer) {clearInterval(this.scrollTimer)this.scrollTimer = null}if (this.refreshTimer) {clearInterval(this.refreshTimer)this.refreshTimer = null}this.isScrolling = falsethis.isRefreshing = falsethis.drawECG()},updateDisplayData() {const nowIndex = this.refreshList.lengthif (nowIndex === 0) returnif (nowIndex < this.displayPoints) {this.showIndex = nowIndex - 1} else {this.showIndex = (nowIndex - 1) % this.displayPoints}for (let i = 0; i < this.displayPoints; i++) {if (i >= this.refreshList.length) breakif (nowIndex <= this.displayPoints) {this.displayData[i] = this.refreshList[i]} else {const times = Math.floor((nowIndex - 1) / this.displayPoints)const temp = times * this.displayPoints + iif (temp < nowIndex) {this.displayData[i] = this.refreshList[temp]}}}},drawECG() {if (!this.ctx) returnconst width = this.canvasWidthconst height = this.canvasHeight// 清空画布this.ctx.clearRect(0, 0, width, height)// 绘制背景this.ctx.setFillStyle('#FFFFFF')this.ctx.fillRect(0, 0, width, height)// 绘制网格this.drawGrid()// 绘制心电图if (this.isScrolling) {this.drawScrollECG()} else if (this.isRefreshing) {this.drawRefreshECG()} else {this.drawStaticECG()}// 绘制标题this.ctx.setFillStyle('#000000')this.ctx.setFontSize(14)this.ctx.fillText(this.currentModeText, 10, 20)this.ctx.draw()},drawGrid() {const width = this.canvasWidthconst height = this.canvasHeightthis.ctx.setStrokeStyle('#DCDCDC')this.ctx.setLineWidth(1)// 绘制垂直线for (let x = 0; x < width; x += 10) {this.ctx.beginPath()this.ctx.moveTo(x, 0)this.ctx.lineTo(x, height)this.ctx.stroke()}// 绘制水平线for (let y = 0; y < height; y += 10) {this.ctx.beginPath()this.ctx.moveTo(0, y)this.ctx.lineTo(width, y)this.ctx.stroke()}// 绘制中心线this.ctx.setStrokeStyle('#FF0000')this.ctx.setLineWidth(2)this.ctx.beginPath()this.ctx.moveTo(0, height / 2)this.ctx.lineTo(width, height / 2)this.ctx.stroke()},drawScrollECG() {if (this.dataList.length === 0) returnconst width = this.canvasWidthconst height = this.canvasHeightthis.ctx.setStrokeStyle('#31CE32')this.ctx.setLineWidth(2)const startIndex = Math.max(0, this.scrollIndex - this.displayPoints)const endIndex = Math.min(this.scrollIndex, this.dataList.length)if (startIndex >= endIndex) returnconst xStep = width / this.displayPointsconst centerY = height / 2this.ctx.beginPath()for (let i = startIndex; i < endIndex; i++) {const x = (i - startIndex) * xStepconst value = this.dataList[i]const y = centerY - (value * 1.5)if (i === startIndex) {this.ctx.moveTo(x, y)} else {this.ctx.lineTo(x, y)}}this.ctx.stroke()},drawRefreshECG() {if (this.displayData.length === 0) returnconst width = this.canvasWidthconst height = this.canvasHeightthis.ctx.setStrokeStyle('#31CE32')this.ctx.setLineWidth(2)const xStep = width / this.displayPointsconst centerY = height / 2this.ctx.beginPath()for (let i = 0; i < this.displayPoints && i < this.displayData.length; i++) {const x = i * xStepconst y = centerY - (this.displayData[i] * 1.5)if (i === 0) {this.ctx.moveTo(x, y)} else {this.ctx.lineTo(x, y)}}this.ctx.stroke()// 绘制当前刷新位置指示器if (this.showIndex >= 0 && this.showIndex < this.displayPoints) {this.ctx.setFillStyle('#FF0000')const x = this.showIndex * xStepthis.ctx.beginPath()this.ctx.arc(x, centerY, 3, 0, 2 * Math.PI)this.ctx.fill()}},drawStaticECG() {if (this.dataList.length === 0) returnconst width = this.canvasWidthconst height = this.canvasHeightthis.ctx.setStrokeStyle('#31CE32')this.ctx.setLineWidth(2)const displaySize = Math.min(this.displayPoints, this.dataList.length)const xStep = width / displaySizeconst centerY = height / 2this.ctx.beginPath()for (let i = 0; i < displaySize; i++) {const x = i * xStepconst value = this.dataList[i]const y = centerY - (value * 1.5)if (i === 0) {this.ctx.moveTo(x, y)} else {this.ctx.lineTo(x, y)}}this.ctx.stroke()}}
}
</script><style scoped>
.ecg-container {display: flex;flex-direction: column;align-items: center;padding: 20rpx;background-color: #f5f5f5;min-height: 100vh;
}.ecg-header {margin-bottom: 20rpx;
}.ecg-title {font-size: 32rpx;font-weight: bold;color: #333;
}.ecg-canvas-container {background-color: #fff;border-radius: 10rpx;padding: 20rpx;box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);margin-bottom: 30rpx;
}.ecg-canvas {width: 100%;height: 400rpx;border: 1rpx solid #e0e0e0;
}.ecg-controls {display: flex;flex-direction: row;justify-content: space-around;width: 100%;max-width: 600rpx;
}.control-btn {padding: 20rpx 40rpx;font-size: 28rpx;border: none;border-radius: 10rpx;background-color: #007AFF;color: #fff;margin: 0 10rpx;
}.control-btn:active {background-color: #0056b3;
}.control-btn[disabled] {background-color: #ccc;color: #999;
}.stop-btn {background-color: #ff3b30;
}.stop-btn:active {background-color: #d9342a;
}
</style>

使用方法

1. 组件引入

将 ecg-chart.vue 文件复制到你的 uniapp 项目 components 目录下。

2. 页面中使用

<template><view><ecg-chart></ecg-chart></view>
</template><script>
import EcgChart from '@/components/ecg-chart.vue'export default {components: {EcgChart}
}
</script>

3. 自定义配置

可以通过 props 自定义配置(可选):

<ecg-chart :canvas-width="375" :canvas-height="200":max-data-points="100":display-points="50"
/>

核心算法

数据生成算法:


// Uniapp版本
const value = Math.sin(i * 0.1) * 50 + Math.sin(i * 0.3) * 20 + (Math.random() - 0.5) * 10;

绘图坐标转换:

// Uniapp: ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke()

平台兼容性

  • ✅ H5 - 完全支持
  • ✅ 微信小程序 - 完全支持
  • ✅ 支付宝小程序 - 完全支持
  • ✅ 百度小程序 - 完全支持
  • ✅ QQ小程序 - 完全支持
  • ✅ 字节跳动小程序 - 完全支持
  • ✅ App (iOS/Android) - 完全支持

样式定制

组件使用 scoped CSS,可以通过以下方式自定义样式:

/* 自定义画布大小 */
.ecg-canvas {width: 100%;height: 500rpx;
}/* 自定义按钮样式 */
.control-btn {background-color: #007AFF;border-radius: 20rpx;
}

注意事项

  1. canvas-id 必须唯一,如果页面有多个心电图组件,需要传入不同的canvas-id
  2. 在小程序中,canvas的宽高需要通过style属性设置
  3. 组件已适配不同屏幕密度,无需额外处理
http://www.dtcms.com/a/593849.html

相关文章:

  • 高性能抗干扰两线电流型霍尔开关SC25898 | 赛卓电子重磅新品
  • 仁怀哪里有做网站的做效果图常用的网站有哪些
  • 2025 11 09 作业
  • 廊坊网站seo服务深圳市光明区官网
  • 大模型调用完全指南(含免费资源汇总)
  • 定义数组指针
  • 做搜狗手机网站点网站第三方统计工具
  • 专业钓场计时计费管理系统:提升运营效率的智能化解决方案
  • 如何做一个与博物馆相关网站卡板技术支持 东莞网站建设
  • 北大软件外事管理系统:以“制度+技术”,筑牢外事管理 数字化屏障
  • wordpress怎么连接主机名aso如何优化
  • PDF文件内容出现重叠现象解析
  • 织梦系统怎么做网站个人网站备案信息填写
  • 基于AutoDL远端服务复现具身智能论文OpenVLA
  • 新能源汽车减速器行星齿轮机构资料整理
  • 网站建设哈尔滨网站建设1企业网站策划书制作
  • 53_AI智能体运维部署之集成Prometheus监控系统:构建可观测的AI应用基础设施
  • 福建平潭建设局网站室内设计师经常用的网站
  • 政务配发移动设备管理解决方案
  • 【CCF-CSP】第三次认证03-02 Z字形扫描
  • 西柏坡旅游网站建设规划书企业网站 cms
  • πRL——首个在线RL微调流式VLA π0/π0.5的框架:通过Flow-Noise和Flow-SDE实现精确对数似然估计,全面提升性能
  • 求n以内自守数个数
  • 找做金融的网站有哪些深圳建设集团招标
  • Java大厂面试真题:从Spring Boot到AI微服务的三轮技术拷问(一)
  • 如何解决 “Error parsing JSON: invalid character ‘<’ looking for beginning of value”
  • 一个网站如何挣钱seo平台优化服务
  • 丽水市城市建设投资有限责任公司网站注册公司在哪核名
  • 深度学习:从零开始手搓一个浅层神经网络(Single Hidden Layer Neural Network)
  • 网站开发 犯法公司起名在线生成器