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
}
/**
* 单条数据库记录的展示模型
* @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 状态
* @param items 当前已加载的记录列表Pair.first=是否逻辑删除,Pair.second=格式化文本
* @param items 当前已加载的记录列表
* @param totalCount 当前过滤条件下的总记录数
* @param hasMore 是否还有更多数据可加载
* @param isLoading 是否正在加载(首次加载或加载更多)
*/
data class DbInspectUiState(
val items: List<Pair<Boolean, String>> = emptyList(),
val items: List<DbRecord> = emptyList(),
val totalCount: Int = 0,
val hasMore: Boolean = false,
val isLoading: Boolean = false
@@ -303,22 +315,25 @@ class DbViewModel : ViewModel() {
}
/**
* 执行分页查询,返回格式化后的列表和总记录数
* 执行分页查询,返回 DbRecord 列表和总记录数
*/
private suspend fun queryPage(
tableIndex: Int,
showDel: Boolean,
offset: Int
): Pair<List<Pair<Boolean, String>>, Int> {
): Pair<List<DbRecord>, 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 ->
(e.isDel == 1) to fmt(
"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
DbRecord(
isDel = e.isDel == 1,
fields = listOf(
"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
@@ -326,12 +341,15 @@ class DbViewModel : ViewModel() {
1 -> {
val total = rep.countCookFoodGoodsAll(showDel)
val rows = rep.getCookFoodGoodsPaged(showDel, limit, offset).map { e ->
(e.isDel == 1) to fmt(
"id" to e.id, "pid" to e.pid, "goodsId" to e.goodsId,
"goodsName" to e.goodsName, "foodId" to e.foodId, "materialType" to e.materialType,
"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
DbRecord(
isDel = e.isDel == 1,
fields = listOf(
"id" to "${e.id}", "pid" to "${e.pid}", "goodsId" to "${e.goodsId}",
"goodsName" to "${e.goodsName}", "foodId" to "${e.foodId}", "materialType" to "${e.materialType}",
"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
@@ -339,11 +357,14 @@ class DbViewModel : ViewModel() {
2 -> {
val total = rep.countSeasoningAll(showDel)
val rows = rep.getSeasoningPaged(showDel, limit, offset).map { e ->
(e.isDel == 1) to fmt(
"id" to e.id, "goodsId" to e.goodsId, "goodsName" to e.goodsName,
"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
DbRecord(
isDel = e.isDel == 1,
fields = listOf(
"id" to "${e.id}", "goodsId" to "${e.goodsId}", "goodsName" to "${e.goodsName}",
"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
@@ -351,23 +372,21 @@ class DbViewModel : ViewModel() {
3 -> {
val total = rep.countSlotAll()
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
}
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 {
@@ -7,7 +7,9 @@ import android.view.View
import android.view.ViewGroup
import android.widget.RadioButton
import android.widget.TextView
import androidx.cardview.widget.CardView
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.shuwei.dish.match.R
@@ -22,8 +24,8 @@ import kotlinx.coroutines.launch
/**
* 数据库调试查看页面
* 支持按表切换、isDel 过滤开关、分页加载(每页 50 条)
* 菜品/食材/调料表每行 3 列等宽显示字段,槽位表保持单列
* 已逻辑删除(isDel=1)的记录以灰色文字 + 半透明卡片区分
* 槽位表无 isDel 字段,切换到该 Tab 时自动隐藏 isDel 控件
*/
class DbInspectFragment : BaseFragment<FragmentDbInspectBinding>() {
@@ -35,10 +37,7 @@ class DbInspectFragment : BaseFragment<FragmentDbInspectBinding>() {
/** 是否显示已删除记录(槽位表忽略此参数) */
private var showDel = true
/**
* 防止在代码中设置 Switch 状态时触发监听器,
* 例如切换回非槽位 Tab 时恢复 Switch 状态
*/
/** 防止代码中设置 Switch 状态时误触发监听器 */
private var ignoreSwitch = false
private val adapter = DbRecordAdapter()
@@ -83,7 +82,7 @@ class DbInspectFragment : BaseFragment<FragmentDbInspectBinding>() {
loadFirstPage()
}
// 刷新按钮:重新加载第一页
// 刷新按钮
binding.tvRefresh.setOnClickListener { loadFirstPage() }
// 加载更多按钮
@@ -98,30 +97,23 @@ class DbInspectFragment : BaseFragment<FragmentDbInspectBinding>() {
}
}
// 初始化 Tab 样式并加载默认表
// 初始化样式并加载
updateTabStyles()
updateDelControlsVisibility()
binding.vTabIndicator.post { moveIndicator(currentTableIndex) }
loadFirstPage()
}
/** 重新从第一页开始加载当前表数据 */
private fun loadFirstPage() {
currentActivity.appViewModel.loadDbInspect(currentTableIndex, showDel)
}
/**
* 根据 UI 状态更新计数文字、加载更多按钮和列表
*/
private fun renderState(state: DbViewModel.DbInspectUiState) {
// 计数文字
binding.tvCount.text = when {
state.isLoading && state.items.isEmpty() -> "加载中..."
state.hasMore -> "已加载 ${state.items.size} / 共 ${state.totalCount}"
else -> "${state.totalCount}"
}
// 加载更多按钮:有更多数据时显示,加载中时显示"加载中..."并禁用点击
when {
state.isLoading && state.hasMore -> {
binding.tvLoadMore.visible()
@@ -135,13 +127,11 @@ class DbInspectFragment : BaseFragment<FragmentDbInspectBinding>() {
}
else -> binding.tvLoadMore.gone()
}
adapter.setData(state.items)
}
/**
* 槽位表index=3)没有 isDel 字段,切换到该 Tab 时隐藏 isDel 相关控件;
* 切换回其他 Tab 时恢复,并同步 Switch 状态
* 槽位表 isDel 字段,切换到时隐藏相关控件;切换回其他 Tab 时恢复
*/
private fun updateDelControlsVisibility() {
val isSlot = currentTableIndex == 3
@@ -151,16 +141,12 @@ class DbInspectFragment : BaseFragment<FragmentDbInspectBinding>() {
} else {
binding.tvDelLabel.visible()
binding.swShowDel.visible()
// 恢复 Switch 显示状态(防止误触发监听器)
ignoreSwitch = true
binding.swShowDel.isChecked = showDel
ignoreSwitch = false
}
}
/**
* 更新所有 Tab 的文字颜色和加粗状态
*/
private fun updateTabStyles() {
tabButtons.forEachIndexed { index, btn ->
val selected = index == currentTableIndex
@@ -169,20 +155,12 @@ class DbInspectFragment : BaseFragment<FragmentDbInspectBinding>() {
}
}
/**
* 将下方指示条平移到选中 Tab 的正下方(150ms 动画)
*/
private fun moveIndicator(index: Int) {
val btn = tabButtons.getOrNull(index) ?: return
val tabWidth = btn.width.toFloat()
val indicatorWidth = tabWidth / 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 {
val lp = binding.vTabIndicator.layoutParams
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 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.addAll(list)
notifyDataSetChanged()
@@ -210,20 +188,57 @@ class DbInspectFragment : BaseFragment<FragmentDbInspectBinding>() {
}
override fun onBindViewHolder(holder: VH, position: Int) {
val (isDeleted, content) = data[position]
holder.tvContent.text = content
// isDel=1 的记录用灰色标识,正常记录用深色
holder.tvContent.setTextColor(
if (isDeleted) Color.parseColor("#BBBBBB") else Color.parseColor("#333333")
)
// 已删除记录卡片略微透明
holder.itemView.alpha = if (isDeleted) 0.6f else 1.0f
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)
}
override fun getItemCount() = data.size
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"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/cardRecord"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
@@ -9,13 +10,28 @@
app:cardElevation="1dp"
app:cardBackgroundColor="#FFFFFF">
<TextView
android:id="@+id/tvContent"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:textSize="11sp"
android:lineSpacingMultiplier="1.3"
android:fontFamily="monospace" />
android:orientation="horizontal">
<!-- 已删除标记条:isDel=1 时显示红色左边框 -->
<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>