修改菜单边距,空调和能耗面板样式修改
329
app/src/main/java/com/example/smarthome/data/WeatherService.kt
Normal file
@ -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<Double, Double, String>? {
|
||||
// 检查权限
|
||||
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 = "未知"
|
||||
)
|
||||
}
|
||||
}
|
||||
398
app/src/main/java/com/example/smarthome/ui/AutomationScreen.kt
Normal file
@ -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<List<Automation>>() {}.type
|
||||
gson.fromJson<List<Automation>>(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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
178
app/src/main/java/com/example/smarthome/ui/SceneScreen.kt
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
284
app/src/main/java/com/example/smarthome/ui/SecurityScreen.kt
Normal file
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
301
app/src/main/java/com/example/smarthome/ui/StatisticsScreen.kt
Normal file
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
app/src/main/res/drawable/background.jpg
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
31
app/src/main/res/drawable/ic_automation.xml
Normal file
@ -0,0 +1,31 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<!-- 自动化图标 - 齿轮和箭头 -->
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M12,2 L10.5,4.5 L7.5,4.5 L6,7 L3.5,8.5 L3.5,11.5 L6,13 L7.5,15.5 L10.5,15.5 L12,18 L13.5,15.5 L16.5,15.5 L18,13 L20.5,11.5 L20.5,8.5 L18,7 L16.5,4.5 L13.5,4.5 Z M12,8 C14.21,8 16,9.79 16,12 C16,14.21 14.21,16 12,16 C9.79,16 8,14.21 8,12 C8,9.79 9.79,8 12,8 Z"/>
|
||||
|
||||
<!-- 内圈 -->
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M12,10 C10.9,10 10,10.9 10,12 C10,13.1 10.9,14 12,14 C13.1,14 14,13.1 14,12 C14,10.9 13.1,10 12,10 Z"/>
|
||||
|
||||
<!-- 循环箭头 -->
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="1.5"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,12 C19,15.87 15.87,19 12,19 C8.13,19 5,15.87 5,12 M5,12 L7,10 M5,12 L7,14"/>
|
||||
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="1.5"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M5,12 C5,8.13 8.13,5 12,5 C15.87,5 19,8.13 19,12 M19,12 L17,10 M19,12 L17,14"/>
|
||||
</vector>
|
||||
25
app/src/main/res/drawable/ic_mode_away.xml
Normal file
@ -0,0 +1,25 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="48"
|
||||
android:viewportHeight="48">
|
||||
|
||||
<!-- 锁的主体 -->
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M32,20 L30,20 L30,16 C30,12.69 27.31,10 24,10 C20.69,10 18,12.69 18,16 L18,20 L16,20 C14.9,20 14,20.9 14,22 L14,36 C14,37.1 14.9,38 16,38 L32,38 C33.1,38 34,37.1 34,36 L34,22 C34,20.9 33.1,20 32,20 Z M24,32 C22.9,32 22,31.1 22,30 C22,28.9 22.9,28 24,28 C25.1,28 26,28.9 26,30 C26,31.1 25.1,32 24,32 Z M28,20 L20,20 L20,16 C20,13.79 21.79,12 24,12 C26.21,12 28,13.79 28,16 Z"/>
|
||||
|
||||
<!-- 锁孔高光 -->
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillAlpha="0.4"
|
||||
android:pathData="M24,28 C25.1,28 26,28.9 26,30 C26,31.1 25.1,32 24,32 C22.9,32 22,31.1 22,30 C22,28.9 22.9,28 24,28 Z"/>
|
||||
|
||||
<!-- 装饰性光晕 -->
|
||||
<path
|
||||
android:strokeColor="#FFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeAlpha="0.3"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M24,6 C13.51,6 5,14.51 5,25 C5,35.49 13.51,44 24,44 C34.49,44 43,35.49 43,25 C43,14.51 34.49,6 24,6 Z"/>
|
||||
</vector>
|
||||
35
app/src/main/res/drawable/ic_mode_fun.xml
Normal file
@ -0,0 +1,35 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="48"
|
||||
android:viewportHeight="48">
|
||||
|
||||
<!-- 游戏手柄完整主体 -->
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M32,14 L16,14 C11.58,14 8,17.58 8,22 L8,30 C8,33 10,36 12,38 C13,37 14,35 14,33 L14,26 C14,28.21 15.79,30 18,30 L30,30 C32.21,30 34,28.21 34,26 L34,33 C34,35 35,37 36,38 C38,36 40,33 40,30 L40,22 C40,17.58 36.42,14 32,14 Z"/>
|
||||
|
||||
<!-- 左侧十字键 -->
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:fillAlpha="0.3"
|
||||
android:pathData="M18,17 L20,17 L20,19 L22,19 L22,21 L20,21 L20,23 L18,23 L18,21 L16,21 L16,19 L18,19 Z"/>
|
||||
|
||||
<!-- 右侧按钮 A -->
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:fillAlpha="0.3"
|
||||
android:pathData="M32,18 C32.83,18 33.5,18.67 33.5,19.5 C33.5,20.33 32.83,21 32,21 C31.17,21 30.5,20.33 30.5,19.5 C30.5,18.67 31.17,18 32,18 Z"/>
|
||||
|
||||
<!-- 右侧按钮 B -->
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:fillAlpha="0.3"
|
||||
android:pathData="M28,18 C28.83,18 29.5,18.67 29.5,19.5 C29.5,20.33 28.83,21 28,21 C27.17,21 26.5,20.33 26.5,19.5 C26.5,18.67 27.17,18 28,18 Z"/>
|
||||
|
||||
<!-- 装饰星星 -->
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillAlpha="0.6"
|
||||
android:pathData="M24,8 L25,11 L28,11 L25.5,13 L26.5,16 L24,14 L21.5,16 L22.5,13 L20,11 L23,11 Z"/>
|
||||
</vector>
|
||||
32
app/src/main/res/drawable/ic_mode_home.xml
Normal file
@ -0,0 +1,32 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="48"
|
||||
android:viewportHeight="48">
|
||||
|
||||
<!-- 房子主体 -->
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M24,8 L10,18 L10,38 C10,39.1 10.9,40 12,40 L20,40 L20,30 L28,30 L28,40 L36,40 C37.1,40 38,39.1 38,38 L38,18 Z"/>
|
||||
|
||||
<!-- 房顶 -->
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M24,4 L4,20 L8,20 L24,8 L40,20 L44,20 Z"/>
|
||||
|
||||
<!-- 门 -->
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillAlpha="0.3"
|
||||
android:pathData="M20,30 L28,30 L28,40 L20,40 Z"/>
|
||||
|
||||
<!-- 窗户 -->
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillAlpha="0.3"
|
||||
android:pathData="M14,22 L18,22 L18,26 L14,26 Z"/>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillAlpha="0.3"
|
||||
android:pathData="M30,22 L34,22 L34,26 L30,26 Z"/>
|
||||
</vector>
|
||||
31
app/src/main/res/drawable/ic_scene.xml
Normal file
@ -0,0 +1,31 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<!-- 场景图标 - 魔法棒 -->
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M20,8 L18,8 L18,6 C18,5.45 17.55,5 17,5 C16.45,5 16,5.45 16,6 L16,8 L14,8 C13.45,8 13,8.45 13,9 C13,9.55 13.45,10 14,10 L16,10 L16,12 C16,12.55 16.45,13 17,13 C17.55,13 18,12.55 18,12 L18,10 L20,10 C20.55,10 21,9.55 21,9 C21,8.45 20.55,8 20,8 Z"/>
|
||||
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M11,4 L10,4 L10,3 C10,2.45 9.55,2 9,2 C8.45,2 8,2.45 8,3 L8,4 L7,4 C6.45,4 6,4.45 6,5 C6,5.55 6.45,6 7,6 L8,6 L8,7 C8,7.55 8.45,8 9,8 C9.55,8 10,7.55 10,7 L10,6 L11,6 C11.55,6 12,5.55 12,5 C12,4.45 11.55,4 11,4 Z"/>
|
||||
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M3.5,11 L3.5,10 L2.5,10 C2.22,10 2,9.78 2,9.5 C2,9.22 2.22,9 2.5,9 L3.5,9 L3.5,8 C3.5,7.72 3.72,7.5 4,7.5 C4.28,7.5 4.5,7.72 4.5,8 L4.5,9 L5.5,9 C5.78,9 6,9.22 6,9.5 C6,9.78 5.78,10 5.5,10 L4.5,10 L4.5,11 C4.5,11.28 4.28,11.5 4,11.5 C3.72,11.5 3.5,11.28 3.5,11 Z"/>
|
||||
|
||||
<!-- 魔法棒主体 -->
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M15.5,14.5 L4.5,3.5 C4.11,3.11 3.48,3.11 3.09,3.5 L3.5,3.91 L14.5,14.91 L15.5,14.5 Z"/>
|
||||
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M15,15 L4,4"/>
|
||||
</vector>
|
||||
32
app/src/main/res/drawable/ic_statistics.xml
Normal file
@ -0,0 +1,32 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<!-- 统计图标 - 柱状图 -->
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M5,19 L5,9 C5,8.45 5.45,8 6,8 L8,8 C8.55,8 9,8.45 9,9 L9,19 C9,19.55 8.55,20 8,20 L6,20 C5.45,20 5,19.55 5,19 Z"/>
|
||||
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M10,19 L10,5 C10,4.45 10.45,4 11,4 L13,4 C13.55,4 14,4.45 14,5 L14,19 C14,19.55 13.55,20 13,20 L11,20 C10.45,20 10,19.55 10,19 Z"/>
|
||||
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M15,19 L15,12 C15,11.45 15.45,11 16,11 L18,11 C18.55,11 19,11.45 19,12 L19,19 C19,19.55 18.55,20 18,20 L16,20 C15.45,20 15,19.55 15,19 Z"/>
|
||||
|
||||
<!-- 趋势线 -->
|
||||
<path
|
||||
android:strokeColor="#FFFFFFFF"
|
||||
android:strokeWidth="1.5"
|
||||
android:strokeLineCap="round"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M3,17 L7,13 L12,15 L17,9 L21,11"/>
|
||||
|
||||
<!-- 箭头 -->
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M21,11 L19,9 L19,13 Z"/>
|
||||
</vector>
|
||||
BIN
app/src/main/res/drawable/icon.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
background.jpg
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
3
ed25519-private.pem
Normal file
@ -0,0 +1,3 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MC4CAQAwBQYDK2VwBCIEIJ2hPOHEhH2nfZaN139WbrOTqR5pTrlNU3mlDO67G8QI
|
||||
-----END PRIVATE KEY-----
|
||||
3
ed25519-public.pem
Normal file
@ -0,0 +1,3 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MCowBQYDK2VwAyEAWqYmIIeu1UuVhPIcTPZpVWRTF9bq2bH+1rY+LeHyv1E=
|
||||
-----END PUBLIC KEY-----
|
||||
1
icon.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1764135432659" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8969" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 540.444444m-312.888889 0a312.888889 312.888889 0 1 0 625.777778 0 312.888889 312.888889 0 1 0-625.777778 0Z" fill="#bfbfbf" p-id="8970" data-spm-anchor-id="a313x.search_index.0.i0.209b3a81CSm3N4" class=""></path><path d="M512 398.222222c227.555556 0 412.444444 56.888889 412.444444 128S739.555556 654.222222 512 654.222222 99.555556 597.333333 99.555556 526.222222 284.444444 398.222222 512 398.222222m0-56.888889c-174.364444 0-469.333333 38.968889-469.333333 184.888889S337.635556 711.111111 512 711.111111s469.333333-38.968889 469.333333-184.888889S686.364444 341.333333 512 341.333333z" fill="" p-id="8971" data-spm-anchor-id="a313x.search_index.0.i6.209b3a81CSm3N4" class=""></path><path d="M312.888889 174.364444c69.973333 0 199.111111 113.777778 307.484444 281.6C744.106667 646.257778 796.444444 832 739.555556 870.968889a45.795556 45.795556 0 0 1-25.884445 7.111111c-69.973333 0-199.111111-113.777778-307.484444-281.6C279.893333 406.186667 227.555556 220.444444 284.444444 181.475556a45.795556 45.795556 0 0 1 28.444445-7.111112m0-56.888888a102.684444 102.684444 0 0 0-56.888889 16.497777c-36.124444 23.893333-71.395556 80.213333-32.711111 213.902223a1077.475556 1077.475556 0 0 0 135.964444 279.893333c93.013333 142.222222 241.777778 307.2 354.986667 307.2a102.684444 102.684444 0 0 0 56.888889-16.497778c36.124444-23.893333 71.395556-80.213333 32.711111-213.902222a1077.475556 1077.475556 0 0 0-135.964444-279.893333C573.44 284.444444 424.675556 117.475556 312.888889 117.475556z" fill="#7dc5eb" p-id="8972" data-spm-anchor-id="a313x.search_index.0.i3.209b3a81CSm3N4" class=""></path><path d="M695.182222 163.271111a47.786667 47.786667 0 0 1 23.04 5.688889c61.155556 35.555556 18.488889 223.857778-95.288889 421.262222-101.546667 175.502222-224.711111 298.951111-294.115555 298.951111a47.786667 47.786667 0 0 1-23.04-5.688889c-61.155556-35.555556-18.488889-223.857778 95.288889-421.262222C502.613333 286.72 625.777778 163.271111 695.182222 163.271111m0-56.888889c-113.777778 0-256 176.355556-343.324444 327.395556s-200.817778 426.666667-74.524445 498.915555a102.115556 102.115556 0 0 0 51.484445 13.368889c113.777778 0 256-176.355556 343.324444-327.395555s200.817778-426.666667 74.524445-498.915556a102.115556 102.115556 0 0 0-51.484445-13.368889z" fill="#e89abe" p-id="8973" data-spm-anchor-id="a313x.search_index.0.i1.209b3a81CSm3N4" class=""></path><path d="M170.666667 625.777778m-56.888889 0a56.888889 56.888889 0 1 0 113.777778 0 56.888889 56.888889 0 1 0-113.777778 0Z" fill="" p-id="8974"></path><path d="M597.333333 170.666667m-56.888889 0a56.888889 56.888889 0 1 0 113.777778 0 56.888889 56.888889 0 1 0-113.777778 0Z" fill="" p-id="8975"></path><path d="M625.777778 881.777778m-56.888889 0a56.888889 56.888889 0 1 0 113.777778 0 56.888889 56.888889 0 1 0-113.777778 0Z" fill="" p-id="8976"></path></svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |