331 lines
18 KiB
TypeScript
331 lines
18 KiB
TypeScript
'use client'
|
||
import { useState } from 'react'
|
||
import { useRouter } from 'next/navigation'
|
||
import { useRef } from 'react'
|
||
import { ArrowLeft, Plus, Trash2, Upload, Info, CheckCircle, FileText, X, ScanLine } from 'lucide-react'
|
||
|
||
/** 板卡类型 -> 可选版本 */
|
||
const versionsByType: Record<string, { version: string; firmware: string }[]> = {
|
||
'主协板': [
|
||
{ version: 'MB-V1.2', firmware: 'v2.1' },
|
||
{ version: 'MB-V2.1', firmware: 'v1.8' },
|
||
],
|
||
'采集板': [
|
||
{ version: 'RX-V1.3', firmware: 'v3.0' },
|
||
{ version: 'RX-V2.1', firmware: 'v2.5' },
|
||
],
|
||
'发射板': [
|
||
{ version: 'TX-V1.5', firmware: 'v1.2' },
|
||
{ version: 'TX-V2.1', firmware: 'v1.0' },
|
||
],
|
||
'升压板': [
|
||
{ version: 'BO-V2.1', firmware: 'v1.1' },
|
||
{ version: 'BO-V2.2', firmware: 'v0.9' },
|
||
],
|
||
'电缆头': [
|
||
{ version: 'CBH-V1.0', firmware: '-' },
|
||
],
|
||
'电缆': [
|
||
{ version: 'CBL-30M', firmware: '-' },
|
||
{ version: 'CBL-60M', firmware: '-' },
|
||
{ version: 'CBL-100M', firmware: '-' },
|
||
],
|
||
'机箱': [
|
||
{ version: 'GD30-CASE-A', firmware: '-' },
|
||
{ version: 'GD30-CASE-B', firmware: '-' },
|
||
{ version: 'GM10-CASE-A', firmware: '-' },
|
||
],
|
||
'电源': [
|
||
{ version: 'BP150-V1.0', firmware: '-' },
|
||
{ version: 'BP300-V1.0', firmware: '-' },
|
||
{ version: 'BP600-V1.0', firmware: '-' },
|
||
{ version: 'BP600-V2.0', firmware: '-' },
|
||
],
|
||
}
|
||
|
||
const typeOptions = Object.keys(versionsByType)
|
||
|
||
interface BoardEntry {
|
||
id: number
|
||
type: string
|
||
version: string
|
||
firmware: string
|
||
sn: string
|
||
productionDate: string
|
||
remark: string
|
||
calibFile: File | null
|
||
}
|
||
|
||
let nextId = 1
|
||
|
||
export default function BoardRegisterPage() {
|
||
const router = useRouter()
|
||
const [entries, setEntries] = useState<BoardEntry[]>([createEntry()])
|
||
const [batchMode, setBatchMode] = useState(false)
|
||
|
||
function createEntry(): BoardEntry {
|
||
return { id: nextId++, type: '采集板', version: 'RX-V2.1', firmware: 'v2.1', sn: '', productionDate: '', remark: '', calibFile: null }
|
||
}
|
||
|
||
const addEntry = () => {
|
||
setEntries(prev => [...prev, createEntry()])
|
||
}
|
||
|
||
const removeEntry = (id: number) => {
|
||
if (entries.length <= 1) return
|
||
setEntries(prev => prev.filter(e => e.id !== id))
|
||
}
|
||
|
||
const updateEntry = (id: number, field: keyof BoardEntry, value: string) => {
|
||
setEntries(prev => prev.map(e => {
|
||
if (e.id !== id) return e
|
||
const updated = { ...e, [field]: value }
|
||
// 切换类型时自动选第一个版本和固件
|
||
if (field === 'type') {
|
||
const versions = versionsByType[value]
|
||
if (versions && versions.length > 0) {
|
||
updated.version = versions[0].version
|
||
updated.firmware = versions[0].firmware
|
||
}
|
||
// 切换到非采集板时清除校准文件
|
||
if (value !== '采集板') updated.calibFile = null
|
||
}
|
||
// 切换版本时自动填充固件
|
||
if (field === 'version') {
|
||
const versions = versionsByType[updated.type]
|
||
const match = versions?.find(m => m.version === value)
|
||
if (match) updated.firmware = match.firmware
|
||
}
|
||
return updated
|
||
}))
|
||
}
|
||
|
||
const fileInputRefs = useRef<Record<number, HTMLInputElement | null>>({})
|
||
|
||
const handleCalibFileChange = (entryId: number, file: File | null) => {
|
||
setEntries(prev => prev.map(e => e.id === entryId ? { ...e, calibFile: file } : e))
|
||
}
|
||
|
||
const formatFileSize = (bytes: number): string => {
|
||
if (bytes < 1024) return bytes + ' B'
|
||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
|
||
return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
|
||
}
|
||
|
||
const isValid = entries.every(e => e.sn.trim() && e.productionDate)
|
||
|
||
return (
|
||
<div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
|
||
<div style={{ flex: 1, padding: 24, paddingBottom: 80 }}>
|
||
{/* Header */}
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 24 }}>
|
||
<button onClick={() => router.push('/materials')} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: 32, height: 32, border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer' }}>
|
||
<ArrowLeft size={16} />
|
||
</button>
|
||
<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>
|
||
|
||
{/* Info Banner */}
|
||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 12, padding: 16, backgroundColor: '#eef5f0', borderRadius: 8, marginBottom: 24, border: '1px solid #a3c4ad' }}>
|
||
<Info size={18} style={{ color: '#4a7c59', flexShrink: 0, marginTop: 2 }} />
|
||
<div style={{ fontSize: 14, color: '#4a7c59', lineHeight: 1.6 }}>
|
||
物料SN号为唯一标识,请确保录入正确。采集板登记后需要进行校准才能用于装配。选择版本后固件版本会自动填充。
|
||
</div>
|
||
</div>
|
||
|
||
{/* Mode Toggle */}
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 24 }}>
|
||
<span style={{ fontSize: 14, color: 'rgba(0,0,0,0.65)' }}>登记模式:</span>
|
||
<div style={{ display: 'flex', gap: 0, borderRadius: 6, overflow: 'hidden', border: '1px solid #D9D9D9' }}>
|
||
<button onClick={() => setBatchMode(false)} style={{ padding: '6px 16px', fontSize: 13, border: 'none', cursor: 'pointer', backgroundColor: !batchMode ? '#4a7c59' : '#fff', color: !batchMode ? '#fff' : 'rgba(0,0,0,0.65)' }}>单个登记</button>
|
||
<button onClick={() => setBatchMode(true)} style={{ padding: '6px 16px', fontSize: 13, border: 'none', cursor: 'pointer', backgroundColor: batchMode ? '#4a7c59' : '#fff', color: batchMode ? '#fff' : 'rgba(0,0,0,0.65)', borderLeft: '1px solid #D9D9D9' }}>批量登记</button>
|
||
</div>
|
||
{batchMode && (
|
||
<span style={{ fontSize: 12, color: 'rgba(0,0,0,0.45)' }}>当前 {entries.length} 条</span>
|
||
)}
|
||
</div>
|
||
|
||
{/* Entry Cards */}
|
||
{entries.map((entry, idx) => (
|
||
<div key={entry.id} style={{ backgroundColor: '#fff', borderRadius: 8, padding: 24, marginBottom: 16, boxShadow: '0 1px 2px rgba(0,0,0,0.05)', border: '1px solid #F0F0F0' }}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }}>
|
||
<h3 style={{ fontSize: 15, fontWeight: 600, margin: 0, color: 'rgba(0,0,0,0.85)' }}>
|
||
{batchMode ? `物料 #${idx + 1}` : '物料信息'}
|
||
</h3>
|
||
{batchMode && entries.length > 1 && (
|
||
<button onClick={() => removeEntry(entry.id)} style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '4px 12px', border: '1px solid #FFCCC7', borderRadius: 6, backgroundColor: '#FFF1F0', color: '#FF4D4F', cursor: 'pointer', fontSize: 13 }}>
|
||
<Trash2 size={13} />移除
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 20 }}>
|
||
{/* 板卡类型 */}
|
||
<div>
|
||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}><span style={{ color: '#FF4D4F' }}>*</span> 物料类型</label>
|
||
<select value={entry.type} onChange={e => updateEntry(entry.id, 'type', e.target.value)} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}>
|
||
{typeOptions.map(t => <option key={t} value={t}>{t}</option>)}
|
||
</select>
|
||
</div>
|
||
|
||
{/* 板卡版本 */}
|
||
<div>
|
||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}><span style={{ color: '#FF4D4F' }}>*</span> 物料版本</label>
|
||
<select value={entry.version} onChange={e => updateEntry(entry.id, 'version', e.target.value)} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14 }}>
|
||
{(versionsByType[entry.type] || []).map(m => <option key={m.version} value={m.version}>{m.version}</option>)}
|
||
</select>
|
||
</div>
|
||
|
||
{/* 固件版本(自动填充,只读) */}
|
||
<div>
|
||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}>固件版本</label>
|
||
<input value={entry.firmware} readOnly style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, backgroundColor: '#FAFAFA', color: 'rgba(0,0,0,0.65)', boxSizing: 'border-box' }} />
|
||
</div>
|
||
|
||
{/* 物料SN */}
|
||
<div>
|
||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}><span style={{ color: '#FF4D4F' }}>*</span> 物料SN号</label>
|
||
<div style={{ display: 'flex', gap: 6 }}>
|
||
<input value={entry.sn} onChange={e => updateEntry(entry.id, 'sn', e.target.value)} placeholder={`扫码或手动输入 如 ${entry.version}-20250401001`} style={{ flex: 1, padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||
<button title="扫码录入SN" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: 36, height: 36, border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer', flexShrink: 0 }}>
|
||
<ScanLine size={16} style={{ color: '#4a7c59' }} />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 生产日期 */}
|
||
<div>
|
||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}><span style={{ color: '#FF4D4F' }}>*</span> 生产日期</label>
|
||
<input type="date" value={entry.productionDate} onChange={e => updateEntry(entry.id, 'productionDate', e.target.value)} style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||
</div>
|
||
|
||
{/* 备注 */}
|
||
<div>
|
||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 6 }}>备注</label>
|
||
<input value={entry.remark} onChange={e => updateEntry(entry.id, 'remark', e.target.value)} placeholder="可选" style={{ width: '100%', padding: '8px 12px', border: '1px solid #D9D9D9', borderRadius: 6, fontSize: 14, boxSizing: 'border-box' }} />
|
||
</div>
|
||
</div>
|
||
|
||
{/* 采集板校准提示 */}
|
||
{entry.type === '采集板' && (
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 16, padding: '8px 12px', backgroundColor: '#FFFBE6', borderRadius: 6, border: '1px solid #FFE58F' }}>
|
||
<Info size={14} style={{ color: '#FAAD14', flexShrink: 0 }} />
|
||
<span style={{ fontSize: 12, color: 'rgba(0,0,0,0.65)' }}>采集板登记后状态为"待校准",需完成校准后才能用于设备装配。</span>
|
||
</div>
|
||
)}
|
||
|
||
{/* 采集板校准文件导入 */}
|
||
{entry.type === '采集板' && (
|
||
<div style={{ marginTop: 16 }}>
|
||
<label style={{ display: 'block', fontSize: 13, fontWeight: 500, marginBottom: 8 }}>校准文件(可选)</label>
|
||
<input
|
||
type="file"
|
||
accept=".csv,.xlsx,.xls,.dat"
|
||
ref={el => { fileInputRefs.current[entry.id] = el }}
|
||
onChange={e => handleCalibFileChange(entry.id, e.target.files?.[0] || null)}
|
||
style={{ display: 'none' }}
|
||
/>
|
||
{entry.calibFile ? (
|
||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 14px', backgroundColor: '#eef5f0', borderRadius: 6, border: '1px solid #a3c4ad' }}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||
<FileText size={16} style={{ color: '#4a7c59', flexShrink: 0 }} />
|
||
<div>
|
||
<div style={{ fontSize: 13, fontWeight: 500, color: 'rgba(0,0,0,0.85)' }}>{entry.calibFile.name}</div>
|
||
<div style={{ fontSize: 12, color: 'rgba(0,0,0,0.45)' }}>{formatFileSize(entry.calibFile.size)}</div>
|
||
</div>
|
||
</div>
|
||
<button onClick={() => { handleCalibFileChange(entry.id, null); if (fileInputRefs.current[entry.id]) fileInputRefs.current[entry.id]!.value = '' }} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: 24, height: 24, border: 'none', background: 'none', cursor: 'pointer', color: 'rgba(0,0,0,0.45)', borderRadius: 4 }}>
|
||
<X size={14} />
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<button onClick={() => fileInputRefs.current[entry.id]?.click()} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8, width: '100%', padding: '14px 0', border: '2px dashed #D9D9D9', borderRadius: 6, backgroundColor: '#FAFAFA', cursor: 'pointer', fontSize: 13, color: 'rgba(0,0,0,0.45)' }}>
|
||
<Upload size={16} />导入校准文件(支持 .csv / .xlsx / .dat)
|
||
</button>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
))}
|
||
|
||
{/* Add More Button (batch mode) */}
|
||
{batchMode && (
|
||
<button onClick={addEntry} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6, width: '100%', padding: '12px 0', border: '2px dashed #D9D9D9', borderRadius: 8, backgroundColor: '#FAFAFA', cursor: 'pointer', fontSize: 14, color: 'rgba(0,0,0,0.45)', marginBottom: 16 }}>
|
||
<Plus size={16} />添加一条物料
|
||
</button>
|
||
)}
|
||
|
||
{/* Preview Summary */}
|
||
{entries.length > 0 && (
|
||
<div style={{ backgroundColor: '#fff', borderRadius: 8, padding: 20, boxShadow: '0 1px 2px rgba(0,0,0,0.05)', border: '1px solid #F0F0F0' }}>
|
||
<h3 style={{ fontSize: 15, fontWeight: 600, margin: '0 0 16px', color: 'rgba(0,0,0,0.85)' }}>登记预览</h3>
|
||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||
<thead>
|
||
<tr style={{ backgroundColor: '#FAFAFA' }}>
|
||
{['序号', '类型', '版本', '固件', 'SN号', '生产日期', '校准文件', '状态'].map(h => (
|
||
<th key={h} style={{ padding: '10px 14px', textAlign: 'left', fontSize: 13, fontWeight: 600, color: 'rgba(0,0,0,0.85)', borderBottom: '1px solid #F0F0F0' }}>{h}</th>
|
||
))}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{entries.map((entry, i) => (
|
||
<tr key={entry.id} style={{ borderBottom: '1px solid #F0F0F0' }}>
|
||
<td style={{ padding: '10px 14px', fontSize: 13, color: 'rgba(0,0,0,0.45)' }}>{i + 1}</td>
|
||
<td style={{ padding: '10px 14px', fontSize: 13 }}>{entry.type}</td>
|
||
<td style={{ padding: '10px 14px', fontSize: 13 }}>{entry.version}</td>
|
||
<td style={{ padding: '10px 14px', fontSize: 13, color: 'rgba(0,0,0,0.65)' }}>{entry.firmware}</td>
|
||
<td style={{ padding: '10px 14px', fontSize: 13, fontWeight: 500, color: entry.sn ? 'rgba(0,0,0,0.85)' : 'rgba(0,0,0,0.25)' }}>{entry.sn || '未填写'}</td>
|
||
<td style={{ padding: '10px 14px', fontSize: 13, color: entry.productionDate ? 'rgba(0,0,0,0.65)' : 'rgba(0,0,0,0.25)' }}>{entry.productionDate || '未填写'}</td>
|
||
<td style={{ padding: '10px 14px', fontSize: 13 }}>
|
||
{entry.type === '采集板' ? (
|
||
entry.calibFile ? (
|
||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4, color: '#4a7c59', fontSize: 12 }}>
|
||
<FileText size={12} />{entry.calibFile.name}
|
||
</span>
|
||
) : (
|
||
<span style={{ fontSize: 12, color: 'rgba(0,0,0,0.25)' }}>未导入</span>
|
||
)
|
||
) : (
|
||
<span style={{ fontSize: 12, color: 'rgba(0,0,0,0.25)' }}>-</span>
|
||
)}
|
||
</td>
|
||
<td style={{ padding: '10px 14px' }}>
|
||
<span style={{ padding: '2px 8px', borderRadius: 4, fontSize: 12, ...(entry.type === '采集板' ? { backgroundColor: '#FFFBE6', color: '#FAAD14', border: '1px solid #FFE58F' } : { backgroundColor: '#E6F7FF', color: '#1890FF', border: '1px solid #91D5FF' }) }}>
|
||
{entry.type === '采集板' ? '待校准' : '在库'}
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Sticky Bottom Bar */}
|
||
<div style={{ position: 'sticky', bottom: 0, backgroundColor: '#fff', borderTop: '1px solid #F0F0F0', padding: '12px 24px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', zIndex: 10 }}>
|
||
<span style={{ fontSize: 13, color: 'rgba(0,0,0,0.45)' }}>
|
||
共 {entries.length} 条物料待登记
|
||
{entries.some(e => e.type === '采集板') && <span style={{ color: '#FAAD14' }}> · 含采集板需后续校准</span>}
|
||
{entries.some(e => e.type === '采集板' && e.calibFile) && <span style={{ color: '#4a7c59' }}> · {entries.filter(e => e.calibFile).length} 个已附校准文件</span>}
|
||
</span>
|
||
<div style={{ display: 'flex', gap: 12 }}>
|
||
<button onClick={() => router.push('/materials')} style={{ padding: '8px 24px', border: '1px solid #D9D9D9', borderRadius: 6, backgroundColor: '#fff', cursor: 'pointer', fontSize: 14 }}>取消</button>
|
||
<button
|
||
onClick={() => router.push('/materials')}
|
||
disabled={!isValid}
|
||
style={{ padding: '8px 24px', border: 'none', borderRadius: 6, backgroundColor: isValid ? '#4a7c59' : '#D9D9D9', color: '#fff', cursor: isValid ? 'pointer' : 'not-allowed', fontSize: 14 }}
|
||
>
|
||
<span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||
<CheckCircle size={14} />确认登记
|
||
</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|