Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
081884b723 | ||
|
|
b7dab20751 | ||
|
|
a7d81d42ea | ||
|
|
85bcdfccaa | ||
|
|
388c0dcfc5 | ||
|
|
57c5362da3 | ||
|
|
b691545ad4 | ||
|
|
6bc653bb74 | ||
|
|
ae07e12200 | ||
|
|
7a9b3db3ca | ||
|
|
71692a509e |
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="com.example.lc_print_sdk"
|
||||||
|
android:versionCode="1"
|
||||||
|
android:versionName="1.0" >
|
||||||
|
|
||||||
|
<uses-sdk
|
||||||
|
android:minSdkVersion="15"
|
||||||
|
android:targetSdkVersion="30" />
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@@ -28,6 +28,9 @@ class SessionManager(private val context: Context) {
|
|||||||
private val KEY_COOKIE_REMEMBER = booleanPreferencesKey("cookie_remember")
|
private val KEY_COOKIE_REMEMBER = booleanPreferencesKey("cookie_remember")
|
||||||
private val KEY_PRINTER_BLACK_MARK = booleanPreferencesKey("printer_black_mark")
|
private val KEY_PRINTER_BLACK_MARK = booleanPreferencesKey("printer_black_mark")
|
||||||
private val KEY_PRINTER_UNWIND_MM = intPreferencesKey("printer_unwind_mm")
|
private val KEY_PRINTER_UNWIND_MM = intPreferencesKey("printer_unwind_mm")
|
||||||
|
private val KEY_LABEL_WIDTH_MM = intPreferencesKey("label_width_mm")
|
||||||
|
private val KEY_LABEL_HEIGHT_MM = intPreferencesKey("label_height_mm")
|
||||||
|
private val KEY_PRINT_RETRACT_MM = intPreferencesKey("print_retract_mm")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
@@ -46,6 +49,18 @@ class SessionManager(private val context: Context) {
|
|||||||
var cachedUnwindMm: Int = 40
|
var cachedUnwindMm: Int = 40
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
var cachedLabelWidthMm: Int = 53
|
||||||
|
private set
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
var cachedLabelHeightMm: Int = 40
|
||||||
|
private set
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
var cachedPrintRetractMm: Int = 10
|
||||||
|
private set
|
||||||
|
|
||||||
fun clearCookieCache() {
|
fun clearCookieCache() {
|
||||||
cachedCookieValue = ""
|
cachedCookieValue = ""
|
||||||
}
|
}
|
||||||
@@ -59,6 +74,9 @@ class SessionManager(private val context: Context) {
|
|||||||
cachedCookieValue = prefs[KEY_COOKIE_VALUE] ?: ""
|
cachedCookieValue = prefs[KEY_COOKIE_VALUE] ?: ""
|
||||||
cachedBlackMarkEnabled = prefs[KEY_PRINTER_BLACK_MARK] ?: true
|
cachedBlackMarkEnabled = prefs[KEY_PRINTER_BLACK_MARK] ?: true
|
||||||
cachedUnwindMm = prefs[KEY_PRINTER_UNWIND_MM] ?: 40
|
cachedUnwindMm = prefs[KEY_PRINTER_UNWIND_MM] ?: 40
|
||||||
|
cachedLabelWidthMm = prefs[KEY_LABEL_WIDTH_MM] ?: 53
|
||||||
|
cachedLabelHeightMm = prefs[KEY_LABEL_HEIGHT_MM] ?: 40
|
||||||
|
cachedPrintRetractMm = prefs[KEY_PRINT_RETRACT_MM] ?: 10
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getApiBaseUrl(): String = cachedBaseUrl.ifEmpty {
|
suspend fun getApiBaseUrl(): String = cachedBaseUrl.ifEmpty {
|
||||||
@@ -115,4 +133,19 @@ class SessionManager(private val context: Context) {
|
|||||||
cachedUnwindMm = mm
|
cachedUnwindMm = mm
|
||||||
context.dataStore.edit { it[KEY_PRINTER_UNWIND_MM] = mm }
|
context.dataStore.edit { it[KEY_PRINTER_UNWIND_MM] = mm }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun setLabelWidthMm(mm: Int) {
|
||||||
|
cachedLabelWidthMm = mm
|
||||||
|
context.dataStore.edit { it[KEY_LABEL_WIDTH_MM] = mm }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun setLabelHeightMm(mm: Int) {
|
||||||
|
cachedLabelHeightMm = mm
|
||||||
|
context.dataStore.edit { it[KEY_LABEL_HEIGHT_MM] = mm }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun setPrintRetractMm(mm: Int) {
|
||||||
|
cachedPrintRetractMm = mm
|
||||||
|
context.dataStore.edit { it[KEY_PRINT_RETRACT_MM] = mm }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.example.ops_android.data.model
|
package com.example.ops_android.data.model
|
||||||
|
|
||||||
|
import com.example.ops_android.data.remote.api.LinkedCustomerInfo
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
data class Item(
|
data class Item(
|
||||||
@@ -14,7 +15,10 @@ data class Item(
|
|||||||
@SerializedName("CreatorID") val user_id: Int? = null,
|
@SerializedName("CreatorID") val user_id: Int? = null,
|
||||||
@SerializedName("barcode") private val _barcode: String? = null,
|
@SerializedName("barcode") private val _barcode: String? = null,
|
||||||
@SerializedName("spec") private val _spec: String? = null,
|
@SerializedName("spec") private val _spec: String? = null,
|
||||||
@SerializedName("move_history") private val _move_history: List<MoveHistory>? = null
|
@SerializedName("move_history") private val _move_history: List<MoveHistory>? = null,
|
||||||
|
// Filled from detail API
|
||||||
|
@kotlin.jvm.Transient val linkedCustomers: List<LinkedCustomerInfo>? = null,
|
||||||
|
@kotlin.jvm.Transient val containerBreadcrumb: String? = null
|
||||||
) {
|
) {
|
||||||
val barcode: String? get() = _barcode
|
val barcode: String? get() = _barcode
|
||||||
val spec: String? get() = _spec
|
val spec: String? get() = _spec
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.example.ops_android.data.model
|
package com.example.ops_android.data.model
|
||||||
|
|
||||||
|
import com.example.ops_android.data.remote.api.LinkedCustomer
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
data class WorkOrder(
|
data class WorkOrder(
|
||||||
@@ -11,7 +12,8 @@ data class WorkOrder(
|
|||||||
@SerializedName("UpdatedAt") val updated_at: String? = null,
|
@SerializedName("UpdatedAt") val updated_at: String? = null,
|
||||||
@SerializedName("UserID") val user_id: Int? = null,
|
@SerializedName("UserID") val user_id: Int? = null,
|
||||||
// Filled from detail API
|
// Filled from detail API
|
||||||
@kotlin.jvm.Transient val commits: List<Commit>? = null
|
@kotlin.jvm.Transient val commits: List<Commit>? = null,
|
||||||
|
@kotlin.jvm.Transient val linkedCustomers: List<LinkedCustomer>? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
data class WorkOrderCommit(
|
data class WorkOrderCommit(
|
||||||
|
|||||||
@@ -101,7 +101,12 @@ class WarehouseRepository(
|
|||||||
return try {
|
return try {
|
||||||
val response = warehouseApi.getItem(GetItemRequest(id))
|
val response = warehouseApi.getItem(GetItemRequest(id))
|
||||||
if (response.errCode == 0 && response.data != null) {
|
if (response.errCode == 0 && response.data != null) {
|
||||||
Result.success(response.data)
|
val resp = response.data
|
||||||
|
val itm = resp.item?.copy(
|
||||||
|
linkedCustomers = resp.customers,
|
||||||
|
containerBreadcrumb = resp.container_breadcrumb
|
||||||
|
)
|
||||||
|
Result.success(resp.copy(item = itm))
|
||||||
} else {
|
} else {
|
||||||
Result.failure(Exception("获取物品详情失败"))
|
Result.failure(Exception("获取物品详情失败"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,11 @@ class WorkOrderRepository(
|
|||||||
return try {
|
return try {
|
||||||
val response = workOrderApi.get(WorkOrderGetRequest(id))
|
val response = workOrderApi.get(WorkOrderGetRequest(id))
|
||||||
if (response.errCode == 0 && response.data?.order != null) {
|
if (response.errCode == 0 && response.data?.order != null) {
|
||||||
Result.success(response.data.order)
|
val detail = response.data
|
||||||
|
val wo = detail.order.copy(
|
||||||
|
linkedCustomers = detail.linkedCustomers
|
||||||
|
)
|
||||||
|
Result.success(wo)
|
||||||
} else {
|
} else {
|
||||||
Result.failure(Exception("获取工单详情失败"))
|
Result.failure(Exception("获取工单详情失败"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,148 @@
|
|||||||
package com.example.ops_android.printer
|
package com.example.ops_android.printer
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.text.Layout
|
||||||
|
import android.text.StaticLayout
|
||||||
|
import android.text.TextPaint
|
||||||
import com.example.ops_android.data.model.Item
|
import com.example.ops_android.data.model.Item
|
||||||
import com.example.ops_android.data.model.PurchaseOrder
|
import com.example.ops_android.data.model.PurchaseOrder
|
||||||
import com.example.ops_android.data.model.WorkOrder
|
import com.example.ops_android.data.model.WorkOrder
|
||||||
|
import com.google.zxing.BarcodeFormat
|
||||||
|
import com.google.zxing.EncodeHintType
|
||||||
|
import com.google.zxing.oned.Code128Writer
|
||||||
|
import com.google.zxing.qrcode.QRCodeWriter
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
object PrintHelper {
|
object PrintHelper {
|
||||||
|
|
||||||
private val printMutex = Mutex()
|
private val printMutex = Mutex()
|
||||||
|
private const val DOTS_PER_MM = 8
|
||||||
|
|
||||||
|
private fun mmToDots(mm: Int): Int = mm * DOTS_PER_MM
|
||||||
|
|
||||||
|
private fun buildQrContent(module: String, id: Int): String {
|
||||||
|
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
|
||||||
|
return JSONObject().apply {
|
||||||
|
put("v", "v1")
|
||||||
|
put("module", module)
|
||||||
|
put("id", id)
|
||||||
|
put("printedAt", sdf.format(Date()))
|
||||||
|
put("type", "ops_sys")
|
||||||
|
}.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateQRBitmap(content: String, size: Int): Bitmap {
|
||||||
|
val hints = mapOf(EncodeHintType.MARGIN to 1)
|
||||||
|
val bitMatrix = QRCodeWriter().encode(content, BarcodeFormat.QR_CODE, size, size, hints)
|
||||||
|
val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
|
||||||
|
for (y in 0 until size) {
|
||||||
|
for (x in 0 until size) {
|
||||||
|
bitmap.setPixel(x, y, if (bitMatrix[x, y]) Color.BLACK else Color.WHITE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bitmap
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawTextBlock(
|
||||||
|
canvas: Canvas,
|
||||||
|
label: String,
|
||||||
|
value: String,
|
||||||
|
x: Float,
|
||||||
|
y: Float,
|
||||||
|
maxWidth: Float,
|
||||||
|
maxLines: Int = 1,
|
||||||
|
textSize: Float = 22f,
|
||||||
|
bold: Boolean = false,
|
||||||
|
lineSpacingExtra: Float = 4f
|
||||||
|
): Float {
|
||||||
|
val text = "$label$value"
|
||||||
|
val paint = TextPaint().apply {
|
||||||
|
color = Color.BLACK
|
||||||
|
this.textSize = textSize
|
||||||
|
isAntiAlias = true
|
||||||
|
isFakeBoldText = bold
|
||||||
|
}
|
||||||
|
|
||||||
|
val builder = StaticLayout.Builder.obtain(text, 0, text.length, paint, maxWidth.toInt())
|
||||||
|
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
|
||||||
|
.setMaxLines(maxLines)
|
||||||
|
.setEllipsize(android.text.TextUtils.TruncateAt.END)
|
||||||
|
.setLineSpacing(lineSpacingExtra, 1f)
|
||||||
|
|
||||||
|
val layout = builder.build()
|
||||||
|
canvas.save()
|
||||||
|
canvas.translate(x, y)
|
||||||
|
layout.draw(canvas)
|
||||||
|
canvas.restore()
|
||||||
|
|
||||||
|
return y + layout.height
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildWorkOrderLabelBitmap(wo: WorkOrder, widthDots: Int, heightDots: Int): Bitmap {
|
||||||
|
val bitmap = Bitmap.createBitmap(widthDots, heightDots, Bitmap.Config.ARGB_8888)
|
||||||
|
val canvas = Canvas(bitmap)
|
||||||
|
canvas.drawColor(Color.WHITE)
|
||||||
|
|
||||||
|
val padding = 12
|
||||||
|
val dividerX = (widthDots * 0.48).toInt()
|
||||||
|
val leftInnerWidth = dividerX - padding * 2
|
||||||
|
val qrSize = (leftInnerWidth * 0.95).toInt().coerceAtLeast(140)
|
||||||
|
val qrX = (dividerX - qrSize) / 2
|
||||||
|
val qrY = padding
|
||||||
|
|
||||||
|
val qrContent = buildQrContent("workorder", wo.id)
|
||||||
|
val qrBitmap = generateQRBitmap(qrContent, qrSize)
|
||||||
|
canvas.drawBitmap(qrBitmap, qrX.toFloat(), qrY.toFloat(), null)
|
||||||
|
|
||||||
|
val sepPaint = Paint().apply {
|
||||||
|
color = Color.BLACK
|
||||||
|
strokeWidth = 1.5f
|
||||||
|
isAntiAlias = true
|
||||||
|
}
|
||||||
|
canvas.drawLine(dividerX.toFloat(), padding.toFloat(), dividerX.toFloat(), (heightDots - padding).toFloat(), sepPaint)
|
||||||
|
|
||||||
|
val textX = (dividerX + padding).toFloat()
|
||||||
|
val textMaxWidth = (widthDots - dividerX - padding * 2).toFloat()
|
||||||
|
var textY = (padding - 2).toFloat()
|
||||||
|
val maxTextY = (heightDots - padding).toFloat()
|
||||||
|
|
||||||
|
textY = drawTextBlock(canvas, "", "工单:${wo.id}", textX, textY, textMaxWidth, textSize = 32f, bold = true, lineSpacingExtra = 6f)
|
||||||
|
|
||||||
|
if (textY < maxTextY) {
|
||||||
|
textY = drawTextBlock(canvas, "", "标题:${wo.title ?: "-"}", textX, textY, textMaxWidth, maxLines = 3, textSize = 28f, lineSpacingExtra = 4f)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textY < maxTextY) {
|
||||||
|
textY = drawTextBlock(canvas, "", "描述:${wo.description ?: "-"}", textX, textY, textMaxWidth, maxLines = 6, textSize = 28f, lineSpacingExtra = 4f)
|
||||||
|
}
|
||||||
|
|
||||||
|
val customerNames = wo.linkedCustomers
|
||||||
|
?.mapNotNull { c ->
|
||||||
|
listOfNotNull(c.first_name, c.last_name)
|
||||||
|
.joinToString(" ")
|
||||||
|
.ifBlank { null }
|
||||||
|
}
|
||||||
|
?.joinToString(", ")
|
||||||
|
?: "-"
|
||||||
|
val customerText = "客户:$customerNames"
|
||||||
|
val customerY = qrY + qrSize + 4
|
||||||
|
val leftTextMaxWidth = (dividerX - padding * 2).toFloat()
|
||||||
|
if (customerY < heightDots - padding) {
|
||||||
|
drawTextBlock(canvas, "", customerText, padding.toFloat(), customerY.toFloat(), leftTextMaxWidth, maxLines = 2, textSize = 28f, lineSpacingExtra = 4f)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bitmap
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun printOrderLabel(
|
suspend fun printOrderLabel(
|
||||||
printerManager: PrinterManager,
|
printerManager: PrinterManager,
|
||||||
@@ -36,14 +167,7 @@ object PrintHelper {
|
|||||||
} else {
|
} else {
|
||||||
printerManager.addLineFeed((unwindMm / 7).toInt().coerceAtLeast(2))
|
printerManager.addLineFeed((unwindMm / 7).toInt().coerceAtLeast(2))
|
||||||
}
|
}
|
||||||
printerManager.commit().fold(
|
printerManager.commit()
|
||||||
onSuccess = {
|
|
||||||
delay(1000)
|
|
||||||
printerManager.closeDevice()
|
|
||||||
Result.success(Unit)
|
|
||||||
},
|
|
||||||
onFailure = { Result.failure(it) }
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Result.failure(e)
|
Result.failure(e)
|
||||||
}
|
}
|
||||||
@@ -53,72 +177,150 @@ object PrintHelper {
|
|||||||
suspend fun printWorkOrderLabel(
|
suspend fun printWorkOrderLabel(
|
||||||
printerManager: PrinterManager,
|
printerManager: PrinterManager,
|
||||||
workOrder: WorkOrder,
|
workOrder: WorkOrder,
|
||||||
|
labelWidthMm: Int = 53,
|
||||||
|
labelHeightMm: Int = 40,
|
||||||
blackMarkEnabled: Boolean = true,
|
blackMarkEnabled: Boolean = true,
|
||||||
unwindMm: Int = 40
|
unwindMm: Int = 40,
|
||||||
|
printRetractMm: Int = 10
|
||||||
): Result<Unit> = printMutex.withLock {
|
): Result<Unit> = printMutex.withLock {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
printerManager.initDevice().onFailure { return@withContext Result.failure(it) }
|
printerManager.initDevice().onFailure { return@withContext Result.failure(it) }
|
||||||
printerManager.setFontSize(24)
|
if (printRetractMm > 0) {
|
||||||
printerManager.setBold(true)
|
printerManager.setUnwindLength(mmToDots(printRetractMm))
|
||||||
printerManager.addText("工单: ${workOrder.title ?: "-"}\n")
|
|
||||||
printerManager.setFontSize(20)
|
|
||||||
printerManager.setBold(false)
|
|
||||||
if (workOrder.id > 0) {
|
|
||||||
printerManager.addBarcode("WO-${workOrder.id}")
|
|
||||||
}
|
}
|
||||||
|
val widthDots = mmToDots(labelWidthMm)
|
||||||
|
val heightDots = mmToDots(labelHeightMm)
|
||||||
|
val bitmap = buildWorkOrderLabelBitmap(workOrder, widthDots, heightDots)
|
||||||
|
printerManager.printImage(bitmap, 1)
|
||||||
if (blackMarkEnabled) {
|
if (blackMarkEnabled) {
|
||||||
printerManager.addLineFeed(2)
|
printerManager.addLineFeed(2)
|
||||||
printerManager.enableMarkDetection(true)
|
printerManager.enableMarkDetection(true)
|
||||||
} else {
|
} else {
|
||||||
printerManager.addLineFeed((unwindMm / 7).toInt().coerceAtLeast(2))
|
printerManager.addLineFeed((unwindMm / 7).toInt().coerceAtLeast(2))
|
||||||
}
|
}
|
||||||
printerManager.commit().fold(
|
printerManager.commit()
|
||||||
onSuccess = {
|
|
||||||
delay(1000)
|
|
||||||
printerManager.closeDevice()
|
|
||||||
Result.success(Unit)
|
|
||||||
},
|
|
||||||
onFailure = { Result.failure(it) }
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Result.failure(e)
|
Result.failure(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun buildItemLabelBitmap(item: Item, widthDots: Int, heightDots: Int): Bitmap {
|
||||||
|
val bitmap = Bitmap.createBitmap(widthDots, heightDots, Bitmap.Config.ARGB_8888)
|
||||||
|
val canvas = Canvas(bitmap)
|
||||||
|
canvas.drawColor(Color.WHITE)
|
||||||
|
|
||||||
|
val padding = 12
|
||||||
|
val barcodeReserve = if (!item.serial_number.isNullOrBlank()) 70 else 0
|
||||||
|
val dividerX = (widthDots * 0.48).toInt()
|
||||||
|
val leftInnerWidth = dividerX - padding * 2
|
||||||
|
val qrSize = (leftInnerWidth * 0.95).toInt().coerceAtLeast(140)
|
||||||
|
val qrX = (dividerX - qrSize) / 2
|
||||||
|
val qrY = padding
|
||||||
|
|
||||||
|
val qrContent = buildQrContent("item", item.id)
|
||||||
|
val qrBitmap = generateQRBitmap(qrContent, qrSize)
|
||||||
|
canvas.drawBitmap(qrBitmap, qrX.toFloat(), qrY.toFloat(), null)
|
||||||
|
|
||||||
|
val sepPaint = Paint().apply {
|
||||||
|
color = Color.BLACK
|
||||||
|
strokeWidth = 1.5f
|
||||||
|
isAntiAlias = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val textX = (dividerX + padding).toFloat()
|
||||||
|
val textMaxWidth = (widthDots - dividerX - padding * 2).toFloat()
|
||||||
|
var textY = (padding - 2).toFloat()
|
||||||
|
val maxTextY = heightDots - padding - barcodeReserve
|
||||||
|
|
||||||
|
textY = drawTextBlock(canvas, "", "物品:${item.id}", textX, textY, textMaxWidth, textSize = 32f, bold = true, lineSpacingExtra = 6f)
|
||||||
|
|
||||||
|
if (textY < maxTextY) {
|
||||||
|
textY = drawTextBlock(canvas, "", "名称:${item.name ?: "-"}", textX, textY, textMaxWidth, maxLines = 3, textSize = 25f, lineSpacingExtra = 4f)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textY < maxTextY && item.quantity != 1) {
|
||||||
|
textY = drawTextBlock(canvas, "", "数量:${item.quantity}", textX, textY, textMaxWidth, textSize = 25f, lineSpacingExtra = 4f)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textY < maxTextY && !item.containerBreadcrumb.isNullOrBlank()) {
|
||||||
|
textY = drawTextBlock(canvas, "", "位置:${item.containerBreadcrumb}", textX, textY, textMaxWidth, maxLines = 4, textSize = 25f, lineSpacingExtra = 4f)
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.drawLine(dividerX.toFloat(), padding.toFloat(), dividerX.toFloat(), maxTextY.toFloat(), sepPaint)
|
||||||
|
|
||||||
|
if (!item.linkedCustomers.isNullOrEmpty()) {
|
||||||
|
val customerNames = item.linkedCustomers
|
||||||
|
.mapNotNull { c ->
|
||||||
|
listOfNotNull(c.first_name, c.last_name)
|
||||||
|
.joinToString(" ")
|
||||||
|
.ifBlank { null }
|
||||||
|
}
|
||||||
|
.joinToString(", ")
|
||||||
|
val customerText = "客户:$customerNames"
|
||||||
|
val customerY = qrY + qrSize + 4
|
||||||
|
val leftTextMaxWidth = (dividerX - padding * 2).toFloat()
|
||||||
|
if (customerY < maxTextY) {
|
||||||
|
drawTextBlock(canvas, "", customerText, padding.toFloat(), customerY.toFloat(), leftTextMaxWidth, maxLines = 2, textSize = 20f, lineSpacingExtra = 2f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!item.serial_number.isNullOrBlank()) {
|
||||||
|
canvas.drawLine(padding.toFloat(), maxTextY.toFloat(), (widthDots - padding).toFloat(), maxTextY.toFloat(), sepPaint)
|
||||||
|
|
||||||
|
val snTextY = maxTextY + 4
|
||||||
|
val snTextHeight = drawTextBlock(canvas, "", item.serial_number ?: "",
|
||||||
|
padding.toFloat(), snTextY.toFloat(),
|
||||||
|
(widthDots - padding * 2).toFloat(),
|
||||||
|
maxLines = 1, textSize = 22f, lineSpacingExtra = 0f
|
||||||
|
) - snTextY
|
||||||
|
|
||||||
|
val barcodeWidth = widthDots - padding * 4
|
||||||
|
val barcodeHeight = 35
|
||||||
|
val barcodeX = (widthDots - barcodeWidth) / 2
|
||||||
|
val barcodeY = snTextY + snTextHeight + 4
|
||||||
|
|
||||||
|
val code128 = Code128Writer()
|
||||||
|
val barcodeMatrix = code128.encode(item.serial_number, BarcodeFormat.CODE_128, barcodeWidth, barcodeHeight)
|
||||||
|
val barcodeBitmap = Bitmap.createBitmap(barcodeWidth, barcodeHeight, Bitmap.Config.ARGB_8888)
|
||||||
|
for (y in 0 until barcodeHeight) {
|
||||||
|
for (x in 0 until barcodeWidth) {
|
||||||
|
barcodeBitmap.setPixel(x, y, if (barcodeMatrix[x, y]) Color.BLACK else Color.WHITE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
canvas.drawBitmap(barcodeBitmap, barcodeX.toFloat(), barcodeY.toFloat(), null)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bitmap
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun printItemLabel(
|
suspend fun printItemLabel(
|
||||||
printerManager: PrinterManager,
|
printerManager: PrinterManager,
|
||||||
item: Item,
|
item: Item,
|
||||||
|
labelWidthMm: Int = 53,
|
||||||
|
labelHeightMm: Int = 40,
|
||||||
blackMarkEnabled: Boolean = true,
|
blackMarkEnabled: Boolean = true,
|
||||||
unwindMm: Int = 40
|
unwindMm: Int = 40,
|
||||||
|
printRetractMm: Int = 10
|
||||||
): Result<Unit> = printMutex.withLock {
|
): Result<Unit> = printMutex.withLock {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
printerManager.initDevice().onFailure { return@withContext Result.failure(it) }
|
printerManager.initDevice().onFailure { return@withContext Result.failure(it) }
|
||||||
printerManager.setFontSize(24)
|
if (printRetractMm > 0) {
|
||||||
printerManager.setBold(true)
|
printerManager.setUnwindLength(mmToDots(printRetractMm))
|
||||||
printerManager.addText("${item.name ?: "-"}\n")
|
|
||||||
printerManager.setFontSize(18)
|
|
||||||
printerManager.setBold(false)
|
|
||||||
item.serial_number?.let { printerManager.addText("编号: $it\n") }
|
|
||||||
if (item.id > 0) {
|
|
||||||
printerManager.addBarcode("ITEM-${item.id}")
|
|
||||||
}
|
}
|
||||||
|
val widthDots = mmToDots(labelWidthMm)
|
||||||
|
val heightDots = mmToDots(labelHeightMm)
|
||||||
|
val bitmap = buildItemLabelBitmap(item, widthDots, heightDots)
|
||||||
|
printerManager.printImage(bitmap, 1)
|
||||||
if (blackMarkEnabled) {
|
if (blackMarkEnabled) {
|
||||||
printerManager.addLineFeed(2)
|
printerManager.addLineFeed(2)
|
||||||
printerManager.enableMarkDetection(true)
|
printerManager.enableMarkDetection(true)
|
||||||
} else {
|
} else {
|
||||||
printerManager.addLineFeed((unwindMm / 7).toInt().coerceAtLeast(2))
|
printerManager.addLineFeed((unwindMm / 7).toInt().coerceAtLeast(2))
|
||||||
}
|
}
|
||||||
printerManager.commit().fold(
|
printerManager.commit()
|
||||||
onSuccess = {
|
|
||||||
delay(1000)
|
|
||||||
printerManager.closeDevice()
|
|
||||||
Result.success(Unit)
|
|
||||||
},
|
|
||||||
onFailure = { Result.failure(it) }
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Result.failure(e)
|
Result.failure(e)
|
||||||
}
|
}
|
||||||
@@ -149,14 +351,7 @@ object PrintHelper {
|
|||||||
} else {
|
} else {
|
||||||
printerManager.addLineFeed((unwindMm / 7).toInt().coerceAtLeast(2))
|
printerManager.addLineFeed((unwindMm / 7).toInt().coerceAtLeast(2))
|
||||||
}
|
}
|
||||||
printerManager.commit().fold(
|
printerManager.commit()
|
||||||
onSuccess = {
|
|
||||||
delay(1000)
|
|
||||||
printerManager.closeDevice()
|
|
||||||
Result.success(Unit)
|
|
||||||
},
|
|
||||||
onFailure = { Result.failure(it) }
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Result.failure(e)
|
Result.failure(e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,8 +46,6 @@ class PrintJob(private val manager: PrinterManager) {
|
|||||||
if (lineFeeds > 0) manager.addLineFeed(lineFeeds).getOrThrow()
|
if (lineFeeds > 0) manager.addLineFeed(lineFeeds).getOrThrow()
|
||||||
|
|
||||||
manager.commit().getOrThrow()
|
manager.commit().getOrThrow()
|
||||||
Thread.sleep(1500)
|
|
||||||
manager.closeDevice().getOrThrow()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 初始化 → 添加内容 → 提交(不关闭) */
|
/** 初始化 → 添加内容 → 提交(不关闭) */
|
||||||
|
|||||||
@@ -301,8 +301,6 @@ class PrinterManager(private val context: Context) {
|
|||||||
initDevice().getOrThrow()
|
initDevice().getOrThrow()
|
||||||
addText(text).getOrThrow()
|
addText(text).getOrThrow()
|
||||||
commit().getOrThrow()
|
commit().getOrThrow()
|
||||||
Thread.sleep(1500)
|
|
||||||
closeDevice().getOrThrow()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun quickLabel(text: String): Result<Unit> = runCatching {
|
fun quickLabel(text: String): Result<Unit> = runCatching {
|
||||||
@@ -310,8 +308,6 @@ class PrinterManager(private val context: Context) {
|
|||||||
enableMarkDetection(true).getOrThrow()
|
enableMarkDetection(true).getOrThrow()
|
||||||
addText(text).getOrThrow()
|
addText(text).getOrThrow()
|
||||||
commit().getOrThrow()
|
commit().getOrThrow()
|
||||||
Thread.sleep(1500)
|
|
||||||
closeDevice().getOrThrow()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 创建链式打印任务 */
|
/** 创建链式打印任务 */
|
||||||
|
|||||||
@@ -80,6 +80,21 @@ fun HomeScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 快捷入口 ──────────────────────────
|
||||||
|
item {
|
||||||
|
Text("快捷入口", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
QuickLinkCard(Icons.Default.Search, "搜索", Color(0xFF4CAF50), Modifier.weight(1f), onSearchClick)
|
||||||
|
QuickLinkCard(Icons.Default.Event, "日程", Color(0xFF9C27B0), Modifier.weight(1f), onScheduleClick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── 今日日程 ──────────────────────────
|
// ── 今日日程 ──────────────────────────
|
||||||
item {
|
item {
|
||||||
Text("今日日程", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
Text("今日日程", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
||||||
@@ -172,21 +187,6 @@ fun HomeScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 快捷入口 ──────────────────────────
|
|
||||||
item {
|
|
||||||
Text("快捷入口", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
|
||||||
) {
|
|
||||||
QuickLinkCard(Icons.Default.Search, "搜索", Color(0xFF4CAF50), Modifier.weight(1f), onSearchClick)
|
|
||||||
QuickLinkCard(Icons.Default.Event, "日程", Color(0xFF9C27B0), Modifier.weight(1f), onScheduleClick)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,7 +111,13 @@ fun OPSNavGraph(isLoggedIn: Boolean) {
|
|||||||
onWorkOrderClick = { id -> rootNavController.navigate(Routes.workOrderDetail(id)) },
|
onWorkOrderClick = { id -> rootNavController.navigate(Routes.workOrderDetail(id)) },
|
||||||
onItemClick = { id -> rootNavController.navigate(Routes.itemDetail(id)) },
|
onItemClick = { id -> rootNavController.navigate(Routes.itemDetail(id)) },
|
||||||
onContainerClick = { id -> rootNavController.navigate(Routes.MAIN) },
|
onContainerClick = { id -> rootNavController.navigate(Routes.MAIN) },
|
||||||
onBack = { rootNavController.popBackStack() }
|
onBack = { rootNavController.popBackStack() },
|
||||||
|
onOpsSysNavigate = { module, id ->
|
||||||
|
when (module) {
|
||||||
|
"workorder" -> rootNavController.navigate(Routes.workOrderDetail(id))
|
||||||
|
"item" -> rootNavController.navigate(Routes.itemDetail(id))
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable(Routes.SCHEDULE) {
|
composable(Routes.SCHEDULE) {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
@@ -51,7 +51,8 @@ fun SearchScreen(
|
|||||||
onWorkOrderClick: (Int) -> Unit,
|
onWorkOrderClick: (Int) -> Unit,
|
||||||
onItemClick: (Int) -> Unit,
|
onItemClick: (Int) -> Unit,
|
||||||
onContainerClick: (Int) -> Unit,
|
onContainerClick: (Int) -> Unit,
|
||||||
onBack: () -> Unit
|
onBack: () -> Unit,
|
||||||
|
onOpsSysNavigate: (module: String, id: Int) -> Unit
|
||||||
) {
|
) {
|
||||||
val appContainer = LocalAppContainer.current
|
val appContainer = LocalAppContainer.current
|
||||||
val viewModel: SearchViewModel = viewModel(
|
val viewModel: SearchViewModel = viewModel(
|
||||||
@@ -67,6 +68,13 @@ fun SearchScreen(
|
|||||||
if (granted) showScanner = true
|
if (granted) showScanner = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(uiState.opsSysNavigation) {
|
||||||
|
uiState.opsSysNavigation?.let { navInfo ->
|
||||||
|
onOpsSysNavigate(navInfo.module, navInfo.id)
|
||||||
|
viewModel.clearOpsSysNavigation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun checkCameraAndScan() {
|
fun checkCameraAndScan() {
|
||||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
|
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
|
||||||
hasCameraPermission = true
|
hasCameraPermission = true
|
||||||
@@ -128,14 +136,6 @@ fun SearchScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prefix hints
|
|
||||||
Text(
|
|
||||||
"前缀快捷搜索: po:采购单 wo:工单 item:物品 warehouse:容器",
|
|
||||||
fontSize = 11.sp,
|
|
||||||
color = Color.Gray,
|
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
|
||||||
|
|
||||||
// Category tabs
|
// Category tabs
|
||||||
PrimaryScrollableTabRow(
|
PrimaryScrollableTabRow(
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import com.example.ops_android.data.model.Container
|
|||||||
import com.example.ops_android.data.model.Item
|
import com.example.ops_android.data.model.Item
|
||||||
import com.example.ops_android.data.model.PurchaseOrder
|
import com.example.ops_android.data.model.PurchaseOrder
|
||||||
import com.example.ops_android.data.model.WorkOrder
|
import com.example.ops_android.data.model.WorkOrder
|
||||||
import com.example.ops_android.data.repository.CustomerRepository
|
|
||||||
import com.example.ops_android.data.repository.OrderRepository
|
import com.example.ops_android.data.repository.OrderRepository
|
||||||
import com.example.ops_android.data.repository.WarehouseRepository
|
import com.example.ops_android.data.repository.WarehouseRepository
|
||||||
import com.example.ops_android.data.repository.WorkOrderRepository
|
import com.example.ops_android.data.repository.WorkOrderRepository
|
||||||
@@ -15,6 +14,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
enum class SearchCategory(val label: String) {
|
enum class SearchCategory(val label: String) {
|
||||||
ALL("全部"),
|
ALL("全部"),
|
||||||
@@ -24,6 +24,11 @@ enum class SearchCategory(val label: String) {
|
|||||||
CONTAINER("容器")
|
CONTAINER("容器")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class OpsSysNavInfo(
|
||||||
|
val module: String,
|
||||||
|
val id: Int
|
||||||
|
)
|
||||||
|
|
||||||
data class SearchUiState(
|
data class SearchUiState(
|
||||||
val query: String = "",
|
val query: String = "",
|
||||||
val category: SearchCategory = SearchCategory.ALL,
|
val category: SearchCategory = SearchCategory.ALL,
|
||||||
@@ -32,7 +37,8 @@ data class SearchUiState(
|
|||||||
val items: List<Item> = emptyList(),
|
val items: List<Item> = emptyList(),
|
||||||
val containers: List<Container> = emptyList(),
|
val containers: List<Container> = emptyList(),
|
||||||
val isSearching: Boolean = false,
|
val isSearching: Boolean = false,
|
||||||
val errorMessage: String? = null
|
val errorMessage: String? = null,
|
||||||
|
val opsSysNavigation: OpsSysNavInfo? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
class SearchViewModel(
|
class SearchViewModel(
|
||||||
@@ -61,7 +67,19 @@ class SearchViewModel(
|
|||||||
val query = _uiState.value.query.trim()
|
val query = _uiState.value.query.trim()
|
||||||
if (query.isEmpty()) return
|
if (query.isEmpty()) return
|
||||||
|
|
||||||
// Parse prefix shortcuts
|
val jsonObj = try { JSONObject(query) } catch (e: Exception) { null }
|
||||||
|
if (jsonObj != null) {
|
||||||
|
_uiState.value = _uiState.value.copy(query = "")
|
||||||
|
if (jsonObj.optString("type") == "ops_sys") {
|
||||||
|
val module = jsonObj.optString("module")
|
||||||
|
val id = jsonObj.optInt("id")
|
||||||
|
if (module.isNotBlank() && id > 0) {
|
||||||
|
_uiState.value = _uiState.value.copy(opsSysNavigation = OpsSysNavInfo(module, id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val (actualCategory, actualQuery) = parsePrefix(query)
|
val (actualCategory, actualQuery) = parsePrefix(query)
|
||||||
_uiState.value = _uiState.value.copy(category = actualCategory)
|
_uiState.value = _uiState.value.copy(category = actualCategory)
|
||||||
|
|
||||||
@@ -125,6 +143,10 @@ class SearchViewModel(
|
|||||||
_uiState.value = SearchUiState(query = _uiState.value.query, category = _uiState.value.category)
|
_uiState.value = SearchUiState(query = _uiState.value.query, category = _uiState.value.category)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clearOpsSysNavigation() {
|
||||||
|
_uiState.value = _uiState.value.copy(opsSysNavigation = null)
|
||||||
|
}
|
||||||
|
|
||||||
private fun parsePrefix(query: String): Pair<SearchCategory, String> {
|
private fun parsePrefix(query: String): Pair<SearchCategory, String> {
|
||||||
val prefixes = mapOf(
|
val prefixes = mapOf(
|
||||||
"po:" to SearchCategory.ORDER,
|
"po:" to SearchCategory.ORDER,
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.example.ops_android.ui.LocalAppContainer
|
import com.example.ops_android.ui.LocalAppContainer
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@@ -120,8 +119,6 @@ fun PrinterTestScreen(onBack: () -> Unit) {
|
|||||||
val pm = printerManager!!
|
val pm = printerManager!!
|
||||||
pm.initDevice().getOrThrow()
|
pm.initDevice().getOrThrow()
|
||||||
pm.commit().getOrThrow()
|
pm.commit().getOrThrow()
|
||||||
delay(1000)
|
|
||||||
pm.closeDevice().getOrThrow()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -137,8 +134,6 @@ fun PrinterTestScreen(onBack: () -> Unit) {
|
|||||||
pm.initDevice().getOrThrow()
|
pm.initDevice().getOrThrow()
|
||||||
pm.addLineFeed(5)
|
pm.addLineFeed(5)
|
||||||
pm.commit().getOrThrow()
|
pm.commit().getOrThrow()
|
||||||
delay(1000)
|
|
||||||
pm.closeDevice().getOrThrow()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -155,8 +150,6 @@ fun PrinterTestScreen(onBack: () -> Unit) {
|
|||||||
pm.initDevice().getOrThrow()
|
pm.initDevice().getOrThrow()
|
||||||
pm.addLineFeed(10)
|
pm.addLineFeed(10)
|
||||||
pm.commit().getOrThrow()
|
pm.commit().getOrThrow()
|
||||||
delay(1000)
|
|
||||||
pm.closeDevice().getOrThrow()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
|||||||
@@ -148,6 +148,49 @@ fun SettingsScreen(
|
|||||||
|
|
||||||
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
|
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
|
||||||
|
|
||||||
|
Text("标签尺寸", style = MaterialTheme.typography.titleMedium)
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = uiState.labelWidthMm,
|
||||||
|
onValueChange = viewModel::onLabelWidthChange,
|
||||||
|
label = { Text("标签宽度 (mm)") },
|
||||||
|
suffix = { Text("mm") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = uiState.labelHeightMm,
|
||||||
|
onValueChange = viewModel::onLabelHeightChange,
|
||||||
|
label = { Text("标签高度 (mm)") },
|
||||||
|
suffix = { Text("mm") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
Button(onClick = { viewModel.saveLabelSize() }, modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Text("保存标签尺寸")
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
|
||||||
|
|
||||||
|
Text("打印校准", style = MaterialTheme.typography.titleMedium)
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = uiState.printRetractMm,
|
||||||
|
onValueChange = viewModel::onPrintRetractMmChange,
|
||||||
|
label = { Text("打印前回退长度 (mm)") },
|
||||||
|
suffix = { Text("mm") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
Button(onClick = { viewModel.savePrintRetractMm() }, modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Text("保存回退长度")
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "关于",
|
text = "关于",
|
||||||
style = MaterialTheme.typography.titleMedium
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
|||||||
@@ -25,7 +25,10 @@ data class SettingsUiState(
|
|||||||
val testSuccess: Boolean = false,
|
val testSuccess: Boolean = false,
|
||||||
val message: String? = null,
|
val message: String? = null,
|
||||||
val blackMarkEnabled: Boolean = true,
|
val blackMarkEnabled: Boolean = true,
|
||||||
val unwindMm: String = "40"
|
val unwindMm: String = "40",
|
||||||
|
val labelWidthMm: String = "53",
|
||||||
|
val labelHeightMm: String = "40",
|
||||||
|
val printRetractMm: String = "10"
|
||||||
)
|
)
|
||||||
|
|
||||||
class SettingsViewModel(
|
class SettingsViewModel(
|
||||||
@@ -56,7 +59,10 @@ class SettingsViewModel(
|
|||||||
private fun loadPrinterSettings() {
|
private fun loadPrinterSettings() {
|
||||||
_uiState.value = _uiState.value.copy(
|
_uiState.value = _uiState.value.copy(
|
||||||
blackMarkEnabled = sessionManager.cachedBlackMarkEnabled,
|
blackMarkEnabled = sessionManager.cachedBlackMarkEnabled,
|
||||||
unwindMm = sessionManager.cachedUnwindMm.toString()
|
unwindMm = sessionManager.cachedUnwindMm.toString(),
|
||||||
|
labelWidthMm = sessionManager.cachedLabelWidthMm.toString(),
|
||||||
|
labelHeightMm = sessionManager.cachedLabelHeightMm.toString(),
|
||||||
|
printRetractMm = sessionManager.cachedPrintRetractMm.toString()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,6 +89,42 @@ class SettingsViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onLabelWidthChange(value: String) {
|
||||||
|
if (value.isEmpty() || value.all { it.isDigit() } && value.toIntOrNull()?.let { it in 10..200 } == true) {
|
||||||
|
_uiState.value = _uiState.value.copy(labelWidthMm = value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onLabelHeightChange(value: String) {
|
||||||
|
if (value.isEmpty() || value.all { it.isDigit() } && value.toIntOrNull()?.let { it in 10..200 } == true) {
|
||||||
|
_uiState.value = _uiState.value.copy(labelHeightMm = value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveLabelSize() {
|
||||||
|
val width = _uiState.value.labelWidthMm.toIntOrNull() ?: 53
|
||||||
|
val height = _uiState.value.labelHeightMm.toIntOrNull() ?: 40
|
||||||
|
viewModelScope.launch {
|
||||||
|
sessionManager.setLabelWidthMm(width)
|
||||||
|
sessionManager.setLabelHeightMm(height)
|
||||||
|
_uiState.value = _uiState.value.copy(message = "标签尺寸已保存")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onPrintRetractMmChange(value: String) {
|
||||||
|
if (value.isEmpty() || value.all { it.isDigit() } && value.toIntOrNull()?.let { it in 0..50 } == true) {
|
||||||
|
_uiState.value = _uiState.value.copy(printRetractMm = value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun savePrintRetractMm() {
|
||||||
|
val mm = _uiState.value.printRetractMm.toIntOrNull() ?: 10
|
||||||
|
viewModelScope.launch {
|
||||||
|
sessionManager.setPrintRetractMm(mm)
|
||||||
|
_uiState.value = _uiState.value.copy(message = "回退长度已保存")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun saveUrl() {
|
fun saveUrl() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val url = _uiState.value.apiBaseUrl.trim()
|
val url = _uiState.value.apiBaseUrl.trim()
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import com.example.ops_android.data.remote.api.ItemResponse
|
|||||||
import com.example.ops_android.data.remote.api.LinkedCustomerInfo
|
import com.example.ops_android.data.remote.api.LinkedCustomerInfo
|
||||||
import com.example.ops_android.data.remote.api.LinkedWorkOrder
|
import com.example.ops_android.data.remote.api.LinkedWorkOrder
|
||||||
import com.example.ops_android.data.repository.WarehouseRepository
|
import com.example.ops_android.data.repository.WarehouseRepository
|
||||||
|
import com.example.ops_android.printer.PrintHelper
|
||||||
import com.example.ops_android.ui.LocalAppContainer
|
import com.example.ops_android.ui.LocalAppContainer
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -123,6 +124,17 @@ fun ItemDetailScreen(id: Int, onBack: () -> Unit, onCreateWorkOrder: (Int) -> Un
|
|||||||
title = { Text("物品详情") },
|
title = { Text("物品详情") },
|
||||||
navigationIcon = { IconButton(onClick = onBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, null, tint = Color.White) } },
|
navigationIcon = { IconButton(onClick = onBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, null, tint = Color.White) } },
|
||||||
actions = {
|
actions = {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
item?.let { itm ->
|
||||||
|
val printerManager = app.printerManager
|
||||||
|
if (printerManager != null) {
|
||||||
|
IconButton(onClick = {
|
||||||
|
scope.launch { PrintHelper.printItemLabel(printerManager, itm, labelWidthMm = app.sessionManager.cachedLabelWidthMm, labelHeightMm = app.sessionManager.cachedLabelHeightMm, blackMarkEnabled = app.sessionManager.cachedBlackMarkEnabled, unwindMm = app.sessionManager.cachedUnwindMm, printRetractMm = app.sessionManager.cachedPrintRetractMm) }
|
||||||
|
}) {
|
||||||
|
Icon(Icons.Default.Print, "打印", tint = Color.White)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
IconButton(onClick = { onCreateWorkOrder(id) }) { Icon(Icons.Default.AddTask, "新建工单", tint = Color.White) }
|
IconButton(onClick = { onCreateWorkOrder(id) }) { Icon(Icons.Default.AddTask, "新建工单", tint = Color.White) }
|
||||||
if (resp?.canModifyItem == true) {
|
if (resp?.canModifyItem == true) {
|
||||||
IconButton(onClick = { showMoveDialog = true }) { Icon(Icons.Default.OpenWith, "移动", tint = Color.White) }
|
IconButton(onClick = { showMoveDialog = true }) { Icon(Icons.Default.OpenWith, "移动", tint = Color.White) }
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ fun WorkOrderDetailScreen(id: Int, onBack: () -> Unit) {
|
|||||||
if (printerManager != null) {
|
if (printerManager != null) {
|
||||||
val sm = com.example.ops_android.ui.LocalAppContainer.current.sessionManager
|
val sm = com.example.ops_android.ui.LocalAppContainer.current.sessionManager
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
scope.launch { com.example.ops_android.printer.PrintHelper.printWorkOrderLabel(printerManager, wo_, sm.cachedBlackMarkEnabled, sm.cachedUnwindMm) }
|
scope.launch { com.example.ops_android.printer.PrintHelper.printWorkOrderLabel(printerManager, wo_, labelWidthMm = sm.cachedLabelWidthMm, labelHeightMm = sm.cachedLabelHeightMm, blackMarkEnabled = sm.cachedBlackMarkEnabled, unwindMm = sm.cachedUnwindMm, printRetractMm = sm.cachedPrintRetractMm) }
|
||||||
}) { Icon(Icons.Default.Print, "打印", tint = Color.White) }
|
}) { Icon(Icons.Default.Print, "打印", tint = Color.White) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
|
||||||
|
After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 982 B |
|
After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
@@ -1,3 +1,3 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">ops_android</string>
|
<string name="app_name">Operations 2</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
#Thu Jun 25 17:53:49 CST 2026
|
#Fri Jun 26 20:46:06 CST 2026
|
||||||
VERSION_CODE=90
|
VERSION_CODE=121
|
||||||
VERSION_NAME=1.2
|
VERSION_NAME=1.2
|
||||||
|
|||||||