feat: 国际进港舱单编辑支持交接图片新增与删除

编辑/新增表单页增加交接图片区(选图、上传、长按删除),
保存时随舱单提交 pic/originalPic/picNumber 三字段,
编辑模式回填已有图片且不重复上传。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-09-02 11:23:43 +08:00
parent 86f77353c0
commit b65e61595b
3 changed files with 150 additions and 20 deletions

View File

@@ -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 解析)

View File

@@ -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 -> {}
} }
} }

View File

@@ -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>