登录注册

This commit is contained in:
2026-05-15 10:44:52 +08:00
parent 193955d983
commit da935a122f
24 changed files with 763 additions and 99 deletions

View File

@@ -0,0 +1,10 @@
package com.example.defenseapplication.bean
/**
* 统一返回数据基类(根据你的后端实际字段调整)
*/
data class BaseInfo(
val message: String? = null,
val code: Int = 0,
val data: Any? = null
)

View File

@@ -0,0 +1,10 @@
package com.example.defenseapplication.bean
data class DeviceBean(
val id: String,
val deviceName: String,
val status: String,
val deviceType: String,
val parentId: String,
val createTime: String?
)

View File

@@ -0,0 +1,15 @@
package com.example.defenseapplication.bean
data class FamilyBean(
val id: String,
val parentId: String,
val parentName: String,
val userId: String,
val nickName: String?,
val idCard: String?,
val phonenumber: String?,
val status: String,
val role: String,
val createTime: String?,
val inviteCode: String?
)

View File

@@ -1,12 +1,5 @@
package bean
//登录-bean
data class Login<T>(
val message: String,
val result: T,
val status: String
)
data class LoginBean(
val headPic: String,
val nickName: String,

View File

@@ -1,12 +1,12 @@
package com.example.defenseapplication.home
import android.content.Intent
import android.content.res.Resources
import android.graphics.Color
import android.graphics.Rect
import android.graphics.drawable.GradientDrawable
import android.os.Bundle
import android.util.TypedValue
import android.widget.Toast
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
@@ -18,10 +18,17 @@ import com.example.defenseapplication.R
import com.example.defenseapplication.adapter.DeviceAdapter
import com.example.defenseapplication.adapter.DeviceUi
import com.example.defenseapplication.adapter.FamilyAdapter
import com.example.defenseapplication.bean.DeviceBean
import com.example.defenseapplication.bean.FamilyBean
import com.example.defenseapplication.databinding.HomeMenuFragmentBinding
import com.example.defenseapplication.utils.RetrofitNetwork
import com.example.defenseapplication.view.DefenseDialogs
import com.example.defenseapplication.view.showDefenseDialog
import com.gyf.immersionbar.ImmersionBar
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
//主页碎片
class HomeMenuFragment : Fragment() {
// ViewBinding
@@ -33,30 +40,9 @@ class HomeMenuFragment : Fragment() {
private var isFamilyExpanded = false
private lateinit var currentFamily: String
private val familyList: List<String> by lazy {
listOf(
getString(R.string.my_family_home),
getString(R.string.workshop),
getString(R.string.parents_home)
)
}
private val familyDeviceMap: Map<String, List<DeviceUi>> by lazy {
mapOf(
getString(R.string.my_family_home) to listOf(
DeviceUi(getString(R.string.smart_hanging_defense), true),
DeviceUi(getString(R.string.smart_defense_device), false),
DeviceUi(getString(R.string.smart_rail_defense), false)
),
getString(R.string.workshop) to listOf(
DeviceUi(getString(R.string.corridor_defense), true),
DeviceUi(getString(R.string.door_defense), true)
),
getString(R.string.parents_home) to listOf(
DeviceUi(getString(R.string.living_room_defense), false),
DeviceUi(getString(R.string.balcony_defense), true)
)
)
}
private var familyList: List<String> = emptyList()
private var familyBeanList: List<FamilyBean> = emptyList()
private var deviceBeanList: List<DeviceBean> = emptyList()
override fun onCreateView(
inflater: LayoutInflater,
@@ -93,13 +79,13 @@ class HomeMenuFragment : Fragment() {
val intent = Intent(requireContext(), ScanActivity::class.java)
startActivity(intent)
}
fetchFamilyList()
}
private fun initView() {
currentFamily = familyList.first()
binding.rvFamilyList.layoutManager = LinearLayoutManager(requireContext())
binding.rvFamilyList.adapter = familyAdapter
familyAdapter.submitList(familyList, currentFamily)
binding.rvDeviceList.layoutManager = GridLayoutManager(requireContext(), 2)
binding.rvDeviceList.adapter = deviceAdapter
@@ -108,9 +94,7 @@ class HomeMenuFragment : Fragment() {
GridHorizontalSpaceItemDecoration(horizontalSpace = dpToPx(8))
)
}
updateDeviceList()
binding.tvCurrentFamily.text = currentFamily
binding.familyHeaderLayout.setOnClickListener {
toggleFamilyList()
}
@@ -121,6 +105,52 @@ class HomeMenuFragment : Fragment() {
}
}
private fun fetchFamilyList() {
CoroutineScope(Dispatchers.IO).launch {
try {
val response = RetrofitNetwork.getNetworkService().getFamilyList()
withContext(Dispatchers.Main) {
if (response.code == 200 && response.data != null) {
familyBeanList = response.data
familyList = response.data.map { it.parentName }
if (familyList.isNotEmpty()) {
currentFamily = familyList.first()
binding.tvCurrentFamily.text = currentFamily
familyAdapter.submitList(familyList, currentFamily)
}
} else {
Toast.makeText(requireContext(), response.msg, Toast.LENGTH_SHORT).show()
}
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
Toast.makeText(requireContext(), R.string.network_error, Toast.LENGTH_SHORT).show()
}
}
fetchDeviceList()
}
}
private fun fetchDeviceList() {
// CoroutineScope(Dispatchers.IO).launch {
// try {
// val response = RetrofitNetwork.getNetworkService().getDeviceList()
// withContext(Dispatchers.Main) {
// if (response.code == 200 && response.data != null) {
// deviceBeanList = response.data
// updateDeviceList()
// } else {
// Toast.makeText(requireContext(), response.msg, Toast.LENGTH_SHORT).show()
// }
// }
// } catch (e: Exception) {
// withContext(Dispatchers.Main) {
// Toast.makeText(requireContext(), R.string.network_error, Toast.LENGTH_SHORT).show()
// }
// }
// }
}
private fun toggleFamilyList() {
isFamilyExpanded = !isFamilyExpanded
val visibility = if (isFamilyExpanded) View.VISIBLE else View.GONE
@@ -143,7 +173,24 @@ class HomeMenuFragment : Fragment() {
}
private fun updateDeviceList() {
deviceAdapter.submitList(familyDeviceMap[currentFamily].orEmpty())
val currentFamilyBean = familyBeanList.find { it.parentName == currentFamily }
val filteredDevices = if (currentFamilyBean != null) {
deviceBeanList.filter { it.parentId == currentFamilyBean.id }
} else {
deviceBeanList
}
val deviceUiList = filteredDevices.map {
DeviceUi(it.deviceName, it.status == "online")
}
if (deviceUiList.isEmpty()) {
deviceAdapter.submitList(listOf(
DeviceUi(getString(R.string.no_device), false)
))
} else {
deviceAdapter.submitList(deviceUiList)
}
}
private fun onDeviceClicked(device: DeviceUi) {

View File

@@ -9,7 +9,7 @@ import androidx.appcompat.app.AppCompatActivity
import bean.ForgotRequest
import com.example.defenseapplication.R
import com.example.defenseapplication.databinding.ForgetPasswordActivityBinding
import com.example.defenseapplication.utils.HttpUtils
import com.example.defenseapplication.utils.RetrofitNetwork
import com.google.firebase.crashlytics.buildtools.reloc.org.apache.http.util.TextUtils
import com.gyf.immersionbar.ImmersionBar
import kotlinx.coroutines.CoroutineScope
@@ -79,7 +79,7 @@ class ForgetPasswordActivity : AppCompatActivity() {
CoroutineScope(Dispatchers.IO).launch {
try {
val response = HttpUtils.api.getCode(phoneNumber)
val response = RetrofitNetwork.getNetworkService().getCode(phoneNumber)
withContext(Dispatchers.Main) {
if (response.code == 200) {
Toast.makeText(this@ForgetPasswordActivity, "验证码已发送", Toast.LENGTH_SHORT).show()
@@ -173,7 +173,7 @@ class ForgetPasswordActivity : AppCompatActivity() {
smsCode = smsCode,
password = newPassword
)
val response = HttpUtils.api.forgotPassword(forgotRequest)
val response = RetrofitNetwork.getNetworkService().forgotPassword(forgotRequest)
withContext(Dispatchers.Main) {
binding.btnNext.isEnabled = true
binding.btnNext.text = "确定"

View File

@@ -14,7 +14,7 @@ import bean.LoginRequest
import com.example.defenseapplication.R
import com.example.defenseapplication.databinding.ActivityLoginBinding
import com.example.defenseapplication.home.HomeActivity
import com.example.defenseapplication.utils.HttpUtils
import com.example.defenseapplication.utils.RetrofitNetwork
import com.example.defenseapplication.utils.UserinfoUtil
import com.gyf.immersionbar.ImmersionBar
import kotlinx.coroutines.CoroutineScope
@@ -200,7 +200,7 @@ class LoginActivity : AppCompatActivity() {
private fun sendVerificationCode(phone: String) {
CoroutineScope(Dispatchers.IO).launch {
try {
val response = HttpUtils.api.getCode(phone)
val response = RetrofitNetwork.getNetworkService().getCode(phone)
withContext(Dispatchers.Main) {
if (response.code == 200) {
Toast.makeText(this@LoginActivity, getString(R.string.code_sent_to, phone), Toast.LENGTH_SHORT).show()
@@ -227,7 +227,7 @@ class LoginActivity : AppCompatActivity() {
smsCode = smsCode
)
android.util.Log.d("LoginActivity", "短信登录请求: $loginRequest")
val response = HttpUtils.api.login(loginRequest)
val response = RetrofitNetwork.getNetworkService().login(loginRequest)
android.util.Log.d("LoginActivity", "短信登录响应: code=${response.code}, msg=${response.msg}")
withContext(Dispatchers.Main) {
if (response.code == 200 && response.data != null) {
@@ -260,7 +260,7 @@ class LoginActivity : AppCompatActivity() {
password = password
)
android.util.Log.d("LoginActivity", "密码登录请求: $loginRequest")
val response = HttpUtils.api.login(loginRequest)
val response = RetrofitNetwork.getNetworkService().login(loginRequest)
android.util.Log.d("LoginActivity", "密码登录响应: code=${response.code}, msg=${response.msg}")
withContext(Dispatchers.Main) {
if (response.code == 200 && response.data != null) {

View File

@@ -10,7 +10,7 @@ import androidx.appcompat.app.AppCompatActivity
import bean.RegisterRequest
import com.example.defenseapplication.R
import com.example.defenseapplication.databinding.RegisterActivityBinding
import com.example.defenseapplication.utils.HttpUtils
import com.example.defenseapplication.utils.RetrofitNetwork
import com.gyf.immersionbar.ImmersionBar
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -77,7 +77,7 @@ class RegisterActivity : AppCompatActivity() {
CoroutineScope(Dispatchers.IO).launch {
try {
val response = HttpUtils.api.getCode(userName)
val response = RetrofitNetwork.getNetworkService().getCode(userName)
withContext(Dispatchers.Main) {
if (response.code == 200) {
Toast.makeText(this@RegisterActivity, "验证码已发送", Toast.LENGTH_SHORT).show()
@@ -171,7 +171,7 @@ class RegisterActivity : AppCompatActivity() {
inviteCode = if (inviteCode.isEmpty()) null else inviteCode,
faceAddr = "https://tiebapic.baidu.com/forum/pic/item/2924ab18972bd407021da3e33d899e510eb309d1.jpg?tbpicau=2026-04-22-05_44105254d24a81214177d11b72513421"
)
val response = HttpUtils.api.register(registerRequest)
val response = RetrofitNetwork.getNetworkService().register(registerRequest)
withContext(Dispatchers.Main) {
isLoading = false
binding.btnNext.isEnabled = true

View File

@@ -0,0 +1,190 @@
package com.example.defenseapplication.utils
import android.annotation.SuppressLint
import android.util.Log
import com.example.defenseapplication.bean.BaseInfo
import com.google.gson.Gson
import com.google.gson.JsonParser
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Protocol
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import okhttp3.logging.HttpLoggingInterceptor
import java.nio.charset.Charset
/**
* 网络配置与拦截器
*/
object ApiConfig {
private const val TAG = "ApiConfig"
private val gson = Gson()
@Volatile
private var authToken: String = ""
fun updateAuthToken(token: String) {
authToken = token
}
fun hasAuthToken(): Boolean {
return authToken.isNotBlank()
}
/**
* 获取日志拦截器
* 完善:针对上传文件等二进制流时,降低日志级别为 HEADERS避免输出大量字节数据
*/
fun getLoggingInterceptor(): Interceptor {
val logger = HttpLoggingInterceptor.Logger { message ->
Log.d(TAG, message)
}
return Interceptor { chain ->
val request = chain.request()
if (/*!BuildConfig.DEBUG*/false) {
return@Interceptor chain.proceed(request)
}
// 1. 判断请求是否为二进制或多部分表单上传
val requestBody = request.body
val requestContentType = requestBody?.contentType()?.toString()?.lowercase() ?: ""
val isBinaryRequest = requestContentType.contains("multipart") ||
requestContentType.contains("octet-stream") ||
requestContentType.contains("image") ||
requestContentType.contains("video") ||
requestContentType.contains("audio")
// 2. 动态设置日志级别
val loggingInterceptor = HttpLoggingInterceptor(logger).apply {
level = if (isBinaryRequest) {
// 二进制上传只打印 Header防止 Body 字节码干扰
HttpLoggingInterceptor.Level.HEADERS
} else {
HttpLoggingInterceptor.Level.BODY
}
}
loggingInterceptor.intercept(chain)
}
}
/**
* 统一拦截器处理异常、Token失效、数据格式化
*/
@SuppressLint("CheckResult")
fun getUnifiedInterceptor() = Interceptor { chain ->
val request = chain.request()
// --- 1. 发起请求并捕获底层网络异常 (无网络、超时等) ---
val response = try {
chain.proceed(request)
} catch (e: Exception) {
Log.e(TAG, "Network Error: ", e)
return@Interceptor buildFakeErrorResponse(request, getErrorMessage(e))
}
// --- 2. 检查响应是否为大文件或二进制流 (直接放行,避免 OOM 和日志污染) ---
val responseBody = response.body ?: return@Interceptor response
val contentType = responseBody.contentType()?.toString()?.lowercase() ?: ""
if (contentType.contains("octet-stream") || contentType.contains("video") ||
contentType.contains("audio") || contentType.contains("image") ||
responseBody.contentLength() > 1024 * 1024
) {
return@Interceptor response
}
// --- 3. 读取 JSON 数据进行统一处理 ---
try {
val source = responseBody.source()
source.request(Long.MAX_VALUE)
val responseString = source.buffer.clone().readString(Charset.defaultCharset())
if (!responseString.trim().startsWith("{")) {
if (response.code == 500) {
return@Interceptor buildFakeErrorResponse(request, "服务器内部错误")
}
return@Interceptor response
}
val jsonElement = JsonParser().parse(responseString)
val jsonObject = jsonElement.asJsonObject
val httpCode = response.code
val businessCode = if (jsonObject.has("code")) jsonObject.get("code").asInt else 0
if (httpCode == 401 || httpCode == 424 || businessCode == 401) {
clearUserLoginInfo()
val msg = if (jsonObject.has("msg")) jsonObject.get("msg").asString else "登录已过期,请重新登录"
return@Interceptor buildFakeErrorResponse(request, msg, code = if (businessCode != 0) businessCode else httpCode)
}
if (httpCode == 500) {
return@Interceptor buildFakeErrorResponse(request, "服务器内部错误(500)")
}
return@Interceptor response
} catch (e: Exception) {
Log.e(TAG, "JSON Parse Error: ", e)
return@Interceptor response
}
}
/**
* 构建一个伪装成 HTTP 200 的统一错误响应体
*/
private fun buildFakeErrorResponse(request: Request, msg: String, code: Int = 1): Response {
val baseInfo = BaseInfo(
message = msg,
code = code,
data = null,
)
val json = gson.toJson(baseInfo)
val responseBody = json.toResponseBody("application/json; charset=utf-8".toMediaTypeOrNull())
return Response.Builder()
.request(request)
.protocol(Protocol.HTTP_1_1)
.code(200)
.message("OK")
.body(responseBody)
.build()
}
private fun getErrorMessage(e: Exception): String {
return "网络请求失败,请检查网络设置"
}
// 统一请求头拦截器
fun createHeadersInterceptor() = Interceptor { chain ->
val requestBuilder = chain.request().newBuilder()
val token = authToken
if (token.isNotBlank()) {
requestBuilder.header("Authorization", buildAuthorizationValue(token))
}
requestBuilder
// .header("Client-Toc", "Y")
// .header("Accept-Language", LanguageHelper.getLanguageServiceStr(BrightApp.getInstance()))
// .header("model", "Android")
// .header("X-App-Version", AppUtils.getAppVersionName())
chain.proceed(requestBuilder.build())
}
private fun buildAuthorizationValue(token: String): String {
return if (token.startsWith("Bearer ")) token else "Bearer $token"
}
private fun clearUserLoginInfo() {
try {
authToken = ""
} catch (e: Exception) {
e.printStackTrace()
}
}
}

View File

@@ -1,6 +1,8 @@
package com.example.defenseapplication.utils
import bean.ForgotRequest
import com.example.defenseapplication.bean.DeviceBean
import com.example.defenseapplication.bean.FamilyBean
import com.example.defenseapplication.bean.GetCodeBean
import bean.LoginBean
import bean.LoginRequest
@@ -34,5 +36,17 @@ interface ApiService {
*/
@PUT("/app/forgot")
suspend fun forgotPassword(@Body forgotRequest: ForgotRequest): GetCodeBean<Any>
/**
* 获取我所在家庭列表
*/
@GET("/app/selectFamily")
suspend fun getFamilyList(): GetCodeBean<List<FamilyBean>>
/**
* 切换家庭
*/
//@POST("/app/switchFamily")
//suspend fun switchFamily(@Body request: com.example.defenseapplication.bean.SwitchFamilyRequest): GetCodeBean<Any>
}

View File

@@ -1,8 +0,0 @@
package com.example.defenseapplication.utils
data class Post(
val userId: Int,
val id: Int,
val title: String,
val body: String
)

View File

@@ -1,43 +0,0 @@
package com.example.defenseapplication.utils
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
//网络工具类
object HttpUtils {
val baseURL = "https://stem-boozy-margarine.ngrok-free.dev"
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
.writeTimeout(3, TimeUnit.MINUTES)
.readTimeout(3, TimeUnit.MINUTES)
.addInterceptor(object : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val s = UserinfoUtil.getUserInfo()
val aa = chain.request().newBuilder()
.addHeader("token", s?.token ?: "")
.addHeader("userId", "${s?.userId ?: 0}")
.build()
return chain.proceed(aa)
}
})
.build()
val retrofit = Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClient)
.baseUrl(baseURL)
.build()
val api = retrofit.create(ApiService::class.java)
}

View File

@@ -0,0 +1,18 @@
package com.smart.data.repository
import com.example.defenseapplication.utils.ApiService
import com.example.defenseapplication.utils.RetrofitNetwork
class MainRepository(val apiService: ApiService = RetrofitNetwork.getNetworkService()) {
companion object {
fun getInstance(): Array<Any> {
return arrayOf(MainRepository(RetrofitNetwork.getNetworkService()))
}
fun getInstance(vararg any: Any): Array<Any> {
return arrayOf(MainRepository(RetrofitNetwork.getNetworkService()), *any)
}
}
}

View File

@@ -0,0 +1,75 @@
package com.example.defenseapplication.utils
import com.google.gson.Gson
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
/**
* 统一网络请求封装
*/
object RetrofitNetwork {
private const val DEFAULT_TIMEOUT = 30L
const val BASE_URL = "https://stem-boozy-margarine.ngrok-free.dev"
private var retrofit: Retrofit? = null
private val gson = Gson()
/**
* 动态修改 BaseUrl
*/
fun setBaseUrl(baseUrl: String) {
retrofit = createRetrofit(baseUrl)
}
fun getNetworkService(): ApiService = getService(ApiService::class.java)
fun <T> getService(service: Class<T>, baseUrl: String? = null): T {
if (retrofit == null || baseUrl != null) {
retrofit = createRetrofit(baseUrl ?: BASE_URL)
}
return retrofit!!.create(service)
}
/**
* 专门用于下载的 Service (无日志,防止内存溢出)
*/
fun createRetrofitDownload(): ApiService {
return Retrofit.Builder()
.baseUrl(normalizeBaseUrl(BASE_URL))
.client(createOkHttpClient(enableLogging = false))
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
.create(ApiService::class.java)
}
private fun createRetrofit(baseUrl: String): Retrofit {
return Retrofit.Builder()
.baseUrl(normalizeBaseUrl(baseUrl))
.client(createOkHttpClient(enableLogging = true))
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
}
private fun normalizeBaseUrl(baseUrl: String): String {
val trimmed = baseUrl.trim()
return if (trimmed.endsWith("/")) trimmed else "$trimmed/"
}
private fun createOkHttpClient(enableLogging: Boolean): OkHttpClient {
val builder = OkHttpClient.Builder()
.connectTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS)
.readTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS)
.writeTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS)
.addInterceptor(ApiConfig.createHeadersInterceptor())
.addInterceptor(ApiConfig.getUnifiedInterceptor()) // 使用 ApiConfig 中的统一拦截器
if (enableLogging) {
builder.addInterceptor(ApiConfig.getLoggingInterceptor()) // 使用 ApiConfig 中的日志拦截器
}
return builder.build()
}
}

View File

@@ -20,4 +20,4 @@
<color name="green_light">#E8F7F7</color>
<color name="gray_divider">#E5E5E5</color>
</resources>
</resources>

View File

@@ -74,6 +74,7 @@
<string name="bind_device">Bind Device</string>
<string name="unbind_device">Unbind Device</string>
<string name="device_not_found">No devices found</string>
<string name="no_device">No device</string>
<!-- Add Device -->
<string name="add_device_title">Add Device</string>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-files-path
name="my_images"
path="Pictures" />
</paths>