feat(db): 为数据库调试页面添加多字段动态筛选功能

- 在 CookFoodDao、CookFoodGoodsDao、SeasoningDao 和 SeasoningSlotDao 中添加 RawQuery 支持
- 为各数据访问对象添加动态条件分页查询和统计方法
- 在 DbRepository 中扩展动态查询相关方法
- 在 DbViewModel 中实现 DbInspectFilter 数据类和 buildQueries 动态 SQL 构建逻辑
- 更新 DbInspectFragment 添加筛选字段配置和 UI 生成逻辑
- 重构筛选条件收集和查询流程,支持精确匹配和模糊搜索
- 修改刷新按钮为查询按钮,更新 Tab 切换时的筛选框重建逻辑
- 调整设置页面标题从"设备设置"改为"设置"
This commit is contained in:
2026-05-18 15:52:46 +08:00
parent df01312577
commit 28784fa7c1
9 changed files with 326 additions and 39 deletions
@@ -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.CookFoodGoodsEntity
import com.shuwei.dish.match.db.entity.SeasoningEntity import com.shuwei.dish.match.db.entity.SeasoningEntity
import com.shuwei.dish.match.db.entity.SeasoningSlotEntity import com.shuwei.dish.match.db.entity.SeasoningSlotEntity
import androidx.sqlite.db.SupportSQLiteQuery
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -233,6 +234,40 @@ class DbRepository {
db.seasoningSlotDao().countSlotAll() 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 张表的测试数据 * 物理清空全部 4 张表的测试数据
* 清除顺序:先删子表(食材),再删主表(菜品),最后删配置表(调料、槽位) * 清除顺序:先删子表(食材),再删主表(菜品),最后删配置表(调料、槽位)
@@ -2,6 +2,7 @@ package com.shuwei.dish.match.db
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.sqlite.db.SimpleSQLiteQuery
import com.shuwei.dish.match.model.CollectedFoodItem import com.shuwei.dish.match.model.CollectedFoodItem
import com.shuwei.dish.match.db.entity.CookFoodEntity import com.shuwei.dish.match.db.entity.CookFoodEntity
import com.shuwei.dish.match.objbox.ObjectBox import com.shuwei.dish.match.objbox.ObjectBox
@@ -332,71 +333,89 @@ class DbViewModel : ViewModel() {
* @param totalCount 当前过滤条件下的总记录数 * @param totalCount 当前过滤条件下的总记录数
* @param hasMore 是否还有更多数据可加载 * @param hasMore 是否还有更多数据可加载
* @param isLoading 是否正在加载(首次加载或加载更多) * @param isLoading 是否正在加载(首次加载或加载更多)
* @param filter 当前生效的筛选条件,供"加载更多"复用
*/ */
data class DbInspectUiState( data class DbInspectUiState(
val items: List<DbRecord> = emptyList(), val items: List<DbRecord> = emptyList(),
val totalCount: Int = 0, val totalCount: Int = 0,
val hasMore: Boolean = false, 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<String, String> = emptyMap(),
val likeFields: Map<String, String> = emptyMap()
) )
private val _dbInspectState = MutableStateFlow(DbInspectUiState()) private val _dbInspectState = MutableStateFlow(DbInspectUiState())
val dbInspectState: StateFlow<DbInspectUiState> = _dbInspectState.asStateFlow() val dbInspectState: StateFlow<DbInspectUiState> = _dbInspectState.asStateFlow()
/** /**
* 加载指定表的第一页数据(切换 Tab / 切换 isDel 过滤时调用) * 加载指定表的第一页数据(切换 Tab / 修改筛选条件时调用)
* @param tableIndex 0=菜品 1=食材 2=调料 3=槽位 * @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 { viewModelScope.launch {
// 立即清空旧数据并显示加载状态,避免切换 Tab 时旧数据闪现 // 立即清空旧数据并显示加载状态,避免切换 Tab 时旧数据闪现
_dbInspectState.value = DbInspectUiState(isLoading = true) _dbInspectState.value = DbInspectUiState(isLoading = true, filter = filter)
val (items, total) = queryPage(tableIndex, showDel, offset = 0) val (items, total) = queryPage(tableIndex, filter, offset = 0)
_dbInspectState.value = DbInspectUiState( _dbInspectState.value = DbInspectUiState(
items = items, items = items,
totalCount = total, totalCount = total,
hasMore = items.size < total, hasMore = items.size < total,
isLoading = false isLoading = false,
filter = filter
) )
} }
} }
/** /**
* 追加加载下一页数据(点击"加载更多"时调用) * 追加加载下一页数据(点击"加载更多"时调用)
* 筛选条件从当前 state 中复用,无需重新传入
* @param tableIndex 0=菜品 1=食材 2=调料 3=槽位 * @param tableIndex 0=菜品 1=食材 2=调料 3=槽位
* @param showDel true=包含已删除记录
*/ */
fun loadMoreDbInspect(tableIndex: Int, showDel: Boolean) { fun loadMoreDbInspect(tableIndex: Int) {
viewModelScope.launch { viewModelScope.launch {
val current = _dbInspectState.value val current = _dbInspectState.value
// 已在加载中或无更多数据时直接返回,防止重复请求 // 已在加载中或无更多数据时直接返回,防止重复请求
if (!current.hasMore || current.isLoading) return@launch if (!current.hasMore || current.isLoading) return@launch
_dbInspectState.value = current.copy(isLoading = true) _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 val merged = current.items + newItems
_dbInspectState.value = DbInspectUiState( _dbInspectState.value = DbInspectUiState(
items = merged, items = merged,
totalCount = total, totalCount = total,
hasMore = merged.size < total, hasMore = merged.size < total,
isLoading = false isLoading = false,
filter = current.filter
) )
} }
} }
/** /**
* 执行分页查询,返回 DbRecord 列表和总记录数 * 执行分页查询,返回 DbRecord 列表和总记录数
* 通过 buildQueries 将 filter 转换为动态 SQL
*/ */
private suspend fun queryPage( private suspend fun queryPage(
tableIndex: Int, tableIndex: Int,
showDel: Boolean, filter: DbInspectFilter,
offset: Int offset: Int
): Pair<List<DbRecord>, Int> { ): Pair<List<DbRecord>, Int> {
val limit = DB_INSPECT_PAGE_SIZE val limit = DB_INSPECT_PAGE_SIZE
return when (tableIndex) { return when (tableIndex) {
0 -> { 0 -> {
val total = rep.countCookFoodAll(showDel) val (pageQ, countQ) = buildQueries("dm_cook_food", "createTime DESC", true, filter, limit, offset)
val rows = rep.getCookFoodPaged(showDel, limit, offset).map { e -> val total = rep.countCookFoodFiltered(countQ)
val rows = rep.getCookFoodPagedFiltered(pageQ).map { e ->
DbRecord( DbRecord(
isDel = e.isDel == 1, isDel = e.isDel == 1,
fields = listOf( fields = listOf(
@@ -410,8 +429,9 @@ class DbViewModel : ViewModel() {
rows to total rows to total
} }
1 -> { 1 -> {
val total = rep.countCookFoodGoodsAll(showDel) val (pageQ, countQ) = buildQueries("dm_cook_food_goods", "createTime DESC", true, filter, limit, offset)
val rows = rep.getCookFoodGoodsPaged(showDel, limit, offset).map { e -> val total = rep.countCookFoodGoodsFiltered(countQ)
val rows = rep.getCookFoodGoodsPagedFiltered(pageQ).map { e ->
DbRecord( DbRecord(
isDel = e.isDel == 1, isDel = e.isDel == 1,
fields = listOf( fields = listOf(
@@ -426,8 +446,9 @@ class DbViewModel : ViewModel() {
rows to total rows to total
} }
2 -> { 2 -> {
val total = rep.countSeasoningAll(showDel) val (pageQ, countQ) = buildQueries("dm_seasoning", "sort ASC", true, filter, limit, offset)
val rows = rep.getSeasoningPaged(showDel, limit, offset).map { e -> val total = rep.countSeasoningFiltered(countQ)
val rows = rep.getSeasoningPagedFiltered(pageQ).map { e ->
DbRecord( DbRecord(
isDel = e.isDel == 1, isDel = e.isDel == 1,
fields = listOf( fields = listOf(
@@ -441,15 +462,16 @@ class DbViewModel : ViewModel() {
rows to total rows to total
} }
3 -> { 3 -> {
val total = rep.countSlotAll() val (pageQ, countQ) = buildQueries("dm_seasoning_slot", "deviceId ASC, address ASC", false, filter, limit, offset)
val rows = rep.getSlotPaged(limit, offset).map { e -> val total = rep.countSlotFiltered(countQ)
val rows = rep.getSlotPagedFiltered(pageQ).map { e ->
DbRecord( DbRecord(
isDel = false, isDel = false,
fields = listOf( fields = listOf(
"deviceId" to e.deviceId, "address" to "${e.address}", "deviceId" to e.deviceId, "address" to "${e.address}",
"goodsId" to e.goodsId, "goodsName" to e.goodsName "goodsId" to e.goodsId, "goodsName" to e.goodsName
), ),
columnsPerRow = 1 // 槽位表字段少,保持单列显示 columnsPerRow = 1
) )
} }
rows to total 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<SimpleSQLiteQuery, SimpleSQLiteQuery> {
val conditions = mutableListOf<String>()
val args = mutableListOf<Any>()
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 { // viewModelScope.launch {
@@ -4,7 +4,9 @@ import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import androidx.room.RawQuery
import androidx.room.Update import androidx.room.Update
import androidx.sqlite.db.SupportSQLiteQuery
import com.shuwei.dish.match.db.entity.CookFoodEntity import com.shuwei.dish.match.db.entity.CookFoodEntity
@Dao @Dao
@@ -48,4 +50,12 @@ interface CookFoodDao {
/** 物理清空全表,用于清除测试数据 */ /** 物理清空全表,用于清除测试数据 */
@Query("DELETE FROM dm_cook_food") @Query("DELETE FROM dm_cook_food")
suspend fun deleteAll() suspend fun deleteAll()
/** 动态条件分页查询菜品记录,由 ViewModel 构建 SQL */
@RawQuery
suspend fun queryPagedFiltered(query: SupportSQLiteQuery): List<CookFoodEntity>
/** 动态条件统计菜品记录总数,由 ViewModel 构建 SQL */
@RawQuery
suspend fun countFiltered(query: SupportSQLiteQuery): Int
} }
@@ -4,7 +4,9 @@ import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import androidx.room.RawQuery
import androidx.room.Update import androidx.room.Update
import androidx.sqlite.db.SupportSQLiteQuery
import com.shuwei.dish.match.db.entity.CookFoodGoodsEntity import com.shuwei.dish.match.db.entity.CookFoodGoodsEntity
@Dao @Dao
@@ -48,4 +50,12 @@ interface CookFoodGoodsDao {
/** 物理清空全表,用于清除测试数据 */ /** 物理清空全表,用于清除测试数据 */
@Query("DELETE FROM dm_cook_food_goods") @Query("DELETE FROM dm_cook_food_goods")
suspend fun deleteAll() suspend fun deleteAll()
/** 动态条件分页查询食材记录,由 ViewModel 构建 SQL */
@RawQuery
suspend fun queryPagedFiltered(query: SupportSQLiteQuery): List<CookFoodGoodsEntity>
/** 动态条件统计食材记录总数,由 ViewModel 构建 SQL */
@RawQuery
suspend fun countFiltered(query: SupportSQLiteQuery): Int
} }
@@ -4,7 +4,9 @@ import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import androidx.room.RawQuery
import androidx.room.Update import androidx.room.Update
import androidx.sqlite.db.SupportSQLiteQuery
import com.shuwei.dish.match.db.entity.SeasoningEntity import com.shuwei.dish.match.db.entity.SeasoningEntity
@Dao @Dao
@@ -57,4 +59,12 @@ interface SeasoningDao {
/** 物理清空全表,用于清除测试数据(比 clearAllSeasoning 的逻辑删除更彻底) */ /** 物理清空全表,用于清除测试数据(比 clearAllSeasoning 的逻辑删除更彻底) */
@Query("DELETE FROM dm_seasoning") @Query("DELETE FROM dm_seasoning")
suspend fun deleteAll() suspend fun deleteAll()
/** 动态条件分页查询调料记录,由 ViewModel 构建 SQL */
@RawQuery
suspend fun queryPagedFiltered(query: SupportSQLiteQuery): List<SeasoningEntity>
/** 动态条件统计调料记录总数,由 ViewModel 构建 SQL */
@RawQuery
suspend fun countFiltered(query: SupportSQLiteQuery): Int
} }
@@ -4,6 +4,8 @@ import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import androidx.room.RawQuery
import androidx.sqlite.db.SupportSQLiteQuery
import com.shuwei.dish.match.db.entity.SeasoningSlotEntity import com.shuwei.dish.match.db.entity.SeasoningSlotEntity
/** /**
@@ -64,4 +66,12 @@ interface SeasoningSlotDao {
/** 物理清空全表,用于清除测试数据 */ /** 物理清空全表,用于清除测试数据 */
@Query("DELETE FROM dm_seasoning_slot") @Query("DELETE FROM dm_seasoning_slot")
suspend fun deleteAll() suspend fun deleteAll()
/** 动态条件分页查询槽位记录,由 ViewModel 构建 SQL */
@RawQuery
suspend fun queryPagedFiltered(query: SupportSQLiteQuery): List<SeasoningSlotEntity>
/** 动态条件统计槽位记录总数,由 ViewModel 构建 SQL */
@RawQuery
suspend fun countFiltered(query: SupportSQLiteQuery): Int
} }
@@ -52,7 +52,7 @@ class SettingActivity : BaseActivity() {
setTitleBar(titleBarAction = { setTitleBar(titleBarAction = {
it.visible() it.visible()
}, titleAction = { }, titleAction = {
it.text = "备设" it.text = "设置"
}, rightIconAction = { }, rightIconAction = {
it.visible() it.visible()
// 点击右上角图标,展示全部隐藏的开发者选项 // 点击右上角图标,展示全部隐藏的开发者选项
@@ -2,9 +2,13 @@ package com.shuwei.dish.match.ui.fragment
import android.graphics.Color import android.graphics.Color
import android.graphics.Typeface import android.graphics.Typeface
import android.text.InputType
import android.view.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.RadioButton import android.widget.RadioButton
import android.widget.TextView import android.widget.TextView
import androidx.cardview.widget.CardView 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.gone
import com.shuwei.dish.match.utils.ext.visible import com.shuwei.dish.match.utils.ext.visible
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import androidx.core.graphics.toColorInt
/** /**
* 数据库调试查看页面 * 数据库调试查看页面
* 支持按表切换、isDel 过滤开关、分页加载(每页 50 条) * 支持按表切换、isDel 过滤开关、多字段筛选、分页加载
* 菜品/食材/调料表每行 3 列等宽显示字段,槽位表保持单列
* 已逻辑删除(isDel=1)的记录以灰色文字 + 半透明卡片区分
*/ */
class DbInspectFragment : BaseFragment<FragmentDbInspectBinding>() { class DbInspectFragment : BaseFragment<FragmentDbInspectBinding>() {
@@ -45,6 +48,55 @@ class DbInspectFragment : BaseFragment<FragmentDbInspectBinding>() {
/** Tab RadioButton 列表,用于统一更新选中样式 */ /** Tab RadioButton 列表,用于统一更新选中样式 */
private val tabButtons = mutableListOf<RadioButton>() private val tabButtons = mutableListOf<RadioButton>()
/**
* 每张表对应的筛选字段配置
* @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<String, EditText>()
override fun inflateBinding( override fun inflateBinding(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup? container: ViewGroup?
@@ -60,18 +112,19 @@ class DbInspectFragment : BaseFragment<FragmentDbInspectBinding>() {
listOf(binding.rbCookFood, binding.rbGoods, binding.rbSeasoning, binding.rbSlot) listOf(binding.rbCookFood, binding.rbGoods, binding.rbSeasoning, binding.rbSlot)
) )
// Tab 切换:重置第一页 // Tab 切换:重置筛选框并加载第一页
binding.rgTabs.setOnCheckedChangeListener { _, checkedId -> binding.rgTabs.setOnCheckedChangeListener { _, checkedId ->
currentTableIndex = when (checkedId) { currentTableIndex = when (checkedId) {
R.id.rbCookFood -> 0 R.id.rbCookFood -> 0
R.id.rbGoods -> 1 R.id.rbGoods -> 1
R.id.rbSeasoning -> 2 R.id.rbSeasoning -> 2
R.id.rbSlot -> 3 R.id.rbSlot -> 3
else -> 0 else -> 0
} }
updateTabStyles() updateTabStyles()
moveIndicator(currentTableIndex) moveIndicator(currentTableIndex)
updateDelControlsVisibility() updateDelControlsVisibility()
rebuildFilterUI()
loadFirstPage() loadFirstPage()
} }
@@ -82,12 +135,12 @@ class DbInspectFragment : BaseFragment<FragmentDbInspectBinding>() {
loadFirstPage() loadFirstPage()
} }
// 刷新按钮 // 查询按钮:收集筛选条件并加载第一页
binding.tvRefresh.setOnClickListener { loadFirstPage() } binding.tvRefresh.setOnClickListener { loadFirstPage() }
// 加载更多按钮 // 加载更多按钮:筛选条件从 state 中复用,无需重新传入
binding.tvLoadMore.setOnClickListener { binding.tvLoadMore.setOnClickListener {
currentActivity.dbViewModel.loadMoreDbInspect(currentTableIndex, showDel) currentActivity.dbViewModel.loadMoreDbInspect(currentTableIndex)
} }
// 订阅 UI 状态 // 订阅 UI 状态
@@ -100,12 +153,89 @@ class DbInspectFragment : BaseFragment<FragmentDbInspectBinding>() {
// 初始化样式并加载 // 初始化样式并加载
updateTabStyles() updateTabStyles()
updateDelControlsVisibility() updateDelControlsVisibility()
rebuildFilterUI()
binding.vTabIndicator.post { moveIndicator(currentTableIndex) } binding.vTabIndicator.post { moveIndicator(currentTableIndex) }
loadFirstPage() loadFirstPage()
} }
/** 收集当前筛选框的值,构建 DbInspectFilter */
private fun collectFilter(): DbViewModel.DbInspectFilter {
val exactFields = mutableMapOf<String, String>()
val likeFields = mutableMapOf<String, String>()
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() { 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) { private fun renderState(state: DbViewModel.DbInspectUiState) {
@@ -168,6 +298,9 @@ class DbInspectFragment : BaseFragment<FragmentDbInspectBinding>() {
} }
} }
/** dp 转 px 便捷扩展 */
private val Int.dp: Int get() = (this * resources.displayMetrics.density + 0.5f).toInt()
// ---- 外层 Adapter:每条数据库记录对应一个 CardView ---- // ---- 外层 Adapter:每条数据库记录对应一个 CardView ----
/** 外层列表适配器,每个 item 内嵌一个 GridLayoutManager 的 RecyclerView 展示字段 */ /** 外层列表适配器,每个 item 内嵌一个 GridLayoutManager 的 RecyclerView 展示字段 */
@@ -191,17 +324,14 @@ class DbInspectFragment : BaseFragment<FragmentDbInspectBinding>() {
val record = data[position] val record = data[position]
if (record.isDel) { if (record.isDel) {
// 已删除:浅红背景 + 左侧红色标记条 + 文字颜色正常(深色可读)
holder.card.setCardBackgroundColor(Color.parseColor("#FFF3F3")) holder.card.setCardBackgroundColor(Color.parseColor("#FFF3F3"))
holder.vDelMark.visibility = View.VISIBLE holder.vDelMark.visibility = View.VISIBLE
} else { } else {
// 正常记录:白色背景,隐藏标记条
holder.card.setCardBackgroundColor(Color.WHITE) holder.card.setCardBackgroundColor(Color.WHITE)
holder.vDelMark.visibility = View.GONE holder.vDelMark.visibility = View.GONE
} }
val textColor = if (record.isDel) Color.parseColor("#C62828") else Color.parseColor("#333333") 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.layoutManager = GridLayoutManager(holder.rvFields.context, record.columnsPerRow)
holder.rvFields.adapter = DbFieldAdapter(record.fields, textColor) holder.rvFields.adapter = DbFieldAdapter(record.fields, textColor)
} }
@@ -106,11 +106,22 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingHorizontal="12dp" android:paddingHorizontal="12dp"
android:paddingVertical="6dp" android:paddingVertical="6dp"
android:text="刷新" android:text="查询"
android:textSize="12sp" android:textSize="12sp"
android:textColor="#5C77F7" /> android:textColor="#5C77F7" />
</LinearLayout> </LinearLayout>
<!-- 筛选条件区域:根据当前 Tab 动态生成对应的筛选框 -->
<LinearLayout
android:id="@+id/llFilterContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="#FFFFFF"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:paddingBottom="6dp" />
<!-- 记录列表 --> <!-- 记录列表 -->
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvRecords" android:id="@+id/rvRecords"