enterprise-saa-s-dashboard-.../src/app/materials/page.tsx

380 lines
25 KiB
TypeScript
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.

'use client'
import { useState, useMemo } from 'react'
import Link from 'next/link'
import { ChevronLeft, ChevronRight, Plus, Download, Eye, X, FileText, Download as DownloadIcon, Trash2 } from 'lucide-react'
import { useApi } from '@/lib/hooks'
interface BoardCard {
id: number
sn: string
name: string
category: string
type: string
device_model: string
version: string
description: string
firmware: string
status: string
device_sn: string
production_date: string
calib_status: string
calib_date: string
}
interface CategoryItem { id: number; name: string; status: string }
const statusOptions = ['全部', '在库', '已装配', '故障', '报废']
const calibStatusOptions = ['全部', '合格', '不合格', '待校准']
/** 模拟校准文件数据key 为板卡 SN */
const calibrationFilesMap: Record<string, { id: string; fileName: string; fileSize: number; md5: string; uploadTime: string; channels: { channel: string; refValue: number; measuredValue: number; deviation: number; result: string }[] }[]> = {
'ACB-6000-20250110001': [
{ id: 'cf-1', fileName: 'ACB-6000-20250110001_calib_20250112.csv', fileSize: 24576, md5: 'a1b2c3d4e5f6', uploadTime: '2025-01-12 14:30:00', channels: [
{ channel: 'CH1', refValue: 100.0, measuredValue: 99.8, deviation: -0.2, result: '合格' },
{ channel: 'CH2', refValue: 200.0, measuredValue: 200.3, deviation: 0.15, result: '合格' },
{ channel: 'CH3', refValue: 500.0, measuredValue: 499.5, deviation: -0.1, result: '合格' },
{ channel: 'CH4', refValue: 1000.0, measuredValue: 999.2, deviation: -0.08, result: '合格' },
]},
],
'ACB-6000-20250110002': [
{ id: 'cf-2', fileName: 'ACB-6000-20250110002_calib_20250112.csv', fileSize: 23040, md5: 'b2c3d4e5f6a1', uploadTime: '2025-01-12 15:10:00', channels: [
{ channel: 'CH1', refValue: 100.0, measuredValue: 100.1, deviation: 0.1, result: '合格' },
{ channel: 'CH2', refValue: 200.0, measuredValue: 199.7, deviation: -0.15, result: '合格' },
{ channel: 'CH3', refValue: 500.0, measuredValue: 500.8, deviation: 0.16, result: '合格' },
]},
],
'ACB-5000-20241205001': [
{ id: 'cf-3', fileName: 'ACB-5000-20241205001_calib_20241208.csv', fileSize: 18432, md5: 'c3d4e5f6a1b2', uploadTime: '2024-12-08 09:45:00', channels: [
{ channel: 'CH1', refValue: 100.0, measuredValue: 99.9, deviation: -0.1, result: '合格' },
{ channel: 'CH2', refValue: 200.0, measuredValue: 200.5, deviation: 0.25, result: '合格' },
]},
],
'ACB-6000-20241120001': [
{ id: 'cf-4', fileName: 'ACB-6000-20241120001_calib_20250210.csv', fileSize: 25600, md5: 'd4e5f6a1b2c3', uploadTime: '2025-02-10 11:20:00', channels: [
{ channel: 'CH1', refValue: 100.0, measuredValue: 98.2, deviation: -1.8, result: '不合格' },
{ channel: 'CH2', refValue: 200.0, measuredValue: 203.6, deviation: 1.8, result: '不合格' },
{ channel: 'CH3', refValue: 500.0, measuredValue: 500.1, deviation: 0.02, result: '合格' },
]},
],
}
function formatFileSize(bytes: number): string {
if (bytes < 1024) return bytes + ' B'
return (bytes / 1024).toFixed(1) + ' KB'
}
function getStatusStyle(status: string) {
switch (status) {
case '在库': return { backgroundColor: '#E6F7FF', color: '#1890FF', border: '1px solid #91D5FF' }
case '已装配': return { backgroundColor: '#F6FFED', color: '#52C41A', border: '1px solid #B7EB8F' }
case '故障': return { backgroundColor: '#FFF1F0', color: '#FF4D4F', border: '1px solid #FFCCC7' }
case '报废': return { backgroundColor: '#FAFAFA', color: 'rgba(0,0,0,0.45)', border: '1px solid #D9D9D9' }
default: return { backgroundColor: '#FAFAFA', color: 'rgba(0,0,0,0.45)', border: '1px solid #D9D9D9' }
}
}
function getCalibStyle(status: string) {
switch (status) {
case '合格': return { backgroundColor: '#F6FFED', color: '#52C41A', border: '1px solid #B7EB8F' }
case '不合格': return { backgroundColor: '#FFF1F0', color: '#FF4D4F', border: '1px solid #FFCCC7' }
case '待校准': return { backgroundColor: '#FFFBE6', color: '#FAAD14', border: '1px solid #FFE58F' }
default: return {}
}
}
export default function BoardCardsPage() {
const { data: boardCardsData, loading, refetch } = useApi<BoardCard[]>('/api/materials', [])
const { data: categoriesData } = useApi<CategoryItem[]>('/api/material-categories', [])
const typeOptions = ['全部', ...categoriesData.filter(c => c.status === '启用').map(c => c.name)]
const [filterType, setFilterType] = useState('全部')
const [filterStatus, setFilterStatus] = useState('全部')
const [filterCalib, setFilterCalib] = useState('全部')
const [searchText, setSearchText] = useState('')
const [currentPage, setCurrentPage] = useState(1)
const [detailDrawer, setDetailDrawer] = useState<BoardCard | null>(null)
const [calibFileDrawer, setCalibFileDrawer] = useState<BoardCard | null>(null)
const pageSize = 8
if (loading) return <div style={{ padding: 24 }}>...</div>
const filtered = boardCardsData.filter(b => {
if (filterType !== '全部' && b.category !== filterType) return false
if (filterStatus !== '全部' && b.status !== filterStatus) return false
if (filterCalib !== '全部' && b.calib_status !== filterCalib) return false
if (searchText && !b.sn.toLowerCase().includes(searchText.toLowerCase()) && !(b.category || b.name).toLowerCase().includes(searchText.toLowerCase())) return false
return true
})
const totalPages = Math.ceil(filtered.length / pageSize)
const paged = filtered.slice((currentPage - 1) * pageSize, currentPage * pageSize)
// 统计
const stats = {
total: boardCardsData.length,
inStock: boardCardsData.filter(b => b.status === '在库').length,
assembled: boardCardsData.filter(b => b.status === '已装配').length,
faulty: boardCardsData.filter(b => b.status === '故障').length,
pendingCalib: boardCardsData.filter(b => b.calib_status === '待校准').length,
}
return (
<div style={{ padding: 24 }}>
{/* Header */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
<div>
<h2 style={{ fontSize: 20, fontWeight: 600, margin: 0 }}></h2>
<p style={{ fontSize: 14, color: 'rgba(0,0,0,0.45)', margin: '4px 0 0' }}></p>
</div>
<div style={{ display: 'flex', gap: 12 }}>
<button style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '8px 16px', border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer', fontSize: 14 }}>
<Download size={16} />
</button>
<Link href="/materials/register" style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '8px 16px', border: 'none', borderRadius: 6, backgroundColor: '#4a7c59', color: '#fff', cursor: 'pointer', fontSize: 14, textDecoration: 'none' }}>
<Plus size={16} />
</Link>
</div>
</div>
{/* Stats Cards */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: 16, marginBottom: 24 }}>
{[
{ label: '物料总数', value: stats.total, color: '#4a7c59', bg: '#eef5f0' },
{ label: '在库', value: stats.inStock, color: '#1890FF', bg: '#E6F7FF' },
{ label: '已装配', value: stats.assembled, color: '#52C41A', bg: '#F6FFED' },
{ label: '故障', value: stats.faulty, color: '#FF4D4F', bg: '#FFF1F0' },
{ label: '待校准', value: stats.pendingCalib, color: '#FAAD14', bg: '#FFFBE6' },
].map(s => (
<div key={s.label} style={{ backgroundColor: '#fff', borderRadius: 8, padding: 16, boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}>
<div style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)', marginBottom: 8 }}>{s.label}</div>
<div style={{ fontSize: 24, fontWeight: 600, color: s.color }}>{s.value}</div>
</div>
))}
</div>
{/* Filter */}
<div style={{ backgroundColor: '#fff', borderRadius: 8, padding: 20, marginBottom: 24, boxShadow: '0 1px 2px rgba(0,0,0,0.05)' }}>
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 16 }}>
<div style={{ flex: 1 }}>
<label style={{ display: 'block', fontSize: 13, color: 'rgba(0,0,0,0.65)', marginBottom: 6 }}></label>
<select value={filterType} onChange={e => { setFilterType(e.target.value); setCurrentPage(1) }} style={{ width: '100%', padding: '6px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}>
{typeOptions.map(t => <option key={t} value={t}>{t}</option>)}
</select>
</div>
<div style={{ flex: 1 }}>
<label style={{ display: 'block', fontSize: 13, color: 'rgba(0,0,0,0.65)', marginBottom: 6 }}></label>
<select value={filterStatus} onChange={e => { setFilterStatus(e.target.value); setCurrentPage(1) }} style={{ width: '100%', padding: '6px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}>
{statusOptions.map(s => <option key={s} value={s}>{s}</option>)}
</select>
</div>
<div style={{ flex: 1 }}>
<label style={{ display: 'block', fontSize: 13, color: 'rgba(0,0,0,0.65)', marginBottom: 6 }}></label>
<select value={filterCalib} onChange={e => { setFilterCalib(e.target.value); setCurrentPage(1) }} style={{ width: '100%', padding: '6px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}>
{calibStatusOptions.map(c => <option key={c} value={c}>{c}</option>)}
</select>
</div>
<div style={{ flex: 1 }}>
<label style={{ display: 'block', fontSize: 13, color: 'rgba(0,0,0,0.65)', marginBottom: 6 }}></label>
<input type="text" value={searchText} onChange={e => { setSearchText(e.target.value); setCurrentPage(1) }} placeholder="搜索SN或物料分类" style={{ width: '100%', padding: '6px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
</div>
</div>
</div>
{/* Table */}
<div style={{ backgroundColor: '#fff', borderRadius: 8, boxShadow: '0 1px 2px rgba(0,0,0,0.05)', overflow: 'hidden' }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ backgroundColor: '#FAFAFA' }}>
{['物料SN', '物料分类', '物料版本', '生产日期', '状态', '所属设备', '操作'].map(h => (
<th key={h} style={{ padding: '12px 14px', textAlign: 'left', fontSize: 13, fontWeight: 600, color: 'rgba(0,0,0,0.85)', borderBottom: '1px solid #F0F0F0', whiteSpace: 'nowrap' }}>{h}</th>
))}
</tr>
</thead>
<tbody>
{paged.map(row => (
<tr key={row.id} style={{ borderBottom: '1px solid #F0F0F0' }}>
<td style={{ padding: '12px 14px', fontSize: 13, fontWeight: 500 }}>{row.sn}</td>
<td style={{ padding: '12px 14px', fontSize: 13, color: 'rgba(0,0,0,0.65)' }}>{row.category || row.name}</td>
<td style={{ padding: '12px 14px', fontSize: 13, color: 'rgba(0,0,0,0.65)' }}>{row.version}</td>
<td style={{ padding: '12px 14px', fontSize: 13, color: 'rgba(0,0,0,0.65)' }}>{row.production_date}</td>
<td style={{ padding: '12px 14px' }}>
<span style={{ ...getStatusStyle(row.status), padding: '2px 8px', borderRadius: 4, fontSize: 12 }}>{row.status}</span>
</td>
<td style={{ padding: '12px 14px', fontSize: 13, color: row.device_sn === '-' ? 'rgba(0,0,0,0.25)' : '#4a7c59', fontWeight: row.device_sn === '-' ? 400 : 500 }}>{row.device_sn}</td>
<td style={{ padding: '12px 14px' }}>
<div style={{ display: 'flex', gap: 12 }}>
<button onClick={() => setDetailDrawer(row)} style={{ color: '#4a7c59', cursor: 'pointer', border: 'none', background: 'none', fontSize: 13, display: 'flex', alignItems: 'center', gap: 4 }}>
<Eye size={14} />
</button>
{row.type === '采集板' && (
<button onClick={() => setCalibFileDrawer(row)} style={{ color: '#4a7c59', cursor: 'pointer', border: 'none', background: 'none', fontSize: 13, display: 'flex', alignItems: 'center', gap: 4 }}>
<FileText size={14} />
</button>
)}
<button onClick={async () => { await fetch('/api/materials', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: row.id }) }); refetch() }} style={{ color: '#FF4D4F', cursor: 'pointer', border: 'none', background: 'none', fontSize: 13, display: 'flex', alignItems: 'center', gap: 4 }}>
<Trash2 size={14} />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
{/* Pagination */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px 16px', borderTop: '1px solid #F0F0F0' }}>
<span style={{ fontSize: 14, color: 'rgba(0,0,0,0.45)' }}>
{filtered.length > 0 ? (currentPage - 1) * pageSize + 1 : 0}-{Math.min(currentPage * pageSize, filtered.length)} / {filtered.length}
</span>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<button onClick={() => setCurrentPage(p => Math.max(1, p - 1))} disabled={currentPage === 1} style={{ padding: '4px 8px', border: '1px solid #D9D9D9', borderRadius: 4, backgroundColor: '#fff', cursor: currentPage === 1 ? 'not-allowed' : 'pointer', opacity: currentPage === 1 ? 0.4 : 1 }}><ChevronLeft size={16} /></button>
{Array.from({ length: totalPages }, (_, i) => (
<button key={i} onClick={() => setCurrentPage(i + 1)} style={{ width: 32, height: 32, borderRadius: 4, fontSize: 14, border: currentPage === i + 1 ? '1px solid #4a7c59' : '1px solid #D9D9D9', color: currentPage === i + 1 ? '#4a7c59' : 'rgba(0,0,0,0.65)', backgroundColor: currentPage === i + 1 ? '#eef5f0' : '#fff', cursor: 'pointer' }}>{i + 1}</button>
))}
<button onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))} disabled={currentPage === totalPages || totalPages === 0} style={{ padding: '4px 8px', border: '1px solid #D9D9D9', borderRadius: 4, backgroundColor: '#fff', cursor: currentPage === totalPages || totalPages === 0 ? 'not-allowed' : 'pointer', opacity: currentPage === totalPages || totalPages === 0 ? 0.4 : 1 }}><ChevronRight size={16} /></button>
</div>
</div>
</div>
{/* Detail Drawer */}
{detailDrawer && (
<div style={{ position: 'fixed', inset: 0, zIndex: 50 }}>
<div onClick={() => setDetailDrawer(null)} style={{ position: 'absolute', inset: 0, backgroundColor: 'rgba(0,0,0,0.45)' }} />
<div style={{ position: 'absolute', right: 0, top: 0, bottom: 0, width: 520, backgroundColor: '#fff', boxShadow: '-2px 0 8px rgba(0,0,0,0.15)', display: 'flex', flexDirection: 'column' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '16px 24px', borderBottom: '1px solid #F0F0F0' }}>
<h3 style={{ fontSize: 16, fontWeight: 600, margin: 0 }}></h3>
<button onClick={() => setDetailDrawer(null)} style={{ border: 'none', background: 'none', cursor: 'pointer', padding: 4 }}><X size={20} /></button>
</div>
<div style={{ flex: 1, overflow: 'auto', padding: 24 }}>
{/* 基本信息 */}
<div style={{ padding: 16, backgroundColor: '#FAFAFA', borderRadius: 8, marginBottom: 16, border: '1px solid #F0F0F0' }}>
<h4 style={{ fontSize: 14, fontWeight: 600, marginBottom: 12, marginTop: 0 }}></h4>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, fontSize: 13 }}>
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>SN</span>{detailDrawer.sn}</div>
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}></span>{detailDrawer.category || detailDrawer.name}</div>
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}></span>{detailDrawer.version}</div>
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}></span>{detailDrawer.production_date}</div>
<div>
<span style={{ color: 'rgba(0,0,0,0.45)' }}></span>
<span style={{ ...getStatusStyle(detailDrawer.status), padding: '1px 6px', borderRadius: 4, fontSize: 12 }}>{detailDrawer.status}</span>
</div>
{detailDrawer.description && <div><span style={{ color: 'rgba(0,0,0,0.45)' }}></span>{detailDrawer.description}</div>}
</div>
</div>
{/* 装配信息 */}
<div style={{ padding: 16, backgroundColor: '#FAFAFA', borderRadius: 8, marginBottom: 16, border: '1px solid #F0F0F0' }}>
<h4 style={{ fontSize: 14, fontWeight: 600, marginBottom: 12, marginTop: 0 }}></h4>
<div style={{ fontSize: 13 }}>
<span style={{ color: 'rgba(0,0,0,0.45)' }}></span>
{detailDrawer.device_sn === '-' ? (
<span style={{ color: 'rgba(0,0,0,0.25)' }}></span>
) : (
<span style={{ color: '#4a7c59', fontWeight: 500 }}>{detailDrawer.device_sn}</span>
)}
</div>
</div>
{/* 校准信息 */}
{detailDrawer.calib_status && detailDrawer.calib_status !== '-' && (
<div style={{ padding: 16, backgroundColor: '#FAFAFA', borderRadius: 8, marginBottom: 16, border: '1px solid #F0F0F0' }}>
<h4 style={{ fontSize: 14, fontWeight: 600, marginBottom: 12, marginTop: 0 }}></h4>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, fontSize: 13 }}>
<div>
<span style={{ color: 'rgba(0,0,0,0.45)' }}></span>
{detailDrawer.calib_status !== '-' ? (
<span style={{ ...getCalibStyle(detailDrawer.calib_status), padding: '1px 6px', borderRadius: 4, fontSize: 12 }}>{detailDrawer.calib_status}</span>
) : (
<span style={{ color: 'rgba(0,0,0,0.25)' }}>-</span>
)}
</div>
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}></span>{detailDrawer.calib_date}</div>
</div>
</div>
)}
</div>
<div style={{ padding: '16px 24px', borderTop: '1px solid #F0F0F0', display: 'flex', justifyContent: 'flex-end' }}>
<button onClick={() => setDetailDrawer(null)} style={{ padding: '8px 20px', border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer', fontSize: 14 }}></button>
</div>
</div>
</div>
)}
{/* Calibration File Drawer */}
{calibFileDrawer && (() => {
const files = calibrationFilesMap[calibFileDrawer.sn] || []
return (
<div style={{ position: 'fixed', inset: 0, zIndex: 50 }}>
<div onClick={() => setCalibFileDrawer(null)} style={{ position: 'absolute', inset: 0, backgroundColor: 'rgba(0,0,0,0.45)' }} />
<div style={{ position: 'absolute', right: 0, top: 0, bottom: 0, width: 640, backgroundColor: '#fff', boxShadow: '-2px 0 8px rgba(0,0,0,0.15)', display: 'flex', flexDirection: 'column' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '16px 24px', borderBottom: '1px solid #F0F0F0' }}>
<h3 style={{ fontSize: 16, fontWeight: 600, margin: 0 }}> - {calibFileDrawer.sn}</h3>
<button onClick={() => setCalibFileDrawer(null)} style={{ border: 'none', background: 'none', cursor: 'pointer', padding: 4 }}><X size={20} /></button>
</div>
<div style={{ flex: 1, overflow: 'auto', padding: 24 }}>
{files.length === 0 ? (
<div style={{ textAlign: 'center', padding: '48px 0', color: 'rgba(0,0,0,0.25)' }}>
<FileText size={40} style={{ marginBottom: 12, opacity: 0.3 }} />
<div style={{ fontSize: 14 }}></div>
</div>
) : (
files.map(file => (
<div key={file.id} style={{ marginBottom: 20 }}>
{/* 文件信息卡片 */}
<div style={{ padding: 16, backgroundColor: '#FAFAFA', borderRadius: 8, border: '1px solid #F0F0F0', marginBottom: 12 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<FileText size={16} style={{ color: '#4a7c59' }} />
<span style={{ fontSize: 14, fontWeight: 500 }}>{file.fileName}</span>
</div>
<button style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '4px 12px', border: '1px solid #D9D9D9', borderRadius: 4, backgroundColor: '#fff', cursor: 'pointer', fontSize: 12, color: '#4a7c59' }}>
<Download size={12} />
</button>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 8, fontSize: 12, color: 'rgba(0,0,0,0.65)' }}>
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}></span>{formatFileSize(file.fileSize)}</div>
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}>MD5</span><span style={{ fontFamily: 'monospace' }}>{file.md5}</span></div>
<div><span style={{ color: 'rgba(0,0,0,0.45)' }}></span>{file.uploadTime}</div>
</div>
</div>
{/* 校准数据内容 */}
<div style={{ borderRadius: 8, border: '1px solid #F0F0F0', overflow: 'hidden' }}>
<div style={{ padding: '10px 16px', backgroundColor: '#eef5f0', fontSize: 13, fontWeight: 600, color: '#4a7c59' }}></div>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ backgroundColor: '#FAFAFA' }}>
{['通道', '参考值', '测量值', '偏差(%)', '结果'].map(h => (
<th key={h} style={{ padding: '8px 12px', textAlign: 'left', fontSize: 12, fontWeight: 600, color: 'rgba(0,0,0,0.85)', borderBottom: '1px solid #F0F0F0' }}>{h}</th>
))}
</tr>
</thead>
<tbody>
{file.channels.map(ch => (
<tr key={ch.channel} style={{ borderBottom: '1px solid #F0F0F0' }}>
<td style={{ padding: '8px 12px', fontSize: 12, fontWeight: 500 }}>{ch.channel}</td>
<td style={{ padding: '8px 12px', fontSize: 12, fontFamily: 'monospace' }}>{ch.refValue.toFixed(1)}</td>
<td style={{ padding: '8px 12px', fontSize: 12, fontFamily: 'monospace' }}>{ch.measuredValue.toFixed(1)}</td>
<td style={{ padding: '8px 12px', fontSize: 12, fontFamily: 'monospace', color: Math.abs(ch.deviation) > 1 ? '#FF4D4F' : 'rgba(0,0,0,0.65)' }}>{ch.deviation > 0 ? '+' : ''}{ch.deviation.toFixed(2)}</td>
<td style={{ padding: '8px 12px' }}>
<span style={{ padding: '1px 6px', borderRadius: 4, fontSize: 11, ...(ch.result === '合格' ? { backgroundColor: '#F6FFED', color: '#52C41A', border: '1px solid #B7EB8F' } : { backgroundColor: '#FFF1F0', color: '#FF4D4F', border: '1px solid #FFCCC7' }) }}>{ch.result}</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
))
)}
</div>
<div style={{ padding: '16px 24px', borderTop: '1px solid #F0F0F0', display: 'flex', justifyContent: 'flex-end' }}>
<button onClick={() => setCalibFileDrawer(null)} style={{ padding: '8px 20px', border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer', fontSize: 14 }}></button>
</div>
</div>
</div>
)
})()}
</div>
)
}