StandAPP/app/src/main/assets/print_demo.html

938 lines
40 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>智能调度终端 - 打印演示</title>
<style>
body { font-family: -apple-system, sans-serif; background: #f4f7f9; margin: 0; padding: 20px; color: #333; }
.header { text-align: center; padding: 20px 0; background: #004A8F; color: white; border-radius: 8px 8px 0 0; margin: -20px -20px 20px -20px; }
.card { background: white; padding: 20px; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); max-width: 500px; margin: 0 auto; }
.info-box { background: #eef2f6; padding: 15px; border-radius: 8px; margin-bottom: 20px; font-size: 14px; }
.btn {
display: block; width: 100%; padding: 15px; margin: 12px 0;
background: #3B82F6; color: white; border: none; border-radius: 8px;
font-size: 16px; font-weight: bold; cursor: pointer; transition: background 0.3s;
}
.btn:active { background: #2563EB; }
.btn-green { background: #10B981; }
.btn-orange { background: #F59E0B; }
.btn-red { background: #ef4444; }
.status-tag { display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: bold; margin-bottom: 10px; }
.status-online { background: #D1FAE5; color: #065F46; }
.status-offline { background: #FEE2E2; color: #991B1B; }
.bill-preview { border: 1px dashed #aaa; padding: 15px; background: #fff; margin-top: 20px; }
.footer { text-align: center; margin-top: 30px; font-size: 12px; color: #999; }
.exit-notice { color: #ef4444; font-weight: bold; font-size: 12px; margin-top: 5px; }
/* WebSocket Styles */
.ws-card { margin-top: 20px; border-top: 2px solid #eee; padding-top: 20px; }
.ws-status { font-size: 12px; padding: 4px 8px; border-radius: 4px; margin-left: 10px; }
.ws-connected { background: #dcfce7; color: #166534; }
.ws-disconnected { background: #fee2e2; color: #991b1b; }
.ws-input-group { margin-top: 10px; display: flex; flex-direction: column; gap: 5px; }
.ws-input {
padding: 10px; border: 1px solid #cbd5e1; border-radius: 6px;
font-size: 12px; font-family: monospace; width: 100%; box-sizing: border-box;
background: #f8fafc; color: #334155;
}
.log-container {
background: #1e293b; color: #f8fafc; padding: 10px;
border-radius: 6px; font-family: monospace; font-size: 11px;
max-height: 200px; overflow-y: auto; margin-top: 10px;
}
.log-item { margin-bottom: 4px; border-bottom: 1px solid #334155; padding-bottom: 2px; }
.log-time { color: #94a3b8; margin-right: 5px; }
.log-msg { word-break: break-all; }
/* PTT Module Styles */
.ptt-card { border-top: 2px solid #10B981 !important; margin-top: 20px; }
.ptt-label { font-size: 11px; color: #64748b; margin-top: 5px; display: block; }
.ptt-btn-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-top: 10px; }
.ptt-btn { margin: 0 !important; padding: 8px !important; font-size: 12px !important; }
/* Custom Modal Styles */
.modal-overlay {
display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.5); z-index: 1000; align-items: center; justify-content: center;
}
.modal-content {
background: white; padding: 20px; border-radius: 12px; width: 85%; max-width: 400px;
box-shadow: 0 10px 25px rgba(0,0,0,0.2);
}
.modal-header { font-weight: bold; font-size: 18px; margin-bottom: 15px; color: #004A8F; border-bottom: 1px solid #eee; padding-bottom: 10px; }
.modal-body { font-size: 14px; line-height: 1.8; color: #444; word-break: break-all; }
.modal-footer { margin-top: 20px; text-align: right; }
</style>
</head>
<body>
<div class="card">
<div class="header">
<h3>打印控制台</h3>
</div>
<div id="inject-status" class="status-tag status-offline">未检测到 App 接口</div>
<div class="info-box">
<strong>设备信息:</strong><br>
应用版本: <span id="app-version">Unknown</span><br>
环境检测: <span id="env-type">Browser</span><br>
打印机: <span id="printer-status" style="font-weight: bold; color: #ef4444;">未连接</span>
</div>
<!-- 打印功能网格 -->
<div style="margin-bottom: 20px;">
<div style="font-size: 13px; font-weight: bold; color: #64748b; margin-bottom: 8px;">基础打印测试</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
<button class="btn btn-green" style="margin:0; padding:10px; font-size:13px;" onclick="callNativePrintText()">文本打印</button>
<button class="btn btn-green" style="margin:0; padding:10px; font-size:13px;" onclick="callNativePrintQR()">二维码打印</button>
<button class="btn btn-green" style="margin:0; padding:10px; font-size:13px;" onclick="callNativePrintBarcode()">条形码打印</button>
<button class="btn btn-green" style="margin:0; padding:10px; font-size:13px;" onclick="callNativePrintImg()">图片打印</button>
</div>
</div>
<div style="margin-bottom: 20px;">
<div style="font-size: 13px; font-weight: bold; color: #64748b; margin-bottom: 8px;">高级业务样式与测试</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
<button class="btn btn-orange" style="margin:0; padding:10px; font-size:13px;" onclick="callNativePrintColumns()">分栏打印测试</button>
<button class="btn btn-orange" style="margin:0; padding:10px; font-size:13px;" onclick="callNativePrintAlarm()">警情通知单</button>
<button class="btn btn-orange" style="margin:0; padding:10px; font-size:13px;" onclick="callNativePrintDemo(false)">2寸演示单据</button>
<button class="btn btn-orange" style="margin:0; padding:10px; font-size:13px;" onclick="callNativePrintDemo(true)">3寸演示单据</button>
<button class="btn" style="margin:0; padding:10px; font-size:13px; background: #0ea5e9; grid-column: span 2;" onclick="testES6Features()">ES6 引擎特性测试</button>
</div>
</div>
<div style="margin-bottom: 20px;">
<div style="font-size: 13px; font-weight: bold; color: #64748b; margin-bottom: 8px;">硬件控制与维护</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
<button class="btn" style="margin:0; padding:10px; font-size:13px; background: #6366f1;" onclick="callNativeTestPage()">打印自检页</button>
<button class="btn" style="margin:0; padding:10px; font-size:13px; background: #8b5cf6;" onclick="callNativeCheckPaper()">查询纸张状态</button>
<button class="btn btn-red" style="margin:0; padding:10px; font-size:13px;" onclick="callNativeCutPaper(false)">执行半切</button>
<button class="btn btn-red" style="margin:0; padding:10px; font-size:13px;" onclick="callNativeCutPaper(true)">执行全切</button>
</div>
</div>
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
<button class="btn" style="background: #475569;" onclick="callSystemPrint()">1. 调用系统标准打印 (A4)</button>
<p style="font-size: 12px; color: #666;">(支持 PDF 导出、网络打印机、A4 适配)</p>
<button class="btn btn-green" onclick="callRawPrint()">2. 发送热敏指令打印</button>
<p style="font-size: 12px; color: #666;">(适用于蓝牙/USB 小票机,传输原始 JSON)</p>
<button class="btn btn-orange" onclick="callUpdate()">3. 检查系统更新</button>
<button class="btn" style="background: #10B981;" onclick="callUploadLogs()">4. 上传系统日志</button>
<button class="btn" style="background: #64748b;" onclick="callAbout()">5. 关于应用</button>
<button class="btn btn-red" onclick="callExit()">6. 退出应用</button>
<!-- WebSocket 控制区 -->
<div class="ws-card">
<div style="display: flex; align-items: center; justify-content: space-between;">
<strong>WebSocket 实时推送</strong>
<span id="ws-status-tag" class="ws-status ws-disconnected">未连接</span>
</div>
<div style="margin-top: 10px; font-size: 13px;">
ID: <span id="ws-client-id" style="color: #6366f1;">-</span>
</div>
<!-- WS 地址配置 -->
<div class="ws-input-group">
<label style="font-size: 12px; color: #64748b;">WebSocket 连接地址:</label>
<input type="text" id="ws-url-input" class="ws-input"
value="wss://template.gyouzhe.com/api/ws/{clientId}?Authorization=qiangfengsuiyuehen">
</div>
<div style="display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap;">
<button class="btn btn-green" style="margin:0; flex:1; padding: 10px; min-width: 120px;" onclick="toggleWS()">连接/断开 WS</button>
<button class="btn" style="margin:0; flex:1; padding: 10px; min-width: 120px; background: #6366f1;" onclick="manualAddRecord()">手动新增记录</button>
<button class="btn btn-orange" style="margin:0; flex:1; padding: 10px; min-width: 120px;" onclick="loadHistory()">从终端同步</button>
<button class="btn btn-red" style="margin:0; flex:1; padding: 10px; min-width: 120px; background: #991b1b;" onclick="clearHistory()">清空终端存储</button>
</div>
<div id="log-list" class="log-container">
<div class="log-item">等待连接...</div>
</div>
</div>
<!-- PTT 业务测试模块 -->
<div class="ws-card ptt-card">
<div style="display: flex; align-items: center; justify-content: space-between;">
<strong style="color: #047857;">PTT 业务功能测试</strong>
<div style="display: flex; gap: 5px;">
<span id="ptt-talk-status" class="ws-status" style="background: #e2e8f0; color: #64748b; display: none;">通话中</span>
<span id="ptt-status-tag" class="ws-status ws-disconnected">PTT 未登录</span>
</div>
</div>
<div style="margin-top: 8px; font-size: 13px;">
当前群组: <span id="ptt-current-group" style="color: #6366f1; font-weight: bold;">-</span>
<span id="ptt-current-group-id" style="color: #94a3b8; font-size: 11px; margin-left: 5px;"></span>
</div>
<div class="ws-input-group" style="margin-top: 10px;">
<div style="display: flex; gap: 8px;">
<div style="flex: 1;">
<span class="ptt-label">群组 ID (十六进制):</span>
<input type="text" id="ptt-gid" class="ws-input" value="00000001">
</div>
</div>
<div style="display: flex; gap: 8px;">
<div style="flex: 4;">
<span class="ptt-label">AT 指令 (十六进制):</span>
<input type="text" id="ptt-at-cmd" class="ws-input" placeholder="e.g. 0D0000">
</div>
<div style="flex: 1; display: flex; align-items: flex-end;">
<button class="btn ptt-btn ptt-action-btn" style="background: #475569; height: 35px;" onclick="pttSendAtCmd()">发送</button>
</div>
</div>
</div>
<!-- PTT 核心对讲按钮 -->
<div style="margin-top: 15px;">
<button id="ptt-speak-btn" class="btn btn-red"
style="height: 80px; font-size: 20px; background: #94a3b8; cursor: not-allowed;"
disabled>长按对讲 (PTT)</button>
</div>
<div class="ptt-btn-grid">
<button class="btn ptt-btn ptt-action-btn" style="background: #6366f1;" onclick="pttGetGroups()" disabled>获取群组</button>
<button class="btn ptt-btn ptt-action-btn" style="background: #8b5cf6;" onclick="pttGetMembers()" disabled>获取成员</button>
<button class="btn ptt-btn ptt-action-btn" style="background: #3b82f6;" onclick="pttJumpGroup()" disabled>切换群组</button>
<button class="btn ptt-btn ptt-action-btn" style="background: #0ea5e9;" onclick="pttGetCurrentGroup()">当前群组</button>
<button class="btn ptt-btn ptt-action-btn btn-orange" onclick="pttForceRelease()" disabled>强制释放</button>
<button class="btn ptt-btn ptt-action-btn" style="background: #000; color: #fca5a5;" onclick="pttSimulateConflict()" disabled>模拟冲突</button>
</div>
<div id="ptt-result" class="log-container" style="background: #0f172a; margin-top: 10px; height: 120px;">
<div class="log-item">等待 PTT 登录成功后激活功能...</div>
</div>
</div>
<div class="bill-preview" id="printArea">
<div style="text-align: center; font-weight: bold; font-size: 18px;">测试签收单</div>
<hr>
<p>单号: SN20260315888</p>
<div id="dynamic-items">
<p>物料: 工业级调度终端 x 5</p>
</div>
<p>时间: <span id="current-time"></span></p>
<div style="height: 40px; border: 1px solid #eee; text-align: center; line-height: 40px; color: #ccc;">
[ 此处为二维码/条码预览 ]
</div>
</div>
</div>
<div class="footer">
SDK Connectivity v1.0
</div>
<!-- 自定义“关于”弹窗 -->
<div id="aboutModal" class="modal-overlay" onclick="closeAbout()">
<div class="modal-content" onclick="event.stopPropagation()">
<div class="modal-header">关于应用信息</div>
<div id="aboutBody" class="modal-body">
正在获取信息...
</div>
<div class="modal-footer">
<button class="btn" style="padding: 10px 20px; background: #64748b; margin: 0;" onclick="closeAbout()">确认</button>
</div>
</div>
</div>
<script>
// 兼容性优化:全部使用 ES5 语法
// 初始化界面
document.getElementById('current-time').innerText = new Date().toLocaleString();
function checkApp() {
var tag = document.getElementById('inject-status');
var versionSpan = document.getElementById('app-version');
var envSpan = document.getElementById('env-type');
if (window.android) {
tag.innerText = "已连接至 App 原生接口";
tag.className = "status-tag status-online";
envSpan.innerText = "Android App Container";
if (window.android.getVersionName) {
versionSpan.innerText = window.android.getVersionName();
}
updatePrinterStatus();
// 每 3 秒同步一次打印机状态
setInterval(updatePrinterStatus, 3000);
// 确认接口可用后再加载历史
loadHistory();
// 通知 Android 页面加载完成
if (window.android.onPageFinished) {
window.android.onPageFinished();
}
} else {
// bridge 尚未注入200ms 后重试
setTimeout(checkApp, 200);
}
}
function updatePrinterStatus() {
if (window.android && window.android.getPrinterStatus) {
var status = JSON.parse(window.android.getPrinterStatus());
var span = document.getElementById('printer-status');
var statusTxt = status.connected ? "已连接 (" + status.port + ")" : "未连接 (" + status.port + ")";
var paperTxt = status.hasPaper ? " [有纸]" : " [⚠️缺纸]";
span.innerText = statusTxt + paperTxt;
span.style.color = (status.connected && status.hasPaper) ? "#10B981" : "#ef4444";
}
}
function callNativePrintText() {
if (window.android && window.android.printText) {
window.android.printText("消防值守台 终端打印测试\n", 2, 1, 20);
window.android.printText("当前时间: " + new Date().toLocaleString() + "\n", 1, 1, 20);
}
}
function callNativePrintQR() {
if (window.android && window.android.printQR) {
window.android.printQR("https://www.gyouzhe.com", 6, 2);
window.android.printText("\n\n\n", 1, 1, 10);
window.android.cutPaper(false);
}
}
function callNativePrintBarcode() {
if (window.android && window.android.printBarcode) {
window.android.printBarcode("88888888", 3, 100, 5, 2, 2);
window.android.printText("\n\n\n", 1, 1, 10);
window.android.cutPaper(false);
}
}
function callNativePrintImg() {
if (window.android && window.android.printImage) {
window.android.printImage("1.jpg", 384, 1, 2);
window.android.printText("\n\n\n", 1, 1, 10);
window.android.cutPaper(false);
}
}
function callNativePrintColumns() {
if (window.android && window.android.printColumns) {
window.android.printColumns("项目", 0, "单价", 180, "数量", 250, "金额", 320);
window.android.printColumnsWithHeight("测试商品", 0, "10.0", 180, "1", 250, "10.0", 320, 70);
window.android.printText("\n\n\n", 1, 1, 10);
window.android.cutPaper(false);
}
}
function callNativePrintAlarm() {
if (window.android && window.android.printText) {
// 1. 标题
window.android.printText("警情通知单", 2, 2, 40);
window.android.cutPaper(false);
// 2. 二维码 (居中)
window.android.printQR("ALARM_20260304172432", 7, 2);
window.android.printText("\n", 1, 1, 20);
window.android.cutPaper(false);
// 3. 详情列表
window.android.printText("报警时间: 2026-03-04 17:24:32", 1, 1, 20);
window.android.printText("报警人: 张先生", 1, 1, 20);
window.android.printText("报警人电话: 18722727636", 1, 1, 20);
window.android.printText("灾害地址: 贵州省安顺市西秀区(赵家山村)宋旗镇赵家山村", 1, 1, 20);
window.android.printText("警情分类: 抢险救援", 1, 1, 20);
window.android.printText("报警描述: 车祸", 1, 1, 20);
window.android.printText("主管救援站联系电话: 0851-33410119", 1, 1, 20);
window.android.printText("出动车辆: ", 1, 1, 20);
// 4. 分割线
window.android.printText("--------------------------------", 1, 1, 10);
// 5. 注意事项
window.android.printText("注意事项:", 1, 1, 20);
window.android.printText("1、请专职消防队人员穿戴好防护装备携带灭火、救援器材赶往现场科学处置。", 1, 1, 20);
window.android.printText("2、到达现场后及时上报情况注意安全做好个人防护和现场警戒。", 1, 1, 20);
window.android.printText("3、根据现场情况迅速组织人员疏散并在确保安全的前提下利用灭火救援装备开展救援。", 1, 1, 20);
window.android.printText("4、待责任消防救援站力量到达现场后主动与其汇报现场已知情况并协助做好灭火救援工作。", 1, 1, 20);
window.android.printText("5、严格落实作战行动安全规定防止触电、溺水、烧伤、坠落、中毒、砸伤、放射线等伤害。", 1, 1, 20);
// 6. 结尾
window.android.printText("--------------------------------", 1, 1, 10);
window.android.printText("\n\n\n", 1, 1, 10);
// 7. 切纸
window.android.cutPaper(false);
}
}
function callNativePrintDemo(is3Inch) {
if (window.android && window.android.printText) {
var width = is3Inch ? 400 : 300;
var right = is3Inch ? 120 : 100;
window.android.printText("\nStandAPP 业务测试单\n", 2, 1, 30);
window.android.printText("--------------------------------\n", 1, 1, 10);
window.android.printColumns("名称", 0, "单价", 180, "数量", 250, "金额", width);
window.android.printColumnsWithHeight("演示商品A", 0, "9.9", 180, "1", 250, "9.90", width, right);
window.android.printText("--------------------------------\n", 1, 1, 10);
window.android.printQR("http://www.standapp.com", 6, 2);
window.android.printText("\n\n\n", 1, 1, 10);
window.android.cutPaper(false);
}
}
function callNativeTestPage() {
if (window.android && window.android.printTestPage) {
window.android.printTestPage();
}
}
function callNativeCheckPaper() {
if (window.android && window.android.checkPaper) {
window.android.checkPaper();
}
}
function callNativeCutPaper(allCut) {
if (window.android && window.android.cutPaper) {
window.android.cutPaper(allCut);
}
}
// 调用系统打印
function callSystemPrint() {
if (window.android && window.android.printPage) {
window.android.printPage();
} else {
alert("当前环境不支持系统打印");
}
}
// 调用原始数据打印
function callRawPrint() {
if (window.android && window.android.printRawData) {
var data = {
type: "receipt",
title: "测试签收单",
sn: "SN20260315888",
items: ["调度终端 x 5"],
timestamp: Date.now()
};
window.android.printRawData(JSON.stringify(data));
} else {
alert("当前环境不支持原始指令打印");
}
}
// 检查更新
function callUpdate() {
if (window.android && window.android.startUpdate) {
window.android.startUpdate("https://example.com/latest.apk");
} else {
alert("更新接口未就绪");
}
}
// 上传日志
var isUploading = false;
function callUploadLogs() {
if (isUploading) {
alert("正在上传中请耐心等待30秒内请勿重复点击...");
return;
}
if (window.android && window.android.uploadLogs) {
isUploading = true;
alert("开始上传日志,请耐心等待...");
window.android.uploadLogs();
setTimeout(function() {
isUploading = false;
}, 30000);
} else {
alert("上传日志接口不可用");
}
}
function callExit() {
if (window.android && window.android.exitApp) {
window.android.exitApp();
} else {
alert("App 退出接口不可用");
}
}
function callAbout() {
if (window.android && window.android.getAboutInfo) {
var infoStr = window.android.getAboutInfo();
try {
var info = JSON.parse(infoStr);
var html = '<strong>应用名称:</strong> ' + info.appName + '<br>' +
'<strong>版本号:</strong> <span style="color: #3b82f6;">' + info.versionName + '</span><br>' +
'<strong>设备型号:</strong> ' + info.model + '<br>' +
'<strong>系统版本:</strong> Android ' + info.osVersion + '<br>' +
'<strong>CPU 架构:</strong> ' + info.cpuAbi + '<br>' +
'<strong>运行模式:</strong> ' + (info.is64Bit ? "64位" : "32位兼容模式");
document.getElementById('aboutBody').innerHTML = html;
document.getElementById('aboutModal').style.display = 'flex';
} catch (e) {
document.getElementById('aboutBody').innerText = "获取信息解析失败: " + infoStr;
document.getElementById('aboutModal').style.display = 'flex';
}
} else {
alert("关于接口未就绪");
}
}
function closeAbout() {
document.getElementById('aboutModal').style.display = 'none';
}
// 打印机查纸回调
window.onPaperStateChanged = function(hasPaper) {
if (hasPaper) {
alert("打印机状态正常:当前有纸");
} else {
alert("⚠️ 打印机缺纸!请装入纸卷!");
}
updatePrinterStatus();
};
// PTT 各种事件回调
window.onPttEvent = function(eventType, payload) {
console.log("收到 PTT 事件: ", eventType, payload);
var msg = "";
switch(eventType) {
case 'GroupList':
msg = "获取群组成功,共 " + payload.length + " 个群组。第一组: " + (payload[0] ? payload[0].groupName : "无");
break;
case 'MemberList':
msg = "获取群成员成功,共 " + payload.length + " 个成员。";
break;
case 'SpeakerUpdate':
msg = "当前讲话人: " + payload.talkName + " (组:" + payload.groupName + ")";
break;
case 'PlayStatus':
msg = (payload.playStatus === 1 ? "开始播放:" : "结束播放:") + payload.talkName;
break;
case 'TempCall':
msg = "临时呼叫消息: " + payload.msg;
break;
case 'UpdateGroup':
msg = "进入群组: " + payload.groupName;
break;
case 'Alarm':
msg = "⚠️收到报警: " + payload.memberName + " (纬度:" + payload.latitude + ")";
break;
case 'StopAlarm':
msg = "群成员停止告警";
break;
case 'SpeakResult':
msg = payload.success ? "话权获取成功" : "话权获取失败";
break;
case 'SpeakEnd':
msg = payload.success ? "话权释放成功" : "话权释放失败";
break;
default:
msg = "收到事件: " + eventType;
}
showPttResult("[回调] " + msg);
};
// ES6 特性测试函数(通过 eval 避免阻塞旧版 JS 引擎整体解析)
function testES6Features() {
try {
var code = [
"const arr = [1, 2, 3];",
"const [a, ...rest] = arr;",
"let sum = rest.reduce((acc, val) => acc + val, 0);",
"const myMap = new Map();",
"myMap.set('key', `Sum is ${sum}`);",
"new Promise((resolve) => {",
" setTimeout(() => resolve(myMap.get('key')), 100);",
"}).then(res => {",
" alert(`ES6 引擎测试成功!\\n解构: a=${a}\\nPromise/Map 返回: ${res}`);",
"});"
].join("");
eval(code);
} catch(e) {
alert("ES6 测试失败 (当前环境可能不支持): " + e.message);
}
}
function addExitText() {
var tag = document.getElementById('inject-status');
if (tag) {
var notice = document.createElement('div');
notice.className = 'exit-notice';
notice.innerText = "⚠️ 检测到退出尝试 (" + new Date().toLocaleTimeString() + ")";
tag.parentNode.insertBefore(notice, tag.nextSibling);
}
}
// 延迟检测
setTimeout(checkApp, 300);
// 同步 PTT 登录状态
setTimeout(function() {
if (window.android && window.android.pttGetLoginStatus) {
var status = window.android.pttGetLoginStatus();
var loginId = window.android.pttGetLoginId ? window.android.pttGetLoginId() : "";
updatePttLoginStatus(status, loginId);
}
if (window.android && window.android.pttGetCurrentGroup) {
try {
var group = JSON.parse(window.android.pttGetCurrentGroup());
if (group.groupId) {
updatePttCurrentGroup(group.groupId, group.groupName);
}
} catch(e) {}
}
}, 500);
// 告知 Android 页面加载完成(已移至 checkApp 中,等 bridge 注入后再调用)
window.onload = function() {};
// === WebSocket 逻辑 ===
var ws = null;
var clientId = "WEB_" + Math.random().toString(36).substr(2, 9);
var heartbeatTimer = null;
var reconnectTimer = null;
var isManualClose = false;
var logList = document.getElementById('log-list');
document.getElementById('ws-client-id').innerText = clientId;
function addLog(msg, save) {
if (save === undefined) save = true;
var time = new Date().toLocaleTimeString();
var item = document.createElement('div');
item.className = 'log-item';
item.innerHTML = '<span class="log-time">[' + time + ']</span><span class="log-msg">' + msg + '</span>';
logList.insertBefore(item, logList.firstChild);
if (logList.childNodes.length > 200) {
logList.removeChild(logList.lastChild);
}
if (save && window.android && window.android.saveMessage) {
window.android.saveMessage(time + ": " + msg);
}
}
function updateWSStatus(connected) {
var tag = document.getElementById('ws-status-tag');
if (connected) {
tag.innerText = "已连接";
tag.className = "ws-status ws-connected";
} else {
tag.innerText = "已断开";
tag.className = "ws-status ws-disconnected";
}
}
function connectWS() {
if (ws) {
ws.onclose = null;
ws.close();
}
var rawUrl = document.getElementById('ws-url-input').value;
var finalUrl = rawUrl.replace("{clientId}", clientId);
addLog("正在尝试连接: " + finalUrl, false);
isManualClose = false;
try {
ws = new WebSocket(finalUrl);
ws.onopen = function() {
updateWSStatus(true);
addLog("连接成功", false);
startHeartbeat();
clearTimeout(reconnectTimer);
};
ws.onmessage = function(evt) {
addLog("收到消息: " + evt.data);
if (evt.data.indexOf("print") !== -1) {
callRawPrint();
}
};
ws.onclose = function() {
updateWSStatus(false);
addLog("连接关闭", false);
stopHeartbeat();
if (!isManualClose) {
addLog("异常断开5秒后自动重连...", false);
reconnectTimer = setTimeout(connectWS, 5000);
}
};
ws.onerror = function(err) {
addLog("连接错误或被拒绝", false);
};
} catch (e) {
addLog("连接失败: " + e.message, false);
}
}
function toggleWS() {
if (ws && ws.readyState === WebSocket.OPEN) {
isManualClose = true;
clearTimeout(reconnectTimer);
reconnectTimer = null;
ws.close();
} else {
connectWS();
}
}
function startHeartbeat() {
stopHeartbeat();
heartbeatTimer = setInterval(function() {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send("ping");
}
}, 30000);
}
function stopHeartbeat() {
clearInterval(heartbeatTimer);
}
function clearHistory() {
if (confirm("确定要清空安卓端存储的所有消息吗?")) {
if (window.android && window.android.clearMessages) {
window.android.clearMessages();
}
logList.innerHTML = '<div class="log-item">记录已清空</div>';
}
}
function manualAddRecord() {
var text = prompt("请输入要保存的消息内容:", "手动测试记录");
if (text) {
addLog("用户新增: " + text);
}
}
function loadHistory() {
if (window.android && window.android.getMessages) {
var history = window.android.getMessages();
logList.innerHTML = "";
if (history) {
var msgs = history.split("|---|");
addLog("系统: 已从安卓端同步 " + msgs.length + " 条记录", false);
for (var i = 0; i < msgs.length; i++) {
var msg = msgs[i];
if (!msg || i > 500) continue;
var item = document.createElement('div');
item.className = 'log-item';
var separatorIndex = msg.indexOf(": ");
if (separatorIndex === -1) continue;
var time = msg.substring(0, separatorIndex);
var content = msg.substring(separatorIndex + 2);
item.innerHTML = '<span class="log-time">[' + time + ']</span><span class="log-msg">' + content + '</span>';
logList.appendChild(item);
}
} else {
addLog("系统: 终端暂无保存的记录", false);
}
} else {
// 不再弹窗checkApp 会在 bridge 就绪后调用 loadHistory
}
}
// === PTT 业务测试逻辑 ===
var pttBtn = document.getElementById('ptt-speak-btn');
var pttStatusTag = document.getElementById('ptt-status-tag');
var pttResult = document.getElementById('ptt-result');
function showPttResult(msg) {
var time = new Date().toLocaleTimeString();
var item = document.createElement('div');
item.className = 'log-item';
item.innerHTML = '<span class="log-time">[' + time + ']</span><span class="log-msg">' + msg + '</span>';
pttResult.insertBefore(item, pttResult.firstChild);
}
// 更新 PTT 通话状态 (由 Android 原生同步调用)
function updatePttTalkStatus(isTalking) {
var statusNode = document.getElementById('ptt-talk-status');
if (isTalking) {
statusNode.style.display = 'inline-block';
statusNode.style.background = '#fee2e2';
statusNode.style.color = '#ef4444';
statusNode.innerText = '● 正在通话';
// 如果 UI 按钮还没更新,强制更新一次
pttBtn.style.background = "#b91c1c";
pttBtn.innerText = "正在通话...";
} else {
statusNode.style.display = 'none';
pttBtn.style.background = "#ef4444";
pttBtn.innerText = "长按对讲 (PTT)";
}
};
// 更新当前群组信息 (由 Android 实时推送)
function updatePttCurrentGroup(groupId, groupName) {
document.getElementById('ptt-current-group').innerText = groupName || '-';
document.getElementById('ptt-current-group-id').innerText = groupId ? '(' + groupId + ')' : '';
showPttResult("进入群组: " + groupName + " (" + groupId + ")");
};
// PTT 登录成功回调 (由 Android 调用)
function onPttLoginSuccess() {
updatePttLoginStatusUI("02");
showPttResult("PTT 业务初始化成功,对讲功能已就绪");
}
// 更新 PTT 登录状态 (由 Android 实时推送)
function updatePttLoginStatus(status, loginId) {
updatePttLoginStatusUI(status);
var statusMap = { "00": "未登录", "01": "登录中...", "02": "已登录" };
showPttResult("登录状态变更: " + (statusMap[status] || status) + " (ID:" + loginId + ")");
};
function updatePttLoginStatusUI(status) {
if (status === "02") {
pttStatusTag.innerText = "PTT 已登录";
pttStatusTag.className = "ws-status ws-connected";
pttBtn.disabled = false;
pttBtn.style.background = "#ef4444";
pttBtn.style.cursor = "pointer";
var actionBtns = document.querySelectorAll('.ptt-action-btn');
for (var i = 0; i < actionBtns.length; i++) {
actionBtns[i].disabled = false;
}
} else if (status === "01") {
pttStatusTag.innerText = "PTT 登录中...";
pttStatusTag.className = "ws-status ws-disconnected";
} else {
pttStatusTag.innerText = "PTT 未登录";
pttStatusTag.className = "ws-status ws-disconnected";
}
}
// 修复 PTT 按键逻辑:支持 TouchEvents 并处理异常中断
if (pttBtn) {
var isDown = false;
var lastTouchTime = 0;
var startSpeak = function(e) {
if (pttBtn.disabled) return;
e.preventDefault();
var now = Date.now();
if (now - lastTouchTime < 300) return; // 物理防抖300ms 内禁止重复触发按下
if (!isDown) {
isDown = true;
lastTouchTime = now;
pttBtn.style.background = "#b91c1c";
pttBtn.innerText = "正在通话...";
if (window.android && window.android.pttSpeakPlay) {
window.android.pttSpeakPlay();
}
showPttResult("申请话权...");
}
};
var stopSpeak = function(e) {
if (isDown) {
// 延迟 50ms 再处理释放确保即使在极速点击下Down 指令也先到达原生层
setTimeout(function() {
if (!isDown) return;
isDown = false;
pttBtn.style.background = "#ef4444";
pttBtn.innerText = "长按对讲 (PTT)";
if (window.android && window.android.pttSpeakRelease) {
window.android.pttSpeakRelease();
}
showPttResult("释放话权");
}, 50);
}
};
// 绑定事件
pttBtn.addEventListener('touchstart', startSpeak);
pttBtn.addEventListener('mousedown', startSpeak);
// 关键:除了 end/up还要处理 cancel 和 leave
pttBtn.addEventListener('touchend', stopSpeak);
pttBtn.addEventListener('touchcancel', stopSpeak);
pttBtn.addEventListener('mouseup', stopSpeak);
pttBtn.addEventListener('mouseleave', stopSpeak);
}
function pttGetGroups() {
if (window.android && window.android.pttGetGroupInfo) {
window.android.pttGetGroupInfo();
showPttResult("已请求群组列表");
}
}
function pttGetMembers() {
var gid = document.getElementById('ptt-gid').value;
if (window.android && window.android.pttGetGroupMemberInfo) {
window.android.pttGetGroupMemberInfo(gid);
showPttResult("已请求群组 [" + gid + "] 成员");
}
}
function pttJumpGroup() {
var gid = document.getElementById('ptt-gid').value;
if (window.android && window.android.pttJumpGroup) {
window.android.pttJumpGroup(gid);
showPttResult("尝试切换至群组: " + gid);
}
}
function pttForceRelease() {
if (window.android && window.android.pttForceRelease) {
window.android.pttForceRelease();
isDown = false;
pttBtn.style.background = "#ef4444";
pttBtn.innerText = "长按对讲 (PTT)";
showPttResult("执行强制释放");
}
}
function pttSimulateConflict() {
if (window.android && window.android.simulatePttConflict) {
window.android.simulatePttConflict();
showPttResult("已触发模拟账号冲突");
}
}
function pttGetCurrentGroup() {
if (window.android && window.android.pttGetCurrentGroup) {
try {
var group = JSON.parse(window.android.pttGetCurrentGroup());
if (group.groupId) {
updatePttCurrentGroup(group.groupId, group.groupName);
} else {
showPttResult("当前无群组信息");
}
} catch(e) {
showPttResult("获取群组信息失败");
}
}
}
function pttSendAtCmd() {
var cmd = document.getElementById('ptt-at-cmd').value.trim();
if (!cmd) {
showPttResult("请输入 AT 指令");
return;
}
if (window.android && window.android.pttSendAtCmd) {
window.android.pttSendAtCmd(cmd);
showPttResult("已发送 AT 指令: " + cmd);
}
}
</script>
</body>
</html>