完善 WOER 品牌与终端交互

This commit is contained in:
FinleyHsu
2026-07-29 11:36:59 +08:00
parent ce6bcb2f51
commit d15802fb2b
18 changed files with 464 additions and 104 deletions
+6 -6
View File
@@ -1,6 +1,6 @@
# 星元智灵
# WOER 回收箱
星元智灵是基于智投终端流程改造的竖屏 Android 投放终端应用。项目保留串口投口控制、称重回包、本地备份和服务器上报框架,并集成本地 SeetaFace6 人脸识别与人员录入流程。
WOER 回收箱是基于智投终端流程改造的竖屏 Android 投放终端应用。项目保留串口投口控制、称重回包、本地备份和服务器上报框架,并集成本地 SeetaFace6 人脸识别与人员录入流程。
本项目为沃尔核材专用 APP,定位为安卓上位机应用。
## 主要功能
@@ -76,11 +76,11 @@ path: /dev/ttyS4
baudRate: 9600
```
如果首页提示串口故障,请检查串口占用情况以及下位机连接。旧智投 APP 与星元智灵同时运行时,可能会竞争 `/dev/ttyS4`
如果首页提示串口故障,请检查串口占用情况以及下位机连接。旧智投 APP 与 WOER 回收箱同时运行时,可能会竞争 `/dev/ttyS4`
## 接口说明
当前机台号、人员、生产批次接口仍为占位实现。接口未接入时页面允许手动填写机台号,不会注入测试人员。后续接入正式接口时,需要替换:
当前机台号、人员、生产批次接口仍为测试数据实现,内置 `WAL-A01``WAL-A02``WAL-B01``WAL-B02` 四台机台供流程测试。后续接入正式接口时,需要替换:
- `app/src/main/java/com/zhitou/terminal/ZhitouBridge.java` 中的 `fetchWorkContext`
- 前端 `app/src/main/assets/www/app.js` 中的接口解析和兜底逻辑
@@ -98,7 +98,7 @@ baudRate: 9600
当前应用版本:
```text
versionName: 1.2.12-wal-review-fix
versionCode: 101
versionName: 1.2.22-ui-spacing
versionCode: 111
package: com.xingyuan.zhiling
```
+2 -2
View File
@@ -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 启动图标与应用内品牌标识。
Binary file not shown.

After

Width:  |  Height:  |  Size: 398 KiB

+163 -65
View File
@@ -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);
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

+1 -1
View File
@@ -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>
+135 -21
View File
@@ -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();
}
/** 人脸平台接口尚未接入;明确返回失败,避免把测试人员写入真实人脸库。 */
Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 37 KiB

+1 -1
View File
@@ -1,3 +1,3 @@
<resources>
<string name="app_name">星元智灵</string>
<string name="app_name">WOER 回收箱</string>
</resources>
+2 -2
View File
@@ -1,6 +1,6 @@
#!/bin/bash
# ============================================================
# 星元智灵 APK 手动编译脚本 (无 Gradle)
# WOER 回收箱 APK 手动编译脚本 (无 Gradle)
# 使用 Android SDK 命令行工具: aapt / javac / d8 / zipalign / apksigner
# ============================================================
set -e
@@ -34,7 +34,7 @@ CLASSES="$BUILD/classes"
DEX="$BUILD/dex"
echo "=========================================="
echo " 星元智灵 APK 编译"
echo " WOER 回收箱 APK 编译"
echo "=========================================="
echo "SDK: $SDK"
echo "BuildTools: 34.0.0"
+64 -4
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>星元智灵异常场景测试</title>
<title>WOER 回收箱异常场景测试</title>
<link rel="stylesheet" href="../../app/src/main/assets/www/styles.css" />
</head>
<body>
@@ -15,6 +15,7 @@
var scenario = params.get("scenario") || "pass";
var baselineDelay = Number(params.get("baselineDelay") || 30);
var weightQueryCounts = {};
var connectionCheckCount = 0;
if (params.get("clear") === "1") localStorage.clear();
window.__nativeCalls = [];
@@ -42,7 +43,9 @@
},
requestConnectionCheck: function (json) {
var request = record("requestConnectionCheck", json);
var serialOnline = scenario !== "serial-fail" && scenario !== "all-fail";
connectionCheckCount += 1;
var serialOnline = scenario !== "serial-fail" && scenario !== "all-fail" &&
!(scenario === "serial-recover" && connectionCheckCount < 3);
var networkOnline = scenario !== "network-fail" && scenario !== "all-fail";
emitConnection({
requestId: request.requestId,
@@ -78,10 +81,67 @@
openThrowDoor: function (json) { record("openThrowDoor", json); return scenario !== "open-fail"; },
closeThrowDoor: function (json) { record("closeThrowDoor", json); return scenario !== "close-fail"; },
reportXingyuanDelivery: function (json) { record("reportXingyuanDelivery", json); return true; },
saveLocalBackup: function (json) { record("saveLocalBackup", json); return "/tmp/test-backup.json"; }
saveLocalBackup: function (json) { record("saveLocalBackup", json); return "/tmp/test-backup.json"; },
setFaceRecognitionActive: function (json) { record("setFaceRecognitionActive", json); },
setFacePreviewLayout: function (json) { record("setFacePreviewLayout", json); },
prepareFaceEnrollment: function (json) { record("prepareFaceEnrollment", json); return true; },
getFaceRecognitionStatus: function () {
return JSON.stringify({ ok: true, status: "ready", message: "SeetaFace6 识别引擎已就绪", enrolledCount: 1 });
},
enrollCurrentFace: function (json) {
record("enrollCurrentFace", json);
return JSON.stringify({ ok: true, message: "人脸录入成功" });
},
setFaceMatchThreshold: function (json) {
var request = record("setFaceMatchThreshold", json);
return JSON.stringify({ ok: true, threshold: Number(request.threshold || 0.75) });
},
listFacePeople: function () {
var people = scenario === "no-people" ? [] : [
{ faceId: "FACE-1000", employeeNo: "1000", name: "测试员" }
];
return JSON.stringify({ ok: true, people: people, count: people.length });
},
verifyMember: function (json) {
var request = record("verifyMember", json);
if (scenario === "auth-fail") return JSON.stringify({ ok: false, error: "测试鉴权失败" });
return JSON.stringify({
ok: true,
memberCode: request.idContent || "1000",
employeeNo: request.idContent || "1000",
memberId: "MEMBER-TEST",
name: "测试员"
});
},
listSerialPorts: function () {
return JSON.stringify({
ok: true,
ports: [
{ path: "/dev/ttyS0", readable: true, writable: true },
{ path: "/dev/ttysWK0", readable: true, writable: true },
{ path: "/dev/ttyUSB0", readable: true, writable: true }
]
});
}
};
})();
</script>
<script src="../../app/src/main/assets/www/app.js"></script>
<script src="../../app/src/main/assets/www/app.js?v=1.2.22"></script>
<script>
(function () {
var scenario = new URLSearchParams(location.search).get("scenario") || "pass";
if (scenario !== "camera-fail") return;
window.setTimeout(function () {
var receiver = window.ZhitouSerial || window.XingyuanSerial;
if (receiver && typeof receiver.onFaceStatus === "function") {
receiver.onFaceStatus({
status: "camera_error",
message: "摄像头启动失败:设备被占用",
enrolledCount: 1
});
}
}, 3400);
})();
</script>
</body>
</html>