Răsfoiți Sursa

1. 用户信息页面开发完成,支持修改用户头像、昵称、绑定手机号和邮箱、性别

bjb 3 luni în urmă
părinte
comite
9e716c25ad

+ 1 - 1
app/src/main/java/com/iscs/bozzys/api/ApiBean.kt

@@ -92,7 +92,7 @@ data class User(
     val createTime: Long = 0L,
     val workstationIds: List<Int>? = null,
     val type: String? = "",
-)
+) : java.io.Serializable
 
 /**
  * 流程模板

+ 24 - 0
app/src/main/java/com/iscs/bozzys/api/ApiRequest.kt

@@ -8,6 +8,10 @@ import com.iscs.bozzys.utils.Storage
 import com.iscs.bozzys.utils.Storage.saveRefreshToken
 import com.iscs.bozzys.utils.Storage.saveToken
 import com.iscs.bozzys.utils.network.Request
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.MultipartBody
+import okhttp3.RequestBody.Companion.asRequestBody
+import java.io.File
 import java.net.UnknownHostException
 
 object ApiRequest {
@@ -287,4 +291,24 @@ object ApiRequest {
         return requestApi { if (params.isNotEmpty()) api.updateMessageRead(params) else api.updateMessageAllRead() }
     }
 
+    /**
+     * 上传文件
+     *
+     * @param file  上传的文件
+     * @param type  文件类型
+     */
+    suspend fun uploadFile(file: File, type: String = "image"): Result<Response<String>> {
+        val part = MultipartBody.Part.createFormData("file", file.name, file.asRequestBody("$type/*".toMediaType()))
+        return requestApi { api.uploadFile(part) }
+    }
+
+    /**
+     * 更新用户信息
+     *
+     * @param params
+     */
+    suspend fun updateUserProfile(params: MutableMap<String, Any>): Result<Response<Boolean>> {
+        return requestApi { api.updateUserProfile(params) }
+    }
+
 }

+ 23 - 0
app/src/main/java/com/iscs/bozzys/api/ApiService.kt

@@ -1,12 +1,15 @@
 package com.iscs.bozzys.api
 
 import com.google.gson.JsonObject
+import okhttp3.MultipartBody
 import retrofit2.http.Body
 import retrofit2.http.GET
 import retrofit2.http.HeaderMap
 import retrofit2.http.Headers
+import retrofit2.http.Multipart
 import retrofit2.http.POST
 import retrofit2.http.PUT
+import retrofit2.http.Part
 import retrofit2.http.Query
 import retrofit2.http.QueryMap
 
@@ -247,4 +250,24 @@ interface ApiService {
     @PUT("/admin-api/system/app-notify-message/update-all-read")
     suspend fun updateMessageAllRead(@HeaderMap headers: Map<String, String> = ApiRequest.getUserHeaders()): Response<Boolean>
 
+    /**
+     * 文件上传
+     */
+    @Multipart
+    @POST("/admin-api/infra/file/upload")
+    suspend fun uploadFile(
+        @Part part: MultipartBody.Part,
+        @HeaderMap headers: Map<String, String> = ApiRequest.getUserHeaders()
+    ): Response<String>
+
+    /**
+     * 更新用户信息
+     */
+    @Headers("Content-Type: application/json")
+    @PUT("/admin-api/system/user/profile/update")
+    suspend fun updateUserProfile(
+        @Body body: MutableMap<String, Any>,
+        @HeaderMap headers: Map<String, String> = ApiRequest.getUserHeaders()
+    ): Response<Boolean>
+
 }

+ 4 - 1
app/src/main/java/com/iscs/bozzys/event/Event.kt

@@ -37,7 +37,7 @@ object RefreshEventBus {
     }
 }
 
-sealed class RefreshEvent(var any: Any = Any()) {
+sealed class RefreshEvent(var value: Any = Any()) {
     // 刷新首页数据 Home/Jobs/Tasks
     object HomeData : RefreshEvent()
 
@@ -49,4 +49,7 @@ sealed class RefreshEvent(var any: Any = Any()) {
 
     // 通知事件
     object Notification : RefreshEvent()
+
+    // 通知更新用户信息
+    object UpdateUserInfo : RefreshEvent()
 }

+ 1 - 1
app/src/main/java/com/iscs/bozzys/ui/common/Loading.kt

@@ -76,7 +76,7 @@ fun LoadingCompose(vm: VMLoading = viewModel()) {
                 horizontalAlignment = Alignment.CenterHorizontally
             ) {
                 CircularProgressIndicator(Modifier.size(30.dp))
-                Text("加载中...", Modifier.padding(top = 5.dp), color = Color.White, fontSize = 12.sp)
+                Text("${vm.state.content}", Modifier.padding(top = 5.dp), color = Color.White, fontSize = 12.sp)
             }
         }
     }

+ 97 - 0
app/src/main/java/com/iscs/bozzys/ui/dialog/InputDialog.kt

@@ -0,0 +1,97 @@
+package com.iscs.bozzys.ui.dialog
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+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 androidx.compose.ui.window.Dialog
+import com.iscs.bozzys.ui.theme.Text
+
+/**
+ * 默认公共弹窗
+ */
+@Composable
+fun InputDialog(
+    show: Boolean = false,
+    title: String = "提示",
+    content: String = "",
+    cancelText: String = "取消",
+    onCancel: () -> Unit = {},
+    confirmText: String = "确认",
+    onConfirm: () -> Unit = {}
+) {
+    if (show) {
+        Dialog(onDismissRequest = {}) {
+            Box(
+                modifier = Modifier
+                    .clip(RoundedCornerShape(12.dp))
+                    .background(Color.White)
+            ) {
+                Column(modifier = Modifier.width(240.dp), horizontalAlignment = Alignment.CenterHorizontally) {
+                    Text(title, fontSize = 16.sp, lineHeight = 36.sp, fontWeight = FontWeight.Bold, color = Text)
+                    Spacer(
+                        Modifier
+                            .fillMaxWidth()
+                            .height(1.dp)
+                            .background(Color(0xFFEEEEEE))
+                    )
+                    Text(
+                        content,
+                        fontSize = 15.sp,
+                        color = Text.copy(alpha = 0.8f),
+                        modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp)
+                    )
+                    Row(Modifier.padding(horizontal = 5.dp, vertical = 10.dp)) {
+                        Text(
+                            cancelText, color = Color.White, modifier = Modifier
+                                .padding(horizontal = 5.dp)
+                                .weight(1f)
+                                .clip(RoundedCornerShape(6.dp))
+                                .background(Color.Gray.copy(alpha = 0.6f))
+                                .clickable(onClick = { onCancel() })
+                                .padding(vertical = 5.dp),
+                            textAlign = TextAlign.Center,
+                            fontSize = 16.sp,
+                            fontWeight = FontWeight.Bold
+                        )
+                        Text(
+                            confirmText,
+                            color = Color.White,
+                            modifier = Modifier
+                                .padding(horizontal = 5.dp)
+                                .weight(1f)
+                                .clip(RoundedCornerShape(6.dp))
+                                .background(MaterialTheme.colorScheme.primary)
+                                .clickable(onClick = { onConfirm() })
+                                .padding(vertical = 5.dp),
+                            textAlign = TextAlign.Center,
+                            fontSize = 16.sp,
+                            fontWeight = FontWeight.Bold
+                        )
+                    }
+                }
+            }
+        }
+    }
+
+}
+
+// data class StateTips(val title: String = "", val content: String = "", val show: Boolean = false, val type: String = "")

+ 1 - 1
app/src/main/java/com/iscs/bozzys/ui/common/Dialog.kt → app/src/main/java/com/iscs/bozzys/ui/dialog/TipsDialog.kt

@@ -1,4 +1,4 @@
-package com.iscs.bozzys.ui.common
+package com.iscs.bozzys.ui.dialog
 
 import androidx.compose.foundation.background
 import androidx.compose.foundation.clickable

+ 3 - 3
app/src/main/java/com/iscs/bozzys/ui/pages/edit/step/PageEditStep.kt

@@ -58,7 +58,7 @@ import com.iscs.bozzys.R
 import com.iscs.bozzys.event.RefreshEvent
 import com.iscs.bozzys.event.RefreshEventBus
 import com.iscs.bozzys.ui.common.PageBase
-import com.iscs.bozzys.ui.common.TipsDialog
+import com.iscs.bozzys.ui.dialog.TipsDialog
 import com.iscs.bozzys.ui.common.Title
 import com.iscs.bozzys.ui.pages.compose.CardContainer
 import com.iscs.bozzys.ui.pages.compose.FormContainer
@@ -161,7 +161,7 @@ class PageEditStep : PageBase() {
                                                     // 刷新节点位置
                                                     vm.updateNode(node)
                                                     // 将当前Node平移到屏幕中间
-                                                    RefreshEventBus.onRefreshData(RefreshEvent.UpdateNodeToMapTopCenter.apply { any = node })
+                                                    RefreshEventBus.onRefreshData(RefreshEvent.UpdateNodeToMapTopCenter.apply { value = node })
                                                     lifecycleScope.launch {
                                                         // 给移动位置增加点动画效果
                                                         delay(280)
@@ -203,7 +203,7 @@ class PageEditStep : PageBase() {
                                 // 刷新节点位置
                                 vm.updateNode(node)
                                 // 将当前Node平移到屏幕中间
-                                RefreshEventBus.onRefreshData(RefreshEvent.UpdateNodeToMapTopCenter.apply { any = node })
+                                RefreshEventBus.onRefreshData(RefreshEvent.UpdateNodeToMapTopCenter.apply { value = node })
                                 lifecycleScope.launch {
                                     // 给移动位置增加点动画效果
                                     delay(280)

+ 1 - 1
app/src/main/java/com/iscs/bozzys/ui/pages/edit/step/compose/ZoomContainer.kt

@@ -135,7 +135,7 @@ fun ZoomPanContainer(
         async {
             RefreshEventBus.events.collect {
                 when (it) {
-                    is RefreshEvent.UpdateNodeToMapTopCenter -> animateNodeToTopCenter(it.any as NodeUI)
+                    is RefreshEvent.UpdateNodeToMapTopCenter -> animateNodeToTopCenter(it.value as NodeUI)
                     else -> {}
                 }
             }

+ 2 - 1
app/src/main/java/com/iscs/bozzys/ui/pages/home/HomeCompose.kt

@@ -54,6 +54,7 @@ import com.iscs.bozzys.ui.pages.message.openPageMessage
 import com.iscs.bozzys.ui.pages.vm.VMHome
 import com.iscs.bozzys.ui.theme.Text
 import com.iscs.bozzys.utils.DateUtil.getShowDateOrTime
+import com.iscs.bozzys.utils.getRoleName
 
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
@@ -120,7 +121,7 @@ private fun TopToolBar(pv: PaddingValues, vm: VMHome) {
             }
             Column(Modifier.padding(horizontal = 10.dp)) {
                 Text(state.user.nickname, fontSize = 16.sp, lineHeight = 16.sp, fontWeight = FontWeight.Medium, color = Color.White)
-                // Text(state.roles.getRoleName(), fontSize = 12.sp, lineHeight = 12.sp, color = Color.White.copy(alpha = 0.8f))
+                Text(state.roles.getRoleName(), fontSize = 12.sp, lineHeight = 12.sp, color = Color.White.copy(alpha = 0.8f))
             }
             Spacer(Modifier.weight(1f))
             Box(modifier = Modifier.fillMaxHeight(), contentAlignment = Alignment.Center) {

+ 33 - 30
app/src/main/java/com/iscs/bozzys/ui/pages/home/SettingsCompose.kt

@@ -43,8 +43,11 @@ import androidx.compose.ui.unit.sp
 import androidx.compose.ui.zIndex
 import coil.compose.AsyncImage
 import com.iscs.bozzys.R
+import com.iscs.bozzys.event.AuthEventBus
+import com.iscs.bozzys.event.RefreshEvent
+import com.iscs.bozzys.event.RefreshEventBus
 import com.iscs.bozzys.ui.common.PageBase
-import com.iscs.bozzys.ui.common.TipsDialog
+import com.iscs.bozzys.ui.dialog.TipsDialog
 import com.iscs.bozzys.ui.pages.compose.CardContainer
 import com.iscs.bozzys.ui.pages.login.openPageLogin
 import com.iscs.bozzys.ui.pages.profile.openPageProfile
@@ -55,6 +58,7 @@ import com.iscs.bozzys.utils.Storage
 import com.iscs.bozzys.utils.Storage.saveTokenIv
 import com.iscs.bozzys.utils.Storage.saveTokenIvValue
 import com.iscs.bozzys.utils.SystemUtil
+import com.iscs.bozzys.utils.getRoleName
 
 @OptIn(ExperimentalStdlibApi::class)
 @Composable
@@ -122,10 +126,8 @@ fun SettingsCompose(pv: PaddingValues, zIndex: Float, vm: VMHome) {
         }
         TipsDialog(show = showExitDialog, content = "确定要退出登录吗?", onCancel = { showExitDialog = false }, onConfirm = {
             showExitDialog = false
-            vm.logout {
-                ctx.openPageLogin()
-                if (ctx is PageBase) ctx.destroyDelay(500)
-            }
+            // 退出登录操作
+            vm.logout { AuthEventBus.onTokenExpired() }
         })
     }
 }
@@ -189,7 +191,7 @@ private fun UserInfo(vm: VMHome) {
                 .fillMaxWidth()
                 .height(90.dp)
                 .background(brush = Brush.horizontalGradient(listOf(Color(0xFFFFF9E5), Color(0xFFFFFEFB))))
-                .clickable { ctx.openPageProfile() }
+                .clickable { ctx.openPageProfile(state.user) }
                 .padding(horizontal = 20.dp),
             verticalAlignment = Alignment.CenterVertically
         ) {
@@ -215,20 +217,21 @@ private fun UserInfo(vm: VMHome) {
                         .clip(CircleShape),
                     contentScale = ContentScale.Crop
                 )
-                CardContainer(
-                    modifier = Modifier
-                        .size(20.dp)
-                        .align(Alignment.BottomEnd)
-                        .clip(RoundedCornerShape(50))
-                        .background(Color.White)
-                        .padding(5.dp)
-                ) {
-                    Icon(
-                        painter = painterResource(R.drawable.camera),
-                        contentDescription = null, tint = MaterialTheme.colorScheme.primary,
-                        modifier = Modifier.fillMaxSize()
-                    )
-                }
+                // 这里暂时隐藏拍照的图标,整体进入内页修改头像
+//                CardContainer(
+//                    modifier = Modifier
+//                        .size(20.dp)
+//                        .align(Alignment.BottomEnd)
+//                        .clip(RoundedCornerShape(50))
+//                        .background(Color.White)
+//                        .padding(5.dp)
+//                ) {
+//                    Icon(
+//                        painter = painterResource(R.drawable.camera),
+//                        contentDescription = null, tint = MaterialTheme.colorScheme.primary,
+//                        modifier = Modifier.fillMaxSize()
+//                    )
+//                }
             }
             Column(
                 modifier = Modifier
@@ -236,16 +239,16 @@ private fun UserInfo(vm: VMHome) {
                     .weight(1f)
             ) {
                 Text(state.user.nickname, fontWeight = FontWeight.Bold, fontSize = 16.sp, lineHeight = 16.sp, color = Text)
-//                Text(
-//                    state.roles.getRoleName(),
-//                    fontSize = 12.sp,
-//                    modifier = Modifier
-//                        .padding(top = 5.dp)
-//                        .clip(RoundedCornerShape(50))
-//                        .background(MaterialTheme.colorScheme.primary)
-//                        .padding(horizontal = 5.dp),
-//                    color = Color.White
-//                )
+                Text(
+                    state.roles.getRoleName(),
+                    fontSize = 12.sp,
+                    modifier = Modifier
+                        .padding(top = 5.dp)
+                        .clip(RoundedCornerShape(50))
+                        .background(MaterialTheme.colorScheme.primary)
+                        .padding(horizontal = 5.dp),
+                    color = Color.White
+                )
             }
             Icon(
                 painter = painterResource(R.drawable.back),

+ 1 - 1
app/src/main/java/com/iscs/bozzys/ui/pages/message/PageMessage.kt

@@ -47,7 +47,7 @@ import com.iscs.bozzys.R
 import com.iscs.bozzys.api.Message
 import com.iscs.bozzys.ui.common.Empty
 import com.iscs.bozzys.ui.common.PageBase
-import com.iscs.bozzys.ui.common.TipsDialog
+import com.iscs.bozzys.ui.dialog.TipsDialog
 import com.iscs.bozzys.ui.pages.compose.CardContainer
 import com.iscs.bozzys.ui.pages.vm.StatePageMessage
 import com.iscs.bozzys.ui.pages.vm.VMMessage

+ 398 - 11
app/src/main/java/com/iscs/bozzys/ui/pages/profile/PageProfile.kt

@@ -5,23 +5,75 @@ import android.content.Context
 import android.content.Intent
 import android.content.pm.PackageManager
 import android.os.Build
+import android.view.WindowManager
 import androidx.activity.result.contract.ActivityResultContracts
+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.Box
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.FlowRow
 import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.material3.Button
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+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.Icon
+import androidx.compose.material3.LocalTextStyle
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.RadioButton
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.rotate
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+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 androidx.compose.ui.window.Dialog
+import androidx.compose.ui.window.DialogProperties
+import androidx.lifecycle.viewmodel.compose.viewModel
+import coil.compose.AsyncImage
+import com.iscs.bozzys.R
+import com.iscs.bozzys.api.User
 import com.iscs.bozzys.ui.common.PageBase
 import com.iscs.bozzys.ui.common.Title
-import com.iscs.bozzys.utils.LogUtil
+import com.iscs.bozzys.ui.pages.compose.CardContainer
+import com.iscs.bozzys.ui.pages.vm.VMProfile
+import com.iscs.bozzys.ui.theme.Main
+import com.iscs.bozzys.ui.theme.Text
 import com.iscs.bozzys.utils.SystemUtil.uriToFile
 import java.io.File
 
 /**
  * 打开用户中心
  */
-fun Context.openPageProfile() {
-    startActivity(Intent(this, PageProfile::class.java))
+fun Context.openPageProfile(user: User) {
+    startActivity(Intent(this, PageProfile::class.java).apply {
+        putExtra("user", user)
+    })
 }
 
 /**
@@ -51,17 +103,352 @@ class PageProfile : PageBase() {
         imageLauncher.launch("image/*")
     }
 
+    /**
+     * 从页面中获取用户数据
+     */
+    private fun getUserInfoFromIntent(): User {
+        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            intent.getSerializableExtra("user", User::class.java) ?: User()
+        } else {
+            @Suppress("DEPRECATION")
+            (intent.getSerializableExtra("user") as? User) ?: User()
+        }
+    }
+
     @Composable
     override fun GetViews(pv: PaddingValues) {
-        Column() {
-            Title(pv, "个人中心")
-            Button({
-                pickerImage {
-                    LogUtil.i("xiaoming", "$it")
+        val vm: VMProfile = viewModel()
+        val state by vm.state.collectAsState()
+        LaunchedEffect(Unit) {
+            vm.toast.initToast()
+            vm.loading.initLoading()
+            vm.init(getUserInfoFromIntent())
+        }
+        Column(horizontalAlignment = Alignment.CenterHorizontally) {
+            Title(pv, "个人信息")
+            // 头像信息
+            CardContainer(modifier = Modifier.padding(16.dp), topRadius = 16.dp, bottomRadius = 16.dp) {
+                Column {
+                    Row(
+                        modifier = Modifier
+                            .fillMaxWidth()
+                            .height(80.dp)
+                            .clickable { pickerImage { vm.updateUserAvatar(it) } }
+                            .padding(horizontal = 12.dp),
+                        verticalAlignment = Alignment.CenterVertically
+                    ) {
+                        Text("头像", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = Text)
+                        Spacer(modifier = Modifier.weight(1f))
+                        AsyncImage(
+                            model = state.user.avatar, contentDescription = null, modifier = Modifier
+                                .size(52.dp)
+                                .clip(CircleShape)
+                        )
+                        Icon(
+                            painter = painterResource(R.drawable.back),
+                            contentDescription = null,
+                            tint = Color(0xFFBBBBBB),
+                            modifier = Modifier
+                                .padding(start = 5.dp)
+                                .size(14.dp)
+                                .rotate(180f)
+                        )
+                    }
+                }
+            }
+            // 其他属性
+            CardContainer(
+                modifier = Modifier
+                    .padding(horizontal = 16.dp)
+                    .padding(bottom = 16.dp), topRadius = 16.dp, bottomRadius = 16.dp
+            ) {
+                Column {
+                    // 用户名
+                    UserInfoItem("用户名", state.user.nickname) {
+                        vm.showModifyDialog("修改用户名", "nickname" to state.user.nickname)
+                    }
+                    SpacerLine()
+                    // 手机号
+                    UserInfoItem("手机号", state.user.mobile?.ifEmpty { "点击绑定" } ?: "点击绑定") {
+                        vm.showModifyDialog("修改手机号", "mobile" to (state.user.mobile ?: ""))
+                    }
+                    SpacerLine()
+                    // 邮箱
+                    UserInfoItem("邮箱", state.user.email?.ifEmpty { "点击绑定" } ?: "点击绑定") {
+                        vm.showModifyDialog("修改邮箱", "email" to (state.user.email ?: ""))
+                    }
+                    SpacerLine()
+                    // 性别
+                    UserInfoItem("性别", if (state.user.sex != 2) "男" else "女") {
+                        vm.showModifyDialog("选择性别", "sex" to "${state.user.sex}")
+                    }
+                }
+                ProfileDialog(
+                    show = state.showModifyDialog,
+                    title = state.modifyDialogTitle,
+                    {
+                        vm.hideModifyDialog()
+                        vm.updateUserInfo()
+                    },
+                    { vm.hideModifyDialog() }) {
+                    // 修改非性别的参数
+                    if (state.modifyKv.first != "sex") BasicTextField(
+                        state.modifyKv.second,
+                        onValueChange = { vm.onModifyValueChanged(it) },
+                        Modifier
+                            .padding(vertical = 16.dp, horizontal = 10.dp)
+                            .fillMaxWidth()
+                            .height(46.dp)
+                            .border(1.dp, shape = RoundedCornerShape(6.dp), color = Color(0xFFE5E6EB))
+                            .padding(horizontal = 10.dp),
+                        singleLine = true,
+                        textStyle = LocalTextStyle.current.copy(fontSize = 16.sp, lineHeight = 20.sp),
+                        decorationBox = { innerTextField ->
+                            Box(contentAlignment = Alignment.CenterStart) {
+                                innerTextField()
+                                if (state.modifyKv.second.isEmpty()) {
+                                    val text = when (state.modifyKv.first) {
+                                        "nickname" -> "请输入用户名"
+                                        "mobile" -> "请输入手机号"
+                                        "email" -> "请输入邮箱"
+                                        else -> "请输入"
+                                    }
+                                    Text(
+                                        text,
+                                        color = Color(0xFF9CA3AF),
+                                        fontSize = 16.sp,
+                                        lineHeight = 20.sp,
+                                        modifier = Modifier.offset(y = (-1).dp)
+                                    )
+                                }
+                            }
+                        },
+                        cursorBrush = SolidColor(Main),
+                        keyboardOptions = KeyboardOptions(keyboardType = if (state.modifyKv.first == "mobile") KeyboardType.Number else KeyboardType.Text)
+                    )
+                    if (state.modifyKv.first == "sex") {
+                        FlowRow(
+                            Modifier
+                                .padding(horizontal = 10.dp, vertical = 16.dp)
+                                .fillMaxWidth()
+                                .heightIn(max = 120.dp)
+                        ) {
+                            Row(
+                                modifier = Modifier
+                                    .clip(RoundedCornerShape(12.dp))
+                                    .clickable(onClick = { vm.onModifyValueChanged("1") })
+                                    .padding(horizontal = 10.dp, vertical = 5.dp),
+                                verticalAlignment = Alignment.CenterVertically
+                            ) {
+                                RadioButton(
+                                    selected = state.modifyKv.second != "2",
+                                    onClick = null,
+                                    modifier = Modifier.size(14.dp),
+                                )
+                                Text(
+                                    text = "男",
+                                    fontSize = 15.sp,
+                                    modifier = Modifier.padding(start = 10.dp),
+                                    color = Text
+                                )
+                            }
+                            Row(
+                                modifier = Modifier
+                                    .clip(RoundedCornerShape(12.dp))
+                                    .clickable(onClick = { vm.onModifyValueChanged("2") })
+                                    .padding(horizontal = 10.dp, vertical = 5.dp),
+                                verticalAlignment = Alignment.CenterVertically
+                            ) {
+                                RadioButton(
+                                    selected = state.modifyKv.second == "2",
+                                    onClick = null,
+                                    modifier = Modifier.size(14.dp),
+                                )
+                                Text(
+                                    text = "女",
+                                    fontSize = 15.sp,
+                                    modifier = Modifier.padding(start = 10.dp),
+                                    color = Text
+                                )
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    @Composable
+    fun UserInfoItem(title: String, desc: String, click: () -> Unit) {
+        Row(
+            modifier = Modifier
+                .fillMaxWidth()
+                .height(60.dp)
+                .clickable { click() }
+                .padding(horizontal = 12.dp),
+            verticalAlignment = Alignment.CenterVertically
+        ) {
+            Text(title, fontSize = 16.sp, fontWeight = FontWeight.Bold, color = Text)
+            Spacer(modifier = Modifier.weight(1f))
+            Text(desc, fontSize = 15.sp, color = Color(0xFF666666))
+            Icon(
+                painter = painterResource(R.drawable.back),
+                contentDescription = null,
+                tint = Color(0xFFBBBBBB),
+                modifier = Modifier
+                    .padding(start = 5.dp)
+                    .size(14.dp)
+                    .rotate(180f)
+            )
+        }
+    }
+
+    /**
+     * 分割线
+     */
+    @Composable
+    fun SpacerLine() {
+        Spacer(
+            modifier = Modifier
+                .fillMaxWidth()
+                .height(1.dp)
+                .background(Color.Gray.copy(alpha = 0.2f))
+                .padding(horizontal = 12.dp)
+        )
+    }
+
+    @Composable
+    fun ProfileDialog(
+        show: Boolean,
+        title: String = "",
+        onConfirm: () -> Unit,
+        onDismiss: () -> Unit,
+        content: @Composable () -> Unit
+    ) {
+        val ctx = LocalContext.current
+        if (!show || ctx !is PageBase) return
+
+        LaunchedEffect(Unit) {
+            ctx.setNavigationLight(false)
+        }
+
+        Dialog(
+            onDismissRequest = onDismiss,
+            properties = DialogProperties(decorFitsSystemWindows = false)
+        ) {
+            val activityWindow = ctx.window
+            val dialogWindow = ctx.getDialogWindow()
+            SideEffect {
+                if (activityWindow != null && dialogWindow != null) {
+                    val attributes = WindowManager.LayoutParams()
+                    // 复制Activity窗口属性
+                    attributes.copyFrom(activityWindow.attributes)
+                    // 这个一定要设置
+                    attributes.type = dialogWindow.attributes.type
+                    // 更新窗口属性
+                    dialogWindow.attributes = attributes
+                    // 设置窗口的宽度和高度,这段代码Dialog源码中就有哦,可以自己去查看
+                    dialogWindow.setLayout(
+                        activityWindow.decorView.width,
+                        activityWindow.decorView.height
+                    )
+                }
+            }
+            Box(
+                modifier = Modifier
+                    .fillMaxSize()
+                    .background(Color.Black.copy(alpha = 0.4f))
+                    .clickable(
+                        indication = null,
+                        interactionSource = remember { MutableInteractionSource() },
+                        onClick = {}),
+                contentAlignment = Alignment.Center
+            ) {
+                CardContainer(
+                    topRadius = 16.dp,
+                    bottomRadius = 16.dp,
+                    modifier = Modifier
+                        .fillMaxWidth(0.65f)
+                        // 防止点击穿透
+                        .clickable(
+                            indication = null,
+                            interactionSource = remember { MutableInteractionSource() },
+                            onClick = {})
+                        .background(Color.White, RoundedCornerShape(16.dp))
+                ) {
+                    Column(Modifier.fillMaxWidth()) {
+                        Row(
+                            Modifier
+                                .fillMaxWidth()
+                                .height(40.dp)
+                                .padding(horizontal = 16.dp),
+                            verticalAlignment = Alignment.CenterVertically
+                        ) {
+                            // Spacer(Modifier.size(40.dp))
+                            Text(
+                                title,
+                                fontSize = 16.sp,
+                                fontWeight = FontWeight.Bold,
+                                color = Text,
+                                modifier = Modifier.weight(1f),
+                                textAlign = TextAlign.Center
+                            )
+//                            Icon(
+//                                painter = painterResource(R.drawable.close),
+//                                contentDescription = null,
+//                                modifier = Modifier
+//                                    .offset(8.dp, 0.dp)
+//                                    .size(36.dp)
+//                                    .clip(RoundedCornerShape(50))
+//                                    .clickable(onClick = { onDismiss() })
+//                                    .padding(10.dp)
+//                            )
+                        }
+                        Spacer(
+                            Modifier
+                                .fillMaxWidth()
+                                .height(0.5.dp)
+                                .background(Color.Black.copy(alpha = 0.05f))
+                        )
+                        content()
+                        Spacer(
+                            Modifier
+                                .fillMaxWidth()
+                                .height(0.5.dp)
+                                .background(Color.Black.copy(alpha = 0.05f))
+                        )
+                        Row(Modifier.padding(horizontal = 5.dp, vertical = 10.dp)) {
+                            Text(
+                                "取消", color = Color.White, modifier = Modifier
+                                    .padding(horizontal = 5.dp)
+                                    .weight(1f)
+                                    .clip(RoundedCornerShape(6.dp))
+                                    .background(Color.Gray.copy(alpha = 0.6f))
+                                    .clickable(onClick = { onDismiss() })
+                                    .padding(vertical = 5.dp),
+                                textAlign = TextAlign.Center,
+                                fontSize = 16.sp,
+                                fontWeight = FontWeight.Bold
+                            )
+                            Text(
+                                "确定",
+                                color = Color.White,
+                                modifier = Modifier
+                                    .padding(horizontal = 5.dp)
+                                    .weight(1f)
+                                    .clip(RoundedCornerShape(6.dp))
+                                    .background(MaterialTheme.colorScheme.primary)
+                                    .clickable(onClick = { onConfirm() })
+                                    .padding(vertical = 5.dp),
+                                textAlign = TextAlign.Center,
+                                fontSize = 16.sp,
+                                fontWeight = FontWeight.Bold
+                            )
+                        }
+                    }
                 }
-            }) {
-                Text("选择照片")
             }
         }
     }
+
 }

+ 4 - 0
app/src/main/java/com/iscs/bozzys/ui/pages/vm/VMHome.kt

@@ -16,6 +16,7 @@ import com.iscs.bozzys.event.RefreshEventBus
 import com.iscs.bozzys.ui.common.VMBase
 import com.iscs.bozzys.ui.theme.Text
 import com.iscs.bozzys.utils.Storage
+import com.iscs.bozzys.utils.Storage.saveUserAvatar
 import kotlinx.coroutines.async
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -54,6 +55,8 @@ class VMHome : VMBase() {
             RefreshEventBus.events.collect { event ->
                 when (event) {
                     is RefreshEvent.HomeData -> refreshHome()
+                    is RefreshEvent.UpdateUserInfo -> _state.value = _state.value.copy(user = event.value as User)
+                    // 其余不处理
                     else -> {}
                 }
             }
@@ -155,6 +158,7 @@ class VMHome : VMBase() {
                 toast.emit(msgRsp.msg)
                 return@launch
             }
+            permissionRsp.data?.user?.avatar.saveUserAvatar()
             _state.value = _state.value.copy(
                 homeTasks = ArrayList(tasksRsp.data?.list ?: emptyList()),
                 messageList = ArrayList(msgRsp.data?.list ?: emptyList()),

+ 1 - 1
app/src/main/java/com/iscs/bozzys/ui/pages/vm/VMLoading.kt

@@ -40,4 +40,4 @@ class VMLoading : ViewModel() {
  * @param show      是否显示loading
  * @param content   loading时提示的内容
  */
-data class StateLoading(var show: Boolean = false, var content: Any = "", var timeout: Int = -1)
+data class StateLoading(var show: Boolean = false, var content: Any = "加载中...", var timeout: Int = -1)

+ 0 - 2
app/src/main/java/com/iscs/bozzys/ui/pages/vm/VMLogin.kt

@@ -12,7 +12,6 @@ import com.iscs.bozzys.utils.BiometricKeyStore
 import com.iscs.bozzys.utils.Storage
 import com.iscs.bozzys.utils.Storage.saveRefreshToken
 import com.iscs.bozzys.utils.Storage.saveToken
-import com.iscs.bozzys.utils.Storage.saveUserName
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
 
@@ -70,7 +69,6 @@ class VMLogin : VMBase() {
                 isLogin = true
                 it.data?.accessToken.saveToken()
                 it.data?.refreshToken.saveRefreshToken()
-                it.data?.nickname.saveUserName()
                 Storage.saveLogin(true)
                 loading.emit(StateLoading(show = false))
                 toast.emit("登录成功")

+ 1 - 1
app/src/main/java/com/iscs/bozzys/ui/pages/vm/VMMessage.kt

@@ -5,7 +5,7 @@ import com.iscs.bozzys.api.ApiRequest
 import com.iscs.bozzys.api.Message
 import com.iscs.bozzys.event.RefreshEvent
 import com.iscs.bozzys.event.RefreshEventBus
-import com.iscs.bozzys.ui.common.StateTips
+import com.iscs.bozzys.ui.dialog.StateTips
 import com.iscs.bozzys.ui.common.VMBase
 import com.iscs.bozzys.utils.DateUtil.getShowDate
 import kotlinx.coroutines.delay

+ 108 - 0
app/src/main/java/com/iscs/bozzys/ui/pages/vm/VMProfile.kt

@@ -0,0 +1,108 @@
+package com.iscs.bozzys.ui.pages.vm
+
+import androidx.lifecycle.viewModelScope
+import com.iscs.bozzys.api.ApiRequest
+import com.iscs.bozzys.api.ApiRequest.getResponse
+import com.iscs.bozzys.api.ApiRequest.isCodeOk
+import com.iscs.bozzys.api.User
+import com.iscs.bozzys.event.RefreshEvent
+import com.iscs.bozzys.event.RefreshEventBus
+import com.iscs.bozzys.ui.common.VMBase
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+import java.io.File
+
+/**
+ * 用户属性编辑界面
+ */
+class VMProfile : VMBase() {
+
+    private val _state = MutableStateFlow(StateProfile())
+
+    val state = _state.asStateFlow()
+
+    /**
+     * 初始化操作
+     */
+    fun init(user: User) {
+        _state.value = _state.value.copy(user = user)
+    }
+
+    /**
+     * 更新用户头像
+     *
+     * @param file 用户选择的头像文件
+     */
+    fun updateUserAvatar(file: File) {
+        viewModelScope.launch {
+            loading.emit(StateLoading(show = true, content = "更新中"))
+            val uploadRsp = ApiRequest.uploadFile(file).getOrElse { it.getResponse() }
+            if (!uploadRsp.code.isCodeOk()) {
+                delay(1000)
+                loading.emit(StateLoading())
+                toast.emit(uploadRsp.msg)
+                return@launch
+            }
+            val avatar = uploadRsp.data ?: ""
+            val updateRsp = ApiRequest.updateUserProfile(mutableMapOf("avatar" to avatar)).getOrElse { it.getResponse() }
+            delay(1000)
+            loading.emit(StateLoading())
+            if (!updateRsp.code.isCodeOk()) {
+                toast.emit(updateRsp.msg)
+                return@launch
+            }
+            // 更新头像数据
+            _state.value = _state.value.copy(user = _state.value.user.copy(avatar = avatar))
+            // 通知首页更新用户信息
+            RefreshEventBus.onRefreshData(RefreshEvent.UpdateUserInfo.apply { value = _state.value.user })
+        }
+    }
+
+    fun showModifyDialog(title: String, kv: Pair<String, String>) {
+        _state.value = _state.value.copy(showModifyDialog = true, modifyDialogTitle = title, modifyKv = kv)
+    }
+
+    fun onModifyValueChanged(value: String) {
+        _state.value = _state.value.copy(modifyKv = _state.value.modifyKv.first to value)
+    }
+
+    fun hideModifyDialog() {
+        _state.value = _state.value.copy(showModifyDialog = false)
+    }
+
+    fun updateUserInfo() {
+        viewModelScope.launch {
+            val kv = _state.value.modifyKv
+            val params: Pair<String, Any> = if (kv.first == "sex") kv.first to kv.second.toInt() else kv
+            loading.emit(StateLoading(show = true, content = "更新中"))
+            val updateRsp = ApiRequest.updateUserProfile(mutableMapOf(params)).getOrElse { it.getResponse() }
+            delay(1000)
+            loading.emit(StateLoading())
+            if (!updateRsp.code.isCodeOk()) {
+                toast.emit(updateRsp.msg)
+                return@launch
+            }
+            val user = when (kv.first) {
+                "nickname" -> _state.value.user.copy(nickname = kv.second)
+                "mobile" -> _state.value.user.copy(mobile = kv.second)
+                "email" -> _state.value.user.copy(email = kv.second)
+                "sex" -> _state.value.user.copy(sex = kv.second.toInt())
+                else -> _state.value.user
+            }
+            // 更新头像数据
+            _state.value = _state.value.copy(user = user)
+            // 通知首页更新用户信息
+            RefreshEventBus.onRefreshData(RefreshEvent.UpdateUserInfo.apply { value = _state.value.user })
+        }
+    }
+
+}
+
+data class StateProfile(
+    val user: User = User(),
+    val showModifyDialog: Boolean = false,
+    val modifyDialogTitle: String = "",
+    val modifyKv: Pair<String, String> = "" to ""
+)

+ 1 - 1
app/src/main/java/com/iscs/bozzys/utils/Exts.kt

@@ -7,7 +7,7 @@ fun List<String>.getRoleName(): String {
     return if (this.contains("super_admin")) {
         "超级管理员"
     } else if (this.contains("jtdrawer")) {
-        "管理员"
+        "参与人"
     } else if (this.contains("jtlocker")) {
         "上锁人"
     } else if (this.contains("jtcolocker")) {

+ 4 - 4
app/src/main/java/com/iscs/bozzys/utils/Storage.kt

@@ -63,15 +63,15 @@ object Storage {
     /**
      * 保存用户名
      */
-    fun String?.saveUserName() {
-        mmkv.encode("user_name", this ?: "")
+    fun String?.saveUserAvatar() {
+        mmkv.encode("user_avatar", this ?: "")
     }
 
     /**
      * 读取用户名
      */
-    fun readUserName(): String {
-        return mmkv.decodeString("user_name", "") ?: ""
+    fun readUserAvatar(): String {
+        return mmkv.decodeString("user_avatar", "") ?: ""
     }
 
     /**