Quellcode durchsuchen

1. 优化作业流程
2. 支持发布作业

bjb vor 4 Monaten
Ursprung
Commit
adf294bc8b
27 geänderte Dateien mit 708 neuen und 132 gelöschten Zeilen
  1. 2 0
      app/build.gradle.kts
  2. 5 0
      app/src/main/AndroidManifest.xml
  3. 83 31
      app/src/main/java/com/iscs/bozzys/api/ApiBean.kt
  4. 23 0
      app/src/main/java/com/iscs/bozzys/api/ApiRequest.kt
  5. 30 1
      app/src/main/java/com/iscs/bozzys/api/ApiService.kt
  6. 1 1
      app/src/main/java/com/iscs/bozzys/ui/pages/compose/FormCompose.kt
  7. 1 1
      app/src/main/java/com/iscs/bozzys/ui/pages/compose/JobListItem.kt
  8. 2 2
      app/src/main/java/com/iscs/bozzys/ui/pages/compose/TaskListItem.kt
  9. 6 1
      app/src/main/java/com/iscs/bozzys/ui/pages/create/job/PageCreateJob.kt
  10. 67 0
      app/src/main/java/com/iscs/bozzys/ui/pages/create/job/PagePushJob.kt
  11. 2 2
      app/src/main/java/com/iscs/bozzys/ui/pages/detail/task/PageDetailTask.kt
  12. 2 0
      app/src/main/java/com/iscs/bozzys/ui/pages/edit/step/PageEditStep.kt
  13. 20 11
      app/src/main/java/com/iscs/bozzys/ui/pages/home/HomeCompose.kt
  14. 55 23
      app/src/main/java/com/iscs/bozzys/ui/pages/home/JobsCompose.kt
  15. 200 6
      app/src/main/java/com/iscs/bozzys/ui/pages/home/MyCompose.kt
  16. 1 1
      app/src/main/java/com/iscs/bozzys/ui/pages/home/PageHome.kt
  17. 32 17
      app/src/main/java/com/iscs/bozzys/ui/pages/home/TasksCompose.kt
  18. 4 4
      app/src/main/java/com/iscs/bozzys/ui/pages/vm/VMDetailTask.kt
  19. 9 3
      app/src/main/java/com/iscs/bozzys/ui/pages/vm/VMEditStep.kt
  20. 65 27
      app/src/main/java/com/iscs/bozzys/ui/pages/vm/VMHome.kt
  21. 7 1
      app/src/main/java/com/iscs/bozzys/ui/pages/vm/VMLogin.kt
  22. 38 0
      app/src/main/java/com/iscs/bozzys/ui/pages/vm/VMPushJob.kt
  23. 18 0
      app/src/main/java/com/iscs/bozzys/utils/Exts.kt
  24. 15 0
      app/src/main/java/com/iscs/bozzys/utils/Storage.kt
  25. 9 0
      app/src/main/res/drawable/camera.xml
  26. 9 0
      app/src/main/res/drawable/logout.xml
  27. 2 0
      gradle/libs.versions.toml

+ 2 - 0
app/build.gradle.kts

@@ -85,6 +85,8 @@ dependencies {
     // 日期和时间选择器 下面第二行的依赖是为了引入Dialog
     implementation(libs.third.date.picker)
     implementation("com.google.android.material:material:1.1.0")
+    // 图片加载
+    implementation(libs.third.image.load)
 
     implementation(libs.androidx.ui)
     implementation(libs.androidx.ui.graphics)

+ 5 - 0
app/src/main/AndroidManifest.xml

@@ -70,6 +70,11 @@
             android:name=".ui.pages.message.PageMessage"
             android:exported="true"
             android:screenOrientation="portrait" />
+        <!--  发布作业  -->
+        <activity
+            android:name=".ui.pages.create.job.PagePushJob"
+            android:exported="true"
+            android:screenOrientation="portrait" />
         <!--  阿里消息推送服务配置  -->
         <service
             android:name=".service.AliPushService"

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

@@ -3,6 +3,7 @@ package com.iscs.bozzys.api
 import android.util.Log
 import androidx.compose.runtime.snapshots.SnapshotStateMap
 import androidx.compose.ui.geometry.Offset
+import com.google.gson.Gson
 import com.iscs.bozzys.ui.pages.edit.step.compose.Anchor
 import com.iscs.bozzys.ui.pages.edit.step.compose.Connection
 import com.iscs.bozzys.ui.pages.edit.step.compose.NodeUI
@@ -46,6 +47,14 @@ class LoginRsp(
  */
 class PageRsp<T>(val total: Int, val list: List<T>)
 
+/**
+ * 获取用户角色
+ *
+ * @param user  用户信息
+ * @param roles 角色信息
+ */
+class PermissionRsp(val user: User, val roles: List<String>)
+
 /**
  * 任务统计
  */
@@ -63,25 +72,25 @@ data class IsolationPoint(val id: Int, val pointName: String, val pointNfc: Stri
  */
 @Serializable
 data class User(
-    val id: Int,
-    val userId: Int?,
-    val username: String,
-    val nickname: String,
-    val cardNfc: String?,
-    val remark: String,
-    val deptId: Int,
-    val deptName: String?,
-    val postIds: List<Int>,
-    val email: String?,
-    val mobile: String?,
-    val sex: Int,
-    val avatar: String,
-    val status: Int,
-    val loginIp: String,
-    val loginDate: Long,
-    val createTime: Long,
-    val workstationIds: List<Int>?,
-    val type: String?,
+    val id: Int = 0,
+    val userId: Int? = null,
+    val username: String = "",
+    val nickname: String = "",
+    val cardNfc: String? = null,
+    val remark: String = "",
+    val deptId: Int = 0,
+    val deptName: String? = null,
+    val postIds: List<Int> = listOf(),
+    val email: String? = null,
+    val mobile: String? = null,
+    val sex: Int = 0,
+    val avatar: String = "",
+    val status: Int = 0,
+    val loginIp: String = "",
+    val loginDate: Long = 0L,
+    val createTime: Long = 0L,
+    val workstationIds: List<Int>? = null,
+    val type: String? = "",
 )
 
 /**
@@ -140,7 +149,7 @@ data class Node(
      */
     fun checkCanSubmit(forms: List<FormField>): String {
         forms.forEach {
-            Log.d("xiaoming","检查表单:$it")
+            Log.d("NodeOption", "checkCanSubmit -> $it")
             if (it.required) {
                 if ((it.value.getOrNull(0) ?: "").isEmpty()) {
                     return (it.placeholder.getOrNull(0) ?: "").ifEmpty { "请填写${it.label}" }
@@ -202,9 +211,17 @@ data class Node(
     fun updateNodeInfo(forms: List<FormField>): Node {
         val nodeName = forms.find { it.name == "nodeName" }?.value?.getOrNull(0) ?: ""
         if (type != "createJob") {
+            // 解析负责人
             val workerUserId = forms.find { it.name == "workerUserId" }?.value?.getOrNull(0) ?: "0"
+            // 解析关联表单
             val formId = forms.find { it.name == "formId" }?.value?.getOrNull(0) ?: "0"
+            // 解析表单数据
             val formData = forms.find { it.name == "formData" }?.value?.getOrNull(0) ?: ""
+            // 解析隔离方式
+            val isolationType = forms.find { it.name == "isolationType" }?.value?.getOrNull(0)
+            // 解析节点
+            val isolationPoints = forms.find { it.name == "isolationPoints" }?.value ?: emptyList()
+
             return Node(
                 id = id,
                 workId = workId,
@@ -216,7 +233,13 @@ data class Node(
                 position = position,
                 workerUserId = workerUserId.toInt(),
                 formId = formId.toInt(),
-                formData = formData
+                formData = formData,
+                isolationType = isolationType,
+                isolationPoints = isolationPoints.joinToString(
+                    prefix = "[",
+                    postfix = "]",
+                    separator = ","
+                )
             )
         }
         return Node(
@@ -251,7 +274,13 @@ data class Node(
         isolationList: List<IsolationPoint>,
         isLocker: Boolean = false,
     ): List<FormField> {
+        Log.d("NodeOption", "buildFormList -> $this")
         val list = ArrayList<FormField>()
+        val gson = Gson()
+        // 暂不解析,等后端确认后再解析
+        // val dataMap: MutableMap<String, Any?> = gson.fromJson(data.ifEmpty { "{}" }, object : TypeToken<MutableMap<String, Any?>>() {}.type)
+        // 携带数据
+        // Log.d("NodeOption", "dataMap -> ${gson.toJson(dataMap)}")
         // 添加节点名称
         list += FormField(
             id.toString(),
@@ -282,7 +311,7 @@ data class Node(
             name = "formId",
             true,
             placeholder = listOf("请选择业务表单"),
-            options = workFormList.map { FormOption(it.name, it.id.toString()) },
+            options = workFormList.map { FormOption(it.name?:"", it.id.toString()) },
             value = if (formId > 0) listOf(formId.toString()) else listOf()
         )
         // 隔离配置
@@ -293,6 +322,12 @@ data class Node(
             val formField = list.find { it.name == "formId" }?.copy(required = false)
             list.removeIf { it.name == "formId" }
             formField?.let { list.add(it) }
+            // 抽取隔离方式value值
+            val valueOfIsolationType = if (isolationType.isNullOrEmpty()) {
+                listOf()
+//                val type = dataMap["isolationMethod"]
+//                if (type == null) listOf() else listOf("$type")
+            } else listOf(isolationType)
             // 隔离方式
             list += FormField(
                 id.toString(),
@@ -302,9 +337,25 @@ data class Node(
                 true,
                 placeholder = listOf("请选择隔离方式"),
                 options = isolationMethodList.map { FormOption(it.label, it.value) },
-                value = if (isolationType.isNullOrEmpty()) listOf() else listOf(isolationType)
+                value = valueOfIsolationType
             )
+            // 抽取隔离点位value值
             val points = if (isolationPoints.isNullOrEmpty()) "" else isolationPoints.replace("[", "").replace("]", "")
+            val valueOfIsolationPoints = if (points.isNotEmpty()) {
+                points.split(",")
+            } else {
+                listOf()
+//                when (val value = dataMap["isolationPoints"]) {
+//                    // 如果是数组
+//                    is List<*> -> value.map { "$it".toFloat().toInt().toString() }
+//                    // 如果是String
+//                    is String -> {
+//                        value.replace("[", "").replace("]", "").split(",")
+//                    }
+//                    // 其他不处理
+//                    else -> listOf()
+//                }
+            }
             // 隔离点选择,可多选
             list += FormField(
                 id.toString(),
@@ -315,7 +366,7 @@ data class Node(
                 listOf("请选择隔离点"),
                 multiSelect = true,
                 options = isolationList.map { FormOption(it.pointName, it.id.toString()) },
-                value = if (points.isNotEmpty()) points.split(",") else listOf()
+                value = valueOfIsolationPoints
             )
             if (isLocker) {
                 val locker = nodeUserList?.find { it.type == "jtlocker" }
@@ -396,12 +447,12 @@ data class Node(
 @Serializable
 data class TaskFormInfo(
     val id: Int = 0,
-    val name: String = "",
-    val conf: String = "",
-    val fields: List<String> = emptyList(),
-    val status: Int = -1,
-    val remark: String = "",
-    val createTime: Long = 0L
+    val name: String? = null,
+    val conf: String? = null,
+    val fields: List<String>? = null,
+    val status: Int? = null,
+    val remark: String? = null,
+    val createTime: Long? = null
 )
 
 /**
@@ -489,7 +540,8 @@ data class Task(
     val completionTime: Long?,
     val cancellationTime: Long?,
     val cancellationReason: String?,
-    val workerUserName: String?,
+    val initiatorName: String?,     // 发起人
+    val workerUserName: String?,    // 当前执行人
     val workTime: Long?,
     val currentNodeId: String?,
     val currentNodeName: String?,

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

@@ -112,6 +112,13 @@ object ApiRequest {
         return requestApi { api.login(params) }
     }
 
+    /**
+     * 退出登录操作
+     */
+    suspend fun logout(): Result<Response<Boolean>> {
+        return requestApi { api.logout() }
+    }
+
     /**
      * 刷新Token操作
      *
@@ -121,6 +128,13 @@ object ApiRequest {
         return requestApi { api.refreshToken(refreshToken) }
     }
 
+    /**
+     * 获取用户权限信息
+     */
+    suspend fun getUserPermission(): Result<Response<PermissionRsp>> {
+        return requestApi { api.getUserPermission() }
+    }
+
     /**
      * 查询作业列表数据
      *
@@ -191,6 +205,15 @@ object ApiRequest {
         return requestApi { api.createJob(params) }
     }
 
+    /**
+     * 发布作业
+     *
+     * @param params 请求参数
+     */
+    suspend fun pushJob(params: MutableMap<String, Any>): Result<Response<Boolean>> {
+        return requestApi { api.pushJob(params) }
+    }
+
     /**
      * 查询作业详情
      *

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

@@ -25,6 +25,13 @@ interface ApiService {
         @HeaderMap headers: Map<String, String> = ApiRequest.getUserHeaders()
     ): Response<LoginRsp>
 
+    /**
+     * 退出登录
+     */
+    @Headers("Content-Type: application/json")
+    @POST("/admin-api/system/auth/logout")
+    suspend fun logout(@HeaderMap headers: Map<String, String> = ApiRequest.getUserHeaders()): Response<Boolean>
+
     /**
      * 刷新Token使用
      */
@@ -35,11 +42,23 @@ interface ApiService {
         @HeaderMap headers: Map<String, String> = ApiRequest.getUserHeaders()
     ): Response<LoginRsp>
 
+    /**
+     * 获取用户权限信息
+     */
+    @Headers("Content-Type: application/x-www-form-urlencoded")
+    @GET("/admin-api/system/auth/get-permission-info")
+    suspend fun getUserPermission(
+        @HeaderMap headers: Map<String, String> = ApiRequest.getUserHeaders()
+    ): Response<PermissionRsp>
+
     /**
      * 获取作业列表数据
+     *
+     * app /admin-api/adroid/iscs/workflow/getAppMyTicketPage
+     * web /admin-api/iscs/workflow-work/getWorkflowWorkPage
      */
     @Headers("Content-Type: application/x-www-form-urlencoded")
-    @GET("/admin-api/iscs/workflow-work/getWorkflowWorkPage")
+    @GET("/admin-api/adroid/iscs/workflow/getAppMyTicketPage")
     suspend fun getJobs(
         @QueryMap params: MutableMap<String, Any>,
         @HeaderMap headers: Map<String, String> = ApiRequest.getUserHeaders()
@@ -114,6 +133,16 @@ interface ApiService {
         @HeaderMap headers: Map<String, String> = ApiRequest.getUserHeaders()
     ): Response<Int>
 
+    /**
+     * 发布作业
+     */
+    @Headers("Content-Type: application/json")
+    @POST("/admin-api/iscs/workflow-work/updateStartWork")
+    suspend fun pushJob(
+        @Body body: MutableMap<String, Any>,
+        @HeaderMap headers: Map<String, String> = ApiRequest.getUserHeaders()
+    ): Response<Boolean>
+
     /**
      * 查询作业详情
      */

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

@@ -436,7 +436,7 @@ fun FormSelect(
                                 values.clear()
                                 values.add(option.value)
                             }
-                            onSelectChange(values)
+                            onSelectChange(ArrayList(values))
                             if (!multiSelect) expanded = false
                         }
                     )

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

@@ -62,7 +62,7 @@ fun JobListItem(job: Job) {
                     .padding(end = 10.dp)
                     .weight(1f)
             )
-            Text(
+            if (job.urgencyLevel != "0") Text(
                 levelInfo.first, modifier = Modifier
                     .height(24.dp)
                     .clip(RoundedCornerShape(12.dp))

+ 2 - 2
app/src/main/java/com/iscs/bozzys/ui/pages/compose/TaskListItem.kt

@@ -53,7 +53,7 @@ fun TaskListItem(task: Task) {
         Row {
             Text(task.name, fontSize = 15.sp, fontWeight = FontWeight.Medium, color = Text)
             Spacer(Modifier.weight(1f))
-            Text(
+            if (task.urgencyLevel != "0") Text(
                 levelInfo.first, modifier = Modifier
                     .height(24.dp)
                     .clip(RoundedCornerShape(12.dp))
@@ -141,7 +141,7 @@ private fun getLevelNameAndColor(level: String): Pair<String, Color> {
 /**
  * 获取任务状态名称
  */
- fun getTaskStatusName(approvalStatus: String): String {
+fun getTaskStatusName(approvalStatus: String): String {
     return when (approvalStatus) {
         "approved" -> "已审核"
         "running" -> "未审核"

+ 6 - 1
app/src/main/java/com/iscs/bozzys/ui/pages/create/job/PageCreateJob.kt

@@ -82,7 +82,12 @@ class PageCreateJob : PageBase() {
                     .padding(top = 15.dp, bottom = (15f + pv.calculateBottomPadding().value).dp)
             ) {
                 Button(
-                    onClick = { vm.onNext { openPageEditStep(it) } }, modifier = Modifier
+                    onClick = {
+                        vm.onNext {
+                            openPageEditStep(it)
+                            destroyDelay(500)
+                        }
+                    }, modifier = Modifier
                         .padding(horizontal = 16.dp)
                         .fillMaxWidth()
                         .height(50.dp)

+ 67 - 0
app/src/main/java/com/iscs/bozzys/ui/pages/create/job/PagePushJob.kt

@@ -0,0 +1,67 @@
+package com.iscs.bozzys.ui.pages.create.job
+
+import android.content.Context
+import android.content.Intent
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+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.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.iscs.bozzys.ui.common.PageBase
+import com.iscs.bozzys.ui.common.Title
+import com.iscs.bozzys.ui.pages.vm.VMPushJob
+
+/**
+ * 打开发布作业
+ */
+fun Context.openPagePushJob(id: Int) {
+    startActivity(Intent(this, PagePushJob::class.java).apply {
+        putExtra("id", id)
+    })
+}
+
+/**
+ * 作业发布操作
+ */
+class PagePushJob : PageBase() {
+
+    @Composable
+    override fun GetViews(pv: PaddingValues) {
+        val vm: VMPushJob = viewModel()
+        val id = intent.getIntExtra("id", 0)
+        LaunchedEffect(Unit) {
+            vm.toast.showToast()
+            vm.loading.showLoading()
+        }
+        Column(Modifier.fillMaxSize()) {
+            Title(pv, "发布作业")
+            Spacer(modifier = Modifier.weight(1f))
+            Button(
+                onClick = {
+                    vm.onPush(id) {
+                        destroyDelay(1000)
+                    }
+                }, modifier = Modifier
+                    .padding(horizontal = 16.dp)
+                    .padding(bottom = 10.dp)
+                    .fillMaxWidth()
+                    .height(46.dp),
+                shape = RoundedCornerShape(6.dp)
+            ) {
+                Text("发布作业", fontSize = 16.sp, fontWeight = FontWeight.Bold)
+            }
+        }
+    }
+}

+ 2 - 2
app/src/main/java/com/iscs/bozzys/ui/pages/detail/task/PageDetailTask.kt

@@ -171,7 +171,7 @@ class PageDetailTask : PageBase() {
                         Text("车间A区", fontSize = 12.sp, color = Text)
                     }
                     // 负责人
-                    if (!task.workerUserName.isNullOrEmpty()) Row(
+                    if (!task.initiatorName.isNullOrEmpty()) Row(
                         modifier = Modifier
                             .padding(horizontal = 3.dp, vertical = 3.dp)
                             .clip(RoundedCornerShape(50))
@@ -187,7 +187,7 @@ class PageDetailTask : PageBase() {
                                 .padding(end = 3.dp)
                                 .size(14.dp)
                         )
-                        Text(task.workerUserName, fontSize = 12.sp, color = Text)
+                        Text(task.initiatorName, fontSize = 12.sp, color = Text)
                     }
                     // 任务状态
                     Row(

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

@@ -62,6 +62,7 @@ import com.iscs.bozzys.ui.common.TipsDialog
 import com.iscs.bozzys.ui.common.Title
 import com.iscs.bozzys.ui.pages.compose.CardContainer
 import com.iscs.bozzys.ui.pages.compose.FormContainer
+import com.iscs.bozzys.ui.pages.create.job.openPagePushJob
 import com.iscs.bozzys.ui.pages.edit.step.compose.NodeItem
 import com.iscs.bozzys.ui.pages.edit.step.compose.ZoomPanContainer
 import com.iscs.bozzys.ui.pages.vm.VMEditStep
@@ -240,6 +241,7 @@ class PageEditStep : PageBase() {
             vm.hideTipsDialog()
         }, onConfirm = {
             vm.hideTipsDialog()
+            openPagePushJob(intent.getIntExtra("job_id", 0))
             // 这里需要跳转到下一个页面,且关闭当前页面
             destroyDelay(500)
         })

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

@@ -46,12 +46,14 @@ import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 import androidx.compose.ui.zIndex
+import coil.compose.AsyncImage
 import com.iscs.bozzys.R
 import com.iscs.bozzys.api.Message
 import com.iscs.bozzys.ui.pages.compose.TaskListItem
 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.getRoleName
 
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
@@ -99,19 +101,26 @@ private fun TopToolBar(pv: PaddingValues, vmHome: VMHome) {
                 .padding(start = 16.dp, end = 10.dp),
             verticalAlignment = Alignment.CenterVertically
         ) {
-            Icon(
-                painter = painterResource(R.drawable.user),
+            Box(
                 modifier = Modifier
-                    .size(36.dp)
-                    .clip(RoundedCornerShape(50))
-                    .background(Color(0xFFFFD700))
-                    .padding(10.dp),
-                contentDescription = null,
-                tint = Color.White
-            )
+                    .size(40.dp)
+                    .clip(RoundedCornerShape(50)),
+                contentAlignment = Alignment.Center
+            ) {
+                Icon(
+                    painter = painterResource(R.drawable.user),
+                    modifier = Modifier
+                        .fillMaxSize()
+                        .background(Color(0xFFFFD700))
+                        .padding(10.dp),
+                    contentDescription = null,
+                    tint = Color.White
+                )
+                AsyncImage(model = state.user.avatar, contentDescription = null, modifier = Modifier.fillMaxSize())
+            }
             Column(Modifier.padding(horizontal = 10.dp)) {
-                Text(state.username, fontSize = 16.sp, lineHeight = 16.sp, fontWeight = FontWeight.Medium, color = Color.White)
-                Text("操作员 电气维护组", fontSize = 12.sp, lineHeight = 12.sp, color = Color.White.copy(alpha = 0.8f))
+                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))
             }
             Spacer(Modifier.weight(1f))
             Box(modifier = Modifier.fillMaxHeight(), contentAlignment = Alignment.Center) {

+ 55 - 23
app/src/main/java/com/iscs/bozzys/ui/pages/home/JobsCompose.kt

@@ -17,6 +17,8 @@ import androidx.compose.foundation.lazy.items
 import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.Icon
 import androidx.compose.material3.LocalTextStyle
@@ -37,8 +39,10 @@ import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.SolidColor
 import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
 import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.ImeAction
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 import androidx.compose.ui.zIndex
@@ -46,7 +50,8 @@ import com.iscs.bozzys.R
 import com.iscs.bozzys.ui.common.Empty
 import com.iscs.bozzys.ui.pages.compose.JobListItem
 import com.iscs.bozzys.ui.pages.create.job.openPageCreateJob
-import com.iscs.bozzys.ui.pages.edit.step.openPageEditStep
+import com.iscs.bozzys.ui.pages.create.job.openPagePushJob
+import com.iscs.bozzys.ui.pages.vm.StatePage
 import com.iscs.bozzys.ui.pages.vm.VMHome
 import com.iscs.bozzys.ui.theme.Main
 import com.iscs.bozzys.ui.theme.Text
@@ -77,8 +82,10 @@ fun JobsCompose(pv: PaddingValues, zIndex: Float, vm: VMHome) {
  * 顶部工具栏
  */
 @Composable
-private fun TopToolBar(pv: PaddingValues, vmHome: VMHome) {
+private fun TopToolBar(pv: PaddingValues, vm: VMHome) {
+    val state by vm.state.collectAsState()
     val ctx = LocalContext.current
+    val keyboard = LocalSoftwareKeyboardController.current
     var keywords by remember { mutableStateOf("") }
     Column(
         modifier = Modifier
@@ -94,22 +101,29 @@ private fun TopToolBar(pv: PaddingValues, vmHome: VMHome) {
         ) {
             Text("我的作业", fontSize = 18.sp, color = Color.White, fontWeight = FontWeight.Medium)
             Spacer(Modifier.weight(1f))
-            Icon(
-                painter = painterResource(R.drawable.add),
-                contentDescription = null,
-                modifier = Modifier
-                    .size(36.dp)
-                    .clip(RoundedCornerShape(6.dp))
-                    .clickable(onClick = {
-                        // 前期SOP没有做流程选择,这里先直接跳转到创建页面
-                        // ctx.openPageSelectJobType()
-                        ctx.openPageCreateJob()
-                        // 测试填写表单数据
-                        // ctx.openPageEditStep(36)
-                    })
-                    .padding(6.dp),
-                tint = Color.White
-            )
+            if (state.roles.contains("super_admin")) {
+                Icon(
+                    painter = painterResource(R.drawable.add),
+                    contentDescription = null,
+                    modifier = Modifier
+                        .size(36.dp)
+                        .clip(RoundedCornerShape(6.dp))
+                        .clickable(onClick = {
+                            // 前期SOP没有做流程选择,这里先直接跳转到创建页面
+                            // ctx.openPageSelectJobType()
+                            ctx.openPageCreateJob()
+                            // 测试流程模板编辑数据
+                            // ctx.openPageEditStep(34)
+                            // 测试发布作业
+                            ctx.openPagePushJob(66)
+                        })
+                        .padding(6.dp),
+                    tint = Color.White
+                )
+            } else {
+                Spacer(modifier = Modifier.size(36.dp))
+            }
+
         }
         Row(
             Modifier
@@ -131,7 +145,7 @@ private fun TopToolBar(pv: PaddingValues, vmHome: VMHome) {
                     Box(contentAlignment = Alignment.CenterStart) {
                         innerTextField()
                         if (keywords.isEmpty()) {
-                            val text = "搜索作业票、作业名称..."
+                            val text = "搜索作业名称或作业编号..."
                             Text(
                                 text,
                                 color = Color.White.copy(alpha = 0.8f),
@@ -142,6 +156,11 @@ private fun TopToolBar(pv: PaddingValues, vmHome: VMHome) {
                     }
                 },
                 cursorBrush = SolidColor(Color.White),
+                keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
+                keyboardActions = KeyboardActions(onSearch = {
+                    keyboard?.hide()
+                    vm.getJobList(StatePage(page = 1, type = "", keywords = keywords))
+                })
             )
         }
     }
@@ -152,6 +171,7 @@ private fun TopToolBar(pv: PaddingValues, vmHome: VMHome) {
  */
 @Composable
 private fun FilterBar(vm: VMHome) {
+    val keyboard = LocalSoftwareKeyboardController.current
     val state by vm.state.collectAsState()
     Row(
         modifier = Modifier
@@ -166,7 +186,10 @@ private fun FilterBar(vm: VMHome) {
                 .padding(end = 10.dp)
                 .clip(RoundedCornerShape(50))
                 .background(color = if (state.jobPage.type == "running") Main else Color(0xFFF0F0F0))
-                .clickable(onClick = { vm.getJobList(state.jobPage.copy(page = 1, type = "running")) })
+                .clickable(onClick = {
+                    keyboard?.hide()
+                    vm.getJobList(state.jobPage.copy(page = 1, type = "running", keywords = ""))
+                })
                 .padding(horizontal = 12.dp, vertical = 5.dp),
         )
         Text(
@@ -175,7 +198,10 @@ private fun FilterBar(vm: VMHome) {
                 .padding(end = 10.dp)
                 .clip(RoundedCornerShape(50))
                 .background(color = if (state.jobPage.type == "completed") Main else Color(0xFFF0F0F0))
-                .clickable(onClick = { vm.getJobList(state.jobPage.copy(page = 1, type = "completed")) })
+                .clickable(onClick = {
+                    keyboard?.hide()
+                    vm.getJobList(state.jobPage.copy(page = 1, type = "completed", keywords = ""))
+                })
                 .padding(horizontal = 12.dp, vertical = 5.dp),
         )
         Text(
@@ -184,7 +210,10 @@ private fun FilterBar(vm: VMHome) {
                 .padding(end = 10.dp)
                 .clip(RoundedCornerShape(50))
                 .background(color = if (state.jobPage.type == "cancelled") Main else Color(0xFFF0F0F0))
-                .clickable(onClick = { vm.getJobList(state.jobPage.copy(page = 1, type = "cancelled")) })
+                .clickable(onClick = {
+                    keyboard?.hide()
+                    vm.getJobList(state.jobPage.copy(page = 1, type = "cancelled", keywords = ""))
+                })
                 .padding(horizontal = 12.dp, vertical = 5.dp),
         )
         Text(
@@ -193,7 +222,10 @@ private fun FilterBar(vm: VMHome) {
                 .padding(end = 10.dp)
                 .clip(RoundedCornerShape(50))
                 .background(color = if (state.jobPage.type == "") Main else Color(0xFFF0F0F0))
-                .clickable(onClick = { vm.getJobList(state.jobPage.copy(page = 1, type = "")) })
+                .clickable(onClick = {
+                    keyboard?.hide()
+                    vm.getJobList(state.jobPage.copy(page = 1, type = "", keywords = ""))
+                })
                 .padding(horizontal = 12.dp, vertical = 5.dp),
         )
     }

+ 200 - 6
app/src/main/java/com/iscs/bozzys/ui/pages/home/MyCompose.kt

@@ -1,29 +1,223 @@
 package com.iscs.bozzys.ui.pages.home
 
 import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.PaddingValues
+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.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
 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.geometry.Offset
+import androidx.compose.ui.graphics.Brush
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
 import androidx.compose.ui.zIndex
+import coil.compose.AsyncImage
+import com.iscs.bozzys.R
+import com.iscs.bozzys.ui.common.PageBase
+import com.iscs.bozzys.ui.common.TipsDialog
+import com.iscs.bozzys.ui.pages.compose.CardContainer
+import com.iscs.bozzys.ui.pages.login.openPageLogin
+import com.iscs.bozzys.ui.pages.vm.VMHome
+import com.iscs.bozzys.ui.theme.Text
+import com.iscs.bozzys.utils.getRoleName
 
 @Composable
-fun MyCompose(pv: PaddingValues, zIndex: Float) {
+fun MyCompose(pv: PaddingValues, zIndex: Float, vm: VMHome) {
+    var showExitDialog by remember { mutableStateOf(false) }
+    val ctx = LocalContext.current
     Column(
         modifier = Modifier
             .fillMaxSize()
             .zIndex(zIndex)
-            .background(Color.White)
+            .background(MaterialTheme.colorScheme.background)
+            .padding(bottom = pv.calculateBottomPadding())
             .pointerInput(Unit) {},
-        verticalArrangement = Arrangement.Center,
-        horizontalAlignment = Alignment.CenterHorizontally
     ) {
-        Text("设置")
+        // 顶部状态栏
+        TopToolBar(pv, vm)
+        // 用户信息
+        UserInfo(vm)
+        Spacer(modifier = Modifier.weight(1f))
+        // 底部退出登录
+        Button(
+            { showExitDialog = true },
+            shape = RoundedCornerShape(6.dp),
+            modifier = Modifier
+                .padding(horizontal = 20.dp, vertical = 10.dp)
+                .fillMaxWidth()
+                .height(46.dp),
+            colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFFA666C))
+        ) {
+            Row(verticalAlignment = Alignment.CenterVertically) {
+                Text("退出登录", fontSize = 18.sp, fontWeight = FontWeight.Bold)
+                Icon(
+                    painter = painterResource(R.drawable.logout), contentDescription = null, modifier = Modifier
+                        .padding(start = 5.dp)
+                        .size(24.dp)
+                )
+            }
+        }
+        TipsDialog(show = showExitDialog, content = "确定要退出登录吗?", onCancel = { showExitDialog = false }, onConfirm = {
+            showExitDialog = false
+            vm.logout {
+                ctx.openPageLogin()
+                if (ctx is PageBase) ctx.destroyDelay(500)
+            }
+        })
+    }
+}
+
+/**
+ * 顶部工具栏
+ */
+@Composable
+private fun TopToolBar(pv: PaddingValues, vm: VMHome) {
+    val ctx = LocalContext.current
+    val state by vm.state.collectAsState()
+    Column(
+        modifier = Modifier
+            .fillMaxWidth()
+            .background(brush = Brush.horizontalGradient(listOf(Color(0xFFFF8C00), Color(0xFFFFA500))))
+            .padding(top = pv.calculateTopPadding())
+    ) {
+        Row(
+            modifier = Modifier
+                .fillMaxWidth()
+                .height(66.dp)
+                .padding(start = 16.dp, end = 10.dp),
+            verticalAlignment = Alignment.CenterVertically
+        ) {
+            Box(
+                modifier = Modifier
+                    .size(40.dp)
+                    .clip(RoundedCornerShape(50)),
+                contentAlignment = Alignment.Center
+            ) {
+                Icon(
+                    painter = painterResource(R.drawable.user),
+                    modifier = Modifier
+                        .fillMaxSize()
+                        .background(Color(0xFFFFD700))
+                        .padding(10.dp),
+                    contentDescription = null,
+                    tint = Color.White
+                )
+                AsyncImage(model = state.user.avatar, contentDescription = null, modifier = Modifier.fillMaxSize())
+            }
+            Column(Modifier.padding(horizontal = 10.dp)) {
+                Text("设置", fontSize = 16.sp, lineHeight = 16.sp, fontWeight = FontWeight.Medium, color = Color.White)
+                Text("个人中心", fontSize = 12.sp, lineHeight = 12.sp, color = Color.White.copy(alpha = 0.8f))
+            }
+            Spacer(Modifier.weight(1f))
+        }
+    }
+}
+
+/**
+ * 用户信息
+ */
+@Composable
+private fun UserInfo(vm: VMHome) {
+    val state by vm.state.collectAsState()
+    CardContainer(modifier = Modifier.padding(16.dp)) {
+        Row(
+            modifier = Modifier
+                .fillMaxWidth()
+                .height(90.dp)
+                .background(brush = Brush.horizontalGradient(listOf(Color(0xFFFFF9E5), Color(0xFFFFFEFB))))
+                .padding(horizontal = 20.dp),
+            verticalAlignment = Alignment.CenterVertically
+        ) {
+            Box(
+                modifier = Modifier
+                    .size(60.dp)
+                    .background(
+                        brush = Brush.linearGradient(
+                            colors = listOf(Color(0xFFFFAE00), Color(0xFFF7C700)),
+                            start = Offset(0f, 0f),
+                            end = Offset.Infinite
+                        ),
+                        shape = RoundedCornerShape(50)
+                    ),
+                contentAlignment = Alignment.Center
+            ) {
+                Text("${state.user.nickname.ifEmpty { "P" }[0]}", color = Text, fontWeight = FontWeight.Bold)
+                AsyncImage(
+                    model = state.user.avatar,
+                    contentDescription = null,
+                    modifier = Modifier
+                        .fillMaxSize()
+                        .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()
+                    )
+                }
+            }
+            Column(
+                modifier = Modifier
+                    .padding(horizontal = 10.dp)
+                    .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
+                )
+            }
+            Icon(
+                painter = painterResource(R.drawable.back),
+                contentDescription = null,
+                modifier = Modifier
+                    .size(18.dp)
+                    .rotate(180f),
+                tint = Color(0xFF999999)
+            )
+        }
     }
 }

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

@@ -80,7 +80,7 @@ class PageHome : PageBase() {
                         0 -> HomeCompose(pv, if (state.navigationId == 0) 99f else 0f, vm)
                         1 -> JobsCompose(pv, if (state.navigationId == 1) 99f else 0f, vm)
                         2 -> TasksCompose(pv, if (state.navigationId == 2) 99f else 0f, vm)
-                        3 -> MyCompose(pv, if (state.navigationId == 3) 99f else 0f)
+                        3 -> MyCompose(pv, if (state.navigationId == 3) 99f else 0f, vm)
                     }
                 }
 

+ 32 - 17
app/src/main/java/com/iscs/bozzys/ui/pages/home/TasksCompose.kt

@@ -16,6 +16,8 @@ import androidx.compose.foundation.lazy.items
 import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.LocalTextStyle
 import androidx.compose.material3.Text
@@ -34,8 +36,9 @@ import androidx.compose.ui.graphics.Brush
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.SolidColor
 import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
 import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.ImeAction
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 import androidx.compose.ui.zIndex
@@ -74,7 +77,7 @@ fun TasksCompose(pv: PaddingValues, zIndex: Float, vm: VMHome) {
  */
 @Composable
 private fun TopToolBar(pv: PaddingValues, vm: VMHome) {
-    val ctx = LocalContext.current
+    val keyboard = LocalSoftwareKeyboardController.current
     var keywords by remember { mutableStateOf("") }
     Column(
         modifier = Modifier
@@ -94,18 +97,6 @@ private fun TopToolBar(pv: PaddingValues, vm: VMHome) {
                     .weight(1f)
                     .height(36.dp)
             )
-
-            // 任务没有新建,只有查看
-//            Icon(
-//                painter = painterResource(R.drawable.add),
-//                contentDescription = null,
-//                modifier = Modifier
-//                    .size(36.dp)
-//                    .clip(RoundedCornerShape(6.dp))
-//                    .clickable(onClick = { ctx.openPageSelectJobType() })
-//                    .padding(6.dp),
-//                tint = Color.White
-//            )
         }
         Row(
             Modifier
@@ -127,7 +118,7 @@ private fun TopToolBar(pv: PaddingValues, vm: VMHome) {
                     Box(contentAlignment = Alignment.CenterStart) {
                         innerTextField()
                         if (keywords.isEmpty()) {
-                            val text = "搜索任务名称..."
+                            val text = "搜索任务名称或任务编号..."
                             Text(
                                 text,
                                 color = Color.White.copy(alpha = 0.8f),
@@ -138,6 +129,11 @@ private fun TopToolBar(pv: PaddingValues, vm: VMHome) {
                     }
                 },
                 cursorBrush = SolidColor(Color.White),
+                keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
+                keyboardActions = KeyboardActions(onSearch = {
+                    keyboard?.hide()
+                    vm.getTaskList(StatePage(page = 1, type = "", keywords = keywords))
+                })
             )
         }
     }
@@ -148,6 +144,7 @@ private fun TopToolBar(pv: PaddingValues, vm: VMHome) {
  */
 @Composable
 private fun FilterBar(vm: VMHome) {
+    val keyboard = LocalSoftwareKeyboardController.current
     val state by vm.state.collectAsState()
     Row(
         modifier = Modifier
@@ -164,7 +161,10 @@ private fun FilterBar(vm: VMHome) {
                 .background(
                     color = if (state.taskPage.type == "running") Main else Color(0xFFF0F0F0)
                 )
-                .clickable(onClick = { vm.getTaskList(StatePage(type = "running", page = 1)) })
+                .clickable(onClick = {
+                    keyboard?.hide()
+                    vm.getTaskList(StatePage(type = "running", page = 1, keywords = ""))
+                })
                 .padding(horizontal = 12.dp, vertical = 5.dp),
         )
         Text(
@@ -173,7 +173,22 @@ private fun FilterBar(vm: VMHome) {
                 .padding(end = 10.dp)
                 .clip(RoundedCornerShape(50))
                 .background(color = if (state.taskPage.type == "approved") Main else Color(0xFFF0F0F0))
-                .clickable(onClick = { vm.getTaskList(StatePage(type = "approved", page = 1)) })
+                .clickable(onClick = {
+                    keyboard?.hide()
+                    vm.getTaskList(StatePage(type = "approved", page = 1, keywords = ""))
+                })
+                .padding(horizontal = 12.dp, vertical = 5.dp),
+        )
+        Text(
+            "全部", fontSize = 14.sp, color = if (state.taskPage.type == "") Color.White else Color(0xFF666666),
+            modifier = Modifier
+                .padding(end = 10.dp)
+                .clip(RoundedCornerShape(50))
+                .background(color = if (state.taskPage.type == "") Main else Color(0xFFF0F0F0))
+                .clickable(onClick = {
+                    keyboard?.hide()
+                    vm.getTaskList(StatePage(type = "", page = 1, keywords = ""))
+                })
                 .padding(horizontal = 12.dp, vertical = 5.dp),
         )
     }

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

@@ -1,6 +1,5 @@
 package com.iscs.bozzys.ui.pages.vm
 
-import android.util.Log
 import androidx.lifecycle.viewModelScope
 import com.iscs.bozzys.api.ApiRequest
 import com.iscs.bozzys.api.ApiRequest.getResponse
@@ -53,17 +52,18 @@ class VMDetailTask : VMBase() {
                 }
                 if (!taskInfo.formData.isNullOrEmpty()) {
                     val json = Json { ignoreUnknownKeys = true }
-                    formInfo = json.decodeFromString<TaskFormInfo>(taskInfo.formData)
+                    formInfo = json.decodeFromString<TaskFormInfo>(taskInfo.formData.ifEmpty { "{}" })
                     // 有些表单不需要显示的逻辑处理
                     // 解除隔离和还锁不需要录入信息
                     if (listOf("isolation", "returnLock").contains(taskInfo.type)) formInfo = TaskFormInfo()
-                    _state.value = _state.value.copy(forms = formInfo.fields.getFormListByJsonList(), node = taskInfo)
+                    _state.value = _state.value.copy(forms = formInfo.fields?.getFormListByJsonList() ?: emptyList(), node = taskInfo)
                     delay(500)
                     loading.emit(StateLoading())
                 } else {
                     ApiRequest.getTaskFormInfoByFormId(it.data?.formId ?: 0).onSuccess { formInfo ->
                         this@VMDetailTask.formInfo = formInfo.data ?: TaskFormInfo()
-                        _state.value = _state.value.copy(forms = this@VMDetailTask.formInfo.fields.getFormListByJsonList(), node = taskInfo)
+                        _state.value =
+                            _state.value.copy(forms = this@VMDetailTask.formInfo.fields?.getFormListByJsonList() ?: emptyList(), node = taskInfo)
                         delay(500)
                         loading.emit(StateLoading())
                     }.onFailure { err ->

+ 9 - 3
app/src/main/java/com/iscs/bozzys/ui/pages/vm/VMEditStep.kt

@@ -176,6 +176,11 @@ class VMEditStep : VMBase() {
      * @param form  变化的表单
      */
     fun onNodeFormChanged(form: FormField) {
+        // 更新表单数据到节点对象中
+        val node = _state.value.node.value.node?.updateNodeInfo(_state.value.nodeForms) ?: return
+        // 更新当前选中的节点和更新nodes列表中的数据
+        _state.value = _state.value.copy(node = mutableStateOf(_state.value.node.value.copy(node = node)))
+        _state.value.nodes[node.id.toString()] = _state.value.node.value.copy(node = node)
         // 如果是隔离方式变化
         if (form.name == "isolationType") {
             val value = form.value.getOrNull(0) ?: "-1"
@@ -250,8 +255,9 @@ class VMEditStep : VMBase() {
             val nodes = _state.value.nodes
             // 找到下一个需要处理的节点
             var record: NodeUI? = null
-            for (nui in nodes) {
-                val node = nui.value.node ?: continue
+            // 按照节点创建顺序进行校验
+            for (nui in nodes.map { it.value }.sortedBy { it.id }) {
+                val node = nui.node ?: continue
                 val tips = node.checkCanSubmit(
                     node.buildFormList(
                         workerUserList,
@@ -264,7 +270,7 @@ class VMEditStep : VMBase() {
                     )
                 )
                 if (tips.isNotEmpty()) {
-                    record = nui.value
+                    record = nui
                     done(false, record)
                     break
                 }

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

@@ -10,11 +10,14 @@ import com.iscs.bozzys.api.Job
 import com.iscs.bozzys.api.Message
 import com.iscs.bozzys.api.PageRsp
 import com.iscs.bozzys.api.Task
+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 com.iscs.bozzys.ui.theme.Text
 import com.iscs.bozzys.utils.Storage
+import com.iscs.bozzys.utils.Storage.saveRefreshToken
+import com.iscs.bozzys.utils.Storage.saveToken
 import kotlinx.coroutines.async
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -38,7 +41,7 @@ class VMHome : VMBase() {
                 NavBarItem(2, "任务", R.drawable.tasks),
                 NavBarItem(3, "设置", R.drawable.settings)
             )
-            _state.value = _state.value.copy(navs = navs, username = Storage.readUserName())
+            _state.value = _state.value.copy(navs = navs)
             // 延时等待页面配置数据初步加载完成
             delay(500)
             refreshHome(true)
@@ -94,6 +97,15 @@ class VMHome : VMBase() {
     fun getHomeTaskData(showLoading: Boolean = false, done: () -> Unit = {}) {
         viewModelScope.launch {
             loading.emit(StateLoading(show = showLoading))
+            // 查询权限信息
+            val permissionRsp = ApiRequest.getUserPermission().getOrElse { it.getResponse() }
+            if (!permissionRsp.code.isCodeOk()) {
+                _state.value = _state.value.copy(isHomeTabRefresh = false)
+                delay(500)
+                loading.emit(StateLoading())
+                toast.emit("获取用户信息失败,请稍后重试")
+                return@launch
+            }
             // 查询顶部的任务个数
             val statisticsRsp = ApiRequest.getTaskStatistics().getOrElse { it.getResponse() }
             if (!statisticsRsp.code.isCodeOk()) {
@@ -129,7 +141,9 @@ class VMHome : VMBase() {
                 homeTasks = ArrayList(tasks),
                 todoCountRunning = statisticsRsp.data?.inProgressCount ?: 0,
                 todoCountFinish = statisticsRsp.data?.completedCount ?: 0,
-                isHomeTabRefresh = false
+                isHomeTabRefresh = false,
+                roles = permissionRsp.data?.roles ?: emptyList(),
+                user = permissionRsp.data?.user ?: User()
             )
             // 更新进行中的任务数字
             updateUnReadCount(1, jobRsp.data?.total ?: 0)
@@ -151,18 +165,19 @@ class VMHome : VMBase() {
         viewModelScope.launch {
             // 下拉即刷新
             _state.value = _state.value.copy(taskPage = _state.value.taskPage.copy(isRefresh = true, type = page.type))
-            ApiRequest.getTasks(hashMapOf("pageNo" to page.page, "pageSize" to page.pageSize, "approvalStatus" to page.type)).onSuccess {
-                if (page.page == 1) _state.value.taskList.clear()
-                val tasks = it.data?.list ?: emptyList()
-                _state.value.taskList.addAll(tasks)
-                delay(1000)
-                // 存储页面数据
-                _state.value = _state.value.copy(taskPage = page)
-            }.onFailure {
-                delay(1000)
-                _state.value = _state.value.copy(taskPage = _state.value.taskPage.copy(isRefresh = false))
-                toast.emit(it.getResponse<PageRsp<Task>>().msg)
-            }
+            ApiRequest.getTasks(hashMapOf("pageNo" to page.page, "pageSize" to page.pageSize, "approvalStatus" to page.type, "key" to page.keywords))
+                .onSuccess {
+                    if (page.page == 1) _state.value.taskList.clear()
+                    val tasks = it.data?.list ?: emptyList()
+                    _state.value.taskList.addAll(tasks)
+                    delay(1000)
+                    // 存储页面数据
+                    _state.value = _state.value.copy(taskPage = page.copy(isRefresh = false))
+                }.onFailure {
+                    delay(1000)
+                    _state.value = _state.value.copy(taskPage = _state.value.taskPage.copy(isRefresh = false))
+                    toast.emit(it.getResponse<PageRsp<Task>>().msg)
+                }
         }
     }
 
@@ -170,20 +185,40 @@ class VMHome : VMBase() {
      * 获取作业数据,分页
      */
     fun getJobList(page: StatePage) {
+        // 正在刷新禁止重复进行
+        if (_state.value.jobPage.isRefresh) return
         viewModelScope.launch {
             // 下拉即刷新
             _state.value = _state.value.copy(jobPage = _state.value.jobPage.copy(isRefresh = true, type = page.type))
-            ApiRequest.getJobs(hashMapOf("pageNo" to page.page, "pageSize" to page.pageSize, "status" to page.type)).onSuccess {
-                if (page.page == 1) _state.value.jobList.clear()
-                val tasks = it.data?.list ?: emptyList()
-                _state.value.jobList.addAll(tasks)
-                // 存储页面数据
-                delay(1000)
-                _state.value = _state.value.copy(jobPage = page)
+            ApiRequest.getJobs(hashMapOf("pageNo" to page.page, "pageSize" to page.pageSize, "status" to page.type, "name" to page.keywords))
+                .onSuccess {
+                    if (page.page == 1) _state.value.jobList.clear()
+                    val tasks = it.data?.list ?: emptyList()
+                    _state.value.jobList.addAll(tasks)
+                    // 存储页面数据
+                    delay(1000)
+                    _state.value = _state.value.copy(jobPage = page)
+                }.onFailure {
+                    delay(1000)
+                    _state.value = _state.value.copy(jobPage = _state.value.jobPage.copy(isRefresh = false))
+                    toast.emit(it.getResponse<PageRsp<Job>>().msg)
+                }
+        }
+    }
+
+    /**
+     * 退出登录操作
+     */
+    fun logout(done: () -> Unit) {
+        viewModelScope.launch {
+            ApiRequest.logout().onSuccess {
+                // 清除本地数据
+                Storage.saveLogin(false)
+                "".saveToken()
+                "".saveRefreshToken()
+                done()
             }.onFailure {
-                delay(1000)
-                _state.value = _state.value.copy(jobPage = _state.value.jobPage.copy(isRefresh = false))
-                toast.emit(it.getResponse<PageRsp<Job>>().msg)
+                toast.emit(it.getResponse<Any>().msg)
             }
         }
     }
@@ -222,14 +257,14 @@ class VMHome : VMBase() {
  * @param taskPage          任务列表分页器
  * @param jobList           作业列表
  * @param jobPage           作业列表分页器
- * @param todoCountPending  待处理
  * @param todoCountRunning  进行中
  * @param todoCountFinish   已完成
+ * @param roles             用户角色
+ * @param user              当前用户
  */
 data class StateHome(
     val navs: List<NavBarItem> = arrayListOf(),
     val navigationId: Int = 0,
-    val username: String = "",
     val isHomeTabRefresh: Boolean = false,
     val homeTasks: List<Task> = listOf(),
     val messageList: List<Message> = listOf(),
@@ -239,6 +274,8 @@ data class StateHome(
     val jobList: MutableList<Job> = mutableListOf(),
     val todoCountRunning: Int = 0,
     val todoCountFinish: Int = 0,
+    val roles: List<String> = listOf(),
+    val user: User = User()
 )
 
 /**
@@ -266,6 +303,7 @@ data class NavBarItem(
  * @param page      当前页面
  * @param pageSize  页面大小
  * @param type      请求数据类型 unaudited 默认取进行中的
+ * @param keywords  关键字
  * @param isRefresh 是否正在刷新
  */
-data class StatePage(val page: Int = 1, val pageSize: Int = 10, val type: String = "", val isRefresh: Boolean = false)
+data class StatePage(val page: Int = 1, val pageSize: Int = 10, val type: String = "", val keywords: String = "", val isRefresh: Boolean = false)

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

@@ -23,6 +23,9 @@ class VMLogin : VMBase() {
     var state by mutableStateOf(StateLogin())
         private set
 
+    // 是否登录成功
+    private var isLogin = false
+
     /**
      * 更新登录方式
      *
@@ -54,6 +57,7 @@ class VMLogin : VMBase() {
      * 执行登录操作
      */
     fun login(done: () -> Unit) {
+        if (isLogin) return
         viewModelScope.launch {
             val check = checkCanLogin()
             if (check != 0) {
@@ -62,15 +66,17 @@ class VMLogin : VMBase() {
             }
             loading.emit(StateLoading(show = true))
             ApiRequest.login(state.account, state.code).onSuccess {
+                isLogin = true
                 it.data?.accessToken.saveToken()
                 it.data?.refreshToken.saveRefreshToken()
                 it.data?.nickname.saveUserName()
                 Storage.saveLogin(true)
                 loading.emit(StateLoading(show = false))
-                delay(500)
                 toast.emit("登录成功")
+                delay(500)
                 done.invoke()
             }.onFailure {
+                isLogin = false
                 delay(500)
                 loading.emit(StateLoading(show = false))
                 toast.emit(it.getResponse<Any>().msg)

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

@@ -0,0 +1,38 @@
+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.event.RefreshEvent
+import com.iscs.bozzys.event.RefreshEventBus
+import com.iscs.bozzys.ui.common.VMBase
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+/**
+ * 发布作业
+ */
+class VMPushJob : VMBase() {
+
+    /**
+     * 发布作业
+     */
+    fun onPush(id: Int, done: () -> Unit) {
+        viewModelScope.launch {
+            loading.emit(StateLoading(show = true))
+            ApiRequest.pushJob(mutableMapOf("startType" to "0", "workId" to id)).onSuccess {
+                delay(1000)
+                loading.emit(StateLoading())
+                toast.emit("发布成功")
+                // 刷新首页数据
+                RefreshEventBus.onRefreshData(RefreshEvent.HomeData)
+                done()
+            }.onFailure {
+                delay(1000)
+                loading.emit(StateLoading())
+                toast.emit(it.getResponse<Any>().msg)
+            }
+        }
+    }
+
+}

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

@@ -0,0 +1,18 @@
+package com.iscs.bozzys.utils
+
+/**
+ * 获取角色名称
+ */
+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")) {
+        "共锁人"
+    } else {
+        ""
+    }
+}

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

@@ -74,4 +74,19 @@ object Storage {
         return mmkv.decodeString("user_name", "") ?: ""
     }
 
+    /**
+     * 保存角色类型
+     */
+    fun List<String>.saveRole(): Boolean {
+        val str = this.joinToString(separator = ",")
+        return mmkv.encode("user_role", str)
+    }
+
+    /**
+     * 读取用户角色
+     */
+    fun readRole(): List<String> {
+        return (mmkv.decodeString("user_role", "") ?: "").split(",")
+    }
+
 }

+ 9 - 0
app/src/main/res/drawable/camera.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="32dp"
+    android:height="32dp"
+    android:viewportWidth="1024"
+    android:viewportHeight="1024">
+  <path
+      android:pathData="M860.4,207.4 L509.3,207.4c0,-52.8 -43,-95.8 -95.8,-95.8l-63.8,0c-52.8,0 -95.8,43 -95.8,95.8L158.4,207.4c-52.8,0 -95.8,43 -95.8,95.8l0,510.5c0,52.8 43,95.8 95.8,95.8l702,0c52.8,0 95.8,-43 95.8,-95.8L956.2,303.3C956.1,250.5 913.1,207.4 860.4,207.4zM509.3,750c-105.6,0 -191.5,-85.8 -191.5,-191.5s85.8,-191.5 191.5,-191.5 191.5,85.8 191.5,191.5S615,750 509.3,750zM812.4,430.9c-26.5,0 -47.8,-21.5 -47.8,-47.8s21.5,-47.8 47.8,-47.8c26.5,0 47.8,21.5 47.8,47.8S838.9,430.9 812.4,430.9z"
+      android:fillColor="#575B66"/>
+</vector>

+ 9 - 0
app/src/main/res/drawable/logout.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="32dp"
+    android:height="32dp"
+    android:viewportWidth="1024"
+    android:viewportHeight="1024">
+  <path
+      android:fillColor="#FF000000"
+      android:pathData="M573.2,646.5L309.8,646.5c-12.6,0 -20.8,-5.8 -22.9,-16.1 -0.4,-2.1 -0.4,-4.2 -0.4,-6.4L286.5,397.2c0,-14.3 7.8,-22 22.3,-22L573.1,375.2v-6.9,-201.8c0,-9.3 4.4,-16.6 11.9,-19.5 8.6,-3.3 16.2,-1.8 22.7,5.1 3.7,3.9 7.8,7.5 11.6,11.4 48.2,48.1 96.3,96.2 144.5,144.2 61.9,61.7 123.7,123.4 185.6,185.2 10.3,10.3 10.4,21.1 0.2,31.2 -88.5,88.4 -177,176.8 -265.6,265.1 -25.2,25.1 -50.5,50.1 -75.7,75.2 -6.5,6.5 -13.7,9.3 -22.5,6 -8.6,-3.3 -12.6,-10 -12.6,-20.7L573.2,646.5zM414.2,149.2v76.4L212.9,225.6c-41,0 -71.3,30.3 -71.3,71.4 0,142.3 0.3,284.6 -0.2,426.9 -0.1,37.7 28.5,64.8 55,69.7 5.9,1.1 11.9,1.6 17.9,1.6 64.2,0.1 128.4,0.1 192.6,0.1h7.2v76c-0.9,0.2 -2,0.6 -3,0.6 -68.2,0 -136.5,0.7 -204.7,-0.3 -66.7,-1 -125.7,-51.6 -138.5,-117.1 -1.8,-9.2 -2.7,-18.8 -2.7,-28.2 -0.2,-143.8 -0.5,-287.7 0.1,-431.5 0.3,-60.1 30.1,-103.2 82.2,-131.2 18.4,-9.9 38.7,-14.4 59.6,-14.4 67.7,-0.2 135.3,-0.1 203,-0.1 1.1,0 2.3,0.1 4.1,0.1zM414.2,149.2"/>
+</vector>

+ 2 - 0
gradle/libs.versions.toml

@@ -51,6 +51,8 @@ androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics", ve
 third-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version = "1.6.0" }
 # 时间选择器
 third-date-picker = { group = "com.github.loper7", name = "DateTimePicker", version = "0.6.3" }
+# 图片加载
+third-image-load = { group = "io.coil-kt", name = "coil-compose", version = "2.6.0" }
 
 [plugins]
 android-application = { id = "com.android.application", version.ref = "agp" }