完善 WOER 品牌与终端交互
@@ -1,7 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.xingyuan.zhiling"
|
||||
android:versionCode="101"
|
||||
android:versionName="1.2.12-wal-review-fix">
|
||||
android:versionCode="111"
|
||||
android:versionName="1.2.22-ui-spacing">
|
||||
|
||||
<uses-sdk
|
||||
android:minSdkVersion="21"
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
沃尔核材官方 Logo 来源:
|
||||
https://www.woer.com/dist/images/favicon.ico
|
||||
|
||||
获取日期:2026-07-29
|
||||
用途:WOER 回收箱 Android 启动图标与应用内品牌标识。
|
||||
|
After Width: | Height: | Size: 398 KiB |
@@ -62,8 +62,8 @@
|
||||
|
||||
var I18N = {
|
||||
"zh-CN": {
|
||||
appName: "星元智灵",
|
||||
subtitle: "智能投放终端",
|
||||
appName: "WOER 回收箱",
|
||||
subtitle: "智能回收终端",
|
||||
homePrompt: "请点击下方按钮开始投放",
|
||||
homeHint: "请选择本次投放的机台号、人员和生产批次",
|
||||
admin: "管理员登录",
|
||||
@@ -230,8 +230,8 @@
|
||||
collectionDoorCommandSent: "已发送清运门开启指令"
|
||||
},
|
||||
"zh-TW": {
|
||||
appName: "星元智靈",
|
||||
subtitle: "智慧投放終端",
|
||||
appName: "WOER 回收箱",
|
||||
subtitle: "智慧回收終端",
|
||||
homePrompt: "請點擊下方按鈕開始投放",
|
||||
homeHint: "請選擇本次投放的機台號、人員和生產批次",
|
||||
admin: "管理員登入",
|
||||
@@ -398,8 +398,8 @@
|
||||
collectionDoorCommandSent: "已發送清運門開啟指令"
|
||||
},
|
||||
en: {
|
||||
appName: "Xingyuan Zhiling",
|
||||
subtitle: "Smart Delivery Terminal",
|
||||
appName: "WOER 回收箱",
|
||||
subtitle: "Smart Recycling Terminal",
|
||||
homePrompt: "Tap the button below to start delivery",
|
||||
homeHint: "Choose the machine number, operator, and production batch",
|
||||
admin: "Admin Login",
|
||||
@@ -604,7 +604,7 @@
|
||||
deviceStatus: defaultDeviceStatus(),
|
||||
check: defaultCheck(),
|
||||
auth: { status: "idle", message: "" },
|
||||
employeeLogin: { open: false, value: "", message: "" },
|
||||
employeeLogin: { open: false, employeeNo: "", message: "" },
|
||||
faceSync: { syncing: false, lastAt: 0, count: 0, message: "尚未同步" },
|
||||
faceRecognition: { status: "starting", message: "正在启动人脸识别", enrolledCount: 0 },
|
||||
faceEnrollment: { employeeNo: "", name: "", selectedFaceId: "", capturing: false, message: "" },
|
||||
@@ -622,13 +622,14 @@
|
||||
portSettingsPort: 0,
|
||||
testResults: null,
|
||||
lowerVersion: null,
|
||||
lowerImei: null
|
||||
lowerImei: null,
|
||||
serialPorts: { loaded: false, loading: false, items: [], message: "" }
|
||||
};
|
||||
|
||||
function defaultConfig() {
|
||||
return {
|
||||
language: "zh-CN",
|
||||
appVersion: "1.2.12-wal-review-fix",
|
||||
appVersion: "1.2.22-ui-spacing",
|
||||
adminPassword: "88888888",
|
||||
portCount: 2,
|
||||
debugLog: {
|
||||
@@ -1434,7 +1435,7 @@
|
||||
return (
|
||||
'<header class="topbar">' +
|
||||
'<div class="brand">' +
|
||||
'<div class="brand-mark">星</div>' +
|
||||
'<div class="brand-mark"><img src="./assets/woer-logo-mark.png" alt="" draggable="false" /></div>' +
|
||||
'<div><h1 class="brand-title">' +
|
||||
escapeHtml(t("appName")) +
|
||||
'</h1><p class="brand-subtitle">' +
|
||||
@@ -1477,7 +1478,8 @@
|
||||
}
|
||||
|
||||
function isFacePreviewActive() {
|
||||
return state.screen === "home" || Boolean(
|
||||
// 工号弹窗显示时先隐藏原生 SurfaceView,否则摄像头层会盖住 WebView 中央弹窗。
|
||||
return (state.screen === "home" && !(state.employeeLogin && state.employeeLogin.open)) || Boolean(
|
||||
state.screen === "settings" &&
|
||||
state.activeTab === "face" &&
|
||||
state.faceEnrollment &&
|
||||
@@ -1540,7 +1542,7 @@
|
||||
'<section class="screen boot-screen">' +
|
||||
'<div class="content" style="display:flex">' +
|
||||
'<div class="boot-panel">' +
|
||||
'<h1 class="boot-title">星元智灵启动自检</h1>' +
|
||||
'<h1 class="boot-title">WOER 回收箱启动自检</h1>' +
|
||||
'<p class="boot-desc">' +
|
||||
escapeHtml(bootDesc) +
|
||||
"</p>" +
|
||||
@@ -1596,7 +1598,7 @@
|
||||
escapeHtml("请正对摄像头进行人脸识别") +
|
||||
"</h2>" +
|
||||
'<p class="prompt-subtitle">' +
|
||||
escapeHtml("人脸识别失败可输入工号进行登陆投放") +
|
||||
escapeHtml("人脸识别失败可选择工号进行登陆投放") +
|
||||
"</p>" +
|
||||
'<div id="face-camera-preview" class="camera-preview-host home-camera-preview"><span>正在连接 USB 摄像头画面…</span></div>' +
|
||||
(faceFeedback ? '<div class="face-live-feedback bad">' + escapeHtml(faceFeedback) + '</div>' : '') +
|
||||
@@ -1613,6 +1615,10 @@
|
||||
"</div>" +
|
||||
renderEmployeeLoginDialog() +
|
||||
"</div>" +
|
||||
'<footer class="home-brand-footer" aria-label="沃尔核材·数字化生产,智能回收终端">' +
|
||||
'<span class="home-brand-footer-company">沃尔核材·数字化生产</span>' +
|
||||
'<span class="home-brand-footer-terminal">智能回收终端</span>' +
|
||||
"</footer>" +
|
||||
"</section>"
|
||||
);
|
||||
}
|
||||
@@ -1627,12 +1633,11 @@
|
||||
}
|
||||
|
||||
function renderHomeActions(reason) {
|
||||
var blocked = Boolean(reason);
|
||||
var serialBlocked = !state.deviceStatus.serialOnline;
|
||||
return (
|
||||
'<div class="home-actions">' +
|
||||
'<button class="btn primary home-login-btn" data-action="open-employee-login"' +
|
||||
(blocked ? " disabled" : "") +
|
||||
">" +
|
||||
(serialBlocked ? ' disabled aria-disabled="true" title="串口异常,无法选择工号"' : "") + ">" +
|
||||
"工号登录" +
|
||||
"</button>" +
|
||||
"</div>"
|
||||
@@ -1640,28 +1645,52 @@
|
||||
}
|
||||
|
||||
function renderEmployeeLoginDialog() {
|
||||
var login = state.employeeLogin || { open: false, value: "", message: "" };
|
||||
var login = state.employeeLogin || { open: false, employeeNo: "", message: "" };
|
||||
var people = employeeLoginPeople();
|
||||
var serialBlocked = !state.deviceStatus.serialOnline;
|
||||
if (!login.open) return "";
|
||||
var choices = people.length
|
||||
? people.map(function (person) {
|
||||
var active = String(login.employeeNo || "") === person.employeeNo;
|
||||
return '<button type="button" class="employee-choice' + (active ? " selected" : "") +
|
||||
'" data-action="select-employee" data-employee-no="' + attr(person.employeeNo) + '"' +
|
||||
(serialBlocked ? " disabled" : "") + '><span>' + escapeHtml(person.name || "未命名人员") +
|
||||
'</span><strong>工号:' + escapeHtml(person.employeeNo) + "</strong></button>";
|
||||
}).join("")
|
||||
: '<div class="empty-work-picker-list">暂无人员,请先到“设置 → 人脸”新增人员。</div>';
|
||||
return (
|
||||
'<div class="work-picker-backdrop employee-login-backdrop">' +
|
||||
'<div class="work-picker-dialog employee-login-dialog" role="dialog" aria-modal="true" aria-label="工号登录">' +
|
||||
'<div class="work-picker-head"><h3>工号登录</h3><button class="btn" data-action="close-employee-login">取消</button></div>' +
|
||||
'<p class="section-desc">请输入工号。系统将向门口终端机发起身份鉴权。</p>' +
|
||||
'<div class="employee-number-display">' + escapeHtml(login.value || "请输入工号") + "</div>" +
|
||||
'<p class="section-desc">请选择需要登录的人员,系统将使用该人员工号发起身份鉴权。</p>' +
|
||||
(serialBlocked ? '<p class="section-desc danger-text">串口异常,当前不能选择工号。</p>' : "") +
|
||||
'<div class="employee-choice-list">' + choices + "</div>" +
|
||||
(login.message ? '<p class="section-desc danger-text">' + escapeHtml(login.message) + "</p>" : "") +
|
||||
'<div class="keypad employee-keypad">' +
|
||||
[1, 2, 3, 4, 5, 6, 7, 8, 9].map(function (key) {
|
||||
return '<button class="keypad-key" data-action="employee-key" data-key="' + key + '">' + key + "</button>";
|
||||
}).join("") +
|
||||
'<button class="keypad-key utility" data-action="employee-clear">清空</button>' +
|
||||
'<button class="keypad-key" data-action="employee-key" data-key="0">0</button>' +
|
||||
'<button class="keypad-key utility" data-action="employee-backspace">退格</button>' +
|
||||
"</div>" +
|
||||
'<div class="actions"><button class="btn primary" data-action="submit-employee-login">确认</button></div>' +
|
||||
'<div class="actions"><button class="btn primary" data-action="submit-employee-login"' +
|
||||
(serialBlocked || !login.employeeNo ? " disabled" : "") + ">确认登录</button></div>" +
|
||||
"</div></div>"
|
||||
);
|
||||
}
|
||||
|
||||
function employeeLoginPeople() {
|
||||
var seen = {};
|
||||
return (Array.isArray(state.facePeople) && state.facePeople.length ? state.facePeople : state.faceDirectory || [])
|
||||
.map(function (person) {
|
||||
return {
|
||||
employeeNo: String(person && person.employeeNo || "").trim(),
|
||||
name: String(person && person.name || "").trim()
|
||||
};
|
||||
})
|
||||
.filter(function (person) {
|
||||
if (!person.employeeNo || seen[person.employeeNo]) return false;
|
||||
seen[person.employeeNo] = true;
|
||||
return true;
|
||||
})
|
||||
.sort(function (first, second) {
|
||||
return first.employeeNo.localeCompare(second.employeeNo);
|
||||
});
|
||||
}
|
||||
|
||||
function homeBlockReason() {
|
||||
if (state.deviceStatus.maintenance || state.deviceStatus.deviceDisabled) return t("maintenance");
|
||||
if (!state.deviceStatus.collectionDoorClosed) return "收运门未关闭";
|
||||
@@ -2121,7 +2150,7 @@
|
||||
'<div class="panel"><h2 class="panel-title">' + escapeHtml(t("deviceParams")) + '</h2><div class="form-grid">' +
|
||||
field(t("deviceNo"), "config.device.deviceNo", state.config.device.deviceNo || "") +
|
||||
field(t("terminalImei"), "config.device.terminalId", state.config.device.terminalId) +
|
||||
field(t("serialPath"), "config.device.serialPath", state.config.device.serialPath) +
|
||||
serialPathField() +
|
||||
"</div></div>" +
|
||||
renderMaintenanceSettings() +
|
||||
renderDeviceDisabledSettings() +
|
||||
@@ -2139,6 +2168,71 @@
|
||||
);
|
||||
}
|
||||
|
||||
function serialPathField() {
|
||||
var current = String(state.config.device.serialPath || "/dev/ttyS4");
|
||||
var serialState = state.serialPorts || { loaded: false, loading: false, items: [], message: "" };
|
||||
var items = Array.isArray(serialState.items) ? serialState.items.slice() : [];
|
||||
var hasCurrent = items.some(function (item) { return item.path === current; });
|
||||
if (!hasCurrent) {
|
||||
items.unshift({ path: current, readable: false, writable: false, configuredOnly: true });
|
||||
}
|
||||
if (!serialState.loaded && !serialState.loading) {
|
||||
serialState.loading = true;
|
||||
state.serialPorts = serialState;
|
||||
window.setTimeout(function () { refreshSerialPorts(false); }, 0);
|
||||
}
|
||||
var options = items.map(function (item) {
|
||||
var permission = item.configuredOnly
|
||||
? "当前配置,未检测到"
|
||||
: item.readable && item.writable ? "可读写" : "需获取权限";
|
||||
return '<option value="' + attr(item.path) + '"' + selected(item.path, current) + '>' +
|
||||
escapeHtml(item.path + "(" + permission + ")") + "</option>";
|
||||
}).join("");
|
||||
var hint = serialState.loading
|
||||
? "正在扫描安卓设备串口…"
|
||||
: serialState.message || "选择串口后,请点击“重新启动自检”验证连接。";
|
||||
return (
|
||||
'<div class="field serial-path-field"><label>' + escapeHtml(t("serialPath")) + "</label>" +
|
||||
'<div class="serial-path-row"><select data-bind="config.device.serialPath">' + options + "</select>" +
|
||||
'<button class="btn" type="button" data-action="refresh-serial-ports">刷新串口</button></div>' +
|
||||
'<p class="small serial-path-hint">' + escapeHtml(hint) + "</p></div>"
|
||||
);
|
||||
}
|
||||
|
||||
function refreshSerialPorts(notify) {
|
||||
var bridge = nativeBridge();
|
||||
var serialState = state.serialPorts || { loaded: false, loading: false, items: [], message: "" };
|
||||
serialState.loading = true;
|
||||
state.serialPorts = serialState;
|
||||
try {
|
||||
if (!bridge || typeof bridge.listSerialPorts !== "function") {
|
||||
throw new Error("当前版本未加载串口扫描模块");
|
||||
}
|
||||
var result = JSON.parse(String(bridge.listSerialPorts("{}") || "{}"));
|
||||
if (!result.ok) throw new Error(result.error || "串口扫描失败");
|
||||
var seen = {};
|
||||
serialState.items = (Array.isArray(result.ports) ? result.ports : []).filter(function (item) {
|
||||
var path = String(item && item.path || "");
|
||||
if (!/^\/dev\/[A-Za-z0-9._/-]+$/.test(path) || seen[path]) return false;
|
||||
seen[path] = true;
|
||||
item.path = path;
|
||||
return true;
|
||||
});
|
||||
serialState.message = serialState.items.length
|
||||
? "已发现 " + serialState.items.length + " 个串口设备。"
|
||||
: "未发现串口设备,已保留当前配置。";
|
||||
if (notify) showToast(serialState.message);
|
||||
} catch (error) {
|
||||
serialState.items = [];
|
||||
serialState.message = "串口扫描失败:" + (error.message || error);
|
||||
if (notify) showToast(serialState.message);
|
||||
}
|
||||
serialState.loaded = true;
|
||||
serialState.loading = false;
|
||||
state.serialPorts = serialState;
|
||||
render();
|
||||
}
|
||||
|
||||
function renderMaintenanceSettings() {
|
||||
return (
|
||||
'<div class="panel"><h2 class="panel-title">' + escapeHtml(t("maintenanceMode")) + "</h2>" +
|
||||
@@ -2754,45 +2848,45 @@
|
||||
}
|
||||
|
||||
function openEmployeeLogin() {
|
||||
if (homeBlockReason()) {
|
||||
showToast(homeBlockReason());
|
||||
if (!state.deviceStatus.serialOnline) {
|
||||
showToast("串口异常,无法选择工号");
|
||||
return;
|
||||
}
|
||||
state.employeeLogin = { open: true, value: "", message: "" };
|
||||
state.employeeLogin = { open: true, employeeNo: "", message: "" };
|
||||
render();
|
||||
}
|
||||
|
||||
function closeEmployeeLogin() {
|
||||
state.employeeLogin = { open: false, value: "", message: "" };
|
||||
state.employeeLogin = { open: false, employeeNo: "", message: "" };
|
||||
render();
|
||||
}
|
||||
|
||||
function appendEmployeeDigit(digit) {
|
||||
var login = state.employeeLogin;
|
||||
if (!login || !login.open || login.value.length >= 20) return;
|
||||
login.value += String(digit);
|
||||
login.message = "";
|
||||
render();
|
||||
}
|
||||
|
||||
function backspaceEmployeeDigit() {
|
||||
if (!state.employeeLogin || !state.employeeLogin.open) return;
|
||||
state.employeeLogin.value = state.employeeLogin.value.slice(0, -1);
|
||||
state.employeeLogin.message = "";
|
||||
render();
|
||||
}
|
||||
|
||||
function clearEmployeeDigits() {
|
||||
if (!state.employeeLogin || !state.employeeLogin.open) return;
|
||||
state.employeeLogin.value = "";
|
||||
function selectEmployeeForLogin(target) {
|
||||
if (!state.deviceStatus.serialOnline || !state.employeeLogin || !state.employeeLogin.open) return;
|
||||
var employeeNo = String(target.getAttribute("data-employee-no") || "").trim();
|
||||
if (!employeeNo || !employeeLoginPeople().some(function (person) { return person.employeeNo === employeeNo; })) return;
|
||||
state.employeeLogin.employeeNo = employeeNo;
|
||||
state.employeeLogin.message = "";
|
||||
render();
|
||||
}
|
||||
|
||||
function submitEmployeeLogin() {
|
||||
var value = String((state.employeeLogin && state.employeeLogin.value) || "").trim();
|
||||
if (!/^\d{1,20}$/.test(value)) {
|
||||
state.employeeLogin.message = "请输入正确的工号";
|
||||
var reason;
|
||||
if (!state.deviceStatus.serialOnline) {
|
||||
state.employeeLogin.message = "串口异常,无法使用工号登录";
|
||||
render();
|
||||
return;
|
||||
}
|
||||
var value = String((state.employeeLogin && state.employeeLogin.employeeNo) || "").trim();
|
||||
if (!value || !employeeLoginPeople().some(function (person) { return person.employeeNo === value; })) {
|
||||
state.employeeLogin.message = "请选择人员";
|
||||
render();
|
||||
return;
|
||||
}
|
||||
reason = homeBlockReason();
|
||||
if (reason) {
|
||||
// 鉴权前设备状态可能发生变化;保持弹窗状态一致并直接展示阻断原因。
|
||||
state.employeeLogin.message = reason;
|
||||
render();
|
||||
return;
|
||||
}
|
||||
@@ -3065,7 +3159,7 @@
|
||||
if (state.screen !== "home" && state.screen !== "auth") return;
|
||||
if (state.screen === "auth" && priority <= Number(state.auth.priority || 0)) return;
|
||||
if (!identityRaw) {
|
||||
showToast(source === "face" ? "等待人脸识别结果" : source === "employee" ? "请输入工号" : "请刷 IC/ID 卡");
|
||||
showToast(source === "face" ? "等待人脸识别结果" : source === "employee" ? "请选择工号" : "请刷 IC/ID 卡");
|
||||
return;
|
||||
}
|
||||
var identity = identityRaw;
|
||||
@@ -3080,7 +3174,7 @@
|
||||
status: "reading",
|
||||
priority: priority,
|
||||
token: Date.now() + "-" + Math.random(),
|
||||
message: source === "face" ? "已识别人脸,正在核对本地人脸库。" : source === "card" ? "已读取 IC/ID 卡,正在向门口终端机鉴权。" : "正在向门口终端机校验工号。"
|
||||
message: source === "face" ? "已识别人脸,正在核对本地人脸库。" : source === "card" ? "已读取 IC/ID 卡,正在向门口终端机鉴权。" : "请稍候,正在建立用户会话。"
|
||||
};
|
||||
requestToken = state.auth.token;
|
||||
render();
|
||||
@@ -3614,7 +3708,7 @@
|
||||
payload: {
|
||||
deviceNo: String(state.config.device.deviceNo || ""),
|
||||
deviceUniqueId: String(state.config.device.terminalId || state.lowerImei || ""),
|
||||
deviceName: "星元智灵回收箱",
|
||||
deviceName: "WOER 回收箱",
|
||||
deliveryTime: nowText(Date.now()),
|
||||
createdAt: Date.now(),
|
||||
employeeName: work.employeeName || work.person || "",
|
||||
@@ -3905,7 +3999,7 @@
|
||||
timestamp: record.createdAt,
|
||||
deviceNo: String(state.config.device.deviceNo || ""),
|
||||
deviceUniqueId: String(state.config.device.terminalId || state.lowerImei || ""),
|
||||
deviceName: "星元智灵回收箱",
|
||||
deviceName: "WOER 回收箱",
|
||||
machineNo: work.machineNo || record.payload.machineNo || "",
|
||||
person: work.employeeName || work.person || record.payload.employeeName || record.payload.person || "",
|
||||
employeeNo: work.employeeNo || record.payload.employeeNo || "",
|
||||
@@ -4045,7 +4139,7 @@
|
||||
startHeartbeat: "启动心跳 1001",
|
||||
reportMemberVerify: "发送会员校验 4301",
|
||||
reportDelivery: "发送投递记录 6011",
|
||||
reportXingyuanDelivery: "发送星元智灵投放结果",
|
||||
reportXingyuanDelivery: "发送 WOER 回收箱投放结果",
|
||||
reportDeliveryResult: "发送投放结果",
|
||||
fetchWorkContext: "获取投放信息",
|
||||
exportLogs: "导出日志",
|
||||
@@ -4445,7 +4539,7 @@
|
||||
state.portSettingsPort = 0;
|
||||
state.delivery = null;
|
||||
state.auth = { status: "idle", message: "" };
|
||||
state.employeeLogin = { open: false, value: "", message: "" };
|
||||
state.employeeLogin = { open: false, employeeNo: "", message: "" };
|
||||
render();
|
||||
}
|
||||
|
||||
@@ -4921,6 +5015,13 @@
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
// 终端为固定业务界面:禁止所有页面的长按菜单、文字选择和图片拖拽。
|
||||
["contextmenu", "selectstart", "dragstart"].forEach(function (eventName) {
|
||||
document.addEventListener(eventName, function (event) {
|
||||
event.preventDefault();
|
||||
}, true);
|
||||
});
|
||||
|
||||
window.addEventListener("resize", function () {
|
||||
syncFacePreviewLayout(isFacePreviewActive());
|
||||
});
|
||||
@@ -4940,9 +5041,7 @@
|
||||
if (action === "open-work-info") openWorkInfo(false);
|
||||
if (action === "open-employee-login") openEmployeeLogin();
|
||||
if (action === "close-employee-login") closeEmployeeLogin();
|
||||
if (action === "employee-key") appendEmployeeDigit(target.getAttribute("data-key"));
|
||||
if (action === "employee-backspace") backspaceEmployeeDigit();
|
||||
if (action === "employee-clear") clearEmployeeDigits();
|
||||
if (action === "select-employee") selectEmployeeForLogin(target);
|
||||
if (action === "submit-employee-login") submitEmployeeLogin();
|
||||
if (action === "refresh-work-info") fetchWorkContext();
|
||||
if (action === "submit-work-info") submitWorkInfo();
|
||||
@@ -4979,6 +5078,7 @@
|
||||
if (action === "check-lower-version") checkLowerVersion();
|
||||
if (action === "get-lower-imei") getLowerImei();
|
||||
if (action === "get-lower-info") getLowerInfo();
|
||||
if (action === "refresh-serial-ports") refreshSerialPorts(true);
|
||||
if (action === "retry-cache") retryCache();
|
||||
if (action === "cleanup-cache") {
|
||||
cleanupCache(true);
|
||||
@@ -5045,8 +5145,6 @@
|
||||
if (state.screen === "adminLogin" && /^[0-9]$/.test(event.key)) appendAdminDigit(event.key);
|
||||
if (state.screen === "adminLogin" && event.key === "Backspace") backspaceAdminDigit();
|
||||
if (state.screen === "adminLogin" && event.key === "Enter") submitAdmin();
|
||||
if (state.screen === "home" && state.employeeLogin && state.employeeLogin.open && /^[0-9]$/.test(event.key)) appendEmployeeDigit(event.key);
|
||||
if (state.screen === "home" && state.employeeLogin && state.employeeLogin.open && event.key === "Backspace") backspaceEmployeeDigit();
|
||||
if (state.screen === "home" && state.employeeLogin && state.employeeLogin.open && event.key === "Enter") submitEmployeeLogin();
|
||||
if (state.screen === "workInfo" && event.key === "Enter") submitWorkInfo();
|
||||
if (state.screen === "delivery" && event.key === "Escape") closeDoor(true);
|
||||
|
||||
|
After Width: | Height: | Size: 18 KiB |
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#1b3134" />
|
||||
<title>星元智灵</title>
|
||||
<title>WOER 回收箱</title>
|
||||
<link rel="icon" href="data:," />
|
||||
<link rel="stylesheet" href="./styles.css" />
|
||||
</head>
|
||||
|
||||
@@ -15,6 +15,14 @@
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
img {
|
||||
-webkit-user-drag: none;
|
||||
user-drag: none;
|
||||
}
|
||||
|
||||
html,
|
||||
@@ -78,19 +86,30 @@ select:disabled {
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
gap: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.brand-mark + div {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.brand-mark {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 2px;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
color: var(--deep);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.brand-mark img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
@@ -279,7 +298,9 @@ select:disabled {
|
||||
|
||||
.home-main {
|
||||
flex: 1;
|
||||
display: block;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
padding: 22px;
|
||||
}
|
||||
|
||||
@@ -347,7 +368,8 @@ select:disabled {
|
||||
|
||||
.home-lower {
|
||||
position: relative;
|
||||
min-height: calc(100vh - 118px);
|
||||
flex: 1;
|
||||
min-height: calc(100vh - 182px);
|
||||
display: grid;
|
||||
grid-template-rows: 1fr auto;
|
||||
align-items: center;
|
||||
@@ -357,6 +379,33 @@ select:disabled {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.home-brand-footer {
|
||||
width: 100%;
|
||||
flex: 0 0 auto;
|
||||
min-height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
padding: 0 38px 20px;
|
||||
border-top: 1px solid var(--line);
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
color: #94a098;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.04em;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.home-brand-footer-company,
|
||||
.home-brand-footer-terminal {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.home-brand-footer-terminal {
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.home-prompt {
|
||||
text-align: center;
|
||||
}
|
||||
@@ -621,6 +670,7 @@ select:disabled {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.home-login-btn {
|
||||
@@ -708,6 +758,11 @@ select:disabled {
|
||||
|
||||
.work-picker-backdrop {
|
||||
position: fixed;
|
||||
/* Android 7 的旧版 WebView 不支持 inset,必须保留四边定位作为兼容写法。 */
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
inset: 0;
|
||||
z-index: 50;
|
||||
display: grid;
|
||||
@@ -716,6 +771,16 @@ select:disabled {
|
||||
background: rgba(10, 28, 30, 0.38);
|
||||
}
|
||||
|
||||
.employee-login-backdrop {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.work-picker-dialog {
|
||||
width: min(92vw, 620px);
|
||||
max-height: min(72vh, 560px);
|
||||
@@ -729,28 +794,47 @@ select:disabled {
|
||||
|
||||
.employee-login-dialog {
|
||||
width: min(92vw, 520px);
|
||||
max-height: calc(100dvh - 48px);
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.employee-number-display {
|
||||
min-height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 14px;
|
||||
padding: 10px 16px;
|
||||
.employee-choice-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
margin: 12px 0 18px;
|
||||
}
|
||||
|
||||
.employee-choice {
|
||||
min-height: 76px;
|
||||
padding: 12px 14px;
|
||||
border: 2px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: #f8fbfa;
|
||||
font-size: 28px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 2px;
|
||||
color: var(--ink);
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.employee-keypad {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
margin: 12px 0 18px;
|
||||
.employee-choice span {
|
||||
font-size: 20px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.employee-choice strong {
|
||||
color: var(--muted);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.employee-choice.selected {
|
||||
border-color: var(--teal);
|
||||
background: #e5f4ef;
|
||||
}
|
||||
|
||||
.employee-choice:disabled {
|
||||
background: #f1f3f2;
|
||||
color: #98a2a3;
|
||||
}
|
||||
|
||||
.work-picker-head {
|
||||
@@ -1087,6 +1171,25 @@ select:disabled {
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.serial-path-field {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.serial-path-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 10px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.serial-path-row .btn {
|
||||
min-width: 104px;
|
||||
}
|
||||
|
||||
.serial-path-hint {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.readonly-value {
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
@@ -1623,6 +1726,12 @@ select:disabled {
|
||||
padding: 22px 16px 76px;
|
||||
}
|
||||
|
||||
.home-brand-footer {
|
||||
min-height: 64px;
|
||||
padding: 0 16px 20px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.home-camera-preview {
|
||||
width: calc(100% + 32px);
|
||||
max-width: none;
|
||||
@@ -1752,10 +1861,15 @@ select:disabled {
|
||||
}
|
||||
|
||||
.home-lower {
|
||||
min-height: calc(100dvh - 92px);
|
||||
min-height: calc(100vh - 156px);
|
||||
min-height: calc(100dvh - 156px);
|
||||
padding: 22px 14px 76px;
|
||||
}
|
||||
|
||||
.home-brand-footer {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.prompt-title {
|
||||
font-size: clamp(26px, 7vw, 38px);
|
||||
}
|
||||
|
||||
@@ -116,6 +116,16 @@ public class MainActivity extends Activity implements
|
||||
settings.setLoadWithOverviewMode(true);
|
||||
settings.setUseWideViewPort(true);
|
||||
|
||||
// 终端所有业务页面均禁止长按:拦截 WebView 的文本选择、上下文菜单和触觉反馈。
|
||||
webView.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View view) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
webView.setLongClickable(false);
|
||||
webView.setHapticFeedbackEnabled(false);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
// 页面只通过受控的原生桥通讯,不允许 file:// 页面任意读取文件或跨域访问。
|
||||
settings.setAllowFileAccessFromFileURLs(false);
|
||||
|
||||
@@ -10,6 +10,10 @@ import java.io.FileReader;
|
||||
import java.io.FileWriter;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
@@ -206,6 +210,56 @@ final class ZhitouBridge {
|
||||
}
|
||||
}
|
||||
|
||||
/** 扫描常见 Android 串口设备节点,供设置页选择,不在扫描阶段占用串口。 */
|
||||
@JavascriptInterface
|
||||
public String listSerialPorts(String json) {
|
||||
JSONObject result = new JSONObject();
|
||||
JSONArray ports = new JSONArray();
|
||||
try {
|
||||
File[] devices = new File("/dev").listFiles();
|
||||
if (devices == null) {
|
||||
throw new IllegalStateException("无法读取 /dev 目录");
|
||||
}
|
||||
List<File> candidates = new ArrayList<File>();
|
||||
for (File device : devices) {
|
||||
String name = device == null ? "" : device.getName();
|
||||
if (isSerialDeviceName(name)) {
|
||||
candidates.add(device);
|
||||
}
|
||||
}
|
||||
Collections.sort(candidates, new Comparator<File>() {
|
||||
@Override
|
||||
public int compare(File first, File second) {
|
||||
return first.getAbsolutePath().compareToIgnoreCase(second.getAbsolutePath());
|
||||
}
|
||||
});
|
||||
for (File device : candidates) {
|
||||
JSONObject item = new JSONObject();
|
||||
item.put("path", device.getAbsolutePath());
|
||||
item.put("readable", device.canRead());
|
||||
item.put("writable", device.canWrite());
|
||||
ports.put(item);
|
||||
}
|
||||
result.put("ok", true);
|
||||
result.put("ports", ports);
|
||||
result.put("count", ports.length());
|
||||
} catch (Exception error) {
|
||||
try {
|
||||
result.put("ok", false);
|
||||
result.put("ports", ports);
|
||||
result.put("count", 0);
|
||||
result.put("error", error.getMessage() == null ? "串口扫描失败" : error.getMessage());
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
private boolean isSerialDeviceName(String name) {
|
||||
return name != null && name.matches(
|
||||
"^(ttyS|ttyUSB|ttyACM|ttyAMA|ttyFIQ|ttyXRUSB|ttyHS|ttyMT|ttymxc|ttysWK|ttyRK|ttyAP)[0-9]+$");
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public String getLowerVersion(String json) {
|
||||
try {
|
||||
@@ -386,8 +440,27 @@ final class ZhitouBridge {
|
||||
|
||||
@JavascriptInterface
|
||||
public String fetchWorkContext(String json) {
|
||||
// 正式机台接口未接入前返回空数组,前端仍允许手动填写,不能注入测试人员或批次。
|
||||
return new JSONArray().toString();
|
||||
JSONArray machines = new JSONArray();
|
||||
try {
|
||||
machines.put(new JSONObject()
|
||||
.put("machineNo", "WAL-A01")
|
||||
.put("persons", new JSONArray().put("张伟").put("李娜"))
|
||||
.put("batches", new JSONArray().put("20260723-A01").put("20260723-A02")));
|
||||
machines.put(new JSONObject()
|
||||
.put("machineNo", "WAL-A02")
|
||||
.put("persons", new JSONArray().put("王强").put("赵敏"))
|
||||
.put("batches", new JSONArray().put("20260723-B01").put("20260723-B02")));
|
||||
machines.put(new JSONObject()
|
||||
.put("machineNo", "WAL-B01")
|
||||
.put("persons", new JSONArray().put("陈晨").put("刘洋"))
|
||||
.put("batches", new JSONArray().put("20260723-C01").put("20260723-C02")));
|
||||
machines.put(new JSONObject()
|
||||
.put("machineNo", "WAL-B02")
|
||||
.put("persons", new JSONArray().put("周杰").put("孙悦"))
|
||||
.put("batches", new JSONArray().put("20260723-D01").put("20260723-D02")));
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return machines.toString();
|
||||
}
|
||||
|
||||
/** 人脸平台接口尚未接入;明确返回失败,避免把测试人员写入真实人脸库。 */
|
||||
|
||||
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 37 KiB |
@@ -1,3 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">星元智灵</string>
|
||||
<string name="app_name">WOER 回收箱</string>
|
||||
</resources>
|
||||
|
||||