diff --git a/app/src/main/java/com/example/smarthome/data/WeatherService.kt b/app/src/main/java/com/example/smarthome/data/WeatherService.kt new file mode 100644 index 0000000..78f546e --- /dev/null +++ b/app/src/main/java/com/example/smarthome/data/WeatherService.kt @@ -0,0 +1,329 @@ +package com.example.smarthome.data + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import android.location.Geocoder +import android.util.Log +import androidx.core.content.ContextCompat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import okhttp3.Request +import org.json.JSONObject +import java.util.Locale +import java.util.concurrent.TimeUnit + +data class WeatherInfo( + val temperature: String, + val weather: String, + val humidity: String, + val airQuality: String, + val aqi: Int, + val city: String +) + +object WeatherService { + private const val TAG = "WeatherService" + + private val client = OkHttpClient.Builder() + .connectTimeout(15, TimeUnit.SECONDS) + .readTimeout(15, TimeUnit.SECONDS) + .build() + + /** + * 获取天气信息 + * 优先使用GPS定位,失败则用IP定位 + */ + suspend fun getWeather(context: Context): WeatherInfo { + return withContext(Dispatchers.IO) { + // 1. 优先尝试GPS定位 + val gpsLocation = getGpsLocation(context) + if (gpsLocation != null) { + val (lat, lon, city) = gpsLocation + Log.d(TAG, "GPS location: $city ($lat, $lon)") + + // 用GPS坐标获取天气 + val weatherResult = getWeatherByLocation(lat, lon, city) + if (weatherResult != null) { + return@withContext weatherResult + } + } + + // 2. GPS失败,尝试wttr.in(IP定位) + Log.w(TAG, "GPS failed, trying wttr.in...") + val wttrResult = tryWttrIn() + if (wttrResult != null) { + return@withContext wttrResult + } + + // 3. wttr.in失败,尝试备用方案 + Log.w(TAG, "wttr.in failed, trying backup...") + val backupResult = tryBackupApi() + if (backupResult != null) { + return@withContext backupResult + } + + // 都失败了,返回模拟数据 + Log.e(TAG, "All weather APIs failed") + getSimulatedWeather() + } + } + + // 获取GPS位置 + @Suppress("DEPRECATION") + private fun getGpsLocation(context: Context): Triple? { + // 检查权限 + if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && + ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + Log.w(TAG, "No location permission") + return null + } + + val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as android.location.LocationManager + + // 尝试获取缓存的位置 + val providers = listOf( + android.location.LocationManager.GPS_PROVIDER, + android.location.LocationManager.NETWORK_PROVIDER, + android.location.LocationManager.PASSIVE_PROVIDER + ) + + for (provider in providers) { + try { + val location = locationManager.getLastKnownLocation(provider) + if (location != null) { + val lat = location.latitude + val lon = location.longitude + val city = getCityFromLocation(context, lat, lon) + Log.d(TAG, "Got location from $provider: $city ($lat, $lon)") + return Triple(lat, lon, city) + } + } catch (e: Exception) { + Log.w(TAG, "Failed to get location from $provider: ${e.message}") + } + } + + Log.w(TAG, "No cached GPS location available") + return null + } + + // 通过经纬度获取城市名 + @Suppress("DEPRECATION") + private fun getCityFromLocation(context: Context, lat: Double, lon: Double): String { + return try { + val geocoder = Geocoder(context, Locale.CHINESE) + val addresses = geocoder.getFromLocation(lat, lon, 1) + if (!addresses.isNullOrEmpty()) { + addresses[0].locality ?: addresses[0].subAdminArea ?: addresses[0].adminArea ?: "未知" + } else { + "未知" + } + } catch (e: Exception) { + Log.e(TAG, "Geocoder failed", e) + "未知" + } + } + + // 通过经纬度获取天气 + private fun getWeatherByLocation(lat: Double, lon: Double, city: String): WeatherInfo? { + return try { + // 使用7timer获取天气 + val weatherUrl = "https://www.7timer.info/bin/civil.php?lon=$lon&lat=$lat&ac=0&unit=metric&output=json" + val weatherRequest = Request.Builder().url(weatherUrl).build() + val weatherResponse = client.newCall(weatherRequest).execute() + val weatherBody = weatherResponse.body?.string() ?: "{}" + + if (!weatherResponse.isSuccessful) { + Log.e(TAG, "7timer failed: ${weatherResponse.code}") + return null + } + + val weatherJson = JSONObject(weatherBody) + val dataseries = weatherJson.optJSONArray("dataseries")?.optJSONObject(0) + + if (dataseries == null) { + Log.w(TAG, "7timer: No data") + return null + } + + val temp = dataseries.optInt("temp2m", 0).toString() + val humidity = dataseries.optInt("rh2m", 0).toString() + val weatherCode = dataseries.optString("weather", "") + val weather = convertWeatherCode(weatherCode) + + Log.d(TAG, "GPS weather success: $temp°C, $weather, $city") + + WeatherInfo(temp, weather, humidity, "良", 50, city) + } catch (e: Exception) { + Log.e(TAG, "getWeatherByLocation exception", e) + null + } + } + + // wttr.in API + private fun tryWttrIn(): WeatherInfo? { + return try { + val url = "https://wttr.in/?format=j1&lang=zh" + val request = Request.Builder() + .url(url) + .header("User-Agent", "SmartHome/1.0") + .build() + + val response = client.newCall(request).execute() + val body = response.body?.string() ?: "{}" + Log.d(TAG, "wttr.in response code: ${response.code}") + + if (!response.isSuccessful) { + Log.e(TAG, "wttr.in request failed: ${response.code}") + return null + } + + val json = JSONObject(body) + val currentCondition = json.optJSONArray("current_condition")?.optJSONObject(0) + val nearestArea = json.optJSONArray("nearest_area")?.optJSONObject(0) + + if (currentCondition == null) { + Log.w(TAG, "wttr.in: No current condition data") + return null + } + + val areaName = nearestArea?.optJSONArray("areaName")?.optJSONObject(0)?.optString("value", "") + val region = nearestArea?.optJSONArray("region")?.optJSONObject(0)?.optString("value", "") + + val cityName = when { + !areaName.isNullOrBlank() && areaName != "Unknown" -> convertToChineseCityName(areaName) + !region.isNullOrBlank() && region != "Unknown" -> convertToChineseCityName(region) + else -> "未知" + } + + val weatherDescArray = currentCondition.optJSONArray("lang_zh") + val weatherDesc = weatherDescArray?.optJSONObject(0)?.optString("value", "未知") ?: "未知" + val temp = currentCondition.optString("temp_C", "--") + val humidity = currentCondition.optString("humidity", "--") + + Log.d(TAG, "wttr.in success: $temp°C, $weatherDesc, $cityName") + + WeatherInfo(temp, weatherDesc, humidity, "良", 50, cityName) + } catch (e: Exception) { + Log.e(TAG, "wttr.in exception", e) + null + } + } + + // 备用API - 使用ip-api获取位置 + 7timer获取天气 + private fun tryBackupApi(): WeatherInfo? { + return try { + // 1. 获取IP位置 + val ipUrl = "http://ip-api.com/json/?lang=zh-CN" + val ipRequest = Request.Builder().url(ipUrl).build() + val ipResponse = client.newCall(ipRequest).execute() + val ipBody = ipResponse.body?.string() ?: "{}" + Log.d(TAG, "ip-api response: $ipBody") + + val ipJson = JSONObject(ipBody) + val city = ipJson.optString("city", "未知") + val lat = ipJson.optDouble("lat", 0.0) + val lon = ipJson.optDouble("lon", 0.0) + + if (lat == 0.0 && lon == 0.0) { + Log.w(TAG, "ip-api: Invalid location") + return null + } + + Log.d(TAG, "Location: $city ($lat, $lon)") + + // 2. 获取天气 - 使用7timer(免费,无需注册) + val weatherUrl = "https://www.7timer.info/bin/civil.php?lon=$lon&lat=$lat&ac=0&unit=metric&output=json" + val weatherRequest = Request.Builder().url(weatherUrl).build() + val weatherResponse = client.newCall(weatherRequest).execute() + val weatherBody = weatherResponse.body?.string() ?: "{}" + Log.d(TAG, "7timer response code: ${weatherResponse.code}") + + if (!weatherResponse.isSuccessful) { + Log.e(TAG, "7timer failed: ${weatherResponse.code}") + return null + } + + val weatherJson = JSONObject(weatherBody) + val dataseries = weatherJson.optJSONArray("dataseries")?.optJSONObject(0) + + if (dataseries == null) { + Log.w(TAG, "7timer: No data") + return null + } + + val temp = dataseries.optInt("temp2m", 0).toString() + val humidity = dataseries.optInt("rh2m", 0).toString() + val weatherCode = dataseries.optString("weather", "") + val weather = convertWeatherCode(weatherCode) + + Log.d(TAG, "7timer success: $temp°C, $weather, $city") + + WeatherInfo(temp, weather, humidity, "良", 50, city) + } catch (e: Exception) { + Log.e(TAG, "Backup API exception", e) + null + } + } + + // 7timer天气代码转中文 + private fun convertWeatherCode(code: String): String { + return when { + code.contains("clear") -> "晴" + code.contains("pcloudy") -> "多云" + code.contains("mcloudy") -> "多云" + code.contains("cloudy") -> "阴" + code.contains("humid") -> "潮湿" + code.contains("lightrain") -> "小雨" + code.contains("rain") -> "雨" + code.contains("oshower") -> "阵雨" + code.contains("ishower") -> "阵雨" + code.contains("lightsnow") -> "小雪" + code.contains("snow") -> "雪" + code.contains("ts") -> "雷暴" + else -> "晴" + } + } + + // 拼音城市名转中文(常用城市) + private fun convertToChineseCityName(pinyin: String): String { + val cityMap = mapOf( + "Beijing" to "北京", + "Shanghai" to "上海", + "Guangzhou" to "广州", + "Shenzhen" to "深圳", + "Hangzhou" to "杭州", + "Nanjing" to "南京", + "Wuxi" to "无锡", + "Suzhou" to "苏州", + "Chengdu" to "成都", + "Chongqing" to "重庆", + "Wuhan" to "武汉", + "Xi'an" to "西安", + "Tianjin" to "天津", + "Qingdao" to "青岛", + "Dalian" to "大连", + "Xiamen" to "厦门", + "Ningbo" to "宁波", + "Changsha" to "长沙", + "Jiangsu" to "江苏", + "Zhejiang" to "浙江", + "Guangdong" to "广东", + "Sichuan" to "四川" + ) + return cityMap[pinyin] ?: pinyin + } + + // 模拟天气数据(当API不可用时使用) + fun getSimulatedWeather(): WeatherInfo { + return WeatherInfo( + temperature = "24", + weather = "晴", + humidity = "45", + airQuality = "良", + aqi = 50, + city = "未知" + ) + } +} diff --git a/app/src/main/java/com/example/smarthome/ui/AutomationScreen.kt b/app/src/main/java/com/example/smarthome/ui/AutomationScreen.kt new file mode 100644 index 0000000..672eda5 --- /dev/null +++ b/app/src/main/java/com/example/smarthome/ui/AutomationScreen.kt @@ -0,0 +1,398 @@ +package com.example.smarthome.ui + +import android.content.Context +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken + +data class Automation( + val id: String = java.util.UUID.randomUUID().toString(), + val name: String, + val trigger: String, + val action: String, + var isEnabled: Boolean = true +) + +@Composable +fun AutomationScreen() { + val context = LocalContext.current + val prefs = remember { context.getSharedPreferences("automations", Context.MODE_PRIVATE) } + val gson = remember { Gson() } + + // 加载保存的自动化列表 + val savedAutomations = remember { + val json = prefs.getString("automation_list", null) + if (json != null) { + val type = object : TypeToken>() {}.type + gson.fromJson>(json, type) + } else { + listOf( + Automation(name = "早安自动化", trigger = "每天 7:00", action = "开启卧室灯光,播放音乐", isEnabled = true), + Automation(name = "离家自动化", trigger = "检测到离家", action = "关闭所有灯光和空调,开启安防", isEnabled = true), + Automation(name = "回家自动化", trigger = "检测到回家", action = "开启客厅灯光和空调", isEnabled = true), + Automation(name = "晚安自动化", trigger = "每天 23:00", action = "关闭所有灯光,空调调至睡眠模式", isEnabled = false), + Automation(name = "温度控制", trigger = "温度 > 28°C", action = "自动开启空调", isEnabled = true), + Automation(name = "安全警报", trigger = "检测到异常", action = "发送通知并开启警报", isEnabled = true) + ) + } + } + + var automations by remember { mutableStateOf(savedAutomations) } + var showAddDialog by remember { mutableStateOf(false) } + + // 保存到SharedPreferences + fun saveAutomations() { + val json = gson.toJson(automations) + prefs.edit().putString("automation_list", json).apply() + } + + fun toggleAutomation(automation: Automation) { + automations = automations.map { + if (it.id == automation.id) it.copy(isEnabled = !it.isEnabled) else it + } + saveAutomations() + } + + fun addAutomation(name: String, trigger: String, action: String) { + automations = automations + Automation(name = name, trigger = trigger, action = action) + saveAutomations() + } + + fun deleteAutomation(automation: Automation) { + automations = automations.filter { it.id != automation.id } + saveAutomations() + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column { + Text( + text = "自动化", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + color = Color.White + ) + Text( + text = "${automations.count { it.isEnabled }} 个自动化运行中", + fontSize = 14.sp, + color = Color(0xFF9AA0A6), + modifier = Modifier.padding(top = 4.dp) + ) + } + + Box( + modifier = Modifier + .height(40.dp) + .clip(RoundedCornerShape(20.dp)) + .background( + Brush.linearGradient( + listOf(Color(0xFFA9F0FF), Color(0xFFB89CFF)) + ) + ) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { showAddDialog = true } + .padding(horizontal = 20.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = "+ 新建", + color = Color.Black, + fontWeight = FontWeight.Medium, + fontSize = 14.sp + ) + } + } + + Spacer(modifier = Modifier.height(24.dp)) + + LazyColumn( + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + items(automations) { automation -> + AutomationCard( + automation = automation, + onToggle = { toggleAutomation(automation) }, + onDelete = { deleteAutomation(automation) } + ) + } + } + } + + if (showAddDialog) { + AddAutomationDialog( + onDismiss = { showAddDialog = false }, + onConfirm = { name, trigger, action -> + addAutomation(name, trigger, action) + showAddDialog = false + } + ) + } +} + +@Composable +fun AutomationCard(automation: Automation, onToggle: () -> Unit, onDelete: () -> Unit) { + var showDeleteConfirm by remember { mutableStateOf(false) } + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(20.dp)) + .background(Color(0x50121212)) + .padding(16.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = automation.name, + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + color = Color.White + ) + Spacer(modifier = Modifier.height(8.dp)) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .clip(RoundedCornerShape(8.dp)) + .background(Color(0x33A9F0FF)) + .padding(horizontal = 8.dp, vertical = 4.dp) + ) { + Text( + text = automation.trigger, + fontSize = 11.sp, + color = Color(0xFFA9F0FF) + ) + } + Text(text = "→", color = Color(0xFF9AA0A6), fontSize = 12.sp) + Text( + text = automation.action, + fontSize = 12.sp, + color = Color(0xFF9AA0A6) + ) + } + } + + Box( + modifier = Modifier + .size(52.dp, 30.dp) + .clip(RoundedCornerShape(15.dp)) + .background( + if (automation.isEnabled) { + Brush.linearGradient( + listOf(Color(0xFFA9F0FF), Color(0xFFB89CFF)) + ) + } else { + Brush.linearGradient( + listOf(Color(0x33222222), Color(0x33222222)) + ) + } + ) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { onToggle() } + .padding(3.dp) + ) { + Box( + modifier = Modifier + .size(24.dp) + .clip(RoundedCornerShape(12.dp)) + .background(Color.White) + .align(if (automation.isEnabled) Alignment.CenterEnd else Alignment.CenterStart) + ) + } + } + } +} + +@Composable +fun AddAutomationDialog( + onDismiss: () -> Unit, + onConfirm: (String, String, String) -> Unit +) { + var name by remember { mutableStateOf("") } + var trigger by remember { mutableStateOf("") } + var action by remember { mutableStateOf("") } + + Box( + modifier = Modifier + .fillMaxSize() + .background(Color(0x80000000)) + .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(16.dp) + ) { + Text( + text = "新建自动化", + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + color = Color.White + ) + + TextField( + value = name, + onValueChange = { name = it }, + placeholder = { Text("自动化名称", color = Color(0xFF9AA0A6)) }, + modifier = Modifier.fillMaxWidth(), + colors = TextFieldDefaults.colors( + focusedContainerColor = Color(0x28FFFFFF), + unfocusedContainerColor = Color(0x28FFFFFF), + focusedTextColor = Color.White, + unfocusedTextColor = Color.White, + cursorColor = Color(0xFFA9F0FF), + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent + ), + shape = RoundedCornerShape(12.dp), + singleLine = true + ) + + TextField( + value = trigger, + onValueChange = { trigger = it }, + placeholder = { Text("触发条件(如:每天 7:00)", color = Color(0xFF9AA0A6)) }, + modifier = Modifier.fillMaxWidth(), + colors = TextFieldDefaults.colors( + focusedContainerColor = Color(0x28FFFFFF), + unfocusedContainerColor = Color(0x28FFFFFF), + focusedTextColor = Color.White, + unfocusedTextColor = Color.White, + cursorColor = Color(0xFFA9F0FF), + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent + ), + shape = RoundedCornerShape(12.dp), + singleLine = true + ) + + TextField( + value = action, + onValueChange = { action = it }, + placeholder = { Text("执行动作", color = Color(0xFF9AA0A6)) }, + modifier = Modifier.fillMaxWidth(), + colors = TextFieldDefaults.colors( + focusedContainerColor = Color(0x28FFFFFF), + unfocusedContainerColor = Color(0x28FFFFFF), + focusedTextColor = Color.White, + unfocusedTextColor = Color.White, + cursorColor = Color(0xFFA9F0FF), + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent + ), + shape = RoundedCornerShape(12.dp), + maxLines = 2 + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Box( + modifier = Modifier + .weight(1f) + .height(48.dp) + .clip(RoundedCornerShape(12.dp)) + .background(Color(0x28FFFFFF)) + .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 = name.isNotBlank() && trigger.isNotBlank() && action.isNotBlank() + ) { + if (name.isNotBlank() && trigger.isNotBlank() && action.isNotBlank()) { + onConfirm(name.trim(), trigger.trim(), action.trim()) + } + } + .then( + if (name.isNotBlank() && trigger.isNotBlank() && action.isNotBlank()) { + Modifier + } else { + Modifier.alpha(0.5f) + } + ), + contentAlignment = Alignment.Center + ) { + Text( + text = "确定", + color = Color.Black, + fontWeight = FontWeight.Bold + ) + } + } + } + } + } +} diff --git a/app/src/main/java/com/example/smarthome/ui/MainScaffold.kt b/app/src/main/java/com/example/smarthome/ui/MainScaffold.kt index b4659b3..49c4748 100644 --- a/app/src/main/java/com/example/smarthome/ui/MainScaffold.kt +++ b/app/src/main/java/com/example/smarthome/ui/MainScaffold.kt @@ -60,15 +60,18 @@ fun MainScaffold( onNavItemSelect = onNavItemSelect ) - // 根据选中的导航项显示不同的内容 - 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) + // 内容区域 - 减少左侧间距 + 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) + } } } } @@ -95,7 +98,7 @@ fun DashboardContent( } } ) { - Column(modifier = Modifier.fillMaxSize().padding(16.dp)) { + Column(modifier = Modifier.fillMaxSize().padding(start = 0.dp, end = 16.dp, top = 16.dp, bottom = 16.dp)) { TopBar() RoomTabs( selectedRoom = selectedRoom, @@ -486,7 +489,7 @@ fun NavItem(title: String, selected: Boolean) { @Composable fun SideNavRail(selectedNavItem: Int = 0, onNavItemSelect: (Int) -> Unit = {}) { - Column(modifier = Modifier.width(120.dp).fillMaxHeight().padding(12.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { + Column(modifier = Modifier.width(100.dp).fillMaxHeight().padding(start = 12.dp, end = 4.dp, top = 12.dp, bottom = 12.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { Spacer(modifier = Modifier.height(48.dp)) NavRailItem("控制台", R.drawable.ic_dashboard, selectedNavItem == 0, onClick = { onNavItemSelect(0) }) NavRailItem("场景", R.drawable.ic_scene, selectedNavItem == 1, onClick = { onNavItemSelect(1) }) @@ -831,29 +834,23 @@ fun UsageStatusChart(modifier: Modifier = Modifier, roomName: String = "房间") 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()) } + val costToday = remember(roomName) { String.format("%.1f", 2 + Math.random() * 8) } + + // 主题色 + val accentColor = Color(0xFF00D1FF) Box( modifier = modifier - .height(220.dp) + .height(260.dp) .clip(RoundedCornerShape(24.dp)) .background( - Brush.linearGradient( + Brush.verticalGradient( colors = listOf( - Color(0x60121212), - Color(0x50121212) + accentColor.copy(alpha = 0.15f), + Color(0xFF1A1A2E).copy(alpha = 0.9f) ) ) ) - .border( - width = 1.dp, - brush = Brush.linearGradient( - colors = listOf( - Color(0x22FFFFFF), - Color(0x0AFFFFFF) - ) - ), - shape = RoundedCornerShape(24.dp) - ) .padding(16.dp) ) { Column( @@ -866,140 +863,153 @@ fun UsageStatusChart(modifier: Modifier = Modifier, roomName: String = "房间") 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) - ) + 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) + ) + } } // 趋势指示器 Box( modifier = Modifier - .clip(RoundedCornerShape(12.dp)) + .clip(RoundedCornerShape(8.dp)) .background( if (trend == "↑") Color(0x33FF5252) else Color(0x3300E676) ) - .padding(horizontal = 10.dp, vertical = 6.dp) + .padding(horizontal = 8.dp, vertical = 4.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 - ) - } + Text( + text = "$trend${trendPercent}%", + color = if (trend == "↑") Color(0xFFFF5252) else Color(0xFF00E676), + fontSize = 12.sp, + fontWeight = FontWeight.Bold + ) } } - // 数据指标区域 + // 主数据显示 Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(16.dp) + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Bottom ) { - // 总消耗 - Column( - modifier = Modifier.weight(1f) - ) { - Row( - verticalAlignment = Alignment.Bottom, - horizontalArrangement = Arrangement.spacedBy(4.dp) - ) { + // 今日用电 + Column { + Row(verticalAlignment = Alignment.Top) { Text( text = totalPower, - color = Color(0xFFA9F0FF), - fontSize = 20.sp, - fontWeight = FontWeight.Bold + fontSize = 42.sp, + color = Color.White, + fontWeight = FontWeight.Light ) Text( text = "kWh", - color = Color(0xFF9AA0A6), - fontSize = 12.sp, - modifier = Modifier.padding(bottom = 2.dp) + fontSize = 14.sp, + color = Color.White.copy(alpha = 0.6f), + modifier = Modifier.padding(top = 10.dp, start = 4.dp) ) } Text( - text = "总消耗", - color = Color(0xFF9AA0A6), - fontSize = 11.sp + text = "今日用电", + fontSize = 12.sp, + color = Color(0xFF9AA0A6) ) } - // 运行时长 - 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) - ) - } + // 预计费用 + Column(horizontalAlignment = Alignment.End) { Text( - text = "运行时长", - color = Color(0xFF9AA0A6), - fontSize = 11.sp + text = "¥$costToday", + fontSize = 20.sp, + color = accentColor, + fontWeight = FontWeight.Bold ) - } - - // 活跃设备 - 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 + text = "预计费用", + fontSize = 11.sp, + color = Color(0xFF9AA0A6) ) } } - Spacer(modifier = Modifier.height(4.dp)) + // 详细数据行 + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .background(Color(0xFF2A2A3E).copy(alpha = 0.5f)) + .padding(12.dp), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + // 运行时长 + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = "$totalHours", + color = Color(0xFFB89CFF), + fontSize = 18.sp, + fontWeight = FontWeight.Bold + ) + 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( + text = "$activeDevices", + color = Color(0xFF00E676), + fontSize = 18.sp, + fontWeight = FontWeight.Bold + ) + 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( + text = "${(800 + (Math.random() * 400).toInt())}", + color = Color(0xFFFFB74D), + fontSize = 18.sp, + fontWeight = FontWeight.Bold + ) + Text(text = "W峰值", fontSize = 10.sp, color = Color(0xFF9AA0A6)) + } + } // 图表区域 Box( @@ -1011,59 +1021,32 @@ fun UsageStatusChart(modifier: Modifier = Modifier, roomName: String = "房间") modifier = Modifier .fillMaxWidth() .align(Alignment.BottomCenter), - horizontalArrangement = Arrangement.spacedBy(6.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.Bottom ) { values.forEachIndexed { index, v -> - val normalizedHeight = (v / maxValue) * 60 + val normalizedHeight = (v / maxValue) * 50 val isHighlight = index == values.size - 1 Box( modifier = Modifier .weight(1f) .height(normalizedHeight.dp) - .clip(RoundedCornerShape(topStart = 4.dp, topEnd = 4.dp)) + .clip(RoundedCornerShape(topStart = 3.dp, topEnd = 3.dp)) .background( if (isHighlight) { Brush.verticalGradient( - colors = listOf( - Color(0xFFA9F0FF), - Color(0xFF00D1FF) - ) + colors = listOf(accentColor, accentColor.copy(alpha = 0.5f)) ) } else { Brush.verticalGradient( - colors = listOf( - Color(0xFF3A3A3A), - Color(0xFF2A2A2A) - ) + colors = listOf(Color(0xFF4A4A5A), Color(0xFF3A3A4A)) ) } ) ) } } - - // 时间标签 - 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 - ) - } } } } @@ -1073,76 +1056,180 @@ fun UsageStatusChart(modifier: Modifier = Modifier, roomName: String = "房间") fun AirConditionerCard(modifier: Modifier = Modifier, roomName: String = "房间") { var temp by remember { mutableStateOf(24f) } var isOn by remember { mutableStateOf(true) } + var selectedMode by remember { mutableStateOf(0) } // 0:制冷 1:制热 2:除湿 3:送风 + var fanSpeed by remember { mutableStateOf(1) } // 0:自动 1:低 2:中 3:高 - Box(modifier = modifier.height(220.dp).clip(RoundedCornerShape(24.dp)).background(Color(0x50121212)).padding(16.dp)) { - Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { - Row(horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { - Column { - Text(text = "空调", fontWeight = FontWeight.SemiBold, color = Color.White, fontSize = 18.sp) - Text(text = roomName, fontSize = 12.sp, color = Color(0xFF9AA0A6)) + val modes = listOf("❄️" to "制冷", "🔥" to "制热", "💧" to "除湿", "🌀" to "送风") + val fanSpeeds = listOf("自动", "低", "中", "高") + + // 根据模式决定主题色 + val accentColor = when (selectedMode) { + 0 -> Color(0xFF4FC3F7) // 制冷 - 蓝色 + 1 -> Color(0xFFFF8A65) // 制热 - 橙色 + 2 -> Color(0xFF81C784) // 除湿 - 绿色 + else -> Color(0xFFB0BEC5) // 送风 - 灰色 + } + + 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)) { + // 空调图标 + Box( + modifier = Modifier + .size(36.dp) + .clip(RoundedCornerShape(10.dp)) + .background(accentColor.copy(alpha = if (isOn) 0.3f else 0.1f)), + 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)) + } } - Switch(checked = isOn, onCheckedChange = { isOn = it }) + Switch( + checked = isOn, + onCheckedChange = { isOn = it }, + colors = SwitchDefaults.colors( + checkedThumbColor = Color.White, + checkedTrackColor = accentColor, + uncheckedThumbColor = Color.Gray, + uncheckedTrackColor = Color(0xFF3A3A4E) + ) + ) } + // 温度显示和调节 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(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) + ) + } - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - // 降温按钮 - Box( - modifier = Modifier - .size(40.dp) - .clip(RoundedCornerShape(12.dp)) - .background(Color(0x55FFFFFF)) - .clickable(enabled = isOn && temp > 16f) { - if (temp > 16f) temp -= 1f - }, - contentAlignment = Alignment.Center - ) { - Text(text = "−", color = Color.White, fontSize = 24.sp, fontWeight = FontWeight.Bold) - } - + // 温度调节按钮 + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { // 升温按钮 Box( modifier = Modifier - .size(40.dp) - .clip(RoundedCornerShape(12.dp)) - .background(Color(0x55FFFFFF)) - .clickable(enabled = isOn && temp < 32f) { - if (temp < 32f) temp += 1f - }, + .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 = Color.White, fontSize = 24.sp, fontWeight = FontWeight.Bold) + 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) } } } - // 自定义滑动条 - CustomSlider( - value = temp, - onValueChange = { temp = it }, - valueRange = 16f..32f, - enabled = isOn - ) - - // 温度标记 + // 模式选择 Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween + horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - 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)) + modes.forEachIndexed { index, (icon, name) -> + val isSelected = selectedMode == index + Box( + modifier = Modifier + .weight(1f) + .height(44.dp) + .clip(RoundedCornerShape(12.dp)) + .background( + if (isSelected && isOn) accentColor.copy(alpha = 0.4f) + else Color(0xFF2A2A3E).copy(alpha = 0.6f) + ) + .clickable(enabled = isOn) { selectedMode = index }, + contentAlignment = Alignment.Center + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text(text = icon, fontSize = 14.sp) + Text( + text = name, + fontSize = 10.sp, + color = if (isSelected && isOn) Color.White else Color(0xFF9AA0A6) + ) + } + } + } + } + + // 风速选择 + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text(text = "风速", fontSize = 12.sp, color = Color(0xFF9AA0A6)) + fanSpeeds.forEachIndexed { index, speed -> + val isSelected = fanSpeed == index + Box( + modifier = Modifier + .weight(1f) + .height(28.dp) + .clip(RoundedCornerShape(8.dp)) + .background( + if (isSelected && isOn) accentColor.copy(alpha = 0.5f) + else Color(0xFF2A2A3E).copy(alpha = 0.4f) + ) + .clickable(enabled = isOn) { fanSpeed = index }, + contentAlignment = Alignment.Center + ) { + Text( + text = speed, + fontSize = 11.sp, + color = if (isSelected && isOn) Color.White else Color(0xFF9AA0A6) + ) + } + } } } } diff --git a/app/src/main/java/com/example/smarthome/ui/SceneScreen.kt b/app/src/main/java/com/example/smarthome/ui/SceneScreen.kt new file mode 100644 index 0000000..87495ec --- /dev/null +++ b/app/src/main/java/com/example/smarthome/ui/SceneScreen.kt @@ -0,0 +1,178 @@ +package com.example.smarthome.ui + +import android.content.Context +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +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.material3.* +import androidx.compose.runtime.* +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.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.smarthome.R + +data class Scene( + val id: String, + val name: String, + val description: String, + val icon: Int, + val gradient: Brush, + var isActive: Boolean = false +) + +@Composable +fun SceneScreen() { + val context = LocalContext.current + val prefs = remember { context.getSharedPreferences("scenes", Context.MODE_PRIVATE) } + + val scenes = remember { + listOf( + Scene("home", "回家模式", "开启灯光、空调,关闭安防", R.drawable.ic_mode_home, + Brush.linearGradient(listOf(Color(0xFF00D1FF), Color(0xFF00C9A7)))), + Scene("away", "出门模式", "关闭所有设备,开启安防", R.drawable.ic_mode_away, + Brush.linearGradient(listOf(Color(0xFFFF8A65), Color(0xFFFF7043)))), + Scene("fun", "玩乐模式", "影音设备开启,灯光调暗", R.drawable.ic_mode_fun, + Brush.linearGradient(listOf(Color(0xFF6A3DFF), Color(0xFF3A0CA3)))), + Scene("sleep", "睡眠模式", "关闭灯光,空调调至舒适温度", R.drawable.ic_curtain, + Brush.linearGradient(listOf(Color(0xFF5E35B1), Color(0xFF311B92)))), + Scene("reading", "阅读模式", "主灯调亮,关闭电视", R.drawable.ic_light, + Brush.linearGradient(listOf(Color(0xFFFFD54F), Color(0xFFFF8F00)))), + Scene("movie", "观影模式", "灯光调暗,窗帘关闭", R.drawable.ic_media, + Brush.linearGradient(listOf(Color(0xFFE91E63), Color(0xFFC2185B)))) + ).map { scene -> + scene.copy(isActive = prefs.getBoolean(scene.id, false)) + } + } + + var sceneStates by remember { mutableStateOf(scenes.associate { it.id to it.isActive }) } + + fun toggleScene(sceneId: String) { + val newState = !(sceneStates[sceneId] ?: false) + sceneStates = sceneStates.toMutableMap().apply { put(sceneId, newState) } + prefs.edit().putBoolean(sceneId, newState).apply() + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + Text( + text = "场景模式", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + color = Color.White, + modifier = Modifier.padding(bottom = 8.dp) + ) + + Text( + text = "一键切换家居场景", + fontSize = 14.sp, + color = Color(0xFF9AA0A6), + modifier = Modifier.padding(bottom = 24.dp) + ) + + LazyVerticalGrid( + columns = GridCells.Adaptive(minSize = 160.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + items(scenes) { scene -> + SceneCard( + scene = scene, + isActive = sceneStates[scene.id] ?: false, + onToggle = { toggleScene(scene.id) } + ) + } + } + } +} + +@Composable +fun SceneCard(scene: Scene, isActive: Boolean, onToggle: () -> Unit) { + + Box( + modifier = Modifier + .height(180.dp) + .then( + if (isActive) { + Modifier + .shadow(12.dp, RoundedCornerShape(24.dp)) + .clip(RoundedCornerShape(24.dp)) + .background(scene.gradient) + } else { + Modifier + .clip(RoundedCornerShape(24.dp)) + .background(Color(0x50121212)) + } + ) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { onToggle() } + .padding(20.dp) + ) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.SpaceBetween + ) { + Box( + modifier = Modifier + .size(56.dp) + .clip(RoundedCornerShape(16.dp)) + .background(Color(0x33FFFFFF)), + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource(id = scene.icon), + contentDescription = scene.name, + modifier = Modifier.size(32.dp), + colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(Color.White) + ) + } + + Column { + Text( + text = scene.name, + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + color = Color.White + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = scene.description, + fontSize = 12.sp, + color = Color(0xCCFFFFFF), + lineHeight = 16.sp + ) + } + } + + if (isActive) { + Box( + modifier = Modifier + .align(Alignment.TopEnd) + .size(24.dp) + .clip(RoundedCornerShape(12.dp)) + .background(Color(0x33FFFFFF)), + contentAlignment = Alignment.Center + ) { + Text(text = "✓", color = Color.White, fontSize = 14.sp, fontWeight = FontWeight.Bold) + } + } + } +} diff --git a/app/src/main/java/com/example/smarthome/ui/SecurityScreen.kt b/app/src/main/java/com/example/smarthome/ui/SecurityScreen.kt new file mode 100644 index 0000000..ba6b14d --- /dev/null +++ b/app/src/main/java/com/example/smarthome/ui/SecurityScreen.kt @@ -0,0 +1,284 @@ +package com.example.smarthome.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.sp +import com.example.smarthome.R + +data class SecurityEvent( + val title: String, + val time: String, + val type: String, // "info", "warning", "alert" + val description: String +) + +@Composable +fun SecurityScreen() { + val context = LocalContext.current + val sharedPreferences = remember { + context.getSharedPreferences("smart_home_prefs", android.content.Context.MODE_PRIVATE) + } + + // 从SharedPreferences加载安防状态 + var isArmed by remember { + mutableStateOf(sharedPreferences.getBoolean("security_armed", false)) + } + + // 保存安防状态的函数 + fun saveArmedState(armed: Boolean) { + sharedPreferences.edit().putBoolean("security_armed", armed).apply() + isArmed = armed + } + + val events = remember { + listOf( + SecurityEvent("前门已锁定", "2分钟前", "info", "智能门锁自动上锁"), + SecurityEvent("检测到异常移动", "15分钟前", "warning", "客厅摄像头检测到移动"), + SecurityEvent("窗户已关闭", "1小时前", "info", "所有窗户传感器确认关闭"), + SecurityEvent("烟雾警报测试", "今天 10:30", "info", "烟雾探测器自检正常"), + SecurityEvent("门铃按下", "昨天 18:45", "info", "有访客按门铃") + ) + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + Text( + text = "安全中心", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + color = Color.White, + modifier = Modifier.padding(bottom = 8.dp) + ) + + Text( + text = if (isArmed) "安防系统已启动" else "安防系统未启动", + fontSize = 14.sp, + color = if (isArmed) Color(0xFF00E676) else Color(0xFF9AA0A6), + modifier = Modifier.padding(bottom = 24.dp) + ) + + LazyColumn( + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + item { + SecurityStatusCard(isArmed = isArmed, onToggle = { saveArmedState(it) }) + } + + item { + SecurityDevicesCard() + } + + item { + Text( + text = "最近事件", + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = Color.White, + modifier = Modifier.padding(vertical = 8.dp) + ) + } + + items(events) { event -> + SecurityEventCard(event) + } + } + } +} + +@Composable +fun SecurityStatusCard(isArmed: Boolean, onToggle: (Boolean) -> Unit) { + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(24.dp)) + .background( + if (isArmed) { + Brush.linearGradient( + listOf(Color(0xFF00E676), Color(0xFF00C853)) + ) + } else { + Brush.linearGradient( + listOf(Color(0x60121212), Color(0x50121212)) + ) + } + ) + .padding(24.dp) + ) { + Column { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column { + Text( + text = if (isArmed) "安防已启动" else "安防未启动", + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + color = if (isArmed) Color.Black else Color.White + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = if (isArmed) "家庭安全受到保护" else "点击启动安防系统", + fontSize = 14.sp, + color = if (isArmed) Color(0xCC000000) else Color(0xFF9AA0A6) + ) + } + + Box( + modifier = Modifier + .size(64.dp) + .clip(RoundedCornerShape(16.dp)) + .background( + if (isArmed) Color(0x33000000) else Color(0x33FFFFFF) + ) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { onToggle(!isArmed) }, + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource(id = R.drawable.ic_security), + contentDescription = "安防", + modifier = Modifier.size(32.dp), + colorFilter = androidx.compose.ui.graphics.ColorFilter.tint( + if (isArmed) Color.Black else Color.White + ) + ) + } + } + } + } +} + +@Composable +fun SecurityDevicesCard() { + val devices = listOf( + "智能门锁" to true, + "门窗传感器" to true, + "烟雾探测器" to true, + "摄像头" to true, + "燃气传感器" to true + ) + + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(24.dp)) + .background(Color(0x50121212)) + .padding(20.dp) + ) { + Column { + Text( + text = "安防设备", + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = Color.White, + modifier = Modifier.padding(bottom = 16.dp) + ) + + devices.forEach { (name, status) -> + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = name, + fontSize = 14.sp, + color = Color.White + ) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .size(8.dp) + .clip(RoundedCornerShape(4.dp)) + .background(if (status) Color(0xFF00E676) else Color(0xFFFF5252)) + ) + Text( + text = if (status) "正常" else "异常", + fontSize = 12.sp, + color = if (status) Color(0xFF00E676) else Color(0xFFFF5252) + ) + } + } + } + } + } +} + +@Composable +fun SecurityEventCard(event: SecurityEvent) { + val backgroundColor = when (event.type) { + "alert" -> Color(0x33FF5252) + "warning" -> Color(0x33FFA726) + else -> Color(0x50121212) + } + + val accentColor = when (event.type) { + "alert" -> Color(0xFFFF5252) + "warning" -> Color(0xFFFFA726) + else -> Color(0xFFA9F0FF) + } + + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .background(backgroundColor) + .padding(16.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = event.title, + fontSize = 15.sp, + fontWeight = FontWeight.SemiBold, + color = Color.White + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = event.description, + fontSize = 12.sp, + color = Color(0xFF9AA0A6) + ) + } + Text( + text = event.time, + fontSize = 11.sp, + color = accentColor + ) + } + } +} diff --git a/app/src/main/java/com/example/smarthome/ui/StatisticsScreen.kt b/app/src/main/java/com/example/smarthome/ui/StatisticsScreen.kt new file mode 100644 index 0000000..120ee54 --- /dev/null +++ b/app/src/main/java/com/example/smarthome/ui/StatisticsScreen.kt @@ -0,0 +1,301 @@ +package com.example.smarthome.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun StatisticsScreen() { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + Text( + text = "能耗统计", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + color = Color.White, + modifier = Modifier.padding(bottom = 8.dp) + ) + + Text( + text = "本月数据分析", + fontSize = 14.sp, + color = Color(0xFF9AA0A6), + modifier = Modifier.padding(bottom = 24.dp) + ) + + LazyColumn( + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + item { OverallStatsCard() } + item { RoomStatsCard() } + item { DeviceRankingCard() } + } + } +} + +@Composable +fun OverallStatsCard() { + Box( + modifier = Modifier + .fillMaxWidth() + .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(20.dp) + ) { + Column { + Text( + text = "本月总览", + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = Color.White, + modifier = Modifier.padding(bottom = 16.dp) + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + StatItem( + label = "总消耗", + value = "342.5", + unit = "kWh", + color = Color(0xFFA9F0FF), + modifier = Modifier.weight(1f) + ) + StatItem( + label = "总费用", + value = "¥205", + unit = "", + color = Color(0xFFB89CFF), + modifier = Modifier.weight(1f) + ) + StatItem( + label = "运行时长", + value = "720", + unit = "小时", + color = Color(0xFF00E676), + modifier = Modifier.weight(1f) + ) + } + } + } +} + +@Composable +fun StatItem(label: String, value: String, unit: String, color: Color, modifier: Modifier = Modifier) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row( + verticalAlignment = Alignment.Bottom, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = value, + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + color = color + ) + if (unit.isNotEmpty()) { + Text( + text = unit, + fontSize = 12.sp, + color = Color(0xFF9AA0A6), + modifier = Modifier.padding(bottom = 4.dp) + ) + } + } + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = label, + fontSize = 12.sp, + color = Color(0xFF9AA0A6) + ) + } +} + +@Composable +fun RoomStatsCard() { + val rooms = listOf( + Triple("客厅", 85.2f, 0.35f), + Triple("卧室", 62.8f, 0.26f), + Triple("厨房", 48.5f, 0.20f), + Triple("影音室", 32.1f, 0.13f), + Triple("游戏房", 15.9f, 0.06f) + ) + + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(24.dp)) + .background(Color(0x50121212)) + .padding(20.dp) + ) { + Column { + Text( + text = "房间能耗排名", + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = Color.White, + modifier = Modifier.padding(bottom = 16.dp) + ) + + rooms.forEach { (name, value, percentage) -> + RoomStatItem(name, value, percentage) + Spacer(modifier = Modifier.height(12.dp)) + } + } + } +} + +@Composable +fun RoomStatItem(name: String, value: Float, percentage: Float) { + Column { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = name, + fontSize = 14.sp, + color = Color.White + ) + Text( + text = "${value} kWh", + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = Color(0xFFA9F0FF) + ) + } + Spacer(modifier = Modifier.height(6.dp)) + Box( + modifier = Modifier + .fillMaxWidth() + .height(6.dp) + .clip(RoundedCornerShape(3.dp)) + .background(Color(0x33FFFFFF)) + ) { + Box( + modifier = Modifier + .fillMaxWidth(percentage) + .fillMaxHeight() + .background( + Brush.horizontalGradient( + listOf(Color(0xFFA9F0FF), Color(0xFFB89CFF)) + ) + ) + ) + } + } +} + +@Composable +fun DeviceRankingCard() { + val devices = listOf( + "空调" to "156.3 kWh", + "热水器" to "78.5 kWh", + "冰箱" to "45.2 kWh", + "洗衣机" to "32.8 kWh", + "电视" to "29.7 kWh" + ) + + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(24.dp)) + .background(Color(0x50121212)) + .padding(20.dp) + ) { + Column { + Text( + text = "设备能耗排名", + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + color = Color.White, + modifier = Modifier.padding(bottom = 16.dp) + ) + + devices.forEachIndexed { index, (name, value) -> + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .size(32.dp) + .clip(RoundedCornerShape(8.dp)) + .background( + when (index) { + 0 -> Brush.linearGradient(listOf(Color(0xFFFFD700), Color(0xFFFFA500))) + 1 -> Brush.linearGradient(listOf(Color(0xFFC0C0C0), Color(0xFF808080))) + 2 -> Brush.linearGradient(listOf(Color(0xFFCD7F32), Color(0xFF8B4513))) + else -> Brush.linearGradient(listOf(Color(0x33FFFFFF), Color(0x33FFFFFF))) + } + ), + contentAlignment = Alignment.Center + ) { + Text( + text = "${index + 1}", + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + color = Color.White + ) + } + Text( + text = name, + fontSize = 14.sp, + color = Color.White + ) + } + Text( + text = value, + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = Color(0xFF9AA0A6) + ) + } + } + } + } +} diff --git a/app/src/main/res/drawable/background.jpg b/app/src/main/res/drawable/background.jpg new file mode 100644 index 0000000..e6adbd2 Binary files /dev/null and b/app/src/main/res/drawable/background.jpg differ diff --git a/app/src/main/res/drawable/ic_automation.xml b/app/src/main/res/drawable/ic_automation.xml new file mode 100644 index 0000000..497ab8e --- /dev/null +++ b/app/src/main/res/drawable/ic_automation.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_mode_away.xml b/app/src/main/res/drawable/ic_mode_away.xml new file mode 100644 index 0000000..9061809 --- /dev/null +++ b/app/src/main/res/drawable/ic_mode_away.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_mode_fun.xml b/app/src/main/res/drawable/ic_mode_fun.xml new file mode 100644 index 0000000..0db5bb5 --- /dev/null +++ b/app/src/main/res/drawable/ic_mode_fun.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_mode_home.xml b/app/src/main/res/drawable/ic_mode_home.xml new file mode 100644 index 0000000..bd23778 --- /dev/null +++ b/app/src/main/res/drawable/ic_mode_home.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_scene.xml b/app/src/main/res/drawable/ic_scene.xml new file mode 100644 index 0000000..d4c209f --- /dev/null +++ b/app/src/main/res/drawable/ic_scene.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_statistics.xml b/app/src/main/res/drawable/ic_statistics.xml new file mode 100644 index 0000000..b94951b --- /dev/null +++ b/app/src/main/res/drawable/ic_statistics.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/icon.png b/app/src/main/res/drawable/icon.png new file mode 100644 index 0000000..bf7a8e0 Binary files /dev/null and b/app/src/main/res/drawable/icon.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..e2d9ed7 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..e2d9ed7 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..4cfa370 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..4cfa370 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..b0c2dfb Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..b0c2dfb Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..b2911e1 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..b2911e1 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..3f55fb0 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..3f55fb0 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/background.jpg b/background.jpg new file mode 100644 index 0000000..e6adbd2 Binary files /dev/null and b/background.jpg differ diff --git a/ed25519-private.pem b/ed25519-private.pem new file mode 100644 index 0000000..8b6195b --- /dev/null +++ b/ed25519-private.pem @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIJ2hPOHEhH2nfZaN139WbrOTqR5pTrlNU3mlDO67G8QI +-----END PRIVATE KEY----- diff --git a/ed25519-public.pem b/ed25519-public.pem new file mode 100644 index 0000000..f7a4cc6 --- /dev/null +++ b/ed25519-public.pem @@ -0,0 +1,3 @@ +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEAWqYmIIeu1UuVhPIcTPZpVWRTF9bq2bH+1rY+LeHyv1E= +-----END PUBLIC KEY----- diff --git a/icon.svg b/icon.svg new file mode 100644 index 0000000..779d558 --- /dev/null +++ b/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file