SEC_Warehouse/components/vital-wave.vue

180 lines
4.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- 心率历史波形: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>