refactor(fragment): DbInspectFragment 改用嵌套 RecyclerView 展示字段,优化已删除记录视觉样式

This commit is contained in:
2026-05-07 18:13:17 +08:00
parent c17402c3c2
commit 0648f458a7
4 changed files with 138 additions and 79 deletions
@@ -244,15 +244,27 @@ class DbViewModel : ViewModel() {
const val DB_INSPECT_PAGE_SIZE = 50 const val DB_INSPECT_PAGE_SIZE = 50
} }
/**
* 单条数据库记录的展示模型
* @param isDel 是否已逻辑删除
* @param fields 字段名-值对列表,按顺序排列
* @param columnsPerRow 每行显示的列数,默认 3;槽位表传 1 保持单列
*/
data class DbRecord(
val isDel: Boolean,
val fields: List<Pair<String, String>>,
val columnsPerRow: Int = 3
)
/** /**
* 数据库调试页面 UI 状态 * 数据库调试页面 UI 状态
* @param items 当前已加载的记录列表Pair.first=是否逻辑删除,Pair.second=格式化文本 * @param items 当前已加载的记录列表
* @param totalCount 当前过滤条件下的总记录数 * @param totalCount 当前过滤条件下的总记录数
* @param hasMore 是否还有更多数据可加载 * @param hasMore 是否还有更多数据可加载
* @param isLoading 是否正在加载(首次加载或加载更多) * @param isLoading 是否正在加载(首次加载或加载更多)
*/ */
data class DbInspectUiState( data class DbInspectUiState(
val items: List<Pair<Boolean, String>> = 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
@@ -303,22 +315,25 @@ class DbViewModel : ViewModel() {
} }
/** /**
* 执行分页查询,返回格式化后的列表和总记录数 * 执行分页查询,返回 DbRecord 列表和总记录数
*/ */
private suspend fun queryPage( private suspend fun queryPage(
tableIndex: Int, tableIndex: Int,
showDel: Boolean, showDel: Boolean,
offset: Int offset: Int
): Pair<List<Pair<Boolean, String>>, 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 total = rep.countCookFoodAll(showDel)
val rows = rep.getCookFoodPaged(showDel, limit, offset).map { e -> val rows = rep.getCookFoodPaged(showDel, limit, offset).map { e ->
(e.isDel == 1) to fmt( DbRecord(
"id" to e.id, "foodId" to e.foodId, "foodName" to e.foodName, isDel = e.isDel == 1,
"canteenId" to e.canteenId, "foodWeight" to e.foodWeight, "cookMode" to e.cookMode, fields = listOf(
"dinnerType" to e.dinnerType, "isDel" to e.isDel, "createTime" to e.createTime "id" to "${e.id}", "foodId" to "${e.foodId}", "foodName" to "${e.foodName}",
"canteenId" to "${e.canteenId}", "foodWeight" to "${e.foodWeight}", "cookMode" to "${e.cookMode}",
"dinnerType" to "${e.dinnerType}", "isDel" to "${e.isDel}", "createTime" to "${e.createTime}"
)
) )
} }
rows to total rows to total
@@ -326,12 +341,15 @@ class DbViewModel : ViewModel() {
1 -> { 1 -> {
val total = rep.countCookFoodGoodsAll(showDel) val total = rep.countCookFoodGoodsAll(showDel)
val rows = rep.getCookFoodGoodsPaged(showDel, limit, offset).map { e -> val rows = rep.getCookFoodGoodsPaged(showDel, limit, offset).map { e ->
(e.isDel == 1) to fmt( DbRecord(
"id" to e.id, "pid" to e.pid, "goodsId" to e.goodsId, isDel = e.isDel == 1,
"goodsName" to e.goodsName, "foodId" to e.foodId, "materialType" to e.materialType, fields = listOf(
"relateionType" to e.relateionType, "allEdible" to e.allEdible, "useWeight" to e.useWeight, "id" to "${e.id}", "pid" to "${e.pid}", "goodsId" to "${e.goodsId}",
"sort" to e.sort, "zjmCode" to e.zjmCode, "materId" to e.materId, "goodsName" to "${e.goodsName}", "foodId" to "${e.foodId}", "materialType" to "${e.materialType}",
"isDel" to e.isDel, "createTime" to e.createTime "relateionType" to "${e.relateionType}", "allEdible" to "${e.allEdible}", "useWeight" to "${e.useWeight}",
"sort" to "${e.sort}", "zjmCode" to "${e.zjmCode}", "materId" to "${e.materId}",
"isDel" to "${e.isDel}", "createTime" to "${e.createTime}"
)
) )
} }
rows to total rows to total
@@ -339,11 +357,14 @@ class DbViewModel : ViewModel() {
2 -> { 2 -> {
val total = rep.countSeasoningAll(showDel) val total = rep.countSeasoningAll(showDel)
val rows = rep.getSeasoningPaged(showDel, limit, offset).map { e -> val rows = rep.getSeasoningPaged(showDel, limit, offset).map { e ->
(e.isDel == 1) to fmt( DbRecord(
"id" to e.id, "goodsId" to e.goodsId, "goodsName" to e.goodsName, isDel = e.isDel == 1,
"sort" to e.sort, "materialType" to e.materialType, "useWeight" to e.useWeight, fields = listOf(
"zjmCode" to e.zjmCode, "materId" to e.materId, "isDel" to e.isDel, "id" to "${e.id}", "goodsId" to "${e.goodsId}", "goodsName" to "${e.goodsName}",
"createTime" to e.createTime "sort" to "${e.sort}", "materialType" to "${e.materialType}", "useWeight" to "${e.useWeight}",
"zjmCode" to "${e.zjmCode}", "materId" to "${e.materId}", "isDel" to "${e.isDel}",
"createTime" to "${e.createTime}"
)
) )
} }
rows to total rows to total
@@ -351,23 +372,21 @@ class DbViewModel : ViewModel() {
3 -> { 3 -> {
val total = rep.countSlotAll() val total = rep.countSlotAll()
val rows = rep.getSlotPaged(limit, offset).map { e -> val rows = rep.getSlotPaged(limit, offset).map { e ->
false to "deviceId: ${e.deviceId}\naddress: ${e.address}\ngoodsId: ${e.goodsId}\ngoodsName: ${e.goodsName}" DbRecord(
isDel = false,
fields = listOf(
"deviceId" to e.deviceId, "address" to "${e.address}",
"goodsId" to e.goodsId, "goodsName" to e.goodsName
),
columnsPerRow = 1 // 槽位表字段少,保持单列显示
)
} }
rows to total rows to total
} }
else -> emptyList<Pair<Boolean, String>>() to 0 else -> emptyList<DbRecord>() to 0
} }
} }
/**
* 将字段列表格式化为每行 3 个的紧凑布局
* 格式:key: value | key: value | key: value
*/
private fun fmt(vararg fields: Pair<String, Any?>): String =
fields.toList().chunked(3).joinToString("\n") { row ->
row.joinToString(" | ") { (k, v) -> "$k: ${v ?: "null"}" }
}
//-------------------------------------------------------------------------------------------------------- //--------------------------------------------------------------------------------------------------------
// viewModelScope.launch { // viewModelScope.launch {
@@ -7,7 +7,9 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.RadioButton import android.widget.RadioButton
import android.widget.TextView import android.widget.TextView
import androidx.cardview.widget.CardView
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.shuwei.dish.match.R import com.shuwei.dish.match.R
@@ -22,8 +24,8 @@ import kotlinx.coroutines.launch
/** /**
* 数据库调试查看页面 * 数据库调试查看页面
* 支持按表切换、isDel 过滤开关、分页加载(每页 50 条) * 支持按表切换、isDel 过滤开关、分页加载(每页 50 条)
* 菜品/食材/调料表每行 3 列等宽显示字段,槽位表保持单列
* 已逻辑删除(isDel=1)的记录以灰色文字 + 半透明卡片区分 * 已逻辑删除(isDel=1)的记录以灰色文字 + 半透明卡片区分
* 槽位表无 isDel 字段,切换到该 Tab 时自动隐藏 isDel 控件
*/ */
class DbInspectFragment : BaseFragment<FragmentDbInspectBinding>() { class DbInspectFragment : BaseFragment<FragmentDbInspectBinding>() {
@@ -35,10 +37,7 @@ class DbInspectFragment : BaseFragment<FragmentDbInspectBinding>() {
/** 是否显示已删除记录(槽位表忽略此参数) */ /** 是否显示已删除记录(槽位表忽略此参数) */
private var showDel = true private var showDel = true
/** /** 防止代码中设置 Switch 状态时误触发监听器 */
* 防止在代码中设置 Switch 状态时触发监听器,
* 例如切换回非槽位 Tab 时恢复 Switch 状态
*/
private var ignoreSwitch = false private var ignoreSwitch = false
private val adapter = DbRecordAdapter() private val adapter = DbRecordAdapter()
@@ -83,7 +82,7 @@ class DbInspectFragment : BaseFragment<FragmentDbInspectBinding>() {
loadFirstPage() loadFirstPage()
} }
// 刷新按钮:重新加载第一页 // 刷新按钮
binding.tvRefresh.setOnClickListener { loadFirstPage() } binding.tvRefresh.setOnClickListener { loadFirstPage() }
// 加载更多按钮 // 加载更多按钮
@@ -98,30 +97,23 @@ class DbInspectFragment : BaseFragment<FragmentDbInspectBinding>() {
} }
} }
// 初始化 Tab 样式并加载默认表 // 初始化样式并加载
updateTabStyles() updateTabStyles()
updateDelControlsVisibility() updateDelControlsVisibility()
binding.vTabIndicator.post { moveIndicator(currentTableIndex) } binding.vTabIndicator.post { moveIndicator(currentTableIndex) }
loadFirstPage() loadFirstPage()
} }
/** 重新从第一页开始加载当前表数据 */
private fun loadFirstPage() { private fun loadFirstPage() {
currentActivity.appViewModel.loadDbInspect(currentTableIndex, showDel) currentActivity.appViewModel.loadDbInspect(currentTableIndex, showDel)
} }
/**
* 根据 UI 状态更新计数文字、加载更多按钮和列表
*/
private fun renderState(state: DbViewModel.DbInspectUiState) { private fun renderState(state: DbViewModel.DbInspectUiState) {
// 计数文字
binding.tvCount.text = when { binding.tvCount.text = when {
state.isLoading && state.items.isEmpty() -> "加载中..." state.isLoading && state.items.isEmpty() -> "加载中..."
state.hasMore -> "已加载 ${state.items.size} / 共 ${state.totalCount}" state.hasMore -> "已加载 ${state.items.size} / 共 ${state.totalCount}"
else -> "${state.totalCount}" else -> "${state.totalCount}"
} }
// 加载更多按钮:有更多数据时显示,加载中时显示"加载中..."并禁用点击
when { when {
state.isLoading && state.hasMore -> { state.isLoading && state.hasMore -> {
binding.tvLoadMore.visible() binding.tvLoadMore.visible()
@@ -135,13 +127,11 @@ class DbInspectFragment : BaseFragment<FragmentDbInspectBinding>() {
} }
else -> binding.tvLoadMore.gone() else -> binding.tvLoadMore.gone()
} }
adapter.setData(state.items) adapter.setData(state.items)
} }
/** /**
* 槽位表index=3)没有 isDel 字段,切换到该 Tab 时隐藏 isDel 相关控件; * 槽位表 isDel 字段,切换到时隐藏相关控件;切换回其他 Tab 时恢复
* 切换回其他 Tab 时恢复,并同步 Switch 状态
*/ */
private fun updateDelControlsVisibility() { private fun updateDelControlsVisibility() {
val isSlot = currentTableIndex == 3 val isSlot = currentTableIndex == 3
@@ -151,16 +141,12 @@ class DbInspectFragment : BaseFragment<FragmentDbInspectBinding>() {
} else { } else {
binding.tvDelLabel.visible() binding.tvDelLabel.visible()
binding.swShowDel.visible() binding.swShowDel.visible()
// 恢复 Switch 显示状态(防止误触发监听器)
ignoreSwitch = true ignoreSwitch = true
binding.swShowDel.isChecked = showDel binding.swShowDel.isChecked = showDel
ignoreSwitch = false ignoreSwitch = false
} }
} }
/**
* 更新所有 Tab 的文字颜色和加粗状态
*/
private fun updateTabStyles() { private fun updateTabStyles() {
tabButtons.forEachIndexed { index, btn -> tabButtons.forEachIndexed { index, btn ->
val selected = index == currentTableIndex val selected = index == currentTableIndex
@@ -169,20 +155,12 @@ class DbInspectFragment : BaseFragment<FragmentDbInspectBinding>() {
} }
} }
/**
* 将下方指示条平移到选中 Tab 的正下方(150ms 动画)
*/
private fun moveIndicator(index: Int) { private fun moveIndicator(index: Int) {
val btn = tabButtons.getOrNull(index) ?: return val btn = tabButtons.getOrNull(index) ?: return
val tabWidth = btn.width.toFloat() val tabWidth = btn.width.toFloat()
val indicatorWidth = tabWidth / 2f val indicatorWidth = tabWidth / 2f
val indicatorX = btn.x + (tabWidth - indicatorWidth) / 2f val indicatorX = btn.x + (tabWidth - indicatorWidth) / 2f
binding.vTabIndicator.animate().translationX(indicatorX).setDuration(150).start()
binding.vTabIndicator.animate()
.translationX(indicatorX)
.setDuration(150)
.start()
binding.vTabIndicator.post { binding.vTabIndicator.post {
val lp = binding.vTabIndicator.layoutParams val lp = binding.vTabIndicator.layoutParams
lp.width = indicatorWidth.toInt() lp.width = indicatorWidth.toInt()
@@ -190,14 +168,14 @@ class DbInspectFragment : BaseFragment<FragmentDbInspectBinding>() {
} }
} }
// ---- 内部 Adapter ---- // ---- 外层 Adapter:每条数据库记录对应一个 CardView ----
/** 数据库记录列表适配器 */ /** 外层列表适配器,每个 item 内嵌一个 GridLayoutManager 的 RecyclerView 展示字段 */
private inner class DbRecordAdapter : RecyclerView.Adapter<DbRecordAdapter.VH>() { private inner class DbRecordAdapter : RecyclerView.Adapter<DbRecordAdapter.VH>() {
private val data = mutableListOf<Pair<Boolean, String>>() private val data = mutableListOf<DbViewModel.DbRecord>()
fun setData(list: List<Pair<Boolean, String>>) { fun setData(list: List<DbViewModel.DbRecord>) {
data.clear() data.clear()
data.addAll(list) data.addAll(list)
notifyDataSetChanged() notifyDataSetChanged()
@@ -210,20 +188,57 @@ class DbInspectFragment : BaseFragment<FragmentDbInspectBinding>() {
} }
override fun onBindViewHolder(holder: VH, position: Int) { override fun onBindViewHolder(holder: VH, position: Int) {
val (isDeleted, content) = data[position] val record = data[position]
holder.tvContent.text = content
// isDel=1 的记录用灰色标识,正常记录用深色 if (record.isDel) {
holder.tvContent.setTextColor( // 已删除:浅红背景 + 左侧红色标记条 + 文字颜色正常(深色可读)
if (isDeleted) Color.parseColor("#BBBBBB") else Color.parseColor("#333333") holder.card.setCardBackgroundColor(Color.parseColor("#FFF3F3"))
) holder.vDelMark.visibility = View.VISIBLE
// 已删除记录卡片略微透明 } else {
holder.itemView.alpha = if (isDeleted) 0.6f else 1.0f // 正常记录:白色背景,隐藏标记条
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)
} }
override fun getItemCount() = data.size override fun getItemCount() = data.size
inner class VH(itemView: View) : RecyclerView.ViewHolder(itemView) { inner class VH(itemView: View) : RecyclerView.ViewHolder(itemView) {
val tvContent: TextView = itemView.findViewById(R.id.tvContent) val card: CardView = itemView.findViewById(R.id.cardRecord)
val vDelMark: View = itemView.findViewById(R.id.vDelMark)
val rvFields: RecyclerView = itemView.findViewById(R.id.rvFields)
}
}
// ---- 内层 Adapter:每个字段对应一个 TextView,垂直显示字段名和值 ----
/** 内层字段适配器,每个 item 为单个 TextView,显示格式:字段名\n字段值 */
private inner class DbFieldAdapter(
private val fields: List<Pair<String, String>>,
private val textColor: Int
) : RecyclerView.Adapter<DbFieldAdapter.VH>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.list_item_db_field, parent, false)
return VH(view)
}
override fun onBindViewHolder(holder: VH, position: Int) {
val (key, value) = fields[position]
holder.tvField.text = "$key\n$value"
holder.tvField.setTextColor(textColor)
}
override fun getItemCount() = fields.size
inner class VH(itemView: View) : RecyclerView.ViewHolder(itemView) {
val tvField: TextView = itemView.findViewById(R.id.tvField)
} }
} }
} }
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/tvField"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="10sp"
android:fontFamily="monospace"
android:gravity="start"
android:paddingBottom="4dp" />
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" <androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/cardRecord"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
@@ -9,13 +10,28 @@
app:cardElevation="1dp" app:cardElevation="1dp"
app:cardBackgroundColor="#FFFFFF"> app:cardBackgroundColor="#FFFFFF">
<TextView <LinearLayout
android:id="@+id/tvContent"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="10dp" android:orientation="horizontal">
android:textSize="11sp"
android:lineSpacingMultiplier="1.3" <!-- 已删除标记条:isDel=1 时显示红色左边框 -->
android:fontFamily="monospace" /> <View
android:id="@+id/vDelMark"
android:layout_width="4dp"
android:layout_height="match_parent"
android:background="#E53935"
android:visibility="gone" />
<!-- 字段由内层 RecyclerViewGridLayoutManager)填充 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvFields"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false"
android:padding="8dp"
android:clipToPadding="false" />
</LinearLayout>
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>