打印机接口

This commit is contained in:
fengge 2026-04-21 23:54:40 +08:00
parent d3e4a6de53
commit a87aac59ce
6 changed files with 419 additions and 11 deletions

View File

@ -76,10 +76,44 @@
<div class="info-box">
<strong>设备信息:</strong><br>
应用版本: <span id="app-version">Unknown</span><br>
环境检测: <span id="env-type">Browser</span>
环境检测: <span id="env-type">Browser</span><br>
打印机: <span id="printer-status" style="font-weight: bold; color: #ef4444;">未连接</span>
</div>
<button class="btn" onclick="callSystemPrint()">1. 调用系统标准打印</button>
<!-- 打印功能网格 -->
<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>
</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>
@ -219,6 +253,134 @@
if (window.android.getVersionName) {
versionSpan.innerText = window.android.getVersionName();
}
updatePrinterStatus();
// 每 3 秒同步一次打印机状态
setInterval(updatePrinterStatus, 3000);
}
}
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);
}
}

View File

@ -9,11 +9,152 @@ import android.print.PrintAttributes
import android.print.PrintManager
import com.stand.standapp.R
import com.stand.standapp.utils.LogManager
import com.stand.standapp.printer.PrinterManager
import timber.log.Timber
import org.json.JSONObject
class AndroidInterface(private val activity: MainActivity) {
/**
* JS 调用获取打印机当前连接状态
*/
@JavascriptInterface
fun getPrinterStatus(): String {
return try {
val json = JSONObject()
val connected = PrinterManager.isConnected()
json.put("connected", connected)
json.put("hasPaper", PrinterManager.isPaperReady())
json.put("port", AppConfig.getSerialPort(activity))
json.toString()
} catch (e: Exception) {
"{\"connected\":false, \"hasPaper\":false, \"error\":\"${e.message}\"}"
}
}
/**
* JS 调用通用打印文本
*/
@JavascriptInterface
fun printText(text: String?, size: Int, align: Int, space: Int) {
if (text.isNullOrEmpty()) return
try {
PrinterManager.getFactory()?.let {
if (it.isConnection) {
it.PrintText(text, size, align, space)
}
}
} catch (e: Throwable) {
Timber.tag("JS_Interface").e(e, "printText failed")
}
}
/**
* JS 调用打印条码
*/
@JavascriptInterface
fun printBarcode(data: String?, size: Int, height: Int, align: Int, text_pos: Int, type: Int) {
if (data.isNullOrEmpty()) return
try {
PrinterManager.getFactory()?.PrintBarcode(data, size, height, align, text_pos, type)
} catch (e: Throwable) {
Timber.tag("JS_Interface").e(e, "printBarcode failed")
}
}
/**
* JS 调用打印二维码
*/
@JavascriptInterface
fun printQR(data: String?, size: Int, l_m: Int) {
if (data.isNullOrEmpty()) return
try {
PrinterManager.getFactory()?.PrintQR(data, size, l_m)
} catch (e: Throwable) {
Timber.tag("JS_Interface").e(e, "printQR failed")
}
}
/**
* JS 调用打印图片 ( Assets 获取)
*/
@JavascriptInterface
fun printImage(fileName: String?, width: Int, align: Int, mode: Int) {
if (fileName.isNullOrEmpty()) return
try {
val stream = activity.assets.open(fileName)
val bitmap = android.graphics.BitmapFactory.decodeStream(stream)
PrinterManager.getFactory()?.PrintImage(bitmap, width, align, mode)
stream.close()
} catch (e: Exception) {
Timber.tag("JS_Interface").e(e, "printImage failed: $fileName")
}
}
/**
* JS 调用分栏打印 (4)
*/
@JavascriptInterface
fun printColumns(c1: String?, p1: Int, c2: String?, p2: Int, c3: String?, p3: Int, c4: String?, p4: Int) {
try {
PrinterManager.getFactory()?.Printcolumncontent(c1 ?: "", p1, c2 ?: "", p2, c3 ?: "", p3, c4 ?: "", p4)
} catch (e: Throwable) {
Timber.tag("JS_Interface").e(e, "printColumns failed")
}
}
/**
* JS 调用分栏打印 (带行高)
*/
@JavascriptInterface
fun printColumnsWithHeight(c1: String?, p1: Int, c2: String?, p2: Int, c3: String?, p3: Int, c4: String?, p4: Int, height: Int) {
try {
PrinterManager.getFactory()?.Printcolumncontent(c1 ?: "", p1, c2 ?: "", p2, c3 ?: "", p3, c4 ?: "", p4, height)
} catch (e: Throwable) {
Timber.tag("JS_Interface").e(e, "printColumnsWithHeight failed")
}
}
/**
* JS 调用切纸
*/
@JavascriptInterface
fun cutPaper(allCut: Boolean) {
try {
PrinterManager.getFactory()?.let {
if (it.isConnection) {
if (allCut) it.PaperAllCut() else it.PaperCut()
}
}
} catch (e: Throwable) {
Timber.tag("JS_Interface").e(e, "cutPaper failed")
}
}
/**
* JS 调用打印自检页
*/
@JavascriptInterface
fun printTestPage() {
try {
PrinterManager.getFactory()?.PrintTestPage()
} catch (e: Throwable) {
Timber.tag("JS_Interface").e(e, "printTestPage failed")
}
}
/**
* JS 调用主动检查纸张状态 (会触发回调)
*/
@JavascriptInterface
fun checkPaper() {
try {
PrinterManager.getFactory()?.Check_Paper()
} catch (e: Throwable) {
Timber.tag("JS_Interface").e(e, "checkPaper failed")
}
}
/**
* JS 调用压缩并导出日志
* 返回压缩后的文件路径
@ -76,15 +217,15 @@ class AndroidInterface(private val activity: MainActivity) {
/**
* JS 调用原始数据打印
* 适用于热敏小票打印机 (蓝牙USB网络)
* @param data 可以是 JSON 字符串包含打印的指令或文本
* @param data String 可以是 JSON 字符串包含打印的指令或文本
*/
@JavascriptInterface
fun printRawData(data: String) {
fun printRawData(data: String?) {
if (data.isNullOrEmpty()) return
try {
Timber.tag("Print").d("收到 H5 原始打印数据: $data")
Handler(Looper.getMainLooper()).post {
try {
// 这里可以弹出一个选择框让用户选择打印机,或者根据配置自动连接
Toast.makeText(activity, "H5指令打印: $data", Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
Timber.tag("Print").e(e, "Toast显示失败")
@ -99,7 +240,7 @@ class AndroidInterface(private val activity: MainActivity) {
* JS 调用在线升级 (手动触发检查)
*/
@JavascriptInterface
fun startUpdate(apkUrl: String) {
fun startUpdate(apkUrl: String?) {
Handler(Looper.getMainLooper()).post {
try {
UpdateManager(activity).checkUpdate()
@ -172,7 +313,8 @@ class AndroidInterface(private val activity: MainActivity) {
* JS 调用保存消息记录
*/
@JavascriptInterface
fun saveMessage(message: String) {
fun saveMessage(message: String?) {
if (message.isNullOrEmpty()) return
try {
val prefs = activity.getSharedPreferences("msg_records", Context.MODE_PRIVATE)
val currentMsgs = prefs.getString("history", "") ?: ""

View File

@ -13,7 +13,7 @@ object AppConfig {
// 默认配置
const val DEFAULT_SERVER_URL = "https://template.gyouzhe.com"
const val DEFAULT_SERIAL_PORT = "/dev/ttyACM0"
const val DEFAULT_SERIAL_PORT = "ttyACM0"
private fun getPrefs(context: Context): SharedPreferences {
return context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)

View File

@ -127,8 +127,12 @@ class LoginActivity : AppCompatActivity() {
AppConfig.setSavedPassword(this@LoginActivity, "")
}
Toast.makeText(this@LoginActivity, "登录成功", Toast.LENGTH_SHORT).show()
startActivity(Intent(this@LoginActivity, MainActivity::class.java))
Toast.makeText(this@LoginActivity, "登录成功", Toast.LENGTH_SHORT).show()
// 初始化打印机 (自动重连)
com.stand.standapp.printer.PrinterManager.init(this@LoginActivity)
startActivity(Intent(this@LoginActivity, MainActivity::class.java))
finish()
} else {
val msg = json.optString("msg", "登录失败")

View File

@ -0,0 +1,100 @@
package com.stand.standapp.printer
import android.content.Context
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import com.dp.dp_serialportlist.Serialport_Factory
import com.stand.standapp.AppConfig
import timber.log.Timber
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
object PrinterManager {
private var sf: Serialport_Factory? = null
private var isConnecting = false
private val executor = Executors.newSingleThreadScheduledExecutor()
private var mContext: Context? = null
private var hasPaper: Boolean = true // 默认为有纸
private val mHandler = Handler(Looper.getMainLooper()) { msg ->
if (msg.what == Serialport_Factory.PAPER_STATE) {
hasPaper = msg.data.getBoolean("PAPER_STATE")
val statusStr = if (hasPaper) "有纸" else "缺纸"
Timber.tag("Printer").i("打印机状态变更: %s", statusStr)
// 在 Android 系统层面主动提示
mContext?.let {
if (!hasPaper) {
android.widget.Toast.makeText(it, "⚠️ 警告:打印机缺纸!", android.widget.Toast.LENGTH_LONG).show()
}
}
}
true
}
fun isPaperReady(): Boolean = hasPaper
fun init(context: Context) {
if (sf != null) return
mContext = context.applicationContext
// 全异步初始化,不阻塞主线程跳转
executor.execute {
try {
Timber.tag("Printer").i("开始异步初始化打印机组件...")
sf = Serialport_Factory.getSerialport_Factory(mContext, mHandler)
// 启动自动重连定时任务
executor.scheduleWithFixedDelay({
checkAndConnect()
}, 0, 10, TimeUnit.SECONDS)
Timber.tag("Printer").i("打印机组件初始化任务已分发")
} catch (e: Exception) {
Timber.tag("Printer").e(e, "打印机组件初始化失败")
}
}
}
@Synchronized
private fun checkAndConnect() {
val factory = sf ?: return
if (factory.isConnection) return
val path = AppConfig.getSerialPort(mContext!!)
val baudrate = "9600"
Timber.tag("Printer").d("尝试自动连接打印机: %s @ %s", path, baudrate)
try {
if (factory.OpenPort(path, baudrate)) {
Timber.tag("Printer").i("打印机自动连接成功")
}
} catch (e: Throwable) {
Timber.tag("Printer").e(e, "打印机自动连接异常")
}
}
fun getFactory(): Serialport_Factory? = sf
fun isConnected(): Boolean = sf?.isConnection ?: false
/**
* 提供便捷的打印方法
*/
fun printText(text: String, size: Int = 1, align: Int = 1, space: Int = 10) {
sf?.let {
if (it.isConnection) {
it.PrintText(text, size, align, space)
}
}
}
fun cutPaper() {
sf?.let {
if (it.isConnection) {
it.PaperCut()
}
}
}
}

View File

@ -76,7 +76,7 @@
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="@drawable/bg_input_field"
android:hint="/dev/ttyACM0"
android:hint="ttyACM0"
android:inputType="text"
android:paddingHorizontal="16dp"
android:textColor="#0F172A"