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
|
|
|
|
|
|
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
|
|
|
|
|
|
import androidx.compose.foundation.rememberScrollState
|
|
|
|
|
|
import androidx.compose.foundation.gestures.detectTapGestures
|
|
|
|
|
|
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
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 根据选中的导航项显示不同的内容
|
|
|
|
|
|
when (selectedNavItem) {
|
|
|
|
|
|
0 -> DashboardContent(selectedRoom, onRoomSelect, rooms, onAddRoom, onDeleteRoom)
|
2025-11-27 02:56:42 +00:00
|
|
|
|
1 -> SceneScreen()
|
|
|
|
|
|
2 -> AutomationScreen()
|
|
|
|
|
|
3 -> StatisticsScreen()
|
|
|
|
|
|
4 -> SecurityScreen()
|
2025-11-26 02:30:03 +00:00
|
|
|
|
5 -> SettingsContent()
|
|
|
|
|
|
else -> DashboardContent(selectedRoom, onRoomSelect, rooms, onAddRoom, onDeleteRoom)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@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 }
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
) {
|
|
|
|
|
|
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
|
|
|
|
|
|
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() {
|
|
|
|
|
|
Column(modifier = Modifier.fillMaxSize()) {
|
|
|
|
|
|
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
|
|
|
|
|
AirConditionerCard(modifier = Modifier.weight(1f), roomName = "全屋")
|
|
|
|
|
|
UsageStatusChart(modifier = Modifier.weight(1f), roomName = "全屋")
|
|
|
|
|
|
}
|
2025-11-26 03:43:39 +00:00
|
|
|
|
LightRow(modifier = Modifier.fillMaxWidth().padding(top = 16.dp, bottom = 16.dp), roomName = "全屋")
|
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
|
|
|
|
)
|
|
|
|
|
|
AllDevicesOverview(modifier = Modifier.fillMaxSize())
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
|
|
fun SpecificRoomContent(selectedRoom: Int, roomName: String) {
|
|
|
|
|
|
Column(modifier = Modifier.fillMaxSize()) {
|
|
|
|
|
|
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
|
|
|
|
)
|
|
|
|
|
|
MyDevicesGrid(selectedRoom = selectedRoom, modifier = Modifier.fillMaxSize())
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@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)
|
|
|
|
|
|
)
|
|
|
|
|
|
LazyVerticalGrid(
|
|
|
|
|
|
columns = GridCells.Adaptive(minSize = 140.dp),
|
|
|
|
|
|
modifier = modifier,
|
|
|
|
|
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
|
|
|
|
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
|
|
|
|
|
) {
|
|
|
|
|
|
items(allDevices) { d -> DeviceCard(d) }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 固定的添加按钮
|
|
|
|
|
|
GradientButton("+ 添加", onClick = onAddRoomClick)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@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
|
|
|
|
|
|
fun DeviceCard(d: Device) {
|
|
|
|
|
|
var active by remember { mutableStateOf(d.on) }
|
2025-11-27 02:56:42 +00:00
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.height(120.dp)
|
|
|
|
|
|
.clip(RoundedCornerShape(24.dp))
|
|
|
|
|
|
.background(Color(0x50121212))
|
|
|
|
|
|
.padding(16.dp)
|
|
|
|
|
|
) {
|
2025-11-26 02:30:03 +00:00
|
|
|
|
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
|
|
|
|
|
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
|
|
|
|
|
Image(painter = painterResource(id = d.icon), contentDescription = null, modifier = Modifier.size(40.dp))
|
|
|
|
|
|
Column {
|
|
|
|
|
|
Text(text = d.name, fontWeight = FontWeight.SemiBold)
|
|
|
|
|
|
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
|
|
|
|
|
Box(modifier = Modifier.size(8.dp).clip(RoundedCornerShape(4.dp)).background(if (active) Color(0xFF00E676) else Color(0xFF9AA0A6)))
|
|
|
|
|
|
Text(text = d.sub, color = Color(0xFF9AA0A6))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
|
|
|
|
|
Text(text = if (active) "运行中" else "待机中", color = if (active) Color(0xFF00E676) else Color(0xFF9AA0A6))
|
|
|
|
|
|
Toggle(active) { active = it }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@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 = {}) {
|
|
|
|
|
|
Column(modifier = Modifier.width(120.dp).fillMaxHeight().padding(12.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
2025-11-26 03:23:00 +00:00
|
|
|
|
Spacer(modifier = Modifier.height(48.dp))
|
2025-11-26 02:30:03 +00:00
|
|
|
|
NavRailItem("控制台", R.drawable.ic_dashboard, selectedNavItem == 0, onClick = { onNavItemSelect(0) })
|
2025-11-27 02:56:42 +00:00
|
|
|
|
NavRailItem("场景", R.drawable.ic_scene, selectedNavItem == 1, onClick = { onNavItemSelect(1) })
|
|
|
|
|
|
NavRailItem("自动化", R.drawable.ic_automation, selectedNavItem == 2, onClick = { onNavItemSelect(2) })
|
|
|
|
|
|
NavRailItem("统计", R.drawable.ic_statistics, selectedNavItem == 3, onClick = { onNavItemSelect(3) })
|
|
|
|
|
|
NavRailItem("安全", R.drawable.ic_security, selectedNavItem == 4, onClick = { onNavItemSelect(4) })
|
2025-11-26 02:30:03 +00:00
|
|
|
|
NavRailItem("设置", R.drawable.ic_settings, selectedNavItem == 5, onClick = { onNavItemSelect(5) })
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@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
|
|
|
|
|
|
.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),
|
|
|
|
|
|
fontSize = 13.sp,
|
|
|
|
|
|
fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal,
|
|
|
|
|
|
maxLines = 1
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@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) }
|
|
|
|
|
|
|
|
|
|
|
|
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-26 03:23:00 +00:00
|
|
|
|
Text(text = "控制台", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, color = Color.White)
|
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
|
|
|
|
// 天气信息卡片
|
|
|
|
|
|
WeatherCard(weatherInfo = weatherInfo, isLoading = isLoading)
|
2025-11-26 02:30:03 +00:00
|
|
|
|
GradientButton("+ 添加设备")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 根据天气状况决定背景颜色 - 增加颜色差异让流动更明显
|
|
|
|
|
|
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 {
|
|
|
|
|
|
// 晴天:暗橙色流动
|
|
|
|
|
|
weatherInfo.weather.contains("晴") ->
|
|
|
|
|
|
listOf(
|
|
|
|
|
|
Color(0xFFB8450A).copy(alpha = 0.55f),
|
|
|
|
|
|
Color(0xFFC06000).copy(alpha = 0.55f),
|
|
|
|
|
|
Color(0xFFB8450A).copy(alpha = 0.55f)
|
|
|
|
|
|
)
|
|
|
|
|
|
// 雨天:深紫深蓝流动
|
|
|
|
|
|
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 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-26 03:23:00 +00:00
|
|
|
|
LazyVerticalGrid(
|
|
|
|
|
|
columns = GridCells.Adaptive(minSize = 140.dp),
|
|
|
|
|
|
modifier = modifier,
|
|
|
|
|
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
|
|
|
|
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
|
|
|
|
|
) {
|
2025-11-26 02:30:03 +00:00
|
|
|
|
items(devices) { d -> DeviceCard(d) }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Composable
|
2025-11-26 03:23:00 +00:00
|
|
|
|
fun UsageStatusChart(modifier: Modifier = Modifier, roomName: String = "房间") {
|
|
|
|
|
|
// 根据房间生成不同的数据
|
|
|
|
|
|
val values = remember(roomName) {
|
2025-11-27 02:56:42 +00:00
|
|
|
|
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 }
|
|
|
|
|
|
val totalPower = remember(roomName) { (15.0 + Math.random() * 30).toString().take(4) }
|
2025-11-26 03:23:00 +00:00
|
|
|
|
val totalHours = remember(roomName) { (10 + (Math.random() * 40).toInt()) }
|
2025-11-27 02:56:42 +00:00
|
|
|
|
val activeDevices = remember(roomName) { (3 + (Math.random() * 8).toInt()) }
|
|
|
|
|
|
val trend = remember(roomName) { if (Math.random() > 0.5) "↑" else "↓" }
|
|
|
|
|
|
val trendPercent = remember(roomName) { (5 + (Math.random() * 15).toInt()) }
|
2025-11-26 03:23:00 +00:00
|
|
|
|
|
2025-11-27 02:56:42 +00:00
|
|
|
|
Box(
|
|
|
|
|
|
modifier = modifier
|
|
|
|
|
|
.height(220.dp)
|
|
|
|
|
|
.clip(RoundedCornerShape(24.dp))
|
|
|
|
|
|
.background(
|
|
|
|
|
|
Brush.linearGradient(
|
|
|
|
|
|
colors = listOf(
|
|
|
|
|
|
Color(0x60121212),
|
|
|
|
|
|
Color(0x50121212)
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
.border(
|
|
|
|
|
|
width = 1.dp,
|
|
|
|
|
|
brush = Brush.linearGradient(
|
|
|
|
|
|
colors = listOf(
|
|
|
|
|
|
Color(0x22FFFFFF),
|
|
|
|
|
|
Color(0x0AFFFFFF)
|
|
|
|
|
|
)
|
|
|
|
|
|
),
|
|
|
|
|
|
shape = RoundedCornerShape(24.dp)
|
|
|
|
|
|
)
|
|
|
|
|
|
.padding(16.dp)
|
|
|
|
|
|
) {
|
|
|
|
|
|
Column(
|
|
|
|
|
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
|
|
|
|
|
modifier = Modifier.fillMaxSize()
|
|
|
|
|
|
) {
|
|
|
|
|
|
// 标题区域
|
|
|
|
|
|
Row(
|
|
|
|
|
|
modifier = Modifier.fillMaxWidth(),
|
|
|
|
|
|
horizontalArrangement = Arrangement.SpaceBetween,
|
|
|
|
|
|
verticalAlignment = Alignment.CenterVertically
|
|
|
|
|
|
) {
|
|
|
|
|
|
Column {
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = "能耗统计",
|
|
|
|
|
|
fontWeight = FontWeight.Bold,
|
|
|
|
|
|
color = Color.White,
|
|
|
|
|
|
fontSize = 16.sp
|
|
|
|
|
|
)
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = roomName,
|
|
|
|
|
|
fontSize = 12.sp,
|
|
|
|
|
|
color = Color(0xFF9AA0A6)
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 趋势指示器
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.clip(RoundedCornerShape(12.dp))
|
|
|
|
|
|
.background(
|
|
|
|
|
|
if (trend == "↑") Color(0x33FF5252) else Color(0x3300E676)
|
|
|
|
|
|
)
|
|
|
|
|
|
.padding(horizontal = 10.dp, vertical = 6.dp)
|
|
|
|
|
|
) {
|
|
|
|
|
|
Row(
|
|
|
|
|
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
|
|
|
|
|
verticalAlignment = Alignment.CenterVertically
|
|
|
|
|
|
) {
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = trend,
|
|
|
|
|
|
color = if (trend == "↑") Color(0xFFFF5252) else Color(0xFF00E676),
|
|
|
|
|
|
fontSize = 14.sp,
|
|
|
|
|
|
fontWeight = FontWeight.Bold
|
|
|
|
|
|
)
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = "${trendPercent}%",
|
|
|
|
|
|
color = if (trend == "↑") Color(0xFFFF5252) else Color(0xFF00E676),
|
|
|
|
|
|
fontSize = 12.sp,
|
|
|
|
|
|
fontWeight = FontWeight.Medium
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-26 03:23:00 +00:00
|
|
|
|
}
|
2025-11-27 02:56:42 +00:00
|
|
|
|
|
|
|
|
|
|
// 数据指标区域
|
|
|
|
|
|
Row(
|
|
|
|
|
|
modifier = Modifier.fillMaxWidth(),
|
|
|
|
|
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
|
|
|
|
|
) {
|
|
|
|
|
|
// 总消耗
|
|
|
|
|
|
Column(
|
|
|
|
|
|
modifier = Modifier.weight(1f)
|
|
|
|
|
|
) {
|
|
|
|
|
|
Row(
|
|
|
|
|
|
verticalAlignment = Alignment.Bottom,
|
|
|
|
|
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
|
|
|
|
|
) {
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = totalPower,
|
|
|
|
|
|
color = Color(0xFFA9F0FF),
|
|
|
|
|
|
fontSize = 20.sp,
|
|
|
|
|
|
fontWeight = FontWeight.Bold
|
|
|
|
|
|
)
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = "kWh",
|
|
|
|
|
|
color = Color(0xFF9AA0A6),
|
|
|
|
|
|
fontSize = 12.sp,
|
|
|
|
|
|
modifier = Modifier.padding(bottom = 2.dp)
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = "总消耗",
|
|
|
|
|
|
color = Color(0xFF9AA0A6),
|
|
|
|
|
|
fontSize = 11.sp
|
|
|
|
|
|
)
|
2025-11-26 03:23:00 +00:00
|
|
|
|
}
|
2025-11-27 02:56:42 +00:00
|
|
|
|
|
|
|
|
|
|
// 运行时长
|
|
|
|
|
|
Column(
|
|
|
|
|
|
modifier = Modifier.weight(1f)
|
|
|
|
|
|
) {
|
|
|
|
|
|
Row(
|
|
|
|
|
|
verticalAlignment = Alignment.Bottom,
|
|
|
|
|
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
|
|
|
|
|
) {
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = "$totalHours",
|
|
|
|
|
|
color = Color(0xFFB89CFF),
|
|
|
|
|
|
fontSize = 20.sp,
|
|
|
|
|
|
fontWeight = FontWeight.Bold
|
|
|
|
|
|
)
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = "小时",
|
|
|
|
|
|
color = Color(0xFF9AA0A6),
|
|
|
|
|
|
fontSize = 12.sp,
|
|
|
|
|
|
modifier = Modifier.padding(bottom = 2.dp)
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = "运行时长",
|
|
|
|
|
|
color = Color(0xFF9AA0A6),
|
|
|
|
|
|
fontSize = 11.sp
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 活跃设备
|
|
|
|
|
|
Column(
|
|
|
|
|
|
modifier = Modifier.weight(1f)
|
|
|
|
|
|
) {
|
|
|
|
|
|
Row(
|
|
|
|
|
|
verticalAlignment = Alignment.Bottom,
|
|
|
|
|
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
|
|
|
|
|
) {
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = "$activeDevices",
|
|
|
|
|
|
color = Color(0xFF00E676),
|
|
|
|
|
|
fontSize = 20.sp,
|
|
|
|
|
|
fontWeight = FontWeight.Bold
|
|
|
|
|
|
)
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = "台",
|
|
|
|
|
|
color = Color(0xFF9AA0A6),
|
|
|
|
|
|
fontSize = 12.sp,
|
|
|
|
|
|
modifier = Modifier.padding(bottom = 2.dp)
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = "活跃设备",
|
|
|
|
|
|
color = Color(0xFF9AA0A6),
|
|
|
|
|
|
fontSize = 11.sp
|
|
|
|
|
|
)
|
2025-11-26 03:23:00 +00:00
|
|
|
|
}
|
2025-11-26 02:30:03 +00:00
|
|
|
|
}
|
2025-11-27 02:56:42 +00:00
|
|
|
|
|
|
|
|
|
|
Spacer(modifier = Modifier.height(4.dp))
|
|
|
|
|
|
|
|
|
|
|
|
// 图表区域
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.fillMaxWidth()
|
|
|
|
|
|
.weight(1f)
|
|
|
|
|
|
) {
|
|
|
|
|
|
Row(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.fillMaxWidth()
|
|
|
|
|
|
.align(Alignment.BottomCenter),
|
|
|
|
|
|
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
|
|
|
|
|
verticalAlignment = Alignment.Bottom
|
|
|
|
|
|
) {
|
|
|
|
|
|
values.forEachIndexed { index, v ->
|
|
|
|
|
|
val normalizedHeight = (v / maxValue) * 60
|
|
|
|
|
|
val isHighlight = index == values.size - 1
|
|
|
|
|
|
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.weight(1f)
|
|
|
|
|
|
.height(normalizedHeight.dp)
|
|
|
|
|
|
.clip(RoundedCornerShape(topStart = 4.dp, topEnd = 4.dp))
|
|
|
|
|
|
.background(
|
|
|
|
|
|
if (isHighlight) {
|
|
|
|
|
|
Brush.verticalGradient(
|
|
|
|
|
|
colors = listOf(
|
|
|
|
|
|
Color(0xFFA9F0FF),
|
|
|
|
|
|
Color(0xFF00D1FF)
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Brush.verticalGradient(
|
|
|
|
|
|
colors = listOf(
|
|
|
|
|
|
Color(0xFF3A3A3A),
|
|
|
|
|
|
Color(0xFF2A2A2A)
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 时间标签
|
|
|
|
|
|
Row(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.fillMaxWidth()
|
|
|
|
|
|
.align(Alignment.BottomCenter)
|
|
|
|
|
|
.offset(y = 16.dp),
|
|
|
|
|
|
horizontalArrangement = Arrangement.SpaceBetween
|
|
|
|
|
|
) {
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = "12h前",
|
|
|
|
|
|
fontSize = 10.sp,
|
|
|
|
|
|
color = Color(0xFF666666)
|
|
|
|
|
|
)
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = "现在",
|
|
|
|
|
|
fontSize = 10.sp,
|
|
|
|
|
|
color = Color(0xFFA9F0FF),
|
|
|
|
|
|
fontWeight = FontWeight.Medium
|
|
|
|
|
|
)
|
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 02:56:42 +00:00
|
|
|
|
Box(modifier = modifier.height(220.dp).clip(RoundedCornerShape(24.dp)).background(Color(0x50121212)).padding(16.dp)) {
|
2025-11-26 03:43:39 +00:00
|
|
|
|
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
2025-11-26 02:30:03 +00:00
|
|
|
|
Row(horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
2025-11-26 03:23:00 +00:00
|
|
|
|
Column {
|
2025-11-26 03:43:39 +00:00
|
|
|
|
Text(text = "空调", fontWeight = FontWeight.SemiBold, color = Color.White, fontSize = 18.sp)
|
2025-11-26 03:23:00 +00:00
|
|
|
|
Text(text = roomName, fontSize = 12.sp, color = Color(0xFF9AA0A6))
|
|
|
|
|
|
}
|
|
|
|
|
|
Switch(checked = isOn, onCheckedChange = { isOn = it })
|
2025-11-26 02:30:03 +00:00
|
|
|
|
}
|
2025-11-26 03:43:39 +00:00
|
|
|
|
|
|
|
|
|
|
Row(
|
|
|
|
|
|
modifier = Modifier.fillMaxWidth(),
|
|
|
|
|
|
horizontalArrangement = Arrangement.SpaceBetween,
|
|
|
|
|
|
verticalAlignment = Alignment.CenterVertically
|
|
|
|
|
|
) {
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = "${temp.toInt()}°C",
|
|
|
|
|
|
style = MaterialTheme.typography.headlineLarge,
|
|
|
|
|
|
color = Color.White,
|
|
|
|
|
|
fontWeight = FontWeight.Bold
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
|
|
|
|
// 降温按钮
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.size(40.dp)
|
|
|
|
|
|
.clip(RoundedCornerShape(12.dp))
|
2025-11-27 02:56:42 +00:00
|
|
|
|
.background(Color(0x55FFFFFF))
|
2025-11-26 03:43:39 +00:00
|
|
|
|
.clickable(enabled = isOn && temp > 16f) {
|
|
|
|
|
|
if (temp > 16f) temp -= 1f
|
|
|
|
|
|
},
|
|
|
|
|
|
contentAlignment = Alignment.Center
|
|
|
|
|
|
) {
|
|
|
|
|
|
Text(text = "−", color = Color.White, fontSize = 24.sp, fontWeight = FontWeight.Bold)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 升温按钮
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.size(40.dp)
|
|
|
|
|
|
.clip(RoundedCornerShape(12.dp))
|
2025-11-27 02:56:42 +00:00
|
|
|
|
.background(Color(0x55FFFFFF))
|
2025-11-26 03:43:39 +00:00
|
|
|
|
.clickable(enabled = isOn && temp < 32f) {
|
|
|
|
|
|
if (temp < 32f) temp += 1f
|
|
|
|
|
|
},
|
|
|
|
|
|
contentAlignment = Alignment.Center
|
|
|
|
|
|
) {
|
|
|
|
|
|
Text(text = "+", color = Color.White, fontSize = 24.sp, fontWeight = FontWeight.Bold)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 自定义滑动条
|
|
|
|
|
|
CustomSlider(
|
2025-11-26 03:23:00 +00:00
|
|
|
|
value = temp,
|
|
|
|
|
|
onValueChange = { temp = it },
|
|
|
|
|
|
valueRange = 16f..32f,
|
|
|
|
|
|
enabled = isOn
|
|
|
|
|
|
)
|
2025-11-26 03:43:39 +00:00
|
|
|
|
|
|
|
|
|
|
// 温度标记
|
|
|
|
|
|
Row(
|
|
|
|
|
|
modifier = Modifier.fillMaxWidth(),
|
|
|
|
|
|
horizontalArrangement = Arrangement.SpaceBetween
|
|
|
|
|
|
) {
|
|
|
|
|
|
Text(text = "16°C", fontSize = 12.sp, color = Color(0xFF9AA0A6))
|
|
|
|
|
|
Text(text = "24°C", fontSize = 12.sp, color = Color(0xFF9AA0A6))
|
|
|
|
|
|
Text(text = "32°C", fontSize = 12.sp, color = Color(0xFF9AA0A6))
|
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-26 03:43:39 +00:00
|
|
|
|
Row(
|
|
|
|
|
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
|
|
|
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
|
|
|
|
modifier = Modifier.pointerInput(enabled) {
|
|
|
|
|
|
if (enabled) {
|
|
|
|
|
|
detectTapGestures { offset ->
|
|
|
|
|
|
// 计算点击的是第几个点
|
|
|
|
|
|
val dotWidth = 6.dp.toPx()
|
|
|
|
|
|
val spacing = 4.dp.toPx()
|
|
|
|
|
|
val totalWidth = (dotWidth + spacing) * total - spacing
|
|
|
|
|
|
val clickedIndex = ((offset.x / totalWidth) * total).toInt().coerceIn(0, total - 1)
|
|
|
|
|
|
// 更新百分比
|
|
|
|
|
|
val newPercent = (clickedIndex + 1).toFloat() / total
|
|
|
|
|
|
onPercentChange(newPercent)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
) {
|
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))
|
|
|
|
|
|
.clickable(enabled = enabled) {
|
|
|
|
|
|
// 点击某个点直接设置到该亮度
|
|
|
|
|
|
val newPercent = (i + 1).toFloat() / total
|
|
|
|
|
|
onPercentChange(newPercent)
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
2025-11-26 02:30:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
enum class Mode { HOME, AWAY, FUN }
|
|
|
|
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
|
|
fun ModeButtonsRow(onModeSelected: (Mode) -> Unit) {
|
|
|
|
|
|
BoxWithConstraints(modifier = Modifier.fillMaxWidth()) {
|
|
|
|
|
|
val density = LocalDensity.current
|
|
|
|
|
|
val buttonHeight = when {
|
|
|
|
|
|
maxWidth >= 1024.dp -> with(density) { 500f.toDp() }
|
|
|
|
|
|
maxWidth >= 600.dp -> with(density) { 460f.toDp() }
|
|
|
|
|
|
else -> with(density) { 160f.toDp() }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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-27 02:56:42 +00:00
|
|
|
|
Box(modifier = modifier.height(100.dp).clip(RoundedCornerShape(16.dp)).background(Color(0x50121212)).padding(12.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"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 雨雪下落动画
|
|
|
|
|
|
val dropOffset by infiniteTransition.animateFloat(
|
|
|
|
|
|
initialValue = 0f,
|
|
|
|
|
|
targetValue = 1f,
|
|
|
|
|
|
animationSpec = infiniteRepeatable(
|
|
|
|
|
|
animation = tween(1500, easing = LinearEasing)
|
|
|
|
|
|
),
|
|
|
|
|
|
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("雨") -> {
|
|
|
|
|
|
val dropCount = 8
|
|
|
|
|
|
val dropLength = 8.dp.toPx()
|
|
|
|
|
|
|
|
|
|
|
|
for (i in 0 until dropCount) {
|
|
|
|
|
|
val x = (i * width / dropCount) + (i * 17 % 30)
|
|
|
|
|
|
val startY = (i * 29 % height.toInt()).toFloat()
|
|
|
|
|
|
val currentY = (startY + dropOffset * height) % height
|
|
|
|
|
|
|
|
|
|
|
|
drawLine(
|
|
|
|
|
|
color = Color.White.copy(alpha = 0.3f),
|
|
|
|
|
|
start = Offset(x, currentY),
|
|
|
|
|
|
end = Offset(x - 2.dp.toPx(), currentY + dropLength),
|
|
|
|
|
|
strokeWidth = 1.5f.dp.toPx()
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 雪天:绘制飘落的雪花(圆点)
|
|
|
|
|
|
weather.contains("雪") -> {
|
|
|
|
|
|
val snowCount = 8
|
|
|
|
|
|
|
|
|
|
|
|
for (i in 0 until snowCount) {
|
|
|
|
|
|
val x = (i * width / snowCount) + (i * 23 % 40)
|
|
|
|
|
|
val startY = (i * 41 % height.toInt()).toFloat()
|
|
|
|
|
|
val currentY = (startY + dropOffset * height * 0.5f) % height
|
|
|
|
|
|
|
|
|
|
|
|
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("") }
|
|
|
|
|
|
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.fillMaxSize()
|
|
|
|
|
|
.clickable(
|
|
|
|
|
|
indication = null,
|
|
|
|
|
|
interactionSource = remember { MutableInteractionSource() }
|
|
|
|
|
|
) { onDismiss() },
|
|
|
|
|
|
contentAlignment = Alignment.Center
|
|
|
|
|
|
) {
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.width(400.dp)
|
|
|
|
|
|
.clip(RoundedCornerShape(24.dp))
|
|
|
|
|
|
.background(Color(0xFF1E1E1E))
|
|
|
|
|
|
.clickable(
|
|
|
|
|
|
indication = null,
|
|
|
|
|
|
interactionSource = remember { MutableInteractionSource() }
|
|
|
|
|
|
) { /* 阻止点击穿透 */ }
|
|
|
|
|
|
.padding(24.dp)
|
|
|
|
|
|
) {
|
|
|
|
|
|
Column(
|
|
|
|
|
|
verticalArrangement = Arrangement.spacedBy(20.dp)
|
|
|
|
|
|
) {
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = "添加房间",
|
|
|
|
|
|
fontSize = 20.sp,
|
|
|
|
|
|
fontWeight = FontWeight.Bold,
|
|
|
|
|
|
color = Color.White
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
androidx.compose.material3.TextField(
|
|
|
|
|
|
value = roomName,
|
|
|
|
|
|
onValueChange = { roomName = it },
|
|
|
|
|
|
placeholder = { Text("请输入房间名称", color = Color(0xFF9AA0A6)) },
|
|
|
|
|
|
modifier = Modifier.fillMaxWidth(),
|
|
|
|
|
|
colors = androidx.compose.material3.TextFieldDefaults.colors(
|
2025-11-27 02:56:42 +00:00
|
|
|
|
focusedContainerColor = Color(0x55FFFFFF),
|
|
|
|
|
|
unfocusedContainerColor = Color(0x55FFFFFF),
|
2025-11-26 02:30:03 +00:00
|
|
|
|
focusedTextColor = Color.White,
|
|
|
|
|
|
unfocusedTextColor = Color.White,
|
|
|
|
|
|
cursorColor = Color(0xFFA9F0FF),
|
|
|
|
|
|
focusedIndicatorColor = Color.Transparent,
|
|
|
|
|
|
unfocusedIndicatorColor = Color.Transparent
|
|
|
|
|
|
),
|
|
|
|
|
|
shape = RoundedCornerShape(12.dp),
|
|
|
|
|
|
singleLine = true
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
Row(
|
|
|
|
|
|
modifier = Modifier.fillMaxWidth(),
|
|
|
|
|
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
|
|
|
|
|
) {
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.weight(1f)
|
|
|
|
|
|
.height(48.dp)
|
|
|
|
|
|
.clip(RoundedCornerShape(12.dp))
|
2025-11-27 02:56:42 +00:00
|
|
|
|
.background(Color(0x55FFFFFF))
|
2025-11-26 02:30:03 +00:00
|
|
|
|
.clickable(
|
|
|
|
|
|
indication = null,
|
|
|
|
|
|
interactionSource = remember { MutableInteractionSource() }
|
|
|
|
|
|
) { onDismiss() },
|
|
|
|
|
|
contentAlignment = Alignment.Center
|
|
|
|
|
|
) {
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = "取消",
|
|
|
|
|
|
color = Color(0xFFB0B0B0),
|
|
|
|
|
|
fontWeight = FontWeight.Medium
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Box(
|
|
|
|
|
|
modifier = Modifier
|
|
|
|
|
|
.weight(1f)
|
|
|
|
|
|
.height(48.dp)
|
|
|
|
|
|
.clip(RoundedCornerShape(12.dp))
|
|
|
|
|
|
.background(
|
|
|
|
|
|
Brush.linearGradient(
|
|
|
|
|
|
listOf(Color(0xFFA9F0FF), Color(0xFFB89CFF))
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
.clickable(
|
|
|
|
|
|
indication = null,
|
|
|
|
|
|
interactionSource = remember { MutableInteractionSource() },
|
|
|
|
|
|
enabled = roomName.isNotBlank()
|
|
|
|
|
|
) {
|
|
|
|
|
|
if (roomName.isNotBlank()) {
|
|
|
|
|
|
onConfirm(roomName.trim())
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.alpha(if (roomName.isNotBlank()) 1f else 0.5f),
|
|
|
|
|
|
contentAlignment = Alignment.Center
|
|
|
|
|
|
) {
|
|
|
|
|
|
Text(
|
|
|
|
|
|
text = "确定",
|
|
|
|
|
|
color = Color.Black,
|
|
|
|
|
|
fontWeight = FontWeight.Bold
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|