"use client"; import { useEffect, useMemo, useRef, useState } from "react"; type Step = "home" | "identity" | "weigh" | "result" | "settings"; type Unit = "kg" | "g" | "lb"; type Material = { id: string; name: string; tone: string; icon: string; }; const 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 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); } 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(materials[0]); 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 [apiSettings, setApiSettings] = useState({ employee: "", machines: "", weight: "", upload: "", }); const stableSamples = useRef([]); const netWeight = Math.max(0, rawWeight - zeroOffset - tare); const displayedWeight = totalKg + netWeight; const filteredMachines = useMemo( () => machines.filter((item) => item.toLowerCase().includes(machineQuery.trim().toLowerCase()), ), [machineQuery], ); 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"); setMachine(""); setMachineQuery(""); setTotalKg(0); setRawWeight(0); setZeroOffset(0); setTare(0); setStable(false); stableSamples.current = []; } async function fetchIdentity() { setLoading(true); setNotice(""); try { if (apiSettings.employee) { const response = await fetch(apiSettings.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("当前使用模拟数据,接口接入后将自动切换"); } setStep("identity"); } catch { setNotice("暂时无法连接本地服务器,请点击“重新获取”"); setStep("identity"); } finally { setLoading(false); } } function enterAdmin() { if (password === "888888") { setPasswordOpen(false); setPassword(""); setPasswordError(""); setStep("settings"); } else { setPasswordError("密码不正确,演示密码为 888888"); } } async function submitWeight() { if (!stable) { setNotice("请等待重量稳定后再保存"); return; } setLoading(true); setNotice(""); const payload = { employeeName: employee.name, employeeId: employee.id, machineId: machine, materialType: material.name, materialTypeId: material.id, weightKg: Number(displayedWeight.toFixed(3)), createdAt: new Date().toISOString(), }; try { if (apiSettings.upload) { const response = await fetch(apiSettings.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)); } setStep("result"); } catch { 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},可继续放置下一批`); } 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" && (
安全 · 精准 · 可追溯

废料称重,
简单一点。

自动识别员工信息,稳定采样并上传记录。
请确认秤台处于空载状态后开始。

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}
{stable ? "重量已稳定" : "正在稳定采样"}
{(["kg", "g", "lb"] as Unit[]).map((item) => ( ))}
{formatWeight(displayedWeight, unit)}{unit}
净重 {formatWeight(netWeight, unit)} {unit} 累计 {formatWeight(totalKg, unit)} {unit} 皮重 {formatWeight(tare, unit)} {unit}
选择废料类型点击分类后保存称重记录必选
{materials.map((item) => ( ))}
{notice &&
ⓘ {notice}
}
)} {step === "result" && (
✓
称重完成

数据已保存

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

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

接口与设备设置

接口开发完成后,在此填入本地服务器地址。

{[ ["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]) => ( ))}
当前为空时,应用会使用内置模拟数据,便于先行演示与验收交互流程。
)}
沃尔核材 · 数字化生产设备编号 SCALE-01 · v1.0
{passwordOpen && (
setPasswordOpen(false)}>
event.stopPropagation()}> ⌁

管理员登录

请输入六位管理密码进入设置

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