feat: 添加打印机黑标检测开关和走纸长度设置

- 支持黑标检测开关,关闭时用走纸长度替代

- 打印机设置页面:黑标开关、走纸长度输入、测试入口

- SessionManager 持久化打印机配置

- 新增 PrinterTestScreen 测试页面

- VERSION_CODE 82 -> 90
This commit is contained in:
2026-06-26 15:43:57 +08:00
parent c493dbc31c
commit 627099159f
10 changed files with 373 additions and 21 deletions
@@ -5,6 +5,7 @@ import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import com.example.ops_android.data.model.Cookie
@@ -25,6 +26,8 @@ class SessionManager(private val context: Context) {
private val KEY_COOKIE_NAME = stringPreferencesKey("cookie_name")
private val KEY_COOKIE_EXPIRES_AT = stringPreferencesKey("cookie_expires_at")
private val KEY_COOKIE_REMEMBER = booleanPreferencesKey("cookie_remember")
private val KEY_PRINTER_BLACK_MARK = booleanPreferencesKey("printer_black_mark")
private val KEY_PRINTER_UNWIND_MM = intPreferencesKey("printer_unwind_mm")
}
@Volatile
@@ -35,6 +38,14 @@ class SessionManager(private val context: Context) {
var cachedCookieValue: String = ""
private set
@Volatile
var cachedBlackMarkEnabled: Boolean = true
private set
@Volatile
var cachedUnwindMm: Int = 40
private set
fun clearCookieCache() {
cachedCookieValue = ""
}
@@ -43,9 +54,11 @@ class SessionManager(private val context: Context) {
val cookieValue: Flow<String> = context.dataStore.data.map { prefs -> prefs[KEY_COOKIE_VALUE] ?: "" }
suspend fun init() {
cachedBaseUrl = context.dataStore.data.first()[KEY_API_BASE_URL] ?: ""
val value = context.dataStore.data.first()[KEY_COOKIE_VALUE]
cachedCookieValue = value ?: ""
val prefs = context.dataStore.data.first()
cachedBaseUrl = prefs[KEY_API_BASE_URL] ?: ""
cachedCookieValue = prefs[KEY_COOKIE_VALUE] ?: ""
cachedBlackMarkEnabled = prefs[KEY_PRINTER_BLACK_MARK] ?: true
cachedUnwindMm = prefs[KEY_PRINTER_UNWIND_MM] ?: 40
}
suspend fun getApiBaseUrl(): String = cachedBaseUrl.ifEmpty {
@@ -92,4 +105,14 @@ class SessionManager(private val context: Context) {
}
suspend fun logout() = clearCookie()
suspend fun setBlackMarkEnabled(enabled: Boolean) {
cachedBlackMarkEnabled = enabled
context.dataStore.edit { it[KEY_PRINTER_BLACK_MARK] = enabled }
}
suspend fun setUnwindMm(mm: Int) {
cachedUnwindMm = mm
context.dataStore.edit { it[KEY_PRINTER_UNWIND_MM] = mm }
}
}
@@ -15,7 +15,9 @@ object PrintHelper {
suspend fun printOrderLabel(
printerManager: PrinterManager,
order: PurchaseOrder
order: PurchaseOrder,
blackMarkEnabled: Boolean = true,
unwindMm: Int = 40
): Result<Unit> = printMutex.withLock {
withContext(Dispatchers.IO) {
try {
@@ -28,8 +30,12 @@ object PrintHelper {
if (order.id > 0) {
printerManager.addBarcode("PO-${order.id}")
}
printerManager.addLineFeed(2)
printerManager.enableMarkDetection(true)
if (blackMarkEnabled) {
printerManager.addLineFeed(2)
printerManager.enableMarkDetection(true)
} else {
printerManager.addLineFeed((unwindMm / 7).toInt().coerceAtLeast(2))
}
printerManager.commit().fold(
onSuccess = {
delay(1000)
@@ -46,7 +52,9 @@ object PrintHelper {
suspend fun printWorkOrderLabel(
printerManager: PrinterManager,
workOrder: WorkOrder
workOrder: WorkOrder,
blackMarkEnabled: Boolean = true,
unwindMm: Int = 40
): Result<Unit> = printMutex.withLock {
withContext(Dispatchers.IO) {
try {
@@ -59,8 +67,12 @@ object PrintHelper {
if (workOrder.id > 0) {
printerManager.addBarcode("WO-${workOrder.id}")
}
printerManager.addLineFeed(2)
printerManager.enableMarkDetection(true)
if (blackMarkEnabled) {
printerManager.addLineFeed(2)
printerManager.enableMarkDetection(true)
} else {
printerManager.addLineFeed((unwindMm / 7).toInt().coerceAtLeast(2))
}
printerManager.commit().fold(
onSuccess = {
delay(1000)
@@ -77,7 +89,9 @@ object PrintHelper {
suspend fun printItemLabel(
printerManager: PrinterManager,
item: Item
item: Item,
blackMarkEnabled: Boolean = true,
unwindMm: Int = 40
): Result<Unit> = printMutex.withLock {
withContext(Dispatchers.IO) {
try {
@@ -91,8 +105,12 @@ object PrintHelper {
if (item.id > 0) {
printerManager.addBarcode("ITEM-${item.id}")
}
printerManager.addLineFeed(2)
printerManager.enableMarkDetection(true)
if (blackMarkEnabled) {
printerManager.addLineFeed(2)
printerManager.enableMarkDetection(true)
} else {
printerManager.addLineFeed((unwindMm / 7).toInt().coerceAtLeast(2))
}
printerManager.commit().fold(
onSuccess = {
delay(1000)
@@ -110,7 +128,9 @@ object PrintHelper {
suspend fun printContainerLabel(
printerManager: PrinterManager,
containerName: String,
barcode: String? = null
barcode: String? = null,
blackMarkEnabled: Boolean = true,
unwindMm: Int = 40
): Result<Unit> = printMutex.withLock {
withContext(Dispatchers.IO) {
try {
@@ -123,8 +143,12 @@ object PrintHelper {
printerManager.setBold(false)
printerManager.addBarcode(barcode)
}
printerManager.addLineFeed(2)
printerManager.enableMarkDetection(true)
if (blackMarkEnabled) {
printerManager.addLineFeed(2)
printerManager.enableMarkDetection(true)
} else {
printerManager.addLineFeed((unwindMm / 7).toInt().coerceAtLeast(2))
}
printerManager.commit().fold(
onSuccess = {
delay(1000)
@@ -255,6 +255,20 @@ class PrinterManager(private val context: Context) {
printManager!!.setUnwindPaperLen(len)
}
fun getUnwindPaperLenInt(): Int? = try {
printManager?.unwindPaperLen ?: printManager?.getUnwindPaperLen()
} catch (e: Exception) {
Log.e(TAG, "getUnwindPaperLen failed", e)
null
}
fun getFeedPaperSpaceInt(): Int? = try {
printManager?.feedPaperSpace ?: printManager?.getFeedPaperSpace()
} catch (e: Exception) {
Log.e(TAG, "getFeedPaperSpace failed", e)
null
}
// ══════════════════════════════════════════════════════════
// 黑标 / 标签
// ══════════════════════════════════════════════════════════
@@ -29,6 +29,7 @@ import com.example.ops_android.ui.profile.ProfileScreen
import com.example.ops_android.ui.schedule.ScheduleScreen
import com.example.ops_android.ui.search.SearchScreen
import com.example.ops_android.ui.settings.SettingsScreen
import com.example.ops_android.ui.settings.PrinterTestScreen
import com.example.ops_android.ui.warehouse.ItemDetailScreen
import com.example.ops_android.ui.warehouse.ItemFormScreen
import com.example.ops_android.ui.warehouse.WarehouseScreen
@@ -49,6 +50,7 @@ object Routes {
const val ITEM_DETAIL = "item_detail/{id}"
const val ITEM_FORM = "item_form/{containerId}?editId={editId}"
const val CUSTOMER_LIST = "customer_list"
const val PRINTER_TEST = "printer_test"
fun orderDetail(orderId: Int) = "order_detail/$orderId"
fun orderForm(editId: Int? = null) = if (editId != null) "order_form?editId=$editId" else "order_form"
@@ -95,7 +97,13 @@ fun OPSNavGraph(isLoggedIn: Boolean) {
)
}
composable(Routes.SETTINGS) {
SettingsScreen(onBack = { rootNavController.popBackStack() })
SettingsScreen(
onBack = { rootNavController.popBackStack() },
onPrinterTest = { rootNavController.navigate(Routes.PRINTER_TEST) }
)
}
composable(Routes.PRINTER_TEST) {
PrinterTestScreen(onBack = { rootNavController.popBackStack() })
}
composable(Routes.SEARCH) {
SearchScreen(
@@ -58,9 +58,10 @@ fun OrderDetailScreen(
order?.let { o ->
val printerManager = LocalAppContainer.current.printerManager
if (printerManager != null) {
val sm = LocalAppContainer.current.sessionManager
IconButton(onClick = {
scope.launch {
com.example.ops_android.printer.PrintHelper.printOrderLabel(printerManager, o)
com.example.ops_android.printer.PrintHelper.printOrderLabel(printerManager, o, sm.cachedBlackMarkEnabled, sm.cachedUnwindMm)
}
}) {
Icon(Icons.Default.Print, "打印", tint = Color.White)
@@ -0,0 +1,225 @@
package com.example.ops_android.ui.settings
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.ops_android.ui.LocalAppContainer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PrinterTestScreen(onBack: () -> Unit) {
val appContainer = LocalAppContainer.current
val printerManager = appContainer.printerManager
val sessionManager = appContainer.sessionManager
val scope = rememberCoroutineScope()
var isTesting by remember { mutableStateOf(false) }
var testResult by remember { mutableStateOf<String?>(null) }
var testSuccess by remember { mutableStateOf<Boolean?>(null) }
val isConnected = remember { derivedStateOf { printerManager?.isConnected() == true } }
val firmware = remember { derivedStateOf { printerManager?.getFirmwareVersion() } }
val unwindMm = remember { derivedStateOf { sessionManager.cachedUnwindMm } }
val blackMarkEnabled = remember { derivedStateOf { sessionManager.cachedBlackMarkEnabled } }
fun runTest(label: String, action: suspend () -> Unit) {
val pm = printerManager ?: return
isTesting = true
testResult = null
testSuccess = null
scope.launch {
withContext(Dispatchers.IO) {
try {
action()
testSuccess = true
testResult = "[$label] 执行完成"
} catch (e: Exception) {
testSuccess = false
testResult = "[$label] 失败: ${e.message}"
} finally {
isTesting = false
}
}
}
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("打印机测试") },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "返回")
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color(0xFF667EEA),
titleContentColor = Color.White,
navigationIconContentColor = Color.White
)
)
}
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(16.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Card {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text("连接状态", color = Color.Gray)
Text(
if (isConnected.value) "已连接" else "未连接",
color = if (isConnected.value) Color(0xFF4CAF50) else Color(0xFFF44336)
)
}
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text("固件版本", color = Color.Gray)
Text(firmware.value ?: "未知")
}
}
}
Card {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text("黑标检测", color = Color.Gray)
Text(if (blackMarkEnabled.value) "启用" else "禁用")
}
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text("走纸长度", color = Color.Gray)
Text("${unwindMm.value} mm")
}
}
}
HorizontalDivider()
Text("走纸测试", style = MaterialTheme.typography.titleMedium)
Button(
onClick = {
runTest("空走纸") {
val pm = printerManager!!
pm.initDevice().getOrThrow()
pm.commit().getOrThrow()
delay(1000)
pm.closeDevice().getOrThrow()
}
},
modifier = Modifier.fillMaxWidth(),
enabled = isConnected.value && !isTesting
) {
Text("测试A:空走纸(无内容,看默认基线)")
}
Button(
onClick = {
runTest("空走纸 + 5行换行") {
val pm = printerManager!!
pm.initDevice().getOrThrow()
pm.addLineFeed(5)
pm.commit().getOrThrow()
delay(1000)
pm.closeDevice().getOrThrow()
}
},
modifier = Modifier.fillMaxWidth(),
enabled = isConnected.value && !isTesting,
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF5C6BC0))
) {
Text("测试B:空走纸 + 5行换行")
}
Button(
onClick = {
runTest("空走纸 + 10行换行") {
val pm = printerManager!!
pm.initDevice().getOrThrow()
pm.addLineFeed(10)
pm.commit().getOrThrow()
delay(1000)
pm.closeDevice().getOrThrow()
}
},
modifier = Modifier.fillMaxWidth(),
enabled = isConnected.value && !isTesting,
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF8D6E63))
) {
Text("测试C:空走纸 + 10行换行")
}
Text(
"目标:对比B(5行)和C(10行)的出纸长度,算出每行=多少mm,填入代码中校准",
fontSize = 13.sp,
color = Color.Gray
)
HorizontalDivider()
Text("SDK调试", style = MaterialTheme.typography.titleMedium)
Button(
onClick = {
val pm = printerManager
if (pm != null) {
val unwindVal = pm.getUnwindPaperLenInt()
val feedSpaceVal = pm.getFeedPaperSpaceInt()
testSuccess = null
testResult = "getUnwindPaperLen = $unwindVal, getFeedPaperSpace = $feedSpaceVal"
}
},
modifier = Modifier.fillMaxWidth(),
enabled = isConnected.value && !isTesting
) {
Text("读取 SDK 内部走纸参数")
}
if (!isConnected.value && printerManager != null) {
Button(
onClick = { printerManager?.bindService() },
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondary)
) {
Text("绑定打印服务")
}
}
testResult?.let {
Card(
colors = CardDefaults.cardColors(
containerColor = if (testSuccess == true) Color(0xFFE8F5E9) else if (testSuccess == false) Color(0xFFFFEBEE) else Color(0xFFF5F5F5)
)
) {
Text(
text = it,
modifier = Modifier.padding(12.dp),
color = when (testSuccess) {
true -> Color(0xFF2E7D32)
false -> Color(0xFFC62828)
else -> Color.Gray
},
fontSize = 14.sp
)
}
}
}
}
}
@@ -20,6 +20,7 @@ import com.example.ops_android.ui.LocalAppContainer
@Composable
fun SettingsScreen(
onBack: () -> Unit,
onPrinterTest: () -> Unit = {},
viewModel: SettingsViewModel = viewModel(factory = SettingsViewModel.Factory(LocalAppContainer.current.sessionManager))
) {
val uiState by viewModel.uiState.collectAsState()
@@ -121,6 +122,32 @@ fun SettingsScreen(
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
Text("打印机设置", style = MaterialTheme.typography.titleMedium)
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
Text("黑标检测")
Switch(checked = uiState.blackMarkEnabled, onCheckedChange = viewModel::onBlackMarkChange)
}
if (!uiState.blackMarkEnabled) {
OutlinedTextField(
value = uiState.unwindMm,
onValueChange = viewModel::onUnwindMmChange,
label = { Text("单次走纸长度 (mm)") },
suffix = { Text("mm") },
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
Button(onClick = { viewModel.saveUnwindMm() }, modifier = Modifier.fillMaxWidth()) {
Text("保存走纸长度")
}
Button(onClick = onPrinterTest, modifier = Modifier.fillMaxWidth(), colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondary)) {
Text("测试打印机")
}
}
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
Text(
text = "关于",
style = MaterialTheme.typography.titleMedium
@@ -23,7 +23,9 @@ data class SettingsUiState(
val isTesting: Boolean = false,
val testResult: String? = null,
val testSuccess: Boolean = false,
val message: String? = null
val message: String? = null,
val blackMarkEnabled: Boolean = true,
val unwindMm: String = "40"
)
class SettingsViewModel(
@@ -41,6 +43,7 @@ class SettingsViewModel(
init {
loadCurrentUrl()
loadPrinterSettings()
}
private fun loadCurrentUrl() {
@@ -50,10 +53,36 @@ class SettingsViewModel(
}
}
private fun loadPrinterSettings() {
_uiState.value = _uiState.value.copy(
blackMarkEnabled = sessionManager.cachedBlackMarkEnabled,
unwindMm = sessionManager.cachedUnwindMm.toString()
)
}
fun onUrlChange(value: String) {
_uiState.value = _uiState.value.copy(apiBaseUrl = value, message = null)
}
fun onBlackMarkChange(enabled: Boolean) {
_uiState.value = _uiState.value.copy(blackMarkEnabled = enabled)
viewModelScope.launch { sessionManager.setBlackMarkEnabled(enabled) }
}
fun onUnwindMmChange(value: String) {
if (value.isEmpty() || value.all { it.isDigit() } && value.toIntOrNull()?.let { it in 1..200 } == true) {
_uiState.value = _uiState.value.copy(unwindMm = value)
}
}
fun saveUnwindMm() {
val mm = _uiState.value.unwindMm.toIntOrNull() ?: 40
viewModelScope.launch {
sessionManager.setUnwindMm(mm)
_uiState.value = _uiState.value.copy(message = "走纸长度已保存")
}
}
fun saveUrl() {
viewModelScope.launch {
val url = _uiState.value.apiBaseUrl.trim()
@@ -67,8 +67,9 @@ fun WorkOrderDetailScreen(id: Int, onBack: () -> Unit) {
wo?.let { wo_ ->
val printerManager = com.example.ops_android.ui.LocalAppContainer.current.printerManager
if (printerManager != null) {
val sm = com.example.ops_android.ui.LocalAppContainer.current.sessionManager
IconButton(onClick = {
scope.launch { com.example.ops_android.printer.PrintHelper.printWorkOrderLabel(printerManager, wo_) }
scope.launch { com.example.ops_android.printer.PrintHelper.printWorkOrderLabel(printerManager, wo_, sm.cachedBlackMarkEnabled, sm.cachedUnwindMm) }
}) { Icon(Icons.Default.Print, "打印", tint = Color.White) }
}
}
+2 -2
View File
@@ -1,3 +1,3 @@
#Thu Jun 25 15:33:48 CST 2026
VERSION_CODE=82
#Thu Jun 25 17:53:49 CST 2026
VERSION_CODE=90
VERSION_NAME=1.2