夜雨聆风学习资料网

ARTICLE · 1227076

保姆级学习开发安卓手机软件实战(十二)--主界面搭建(更换主题)

保姆级学习开发安卓手机软件实战(十二)--主界面搭建(更换主题)

因为基本通信搭建好了,然后也是我们终于讨论出所有大致功能,我让AI设计了几版UI,最后选定了这款灰橙色作为主题色,主界面效果如下: 平板效果: 手机效果:

(我这个实战步骤可能比较曲折。。。实在是公司开发软件的流程是走一步看一步,随时用不惯再改,然后又突如其来的各种想法,没办法有需求文档后稳步开发,这是不太好的地方,但是也算挑战吧。。。)

大家应该还记得,首先是主题色那几个文件的替换了:

步骤 1:整个替换 ui/theme/Color.kt

package com.example.jstz1.ui.themeimport androidx.compose.ui.graphics.Color/** * 风格B「工业石墨橙」色板 * * 1:1 对应《ui/主界面-风格B-工业石墨橙.html》第 7~19 行的 :root 变量, * 命名沿用 CSS 变量名(HTML 里 --accent,这里就是 Accent), * 以后想改色直接对着效果图找,不用翻译。 */// ========== 背景层 ==========val Bg     = Color(0xFF0E1114)   // --bg        最底层val Stage  = Color(0xFF14181C)   // --stage     内容舞台val PanelA = Color(0xFF1D2228)   // --panel-a   面板渐变·上val PanelB = Color(0xFF181C21)   // --panel-b   面板渐变·下val CardA  = Color(0xFF232930)   // --card-a    卡片渐变·上val CardB  = Color(0xFF1D2228)   // --card-b    卡片渐变·下// ========== 描边 ==========val PBorder  = Color(0xFF333B44) // --pborder   面板描边val PBorder2 = Color(0xFF47525F) // --pborder2  卡片描边(更亮一档)// ========== 文字(三级)==========val Text = Color(0xFFE9EDF2)     // --text      主文字val Sub  = Color(0xFF9AA6B4)     // --sub       次要文字val Dim  = Color(0xFF64707D)     // --dim       弱化文字// ========== 主色:石墨橙 ==========val Accent   = Color(0xFFFF8A1E) // --accent    按钮/选中/装饰线val AccentBg = Color(0xFF3A2A18) // --accent-bg 橙色淡底(选中态)val OnAccent = Color(0xFF231303) // --on-accent 橙底上的字(深棕近黑)// ========== 状态三色 ==========val Ok     = Color(0xFF57B87E)   // --ok    在线 / 运行val OkDark = Color(0xFF0B1512)   // --ok-dark(仅单设备界面用到)val Err    = Color(0xFFE85D5D)   // --err   异常 / 停止val Warn   = Color(0xFFE8B33B)   // --warn  报警// ========== 按钮 ==========val Btn  = Color(0xFF232930)     // --btn   普通按钮底val BtnT = Color(0xFFD7DEE6)     // --btn-t 按钮文字// ========== 群控专用(第 5.3 节用)==========val RunBg  = Color(0xFF1E5E46)   // --run-bg  运行按钮底val RunBd  = Color(0xFF57B87E)   // --run-bd  运行按钮描边val StopBg = Color(0xFF6E2A2A)   // --stop-bg 停止按钮底val StopBd = Color(0xFFE85D5D)   // --stop-bd 停止按钮描边// ========== 顶栏 / 底栏 ==========val TopA = Color(0xFF22272D)     // --top-a  顶栏渐变·上val TopB = Color(0xFF191D22)     // --top-b  顶栏渐变·下val Bar  = Color(0xFF191D22)     // --bar    底栏

步骤 2:整个替换 ui/theme/Theme.kt

package com.example.jstz1.ui.themeimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.darkColorSchemeimport androidx.compose.runtime.Composable/** * 把「石墨橙设计变量」填进 Material3 的语义槽。 * * 设计稿给的是「这个色叫什么」(accent / panel-a / ok) * Material3 要的是「这个色用在哪」(primary / surface / tertiary) * 这里映射一次,之后 UI 代码只写 MaterialTheme.colorScheme.xxx, * 不直接碰色值——想换主题时改这一处即可。 */private val JstzColorScheme = darkColorScheme(    // 主色 = 石墨橙    primary            = Accent,    onPrimary          = OnAccent,    primaryContainer   = AccentBg,    onPrimaryContainer = Accent,    // 次要 = 普通按钮    secondary          = Btn,    onSecondary        = BtnT,    // 背景 / 面板 / 卡片    background         = Bg,    onBackground       = Text,    surface            = PanelA,    onSurface          = Text,    surfaceVariant     = CardA,    onSurfaceVariant   = Sub,    // 描边    outline            = PBorder,    outlineVariant     = PBorder2,    // 第三色 = 运行/成功(卡片圆点用)    tertiary           = Ok,    onTertiary         = OkDark,    // 错误    error              = Err,    onError            = OnAccent,    errorContainer     = StopBg,    onErrorContainer   = Err,)/** * 工业 HMI 用固定深色主题: * 不跟随系统深色/浅色,也不吃 Android 12+ 的动态取色 * (取色会把你的橙色换成用户壁纸色,控制屏不能这么干)。 */@ComposablefunJSTZ1Theme(content: @Composable () -> Unit) {    MaterialTheme(        colorScheme = JstzColorScheme,        typography = Typography,        content = content    )}

步骤 3:主界面布局

整个替换 ui/screen/DeviceListScreen.kt

package com.example.jstz1.ui.screenimport androidx.compose.foundation.backgroundimport androidx.compose.foundation.borderimport androidx.compose.foundation.clickableimport androidx.compose.foundation.rememberScrollStateimport androidx.compose.foundation.verticalScrollimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Boximport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.Spacerimport androidx.compose.foundation.layout.fillMaxHeightimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.defaultMinSizeimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.heightimport androidx.compose.foundation.layout.paddingimport androidx.compose.foundation.layout.sizeimport androidx.compose.foundation.layout.widthimport androidx.compose.foundation.lazy.grid.GridCellsimport androidx.compose.foundation.lazy.grid.LazyVerticalGridimport androidx.compose.foundation.lazy.grid.itemsimport androidx.compose.foundation.shape.CircleShapeimport androidx.compose.foundation.shape.RoundedCornerShapeimport androidx.compose.material3.Cardimport androidx.compose.material3.CardDefaultsimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.Surfaceimport androidx.compose.material3.Textimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableIntStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.setValueimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.draw.alphaimport androidx.compose.ui.graphics.Brushimport androidx.compose.ui.graphics.Colorimport androidx.compose.ui.text.font.FontWeightimport androidx.compose.ui.unit.dpimport androidx.compose.ui.unit.spimport androidx.lifecycle.compose.collectAsStateWithLifecycleimport androidx.lifecycle.viewmodel.compose.viewModelimport com.example.jstz1.data.model.Deviceimport com.example.jstz1.data.model.DeviceState// 这几个是「Material3 没有对应语义槽」的设计变量,只能直接用:// 运行/停止按钮的底色与描边、普通按钮、状态三色、底栏import com.example.jstz1.ui.theme.Barimport com.example.jstz1.ui.theme.Btnimport com.example.jstz1.ui.theme.Dimimport com.example.jstz1.ui.theme.Errimport com.example.jstz1.ui.theme.Okimport com.example.jstz1.ui.theme.PBorder2import com.example.jstz1.ui.theme.RunBdimport com.example.jstz1.ui.theme.RunBgimport com.example.jstz1.ui.theme.StopBdimport com.example.jstz1.ui.theme.StopBgimport com.example.jstz1.ui.theme.TopAimport com.example.jstz1.ui.theme.TopBimport com.example.jstz1.ui.theme.Warn// 卡片圆角 / 面板圆角,对齐效果图的 --radius:4pxprivate val SHAPE = RoundedCornerShape(4.dp)// 群控按钮(对齐效果图 .b 的 min-height:44px)private val CTRL_BTN_HEIGHT = 36.dp/** * 主界面 * * 布局:主体(左 60% 设备列表 + 右 40% 群控)+ 底部统计栏 * 顶栏(用户/角色/退出)放到第 10 节做权限体系时一起加—— * 现在加了也是写死的假数据。 */@ComposablefunDeviceListScreen(    modifier: Modifier = Modifier,    vm: DeviceListViewModel = viewModel()) {    val state by vm.uiState.collectAsStateWithLifecycle()    /**     * 底栏统计「现算」而不是「存字段」:     * devices / devicesStates 一变,这三行自动跟着重算。     *  Kotlin 标准库的 count { } 函数,它接受一个谓词(条件表达式),返回满足条件的元素个数     */    val onlineCnt  = state.devices.count { state.devicesStates.containsKey(it.devId) }    val enabledCnt = state.devices.count { it.enable }    val alarmCnt   = state.devicesStates.values.count { it.alarm.hasAlarm }    Surface(        modifier = modifier.fillMaxSize(),        color = MaterialTheme.colorScheme.background    ) {        Column(Modifier.fillMaxSize()) {            // ===== 顶栏 =====            TopBar()            // ===== 主体:左 60% / 右 40% =====            Row(                modifier = Modifier                    .fillMaxWidth()                    .weight(1f)                 // 吃掉顶栏以下、底栏以上的全部高度                    .padding(12.dp),                horizontalArrangement = Arrangement.spacedBy(12.dp)            ) {                DeviceListPanel(                    devices     = state.devices,                    states      = state.devicesStates,                    onlineCount = onlineCnt,                    onToggle    = { vm.toggleEnabled(it) },                    onReset     = { vm.resetFactory(it) },                    modifier    = Modifier.weight(0.6f).fillMaxHeight()                )                GroupControlPanel(                    targetCount = onlineCnt,                    modifier    = Modifier.weight(0.4f).fillMaxHeight()                )            }            // ===== 底栏 =====            BottomBar(                total   = state.devices.size,                online  = onlineCnt,                enabled = enabledCnt,                alarm   = alarmCnt            )        }    }}// ============ 顶栏 ============/** * 顶栏(对应 .top) * * 注意:admin / 管理员 / 退出登录目前是写死占位, * 第 10 节做登录与权限体系时换成真实登录用户。 */@Composableprivate funTopBar() {    Column(        Modifier            .fillMaxWidth()            .background(Brush.verticalGradient(listOf(TopA, TopB)))    ) {        Row(            Modifier                .fillMaxWidth()                .height(52.dp)                .padding(horizontal = 20.dp),            verticalAlignment = Alignment.CenterVertically        ) {            // logo            Box(                Modifier                    .size(34.dp)                    .background(MaterialTheme.colorScheme.surfaceVariant, RoundedCornerShape(6.dp))                    .border(1.dp, MaterialTheme.colorScheme.primary, RoundedCornerShape(6.dp)),                contentAlignment = Alignment.Center            ) {                Text(                    "J",                    style      = MaterialTheme.typography.titleMedium,                    fontWeight = FontWeight.Bold,                    color      = MaterialTheme.colorScheme.primary                )            }            Spacer(Modifier.weight(1f))            Text(                "控制系统",                style         = MaterialTheme.typography.titleLarge,                color         = MaterialTheme.colorScheme.onBackground,                letterSpacing = 4.sp            )            Spacer(Modifier.weight(1f))            // 用户 + 角色            Text(                "admin",                style = MaterialTheme.typography.labelMedium,                color = MaterialTheme.colorScheme.onSurfaceVariant            )            Spacer(Modifier.width(6.dp))            Box(                Modifier                    .background(MaterialTheme.colorScheme.primary, RoundedCornerShape(9.dp))                    .padding(horizontal = 8.dp, vertical = 2.dp)            ) {                Text(                    "用户管理",                    style      = MaterialTheme.typography.labelSmall,                    fontWeight = FontWeight.Bold,                    color      = MaterialTheme.colorScheme.onPrimary                )            }            Spacer(Modifier.width(14.dp))            // 退出登录            Text(                "退出登录",                style    = MaterialTheme.typography.labelMedium,                color    = MaterialTheme.colorScheme.onSurfaceVariant,                modifier = Modifier                    .border(1.dp, MaterialTheme.colorScheme.outlineVariant, RoundedCornerShape(6.dp))                    .padding(horizontal = 8.dp, vertical = 4.dp)            )        }        // 底部橙色装饰线        Box(            Modifier                .fillMaxWidth()                .height(2.dp)                .background(                    Brush.horizontalGradient(                        0.0f to Color.Transparent,                        0.2f to MaterialTheme.colorScheme.primary,                        0.8f to MaterialTheme.colorScheme.primary,                        1.0f to Color.Transparent                    )                )        )    }}// ============ 左 60%:设备列表 ============@Composableprivate funDeviceListPanel(    devices: List<Device>,    states: Map<String, DeviceState>,    onlineCount: Int,    onToggle: (String) -> Unit,    onReset: (String) -> Unit,    modifier: Modifier = Modifier) {    Column(        modifier            .background(MaterialTheme.colorScheme.surface, SHAPE)            .border(1.dp, MaterialTheme.colorScheme.outline, SHAPE)            .padding(14.dp)    ) {        // ---- 标题行 ----        Row(verticalAlignment = Alignment.CenterVertically) {            Text(                "设备列表",                style = MaterialTheme.typography.titleMedium,                color = MaterialTheme.colorScheme.onBackground            )            Spacer(Modifier.weight(1f))            Box(                Modifier                    .background(MaterialTheme.colorScheme.primary, RoundedCornerShape(6.dp))                    .padding(horizontal = 12.dp, vertical = 5.dp)            ) {                Text(                    "管理设备",                    style = MaterialTheme.typography.labelMedium,                    color = MaterialTheme.colorScheme.onPrimary   // 橙底上的深棕字                )            }        }        Spacer(Modifier.height(10.dp))        // ---- 卡片网格(2 列)----        LazyVerticalGrid(            columns               = GridCells.Fixed(2),            modifier              = Modifier.fillMaxWidth().weight(1f),            verticalArrangement   = Arrangement.spacedBy(12.dp),            horizontalArrangement = Arrangement.spacedBy(12.dp)        ) {            items(items = devices, key = { it.devId }) { device ->                DeviceCard(                    device   = device,                    state    = states[device.devId],                    onToggle = { onToggle(device.devId) },                    onReset  = { onReset(device.devId) }                )            }        }    }}// ============ 右 40%:控制 ============@Composableprivate funGroupControlPanel(    targetCount: Int,    modifier: Modifier = Modifier) {    // 工作模式:界面 0/1,下发时 +1 转成协议值(1=酸解烃,2=溶解烃)    var workMode by remember { mutableIntStateOf(0) }    // 运行模式:直接存协议值 0~4,-1 表示还没选    var runMode by remember { mutableIntStateOf(-1) }    val canSend = runMode >= 0 && targetCount > 0    Column(        modifier            .background(MaterialTheme.colorScheme.surface, SHAPE)            .border(1.dp, MaterialTheme.colorScheme.outline, SHAPE)            // 窗口小于 1280×800 时内容放不下,允许滚动,避免被裁掉            .verticalScroll(rememberScrollState())            .padding(14.dp),        verticalArrangement = Arrangement.spacedBy(10.dp)    ) {        Block( ) {            Text(                "工作模式:",                style = MaterialTheme.typography.labelSmall,                color = MaterialTheme.colorScheme.onSurfaceVariant            )            Spacer(Modifier.height(6.dp))            SegmentedControl(                items         = listOf("酸解烃", "溶解烃"),                selectedIndex = workMode,                onSelect      = { workMode = it }            )            Spacer(Modifier.height(10.dp))            Text(                "运行模式:",                style = MaterialTheme.typography.labelSmall,                color = MaterialTheme.colorScheme.onSurfaceVariant            )            Spacer(Modifier.height(6.dp))            Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {                SelectableButton(                    label    = "运行",                    selected = runMode == 1,          // 1 = 自动                    onBg     = RunBg,                    onBorder = RunBd,                    onText   = MaterialTheme.colorScheme.onBackground,                    modifier = Modifier.weight(1f)                ) { runMode = 1 }                SelectableButton(                    label    = "停止",                    selected = runMode == 0,                    onBg     = StopBg,                    onBorder = StopBd,                    onText   = MaterialTheme.colorScheme.onBackground,                    modifier = Modifier.weight(1f)                ) { runMode = 0 }            }            Spacer(Modifier.height(8.dp))            Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {                SelectableButton(                    label    = "排液",                    selected = runMode == 2,                    onBg     = MaterialTheme.colorScheme.primaryContainer,                    onBorder = MaterialTheme.colorScheme.primary,                    onText   = MaterialTheme.colorScheme.primary,                    modifier = Modifier.weight(1f)                ) { runMode = 2 }                SelectableButton(                    label    = "抽真空",                    selected = runMode == 3,                    onBg     = MaterialTheme.colorScheme.primaryContainer,                    onBorder = MaterialTheme.colorScheme.primary,                    onText   = MaterialTheme.colorScheme.primary,                    modifier = Modifier.weight(1f)                ) { runMode = 3 }            }            Spacer(Modifier.height(8.dp))            SelectableButton(                label    = "清洗",                selected = runMode == 4,                onBg     = MaterialTheme.colorScheme.primaryContainer,                onBorder = Warn,                onText   = MaterialTheme.colorScheme.primary,            ) { runMode = 4 }            Spacer(Modifier.height(10.dp))            if(runMode == 4){                Text(                    "清洗前请确认碱液瓶、酸液瓶、饱和盐水瓶已装入清水",                    style = MaterialTheme.typography.labelSmall,                    color = Warn                )            }            Spacer(Modifier.height(4.dp))            Text(                if (targetCount > 0) "将向 total", MaterialTheme.colorScheme.onBackground)        Sep()        Stat("在线",   "enabled", MaterialTheme.colorScheme.onBackground)        Sep()        Stat("报警",   "label ",            style = MaterialTheme.typography.labelSmall,            color = MaterialTheme.colorScheme.onSurfaceVariant        )        Text(            value,            style = MaterialTheme.typography.labelSmall,            color = valueColor,            fontWeight = FontWeight.Bold        )    }}@Composableprivate funSep() {    Spacer(Modifier.width(10.dp))    Text("|", style = MaterialTheme.typography.labelSmall, color = PBorder2)    Spacer(Modifier.width(10.dp))}// ==================== 通用小件 ====================/** 群控面板里的一个分区(对应 .block)*/@Composableprivate funBlock(    title: String ="",    modifier: Modifier = Modifier,    content: @Composable () -> Unit) {    Column(        modifier            .background(MaterialTheme.colorScheme.surfaceVariant, SHAPE)            .border(1.dp, MaterialTheme.colorScheme.outline, SHAPE)            .padding(12.dp)    ) {        if(title!=""){            Text(                title,                style = MaterialTheme.typography.labelMedium,                color = MaterialTheme.colorScheme.onSurfaceVariant            )            Spacer(Modifier.height(9.dp))        }        content()    }}/** 二选一分段控件(对应 .seg)*/@Composableprivate funSegmentedControl(    items: List<String>,    selectedIndex: Int,    onSelect: (Int) -> Unit) {    Row(        Modifier            .height(CTRL_BTN_HEIGHT)            .border(1.dp, MaterialTheme.colorScheme.outlineVariant, RoundedCornerShape(7.dp))    ) {        items.forEachIndexed { i, label ->            val selected = i == selectedIndex            Box(                Modifier                    .weight(1f)                    .fillMaxHeight()                    .background(                        if (selected) MaterialTheme.colorScheme.primaryContainer                        else Color.Transparent                    )                    .clickable { onSelect(i) },                contentAlignment = Alignment.Center            ) {                Text(                    label,                    style = MaterialTheme.typography.bodyMedium,                    color = if (selected) MaterialTheme.colorScheme.onBackground                    else MaterialTheme.colorScheme.onSurfaceVariant                )            }        }    }}/** * 可选中的模式按钮(对应效果图 .b / .b.run / .b.stop / .b.warn) * * 选中 → 用该模式自己的配色(运行绿 / 停止红 / 橙色系) * 未选 → 灰底灰边,一眼看出"还没选" * * 运行/停止的配色 Material3 没有对应语义槽(primary 是橙、tertiary 是绿), * 所以直接吃设计变量。 */@Composableprivate funSelectableButton(    label: String,    selected: Boolean,    onBg: Color,    onBorder: Color,    onText: Color,    offText: Color? = null,    modifier: Modifier = Modifier,    onClick: () -> Unit) {    Box(        modifier            .fillMaxWidth()            .defaultMinSize(minHeight = CTRL_BTN_HEIGHT)            .background(if (selected) onBg else Btn, RoundedCornerShape(6.dp))            .border(1.dp, if (selected) onBorder else PBorder2, RoundedCornerShape(6.dp))            .clickable(onClick = onClick),        contentAlignment = Alignment.Center    ) {        Text(            label,            style = MaterialTheme.typography.bodyMedium,            color = when {                selected        -> onText                offText != null -> offText                else            -> MaterialTheme.colorScheme.onSurfaceVariant            }        )    }}/** 主操作按钮(确定下发):不可用时自动置灰并点不动 */@Composableprivate funPrimaryAction(label: String, enabled: Boolean, onClick: () -> Unit) {    val container = if (enabled) MaterialTheme.colorScheme.primary else Btn    val border    = if (enabled) MaterialTheme.colorScheme.primary else PBorder2    val content   = if (enabled) MaterialTheme.colorScheme.onPrimary                    else MaterialTheme.colorScheme.onSurfaceVariant    Box(        Modifier            .fillMaxWidth()            .height(CTRL_BTN_HEIGHT)            .background(container, RoundedCornerShape(7.dp))            .border(1.dp, border, RoundedCornerShape(7.dp))            .clickable(enabled = enabled, onClick = onClick),        contentAlignment = Alignment.Center    ) {        Text(            label,            style      = MaterialTheme.typography.titleSmall,            fontWeight = FontWeight.Bold,            color      = content        )    }}/** 快捷入口(对应效果图 .q)—— 第 9/10/11 节接真实页面 */@Composableprivate funQuickItem(title: String, subtitle: String, onClick: () -> Unit = {}) {    Row(        Modifier            .fillMaxWidth()            .background(MaterialTheme.colorScheme.surfaceVariant, SHAPE)            .border(1.dp, MaterialTheme.colorScheme.outline, SHAPE)            .clickable(onClick = onClick)            .padding(horizontal = 12.dp, vertical = 8.dp),        verticalAlignment = Alignment.CenterVertically    ) {        // 图标位先用首字占位,后面换成矢量图标        Box(            Modifier                .size(30.dp)                .background(MaterialTheme.colorScheme.primaryContainer, RoundedCornerShape(7.dp))                .border(1.dp, MaterialTheme.colorScheme.outlineVariant, RoundedCornerShape(7.dp)),            contentAlignment = Alignment.Center        ) {            Text(                title.take(1),                style = MaterialTheme.typography.labelMedium,                color = MaterialTheme.colorScheme.primary            )        }        Spacer(Modifier.width(10.dp))        Column {            Text(                title,                style = MaterialTheme.typography.bodyMedium,                color = MaterialTheme.colorScheme.onSurface            )            Text(                subtitle,                style = MaterialTheme.typography.labelSmall,                color = Dim            )        }    }}/** * 紧凑开关(对应效果图 .switch:34×18 轨道 + 14dp 圆钮) * * 为什么不用 Material3 的 Switch: * 它最小 52×32dp,两张卡片并排时把「恢复出厂」挤出卡片外。 */@Composableprivate funMiniSwitch(checked: Boolean, onToggle: () -> Unit) {    Box(        Modifier            .size(width = 34.dp, height = 18.dp)            .background(                if (checked) MaterialTheme.colorScheme.primaryContainer                else MaterialTheme.colorScheme.outline,                RoundedCornerShape(9.dp)            )            .border(                1.dp,                if (checked) MaterialTheme.colorScheme.primary else Color.Transparent,                RoundedCornerShape(9.dp)            )            .clickable(onClick = onToggle),        contentAlignment = if (checked) Alignment.CenterEnd else Alignment.CenterStart    ) {        Box(            Modifier                .padding(horizontal = 2.dp)                .size(14.dp)                .background(                    if (checked) MaterialTheme.colorScheme.primary                    else MaterialTheme.colorScheme.onSurfaceVariant,                    CircleShape                )        )    }}// ==================== 设备卡片 ====================/** * 一张设备卡片(对应 .card) * * 在线判据:states 里有没有它 = 有没有收到过 0xB1 上报。 * 第 6 节加 5 秒看门狗后,这个判据会换成「5 秒内有没有收到过」。 */@Composableprivate funDeviceCard(    device: Device,    state: DeviceState?,    onToggle: () -> Unit,    onReset: () -> Unit) {    val online   = state != null    val hasAlarm = state?.alarm?.hasAlarm == true    // 状态 → 圆点颜色 + 文字,三态(离线 / 在线 / 在线·报警)    val dotColor = when {        !online   -> MaterialTheme.colorScheme.onSurfaceVariant        hasAlarm  -> MaterialTheme.colorScheme.error        else      -> Ok    }    val statusText = when {        !online   -> "离线"        hasAlarm  -> "在线 · 报警"        else      -> "在线"    }    Card(        modifier = Modifier            .fillMaxWidth()            .alpha(if (device.enable) 1f else 0.6f)      // 禁用 → 整卡半透明            .border(                1.dp,                if (hasAlarm) MaterialTheme.colorScheme.error                else MaterialTheme.colorScheme.outlineVariant,                SHAPE            ),        colors = CardDefaults.cardColors(            containerColor = MaterialTheme.colorScheme.surfaceVariant        )    ) {        Column(Modifier.fillMaxWidth().padding(12.dp)) {            // ---- 顶:编号 + 状态 ----            Row(verticalAlignment = Alignment.Top) {                Text(                    device.devId,                    modifier   = Modifier.weight(1f),                    style      = MaterialTheme.typography.titleSmall,                    fontWeight = FontWeight.Bold,                    color      = MaterialTheme.colorScheme.onSurface                )                Row(verticalAlignment = Alignment.CenterVertically) {                    Box(Modifier.size(7.dp).background(dotColor, CircleShape))                    Spacer(Modifier.width(5.dp))                    Text(                        statusText,                        style = MaterialTheme.typography.labelSmall,                        color = dotColor                    )                }            }            Spacer(Modifier.height(8.dp))            // ---- 数据三行(对齐效果图 .rows)----            val vac = if (state != null) "%.1f kPa".format(state.vacuumKpa) else "—"            val vol = if (state != null) "%.1f mL".format(state.volume)     else "—"            DataRow("流程",     state?.process?.label ?: "—")            DataRow("真空·气量", "$vac · $vol")            DataRow(                "报警",                if (hasAlarm) state?.alarm?.label ?: "—" else "—",                if (hasAlarm) MaterialTheme.colorScheme.error else null            )            Spacer(Modifier.height(9.dp))            // ---- 底:启用开关 + 恢复出厂 ----            Row(verticalAlignment = Alignment.CenterVertically) {                MiniSwitch(checked = device.enable, onToggle = onToggle)                Spacer(Modifier.width(6.dp))                Text(                    if (device.enable) "启用" else "已禁用",                    style = MaterialTheme.typography.labelSmall,                    color = MaterialTheme.colorScheme.onSurfaceVariant,                    maxLines = 1                )                Spacer(Modifier.weight(1f))                Text(                    "恢复出厂",                    modifier = Modifier                        .clickable(onClick = onReset)                        .padding(horizontal = 2.dp, vertical = 4.dp),                    style = MaterialTheme.typography.labelSmall,                    color = MaterialTheme.colorScheme.primary,                    maxLines = 1,                    softWrap = false                )            }        }    }}@Composableprivate funDataRow(label: String, value: String, valueColor: Color? = null) {    Row(verticalAlignment = Alignment.CenterVertically) {        Text(            "$label ",            style = MaterialTheme.typography.bodySmall,            color = MaterialTheme.colorScheme.onSurfaceVariant        )        Text(            value,            style = MaterialTheme.typography.bodySmall,            color = valueColor ?: MaterialTheme.colorScheme.onSurface        )    }}

相关学习资料