增加语言选择

This commit is contained in:
zzh 2025-11-27 16:16:34 +08:00
parent ae9723964e
commit 85af2fbc2c
19 changed files with 2234 additions and 44 deletions

View File

@ -28,11 +28,15 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.runtime.collectAsState
import com.example.smarthome.data.BackgroundManager
import com.example.smarthome.data.LanguageManager
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 初始化语言管理器
LanguageManager.init(this)
// 隐藏状态栏和导航栏
hideStatusBar()

View File

@ -0,0 +1,88 @@
package com.example.smarthome.data
import android.content.Context
import android.content.SharedPreferences
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import com.example.smarthome.R
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
/**
* 背景壁纸管理器
*/
object BackgroundManager {
private const val PREFS_NAME = "background_prefs"
private const val KEY_BACKGROUND = "selected_background"
private val _selectedBackground = MutableStateFlow(0)
val selectedBackground: StateFlow<Int> = _selectedBackground.asStateFlow()
// 可用的背景列表
val backgrounds = listOf(
BackgroundOption(
id = 0,
name = "丝绸白",
resourceId = R.drawable.background1,
thumbId = R.drawable.background1_thumb,
description = "优雅的白色丝绸质感",
isDark = false
),
BackgroundOption(
id = 1,
name = "星空海",
resourceId = R.drawable.background2,
thumbId = R.drawable.background2_thumb,
description = "深邃的星空与荧光海浪",
isDark = true
),
BackgroundOption(
id = 2,
name = "紫金绸",
resourceId = R.drawable.background3,
thumbId = R.drawable.background3_thumb,
description = "紫色金丝流动质感",
isDark = true
)
)
fun init(context: Context) {
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
_selectedBackground.value = prefs.getInt(KEY_BACKGROUND, 0)
}
fun setBackground(context: Context, backgroundId: Int) {
_selectedBackground.value = backgroundId
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
.edit()
.putInt(KEY_BACKGROUND, backgroundId)
.apply()
}
fun getCurrentBackgroundResource(): Int {
val id = _selectedBackground.value
return backgrounds.getOrNull(id)?.resourceId ?: R.drawable.background1
}
}
data class BackgroundOption(
val id: Int,
val name: String,
val resourceId: Int,
val thumbId: Int,
val description: String,
val isDark: Boolean = false
)
// Compose 便捷方法
@Composable
fun rememberSelectedBackground(): Int {
val context = LocalContext.current
remember { BackgroundManager.init(context) }
val selected by BackgroundManager.selectedBackground.collectAsState()
return BackgroundManager.backgrounds.getOrNull(selected)?.resourceId ?: R.drawable.background1
}

View File

@ -0,0 +1,476 @@
package com.example.smarthome.data
import android.content.Context
import android.util.Log
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import java.util.LinkedList
import java.util.Queue
/**
* CAN 总线服务
*
* 依赖库需要在 build.gradle 中添加:
* implementation("com.vi:vioserial:x.x.x") // 串口通信库
*
* CAN 帧格式与原代码一致:
* AA 01 00 08 00 00 XX XX [8字节数据] 7A
* - AA: 帧头
* - 01 00: 固定
* - 08: 数据长度8字节
* - 00 00 XX XX: CAN ID格口地址
* - [8字节]: 命令数据ASCII HEX
* - 7A: 帧尾
*/
// ==================== 常量定义 ====================
object CanConstants {
const val TAG = "CanService"
// 默认串口配置
const val DEFAULT_COM = "/dev/ttyS4" // 方派10寸PAD
// const val DEFAULT_COM = "/dev/ttysWK1" // 萤火虫10寸PAD
// const val DEFAULT_COM = "/dev/ttyS3" // 梵骏10寸PAD
const val BAUD_RATE = 115200
// CAN 帧格式
const val FRAME_HEAD = "AA0100"
const val FRAME_DATA_LEN = "08"
const val FRAME_TAIL = "7A"
// 门锁状态(根据锁类型可能相反)
const val DOOR_OPEN_STATUS = "1" // 新锁1=开
const val DOOR_CLOSE_STATUS = "0" // 新锁0=关
}
// ==================== CAN 命令定义 ====================
/**
* CAN 命令枚举与原代码一致
*/
object CanCommands {
// 门锁控制
const val LON = "LON" // 开锁
const val LOF = "LOF" // 关锁
const val LST = "LST" // 查询锁状态
// 电源控制
const val PON = "PON" // 上电
const val POF = "POF" // 断电
// LED 控制
const val RON = "RON" // 红灯亮
const val GON = "GON" // 绿灯亮
const val AOF = "AOF" // 灯灭
// 设备信息读取
const val PVD = "PVD" // 读取 PID/VID
const val MFG = "MFG" // 读取制造商
const val PRO = "PRO" // 读取产品名
const val DSN = "DSN" // 读取设备序列号
const val CON = "CON" // 读取连接状态
// 固件相关
const val VER = "1VER" // 读取固件版本
const val LEN = "1LEN" // 固件长度
const val MD5 = "1MD5" // 固件MD5
const val MDE = "1MDE" // 固件MD5结束
}
// ==================== 数据模型 ====================
/**
* CAN 消息
*/
data class CanMessage(
val cellName: String, // 格口名称,如 "A01"
val command: String, // 命令,如 "LON"
val data: String = "", // 附加数据
val rawFrame: String = "" // 原始帧数据
)
/**
* CAN 响应
*/
data class CanResponse(
val cellName: String,
val command: String,
val value: String,
val rawData: String
)
// ==================== CAN 服务接口 ====================
interface ICanService {
val isConnected: StateFlow<Boolean>
val lastResponse: StateFlow<CanResponse?>
// 串口管理
fun openCom(comPath: String = CanConstants.DEFAULT_COM, baudRate: Int = CanConstants.BAUD_RATE): Boolean
fun closeCom()
// 发送命令
fun sendCmd(cellName: String, cmd: String)
fun sendData(addr: String, cmd: String, data: String)
// 门锁控制
fun openDoor(cellName: String)
fun closeDoor(cellName: String)
fun queryDoorStatus(cellName: String)
// 电源控制
fun powerOn(cellName: String)
fun powerOff(cellName: String)
// LED 控制
fun ledRed(cellName: String)
fun ledGreen(cellName: String)
fun ledOff(cellName: String)
// 设备信息
fun readDeviceSn(cellName: String)
fun readDevicePvd(cellName: String)
fun readDeviceMfg(cellName: String)
fun readDevicePro(cellName: String)
fun readConnectStatus(cellName: String)
// 批量操作
fun openDoors(cellNames: List<String>)
fun powerOnAll(cellNames: List<String>)
fun powerOffAll(cellNames: List<String>)
fun ledGreenAll(cellNames: List<String>)
fun ledOffAll(cellNames: List<String>)
// 回调
fun setResponseCallback(callback: (CanResponse) -> Unit)
}
// ==================== CAN 服务实现(模拟)====================
class CanServiceImpl private constructor(
private val context: Context
) : ICanService {
companion object {
@Volatile
private var instance: CanServiceImpl? = null
fun getInstance(context: Context): CanServiceImpl {
return instance ?: synchronized(this) {
instance ?: CanServiceImpl(context.applicationContext).also { instance = it }
}
}
}
// 状态
private val _isConnected = MutableStateFlow(false)
override val isConnected: StateFlow<Boolean> = _isConnected.asStateFlow()
private val _lastResponse = MutableStateFlow<CanResponse?>(null)
override val lastResponse: StateFlow<CanResponse?> = _lastResponse.asStateFlow()
// 发送队列
private val sendQueue: Queue<String> = LinkedList()
// 回调
private var responseCallback: ((CanResponse) -> Unit)? = null
// TODO: 真实串口对象
// private var comSerial: COMSerial? = null
// ==================== 串口管理 ====================
override fun openCom(comPath: String, baudRate: Int): Boolean {
Log.d(CanConstants.TAG, "openCom: $comPath, baudRate: $baudRate")
// TODO: 打开真实串口
// comSerial = COMSerial.instance()
// val result = comSerial?.open(comPath, baudRate) ?: false
// if (result) {
// comSerial?.setOnDataListener(onComDataListener)
// _isConnected.value = true
// startSendThread()
// startReceiveThread()
// }
// return result
// 模拟连接成功
_isConnected.value = true
return true
}
override fun closeCom() {
Log.d(CanConstants.TAG, "closeCom")
_isConnected.value = false
// TODO: 关闭真实串口
// comSerial?.close()
// comSerial = null
}
// ==================== 发送命令 ====================
override fun sendCmd(cellName: String, cmd: String) {
val addr = convertCellNameToCanId(cellName)
val convertCmd = stringToHexAscii(cmd.padEnd(7, ':').take(7))
val canPackage = buildCanFrame(addr, convertCmd)
sendPortData(canPackage, "$cellName$cmd")
}
override fun sendData(addr: String, cmd: String, data: String) {
val convertCmd = when {
cmd == CanCommands.VER -> stringToHexAscii(cmd.padEnd(8, ':'))
cmd in listOf(CanCommands.LEN, CanCommands.MD5, CanCommands.MDE) ->
stringToHexAscii(cmd) + data
cmd.isEmpty() -> data.padEnd(16, ':'.code.toByte().toInt().toChar())
else -> stringToHexAscii(cmd.padEnd(8, ':'))
}
val canPackage = buildCanFrame("0000$addr", convertCmd)
sendPortData(canPackage, "$addr$cmd")
}
// ==================== 门锁控制 ====================
override fun openDoor(cellName: String) {
sendCmd(cellName, CanCommands.LON)
}
override fun closeDoor(cellName: String) {
sendCmd(cellName, CanCommands.LOF)
}
override fun queryDoorStatus(cellName: String) {
sendCmd(cellName, CanCommands.LST)
}
// ==================== 电源控制 ====================
override fun powerOn(cellName: String) {
sendCmd(cellName, CanCommands.PON)
}
override fun powerOff(cellName: String) {
sendCmd(cellName, CanCommands.POF)
}
// ==================== LED 控制 ====================
override fun ledRed(cellName: String) {
sendCmd(cellName, CanCommands.RON)
}
override fun ledGreen(cellName: String) {
sendCmd(cellName, CanCommands.GON)
}
override fun ledOff(cellName: String) {
sendCmd(cellName, CanCommands.AOF)
}
// ==================== 设备信息 ====================
override fun readDeviceSn(cellName: String) {
sendCmd(cellName, CanCommands.DSN)
}
override fun readDevicePvd(cellName: String) {
sendCmd(cellName, CanCommands.PVD)
}
override fun readDeviceMfg(cellName: String) {
sendCmd(cellName, CanCommands.MFG)
}
override fun readDevicePro(cellName: String) {
sendCmd(cellName, CanCommands.PRO)
}
override fun readConnectStatus(cellName: String) {
sendCmd(cellName, CanCommands.CON)
}
// ==================== 批量操作 ====================
override fun openDoors(cellNames: List<String>) {
cellNames.forEach { openDoor(it) }
}
override fun powerOnAll(cellNames: List<String>) {
cellNames.forEach { powerOn(it) }
}
override fun powerOffAll(cellNames: List<String>) {
cellNames.forEach { powerOff(it) }
}
override fun ledGreenAll(cellNames: List<String>) {
cellNames.forEach { ledGreen(it) }
}
override fun ledOffAll(cellNames: List<String>) {
cellNames.forEach { ledOff(it) }
}
// ==================== 回调 ====================
override fun setResponseCallback(callback: (CanResponse) -> Unit) {
responseCallback = callback
}
// ==================== 内部方法 ====================
private fun sendPortData(frame: String, info: String) {
if (!_isConnected.value) {
Log.w(CanConstants.TAG, "sendPortData: 串口未打开")
return
}
Log.d(CanConstants.TAG, "sendPortData: $info -> $frame")
sendQueue.add("$frame$info")
// TODO: 真实发送
// comSerial?.send(hexStringToBytes(frame))
}
/**
* 构建 CAN
*/
private fun buildCanFrame(canId: String, data: String): String {
return "${CanConstants.FRAME_HEAD}${CanConstants.FRAME_DATA_LEN}$canId$data${CanConstants.FRAME_TAIL}"
}
/**
* 格口名称转 CAN ID
* 例如: "A01" -> "0101", "B02" -> "0202"
*/
private fun convertCellNameToCanId(cellName: String): String {
if (cellName.length < 3) return "0000"
val cabinet = cellName[0] // A, B, C...
val cell = cellName.substring(1).toIntOrNull() ?: 0
val cabinetId = (cabinet.uppercaseChar() - 'A' + 1).toString().padStart(2, '0')
val cellId = cell.toString().padStart(2, '0')
return "$cabinetId$cellId"
}
/**
* CAN ID 转格口名称
*/
private fun convertCanIdToCellName(canId: String): String {
if (canId.length < 4) return ""
val cabinetNum = canId.substring(0, 2).toIntOrNull() ?: 0
val cellNum = canId.substring(2, 4).toIntOrNull() ?: 0
val cabinet = ('A' + cabinetNum - 1)
return "$cabinet${cellNum.toString().padStart(2, '0')}"
}
/**
* 字符串转 HEX ASCII
*/
private fun stringToHexAscii(str: String): String {
return str.map { Integer.toHexString(it.code).padStart(2, '0') }.joinToString("")
}
/**
* HEX 字符串转字节数组
*/
private fun hexStringToBytes(hex: String): ByteArray {
val len = hex.length / 2
val result = ByteArray(len)
for (i in 0 until len) {
result[i] = hex.substring(i * 2, i * 2 + 2).toInt(16).toByte()
}
return result
}
/**
* 处理接收到的数据由串口回调调用
*/
internal fun handleReceivedData(data: String) {
Log.d(CanConstants.TAG, "handleReceivedData: $data")
// 解析 CAN 帧
// 格式: AA0100 08 CANID DATA 7A
if (data.length < 26 || !data.startsWith("AA") || !data.endsWith("7A")) {
return
}
try {
val canId = data.substring(10, 14)
val payload = data.substring(14, 30)
val cellName = convertCanIdToCellName(canId)
val (cmd, value) = parsePayload(payload)
val response = CanResponse(
cellName = cellName,
command = cmd,
value = value,
rawData = data
)
_lastResponse.value = response
responseCallback?.invoke(response)
} catch (e: Exception) {
Log.e(CanConstants.TAG, "parseReceivedData error", e)
}
}
/**
* 解析数据负载
*/
private fun parsePayload(payload: String): Pair<String, String> {
// 将 HEX 转回 ASCII
val ascii = StringBuilder()
for (i in payload.indices step 2) {
val byte = payload.substring(i, i + 2).toInt(16)
if (byte != 0x3A) { // 不是冒号
ascii.append(byte.toChar())
}
}
val str = ascii.toString()
val cmd = str.take(3)
val value = str.drop(3).trim()
return cmd to value
}
}
// ==================== 便捷扩展 ====================
fun Context.getCanService(): ICanService = CanServiceImpl.getInstance(this)
// ==================== 操作类型常量(与原代码一致)====================
object OperationType {
const val OPT_BORROW = 2 // 借
const val OPT_RETURN = 3 // 还
const val OPT_REPAIRDEVIN = 4 // 报修归还
const val OPT_REPAIRDEVOUT = 5 // 维修设备出柜
const val OPT_BATCHOUT = 6 // 批量出柜
const val OPT_BATCHIN = 7 // 批量新增入柜
const val OPT_RESETSELECTECELLS = 8 // 恢复异常格口状态
const val OPT_REPAIRSELECTECELLS = 9 // 格口报修
const val OPT_PONSELECTECELLS = 10 // 指定格口上电
const val OPT_POFSELECTECELLS = 11 // 指定格口断电
const val OPT_RONSELECTECELLS = 12 // 指定格口红灯亮
const val OPT_GONSELECTECELLS = 13 // 指定格口绿灯亮
const val OPT_AOFSELECTECELLS = 14 // 指定格口灯灭
const val OPT_DEVINFSELECTECELLS = 15 // 指定格口设备信息
const val OPT_FIRMWAREVERSION = 16 // 主板固件版本
const val OPT_OPENDOORS = 17 // 打开柜门
const val OPT_REPAIRCELLOPEN = 18 // 报修格口开门
const val OPT_LOSTCELLRON = 19 // 丢失格口红灯
const val OPT_DOUBLECHECK = 20 // 检测打开门的柜子格口数据是否异常
}

View File

@ -0,0 +1,406 @@
package com.example.smarthome.data
import android.content.Context
import android.util.Log
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
/**
* 设备管理器 - 整合 MQTT CAN 服务
* 提供给 UI 层使用的统一接口
*
* 使用示例:
* ```kotlin
* @Composable
* fun DeviceControlScreen() {
* val deviceManager = rememberDeviceManager()
*
* // 初始化
* LaunchedEffect(Unit) {
* deviceManager.initialize()
* }
*
* // 控制设备
* Button(onClick = { deviceManager.openDoor("A01") }) {
* Text("开门")
* }
* }
* ```
*/
class DeviceManager private constructor(
private val context: Context
) {
companion object {
private const val TAG = "DeviceManager"
@Volatile
private var instance: DeviceManager? = null
fun getInstance(context: Context): DeviceManager {
return instance ?: synchronized(this) {
instance ?: DeviceManager(context.applicationContext).also { instance = it }
}
}
}
// 服务
private val mqttService: IMqttService = MqttServiceImpl.getInstance(context)
private val canService: ICanService = CanServiceImpl.getInstance(context)
// 协程作用域
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
// 状态
private val _isInitialized = MutableStateFlow(false)
val isInitialized: StateFlow<Boolean> = _isInitialized.asStateFlow()
private val _deviceSn = MutableStateFlow("")
val deviceSn: StateFlow<String> = _deviceSn.asStateFlow()
// 暴露服务状态
val mqttConnectionState = mqttService.connectionState
val canConnectionState = canService.isConnected
val lastCanResponse = canService.lastResponse
// 当前操作
private val _currentOperation = MutableStateFlow(-1)
val currentOperation: StateFlow<Int> = _currentOperation.asStateFlow()
// 格口列表
private val _openCellList = MutableStateFlow<List<String>>(emptyList())
val openCellList: StateFlow<List<String>> = _openCellList.asStateFlow()
// ==================== 初始化 ====================
/**
* 初始化设备管理器
*/
fun initialize(
deviceSn: String = "",
mqttHost: String = "",
mqttPassword: String = "",
comPath: String = CanConstants.DEFAULT_COM
) {
Log.d(TAG, "initialize")
_deviceSn.value = deviceSn
// 初始化 CAN 串口
scope.launch {
val canResult = canService.openCom(comPath)
Log.d(TAG, "CAN openCom result: $canResult")
}
// 初始化 MQTT如果配置了
if (mqttHost.isNotEmpty() && deviceSn.isNotEmpty()) {
scope.launch {
mqttService.mqttInit(mqttHost, deviceSn, mqttPassword)
mqttService.mqttConnect()
}
}
// 设置回调
setupCallbacks()
_isInitialized.value = true
}
/**
* 关闭设备管理器
*/
fun shutdown() {
Log.d(TAG, "shutdown")
canService.closeCom()
mqttService.mqttClose()
_isInitialized.value = false
}
private fun setupCallbacks() {
// CAN 响应回调
canService.setResponseCallback { response ->
Log.d(TAG, "CAN Response: ${response.cellName} ${response.command} = ${response.value}")
handleCanResponse(response)
}
// MQTT 消息回调
mqttService.setMessageCallback { topic, message ->
Log.d(TAG, "MQTT Message: $topic -> $message")
handleMqttMessage(topic, message)
}
}
// ==================== 门锁控制 ====================
/**
* 开门
*/
fun openDoor(cellName: String) {
Log.d(TAG, "openDoor: $cellName")
canService.ledGreen(cellName) // 先亮绿灯
canService.openDoor(cellName) // 再开锁
}
/**
* 批量开门
*/
fun openDoors(cellNames: List<String>) {
Log.d(TAG, "openDoors: $cellNames")
_openCellList.value = cellNames
cellNames.forEach { cellName ->
canService.ledGreen(cellName)
}
cellNames.forEach { cellName ->
canService.openDoor(cellName)
}
}
/**
* 查询门状态
*/
fun queryDoorStatus(cellName: String) {
canService.queryDoorStatus(cellName)
}
// ==================== 电源控制 ====================
/**
* 格口上电
*/
fun powerOn(cellName: String) {
canService.powerOn(cellName)
}
/**
* 格口断电
*/
fun powerOff(cellName: String) {
canService.powerOff(cellName)
}
/**
* 批量上电
*/
fun powerOnAll(cellNames: List<String>) {
canService.powerOnAll(cellNames)
}
/**
* 批量断电
*/
fun powerOffAll(cellNames: List<String>) {
canService.powerOffAll(cellNames)
}
// ==================== LED 控制 ====================
fun ledRed(cellName: String) = canService.ledRed(cellName)
fun ledGreen(cellName: String) = canService.ledGreen(cellName)
fun ledOff(cellName: String) = canService.ledOff(cellName)
fun ledOffAll(cellNames: List<String>) = canService.ledOffAll(cellNames)
// ==================== 设备信息 ====================
/**
* 读取设备序列号
*/
fun readDeviceSn(cellName: String) {
canService.readDeviceSn(cellName)
}
/**
* 读取设备连接状态
*/
fun readConnectStatus(cellName: String) {
canService.readConnectStatus(cellName)
}
// ==================== 业务操作 ====================
/**
* 借设备
*/
fun borrowDevice(cellName: String, operator: String, userId: String) {
Log.d(TAG, "borrowDevice: $cellName by $operator")
_currentOperation.value = OperationType.OPT_BORROW
// 亮绿灯
canService.ledGreen(cellName)
// 开门
canService.openDoor(cellName)
// 上报 MQTT如果连接
if (mqttService.isMqttConnect) {
val event = DeviceEvent(
opType = "borrow",
deviceCode = _deviceSn.value,
cabinetName = cellName.take(1),
devSn = "",
doorName = cellName,
devType = "",
userId = userId,
batchId = generateBatchId(),
opTime = getCurrentTime(),
doorInfo = DoorInfo(cellName = cellName)
)
mqttService.publishBorrowdevEvent(event)
}
}
/**
* 还设备
*/
fun returnDevice(cellName: String, operator: String, userId: String) {
Log.d(TAG, "returnDevice: $cellName by $operator")
_currentOperation.value = OperationType.OPT_RETURN
// 亮绿灯
canService.ledGreen(cellName)
// 开门
canService.openDoor(cellName)
}
/**
* 报修归还
*/
fun repairReturn(cellName: String, operator: String) {
Log.d(TAG, "repairReturn: $cellName")
_currentOperation.value = OperationType.OPT_REPAIRDEVIN
// 亮红灯
canService.ledRed(cellName)
// 开门
canService.openDoor(cellName)
}
/**
* 结束当前操作
*/
fun endOperation() {
Log.d(TAG, "endOperation")
_currentOperation.value = -1
// 关闭所有灯
_openCellList.value.forEach { cellName ->
canService.ledOff(cellName)
}
_openCellList.value = emptyList()
}
// ==================== 回调处理 ====================
private fun handleCanResponse(response: CanResponse) {
when (response.command) {
CanCommands.LON -> handleDoorOpenResponse(response)
CanCommands.LST -> handleDoorStatusResponse(response)
CanCommands.DSN -> handleDeviceSnResponse(response)
CanCommands.CON -> handleConnectStatusResponse(response)
CanCommands.PVD -> handlePvdResponse(response)
CanCommands.MFG -> handleMfgResponse(response)
CanCommands.PRO -> handleProResponse(response)
}
}
private fun handleDoorOpenResponse(response: CanResponse) {
if (response.value == CanConstants.DOOR_OPEN_STATUS) {
Log.d(TAG, "${response.cellName} 开门成功")
// 开始监测门状态
canService.queryDoorStatus(response.cellName)
} else {
Log.d(TAG, "${response.cellName} 开门失败")
}
}
private fun handleDoorStatusResponse(response: CanResponse) {
if (response.value == CanConstants.DOOR_CLOSE_STATUS) {
Log.d(TAG, "${response.cellName} 门已关闭")
// 门关闭后的处理
canService.ledOff(response.cellName)
// 如果是还设备操作读取设备SN
if (_currentOperation.value == OperationType.OPT_RETURN ||
_currentOperation.value == OperationType.OPT_BATCHIN) {
canService.readDeviceSn(response.cellName)
}
} else {
// 门还开着,继续监测
canService.queryDoorStatus(response.cellName)
}
}
private fun handleDeviceSnResponse(response: CanResponse) {
Log.d(TAG, "${response.cellName} 设备SN: ${response.value}")
// 上报设备信息到 MQTT
if (mqttService.isMqttConnect) {
val doorInfo = DoorInfo(
cellName = response.cellName,
devSn = response.value
)
mqttService.publishDb(doorInfo)
}
}
private fun handleConnectStatusResponse(response: CanResponse) {
val connected = response.value == "1"
Log.d(TAG, "${response.cellName} 设备连接状态: $connected")
}
private fun handlePvdResponse(response: CanResponse) {
Log.d(TAG, "${response.cellName} PVD: ${response.value}")
}
private fun handleMfgResponse(response: CanResponse) {
Log.d(TAG, "${response.cellName} MFG: ${response.value}")
}
private fun handleProResponse(response: CanResponse) {
Log.d(TAG, "${response.cellName} PRO: ${response.value}")
}
private fun handleMqttMessage(topic: String, message: String) {
// 处理 MQTT 下发的命令
// 根据 topic 和 message 执行相应操作
}
// ==================== 工具方法 ====================
private fun generateBatchId(): String {
return (1..15).map { (0..9).random() }.joinToString("")
}
private fun getCurrentTime(): String {
val sdf = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault())
return sdf.format(java.util.Date())
}
}
// ==================== Compose 集成 ====================
@Composable
fun rememberDeviceManager(): DeviceManager {
val context = LocalContext.current
return remember { DeviceManager.getInstance(context) }
}
@Composable
fun rememberMqttConnectionState(): MqttConnectionState {
val manager = rememberDeviceManager()
val state by manager.mqttConnectionState.collectAsState()
return state
}
@Composable
fun rememberCanConnectionState(): Boolean {
val manager = rememberDeviceManager()
val state by manager.canConnectionState.collectAsState()
return state
}

View File

@ -0,0 +1,437 @@
package com.example.smarthome.data
import android.app.Activity
import android.content.Context
import android.content.Intent
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
/**
* 语言管理器 - 管理应用多语言
*/
object LanguageManager {
// 支持的语言
enum class Language(val code: String, val displayName: String, val nativeName: String) {
CHINESE("zh", "中文", "简体中文"),
ENGLISH("en", "English", "English"),
KOREAN("ko", "한국어", "한국어"),
JAPANESE("ja", "日本語", "日本語"),
RUSSIAN("ru", "Русский", "Русский")
}
private val _currentLanguage = MutableStateFlow(Language.CHINESE)
val currentLanguage: StateFlow<Language> = _currentLanguage
private const val PREFS_NAME = "language_prefs"
private const val KEY_LANGUAGE = "selected_language"
fun init(context: Context) {
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
val savedCode = prefs.getString(KEY_LANGUAGE, Language.CHINESE.code) ?: Language.CHINESE.code
_currentLanguage.value = Language.entries.find { it.code == savedCode } ?: Language.CHINESE
}
fun setLanguage(context: Context, language: Language) {
val currentLang = _currentLanguage.value
if (currentLang == language) return // 语言没变,不需要重启
// 先同步保存语言设置使用commit确保保存完成后再重启
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
.edit()
.putString(KEY_LANGUAGE, language.code)
.commit() // 使用commit同步保存而不是apply异步保存
_currentLanguage.value = language
// 重启应用
restartApp(context)
}
private fun restartApp(context: Context) {
val packageManager = context.packageManager
val intent = packageManager.getLaunchIntentForPackage(context.packageName)
intent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
context.startActivity(intent)
if (context is Activity) {
context.finish()
}
Runtime.getRuntime().exit(0)
}
// 获取翻译文本
fun getString(key: String): String {
return translations[_currentLanguage.value]?.get(key) ?: translations[Language.CHINESE]?.get(key) ?: key
}
// 翻译字典
private val translations: Map<Language, Map<String, String>> = mapOf(
Language.CHINESE to mapOf(
// 导航
"nav_dashboard" to "控制台",
"nav_scene" to "场景",
"nav_automation" to "自动化",
"nav_statistics" to "统计",
"nav_security" to "安全",
"nav_settings" to "设置",
// 设置页面
"settings_title" to "设置",
"settings_account" to "账户设置",
"settings_profile" to "个人信息",
"settings_profile_desc" to "管理您的个人资料",
"settings_security" to "账户安全",
"settings_security_desc" to "密码和安全设置",
"settings_device" to "设备管理",
"settings_auto_connect" to "自动连接",
"settings_auto_connect_desc" to "自动连接到家庭网络",
"settings_notifications" to "设备通知",
"settings_notifications_desc" to "接收设备状态通知",
"settings_display" to "显示设置",
"settings_background" to "背景壁纸",
"settings_dark_mode" to "深色模式",
"settings_dark_mode_desc" to "使用深色主题",
"settings_language" to "语言",
"settings_about" to "关于",
"settings_version" to "版本信息",
"settings_privacy" to "隐私政策",
"settings_privacy_desc" to "查看隐私政策",
"settings_terms" to "用户协议",
"settings_terms_desc" to "查看用户协议",
"settings_logout" to "退出登录",
// 控制台
"dashboard_title" to "控制台",
"room_all" to "全屋",
"room_living" to "客厅",
"room_bedroom" to "卧室",
"room_kitchen" to "厨房",
"room_bathroom" to "浴室",
// 空调
"ac_title" to "空调",
"ac_mode_cool" to "制冷",
"ac_mode_heat" to "制热",
"ac_mode_dry" to "除湿",
"ac_mode_fan" to "送风",
"ac_fan_speed" to "风速",
"ac_fan_auto" to "自动",
"ac_fan_low" to "",
"ac_fan_medium" to "",
"ac_fan_high" to "",
// 灯光
"light_title" to "灯光",
"light_brightness" to "亮度",
// 窗帘
"curtain_title" to "窗帘",
// 天气
"weather_humidity" to "湿度",
// 通用
"add_device" to "+ 添加设备",
"on" to "",
"off" to "",
"cancel" to "取消",
"confirm" to "确认",
"select_language" to "选择语言"
),
Language.ENGLISH to mapOf(
// Navigation
"nav_dashboard" to "Dashboard",
"nav_scene" to "Scenes",
"nav_automation" to "Automation",
"nav_statistics" to "Statistics",
"nav_security" to "Security",
"nav_settings" to "Settings",
// Settings
"settings_title" to "Settings",
"settings_account" to "Account Settings",
"settings_profile" to "Profile",
"settings_profile_desc" to "Manage your profile",
"settings_security" to "Account Security",
"settings_security_desc" to "Password and security settings",
"settings_device" to "Device Management",
"settings_auto_connect" to "Auto Connect",
"settings_auto_connect_desc" to "Auto connect to home network",
"settings_notifications" to "Device Notifications",
"settings_notifications_desc" to "Receive device status notifications",
"settings_display" to "Display Settings",
"settings_background" to "Background",
"settings_dark_mode" to "Dark Mode",
"settings_dark_mode_desc" to "Use dark theme",
"settings_language" to "Language",
"settings_about" to "About",
"settings_version" to "Version",
"settings_privacy" to "Privacy Policy",
"settings_privacy_desc" to "View privacy policy",
"settings_terms" to "Terms of Service",
"settings_terms_desc" to "View terms of service",
"settings_logout" to "Log Out",
// Dashboard
"dashboard_title" to "Dashboard",
"room_all" to "All Rooms",
"room_living" to "Living Room",
"room_bedroom" to "Bedroom",
"room_kitchen" to "Kitchen",
"room_bathroom" to "Bathroom",
// AC
"ac_title" to "Air Conditioner",
"ac_mode_cool" to "Cool",
"ac_mode_heat" to "Heat",
"ac_mode_dry" to "Dry",
"ac_mode_fan" to "Fan",
"ac_fan_speed" to "Fan Speed",
"ac_fan_auto" to "Auto",
"ac_fan_low" to "Low",
"ac_fan_medium" to "Med",
"ac_fan_high" to "High",
// Light
"light_title" to "Light",
"light_brightness" to "Brightness",
// Curtain
"curtain_title" to "Curtain",
// Weather
"weather_humidity" to "Humidity",
// Common
"add_device" to "+ Add Device",
"on" to "On",
"off" to "Off",
"cancel" to "Cancel",
"confirm" to "Confirm",
"select_language" to "Select Language"
),
Language.KOREAN to mapOf(
// 네비게이션
"nav_dashboard" to "대시보드",
"nav_scene" to "장면",
"nav_automation" to "자동화",
"nav_statistics" to "통계",
"nav_security" to "보안",
"nav_settings" to "설정",
// 설정
"settings_title" to "설정",
"settings_account" to "계정 설정",
"settings_profile" to "프로필",
"settings_profile_desc" to "프로필 관리",
"settings_security" to "계정 보안",
"settings_security_desc" to "비밀번호 및 보안 설정",
"settings_device" to "기기 관리",
"settings_auto_connect" to "자동 연결",
"settings_auto_connect_desc" to "홈 네트워크에 자동 연결",
"settings_notifications" to "기기 알림",
"settings_notifications_desc" to "기기 상태 알림 받기",
"settings_display" to "디스플레이 설정",
"settings_background" to "배경화면",
"settings_dark_mode" to "다크 모드",
"settings_dark_mode_desc" to "다크 테마 사용",
"settings_language" to "언어",
"settings_about" to "정보",
"settings_version" to "버전 정보",
"settings_privacy" to "개인정보 처리방침",
"settings_privacy_desc" to "개인정보 처리방침 보기",
"settings_terms" to "이용약관",
"settings_terms_desc" to "이용약관 보기",
"settings_logout" to "로그아웃",
// 대시보드
"dashboard_title" to "대시보드",
"room_all" to "전체",
"room_living" to "거실",
"room_bedroom" to "침실",
"room_kitchen" to "주방",
"room_bathroom" to "욕실",
// 에어컨
"ac_title" to "에어컨",
"ac_mode_cool" to "냉방",
"ac_mode_heat" to "난방",
"ac_mode_dry" to "제습",
"ac_mode_fan" to "송풍",
"ac_fan_speed" to "풍속",
"ac_fan_auto" to "자동",
"ac_fan_low" to "",
"ac_fan_medium" to "",
"ac_fan_high" to "",
// 조명
"light_title" to "조명",
"light_brightness" to "밝기",
// 커튼
"curtain_title" to "커튼",
// 날씨
"weather_humidity" to "습도",
// 공통
"add_device" to "+ 기기 추가",
"on" to "켜짐",
"off" to "꺼짐",
"cancel" to "취소",
"confirm" to "확인",
"select_language" to "언어 선택"
),
Language.JAPANESE to mapOf(
// ナビゲーション
"nav_dashboard" to "ダッシュボード",
"nav_scene" to "シーン",
"nav_automation" to "オートメーション",
"nav_statistics" to "統計",
"nav_security" to "セキュリティ",
"nav_settings" to "設定",
// 設定
"settings_title" to "設定",
"settings_account" to "アカウント設定",
"settings_profile" to "プロフィール",
"settings_profile_desc" to "プロフィールを管理",
"settings_security" to "アカウントセキュリティ",
"settings_security_desc" to "パスワードとセキュリティ設定",
"settings_device" to "デバイス管理",
"settings_auto_connect" to "自動接続",
"settings_auto_connect_desc" to "ホームネットワークに自動接続",
"settings_notifications" to "デバイス通知",
"settings_notifications_desc" to "デバイス状態の通知を受信",
"settings_display" to "表示設定",
"settings_background" to "背景",
"settings_dark_mode" to "ダークモード",
"settings_dark_mode_desc" to "ダークテーマを使用",
"settings_language" to "言語",
"settings_about" to "情報",
"settings_version" to "バージョン情報",
"settings_privacy" to "プライバシーポリシー",
"settings_privacy_desc" to "プライバシーポリシーを見る",
"settings_terms" to "利用規約",
"settings_terms_desc" to "利用規約を見る",
"settings_logout" to "ログアウト",
// ダッシュボード
"dashboard_title" to "ダッシュボード",
"room_all" to "全室",
"room_living" to "リビング",
"room_bedroom" to "寝室",
"room_kitchen" to "キッチン",
"room_bathroom" to "バスルーム",
// エアコン
"ac_title" to "エアコン",
"ac_mode_cool" to "冷房",
"ac_mode_heat" to "暖房",
"ac_mode_dry" to "除湿",
"ac_mode_fan" to "送風",
"ac_fan_speed" to "風速",
"ac_fan_auto" to "自動",
"ac_fan_low" to "",
"ac_fan_medium" to "",
"ac_fan_high" to "",
// 照明
"light_title" to "照明",
"light_brightness" to "明るさ",
// カーテン
"curtain_title" to "カーテン",
// 天気
"weather_humidity" to "湿度",
// 共通
"add_device" to "+ デバイス追加",
"on" to "オン",
"off" to "オフ",
"cancel" to "キャンセル",
"confirm" to "確認",
"select_language" to "言語を選択"
),
Language.RUSSIAN to mapOf(
// Навигация
"nav_dashboard" to "Панель",
"nav_scene" to "Сцены",
"nav_automation" to "Автоматизация",
"nav_statistics" to "Статистика",
"nav_security" to "Безопасность",
"nav_settings" to "Настройки",
// Настройки
"settings_title" to "Настройки",
"settings_account" to "Настройки аккаунта",
"settings_profile" to "Профиль",
"settings_profile_desc" to "Управление профилем",
"settings_security" to "Безопасность аккаунта",
"settings_security_desc" to "Пароль и настройки безопасности",
"settings_device" to "Управление устройствами",
"settings_auto_connect" to "Автоподключение",
"settings_auto_connect_desc" to "Автоподключение к домашней сети",
"settings_notifications" to "Уведомления",
"settings_notifications_desc" to "Получать уведомления о состоянии",
"settings_display" to "Настройки дисплея",
"settings_background" to "Фон",
"settings_dark_mode" to "Тёмный режим",
"settings_dark_mode_desc" to "Использовать тёмную тему",
"settings_language" to "Язык",
"settings_about" to "О приложении",
"settings_version" to "Версия",
"settings_privacy" to "Политика конфиденциальности",
"settings_privacy_desc" to "Просмотреть политику",
"settings_terms" to "Условия использования",
"settings_terms_desc" to "Просмотреть условия",
"settings_logout" to "Выйти",
// Панель
"dashboard_title" to "Панель",
"room_all" to "Все комнаты",
"room_living" to "Гостиная",
"room_bedroom" to "Спальня",
"room_kitchen" to "Кухня",
"room_bathroom" to "Ванная",
// Кондиционер
"ac_title" to "Кондиционер",
"ac_mode_cool" to "Охлаждение",
"ac_mode_heat" to "Обогрев",
"ac_mode_dry" to "Осушение",
"ac_mode_fan" to "Вентиляция",
"ac_fan_speed" to "Скорость",
"ac_fan_auto" to "Авто",
"ac_fan_low" to "Низ",
"ac_fan_medium" to "Сред",
"ac_fan_high" to "Выс",
// Освещение
"light_title" to "Свет",
"light_brightness" to "Яркость",
// Шторы
"curtain_title" to "Шторы",
// Погода
"weather_humidity" to "Влажность",
// Общее
"add_device" to "+ Добавить",
"on" to "Вкл",
"off" to "Выкл",
"cancel" to "Отмена",
"confirm" to "ОК",
"select_language" to "Выбрать язык"
)
)
}
// 便捷函数
fun tr(key: String): String = LanguageManager.getString(key)

View File

@ -0,0 +1,486 @@
package com.example.smarthome.data
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.json.JSONObject
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
/**
* MQTT 服务
*
* 依赖库需要在 build.gradle 中添加:
* implementation("org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5")
* implementation("org.eclipse.paho:org.eclipse.paho.android.service:1.1.1")
*
* AndroidManifest.xml 需要添加:
* <service android:name="org.eclipse.paho.android.service.MqttService" />
*/
// ==================== 常量定义(与原代码一致)====================
object MqttConstants {
const val TAG = "MqttService"
// 默认服务器配置
const val HOST_DOMAIN = "mqtt.cd-iot.cn"
const val HOST_DOMAIN_API = "https://www.cd-iot.cn"
const val HOST_PORT = ":1884"
// 主题前缀
const val PUBLISH_HEAD = "bcup/0jnqd7slms9vvaf8/"
const val PUBLISH_TAIL_EVENT = "/event"
const val PUBLISH_TAIL_PROP = "/prop"
const val PUBLISH_TAIL_UPDATEREPLY = "/upgrade/reply"
const val PUBLISH_TAIL_MESSAGEREPLY = "/message/reply"
const val SUBSCRIBE_HEAD = "bcdown/0jnqd7slms9vvaf8/"
const val SUBSCRIBE_TAIL_UPDATE = "/upgrade/set"
const val SUBSCRIBE_TAIL_PROPERTYSET = "/property/set"
const val SUBSCRIBE_TAIL_SERVER = "/service/invoke"
// QoS 等级
const val QOS_DEFAULT = 2
}
// ==================== 数据模型 ====================
/**
* MQTT 连接状态
*/
enum class MqttConnectionState {
DISCONNECTED,
CONNECTING,
CONNECTED,
CONNECTION_LOST
}
/**
* 设备信息与原代码 Door 实体对应
*/
data class DoorInfo(
val cellName: String = "", // 格口名称
val cabinetName: String = "", // 柜子名称
val cellState: String = "", // 格口状态
val cellVoltageType: String = "", // 电压类型
val cellConnectorType: String = "", // 接口类型
val cellRepairType: String = "", // 格口维修类型
val devType: String = "", // 设备类型
val devCheckType: String = "", // 设备检测类型
val devSn: String = "", // 设备序列号
val devPvd: String = "", // 设备PID/VID
val devPro: String = "", // 设备产品名
val devMfg: String = "", // 设备制造商
val devPwr: String = "", // 设备电量
val devState: String = "", // 设备状态
val devRepairType: String = "" // 设备维修类型
)
/**
* 借还设备事件
*/
data class DeviceEvent(
val opType: String, // 操作类型
val deviceCode: String, // 设备编码柜子SN
val cabinetName: String, // 柜子名称
val devSn: String, // 设备序列号
val doorName: String, // 格口名称
val devType: String, // 设备类型
val userId: String, // 用户ID
val batchId: String, // 批次ID
val opTime: String, // 操作时间
val doorInfo: DoorInfo // 格口信息
)
// ==================== MQTT 服务接口 ====================
interface IMqttService {
val connectionState: StateFlow<MqttConnectionState>
val isMqttConnect: Boolean
// 初始化和连接
fun mqttInit(hostIp: String, deviceSn: String, devicePassword: String)
fun mqttConnect()
fun mqttClose()
// 订阅
fun subscribeUpdate()
fun subscribeLog()
fun subscribeUploadLog()
// 发布 - 设备状态同步
fun publishDb(doorInfo: DoorInfo)
fun publishBorrowdevEvent(event: DeviceEvent)
fun publishReturndevEvent(event: DeviceEvent)
fun publishLosedevEvent(event: DeviceEvent)
// 发布 - 系统消息
fun publishSoftwareVersion(version: String)
fun publishDownLoadReply(code: String, messageId: String, taskId: String)
fun publishLogupload(messageId: String, filename: String, fileUrl: String)
fun publishInitDeviceReply(messageId: String, opType: String)
fun publishCellSetReply(messageId: String)
// 回调设置
fun setMessageCallback(callback: (topic: String, message: String) -> Unit)
fun setConnectionCallback(callback: (MqttConnectionState) -> Unit)
}
// ==================== MQTT 服务实现(模拟)====================
class MqttServiceImpl private constructor(
private val context: Context
) : IMqttService {
companion object {
@Volatile
private var instance: MqttServiceImpl? = null
fun getInstance(context: Context): MqttServiceImpl {
return instance ?: synchronized(this) {
instance ?: MqttServiceImpl(context.applicationContext).also { instance = it }
}
}
}
// 状态
private val _connectionState = MutableStateFlow(MqttConnectionState.DISCONNECTED)
override val connectionState: StateFlow<MqttConnectionState> = _connectionState.asStateFlow()
override val isMqttConnect: Boolean get() = _connectionState.value == MqttConnectionState.CONNECTED
// 配置
private var hostIp: String = ""
private var deviceSn: String = ""
private var clientId: String = ""
private var username: String = ""
private var password: String = ""
// 主题
private var topicPublishDb: String = ""
private var topicPublishSoftwareVersion: String = ""
private var topicPublishUpdateReply: String = ""
private var topicPublishLogUpload: String = ""
private var topicPublishInitDeviceReply: String = ""
private var topicPublishBorrowDev: String = ""
private var topicPublishReturnDev: String = ""
private var topicPublishLoseDev: String = ""
private var topicSubscribeUpdate: String = ""
private var topicSubscribePropertySet: String = ""
private var topicSubscribeServer: String = ""
// 回调
private var messageCallback: ((String, String) -> Unit)? = null
private var connectionCallback: ((MqttConnectionState) -> Unit)? = null
// SharedPreferences
private val sharedPreferences: SharedPreferences by lazy {
context.getSharedPreferences("mqtt_prefs", Context.MODE_PRIVATE)
}
// TODO: 真实 MQTT 客户端
// private var mqttAndroidClient: MqttAndroidClient? = null
// private var mqttConnectOptions: MqttConnectOptions? = null
// ==================== 初始化 ====================
override fun mqttInit(hostIp: String, deviceSn: String, devicePassword: String) {
if (hostIp.isEmpty()) {
Log.w(MqttConstants.TAG, "mqttInit: hostIp is empty")
return
}
Log.d(MqttConstants.TAG, "mqttInit")
this.hostIp = hostIp
this.deviceSn = deviceSn
// 初始化主题(与原代码一致)
topicPublishDb = "${MqttConstants.PUBLISH_HEAD}$deviceSn${MqttConstants.PUBLISH_TAIL_EVENT}"
topicPublishSoftwareVersion = "${MqttConstants.PUBLISH_HEAD}$deviceSn${MqttConstants.PUBLISH_TAIL_PROP}"
topicPublishUpdateReply = "${MqttConstants.PUBLISH_HEAD}$deviceSn${MqttConstants.PUBLISH_TAIL_UPDATEREPLY}"
topicPublishLogUpload = "${MqttConstants.PUBLISH_HEAD}$deviceSn${MqttConstants.PUBLISH_TAIL_MESSAGEREPLY}"
topicPublishInitDeviceReply = "${MqttConstants.PUBLISH_HEAD}$deviceSn${MqttConstants.PUBLISH_TAIL_MESSAGEREPLY}"
topicPublishBorrowDev = "${MqttConstants.PUBLISH_HEAD}$deviceSn${MqttConstants.PUBLISH_TAIL_EVENT}"
topicPublishReturnDev = "${MqttConstants.PUBLISH_HEAD}$deviceSn${MqttConstants.PUBLISH_TAIL_EVENT}"
topicPublishLoseDev = "${MqttConstants.PUBLISH_HEAD}$deviceSn${MqttConstants.PUBLISH_TAIL_EVENT}"
topicSubscribeUpdate = "${MqttConstants.SUBSCRIBE_HEAD}$deviceSn${MqttConstants.SUBSCRIBE_TAIL_UPDATE}"
topicSubscribePropertySet = "${MqttConstants.SUBSCRIBE_HEAD}$deviceSn${MqttConstants.SUBSCRIBE_TAIL_PROPERTYSET}"
topicSubscribeServer = "${MqttConstants.SUBSCRIBE_HEAD}$deviceSn${MqttConstants.SUBSCRIBE_TAIL_SERVER}"
// 生成认证信息(与原代码一致)
clientId = deviceSn
username = "$deviceSn|${System.currentTimeMillis()}"
password = md5("$username|$devicePassword")
Log.d(MqttConstants.TAG, "CLIENTID: $clientId")
Log.d(MqttConstants.TAG, "USERNAME: $username")
val serverUri = "tcp://$hostIp${MqttConstants.HOST_PORT}"
Log.d(MqttConstants.TAG, "serverURI: $serverUri")
// TODO: 初始化真实 MQTT 客户端
// mqttAndroidClient = MqttAndroidClient(context, serverUri, clientId)
// mqttAndroidClient?.setCallback(mqttCallback)
// mqttConnectOptions = MqttConnectOptions().apply {
// isCleanSession = true
// connectionTimeout = 10
// keepAliveInterval = 20
// userName = this@MqttServiceImpl.username
// password = this@MqttServiceImpl.password.toCharArray()
// }
}
override fun mqttConnect() {
Log.d(MqttConstants.TAG, "mqttConnect")
_connectionState.value = MqttConnectionState.CONNECTING
connectionCallback?.invoke(MqttConnectionState.CONNECTING)
// TODO: 真实连接
// try {
// mqttAndroidClient?.connect(mqttConnectOptions, null, object : IMqttActionListener {
// override fun onSuccess(asyncActionToken: IMqttToken?) {
// Log.d(TAG, "MQTT连接成功!")
// _connectionState.value = MqttConnectionState.CONNECTED
// connectionCallback?.invoke(MqttConnectionState.CONNECTED)
// publishSoftwareVersion(getSoftwareVersion())
// subscribeUpdate()
// subscribeLog()
// subscribeUploadLog()
// }
// override fun onFailure(asyncActionToken: IMqttToken?, exception: Throwable?) {
// Log.d(TAG, "MQTT连接失败!")
// mqttClose()
// }
// })
// } catch (e: MqttException) {
// e.printStackTrace()
// }
// 模拟连接成功
_connectionState.value = MqttConnectionState.CONNECTED
connectionCallback?.invoke(MqttConnectionState.CONNECTED)
sharedPreferences.edit().putBoolean("connected", true).apply()
}
override fun mqttClose() {
Log.d(MqttConstants.TAG, "mqttClose")
_connectionState.value = MqttConnectionState.DISCONNECTED
connectionCallback?.invoke(MqttConnectionState.DISCONNECTED)
sharedPreferences.edit().putBoolean("connected", false).apply()
// TODO: 断开真实连接
// try {
// mqttAndroidClient?.disconnect()
// } catch (e: MqttException) {
// e.printStackTrace()
// }
}
// ==================== 订阅 ====================
override fun subscribeUpdate() {
if (!isMqttConnect) return
Log.d(MqttConstants.TAG, "subscribeUpdate: $topicSubscribeUpdate")
// TODO: mqttAndroidClient?.subscribe(topicSubscribeUpdate, MqttConstants.QOS_DEFAULT)
}
override fun subscribeLog() {
if (!isMqttConnect) return
Log.d(MqttConstants.TAG, "subscribeLog: $topicSubscribePropertySet")
// TODO: mqttAndroidClient?.subscribe(topicSubscribePropertySet, MqttConstants.QOS_DEFAULT)
}
override fun subscribeUploadLog() {
if (!isMqttConnect) return
Log.d(MqttConstants.TAG, "subscribeUploadLog: $topicSubscribeServer")
// TODO: mqttAndroidClient?.subscribe(topicSubscribeServer, MqttConstants.QOS_DEFAULT)
}
// ==================== 发布 - 设备状态 ====================
override fun publishDb(doorInfo: DoorInfo) {
val message = buildDoorInfoSyncEvent(doorInfo)
publish(topicPublishDb, message)
}
override fun publishBorrowdevEvent(event: DeviceEvent) {
val message = buildBorrowDeviceEvent(event)
publish(topicPublishBorrowDev, message)
}
override fun publishReturndevEvent(event: DeviceEvent) {
val message = buildReturnDeviceEvent(event)
publish(topicPublishReturnDev, message)
}
override fun publishLosedevEvent(event: DeviceEvent) {
val message = buildLoseDeviceEvent(event)
publish(topicPublishLoseDev, message)
}
// ==================== 发布 - 系统消息 ====================
override fun publishSoftwareVersion(version: String) {
val message = """{"identifier":"firmware_version","firmware_version":"$version"}"""
publish(topicPublishSoftwareVersion, message)
}
override fun publishDownLoadReply(code: String, messageId: String, taskId: String) {
val replyMsg = when (code) {
"0" -> "等待升级"
"1" -> "已发送设备"
"2" -> "设备收到"
"3" -> "升级成功"
"4" -> "升级失败"
"5" -> "设备离线停止推送"
else -> "未知错误码"
}
val message = """{"messageId":"$messageId","result":{"code":"$code","msg":"$replyMsg","taskId":"$taskId"}}"""
publish(topicPublishUpdateReply, message)
}
override fun publishLogupload(messageId: String, filename: String, fileUrl: String) {
val message = """{"messageId":"$messageId","result":{"code":"200","msg":{"type":"upload_log_reply","fileName":"$filename","fileUrl":"$fileUrl"}}}"""
publish(topicPublishLogUpload, message)
}
override fun publishInitDeviceReply(messageId: String, opType: String) {
val dstr = getDataString()
val message = """{"messageId":"$messageId","result":{"code":"200","msg":{"type":"init_device_reply","status":"ok","optype":"$opType","time":"$dstr"}}}"""
publish(topicPublishInitDeviceReply, message)
}
override fun publishCellSetReply(messageId: String) {
val dstr = getDataString()
val message = """{"messageId":"$messageId","result":{"code":"200","msg":{"type":"door_set_reply","status":"ok","time":"$dstr"}}}"""
publish(topicPublishInitDeviceReply, message)
}
// ==================== 回调 ====================
override fun setMessageCallback(callback: (topic: String, message: String) -> Unit) {
messageCallback = callback
}
override fun setConnectionCallback(callback: (MqttConnectionState) -> Unit) {
connectionCallback = callback
}
// ==================== 内部方法 ====================
private fun publish(topic: String, message: String) {
if (!isMqttConnect) {
Log.w(MqttConstants.TAG, "publish failed: not connected")
return
}
Log.d(MqttConstants.TAG, "publish to $topic: $message")
// TODO: 真实发布
// try {
// mqttAndroidClient?.publish(topic, message.toByteArray(), MqttConstants.QOS_DEFAULT, false)
// } catch (e: MqttException) {
// e.printStackTrace()
// }
}
/**
* 处理收到的消息 MqttCallback 调用
*/
internal fun handleMessage(topic: String, payload: String) {
Log.d(MqttConstants.TAG, "MQTT收到消息topic:$topic msg:$payload")
messageCallback?.invoke(topic, payload)
try {
when (topic) {
topicSubscribeUpdate -> handleUpdateMessage(payload)
topicSubscribePropertySet -> handlePropertySetMessage(payload)
topicSubscribeServer -> handleServerMessage(payload)
}
} catch (e: Exception) {
Log.e(MqttConstants.TAG, "handleMessage error", e)
}
}
private fun handleUpdateMessage(payload: String) {
val jsonObject = JSONObject(payload)
val body = jsonObject.getString("body")
val bodyJson = JSONObject(body)
val messageId = bodyJson.getString("messageId")
val taskId = bodyJson.getString("taskId")
val firmwareVersion = bodyJson.getString("firmwareVersion")
val url = bodyJson.getString("url")
sharedPreferences.edit().apply {
putString("fw", firmwareVersion)
putString("url", url)
putString("mid", messageId)
putString("tid", taskId)
putString("vcheck", "1")
apply()
}
}
private fun handlePropertySetMessage(payload: String) {
val jsonObject = JSONObject(payload)
val identifier = jsonObject.getString("identifier")
val body = jsonObject.getString("body")
Log.d(MqttConstants.TAG, "PropertySet: $identifier = $body")
}
private fun handleServerMessage(payload: String) {
val jsonObject = JSONObject(payload)
val identifier = jsonObject.getString("identifier")
Log.d(MqttConstants.TAG, "Server identifier: $identifier")
// 根据 identifier 处理不同的服务调用
}
// ==================== 消息构建 ====================
private fun buildDoorInfoSyncEvent(info: DoorInfo): String {
return """{"door_info_sync_event":{"devType":"${info.devType}","devSn":"${info.devSn}","devCheckType":"${info.devCheckType}","cabinetName":"${info.cabinetName}","doorState":"${info.cellState}","connectorType":"${info.cellConnectorType}","devPvd":"${info.devPvd}","doorName":"${info.cellName}","doorRepairType":"${info.cellRepairType}","devRepairType":"${info.devRepairType}","devPro":"${info.devPro}","devState":"${info.devState}","devMfg":"${info.devMfg}","devPwr":"${info.devPwr}","doorVoltageType":"${info.cellVoltageType}"}}"""
}
private fun buildBorrowDeviceEvent(event: DeviceEvent): String {
val doorInfo = event.doorInfo
return """{"borrow_device_event":{"opType":"${event.opType}","deviceCode":"${event.deviceCode}","cabinetName":"${event.cabinetName}","devSn":"${event.devSn}","doorName":"${event.doorName}","devType":"${event.devType}","userId":"${event.userId}","batchId":"${event.batchId}","borrowTime":"${event.opTime}","doorInfo":${buildDoorInfoJson(doorInfo)}}}"""
}
private fun buildReturnDeviceEvent(event: DeviceEvent): String {
val doorInfo = event.doorInfo
return """{"return_device_event":{"opType":"${event.opType}","deviceCode":"${event.deviceCode}","cabinetName":"${event.cabinetName}","devSn":"${event.devSn}","doorName":"${event.doorName}","devType":"${event.devType}","userId":"${event.userId}","batchId":"${event.batchId}","doorRepairType":"${doorInfo.cellRepairType}","devRepairType":"${doorInfo.devRepairType}","returnTime":"${event.opTime}","doorInfo":${buildDoorInfoJson(doorInfo)}}}"""
}
private fun buildLoseDeviceEvent(event: DeviceEvent): String {
val doorInfo = event.doorInfo
return """{"lose_device_event":{"opType":"${event.opType}","deviceCode":"${event.deviceCode}","cabinetName":"${event.cabinetName}","devSn":"${event.devSn}","doorName":"${event.doorName}","devType":"${event.devType}","userId":"${event.userId}","batchId":"${event.batchId}","doorRepairType":"${doorInfo.cellRepairType}","devRepairType":"${doorInfo.devRepairType}","loseTime":"${event.opTime}","doorInfo":${buildDoorInfoJson(doorInfo)}}}"""
}
private fun buildDoorInfoJson(info: DoorInfo): String {
return """{"devType":"${info.devType}","devSn":"${info.devSn}","devCheckType":"${info.devCheckType}","cabinetName":"${info.cabinetName}","doorState":"${info.cellState}","connectorType":"${info.cellConnectorType}","devPvd":"${info.devPvd}","doorName":"${info.cellName}","doorRepairType":"${info.cellRepairType}","devRepairType":"${info.devRepairType}","devPro":"${info.devPro}","devState":"${info.devState}","devMfg":"${info.devMfg}","devPwr":"${info.devPwr}","doorVoltageType":"${info.cellVoltageType}"}"""
}
// ==================== 工具方法 ====================
private fun md5(input: String): String {
return try {
val md = MessageDigest.getInstance("MD5")
val digest = md.digest(input.toByteArray())
digest.joinToString("") { "%02x".format(it) }
} catch (e: NoSuchAlgorithmException) {
""
}
}
private fun getDataString(): String {
val sdf = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault())
return sdf.format(java.util.Date())
}
}
// ==================== 便捷扩展 ====================
fun Context.getMqttService(): IMqttService = MqttServiceImpl.getInstance(this)

View File

@ -32,6 +32,8 @@ import androidx.compose.foundation.border
import com.example.smarthome.R
import com.example.smarthome.data.WeatherInfo
import com.example.smarthome.data.WeatherService
import com.example.smarthome.data.LanguageManager
import com.example.smarthome.data.tr
import kotlinx.coroutines.launch
import kotlinx.coroutines.delay
import androidx.compose.ui.platform.LocalDensity
@ -139,10 +141,10 @@ fun RoomContent(selectedRoom: Int, roomName: String) {
fun OverviewRoomContent() {
Column(modifier = Modifier.fillMaxSize()) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp)) {
AirConditionerCard(modifier = Modifier.weight(1f), roomName = "全屋")
UsageStatusChart(modifier = Modifier.weight(1f), roomName = "全屋")
AirConditionerCard(modifier = Modifier.weight(1f), roomName = tr("room_all"))
UsageStatusChart(modifier = Modifier.weight(1f), roomName = tr("room_all"))
}
LightRow(modifier = Modifier.fillMaxWidth().padding(top = 16.dp, bottom = 16.dp), roomName = "全屋")
LightRow(modifier = Modifier.fillMaxWidth().padding(top = 16.dp, bottom = 16.dp), roomName = tr("room_all"))
ModeButtonsRow(onModeSelected = { })
Spacer(modifier = Modifier.height(24.dp))
@ -502,14 +504,17 @@ fun SideNavRail(selectedNavItem: Int = 0, onNavItemSelect: (Int) -> Unit = {}) {
verticalArrangement = Arrangement.SpaceBetween
) {
// 上半部分:导航项
// 监听语言变化以触发重组
val currentLang by LanguageManager.currentLanguage.collectAsState()
Column(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) })
NavRailItem("自动化", R.drawable.ic_automation, selectedNavItem == 2, onClick = { onNavItemSelect(2) })
NavRailItem("统计", R.drawable.ic_statistics, selectedNavItem == 3, onClick = { onNavItemSelect(3) })
NavRailItem("安全", R.drawable.ic_security, selectedNavItem == 4, onClick = { onNavItemSelect(4) })
NavRailItem("设置", R.drawable.ic_settings, selectedNavItem == 5, onClick = { onNavItemSelect(5) })
NavRailItem(tr("nav_dashboard"), R.drawable.ic_dashboard, selectedNavItem == 0, onClick = { onNavItemSelect(0) })
NavRailItem(tr("nav_scene"), R.drawable.ic_scene, selectedNavItem == 1, onClick = { onNavItemSelect(1) })
NavRailItem(tr("nav_automation"), R.drawable.ic_automation, selectedNavItem == 2, onClick = { onNavItemSelect(2) })
NavRailItem(tr("nav_statistics"), R.drawable.ic_statistics, selectedNavItem == 3, onClick = { onNavItemSelect(3) })
NavRailItem(tr("nav_security"), R.drawable.ic_security, selectedNavItem == 4, onClick = { onNavItemSelect(4) })
NavRailItem(tr("nav_settings"), R.drawable.ic_settings, selectedNavItem == 5, onClick = { onNavItemSelect(5) })
}
// 底部:模式切换按钮(暂时注释)
@ -578,6 +583,7 @@ fun NavRailItem(text: String, iconRes: Int, selected: Boolean, onClick: () -> Un
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(6.dp),
modifier = Modifier
.width(80.dp) // 固定宽度,防止文字长度不一致导致图标不对齐
.scale(scale)
.clickable(
indication = null,
@ -615,9 +621,10 @@ fun NavRailItem(text: String, iconRes: Int, selected: Boolean, onClick: () -> Un
Text(
text = text,
color = if (selected) Color.White else Color(0xFFB0B0B0),
fontSize = 13.sp,
fontSize = 12.sp,
fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal,
maxLines = 1
maxLines = 1,
textAlign = TextAlign.Center
)
}
}
@ -638,7 +645,7 @@ fun TopBar() {
}
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
Text(text = "控制台", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, color = Color.White)
Text(text = tr("dashboard_title"), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, color = Color.White)
WeatherCard(weatherInfo = weatherInfo, isLoading = isLoading)
}
}
@ -1123,12 +1130,12 @@ fun AirConditionerCard(modifier: Modifier = Modifier, roomName: String = "房间
// 模式emoji图标
val modeIcons = listOf(
"❄️" to "制冷",
"☀️" to "制热",
"💧" to "除湿",
"🌀" to "送风"
"❄️" to tr("ac_mode_cool"),
"☀️" to tr("ac_mode_heat"),
"💧" to tr("ac_mode_dry"),
"🌀" to tr("ac_mode_fan")
)
val fanSpeeds = listOf("自动", "", "", "")
val fanSpeeds = listOf(tr("ac_fan_auto"), tr("ac_fan_low"), tr("ac_fan_medium"), tr("ac_fan_high"))
// 图标动画
val infiniteTransition = rememberInfiniteTransition(label = "ac_icon")
@ -1212,7 +1219,7 @@ fun AirConditionerCard(modifier: Modifier = Modifier, roomName: String = "房间
)
}
Column {
Text(text = "空调", fontWeight = FontWeight.SemiBold, color = Color.White, fontSize = 16.sp)
Text(text = tr("ac_title"), fontWeight = FontWeight.SemiBold, color = Color.White, fontSize = 16.sp)
Text(text = roomName, fontSize = 11.sp, color = Color(0xFF9AA0A6))
}
}
@ -1320,7 +1327,7 @@ fun AirConditionerCard(modifier: Modifier = Modifier, roomName: String = "房间
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(text = "风速", fontSize = 12.sp, color = Color(0xFF9AA0A6))
Text(text = tr("ac_fan_speed"), fontSize = 12.sp, color = Color(0xFF9AA0A6))
fanSpeeds.forEachIndexed { index, speed ->
val isSelected = fanSpeed == index
Box(

View File

@ -22,7 +22,10 @@ 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 androidx.compose.ui.window.Dialog
import com.example.smarthome.data.BackgroundManager
import com.example.smarthome.data.LanguageManager
import com.example.smarthome.data.tr
@Composable
fun SettingsScreen(onBack: () -> Unit) {
@ -44,52 +47,67 @@ fun SettingsScreen(onBack: () -> Unit) {
@Composable
fun SettingsContentList() {
val context = LocalContext.current
val currentLang by LanguageManager.currentLanguage.collectAsState()
var showLanguageDialog by remember { mutableStateOf(false) }
// 语言选择对话框
if (showLanguageDialog) {
LanguageSelectDialog(
currentLanguage = currentLang,
onLanguageSelect = { lang ->
LanguageManager.setLanguage(context, lang)
showLanguageDialog = false
},
onDismiss = { showLanguageDialog = false }
)
}
LazyColumn(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
item { SettingsSection(title = "账户设置") {
item { SettingsSection(title = tr("settings_account")) {
SettingsItem(
title = "个人信息",
subtitle = "管理您的个人资料",
title = tr("settings_profile"),
subtitle = tr("settings_profile_desc"),
onClick = { }
)
SettingsItem(
title = "账户安全",
subtitle = "密码和安全设置",
title = tr("settings_security"),
subtitle = tr("settings_security_desc"),
onClick = { }
)
}}
item { SettingsSection(title = "设备管理") {
item { SettingsSection(title = tr("settings_device")) {
var autoConnect by remember { mutableStateOf(true) }
SettingsSwitchItem(
title = "自动连接",
subtitle = "自动连接到家庭网络",
title = tr("settings_auto_connect"),
subtitle = tr("settings_auto_connect_desc"),
checked = autoConnect,
onCheckedChange = { autoConnect = it }
)
var notifications by remember { mutableStateOf(true) }
SettingsSwitchItem(
title = "设备通知",
subtitle = "接收设备状态通知",
title = tr("settings_notifications"),
subtitle = tr("settings_notifications_desc"),
checked = notifications,
onCheckedChange = { notifications = it }
)
}}
item { SettingsSection(title = "显示设置") {
item { SettingsSection(title = tr("settings_display")) {
BackgroundSelector()
val context = LocalContext.current
val selectedBg by BackgroundManager.selectedBackground.collectAsState()
// 深色模式根据当前背景是否为暗色来判断
val isDarkMode = BackgroundManager.backgrounds.getOrNull(selectedBg)?.isDark ?: false
SettingsSwitchItem(
title = "深色模式",
subtitle = "使用深色主题",
title = tr("settings_dark_mode"),
subtitle = tr("settings_dark_mode_desc"),
checked = isDarkMode,
onCheckedChange = { dark ->
// 切换深色/浅色模式时自动更换背景
@ -102,26 +120,26 @@ fun SettingsContentList() {
)
SettingsItem(
title = "语言",
subtitle = "简体中文",
onClick = { }
title = tr("settings_language"),
subtitle = currentLang.nativeName,
onClick = { showLanguageDialog = true }
)
}}
item { SettingsSection(title = "关于") {
item { SettingsSection(title = tr("settings_about")) {
SettingsItem(
title = "版本信息",
title = tr("settings_version"),
subtitle = "v1.0.0",
onClick = { }
)
SettingsItem(
title = "隐私政策",
subtitle = "查看隐私政策",
title = tr("settings_privacy"),
subtitle = tr("settings_privacy_desc"),
onClick = { }
)
SettingsItem(
title = "用户协议",
subtitle = "查看用户协议",
title = tr("settings_terms"),
subtitle = tr("settings_terms_desc"),
onClick = { }
)
}}
@ -133,6 +151,98 @@ fun SettingsContentList() {
}
}
@Composable
fun LanguageSelectDialog(
currentLanguage: LanguageManager.Language,
onLanguageSelect: (LanguageManager.Language) -> Unit,
onDismiss: () -> Unit
) {
Dialog(onDismissRequest = onDismiss) {
Box(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(20.dp))
.background(Color(0xFF1E1E2E))
.padding(20.dp)
) {
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
Text(
text = tr("select_language"),
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = Color.White
)
Spacer(modifier = Modifier.height(8.dp))
LanguageManager.Language.entries.forEach { lang ->
val isSelected = lang == currentLanguage
Box(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.background(
if (isSelected) Color(0xFF6C63FF).copy(alpha = 0.3f)
else Color(0xFF2A2A3E)
)
.border(
width = if (isSelected) 2.dp else 0.dp,
color = if (isSelected) Color(0xFF6C63FF) else Color.Transparent,
shape = RoundedCornerShape(12.dp)
)
.clickable { onLanguageSelect(lang) }
.padding(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(
text = lang.nativeName,
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
color = Color.White
)
if (lang.displayName != lang.nativeName) {
Text(
text = lang.displayName,
fontSize = 12.sp,
color = Color(0xFF9AA0A6)
)
}
}
if (isSelected) {
Text(text = "", color = Color(0xFF6C63FF), fontSize = 18.sp)
}
}
}
}
Spacer(modifier = Modifier.height(8.dp))
// 取消按钮
Box(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.background(Color(0xFF3A3A4E))
.clickable { onDismiss() }
.padding(14.dp),
contentAlignment = Alignment.Center
) {
Text(
text = tr("cancel"),
color = Color.White,
fontSize = 14.sp
)
}
}
}
}
}
@Composable
fun SettingsTopBar(onBack: () -> Unit) {
Row(
@ -158,7 +268,7 @@ fun SettingsTopBar(onBack: () -> Unit) {
Text(text = "", color = Color.White, fontSize = 20.sp)
}
Text(
text = "设置",
text = tr("settings_title"),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = Color.White
@ -344,7 +454,7 @@ fun LogoutButton(onClick: () -> Unit) {
contentAlignment = Alignment.Center
) {
Text(
text = "退出登录",
text = tr("settings_logout"),
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = Color.White

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,63 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<!-- 制冷图标 - 雪花 -->
<!-- 垂直线 -->
<path
android:strokeColor="#FFFFFFFF"
android:strokeWidth="2"
android:strokeLineCap="round"
android:fillColor="#00000000"
android:pathData="M12,2 L12,22"/>
<!-- 左上到右下斜线 -->
<path
android:strokeColor="#FFFFFFFF"
android:strokeWidth="2"
android:strokeLineCap="round"
android:fillColor="#00000000"
android:pathData="M4.93,4.93 L19.07,19.07"/>
<!-- 右上到左下斜线 -->
<path
android:strokeColor="#FFFFFFFF"
android:strokeWidth="2"
android:strokeLineCap="round"
android:fillColor="#00000000"
android:pathData="M19.07,4.93 L4.93,19.07"/>
<!-- 顶部分叉 -->
<path
android:strokeColor="#FFFFFFFF"
android:strokeWidth="1.5"
android:strokeLineCap="round"
android:fillColor="#00000000"
android:pathData="M12,5 L10,7 M12,5 L14,7"/>
<!-- 底部分叉 -->
<path
android:strokeColor="#FFFFFFFF"
android:strokeWidth="1.5"
android:strokeLineCap="round"
android:fillColor="#00000000"
android:pathData="M12,19 L10,17 M12,19 L14,17"/>
<!-- 左上分叉 -->
<path
android:strokeColor="#FFFFFFFF"
android:strokeWidth="1.5"
android:strokeLineCap="round"
android:fillColor="#00000000"
android:pathData="M7.05,7.05 L7.05,9.5 M7.05,7.05 L9.5,7.05"/>
<!-- 右下分叉 -->
<path
android:strokeColor="#FFFFFFFF"
android:strokeWidth="1.5"
android:strokeLineCap="round"
android:fillColor="#00000000"
android:pathData="M16.95,16.95 L16.95,14.5 M16.95,16.95 L14.5,16.95"/>
</vector>

View File

@ -0,0 +1,19 @@
<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 C12,2 6,10 6,14 C6,17.31 8.69,20 12,20 C15.31,20 18,17.31 18,14 C18,10 12,2 12,2 Z"/>
<!-- 水滴内的波纹 -->
<path
android:strokeColor="#00000000"
android:strokeWidth="0"
android:fillColor="#FFFFFF"
android:fillAlpha="0.3"
android:pathData="M12,18 C9.79,18 8,16.21 8,14 C8,12.5 9.5,10 12,7 L12,18 Z"/>
</vector>

View File

@ -0,0 +1,25 @@
<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,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:fillColor="#FFFFFFFF"
android:pathData="M12,2 C12,2 14,6 14,8 C14,9.1 13.1,10 12,10 C10.9,10 10,9.1 10,8 C10,6 12,2 12,2 Z"/>
<path
android:fillColor="#FFFFFFFF"
android:pathData="M20.66,17 C20.66,17 16.66,17 14.93,16 C13.93,15.42 13.42,14.07 14,13.07 C14.58,12.07 15.93,11.58 16.93,12.15 C18.66,13.15 20.66,17 20.66,17 Z"/>
<path
android:fillColor="#FFFFFFFF"
android:pathData="M3.34,17 C3.34,17 7.34,17 9.07,16 C10.07,15.42 10.58,14.07 10,13.07 C9.42,12.07 8.07,11.58 7.07,12.15 C5.34,13.15 3.34,17 3.34,17 Z"/>
</vector>

View File

@ -0,0 +1,69 @@
<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,8 C9.79,8 8,9.79 8,12 C8,14.21 9.79,16 12,16 C14.21,16 16,14.21 16,12 C16,9.79 14.21,8 12,8 Z"/>
<!-- 光芒 -->
<path
android:strokeColor="#FFFFFFFF"
android:strokeWidth="2"
android:strokeLineCap="round"
android:fillColor="#00000000"
android:pathData="M12,2 L12,5"/>
<path
android:strokeColor="#FFFFFFFF"
android:strokeWidth="2"
android:strokeLineCap="round"
android:fillColor="#00000000"
android:pathData="M12,19 L12,22"/>
<path
android:strokeColor="#FFFFFFFF"
android:strokeWidth="2"
android:strokeLineCap="round"
android:fillColor="#00000000"
android:pathData="M2,12 L5,12"/>
<path
android:strokeColor="#FFFFFFFF"
android:strokeWidth="2"
android:strokeLineCap="round"
android:fillColor="#00000000"
android:pathData="M19,12 L22,12"/>
<path
android:strokeColor="#FFFFFFFF"
android:strokeWidth="2"
android:strokeLineCap="round"
android:fillColor="#00000000"
android:pathData="M4.93,4.93 L7.05,7.05"/>
<path
android:strokeColor="#FFFFFFFF"
android:strokeWidth="2"
android:strokeLineCap="round"
android:fillColor="#00000000"
android:pathData="M16.95,16.95 L19.07,19.07"/>
<path
android:strokeColor="#FFFFFFFF"
android:strokeWidth="2"
android:strokeLineCap="round"
android:fillColor="#00000000"
android:pathData="M4.93,19.07 L7.05,16.95"/>
<path
android:strokeColor="#FFFFFFFF"
android:strokeWidth="2"
android:strokeLineCap="round"
android:fillColor="#00000000"
android:pathData="M16.95,7.05 L19.07,4.93"/>
</vector>

BIN
background2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB