操作密码

This commit is contained in:
2026-05-22 15:51:13 +08:00
parent d066886b66
commit edbe5629fd
28 changed files with 942 additions and 190 deletions

View File

@@ -0,0 +1,5 @@
package com.example.defenseapplication.bean
data class VerifyOptionPasswordRequest(
val optionPassword: String
)

View File

@@ -8,6 +8,7 @@ import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.example.defenseapplication.R
import com.example.defenseapplication.bean.UserInfoBean
import com.example.defenseapplication.databinding.PersonalFragmentBinding
@@ -203,8 +204,13 @@ class PersonalFragment : Fragment() {
binding.tvPhone.text = it
}
userInfo.avatar?.let {
val requestOptions = RequestOptions()
.placeholder(R.drawable.img) // 加载中显示
.error(R.drawable.img) // 失败时显示
Glide.with(this)
.load(it)
.load(it) // 如果 avatar 为 nullGlide 会直接回调 error
.apply(requestOptions)
.circleCrop()
.into(binding.ivAvatar)
}

View File

@@ -1,6 +1,8 @@
package com.example.defenseapplication.personal
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.blankj.utilcode.util.SPUtils
@@ -36,45 +38,49 @@ class OperationPasswordActivity : AppCompatActivity() {
true -> {
binding.etPassword.isEnabled = true
binding.etConfirmPassword.isEnabled = true
binding.btnConfirmReset.isEnabled = true
Toast.makeText(this, getString(R.string.operation_password_enabled), Toast.LENGTH_SHORT).show()
}
false -> {
binding.etPassword.isEnabled = false
binding.etConfirmPassword.isEnabled = false
binding.btnConfirmReset.isEnabled = false
SPUtils.getInstance().remove("operation_password")
Toast.makeText(this, getString(R.string.operation_password_disabled), Toast.LENGTH_SHORT).show()
}
}
}
binding.btnConfirmReset.setOnClickListener {
val password = binding.etPassword.text.toString()
val confirmPassword = binding.etConfirmPassword.text.toString()
binding.etPassword.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
if (password.isEmpty()) {
Toast.makeText(this, getString(R.string.password_cannot_empty), Toast.LENGTH_SHORT).show()
return@setOnClickListener
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
autoValidatePassword()
}
})
if (password.length != 6) {
Toast.makeText(this, getString(R.string.password_must_be_6_digits), Toast.LENGTH_SHORT).show()
return@setOnClickListener
binding.etConfirmPassword.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
autoValidatePassword()
}
})
}
if (confirmPassword.isEmpty()) {
Toast.makeText(this, getString(R.string.please_confirm_password), Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
private fun autoValidatePassword() {
val password = binding.etPassword.text.toString()
val confirmPassword = binding.etConfirmPassword.text.toString()
if (password != confirmPassword) {
if (password.length == 6 && confirmPassword.length == 6) {
if (password == confirmPassword) {
SPUtils.getInstance().put("operation_password", password)
Toast.makeText(this, getString(R.string.operation_success), Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, getString(R.string.password_mismatch), Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
SPUtils.getInstance().put("operation_password", password)
Toast.makeText(this, getString(R.string.operation_success), Toast.LENGTH_SHORT).show()
}
}
}

View File

@@ -5,10 +5,14 @@ import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.example.defenseapplication.R
import com.example.defenseapplication.bean.VerifyOptionPasswordRequest
import com.example.defenseapplication.databinding.PasswordManagementActivityBinding
import com.example.defenseapplication.view.PasswordInputDialog
import com.blankj.utilcode.util.SPUtils
import com.example.defenseapplication.utils.RetrofitNetwork
import com.gyf.immersionbar.ImmersionBar
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
//密码管理
class PasswordManagementActivity : AppCompatActivity() {
@@ -45,18 +49,26 @@ class PasswordManagementActivity : AppCompatActivity() {
private fun showPasswordDialog() {
passwordDialog = PasswordInputDialog(this).apply {
setOnConfirmListener { inputPassword ->
val savedPassword = SPUtils.getInstance().getString("operation_password", "111111")
if (savedPassword.isEmpty()) {
Toast.makeText(this@PasswordManagementActivity, getString(R.string.operation_password_not_set), Toast.LENGTH_SHORT).show()
dismiss()
return@setOnConfirmListener
}
if (inputPassword == savedPassword) {
startActivity(Intent(this@PasswordManagementActivity, OperationPasswordActivity::class.java))
dismiss()
} else {
showError(getString(R.string.password_error))
clearPassword()
CoroutineScope(Dispatchers.IO).launch {
try {
val response = RetrofitNetwork.getNetworkService().verifyOptionPassword(VerifyOptionPasswordRequest(inputPassword))
withContext(Dispatchers.Main) {
if (response.code == 200) {
startActivity(Intent(this@PasswordManagementActivity, OperationPasswordActivity::class.java))
dismiss()
} else {
Toast.makeText(this@PasswordManagementActivity, response.msg ?: "校验失败", Toast.LENGTH_SHORT).show()
showError(getString(R.string.password_error))
clearPassword()
}
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
Toast.makeText(this@PasswordManagementActivity, "网络请求失败", Toast.LENGTH_SHORT).show()
showError(getString(R.string.password_error))
clearPassword()
}
}
}
}
setOnDismissListener {

View File

@@ -2,12 +2,14 @@ package com.example.defenseapplication.personal
import android.os.Build
import android.os.Bundle
import android.util.Log
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import android.widget.Toast
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.example.defenseapplication.R
import com.example.defenseapplication.bean.UpdateUserInfo
import com.example.defenseapplication.bean.UserInfoBean
@@ -45,27 +47,18 @@ class PersonalProfileActivity : AppCompatActivity() {
}
userInfo?.let { info ->
info.avatar?.let { avatarUrl ->
Glide.with(this)
.load(avatarUrl)
.circleCrop()
.into(binding.ivAvatar)
} ?: run {
Glide.with(this)
.load(R.drawable.img)
.circleCrop()
.into(binding.ivAvatar)
}
Log.d("头像", "头像 URL = ${info.avatar}")
val requestOptions = RequestOptions()
.placeholder(R.drawable.img) // 加载中显示
.error(R.drawable.img) // 失败时显示
binding.tvNickname.text = info.nickName ?: getString(R.string.nickname_default)
binding.tvPhone.text = info.phonenumber ?: "13589898989"
} ?: run {
Glide.with(this)
.load(R.drawable.img)
.load(info.avatar) // 如果 avatar 为 nullGlide 会直接回调 error
.apply(requestOptions)
.circleCrop()
.into(binding.ivAvatar)
binding.tvNickname.text = getString(R.string.nickname_default)
binding.tvPhone.text = "13589898989"
binding.tvNickname.text = info.nickName ?: getString(R.string.nickname_default)
binding.tvPhone.text = info.phonenumber ?: ""
}
binding.itemNickname.setOnClickListener {

View File

@@ -17,6 +17,7 @@ import com.example.defenseapplication.bean.SwitchFamilyRequest
import com.example.defenseapplication.bean.UpdatePassword
import com.example.defenseapplication.bean.UpdateUserInfo
import com.example.defenseapplication.bean.UserInfoBean
import com.example.defenseapplication.bean.VerifyOptionPasswordRequest
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
@@ -156,6 +157,11 @@ interface ApiService {
@POST("/app/retrievePassword")
suspend fun retrievePassword(@Body request: ForgotRequest): GetCodeBean<ForgotRequest>
/**
* 校验操作密码
*/
@POST("/app/verifyOptionPassword")
suspend fun verifyOptionPassword(@Body request: VerifyOptionPasswordRequest): GetCodeBean<Any>

View File

@@ -1,13 +1,18 @@
package com.example.defenseapplication.view
package com.example.defenseapplication.personal
import android.app.Dialog
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.text.Editable
import android.text.InputFilter
import android.text.TextWatcher
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager
import android.view.animation.AnimationUtils
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import com.example.defenseapplication.R
import com.example.defenseapplication.databinding.DialogPasswordInputBinding
@@ -18,6 +23,9 @@ class PasswordInputDialog(context: Context) : Dialog(context) {
private var onConfirmListener: ((String) -> Unit)? = null
private var onDismissListener: (() -> Unit)? = null
// 6个密码输入框数组
private lateinit var passwordEditTexts: Array<EditText>
init {
initView()
}
@@ -26,68 +34,204 @@ class PasswordInputDialog(context: Context) : Dialog(context) {
binding = DialogPasswordInputBinding.inflate(LayoutInflater.from(context))
setContentView(binding.root)
setCanceledOnTouchOutside(true)
window?.apply {
setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
val params = attributes
params.gravity = Gravity.CENTER
params.width = WindowManager.LayoutParams.MATCH_PARENT
// 设置弹窗宽度:屏幕宽度 - 左边距37 - 右边距36
val displayMetrics = context.resources.displayMetrics
val screenWidth = displayMetrics.widthPixels
params.width = screenWidth - 73 // 37 + 36
params.height = WindowManager.LayoutParams.WRAP_CONTENT
params.horizontalMargin = 40f
setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN)
attributes = params
}
// 初始化输入框数组
passwordEditTexts = arrayOf(
binding.etPwd0,
binding.etPwd1,
binding.etPwd2,
binding.etPwd3,
binding.etPwd4,
binding.etPwd5
)
setupPasswordInput()
binding.btnCancel.setOnClickListener {
binding.ivClose.setOnClickListener {
dismiss()
}
binding.btnConfirm.setOnClickListener {
val password = getPassword()
if (password.length == 6) {
onConfirmListener?.invoke(password)
dismiss()
}
// 自动弹出键盘
binding.root.post {
passwordEditTexts[0].requestFocus()
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(passwordEditTexts[0], InputMethodManager.SHOW_IMPLICIT)
}
}
private fun setupPasswordInput() {
val editTexts = listOf(
binding.etPassword1,
binding.etPassword2,
binding.etPassword3,
binding.etPassword4,
binding.etPassword5,
binding.etPassword6
)
// 为每个输入框设置监听器
for (i in passwordEditTexts.indices) {
val currentEt = passwordEditTexts[i]
editTexts.forEachIndexed { index, editText ->
editText.setOnKeyListener { _, _, _ ->
if (editText.text.isNotEmpty()) {
if (index < editTexts.size - 1) {
editTexts[index + 1].requestFocus()
// 设置只能输入数字
currentEt.filters = arrayOf(InputFilter.LengthFilter(1))
currentEt.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
// 用户手动输入或删除时清除错误
if (count > 0 || before > 0) {
clearError()
}
}
override fun afterTextChanged(s: Editable?) {
val text = s?.toString() ?: ""
if (text.isNotEmpty()) {
// 输入了内容,自动跳转到下一个输入框
if (i < passwordEditTexts.size - 1) {
// 检查是否按顺序输入:当前位置之前的所有输入框都必须有内容
if (isInputInOrder(i)) {
passwordEditTexts[i + 1].requestFocus()
} else {
// 不是按顺序输入,清空当前输入
currentEt.text?.clear()
}
}
// 检查是否已输入完6位密码
checkPasswordComplete()
} else {
editText.clearFocus()
// 删除了内容,自动跳转到上一个有内容的输入框
if (currentEt.hasFocus()) {
// 从当前位置往前查找第一个有内容的输入框
val previousFilledIndex = findPreviousFilledIndex(i)
if (previousFilledIndex >= 0) {
passwordEditTexts[previousFilledIndex].requestFocus()
// 选中内容,方便继续删除
passwordEditTexts[previousFilledIndex].selectAll()
} else if (i > 0) {
// 如果前面都为空,保持在第一个输入框
passwordEditTexts[0].requestFocus()
}
}
}
}
})
// 设置键盘监听器,处理当前框为空时按删除键的情况
currentEt.setOnKeyListener { _, keyCode, event ->
// 只处理删除键按下事件
if (keyCode == android.view.KeyEvent.KEYCODE_DEL && event.action == android.view.KeyEvent.ACTION_DOWN) {
val currentText = currentEt.text.toString()
// 如果当前输入框为空,跳转到上一个有内容的输入框并删除
if (currentText.isEmpty() && i > 0) {
val previousFilledIndex = findPreviousFilledIndex(i)
if (previousFilledIndex >= 0) {
// 删除上一个输入框的内容
passwordEditTexts[previousFilledIndex].text?.clear()
// 继续向前查找下一个有内容的输入框
val newPreviousIndex = findPreviousFilledIndex(previousFilledIndex)
if (newPreviousIndex >= 0) {
passwordEditTexts[newPreviousIndex].requestFocus()
passwordEditTexts[newPreviousIndex].selectAll()
} else {
// 如果前面都为空,定位到第一个输入框
passwordEditTexts[0].requestFocus()
}
return@setOnKeyListener true
}
}
}
false
}
editText.setOnFocusChangeListener { v, hasFocus ->
if (hasFocus) {
(v as EditText).setSelection((v.text?.length ?: 0))
// 设置触摸监听器,确保按顺序输入
currentEt.setOnTouchListener { _, _ ->
// 如果不是第一个输入框,检查前面的输入框是否都有内容
if (i > 0) {
val previousText = passwordEditTexts[i - 1].text.toString()
if (previousText.isEmpty()) {
// 前面的输入框为空,自动跳转到第一个空的输入框
val firstEmptyIndex = findFirstEmptyIndex()
if (firstEmptyIndex < i) {
passwordEditTexts[firstEmptyIndex].requestFocus()
return@setOnTouchListener true
}
}
}
false
}
}
}
/**
* 检查是否按顺序输入
*/
private fun isInputInOrder(currentIndex: Int): Boolean {
for (i in 0 until currentIndex) {
if (passwordEditTexts[i].text.toString().isEmpty()) {
return false
}
}
return true
}
/**
* 查找第一个空的输入框索引
*/
private fun findFirstEmptyIndex(): Int {
for (i in passwordEditTexts.indices) {
if (passwordEditTexts[i].text.toString().isEmpty()) {
return i
}
}
return passwordEditTexts.size
}
/**
* 从指定位置往前查找第一个有内容的输入框索引
* @param startIndex 开始查找的位置(不包含)
* @return 第一个有内容的输入框索引,未找到返回 -1
*/
private fun findPreviousFilledIndex(startIndex: Int): Int {
for (i in startIndex - 1 downTo 0) {
if (passwordEditTexts[i].text.toString().isNotEmpty()) {
return i
}
}
return -1
}
/**
* 检查密码是否输入完成
*/
private fun checkPasswordComplete() {
val password = getPassword()
if (password.length == 6) {
autoVerifyPassword()
}
}
private fun autoVerifyPassword() {
val password = getPassword()
if (password.length == 6) {
onConfirmListener?.invoke(password)
}
}
private fun getPassword(): String {
return binding.etPassword1.text.toString() +
binding.etPassword2.text.toString() +
binding.etPassword3.text.toString() +
binding.etPassword4.text.toString() +
binding.etPassword5.text.toString() +
binding.etPassword6.text.toString()
val sb = StringBuilder()
for (et in passwordEditTexts) {
sb.append(et.text.toString())
}
return sb.toString()
}
fun setOnConfirmListener(listener: (String) -> Unit): PasswordInputDialog {
@@ -103,6 +247,15 @@ class PasswordInputDialog(context: Context) : Dialog(context) {
fun showError(message: String) {
binding.tvError.text = message
binding.tvError.visibility = View.VISIBLE
shakePasswordInput()
}
private fun shakePasswordInput() {
val shakeAnimation = AnimationUtils.loadAnimation(context, R.anim.shake)
// 对每个输入框应用抖动动画
for (et in passwordEditTexts) {
et.startAnimation(shakeAnimation)
}
}
fun clearError() {
@@ -111,16 +264,16 @@ class PasswordInputDialog(context: Context) : Dialog(context) {
}
fun clearPassword() {
binding.etPassword1.text?.clear()
binding.etPassword2.text?.clear()
binding.etPassword3.text?.clear()
binding.etPassword4.text?.clear()
binding.etPassword5.text?.clear()
binding.etPassword6.text?.clear()
binding.etPassword1.requestFocus()
for (et in passwordEditTexts) {
et.text?.clear()
}
passwordEditTexts[0].requestFocus()
}
override fun dismiss() {
// 隐藏键盘
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(passwordEditTexts[0].windowToken, 0)
super.dismiss()
onDismissListener?.invoke()
}