调整导出 CSV 格式,移除 recordId 和 timestamp,时间单位改为毫秒;优化图表显示逻辑,默认隐藏原始和滤波码值
This commit is contained in:
parent
91b889a05f
commit
2acda5959a
@ -48,20 +48,21 @@ export async function POST(req: NextRequest) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// CSV
|
// CSV(去掉 recordId、timestamp,timeSec 改为毫秒单位)
|
||||||
let csv = 'recordId,timestamp,index,timeSec,code,force\n'
|
let csv = 'index,timeMs,code,force\n'
|
||||||
for (const r of rows) {
|
for (const r of rows) {
|
||||||
let step = r.sample_count > 0 ? r.duration / r.sample_count : 0
|
// 以毫秒为单位的步长
|
||||||
|
let stepMs = r.sample_count > 0 ? (r.duration * 1000) / r.sample_count : 0
|
||||||
if (r.rec_start_ms != null && r.rec_end_ms != null && r.sample_count > 0) {
|
if (r.rec_start_ms != null && r.rec_end_ms != null && r.sample_count > 0) {
|
||||||
const dtMs = Number((r.rec_end_ms as bigint) - (r.rec_start_ms as bigint))
|
const dtMs = Number((r.rec_end_ms as bigint) - (r.rec_start_ms as bigint))
|
||||||
if (Number.isFinite(dtMs) && dtMs > 0) step = dtMs / 1000 / r.sample_count
|
if (Number.isFinite(dtMs) && dtMs > 0) stepMs = dtMs / r.sample_count
|
||||||
}
|
}
|
||||||
const hasFit = r.fit_a != null
|
const hasFit = r.fit_a != null
|
||||||
for (let i = 0; i < r.code_data.length; i++) {
|
for (let i = 0; i < r.code_data.length; i++) {
|
||||||
const t = (i * step).toFixed(6)
|
const t = (i * stepMs).toFixed(3)
|
||||||
const code = r.code_data[i]
|
const code = r.code_data[i]
|
||||||
const force = hasFit ? r.fit_a! * code + (r.fit_b ?? 0) : ''
|
const force = hasFit ? r.fit_a! * code + (r.fit_b ?? 0) : ''
|
||||||
csv += `${r.id},${new Date(r.timestamp).toISOString()},${i},${t},${code},${hasFit ? force : ''}\n`
|
csv += `${i},${t},${code},${hasFit ? force : ''}\n`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new Response(csv, {
|
return new Response(csv, {
|
||||||
|
|||||||
@ -45,15 +45,14 @@ export default function Page({ params }: { params: Promise<{ id: string }> }) {
|
|||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [rec, setRec] = useState<Detail | null>(null)
|
const [rec, setRec] = useState<Detail | null>(null)
|
||||||
// 滤波相关状态
|
// 滤波相关状态
|
||||||
const [filterType, setFilterType] = useState<'none' | 'ema' | 'sg' | 'hampel_sg' | 'gauss'>('hampel_sg')
|
const [filterType, setFilterType] = useState<'none' | 'ema' | 'sg' | 'hampel_sg' | 'gauss'>('ema')
|
||||||
const [emaAlpha, setEmaAlpha] = useState(0.01)
|
const [emaAlpha, setEmaAlpha] = useState(0.01)
|
||||||
const [sgWindow, setSgWindow] = useState(21)
|
const [sgWindow, setSgWindow] = useState(21)
|
||||||
const [sgOrder, setSgOrder] = useState<2 | 3>(3)
|
const [sgOrder, setSgOrder] = useState<2 | 3>(3)
|
||||||
const [hampelWindow, setHampelWindow] = useState(11)
|
const [hampelWindow, setHampelWindow] = useState(11)
|
||||||
const [hampelK, setHampelK] = useState(2)
|
const [hampelK, setHampelK] = useState(2)
|
||||||
const [gaussSigma, setGaussSigma] = useState(5)
|
const [gaussSigma, setGaussSigma] = useState(5)
|
||||||
const [showOriginal, setShowOriginal] = useState(false)
|
// 图例控制显示,不再使用自定义 checkbox;默认隐藏在数据集上配置
|
||||||
const [showFiltered, setShowFiltered] = useState(true)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
@ -76,10 +75,7 @@ export default function Page({ params }: { params: Promise<{ id: string }> }) {
|
|||||||
return () => controller.abort()
|
return () => controller.abort()
|
||||||
}, [id])
|
}, [id])
|
||||||
|
|
||||||
// 若存在拟合,默认不显示滤波码值(仅在记录加载时初始化一次)
|
// 显示策略改由 Chart.js 图例控制,默认隐藏在数据集 hidden 字段中设置
|
||||||
useEffect(() => {
|
|
||||||
if (rec?.fit) setShowFiltered(false)
|
|
||||||
}, [rec?.fit])
|
|
||||||
|
|
||||||
// 侦测小屏(手机)以调整 Chart 布局
|
// 侦测小屏(手机)以调整 Chart 布局
|
||||||
const [isSmall, setIsSmall] = useState(false)
|
const [isSmall, setIsSmall] = useState(false)
|
||||||
@ -121,7 +117,8 @@ export default function Page({ params }: { params: Promise<{ id: string }> }) {
|
|||||||
|
|
||||||
const timeAxis = useMemo(() => {
|
const timeAxis = useMemo(() => {
|
||||||
if (!rec) return [] as number[]
|
if (!rec) return [] as number[]
|
||||||
const step = rec.sampleCount > 0 ? (rec.duration * 1000) / rec.sampleCount : 0
|
// 使用秒为单位,保留3位小数
|
||||||
|
const step = rec.sampleCount > 0 ? rec.duration / rec.sampleCount : 0
|
||||||
return Array.from({ length: rec.sampleCount }, (_, i) => +(i * step).toFixed(3))
|
return Array.from({ length: rec.sampleCount }, (_, i) => +(i * step).toFixed(3))
|
||||||
}, [rec])
|
}, [rec])
|
||||||
|
|
||||||
@ -351,15 +348,7 @@ export default function Page({ params }: { params: Promise<{ id: string }> }) {
|
|||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<label className="flex items-center gap-2 text-sm">
|
{/* 由 Chart.js 图例控制数据集显示,不再使用本地 checkbox */}
|
||||||
<input type="checkbox" className="rounded" checked={showFiltered} onChange={(e) => setShowFiltered(e.target.checked)} />
|
|
||||||
<span>显示滤波码值</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="flex items-center gap-2 text-sm">
|
|
||||||
<input type="checkbox" className="rounded" checked={showOriginal} onChange={(e) => setShowOriginal(e.target.checked)} />
|
|
||||||
<span>叠加原始</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<span className="hidden text-xs text-zinc-500 sm:inline">按 Ctrl+滚轮 缩放,拖拽平移</span>
|
<span className="hidden text-xs text-zinc-500 sm:inline">按 Ctrl+滚轮 缩放,拖拽平移</span>
|
||||||
</div>
|
</div>
|
||||||
@ -369,35 +358,29 @@ export default function Page({ params }: { params: Promise<{ id: string }> }) {
|
|||||||
data={{
|
data={{
|
||||||
labels: timeAxis,
|
labels: timeAxis,
|
||||||
datasets: [
|
datasets: [
|
||||||
...(showOriginal && rec
|
{
|
||||||
? [
|
type: 'line' as const,
|
||||||
{
|
label: '原始码值',
|
||||||
type: 'line' as const,
|
data: rec.code,
|
||||||
label: '原始码值',
|
borderColor: '#a1a1aa',
|
||||||
data: rec.code,
|
backgroundColor: 'rgba(161,161,170,0.2)',
|
||||||
borderColor: '#a1a1aa',
|
borderDash: [4, 4] as any,
|
||||||
backgroundColor: 'rgba(161,161,170,0.2)',
|
borderWidth: 1,
|
||||||
borderDash: [4, 4] as any,
|
yAxisID: 'yCode',
|
||||||
borderWidth: 1,
|
pointRadius: 0,
|
||||||
yAxisID: 'yCode',
|
hidden: true, // 默认不显示原始
|
||||||
pointRadius: 0,
|
},
|
||||||
},
|
{
|
||||||
]
|
type: 'line' as const,
|
||||||
: []),
|
label: '滤波码值',
|
||||||
...(showFiltered
|
data: filterType === 'none' ? rec.code : filteredCode,
|
||||||
? [
|
borderColor: '#0ea5e9',
|
||||||
{
|
backgroundColor: 'rgba(14,165,233,0.2)',
|
||||||
type: 'line' as const,
|
borderWidth: 1.5,
|
||||||
label: '滤波码值',
|
yAxisID: 'yCode',
|
||||||
data: filterType === 'none' ? rec.code : filteredCode,
|
pointRadius: 0,
|
||||||
borderColor: '#0ea5e9',
|
hidden: Boolean(rec?.fit), // 存在拟合时默认不显示滤波码值
|
||||||
backgroundColor: 'rgba(14,165,233,0.2)',
|
},
|
||||||
borderWidth: 1.5,
|
|
||||||
yAxisID: 'yCode',
|
|
||||||
pointRadius: 0,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
...(rec.fit && forceSeries
|
...(rec.fit && forceSeries
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
@ -431,7 +414,7 @@ export default function Page({ params }: { params: Promise<{ id: string }> }) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
x: { title: { display: !isSmall, text: '时间 (ms)' }, ticks: { maxRotation: 0, font: { size: isSmall ? 10 : 12 } } },
|
x: { title: { display: !isSmall, text: '时间 (s)' }, ticks: { maxRotation: 0, font: { size: isSmall ? 10 : 12 } } },
|
||||||
yCode: { type: 'linear' as const, position: 'left' as const, title: { display: !isSmall, text: '码值' }, ticks: { font: { size: isSmall ? 10 : 12 } } },
|
yCode: { type: 'linear' as const, position: 'left' as const, title: { display: !isSmall, text: '码值' }, ticks: { font: { size: isSmall ? 10 : 12 } } },
|
||||||
yForce: { type: 'linear' as const, position: 'right' as const, title: { display: !isSmall, text: '力值 (mN)' }, grid: { drawOnChartArea: false }, ticks: { font: { size: isSmall ? 10 : 12 } } },
|
yForce: { type: 'linear' as const, position: 'right' as const, title: { display: !isSmall, text: '力值 (mN)' }, grid: { drawOnChartArea: false }, ticks: { font: { size: isSmall ? 10 : 12 } } },
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user