2025-11-26 02:30:03 +00:00
|
|
|
|
package com.example.smarthome.ui
|
|
|
|
|
|
|
|
|
|
|
|
import androidx.compose.foundation.Image
|
|
|
|
|
|
import androidx.compose.foundation.background
|
|
|
|
|
|
import androidx.compose.foundation.clickable
|
2025-11-27 04:32:01 +00:00
|
|
|
|
import androidx.compose.foundation.layout.*
|
2025-11-26 02:30:03 +00:00
|
|
|
|
import androidx.compose.foundation.lazy.grid.GridCells
|
|
|
|
|
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
|
|
|
|
|
import androidx.compose.foundation.lazy.grid.items
|
|
|
|
|
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
|
|
|
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
|
|
|
|
import androidx.compose.foundation.interaction.PressInteraction
|
2025-11-27 04:32:01 +00:00
|
|
|
|
import androidx.compose.material3.*
|
|
|
|
|
|
import androidx.compose.runtime.*
|
|
|
|
|
|
import androidx.compose.animation.core.*
|
2025-11-26 02:30:03 +00:00
|
|
|
|
import androidx.compose.ui.Alignment
|
|
|
|
|
|
import androidx.compose.ui.Modifier
|
|
|
|
|
|
import androidx.compose.ui.draw.clip
|
|
|
|
|
|
import androidx.compose.ui.draw.shadow
|
|
|
|
|
|
import androidx.compose.ui.draw.scale
|
|
|
|
|
|
import androidx.compose.ui.draw.alpha
|
2025-11-27 04:32:01 +00:00
|
|
|
|
import androidx.compose.ui.geometry.Offset
|
2025-11-26 02:30:03 +00:00
|
|
|
|
import androidx.compose.ui.graphics.Brush
|
|
|
|
|
|
import androidx.compose.ui.graphics.Color
|
2025-11-27 04:32:01 +00:00
|
|
|
|
import androidx.compose.ui.graphics.Shadow
|
2025-11-26 02:30:03 +00:00
|
|
|
|
import androidx.compose.ui.res.painterResource
|
|
|
|
|
|
import androidx.compose.ui.text.font.FontWeight
|
|
|
|
|
|
import androidx.compose.ui.text.style.TextAlign
|
|
|
|
|
|
import androidx.compose.ui.unit.dp
|
|
|
|
|
|
import androidx.compose.ui.unit.sp
|
|
|
|
|
|
import androidx.compose.foundation.border
|
2025-11-27 04:32:01 +00:00
|
|
|
|
import com.example.smarthome.R
|
|
|
|
|
|
import com.example.smarthome.data.WeatherInfo
|
|
|
|
|
|
import com.example.smarthome.data.WeatherService
|
2025-11-27 08:16:34 +00:00
|
|
|
|
import com.example.smarthome.data.LanguageManager
|
|
|
|
|
|
import com.example.smarthome.data.tr
|
2025-11-27 04:32:01 +00:00
|
|
|
|
import kotlinx.coroutines.launch
|
|
|
|
|
|
import kotlinx.coroutines.delay
|
2025-11-26 02:30:03 +00:00
|
|
|
|
import androidx.compose.ui.platform.LocalDensity
|
|
|
|
|
|
import androidx.compose.ui.platform.LocalView
|
|
|
|
|
|
import android.view.SoundEffectConstants
|
|
|
|
|
|
import androidx.compose.foundation.horizontalScroll
|
2025-11-28 11:56:58 +00:00
|
|
|
|
import androidx.compose.foundation.verticalScroll
|
2025-11-26 02:30:03 +00:00
|
|
|
|
import androidx.compose.foundation.rememberScrollState
|
|
|
|
|
|
import androidx.compose.foundation.gestures.detectTapGestures
|
2025-11-27 06:15:31 +00:00
|
|
|
|
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
|
2025-11-26 02:30:03 +00:00
|
|
|
|
import androidx.compose.ui.input.pointer.pointerInput
|
|
|
|
|
|
import androidx.compose.foundation.layout.offset
|
|
|
|
|
|
import androidx.compose.ui.draw.rotate
|
|
|
|
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
|
|
fun MainScaffold(
|
|
|
|
|
|
selectedRoom: Int,
|
|
|
|
|
|
onRoomSelect: (Int) -> Unit,
|
|
|
|
|
|
selectedNavItem: Int = 0,
|
|
|
|
|
|
onNavItemSelect: (Int) -> Unit = {},
|
|
|
|
|
|
rooms: List<String> = listOf("总览", "客厅", "厨房", "卧室", "影音室", "游戏房"),
|
|
|
|
|
|
onAddRoom: (String) -> Unit = {},
|
|
|
|
|
|
onDeleteRoom: (Int) -> Unit = {}
|
|
|
|
|
|
) {
|
2025-11-26 03:23:00 +00:00
|
|
|
|
Row(modifier = Modifier.fillMaxSize()) {
|
2025-11-26 02:30:03 +00:00
|
|
|
|
SideNavRail(
|
|
|
|
|
|
selectedNavItem = selectedNavItem,
|
|
|
|
|
|
onNavItemSelect = onNavItemSelect
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-11-27 05:36:47 +00:00
|
|
|
|
// 内容区域 - 减少左侧间距
|
|
|
|
|
|
Box(modifier = Modifier.fillMaxSize()) {
|
|
|
|
|
|
// 根据选中的导航项显示不同的内容
|
|
|
|
|
|
when (selectedNavItem) {
|
|
|
|
|
|
0 -> DashboardContent(selectedRoom, onRoomSelect, rooms, onAddRoom, onDeleteRoom)
|
|
|
|
|
|
1 -> SceneScreen()
|
|
|
|
|
|
2 -> AutomationScreen()
|
|
|
|
|
|
3 -> StatisticsScreen()
|
|
|
|
|
|
4 -> SecurityScreen()
|
|
|
|
|
|
5 -> SettingsContent()
|
|
|
|
|
|
else -> DashboardContent(selectedRoom, onRoomSelect, rooms, onAddRoom, onDeleteRoom)
|
|
|
|
|
|
}
|
2025-11-26 02:30:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
|
|
fun DashboardContent(
|
|
|
|
|
|
selectedRoom: Int,
|
|
|
|
|
|
onRoomSelect: (Int) -> Unit,
|
|
|
|
|
|
rooms: List<String> = listOf("总览", "客厅", "厨房", "卧室", "影音室", "游戏房"),
|
|
|
|
|
|
onAddRoom: (String) -> Unit = {},
|
|
|
|
|
|
onDeleteRoom: (Int) -> Unit = {}
|
|
|
|
|
|
) {
|
|
|
|
|
|
var showAddRoomDialog by remember { mutableStateOf(false) }
|
|
|
|
|
|
var editMode by remember { mutableStateOf(false) }
|
|
|
|
|
|
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.fillMaxSize()
|
|
|
|
|
|
.pointerInput(editMode) {
|
|
|
|
|
|
if (editMode) {
|
|
|
|
|
|
detectTapGestures(
|
|
|
|
|
|
onTap = { editMode = false }
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
) {
|
2025-11-27 05:36:47 +00:00
|
|
|
|
Column(modifier = Modifier.fillMaxSize().padding(start = 0.dp, end = 16.dp, top = 16.dp, bottom = 16.dp)) {
|
2025-11-26 02:30:03 +00:00
|
|
|
|
TopBar()
|
|
|
|
|
|
RoomTabs(
|
|
|
|
|
|
selectedRoom = selectedRoom,
|
|
|
|
|
|
onRoomSelect = onRoomSelect,
|
|
|
|
|
|
rooms = rooms,
|
|
|
|
|
|
onAddRoomClick = { showAddRoomDialog = true },
|
|
|
|
|
|
onDeleteRoom = onDeleteRoom,
|
|
|
|
|
|
editMode = editMode,
|
|
|
|
|
|
onEditModeChange = { editMode = it }
|
|
|
|
|
|
)
|
2025-11-26 03:23:00 +00:00
|
|
|
|
// 根据选中的房间显示不同的内容
|
|
|
|
|
|
RoomContent(selectedRoom = selectedRoom, roomName = rooms.getOrNull(selectedRoom) ?: "总览")
|
2025-11-26 02:30:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (showAddRoomDialog) {
|
|
|
|
|
|
AddRoomDialog(
|
|
|
|
|
|
onDismiss = { showAddRoomDialog = false },
|
|
|
|
|
|
onConfirm = { roomName ->
|
|
|
|
|
|
onAddRoom(roomName)
|
|
|
|
|
|
showAddRoomDialog = false
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-26 03:23:00 +00:00
|
|
|
|
@Composable
|
|
|
|
|
|
fun RoomContent(selectedRoom: Int, roomName: String) {
|
|
|
|
|
|
// 根据房间索引显示不同的内容
|
|
|
|
|
|
when (selectedRoom) {
|
|
|
|
|
|
0 -> OverviewRoomContent() // 总览
|
|
|
|
|
|
else -> SpecificRoomContent(selectedRoom, roomName) // 具体房间
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
|
|
fun OverviewRoomContent() {
|
2025-11-28 11:56:58 +00:00
|
|
|
|
Column(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.fillMaxSize()
|
|
|
|
|
|
.verticalScroll(rememberScrollState())
|
|
|
|
|
|
) {
|
2025-11-26 03:23:00 +00:00
|
|
|
|
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
2025-11-27 08:16:34 +00:00
|
|
|
|
AirConditionerCard(modifier = Modifier.weight(1f), roomName = tr("room_all"))
|
|
|
|
|
|
UsageStatusChart(modifier = Modifier.weight(1f), roomName = tr("room_all"))
|
2025-11-26 03:23:00 +00:00
|
|
|
|
}
|
2025-11-27 08:16:34 +00:00
|
|
|
|
LightRow(modifier = Modifier.fillMaxWidth().padding(top = 16.dp, bottom = 16.dp), roomName = tr("room_all"))
|
2025-11-26 03:23:00 +00:00
|
|
|
|
ModeButtonsRow(onModeSelected = { })
|
|
|
|
|
|
|
2025-11-26 03:43:39 +00:00
|
|
|
|
Spacer(modifier = Modifier.height(24.dp))
|
|
|
|
|
|
|
2025-11-26 03:23:00 +00:00
|
|
|
|
// 显示所有房间的设备摘要
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = "所有设备",
|
|
|
|
|
|
fontSize = 18.sp,
|
|
|
|
|
|
fontWeight = FontWeight.Bold,
|
|
|
|
|
|
color = Color.White,
|
2025-11-26 03:43:39 +00:00
|
|
|
|
modifier = Modifier.padding(bottom = 12.dp)
|
2025-11-26 03:23:00 +00:00
|
|
|
|
)
|
2025-11-28 11:56:58 +00:00
|
|
|
|
AllDevicesOverview(modifier = Modifier.fillMaxWidth())
|
2025-11-26 03:23:00 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
|
|
fun SpecificRoomContent(selectedRoom: Int, roomName: String) {
|
2025-11-28 11:56:58 +00:00
|
|
|
|
Column(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.fillMaxSize()
|
|
|
|
|
|
.verticalScroll(rememberScrollState())
|
|
|
|
|
|
) {
|
2025-11-26 03:23:00 +00:00
|
|
|
|
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
|
|
|
|
|
AirConditionerCard(modifier = Modifier.weight(1f), roomName = roomName)
|
|
|
|
|
|
UsageStatusChart(modifier = Modifier.weight(1f), roomName = roomName)
|
|
|
|
|
|
}
|
2025-11-26 03:43:39 +00:00
|
|
|
|
LightRow(modifier = Modifier.fillMaxWidth().padding(top = 16.dp, bottom = 16.dp), roomName = roomName)
|
|
|
|
|
|
|
|
|
|
|
|
Spacer(modifier = Modifier.height(24.dp))
|
2025-11-26 03:23:00 +00:00
|
|
|
|
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = "$roomName 设备",
|
|
|
|
|
|
fontSize = 18.sp,
|
|
|
|
|
|
fontWeight = FontWeight.Bold,
|
|
|
|
|
|
color = Color.White,
|
2025-11-26 03:43:39 +00:00
|
|
|
|
modifier = Modifier.padding(bottom = 12.dp)
|
2025-11-26 03:23:00 +00:00
|
|
|
|
)
|
2025-11-28 11:56:58 +00:00
|
|
|
|
MyDevicesGrid(selectedRoom = selectedRoom, modifier = Modifier.fillMaxWidth())
|
2025-11-26 03:23:00 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
|
|
fun AllDevicesOverview(modifier: Modifier = Modifier) {
|
|
|
|
|
|
val allDevices = listOf(
|
|
|
|
|
|
Device("客厅空调", "24°C", R.drawable.ic_ac, true),
|
|
|
|
|
|
Device("客厅灯光", "80%", R.drawable.ic_light, true),
|
|
|
|
|
|
Device("厨房灯光", "60%", R.drawable.ic_light, true),
|
|
|
|
|
|
Device("卧室空调", "22°C", R.drawable.ic_ac, false),
|
|
|
|
|
|
Device("影音室电视", "运行中", R.drawable.ic_media, true),
|
|
|
|
|
|
Device("游戏房主机", "待机中", R.drawable.ic_media, false)
|
|
|
|
|
|
)
|
2025-11-28 11:56:58 +00:00
|
|
|
|
Column(
|
2025-11-26 03:23:00 +00:00
|
|
|
|
modifier = modifier,
|
2025-11-28 11:56:58 +00:00
|
|
|
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
2025-11-26 03:23:00 +00:00
|
|
|
|
) {
|
2025-11-28 11:56:58 +00:00
|
|
|
|
allDevices.forEach { d -> DeviceCard(d) }
|
2025-11-26 03:23:00 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-26 02:30:03 +00:00
|
|
|
|
@Composable
|
|
|
|
|
|
fun HeaderHero() {
|
|
|
|
|
|
TopBar()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
|
|
fun StatusChips() {
|
|
|
|
|
|
SideNavRail()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
|
|
fun Chip(text: String) {
|
|
|
|
|
|
Box(modifier = Modifier.clip(RoundedCornerShape(20.dp)).background(Color(0x331A1A1A)).padding(horizontal = 12.dp, vertical = 8.dp)) {
|
|
|
|
|
|
Text(text = text, color = Color.White)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
|
|
fun QuickActions() {
|
|
|
|
|
|
Row(modifier = Modifier.fillMaxWidth().padding(24.dp), horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
|
|
|
|
|
QuickActionCard(
|
|
|
|
|
|
title = "回家模式",
|
|
|
|
|
|
colors = Brush.linearGradient(listOf(Color(0xFFFFD54F), Color(0xFFFF8F00))),
|
|
|
|
|
|
modifier = Modifier.weight(1f),
|
|
|
|
|
|
onClick = { /* TODO: 触发回家场景 */ }
|
|
|
|
|
|
)
|
|
|
|
|
|
QuickActionCard(
|
|
|
|
|
|
title = "出门模式",
|
|
|
|
|
|
colors = Brush.linearGradient(listOf(Color(0xFFFF8A65), Color(0xFFFF7043))),
|
|
|
|
|
|
modifier = Modifier.weight(1f),
|
|
|
|
|
|
onClick = { /* TODO: 触发出门场景 */ }
|
|
|
|
|
|
)
|
|
|
|
|
|
QuickActionCard(
|
|
|
|
|
|
title = "玩乐模式",
|
|
|
|
|
|
colors = Brush.linearGradient(listOf(Color(0xFF6A3DFF), Color(0xFF3A0CA3))),
|
|
|
|
|
|
modifier = Modifier.weight(1f),
|
|
|
|
|
|
onClick = { /* TODO: 触发玩乐场景 */ }
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
|
|
fun QuickActionCard(title: String, colors: Brush, modifier: Modifier = Modifier, onClick: () -> Unit) {
|
|
|
|
|
|
val shape = RoundedCornerShape(24.dp)
|
|
|
|
|
|
var pressed by remember { mutableStateOf(false) }
|
|
|
|
|
|
val scale by androidx.compose.animation.core.animateFloatAsState(
|
|
|
|
|
|
targetValue = if (pressed) 0.97f else 1f,
|
|
|
|
|
|
animationSpec = androidx.compose.animation.core.spring(),
|
|
|
|
|
|
label = "quick_action_scale"
|
|
|
|
|
|
)
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = modifier
|
|
|
|
|
|
.height(88.dp)
|
|
|
|
|
|
.shadow(12.dp, shape, false)
|
|
|
|
|
|
.clip(shape)
|
|
|
|
|
|
.background(colors)
|
|
|
|
|
|
.scale(scale)
|
|
|
|
|
|
.clickable(
|
|
|
|
|
|
indication = null,
|
|
|
|
|
|
interactionSource = remember { MutableInteractionSource() }
|
|
|
|
|
|
) {
|
|
|
|
|
|
pressed = !pressed
|
|
|
|
|
|
onClick()
|
|
|
|
|
|
}
|
|
|
|
|
|
.padding(16.dp),
|
|
|
|
|
|
contentAlignment = Alignment.BottomStart
|
|
|
|
|
|
) {
|
|
|
|
|
|
Text(text = title, fontWeight = FontWeight.Medium, color = Color.White)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
|
|
fun RoomTabs(
|
|
|
|
|
|
selectedRoom: Int,
|
|
|
|
|
|
onRoomSelect: (Int) -> Unit,
|
|
|
|
|
|
rooms: List<String> = listOf("总览", "客厅", "厨房", "卧室", "影音室", "游戏房"),
|
|
|
|
|
|
onAddRoomClick: () -> Unit = {},
|
|
|
|
|
|
onDeleteRoom: (Int) -> Unit = {},
|
|
|
|
|
|
editMode: Boolean = false,
|
|
|
|
|
|
onEditModeChange: (Boolean) -> Unit = {}
|
|
|
|
|
|
) {
|
|
|
|
|
|
Row(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.fillMaxWidth()
|
|
|
|
|
|
.padding(vertical = 16.dp),
|
|
|
|
|
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
|
|
|
|
|
) {
|
|
|
|
|
|
// 可滚动的房间标签区域
|
|
|
|
|
|
Row(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.weight(1f)
|
|
|
|
|
|
.horizontalScroll(rememberScrollState()),
|
|
|
|
|
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
|
|
|
|
|
) {
|
|
|
|
|
|
rooms.forEachIndexed { index, name ->
|
|
|
|
|
|
RoomTab(
|
|
|
|
|
|
name = name,
|
|
|
|
|
|
selected = index == selectedRoom,
|
|
|
|
|
|
editMode = editMode,
|
|
|
|
|
|
canDelete = rooms.size > 1,
|
|
|
|
|
|
onClick = {
|
|
|
|
|
|
if (!editMode) {
|
|
|
|
|
|
onRoomSelect(index)
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
onLongClick = { onEditModeChange(true) },
|
|
|
|
|
|
onDelete = {
|
|
|
|
|
|
onDeleteRoom(index)
|
|
|
|
|
|
onEditModeChange(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 固定的添加按钮
|
2025-11-27 08:00:44 +00:00
|
|
|
|
GradientButton("+ 添加房间", onClick = onAddRoomClick)
|
2025-11-26 02:30:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
|
|
fun RoomTab(
|
|
|
|
|
|
name: String,
|
|
|
|
|
|
selected: Boolean,
|
|
|
|
|
|
editMode: Boolean,
|
|
|
|
|
|
canDelete: Boolean,
|
|
|
|
|
|
onClick: () -> Unit,
|
|
|
|
|
|
onLongClick: () -> Unit,
|
|
|
|
|
|
onDelete: () -> Unit
|
|
|
|
|
|
) {
|
|
|
|
|
|
val scope = rememberCoroutineScope()
|
|
|
|
|
|
|
|
|
|
|
|
// 改进的晃动动画 - 使用无限循环的旋转动画
|
|
|
|
|
|
val infiniteTransition = rememberInfiniteTransition(label = "shake")
|
|
|
|
|
|
val rotation by infiniteTransition.animateFloat(
|
|
|
|
|
|
initialValue = -3f,
|
|
|
|
|
|
targetValue = 3f,
|
|
|
|
|
|
animationSpec = infiniteRepeatable(
|
|
|
|
|
|
animation = tween(150, easing = LinearEasing),
|
|
|
|
|
|
repeatMode = RepeatMode.Reverse
|
|
|
|
|
|
),
|
|
|
|
|
|
label = "rotation"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 缩放动画
|
|
|
|
|
|
val scale by androidx.compose.animation.core.animateFloatAsState(
|
|
|
|
|
|
targetValue = if (editMode && canDelete) 0.95f else 1f,
|
|
|
|
|
|
animationSpec = androidx.compose.animation.core.spring(
|
|
|
|
|
|
dampingRatio = androidx.compose.animation.core.Spring.DampingRatioMediumBouncy,
|
|
|
|
|
|
stiffness = androidx.compose.animation.core.Spring.StiffnessLow
|
|
|
|
|
|
),
|
|
|
|
|
|
label = "scale"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.scale(scale)
|
|
|
|
|
|
.then(
|
|
|
|
|
|
if (editMode && canDelete) {
|
|
|
|
|
|
Modifier.rotate(rotation)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Modifier
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
.pointerInput(Unit) {
|
|
|
|
|
|
detectTapGestures(
|
|
|
|
|
|
onTap = { onClick() },
|
|
|
|
|
|
onLongPress = {
|
|
|
|
|
|
if (canDelete) {
|
|
|
|
|
|
onLongClick()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
) {
|
|
|
|
|
|
val base = Modifier.clip(RoundedCornerShape(20.dp))
|
|
|
|
|
|
val mod = if (selected) {
|
|
|
|
|
|
base.background(brush = Brush.linearGradient(listOf(Color(0xFFA9F0FF), Color(0xFFB89CFF))))
|
|
|
|
|
|
} else {
|
2025-11-27 02:56:42 +00:00
|
|
|
|
base.background(color = Color(0x50121212))
|
2025-11-26 02:30:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = mod.padding(horizontal = 14.dp, vertical = 10.dp)
|
|
|
|
|
|
) {
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = name,
|
|
|
|
|
|
color = if (selected) Color.Black else Color(0xFFB0B0B0),
|
|
|
|
|
|
maxLines = 1
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 删除按钮
|
|
|
|
|
|
if (editMode && canDelete) {
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.size(22.dp)
|
|
|
|
|
|
.offset(x = (-4).dp, y = (-4).dp)
|
|
|
|
|
|
.align(Alignment.TopEnd)
|
|
|
|
|
|
.shadow(4.dp, RoundedCornerShape(11.dp))
|
|
|
|
|
|
.clip(RoundedCornerShape(11.dp))
|
|
|
|
|
|
.background(Color(0xFFFF5252))
|
|
|
|
|
|
.clickable(
|
|
|
|
|
|
indication = null,
|
|
|
|
|
|
interactionSource = remember { MutableInteractionSource() }
|
|
|
|
|
|
) { onDelete() },
|
|
|
|
|
|
contentAlignment = Alignment.Center
|
|
|
|
|
|
) {
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = "×",
|
|
|
|
|
|
color = Color.White,
|
|
|
|
|
|
fontSize = 18.sp,
|
|
|
|
|
|
fontWeight = FontWeight.Bold
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
|
|
fun DeviceGrid() {
|
|
|
|
|
|
val devices = listOf(
|
|
|
|
|
|
Device("空调", "室内 25℃", R.drawable.ic_ac, true),
|
|
|
|
|
|
Device("智能开关", "待机中", R.drawable.ic_light, false),
|
|
|
|
|
|
Device("吊灯", "6500K", R.drawable.ic_light, true),
|
|
|
|
|
|
Device("扫地机", "清扫中", R.drawable.ic_media, true),
|
|
|
|
|
|
Device("空气净化", "运行中", R.drawable.ic_security, true),
|
|
|
|
|
|
Device("饮水机", "待机中", R.drawable.ic_media, false)
|
|
|
|
|
|
)
|
|
|
|
|
|
LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 140.dp), modifier = Modifier.fillMaxSize().padding(24.dp), horizontalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
|
|
|
|
|
items(devices) { d -> DeviceCard(d) }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
data class Device(val name: String, val sub: String, val icon: Int, val on: Boolean)
|
|
|
|
|
|
|
|
|
|
|
|
@Composable
|
2025-11-28 11:56:58 +00:00
|
|
|
|
fun DeviceCard(d: Device, roomName: String = "") {
|
2025-11-26 02:30:03 +00:00
|
|
|
|
var active by remember { mutableStateOf(d.on) }
|
2025-11-28 11:56:58 +00:00
|
|
|
|
val accentColor = if (active) Color(0xFFFFB74D) else Color(0xFF5A5A6E)
|
|
|
|
|
|
|
|
|
|
|
|
Row(
|
2025-11-27 02:56:42 +00:00
|
|
|
|
modifier = Modifier
|
2025-11-28 11:56:58 +00:00
|
|
|
|
.fillMaxWidth()
|
|
|
|
|
|
.clip(RoundedCornerShape(16.dp))
|
|
|
|
|
|
.background(Color(0x40121212))
|
|
|
|
|
|
.padding(12.dp),
|
|
|
|
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
|
|
|
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
2025-11-27 02:56:42 +00:00
|
|
|
|
) {
|
2025-11-28 11:56:58 +00:00
|
|
|
|
// 左侧图标
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.size(48.dp)
|
|
|
|
|
|
.clip(RoundedCornerShape(12.dp))
|
|
|
|
|
|
.background(accentColor.copy(alpha = if (active) 0.3f else 0.15f)),
|
|
|
|
|
|
contentAlignment = Alignment.Center
|
|
|
|
|
|
) {
|
|
|
|
|
|
Image(
|
|
|
|
|
|
painter = painterResource(id = d.icon),
|
|
|
|
|
|
contentDescription = null,
|
|
|
|
|
|
modifier = Modifier.size(28.dp),
|
|
|
|
|
|
colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(
|
|
|
|
|
|
if (active) accentColor else Color(0xFF9AA0A6)
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 中间文字
|
|
|
|
|
|
Column(modifier = Modifier.weight(1f)) {
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = if (roomName.isNotEmpty()) "$roomName ${d.name}" else d.name,
|
|
|
|
|
|
fontWeight = FontWeight.SemiBold,
|
|
|
|
|
|
color = Color.White,
|
|
|
|
|
|
fontSize = 15.sp
|
|
|
|
|
|
)
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = d.sub,
|
|
|
|
|
|
color = if (active) Color(0xFF00E676) else Color(0xFF9AA0A6),
|
|
|
|
|
|
fontSize = 13.sp
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 右侧控制 - 根据设备类型显示不同控制方式
|
|
|
|
|
|
val isLight = d.icon == R.drawable.ic_light || d.name.contains("灯")
|
|
|
|
|
|
val isAC = d.icon == R.drawable.ic_ac || d.name.contains("空调")
|
|
|
|
|
|
val isCurtain = d.icon == R.drawable.ic_curtain || d.name.contains("窗帘")
|
|
|
|
|
|
|
|
|
|
|
|
when {
|
|
|
|
|
|
isLight -> {
|
|
|
|
|
|
// 灯光:亮度调节点
|
|
|
|
|
|
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
|
|
|
|
|
val brightnessLevel = d.sub.replace("%", "").toIntOrNull()?.let { it / 20 } ?: 4
|
|
|
|
|
|
repeat(6) { index ->
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.size(10.dp)
|
|
|
|
|
|
.clip(RoundedCornerShape(5.dp))
|
|
|
|
|
|
.background(
|
|
|
|
|
|
if (active && index < brightnessLevel) Color(0xFF00D1FF)
|
|
|
|
|
|
else Color(0xFF00D1FF).copy(alpha = 0.3f)
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
2025-11-26 02:30:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-28 11:56:58 +00:00
|
|
|
|
isAC -> {
|
|
|
|
|
|
// 空调:显示温度
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = d.sub,
|
|
|
|
|
|
color = if (active) Color(0xFF00D1FF) else Color(0xFF9AA0A6),
|
|
|
|
|
|
fontSize = 16.sp,
|
|
|
|
|
|
fontWeight = FontWeight.Bold
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
isCurtain -> {
|
|
|
|
|
|
// 窗帘:开关状态图标
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = if (active) "▼" else "▲",
|
|
|
|
|
|
color = if (active) Color(0xFF00E676) else Color(0xFF9AA0A6),
|
|
|
|
|
|
fontSize = 18.sp
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
else -> {
|
|
|
|
|
|
// 其他设备:开关按钮
|
|
|
|
|
|
Switch(
|
|
|
|
|
|
checked = active,
|
|
|
|
|
|
onCheckedChange = { active = it },
|
|
|
|
|
|
colors = SwitchDefaults.colors(
|
|
|
|
|
|
checkedThumbColor = Color.White,
|
|
|
|
|
|
checkedTrackColor = Color(0xFF00D1FF),
|
|
|
|
|
|
uncheckedThumbColor = Color.Gray,
|
|
|
|
|
|
uncheckedTrackColor = Color(0xFF3A3A4E)
|
|
|
|
|
|
),
|
|
|
|
|
|
modifier = Modifier.scale(0.8f)
|
|
|
|
|
|
)
|
2025-11-26 02:30:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
|
|
fun Toggle(on: Boolean, change: (Boolean) -> Unit) {
|
|
|
|
|
|
Box(modifier = Modifier.size(48.dp, 28.dp).clip(RoundedCornerShape(18.dp)).background(if (on) Color(0xFF3A0CA3) else Color(0x33222222)).clickable { change(!on) }.padding(4.dp)) {
|
|
|
|
|
|
Box(modifier = Modifier.size(20.dp).clip(RoundedCornerShape(10.dp)).background(Color.White).align(if (on) Alignment.CenterEnd else Alignment.CenterStart))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
|
|
fun BottomNav() {
|
2025-11-27 02:56:42 +00:00
|
|
|
|
Row(modifier = Modifier.fillMaxWidth().padding(16.dp).clip(RoundedCornerShape(28.dp)).background(Color(0x50121212)).padding(8.dp), horizontalArrangement = Arrangement.SpaceEvenly) {
|
2025-11-26 02:30:03 +00:00
|
|
|
|
NavItem("首页", true)
|
|
|
|
|
|
NavItem("设备", false)
|
|
|
|
|
|
NavItem("我的", false)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
|
|
fun NavItem(title: String, selected: Boolean) {
|
|
|
|
|
|
Box(modifier = Modifier.clip(RoundedCornerShape(20.dp)).background(if (selected) Color(0x55121212) else Color.Transparent).padding(horizontal = 18.dp, vertical = 10.dp)) {
|
|
|
|
|
|
Text(text = title, color = if (selected) Color.White else Color(0xFFB0B0B0))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
|
|
fun SideNavRail(selectedNavItem: Int = 0, onNavItemSelect: (Int) -> Unit = {}) {
|
2025-11-27 08:00:44 +00:00
|
|
|
|
val context = androidx.compose.ui.platform.LocalContext.current
|
|
|
|
|
|
val selectedBg by com.example.smarthome.data.BackgroundManager.selectedBackground.collectAsState()
|
|
|
|
|
|
val isDarkMode = com.example.smarthome.data.BackgroundManager.backgrounds.getOrNull(selectedBg)?.isDark ?: false
|
|
|
|
|
|
|
|
|
|
|
|
Column(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.width(100.dp)
|
|
|
|
|
|
.fillMaxHeight()
|
|
|
|
|
|
.padding(start = 12.dp, end = 4.dp, top = 12.dp, bottom = 12.dp),
|
|
|
|
|
|
verticalArrangement = Arrangement.SpaceBetween
|
|
|
|
|
|
) {
|
|
|
|
|
|
// 上半部分:导航项
|
2025-11-27 08:16:34 +00:00
|
|
|
|
// 监听语言变化以触发重组
|
|
|
|
|
|
val currentLang by LanguageManager.currentLanguage.collectAsState()
|
|
|
|
|
|
|
2025-11-27 08:00:44 +00:00
|
|
|
|
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
|
|
|
|
|
Spacer(modifier = Modifier.height(48.dp))
|
2025-11-27 08:16:34 +00:00
|
|
|
|
NavRailItem(tr("nav_dashboard"), R.drawable.ic_dashboard, selectedNavItem == 0, onClick = { onNavItemSelect(0) })
|
|
|
|
|
|
NavRailItem(tr("nav_scene"), R.drawable.ic_scene, selectedNavItem == 1, onClick = { onNavItemSelect(1) })
|
|
|
|
|
|
NavRailItem(tr("nav_automation"), R.drawable.ic_automation, selectedNavItem == 2, onClick = { onNavItemSelect(2) })
|
|
|
|
|
|
NavRailItem(tr("nav_statistics"), R.drawable.ic_statistics, selectedNavItem == 3, onClick = { onNavItemSelect(3) })
|
|
|
|
|
|
NavRailItem(tr("nav_security"), R.drawable.ic_security, selectedNavItem == 4, onClick = { onNavItemSelect(4) })
|
|
|
|
|
|
NavRailItem(tr("nav_settings"), R.drawable.ic_settings, selectedNavItem == 5, onClick = { onNavItemSelect(5) })
|
2025-11-27 08:00:44 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 底部:模式切换按钮(暂时注释)
|
|
|
|
|
|
/*
|
|
|
|
|
|
ThemeModeToggle(
|
|
|
|
|
|
isDarkMode = isDarkMode,
|
|
|
|
|
|
onToggle = {
|
|
|
|
|
|
if (isDarkMode) {
|
|
|
|
|
|
com.example.smarthome.data.BackgroundManager.setBackground(context, 0) // 丝绸白
|
|
|
|
|
|
} else {
|
|
|
|
|
|
com.example.smarthome.data.BackgroundManager.setBackground(context, 2) // 紫金绸
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
*/
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
@Composable
|
|
|
|
|
|
fun ThemeModeToggle(isDarkMode: Boolean, onToggle: () -> Unit) {
|
|
|
|
|
|
Column(
|
|
|
|
|
|
horizontalAlignment = Alignment.CenterHorizontally,
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.clickable(
|
|
|
|
|
|
indication = null,
|
|
|
|
|
|
interactionSource = remember { MutableInteractionSource() }
|
|
|
|
|
|
) { onToggle() }
|
|
|
|
|
|
.padding(vertical = 4.dp)
|
|
|
|
|
|
) {
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.size(48.dp)
|
|
|
|
|
|
.clip(RoundedCornerShape(14.dp))
|
|
|
|
|
|
.background(
|
|
|
|
|
|
if (isDarkMode) {
|
|
|
|
|
|
Brush.linearGradient(
|
|
|
|
|
|
listOf(Color(0xFF2C2C54), Color(0xFF1A1A2E))
|
|
|
|
|
|
)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Brush.linearGradient(
|
|
|
|
|
|
listOf(Color(0xFFFFE082), Color(0xFFFFD54F))
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
),
|
|
|
|
|
|
contentAlignment = Alignment.Center
|
|
|
|
|
|
) {
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = if (isDarkMode) "🌙" else "☀️",
|
|
|
|
|
|
fontSize = 20.sp
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
2025-11-26 02:30:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-27 08:00:44 +00:00
|
|
|
|
*/
|
2025-11-26 02:30:03 +00:00
|
|
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
|
|
fun NavRailItem(text: String, iconRes: Int, selected: Boolean, onClick: () -> Unit = {}) {
|
|
|
|
|
|
val scale by androidx.compose.animation.core.animateFloatAsState(
|
|
|
|
|
|
targetValue = if (selected) 1.05f else 1f,
|
|
|
|
|
|
animationSpec = androidx.compose.animation.core.spring(),
|
|
|
|
|
|
label = "nav_scale"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
Column(
|
|
|
|
|
|
horizontalAlignment = Alignment.CenterHorizontally,
|
|
|
|
|
|
verticalArrangement = Arrangement.spacedBy(6.dp),
|
|
|
|
|
|
modifier = Modifier
|
2025-11-27 08:16:34 +00:00
|
|
|
|
.width(80.dp) // 固定宽度,防止文字长度不一致导致图标不对齐
|
2025-11-26 02:30:03 +00:00
|
|
|
|
.scale(scale)
|
|
|
|
|
|
.clickable(
|
|
|
|
|
|
indication = null,
|
|
|
|
|
|
interactionSource = remember { MutableInteractionSource() }
|
|
|
|
|
|
) { onClick() }
|
|
|
|
|
|
.padding(vertical = 4.dp)
|
|
|
|
|
|
) {
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.size(48.dp)
|
|
|
|
|
|
.shadow(if (selected) 8.dp else 0.dp, RoundedCornerShape(14.dp))
|
|
|
|
|
|
.clip(RoundedCornerShape(14.dp))
|
|
|
|
|
|
.background(
|
|
|
|
|
|
if (selected) {
|
|
|
|
|
|
Brush.linearGradient(
|
|
|
|
|
|
listOf(Color(0xFFA9F0FF), Color(0xFFB89CFF))
|
|
|
|
|
|
)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Brush.linearGradient(
|
|
|
|
|
|
listOf(Color(0x22FFFFFF), Color(0x22FFFFFF))
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
),
|
|
|
|
|
|
contentAlignment = Alignment.Center
|
|
|
|
|
|
) {
|
|
|
|
|
|
Image(
|
|
|
|
|
|
painter = painterResource(id = iconRes),
|
|
|
|
|
|
contentDescription = text,
|
|
|
|
|
|
modifier = Modifier.size(24.dp),
|
|
|
|
|
|
colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(
|
|
|
|
|
|
if (selected) Color.Black else Color(0xFFB0B0B0)
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = text,
|
|
|
|
|
|
color = if (selected) Color.White else Color(0xFFB0B0B0),
|
2025-11-27 08:16:34 +00:00
|
|
|
|
fontSize = 12.sp,
|
2025-11-26 02:30:03 +00:00
|
|
|
|
fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal,
|
2025-11-27 08:16:34 +00:00
|
|
|
|
maxLines = 1,
|
|
|
|
|
|
textAlign = TextAlign.Center
|
2025-11-26 02:30:03 +00:00
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
|
|
fun TopBar() {
|
2025-11-27 04:32:01 +00:00
|
|
|
|
val context = androidx.compose.ui.platform.LocalContext.current
|
2025-11-27 02:56:42 +00:00
|
|
|
|
var weatherInfo by remember { mutableStateOf(WeatherService.getSimulatedWeather()) }
|
|
|
|
|
|
var isLoading by remember { mutableStateOf(true) }
|
|
|
|
|
|
|
2025-11-28 05:21:09 +00:00
|
|
|
|
// 获取用户信息
|
|
|
|
|
|
val userInfo by com.example.smarthome.data.UserManager.userInfo.collectAsState()
|
|
|
|
|
|
|
2025-11-27 02:56:42 +00:00
|
|
|
|
LaunchedEffect(Unit) {
|
|
|
|
|
|
try {
|
2025-11-27 04:32:01 +00:00
|
|
|
|
weatherInfo = WeatherService.getWeather(context)
|
2025-11-27 02:56:42 +00:00
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
|
// 使用模拟数据
|
|
|
|
|
|
}
|
|
|
|
|
|
isLoading = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-26 02:30:03 +00:00
|
|
|
|
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
|
2025-11-27 08:16:34 +00:00
|
|
|
|
Text(text = tr("dashboard_title"), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, color = Color.White)
|
2025-11-28 05:21:09 +00:00
|
|
|
|
|
|
|
|
|
|
Row(
|
|
|
|
|
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
|
|
|
|
|
verticalAlignment = Alignment.CenterVertically
|
|
|
|
|
|
) {
|
|
|
|
|
|
WeatherCard(weatherInfo = weatherInfo, isLoading = isLoading)
|
|
|
|
|
|
|
|
|
|
|
|
// 用户头像
|
|
|
|
|
|
UserAvatar(
|
|
|
|
|
|
nickname = userInfo.nickname,
|
|
|
|
|
|
avatarUrl = userInfo.avatarUrl,
|
|
|
|
|
|
onClick = { /* 点击头像可以打开用户菜单 */ }
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
|
|
fun UserAvatar(
|
|
|
|
|
|
nickname: String,
|
|
|
|
|
|
avatarUrl: String,
|
|
|
|
|
|
onClick: () -> Unit
|
|
|
|
|
|
) {
|
|
|
|
|
|
val context = androidx.compose.ui.platform.LocalContext.current
|
|
|
|
|
|
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.size(40.dp)
|
|
|
|
|
|
.shadow(8.dp, androidx.compose.foundation.shape.CircleShape)
|
|
|
|
|
|
.clip(androidx.compose.foundation.shape.CircleShape)
|
|
|
|
|
|
.background(
|
|
|
|
|
|
Brush.linearGradient(
|
|
|
|
|
|
listOf(Color(0xFFA9F0FF), Color(0xFFB89CFF))
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
.border(
|
|
|
|
|
|
width = 2.dp,
|
|
|
|
|
|
brush = Brush.linearGradient(
|
|
|
|
|
|
listOf(Color.White.copy(alpha = 0.5f), Color.White.copy(alpha = 0.2f))
|
|
|
|
|
|
),
|
|
|
|
|
|
shape = androidx.compose.foundation.shape.CircleShape
|
|
|
|
|
|
)
|
|
|
|
|
|
.clickable(
|
|
|
|
|
|
indication = null,
|
|
|
|
|
|
interactionSource = remember { MutableInteractionSource() }
|
|
|
|
|
|
) { onClick() },
|
|
|
|
|
|
contentAlignment = Alignment.Center
|
|
|
|
|
|
) {
|
|
|
|
|
|
if (avatarUrl.isNotEmpty()) {
|
|
|
|
|
|
// 如果有头像URL,可以用Coil加载网络图片
|
|
|
|
|
|
// 这里暂时用首字母代替
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = nickname.firstOrNull()?.uppercase() ?: "U",
|
|
|
|
|
|
fontSize = 16.sp,
|
|
|
|
|
|
fontWeight = FontWeight.Bold,
|
|
|
|
|
|
color = Color.White
|
|
|
|
|
|
)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 显示昵称首字母
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = nickname.firstOrNull()?.uppercase() ?: "U",
|
|
|
|
|
|
fontSize = 16.sp,
|
|
|
|
|
|
fontWeight = FontWeight.Bold,
|
|
|
|
|
|
color = Color.White
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
2025-11-26 02:30:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-27 02:56:42 +00:00
|
|
|
|
@Composable
|
|
|
|
|
|
fun WeatherCard(weatherInfo: WeatherInfo, isLoading: Boolean) {
|
2025-11-27 04:32:01 +00:00
|
|
|
|
// 动画:创建无限循环的偏移量
|
|
|
|
|
|
val infiniteTransition = rememberInfiniteTransition(label = "weather")
|
|
|
|
|
|
val offsetAnim by infiniteTransition.animateFloat(
|
|
|
|
|
|
initialValue = 0f,
|
|
|
|
|
|
targetValue = 1500f, // 增加偏移量,让流动范围更大
|
|
|
|
|
|
animationSpec = infiniteRepeatable(
|
|
|
|
|
|
animation = tween(durationMillis = 15000, easing = LinearEasing), // 加快一点速度
|
|
|
|
|
|
repeatMode = RepeatMode.Reverse
|
|
|
|
|
|
),
|
|
|
|
|
|
label = "gradient"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-11-27 04:45:22 +00:00
|
|
|
|
// 根据天气状况决定背景颜色
|
2025-11-27 04:32:01 +00:00
|
|
|
|
val colors = if (isLoading) {
|
|
|
|
|
|
listOf(
|
|
|
|
|
|
Color(0xFF2C3E50).copy(alpha = 0.6f),
|
|
|
|
|
|
Color(0xFF3498DB).copy(alpha = 0.6f),
|
|
|
|
|
|
Color(0xFF2C3E50).copy(alpha = 0.6f)
|
|
|
|
|
|
)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
when {
|
2025-11-27 04:45:22 +00:00
|
|
|
|
// 晴天:深橙色流动
|
2025-11-27 04:32:01 +00:00
|
|
|
|
weatherInfo.weather.contains("晴") ->
|
|
|
|
|
|
listOf(
|
2025-11-27 04:45:22 +00:00
|
|
|
|
Color(0xFF8B3000).copy(alpha = 0.65f),
|
|
|
|
|
|
Color(0xFF9A4500).copy(alpha = 0.65f),
|
|
|
|
|
|
Color(0xFF8B3000).copy(alpha = 0.65f)
|
2025-11-27 04:32:01 +00:00
|
|
|
|
)
|
|
|
|
|
|
// 雨天:深紫深蓝流动
|
|
|
|
|
|
weatherInfo.weather.contains("雨") ->
|
|
|
|
|
|
listOf(
|
|
|
|
|
|
Color(0xFF2C3E50).copy(alpha = 0.6f),
|
|
|
|
|
|
Color(0xFF4CA1AF).copy(alpha = 0.6f),
|
|
|
|
|
|
Color(0xFF2C3E50).copy(alpha = 0.6f)
|
|
|
|
|
|
)
|
|
|
|
|
|
// 阴天/多云:灰蓝流动
|
|
|
|
|
|
weatherInfo.weather.contains("云") || weatherInfo.weather.contains("阴") ->
|
|
|
|
|
|
listOf(
|
|
|
|
|
|
Color(0xFF606c88).copy(alpha = 0.6f),
|
|
|
|
|
|
Color(0xFF3f4c6b).copy(alpha = 0.6f),
|
|
|
|
|
|
Color(0xFF606c88).copy(alpha = 0.6f)
|
|
|
|
|
|
)
|
|
|
|
|
|
// 雪天:冰蓝流动
|
|
|
|
|
|
weatherInfo.weather.contains("雪") ->
|
|
|
|
|
|
listOf(
|
|
|
|
|
|
Color(0xFF5a7fa4).copy(alpha = 0.6f),
|
|
|
|
|
|
Color(0xFF8ad0d4).copy(alpha = 0.6f),
|
|
|
|
|
|
Color(0xFF5a7fa4).copy(alpha = 0.6f)
|
|
|
|
|
|
)
|
|
|
|
|
|
// 默认:蓝青流动
|
|
|
|
|
|
else ->
|
|
|
|
|
|
listOf(
|
|
|
|
|
|
Color(0xFF1090B0).copy(alpha = 0.6f),
|
|
|
|
|
|
Color(0xFF000046).copy(alpha = 0.6f),
|
|
|
|
|
|
Color(0xFF1090B0).copy(alpha = 0.6f)
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
val backgroundBrush = Brush.linearGradient(
|
|
|
|
|
|
colors = colors,
|
|
|
|
|
|
start = Offset(offsetAnim, offsetAnim),
|
|
|
|
|
|
end = Offset(offsetAnim + 500f, offsetAnim + 500f)
|
|
|
|
|
|
)
|
2025-11-27 04:45:22 +00:00
|
|
|
|
|
2025-11-27 02:56:42 +00:00
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.clip(RoundedCornerShape(16.dp))
|
2025-11-27 04:32:01 +00:00
|
|
|
|
.background(backgroundBrush) // 使用动态渐变背景
|
2025-11-27 02:56:42 +00:00
|
|
|
|
) {
|
2025-11-27 04:32:01 +00:00
|
|
|
|
// 天气特效层(在背景之上,内容之下)- 使用固定大小
|
|
|
|
|
|
WeatherEffectLayer(
|
|
|
|
|
|
weather = weatherInfo.weather,
|
|
|
|
|
|
modifier = Modifier.size(width = 300.dp, height = 60.dp)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-11-27 02:56:42 +00:00
|
|
|
|
Row(
|
2025-11-27 04:32:01 +00:00
|
|
|
|
modifier = Modifier.padding(horizontal = 20.dp, vertical = 12.dp),
|
|
|
|
|
|
horizontalArrangement = Arrangement.spacedBy(20.dp),
|
2025-11-27 02:56:42 +00:00
|
|
|
|
verticalAlignment = Alignment.CenterVertically
|
|
|
|
|
|
) {
|
|
|
|
|
|
// 温度和天气
|
|
|
|
|
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = if (isLoading) "--" else "${weatherInfo.temperature}°C",
|
2025-11-27 04:32:01 +00:00
|
|
|
|
fontSize = 22.sp,
|
2025-11-27 02:56:42 +00:00
|
|
|
|
fontWeight = FontWeight.Bold,
|
2025-11-27 04:32:01 +00:00
|
|
|
|
color = Color.White,
|
|
|
|
|
|
style = androidx.compose.ui.text.TextStyle(
|
|
|
|
|
|
shadow = Shadow(color = Color.Black.copy(alpha = 0.3f), blurRadius = 4f)
|
|
|
|
|
|
)
|
2025-11-27 02:56:42 +00:00
|
|
|
|
)
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = if (isLoading) "加载中" else weatherInfo.weather,
|
2025-11-27 04:32:01 +00:00
|
|
|
|
fontSize = 14.sp,
|
|
|
|
|
|
fontWeight = FontWeight.Medium,
|
|
|
|
|
|
color = Color.White,
|
|
|
|
|
|
style = androidx.compose.ui.text.TextStyle(
|
|
|
|
|
|
shadow = Shadow(color = Color.Black.copy(alpha = 0.3f), blurRadius = 4f)
|
|
|
|
|
|
)
|
2025-11-27 02:56:42 +00:00
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 分隔线
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.width(1.dp)
|
2025-11-27 04:32:01 +00:00
|
|
|
|
.height(36.dp)
|
|
|
|
|
|
.background(Color.White.copy(alpha = 0.3f))
|
2025-11-27 02:56:42 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 湿度
|
|
|
|
|
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = if (isLoading) "--" else "${weatherInfo.humidity}%",
|
2025-11-27 04:32:01 +00:00
|
|
|
|
fontSize = 18.sp,
|
|
|
|
|
|
fontWeight = FontWeight.SemiBold,
|
|
|
|
|
|
color = Color.White,
|
|
|
|
|
|
style = androidx.compose.ui.text.TextStyle(
|
|
|
|
|
|
shadow = Shadow(color = Color.Black.copy(alpha = 0.3f), blurRadius = 4f)
|
|
|
|
|
|
)
|
2025-11-27 02:56:42 +00:00
|
|
|
|
)
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = "湿度",
|
2025-11-27 04:32:01 +00:00
|
|
|
|
fontSize = 13.sp,
|
|
|
|
|
|
color = Color.White.copy(alpha = 0.8f),
|
|
|
|
|
|
style = androidx.compose.ui.text.TextStyle(
|
|
|
|
|
|
shadow = Shadow(color = Color.Black.copy(alpha = 0.3f), blurRadius = 4f)
|
|
|
|
|
|
)
|
2025-11-27 02:56:42 +00:00
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 分隔线
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.width(1.dp)
|
2025-11-27 04:32:01 +00:00
|
|
|
|
.height(36.dp)
|
|
|
|
|
|
.background(Color.White.copy(alpha = 0.3f))
|
2025-11-27 02:56:42 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 空气质量
|
|
|
|
|
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
|
|
|
|
|
val aqiColor = when {
|
2025-11-27 04:32:01 +00:00
|
|
|
|
weatherInfo.aqi <= 50 -> Color(0xFF4CAF50)
|
2025-11-27 02:56:42 +00:00
|
|
|
|
weatherInfo.aqi <= 100 -> Color(0xFFFFEB3B)
|
|
|
|
|
|
weatherInfo.aqi <= 150 -> Color(0xFFFF9800)
|
|
|
|
|
|
else -> Color(0xFFFF5252)
|
|
|
|
|
|
}
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = if (isLoading) "--" else weatherInfo.airQuality,
|
2025-11-27 04:32:01 +00:00
|
|
|
|
fontSize = 18.sp,
|
|
|
|
|
|
fontWeight = FontWeight.SemiBold,
|
|
|
|
|
|
color = aqiColor,
|
|
|
|
|
|
style = androidx.compose.ui.text.TextStyle(
|
|
|
|
|
|
shadow = Shadow(color = Color.Black.copy(alpha = 0.3f), blurRadius = 4f)
|
|
|
|
|
|
)
|
2025-11-27 02:56:42 +00:00
|
|
|
|
)
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = if (isLoading) "AQI" else "AQI ${weatherInfo.aqi}",
|
2025-11-27 04:32:01 +00:00
|
|
|
|
fontSize = 13.sp,
|
|
|
|
|
|
color = Color.White.copy(alpha = 0.8f),
|
|
|
|
|
|
style = androidx.compose.ui.text.TextStyle(
|
|
|
|
|
|
shadow = Shadow(color = Color.Black.copy(alpha = 0.3f), blurRadius = 4f)
|
|
|
|
|
|
)
|
2025-11-27 02:56:42 +00:00
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 城市
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = weatherInfo.city,
|
2025-11-27 04:32:01 +00:00
|
|
|
|
fontSize = 15.sp,
|
|
|
|
|
|
fontWeight = FontWeight.Medium,
|
|
|
|
|
|
color = Color.White,
|
|
|
|
|
|
style = androidx.compose.ui.text.TextStyle(
|
|
|
|
|
|
shadow = Shadow(color = Color.Black.copy(alpha = 0.3f), blurRadius = 4f)
|
|
|
|
|
|
)
|
2025-11-27 02:56:42 +00:00
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-26 02:30:03 +00:00
|
|
|
|
@Composable
|
|
|
|
|
|
fun GradientButton(text: String, onClick: () -> Unit = {}) {
|
|
|
|
|
|
Box(modifier = Modifier.height(36.dp).clip(RoundedCornerShape(20.dp)).background(Brush.linearGradient(listOf(Color(0xFFA9F0FF), Color(0xFFB89CFF)))).clickable(indication = null, interactionSource = remember { MutableInteractionSource() }) { onClick() }.padding(horizontal = 16.dp), contentAlignment = Alignment.Center) {
|
|
|
|
|
|
Text(text = text, color = Color.Black, fontWeight = FontWeight.Medium)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
|
|
fun MyDevicesGrid(selectedRoom: Int, modifier: Modifier = Modifier) {
|
|
|
|
|
|
val devices = when (selectedRoom) {
|
2025-11-26 03:23:00 +00:00
|
|
|
|
1 -> listOf( // 客厅
|
|
|
|
|
|
Device("智能电视", "运行中", R.drawable.ic_media, true),
|
|
|
|
|
|
Device("音响", "待机中", R.drawable.ic_media, false),
|
|
|
|
|
|
Device("空调", "24°C", R.drawable.ic_ac, true),
|
|
|
|
|
|
Device("主灯", "80%", R.drawable.ic_light, true),
|
|
|
|
|
|
Device("窗帘", "已打开", R.drawable.ic_curtain, true),
|
|
|
|
|
|
Device("插座", "运行中", R.drawable.ic_light, true)
|
|
|
|
|
|
)
|
|
|
|
|
|
2 -> listOf( // 厨房
|
|
|
|
|
|
Device("冰箱", "运行中", R.drawable.ic_ac, true),
|
|
|
|
|
|
Device("微波炉", "待机中", R.drawable.ic_media, false),
|
|
|
|
|
|
Device("油烟机", "待机中", R.drawable.ic_media, false),
|
|
|
|
|
|
Device("灯光", "60%", R.drawable.ic_light, true)
|
|
|
|
|
|
)
|
|
|
|
|
|
3 -> listOf( // 卧室
|
|
|
|
|
|
Device("空调", "22°C", R.drawable.ic_ac, true),
|
|
|
|
|
|
Device("床头灯", "40%", R.drawable.ic_light, true),
|
|
|
|
|
|
Device("窗帘", "已关闭", R.drawable.ic_curtain, false),
|
|
|
|
|
|
Device("加湿器", "运行中", R.drawable.ic_media, true),
|
|
|
|
|
|
Device("空气净化器", "运行中", R.drawable.ic_security, true)
|
|
|
|
|
|
)
|
|
|
|
|
|
4 -> listOf( // 影音室
|
|
|
|
|
|
Device("投影仪", "运行中", R.drawable.ic_media, true),
|
|
|
|
|
|
Device("功放", "运行中", R.drawable.ic_media, true),
|
|
|
|
|
|
Device("音响系统", "运行中", R.drawable.ic_media, true),
|
|
|
|
|
|
Device("灯光", "20%", R.drawable.ic_light, true),
|
|
|
|
|
|
Device("窗帘", "已关闭", R.drawable.ic_curtain, false),
|
|
|
|
|
|
Device("空调", "23°C", R.drawable.ic_ac, true)
|
|
|
|
|
|
)
|
|
|
|
|
|
5 -> listOf( // 游戏房
|
|
|
|
|
|
Device("游戏主机", "运行中", R.drawable.ic_media, true),
|
|
|
|
|
|
Device("显示器", "运行中", R.drawable.ic_media, true),
|
|
|
|
|
|
Device("RGB灯带", "运行中", R.drawable.ic_light, true),
|
|
|
|
|
|
Device("空调", "20°C", R.drawable.ic_ac, true),
|
|
|
|
|
|
Device("路由器", "运行中", R.drawable.ic_security, true)
|
2025-11-26 02:30:03 +00:00
|
|
|
|
)
|
|
|
|
|
|
else -> emptyList()
|
|
|
|
|
|
}
|
2025-11-28 11:56:58 +00:00
|
|
|
|
Column(
|
2025-11-26 03:23:00 +00:00
|
|
|
|
modifier = modifier,
|
2025-11-28 11:56:58 +00:00
|
|
|
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
2025-11-26 03:23:00 +00:00
|
|
|
|
) {
|
2025-11-28 11:56:58 +00:00
|
|
|
|
devices.forEach { d -> DeviceCard(d) }
|
2025-11-26 02:30:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-28 11:56:58 +00:00
|
|
|
|
// 各房间的固定能耗数据 (用电量kWh, 运行时长h, 设备数, 趋势, 趋势百分比, 费用, 峰值功率W)
|
|
|
|
|
|
private val roomEnergyData = mapOf(
|
|
|
|
|
|
"客厅" to RoomEnergy(3.2, 8, 6, "↑", 8, 1.8, 320),
|
|
|
|
|
|
"厨房" to RoomEnergy(2.1, 4, 4, "↓", 12, 1.2, 1800),
|
|
|
|
|
|
"卧室" to RoomEnergy(1.5, 6, 5, "↓", 5, 0.9, 180),
|
|
|
|
|
|
"影音室" to RoomEnergy(2.8, 5, 6, "↑", 15, 1.6, 450),
|
|
|
|
|
|
"游戏房" to RoomEnergy(3.5, 7, 5, "↑", 18, 2.0, 380)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
private data class RoomEnergy(
|
|
|
|
|
|
val power: Double, // 今日用电量 kWh
|
|
|
|
|
|
val hours: Int, // 今日运行时长 h
|
|
|
|
|
|
val devices: Int, // 活跃设备数
|
|
|
|
|
|
val trend: String, // 趋势方向
|
|
|
|
|
|
val trendPercent: Int, // 趋势百分比
|
|
|
|
|
|
val cost: Double, // 今日费用 元
|
|
|
|
|
|
val peakWatt: Int // 峰值功率 W
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-11-26 02:30:03 +00:00
|
|
|
|
@Composable
|
2025-11-26 03:23:00 +00:00
|
|
|
|
fun UsageStatusChart(modifier: Modifier = Modifier, roomName: String = "房间") {
|
2025-11-28 11:56:58 +00:00
|
|
|
|
// 判断是否为总览
|
|
|
|
|
|
val isOverview = roomName == tr("room_all") || roomName == "总览" || roomName == "全部"
|
|
|
|
|
|
|
|
|
|
|
|
// 根据房间获取数据,总览时汇总所有房间
|
|
|
|
|
|
val roomData = if (isOverview) {
|
|
|
|
|
|
// 汇总所有房间数据
|
|
|
|
|
|
val allRooms = roomEnergyData.values.toList()
|
|
|
|
|
|
RoomEnergy(
|
|
|
|
|
|
power = allRooms.sumOf { it.power },
|
|
|
|
|
|
hours = allRooms.maxOf { it.hours }, // 取最长运行时间(今日已过时间)
|
|
|
|
|
|
devices = allRooms.sumOf { it.devices },
|
|
|
|
|
|
trend = if (allRooms.count { it.trend == "↑" } > allRooms.size / 2) "↑" else "↓",
|
|
|
|
|
|
trendPercent = allRooms.map { it.trendPercent }.average().toInt(),
|
|
|
|
|
|
cost = allRooms.sumOf { it.cost },
|
|
|
|
|
|
peakWatt = allRooms.maxOf { it.peakWatt } // 取最大峰值
|
|
|
|
|
|
)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
roomEnergyData[roomName] ?: RoomEnergy(2.0, 5, 4, "↓", 8, 1.2, 200)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-26 03:23:00 +00:00
|
|
|
|
val values = remember(roomName) {
|
2025-11-28 11:56:58 +00:00
|
|
|
|
if (isOverview) {
|
|
|
|
|
|
// 总览:生成更高的数值
|
|
|
|
|
|
List(12) { (40..90).random().toFloat() }
|
|
|
|
|
|
} else {
|
|
|
|
|
|
List(12) { (15..45).random().toFloat() }
|
|
|
|
|
|
}
|
2025-11-26 03:23:00 +00:00
|
|
|
|
}
|
2025-11-27 02:56:42 +00:00
|
|
|
|
val maxValue = remember(values) { values.maxOrNull() ?: 45f }
|
2025-11-28 11:56:58 +00:00
|
|
|
|
val totalPower = String.format("%.1f", roomData.power)
|
|
|
|
|
|
val totalHours = roomData.hours
|
|
|
|
|
|
val activeDevices = roomData.devices
|
|
|
|
|
|
val trend = roomData.trend
|
|
|
|
|
|
val trendPercent = roomData.trendPercent
|
|
|
|
|
|
val costToday = String.format("%.1f", roomData.cost)
|
|
|
|
|
|
val peakWatt = roomData.peakWatt
|
2025-11-27 05:36:47 +00:00
|
|
|
|
|
|
|
|
|
|
// 主题色
|
|
|
|
|
|
val accentColor = Color(0xFF00D1FF)
|
2025-11-26 03:23:00 +00:00
|
|
|
|
|
2025-11-27 02:56:42 +00:00
|
|
|
|
Box(
|
|
|
|
|
|
modifier = modifier
|
2025-11-27 05:36:47 +00:00
|
|
|
|
.height(260.dp)
|
2025-11-27 02:56:42 +00:00
|
|
|
|
.clip(RoundedCornerShape(24.dp))
|
|
|
|
|
|
.background(
|
2025-11-27 05:36:47 +00:00
|
|
|
|
Brush.verticalGradient(
|
2025-11-27 02:56:42 +00:00
|
|
|
|
colors = listOf(
|
2025-11-27 05:36:47 +00:00
|
|
|
|
accentColor.copy(alpha = 0.15f),
|
|
|
|
|
|
Color(0xFF1A1A2E).copy(alpha = 0.9f)
|
2025-11-27 02:56:42 +00:00
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
.padding(16.dp)
|
|
|
|
|
|
) {
|
|
|
|
|
|
Column(
|
|
|
|
|
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
|
|
|
|
|
modifier = Modifier.fillMaxSize()
|
|
|
|
|
|
) {
|
|
|
|
|
|
// 标题区域
|
|
|
|
|
|
Row(
|
|
|
|
|
|
modifier = Modifier.fillMaxWidth(),
|
|
|
|
|
|
horizontalArrangement = Arrangement.SpaceBetween,
|
|
|
|
|
|
verticalAlignment = Alignment.CenterVertically
|
|
|
|
|
|
) {
|
2025-11-27 05:36:47 +00:00
|
|
|
|
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
|
|
|
|
// 图标
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.size(36.dp)
|
|
|
|
|
|
.clip(RoundedCornerShape(10.dp))
|
|
|
|
|
|
.background(accentColor.copy(alpha = 0.2f)),
|
|
|
|
|
|
contentAlignment = Alignment.Center
|
|
|
|
|
|
) {
|
|
|
|
|
|
Text(text = "⚡", fontSize = 18.sp)
|
|
|
|
|
|
}
|
|
|
|
|
|
Column {
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = "能耗统计",
|
|
|
|
|
|
fontWeight = FontWeight.SemiBold,
|
|
|
|
|
|
color = Color.White,
|
|
|
|
|
|
fontSize = 16.sp
|
|
|
|
|
|
)
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = roomName,
|
|
|
|
|
|
fontSize = 11.sp,
|
|
|
|
|
|
color = Color(0xFF9AA0A6)
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
2025-11-27 02:56:42 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 趋势指示器
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
2025-11-27 05:36:47 +00:00
|
|
|
|
.clip(RoundedCornerShape(8.dp))
|
2025-11-27 02:56:42 +00:00
|
|
|
|
.background(
|
|
|
|
|
|
if (trend == "↑") Color(0x33FF5252) else Color(0x3300E676)
|
|
|
|
|
|
)
|
2025-11-27 05:36:47 +00:00
|
|
|
|
.padding(horizontal = 8.dp, vertical = 4.dp)
|
2025-11-27 02:56:42 +00:00
|
|
|
|
) {
|
2025-11-27 05:36:47 +00:00
|
|
|
|
Text(
|
|
|
|
|
|
text = "$trend${trendPercent}%",
|
|
|
|
|
|
color = if (trend == "↑") Color(0xFFFF5252) else Color(0xFF00E676),
|
|
|
|
|
|
fontSize = 12.sp,
|
|
|
|
|
|
fontWeight = FontWeight.Bold
|
|
|
|
|
|
)
|
2025-11-27 02:56:42 +00:00
|
|
|
|
}
|
2025-11-26 03:23:00 +00:00
|
|
|
|
}
|
2025-11-27 02:56:42 +00:00
|
|
|
|
|
2025-11-27 05:36:47 +00:00
|
|
|
|
// 主数据显示
|
2025-11-27 02:56:42 +00:00
|
|
|
|
Row(
|
|
|
|
|
|
modifier = Modifier.fillMaxWidth(),
|
2025-11-27 05:36:47 +00:00
|
|
|
|
horizontalArrangement = Arrangement.SpaceBetween,
|
|
|
|
|
|
verticalAlignment = Alignment.Bottom
|
2025-11-27 02:56:42 +00:00
|
|
|
|
) {
|
2025-11-27 05:36:47 +00:00
|
|
|
|
// 今日用电
|
|
|
|
|
|
Column {
|
|
|
|
|
|
Row(verticalAlignment = Alignment.Top) {
|
2025-11-27 02:56:42 +00:00
|
|
|
|
Text(
|
|
|
|
|
|
text = totalPower,
|
2025-11-27 05:36:47 +00:00
|
|
|
|
fontSize = 42.sp,
|
|
|
|
|
|
color = Color.White,
|
|
|
|
|
|
fontWeight = FontWeight.Light
|
2025-11-27 02:56:42 +00:00
|
|
|
|
)
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = "kWh",
|
2025-11-27 05:36:47 +00:00
|
|
|
|
fontSize = 14.sp,
|
|
|
|
|
|
color = Color.White.copy(alpha = 0.6f),
|
|
|
|
|
|
modifier = Modifier.padding(top = 10.dp, start = 4.dp)
|
2025-11-27 02:56:42 +00:00
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
Text(
|
2025-11-27 05:36:47 +00:00
|
|
|
|
text = "今日用电",
|
|
|
|
|
|
fontSize = 12.sp,
|
|
|
|
|
|
color = Color(0xFF9AA0A6)
|
2025-11-27 02:56:42 +00:00
|
|
|
|
)
|
2025-11-26 03:23:00 +00:00
|
|
|
|
}
|
2025-11-27 02:56:42 +00:00
|
|
|
|
|
2025-11-27 05:36:47 +00:00
|
|
|
|
// 预计费用
|
|
|
|
|
|
Column(horizontalAlignment = Alignment.End) {
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = "¥$costToday",
|
|
|
|
|
|
fontSize = 20.sp,
|
|
|
|
|
|
color = accentColor,
|
|
|
|
|
|
fontWeight = FontWeight.Bold
|
|
|
|
|
|
)
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = "预计费用",
|
|
|
|
|
|
fontSize = 11.sp,
|
|
|
|
|
|
color = Color(0xFF9AA0A6)
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 详细数据行
|
|
|
|
|
|
Row(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.fillMaxWidth()
|
|
|
|
|
|
.clip(RoundedCornerShape(12.dp))
|
|
|
|
|
|
.background(Color(0xFF2A2A3E).copy(alpha = 0.5f))
|
|
|
|
|
|
.padding(12.dp),
|
|
|
|
|
|
horizontalArrangement = Arrangement.SpaceEvenly
|
|
|
|
|
|
) {
|
2025-11-27 02:56:42 +00:00
|
|
|
|
// 运行时长
|
2025-11-27 05:36:47 +00:00
|
|
|
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
2025-11-27 02:56:42 +00:00
|
|
|
|
Text(
|
2025-11-27 05:36:47 +00:00
|
|
|
|
text = "$totalHours",
|
|
|
|
|
|
color = Color(0xFFB89CFF),
|
|
|
|
|
|
fontSize = 18.sp,
|
|
|
|
|
|
fontWeight = FontWeight.Bold
|
2025-11-27 02:56:42 +00:00
|
|
|
|
)
|
2025-11-27 05:36:47 +00:00
|
|
|
|
Text(text = "小时", fontSize = 10.sp, color = Color(0xFF9AA0A6))
|
2025-11-27 02:56:42 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-27 05:36:47 +00:00
|
|
|
|
// 分隔线
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.width(1.dp)
|
|
|
|
|
|
.height(30.dp)
|
|
|
|
|
|
.background(Color(0xFF3A3A4E))
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-11-27 02:56:42 +00:00
|
|
|
|
// 活跃设备
|
2025-11-27 05:36:47 +00:00
|
|
|
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
2025-11-27 02:56:42 +00:00
|
|
|
|
Text(
|
2025-11-27 05:36:47 +00:00
|
|
|
|
text = "$activeDevices",
|
|
|
|
|
|
color = Color(0xFF00E676),
|
|
|
|
|
|
fontSize = 18.sp,
|
|
|
|
|
|
fontWeight = FontWeight.Bold
|
2025-11-27 02:56:42 +00:00
|
|
|
|
)
|
2025-11-27 05:36:47 +00:00
|
|
|
|
Text(text = "设备", fontSize = 10.sp, color = Color(0xFF9AA0A6))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 分隔线
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.width(1.dp)
|
|
|
|
|
|
.height(30.dp)
|
|
|
|
|
|
.background(Color(0xFF3A3A4E))
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 峰值功率
|
|
|
|
|
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
|
|
|
|
|
Text(
|
2025-11-28 11:56:58 +00:00
|
|
|
|
text = "$peakWatt",
|
2025-11-27 05:36:47 +00:00
|
|
|
|
color = Color(0xFFFFB74D),
|
|
|
|
|
|
fontSize = 18.sp,
|
|
|
|
|
|
fontWeight = FontWeight.Bold
|
|
|
|
|
|
)
|
|
|
|
|
|
Text(text = "W峰值", fontSize = 10.sp, color = Color(0xFF9AA0A6))
|
2025-11-26 03:23:00 +00:00
|
|
|
|
}
|
2025-11-26 02:30:03 +00:00
|
|
|
|
}
|
2025-11-27 02:56:42 +00:00
|
|
|
|
|
|
|
|
|
|
// 图表区域
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.fillMaxWidth()
|
|
|
|
|
|
.weight(1f)
|
|
|
|
|
|
) {
|
|
|
|
|
|
Row(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.fillMaxWidth()
|
|
|
|
|
|
.align(Alignment.BottomCenter),
|
2025-11-27 05:36:47 +00:00
|
|
|
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
2025-11-27 02:56:42 +00:00
|
|
|
|
verticalAlignment = Alignment.Bottom
|
|
|
|
|
|
) {
|
|
|
|
|
|
values.forEachIndexed { index, v ->
|
2025-11-27 05:36:47 +00:00
|
|
|
|
val normalizedHeight = (v / maxValue) * 50
|
2025-11-27 02:56:42 +00:00
|
|
|
|
val isHighlight = index == values.size - 1
|
|
|
|
|
|
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.weight(1f)
|
|
|
|
|
|
.height(normalizedHeight.dp)
|
2025-11-27 05:36:47 +00:00
|
|
|
|
.clip(RoundedCornerShape(topStart = 3.dp, topEnd = 3.dp))
|
2025-11-27 02:56:42 +00:00
|
|
|
|
.background(
|
|
|
|
|
|
if (isHighlight) {
|
|
|
|
|
|
Brush.verticalGradient(
|
2025-11-27 05:36:47 +00:00
|
|
|
|
colors = listOf(accentColor, accentColor.copy(alpha = 0.5f))
|
2025-11-27 02:56:42 +00:00
|
|
|
|
)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Brush.verticalGradient(
|
2025-11-27 05:36:47 +00:00
|
|
|
|
colors = listOf(Color(0xFF4A4A5A), Color(0xFF3A3A4A))
|
2025-11-27 02:56:42 +00:00
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-26 02:30:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Composable
|
2025-11-26 03:23:00 +00:00
|
|
|
|
fun AirConditionerCard(modifier: Modifier = Modifier, roomName: String = "房间") {
|
2025-11-26 02:30:03 +00:00
|
|
|
|
var temp by remember { mutableStateOf(24f) }
|
2025-11-26 03:23:00 +00:00
|
|
|
|
var isOn by remember { mutableStateOf(true) }
|
2025-11-27 05:36:47 +00:00
|
|
|
|
var selectedMode by remember { mutableStateOf(0) } // 0:制冷 1:制热 2:除湿 3:送风
|
|
|
|
|
|
var fanSpeed by remember { mutableStateOf(1) } // 0:自动 1:低 2:中 3:高
|
2025-11-26 03:23:00 +00:00
|
|
|
|
|
2025-11-27 08:00:44 +00:00
|
|
|
|
// 模式emoji图标
|
|
|
|
|
|
val modeIcons = listOf(
|
2025-11-27 08:16:34 +00:00
|
|
|
|
"❄️" to tr("ac_mode_cool"),
|
|
|
|
|
|
"☀️" to tr("ac_mode_heat"),
|
|
|
|
|
|
"💧" to tr("ac_mode_dry"),
|
|
|
|
|
|
"🌀" to tr("ac_mode_fan")
|
2025-11-27 08:00:44 +00:00
|
|
|
|
)
|
2025-11-28 05:21:09 +00:00
|
|
|
|
|
|
|
|
|
|
// 风速旋转速度(根据档位变化)
|
|
|
|
|
|
val fanRotationDuration = when (fanSpeed) {
|
|
|
|
|
|
0 -> 2000 // 自动
|
|
|
|
|
|
1 -> 3000 // 低速
|
|
|
|
|
|
2 -> 1500 // 中速
|
|
|
|
|
|
else -> 800 // 高速
|
|
|
|
|
|
}
|
2025-11-27 05:36:47 +00:00
|
|
|
|
|
2025-11-27 08:00:44 +00:00
|
|
|
|
// 图标动画
|
|
|
|
|
|
val infiniteTransition = rememberInfiniteTransition(label = "ac_icon")
|
|
|
|
|
|
|
|
|
|
|
|
// 旋转动画(制冷、制热、送风)
|
|
|
|
|
|
val iconRotation by infiniteTransition.animateFloat(
|
|
|
|
|
|
initialValue = 0f,
|
|
|
|
|
|
targetValue = 360f,
|
|
|
|
|
|
animationSpec = infiniteRepeatable(
|
|
|
|
|
|
animation = tween(durationMillis = 3000, easing = LinearEasing),
|
|
|
|
|
|
repeatMode = RepeatMode.Restart
|
|
|
|
|
|
),
|
|
|
|
|
|
label = "icon_rotation"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 水滴下落动画(除湿)
|
|
|
|
|
|
val dropOffset by infiniteTransition.animateFloat(
|
|
|
|
|
|
initialValue = -2f,
|
|
|
|
|
|
targetValue = 2f,
|
|
|
|
|
|
animationSpec = infiniteRepeatable(
|
|
|
|
|
|
animation = tween(durationMillis = 800, easing = androidx.compose.animation.core.EaseInOut),
|
|
|
|
|
|
repeatMode = RepeatMode.Reverse
|
|
|
|
|
|
),
|
|
|
|
|
|
label = "drop_offset"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-11-28 05:21:09 +00:00
|
|
|
|
// 风扇旋转动画
|
|
|
|
|
|
val fanRotation by infiniteTransition.animateFloat(
|
|
|
|
|
|
initialValue = 0f,
|
|
|
|
|
|
targetValue = 360f,
|
|
|
|
|
|
animationSpec = infiniteRepeatable(
|
|
|
|
|
|
animation = tween(durationMillis = fanRotationDuration, easing = LinearEasing),
|
|
|
|
|
|
repeatMode = RepeatMode.Restart
|
|
|
|
|
|
),
|
|
|
|
|
|
label = "fan_rotation"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 风速标签
|
|
|
|
|
|
val fanSpeedLabel = when (fanSpeed) {
|
|
|
|
|
|
0 -> tr("ac_fan_auto")
|
|
|
|
|
|
1 -> tr("ac_fan_low")
|
|
|
|
|
|
2 -> tr("ac_fan_medium")
|
|
|
|
|
|
else -> tr("ac_fan_high")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-27 05:36:47 +00:00
|
|
|
|
// 根据模式决定主题色
|
|
|
|
|
|
val accentColor = when (selectedMode) {
|
|
|
|
|
|
0 -> Color(0xFF4FC3F7) // 制冷 - 蓝色
|
|
|
|
|
|
1 -> Color(0xFFFF8A65) // 制热 - 橙色
|
|
|
|
|
|
2 -> Color(0xFF81C784) // 除湿 - 绿色
|
|
|
|
|
|
else -> Color(0xFFB0BEC5) // 送风 - 灰色
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-27 08:00:44 +00:00
|
|
|
|
// 当前模式的emoji
|
|
|
|
|
|
val currentEmoji = modeIcons[selectedMode].first
|
|
|
|
|
|
|
2025-11-27 05:36:47 +00:00
|
|
|
|
Box(
|
|
|
|
|
|
modifier = modifier
|
|
|
|
|
|
.height(260.dp)
|
|
|
|
|
|
.clip(RoundedCornerShape(24.dp))
|
|
|
|
|
|
.background(
|
|
|
|
|
|
Brush.verticalGradient(
|
|
|
|
|
|
colors = if (isOn) listOf(
|
|
|
|
|
|
accentColor.copy(alpha = 0.3f),
|
|
|
|
|
|
Color(0xFF1A1A2E).copy(alpha = 0.9f)
|
|
|
|
|
|
) else listOf(
|
|
|
|
|
|
Color(0xFF2A2A3E).copy(alpha = 0.6f),
|
|
|
|
|
|
Color(0xFF1A1A2E).copy(alpha = 0.9f)
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
.padding(16.dp)
|
|
|
|
|
|
) {
|
|
|
|
|
|
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
|
|
|
|
|
// 顶部:标题和开关
|
|
|
|
|
|
Row(
|
|
|
|
|
|
horizontalArrangement = Arrangement.SpaceBetween,
|
|
|
|
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
|
|
|
|
modifier = Modifier.fillMaxWidth()
|
|
|
|
|
|
) {
|
|
|
|
|
|
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
2025-11-27 08:00:44 +00:00
|
|
|
|
// 空调图标 - 根据模式应用不同动画
|
2025-11-27 05:36:47 +00:00
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.size(36.dp)
|
|
|
|
|
|
.clip(RoundedCornerShape(10.dp))
|
|
|
|
|
|
.background(accentColor.copy(alpha = if (isOn) 0.3f else 0.1f)),
|
|
|
|
|
|
contentAlignment = Alignment.Center
|
|
|
|
|
|
) {
|
2025-11-27 08:00:44 +00:00
|
|
|
|
// 根据模式选择动画:除湿用上下移动,其他用旋转
|
|
|
|
|
|
val emojiModifier = when {
|
|
|
|
|
|
!isOn -> Modifier
|
|
|
|
|
|
selectedMode == 2 -> Modifier.offset(y = dropOffset.dp) // 除湿:上下移动
|
|
|
|
|
|
else -> Modifier.rotate(iconRotation) // 其他:旋转
|
|
|
|
|
|
}
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = currentEmoji,
|
|
|
|
|
|
fontSize = 18.sp,
|
|
|
|
|
|
modifier = emojiModifier
|
|
|
|
|
|
)
|
2025-11-27 05:36:47 +00:00
|
|
|
|
}
|
|
|
|
|
|
Column {
|
2025-11-27 08:16:34 +00:00
|
|
|
|
Text(text = tr("ac_title"), fontWeight = FontWeight.SemiBold, color = Color.White, fontSize = 16.sp)
|
2025-11-27 05:36:47 +00:00
|
|
|
|
Text(text = roomName, fontSize = 11.sp, color = Color(0xFF9AA0A6))
|
|
|
|
|
|
}
|
2025-11-26 03:23:00 +00:00
|
|
|
|
}
|
2025-11-27 05:36:47 +00:00
|
|
|
|
Switch(
|
|
|
|
|
|
checked = isOn,
|
|
|
|
|
|
onCheckedChange = { isOn = it },
|
|
|
|
|
|
colors = SwitchDefaults.colors(
|
|
|
|
|
|
checkedThumbColor = Color.White,
|
|
|
|
|
|
checkedTrackColor = accentColor,
|
|
|
|
|
|
uncheckedThumbColor = Color.Gray,
|
|
|
|
|
|
uncheckedTrackColor = Color(0xFF3A3A4E)
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
2025-11-26 02:30:03 +00:00
|
|
|
|
}
|
2025-11-26 03:43:39 +00:00
|
|
|
|
|
2025-11-27 05:36:47 +00:00
|
|
|
|
// 温度显示和调节
|
2025-11-26 03:43:39 +00:00
|
|
|
|
Row(
|
|
|
|
|
|
modifier = Modifier.fillMaxWidth(),
|
|
|
|
|
|
horizontalArrangement = Arrangement.SpaceBetween,
|
|
|
|
|
|
verticalAlignment = Alignment.CenterVertically
|
|
|
|
|
|
) {
|
2025-11-27 05:36:47 +00:00
|
|
|
|
// 大温度显示
|
|
|
|
|
|
Row(verticalAlignment = Alignment.Top) {
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = "${temp.toInt()}",
|
|
|
|
|
|
fontSize = 52.sp,
|
|
|
|
|
|
color = if (isOn) Color.White else Color.Gray,
|
|
|
|
|
|
fontWeight = FontWeight.Light
|
|
|
|
|
|
)
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = "°C",
|
|
|
|
|
|
fontSize = 20.sp,
|
|
|
|
|
|
color = if (isOn) Color.White.copy(alpha = 0.7f) else Color.Gray,
|
|
|
|
|
|
modifier = Modifier.padding(top = 8.dp)
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
2025-11-26 03:43:39 +00:00
|
|
|
|
|
2025-11-28 05:21:09 +00:00
|
|
|
|
// 风速按钮 + 温度调节按钮
|
|
|
|
|
|
Row(
|
|
|
|
|
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
|
|
|
|
verticalAlignment = Alignment.CenterVertically
|
|
|
|
|
|
) {
|
|
|
|
|
|
// 风速按钮(点击循环切换)
|
|
|
|
|
|
Column(
|
|
|
|
|
|
horizontalAlignment = Alignment.CenterHorizontally,
|
2025-11-27 05:36:47 +00:00
|
|
|
|
modifier = Modifier
|
2025-11-28 05:21:09 +00:00
|
|
|
|
.clip(RoundedCornerShape(12.dp))
|
|
|
|
|
|
.background(if (isOn) accentColor.copy(alpha = 0.2f) else Color(0xFF3A3A4E))
|
|
|
|
|
|
.clickable(enabled = isOn) { fanSpeed = (fanSpeed + 1) % 4 }
|
|
|
|
|
|
.padding(horizontal = 12.dp, vertical = 8.dp)
|
2025-11-27 05:36:47 +00:00
|
|
|
|
) {
|
2025-11-28 05:21:09 +00:00
|
|
|
|
// 风扇图标(根据风速显示不同图片,带旋转动画)
|
|
|
|
|
|
val fanIconRes = when (fanSpeed) {
|
|
|
|
|
|
1 -> R.drawable.fan_low // 低速 - 3叶片
|
|
|
|
|
|
2 -> R.drawable.fan_medium // 中速 - 4叶片
|
|
|
|
|
|
3 -> R.drawable.fan_high // 高速 - 5叶片
|
|
|
|
|
|
else -> R.drawable.fan_medium // 自动 - 默认4叶片
|
|
|
|
|
|
}
|
|
|
|
|
|
Image(
|
|
|
|
|
|
painter = painterResource(id = fanIconRes),
|
|
|
|
|
|
contentDescription = "Fan Speed",
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.size(24.dp)
|
|
|
|
|
|
.then(if (isOn) Modifier.rotate(fanRotation) else Modifier)
|
|
|
|
|
|
)
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = fanSpeedLabel,
|
|
|
|
|
|
fontSize = 10.sp,
|
|
|
|
|
|
color = if (isOn) Color.White else Color.Gray,
|
|
|
|
|
|
maxLines = 1
|
|
|
|
|
|
)
|
2025-11-27 05:36:47 +00:00
|
|
|
|
}
|
2025-11-28 05:21:09 +00:00
|
|
|
|
|
|
|
|
|
|
// 温度调节按钮
|
|
|
|
|
|
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
|
|
|
|
|
// 升温按钮
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.size(36.dp)
|
|
|
|
|
|
.clip(RoundedCornerShape(10.dp))
|
|
|
|
|
|
.background(if (isOn) accentColor.copy(alpha = 0.3f) else Color(0xFF3A3A4E))
|
|
|
|
|
|
.clickable(enabled = isOn && temp < 32f) { temp += 1f },
|
|
|
|
|
|
contentAlignment = Alignment.Center
|
|
|
|
|
|
) {
|
|
|
|
|
|
Text(text = "▲", color = if (isOn) Color.White else Color.Gray, fontSize = 14.sp)
|
|
|
|
|
|
}
|
|
|
|
|
|
// 降温按钮
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.size(36.dp)
|
|
|
|
|
|
.clip(RoundedCornerShape(10.dp))
|
|
|
|
|
|
.background(if (isOn) accentColor.copy(alpha = 0.3f) else Color(0xFF3A3A4E))
|
|
|
|
|
|
.clickable(enabled = isOn && temp > 16f) { temp -= 1f },
|
|
|
|
|
|
contentAlignment = Alignment.Center
|
|
|
|
|
|
) {
|
|
|
|
|
|
Text(text = "▼", color = if (isOn) Color.White else Color.Gray, fontSize = 14.sp)
|
|
|
|
|
|
}
|
2025-11-26 03:43:39 +00:00
|
|
|
|
}
|
2025-11-27 05:36:47 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 模式选择
|
|
|
|
|
|
Row(
|
|
|
|
|
|
modifier = Modifier.fillMaxWidth(),
|
|
|
|
|
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
|
|
|
|
) {
|
2025-11-27 08:00:44 +00:00
|
|
|
|
modeIcons.forEachIndexed { index, (emoji, name) ->
|
2025-11-27 05:36:47 +00:00
|
|
|
|
val isSelected = selectedMode == index
|
2025-11-27 08:00:44 +00:00
|
|
|
|
val modeColor = when (index) {
|
|
|
|
|
|
0 -> Color(0xFF4FC3F7)
|
|
|
|
|
|
1 -> Color(0xFFFF8A65)
|
|
|
|
|
|
2 -> Color(0xFF81C784)
|
|
|
|
|
|
else -> Color(0xFFB0BEC5)
|
|
|
|
|
|
}
|
2025-11-26 03:43:39 +00:00
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
2025-11-27 05:36:47 +00:00
|
|
|
|
.weight(1f)
|
2025-11-28 05:21:09 +00:00
|
|
|
|
.heightIn(min = 44.dp)
|
2025-11-26 03:43:39 +00:00
|
|
|
|
.clip(RoundedCornerShape(12.dp))
|
2025-11-27 05:36:47 +00:00
|
|
|
|
.background(
|
2025-11-27 08:00:44 +00:00
|
|
|
|
if (isSelected && isOn) modeColor.copy(alpha = 0.4f)
|
2025-11-27 05:36:47 +00:00
|
|
|
|
else Color(0xFF2A2A3E).copy(alpha = 0.6f)
|
|
|
|
|
|
)
|
2025-11-28 05:21:09 +00:00
|
|
|
|
.clickable(enabled = isOn) { selectedMode = index }
|
|
|
|
|
|
.padding(vertical = 4.dp),
|
2025-11-26 03:43:39 +00:00
|
|
|
|
contentAlignment = Alignment.Center
|
|
|
|
|
|
) {
|
2025-11-27 05:36:47 +00:00
|
|
|
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
2025-11-27 08:00:44 +00:00
|
|
|
|
Text(text = emoji, fontSize = 14.sp)
|
2025-11-27 05:36:47 +00:00
|
|
|
|
Text(
|
|
|
|
|
|
text = name,
|
|
|
|
|
|
fontSize = 10.sp,
|
2025-11-28 05:21:09 +00:00
|
|
|
|
color = if (isSelected && isOn) Color.White else Color(0xFF9AA0A6),
|
|
|
|
|
|
maxLines = 1
|
2025-11-27 05:36:47 +00:00
|
|
|
|
)
|
|
|
|
|
|
}
|
2025-11-26 03:43:39 +00:00
|
|
|
|
}
|
2025-11-27 05:36:47 +00:00
|
|
|
|
}
|
2025-11-26 02:30:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-26 03:43:39 +00:00
|
|
|
|
@OptIn(androidx.compose.material3.ExperimentalMaterial3Api::class)
|
|
|
|
|
|
@Composable
|
|
|
|
|
|
fun CustomSlider(
|
|
|
|
|
|
value: Float,
|
|
|
|
|
|
onValueChange: (Float) -> Unit,
|
|
|
|
|
|
valueRange: ClosedFloatingPointRange<Float>,
|
|
|
|
|
|
enabled: Boolean = true
|
|
|
|
|
|
) {
|
|
|
|
|
|
val interactionSource = remember { MutableInteractionSource() }
|
|
|
|
|
|
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.fillMaxWidth()
|
|
|
|
|
|
.height(32.dp),
|
|
|
|
|
|
contentAlignment = Alignment.Center
|
|
|
|
|
|
) {
|
|
|
|
|
|
Slider(
|
|
|
|
|
|
value = value,
|
|
|
|
|
|
onValueChange = onValueChange,
|
|
|
|
|
|
valueRange = valueRange,
|
|
|
|
|
|
enabled = enabled,
|
|
|
|
|
|
interactionSource = interactionSource,
|
|
|
|
|
|
colors = androidx.compose.material3.SliderDefaults.colors(
|
|
|
|
|
|
thumbColor = Color.White,
|
|
|
|
|
|
activeTrackColor = Brush.linearGradient(
|
|
|
|
|
|
listOf(Color(0xFF00D1FF), Color(0xFFA9F0FF))
|
|
|
|
|
|
).let { Color(0xFF00D1FF) },
|
2025-11-27 02:56:42 +00:00
|
|
|
|
inactiveTrackColor = Color(0x55FFFFFF),
|
2025-11-26 03:43:39 +00:00
|
|
|
|
disabledThumbColor = Color(0x66FFFFFF),
|
2025-11-27 02:56:42 +00:00
|
|
|
|
disabledActiveTrackColor = Color(0x55FFFFFF),
|
|
|
|
|
|
disabledInactiveTrackColor = Color(0x55FFFFFF)
|
2025-11-26 03:43:39 +00:00
|
|
|
|
),
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.fillMaxWidth()
|
|
|
|
|
|
.height(24.dp),
|
|
|
|
|
|
thumb = {
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.size(20.dp)
|
|
|
|
|
|
.shadow(4.dp, RoundedCornerShape(10.dp))
|
|
|
|
|
|
.clip(RoundedCornerShape(10.dp))
|
|
|
|
|
|
.background(Color.White)
|
|
|
|
|
|
)
|
|
|
|
|
|
},
|
|
|
|
|
|
track = { sliderState ->
|
|
|
|
|
|
val fraction = (sliderState.value - valueRange.start) / (valueRange.endInclusive - valueRange.start)
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.fillMaxWidth()
|
|
|
|
|
|
.height(6.dp)
|
|
|
|
|
|
.clip(RoundedCornerShape(3.dp))
|
|
|
|
|
|
) {
|
|
|
|
|
|
// 未激活轨道
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.fillMaxSize()
|
2025-11-27 02:56:42 +00:00
|
|
|
|
.background(if (enabled) Color(0x55FFFFFF) else Color(0x55FFFFFF))
|
2025-11-26 03:43:39 +00:00
|
|
|
|
)
|
|
|
|
|
|
// 激活轨道
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.fillMaxWidth(fraction)
|
|
|
|
|
|
.fillMaxHeight()
|
|
|
|
|
|
.background(
|
|
|
|
|
|
if (enabled) {
|
|
|
|
|
|
Brush.linearGradient(
|
|
|
|
|
|
listOf(Color(0xFF00D1FF), Color(0xFFA9F0FF))
|
|
|
|
|
|
)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Brush.linearGradient(
|
2025-11-27 02:56:42 +00:00
|
|
|
|
listOf(Color(0x55FFFFFF), Color(0x55FFFFFF))
|
2025-11-26 03:43:39 +00:00
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-26 02:30:03 +00:00
|
|
|
|
@Composable
|
|
|
|
|
|
fun LightList(modifier: Modifier = Modifier) {
|
|
|
|
|
|
val lights = listOf(
|
|
|
|
|
|
"灯光 1" to 0.6f,
|
|
|
|
|
|
"灯光 2" to 0.8f,
|
|
|
|
|
|
"灯光 3" to 0.45f,
|
|
|
|
|
|
"灯光 4" to 0.6f,
|
|
|
|
|
|
"灯光 5" to 0.6f
|
|
|
|
|
|
)
|
|
|
|
|
|
Column(modifier = modifier.fillMaxHeight(), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
|
|
|
|
|
Text(text = "灯光", fontWeight = FontWeight.SemiBold)
|
|
|
|
|
|
lights.forEach { (name, percent) ->
|
2025-11-27 02:56:42 +00:00
|
|
|
|
Row(horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.clip(RoundedCornerShape(16.dp)).background(Color(0x50121212)).padding(12.dp)) {
|
2025-11-26 02:30:03 +00:00
|
|
|
|
Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
2025-11-27 02:56:42 +00:00
|
|
|
|
Box(modifier = Modifier.size(36.dp).clip(RoundedCornerShape(10.dp)).background(Color(0x50121212)))
|
2025-11-26 02:30:03 +00:00
|
|
|
|
Column { Text(text = name); Text(text = "${(percent * 100).toInt()}%", color = Color(0xFF9AA0A6)) }
|
|
|
|
|
|
}
|
|
|
|
|
|
DotsProgress(percent)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Composable
|
2025-11-26 03:43:39 +00:00
|
|
|
|
fun DotsProgress(percent: Float, onPercentChange: (Float) -> Unit = {}, enabled: Boolean = true) {
|
2025-11-26 02:30:03 +00:00
|
|
|
|
val total = 12
|
2025-11-27 06:15:31 +00:00
|
|
|
|
|
2025-11-26 03:43:39 +00:00
|
|
|
|
Row(
|
|
|
|
|
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
|
|
|
|
|
verticalAlignment = Alignment.CenterVertically,
|
2025-11-27 06:15:31 +00:00
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.pointerInput(enabled) {
|
|
|
|
|
|
if (enabled) {
|
|
|
|
|
|
val dotWidth = 8.dp.toPx()
|
2025-11-26 03:43:39 +00:00
|
|
|
|
val spacing = 4.dp.toPx()
|
|
|
|
|
|
val totalWidth = (dotWidth + spacing) * total - spacing
|
2025-11-27 06:15:31 +00:00
|
|
|
|
|
|
|
|
|
|
// 支持拖动手势
|
|
|
|
|
|
detectHorizontalDragGestures(
|
|
|
|
|
|
onDragStart = { offset ->
|
|
|
|
|
|
// 开始拖动时立即更新
|
|
|
|
|
|
val newPercent = (offset.x / totalWidth).coerceIn(0f, 1f)
|
|
|
|
|
|
onPercentChange(newPercent)
|
|
|
|
|
|
},
|
|
|
|
|
|
onDragEnd = { },
|
|
|
|
|
|
onDragCancel = { },
|
|
|
|
|
|
onHorizontalDrag = { change, _ ->
|
|
|
|
|
|
change.consume()
|
|
|
|
|
|
val newPercent = (change.position.x / totalWidth).coerceIn(0f, 1f)
|
|
|
|
|
|
onPercentChange(newPercent)
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.pointerInput(enabled) {
|
|
|
|
|
|
if (enabled) {
|
|
|
|
|
|
val dotWidth = 8.dp.toPx()
|
|
|
|
|
|
val spacing = 4.dp.toPx()
|
|
|
|
|
|
val totalWidth = (dotWidth + spacing) * total - spacing
|
|
|
|
|
|
|
|
|
|
|
|
// 也支持点击
|
|
|
|
|
|
detectTapGestures { offset ->
|
|
|
|
|
|
val newPercent = (offset.x / totalWidth).coerceIn(0f, 1f)
|
|
|
|
|
|
onPercentChange(newPercent)
|
|
|
|
|
|
}
|
2025-11-26 03:43:39 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
) {
|
2025-11-26 02:30:03 +00:00
|
|
|
|
repeat(total) { i ->
|
|
|
|
|
|
val filled = i < (percent * total).toInt()
|
2025-11-26 03:43:39 +00:00
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.size(8.dp)
|
|
|
|
|
|
.clip(RoundedCornerShape(4.dp))
|
|
|
|
|
|
.background(if (filled) Color(0xFFA9F0FF) else Color(0xFF3A3A3A))
|
|
|
|
|
|
)
|
2025-11-26 02:30:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
enum class Mode { HOME, AWAY, FUN }
|
|
|
|
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
|
|
fun ModeButtonsRow(onModeSelected: (Mode) -> Unit) {
|
|
|
|
|
|
BoxWithConstraints(modifier = Modifier.fillMaxWidth()) {
|
2025-11-28 11:56:58 +00:00
|
|
|
|
// 固定高度,确保在滚动前完全显示
|
|
|
|
|
|
val buttonHeight = 100.dp
|
2025-11-26 02:30:03 +00:00
|
|
|
|
|
|
|
|
|
|
var selected by remember { mutableStateOf<Mode?>(null) }
|
|
|
|
|
|
|
|
|
|
|
|
val isVertical = maxWidth < 480.dp
|
|
|
|
|
|
val containerModifier = Modifier.fillMaxWidth()
|
|
|
|
|
|
val arrangement = Arrangement.spacedBy(16.dp)
|
|
|
|
|
|
|
|
|
|
|
|
val items = listOf(
|
|
|
|
|
|
Triple(Mode.HOME, Brush.linearGradient(listOf(Color(0xFF00D1FF), Color(0xFF00C9A7))), "回家"),
|
|
|
|
|
|
Triple(Mode.AWAY, Brush.linearGradient(listOf(Color(0xFFFF8A65), Color(0xFFFF7043))), "出门"),
|
|
|
|
|
|
Triple(Mode.FUN, Brush.linearGradient(listOf(Color(0xFF6A3DFF), Color(0xFF3A0CA3))), "玩乐"),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if (isVertical) {
|
|
|
|
|
|
Column(modifier = containerModifier, verticalArrangement = arrangement) {
|
|
|
|
|
|
items.forEach { (mode, brush, label) ->
|
|
|
|
|
|
ModeButton(
|
|
|
|
|
|
text = label,
|
|
|
|
|
|
modifier = Modifier.fillMaxWidth(),
|
|
|
|
|
|
height = buttonHeight,
|
|
|
|
|
|
brush = brush,
|
|
|
|
|
|
selected = selected == mode,
|
|
|
|
|
|
onClick = {
|
|
|
|
|
|
selected = mode
|
|
|
|
|
|
onModeSelected(mode)
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Row(modifier = containerModifier, horizontalArrangement = arrangement) {
|
|
|
|
|
|
items.forEach { (mode, brush, label) ->
|
|
|
|
|
|
ModeButton(
|
|
|
|
|
|
text = label,
|
|
|
|
|
|
modifier = Modifier.weight(1f),
|
|
|
|
|
|
height = buttonHeight,
|
|
|
|
|
|
brush = brush,
|
|
|
|
|
|
selected = selected == mode,
|
|
|
|
|
|
onClick = {
|
|
|
|
|
|
selected = mode
|
|
|
|
|
|
onModeSelected(mode)
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
|
|
fun ModeButton(
|
|
|
|
|
|
text: String,
|
|
|
|
|
|
modifier: Modifier,
|
|
|
|
|
|
height: androidx.compose.ui.unit.Dp,
|
|
|
|
|
|
brush: Brush,
|
|
|
|
|
|
selected: Boolean,
|
|
|
|
|
|
onClick: () -> Unit
|
|
|
|
|
|
) {
|
|
|
|
|
|
val shape = RoundedCornerShape(24.dp)
|
|
|
|
|
|
val interaction = remember { MutableInteractionSource() }
|
|
|
|
|
|
var pressed by remember { mutableStateOf(false) }
|
|
|
|
|
|
|
2025-11-27 02:56:42 +00:00
|
|
|
|
// 根据文本确定图标
|
|
|
|
|
|
val iconRes = when (text) {
|
|
|
|
|
|
"回家" -> R.drawable.ic_mode_home
|
|
|
|
|
|
"出门" -> R.drawable.ic_mode_away
|
|
|
|
|
|
"玩乐" -> R.drawable.ic_mode_fun
|
|
|
|
|
|
else -> R.drawable.ic_dashboard
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-26 02:30:03 +00:00
|
|
|
|
val scale by androidx.compose.animation.core.animateFloatAsState(
|
|
|
|
|
|
targetValue = if (pressed) 0.96f else 1f,
|
|
|
|
|
|
animationSpec = androidx.compose.animation.core.spring(
|
|
|
|
|
|
dampingRatio = androidx.compose.animation.core.Spring.DampingRatioMediumBouncy,
|
|
|
|
|
|
stiffness = androidx.compose.animation.core.Spring.StiffnessLow
|
|
|
|
|
|
),
|
|
|
|
|
|
label = "scale"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
val elevation by androidx.compose.animation.core.animateDpAsState(
|
|
|
|
|
|
targetValue = if (selected) 16.dp else if (pressed) 4.dp else 8.dp,
|
|
|
|
|
|
animationSpec = androidx.compose.animation.core.tween(300),
|
|
|
|
|
|
label = "elevation"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-11-27 02:56:42 +00:00
|
|
|
|
val iconScale by androidx.compose.animation.core.animateFloatAsState(
|
|
|
|
|
|
targetValue = if (selected) 1.1f else 1f,
|
|
|
|
|
|
animationSpec = androidx.compose.animation.core.spring(
|
|
|
|
|
|
dampingRatio = androidx.compose.animation.core.Spring.DampingRatioMediumBouncy
|
|
|
|
|
|
),
|
|
|
|
|
|
label = "icon_scale"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-11-26 02:30:03 +00:00
|
|
|
|
val view = LocalView.current
|
|
|
|
|
|
|
|
|
|
|
|
LaunchedEffect(interaction) {
|
|
|
|
|
|
interaction.interactions.collect { i ->
|
|
|
|
|
|
when (i) {
|
|
|
|
|
|
is PressInteraction.Press -> pressed = true
|
|
|
|
|
|
is PressInteraction.Release, is PressInteraction.Cancel -> pressed = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = modifier
|
|
|
|
|
|
.height(height)
|
|
|
|
|
|
.scale(scale)
|
2025-11-27 02:56:42 +00:00
|
|
|
|
.clip(shape)
|
|
|
|
|
|
.then(
|
2025-11-26 02:30:03 +00:00
|
|
|
|
if (selected) {
|
2025-11-27 02:56:42 +00:00
|
|
|
|
Modifier
|
|
|
|
|
|
.shadow(elevation, shape, clip = false)
|
|
|
|
|
|
.background(brush, shape)
|
|
|
|
|
|
.border(
|
|
|
|
|
|
width = 2.dp,
|
|
|
|
|
|
brush = Brush.linearGradient(
|
|
|
|
|
|
listOf(
|
|
|
|
|
|
Color(0x99FFFFFF),
|
|
|
|
|
|
Color(0x66FFFFFF)
|
|
|
|
|
|
)
|
|
|
|
|
|
),
|
|
|
|
|
|
shape = shape
|
2025-11-26 02:30:03 +00:00
|
|
|
|
)
|
|
|
|
|
|
} else {
|
2025-11-27 02:56:42 +00:00
|
|
|
|
Modifier.background(Color(0x50121212))
|
|
|
|
|
|
}
|
2025-11-26 02:30:03 +00:00
|
|
|
|
)
|
|
|
|
|
|
.clickable(indication = null, interactionSource = interaction) {
|
|
|
|
|
|
view.playSoundEffect(SoundEffectConstants.CLICK)
|
|
|
|
|
|
onClick()
|
|
|
|
|
|
}
|
|
|
|
|
|
.padding(24.dp),
|
|
|
|
|
|
contentAlignment = Alignment.Center
|
|
|
|
|
|
) {
|
2025-11-27 02:56:42 +00:00
|
|
|
|
Column(
|
|
|
|
|
|
horizontalAlignment = Alignment.CenterHorizontally,
|
|
|
|
|
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
|
|
|
|
|
) {
|
|
|
|
|
|
// 图标容器
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.size(if (selected) 72.dp else 64.dp)
|
|
|
|
|
|
.scale(iconScale),
|
|
|
|
|
|
contentAlignment = Alignment.Center
|
|
|
|
|
|
) {
|
|
|
|
|
|
// 外层光晕效果(仅选中时)
|
|
|
|
|
|
if (selected) {
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.fillMaxSize()
|
|
|
|
|
|
.clip(RoundedCornerShape(20.dp))
|
|
|
|
|
|
.background(
|
|
|
|
|
|
Brush.radialGradient(
|
|
|
|
|
|
colors = listOf(
|
|
|
|
|
|
Color.White.copy(alpha = 0.2f),
|
|
|
|
|
|
Color.Transparent
|
|
|
|
|
|
),
|
|
|
|
|
|
radius = 100f
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 图标背景
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.size(if (selected) 64.dp else 56.dp)
|
|
|
|
|
|
.clip(RoundedCornerShape(18.dp))
|
|
|
|
|
|
.background(
|
|
|
|
|
|
if (selected) {
|
|
|
|
|
|
Brush.linearGradient(
|
|
|
|
|
|
colors = listOf(
|
|
|
|
|
|
Color.White.copy(alpha = 0.25f),
|
|
|
|
|
|
Color.White.copy(alpha = 0.15f)
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Brush.linearGradient(
|
|
|
|
|
|
colors = listOf(
|
|
|
|
|
|
Color(0x55FFFFFF),
|
|
|
|
|
|
Color(0x55FFFFFF)
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
.then(
|
|
|
|
|
|
if (selected) {
|
|
|
|
|
|
Modifier.border(
|
|
|
|
|
|
width = 1.dp,
|
|
|
|
|
|
brush = Brush.linearGradient(
|
|
|
|
|
|
colors = listOf(
|
|
|
|
|
|
Color.White.copy(alpha = 0.4f),
|
|
|
|
|
|
Color.White.copy(alpha = 0.1f)
|
|
|
|
|
|
)
|
|
|
|
|
|
),
|
|
|
|
|
|
shape = RoundedCornerShape(18.dp)
|
|
|
|
|
|
)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Modifier
|
|
|
|
|
|
}
|
|
|
|
|
|
),
|
|
|
|
|
|
contentAlignment = Alignment.Center
|
|
|
|
|
|
) {
|
|
|
|
|
|
Image(
|
|
|
|
|
|
painter = painterResource(id = iconRes),
|
|
|
|
|
|
contentDescription = text,
|
|
|
|
|
|
modifier = Modifier.size(if (selected) 38.dp else 34.dp),
|
|
|
|
|
|
colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(
|
|
|
|
|
|
Color.White
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 文字
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = text,
|
|
|
|
|
|
color = Color.White,
|
|
|
|
|
|
fontWeight = if (selected) FontWeight.Bold else FontWeight.Medium,
|
|
|
|
|
|
fontSize = if (selected) 20.sp else 18.sp,
|
|
|
|
|
|
style = MaterialTheme.typography.titleMedium
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
2025-11-26 02:30:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Composable
|
2025-11-26 03:23:00 +00:00
|
|
|
|
fun LightRow(modifier: Modifier = Modifier, roomName: String = "房间") {
|
|
|
|
|
|
// 根据房间生成不同的灯光数据
|
|
|
|
|
|
val lights = remember(roomName) {
|
|
|
|
|
|
listOf(
|
|
|
|
|
|
"$roomName 主灯" to (0.5f + Math.random().toFloat() * 0.4f),
|
|
|
|
|
|
"$roomName 辅灯" to (0.5f + Math.random().toFloat() * 0.4f)
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
2025-11-26 02:30:03 +00:00
|
|
|
|
Row(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
|
|
|
|
|
lights.forEach { (name, percent) ->
|
|
|
|
|
|
LightCard(name = name, percent = percent, modifier = Modifier.weight(1f))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
|
|
fun LightCard(name: String, percent: Float, modifier: Modifier = Modifier) {
|
2025-11-26 03:43:39 +00:00
|
|
|
|
var brightness by remember { mutableStateOf(percent) }
|
|
|
|
|
|
|
2025-11-28 11:56:58 +00:00
|
|
|
|
Box(modifier = modifier.height(72.dp).clip(RoundedCornerShape(16.dp)).background(Color(0x50121212)).padding(horizontal = 12.dp, vertical = 8.dp)) {
|
2025-11-26 02:30:03 +00:00
|
|
|
|
Row(horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
|
|
|
|
|
Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
2025-11-26 03:43:39 +00:00
|
|
|
|
// 灯光图标 - 带发光效果
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.size(40.dp)
|
|
|
|
|
|
.shadow(
|
|
|
|
|
|
elevation = if (brightness > 0.3f) (brightness * 12).dp else 0.dp,
|
|
|
|
|
|
shape = RoundedCornerShape(12.dp),
|
|
|
|
|
|
spotColor = Color(0xFFFFD54F).copy(alpha = brightness * 0.6f)
|
|
|
|
|
|
)
|
|
|
|
|
|
.clip(RoundedCornerShape(12.dp))
|
|
|
|
|
|
.background(
|
|
|
|
|
|
if (brightness > 0.1f) {
|
|
|
|
|
|
Brush.radialGradient(
|
|
|
|
|
|
colors = listOf(
|
|
|
|
|
|
Color(0xFFFFE082).copy(alpha = brightness),
|
|
|
|
|
|
Color(0xFFFFD54F).copy(alpha = brightness * 0.9f),
|
|
|
|
|
|
Color(0xFFFF8F00).copy(alpha = brightness * 0.7f)
|
|
|
|
|
|
),
|
|
|
|
|
|
radius = 60f
|
|
|
|
|
|
)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Brush.linearGradient(
|
|
|
|
|
|
listOf(
|
|
|
|
|
|
Color(0xFF3A3A3A),
|
|
|
|
|
|
Color(0xFF2A2A2A)
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
),
|
|
|
|
|
|
contentAlignment = Alignment.Center
|
|
|
|
|
|
) {
|
|
|
|
|
|
Image(
|
|
|
|
|
|
painter = painterResource(id = R.drawable.ic_light),
|
|
|
|
|
|
contentDescription = "灯光",
|
|
|
|
|
|
modifier = Modifier.size(24.dp),
|
|
|
|
|
|
colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(
|
|
|
|
|
|
if (brightness > 0.1f) {
|
|
|
|
|
|
Color.White.copy(alpha = 0.95f)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Color(0xFF666666)
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
Column {
|
|
|
|
|
|
Text(text = name, color = Color.White, fontWeight = FontWeight.Medium)
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = "${(brightness * 100).toInt()}%",
|
|
|
|
|
|
color = if (brightness > 0.1f) Color(0xFFFFD54F) else Color(0xFF9AA0A6),
|
|
|
|
|
|
fontSize = 13.sp,
|
|
|
|
|
|
fontWeight = if (brightness > 0.5f) FontWeight.Medium else FontWeight.Normal
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
2025-11-26 02:30:03 +00:00
|
|
|
|
}
|
2025-11-26 03:43:39 +00:00
|
|
|
|
DotsProgress(
|
|
|
|
|
|
percent = brightness,
|
|
|
|
|
|
onPercentChange = { brightness = it },
|
|
|
|
|
|
enabled = true
|
|
|
|
|
|
)
|
2025-11-26 02:30:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-27 04:32:01 +00:00
|
|
|
|
@Composable
|
|
|
|
|
|
fun WeatherEffectLayer(weather: String, modifier: Modifier = Modifier) {
|
|
|
|
|
|
val infiniteTransition = rememberInfiniteTransition(label = "weather_effect")
|
|
|
|
|
|
|
|
|
|
|
|
// 太阳旋转动画
|
|
|
|
|
|
val sunRotation by infiniteTransition.animateFloat(
|
|
|
|
|
|
initialValue = 0f,
|
|
|
|
|
|
targetValue = 360f,
|
|
|
|
|
|
animationSpec = infiniteRepeatable(
|
|
|
|
|
|
animation = tween(20000, easing = LinearEasing)
|
|
|
|
|
|
),
|
|
|
|
|
|
label = "sun_rotation"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-11-27 04:45:22 +00:00
|
|
|
|
// 雨雪下落动画 - 使用更长的周期避免明显停顿
|
2025-11-27 04:32:01 +00:00
|
|
|
|
val dropOffset by infiniteTransition.animateFloat(
|
|
|
|
|
|
initialValue = 0f,
|
|
|
|
|
|
targetValue = 1f,
|
|
|
|
|
|
animationSpec = infiniteRepeatable(
|
2025-11-27 04:45:22 +00:00
|
|
|
|
animation = tween(8000, easing = LinearEasing),
|
|
|
|
|
|
repeatMode = RepeatMode.Restart
|
2025-11-27 04:32:01 +00:00
|
|
|
|
),
|
|
|
|
|
|
label = "drop_offset"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 云朵飘动动画
|
|
|
|
|
|
val cloudOffset by infiniteTransition.animateFloat(
|
|
|
|
|
|
initialValue = -20f,
|
|
|
|
|
|
targetValue = 20f,
|
|
|
|
|
|
animationSpec = infiniteRepeatable(
|
|
|
|
|
|
animation = tween(5000, easing = LinearEasing),
|
|
|
|
|
|
repeatMode = RepeatMode.Reverse
|
|
|
|
|
|
),
|
|
|
|
|
|
label = "cloud_offset"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
androidx.compose.foundation.Canvas(modifier = modifier) {
|
|
|
|
|
|
val width = size.width
|
|
|
|
|
|
val height = size.height
|
|
|
|
|
|
|
|
|
|
|
|
when {
|
|
|
|
|
|
// 晴天:绘制旋转的太阳
|
|
|
|
|
|
weather.contains("晴") -> {
|
|
|
|
|
|
// 太阳核心
|
|
|
|
|
|
val sunCenterX = width - 25.dp.toPx()
|
|
|
|
|
|
val sunCenterY = 25.dp.toPx()
|
|
|
|
|
|
|
|
|
|
|
|
drawCircle(
|
|
|
|
|
|
color = Color(0xFFFFD700).copy(alpha = 0.6f),
|
|
|
|
|
|
radius = 18.dp.toPx(),
|
|
|
|
|
|
center = Offset(sunCenterX, sunCenterY)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 太阳光芒 - 使用数学计算位置
|
|
|
|
|
|
val rayLength = 10.dp.toPx()
|
|
|
|
|
|
val rayStart = 20.dp.toPx()
|
|
|
|
|
|
|
|
|
|
|
|
for (i in 0 until 8) {
|
|
|
|
|
|
val angle = Math.toRadians((sunRotation + i * 45f).toDouble())
|
|
|
|
|
|
val startX = sunCenterX + kotlin.math.cos(angle).toFloat() * rayStart
|
|
|
|
|
|
val startY = sunCenterY + kotlin.math.sin(angle).toFloat() * rayStart
|
|
|
|
|
|
val endX = sunCenterX + kotlin.math.cos(angle).toFloat() * (rayStart + rayLength)
|
|
|
|
|
|
val endY = sunCenterY + kotlin.math.sin(angle).toFloat() * (rayStart + rayLength)
|
|
|
|
|
|
|
|
|
|
|
|
drawLine(
|
|
|
|
|
|
color = Color(0xFFFFD700).copy(alpha = 0.5f),
|
|
|
|
|
|
start = Offset(startX, startY),
|
|
|
|
|
|
end = Offset(endX, endY),
|
|
|
|
|
|
strokeWidth = 2.dp.toPx()
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 雨天:绘制下落的雨滴
|
|
|
|
|
|
weather.contains("雨") -> {
|
2025-11-27 04:45:22 +00:00
|
|
|
|
val dropCount = 20
|
|
|
|
|
|
val dropLength = 10.dp.toPx()
|
2025-11-27 04:32:01 +00:00
|
|
|
|
|
|
|
|
|
|
for (i in 0 until dropCount) {
|
2025-11-27 04:45:22 +00:00
|
|
|
|
// 使用不同的起始位置和速度让雨滴错开
|
|
|
|
|
|
val xOffset = (i * 37 % 100) / 100f
|
|
|
|
|
|
val x = width * xOffset
|
|
|
|
|
|
val speedFactor = 3f + (i % 5) * 0.5f // 更快的速度,一个周期内多次循环
|
|
|
|
|
|
val startOffset = (i * 73 % 100) / 100f // 不同起始位置
|
|
|
|
|
|
val currentY = ((startOffset + dropOffset * speedFactor) % 1f) * height
|
2025-11-27 04:32:01 +00:00
|
|
|
|
|
|
|
|
|
|
drawLine(
|
2025-11-27 04:45:22 +00:00
|
|
|
|
color = Color.White.copy(alpha = 0.35f),
|
2025-11-27 04:32:01 +00:00
|
|
|
|
start = Offset(x, currentY),
|
2025-11-27 04:45:22 +00:00
|
|
|
|
end = Offset(x, currentY + dropLength), // 垂直下落
|
2025-11-27 04:32:01 +00:00
|
|
|
|
strokeWidth = 1.5f.dp.toPx()
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 雪天:绘制飘落的雪花(圆点)
|
|
|
|
|
|
weather.contains("雪") -> {
|
2025-11-27 04:45:22 +00:00
|
|
|
|
val snowCount = 18
|
2025-11-27 04:32:01 +00:00
|
|
|
|
|
|
|
|
|
|
for (i in 0 until snowCount) {
|
2025-11-27 04:45:22 +00:00
|
|
|
|
// 使用不同的起始位置和速度让雪花错开
|
|
|
|
|
|
val xOffset = (i * 41 % 100) / 100f
|
|
|
|
|
|
val x = width * xOffset
|
|
|
|
|
|
val speedFactor = 1.5f + (i % 4) * 0.3f // 雪花下落更慢
|
|
|
|
|
|
val startOffset = (i * 67 % 100) / 100f
|
|
|
|
|
|
val currentY = ((startOffset + dropOffset * speedFactor) % 1f) * height
|
2025-11-27 04:32:01 +00:00
|
|
|
|
|
|
|
|
|
|
drawCircle(
|
|
|
|
|
|
color = Color.White.copy(alpha = 0.5f),
|
|
|
|
|
|
radius = (1.5f + i % 2).dp.toPx(),
|
|
|
|
|
|
center = Offset(x, currentY)
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 多云/阴天:绘制云朵
|
|
|
|
|
|
weather.contains("云") || weather.contains("阴") -> {
|
|
|
|
|
|
// 绘制几个叠加的圆组成云 - 缩小尺寸
|
|
|
|
|
|
val cloudColor = Color.White.copy(alpha = 0.15f)
|
|
|
|
|
|
val baseX = width - 35.dp.toPx() + cloudOffset * 0.5f
|
|
|
|
|
|
val baseY = 25.dp.toPx()
|
|
|
|
|
|
|
|
|
|
|
|
drawCircle(color = cloudColor, radius = 15.dp.toPx(), center = Offset(baseX, baseY))
|
|
|
|
|
|
drawCircle(color = cloudColor, radius = 12.dp.toPx(), center = Offset(baseX - 18.dp.toPx(), baseY + 5.dp.toPx()))
|
|
|
|
|
|
drawCircle(color = cloudColor, radius = 14.dp.toPx(), center = Offset(baseX + 15.dp.toPx(), baseY + 3.dp.toPx()))
|
|
|
|
|
|
drawCircle(color = cloudColor, radius = 10.dp.toPx(), center = Offset(baseX - 8.dp.toPx(), baseY - 8.dp.toPx()))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-26 02:30:03 +00:00
|
|
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
|
|
fun AddRoomDialog(
|
|
|
|
|
|
onDismiss: () -> Unit,
|
|
|
|
|
|
onConfirm: (String) -> Unit
|
|
|
|
|
|
) {
|
|
|
|
|
|
var roomName by remember { mutableStateOf("") }
|
2025-11-28 11:56:58 +00:00
|
|
|
|
var selectedIcon by remember { mutableStateOf(0) }
|
|
|
|
|
|
|
|
|
|
|
|
// 预设房间图标
|
|
|
|
|
|
val roomIcons = listOf("🛋️", "🍳", "🛏️", "🎬", "🎮", "📚", "🛁", "🌿")
|
|
|
|
|
|
val roomSuggestions = listOf("书房", "阳台", "浴室", "储物间", "健身房", "儿童房")
|
2025-11-26 02:30:03 +00:00
|
|
|
|
|
2025-11-28 11:56:58 +00:00
|
|
|
|
// 背景遮罩(点击关闭)
|
2025-11-26 02:30:03 +00:00
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.fillMaxSize()
|
|
|
|
|
|
.clickable(
|
|
|
|
|
|
indication = null,
|
|
|
|
|
|
interactionSource = remember { MutableInteractionSource() }
|
|
|
|
|
|
) { onDismiss() },
|
|
|
|
|
|
contentAlignment = Alignment.Center
|
|
|
|
|
|
) {
|
2025-11-28 11:56:58 +00:00
|
|
|
|
// 对话框主体
|
2025-11-26 02:30:03 +00:00
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
2025-11-28 11:56:58 +00:00
|
|
|
|
.width(420.dp)
|
|
|
|
|
|
.clip(RoundedCornerShape(28.dp))
|
|
|
|
|
|
.background(
|
|
|
|
|
|
Brush.verticalGradient(
|
|
|
|
|
|
listOf(
|
|
|
|
|
|
Color(0xFF2A2A3E),
|
|
|
|
|
|
Color(0xFF1A1A2E)
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
.border(
|
|
|
|
|
|
width = 1.dp,
|
|
|
|
|
|
brush = Brush.linearGradient(
|
|
|
|
|
|
listOf(Color(0xFF00D1FF).copy(alpha = 0.3f), Color(0xFFB89CFF).copy(alpha = 0.3f))
|
|
|
|
|
|
),
|
|
|
|
|
|
shape = RoundedCornerShape(28.dp)
|
|
|
|
|
|
)
|
2025-11-26 02:30:03 +00:00
|
|
|
|
.clickable(
|
|
|
|
|
|
indication = null,
|
|
|
|
|
|
interactionSource = remember { MutableInteractionSource() }
|
|
|
|
|
|
) { /* 阻止点击穿透 */ }
|
2025-11-28 11:56:58 +00:00
|
|
|
|
.padding(28.dp)
|
2025-11-26 02:30:03 +00:00
|
|
|
|
) {
|
|
|
|
|
|
Column(
|
2025-11-28 11:56:58 +00:00
|
|
|
|
verticalArrangement = Arrangement.spacedBy(24.dp)
|
2025-11-26 02:30:03 +00:00
|
|
|
|
) {
|
2025-11-28 11:56:58 +00:00
|
|
|
|
// 标题区域
|
|
|
|
|
|
Row(
|
2025-11-26 02:30:03 +00:00
|
|
|
|
modifier = Modifier.fillMaxWidth(),
|
2025-11-28 11:56:58 +00:00
|
|
|
|
horizontalArrangement = Arrangement.SpaceBetween,
|
|
|
|
|
|
verticalAlignment = Alignment.CenterVertically
|
|
|
|
|
|
) {
|
|
|
|
|
|
Row(
|
|
|
|
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
|
|
|
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
|
|
|
|
|
) {
|
|
|
|
|
|
// 图标
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.size(44.dp)
|
|
|
|
|
|
.clip(RoundedCornerShape(12.dp))
|
|
|
|
|
|
.background(
|
|
|
|
|
|
Brush.linearGradient(
|
|
|
|
|
|
listOf(Color(0xFF00D1FF).copy(alpha = 0.3f), Color(0xFFB89CFF).copy(alpha = 0.3f))
|
|
|
|
|
|
)
|
|
|
|
|
|
),
|
|
|
|
|
|
contentAlignment = Alignment.Center
|
|
|
|
|
|
) {
|
|
|
|
|
|
Text(text = "🏠", fontSize = 22.sp)
|
|
|
|
|
|
}
|
|
|
|
|
|
Column {
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = "添加新房间",
|
|
|
|
|
|
fontSize = 20.sp,
|
|
|
|
|
|
fontWeight = FontWeight.Bold,
|
|
|
|
|
|
color = Color.White
|
|
|
|
|
|
)
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = "创建一个新的智能空间",
|
|
|
|
|
|
fontSize = 12.sp,
|
|
|
|
|
|
color = Color(0xFF9AA0A6)
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 关闭按钮
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.size(32.dp)
|
|
|
|
|
|
.clip(RoundedCornerShape(8.dp))
|
|
|
|
|
|
.background(Color(0x33FFFFFF))
|
|
|
|
|
|
.clickable { onDismiss() },
|
|
|
|
|
|
contentAlignment = Alignment.Center
|
|
|
|
|
|
) {
|
|
|
|
|
|
Text(text = "✕", color = Color(0xFF9AA0A6), fontSize = 14.sp)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 房间名称输入
|
|
|
|
|
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = "房间名称",
|
|
|
|
|
|
fontSize = 14.sp,
|
|
|
|
|
|
fontWeight = FontWeight.Medium,
|
|
|
|
|
|
color = Color(0xFFB0B0B0)
|
|
|
|
|
|
)
|
|
|
|
|
|
androidx.compose.material3.TextField(
|
|
|
|
|
|
value = roomName,
|
|
|
|
|
|
onValueChange = { roomName = it },
|
|
|
|
|
|
placeholder = { Text("例如:书房、阳台...", color = Color(0xFF6A6A7A)) },
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.fillMaxWidth()
|
|
|
|
|
|
.height(56.dp),
|
|
|
|
|
|
colors = androidx.compose.material3.TextFieldDefaults.colors(
|
|
|
|
|
|
focusedContainerColor = Color(0xFF1A1A2E),
|
|
|
|
|
|
unfocusedContainerColor = Color(0xFF1A1A2E),
|
|
|
|
|
|
focusedTextColor = Color.White,
|
|
|
|
|
|
unfocusedTextColor = Color.White,
|
|
|
|
|
|
cursorColor = Color(0xFF00D1FF),
|
|
|
|
|
|
focusedIndicatorColor = Color.Transparent,
|
|
|
|
|
|
unfocusedIndicatorColor = Color.Transparent
|
|
|
|
|
|
),
|
|
|
|
|
|
shape = RoundedCornerShape(14.dp),
|
|
|
|
|
|
singleLine = true,
|
|
|
|
|
|
textStyle = androidx.compose.ui.text.TextStyle(fontSize = 16.sp)
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 快速选择建议
|
|
|
|
|
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = "快速选择",
|
|
|
|
|
|
fontSize = 14.sp,
|
|
|
|
|
|
fontWeight = FontWeight.Medium,
|
|
|
|
|
|
color = Color(0xFFB0B0B0)
|
|
|
|
|
|
)
|
|
|
|
|
|
Row(
|
|
|
|
|
|
modifier = Modifier.fillMaxWidth(),
|
|
|
|
|
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
|
|
|
|
) {
|
|
|
|
|
|
roomSuggestions.take(3).forEach { suggestion ->
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.clip(RoundedCornerShape(20.dp))
|
|
|
|
|
|
.background(
|
|
|
|
|
|
if (roomName == suggestion) Color(0xFF00D1FF).copy(alpha = 0.3f)
|
|
|
|
|
|
else Color(0xFF1A1A2E)
|
|
|
|
|
|
)
|
|
|
|
|
|
.border(
|
|
|
|
|
|
width = 1.dp,
|
|
|
|
|
|
color = if (roomName == suggestion) Color(0xFF00D1FF) else Color(0xFF3A3A4E),
|
|
|
|
|
|
shape = RoundedCornerShape(20.dp)
|
|
|
|
|
|
)
|
|
|
|
|
|
.clickable { roomName = suggestion }
|
|
|
|
|
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
|
|
|
|
|
) {
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = suggestion,
|
|
|
|
|
|
color = if (roomName == suggestion) Color.White else Color(0xFF9AA0A6),
|
|
|
|
|
|
fontSize = 13.sp
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Row(
|
|
|
|
|
|
modifier = Modifier.fillMaxWidth(),
|
|
|
|
|
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
|
|
|
|
) {
|
|
|
|
|
|
roomSuggestions.drop(3).forEach { suggestion ->
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.clip(RoundedCornerShape(20.dp))
|
|
|
|
|
|
.background(
|
|
|
|
|
|
if (roomName == suggestion) Color(0xFF00D1FF).copy(alpha = 0.3f)
|
|
|
|
|
|
else Color(0xFF1A1A2E)
|
|
|
|
|
|
)
|
|
|
|
|
|
.border(
|
|
|
|
|
|
width = 1.dp,
|
|
|
|
|
|
color = if (roomName == suggestion) Color(0xFF00D1FF) else Color(0xFF3A3A4E),
|
|
|
|
|
|
shape = RoundedCornerShape(20.dp)
|
|
|
|
|
|
)
|
|
|
|
|
|
.clickable { roomName = suggestion }
|
|
|
|
|
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
|
|
|
|
|
) {
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = suggestion,
|
|
|
|
|
|
color = if (roomName == suggestion) Color.White else Color(0xFF9AA0A6),
|
|
|
|
|
|
fontSize = 13.sp
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 选择图标
|
|
|
|
|
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = "选择图标",
|
|
|
|
|
|
fontSize = 14.sp,
|
|
|
|
|
|
fontWeight = FontWeight.Medium,
|
|
|
|
|
|
color = Color(0xFFB0B0B0)
|
|
|
|
|
|
)
|
|
|
|
|
|
Row(
|
|
|
|
|
|
modifier = Modifier.fillMaxWidth(),
|
|
|
|
|
|
horizontalArrangement = Arrangement.SpaceEvenly
|
|
|
|
|
|
) {
|
|
|
|
|
|
roomIcons.forEachIndexed { index, icon ->
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.size(44.dp)
|
|
|
|
|
|
.clip(RoundedCornerShape(12.dp))
|
|
|
|
|
|
.then(
|
|
|
|
|
|
if (selectedIcon == index)
|
|
|
|
|
|
Modifier.background(Brush.linearGradient(listOf(Color(0xFF00D1FF), Color(0xFFB89CFF))))
|
|
|
|
|
|
else Modifier.background(Color(0xFF1A1A2E))
|
|
|
|
|
|
)
|
|
|
|
|
|
.border(
|
|
|
|
|
|
width = if (selectedIcon == index) 0.dp else 1.dp,
|
|
|
|
|
|
color = Color(0xFF3A3A4E),
|
|
|
|
|
|
shape = RoundedCornerShape(12.dp)
|
|
|
|
|
|
)
|
|
|
|
|
|
.clickable { selectedIcon = index },
|
|
|
|
|
|
contentAlignment = Alignment.Center
|
|
|
|
|
|
) {
|
|
|
|
|
|
Text(text = icon, fontSize = 20.sp)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-26 02:30:03 +00:00
|
|
|
|
|
2025-11-28 11:56:58 +00:00
|
|
|
|
// 按钮区域
|
2025-11-26 02:30:03 +00:00
|
|
|
|
Row(
|
2025-11-28 11:56:58 +00:00
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.fillMaxWidth()
|
|
|
|
|
|
.padding(top = 8.dp),
|
2025-11-26 02:30:03 +00:00
|
|
|
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
|
|
|
|
|
) {
|
2025-11-28 11:56:58 +00:00
|
|
|
|
// 取消按钮
|
2025-11-26 02:30:03 +00:00
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.weight(1f)
|
2025-11-28 11:56:58 +00:00
|
|
|
|
.height(52.dp)
|
|
|
|
|
|
.clip(RoundedCornerShape(14.dp))
|
|
|
|
|
|
.background(Color(0xFF1A1A2E))
|
|
|
|
|
|
.border(
|
|
|
|
|
|
width = 1.dp,
|
|
|
|
|
|
color = Color(0xFF3A3A4E),
|
|
|
|
|
|
shape = RoundedCornerShape(14.dp)
|
|
|
|
|
|
)
|
|
|
|
|
|
.clickable { onDismiss() },
|
2025-11-26 02:30:03 +00:00
|
|
|
|
contentAlignment = Alignment.Center
|
|
|
|
|
|
) {
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = "取消",
|
|
|
|
|
|
color = Color(0xFFB0B0B0),
|
2025-11-28 11:56:58 +00:00
|
|
|
|
fontWeight = FontWeight.Medium,
|
|
|
|
|
|
fontSize = 15.sp
|
2025-11-26 02:30:03 +00:00
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-28 11:56:58 +00:00
|
|
|
|
// 确定按钮
|
2025-11-26 02:30:03 +00:00
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.weight(1f)
|
2025-11-28 11:56:58 +00:00
|
|
|
|
.height(52.dp)
|
|
|
|
|
|
.clip(RoundedCornerShape(14.dp))
|
2025-11-26 02:30:03 +00:00
|
|
|
|
.background(
|
2025-11-28 11:56:58 +00:00
|
|
|
|
if (roomName.isNotBlank())
|
|
|
|
|
|
Brush.linearGradient(listOf(Color(0xFF00D1FF), Color(0xFFB89CFF)))
|
|
|
|
|
|
else
|
|
|
|
|
|
Brush.linearGradient(listOf(Color(0xFF3A3A4E), Color(0xFF3A3A4E)))
|
2025-11-26 02:30:03 +00:00
|
|
|
|
)
|
2025-11-28 11:56:58 +00:00
|
|
|
|
.clickable(enabled = roomName.isNotBlank()) {
|
2025-11-26 02:30:03 +00:00
|
|
|
|
if (roomName.isNotBlank()) {
|
|
|
|
|
|
onConfirm(roomName.trim())
|
|
|
|
|
|
}
|
2025-11-28 11:56:58 +00:00
|
|
|
|
},
|
2025-11-26 02:30:03 +00:00
|
|
|
|
contentAlignment = Alignment.Center
|
|
|
|
|
|
) {
|
2025-11-28 11:56:58 +00:00
|
|
|
|
Row(
|
|
|
|
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
|
|
|
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
|
|
|
|
) {
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = "✓",
|
|
|
|
|
|
color = if (roomName.isNotBlank()) Color.White else Color(0xFF6A6A7A),
|
|
|
|
|
|
fontSize = 16.sp
|
|
|
|
|
|
)
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = "添加房间",
|
|
|
|
|
|
color = if (roomName.isNotBlank()) Color.White else Color(0xFF6A6A7A),
|
|
|
|
|
|
fontWeight = FontWeight.Bold,
|
|
|
|
|
|
fontSize = 15.sp
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
2025-11-26 02:30:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|