180 lines
4.6 KiB
Vue
180 lines
4.6 KiB
Vue
<!-- 心率历史波形:canvas 自绘红心电风格面积图(复刻 web 端 TinyArea 视觉) -->
|
||
<template>
|
||
<view class="wave-box">
|
||
<canvas
|
||
:id="canvasId"
|
||
class="wave-canvas"
|
||
:canvas-id="canvasId"
|
||
:style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"
|
||
/>
|
||
<!-- ECG 风格虚线分割:上/中/下三部分 -->
|
||
<view class="wave-divider wave-divider--top" />
|
||
<view class="wave-divider wave-divider--bottom" />
|
||
<view v-if="empty" class="wave-empty">
|
||
<text>暂无数据</text>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
// 与 props.canvasHeight 保持一致;波形上下各留 8px 呼吸空间
|
||
const V_PADDING = 8
|
||
|
||
export default {
|
||
name: 'vital-wave',
|
||
props: {
|
||
// 数据点(升序 number 数组)
|
||
points: {
|
||
type: Array,
|
||
default: () => []
|
||
},
|
||
// canvas id,多实例场景需外部保证唯一
|
||
canvasId: {
|
||
type: String,
|
||
default: 'vitalWave'
|
||
}
|
||
},
|
||
data() {
|
||
return {
|
||
canvasWidth: 200, // 容器实际宽(px),mounted 后测量
|
||
canvasHeight: 110
|
||
}
|
||
},
|
||
computed: {
|
||
empty() {
|
||
return !this.points || this.points.length < 2
|
||
}
|
||
},
|
||
watch: {
|
||
// 数据变化重绘(详情页轮询刷新时触发)
|
||
points() {
|
||
this.draw()
|
||
}
|
||
},
|
||
mounted() {
|
||
this.measure()
|
||
},
|
||
methods: {
|
||
// 测量容器宽度:宽度自适应父级,高度固定 110px 与 web 端一致
|
||
measure() {
|
||
uni
|
||
.createSelectorQuery()
|
||
.in(this)
|
||
.select('.wave-box')
|
||
.boundingClientRect((rect) => {
|
||
if (rect && rect.width > 0) {
|
||
this.canvasWidth = Math.floor(rect.width)
|
||
this.$nextTick(() => this.draw())
|
||
}
|
||
})
|
||
.exec()
|
||
},
|
||
// 绘制平滑面积图(对齐 web 端 TinyArea:smooth 曲线 + 自适应 y 值域 + padding:0)
|
||
draw() {
|
||
if (this.empty) return
|
||
const ctx = uni.createCanvasContext(this.canvasId, this)
|
||
const w = this.canvasWidth
|
||
const h = this.canvasHeight
|
||
const pts = this.points
|
||
const min = Math.min(...pts)
|
||
const max = Math.max(...pts)
|
||
// 值域映射到画布;无波动时居中画直线
|
||
const span = max - min || 1
|
||
|
||
const xAt = (i) => (pts.length <= 1 ? 0 : (i / (pts.length - 1)) * w)
|
||
const yAt = (v) => {
|
||
const t = (v - min) / span
|
||
return h - V_PADDING - t * (h - V_PADDING * 2)
|
||
}
|
||
|
||
// smooth: true —— Catmull-Rom 样条插值(G2 smooth 的实现方式),
|
||
// 每个点作为控制点求相邻点间的三次贝塞尔,曲线过每个数据点
|
||
const pathTo = (closeBottom) => {
|
||
ctx.beginPath()
|
||
ctx.moveTo(xAt(0), yAt(pts[0]))
|
||
if (pts.length === 2) {
|
||
ctx.lineTo(xAt(1), yAt(pts[1]))
|
||
}
|
||
for (let i = 0; i < pts.length - 1; i++) {
|
||
const x0 = xAt(Math.max(0, i - 1))
|
||
const y0 = yAt(pts[Math.max(0, i - 1)])
|
||
const x1 = xAt(i)
|
||
const y1 = yAt(pts[i])
|
||
const x2 = xAt(i + 1)
|
||
const y2 = yAt(pts[i + 1])
|
||
const x3 = xAt(Math.min(pts.length - 1, i + 2))
|
||
const y3 = yAt(pts[Math.min(pts.length - 1, i + 2)])
|
||
// Catmull-Rom → 贝塞尔控制点
|
||
const c1x = x1 + (x2 - x0) / 6
|
||
const c1y = y1 + (y2 - y0) / 6
|
||
const c2x = x2 - (x3 - x1) / 6
|
||
const c2y = y2 - (y3 - y1) / 6
|
||
ctx.bezierCurveTo(c1x, c1y, c2x, c2y, x2, y2)
|
||
}
|
||
if (closeBottom) {
|
||
ctx.lineTo(w, h)
|
||
ctx.lineTo(0, h)
|
||
ctx.closePath()
|
||
}
|
||
}
|
||
|
||
// 面积:曲线 + 封底,渐变填充(对齐 l(270) 0:#f5222d 1:transparent)
|
||
pathTo(true)
|
||
const grad = ctx.createLinearGradient(0, 0, 0, h)
|
||
grad.addColorStop(0, 'rgba(245, 34, 45, 0.25)')
|
||
grad.addColorStop(1, 'rgba(245, 34, 45, 0)')
|
||
ctx.setFillStyle && ctx.setFillStyle(grad)
|
||
ctx.fill()
|
||
|
||
// 描边线:2px
|
||
pathTo(false)
|
||
ctx.setStrokeStyle && ctx.setStrokeStyle('#f5222d')
|
||
ctx.setLineWidth && ctx.setLineWidth(2)
|
||
ctx.stroke()
|
||
|
||
ctx.draw()
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.wave-box {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 220rpx;
|
||
}
|
||
|
||
.wave-canvas {
|
||
display: block;
|
||
}
|
||
|
||
.wave-divider {
|
||
position: absolute;
|
||
left: 0;
|
||
right: 0;
|
||
border-top: 1px dashed rgba(245, 34, 45, 0.25);
|
||
|
||
&--top {
|
||
top: 33.3%;
|
||
}
|
||
|
||
&--bottom {
|
||
top: 66.6%;
|
||
}
|
||
}
|
||
|
||
.wave-empty {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 24rpx;
|
||
color: #b5b9c0;
|
||
}
|
||
</style>
|