设置字体和应用图标
@ -71,6 +71,9 @@ dependencies {
|
||||
implementation("com.google.code.gson:gson:2.10.1")
|
||||
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
||||
implementation("com.google.android.gms:play-services-location:21.0.1")
|
||||
|
||||
// ZXing 二维码生成
|
||||
implementation("com.google.zxing:core:3.5.2")
|
||||
|
||||
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12")
|
||||
|
||||
|
||||
@ -20,6 +20,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.example.smarthome.ui.MainScaffold
|
||||
import com.example.smarthome.ui.LoginScreen
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.ui.res.painterResource
|
||||
@ -29,6 +30,7 @@ import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import com.example.smarthome.data.BackgroundManager
|
||||
import com.example.smarthome.data.LanguageManager
|
||||
import com.example.smarthome.data.UserManager
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@ -36,6 +38,8 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
// 初始化语言管理器
|
||||
LanguageManager.init(this)
|
||||
// 初始化用户管理器
|
||||
UserManager.init(this)
|
||||
|
||||
// 隐藏状态栏和导航栏
|
||||
hideStatusBar()
|
||||
@ -94,6 +98,26 @@ class MainActivity : ComponentActivity() {
|
||||
@Composable
|
||||
fun AppRoot() {
|
||||
val context = androidx.compose.ui.platform.LocalContext.current
|
||||
|
||||
// 监听登录状态
|
||||
val isLoggedIn by UserManager.isLoggedIn.collectAsState()
|
||||
|
||||
if (!isLoggedIn) {
|
||||
// 未登录,显示登录页面
|
||||
LoginScreen(
|
||||
onLoginSuccess = {
|
||||
// 登录成功后会自动更新isLoggedIn状态
|
||||
}
|
||||
)
|
||||
} else {
|
||||
// 已登录,显示主界面
|
||||
MainContent()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MainContent() {
|
||||
val context = androidx.compose.ui.platform.LocalContext.current
|
||||
val sharedPreferences = remember {
|
||||
context.getSharedPreferences("smart_home_prefs", android.content.Context.MODE_PRIVATE)
|
||||
}
|
||||
|
||||
@ -130,6 +130,41 @@ object LanguageManager {
|
||||
// 天气
|
||||
"weather_humidity" to "湿度",
|
||||
|
||||
// 登录
|
||||
"app_name" to "智能家居",
|
||||
"app_slogan" to "让生活更智能",
|
||||
"login_wechat_scan" to "微信扫码登录",
|
||||
"login_wechat_hint" to "使用微信扫描二维码登录",
|
||||
"login_waiting_scan" to "等待扫码...",
|
||||
"login_scanned" to "扫码成功",
|
||||
"login_confirm_on_phone" to "请在手机上确认登录",
|
||||
"login_success" to "登录成功",
|
||||
"login_simulate" to "模拟微信登录",
|
||||
"login_other_methods" to "其他登录方式",
|
||||
"login_phone" to "手机号",
|
||||
"login_password" to "密码",
|
||||
"login_agreement" to "登录即表示同意《用户协议》和《隐私政策》",
|
||||
"login_refresh_qr" to "刷新二维码",
|
||||
"login_wechat" to "微信登录",
|
||||
"switch_to_phone" to "使用手机号登录",
|
||||
|
||||
// 手机号登录
|
||||
"phone_login_title" to "手机号登录",
|
||||
"phone_input_hint" to "输入手机号,快速登录/注册",
|
||||
"phone_placeholder" to "请输入手机号",
|
||||
"phone_invalid" to "请输入正确的11位手机号",
|
||||
"phone_agreement" to "未注册的手机号将自动创建账号",
|
||||
"get_verify_code" to "获取验证码",
|
||||
"code_sent_to" to "验证码已发送至",
|
||||
"change_phone" to "更换",
|
||||
"code_invalid" to "请输入6位验证码",
|
||||
"code_wrong" to "验证码错误,请重新输入",
|
||||
"resend_code" to "重新发送",
|
||||
"resend_code_in" to "重新发送",
|
||||
"login_register" to "登录/注册",
|
||||
"login_failed" to "登录失败,请重试",
|
||||
"debug_code_hint" to "开发测试验证码:",
|
||||
|
||||
// 通用
|
||||
"add_device" to "+ 添加设备",
|
||||
"on" to "开",
|
||||
@ -203,6 +238,41 @@ object LanguageManager {
|
||||
// Weather
|
||||
"weather_humidity" to "Humidity",
|
||||
|
||||
// Login
|
||||
"app_name" to "Smart Home",
|
||||
"app_slogan" to "Make life smarter",
|
||||
"login_wechat_scan" to "WeChat Scan Login",
|
||||
"login_wechat_hint" to "Scan QR code with WeChat to login",
|
||||
"login_waiting_scan" to "Waiting for scan...",
|
||||
"login_scanned" to "Scanned",
|
||||
"login_confirm_on_phone" to "Please confirm on your phone",
|
||||
"login_success" to "Login successful",
|
||||
"login_simulate" to "Simulate WeChat Login",
|
||||
"login_other_methods" to "Other login methods",
|
||||
"login_phone" to "Phone",
|
||||
"login_password" to "Password",
|
||||
"login_agreement" to "By logging in, you agree to our Terms and Privacy Policy",
|
||||
"login_refresh_qr" to "Refresh QR Code",
|
||||
"login_wechat" to "WeChat",
|
||||
"switch_to_phone" to "Login with phone number",
|
||||
|
||||
// Phone Login
|
||||
"phone_login_title" to "Phone Login",
|
||||
"phone_input_hint" to "Enter phone number to login/register",
|
||||
"phone_placeholder" to "Enter phone number",
|
||||
"phone_invalid" to "Please enter a valid phone number",
|
||||
"phone_agreement" to "Unregistered numbers will create a new account",
|
||||
"get_verify_code" to "Get Code",
|
||||
"code_sent_to" to "Code sent to",
|
||||
"change_phone" to "Change",
|
||||
"code_invalid" to "Please enter 6-digit code",
|
||||
"code_wrong" to "Wrong code, please try again",
|
||||
"resend_code" to "Resend",
|
||||
"resend_code_in" to "Resend in",
|
||||
"login_register" to "Login/Register",
|
||||
"login_failed" to "Login failed, please try again",
|
||||
"debug_code_hint" to "Debug code:",
|
||||
|
||||
// Common
|
||||
"add_device" to "+ Add Device",
|
||||
"on" to "On",
|
||||
|
||||
278
app/src/main/java/com/example/smarthome/data/UserManager.kt
Normal file
@ -0,0 +1,278 @@
|
||||
package com.example.smarthome.data
|
||||
|
||||
import android.content.Context
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* 用户管理器 - 管理用户登录状态和信息
|
||||
* 支持手机号+验证码注册登录
|
||||
*/
|
||||
object UserManager {
|
||||
|
||||
private const val PREFS_NAME = "user_prefs"
|
||||
private const val KEY_IS_LOGGED_IN = "is_logged_in"
|
||||
private const val KEY_USER_ID = "user_id"
|
||||
private const val KEY_PHONE = "phone"
|
||||
private const val KEY_NICKNAME = "nickname"
|
||||
private const val KEY_AVATAR_URL = "avatar_url"
|
||||
private const val KEY_REGISTER_TIME = "register_time"
|
||||
private const val KEY_LAST_LOGIN_TIME = "last_login_time"
|
||||
private const val KEY_GENDER = "gender"
|
||||
private const val KEY_BIRTHDAY = "birthday"
|
||||
private const val KEY_EMAIL = "email"
|
||||
private const val KEY_ADDRESS = "address"
|
||||
|
||||
// 用户数据库(模拟,实际应该用后端数据库)
|
||||
private const val USERS_PREFS = "users_database"
|
||||
|
||||
data class UserInfo(
|
||||
val userId: String = "",
|
||||
val phone: String = "",
|
||||
val nickname: String = "",
|
||||
val avatarUrl: String = "",
|
||||
val registerTime: Long = 0L,
|
||||
val lastLoginTime: Long = 0L,
|
||||
val gender: Int = 0,
|
||||
val birthday: String = "",
|
||||
val email: String = "",
|
||||
val address: String = ""
|
||||
)
|
||||
|
||||
private val _isLoggedIn = MutableStateFlow(false)
|
||||
val isLoggedIn: StateFlow<Boolean> = _isLoggedIn
|
||||
|
||||
private val _userInfo = MutableStateFlow(UserInfo())
|
||||
val userInfo: StateFlow<UserInfo> = _userInfo
|
||||
|
||||
// 验证码存储(模拟,实际应该用后端+短信服务)
|
||||
private var currentVerifyCode: String = ""
|
||||
private var currentVerifyPhone: String = ""
|
||||
private var verifyCodeExpireTime: Long = 0L
|
||||
|
||||
fun init(context: Context) {
|
||||
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
_isLoggedIn.value = prefs.getBoolean(KEY_IS_LOGGED_IN, false)
|
||||
if (_isLoggedIn.value) {
|
||||
_userInfo.value = UserInfo(
|
||||
userId = prefs.getString(KEY_USER_ID, "") ?: "",
|
||||
phone = prefs.getString(KEY_PHONE, "") ?: "",
|
||||
nickname = prefs.getString(KEY_NICKNAME, "") ?: "",
|
||||
avatarUrl = prefs.getString(KEY_AVATAR_URL, "") ?: "",
|
||||
registerTime = prefs.getLong(KEY_REGISTER_TIME, 0L),
|
||||
lastLoginTime = prefs.getLong(KEY_LAST_LOGIN_TIME, 0L),
|
||||
gender = prefs.getInt(KEY_GENDER, 0),
|
||||
birthday = prefs.getString(KEY_BIRTHDAY, "") ?: "",
|
||||
email = prefs.getString(KEY_EMAIL, "") ?: "",
|
||||
address = prefs.getString(KEY_ADDRESS, "") ?: ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送验证码(模拟)
|
||||
* 实际项目中应该调用短信服务商API
|
||||
* @return 验证码(仅用于开发测试显示)
|
||||
*/
|
||||
fun sendVerifyCode(phone: String): String {
|
||||
// 生成6位随机验证码
|
||||
currentVerifyCode = (100000..999999).random().toString()
|
||||
currentVerifyPhone = phone
|
||||
verifyCodeExpireTime = System.currentTimeMillis() + 5 * 60 * 1000 // 5分钟有效
|
||||
|
||||
// 实际项目中这里调用短信API发送验证码
|
||||
// SmsService.send(phone, "您的验证码是: $currentVerifyCode")
|
||||
|
||||
return currentVerifyCode // 开发测试用,实际不应返回
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证验证码
|
||||
*/
|
||||
fun verifyCode(phone: String, code: String): Boolean {
|
||||
if (phone != currentVerifyPhone) return false
|
||||
if (System.currentTimeMillis() > verifyCodeExpireTime) return false
|
||||
return code == currentVerifyCode
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否已注册
|
||||
*/
|
||||
fun isUserRegistered(context: Context, phone: String): Boolean {
|
||||
val usersPrefs = context.getSharedPreferences(USERS_PREFS, Context.MODE_PRIVATE)
|
||||
return usersPrefs.contains("user_$phone")
|
||||
}
|
||||
|
||||
/**
|
||||
* 手机号登录/注册
|
||||
* 如果用户不存在则自动注册
|
||||
*/
|
||||
fun loginWithPhone(context: Context, phone: String): Result<UserInfo> {
|
||||
val usersPrefs = context.getSharedPreferences(USERS_PREFS, Context.MODE_PRIVATE)
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
val userInfo: UserInfo
|
||||
val isNewUser = !usersPrefs.contains("user_$phone")
|
||||
|
||||
if (isNewUser) {
|
||||
// 新用户注册
|
||||
val userId = UUID.randomUUID().toString()
|
||||
userInfo = UserInfo(
|
||||
userId = userId,
|
||||
phone = phone,
|
||||
nickname = "用户${phone.takeLast(4)}", // 默认昵称
|
||||
avatarUrl = "",
|
||||
registerTime = currentTime,
|
||||
lastLoginTime = currentTime,
|
||||
gender = 0,
|
||||
birthday = "",
|
||||
email = "",
|
||||
address = ""
|
||||
)
|
||||
|
||||
// 保存到用户数据库
|
||||
usersPrefs.edit()
|
||||
.putString("user_$phone", userId)
|
||||
.putString("${userId}_phone", phone)
|
||||
.putString("${userId}_nickname", userInfo.nickname)
|
||||
.putString("${userId}_avatar", "")
|
||||
.putLong("${userId}_register", currentTime)
|
||||
.putLong("${userId}_lastlogin", currentTime)
|
||||
.putInt("${userId}_gender", 0)
|
||||
.putString("${userId}_birthday", "")
|
||||
.putString("${userId}_email", "")
|
||||
.putString("${userId}_address", "")
|
||||
.apply()
|
||||
} else {
|
||||
// 已有用户登录
|
||||
val userId = usersPrefs.getString("user_$phone", "") ?: ""
|
||||
userInfo = UserInfo(
|
||||
userId = userId,
|
||||
phone = phone,
|
||||
nickname = usersPrefs.getString("${userId}_nickname", "用户${phone.takeLast(4)}") ?: "",
|
||||
avatarUrl = usersPrefs.getString("${userId}_avatar", "") ?: "",
|
||||
registerTime = usersPrefs.getLong("${userId}_register", currentTime),
|
||||
lastLoginTime = currentTime,
|
||||
gender = usersPrefs.getInt("${userId}_gender", 0),
|
||||
birthday = usersPrefs.getString("${userId}_birthday", "") ?: "",
|
||||
email = usersPrefs.getString("${userId}_email", "") ?: "",
|
||||
address = usersPrefs.getString("${userId}_address", "") ?: ""
|
||||
)
|
||||
|
||||
// 更新最后登录时间
|
||||
usersPrefs.edit()
|
||||
.putLong("${userId}_lastlogin", currentTime)
|
||||
.apply()
|
||||
}
|
||||
|
||||
// 设置当前登录状态
|
||||
_isLoggedIn.value = true
|
||||
_userInfo.value = userInfo
|
||||
|
||||
// 保存当前登录用户
|
||||
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
.putBoolean(KEY_IS_LOGGED_IN, true)
|
||||
.putString(KEY_USER_ID, userInfo.userId)
|
||||
.putString(KEY_PHONE, userInfo.phone)
|
||||
.putString(KEY_NICKNAME, userInfo.nickname)
|
||||
.putString(KEY_AVATAR_URL, userInfo.avatarUrl)
|
||||
.putLong(KEY_REGISTER_TIME, userInfo.registerTime)
|
||||
.putLong(KEY_LAST_LOGIN_TIME, userInfo.lastLoginTime)
|
||||
.putInt(KEY_GENDER, userInfo.gender)
|
||||
.putString(KEY_BIRTHDAY, userInfo.birthday)
|
||||
.putString(KEY_EMAIL, userInfo.email)
|
||||
.putString(KEY_ADDRESS, userInfo.address)
|
||||
.apply()
|
||||
|
||||
return Result.success(userInfo)
|
||||
}
|
||||
|
||||
/**
|
||||
* 旧的登录方法(兼容微信登录等)
|
||||
*/
|
||||
fun login(context: Context, userId: String, nickname: String, avatarUrl: String) {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
_isLoggedIn.value = true
|
||||
_userInfo.value = UserInfo(
|
||||
userId = userId,
|
||||
nickname = nickname,
|
||||
avatarUrl = avatarUrl,
|
||||
lastLoginTime = currentTime
|
||||
)
|
||||
|
||||
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
.putBoolean(KEY_IS_LOGGED_IN, true)
|
||||
.putString(KEY_USER_ID, userId)
|
||||
.putString(KEY_NICKNAME, nickname)
|
||||
.putString(KEY_AVATAR_URL, avatarUrl)
|
||||
.putLong(KEY_LAST_LOGIN_TIME, currentTime)
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun logout(context: Context) {
|
||||
_isLoggedIn.value = false
|
||||
_userInfo.value = UserInfo()
|
||||
|
||||
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
.clear()
|
||||
.apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户信息
|
||||
*/
|
||||
fun updateUserInfo(
|
||||
context: Context,
|
||||
nickname: String? = null,
|
||||
avatarUrl: String? = null,
|
||||
gender: Int? = null,
|
||||
birthday: String? = null,
|
||||
email: String? = null,
|
||||
address: String? = null
|
||||
) {
|
||||
val current = _userInfo.value
|
||||
val updated = current.copy(
|
||||
nickname = nickname ?: current.nickname,
|
||||
avatarUrl = avatarUrl ?: current.avatarUrl,
|
||||
gender = gender ?: current.gender,
|
||||
birthday = birthday ?: current.birthday,
|
||||
email = email ?: current.email,
|
||||
address = address ?: current.address
|
||||
)
|
||||
_userInfo.value = updated
|
||||
|
||||
// 保存到当前登录用户
|
||||
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit()
|
||||
nickname?.let { prefs.putString(KEY_NICKNAME, it) }
|
||||
avatarUrl?.let { prefs.putString(KEY_AVATAR_URL, it) }
|
||||
gender?.let { prefs.putInt(KEY_GENDER, it) }
|
||||
birthday?.let { prefs.putString(KEY_BIRTHDAY, it) }
|
||||
email?.let { prefs.putString(KEY_EMAIL, it) }
|
||||
address?.let { prefs.putString(KEY_ADDRESS, it) }
|
||||
prefs.apply()
|
||||
|
||||
// 同步到用户数据库
|
||||
if (current.userId.isNotEmpty()) {
|
||||
val usersPrefs = context.getSharedPreferences(USERS_PREFS, Context.MODE_PRIVATE).edit()
|
||||
nickname?.let { usersPrefs.putString("${current.userId}_nickname", it) }
|
||||
avatarUrl?.let { usersPrefs.putString("${current.userId}_avatar", it) }
|
||||
gender?.let { usersPrefs.putInt("${current.userId}_gender", it) }
|
||||
birthday?.let { usersPrefs.putString("${current.userId}_birthday", it) }
|
||||
email?.let { usersPrefs.putString("${current.userId}_email", it) }
|
||||
address?.let { usersPrefs.putString("${current.userId}_address", it) }
|
||||
usersPrefs.apply()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateAvatar(context: Context, avatarUrl: String) {
|
||||
updateUserInfo(context, avatarUrl = avatarUrl)
|
||||
}
|
||||
|
||||
fun updateNickname(context: Context, nickname: String) {
|
||||
updateUserInfo(context, nickname = nickname)
|
||||
}
|
||||
}
|
||||
620
app/src/main/java/com/example/smarthome/ui/LoginScreen.kt
Normal file
@ -0,0 +1,620 @@
|
||||
package com.example.smarthome.ui
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color as AndroidColor
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
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.blur
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.smarthome.R
|
||||
import com.example.smarthome.data.UserManager
|
||||
import com.example.smarthome.data.tr
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.qrcode.QRCodeWriter
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.json.JSONObject
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
|
||||
/**
|
||||
* 登录配置 - 修改这里对接你的后端
|
||||
*/
|
||||
object LoginConfig {
|
||||
// 后端API地址(需要替换成你的实际地址)
|
||||
const val BASE_URL = "https://your-backend.com/api"
|
||||
|
||||
// 获取登录二维码的接口
|
||||
const val GET_QR_CODE = "$BASE_URL/wechat/qrcode"
|
||||
|
||||
// 检查扫码状态的接口
|
||||
const val CHECK_STATUS = "$BASE_URL/wechat/check"
|
||||
|
||||
// 轮询间隔(毫秒)
|
||||
const val POLL_INTERVAL = 2000L
|
||||
|
||||
// 二维码过期时间(毫秒)
|
||||
const val QR_EXPIRE_TIME = 300000L // 5分钟
|
||||
|
||||
// 是否使用模拟模式(开发测试用)
|
||||
var USE_MOCK = true
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoginScreen(
|
||||
onLoginSuccess: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
// 登录方式:phone=手机号登录,wechat=微信扫码登录
|
||||
var loginMethod by remember { mutableStateOf("phone") }
|
||||
|
||||
// 扫码状态(微信登录用)
|
||||
var scanStatus by remember { mutableStateOf(ScanStatus.WAITING) }
|
||||
|
||||
// 二维码内容
|
||||
var qrCodeContent by remember { mutableStateOf("") }
|
||||
var qrCodeBitmap by remember { mutableStateOf<Bitmap?>(null) }
|
||||
var sessionId by remember { mutableStateOf("") }
|
||||
var errorMessage by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
// 获取二维码(仅微信登录时)
|
||||
LaunchedEffect(loginMethod) {
|
||||
if (loginMethod == "wechat") {
|
||||
if (LoginConfig.USE_MOCK) {
|
||||
qrCodeContent = "https://smarthome.example.com/login?session=${System.currentTimeMillis()}"
|
||||
qrCodeBitmap = generateQRCode(qrCodeContent, 400)
|
||||
} else {
|
||||
try {
|
||||
val response = fetchQRCode()
|
||||
qrCodeContent = response.first
|
||||
sessionId = response.second
|
||||
qrCodeBitmap = generateQRCode(qrCodeContent, 400)
|
||||
} catch (e: Exception) {
|
||||
errorMessage = "获取二维码失败: ${e.message}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 轮询检查扫码状态
|
||||
LaunchedEffect(sessionId, scanStatus, loginMethod) {
|
||||
if (loginMethod == "wechat" && sessionId.isNotEmpty() && scanStatus == ScanStatus.WAITING && !LoginConfig.USE_MOCK) {
|
||||
while (scanStatus == ScanStatus.WAITING) {
|
||||
delay(LoginConfig.POLL_INTERVAL)
|
||||
try {
|
||||
val status = checkScanStatus(sessionId)
|
||||
when (status.status) {
|
||||
"scanned" -> scanStatus = ScanStatus.SCANNED
|
||||
"confirmed" -> {
|
||||
scanStatus = ScanStatus.CONFIRMED
|
||||
UserManager.login(
|
||||
context,
|
||||
userId = status.userId,
|
||||
nickname = status.nickname,
|
||||
avatarUrl = status.avatarUrl
|
||||
)
|
||||
delay(500)
|
||||
onLoginSuccess()
|
||||
}
|
||||
"expired" -> {
|
||||
errorMessage = "二维码已过期,请刷新"
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// 忽略网络错误,继续轮询
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟扫码动画
|
||||
val infiniteTransition = rememberInfiniteTransition(label = "scan")
|
||||
val scanLineOffset by infiniteTransition.animateFloat(
|
||||
initialValue = 0f,
|
||||
targetValue = 1f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(2000, easing = LinearEasing),
|
||||
repeatMode = RepeatMode.Restart
|
||||
),
|
||||
label = "scan_line"
|
||||
)
|
||||
|
||||
// 光晕动画
|
||||
val glowAlpha by infiniteTransition.animateFloat(
|
||||
initialValue = 0.3f,
|
||||
targetValue = 0.7f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(1500, easing = EaseInOut),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
),
|
||||
label = "glow"
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
// 背景
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.background2),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
|
||||
// 模糊遮罩
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black.copy(alpha = 0.3f))
|
||||
)
|
||||
|
||||
// 主内容
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
// Logo和标题
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(80.dp)
|
||||
.shadow(16.dp, CircleShape)
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
Brush.linearGradient(
|
||||
listOf(Color(0xFFA9F0FF), Color(0xFFB89CFF))
|
||||
)
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "🏠",
|
||||
fontSize = 36.sp
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = tr("app_name"),
|
||||
fontSize = 28.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.White
|
||||
)
|
||||
|
||||
Text(
|
||||
text = tr("app_slogan"),
|
||||
fontSize = 14.sp,
|
||||
color = Color.White.copy(alpha = 0.7f)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
// 根据登录方式显示不同内容
|
||||
if (loginMethod == "phone") {
|
||||
// 手机号登录
|
||||
PhoneLoginScreen(
|
||||
onLoginSuccess = onLoginSuccess,
|
||||
onBack = { /* 无处可返回 */ }
|
||||
)
|
||||
} else {
|
||||
// 微信扫码登录卡片
|
||||
WeChatLoginCard(
|
||||
qrCodeBitmap = qrCodeBitmap,
|
||||
errorMessage = errorMessage,
|
||||
scanStatus = scanStatus,
|
||||
scanLineOffset = scanLineOffset,
|
||||
glowAlpha = glowAlpha,
|
||||
onRefreshQR = {
|
||||
qrCodeBitmap = null
|
||||
errorMessage = null
|
||||
scanStatus = ScanStatus.WAITING
|
||||
qrCodeContent = "https://smarthome.example.com/login?session=${System.currentTimeMillis()}"
|
||||
qrCodeBitmap = generateQRCode(qrCodeContent, 400)
|
||||
},
|
||||
onSimulateLogin = {
|
||||
scanStatus = ScanStatus.SCANNED
|
||||
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
|
||||
scanStatus = ScanStatus.CONFIRMED
|
||||
UserManager.login(
|
||||
context,
|
||||
userId = "wechat_user_001",
|
||||
nickname = "微信用户",
|
||||
avatarUrl = ""
|
||||
)
|
||||
onLoginSuccess()
|
||||
}, 1000)
|
||||
},
|
||||
onSwitchToPhone = { loginMethod = "phone" }
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// 切换登录方式
|
||||
if (loginMethod == "phone") {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = tr("login_other_methods"),
|
||||
fontSize = 12.sp,
|
||||
color = Color.White.copy(alpha = 0.5f)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(24.dp)
|
||||
) {
|
||||
// 微信登录
|
||||
LoginMethodButton(
|
||||
icon = "💬",
|
||||
label = tr("login_wechat"),
|
||||
onClick = { loginMethod = "wechat" }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// 底部协议
|
||||
Text(
|
||||
text = tr("login_agreement"),
|
||||
fontSize = 11.sp,
|
||||
color = Color.White.copy(alpha = 0.4f),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(horizontal = 32.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WeChatLoginCard(
|
||||
qrCodeBitmap: Bitmap?,
|
||||
errorMessage: String?,
|
||||
scanStatus: ScanStatus,
|
||||
scanLineOffset: Float,
|
||||
glowAlpha: Float,
|
||||
onRefreshQR: () -> Unit,
|
||||
onSimulateLogin: () -> Unit,
|
||||
onSwitchToPhone: () -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(360.dp)
|
||||
.shadow(24.dp, RoundedCornerShape(28.dp))
|
||||
.clip(RoundedCornerShape(28.dp))
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
listOf(
|
||||
Color.White.copy(alpha = 0.15f),
|
||||
Color.White.copy(alpha = 0.05f)
|
||||
)
|
||||
)
|
||||
)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
brush = Brush.verticalGradient(
|
||||
listOf(
|
||||
Color.White.copy(alpha = 0.3f),
|
||||
Color.White.copy(alpha = 0.1f)
|
||||
)
|
||||
),
|
||||
shape = RoundedCornerShape(28.dp)
|
||||
)
|
||||
.padding(32.dp)
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = tr("login_wechat_scan"),
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.White
|
||||
)
|
||||
|
||||
Text(
|
||||
text = tr("login_wechat_hint"),
|
||||
fontSize = 13.sp,
|
||||
color = Color.White.copy(alpha = 0.6f),
|
||||
modifier = Modifier.padding(top = 4.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// 二维码区域
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(200.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(Color.White)
|
||||
.padding(12.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (qrCodeBitmap != null) {
|
||||
Image(
|
||||
bitmap = qrCodeBitmap.asImageBitmap(),
|
||||
contentDescription = "Login QR Code",
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
} else if (errorMessage != null) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(text = "⚠️", fontSize = 32.sp)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = errorMessage,
|
||||
fontSize = 12.sp,
|
||||
color = Color.Red,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
} else {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(48.dp),
|
||||
color = Color(0xFF07C160)
|
||||
)
|
||||
}
|
||||
|
||||
// 扫描线动画
|
||||
if (scanStatus == ScanStatus.WAITING && qrCodeBitmap != null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(3.dp)
|
||||
.offset(y = (scanLineOffset * 176 - 88).dp)
|
||||
.background(
|
||||
Brush.horizontalGradient(
|
||||
listOf(
|
||||
Color.Transparent,
|
||||
Color(0xFF07C160),
|
||||
Color.Transparent
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// 扫码成功遮罩
|
||||
if (scanStatus == ScanStatus.SCANNED || scanStatus == ScanStatus.CONFIRMED) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.White.copy(alpha = 0.9f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(text = "✓", fontSize = 48.sp, color = Color(0xFF07C160))
|
||||
Text(
|
||||
text = if (scanStatus == ScanStatus.CONFIRMED) tr("login_success") else tr("login_scanned"),
|
||||
fontSize = 14.sp,
|
||||
color = Color(0xFF07C160),
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// 状态提示
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(8.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color(0xFF07C160))
|
||||
.alpha(glowAlpha)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = when (scanStatus) {
|
||||
ScanStatus.WAITING -> tr("login_waiting_scan")
|
||||
ScanStatus.SCANNED -> tr("login_confirm_on_phone")
|
||||
ScanStatus.CONFIRMED -> tr("login_success")
|
||||
},
|
||||
fontSize = 13.sp,
|
||||
color = Color.White.copy(alpha = 0.7f)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// 模拟登录按钮(仅在模拟模式下显示)
|
||||
if (LoginConfig.USE_MOCK) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(48.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(
|
||||
Brush.horizontalGradient(
|
||||
listOf(Color(0xFF07C160), Color(0xFF06AD56))
|
||||
)
|
||||
)
|
||||
.clickable { onSimulateLogin() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(text = "💬", fontSize = 18.sp)
|
||||
Text(
|
||||
text = tr("login_simulate"),
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
// 刷新二维码
|
||||
Text(
|
||||
text = tr("login_refresh_qr"),
|
||||
fontSize = 13.sp,
|
||||
color = Color(0xFF07C160),
|
||||
modifier = Modifier.clickable { onRefreshQR() }
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// 切换到手机号登录
|
||||
Text(
|
||||
text = tr("switch_to_phone"),
|
||||
fontSize = 13.sp,
|
||||
color = Color.White.copy(alpha = 0.6f),
|
||||
modifier = Modifier.clickable { onSwitchToPhone() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成二维码图片
|
||||
*/
|
||||
fun generateQRCode(content: String, size: Int): Bitmap? {
|
||||
return try {
|
||||
val writer = QRCodeWriter()
|
||||
val bitMatrix = writer.encode(content, BarcodeFormat.QR_CODE, size, size)
|
||||
val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.RGB_565)
|
||||
for (x in 0 until size) {
|
||||
for (y in 0 until size) {
|
||||
bitmap.setPixel(x, y, if (bitMatrix[x, y]) AndroidColor.BLACK else AndroidColor.WHITE)
|
||||
}
|
||||
}
|
||||
bitmap
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从后端获取登录二维码
|
||||
* 返回: Pair<二维码内容URL, sessionId>
|
||||
*/
|
||||
suspend fun fetchQRCode(): Pair<String, String> = withContext(Dispatchers.IO) {
|
||||
val url = URL(LoginConfig.GET_QR_CODE)
|
||||
val connection = url.openConnection() as HttpURLConnection
|
||||
connection.requestMethod = "GET"
|
||||
connection.connectTimeout = 10000
|
||||
connection.readTimeout = 10000
|
||||
|
||||
val response = connection.inputStream.bufferedReader().readText()
|
||||
val json = JSONObject(response)
|
||||
|
||||
Pair(
|
||||
json.getString("qrCodeUrl"),
|
||||
json.getString("sessionId")
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查扫码状态
|
||||
*/
|
||||
data class ScanStatusResult(
|
||||
val status: String, // waiting, scanned, confirmed, expired
|
||||
val userId: String = "",
|
||||
val nickname: String = "",
|
||||
val avatarUrl: String = ""
|
||||
)
|
||||
|
||||
suspend fun checkScanStatus(sessionId: String): ScanStatusResult = withContext(Dispatchers.IO) {
|
||||
val url = URL("${LoginConfig.CHECK_STATUS}?sessionId=$sessionId")
|
||||
val connection = url.openConnection() as HttpURLConnection
|
||||
connection.requestMethod = "GET"
|
||||
connection.connectTimeout = 10000
|
||||
connection.readTimeout = 10000
|
||||
|
||||
val response = connection.inputStream.bufferedReader().readText()
|
||||
val json = JSONObject(response)
|
||||
|
||||
ScanStatusResult(
|
||||
status = json.getString("status"),
|
||||
userId = json.optString("userId", ""),
|
||||
nickname = json.optString("nickname", ""),
|
||||
avatarUrl = json.optString("avatarUrl", "")
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoginMethodButton(
|
||||
icon: String,
|
||||
label: String,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onClick() }
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(44.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color.White.copy(alpha = 0.1f))
|
||||
.border(1.dp, Color.White.copy(alpha = 0.2f), CircleShape),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(text = icon, fontSize = 20.sp)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = label,
|
||||
fontSize = 11.sp,
|
||||
color = Color.White.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
enum class ScanStatus {
|
||||
WAITING, // 等待扫码
|
||||
SCANNED, // 已扫码,等待确认
|
||||
CONFIRMED // 已确认登录
|
||||
}
|
||||
@ -635,6 +635,9 @@ fun TopBar() {
|
||||
var weatherInfo by remember { mutableStateOf(WeatherService.getSimulatedWeather()) }
|
||||
var isLoading by remember { mutableStateOf(true) }
|
||||
|
||||
// 获取用户信息
|
||||
val userInfo by com.example.smarthome.data.UserManager.userInfo.collectAsState()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
try {
|
||||
weatherInfo = WeatherService.getWeather(context)
|
||||
@ -646,7 +649,72 @@ fun TopBar() {
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(text = tr("dashboard_title"), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, color = Color.White)
|
||||
WeatherCard(weatherInfo = weatherInfo, isLoading = isLoading)
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
WeatherCard(weatherInfo = weatherInfo, isLoading = isLoading)
|
||||
|
||||
// 用户头像
|
||||
UserAvatar(
|
||||
nickname = userInfo.nickname,
|
||||
avatarUrl = userInfo.avatarUrl,
|
||||
onClick = { /* 点击头像可以打开用户菜单 */ }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserAvatar(
|
||||
nickname: String,
|
||||
avatarUrl: String,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val context = androidx.compose.ui.platform.LocalContext.current
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.shadow(8.dp, androidx.compose.foundation.shape.CircleShape)
|
||||
.clip(androidx.compose.foundation.shape.CircleShape)
|
||||
.background(
|
||||
Brush.linearGradient(
|
||||
listOf(Color(0xFFA9F0FF), Color(0xFFB89CFF))
|
||||
)
|
||||
)
|
||||
.border(
|
||||
width = 2.dp,
|
||||
brush = Brush.linearGradient(
|
||||
listOf(Color.White.copy(alpha = 0.5f), Color.White.copy(alpha = 0.2f))
|
||||
),
|
||||
shape = androidx.compose.foundation.shape.CircleShape
|
||||
)
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) { onClick() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (avatarUrl.isNotEmpty()) {
|
||||
// 如果有头像URL,可以用Coil加载网络图片
|
||||
// 这里暂时用首字母代替
|
||||
Text(
|
||||
text = nickname.firstOrNull()?.uppercase() ?: "U",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.White
|
||||
)
|
||||
} else {
|
||||
// 显示昵称首字母
|
||||
Text(
|
||||
text = nickname.firstOrNull()?.uppercase() ?: "U",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1135,7 +1203,14 @@ fun AirConditionerCard(modifier: Modifier = Modifier, roomName: String = "房间
|
||||
"💧" to tr("ac_mode_dry"),
|
||||
"🌀" to tr("ac_mode_fan")
|
||||
)
|
||||
val fanSpeeds = listOf(tr("ac_fan_auto"), tr("ac_fan_low"), tr("ac_fan_medium"), tr("ac_fan_high"))
|
||||
|
||||
// 风速旋转速度(根据档位变化)
|
||||
val fanRotationDuration = when (fanSpeed) {
|
||||
0 -> 2000 // 自动
|
||||
1 -> 3000 // 低速
|
||||
2 -> 1500 // 中速
|
||||
else -> 800 // 高速
|
||||
}
|
||||
|
||||
// 图标动画
|
||||
val infiniteTransition = rememberInfiniteTransition(label = "ac_icon")
|
||||
@ -1162,6 +1237,25 @@ fun AirConditionerCard(modifier: Modifier = Modifier, roomName: String = "房间
|
||||
label = "drop_offset"
|
||||
)
|
||||
|
||||
// 风扇旋转动画
|
||||
val fanRotation by infiniteTransition.animateFloat(
|
||||
initialValue = 0f,
|
||||
targetValue = 360f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(durationMillis = fanRotationDuration, easing = LinearEasing),
|
||||
repeatMode = RepeatMode.Restart
|
||||
),
|
||||
label = "fan_rotation"
|
||||
)
|
||||
|
||||
// 风速标签
|
||||
val fanSpeedLabel = when (fanSpeed) {
|
||||
0 -> tr("ac_fan_auto")
|
||||
1 -> tr("ac_fan_low")
|
||||
2 -> tr("ac_fan_medium")
|
||||
else -> tr("ac_fan_high")
|
||||
}
|
||||
|
||||
// 根据模式决定主题色
|
||||
val accentColor = when (selectedMode) {
|
||||
0 -> Color(0xFF4FC3F7) // 制冷 - 蓝色
|
||||
@ -1257,29 +1351,66 @@ fun AirConditionerCard(modifier: Modifier = Modifier, roomName: String = "房间
|
||||
)
|
||||
}
|
||||
|
||||
// 温度调节按钮
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
// 升温按钮
|
||||
Box(
|
||||
// 风速按钮 + 温度调节按钮
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// 风速按钮(点击循环切换)
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.background(if (isOn) accentColor.copy(alpha = 0.3f) else Color(0xFF3A3A4E))
|
||||
.clickable(enabled = isOn && temp < 32f) { temp += 1f },
|
||||
contentAlignment = Alignment.Center
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(if (isOn) accentColor.copy(alpha = 0.2f) else Color(0xFF3A3A4E))
|
||||
.clickable(enabled = isOn) { fanSpeed = (fanSpeed + 1) % 4 }
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||
) {
|
||||
Text(text = "▲", color = if (isOn) Color.White else Color.Gray, fontSize = 14.sp)
|
||||
// 风扇图标(根据风速显示不同图片,带旋转动画)
|
||||
val fanIconRes = when (fanSpeed) {
|
||||
1 -> R.drawable.fan_low // 低速 - 3叶片
|
||||
2 -> R.drawable.fan_medium // 中速 - 4叶片
|
||||
3 -> R.drawable.fan_high // 高速 - 5叶片
|
||||
else -> R.drawable.fan_medium // 自动 - 默认4叶片
|
||||
}
|
||||
Image(
|
||||
painter = painterResource(id = fanIconRes),
|
||||
contentDescription = "Fan Speed",
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.then(if (isOn) Modifier.rotate(fanRotation) else Modifier)
|
||||
)
|
||||
Text(
|
||||
text = fanSpeedLabel,
|
||||
fontSize = 10.sp,
|
||||
color = if (isOn) Color.White else Color.Gray,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
// 降温按钮
|
||||
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)
|
||||
|
||||
// 温度调节按钮
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
// 升温按钮
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.background(if (isOn) accentColor.copy(alpha = 0.3f) else Color(0xFF3A3A4E))
|
||||
.clickable(enabled = isOn && temp < 32f) { temp += 1f },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(text = "▲", color = if (isOn) Color.White else Color.Gray, fontSize = 14.sp)
|
||||
}
|
||||
// 降温按钮
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.background(if (isOn) accentColor.copy(alpha = 0.3f) else Color(0xFF3A3A4E))
|
||||
.clickable(enabled = isOn && temp > 16f) { temp -= 1f },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(text = "▼", color = if (isOn) Color.White else Color.Gray, fontSize = 14.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1300,13 +1431,14 @@ fun AirConditionerCard(modifier: Modifier = Modifier, roomName: String = "房间
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(44.dp)
|
||||
.heightIn(min = 44.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(
|
||||
if (isSelected && isOn) modeColor.copy(alpha = 0.4f)
|
||||
else Color(0xFF2A2A3E).copy(alpha = 0.6f)
|
||||
)
|
||||
.clickable(enabled = isOn) { selectedMode = index },
|
||||
.clickable(enabled = isOn) { selectedMode = index }
|
||||
.padding(vertical = 4.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
@ -1314,42 +1446,13 @@ fun AirConditionerCard(modifier: Modifier = Modifier, roomName: String = "房间
|
||||
Text(
|
||||
text = name,
|
||||
fontSize = 10.sp,
|
||||
color = if (isSelected && isOn) Color.White else Color(0xFF9AA0A6)
|
||||
color = if (isSelected && isOn) Color.White else Color(0xFF9AA0A6),
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 风速选择
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(text = tr("ac_fan_speed"), 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
562
app/src/main/java/com/example/smarthome/ui/PhoneLoginScreen.kt
Normal file
@ -0,0 +1,562 @@
|
||||
package com.example.smarthome.ui
|
||||
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
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.draw.shadow
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.smarthome.data.UserManager
|
||||
import com.example.smarthome.data.tr
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
/**
|
||||
* 手机号登录界面
|
||||
*/
|
||||
@Composable
|
||||
fun PhoneLoginScreen(
|
||||
onLoginSuccess: () -> Unit,
|
||||
onBack: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
// 登录步骤:0=输入手机号,1=输入验证码
|
||||
var step by remember { mutableStateOf(0) }
|
||||
|
||||
// 手机号
|
||||
var phoneNumber by remember { mutableStateOf("") }
|
||||
|
||||
// 验证码
|
||||
var verifyCode by remember { mutableStateOf("") }
|
||||
|
||||
// 倒计时
|
||||
var countdown by remember { mutableStateOf(0) }
|
||||
|
||||
// 错误信息
|
||||
var errorMessage by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
// 加载状态
|
||||
var isLoading by remember { mutableStateOf(false) }
|
||||
|
||||
// 开发测试:显示验证码
|
||||
var debugCode by remember { mutableStateOf("") }
|
||||
|
||||
// 倒计时
|
||||
LaunchedEffect(countdown) {
|
||||
if (countdown > 0) {
|
||||
delay(1000)
|
||||
countdown--
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(360.dp)
|
||||
) {
|
||||
// 玻璃态卡片
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.shadow(24.dp, RoundedCornerShape(28.dp))
|
||||
.clip(RoundedCornerShape(28.dp))
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
listOf(
|
||||
Color.White.copy(alpha = 0.15f),
|
||||
Color.White.copy(alpha = 0.05f)
|
||||
)
|
||||
)
|
||||
)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
brush = Brush.verticalGradient(
|
||||
listOf(
|
||||
Color.White.copy(alpha = 0.3f),
|
||||
Color.White.copy(alpha = 0.1f)
|
||||
)
|
||||
),
|
||||
shape = RoundedCornerShape(28.dp)
|
||||
)
|
||||
.padding(28.dp)
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
// 返回按钮和标题
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color.White.copy(alpha = 0.1f))
|
||||
.clickable { onBack() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(text = "←", fontSize = 18.sp, color = Color.White)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Text(
|
||||
text = tr("phone_login_title"),
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.White
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Spacer(modifier = Modifier.width(36.dp))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
if (step == 0) {
|
||||
// 步骤1:输入手机号
|
||||
PhoneInputStep(
|
||||
phoneNumber = phoneNumber,
|
||||
onPhoneChange = {
|
||||
phoneNumber = it.filter { c -> c.isDigit() }.take(11)
|
||||
errorMessage = null
|
||||
},
|
||||
errorMessage = errorMessage,
|
||||
isLoading = isLoading,
|
||||
onSendCode = {
|
||||
if (phoneNumber.length != 11) {
|
||||
errorMessage = tr("phone_invalid")
|
||||
} else {
|
||||
isLoading = true
|
||||
// 发送验证码
|
||||
debugCode = UserManager.sendVerifyCode(phoneNumber)
|
||||
isLoading = false
|
||||
countdown = 60
|
||||
step = 1
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
// 步骤2:输入验证码
|
||||
VerifyCodeStep(
|
||||
phoneNumber = phoneNumber,
|
||||
verifyCode = verifyCode,
|
||||
onCodeChange = {
|
||||
verifyCode = it.filter { c -> c.isDigit() }.take(6)
|
||||
errorMessage = null
|
||||
},
|
||||
countdown = countdown,
|
||||
errorMessage = errorMessage,
|
||||
isLoading = isLoading,
|
||||
debugCode = debugCode,
|
||||
onResendCode = {
|
||||
if (countdown == 0) {
|
||||
debugCode = UserManager.sendVerifyCode(phoneNumber)
|
||||
countdown = 60
|
||||
}
|
||||
},
|
||||
onVerify = {
|
||||
if (verifyCode.length != 6) {
|
||||
errorMessage = tr("code_invalid")
|
||||
} else if (!UserManager.verifyCode(phoneNumber, verifyCode)) {
|
||||
errorMessage = tr("code_wrong")
|
||||
} else {
|
||||
isLoading = true
|
||||
// 登录/注册
|
||||
val result = UserManager.loginWithPhone(context, phoneNumber)
|
||||
isLoading = false
|
||||
if (result.isSuccess) {
|
||||
onLoginSuccess()
|
||||
} else {
|
||||
errorMessage = tr("login_failed")
|
||||
}
|
||||
}
|
||||
},
|
||||
onChangePhone = {
|
||||
step = 0
|
||||
verifyCode = ""
|
||||
errorMessage = null
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PhoneInputStep(
|
||||
phoneNumber: String,
|
||||
onPhoneChange: (String) -> Unit,
|
||||
errorMessage: String?,
|
||||
isLoading: Boolean,
|
||||
onSendCode: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = tr("phone_input_hint"),
|
||||
fontSize = 14.sp,
|
||||
color = Color.White.copy(alpha = 0.7f),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// 手机号输入框
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(Color.White.copy(alpha = 0.1f))
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = if (errorMessage != null) Color.Red.copy(alpha = 0.5f)
|
||||
else Color.White.copy(alpha = 0.2f),
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
)
|
||||
.padding(horizontal = 16.dp),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "+86",
|
||||
fontSize = 16.sp,
|
||||
color = Color.White.copy(alpha = 0.6f)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(1.dp)
|
||||
.height(24.dp)
|
||||
.background(Color.White.copy(alpha = 0.2f))
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
BasicTextField(
|
||||
value = phoneNumber,
|
||||
onValueChange = onPhoneChange,
|
||||
textStyle = TextStyle(
|
||||
fontSize = 18.sp,
|
||||
color = Color.White,
|
||||
letterSpacing = 2.sp
|
||||
),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone),
|
||||
singleLine = true,
|
||||
cursorBrush = SolidColor(Color.White),
|
||||
modifier = Modifier.weight(1f),
|
||||
decorationBox = { innerTextField ->
|
||||
Box {
|
||||
if (phoneNumber.isEmpty()) {
|
||||
Text(
|
||||
text = tr("phone_placeholder"),
|
||||
fontSize = 16.sp,
|
||||
color = Color.White.copy(alpha = 0.3f)
|
||||
)
|
||||
}
|
||||
innerTextField()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 错误信息
|
||||
if (errorMessage != null) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = errorMessage,
|
||||
fontSize = 12.sp,
|
||||
color = Color(0xFFFF6B6B)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// 获取验证码按钮
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(52.dp)
|
||||
.clip(RoundedCornerShape(14.dp))
|
||||
.background(
|
||||
if (phoneNumber.length == 11 && !isLoading)
|
||||
Brush.horizontalGradient(
|
||||
listOf(Color(0xFF667EEA), Color(0xFF764BA2))
|
||||
)
|
||||
else
|
||||
Brush.horizontalGradient(
|
||||
listOf(Color(0xFF4A4A5A), Color(0xFF3A3A4A))
|
||||
)
|
||||
)
|
||||
.clickable(enabled = phoneNumber.length == 11 && !isLoading) { onSendCode() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (isLoading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
color = Color.White,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = tr("get_verify_code"),
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// 协议提示
|
||||
Text(
|
||||
text = tr("phone_agreement"),
|
||||
fontSize = 11.sp,
|
||||
color = Color.White.copy(alpha = 0.4f),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VerifyCodeStep(
|
||||
phoneNumber: String,
|
||||
verifyCode: String,
|
||||
onCodeChange: (String) -> Unit,
|
||||
countdown: Int,
|
||||
errorMessage: String?,
|
||||
isLoading: Boolean,
|
||||
debugCode: String,
|
||||
onResendCode: () -> Unit,
|
||||
onVerify: () -> Unit,
|
||||
onChangePhone: () -> Unit
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = tr("code_sent_to"),
|
||||
fontSize = 14.sp,
|
||||
color = Color.White.copy(alpha = 0.7f)
|
||||
)
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text = "+86 ${phoneNumber.take(3)}****${phoneNumber.takeLast(4)}",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color.White
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
Text(
|
||||
text = tr("change_phone"),
|
||||
fontSize = 13.sp,
|
||||
color = Color(0xFF667EEA),
|
||||
modifier = Modifier.clickable { onChangePhone() }
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// 验证码输入框(6位数字)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { focusRequester.requestFocus() }
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
repeat(6) { index ->
|
||||
val char = verifyCode.getOrNull(index)?.toString() ?: ""
|
||||
val isFocused = verifyCode.length == index
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.aspectRatio(1f)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(Color.White.copy(alpha = if (isFocused) 0.15f else 0.1f))
|
||||
.border(
|
||||
width = if (isFocused) 2.dp else 1.dp,
|
||||
color = if (errorMessage != null) Color.Red.copy(alpha = 0.5f)
|
||||
else if (isFocused) Color(0xFF667EEA)
|
||||
else Color.White.copy(alpha = 0.2f),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = char,
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 隐藏的输入框 - 覆盖整个区域
|
||||
BasicTextField(
|
||||
value = verifyCode,
|
||||
onValueChange = onCodeChange,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.focusRequester(focusRequester)
|
||||
.alpha(0f),
|
||||
textStyle = TextStyle(color = Color.Transparent)
|
||||
)
|
||||
}
|
||||
|
||||
// 错误信息
|
||||
if (errorMessage != null) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = errorMessage,
|
||||
fontSize = 12.sp,
|
||||
color = Color(0xFFFF6B6B)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// 开发测试:显示验证码
|
||||
if (debugCode.isNotEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(Color(0xFF2A2A3E))
|
||||
.padding(12.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "🔧",
|
||||
fontSize = 14.sp
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = tr("debug_code_hint"),
|
||||
fontSize = 12.sp,
|
||||
color = Color.White.copy(alpha = 0.6f)
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(
|
||||
text = debugCode,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color(0xFF667EEA),
|
||||
letterSpacing = 4.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// 重新发送
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
if (countdown > 0) {
|
||||
Text(
|
||||
text = "${tr("resend_code_in")} ${countdown}s",
|
||||
fontSize = 13.sp,
|
||||
color = Color.White.copy(alpha = 0.5f)
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = tr("resend_code"),
|
||||
fontSize = 13.sp,
|
||||
color = Color(0xFF667EEA),
|
||||
modifier = Modifier.clickable { onResendCode() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// 登录按钮
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(52.dp)
|
||||
.clip(RoundedCornerShape(14.dp))
|
||||
.background(
|
||||
if (verifyCode.length == 6 && !isLoading)
|
||||
Brush.horizontalGradient(
|
||||
listOf(Color(0xFF667EEA), Color(0xFF764BA2))
|
||||
)
|
||||
else
|
||||
Brush.horizontalGradient(
|
||||
listOf(Color(0xFF4A4A5A), Color(0xFF3A3A4A))
|
||||
)
|
||||
)
|
||||
.clickable(enabled = verifyCode.length == 6 && !isLoading) { onVerify() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (isLoading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
color = Color.White,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = tr("login_register"),
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -146,7 +146,9 @@ fun SettingsContentList() {
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
LogoutButton(onClick = { })
|
||||
LogoutButton(onClick = {
|
||||
com.example.smarthome.data.UserManager.logout(context)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
app/src/main/res/drawable/fan_high.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
app/src/main/res/drawable/fan_low.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
app/src/main/res/drawable/fan_medium.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
@ -4,7 +4,7 @@
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
|
||||
<!-- 渐变背景 -->
|
||||
<!-- 渐变背景 - 匹配图标的绿蓝渐变 -->
|
||||
<path
|
||||
android:pathData="M0,0 h108 v108 h-108 z">
|
||||
<aapt:attr name="android:fillColor" xmlns:aapt="http://schemas.android.com/aapt">
|
||||
@ -14,8 +14,8 @@
|
||||
android:endY="108"
|
||||
android:endX="108"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#FF1A1A2E"/>
|
||||
<item android:offset="1" android:color="#FF16213E"/>
|
||||
<item android:offset="0" android:color="#FFA8E6CF"/>
|
||||
<item android:offset="1" android:color="#FF88C9E8"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
|
||||
BIN
app/src/main/res/drawable/taozhi_icon.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
app/src/main/res/drawable/taozhi_icon_new.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
app/src/main/res/drawable/taozhi_icon_v2.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
7
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground>
|
||||
<inset android:drawable="@drawable/taozhi_icon_v2" android:inset="10%" />
|
||||
</foreground>
|
||||
</adaptive-icon>
|
||||
7
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground>
|
||||
<inset android:drawable="@drawable/taozhi_icon_v2" android:inset="10%" />
|
||||
</foreground>
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 96 KiB |