diff --git a/app/globals.css b/app/globals.css index 3b311e4..03c0c32 100644 --- a/app/globals.css +++ b/app/globals.css @@ -190,14 +190,89 @@ h1 em { font-style: normal; color: var(--green); } .countdown { position: relative; margin-top: 10px; color: #8b9690; font-size: 9px; overflow: hidden; } .countdown span { position: absolute; left: 0; bottom: -4px; height: 2px; background: var(--green); transition: width 1s linear; } -.settings-card { background: white; border: 1px solid var(--line); border-radius: 15px; padding: 8px 18px; } -.settings-card label { display: grid; grid-template-columns: 160px 1fr; align-items: center; gap: 18px; padding: 13px 0; border-bottom: 1px solid var(--line); } -.settings-card label:last-child { border: 0; } -.settings-card label > span { font-size: 11px; font-weight: 800; } -.settings-card label small { display: block; color: #ac8154; font-size: 8px; margin-top: 4px; font-weight: 400; } -.settings-card input { width: 100%; border: 1px solid #d8dfda; border-radius: 8px; background: #fafbf9; padding: 10px 12px; outline: 0; font-size: 10px; } -.settings-card input:focus { border-color: var(--green); } -.setting-hint { margin-top: 12px; background: #edf5f1; border-radius: 9px; padding: 10px 12px; color: #5f7369; font-size: 9px; } +.settings-view { width: min(980px, calc(100% - 40px)); padding: 21px 0 22px; } +.settings-heading { margin-bottom: 14px; } +.settings-heading h2 { font-size: 25px; } +.settings-layout { display: grid; grid-template-columns: 165px minmax(0, 1fr); min-height: 456px; border: 1px solid var(--line); border-radius: 16px; background: white; overflow: hidden; box-shadow: 0 12px 35px rgba(30,55,43,.06); } +.settings-tabs { padding: 12px 9px; border-right: 1px solid var(--line); background: #f2f6f3; } +.settings-tabs button { width: 100%; display: flex; align-items: center; gap: 9px; position: relative; border: 0; border-radius: 9px; background: transparent; color: #6e7a73; padding: 11px 10px; font-size: 10px; text-align: left; } +.settings-tabs button:hover { background: rgba(255,255,255,.8); color: var(--ink); } +.settings-tabs button.active { background: white; color: var(--green); font-weight: 800; box-shadow: 0 5px 18px rgba(30,55,43,.08); } +.settings-tabs button > span { width: 21px; height: 21px; display: grid; place-items: center; border-radius: 6px; background: #e4ebe7; font-size: 10px; } +.settings-tabs button.active > span { background: var(--mint); } +.settings-tabs button > b { margin-left: auto; min-width: 17px; height: 17px; display: grid; place-items: center; border-radius: 99px; background: #c9792d; color: white; font-size: 8px; } +.settings-panel { min-width: 0; padding: 19px 22px; } +.settings-panel-title { min-height: 42px; display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 15px; } +.settings-panel-title.compact { min-height: 32px; margin-bottom: 10px; } +.settings-panel-title > span > b, .settings-panel-title > span > small { display: block; } +.settings-panel-title > span > b { font-size: 13px; } +.settings-panel-title > span > small { margin-top: 4px; color: #89948e; font-size: 9px; font-weight: 400; } +.settings-panel-title > em { padding: 5px 8px; border-radius: 99px; background: #e9f5ef; color: var(--green); font-size: 8px; font-style: normal; font-weight: 800; } +.settings-form-grid { display: grid; gap: 11px; } +.settings-form-grid.three { grid-template-columns: 1.2fr .85fr 1fr; } +.settings-form-grid.two { grid-template-columns: repeat(2, minmax(0, 1fr)); } +.settings-form-grid label, .password-settings label, .material-editor-row label { display: block; } +.settings-form-grid label > span, .password-settings label > span, .material-editor-row label > span { display: block; margin-bottom: 6px; color: #647069; font-size: 9px; font-weight: 800; } +.settings-form-grid input, .password-settings input, .material-editor-row input { width: 100%; height: 38px; border: 1px solid #d9e0dc; border-radius: 8px; background: #fafbf9; padding: 0 11px; outline: 0; font-size: 10px; color: var(--ink); } +.settings-form-grid input:focus, .password-settings input:focus, .material-editor-row input:focus { border-color: var(--green); box-shadow: 0 0 0 3px rgba(0,127,95,.06); background: white; } +.settings-divider { height: 1px; margin: 18px 0 14px; background: var(--line); } +.material-editor { display: grid; gap: 9px; } +.material-editor-row { display: grid; grid-template-columns: 38px 1fr 1.45fr 22px; align-items: end; gap: 12px; padding: 10px 12px; border: 1px solid #e1e7e3; border-radius: 10px; background: #fbfcfb; } +.material-editor-row > i { align-self: center; color: #b2bbb6; font-size: 9px; font-style: normal; font-weight: 800; } +.material-icon.copper { background: #f3e2d6; color: #a65e2e; } +.material-icon.wire { background: #f3ead2; color: #b27714; } +.material-icon.aluminum { background: #e5ecee; color: #60747c; } +.material-icon.tin { background: #e5e6ed; color: #62687c; } +.feature-settings { display: grid; gap: 10px; } +.feature-row { display: flex; align-items: center; gap: 12px; padding: 12px 14px; border: 1px solid #e0e6e2; border-radius: 11px; background: #fbfcfb; } +.feature-icon { width: 36px; height: 36px; display: grid; place-items: center; flex: 0 0 auto; border-radius: 9px; background: var(--mint); color: var(--green); font-size: 17px; } +.feature-copy { flex: 1; } +.feature-copy b, .feature-copy small { display: block; } +.feature-copy b { font-size: 11px; } +.feature-copy small { margin-top: 4px; color: #8a958f; font-size: 8px; } +.switch { width: 40px; height: 22px; position: relative; flex: 0 0 auto; } +.switch input { position: absolute; opacity: 0; pointer-events: none; } +.switch span { position: absolute; inset: 0; border-radius: 99px; background: #cbd4cf; transition: .2s; } +.switch span::after { content: ""; position: absolute; width: 16px; height: 16px; left: 3px; top: 3px; border-radius: 50%; background: white; box-shadow: 0 2px 5px rgba(0,0,0,.15); transition: .2s; } +.switch input:checked + span { background: var(--green); } +.switch input:checked + span::after { transform: translateX(18px); } +.path-box { display: grid; grid-template-columns: 100px 1fr auto; align-items: center; gap: 10px; margin: -3px 0 0 48px; padding: 9px 11px; border-radius: 8px; background: #f0f4f1; } +.path-box small { color: #7f8b84; font-size: 8px; } +.path-box code { color: #2d5748; font-family: "Courier New", monospace; font-size: 9px; font-weight: 800; } +.path-box em { color: #929c96; font-size: 7px; font-style: normal; } +.print-fields { display: flex; flex-wrap: wrap; gap: 7px; margin: -3px 0 0 48px; } +.print-fields span { padding: 5px 7px; border-radius: 6px; background: #edf5f1; color: #497061; font-size: 7px; } +.data-status-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; } +.data-status-grid > div { display: grid; grid-template-columns: 38px 1fr auto; grid-template-rows: auto auto; align-items: center; gap: 2px 10px; padding: 14px; border: 1px solid #e1e7e3; border-radius: 11px; background: #fbfcfb; } +.data-status-grid > div > span { grid-row: 1 / 3; width: 38px; height: 38px; display: grid; place-items: center; border-radius: 9px; background: var(--mint); color: var(--green); font-size: 17px; } +.data-status-grid small { color: #7e8a83; font-size: 8px; } +.data-status-grid b { grid-column: 2; font-size: 22px; } +.data-status-grid em { grid-column: 3; grid-row: 1 / 3; color: #98a29c; font-size: 8px; font-style: normal; } +.path-box.data-path, .path-box.log-path { margin: 14px 0 0; } +.data-actions { display: flex; align-items: center; gap: 15px; margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--line); } +.data-actions > span { color: #8a958f; font-size: 8px; } +.secondary-action { border: 1px solid #bdd2c8; border-radius: 8px; background: white; color: var(--green); padding: 10px 13px; font-size: 9px; font-weight: 800; } +.secondary-action:hover { background: #f1f8f4; } +.secondary-action.small { padding: 8px 10px; } +.password-settings { max-width: 540px; display: grid; grid-template-columns: repeat(2, 1fr); gap: 13px; } +.password-settings label:first-child { grid-column: 1 / -1; } +.password-settings .secondary-action { grid-column: 1 / -1; justify-self: start; } +.log-title { margin-bottom: 8px; } +.log-title > span > b { display: flex; align-items: center; gap: 7px; } +.live-dot { width: 7px; height: 7px; display: inline-block; border-radius: 50%; background: #35b77c; box-shadow: 0 0 0 4px rgba(53,183,124,.12); } +.log-viewer { height: 286px; margin-top: 12px; overflow-y: auto; border-radius: 10px; background: #13231d; padding: 8px 12px; color: #dce9e2; font-family: "Courier New", monospace; } +.log-entry { display: grid; grid-template-columns: 72px 36px 1fr; gap: 9px; align-items: start; padding: 8px 2px; border-bottom: 1px solid rgba(255,255,255,.06); font-size: 8px; } +.log-entry time { color: #6f8c80; } +.log-entry > span { color: #8aa398; } +.log-entry.success > span { color: #5bd19c; } +.log-entry.warning > span { color: #e0b864; } +.log-entry.error > span { color: #e67b6e; } +.log-entry p { margin: 0; line-height: 1.45; } +.settings-footer-actions { display: flex; align-items: center; justify-content: flex-end; gap: 14px; margin-top: 12px; } +.settings-feedback { color: #87928c; font-size: 9px; } +.settings-feedback.visible { color: var(--green); } +.save-settings { min-width: 180px; border: 0; border-radius: 10px; background: var(--green); color: white; padding: 12px 15px; font-size: 10px; font-weight: 800; box-shadow: 0 8px 20px rgba(0,127,95,.16); } +.save-settings span { float: right; } .modal-backdrop { position: fixed; inset: 0; z-index: 10; display: grid; place-items: center; background: rgba(13,31,24,.58); backdrop-filter: blur(5px); padding: 20px; } .modal { width: min(390px, 100%); position: relative; background: #fff; border-radius: 20px; padding: 30px; text-align: center; box-shadow: 0 30px 80px rgba(0,0,0,.28); } @@ -231,6 +306,22 @@ h1 em { font-style: normal; color: var(--green); } .scale-tools button { justify-content: center; padding: 10px 5px; } .scale-tools button > span { display: none; } .weigh-meta > span.material-meta { display: none; } - .settings-card label { grid-template-columns: 1fr; gap: 7px; } + .settings-view { width: calc(100% - 24px); } + .settings-layout { grid-template-columns: 1fr; } + .settings-tabs { display: flex; overflow-x: auto; border-right: 0; border-bottom: 1px solid var(--line); padding: 7px; } + .settings-tabs button { min-width: 112px; justify-content: center; } + .settings-panel { padding: 17px 14px; } + .settings-form-grid.three, .settings-form-grid.two { grid-template-columns: 1fr; } + .material-editor-row { grid-template-columns: 34px 1fr; } + .material-editor-row label:nth-of-type(2) { grid-column: 2; } + .material-editor-row > i { display: none; } + .path-box { margin-left: 0; grid-template-columns: 1fr; gap: 4px; } + .print-fields { margin-left: 0; } + .password-settings { grid-template-columns: 1fr; } + .password-settings label:first-child { grid-column: auto; } + .password-settings .secondary-action { grid-column: auto; } + .log-entry { grid-template-columns: 62px 34px 1fr; } + .settings-footer-actions { align-items: stretch; flex-direction: column; } + .save-settings { width: 100%; } footer { padding: 0 16px; } } diff --git a/app/page.tsx b/app/page.tsx index 088d354..d6b3242 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -4,20 +4,67 @@ 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 materials: Material[] = [ +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 }; @@ -28,6 +75,10 @@ function formatWeight(valueKg: number, unit: Unit) { 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: "" }); @@ -47,17 +98,29 @@ export default function Home() { const [passwordOpen, setPasswordOpen] = useState(false); const [password, setPassword] = useState(""); const [passwordError, setPasswordError] = useState(""); - const [apiSettings, setApiSettings] = useState({ - employee: "", - machines: "", - weight: "", - upload: "", - }); + 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 ?? materials[0]; + const selectedMaterial = material ?? materialOptions[0]; const filteredMachines = useMemo( () => @@ -67,6 +130,26 @@ export default function Home() { [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(); @@ -123,6 +206,44 @@ export default function Home() { 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) { @@ -133,8 +254,8 @@ export default function Home() { setLoading(true); setNotice(""); try { - if (apiSettings.employee) { - const response = await fetch(apiSettings.employee); + 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 }); @@ -143,8 +264,10 @@ export default function Home() { setEmployee({ name: "张伟", id: "E-0826" }); setNotice("当前使用模拟数据,接口接入后将自动切换"); } + addLog(`人员信息获取成功:${employee.name || "张伟"}`, "success"); setStep("identity"); } catch { + addLog("门禁人员信息获取失败", "error"); setNotice("暂时无法连接本地服务器,请点击“重新获取”"); setStep("identity"); } finally { @@ -153,13 +276,47 @@ export default function Home() { } function enterAdmin() { - if (password === "888888") { + if (password === adminPassword) { setPasswordOpen(false); setPassword(""); setPasswordError(""); + setSettingsMessage(""); + addLog("管理员登录成功", "success"); setStep("settings"); } else { - setPasswordError("密码不正确,演示密码为 888888"); + 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", + ); } } @@ -179,9 +336,11 @@ export default function Home() { weightKg: Number(displayedWeight.toFixed(3)), createdAt: new Date().toISOString(), }; + setLastRecord(payload); + backupRecord(payload); try { - if (apiSettings.upload) { - const response = await fetch(apiSettings.upload, { + if (appSettings.upload) { + const response = await fetch(appSettings.upload, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), @@ -190,8 +349,11 @@ export default function Home() { } 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); @@ -210,6 +372,75 @@ export default function Home() { 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 (
@@ -271,7 +502,7 @@ export default function Home() {

请选择本次需要称重的废料

- {materials.map((item) => ( + {materialOptions.map((item) => (
-
- {[ - ["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 === "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 || "修改完成后请保存设置"} +
-
当前为空时,应用会使用内置模拟数据,便于先行演示与验收交互流程。
-
)} -
沃尔核材 · 数字化生产设备编号 SCALE-01 · v1.0
+
沃尔核材 · 数字化生产{appSettings.deviceName} · {appSettings.deviceId} · v1.1
{passwordOpen && ( @@ -452,8 +835,8 @@ export default function Home() { ⌁

管理员登录

-

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

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

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

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