"use client" import { use, useEffect, useMemo, useState } from 'react' import Link from 'next/link' import { Chart as ChartJS, CategoryScale, LinearScale, PointElement, LineElement, BarElement, Tooltip, Legend, } from 'chart.js' import { Line, Bar, Scatter } from 'react-chartjs-2' ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, BarElement, Tooltip, Legend) type Detail = { id: string timestamp: string code: number[] fit?: { a: number; b: number } sampleCount: number duration: number stats: { codeMin: number codeMax: number codeAvg: number forceMin?: number forceMax?: number forceAvg?: number } } function toFixed(n: number | undefined | null, d = 3) { if (n == null || Number.isNaN(n)) return '-' return n.toFixed(d) } export default function Page({ params }: { params: Promise<{ id: string }> }) { const { id } = use(params) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [rec, setRec] = useState(null) const [emaAlpha, setEmaAlpha] = useState(0.01) const [enableEma, setEnableEma] = useState(true) useEffect(() => { const controller = new AbortController() async function run() { setLoading(true) setError(null) try { const res = await fetch(`/api/records/${id}`, { signal: controller.signal }) if (!res.ok) throw new Error(`HTTP ${res.status}`) const j = await res.json() setRec(j) } catch (e) { // @ts-expect-error any if (e.name !== 'AbortError') setError(e.message || '加载失败') } finally { setLoading(false) } } run() return () => controller.abort() }, [id]) const timeAxis = useMemo(() => { if (!rec) return [] as number[] const step = rec.sampleCount > 0 ? (rec.duration * 1000) / rec.sampleCount : 0 return Array.from({ length: rec.sampleCount }, (_, i) => +(i * step).toFixed(3)) }, [rec]) const smoothedCode = useMemo(() => { if (!rec || !enableEma) return rec?.code || [] return applyEMA(rec.code, emaAlpha) }, [rec, enableEma, emaAlpha]) const forceSeries = useMemo(() => { if (!rec?.fit) return undefined const { a, b } = rec.fit const dataToUse = enableEma ? smoothedCode : rec.code return dataToUse.map((c) => a * c + b) }, [rec, enableEma, smoothedCode]) const codeHistogram = useMemo(() => { const dataToUse = enableEma ? smoothedCode : (rec?.code || []) return buildHistogram(dataToUse, 30) }, [rec, enableEma, smoothedCode]) const forceHistogram = useMemo(() => buildHistogram(forceSeries || [], 30), [forceSeries]) return (
← 返回

记录 {rec?.id || id}

{loading &&
加载中...
} {error &&
{error}
} {rec && (
上传时间
{new Date(rec.timestamp).toLocaleString()}
采样
{rec.sampleCount} 点 · {rec.duration.toFixed(3)} s · {(rec.sampleCount / rec.duration).toFixed(2)} Hz
拟合
{rec.fit ? `y = ${rec.fit.a.toFixed(4)}x + ${rec.fit.b.toFixed(3)}` : '未拟合'}
码值统计
[{rec.stats.codeMin} ~ {rec.stats.codeMax}] · 均值 {toFixed(rec.stats.codeAvg, 2)}
力值统计
{rec.fit ? `[${toFixed(rec.stats.forceMin, 2)} ~ ${toFixed(rec.stats.forceMax, 2)}] mN · 均值 ${toFixed(rec.stats.forceAvg, 2)}` : '—'}
峰峰值
码值 {rec.stats.codeMax - rec.stats.codeMin}{rec.fit && rec.stats.forceMax != null && rec.stats.forceMin != null ? ` · 力值 ${(rec.stats.forceMax - rec.stats.forceMin).toFixed(2)} mN` : ''}
时序曲线
{enableEma && ( )}
码值分布直方图
线性度散点图
{rec.fit && forceSeries ? ( ({ x: c, y: forceSeries[i] })), backgroundColor: '#22c55e', }, ], }} options={{ scales: { x: { title: { display: true, text: '码值' } }, y: { title: { display: true, text: '力值 (mN)' } }, }, plugins: { legend: { display: false } }, }} /> ) : (
无拟合参数,无法绘制
)}
)}
) } function applyEMA(data: number[], alpha: number): number[] { if (data.length === 0) return [] const result: number[] = [data[0]] for (let i = 1; i < data.length; i++) { result.push(alpha * data[i] + (1 - alpha) * result[i - 1]) } return result } function buildHistogram(values: number[], bins: number) { if (!values.length || bins <= 0) return { labels: [] as string[], counts: [] as number[] } let min = values[0] let max = values[0] for (const v of values) { if (v < min) min = v if (v > max) max = v } if (min === max) { return { labels: [min.toFixed(2)], counts: [values.length] } } const step = (max - min) / bins const edges = Array.from({ length: bins + 1 }, (_, i) => min + i * step) const counts = Array.from({ length: bins }, () => 0) for (const v of values) { let idx = Math.floor((v - min) / step) if (idx >= bins) idx = bins - 1 if (idx < 0) idx = 0 counts[idx]++ } const labels = Array.from({ length: bins }, (_, i) => `${edges[i].toFixed(1)}~${edges[i + 1].toFixed(1)}`) return { labels, counts } }