feat(fragment): 新增 DbInspectFragment 本地数据库调试查看功能

- 新增 DbInspectFragment,支持按表切换(菜品/食材/调料/槽位)查看全量数据
- 分页加载(每页 50 条)+ isDel 过滤开关,避免大数据量时内存占用过高
- 切换 Tab 立即清空旧数据防止闪现,loadMoreDbInspect 加并发保护
- 槽位表字段少无 isDel,切换到该 Tab 时自动隐藏 isDel 相关控件
- 菜品/食材/调料表字段改为每行 3 列紧凑布局,槽位表保持逐行显示
- SettingActivity 新增"数据库查看"入口,复用 SingleFragmentActivity 承载
- 4 个 DAO 新增分页查询及计数方法,DbRepository/DbViewModel 同步扩展

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-07 13:44:03 +08:00
co-authored by Claude Sonnet 4.6
parent a64a00e099
commit c17402c3c2
12 changed files with 658 additions and 1 deletions
@@ -168,6 +168,60 @@ class DbRepository {
suspend fun deleteAllSlotsByDeviceId(deviceId: String) = withContext(Dispatchers.IO) {
db.seasoningSlotDao().deleteAllByDeviceId(deviceId)
}
// ---- 调试用:查询各表全量数据(含已删除记录)----
/** 查询 dm_cook_food 全部记录 */
suspend fun getAllCookFood() = withContext(Dispatchers.IO) {
db.cookFoodDao().getAllCookFood()
}
/** 查询 dm_cook_food_goods 全部记录 */
suspend fun getAllCookFoodGoods() = withContext(Dispatchers.IO) {
db.cookFoodGoodsDao().getAllCookFoodGoods()
}
/** 查询 dm_seasoning 全部记录 */
suspend fun getAllSeasoning() = withContext(Dispatchers.IO) {
db.seasoningDao().getAllSeasoning()
}
// ---- 调试用:分页查询(含 isDel 过滤)----
suspend fun getCookFoodPaged(showDel: Boolean, limit: Int, offset: Int) =
withContext(Dispatchers.IO) {
db.cookFoodDao().getCookFoodPaged(if (showDel) 1 else 0, limit, offset)
}
suspend fun countCookFoodAll(showDel: Boolean) = withContext(Dispatchers.IO) {
db.cookFoodDao().countCookFoodAll(if (showDel) 1 else 0)
}
suspend fun getCookFoodGoodsPaged(showDel: Boolean, limit: Int, offset: Int) =
withContext(Dispatchers.IO) {
db.cookFoodGoodsDao().getCookFoodGoodsPaged(if (showDel) 1 else 0, limit, offset)
}
suspend fun countCookFoodGoodsAll(showDel: Boolean) = withContext(Dispatchers.IO) {
db.cookFoodGoodsDao().countCookFoodGoodsAll(if (showDel) 1 else 0)
}
suspend fun getSeasoningPaged(showDel: Boolean, limit: Int, offset: Int) =
withContext(Dispatchers.IO) {
db.seasoningDao().getSeasoningPaged(if (showDel) 1 else 0, limit, offset)
}
suspend fun countSeasoningAll(showDel: Boolean) = withContext(Dispatchers.IO) {
db.seasoningDao().countSeasoningAll(if (showDel) 1 else 0)
}
suspend fun getSlotPaged(limit: Int, offset: Int) = withContext(Dispatchers.IO) {
db.seasoningSlotDao().getSlotPaged(limit, offset)
}
suspend fun countSlotAll() = withContext(Dispatchers.IO) {
db.seasoningSlotDao().countSlotAll()
}
}
//
//class SeasoningRepository(val seasoningDao: SeasoningDao) {
@@ -237,7 +237,139 @@ class DbViewModel : ViewModel() {
suspend fun deleteAllSlotsByDeviceId(deviceId: String) =
rep.deleteAllSlotsByDeviceId(deviceId)
// fun updateAll(list: MutableList<SeasoningEntity>) {
// ---- 数据库调试查看 ----
companion object {
/** 每页加载条数 */
const val DB_INSPECT_PAGE_SIZE = 50
}
/**
* 数据库调试页面 UI 状态
* @param items 当前已加载的记录列表,Pair.first=是否逻辑删除,Pair.second=格式化文本
* @param totalCount 当前过滤条件下的总记录数
* @param hasMore 是否还有更多数据可加载
* @param isLoading 是否正在加载(首次加载或加载更多)
*/
data class DbInspectUiState(
val items: List<Pair<Boolean, String>> = emptyList(),
val totalCount: Int = 0,
val hasMore: Boolean = false,
val isLoading: Boolean = false
)
private val _dbInspectState = MutableStateFlow(DbInspectUiState())
val dbInspectState: StateFlow<DbInspectUiState> = _dbInspectState.asStateFlow()
/**
* 加载指定表的第一页数据(切换 Tab / 切换 isDel 过滤时调用)
* @param tableIndex 0=菜品 1=食材 2=调料 3=槽位
* @param showDel true=包含已删除记录,false=只看有效记录
*/
fun loadDbInspect(tableIndex: Int, showDel: Boolean) {
viewModelScope.launch {
// 立即清空旧数据并显示加载状态,避免切换 Tab 时旧数据闪现
_dbInspectState.value = DbInspectUiState(isLoading = true)
val (items, total) = queryPage(tableIndex, showDel, offset = 0)
_dbInspectState.value = DbInspectUiState(
items = items,
totalCount = total,
hasMore = items.size < total,
isLoading = false
)
}
}
/**
* 追加加载下一页数据(点击"加载更多"时调用)
* @param tableIndex 0=菜品 1=食材 2=调料 3=槽位
* @param showDel true=包含已删除记录
*/
fun loadMoreDbInspect(tableIndex: Int, showDel: Boolean) {
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 merged = current.items + newItems
_dbInspectState.value = DbInspectUiState(
items = merged,
totalCount = total,
hasMore = merged.size < total,
isLoading = false
)
}
}
/**
* 执行分页查询,返回格式化后的列表和总记录数
*/
private suspend fun queryPage(
tableIndex: Int,
showDel: Boolean,
offset: Int
): Pair<List<Pair<Boolean, String>>, 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
)
}
rows to total
}
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
)
}
rows to total
}
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
)
}
rows to total
}
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}"
}
rows to total
}
else -> emptyList<Pair<Boolean, String>>() 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 {
// list.forEach {
// rep.updateSeasoning(it)
@@ -28,4 +28,16 @@ interface CookFoodDao {
// @Query("DELETE FROM dm_cook_food WHERE canteenId = :canteenId AND foodId = :foodId AND cookMode = :cookMode")
@Query("UPDATE dm_cook_food SET isDel = 1 WHERE isDel = 0 AND canteenId = :canteenId AND foodId = :foodId AND cookMode = :cookMode")
suspend fun deleteCookFood(canteenId: String, foodId: String, cookMode: Int)
/** 查询全部菜品记录(含已删除),用于数据库调试查看 */
@Query("SELECT * FROM dm_cook_food ORDER BY createTime DESC")
suspend fun getAllCookFood(): MutableList<CookFoodEntity>
/** 分页查询菜品记录,showDel=true 时包含已删除记录 */
@Query("SELECT * FROM dm_cook_food WHERE (:showDel = 1 OR isDel = 0) ORDER BY createTime DESC LIMIT :limit OFFSET :offset")
suspend fun getCookFoodPaged(showDel: Int, limit: Int, offset: Int): List<CookFoodEntity>
/** 统计菜品记录总数,showDel=true 时包含已删除记录 */
@Query("SELECT COUNT(*) FROM dm_cook_food WHERE (:showDel = 1 OR isDel = 0)")
suspend fun countCookFoodAll(showDel: Int): Int
}
@@ -28,4 +28,16 @@ interface CookFoodGoodsDao {
// @Query("DELETE FROM dm_cook_food_goods WHERE pid = :pid AND foodId = :foodId")
@Query("UPDATE dm_cook_food_goods SET isDel = 1 WHERE isDel = 0 AND pid = :pid AND foodId = :foodId")
suspend fun deleteCookFoodGoodsList(pid: Long, foodId: String)
/** 查询全部食材记录(含已删除),用于数据库调试查看 */
@Query("SELECT * FROM dm_cook_food_goods ORDER BY createTime DESC")
suspend fun getAllCookFoodGoods(): MutableList<CookFoodGoodsEntity>
/** 分页查询食材记录,showDel=true 时包含已删除记录 */
@Query("SELECT * FROM dm_cook_food_goods WHERE (:showDel = 1 OR isDel = 0) ORDER BY createTime DESC LIMIT :limit OFFSET :offset")
suspend fun getCookFoodGoodsPaged(showDel: Int, limit: Int, offset: Int): List<CookFoodGoodsEntity>
/** 统计食材记录总数,showDel=true 时包含已删除记录 */
@Query("SELECT COUNT(*) FROM dm_cook_food_goods WHERE (:showDel = 1 OR isDel = 0)")
suspend fun countCookFoodGoodsAll(showDel: Int): Int
}
@@ -41,4 +41,16 @@ interface SeasoningDao {
// @Query("DELETE FROM dm_seasoning WHERE isDel = 0")
@Query("UPDATE dm_seasoning SET isDel = 1 WHERE isDel = 0")
suspend fun clearAllSeasoning(): Int
/** 查询全部调料记录(含已删除),用于数据库调试查看 */
@Query("SELECT * FROM dm_seasoning ORDER BY sort ASC")
suspend fun getAllSeasoning(): MutableList<SeasoningEntity>
/** 分页查询调料记录,showDel=true 时包含已删除记录 */
@Query("SELECT * FROM dm_seasoning WHERE (:showDel = 1 OR isDel = 0) ORDER BY sort ASC LIMIT :limit OFFSET :offset")
suspend fun getSeasoningPaged(showDel: Int, limit: Int, offset: Int): List<SeasoningEntity>
/** 统计调料记录总数,showDel=true 时包含已删除记录 */
@Query("SELECT COUNT(*) FROM dm_seasoning WHERE (:showDel = 1 OR isDel = 0)")
suspend fun countSeasoningAll(showDel: Int): Int
}
@@ -38,6 +38,14 @@ interface SeasoningSlotDao {
@Query("SELECT * FROM dm_seasoning_slot")
suspend fun queryAll(): List<SeasoningSlotEntity>
/** 分页查询槽位记录(槽位表无 isDel,showDel 参数忽略) */
@Query("SELECT * FROM dm_seasoning_slot ORDER BY deviceId ASC, address ASC LIMIT :limit OFFSET :offset")
suspend fun getSlotPaged(limit: Int, offset: Int): List<SeasoningSlotEntity>
/** 统计槽位记录总数 */
@Query("SELECT COUNT(*) FROM dm_seasoning_slot")
suspend fun countSlotAll(): Int
/**
* 删除指定设备的指定槽位配置
* @param deviceId 目标设备 ID
@@ -76,6 +76,9 @@ class SettingActivity : BaseActivity() {
binding.llFoodCollect.clickWithDebounce {
checkCameraPermission()
}
binding.llDbInspect.clickWithDebounce {
SingleFragmentActivity.start(this, SingleFragmentActivity.PageType.DB_INSPECT)
}
}
private fun openCollectPage() {
@@ -5,6 +5,7 @@ import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.ui.fragment.DbInspectFragment
import com.shuwei.dish.match.ui.fragment.VectorCollectionFragment
import com.shuwei.dish.match.ui.fragment.DeviceConfigFragment
import com.shuwei.dish.match.ui.fragment.SeasoningConfigFragment
@@ -35,6 +36,9 @@ class SingleFragmentActivity : BaseActivity() {
/** 调料配置页 */
SEASONING_CONFIG,
/** 数据库调试查看页 */
DB_INSPECT,
}
companion object {
@@ -96,6 +100,7 @@ class SingleFragmentActivity : BaseActivity() {
PageType.FOOD_COLLECT -> "食材采集"
PageType.COOK_MODE -> "菜品模式"
PageType.SEASONING_CONFIG -> "调料区设置"
PageType.DB_INSPECT -> "数据库查看"
}
/**
@@ -105,6 +110,7 @@ class SingleFragmentActivity : BaseActivity() {
PageType.FOOD_COLLECT -> VectorCollectionFragment()
PageType.COOK_MODE -> DeviceConfigFragment()
PageType.SEASONING_CONFIG -> SeasoningConfigFragment()
PageType.DB_INSPECT -> DbInspectFragment()
}
override fun onResume() {
@@ -0,0 +1,229 @@
package com.shuwei.dish.match.ui.fragment
import android.graphics.Color
import android.graphics.Typeface
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.RadioButton
import android.widget.TextView
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.shuwei.dish.match.R
import com.shuwei.dish.match.base.BaseFragment
import com.shuwei.dish.match.databinding.FragmentDbInspectBinding
import com.shuwei.dish.match.db.DbViewModel
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
/**
* 数据库调试查看页面
* 支持按表切换、isDel 过滤开关、分页加载(每页 50 条)
* 已逻辑删除(isDel=1)的记录以灰色文字 + 半透明卡片区分
* 槽位表无 isDel 字段,切换到该 Tab 时自动隐藏 isDel 控件
*/
class DbInspectFragment : BaseFragment<FragmentDbInspectBinding>() {
private lateinit var currentActivity: SingleFragmentActivity
/** 当前选中的表索引:0=菜品 1=食材 2=调料 3=槽位 */
private var currentTableIndex = 0
/** 是否显示已删除记录(槽位表忽略此参数) */
private var showDel = true
/**
* 防止在代码中设置 Switch 状态时触发监听器,
* 例如切换回非槽位 Tab 时恢复 Switch 状态
*/
private var ignoreSwitch = false
private val adapter = DbRecordAdapter()
/** Tab RadioButton 列表,用于统一更新选中样式 */
private val tabButtons = mutableListOf<RadioButton>()
override fun inflateBinding(
inflater: LayoutInflater,
container: ViewGroup?
): FragmentDbInspectBinding = FragmentDbInspectBinding.inflate(inflater, container, false)
override fun initialize() {
currentActivity = requireActivity() as SingleFragmentActivity
binding.rvRecords.layoutManager = LinearLayoutManager(requireContext())
binding.rvRecords.adapter = adapter
tabButtons.addAll(
listOf(binding.rbCookFood, binding.rbGoods, binding.rbSeasoning, binding.rbSlot)
)
// 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
}
updateTabStyles()
moveIndicator(currentTableIndex)
updateDelControlsVisibility()
loadFirstPage()
}
// isDel 开关:切换后重新加载第一页
binding.swShowDel.setOnCheckedChangeListener { _, isChecked ->
if (ignoreSwitch) return@setOnCheckedChangeListener
showDel = isChecked
loadFirstPage()
}
// 刷新按钮:重新加载第一页
binding.tvRefresh.setOnClickListener { loadFirstPage() }
// 加载更多按钮
binding.tvLoadMore.setOnClickListener {
currentActivity.appViewModel.loadMoreDbInspect(currentTableIndex, showDel)
}
// 订阅 UI 状态
viewLifecycleOwner.lifecycleScope.launch {
currentActivity.appViewModel.dbInspectState.collect { state ->
renderState(state)
}
}
// 初始化 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()
binding.tvLoadMore.text = "加载中..."
binding.tvLoadMore.isClickable = false
}
state.hasMore -> {
binding.tvLoadMore.visible()
binding.tvLoadMore.text = "加载更多"
binding.tvLoadMore.isClickable = true
}
else -> binding.tvLoadMore.gone()
}
adapter.setData(state.items)
}
/**
* 槽位表(index=3)没有 isDel 字段,切换到该 Tab 时隐藏 isDel 相关控件;
* 切换回其他 Tab 时恢复,并同步 Switch 状态
*/
private fun updateDelControlsVisibility() {
val isSlot = currentTableIndex == 3
if (isSlot) {
binding.tvDelLabel.gone()
binding.swShowDel.gone()
} 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
btn.setTextColor(if (selected) Color.parseColor("#5C77F7") else Color.parseColor("#888888"))
btn.setTypeface(null, if (selected) Typeface.BOLD else Typeface.NORMAL)
}
}
/**
* 将下方指示条平移到选中 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.post {
val lp = binding.vTabIndicator.layoutParams
lp.width = indicatorWidth.toInt()
binding.vTabIndicator.layoutParams = lp
}
}
// ---- 内部 Adapter ----
/** 数据库记录列表适配器 */
private inner class DbRecordAdapter : RecyclerView.Adapter<DbRecordAdapter.VH>() {
private val data = mutableListOf<Pair<Boolean, String>>()
fun setData(list: List<Pair<Boolean, String>>) {
data.clear()
data.addAll(list)
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.list_item_db_record, parent, false)
return VH(view)
}
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
}
override fun getItemCount() = data.size
inner class VH(itemView: View) : RecyclerView.ViewHolder(itemView) {
val tvContent: TextView = itemView.findViewById(R.id.tvContent)
}
}
}
@@ -109,6 +109,37 @@
tools:ignore="ContentDescription" />
</LinearLayout>
<LinearLayout
android:id="@+id/llDbInspect"
android:layout_width="match_parent"
android:layout_height="120dp"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginHorizontal="32dp"
android:layout_marginTop="50dp"
android:background="@drawable/shape_white_dc_15_corners"
android:foreground="?android:attr/selectableItemBackground"
android:clipToOutline="true">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="数据库查看"
android:textSize="32sp"
android:textStyle="bold"
android:layout_marginStart="32dp"
android:textColor="@color/black333" />
<ImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginEnd="20dp"
android:adjustViewBounds="true"
android:src="@drawable/ic_arrow_right3"
tools:ignore="ContentDescription" />
</LinearLayout>
<!-- &lt;!&ndash; 底部导航菜单 &ndash;&gt;-->
<!-- <LinearLayout-->
<!-- android:id="@+id/bottomMenu"-->
@@ -0,0 +1,137 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#F5F5F5">
<!-- Tab 切换栏 -->
<RadioGroup
android:id="@+id/rgTabs"
android:layout_width="match_parent"
android:layout_height="48dp"
android:orientation="horizontal"
android:background="#FFFFFF">
<RadioButton
android:id="@+id/rbCookFood"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:button="@null"
android:gravity="center"
android:text="菜品"
android:textSize="14sp"
android:checked="true" />
<RadioButton
android:id="@+id/rbGoods"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:button="@null"
android:gravity="center"
android:text="食材"
android:textSize="14sp" />
<RadioButton
android:id="@+id/rbSeasoning"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:button="@null"
android:gravity="center"
android:text="调料"
android:textSize="14sp" />
<RadioButton
android:id="@+id/rbSlot"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:button="@null"
android:gravity="center"
android:text="槽位"
android:textSize="14sp" />
</RadioGroup>
<!-- Tab 下方指示条 -->
<View
android:id="@+id/vTabIndicator"
android:layout_width="0dp"
android:layout_height="3dp"
android:background="#5C77F7" />
<!-- 工具栏:记录数 + isDel 开关 + 刷新 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingStart="12dp"
android:paddingEnd="4dp"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:background="#FFFFFF">
<TextView
android:id="@+id/tvCount"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="共 0 条"
android:textSize="12sp"
android:textColor="#888888" />
<!-- 显示已删除数据开关 -->
<TextView
android:id="@+id/tvDelLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="显示已删除"
android:textSize="12sp"
android:textColor="#888888"
android:paddingStart="8dp" />
<Switch
android:id="@+id/swShowDel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:paddingEnd="4dp" />
<TextView
android:id="@+id/tvRefresh"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingHorizontal="12dp"
android:paddingVertical="6dp"
android:text="刷新"
android:textSize="12sp"
android:textColor="#5C77F7" />
</LinearLayout>
<!-- 记录列表 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvRecords"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:paddingBottom="8dp"
android:clipToPadding="false" />
<!-- 加载更多按钮 -->
<TextView
android:id="@+id/tvLoadMore"
android:layout_width="match_parent"
android:layout_height="44dp"
android:gravity="center"
android:text="加载更多"
android:textSize="13sp"
android:textColor="#5C77F7"
android:background="#FFFFFF"
android:visibility="gone" />
</LinearLayout>
@@ -0,0 +1,21 @@
<?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:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
app:cardCornerRadius="6dp"
app:cardElevation="1dp"
app:cardBackgroundColor="#FFFFFF">
<TextView
android:id="@+id/tvContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:textSize="11sp"
android:lineSpacingMultiplier="1.3"
android:fontFamily="monospace" />
</androidx.cardview.widget.CardView>