380 lines
25 KiB
TypeScript
380 lines
25 KiB
TypeScript
'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>
|
||
)
|
||
}
|