"use client"; import { useEffect, useMemo, useRef, useState } from "react"; type Step = "home" | "identity" | "weigh" | "result" | "settings"; type Unit = "kg" | "g" | "lb"; type SettingsTab = "device" | "materials" | "peripherals" | "data" | "security" | "logs"; type Material = { id: string; name: string; tone: string; icon: string; }; type AppSettings = { employee: string; machines: string; weight: string; upload: string; deviceName: string; deviceId: string; imei: string; photoEnabled: boolean; printEnabled: boolean; }; type WeightRecord = { employeeName: string; employeeId: string; machineId: string; materialType: string; materialTypeId: string; weightKg: number; createdAt: string; }; type LogEntry = { id: string; time: string; level: "info" | "success" | "warning" | "error"; message: string; }; const DEFAULT_MATERIALS: Material[] = [ { id: "SCRAP_CU_01", name: "废铜", tone: "copper", icon: "Cu" }, { id: "SCRAP_WIRE_02", name: "废电线", tone: "wire", icon: "⚡" }, { id: "SCRAP_AL_03", name: "废铝", tone: "aluminum", icon: "Al" }, { id: "SCRAP_SN_04", name: "废锡", tone: "tin", icon: "Sn" }, ]; const DEFAULT_SETTINGS: AppSettings = { employee: "", machines: "", weight: "", upload: "", deviceName: "智能称重终端 01", deviceId: "SCALE-01", imei: "868120060012345", photoEnabled: false, printEnabled: false, }; const CONFIG_KEY = "smart-scale-config"; const BACKUP_KEY = "smart-scale-backup-records"; const PENDING_KEY = "smart-scale-pending-records"; const LOG_KEY = "smart-scale-logs"; const IMAGE_PATH = "/sdcard/smart/image/"; const CACHE_PATH = "/sdcard/smart/cache/"; const LOG_PATH = "/sdcard/smart/logs/"; const machines = ["A01 绕线机", "A02 押出机", "B03 裁线机", "B06 编织机", "C12 包装机"]; const unitFactors: Record = { kg: 1, g: 1000, lb: 2.20462262 }; function formatWeight(valueKg: number, unit: Unit) { const value = Math.max(0, valueKg) * unitFactors[unit]; const digits = unit === "g" ? 0 : 3; return value.toFixed(digits); } function createFileTimestamp(iso: string) { return iso.replace(/\D/g, "").slice(0, 14); } export default function Home() { const [step, setStep] = useState("home"); const [employee, setEmployee] = useState({ name: "", id: "" }); const [machineQuery, setMachineQuery] = useState(""); const [machine, setMachine] = useState(""); const [loading, setLoading] = useState(false); const [notice, setNotice] = useState(""); const [unit, setUnit] = useState("kg"); const [rawWeight, setRawWeight] = useState(0); const [zeroOffset, setZeroOffset] = useState(0); const [tare, setTare] = useState(0); const [totalKg, setTotalKg] = useState(0); const [material, setMaterial] = useState(null); const [stable, setStable] = useState(false); const [stableProgress, setStableProgress] = useState(0); const [countdown, setCountdown] = useState(10); const [passwordOpen, setPasswordOpen] = useState(false); const [password, setPassword] = useState(""); const [passwordError, setPasswordError] = useState(""); const [adminPassword, setAdminPassword] = useState("88888888"); const [appSettings, setAppSettings] = useState(DEFAULT_SETTINGS); const [materialOptions, setMaterialOptions] = useState(DEFAULT_MATERIALS); const [settingsTab, setSettingsTab] = useState("device"); const [settingsMessage, setSettingsMessage] = useState(""); const [settingsBusy, setSettingsBusy] = useState(false); const [passwordForm, setPasswordForm] = useState({ current: "", next: "", confirm: "" }); const [pendingRecords, setPendingRecords] = useState([]); const [backupRecords, setBackupRecords] = useState([]); const [lastRecord, setLastRecord] = useState(null); const [logs, setLogs] = useState([ { id: "boot", time: new Date().toISOString(), level: "success", message: "应用启动完成,称重设备连接正常", }, ]); const stableSamples = useRef([]); const netWeight = Math.max(0, rawWeight - zeroOffset - tare); const displayedWeight = totalKg + netWeight; const selectedMaterial = material ?? materialOptions[0]; const filteredMachines = useMemo( () => machines.filter((item) => item.toLowerCase().includes(machineQuery.trim().toLowerCase()), ), [machineQuery], ); useEffect(() => { try { const savedConfig = window.localStorage.getItem(CONFIG_KEY); if (savedConfig) { const parsed = JSON.parse(savedConfig); if (parsed.settings) setAppSettings({ ...DEFAULT_SETTINGS, ...parsed.settings }); if (Array.isArray(parsed.materials) && parsed.materials.length === 4) setMaterialOptions(parsed.materials); if (typeof parsed.password === "string") setAdminPassword(parsed.password); } const savedPending = window.localStorage.getItem(PENDING_KEY); const savedBackup = window.localStorage.getItem(BACKUP_KEY); const savedLogs = window.localStorage.getItem(LOG_KEY); if (savedPending) setPendingRecords(JSON.parse(savedPending)); if (savedBackup) setBackupRecords(JSON.parse(savedBackup)); if (savedLogs) setLogs(JSON.parse(savedLogs)); } catch { addLog("本地配置读取失败,已使用默认设置", "warning"); } }, []); useEffect(() => { if (step !== "weigh") return; const start = Date.now(); const timer = window.setInterval(() => { const elapsed = (Date.now() - start) / 1000; const target = elapsed < 1.4 ? 0 : 2.486; const movement = elapsed < 3 ? Math.sin(elapsed * 9) * 0.085 : Math.sin(elapsed * 3) * 0.0012; setRawWeight(Math.max(0, target + movement)); }, 180); return () => window.clearInterval(timer); }, [step]); useEffect(() => { if (step !== "weigh") return; stableSamples.current = [...stableSamples.current.slice(-7), netWeight]; const samples = stableSamples.current; if (samples.length < 6 || netWeight < 0.01) { setStable(false); setStableProgress(Math.min(85, samples.length * 14)); return; } const spread = Math.max(...samples) - Math.min(...samples); const isStable = spread < 0.008; setStable(isStable); setStableProgress(isStable ? 100 : Math.max(25, 90 - spread * 500)); }, [netWeight, step]); useEffect(() => { if (step !== "result") return; setCountdown(10); const timer = window.setInterval(() => { setCountdown((value) => { if (value <= 1) { window.clearInterval(timer); resetToHome(); return 0; } return value - 1; }); }, 1000); return () => window.clearInterval(timer); }, [step]); function resetToHome() { setStep("home"); setMaterial(null); setMachine(""); setMachineQuery(""); setTotalKg(0); setRawWeight(0); setZeroOffset(0); setTare(0); setStable(false); stableSamples.current = []; } function addLog(message: string, level: LogEntry["level"] = "info") { const entry: LogEntry = { id: `${Date.now()}-${Math.random().toString(16).slice(2)}`, time: new Date().toISOString(), level, message, }; setLogs((current) => { const next = [entry, ...current].slice(0, 100); try { window.localStorage.setItem(LOG_KEY, JSON.stringify(next)); } catch { // Native Android builds write to LOG_PATH through the reserved bridge. } return next; }); } function persistConfiguration(nextPassword = adminPassword) { window.localStorage.setItem( CONFIG_KEY, JSON.stringify({ settings: appSettings, materials: materialOptions, password: nextPassword, }), ); addLog("管理员设置已保存", "success"); setSettingsMessage("全部设置已保存,本机配置已更新"); } function updateMaterial(index: number, field: "name" | "id", value: string) { setMaterialOptions((current) => current.map((item, itemIndex) => (itemIndex === index ? { ...item, [field]: value } : item)), ); setSettingsMessage(""); } async function fetchIdentity(chosenMaterial?: Material) { const activeMaterial = chosenMaterial ?? material; if (!activeMaterial) { setNotice("请先选择本次称重的废料类型"); return; } if (chosenMaterial) setMaterial(chosenMaterial); setLoading(true); setNotice(""); try { if (appSettings.employee) { const response = await fetch(appSettings.employee); if (!response.ok) throw new Error("接口不可用"); const data = await response.json(); setEmployee({ name: data.name, id: data.employeeId }); } else { await new Promise((resolve) => window.setTimeout(resolve, 650)); setEmployee({ name: "张伟", id: "E-0826" }); setNotice("当前使用模拟数据,接口接入后将自动切换"); } addLog(`人员信息获取成功:${employee.name || "张伟"}`, "success"); setStep("identity"); } catch { addLog("门禁人员信息获取失败", "error"); setNotice("暂时无法连接本地服务器,请点击“重新获取”"); setStep("identity"); } finally { setLoading(false); } } function enterAdmin() { if (password === adminPassword) { setPasswordOpen(false); setPassword(""); setPasswordError(""); setSettingsMessage(""); addLog("管理员登录成功", "success"); setStep("settings"); } else { addLog("管理员登录失败:密码错误", "warning"); setPasswordError("密码不正确,默认密码为 88888888"); } } function backupRecord(record: WeightRecord) { setBackupRecords((current) => { const next = [...current, record].slice(-500); window.localStorage.setItem(BACKUP_KEY, JSON.stringify(next)); return next; }); addLog(`称重数据已备份至 ${CACHE_PATH}`, "success"); } function queuePendingRecord(record: WeightRecord) { setPendingRecords((current) => { const next = [...current, record]; window.localStorage.setItem(PENDING_KEY, JSON.stringify(next)); return next; }); addLog("上传失败,数据已加入补传队列", "warning"); } function runCompletionHooks(record: WeightRecord) { if (appSettings.photoEnabled) { const filename = `${createFileTimestamp(record.createdAt)}.jpg`; addLog(`USB 摄像头拍照调用:${IMAGE_PATH}${filename}`, "success"); } if (appSettings.printEnabled) { addLog( `打印任务已发送:员工 ${record.employeeName},工号 ${record.employeeId},机台 ${record.machineId},废料 ${record.materialType},重量 ${record.weightKg.toFixed(3)} kg,时间 ${new Date(record.createdAt).toLocaleString("zh-CN", { hour12: false })}`, "success", ); } } async function submitWeight() { if (!stable) { setNotice("请等待重量稳定后再保存"); return; } setLoading(true); setNotice(""); const payload = { employeeName: employee.name, employeeId: employee.id, machineId: machine, materialType: selectedMaterial.name, materialTypeId: selectedMaterial.id, weightKg: Number(displayedWeight.toFixed(3)), createdAt: new Date().toISOString(), }; setLastRecord(payload); backupRecord(payload); try { if (appSettings.upload) { const response = await fetch(appSettings.upload, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (!response.ok) throw new Error("上传失败"); } else { await new Promise((resolve) => window.setTimeout(resolve, 700)); } addLog(`称重数据上传成功:${payload.weightKg.toFixed(3)} kg`, "success"); runCompletionHooks(payload); setStep("result"); } catch { queuePendingRecord(payload); setNotice("数据上传失败,请检查服务器连接后重试"); } finally { setLoading(false); } } function addToTotal() { if (!stable || netWeight <= 0) { setNotice("重量稳定后才能累计"); return; } setTotalKg((value) => value + netWeight); setZeroOffset(rawWeight); setTare(0); stableSamples.current = []; setNotice(`已累计 ${formatWeight(netWeight, unit)} ${unit},可继续放置下一批`); } async function retryPendingUploads() { if (!pendingRecords.length) { setSettingsMessage("当前没有需要补传的数据"); addLog("手动检查补传队列:无待补传数据"); return; } setSettingsBusy(true); setSettingsMessage(""); try { if (appSettings.upload) { for (const record of pendingRecords) { const response = await fetch(appSettings.upload, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(record), }); if (!response.ok) throw new Error("补传失败"); } } else { await new Promise((resolve) => window.setTimeout(resolve, 900)); } const count = pendingRecords.length; setPendingRecords([]); window.localStorage.setItem(PENDING_KEY, "[]"); setSettingsMessage(`补传完成,共成功上传 ${count} 条数据`); addLog(`数据补传完成:${count} 条`, "success"); } catch { setSettingsMessage("补传失败,数据仍保留在本地队列"); addLog("数据补传失败,请检查服务器接口", "error"); } finally { setSettingsBusy(false); } } function changeAdminPassword() { if (passwordForm.current !== adminPassword) { setSettingsMessage("原密码不正确"); return; } if (passwordForm.next.length < 8) { setSettingsMessage("新密码至少需要 8 位"); return; } if (passwordForm.next !== passwordForm.confirm) { setSettingsMessage("两次输入的新密码不一致"); return; } setAdminPassword(passwordForm.next); persistConfiguration(passwordForm.next); setPasswordForm({ current: "", next: "", confirm: "" }); setSettingsMessage("管理员密码已修改,下次登录时生效"); addLog("管理员密码已修改", "success"); } function exportLogs() { const content = logs .map((entry) => `[${new Date(entry.time).toLocaleString("zh-CN", { hour12: false })}] [${entry.level.toUpperCase()}] ${entry.message}`) .join("\n"); const blob = new Blob([content], { type: "text/plain;charset=utf-8" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = `smart-scale-${createFileTimestamp(new Date().toISOString())}.log`; link.click(); URL.revokeObjectURL(url); addLog(`日志已导出,目标目录 ${LOG_PATH}`, "success"); setSettingsMessage(`日志已导出,Android 设备保存至 ${LOG_PATH}`); } return (
设备在线
{step !== "home" && step !== "settings" && (
{["人员确认", "智能称重", "结果上传"].map((label, index) => { const activeIndex = step === "identity" ? 0 : step === "weigh" ? 1 : 2; return (
{index < activeIndex ? "✓" : index + 1} {label}
); })}
)}
{step === "home" && (
安全 · 精准 · 可追溯

废料称重,
简单一点。

选择废料类型后开始,系统将自动识别员工信息,
稳定采样并上传称重记录。

{loading ? "···" : "1"} {loading ? `正在读取${material?.name ?? ""}称重信息…` : "点击右侧废料类型,即刻开始"}
{notice &&
ⓘ {notice}
}
0.001 kg显示精度 连续采样稳定判断 4 种废料分类
)} {step === "identity" && (
01 · 人员与机台

确认本次称重信息

人员信息由门禁机自动获取,请选择对应生产机台。

{employee.name ? employee.name.slice(-1) : "?"}
当前操作员工

{employee.name || "未获取"}

工号 · {employee.id || "—"}

{employee.name ? "✓ 已验证" : "! 待获取"}
⌕ { setMachineQuery(event.target.value); setMachine(""); }} placeholder="输入机台号或名称,如 A01" autoComplete="off" /> {machine && ✓}
{machineQuery && !machine && (
{filteredMachines.length ? filteredMachines.map((item) => ( )) :

没有匹配机台,可点击“重新获取”同步最新数据

}
)} {notice &&
ⓘ {notice}
}
)} {step === "weigh" && (
员工{employee.name} · {employee.id} 机台{machine} 废料类型{selectedMaterial.name} · {selectedMaterial.id}
{stable ? "重量已稳定" : "正在稳定采样"}
{(["kg", "g", "lb"] as Unit[]).map((item) => ( ))}
{formatWeight(displayedWeight, unit)}{unit}
净重 {formatWeight(netWeight, unit)} {unit} 累计 {formatWeight(totalKg, unit)} {unit} 皮重 {formatWeight(tare, unit)} {unit}
{selectedMaterial.icon} 本次废料类型 {selectedMaterial.name} TYPE ID · {selectedMaterial.id} ✓ 已在首页确认
{notice &&
ⓘ {notice}
}
)} {step === "result" && (
✓
称重完成

数据已保存

本次称重记录已进入上传队列

称重凭证{new Date(lastRecord?.createdAt ?? new Date()).toLocaleString("zh-CN", { hour12: false })}
{selectedMaterial.name}{formatWeight(displayedWeight, unit)} {unit} {selectedMaterial.icon}
员工姓名
{employee.name}
员工工号
{employee.id}
生产机台
{machine}
废料类型 ID
{selectedMaterial.id}
上传状态
● 已保存
{countdown} 秒后自动返回首页
)} {step === "settings" && (
管理员中心

应用与设备设置

管理设备、废料字典、外设、数据安全与运行日志。

{settingsTab === "device" && ( <>
设备信息用于标识当前智能称重终端 本机
本地服务器接口留空时自动使用演示数据
{[ ["employee", "人员信息接口", "http://192.168.1.20/api/employee"], ["machines", "机台列表接口", "http://192.168.1.20/api/machines"], ["weight", "电子秤数据接口", "ws://192.168.1.21/weight"], ["upload", "称重上传接口", "http://192.168.1.20/api/records"], ].map(([key, label, placeholder]) => ( ))}
)} {settingsTab === "materials" && ( <>
废料字典修改后将同步更新首页分类与称重数据 4 项
{materialOptions.map((item, index) => (
{item.icon} 0{index + 1}
))}
)} {settingsTab === "peripherals" && ( <>
完成称重后的外设动作功能默认关闭,需由 Android 原生层接入 USB 设备 预留接口
◎ USB 摄像头拍照提交成功后自动拍摄现场照片
照片保存路径{IMAGE_PATH}文件名:YYYYMMDDHHmmss.jpg
▤ 称重凭证打印提交成功后调用 USB / 蓝牙打印机
{["员工名称", "员工工号", "机台号", "废料类型", "废料重量", "称重时间"].map((item) => ✓ {item})}
)} {settingsTab === "data" && ( <>
数据补传与本地备份网络异常时确保称重记录不丢失 自动保护
↥待补传数据{pendingRecords.length}条记录
▣本地备份{backupRecords.length}条记录
本地备份路径{CACHE_PATH}称重提交时自动写入
补传成功后自动清理待传队列,本地备份仍会保留。
)} {settingsTab === "security" && ( <>
管理员密码默认密码为 88888888,建议部署后立即修改 8–12 位
)} {settingsTab === "logs" && ( <>
实时运行日志最多保留最近 100 条运行记录
Android 日志保存路径{LOG_PATH}按日期滚动保存
{logs.map((entry) => (
{entry.level === "success" ? "成功" : entry.level === "warning" ? "警告" : entry.level === "error" ? "错误" : "信息"}

{entry.message}

))}
)}
{settingsMessage || "修改完成后请保存设置"}
)}
沃尔核材 · 数字化生产{appSettings.deviceName} · {appSettings.deviceId} · v1.1
{passwordOpen && (
setPasswordOpen(false)}>
event.stopPropagation()}> ⌁

管理员登录

请输入管理员密码进入应用设置
初始密码为 88888888

setPassword(event.target.value.replace(/\D/g, ""))} onKeyDown={(event) => event.key === "Enter" && enterAdmin()} placeholder="••••••••" aria-label="管理员密码" /> {passwordError && {passwordError}}
)}
); }