From 28784fa7c1cad1ed49eec0bd400a2a8472c6d7fb Mon Sep 17 00:00:00 2001 From: lvmeng <848755140@qq.com> Date: Mon, 18 May 2026 15:52:46 +0800 Subject: [PATCH] =?UTF-8?q?feat(db):=20=E4=B8=BA=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=BA=93=E8=B0=83=E8=AF=95=E9=A1=B5=E9=9D=A2=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=A4=9A=E5=AD=97=E6=AE=B5=E5=8A=A8=E6=80=81=E7=AD=9B=E9=80=89?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 CookFoodDao、CookFoodGoodsDao、SeasoningDao 和 SeasoningSlotDao 中添加 RawQuery 支持 - 为各数据访问对象添加动态条件分页查询和统计方法 - 在 DbRepository 中扩展动态查询相关方法 - 在 DbViewModel 中实现 DbInspectFilter 数据类和 buildQueries 动态 SQL 构建逻辑 - 更新 DbInspectFragment 添加筛选字段配置和 UI 生成逻辑 - 重构筛选条件收集和查询流程,支持精确匹配和模糊搜索 - 修改刷新按钮为查询按钮,更新 Tab 切换时的筛选框重建逻辑 - 调整设置页面标题从"设备设置"改为"设置" --- .../com/shuwei/dish/match/db/DbRepository.kt | 35 ++++ .../com/shuwei/dish/match/db/DbViewModel.kt | 113 +++++++++--- .../shuwei/dish/match/db/dao/CookFoodDao.kt | 10 ++ .../dish/match/db/dao/CookFoodGoodsDao.kt | 10 ++ .../shuwei/dish/match/db/dao/SeasoningDao.kt | 10 ++ .../dish/match/db/dao/SeasoningSlotDao.kt | 10 ++ .../shuwei/dish/match/ui/SettingActivity.kt | 2 +- .../match/ui/fragment/DbInspectFragment.kt | 162 ++++++++++++++++-- .../main/res/layout/fragment_db_inspect.xml | 13 +- 9 files changed, 326 insertions(+), 39 deletions(-) diff --git a/app/src/main/java/com/shuwei/dish/match/db/DbRepository.kt b/app/src/main/java/com/shuwei/dish/match/db/DbRepository.kt index a545d91..a049997 100644 --- a/app/src/main/java/com/shuwei/dish/match/db/DbRepository.kt +++ b/app/src/main/java/com/shuwei/dish/match/db/DbRepository.kt @@ -5,6 +5,7 @@ import com.shuwei.dish.match.db.entity.CookFoodEntity import com.shuwei.dish.match.db.entity.CookFoodGoodsEntity import com.shuwei.dish.match.db.entity.SeasoningEntity import com.shuwei.dish.match.db.entity.SeasoningSlotEntity +import androidx.sqlite.db.SupportSQLiteQuery import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -233,6 +234,40 @@ class DbRepository { db.seasoningSlotDao().countSlotAll() } + // ---- 调试用:动态条件过滤分页查询 ---- + + suspend fun getCookFoodPagedFiltered(query: SupportSQLiteQuery) = withContext(Dispatchers.IO) { + db.cookFoodDao().queryPagedFiltered(query) + } + + suspend fun countCookFoodFiltered(query: SupportSQLiteQuery) = withContext(Dispatchers.IO) { + db.cookFoodDao().countFiltered(query) + } + + suspend fun getCookFoodGoodsPagedFiltered(query: SupportSQLiteQuery) = withContext(Dispatchers.IO) { + db.cookFoodGoodsDao().queryPagedFiltered(query) + } + + suspend fun countCookFoodGoodsFiltered(query: SupportSQLiteQuery) = withContext(Dispatchers.IO) { + db.cookFoodGoodsDao().countFiltered(query) + } + + suspend fun getSeasoningPagedFiltered(query: SupportSQLiteQuery) = withContext(Dispatchers.IO) { + db.seasoningDao().queryPagedFiltered(query) + } + + suspend fun countSeasoningFiltered(query: SupportSQLiteQuery) = withContext(Dispatchers.IO) { + db.seasoningDao().countFiltered(query) + } + + suspend fun getSlotPagedFiltered(query: SupportSQLiteQuery) = withContext(Dispatchers.IO) { + db.seasoningSlotDao().queryPagedFiltered(query) + } + + suspend fun countSlotFiltered(query: SupportSQLiteQuery) = withContext(Dispatchers.IO) { + db.seasoningSlotDao().countFiltered(query) + } + /** * 物理清空全部 4 张表的测试数据 * 清除顺序:先删子表(食材),再删主表(菜品),最后删配置表(调料、槽位) diff --git a/app/src/main/java/com/shuwei/dish/match/db/DbViewModel.kt b/app/src/main/java/com/shuwei/dish/match/db/DbViewModel.kt index df7bc03..e75e2ec 100644 --- a/app/src/main/java/com/shuwei/dish/match/db/DbViewModel.kt +++ b/app/src/main/java/com/shuwei/dish/match/db/DbViewModel.kt @@ -2,6 +2,7 @@ package com.shuwei.dish.match.db import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.sqlite.db.SimpleSQLiteQuery import com.shuwei.dish.match.model.CollectedFoodItem import com.shuwei.dish.match.db.entity.CookFoodEntity import com.shuwei.dish.match.objbox.ObjectBox @@ -332,71 +333,89 @@ class DbViewModel : ViewModel() { * @param totalCount 当前过滤条件下的总记录数 * @param hasMore 是否还有更多数据可加载 * @param isLoading 是否正在加载(首次加载或加载更多) + * @param filter 当前生效的筛选条件,供"加载更多"复用 */ data class DbInspectUiState( val items: List = emptyList(), val totalCount: Int = 0, val hasMore: Boolean = false, - val isLoading: Boolean = false + val isLoading: Boolean = false, + val filter: DbInspectFilter = DbInspectFilter() + ) + + /** + * 数据库调试页面筛选条件 + * @param showDel 是否包含已删除记录 + * @param exactFields 精确匹配字段:列名 → 值 + * @param likeFields 模糊匹配字段:列名 → 值(自动加 %value%) + */ + data class DbInspectFilter( + val showDel: Boolean = true, + val exactFields: Map = emptyMap(), + val likeFields: Map = emptyMap() ) private val _dbInspectState = MutableStateFlow(DbInspectUiState()) val dbInspectState: StateFlow = _dbInspectState.asStateFlow() /** - * 加载指定表的第一页数据(切换 Tab / 切换 isDel 过滤时调用) + * 加载指定表的第一页数据(切换 Tab / 修改筛选条件时调用) * @param tableIndex 0=菜品 1=食材 2=调料 3=槽位 - * @param showDel true=包含已删除记录,false=只看有效记录 + * @param filter 筛选条件,包含 showDel 和各字段过滤值 */ - fun loadDbInspect(tableIndex: Int, showDel: Boolean) { + fun loadDbInspect(tableIndex: Int, filter: DbInspectFilter) { viewModelScope.launch { // 立即清空旧数据并显示加载状态,避免切换 Tab 时旧数据闪现 - _dbInspectState.value = DbInspectUiState(isLoading = true) - val (items, total) = queryPage(tableIndex, showDel, offset = 0) + _dbInspectState.value = DbInspectUiState(isLoading = true, filter = filter) + val (items, total) = queryPage(tableIndex, filter, offset = 0) _dbInspectState.value = DbInspectUiState( items = items, totalCount = total, hasMore = items.size < total, - isLoading = false + isLoading = false, + filter = filter ) } } /** * 追加加载下一页数据(点击"加载更多"时调用) + * 筛选条件从当前 state 中复用,无需重新传入 * @param tableIndex 0=菜品 1=食材 2=调料 3=槽位 - * @param showDel true=包含已删除记录 */ - fun loadMoreDbInspect(tableIndex: Int, showDel: Boolean) { + fun loadMoreDbInspect(tableIndex: Int) { viewModelScope.launch { val current = _dbInspectState.value // 已在加载中或无更多数据时直接返回,防止重复请求 if (!current.hasMore || current.isLoading) return@launch _dbInspectState.value = current.copy(isLoading = true) - val (newItems, total) = queryPage(tableIndex, showDel, offset = current.items.size) + val (newItems, total) = queryPage(tableIndex, current.filter, offset = current.items.size) val merged = current.items + newItems _dbInspectState.value = DbInspectUiState( items = merged, totalCount = total, hasMore = merged.size < total, - isLoading = false + isLoading = false, + filter = current.filter ) } } /** * 执行分页查询,返回 DbRecord 列表和总记录数 + * 通过 buildQueries 将 filter 转换为动态 SQL */ private suspend fun queryPage( tableIndex: Int, - showDel: Boolean, + filter: DbInspectFilter, offset: Int ): Pair, Int> { val limit = DB_INSPECT_PAGE_SIZE return when (tableIndex) { 0 -> { - val total = rep.countCookFoodAll(showDel) - val rows = rep.getCookFoodPaged(showDel, limit, offset).map { e -> + val (pageQ, countQ) = buildQueries("dm_cook_food", "createTime DESC", true, filter, limit, offset) + val total = rep.countCookFoodFiltered(countQ) + val rows = rep.getCookFoodPagedFiltered(pageQ).map { e -> DbRecord( isDel = e.isDel == 1, fields = listOf( @@ -410,8 +429,9 @@ class DbViewModel : ViewModel() { rows to total } 1 -> { - val total = rep.countCookFoodGoodsAll(showDel) - val rows = rep.getCookFoodGoodsPaged(showDel, limit, offset).map { e -> + val (pageQ, countQ) = buildQueries("dm_cook_food_goods", "createTime DESC", true, filter, limit, offset) + val total = rep.countCookFoodGoodsFiltered(countQ) + val rows = rep.getCookFoodGoodsPagedFiltered(pageQ).map { e -> DbRecord( isDel = e.isDel == 1, fields = listOf( @@ -426,8 +446,9 @@ class DbViewModel : ViewModel() { rows to total } 2 -> { - val total = rep.countSeasoningAll(showDel) - val rows = rep.getSeasoningPaged(showDel, limit, offset).map { e -> + val (pageQ, countQ) = buildQueries("dm_seasoning", "sort ASC", true, filter, limit, offset) + val total = rep.countSeasoningFiltered(countQ) + val rows = rep.getSeasoningPagedFiltered(pageQ).map { e -> DbRecord( isDel = e.isDel == 1, fields = listOf( @@ -441,15 +462,16 @@ class DbViewModel : ViewModel() { rows to total } 3 -> { - val total = rep.countSlotAll() - val rows = rep.getSlotPaged(limit, offset).map { e -> + val (pageQ, countQ) = buildQueries("dm_seasoning_slot", "deviceId ASC, address ASC", false, filter, limit, offset) + val total = rep.countSlotFiltered(countQ) + val rows = rep.getSlotPagedFiltered(pageQ).map { e -> DbRecord( isDel = false, fields = listOf( "deviceId" to e.deviceId, "address" to "${e.address}", "goodsId" to e.goodsId, "goodsName" to e.goodsName ), - columnsPerRow = 1 // 槽位表字段少,保持单列显示 + columnsPerRow = 1 ) } rows to total @@ -458,6 +480,55 @@ class DbViewModel : ViewModel() { } } + /** + * 根据筛选条件动态构建分页查询和计数查询的 SQL + * @param tableName 表名 + * @param orderBy ORDER BY 子句(不含关键字) + * @param hasIsDel 是否有 isDel 字段,无则忽略 showDel 参数 + * @param filter 筛选条件 + * @param limit 分页大小 + * @param offset 偏移量 + * @return Pair<分页查询, 计数查询> + */ + private fun buildQueries( + tableName: String, + orderBy: String, + hasIsDel: Boolean, + filter: DbInspectFilter, + limit: Int, + offset: Int + ): Pair { + val conditions = mutableListOf() + val args = mutableListOf() + + if (hasIsDel && !filter.showDel) { + conditions.add("isDel = 0") + } + filter.exactFields.forEach { (col, value) -> + if (value.isNotBlank()) { + conditions.add("$col = ?") + args.add(value) + } + } + filter.likeFields.forEach { (col, value) -> + if (value.isNotBlank()) { + conditions.add("$col LIKE ?") + args.add("%$value%") + } + } + + val where = if (conditions.isEmpty()) "" else "WHERE ${conditions.joinToString(" AND ")}" + val pageQuery = SimpleSQLiteQuery( + "SELECT * FROM $tableName $where ORDER BY $orderBy LIMIT ? OFFSET ?", + (args + limit + offset).toTypedArray() + ) + val countQuery = SimpleSQLiteQuery( + "SELECT COUNT(*) FROM $tableName $where", + args.toTypedArray() + ) + return pageQuery to countQuery + } + //-------------------------------------------------------------------------------------------------------- // viewModelScope.launch { diff --git a/app/src/main/java/com/shuwei/dish/match/db/dao/CookFoodDao.kt b/app/src/main/java/com/shuwei/dish/match/db/dao/CookFoodDao.kt index 2c27f8b..10a8849 100644 --- a/app/src/main/java/com/shuwei/dish/match/db/dao/CookFoodDao.kt +++ b/app/src/main/java/com/shuwei/dish/match/db/dao/CookFoodDao.kt @@ -4,7 +4,9 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import androidx.room.RawQuery import androidx.room.Update +import androidx.sqlite.db.SupportSQLiteQuery import com.shuwei.dish.match.db.entity.CookFoodEntity @Dao @@ -48,4 +50,12 @@ interface CookFoodDao { /** 物理清空全表,用于清除测试数据 */ @Query("DELETE FROM dm_cook_food") suspend fun deleteAll() + + /** 动态条件分页查询菜品记录,由 ViewModel 构建 SQL */ + @RawQuery + suspend fun queryPagedFiltered(query: SupportSQLiteQuery): List + + /** 动态条件统计菜品记录总数,由 ViewModel 构建 SQL */ + @RawQuery + suspend fun countFiltered(query: SupportSQLiteQuery): Int } diff --git a/app/src/main/java/com/shuwei/dish/match/db/dao/CookFoodGoodsDao.kt b/app/src/main/java/com/shuwei/dish/match/db/dao/CookFoodGoodsDao.kt index 6df26c2..c5ec518 100644 --- a/app/src/main/java/com/shuwei/dish/match/db/dao/CookFoodGoodsDao.kt +++ b/app/src/main/java/com/shuwei/dish/match/db/dao/CookFoodGoodsDao.kt @@ -4,7 +4,9 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import androidx.room.RawQuery import androidx.room.Update +import androidx.sqlite.db.SupportSQLiteQuery import com.shuwei.dish.match.db.entity.CookFoodGoodsEntity @Dao @@ -48,4 +50,12 @@ interface CookFoodGoodsDao { /** 物理清空全表,用于清除测试数据 */ @Query("DELETE FROM dm_cook_food_goods") suspend fun deleteAll() + + /** 动态条件分页查询食材记录,由 ViewModel 构建 SQL */ + @RawQuery + suspend fun queryPagedFiltered(query: SupportSQLiteQuery): List + + /** 动态条件统计食材记录总数,由 ViewModel 构建 SQL */ + @RawQuery + suspend fun countFiltered(query: SupportSQLiteQuery): Int } diff --git a/app/src/main/java/com/shuwei/dish/match/db/dao/SeasoningDao.kt b/app/src/main/java/com/shuwei/dish/match/db/dao/SeasoningDao.kt index fa9eee9..ccb3b86 100644 --- a/app/src/main/java/com/shuwei/dish/match/db/dao/SeasoningDao.kt +++ b/app/src/main/java/com/shuwei/dish/match/db/dao/SeasoningDao.kt @@ -4,7 +4,9 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import androidx.room.RawQuery import androidx.room.Update +import androidx.sqlite.db.SupportSQLiteQuery import com.shuwei.dish.match.db.entity.SeasoningEntity @Dao @@ -57,4 +59,12 @@ interface SeasoningDao { /** 物理清空全表,用于清除测试数据(比 clearAllSeasoning 的逻辑删除更彻底) */ @Query("DELETE FROM dm_seasoning") suspend fun deleteAll() + + /** 动态条件分页查询调料记录,由 ViewModel 构建 SQL */ + @RawQuery + suspend fun queryPagedFiltered(query: SupportSQLiteQuery): List + + /** 动态条件统计调料记录总数,由 ViewModel 构建 SQL */ + @RawQuery + suspend fun countFiltered(query: SupportSQLiteQuery): Int } diff --git a/app/src/main/java/com/shuwei/dish/match/db/dao/SeasoningSlotDao.kt b/app/src/main/java/com/shuwei/dish/match/db/dao/SeasoningSlotDao.kt index 38df78c..af2baba 100644 --- a/app/src/main/java/com/shuwei/dish/match/db/dao/SeasoningSlotDao.kt +++ b/app/src/main/java/com/shuwei/dish/match/db/dao/SeasoningSlotDao.kt @@ -4,6 +4,8 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import androidx.room.RawQuery +import androidx.sqlite.db.SupportSQLiteQuery import com.shuwei.dish.match.db.entity.SeasoningSlotEntity /** @@ -64,4 +66,12 @@ interface SeasoningSlotDao { /** 物理清空全表,用于清除测试数据 */ @Query("DELETE FROM dm_seasoning_slot") suspend fun deleteAll() + + /** 动态条件分页查询槽位记录,由 ViewModel 构建 SQL */ + @RawQuery + suspend fun queryPagedFiltered(query: SupportSQLiteQuery): List + + /** 动态条件统计槽位记录总数,由 ViewModel 构建 SQL */ + @RawQuery + suspend fun countFiltered(query: SupportSQLiteQuery): Int } diff --git a/app/src/main/java/com/shuwei/dish/match/ui/SettingActivity.kt b/app/src/main/java/com/shuwei/dish/match/ui/SettingActivity.kt index 9334ed7..ba5bb5e 100644 --- a/app/src/main/java/com/shuwei/dish/match/ui/SettingActivity.kt +++ b/app/src/main/java/com/shuwei/dish/match/ui/SettingActivity.kt @@ -52,7 +52,7 @@ class SettingActivity : BaseActivity() { setTitleBar(titleBarAction = { it.visible() }, titleAction = { - it.text = "设备设置" + it.text = "设置" }, rightIconAction = { it.visible() // 点击右上角图标,展示全部隐藏的开发者选项 diff --git a/app/src/main/java/com/shuwei/dish/match/ui/fragment/DbInspectFragment.kt b/app/src/main/java/com/shuwei/dish/match/ui/fragment/DbInspectFragment.kt index 2814dbf..8182a05 100644 --- a/app/src/main/java/com/shuwei/dish/match/ui/fragment/DbInspectFragment.kt +++ b/app/src/main/java/com/shuwei/dish/match/ui/fragment/DbInspectFragment.kt @@ -2,9 +2,13 @@ package com.shuwei.dish.match.ui.fragment import android.graphics.Color import android.graphics.Typeface +import android.text.InputType +import android.view.Gravity import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.EditText +import android.widget.LinearLayout import android.widget.RadioButton import android.widget.TextView import androidx.cardview.widget.CardView @@ -20,12 +24,11 @@ import com.shuwei.dish.match.ui.SingleFragmentActivity import com.shuwei.dish.match.utils.ext.gone import com.shuwei.dish.match.utils.ext.visible import kotlinx.coroutines.launch +import androidx.core.graphics.toColorInt /** * 数据库调试查看页面 - * 支持按表切换、isDel 过滤开关、分页加载(每页 50 条) - * 菜品/食材/调料表每行 3 列等宽显示字段,槽位表保持单列 - * 已逻辑删除(isDel=1)的记录以灰色文字 + 半透明卡片区分 + * 支持按表切换、isDel 过滤开关、多字段筛选、分页加载 */ class DbInspectFragment : BaseFragment() { @@ -45,6 +48,55 @@ class DbInspectFragment : BaseFragment() { /** Tab RadioButton 列表,用于统一更新选中样式 */ private val tabButtons = mutableListOf() + /** + * 每张表对应的筛选字段配置 + * @param key SQL 列名 + * @param label 显示标签 + * @param hint 输入框提示文字 + * @param isLike true=模糊匹配(LIKE),false=精确匹配(=) + */ + data class FilterField( + val key: String, + val label: String, + val hint: String, + val isLike: Boolean = false + ) + + /** 各表的筛选字段配置 */ + private val filterConfigs = mapOf( + 0 to listOf( + FilterField("foodId", "菜品ID", "精确匹配"), + FilterField("foodName", "菜品名", "模糊搜索", isLike = true), + FilterField("canteenId", "食堂ID", "精确匹配"), + FilterField("cookMode", "菜品模式", "0制作/1采样/2品控"), + FilterField("dinnerType", "餐次", "0默认无/1早餐/2午餐/3晚餐"), + FilterField("submitState", "提交状态", "0本地或手动删除/1接口提交删除") + ), + 1 to listOf( + FilterField("pid", "父ID", "精确匹配"), + FilterField("foodId", "菜品ID", "精确匹配"), + FilterField("goodsId", "食材ID", "精确匹配"), + FilterField("goodsName", "食材名", "模糊搜索", isLike = true), + FilterField("materialType", "物品类型", "1主料/2辅料/3调料"), + FilterField("rawMaterialsType", "净材种类", "精确匹配"), + FilterField("goodsCode", "物料编码", "1主料/2辅料/3调料") + ), + 2 to listOf( + FilterField("goodsId", "调料ID", "精确匹配"), + FilterField("goodsName", "调料名", "模糊搜索", isLike = true), + FilterField("goodsCode", "物料编码", "1主料/2辅料/3调料") + ), + 3 to listOf( + FilterField("deviceId", "设备ID", "精确匹配"), + FilterField("address", "秤硬件地址", "精确匹配"), + FilterField("goodsId", "调料ID", "精确匹配"), + FilterField("goodsName", "调料名", "模糊搜索", isLike = true) + ) + ) + + /** key=列名,value=对应的 EditText,用于查询时收集筛选值 */ + private val filterEditTexts = mutableMapOf() + override fun inflateBinding( inflater: LayoutInflater, container: ViewGroup? @@ -60,18 +112,19 @@ class DbInspectFragment : BaseFragment() { listOf(binding.rbCookFood, binding.rbGoods, binding.rbSeasoning, binding.rbSlot) ) - // Tab 切换:重置到第一页 + // Tab 切换:重置筛选框并加载第一页 binding.rgTabs.setOnCheckedChangeListener { _, checkedId -> currentTableIndex = when (checkedId) { - R.id.rbCookFood -> 0 - R.id.rbGoods -> 1 - R.id.rbSeasoning -> 2 - R.id.rbSlot -> 3 - else -> 0 + R.id.rbCookFood -> 0 + R.id.rbGoods -> 1 + R.id.rbSeasoning -> 2 + R.id.rbSlot -> 3 + else -> 0 } updateTabStyles() moveIndicator(currentTableIndex) updateDelControlsVisibility() + rebuildFilterUI() loadFirstPage() } @@ -82,12 +135,12 @@ class DbInspectFragment : BaseFragment() { loadFirstPage() } - // 刷新按钮 + // 查询按钮:收集筛选条件并加载第一页 binding.tvRefresh.setOnClickListener { loadFirstPage() } - // 加载更多按钮 + // 加载更多按钮:筛选条件从 state 中复用,无需重新传入 binding.tvLoadMore.setOnClickListener { - currentActivity.dbViewModel.loadMoreDbInspect(currentTableIndex, showDel) + currentActivity.dbViewModel.loadMoreDbInspect(currentTableIndex) } // 订阅 UI 状态 @@ -100,12 +153,89 @@ class DbInspectFragment : BaseFragment() { // 初始化样式并加载 updateTabStyles() updateDelControlsVisibility() + rebuildFilterUI() binding.vTabIndicator.post { moveIndicator(currentTableIndex) } loadFirstPage() } + /** 收集当前筛选框的值,构建 DbInspectFilter */ + private fun collectFilter(): DbViewModel.DbInspectFilter { + val exactFields = mutableMapOf() + val likeFields = mutableMapOf() + filterConfigs[currentTableIndex]?.forEach { field -> + val value = filterEditTexts[field.key]?.text?.toString()?.trim() ?: "" + if (value.isNotEmpty()) { + if (field.isLike) likeFields[field.key] = value + else exactFields[field.key] = value + } + } + return DbViewModel.DbInspectFilter( + showDel = showDel, + exactFields = exactFields, + likeFields = likeFields + ) + } + private fun loadFirstPage() { - currentActivity.dbViewModel.loadDbInspect(currentTableIndex, showDel) + currentActivity.dbViewModel.loadDbInspect(currentTableIndex, collectFilter()) + } + + /** + * 根据当前 Tab 重新生成筛选框区域 + * 每行展示 2 个字段,最后一个为奇数时右侧补空占位 + */ + private fun rebuildFilterUI() { + binding.llFilterContainer.removeAllViews() + filterEditTexts.clear() + + val configs = filterConfigs[currentTableIndex] ?: return + val ctx = requireContext() + val hPad = 4.dp + val vPad = 2.dp + + configs.chunked(2).forEach { rowFields -> + val row = LinearLayout(ctx).apply { + orientation = LinearLayout.HORIZONTAL + layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ) + } + + rowFields.forEach { field -> + val cell = LinearLayout(ctx).apply { + orientation = LinearLayout.VERTICAL + setPadding(hPad, vPad, hPad, vPad) + } + + // 字段标签 + cell.addView(TextView(ctx).apply { + text = field.label + textSize = 10f + setTextColor("#888888".toColorInt()) + }) + + // 输入框 + val et = EditText(ctx).apply { + hint = field.hint + textSize = 12f + setSingleLine(true) + inputType = InputType.TYPE_CLASS_TEXT + setPadding(hPad, vPad, hPad, vPad) + } + filterEditTexts[field.key] = et + cell.addView(et) + + row.addView(cell, LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)) + } + + // 奇数字段时右侧补空 View 保持对齐 + if (rowFields.size == 1) { + row.addView(View(ctx), LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)) + } + + binding.llFilterContainer.addView(row) + } } private fun renderState(state: DbViewModel.DbInspectUiState) { @@ -168,6 +298,9 @@ class DbInspectFragment : BaseFragment() { } } + /** dp 转 px 便捷扩展 */ + private val Int.dp: Int get() = (this * resources.displayMetrics.density + 0.5f).toInt() + // ---- 外层 Adapter:每条数据库记录对应一个 CardView ---- /** 外层列表适配器,每个 item 内嵌一个 GridLayoutManager 的 RecyclerView 展示字段 */ @@ -191,17 +324,14 @@ class DbInspectFragment : BaseFragment() { val record = data[position] if (record.isDel) { - // 已删除:浅红背景 + 左侧红色标记条 + 文字颜色正常(深色可读) holder.card.setCardBackgroundColor(Color.parseColor("#FFF3F3")) holder.vDelMark.visibility = View.VISIBLE } else { - // 正常记录:白色背景,隐藏标记条 holder.card.setCardBackgroundColor(Color.WHITE) holder.vDelMark.visibility = View.GONE } val textColor = if (record.isDel) Color.parseColor("#C62828") else Color.parseColor("#333333") - // 每次绑定时重新设置 GridLayoutManager 列数(槽位表为 1,其余为 3) holder.rvFields.layoutManager = GridLayoutManager(holder.rvFields.context, record.columnsPerRow) holder.rvFields.adapter = DbFieldAdapter(record.fields, textColor) } diff --git a/app/src/main/res/layout/fragment_db_inspect.xml b/app/src/main/res/layout/fragment_db_inspect.xml index 0cf2857..acc7120 100644 --- a/app/src/main/res/layout/fragment_db_inspect.xml +++ b/app/src/main/res/layout/fragment_db_inspect.xml @@ -106,11 +106,22 @@ android:layout_height="wrap_content" android:paddingHorizontal="12dp" android:paddingVertical="6dp" - android:text="刷新" + android:text="查询" android:textSize="12sp" android:textColor="#5C77F7" /> + + +