feat: 出港待计重-输入运单号
This commit is contained in:
@@ -21,6 +21,8 @@ import com.lukouguoji.module_base.ktx.formatDate
|
||||
import com.lukouguoji.module_base.ktx.getActivity
|
||||
import com.lukouguoji.module_base.ktx.loge
|
||||
import com.lukouguoji.module_base.ktx.tryCatch
|
||||
import com.lukouguoji.module_base.ui.weight.data.layout.AutoQueryConfig
|
||||
import com.lukouguoji.module_base.ui.weight.search.layout.manager.SearchAutoQueryManager
|
||||
import com.lukouguoji.module_base.util.Common
|
||||
import dev.utils.app.info.KeyValue
|
||||
import java.util.Calendar
|
||||
@@ -102,6 +104,16 @@ class PadSearchLayout : LinearLayout {
|
||||
loadImage(iv, value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动查询配置
|
||||
*/
|
||||
var autoQueryConfig: AutoQueryConfig = AutoQueryConfig()
|
||||
|
||||
/**
|
||||
* 自动查询管理器(延迟初始化)
|
||||
*/
|
||||
private var autoQueryManager: SearchAutoQueryManager? = null
|
||||
|
||||
// 选择日期
|
||||
private val dateClick: (v: View) -> Unit = {
|
||||
if (enable) {
|
||||
@@ -158,6 +170,30 @@ class PadSearchLayout : LinearLayout {
|
||||
setForType()
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用自动查询功能
|
||||
*/
|
||||
fun enableAutoQuery(config: AutoQueryConfig) {
|
||||
this.autoQueryConfig = config
|
||||
if (config.isValid()) {
|
||||
// 初始化查询管理器
|
||||
autoQueryManager = SearchAutoQueryManager(et, config) { newValue ->
|
||||
// 更新值的回调
|
||||
this.value = newValue
|
||||
}
|
||||
autoQueryManager?.attach()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁时清理资源
|
||||
*/
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
autoQueryManager?.detach()
|
||||
autoQueryManager = null
|
||||
}
|
||||
|
||||
private fun setForType() {
|
||||
when (type) {
|
||||
SearchLayoutType.INPUT -> {
|
||||
|
||||
@@ -23,6 +23,8 @@ import com.lukouguoji.module_base.ktx.formatDate
|
||||
import com.lukouguoji.module_base.ktx.getActivity
|
||||
import com.lukouguoji.module_base.ktx.loge
|
||||
import com.lukouguoji.module_base.ktx.tryCatch
|
||||
import com.lukouguoji.module_base.ui.weight.data.layout.AutoQueryConfig
|
||||
import com.lukouguoji.module_base.ui.weight.search.layout.manager.SearchAutoQueryManager
|
||||
import com.lukouguoji.module_base.util.Common
|
||||
import dev.utils.app.info.KeyValue
|
||||
import java.util.Calendar
|
||||
@@ -104,6 +106,16 @@ class PadSearchLayoutNew : LinearLayout {
|
||||
loadImage(iv, value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动查询配置
|
||||
*/
|
||||
var autoQueryConfig: AutoQueryConfig = AutoQueryConfig()
|
||||
|
||||
/**
|
||||
* 自动查询管理器(延迟初始化)
|
||||
*/
|
||||
private var autoQueryManager: SearchAutoQueryManager? = null
|
||||
|
||||
// 选择日期
|
||||
private val dateClick: (v: View) -> Unit = {
|
||||
if (enable) {
|
||||
@@ -179,6 +191,30 @@ class PadSearchLayoutNew : LinearLayout {
|
||||
setForType()
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用自动查询功能
|
||||
*/
|
||||
fun enableAutoQuery(config: AutoQueryConfig) {
|
||||
this.autoQueryConfig = config
|
||||
if (config.isValid()) {
|
||||
// 初始化查询管理器
|
||||
autoQueryManager = SearchAutoQueryManager(et, config) { newValue ->
|
||||
// 更新值的回调
|
||||
this.value = newValue
|
||||
}
|
||||
autoQueryManager?.attach()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁时清理资源
|
||||
*/
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
autoQueryManager?.detach()
|
||||
autoQueryManager = null
|
||||
}
|
||||
|
||||
private fun setForType() {
|
||||
when (type) {
|
||||
SearchLayoutType.INPUT -> {
|
||||
|
||||
@@ -306,6 +306,113 @@ fun setUpperCaseAlphanumeric(layout: PadSearchLayout, enabled: Boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// PadSearchLayout 自动查询功能 BindingAdapter
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* 启用自动查询功能
|
||||
* @param enabled 是否启用
|
||||
*/
|
||||
@BindingAdapter("autoQueryEnabled")
|
||||
fun setSearchLayoutAutoQueryEnabled(layout: PadSearchLayout, enabled: Boolean) {
|
||||
layout.autoQueryConfig.enabled = enabled
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置查询接口地址
|
||||
* @param url 接口地址(如:/IntExpCheckIn/queryWbNoList)
|
||||
*/
|
||||
@BindingAdapter("autoQueryUrl")
|
||||
fun setSearchLayoutAutoQueryUrl(layout: PadSearchLayout, url: String?) {
|
||||
layout.autoQueryConfig.url = url ?: ""
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置查询参数的 key 名称
|
||||
* @param paramKey 参数名(默认 "value")
|
||||
*/
|
||||
@BindingAdapter("autoQueryParamKey")
|
||||
fun setSearchLayoutAutoQueryParamKey(layout: PadSearchLayout, paramKey: String?) {
|
||||
layout.autoQueryConfig.paramKey = paramKey ?: "value"
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置触发查询的最小长度
|
||||
* @param minLength 最小长度(默认 4)
|
||||
*/
|
||||
@BindingAdapter("autoQueryMinLength")
|
||||
fun setSearchLayoutAutoQueryMinLength(layout: PadSearchLayout, minLength: Int?) {
|
||||
layout.autoQueryConfig.minLength = minLength ?: 4
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置触发查询的最大长度
|
||||
* @param maxLength 最大长度(默认 8)
|
||||
*/
|
||||
@BindingAdapter("autoQueryMaxLength")
|
||||
fun setSearchLayoutAutoQueryMaxLength(layout: PadSearchLayout, maxLength: Int?) {
|
||||
layout.autoQueryConfig.maxLength = maxLength ?: 8
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置弹框标题
|
||||
* @param title 标题(默认 "请选择")
|
||||
*/
|
||||
@BindingAdapter("autoQueryTitle")
|
||||
fun setSearchLayoutAutoQueryTitle(layout: PadSearchLayout, title: String?) {
|
||||
layout.autoQueryConfig.title = title ?: "请选择"
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置防抖延迟
|
||||
* @param debounceMillis 延迟毫秒数(默认 300ms)
|
||||
*/
|
||||
@BindingAdapter("autoQueryDebounce")
|
||||
fun setSearchLayoutAutoQueryDebounce(layout: PadSearchLayout, debounceMillis: Long?) {
|
||||
layout.autoQueryConfig.debounceMillis = debounceMillis ?: 300L
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一配置自动查询(所有属性设置完成后调用)
|
||||
*
|
||||
* ⚠️ 重要:必须在所有 autoQuery* 属性之后绑定,使用 requireAll = false
|
||||
*/
|
||||
@BindingAdapter(
|
||||
"autoQueryEnabled",
|
||||
"autoQueryUrl",
|
||||
"autoQueryParamKey",
|
||||
"autoQueryMinLength",
|
||||
"autoQueryMaxLength",
|
||||
"autoQueryTitle",
|
||||
"autoQueryDebounce",
|
||||
requireAll = false
|
||||
)
|
||||
fun configureSearchLayoutAutoQuery(
|
||||
layout: PadSearchLayout,
|
||||
enabled: Boolean?,
|
||||
url: String?,
|
||||
paramKey: String?,
|
||||
minLength: Int?,
|
||||
maxLength: Int?,
|
||||
title: String?,
|
||||
debounceMillis: Long?
|
||||
) {
|
||||
// 应用所有配置
|
||||
enabled?.let { layout.autoQueryConfig.enabled = it }
|
||||
url?.let { layout.autoQueryConfig.url = it }
|
||||
paramKey?.let { layout.autoQueryConfig.paramKey = it }
|
||||
minLength?.let { layout.autoQueryConfig.minLength = it }
|
||||
maxLength?.let { layout.autoQueryConfig.maxLength = it }
|
||||
title?.let { layout.autoQueryConfig.title = it }
|
||||
debounceMillis?.let { layout.autoQueryConfig.debounceMillis = it }
|
||||
|
||||
// 验证并启用自动查询
|
||||
if (layout.autoQueryConfig.isValid()) {
|
||||
layout.enableAutoQuery(layout.autoQueryConfig)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为PadSearchLayoutNew设置大写字母+数字输入限制
|
||||
* 自动转换小写为大写,过滤中文、特殊符号、空格
|
||||
@@ -316,3 +423,110 @@ fun setSearchLayoutNewUpperCaseAlphanumeric(layout: PadSearchLayoutNew, enabled:
|
||||
layout.et.filters = arrayOf(UpperCaseAlphanumericInputFilter())
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// PadSearchLayoutNew 自动查询功能 BindingAdapter
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* 启用自动查询功能
|
||||
* @param enabled 是否启用
|
||||
*/
|
||||
@BindingAdapter("autoQueryEnabled")
|
||||
fun setSearchLayoutNewAutoQueryEnabled(layout: PadSearchLayoutNew, enabled: Boolean) {
|
||||
layout.autoQueryConfig.enabled = enabled
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置查询接口地址
|
||||
* @param url 接口地址(如:/IntExpCheckIn/checked/queryWbNoList)
|
||||
*/
|
||||
@BindingAdapter("autoQueryUrl")
|
||||
fun setSearchLayoutNewAutoQueryUrl(layout: PadSearchLayoutNew, url: String?) {
|
||||
layout.autoQueryConfig.url = url ?: ""
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置查询参数的 key 名称
|
||||
* @param paramKey 参数名(默认 "value")
|
||||
*/
|
||||
@BindingAdapter("autoQueryParamKey")
|
||||
fun setSearchLayoutNewAutoQueryParamKey(layout: PadSearchLayoutNew, paramKey: String?) {
|
||||
layout.autoQueryConfig.paramKey = paramKey ?: "value"
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置触发查询的最小长度
|
||||
* @param minLength 最小长度(默认 4)
|
||||
*/
|
||||
@BindingAdapter("autoQueryMinLength")
|
||||
fun setSearchLayoutNewAutoQueryMinLength(layout: PadSearchLayoutNew, minLength: Int?) {
|
||||
layout.autoQueryConfig.minLength = minLength ?: 4
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置触发查询的最大长度
|
||||
* @param maxLength 最大长度(默认 8)
|
||||
*/
|
||||
@BindingAdapter("autoQueryMaxLength")
|
||||
fun setSearchLayoutNewAutoQueryMaxLength(layout: PadSearchLayoutNew, maxLength: Int?) {
|
||||
layout.autoQueryConfig.maxLength = maxLength ?: 8
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置弹框标题
|
||||
* @param title 标题(默认 "请选择")
|
||||
*/
|
||||
@BindingAdapter("autoQueryTitle")
|
||||
fun setSearchLayoutNewAutoQueryTitle(layout: PadSearchLayoutNew, title: String?) {
|
||||
layout.autoQueryConfig.title = title ?: "请选择"
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置防抖延迟
|
||||
* @param debounceMillis 延迟毫秒数(默认 300ms)
|
||||
*/
|
||||
@BindingAdapter("autoQueryDebounce")
|
||||
fun setSearchLayoutNewAutoQueryDebounce(layout: PadSearchLayoutNew, debounceMillis: Long?) {
|
||||
layout.autoQueryConfig.debounceMillis = debounceMillis ?: 300L
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一配置自动查询(所有属性设置完成后调用)
|
||||
*
|
||||
* ⚠️ 重要:必须在所有 autoQuery* 属性之后绑定,使用 requireAll = false
|
||||
*/
|
||||
@BindingAdapter(
|
||||
"autoQueryEnabled",
|
||||
"autoQueryUrl",
|
||||
"autoQueryParamKey",
|
||||
"autoQueryMinLength",
|
||||
"autoQueryMaxLength",
|
||||
"autoQueryTitle",
|
||||
"autoQueryDebounce",
|
||||
requireAll = false
|
||||
)
|
||||
fun configureSearchLayoutNewAutoQuery(
|
||||
layout: PadSearchLayoutNew,
|
||||
enabled: Boolean?,
|
||||
url: String?,
|
||||
paramKey: String?,
|
||||
minLength: Int?,
|
||||
maxLength: Int?,
|
||||
title: String?,
|
||||
debounceMillis: Long?
|
||||
) {
|
||||
// 应用所有配置
|
||||
enabled?.let { layout.autoQueryConfig.enabled = it }
|
||||
url?.let { layout.autoQueryConfig.url = it }
|
||||
paramKey?.let { layout.autoQueryConfig.paramKey = it }
|
||||
minLength?.let { layout.autoQueryConfig.minLength = it }
|
||||
maxLength?.let { layout.autoQueryConfig.maxLength = it }
|
||||
title?.let { layout.autoQueryConfig.title = it }
|
||||
debounceMillis?.let { layout.autoQueryConfig.debounceMillis = it }
|
||||
|
||||
// 验证并启用自动查询
|
||||
if (layout.autoQueryConfig.isValid()) {
|
||||
layout.enableAutoQuery(layout.autoQueryConfig)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
package com.lukouguoji.module_base.ui.weight.search.layout.manager
|
||||
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.widget.EditText
|
||||
import androidx.lifecycle.ViewTreeLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.alibaba.fastjson.JSONArray
|
||||
import com.lukouguoji.module_base.http.net.NetApply
|
||||
import com.lukouguoji.module_base.ktx.getActivity
|
||||
import com.lukouguoji.module_base.ktx.launchCollect
|
||||
import com.lukouguoji.module_base.ktx.toJson
|
||||
import com.lukouguoji.module_base.ktx.toRequestBody
|
||||
import com.lukouguoji.module_base.ui.weight.data.layout.AutoQueryConfig
|
||||
import com.lukouguoji.module_base.util.Common
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* 搜索框专用自动查询管理器
|
||||
* 负责处理输入监听、防抖、查询请求、结果处理
|
||||
*
|
||||
* 功能:
|
||||
* 1. 监听 EditText 输入变化
|
||||
* 2. 防抖延迟(避免频繁请求)
|
||||
* 3. 防重复查询(相同值不重复请求)
|
||||
* 4. 调用接口查询数据
|
||||
* 5. 处理查询结果(单条填充、多条弹框)
|
||||
* 6. 自动管理协程生命周期
|
||||
*/
|
||||
class SearchAutoQueryManager(
|
||||
private val editText: EditText,
|
||||
private val config: AutoQueryConfig,
|
||||
private val onValueSelected: (String) -> Unit
|
||||
) {
|
||||
|
||||
/** 协程作用域(从 ViewTree 获取) */
|
||||
private var scope: CoroutineScope? = null
|
||||
|
||||
/** 上次查询的值(防重复查询) */
|
||||
private var lastQueriedValue: String = ""
|
||||
|
||||
/** 防抖任务 */
|
||||
private var debounceJob: Job? = null
|
||||
|
||||
/** 文本监听器 */
|
||||
private var textWatcher: TextWatcher? = null
|
||||
|
||||
/**
|
||||
* 绑定到视图(添加文本监听)
|
||||
*/
|
||||
fun attach() {
|
||||
// 获取协程作用域(从 ViewTree 获取 LifecycleOwner)
|
||||
val lifecycleOwner = ViewTreeLifecycleOwner.get(editText)
|
||||
if (lifecycleOwner == null) {
|
||||
// 延迟绑定(等待 ViewTree 附加)
|
||||
editText.post { attach() }
|
||||
return
|
||||
}
|
||||
scope = lifecycleOwner.lifecycleScope
|
||||
|
||||
// 添加文本监听
|
||||
textWatcher = object : TextWatcher {
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
val text = s?.toString() ?: ""
|
||||
handleTextChanged(text)
|
||||
}
|
||||
}
|
||||
|
||||
editText.addTextChangedListener(textWatcher)
|
||||
}
|
||||
|
||||
/**
|
||||
* 解绑(移除监听、取消协程)
|
||||
*/
|
||||
fun detach() {
|
||||
textWatcher?.let { editText.removeTextChangedListener(it) }
|
||||
textWatcher = null
|
||||
debounceJob?.cancel()
|
||||
debounceJob = null
|
||||
scope = null
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理文本变化
|
||||
*/
|
||||
private fun handleTextChanged(text: String) {
|
||||
val trimmedText = text.trim()
|
||||
val length = trimmedText.length
|
||||
|
||||
// 取消之前的防抖任务
|
||||
debounceJob?.cancel()
|
||||
|
||||
// 判断是否需要触发查询
|
||||
if (length in config.minLength..config.maxLength) {
|
||||
// 防抖延迟
|
||||
debounceJob = scope?.launch {
|
||||
delay(config.debounceMillis)
|
||||
performQuery(trimmedText)
|
||||
}
|
||||
} else {
|
||||
// 长度不符合,清空上次查询记录
|
||||
lastQueriedValue = ""
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行查询
|
||||
*/
|
||||
private fun performQuery(value: String) {
|
||||
// 防重复查询
|
||||
if (value == lastQueriedValue) {
|
||||
return
|
||||
}
|
||||
lastQueriedValue = value
|
||||
|
||||
// 构建查询参数
|
||||
val params = mapOf(config.paramKey to value).toRequestBody()
|
||||
|
||||
// 发起网络请求
|
||||
scope?.launchCollect({ NetApply.api.getWbNoList(config.url, params) }) {
|
||||
onSuccess = { result ->
|
||||
val results = result.data ?: emptyList()
|
||||
handleQueryResults(results)
|
||||
}
|
||||
onFailed = { code, msg ->
|
||||
// 查询失败,清空记录(允许重试)
|
||||
lastQueriedValue = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理查询结果
|
||||
*/
|
||||
private fun handleQueryResults(results: List<String>) {
|
||||
when {
|
||||
// 1 条结果:直接填充
|
||||
results.size == 1 -> {
|
||||
onValueSelected(results[0])
|
||||
}
|
||||
|
||||
// 多条结果:显示弹框选择
|
||||
results.size > 1 -> {
|
||||
showSelectionDialog(results)
|
||||
}
|
||||
|
||||
// 0 条结果:不做处理
|
||||
else -> {
|
||||
// 可选:showToast("未找到匹配数据")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示选择弹框
|
||||
*/
|
||||
private fun showSelectionDialog(results: List<String>) {
|
||||
val activity = editText.context.getActivity()
|
||||
|
||||
// 转换为 Common.singleSelect 需要的格式
|
||||
val jsonArray = JSONArray.parseArray(
|
||||
results.map { mapOf("name" to it, "code" to it) }.toJson(false)
|
||||
)
|
||||
|
||||
Common.singleSelect(
|
||||
activity,
|
||||
config.title,
|
||||
jsonArray,
|
||||
null
|
||||
) { position, _ ->
|
||||
// 用户选择后更新值
|
||||
onValueSelected(results[position])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,6 +76,12 @@
|
||||
setOnIconClickListener="@{()-> viewModel.waybillNoScanClick()}"
|
||||
type="@{SearchLayoutType.INPUT}"
|
||||
value="@={viewModel.waybillNo}"
|
||||
autoQueryEnabled="@{true}"
|
||||
autoQueryUrl="@{`/IntExpCheckIn/queryWbNoList`}"
|
||||
autoQueryParamKey="@{`wbNo`}"
|
||||
autoQueryMinLength="@{4}"
|
||||
autoQueryMaxLength="@{8}"
|
||||
autoQueryTitle="@{`选择运单号`}"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1" />
|
||||
|
||||
Reference in New Issue
Block a user