修改头像

This commit is contained in:
2026-05-26 11:50:01 +08:00
parent 5b7d20d38c
commit c073c88e10
7 changed files with 145 additions and 144 deletions

View File

@@ -0,0 +1,13 @@
package com.example.defenseapplication.bean
data class UploadResponse(
val code: Int,
val msg: String,
val data: UploadData?
)
data class UploadData(
val url: String,
val fileName: String?,
val ossId: String?
)

View File

@@ -4,8 +4,6 @@ import android.Manifest
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@@ -26,16 +24,19 @@ import com.example.defenseapplication.R
import com.example.defenseapplication.bean.UpdateUserInfo import com.example.defenseapplication.bean.UpdateUserInfo
import com.example.defenseapplication.bean.UserInfoBean import com.example.defenseapplication.bean.UserInfoBean
import com.example.defenseapplication.databinding.PersonalProfileActivityBinding import com.example.defenseapplication.databinding.PersonalProfileActivityBinding
import com.example.defenseapplication.utils.OssUtil
import com.example.defenseapplication.utils.RetrofitNetwork import com.example.defenseapplication.utils.RetrofitNetwork
import com.example.defenseapplication.view.AvatarSelectDialog import com.example.defenseapplication.view.AvatarSelectDialog
import com.example.defenseapplication.view.CustomDialogFragmentText import com.example.defenseapplication.view.CustomDialogFragmentText
import com.example.defenseapplication.view.LoadingDialog
import com.example.defenseapplication.view.PhoneModifyDialogFragment import com.example.defenseapplication.view.PhoneModifyDialogFragment
import com.gyf.immersionbar.ImmersionBar import com.gyf.immersionbar.ImmersionBar
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
@@ -51,6 +52,9 @@ class PersonalProfileActivity : AppCompatActivity() {
// 拍照返回的文件 // 拍照返回的文件
private var photoFile: File? = null private var photoFile: File? = null
// 加载弹窗
private lateinit var loadingDialog: LoadingDialog
// 请求权限回调 // 请求权限回调
private val requestPermissionsLauncher = registerForActivityResult( private val requestPermissionsLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions() ActivityResultContracts.RequestMultiplePermissions()
@@ -116,6 +120,8 @@ class PersonalProfileActivity : AppCompatActivity() {
.statusBarDarkFont(true) .statusBarDarkFont(true)
.titleBar(binding.topBar) .titleBar(binding.topBar)
.init() .init()
// 初始化加载弹窗
loadingDialog = LoadingDialog(this)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
userInfo = intent.getParcelableExtra("userInfo", UserInfoBean::class.java) userInfo = intent.getParcelableExtra("userInfo", UserInfoBean::class.java)
} else { } else {
@@ -327,7 +333,6 @@ class PersonalProfileActivity : AppCompatActivity() {
".jpg", ".jpg",
storageDir storageDir
).apply { ).apply {
// 保存文件路径以便后续使用
Log.d("PersonalProfile", "创建图片文件: $absolutePath") Log.d("PersonalProfile", "创建图片文件: $absolutePath")
} }
} }
@@ -370,18 +375,34 @@ class PersonalProfileActivity : AppCompatActivity() {
} }
/** /**
* 上传头像到 OSS 并更新个人信息 * 上传头像到服务器并更新个人信息
*/ */
private fun uploadAvatar(filePath: String) { private fun uploadAvatar(filePath: String) {
Toast.makeText(this, getString(R.string.uploading), Toast.LENGTH_SHORT).show() // 显示加载弹窗
loadingDialog.show()
OssUtil.uploadFileAsync(this, filePath, "avatar") { ossUrl -> CoroutineScope(Dispatchers.IO).launch {
runOnUiThread { try {
if (ossUrl != null) { val file = File(filePath)
Log.d("PersonalProfile", "OSS 上传成功URL: $ossUrl") val requestBody = file.asRequestBody("image/jpeg".toMediaTypeOrNull())
updateAvatar(ossUrl) val part = MultipartBody.Part.createFormData("file", file.name, requestBody)
} else {
Toast.makeText(this@PersonalProfileActivity, getString(R.string.upload_failed), Toast.LENGTH_SHORT).show() val uploadResponse = RetrofitNetwork.getNetworkService().uploadFile(part)
withContext(Dispatchers.Main) {
if (uploadResponse.code == 200 && uploadResponse.data?.url != null) {
Log.d("PersonalProfile", "文件上传成功URL: ${uploadResponse.data?.url}")
updateAvatar(uploadResponse.data!!.url)
} else {
loadingDialog.dismiss()
Toast.makeText(this@PersonalProfileActivity, getString(R.string.upload_failed), Toast.LENGTH_SHORT).show()
}
}
} catch (e: Exception) {
e.printStackTrace()
withContext(Dispatchers.Main) {
loadingDialog.dismiss()
Toast.makeText(this@PersonalProfileActivity, getString(R.string.network_error), Toast.LENGTH_SHORT).show()
} }
} }
} }
@@ -399,8 +420,8 @@ class PersonalProfileActivity : AppCompatActivity() {
) )
val response = RetrofitNetwork.getNetworkService().updateUserInfo(request) val response = RetrofitNetwork.getNetworkService().updateUserInfo(request)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
loadingDialog.dismiss()
if (response.code == 200) { if (response.code == 200) {
// 更新本地头像显示
Glide.with(this@PersonalProfileActivity) Glide.with(this@PersonalProfileActivity)
.load(avatarUrl) .load(avatarUrl)
.apply(RequestOptions() .apply(RequestOptions()
@@ -417,6 +438,7 @@ class PersonalProfileActivity : AppCompatActivity() {
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
loadingDialog.dismiss()
Toast.makeText(this@PersonalProfileActivity, getString(R.string.network_error), Toast.LENGTH_SHORT).show() Toast.makeText(this@PersonalProfileActivity, getString(R.string.network_error), Toast.LENGTH_SHORT).show()
} }
} }

View File

@@ -19,10 +19,13 @@ import com.example.defenseapplication.bean.ObtainInviterInformationRequest
import com.example.defenseapplication.bean.SwitchFamilyRequest import com.example.defenseapplication.bean.SwitchFamilyRequest
import com.example.defenseapplication.bean.UpdatePassword import com.example.defenseapplication.bean.UpdatePassword
import com.example.defenseapplication.bean.UpdateUserInfo import com.example.defenseapplication.bean.UpdateUserInfo
import com.example.defenseapplication.bean.UploadResponse
import com.example.defenseapplication.bean.UserInfoBean import com.example.defenseapplication.bean.UserInfoBean
import com.example.defenseapplication.bean.VerifyOptionPasswordRequest import com.example.defenseapplication.bean.VerifyOptionPasswordRequest
import okhttp3.MultipartBody
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Multipart
import retrofit2.http.POST import retrofit2.http.POST
import retrofit2.http.PUT import retrofit2.http.PUT
import retrofit2.http.Query import retrofit2.http.Query
@@ -206,6 +209,12 @@ interface ApiService {
@POST("/app/revokeInvitation") @POST("/app/revokeInvitation")
suspend fun revokeInvitation(@Body request: VerifyOptionPasswordRequest): GetCodeBean<Any> suspend fun revokeInvitation(@Body request: VerifyOptionPasswordRequest): GetCodeBean<Any>
/**
* 上传文件接口
*/
@Multipart
@POST("/app/upload")
suspend fun uploadFile(@retrofit2.http.Part file: MultipartBody.Part): UploadResponse
/** /**
* 修改家庭用户状态 * 修改家庭用户状态

View File

@@ -1,129 +0,0 @@
package com.example.defenseapplication.utils
import android.content.Context
import android.util.Log
import com.alibaba.sdk.android.oss.ClientConfiguration
import com.alibaba.sdk.android.oss.OSS
import com.alibaba.sdk.android.oss.OSSClient
import com.alibaba.sdk.android.oss.common.auth.OSSCredentialProvider
import com.alibaba.sdk.android.oss.common.auth.OSSPlainTextAKSKCredentialProvider
import com.alibaba.sdk.android.oss.model.PutObjectRequest
import com.alibaba.sdk.android.oss.model.PutObjectResult
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/**
* OSS 工具类
* 用于上传文件到阿里云 OSS
*/
object OssUtil {
private const val TAG = "OssUtil"
// OSS 配置信息 - 需要根据实际情况配置
private const val ENDPOINT = "your-oss-endpoint" // 例如: oss-cn-hangzhou.aliyuncs.com
private const val ACCESS_KEY_ID = "your-access-key-id"
private const val ACCESS_KEY_SECRET = "your-access-key-secret"
private const val BUCKET_NAME = "your-bucket-name"
private var ossClient: OSS? = null
/**
* 初始化 OSS 客户端
*/
private fun initOssClient(context: Context): OSS {
if (ossClient == null) {
// 创建凭证提供者
val credentialProvider: OSSCredentialProvider =
OSSPlainTextAKSKCredentialProvider(ACCESS_KEY_ID, ACCESS_KEY_SECRET)
// 创建配置
val conf = ClientConfiguration().apply {
connectionTimeout = 15 * 1000 // 连接超时默认15秒
socketTimeout = 15 * 1000 // socket超时默认15秒
maxConcurrentRequest = 5 // 最大并发请求数默认5
maxErrorRetry = 2 // 失败后最大重试次数默认2
}
// 创建 OSS 客户端
ossClient = OSSClient(context.applicationContext, ENDPOINT, credentialProvider, conf)
}
return ossClient!!
}
/**
* 上传文件到 OSS
* @param context 上下文
* @param filePath 文件路径
* @param folderName 文件夹名称(可选,用于分类存储)
* @return OSS 文件 URL
*/
fun uploadFile(context: Context, filePath: String, folderName: String = "avatar"): String? {
return try {
val file = File(filePath)
if (!file.exists()) {
Log.e(TAG, "File not found: $filePath")
return null
}
// 生成文件名:时间戳 + 原文件名
val timestamp = SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault()).format(Date())
val originalFileName = file.name
val fileName = "${timestamp}_${originalFileName}"
val objectKey = "$folderName/$fileName"
Log.d(TAG, "Uploading file: $filePath to OSS key: $objectKey")
// 创建上传请求
val putObjectRequest = PutObjectRequest(BUCKET_NAME, objectKey, filePath)
// 执行上传
val oss = initOssClient(context)
val result: PutObjectResult = oss.putObject(putObjectRequest)
Log.d(TAG, "Upload success, ETAG: ${result.eTag}")
// 返回文件 URL
"https://$BUCKET_NAME.$ENDPOINT/$objectKey"
} catch (e: Exception) {
Log.e(TAG, "Upload failed: ${e.message}", e)
null
}
}
/**
* 异步上传文件到 OSS
* @param context 上下文
* @param filePath 文件路径
* @param folderName 文件夹名称
* @param callback 回调函数
*/
fun uploadFileAsync(
context: Context,
filePath: String,
folderName: String = "avatar",
callback: (String?) -> Unit
) {
Thread {
val result = uploadFile(context, filePath, folderName)
callback(result)
}.start()
}
/**
* 配置 OSS 参数(动态配置)
* @param endpoint OSS 端点
* @param accessKeyId Access Key ID
* @param accessKeySecret Access Key Secret
* @param bucketName Bucket 名称
*/
fun setOssConfig(endpoint: String, accessKeyId: String, accessKeySecret: String, bucketName: String) {
// 由于常量无法修改,这里需要重新创建客户端
ossClient = null
// 注意:实际项目中应该使用动态配置,这里简化处理
Log.d(TAG, "OSS config updated")
}
}

View File

@@ -0,0 +1,52 @@
package com.example.defenseapplication.view
import android.app.Dialog
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.Gravity
import android.view.LayoutInflater
import android.view.WindowManager
import com.example.defenseapplication.databinding.DialogLoadingBinding
class LoadingDialog(context: Context) {
private val dialog: Dialog = Dialog(context)
private val binding: DialogLoadingBinding
init {
binding = DialogLoadingBinding.inflate(LayoutInflater.from(context))
dialog.setContentView(binding.root)
dialog.window?.apply {
setGravity(Gravity.CENTER)
setLayout(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT
)
setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
setDimAmount(0.5f)
}
dialog.setCancelable(false)
}
fun setMessage(message: String): LoadingDialog {
binding.tvLoading.text = message
return this
}
fun show() {
if (!dialog.isShowing) {
dialog.show()
}
}
fun dismiss() {
if (dialog.isShowing) {
dialog.dismiss()
}
}
fun isShowing(): Boolean {
return dialog.isShowing
}
}

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:padding="32dp"
android:background="@drawable/bg_white_rounded">
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center"
android:indeterminateTint="@color/green"/>
<TextView
android:id="@+id/tv_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/uploading"
android:textColor="@color/black"
android:textSize="16sp"/>
</LinearLayout>

View File

@@ -461,4 +461,12 @@
<string name="select_from_album">从相册选择</string> <string name="select_from_album">从相册选择</string>
<string name="select_date">选择日期</string> <string name="select_date">选择日期</string>
<string name="select_time">选择时间</string> <string name="select_time">选择时间</string>
<string name="camera_permission_denied">需要相机权限</string>
<string name="storage_permission_denied">需要存储权限</string>
<string name="write_storage_permission_denied">需要写入存储权限</string>
<string name="take_photo_failed">拍照失败</string>
<string name="get_file_failed">获取文件失败</string>
<string name="create_file_failed">创建文件失败</string>
<string name="uploading">上传中...</string>
<string name="upload_failed">上传失败</string>
</resources> </resources>