初始代码提交

This commit is contained in:
2026-01-14 10:36:27 +08:00
parent 600aa4dbb0
commit 196cacfe5a
268 changed files with 22545 additions and 2 deletions
@@ -0,0 +1,693 @@
package com.sw.inbound.activity
import android.annotation.SuppressLint
import android.app.Dialog
import android.graphics.Bitmap
import android.net.Uri
import android.os.Bundle
import android.text.TextUtils
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.activity.ComponentActivity
import androidx.activity.viewModels
import androidx.camera.view.PreviewView
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import com.sw.inbound.R
import com.sw.inbound.adapter.FoodCollectionAdapter
import com.sw.inbound.adapter.GoodsSearchAdapter
import com.sw.inbound.databinding.ActivityFoodCollectionBinding
import com.sw.inbound.databinding.LayoutCameraPreviewBinding
import com.sw.inbound.databinding.LayoutEmptySearchBinding
import com.sw.inbound.dialog.CollectSearchDialog
import com.sw.inbound.dialog.CustomDialog
import com.sw.inbound.dialog.Loading
import com.sw.inbound.model.response.SearchGoodsInfo
import com.sw.inbound.objbox.Food
import com.sw.inbound.objbox.FoodCollectionBean
import com.sw.inbound.objbox.FoodModule
import com.sw.inbound.objbox.ObjectBox
import com.sw.inbound.utils.BitmapCropper
import com.sw.inbound.utils.BitmapSaver
import com.sw.inbound.utils.CameraUtils
import com.sw.inbound.utils.ImageUploader
import com.sw.inbound.utils.ImageUtil
import com.sw.inbound.utils.ext.addOnActionSearchListener
import com.sw.inbound.utils.ext.clickWithDebounce
import com.sw.inbound.utils.ext.dp
import com.sw.inbound.utils.ext.gone
import com.sw.inbound.utils.ext.hideKeyboard
import com.sw.inbound.utils.ext.invisible
import com.sw.inbound.utils.ext.startActivity
import com.sw.inbound.utils.ext.toObject
import com.sw.inbound.utils.ext.toast
import com.sw.inbound.utils.ext.visible
import com.sw.inbound.viewmodel.ReceiptViewModel
import dagger.hilt.android.AndroidEntryPoint
import io.objectbox.Box
import io.objectbox.kotlin.boxFor
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import timber.log.Timber
import java.io.File
@SuppressLint("NotifyDataSetChanged")
@AndroidEntryPoint
class FoodCollectionActivity : ComponentActivity() {
companion object {
const val TAG = "FoodCollectionActivity"
const val MAX_COUNT = 102
val PAGE_SIZE = 30
}
private var box: Box<Food>? = null
private val collectList: MutableList<FoodCollectionBean> = mutableListOf<FoodCollectionBean>()
private lateinit var binding: ActivityFoodCollectionBinding
private lateinit var previewView: PreviewView
val viewModel: ReceiptViewModel by viewModels()
private val cameraUtils: CameraUtils by lazy {
CameraUtils(this)
}
private val collectionAdapter: FoodCollectionAdapter by lazy {
FoodCollectionAdapter(collectList).apply {
addOnItemChildClickListener(R.id.ivDelete) { _, _, position ->
collectList[position].let {
it.imageFile = null
it.imageVector = null
it.bitmap = null
it.isFinish = false
it.isShowCamera = true
it.uploadSuccess = false
}
collectionAdapter.notifyItemChanged(position)
// val count = collectList.count { it.imageFile != null && it.uploadSuccess.not() }
// binding.btnUploadImage.text = "待上传图片${count}张"
}
}
}
override fun onResume() {
super.onResume()
cameraUtils.bind()
binding.llCameraFlag.run {
visible()
postDelayed({
gone()
}, 3000)
}
}
override fun onPause() {
super.onPause()
cameraUtils.unbind()
binding.llCameraFlag.visible()
}
@SuppressLint("NotifyDataSetChanged")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityFoodCollectionBinding.inflate(layoutInflater)
setContentView(binding.root)
cameraUtils.initCamera()
binding.root.setOnClickListener {
it.hideKeyboard()
}
val previewBinding =
LayoutCameraPreviewBinding.inflate(layoutInflater, binding.flCameraPreview)
previewView = previewBinding.previewView.also {
it.updateLayoutParams {
width = 420.dp
height = 420.dp
}
}
cameraUtils.setPreviewController(previewView)
binding.btnBack.setOnClickListener { finish() }
binding.rvFoodCollection.let {
it.layoutManager = GridLayoutManager(this, 3, GridLayoutManager.VERTICAL, false)
it.adapter = collectionAdapter
}
repeat(MAX_COUNT) {
collectList.add(FoodCollectionBean(isShowCamera = true))
}
collectionAdapter.notifyDataSetChanged()
binding.ivGoodsSearch.setOnClickListener { searchGoods() }
binding.etGoodsInput.let { v ->
v.addOnActionSearchListener {
searchGoods()
}
}
// binding.btnUploadImage.setOnClickListener {
// val count = collectList.count { it.imageFile!=null && it.uploadSuccess.not() }
// if (count == 0) {
// toast("已全部上传成功")
// return@setOnClickListener
// }
// uploadAllImage()
// }
binding.btnSave.setOnClickListener { _ ->
if (clickIndex == -1) {
toast("请选择物品名称")
return@setOnClickListener
}
val count = collectList.count { it.imageVector != null }
if (count == 0) {
toast("请拍摄物品照片")
return@setOnClickListener
}
// vectorThread()
// uploadAllImage()
upload()
}
binding.btnTakePhoto.clickWithDebounce {
takePhoto()
}
binding.btnCollectedGoods.setOnClickListener {
CollectSearchDialog(this@FoodCollectionActivity).show()
}
binding.btnClearData.setOnClickListener { clearData() }
binding.rvSearch.let {
it.layoutManager = GridLayoutManager(this, 2, LinearLayoutManager.VERTICAL, false)
it.adapter = searchAdapter
}
binding.refreshLayout.let {
it.setEnableRefresh(true)
it.setEnableLoadMore(false)
it.setOnRefreshListener {
pageNo = 1
searchGoods()
}
it.setOnLoadMoreListener {
searchGoods()
}
}
loadEmptyView()
binding.tvTitle.setOnClickListener {
// startActivity<LocalImagePreviewActivity> { }
// val goodsImage = GoodsImage(goodsId = "1993493094888140802", goodsName = "多宝鱼", startTime = 1764120531856L, endTime = 1764121842825L)
// uploadImage(goodsImage)
// val goodsImage = GoodsImage(goodsId = "1986366073649287170", goodsName = "黑鱼(整条鱼)", startTime = 1764122703093L, endTime = 1764123156022L)
// uploadImage(goodsImage)
// val goodsImage = GoodsImage(goodsId = "1986699063868801025", goodsName = "鸡腿肉", startTime = 1764123375841L, endTime = 1764124039162L)
// uploadImage(goodsImage)
}
}
private var pageNo = 1
private var clickIndex = -1
private val searchGoodsList: MutableList<SearchGoodsInfo.Record> = mutableListOf()
private val searchAdapter by lazy {
GoodsSearchAdapter(searchGoodsList).apply {
isStateViewEnable = true
setOnItemClickListener { adapter, view, position ->
clickIndex = position
if (list[position].isSelected) {
return@setOnItemClickListener
}
list.forEach { item ->
item.isSelected = false
}
list[position].isSelected = true
notifyDataSetChanged()
}
}
}
private fun finishRefresh() {
binding.refreshLayout.let {
if (pageNo == 1) {
it.finishRefresh(500)
} else {
it.finishLoadMore(500)
}
}
}
@SuppressLint("NotifyDataSetChanged")
private fun takePhoto() {
val count = collectList.count { it.imageFile != null }
if (count == MAX_COUNT) {
toast("每次只允许保存${MAX_COUNT}条数据")
return
}
cameraUtils.takePhoto(cameraCallback)
}
@SuppressLint("NotifyDataSetChanged")
private fun clearData() {
collectList.forEach {
try {
if (it.bitmap?.isRecycled?.not() == true) {
it.bitmap?.recycle()
}
it.bitmap = null
it.imageVector = null
it.imageFile = null
it.isShowCamera = true
it.isFinish = false
it.uploadSuccess = false
} catch (e: Exception) {
e.printStackTrace()
}
}
collectionAdapter.notifyDataSetChanged()
clickIndex = -1
binding.etGoodsInput.setText("")
searchGoodsList.clear()
searchAdapter.notifyDataSetChanged()
loadEmptyView()
binding.ivFinish.invisible()
// binding.btnUploadImage.text = "待上传图片${0}张"
}
private val cameraCallback: (Uri) -> Unit = { uri ->
val index = collectList.indexOfFirst { it.imageFile == null }
if (index == -1) {
toast("每次只允许保存${MAX_COUNT}条数据")
rerurn@ cameraCallback
}
// toast("index=$index")
try {
ImageUtil.uriToBitmap(this, uri)?.let { bitmap ->
val cropBitmap = BitmapCropper.cropCenter(
original = bitmap,
targetWidth = 1000, targetHeight = 1300,
offsetX = 30, offsetY = 100
)
initBox()
val imageVector = try {
FoodModule.bitmap2FloatArray(cropBitmap, false)
} catch (e: Exception) {
e.printStackTrace()
toast("操作失败")
return@let
}
val foodList = FoodModule.queryFoodNameScore(imageVector)
val filterList =
foodList.filter { it.score < FoodModule.BAG_RATE && it.name.contains("黑袋子") }
if (filterList.isNotEmpty()) {
toast("当前物品可能被袋子遮挡,请检查后重试")
return@let
}
val file = BitmapSaver.saveToAppFilesDir(
cropBitmap, this, "IMG_CROP_${System.currentTimeMillis()}.jpg"
)
Timber.d("${this.javaClass.simpleName}-searchFood-裁剪bitmap保存文件路径:${file?.absolutePath}")
collectList[index].let {
it.imageVector = imageVector
it.bitmap = null
it.imageFile = file
it.isShowCamera = false
it.isFinish = false
it.uploadSuccess = false
}
collectionAdapter.notifyItemChanged(index)
// val count = collectList.count { it.imageFile != null && it.uploadSuccess.not() }
// binding.btnUploadImage.text = "待上传图片${count}张"
if (cropBitmap.isRecycled.not()) {
cropBitmap.recycle()
}
if (bitmap.isRecycled.not()) {
bitmap.recycle()
}
}
} catch (e: Exception) {
e.printStackTrace()
toast("程序异常${e.message}")
}
}
private fun vectorThread() {
Loading.show(this)
Thread {
collectList
//.filter { it.bitmap != null }
// .filter { it.imageVector != null }
.forEachIndexed { index, it ->
//image2VectorTask(it.bitmap!!, index)
it.imageVector?.let{ imageVector ->
image2VectorTask(imageVector = imageVector, position = index)
}
}
runOnUiThread {
window.decorView.postDelayed({
Loading.dismiss()
toast("保存成功")
}, 1000)
}
}.start()
}
private fun image2VectorTask(
bitmap: Bitmap? = null,
imageVector: FloatArray? = null,
position: Int
) {
try {
initBox()
// val imageVector = try {
// FoodModule.bitmap2FloatArray(bitmap)
// } catch (e: Exception) {
// e.printStackTrace()
// //toast("保存失败")
// return
// }
val goods = searchGoodsList[clickIndex]
val saveName = goods.goodsName + goods.goodsCode
ObjectBox.boxStore.runInTx {
box?.put(Food(name = saveName, foodIdx = 0, foodVector = imageVector))
}
collectList[position].let {
// it.imageVector = imageVector
it.isFinish = true
}
runOnUiThread {
collectionAdapter.notifyItemChanged(position)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
@SuppressLint("NotifyDataSetChanged")
private fun searchGoods() {
val searchName = binding.etGoodsInput.text.toString().trim()
if (searchName.isBlank()) {
toast("请输入物品名称")
finishRefresh()
return
}
searchGoods(
searchName = searchName,
pageNo = pageNo
) { records ->
finishRefresh()
binding.etGoodsInput.hideKeyboard()
if (pageNo == 1 && records.isEmpty()) {
searchGoodsList.clear()
searchAdapter.notifyDataSetChanged()
loadEmptyView()
return@searchGoods
}
if (pageNo == 1) {
searchGoodsList.clear()
}
searchGoodsList.addAll(records)
val enableLoadMore = records.size >= PAGE_SIZE
binding.refreshLayout.setEnableLoadMore(enableLoadMore)
if (enableLoadMore) {
pageNo++
}
binding.rvSearch.layoutManager =
GridLayoutManager(this, 2, GridLayoutManager.VERTICAL, false)
searchAdapter.notifyDataSetChanged()
}
}
private var emptyBinding: LayoutEmptySearchBinding? = null
private fun loadEmptyView() {
binding.rvSearch.layoutManager =
LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
if (emptyBinding == null) {
emptyBinding = LayoutEmptySearchBinding.inflate(layoutInflater, binding.rvSearch, false)
}
emptyBinding?.tvContent?.text = "请检查物品名称,或稍后重新搜索"
emptyBinding?.root?.let { layout ->
layout.setOnClickListener { layout.hideKeyboard() }
searchAdapter.stateView = layout
}
}
fun searchGoods(
searchName: String,
pageNo: Int,
pageSize: Int = PAGE_SIZE,
callback: (List<SearchGoodsInfo.Record>) -> Unit
) {
runBlocking {
val records = viewModel.searchGoodsInfoList2(
goodsName = searchName,
pageNo = pageNo,
pageSize = pageSize
)
runOnUiThread {
callback(records)
}
}
}
private fun initBox() {
if (box == null) {
box = ObjectBox.boxStore.boxFor(Food::class)
}
}
// private fun uploadAllImage() {
// Loading.show(this)
// val files = collectList.filter { it.imageFile!=null }.map { it.imageFile!! }
// if (files.isEmpty()) {
// return
// }
// val goods = searchGoodsList[clickIndex]
// for (index in collectList.indices step 5) {
// val end = if(index + 5 < collectList.size - 1) index + 5 else collectList.size - 1
// val subList = collectList.subList(index, end)
// val subFiles = subList.map { it.imageFile }
// viewModel.uploadMultipleImages(goods.id!!, goods.goodsName!!, subFiles) {isSuccess->
// Timber.tag(TAG).d("uploadMultipleImages: ${isSuccess}")
// subList.filter { it.imageFile!=null }.forEach { it.uploadSuccess = isSuccess }
//
// val count = collectList.count { it.imageFile!=null && it.uploadSuccess.not() }
// runOnUiThread {
// binding.btnUploadImage.text = "待上传图片${count}张"
// if (count == 0) {
// Loading.dismiss()
// }
// }
// }
// }
// }
private fun upload() {
lifecycleScope.launch {
val totalFileCount = collectList.count { it.imageFile != null }
showWaitingDialog2("图片上传中0/$totalFileCount")
val goods = searchGoodsList[clickIndex]
val params = HashMap<String, RequestBody>()
params["goodsId"] = goods.id!!.toRequestBody()
params["goodsName"] = goods.goodsName!!.toRequestBody()
//params["foodVector"] = foodVector.toRequestBody()
ImageUploader(
totalList = collectList,
uploadImage = { batch ->
val files = batch.map { it.imageFile }
viewModel.uploadMultipleImages(files = files, params)
},
onProgress = { count, batch ->
runOnUiThread {
showWaitingDialog2("图片上传中$count/$totalFileCount")
batch.forEach {
it.uploadSuccess = true
}
}
},
onError = {
runOnUiThread {
hideWaitingDialog()
toast("上传失败,请稍后重试")
}
},
onComplete = {
hideWaitingDialog()
vectorThread()
}
).processUploads()
}
}
private val receiptViewModel: ReceiptViewModel by viewModels()
fun searchGoodsInfoList(goodsName: String, block: (List<SearchGoodsInfo.Record>) -> Unit) {
runBlocking {
val list = searchGoodsInfoList2(goodsName)
block(list)
}
}
suspend fun searchGoodsInfoList2(
goodsName: String,
pageNo: Int = 1,
pageSize: Int = 10
): List<SearchGoodsInfo.Record> {
return receiptViewModel.searchGoodsInfoList2(goodsName, pageNo, pageSize)
}
private var mDialogWaiting: CustomDialog? = null
/**
* 显示等待提示框
*/
fun showWaitingDialog(tip: String?): Dialog? {
hideWaitingDialog()
val view = View.inflate(this, R.layout.dialog_waiting, null)
if (!TextUtils.isEmpty(tip)) (view.findViewById<View?>(R.id.tvTip) as TextView).text = tip
mDialogWaiting = CustomDialog(this, view, R.style.MyDialog)
mDialogWaiting!!.show()
mDialogWaiting!!.setCancelable(true)
return mDialogWaiting
}
/**
* 隐藏等待提示框
*/
fun hideWaitingDialog() {
mDialogWaiting?.dismiss()
mDialogWaiting = null
}
fun showWaitingDialog2(tip: String?) {
if (mDialogWaiting == null) {
hideWaitingDialog()
val view = View.inflate(this, R.layout.dialog_waiting, null)
mDialogWaiting = CustomDialog(this, view, R.style.MyDialog)
mDialogWaiting!!.show()
mDialogWaiting!!.setCancelable(true)
}
val contentView = mDialogWaiting?.findViewById<ViewGroup>(android.R.id.content)
val tvTip = contentView?.findViewById<TextView>(R.id.tvTip)
tvTip?.text = tip
}
data class RespData(
var code: String? = null,
var data: String? = null,
var msg: String? = null,
)
data class GoodsImage(
var goodsId: String,
var goodsName: String,
var startTime: Long,
var endTime: Long
)
//
// val goodsImageList = mutableListOf<GoodsImage>().apply {
// add(
// GoodsImage(
// goodsId = "1986353555971297282",
// goodsName = "马铃薯[土豆、洋芋]",
// startTime = 1763692477786L,
// endTime = 1763693153825L
// )
// )
// add(GoodsImage(
// goodsId = "1986607476769869825",
// goodsName = "辣椒(青辣椒,螺丝椒)",
// startTime = 1763693182929L,
// endTime = 1763693621664L
// ))
// add(GoodsImage(
// goodsId = "1987880591453859842",
// goodsName = "辣椒(红,小)",
// startTime = 1763693683837L,
// endTime = 1763694154521L
// ))
// add(GoodsImage(
// goodsId = "1986699063868801025",
// goodsName = "鸡腿肉",
// startTime = 1763694221267L,
// endTime = 1763695052924L
// ))
//
// add(GoodsImage(
// goodsId = "1986689570300809217",
// goodsName = "猪腿肉",
// startTime = 1763695212428L,
// endTime = 1763695810578L
// ))
// add(GoodsImage(
// goodsId = "1986673470079029249",
// goodsName = "西兰花[绿菜花]",
// startTime = 1763695924575L,
// endTime = 1763696252669L
// ))
// add(GoodsImage(
// goodsId = "1991473021965070338",
// goodsName = "姜[黄姜]",
// startTime = 1763696258001L,
// endTime = 1763696294831L
// ))
// add(GoodsImage(
// goodsId = "00000",
// goodsName = "其它",
// startTime = 1763696442577L,
// endTime = 1763696990541L
// ))
// }
// private fun uploadImage(goodsImage: GoodsImage) {
// Loading.show(this)
// val cropFile = File(this.cacheDir, "crop")
// val list = mutableListOf<File>()
// val startTime = goodsImage.startTime
// val endTime = goodsImage.endTime
// cropFile.listFiles()?.forEach {
// if (it.isDirectory) {
// return@forEach
// }
// val start = "IMG_CROP_".length
// val end = it.name.indexOf(".jpg")
// val time = it.name.substring(start, end).toLong()
// if (time in startTime..endTime) {
// list.add(it)
// }
// if (list.size < 5) {
// return@forEach
// }
// viewModel.uploadMultipleImages(
// goodsId = goodsImage.goodsId,
// goodsName = goodsImage.goodsName,
// files = list
// ) { isSuccess ->
//// val resp = data?.toObject<RespData>()
//// val isSuccess = resp?.code == "00000"
// binding.ivFinish.run {
// if (isSuccess) visible() else invisible()
// }
// }
// list.clear()
// }
// if (list.isNotEmpty()) {
// viewModel.uploadMultipleImages(
// goodsId = goodsImage.goodsId,
// goodsName = goodsImage.goodsName,
// files = list
// ) { isSuccess ->
//// val resp = data?.toObject<RespData>()
//// val isSuccess = resp?.code == "00000"
// binding.ivFinish.run {
// if (isSuccess) visible() else invisible()
// }
// }
// }
// Loading.dismiss()
// }
}
@@ -0,0 +1,644 @@
package com.sw.inbound.activity
import android.net.Uri
import android.os.Bundle
import android.text.SpannableStringBuilder
import android.text.style.ForegroundColorSpan
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedCallback
import androidx.activity.viewModels
import androidx.camera.view.PreviewView
import androidx.core.graphics.toColorInt
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import com.sw.inbound.utils.ext.appendText
import com.sw.inbound.utils.ext.buildSpannableString
import com.sw.inbound.GlobalData
import com.sw.inbound.R
import com.sw.inbound.databinding.ActivityGoodsListBinding
import com.sw.inbound.databinding.LayoutCameraPreviewBinding
import com.sw.inbound.databinding.ListItemReceiptGoodsBinding
import com.sw.inbound.databinding.ListItemSelfProcurementBinding
import com.sw.inbound.dialog.DialogManager
import com.sw.inbound.dialog.DropdownPopup
import com.sw.inbound.dialog.GoodsRecognizeDialog
import com.sw.inbound.dialog.GoodsSearchDialog
import com.sw.inbound.dialog.Loading
import com.sw.inbound.dialog.WarnDialog
import com.sw.inbound.model.request.GoodsAddParam
import com.sw.inbound.model.request.PurchaseWarehouseParam
import com.sw.inbound.model.request.UploadInfo
import com.sw.inbound.model.response.DictType
import com.sw.inbound.model.response.GoodsInfo
import com.sw.inbound.model.response.SearchGoodsInfo
import com.sw.inbound.objbox.FoodModule
import com.sw.inbound.sdk.SensorScaleUtils
import com.sw.inbound.utils.BitmapCropper
import com.sw.inbound.utils.CameraUtils
import com.sw.inbound.utils.ImageUtil
import com.sw.inbound.utils.PreciseDelayHandler
import com.sw.inbound.utils.ext.dp
import com.sw.inbound.utils.ext.gone
import com.sw.inbound.utils.ext.hideKeyboard
import com.sw.inbound.utils.ext.roundedDecimalPlace
import com.sw.inbound.utils.ext.toJsonString
import com.sw.inbound.utils.ext.toast
import com.sw.inbound.viewmodel.ReceiptViewModel
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import timber.log.Timber
import java.io.File
import kotlin.math.abs
import kotlin.math.min
typealias RecognizeCallback = (List<SearchGoodsInfo.Record>) -> Unit
@AndroidEntryPoint
abstract class GoodsListActivity : ComponentActivity() {
companion object {
private const val TAG = "GoodsListActivity"
public const val ID = "id"
public const val SUPPLIER_ID = "supplierId"
public const val IS_RECEIPT_PAGE = "isReceiptPage"
public const val IS_NEW_RECEIPT = "isNewReceipt"
const val PAGE_SIZE = 10
private const val DELAY_TIME: Long = 3 * 1000
}
var isReceiptPage: Boolean = false
var isNewReceipt: Boolean = false
var id: String = ""
var supplierId: String = ""
lateinit var binding: ActivityGoodsListBinding
val viewModel: ReceiptViewModel by viewModels()
val goodsList: MutableList<GoodsInfo> = mutableListOf()
abstract fun initRecyclerView()
abstract fun initView()
abstract fun getWarehouseList(): List<DictType>
private lateinit var backPressedCallback: OnBackPressedCallback
private lateinit var previewView: PreviewView
private val cameraUtils: CameraUtils by lazy {
CameraUtils(this@GoodsListActivity)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityGoodsListBinding.inflate(layoutInflater)
setContentView(binding.root)
initBackDispatcher()
isReceiptPage = intent.getBooleanExtra(IS_RECEIPT_PAGE, true)
isNewReceipt = intent.getBooleanExtra(IS_NEW_RECEIPT, false)
id = intent.getStringExtra(ID) ?: ""
supplierId = intent.getStringExtra(SUPPLIER_ID) ?: ""
cameraUtils.initCamera()
replaceIncludeContent()
initRecyclerView()
initView()
binding.root.setOnClickListener {
it.hideKeyboard()
}
binding.btnBack.setOnClickListener { handleBackEvent() }
binding.flWarehouseDropdown.setOnClickListener { v ->
if (GlobalData.warehouseTypeList.isEmpty()) {
toast("暂无仓库数据")
return@setOnClickListener
}
if (dropdownPopup == null) {
dropdownList.clear()
dropdownList.addAll(GlobalData.warehouseTypeList)
dropdownPopup = DropdownPopup(
context = this,
list = dropdownList,
popHeight = min(355.dp, dropdownList.size * 71.dp)
) {
binding.tvWareHouse.run {
text = it.value
tag = it.id
}
}.also {
it.arrowImage = binding.ivDropdown
it.bgLayout = binding.flWarehouseDropdown
it.defLayoutBgResId = R.drawable.bg_white_radius10_stroke
}
}
dropdownPopup?.showAsDropDown(v)
}
binding.tvPurchaseNo.setOnClickListener {
handleGoodsResult()
}
binding.tvUserName.text = GlobalData.deviceId
binding.tvUserName.gone()
binding.ivLogout.gone()
val previewBinding =
LayoutCameraPreviewBinding.inflate(layoutInflater, binding.flCameraPreview)
previewView = previewBinding.previewView.also {
it.updateLayoutParams {
width = 1
height = 1
}
}
cameraUtils.setPreviewController(previewView)
//startTime = System.currentTimeMillis()
binding.root.postDelayed({
SensorScaleUtils.addWeightListener {
runOnUiThread {
try {
weightCallback?.invoke(it)
recognizeWeight(weight = it)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}, 1 * 1000)
}
private var weightCallback: ((weight: Int) -> Unit)? = null
fun readWeightInfo(callback: (weight: Int) -> Unit) {
weightCallback = callback
}
private var lastWeight = 0
private fun recognizeWeight(weight: Int) {
Timber.tag(TAG).d("recognizeWeight,lastWeight=$lastWeight,weight=$weight")
//if (this.lastWeight - weight >= 0) {
// this.lastWeight = weight
// //从秤上取物品中
// return
//}
if (weight < 200) {
if (abs(weight) < 10) {
// val clazzName = DialogManager.getDialogList()
// .map { it.javaClass.simpleName }
// .joinToString(",")
// Timber.tag(TAG).d(clazzName)
if (DialogManager.getDialogList().size == 1 &&
DialogManager.contains("GoodsRecognizeDialog") &&
goodsRecognizeDialog?.isShowing == true
) {
goodsRecognizeDialog?.dismiss()
}
}
return
}
this.lastWeight = weight
if (DialogManager.hasShowDialog()) {
return
}
// val checkResult = isFirstRecognize.not()
// //&& System.currentTimeMillis() - startTime <= DELAY_TIME
// Timber.tag(TAG).d("recognizeWeight,checkResult=$checkResult")
// if (checkResult) {
// //Timber.tag(TAG).d("recognizeWeight,等待中:${(System.currentTimeMillis() - startTime)/1000}")
// Timber.tag(TAG).d("recognizeWeight, checkResult=true")
// return
// }
Timber.tag(TAG)
.d("recognizeWeight,recognizeFoodisShowRecognizeDialog=$isShowRecognizeDialog")
recognizeFood()
}
//var startTime = 0L
var isFirstRecognize = true
private val takePhotoSuccessCallback: (Uri) -> Unit = { uri ->
Thread {
try {
searchFood(uri)
} catch (e: Exception) {
e.printStackTrace()
}
}.start()
}
private val delayHandler by lazy {
PreciseDelayHandler()
}
fun delayRecognizeFood() {
if (abs(lastWeight) < 10) return
delayHandler.precisePostDelayed(DELAY_TIME) {
runOnUiThread {
recognizeFood()
}
}
}
fun recognizeFood() {
if (isShowRecognizeDialog.not()) {
if (goodsRecognizeDialog == null) {
isShowRecognizeDialog = true
}
return
}
Timber.tag(TAG).d("recognizeFood,takePhoto")
isShowRecognizeDialog = false
//bindCamera()
try {
cameraUtils.takePhoto(takePhotoSuccessCallback)
isFirstRecognize = false
} catch (e: Exception) {
Timber.tag(TAG).d("recognizeFood,takePhoto异常:${e.message}")
e.printStackTrace()
handleGoodsResult()
}
}
private var recognizeCallback: RecognizeCallback? = null
var isAgainRecognize = false
fun againRecognizeFood(callback: RecognizeCallback) {
Loading.show(this)
isAgainRecognize = true
this.recognizeCallback = callback
try {
cameraUtils.takePhoto(takePhotoSuccessCallback)
} catch (e: Exception) {
Timber.tag(TAG).d("againRecognizeFood,takePhoto异常:${e.message}")
e.printStackTrace()
Loading.dismiss()
runOnUiThread {
recognizeCallback?.invoke(mutableListOf())
}
}
}
private var dropdownPopup: DropdownPopup? = null
private val dropdownList: MutableList<DictType> = mutableListOf()
fun getSpannable(text: String, num: Int, color: String): SpannableStringBuilder {
return buildSpannableString {
appendText(text, ForegroundColorSpan("#FF141428".toColorInt()))
appendText(num.toString(), ForegroundColorSpan(color.toColorInt()))
}
}
var isShowRecognizeDialog = true
private fun searchFood(uri: Uri) {
ImageUtil.uriToBitmap(this, uri)?.let { bitmap ->
val newBmp = BitmapCropper.cropCenter(
original = bitmap,
targetWidth = 1000, targetHeight = 1300,
offsetX = 30, offsetY = 100
)
// val file = BitmapSaver.saveToAppFilesDir(
// newBmp,
// this,
// "IMG_CROP_${System.currentTimeMillis()}.jpg"
// )
// Timber.d("${this.javaClass.simpleName}-searchFood-裁剪bitmap保存文件路径:${file?.absolutePath}")
//var foodList = FoodModule.queryFood(newBmp)
//物品编号
//foodList = foodList.map { "WP"+it.split("WP")[1] }
//物品名称
//foodList = foodList.map { it.split("WP")[0] }
val foodList = FoodModule.getFoodScoreList(newBmp)
if (bitmap.isRecycled.not()) {
bitmap.recycle()
}
if (newBmp.isRecycled.not()) {
newBmp.recycle()
}
if (foodList.isEmpty()) {
recognizeFoodError()
return@let
}
foodList.forEach {
it.name = it.name.split("WP")[0]
}
val filterList =
foodList.filter { it.score < FoodModule.BAG_RATE && it.name.contains("黑袋子") }
if (filterList.isNotEmpty()) {
runOnUiThread {
toast("当前物品可能被袋子遮挡,请检查后重试")
if (isAgainRecognize) {
Loading.dismiss()
recognizeCallback?.invoke(mutableListOf())
isAgainRecognize = false
return@runOnUiThread
}
handleGoodsResult(mutableListOf())
}
return@let
}
viewModel.recognizeGoodsList(
//nameList = foodList,
scoreList = foodList,
) { goodsList ->
runOnUiThread {
if (isAgainRecognize) {
Loading.dismiss()
recognizeCallback?.invoke(goodsList)
isAgainRecognize = false
return@runOnUiThread
}
handleGoodsResult(
goodsList = if (goodsList.size == foodList.size) goodsList else mutableListOf(),
// file = null,
// name = foodList.joinToString(",")
)
}
}
}
}
private fun handleGoodsResult(
goodsList: List<SearchGoodsInfo.Record> = mutableListOf(),
file: File? = null,
name: String? = null,
) {
runOnUiThread {
if (goodsRecognizeDialog?.isShowing == true) {
//isShowRecognizeDialog = false
return@runOnUiThread
}
goodsRecognizeDialog = GoodsRecognizeDialog(
context = this,
list = goodsList.toMutableList(),
goodsFile = file,
goodsName = name,
) {
GoodsSearchDialog(this).show()
}.apply {
if (isFinishing.not() && isDestroyed.not()) {
//isShowRecognizeDialog = false
show()
}
}
}
}
var goodsRecognizeDialog: GoodsRecognizeDialog? = null
private fun replaceIncludeContent() {
binding.flContainer.removeAllViews()
if (isReceiptPage) {
val tempBinding =
ListItemReceiptGoodsBinding.inflate(layoutInflater, binding.flContainer, false)
binding.flContainer.addView(tempBinding.root)
} else {
val tempBinding =
ListItemSelfProcurementBinding.inflate(layoutInflater, binding.flContainer, false)
binding.flContainer.addView(tempBinding.root)
}
}
override fun onPause() {
super.onPause()
unBindCamera()
}
fun bindCamera() {
cameraUtils.bind()
}
fun unBindCamera() {
cameraUtils.unbind()
}
override fun onDestroy() {
try {
DialogManager.dismissAll()
} catch (e: Exception) {
e.printStackTrace()
}
super.onDestroy()
//cameraController?.unbind()
//isCameraReady = false
}
fun Double?.plus2(value: Double?): Double {
return (this ?: 0.0) + (value ?: 0.0)
}
fun addGoods(goods: GoodsInfo) {
if (!isReceiptPage) {
goods.isSelected = true
goodsList.add(goods)
binding.rvGoodsList.adapter?.notifyItemInserted(goodsList.size - 1)
return
}
val item = goodsList.firstOrNull { it.goodId == goods.goodId }
if (item == null || item.unitName != goods.unitName) {
//不存在物品或者物品单位不同
goods.isSelected = true
goodsList.add(goods)
binding.rvGoodsList.adapter?.notifyItemInserted(goodsList.size - 1)
return
}
item.isSelected = true
val finalWeight = item.goodsWeight?.toDouble().plus2(goods.goodsWeight?.toDouble())
val finalNum = item.receivedNum.plus2(goods.receivedNum)
val finalAmount = item.recPriceInItem.plus2(goods.recPriceInItem)
val finalPrice =
if (finalAmount == 0.toDouble() || finalNum == 0.toDouble()) 0.toDouble() else finalAmount / finalNum
item.run {
receivedNum = finalNum
recUnitPriceTaxIn = finalPrice.roundedDecimalPlace(2)
recPriceInItem = finalAmount.roundedDecimalPlace(2)
goodsWeight = finalWeight.toBigDecimal()
}
binding.rvGoodsList.adapter?.notifyItemChanged(goodsList.indexOf(item))
if (this is ReceiptActivity) {
this.setConfirmNum()
}
}
fun searchGoods(
searchName: String,
pageNo: Int,
pageSize: Int = PAGE_SIZE,
callback: (List<SearchGoodsInfo.Record>) -> Unit
) {
runBlocking {
val records = searchGoodsBlock(searchName, pageNo, pageSize)
runOnUiThread {
callback(records)
}
}
}
suspend fun searchGoodsBlock(
searchName: String,
pageNo: Int,
pageSize: Int = PAGE_SIZE
): List<SearchGoodsInfo.Record> {
return viewModel.searchGoodsInfoList2(
goodsName = searchName,
pageNo = pageNo,
pageSize = pageSize
)
}
fun searchMultipleGoods(
goodsNameList: List<String>,
callback: (MutableList<SearchGoodsInfo.Record>) -> Unit
) {
runBlocking {
val list = mutableListOf<SearchGoodsInfo.Record>()
for (goodsName in goodsNameList) {
val records = searchGoodsBlock(goodsName, 1, 1)
if (records.isNotEmpty()) {
list.add(records[0])
}
}
callback(list)
}
}
private fun initBackDispatcher() {
// 创建回调,true表示初始启用状态
backPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
handleBackEvent()
}
}
// 注册回调,使用lifecycleOwner确保生命周期安全
onBackPressedDispatcher.addCallback(this, backPressedCallback)
}
private fun handleBackEvent() {
val count = goodsList.count { it.isLocalGoods }
// 处理返回事件
if (count > 0) {
WarnDialog(
context = this@GoodsListActivity,
content = "存在未收货的物品,请确认是否放弃收货,若放弃则数据不会保存?"
) {
//onBackPressedDispatcher.onBackPressed()
finish()
}.show()
} else {
// 允许默认行为
backPressedCallback.isEnabled = false
//onBackPressedDispatcher.onBackPressed()
finish()
}
}
fun receiptSubmit() {
if (binding.tvWareHouse.tag == null) {
toast("请选择仓库")
binding.flWarehouseDropdown.setBackgroundResource(R.drawable.bg_white_stroke_red)
return
}
goodsList.forEach { goodsInfo ->
goodsInfo.warehouseId = binding.tvWareHouse.tag.toString()
goodsInfo.warehouseName = binding.tvWareHouse.text.toString()
}
if (goodsList.isEmpty()) {
toast("请选择收货物品")
return
}
val uploadInfo = UploadInfo(
id = id,
supplierId = supplierId,
receiveGoodsInfos = goodsList
)
Timber.tag("SelfProcurementActivity")
.d("receiptSubmit确认收货提交数据:${uploadInfo.toJsonString()}")
viewModel.confirmReceipt(uploadInfo) { success ->
if (success) {
toast("确认收货成功")
finish()
} else {
toast("确认收货失败")
}
}
}
fun selfProcurementSubmit() {
if (binding.tvWareHouse.tag == null) {
toast("请选择仓库")
binding.flWarehouseDropdown.setBackgroundResource(R.drawable.bg_white_stroke_red)
return
}
if (goodsList.isEmpty()) {
toast("暂无入库物品,请采集后操作")
return
}
val submitData = goodsList.map {
PurchaseWarehouseParam(
goodsId = it.goodId ?: "",
goodsName = it.goodName,
kcUnitId = it.kcUnitId,
goodsCount = it.goodsCount ?: 0.0,
goodsUnitPrice = it.goodsUnitPrice ?: 0.0,
goodsPrice = it.goodsPrice ?: 0.0,
//goodPurId = it.goodsPurId,
warehouseId = binding.tvWareHouse.tag.toString(),
buyToInventoryValue = it.buyToInventoryValue ?: ""
)
}
Timber.tag("SelfProcurementActivity")
.d("selfProcurementSubmit自采入库提交数据:${submitData.toJsonString()}")
viewModel.addToWarehouse(submitData) { success ->
if (success) {
toast("提交入库成功")
finish()
} else {
toast("提交入库失败")
}
}
}
// fun uploadImage(imageUri: Uri, callback: (String?) -> Unit) {
// viewModel.uploadImage(imageUri) { imgUrl ->
// if (imgUrl.isNullOrBlank()) {
// callback(null)
// return@uploadImage
// }
// callback(imgUrl)
// }
// }
fun createNewGoods(file: File, param: GoodsAddParam, callback: (Boolean) -> Unit) {
viewModel.uploadImage(file) { imgUrl ->
if (imgUrl.isNullOrBlank()) {
toast("图片上传失败")
return@uploadImage
}
param.relativeUrl = imgUrl
viewModel.createNewGoods(param = param, callback = callback)
}
}
private fun recognizeFoodError() {
if (isAgainRecognize) {
runOnUiThread {
Loading.dismiss()
//toast("识别失败,请稍候重试")
recognizeCallback?.invoke(mutableListOf())
}
isAgainRecognize = false
} else {
handleGoodsResult()
}
}
fun uploadImage(goodsId: String, goodsName: String, imageFile: File?) {
lifecycleScope.launch {
if (imageFile == null) {
return@launch
}
val list = mutableListOf<File>()
list.add(imageFile)
val map = hashMapOf<String, RequestBody>()
map["goodsId"] = goodsId.toRequestBody()
map["goodsName"] = goodsName.toRequestBody()
viewModel.uploadMultipleImages(files = list, params = map)
}
}
}
@@ -0,0 +1,97 @@
package com.sw.inbound.activity
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.activity.ComponentActivity
import androidx.recyclerview.widget.GridLayoutManager
import coil.load
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.sw.inbound.databinding.ActivityLocalImagePreviewBinding
import com.sw.inbound.databinding.ListItemImagePreviewBinding
import com.sw.inbound.dialog.Loading
import com.sw.inbound.utils.ext.copyText
import java.io.File
class LocalImagePreviewActivity : ComponentActivity() {
private lateinit var binding: ActivityLocalImagePreviewBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityLocalImagePreviewBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.recyclerView.let {
it.layoutManager = GridLayoutManager(this, 6, GridLayoutManager.VERTICAL, false)
it.adapter = imageAdapter
}
binding.btnBack.setOnClickListener { finish() }
loadData()
}
fun loadData() {
Thread {
runOnUiThread {
Loading.show(this)
}
val cropFile = File(this.cacheDir, "crop")
cropFile.listFiles()?.forEach {
if (it.isDirectory) {
return@forEach
}
val start = "IMG_CROP_".length
val end = it.name.indexOf(".jpg")
val time = it.name.substring(start, end).toLong()
if (time >= 1764086400L) {
imageList.add(ImageBean(file = it, name = "$time"))
}
}
imageList.sortBy { it.name.toLong() }
runOnUiThread {
imageAdapter.notifyDataSetChanged()
Loading.dismiss()
}
}.start()
}
private var isCycle = true
private val imageList = mutableListOf<ImageBean>()
private val imageAdapter: ImageAdapter by lazy {
ImageAdapter(imageList).apply {
setOnItemClickListener { _, _, position ->
imageList[position].name.copyText(this@LocalImagePreviewActivity)
}
}
}
data class ImageBean(
var file: File,
var name: String
)
inner class ImageAdapter(var list: MutableList<ImageBean>) :
BaseQuickAdapter<ImageBean, ImageAdapter.VH>(list) {
inner class VH(var binding: ListItemImagePreviewBinding) : QuickViewHolder(binding.root)
override fun onBindViewHolder(holder: VH, position: Int, item: ImageBean?) {
holder.binding.let {
it.imageView.load(item!!.file)
it.textView.text = item.name
}
}
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val inflater = LayoutInflater.from(context)
val binding = ListItemImagePreviewBinding.inflate(inflater, parent, false)
return VH(binding)
}
}
}
@@ -0,0 +1,101 @@
package com.sw.inbound.activity
import android.annotation.SuppressLint
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager
import com.sw.inbound.adapter.ReceiptGoodsAdapter
import com.sw.inbound.model.response.DictType
import com.sw.inbound.utils.ext.gone
import com.sw.inbound.utils.ext.toast
import com.sw.inbound.utils.ext.visible
import kotlinx.coroutines.launch
class ReceiptActivity : GoodsListActivity() {
private val adapter by lazy {
ReceiptGoodsAdapter(goodsList)
}
override fun initRecyclerView() {
binding.rvGoodsList.let {
it.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
it.adapter = adapter
}
}
@SuppressLint("NotifyDataSetChanged")
override fun initView() {
//initRvTestData()
binding.tvConfirmNum.run {
if (isNewReceipt) gone() else visible()
}
binding.tvNotConfirmNum.run {
if (isNewReceipt) gone() else visible()
}
binding.btnConfirmReceipt.setOnClickListener {
if (isNewReceipt) {
selfProcurementSubmit()
} else {
receiptSubmit()
}
}
if (isNewReceipt.not()) {
viewModel.getReceiveDetail(id) { detail ->
runOnUiThread {
if (detail == null) {
toast("未查询到数据")
return@runOnUiThread
}
binding.tvPurchaseNo.text = "采购单号:${detail.purCode}"
val list = detail.receiveGoodsInfoList
list.forEach { goodsInfo ->
goodsInfo.recUnitPriceTaxInBak = goodsInfo.recUnitPriceTaxIn
goodsInfo.recUnitPriceTaxIn = 0.0
goodsInfo.unitNameBak = goodsInfo.unitName
goodsInfo.receivedNumBak = goodsInfo.receivedNum
}
goodsList.addAll(list)
adapter.notifyDataSetChanged()
setConfirmNum()
}
}
} else {
binding.tvPurchaseNo.text = "新增收货"
}
// lifecycleScope.launch {
// repeatOnLifecycle(Lifecycle.State.CREATED) {
// launch {
// viewModel.adjustedOrders.collect {
// val confirmOrderCount = it.size
// binding.tvConfirmNum.text = getSpannable("已确认 ", num = confirmOrderCount, color = "#FF009632")
// val notConfirmOrderCount = goodsList.size - confirmOrderCount
// binding.tvNotConfirmNum.text = getSpannable("未确认 ", num = notConfirmOrderCount, color = "#FF009632")
// }
// }
// }
// }
}
override fun getWarehouseList(): List<DictType> {
return goodsList.filter {
it.warehouseId.isNullOrBlank().not() && it.warehouseName.isNullOrBlank().not()
}
.distinctBy { it.warehouseId }
.map {
DictType(id = it.warehouseId!!, value = it.warehouseName!!)
}
}
fun setConfirmNum() {
val confirmOrderCount = goodsList.count { it.isAdjusted }
binding.tvConfirmNum.text =
getSpannable("已确认 ", num = confirmOrderCount, color = "#FF009632")
val notConfirmOrderCount = goodsList.size - confirmOrderCount
binding.tvNotConfirmNum.text =
getSpannable("未确认 ", num = notConfirmOrderCount, color = "#FFFF0000")
}
}
@@ -0,0 +1,62 @@
package com.sw.inbound.activity
import android.util.Log
import androidx.recyclerview.widget.LinearLayoutManager
import com.sw.inbound.R
import com.sw.inbound.adapter.SelfProcurementAdapter
import com.sw.inbound.dialog.WarnDialog
import com.sw.inbound.model.request.PurchaseWarehouseParam
import com.sw.inbound.model.response.DictType
import com.sw.inbound.utils.ext.toJsonString
import com.sw.inbound.utils.ext.toast
import timber.log.Timber
import kotlin.getValue
class SelfProcurementActivity : GoodsListActivity() {
private val adapter by lazy {
SelfProcurementAdapter(goodsList).apply {
setOnItemClickListener { adapter, view, position ->
goodsList[position].let {
it.isSelected = it.isSelected.not()
}
notifyItemChanged(position)
}
addOnItemChildClickListener(R.id.ivState) { adapter, view, position ->
///删除行数据弹窗
showWarnDialog(content = "请确认是否删除该条入库数据,删除后将无法恢复?") {
removeAt(position)
}
}
}
}
private fun showWarnDialog(content:String, callback:()-> Unit) {
WarnDialog(context = this, content = content, confirmBlock = callback).show()
}
override fun initRecyclerView() {
binding.rvGoodsList.let {
it.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
it.adapter = adapter
}
}
override fun initView() {
binding.tvPurchaseNo.text = "自采入库"
binding.btnConfirmReceipt.setOnClickListener {
selfProcurementSubmit()
}
}
override fun getWarehouseList(): List<DictType> {
return goodsList.filter {
it.warehouseId.isNullOrBlank().not() && it.warehouseName.isNullOrBlank().not()
}
.distinctBy { it.warehouseId }
.map {
DictType(id = it.warehouseId!!, value = it.warehouseName!!)
}
}
}