diff --git a/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/search/layout/PadSearchLayout.kt b/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/search/layout/PadSearchLayout.kt index 3fd9d09..31d15d4 100644 --- a/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/search/layout/PadSearchLayout.kt +++ b/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/search/layout/PadSearchLayout.kt @@ -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 -> { diff --git a/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/search/layout/PadSearchLayoutNew.kt b/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/search/layout/PadSearchLayoutNew.kt index e4ba4d7..b6354d0 100644 --- a/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/search/layout/PadSearchLayoutNew.kt +++ b/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/search/layout/PadSearchLayoutNew.kt @@ -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 -> { diff --git a/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/search/layout/SearchLayoutKtx.kt b/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/search/layout/SearchLayoutKtx.kt index 2b5193a..b850bd9 100644 --- a/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/search/layout/SearchLayoutKtx.kt +++ b/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/search/layout/SearchLayoutKtx.kt @@ -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设置大写字母+数字输入限制 * 自动转换小写为大写,过滤中文、特殊符号、空格 @@ -315,4 +422,111 @@ fun setSearchLayoutNewUpperCaseAlphanumeric(layout: PadSearchLayoutNew, enabled: if (enabled) { layout.et.filters = arrayOf(UpperCaseAlphanumericInputFilter()) } -} \ No newline at end of file +} + +/////////////////////////////////////////////////////////////////////////// +// 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) + } +} diff --git a/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/search/layout/manager/SearchAutoQueryManager.kt b/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/search/layout/manager/SearchAutoQueryManager.kt new file mode 100644 index 0000000..fd8c0d4 --- /dev/null +++ b/module_base/src/main/java/com/lukouguoji/module_base/ui/weight/search/layout/manager/SearchAutoQueryManager.kt @@ -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) { + when { + // 1 条结果:直接填充 + results.size == 1 -> { + onValueSelected(results[0]) + } + + // 多条结果:显示弹框选择 + results.size > 1 -> { + showSelectionDialog(results) + } + + // 0 条结果:不做处理 + else -> { + // 可选:showToast("未找到匹配数据") + } + } + } + + /** + * 显示选择弹框 + */ + private fun showSelectionDialog(results: List) { + 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]) + } + } +} diff --git a/module_gjc/src/main/res/layout/activity_gjc_weighing_list.xml b/module_gjc/src/main/res/layout/activity_gjc_weighing_list.xml index 813d67f..4f53b42 100644 --- a/module_gjc/src/main/res/layout/activity_gjc_weighing_list.xml +++ b/module_gjc/src/main/res/layout/activity_gjc_weighing_list.xml @@ -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" />