|
@@ -3,15 +3,26 @@
|
|
|
</template>
|
|
|
|
|
|
<script lang="ts" setup>
|
|
|
-import { ref, onMounted } from 'vue'
|
|
|
+import { ref, onMounted, watch } from 'vue'
|
|
|
import * as echarts from 'echarts'
|
|
|
|
|
|
defineOptions({ name: 'BreathLineChart' })
|
|
|
|
|
|
+const props = withDefaults(
|
|
|
+ defineProps<{
|
|
|
+ data: number[]
|
|
|
+ }>(),
|
|
|
+ {
|
|
|
+ data: () => [],
|
|
|
+ }
|
|
|
+)
|
|
|
+
|
|
|
const lineChartRef = ref<HTMLDivElement | null>(null)
|
|
|
+const lineChart = ref<echarts.ECharts | null>(null)
|
|
|
+const option = ref<echarts.EChartsOption>()
|
|
|
|
|
|
onMounted(() => {
|
|
|
- const lineChart = echarts.init(lineChartRef.value!)
|
|
|
+ lineChart.value = echarts.init(lineChartRef.value!)
|
|
|
|
|
|
// -----------------------------
|
|
|
// 配置显示的时间和数据点
|
|
@@ -22,25 +33,8 @@ onMounted(() => {
|
|
|
const xAxisData = Array.from({ length: maxPoints }, (_, i) => i * step)
|
|
|
const lineData: number[] = Array(maxPoints).fill(20) // 初始基准值为20
|
|
|
|
|
|
- let t = 0 // 时间计数器(秒)
|
|
|
-
|
|
|
- // -----------------------------
|
|
|
- // 模拟呼吸参数
|
|
|
- // -----------------------------
|
|
|
- const bpm = 50 // 平均呼吸次数(次/分钟),可以修改
|
|
|
- const basePeriod = 60 / bpm // 平均每次呼吸周期(秒)
|
|
|
- let period = basePeriod // 当前周期
|
|
|
- let amplitude = 10 // 波动幅度
|
|
|
-
|
|
|
- const yBase = 20 // y轴基准线,波形围绕此值波动
|
|
|
-
|
|
|
- // 用于计算实时BPM
|
|
|
- const peakTimes: number[] = []
|
|
|
-
|
|
|
- // -----------------------------
|
|
|
// 初始化图表配置
|
|
|
- // -----------------------------
|
|
|
- const option = {
|
|
|
+ option.value = {
|
|
|
title: [
|
|
|
{
|
|
|
text: '呼吸率曲线', // 主标题
|
|
@@ -68,7 +62,7 @@ onMounted(() => {
|
|
|
type: 'value',
|
|
|
min: 0,
|
|
|
max: 40,
|
|
|
- name: '呼吸幅度(次)',
|
|
|
+ name: '呼吸率(次)',
|
|
|
nameLocation: 'end',
|
|
|
nameGap: 20,
|
|
|
nameTextStyle: { fontSize: 12, color: '#888888', fontWeight: 'normal' },
|
|
@@ -87,75 +81,34 @@ onMounted(() => {
|
|
|
animationDuration: 100,
|
|
|
}
|
|
|
|
|
|
- lineChart.setOption(option)
|
|
|
-
|
|
|
- // -----------------------------
|
|
|
- // 定时生成模拟呼吸数据
|
|
|
- // -----------------------------
|
|
|
- setInterval(() => {
|
|
|
- t += step // 更新时间
|
|
|
+ lineChart.value.setOption(option.value)
|
|
|
|
|
|
- // -----------------------------
|
|
|
- // 调整波形随机性
|
|
|
- // -----------------------------
|
|
|
- if (Math.random() < 0.1) {
|
|
|
- // 每隔一段时间随机调整周期和幅度,使波形更自然
|
|
|
- period = basePeriod * (0.8 + Math.random() * 0.4) // ±20% ~ ±40%
|
|
|
- amplitude = 8 + Math.random() * 6 // 幅度 8~14
|
|
|
- }
|
|
|
-
|
|
|
- // -----------------------------
|
|
|
- // 生成当前点的呼吸值
|
|
|
- // -----------------------------
|
|
|
- // 正弦波 + 小扰动
|
|
|
- const val = yBase + amplitude * Math.sin(((2 * Math.PI) / period) * t) + (Math.random() * 2 - 1)
|
|
|
- const clampedVal = Math.min(40, Math.max(0, val)) // 保证不超过y轴范围
|
|
|
-
|
|
|
- // -----------------------------
|
|
|
- // 更新折线数据
|
|
|
- // -----------------------------
|
|
|
- lineData.shift() // 删除最旧的数据点
|
|
|
- lineData.push(clampedVal) // 添加新数据点
|
|
|
+ // 自适应窗口大小
|
|
|
+ window.addEventListener('resize', () => lineChart.value!.resize())
|
|
|
+})
|
|
|
|
|
|
- // -----------------------------
|
|
|
- // 检测波峰用于计算实时BPM
|
|
|
- // -----------------------------
|
|
|
- const lastIndex = lineData.length - 1
|
|
|
- if (lastIndex >= 2) {
|
|
|
- // 当前点大于前一点和后一点,则为峰
|
|
|
- if (
|
|
|
- lineData[lastIndex - 2] < lineData[lastIndex - 1] &&
|
|
|
- lineData[lastIndex] < lineData[lastIndex - 1]
|
|
|
- ) {
|
|
|
- const peakTime = t
|
|
|
- peakTimes.push(peakTime)
|
|
|
- // 保留最近60秒的峰,用于计算BPM
|
|
|
- while (peakTimes.length && peakTimes[0] < t - 60) peakTimes.shift()
|
|
|
- }
|
|
|
+watch(
|
|
|
+ () => props.data,
|
|
|
+ (newData) => {
|
|
|
+ const list = newData
|
|
|
+ if (list.length > 60) {
|
|
|
+ list.shift()
|
|
|
}
|
|
|
-
|
|
|
- // -----------------------------
|
|
|
- // 计算实时BPM
|
|
|
- // -----------------------------
|
|
|
- const currentBPM = peakTimes.length
|
|
|
-
|
|
|
- // -----------------------------
|
|
|
- // 更新图表
|
|
|
- // -----------------------------
|
|
|
- lineChart.setOption(
|
|
|
+ lineChart.value!.setOption(
|
|
|
{
|
|
|
- series: [{ ...option.series[0], data: lineData.slice() }], // 更新折线数据
|
|
|
- title: [{}, { text: `BPM: ${currentBPM} 次/分钟` }, {}], // 更新实时BPM
|
|
|
+ series:
|
|
|
+ option.value && option.value.series
|
|
|
+ ? Array.isArray(option.value.series)
|
|
|
+ ? [{ ...option.value.series[0], data: list }]
|
|
|
+ : [{ ...option.value.series, data: list }]
|
|
|
+ : [{ data: list }], // 更新折线数据
|
|
|
+ title: [{}, { text: `BPM: ${newData[newData.length - 1]} 次/分钟` }, {}], // 更新实时BPM
|
|
|
},
|
|
|
{ notMerge: false }
|
|
|
)
|
|
|
- }, 100) // 每0.5秒刷新一次数据
|
|
|
-
|
|
|
- // -----------------------------
|
|
|
- // 自适应窗口大小
|
|
|
- // -----------------------------
|
|
|
- window.addEventListener('resize', () => lineChart.resize())
|
|
|
-})
|
|
|
+ },
|
|
|
+ { deep: true }
|
|
|
+)
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|