feat: 国际进港舱单编辑支持交接图片新增与删除
编辑/新增表单页增加交接图片区(选图、上传、长按删除), 保存时随舱单提交 pic/originalPic/picNumber 三字段, 编辑模式回填已有图片且不重复上传。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import com.lukouguoji.gjj.viewModel.GjjManifestAddViewModel
|
|||||||
import com.lukouguoji.module_base.base.BaseBindingActivity
|
import com.lukouguoji.module_base.base.BaseBindingActivity
|
||||||
import com.lukouguoji.module_base.common.Constant
|
import com.lukouguoji.module_base.common.Constant
|
||||||
import com.lukouguoji.module_base.common.DetailsPageType
|
import com.lukouguoji.module_base.common.DetailsPageType
|
||||||
|
import com.lukouguoji.module_base.ktx.addOnItemClickListener
|
||||||
import com.lukouguoji.module_base.ktx.noNull
|
import com.lukouguoji.module_base.ktx.noNull
|
||||||
import com.lukouguoji.module_base.ktx.setUpperCaseAlphanumericFilter
|
import com.lukouguoji.module_base.ktx.setUpperCaseAlphanumericFilter
|
||||||
|
|
||||||
@@ -31,6 +32,10 @@ class GjjManifestAddActivity :
|
|||||||
binding.actualWeightInput.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL
|
binding.actualWeightInput.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL
|
||||||
binding.billingWeightInput.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL
|
binding.billingWeightInput.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL
|
||||||
|
|
||||||
|
// 交接图片列表
|
||||||
|
viewModel.rv = binding.rvPic
|
||||||
|
binding.rvPic.addOnItemClickListener(viewModel)
|
||||||
|
|
||||||
viewModel.initOnCreated(intent)
|
viewModel.initOnCreated(intent)
|
||||||
|
|
||||||
// 动态设置标题(必须在 initOnCreated 之后,pageType 已从 Intent 解析)
|
// 动态设置标题(必须在 initOnCreated 之后,pageType 已从 Intent 解析)
|
||||||
|
|||||||
@@ -5,26 +5,45 @@ import android.view.View
|
|||||||
import androidx.lifecycle.MediatorLiveData
|
import androidx.lifecycle.MediatorLiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.lukouguoji.gjj.R
|
||||||
import com.lukouguoji.module_base.base.BaseViewModel
|
import com.lukouguoji.module_base.base.BaseViewModel
|
||||||
|
import com.lukouguoji.module_base.bean.FileBean
|
||||||
import com.lukouguoji.module_base.common.Constant
|
import com.lukouguoji.module_base.common.Constant
|
||||||
import com.lukouguoji.module_base.common.ConstantEvent
|
import com.lukouguoji.module_base.common.ConstantEvent
|
||||||
import com.lukouguoji.module_base.common.DetailsPageType
|
import com.lukouguoji.module_base.common.DetailsPageType
|
||||||
import com.lukouguoji.module_base.http.net.NetApply
|
import com.lukouguoji.module_base.http.net.NetApply
|
||||||
import com.lukouguoji.module_base.impl.FlowBus
|
import com.lukouguoji.module_base.impl.FlowBus
|
||||||
|
import com.lukouguoji.module_base.impl.ImageSelectNewViewHolder
|
||||||
|
import com.lukouguoji.module_base.interfaces.IOnItemClickListener
|
||||||
|
import com.lukouguoji.module_base.ktx.commonAdapter
|
||||||
import com.lukouguoji.module_base.ktx.finish
|
import com.lukouguoji.module_base.ktx.finish
|
||||||
import com.lukouguoji.module_base.ktx.formatDate
|
import com.lukouguoji.module_base.ktx.formatDate
|
||||||
import com.lukouguoji.module_base.ktx.launchCollect
|
import com.lukouguoji.module_base.ktx.launchCollect
|
||||||
import com.lukouguoji.module_base.ktx.launchLoadingCollect
|
import com.lukouguoji.module_base.ktx.launchLoadingCollect
|
||||||
|
import com.lukouguoji.module_base.ktx.loadMore
|
||||||
import com.lukouguoji.module_base.ktx.noNull
|
import com.lukouguoji.module_base.ktx.noNull
|
||||||
import com.lukouguoji.module_base.ktx.showToast
|
import com.lukouguoji.module_base.ktx.showToast
|
||||||
import com.lukouguoji.module_base.ktx.toRequestBody
|
import com.lukouguoji.module_base.ktx.toRequestBody
|
||||||
import com.lukouguoji.module_base.ktx.verifyNullOrEmpty
|
import com.lukouguoji.module_base.ktx.verifyNullOrEmpty
|
||||||
import com.lukouguoji.module_base.util.DictUtils
|
import com.lukouguoji.module_base.util.DictUtils
|
||||||
|
import com.lukouguoji.module_base.util.MediaUtil
|
||||||
|
import com.lukouguoji.module_base.util.UploadUtil
|
||||||
import dev.utils.app.info.KeyValue
|
import dev.utils.app.info.KeyValue
|
||||||
import dev.utils.common.DateUtils
|
import dev.utils.common.DateUtils
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.asFlow
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onCompletion
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.onStart
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class GjjManifestAddViewModel : BaseViewModel() {
|
class GjjManifestAddViewModel : BaseViewModel(), IOnItemClickListener {
|
||||||
|
|
||||||
// 页面类型(必须用LiveData)
|
// 页面类型(必须用LiveData)
|
||||||
val pageType = MutableLiveData(DetailsPageType.Add)
|
val pageType = MutableLiveData(DetailsPageType.Add)
|
||||||
@@ -167,6 +186,11 @@ class GjjManifestAddViewModel : BaseViewModel() {
|
|||||||
val waybillTypeList = MutableLiveData<List<KeyValue>>()
|
val waybillTypeList = MutableLiveData<List<KeyValue>>()
|
||||||
val waybillType = MutableLiveData("")
|
val waybillType = MutableLiveData("")
|
||||||
|
|
||||||
|
// ========== 交接图片 ==========
|
||||||
|
val itemLayoutId = R.layout.item_image_select_new
|
||||||
|
val itemViewHolder = ImageSelectNewViewHolder::class.java
|
||||||
|
var rv: RecyclerView? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化(从Intent获取参数)
|
* 初始化(从Intent获取参数)
|
||||||
*/
|
*/
|
||||||
@@ -205,6 +229,9 @@ class GjjManifestAddViewModel : BaseViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 交接图片:末尾追加空白占位项作为添加按钮
|
||||||
|
rv?.post { rv?.commonAdapter()?.addItem(FileBean()) }
|
||||||
|
|
||||||
// 加载下拉列表(在编辑数据加载之后,以便使用 checkedValue 将选中项置顶)
|
// 加载下拉列表(在编辑数据加载之后,以便使用 checkedValue 将选中项置顶)
|
||||||
loadDictLists()
|
loadDictLists()
|
||||||
}
|
}
|
||||||
@@ -302,6 +329,21 @@ class GjjManifestAddViewModel : BaseViewModel() {
|
|||||||
businessType.value = manifest.businessType
|
businessType.value = manifest.businessType
|
||||||
goodsType.value = manifest.cargoType
|
goodsType.value = manifest.cargoType
|
||||||
waybillType.value = manifest.awbType
|
waybillType.value = manifest.awbType
|
||||||
|
|
||||||
|
// 回填交接图片:path 取原图 URL 确保预览清晰,url 取缩略图用于提交
|
||||||
|
val picList = manifest.pic.noNull().split(",").filter { it.isNotEmpty() }
|
||||||
|
val originalList = manifest.originalPic.noNull().split(",").filter { it.isNotEmpty() }
|
||||||
|
if (picList.isNotEmpty()) {
|
||||||
|
val images = picList.mapIndexed { index, picUrl ->
|
||||||
|
val originalUrl = originalList.getOrElse(index) { picUrl }
|
||||||
|
FileBean(
|
||||||
|
path = MediaUtil.fillUrl(originalUrl),
|
||||||
|
url = picUrl,
|
||||||
|
originalPic = originalUrl
|
||||||
|
)
|
||||||
|
}
|
||||||
|
rv?.loadMore(images)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -411,30 +453,82 @@ class GjjManifestAddViewModel : BaseViewModel() {
|
|||||||
paramsMap["prefix"] = prefix
|
paramsMap["prefix"] = prefix
|
||||||
}
|
}
|
||||||
|
|
||||||
val params = paramsMap.toRequestBody()
|
// 先上传本地新增的交接图片,全部完成后再组装三字段提交表单
|
||||||
|
var uploadFailed = false
|
||||||
launchLoadingCollect({
|
(rv?.commonAdapter()?.items ?: emptyList())
|
||||||
if (isModify) {
|
.asFlow()
|
||||||
NetApply.api.gjjManifestUpdate(params)
|
.map { it as FileBean }
|
||||||
} else {
|
.filter { it.path.isNotEmpty() && it.url.isEmpty() }
|
||||||
NetApply.api.gjjManifestInsert(params)
|
.onEach {
|
||||||
|
val data = UploadUtil.upload(it.path).data
|
||||||
|
// UploadUtil 返回:newName=原图(较大),zipFileName=缩略图(较小)
|
||||||
|
// FileBean.url 用作缩略图标识,FileBean.originalPic 用作原图标识
|
||||||
|
it.url = data?.zipFileName ?: ""
|
||||||
|
it.originalPic = data?.newName ?: ""
|
||||||
}
|
}
|
||||||
}) {
|
.flowOn(Dispatchers.IO)
|
||||||
onSuccess = {
|
.onStart { showLoading() }
|
||||||
if (it.verifySuccess()) {
|
.catch {
|
||||||
val successMsg = if (isModify) "修改成功" else "保存成功"
|
uploadFailed = true
|
||||||
showToast(successMsg)
|
showToast(it.message.noNull("上传图片失败"))
|
||||||
|
dismissLoading()
|
||||||
|
}
|
||||||
|
.onCompletion {
|
||||||
|
if (uploadFailed) return@onCompletion
|
||||||
|
|
||||||
// 发送刷新事件
|
// 交接图片三字段:pic=缩略图、originalPic=原图、picNumber=数量
|
||||||
viewModelScope.launch {
|
val picBeans = (rv?.commonAdapter()?.items ?: emptyList())
|
||||||
FlowBus.with<String>(ConstantEvent.EVENT_REFRESH).emit("refresh")
|
.filterIsInstance<FileBean>()
|
||||||
|
.filter { it.path.isNotEmpty() }
|
||||||
|
paramsMap["picNumber"] = picBeans.size.toString()
|
||||||
|
paramsMap["pic"] = picBeans.joinToString(",") { MediaUtil.removeUrl(it.url) }
|
||||||
|
paramsMap["originalPic"] =
|
||||||
|
picBeans.joinToString(",") { MediaUtil.removeUrl(it.originalPic) }
|
||||||
|
|
||||||
|
val params = paramsMap.toRequestBody()
|
||||||
|
|
||||||
|
launchLoadingCollect({
|
||||||
|
if (isModify) {
|
||||||
|
NetApply.api.gjjManifestUpdate(params)
|
||||||
|
} else {
|
||||||
|
NetApply.api.gjjManifestInsert(params)
|
||||||
}
|
}
|
||||||
|
}) {
|
||||||
|
onSuccess = {
|
||||||
|
if (it.verifySuccess()) {
|
||||||
|
val successMsg = if (isModify) "修改成功" else "保存成功"
|
||||||
|
showToast(successMsg)
|
||||||
|
|
||||||
view.context.finish()
|
// 发送刷新事件
|
||||||
} else {
|
viewModelScope.launch {
|
||||||
showToast(it.msg.noNull("保存失败"))
|
FlowBus.with<String>(ConstantEvent.EVENT_REFRESH).emit("refresh")
|
||||||
|
}
|
||||||
|
|
||||||
|
view.context.finish()
|
||||||
|
} else {
|
||||||
|
showToast(it.msg.noNull("保存失败"))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.launchIn(viewModelScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 交接图片列表点击回调
|
||||||
|
* 长按图片(R.id.rl)切换删除按钮显隐,点击删除按钮(R.id.iv_delete)移除该图片
|
||||||
|
*/
|
||||||
|
override fun onItemClick(position: Int, type: Int) {
|
||||||
|
val adapter = rv?.commonAdapter() ?: return
|
||||||
|
val bean = adapter.getItem(position) as? FileBean ?: return
|
||||||
|
when (type) {
|
||||||
|
R.id.rl -> {
|
||||||
|
bean.canDelete.set(!bean.canDelete.get())
|
||||||
|
}
|
||||||
|
R.id.iv_delete -> {
|
||||||
|
adapter.removeItem(position)
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<data>
|
<data>
|
||||||
@@ -356,7 +357,37 @@
|
|||||||
android:layout_weight="0.605" />
|
android:layout_weight="0.605" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
<!-- -->
|
|
||||||
|
<!-- 第8行:交接图片 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
android:layout_marginTop="15dp"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
completeSpace="@{5}"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:text="交接图片"
|
||||||
|
android:textColor="@color/text_gray" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/rv_pic"
|
||||||
|
itemLayoutId="@{viewModel.itemLayoutId}"
|
||||||
|
viewHolder="@{viewModel.itemViewHolder}"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="10dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:overScrollMode="never"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||||
|
app:spanCount="9"
|
||||||
|
tools:listitem="@layout/item_image_select_new" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user