فهرست منبع

1. 首页工作和任务列表接口对接
2. 刷新token接口对接
3. 表单选择新增选择起止日期
4. 其他功能优化

bjb 4 ماه پیش
والد
کامیت
185429974c
28فایلهای تغییر یافته به همراه2085 افزوده شده و 320 حذف شده
  1. 2 2
      .idea/deploymentTargetSelector.xml
  2. 11 0
      app/src/main/AndroidManifest.xml
  3. 78 18
      app/src/main/java/com/iscs/bozzys/api/ApiBean.kt
  4. 78 10
      app/src/main/java/com/iscs/bozzys/api/ApiRequest.kt
  5. 31 2
      app/src/main/java/com/iscs/bozzys/api/ApiService.kt
  6. 25 0
      app/src/main/java/com/iscs/bozzys/event/AuthEvent.kt
  7. 17 0
      app/src/main/java/com/iscs/bozzys/ui/base/PageBase.kt
  8. 2 2
      app/src/main/java/com/iscs/bozzys/ui/base/VMBase.kt
  9. 310 87
      app/src/main/java/com/iscs/bozzys/ui/pages/compose/FormCompose.kt
  10. 56 34
      app/src/main/java/com/iscs/bozzys/ui/pages/compose/JobListItem.kt
  11. 152 0
      app/src/main/java/com/iscs/bozzys/ui/pages/compose/TaskListItem.kt
  12. 20 11
      app/src/main/java/com/iscs/bozzys/ui/pages/create/job/PageCreateJob.kt
  13. 473 0
      app/src/main/java/com/iscs/bozzys/ui/pages/detail/task/PageDetailTask.kt
  14. 27 9
      app/src/main/java/com/iscs/bozzys/ui/pages/edit/step/PageEditStep.kt
  15. 58 23
      app/src/main/java/com/iscs/bozzys/ui/pages/home/HomeCompose.kt
  16. 41 27
      app/src/main/java/com/iscs/bozzys/ui/pages/home/JobsCompose.kt
  17. 9 10
      app/src/main/java/com/iscs/bozzys/ui/pages/home/PageHome.kt
  18. 257 0
      app/src/main/java/com/iscs/bozzys/ui/pages/home/TasksCompose.kt
  19. 2 5
      app/src/main/java/com/iscs/bozzys/ui/pages/login/PageLogin.kt
  20. 94 54
      app/src/main/java/com/iscs/bozzys/ui/pages/message/PageMessage.kt
  21. 61 0
      app/src/main/java/com/iscs/bozzys/ui/pages/vm/VMDetailTask.kt
  22. 97 20
      app/src/main/java/com/iscs/bozzys/ui/pages/vm/VMHome.kt
  23. 8 6
      app/src/main/java/com/iscs/bozzys/ui/pages/vm/VMLogin.kt
  24. 14 0
      app/src/main/java/com/iscs/bozzys/utils/DateUtil.kt
  25. 62 0
      app/src/main/java/com/iscs/bozzys/utils/SerializerUtil.kt
  26. 14 0
      app/src/main/java/com/iscs/bozzys/utils/Storage.kt
  27. 12 0
      app/src/main/res/drawable/tasks.xml
  28. 74 0
      app/src/main/res/layout/date_range_picker.xml

+ 2 - 2
.idea/deploymentTargetSelector.xml

@@ -4,10 +4,10 @@
     <selectionStates>
       <SelectionState runConfigName="app">
         <option name="selectionMode" value="DROPDOWN" />
-        <DropdownSelection timestamp="2025-12-24T06:35:24.548830500Z">
+        <DropdownSelection timestamp="2025-12-26T04:01:18.101228300Z">
           <Target type="DEFAULT_BOOT">
             <handle>
-              <DeviceId pluginId="Default" identifier="serial=192.168.0.38:5555;connection=6171309d" />
+              <DeviceId pluginId="PhysicalDevice" identifier="serial=d8d12db95670c08" />
             </handle>
           </Target>
         </DropdownSelection>

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

@@ -31,6 +31,7 @@
         <activity
             android:name=".ui.pages.login.PageLogin"
             android:exported="true"
+            android:launchMode="singleTask"
             android:screenOrientation="portrait" />
         <!--  主页面  -->
         <activity
@@ -59,6 +60,16 @@
             android:name=".ui.pages.detail.job.PageDetailJob"
             android:exported="true"
             android:screenOrientation="portrait" />
+        <!--  任务详情  -->
+        <activity
+            android:name=".ui.pages.detail.task.PageDetailTask"
+            android:exported="true"
+            android:screenOrientation="portrait" />
+        <!--  消息中心  -->
+        <activity
+            android:name=".ui.pages.message.PageMessage"
+            android:exported="true"
+            android:screenOrientation="portrait" />
         <!--  阿里消息推送服务配置  -->
         <service
             android:name=".service.AliPushService"

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

@@ -1,8 +1,6 @@
 package com.iscs.bozzys.api
 
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
+import com.iscs.bozzys.utils.PlaceholderSerializer
 import kotlinx.serialization.Serializable
 
 
@@ -13,7 +11,7 @@ import kotlinx.serialization.Serializable
  * @param msg
  * @param data
  */
-open class Response<T>(var code: Int = 0, var msg: String = "", var data: T? = null)
+open class Response<T>(var code: Int = 500, var msg: String = "", var data: T? = null)
 
 /**
  * 账号登录接口响应数据
@@ -21,17 +19,55 @@ open class Response<T>(var code: Int = 0, var msg: String = "", var data: T? = n
  * @param userId        用户id
  * @param username      用户名
  * @param nickname      用户昵称
- * @param accessToken   accessToken
+ * @param accessToken   使用token
+ * @param refreshToken  刷新token
  */
-class LoginRsp(val userId: Int, val username: String, val nickname: String, val accessToken: String)
+class LoginRsp(val userId: Int, val username: String, val nickname: String, val accessToken: String, val refreshToken: String)
 
 /**
- * 获取工作列表响应
+ * 获取分页数据
  *
- * @param total 总
- * @param list  获取到的
+ * @param total 数据总数
+ * @param list  当前获取到的数
  */
-class JobsRsp(val total: Int, val list: List<Job>)
+class PageRsp<T>(val total: Int, val list: List<T>)
+
+/**
+ * 任务信息
+ */
+class TaskInfo(
+    val id: Int = 0,
+    val workId: Int = 0,
+    val uuid: String = "",
+    val parentUuid: String = "",
+    val childrenUuid: String = "",
+    val nodeName: String = "",
+    val nodeIcon: String = "",
+    val type: String = "",
+    val position: String = "",
+    val data: String = "",
+    val description: String? = null,
+    val workerUserId: Int = 0,
+    val workerGroupId: Int? = null,
+    val formId: Int = 0,
+    val formData: String? = null,
+    val createTime: Long = 0L,
+    val approvalStatus: String = "",
+    val approvalOpinion: String = "",
+)
+
+/**
+ * 任务表单信息
+ */
+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
+)
 
 /**
  * 作业基类信息
@@ -51,13 +87,38 @@ data class Job(
     val initiatorTime: Long,
     val status: String,                 // 作业状态
     val createTime: Long,               // 创建时间
-    val currentNodeId: Int? = null,
+    val currentNodeId: String? = null,
     val currentNodeName: String? = null,
     val completionTime: Long? = null,
     val cancellationTime: Long? = null,
-    val cancellationReason: String? = null,
+    val cancellationReason: String? = null
+)
 
-    )
+/**
+ * Task任务
+ */
+@Serializable
+data class Task(
+    val workId: Int,
+    val nodeId: Int,
+    val orderNo: String,
+    val name: String,
+    val urgencyLevel: String,
+    val completionTime: Long?,
+    val cancellationTime: Long?,
+    val cancellationReason: String?,
+    val workerUserName: String?,
+    val workTime: Long?,
+    val currentNodeId: String?,
+    val currentNodeName: String?,
+    val approvalStatus: String
+) : java.io.Serializable
+
+/**
+ * 消息
+ */
+@Serializable
+data class Message(val id: Int = 0)
 
 /**
  * 表单需要的组件
@@ -80,15 +141,14 @@ class FormField(
     val label: String = "",
     val name: String = "",
     val required: Boolean = false,
-    val placeholder: String = "",
+    @Serializable(with = PlaceholderSerializer::class)
+    val placeholder: List<String> = listOf(""),
+    var value: List<String> = listOf(""),
     val options: List<FormOption> = listOf(),
     val cardTitle: String = "",
     val gridColumns: Int = 0,
     val children: List<FormField> = listOf()
-) {
-    // 可变参数,参数变化能监听到
-    var value by mutableStateOf("")
-}
+)
 
 /**
  * 表单Option字段

+ 78 - 10
app/src/main/java/com/iscs/bozzys/api/ApiRequest.kt

@@ -1,9 +1,12 @@
 package com.iscs.bozzys.api
 
-import android.util.Log
 import com.google.gson.Gson
 import com.google.gson.JsonObject
+import com.iscs.bozzys.api.ApiRequest.refreshToken
+import com.iscs.bozzys.event.AuthEventBus
 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 java.net.UnknownHostException
 
@@ -18,7 +21,7 @@ object ApiRequest {
      * @param headers 用于自定义请求头参数
      * @return 默认请求头参数,如果用户携带请求头参数与默认一致,优先使用用户携带
      */
-    private fun getUserHeaders(headers: Map<String, String>): Map<String, String> {
+    private fun getUserHeaders(headers: Map<String, String> = emptyMap()): Map<String, String> {
         val map = HashMap<String, String>()
         map["tenant-id"] = "1"
         map["Authorization"] = "Bearer ${Storage.readToken()}"
@@ -34,15 +37,20 @@ object ApiRequest {
      * @return 封装对外出去的异常总响应
      */
     private fun <T> dealException(e: Exception, rsp: Response<T>): Exception {
-        Log.d("xiaoming","Request Api Failed -> $e")
-        rsp.code = 404
-        rsp.msg = "network error"
+        rsp.msg = e.message?.ifEmpty { "Network error" } ?: "Network error"
         when {
             e is UnknownHostException -> {
                 rsp.code = 500
-                rsp.msg = "network error"
+                rsp.msg = "Network error"
             }
         }
+        // 未登录/或者授权过期,这里要进行退出登录操作
+        if (rsp.code == 401) {
+            // 这里执行退出登录操作
+            "".saveToken()
+            Storage.saveLogin(false)
+            AuthEventBus.onTokenExpired()
+        }
         return Exception(Gson().toJson(rsp))
     }
 
@@ -51,7 +59,29 @@ object ApiRequest {
      */
     private suspend fun <T> requestApi(apiFun: suspend () -> Response<T>): Result<Response<T>> {
         return try {
-            Result.success(apiFun())
+            val rsp = apiFun()
+            // 请求成功
+            if (listOf(0, 200).contains(rsp.code)) {
+                Result.success(rsp)
+            } else if (rsp.code == 401) {
+                // 授权过期,需要重新授权
+                val refreshRsp = refreshToken(Storage.readRefreshToken()).getOrNull()
+                if (listOf(0, 200).contains(refreshRsp?.code ?: -1)) {
+                    refreshRsp?.data?.accessToken.saveToken()
+                    refreshRsp?.data?.refreshToken.saveRefreshToken()
+                    // 刷新完成执行一次请求并返回数据
+                    val rsp = apiFun()
+                    if (listOf(0, 200).contains(rsp.code)) {
+                        Result.success(rsp)
+                    } else {
+                        Result.failure(dealException(Exception(rsp.msg), rsp))
+                    }
+                } else {
+                    Result.failure(dealException(Exception(rsp.msg), rsp))
+                }
+            } else {
+                Result.failure(dealException(Exception(rsp.msg), rsp))
+            }
         } catch (e: Exception) {
             Result.failure(dealException(e, Response<T>()))
         }
@@ -77,14 +107,52 @@ object ApiRequest {
         val params = JsonObject()
         params.addProperty("username", username)
         params.addProperty("password", pwd)
-        return requestApi { api.login(getUserHeaders(emptyMap()), params) }
+        return requestApi { api.login(getUserHeaders(), params) }
+    }
+
+    /**
+     * 刷新Token操作
+     *
+     * @param refreshToken
+     */
+    suspend fun refreshToken(refreshToken: String): Result<Response<LoginRsp>> {
+        return requestApi { api.refreshToken(getUserHeaders(), refreshToken) }
+    }
+
+    /**
+     * 查询作业列表数据
+     *
+     * @param params
+     */
+    suspend fun getJobs(params: MutableMap<String, Any>): Result<Response<PageRsp<Job>>> {
+        return requestApi { api.getJobs(getUserHeaders(), params) }
     }
 
     /**
      * 查询作业列表数据
+     *
+     * @param params
+     */
+    suspend fun getTasks(params: MutableMap<String, Any>): Result<Response<PageRsp<Task>>> {
+        return requestApi { api.getTasks(getUserHeaders(), params) }
+    }
+
+    /**
+     * 通过节点Id获取任务信息
+     *
+     * @param nodeId    节点Id
+     */
+    suspend fun getTaskInfoByNodeId(nodeId: Int): Result<Response<TaskInfo>> {
+        return requestApi { api.getTaskInfo(getUserHeaders(), mutableMapOf("nodeId" to nodeId)) }
+    }
+
+    /**
+     * 获取任务表单信息
+     *
+     * @param formId    表单id
      */
-    suspend fun getJobs(params: MutableMap<String, Any>): Result<Response<JobsRsp>> {
-        return requestApi { api.getJobs(getUserHeaders(emptyMap()), params) }
+    suspend fun getTaskFormInfoByFormId(formId: Int): Result<Response<TaskFormInfo>> {
+        return requestApi { api.getTaskFormInfo(getUserHeaders(), mutableMapOf("id" to formId)) }
     }
 
 }

+ 31 - 2
app/src/main/java/com/iscs/bozzys/api/ApiService.kt

@@ -6,6 +6,7 @@ import retrofit2.http.GET
 import retrofit2.http.HeaderMap
 import retrofit2.http.Headers
 import retrofit2.http.POST
+import retrofit2.http.Query
 import retrofit2.http.QueryMap
 
 /**
@@ -21,10 +22,38 @@ interface ApiService {
     suspend fun login(@HeaderMap headers: Map<String, String>, @Body body: JsonObject): Response<LoginRsp>
 
     /**
-     * 获取作业数据
+     * 刷新Token使用
+     */
+    @Headers("Content-Type: application/json")
+    @POST("/admin-api/system/auth/refresh-token")
+    suspend fun refreshToken(@HeaderMap headers: Map<String, String>, @Query("refreshToken") refreshToken: String): Response<LoginRsp>
+
+    /**
+     * 获取作业列表数据
      */
     @Headers("Content-Type: application/x-www-form-urlencoded")
     @GET("/admin-api/iscs/workflow-work/getWorkflowWorkPage")
-    suspend fun getJobs(@HeaderMap headers: Map<String, String>, @QueryMap params: MutableMap<String, Any>): Response<JobsRsp>
+    suspend fun getJobs(@HeaderMap headers: Map<String, String>, @QueryMap params: MutableMap<String, Any>): Response<PageRsp<Job>>
+
+    /**
+     * 获取任务列表数据
+     */
+    @Headers("Content-Type: application/x-www-form-urlencoded")
+    @GET("/admin-api/iscs/workflow-work/getMyWorkPage")
+    suspend fun getTasks(@HeaderMap headers: Map<String, String>, @QueryMap params: MutableMap<String, Any>): Response<PageRsp<Task>>
+
+    /**
+     * 通过节点Id获取任务信息
+     */
+    @Headers("Content-Type: application/x-www-form-urlencoded")
+    @GET("/admin-api/iscs/workflow-work/getMyWorkNodeDetail")
+    suspend fun getTaskInfo(@HeaderMap headers: Map<String, String>, @QueryMap params: MutableMap<String, Any>): Response<TaskInfo>
+
+    /**
+     * 获取当前任务的表单信息
+     */
+    @Headers("Content-Type: application/x-www-form-urlencoded")
+    @GET("/admin-api/bpm/form/get")
+    suspend fun getTaskFormInfo(@HeaderMap headers: Map<String, String>, @QueryMap params: MutableMap<String, Any>): Response<TaskFormInfo>
 
 }

+ 25 - 0
app/src/main/java/com/iscs/bozzys/event/AuthEvent.kt

@@ -0,0 +1,25 @@
+package com.iscs.bozzys.event
+
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
+
+/**
+ * 授权事件总线
+ */
+object AuthEventBus {
+
+    private val _events = MutableSharedFlow<AuthEvent>(extraBufferCapacity = 1)
+
+    val events = _events.asSharedFlow()
+
+    fun onTokenExpired() {
+        _events.tryEmit(AuthEvent.TokenExpired)
+    }
+}
+
+/**
+ * 授权事件封装
+ */
+sealed class AuthEvent {
+    object TokenExpired : AuthEvent()
+}

+ 17 - 0
app/src/main/java/com/iscs/bozzys/ui/base/PageBase.kt

@@ -22,6 +22,9 @@ import androidx.core.view.WindowInsetsCompat
 import androidx.core.view.WindowInsetsControllerCompat
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.viewmodel.compose.viewModel
+import com.iscs.bozzys.event.AuthEvent
+import com.iscs.bozzys.event.AuthEventBus
+import com.iscs.bozzys.ui.pages.login.openPageLogin
 import com.iscs.bozzys.ui.pages.vm.StateLoading
 import com.iscs.bozzys.ui.pages.vm.VMLoading
 import com.iscs.bozzys.ui.theme.BozzysTheme
@@ -57,6 +60,7 @@ abstract class PageBase(
                 darkScrim = Color.Transparent.toArgb()
             )
         )
+        initEvent()
         setContent {
             // 全局使用黑主题色,主要原因顶部状态栏黑色和设计冲突
             BozzysTheme {
@@ -74,6 +78,19 @@ abstract class PageBase(
         }
     }
 
+    /**
+     * 所有事件总线初始化操作
+     */
+    private fun initEvent() {
+        lifecycleScope.launch {
+            AuthEventBus.events.collect { event ->
+                when (event) {
+                    is AuthEvent.TokenExpired -> openPageLogin()
+                }
+            }
+        }
+    }
+
     /**
      * 提供外部 用于构建页面视图使用
      *

+ 2 - 2
app/src/main/java/com/iscs/bozzys/ui/base/VMBase.kt

@@ -12,7 +12,7 @@ open class VMBase : ViewModel() {
     /**
      * Toast 事件
      */
-    val toastEvent = MutableSharedFlow<Any>()
+    val toast = MutableSharedFlow<Any>()
 
-    val loadingEvent = MutableSharedFlow<StateLoading>()
+    val loading = MutableSharedFlow<StateLoading>()
 }

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

@@ -1,5 +1,6 @@
 package com.iscs.bozzys.ui.pages.compose
 
+import android.util.Log
 import androidx.compose.foundation.border
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Box
@@ -25,6 +26,7 @@ import androidx.compose.material3.LocalTextStyle
 import androidx.compose.material3.RadioButton
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
@@ -48,6 +50,8 @@ import com.iscs.bozzys.R
 import com.iscs.bozzys.api.FormField
 import com.iscs.bozzys.api.FormOption
 import com.iscs.bozzys.ui.theme.Main
+import com.iscs.bozzys.ui.theme.Text
+import com.iscs.bozzys.utils.DateUtil.dateToTimestamp
 import com.iscs.bozzys.utils.DateUtil.format
 import com.loper7.date_time_picker.DateTimeConfig
 import com.loper7.date_time_picker.dialog.CardDatePickerDialog
@@ -61,7 +65,15 @@ import kotlinx.serialization.json.Json
  * @param onValueChange 表单内容发生变化
  */
 @Composable
-fun FormInput(label: String, value: String, onValueChange: (String) -> Unit, placeholder: String = "", required: Boolean = false) {
+fun FormInput(
+    label: String,
+    value: List<String>,
+    onValueChange: (List<String>) -> Unit,
+    placeholder: List<String> = listOf(),
+    required: Boolean = false,
+    enable: Boolean = true,
+) {
+    var text by remember { mutableStateOf(value.getOrNull(0) ?: "") }
     Column(
         Modifier
             .fillMaxWidth()
@@ -72,13 +84,22 @@ fun FormInput(label: String, value: String, onValueChange: (String) -> Unit, pla
                 .fillMaxWidth()
                 .height(40.dp), verticalAlignment = Alignment.CenterVertically
         ) {
-            Text(label, fontSize = 14.sp, fontWeight = FontWeight.Bold)
-            if (required) Text("*", fontSize = 14.sp, color = Color(0xFFFF4D4F), modifier = Modifier.padding(start = 3.dp))
+            Text(label, fontSize = 14.sp, fontWeight = FontWeight.Bold, color = Text.copy(alpha = if (enable) 1f else 0.6f))
+            if (required) Text(
+                "*",
+                fontSize = 14.sp,
+                color = Color(0xFFFF4D4F).copy(alpha = if (enable) 1f else 0.6f),
+                modifier = Modifier.padding(start = 3.dp)
+            )
         }
         BasicTextField(
-            value,
-            onValueChange = onValueChange,
-            Modifier
+            text,
+            onValueChange = {
+                text = it
+                onValueChange(listOf(text))
+            },
+            enabled = enable,
+            modifier = Modifier
                 .fillMaxWidth()
                 .height(46.dp)
                 .border(1.dp, shape = RoundedCornerShape(6.dp), color = Color(0xFFE5E6EB))
@@ -87,10 +108,76 @@ fun FormInput(label: String, value: String, onValueChange: (String) -> Unit, pla
             textStyle = LocalTextStyle.current.copy(fontSize = 14.sp, lineHeight = 18.sp),
             decorationBox = { innerTextField ->
                 Box(contentAlignment = Alignment.CenterStart) {
+                    innerTextField()
+                    if (text.isEmpty()) {
+                        val ph = placeholder.getOrNull(0) ?: "请输入$label"
+                        Text(ph, color = Color(0xFF9CA3AF), fontSize = 14.sp, lineHeight = 18.sp)
+                    }
+                }
+            },
+            cursorBrush = SolidColor(Main),
+            keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
+        )
+    }
+}
+
+/**
+ * 表单多行文本输入
+ *
+ * @param label         标题
+ * @param value         输入的内容
+ * @param onValueChange 输入内容发生变化
+ * @param placeholder   占位内容
+ * @param enable        是否使能
+ */
+@Composable
+fun FormTextarea(
+    label: String,
+    value: List<String>,
+    onValueChange: (List<String>) -> Unit,
+    placeholder: List<String> = listOf(""),
+    required: Boolean = false,
+    enable: Boolean = true,
+) {
+    var text by remember { mutableStateOf(value.getOrNull(0) ?: "") }
+    Column(
+        Modifier
+            .fillMaxWidth()
+            .heightIn(max = 160.dp)
+    ) {
+        Row(
+            Modifier
+                .fillMaxWidth()
+                .height(40.dp), verticalAlignment = Alignment.CenterVertically
+        ) {
+            Text(label, fontSize = 14.sp, fontWeight = FontWeight.Bold, color = Text.copy(alpha = if (enable) 1f else 0.6f))
+            if (required) Text(
+                "*",
+                fontSize = 14.sp,
+                color = Color(0xFFFF4D4F).copy(alpha = if (enable) 1f else 0.6f),
+                modifier = Modifier.padding(start = 3.dp)
+            )
+        }
+        BasicTextField(
+            text,
+            onValueChange = {
+                text = it
+                onValueChange(listOf(it))
+            },
+            enabled = enable,
+            modifier = Modifier
+                .fillMaxWidth()
+                .heightIn(min = 120.dp)
+                .border(1.dp, shape = RoundedCornerShape(6.dp), color = Color(0xFFE5E6EB))
+                .padding(10.dp)
+                .verticalScroll(rememberScrollState()),
+            textStyle = LocalTextStyle.current.copy(fontSize = 14.sp, lineHeight = 18.sp),
+            decorationBox = { innerTextField ->
+                Box(contentAlignment = Alignment.TopStart) {
                     innerTextField()
                     if (value.isEmpty()) {
-                        val text = placeholder.ifEmpty { "请输入$label" }
-                        Text(text, color = Color(0xFF9CA3AF), fontSize = 14.sp, lineHeight = 18.sp)
+                        val ph = placeholder.getOrNull(0) ?: "请输入$label"
+                        Text(ph, color = Color(0xFF9CA3AF), fontSize = 14.sp, lineHeight = 18.sp)
                     }
                 }
             },
@@ -100,6 +187,7 @@ fun FormInput(label: String, value: String, onValueChange: (String) -> Unit, pla
     }
 }
 
+
 /**
  * 表单选择器
  *
@@ -109,11 +197,20 @@ fun FormInput(label: String, value: String, onValueChange: (String) -> Unit, pla
  * @param onSelectChange    选择变化监听
  */
 @Composable
-fun FormSelect(label: String, value: String, options: List<FormOption>, onSelectChange: (FormOption) -> Unit, required: Boolean = false) {
+fun FormSelect(
+    label: String,
+    value: List<String>,
+    options: List<FormOption>,
+    onSelectChange: (List<String>) -> Unit,
+    placeholder: List<String> = listOf(""),
+    required: Boolean = false,
+    enable: Boolean = true,
+) {
     var expanded by remember { mutableStateOf(false) }
     var width by remember { mutableStateOf(0) }
     val density = LocalDensity.current
-    val find = options.find { it.value == value }
+    // 当前表单中的状态
+    var opt by remember { mutableStateOf(options.find { it.value == value.getOrNull(0) }) }
     Column(
         Modifier
             .fillMaxWidth()
@@ -125,8 +222,13 @@ fun FormSelect(label: String, value: String, options: List<FormOption>, onSelect
                 .fillMaxWidth()
                 .height(40.dp), verticalAlignment = Alignment.CenterVertically
         ) {
-            Text(label, fontSize = 14.sp, fontWeight = FontWeight.Bold)
-            if (required) Text("*", fontSize = 14.sp, color = Color(0xFFFF4D4F), modifier = Modifier.padding(start = 3.dp))
+            Text(label, fontSize = 14.sp, fontWeight = FontWeight.Bold, color = Text.copy(alpha = if (enable) 1f else 0.6f))
+            if (required) Text(
+                "*",
+                fontSize = 14.sp,
+                color = Color(0xFFFF4D4F).copy(alpha = if (enable) 1f else 0.6f),
+                modifier = Modifier.padding(start = 3.dp)
+            )
         }
         Row(
             Modifier
@@ -134,21 +236,21 @@ fun FormSelect(label: String, value: String, options: List<FormOption>, onSelect
                 .height(46.dp)
                 .border(1.dp, shape = RoundedCornerShape(6.dp), color = Color(0xFFE5E6EB))
                 .clip(RoundedCornerShape(6.dp))
-                .clickable(onClick = { expanded = true })
+                .clickable(onClick = { expanded = true }, enabled = enable)
                 .padding(horizontal = 10.dp),
             verticalAlignment = Alignment.CenterVertically
         ) {
             Text(
-                find?.label ?: "请选择$label",
-                color = Color.Black,
+                opt?.label ?: "请选择$label",
+                color = Text.copy(alpha = if (enable) 1f else 0.6f),
                 fontSize = 14.sp,
                 lineHeight = 18.sp,
-                modifier = Modifier.weight(1f)
+                modifier = Modifier.weight(1f),
             )
             Icon(
                 painter = painterResource(R.drawable.back), contentDescription = "", modifier = Modifier
                     .rotate(if (expanded) -90f else 180f)
-                    .size(12.dp), tint = Color.Black
+                    .size(12.dp), tint = Text.copy(alpha = if (enable) 1f else 0.6f)
             )
         }
         val widthDp = with(density) { width.toDp() }
@@ -167,7 +269,8 @@ fun FormSelect(label: String, value: String, options: List<FormOption>, onSelect
                         modifier = Modifier.fillMaxWidth(),
                         text = { Text(option.label) },
                         onClick = {
-                            onSelectChange(option)
+                            opt = option
+                            onSelectChange(listOf(option.value))
                             expanded = false
                         }
                     )
@@ -177,54 +280,6 @@ fun FormSelect(label: String, value: String, options: List<FormOption>, onSelect
     }
 }
 
-/**
- * 表单多行文本输入
- *
- * @param label         标题
- * @param value         输入的内容
- * @param onValueChange 输入内容发生变化
- * @param placeholder   占位内容
- */
-@Composable
-fun FormTextarea(label: String, value: String, onValueChange: (String) -> Unit, placeholder: String = "", required: Boolean = false) {
-    Column(
-        Modifier
-            .fillMaxWidth()
-            .heightIn(max = 160.dp)
-    ) {
-        Row(
-            Modifier
-                .fillMaxWidth()
-                .height(40.dp), verticalAlignment = Alignment.CenterVertically
-        ) {
-            Text(label, fontSize = 14.sp, fontWeight = FontWeight.Bold)
-            if (required) Text("*", fontSize = 14.sp, color = Color(0xFFFF4D4F), modifier = Modifier.padding(start = 3.dp))
-        }
-        BasicTextField(
-            value,
-            onValueChange = onValueChange,
-            Modifier
-                .fillMaxWidth()
-                .heightIn(min = 120.dp)
-                .border(1.dp, shape = RoundedCornerShape(6.dp), color = Color(0xFFE5E6EB))
-                .padding(10.dp)
-                .verticalScroll(rememberScrollState()),
-            textStyle = LocalTextStyle.current.copy(fontSize = 14.sp, lineHeight = 18.sp),
-            decorationBox = { innerTextField ->
-                Box(contentAlignment = Alignment.TopStart) {
-                    innerTextField()
-                    if (value.isEmpty()) {
-                        val text = placeholder.ifEmpty { "请输入$label" }
-                        Text(text, color = Color(0xFF9CA3AF), fontSize = 14.sp, lineHeight = 18.sp)
-                    }
-                }
-            },
-            cursorBrush = SolidColor(Main),
-            keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text)
-        )
-    }
-}
-
 /**
  * 表单单选组件
  *
@@ -237,11 +292,13 @@ fun FormTextarea(label: String, value: String, onValueChange: (String) -> Unit,
 @Composable
 fun FormRadio(
     label: String,
-    value: String,
+    value: List<String>,
     options: List<FormOption>,
-    onSelectChange: (FormOption) -> Unit,
-    required: Boolean = false
+    onSelectChange: (List<String>) -> Unit,
+    required: Boolean = false,
+    enable: Boolean = true,
 ) {
+    var text by remember { mutableStateOf(value.getOrNull(0) ?: "") }
     Column(
         Modifier
             .fillMaxWidth()
@@ -252,8 +309,13 @@ fun FormRadio(
                 .fillMaxWidth()
                 .height(40.dp), verticalAlignment = Alignment.CenterVertically
         ) {
-            Text(label, fontSize = 14.sp, fontWeight = FontWeight.Bold)
-            if (required) Text("*", fontSize = 14.sp, color = Color(0xFFFF4D4F), modifier = Modifier.padding(start = 3.dp))
+            Text(label, fontSize = 14.sp, fontWeight = FontWeight.Bold, color = Text.copy(alpha = if (enable) 1f else 0.6f))
+            if (required) Text(
+                "*",
+                fontSize = 14.sp,
+                color = Color(0xFFFF4D4F).copy(alpha = if (enable) 1f else 0.6f),
+                modifier = Modifier.padding(start = 3.dp)
+            )
         }
         FlowRow(
             Modifier
@@ -264,12 +326,20 @@ fun FormRadio(
                 Row(
                     modifier = Modifier
                         .clip(RoundedCornerShape(12.dp))
-                        .clickable { onSelectChange(item) }
+                        .clickable(onClick = {
+                            text = item.value
+                            onSelectChange(listOf(item.value))
+                        }, enabled = enable)
                         .padding(horizontal = 10.dp, vertical = 5.dp),
                     verticalAlignment = Alignment.CenterVertically
                 ) {
-                    RadioButton(selected = value == item.value, onClick = null, modifier = Modifier.size(14.dp))
-                    Text(text = item.label, fontSize = 15.sp, modifier = Modifier.padding(start = 10.dp))
+                    RadioButton(selected = text == item.value, onClick = null, modifier = Modifier.size(14.dp), enabled = enable)
+                    Text(
+                        text = item.label,
+                        fontSize = 15.sp,
+                        modifier = Modifier.padding(start = 10.dp),
+                        color = Text.copy(alpha = if (enable) 1f else 0.6f)
+                    )
                 }
             }
         }
@@ -277,15 +347,15 @@ fun FormRadio(
 }
 
 /**
- * 表单单选组件
+ * 表单日期时间选择组件
  *
  * @param label             标题
  * @param value             当前选中
  * @param onSelectChange    当前选中
  */
 @Composable
-fun FormDateSelect(label: String, value: Long, onSelectChange: (Long) -> Unit, required: Boolean = false) {
-    val date = if (value <= 0) System.currentTimeMillis() else value
+fun FormDateSelect(label: String, value: List<String>, onSelectChange: (List<String>) -> Unit, required: Boolean = false, enable: Boolean = true) {
+    var date by remember { mutableStateOf((value.getOrNull(0) ?: "").dateToTimestamp("yyyy/MM/dd HH:mm")) }
     val picker = CardDatePickerDialog.builder(LocalContext.current)
         .setTitle("请选择日期")
         .setDefaultTime(date)
@@ -294,8 +364,9 @@ fun FormDateSelect(label: String, value: Long, onSelectChange: (Long) -> Unit, r
         .showBackNow(false)
         .showFocusDateInfo(false)
         .showDateLabel(false)
-        .setOnChoose { millisecond ->
-            onSelectChange(millisecond)
+        .setOnChoose { timestamp ->
+            date = timestamp
+            onSelectChange(listOf(timestamp.toString()))
         }.build()
 
     Column(
@@ -309,8 +380,13 @@ fun FormDateSelect(label: String, value: Long, onSelectChange: (Long) -> Unit, r
                 .height(40.dp),
             verticalAlignment = Alignment.CenterVertically
         ) {
-            Text(label, fontSize = 14.sp, fontWeight = FontWeight.Bold)
-            if (required) Text("*", fontSize = 14.sp, color = Color(0xFFFF4D4F), modifier = Modifier.padding(start = 3.dp))
+            Text(label, fontSize = 14.sp, fontWeight = FontWeight.Bold, color = Text.copy(alpha = if (enable) 1f else 0.6f))
+            if (required) Text(
+                "*",
+                fontSize = 14.sp,
+                color = Color(0xFFFF4D4F).copy(alpha = if (enable) 1f else 0.6f),
+                modifier = Modifier.padding(start = 3.dp)
+            )
         }
 
         Text(
@@ -320,20 +396,142 @@ fun FormDateSelect(label: String, value: Long, onSelectChange: (Long) -> Unit, r
                 .height(46.dp)
                 .border(1.dp, shape = RoundedCornerShape(6.dp), color = Color(0xFFE5E6EB))
                 .clip(RoundedCornerShape(6.dp))
-                .clickable(onClick = {
-                    picker.show()
-                })
+                .clickable(onClick = { picker.show() }, enabled = enable)
                 .padding(10.dp),
             fontSize = 14.sp,
-            lineHeight = 26.sp
+            lineHeight = 26.sp,
+            color = Text.copy(alpha = if (enable) 1f else 0.6f)
         )
     }
 }
 
+/**
+ * 表单日期范围选择组件
+ *
+ * @param label             标题
+ * @param value             当前选中
+ * @param onSelectChange    当前选中
+ */
+@Composable
+fun FormDateRangeSelect(
+    label: String,
+    value: List<String>,
+    onSelectChange: (List<String>) -> Unit,
+    placeholder: List<String> = listOf("", ""),
+    required: Boolean = false,
+    enable: Boolean = true
+) {
+    val ctx = LocalContext.current
+    var start by remember { mutableStateOf("".dateToTimestamp("yyyy/MM/dd")) }
+    var end by remember { mutableStateOf("".dateToTimestamp("yyyy/MM/dd")) }
+    LaunchedEffect(Unit) {
+        if (value.size == 2) {
+            start = value[0].dateToTimestamp("yyyy/MM/dd")
+            end = value[1].dateToTimestamp("yyyy/MM/dd")
+        }
+    }
+
+    Column(
+        Modifier
+            .fillMaxWidth()
+            .heightIn(max = 90.dp)
+    ) {
+        Row(
+            Modifier
+                .fillMaxWidth()
+                .height(40.dp),
+            verticalAlignment = Alignment.CenterVertically
+        ) {
+            Text(label, fontSize = 14.sp, fontWeight = FontWeight.Bold, color = Text.copy(alpha = if (enable) 1f else 0.6f))
+            if (required) Text(
+                "*",
+                fontSize = 14.sp,
+                color = Color(0xFFFF4D4F).copy(alpha = if (enable) 1f else 0.6f),
+                modifier = Modifier.padding(start = 3.dp)
+            )
+        }
+
+        Row(
+            modifier = Modifier
+                .fillMaxWidth()
+                .height(46.dp),
+            verticalAlignment = Alignment.CenterVertically
+        ) {
+            Text(
+                start.format("yyyy/MM/dd"),
+                modifier = Modifier
+                    .weight(1f)
+                    .height(46.dp)
+                    .border(1.dp, shape = RoundedCornerShape(6.dp), color = Color(0xFFE5E6EB))
+                    .clip(RoundedCornerShape(6.dp))
+                    .clickable(onClick = {
+                        CardDatePickerDialog.builder(ctx)
+                            .setTitle("请选择开始日期")
+                            .setDefaultTime(start)
+                            .setDisplayType(mutableListOf(DateTimeConfig.YEAR, DateTimeConfig.MONTH, DateTimeConfig.DAY))
+                            .setPickerLayout(R.layout.date_range_picker)
+                            .showBackNow(false)
+                            .showFocusDateInfo(false)
+                            .showDateLabel(false)
+                            .setOnChoose { timestamp ->
+                                start = timestamp
+                                onSelectChange(listOf(start.toString(), end.toString()))
+                            }.build().show()
+                    }, enabled = enable)
+                    .padding(10.dp),
+                fontSize = 14.sp,
+                lineHeight = 26.sp,
+                color = Text.copy(alpha = if (enable) 1f else 0.6f)
+            )
+            Text("-", modifier = Modifier.padding(horizontal = 5.dp), color = Text.copy(alpha = if (enable) 1f else 0.6f))
+            Text(
+                end.format("yyyy/MM/dd"),
+                modifier = Modifier
+                    .weight(1f)
+                    .height(46.dp)
+                    .border(1.dp, shape = RoundedCornerShape(6.dp), color = Color(0xFFE5E6EB))
+                    .clip(RoundedCornerShape(6.dp))
+                    .clickable(onClick = {
+                        CardDatePickerDialog.builder(ctx)
+                            .setTitle("请选择结束日期")
+                            .setDefaultTime(end)
+                            .setDisplayType(mutableListOf(DateTimeConfig.YEAR, DateTimeConfig.MONTH, DateTimeConfig.DAY))
+                            .setPickerLayout(R.layout.date_range_picker)
+                            .showBackNow(false)
+                            .showFocusDateInfo(false)
+                            .showDateLabel(false)
+                            .setOnChoose { timestamp ->
+                                end = timestamp
+                                onSelectChange(listOf(start.toString(), end.toString()))
+                            }.build().show()
+                    }, enabled = enable)
+                    .padding(10.dp),
+                fontSize = 14.sp,
+                lineHeight = 26.sp,
+                color = Text.copy(alpha = if (enable) 1f else 0.6f)
+            )
+        }
+
+    }
+}
+
+/**
+ * 将服务端返回的表单String JSON数组转化为可用的表单数据
+ */
+fun List<String>.getFormListByJsonList(): List<FormField> {
+    val list = ArrayList<FormField>()
+    this.forEach { form ->
+        Log.d("FormCompose", form)
+        val forms = form.getFormListByJsonObject()
+        list.addAll(forms)
+    }
+    return list
+}
+
 /**
  * 转换表单列表
  */
-fun String.getFormList(): List<FormField> {
+fun String.getFormListByJsonList(): List<FormField> {
     val json = Json { ignoreUnknownKeys = true }
     val fields = json.decodeFromString<List<FormField>>(this)
     val list = ArrayList<FormField>()
@@ -349,4 +547,29 @@ fun String.getFormList(): List<FormField> {
     fields.forEach()
 
     return list
-}
+}
+
+/**
+ * 转换表单列表
+ */
+fun String.getFormListByJsonObject(): List<FormField> {
+    val json = Json { ignoreUnknownKeys = true }
+    val form = json.decodeFromString<FormField>(this)
+    val list = ArrayList<FormField>()
+
+    fun List<FormField>.forEach() {
+        this.forEach { ch ->
+            if (ch.children.isNotEmpty()) ch.children.forEach()
+            else list.add(ch)
+        }
+    }
+
+    // 处理表单
+    if (form.children.isNotEmpty()) {
+        form.children.forEach()
+    } else {
+        list.add(form)
+    }
+
+    return list
+}

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

@@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.shape.RoundedCornerShape
@@ -30,31 +31,20 @@ import androidx.compose.ui.unit.sp
 import com.iscs.bozzys.R
 import com.iscs.bozzys.api.Job
 import com.iscs.bozzys.ui.pages.detail.job.openPageDetailJob
+import com.iscs.bozzys.ui.pages.detail.task.openPageDetailTask
 import com.iscs.bozzys.ui.theme.Text
 
 @OptIn(ExperimentalLayoutApi::class)
 @Composable
 fun JobListItem(job: Job) {
     val ctx = LocalContext.current
-
-    /**
-     * 获取等级名称和背景色
-     */
-    fun getLevelNameAndColor(level: String): Pair<String, Color> {
-        return when (level) {
-            "1" -> "紧急" to Color(0xFFFF9800)
-            "2" -> "非常紧急" to Color(0xFFFF4500)
-            else -> "正常" to Color(0xFF1E90FF)
-        }
-    }
-
+    // 获取等级名称和颜色
     val levelInfo = getLevelNameAndColor(job.urgencyLevel)
-
     Column(
         Modifier
             .padding(top = 1.dp, bottom = 15.dp)
             .fillMaxWidth()
-            .height(156.dp)
+            .heightIn(max = 200.dp)
             .border(1.dp, color = Color(0xFFEEEEEE), shape = RoundedCornerShape(12.dp))
             .clip(RoundedCornerShape(12.dp))
             .background(Color(0xFFF8F9FA))
@@ -75,57 +65,89 @@ fun JobListItem(job: Job) {
                 color = Color.White
             )
         }
-        FlowRow(Modifier.padding(top = 10.dp)) {
+        FlowRow(
+            Modifier
+                .padding(top = 10.dp)
+                .fillMaxWidth()
+        ) {
+            // 当前任务号
             Row(Modifier.padding(end = 12.dp), verticalAlignment = Alignment.CenterVertically) {
                 Icon(
-                    painterResource(R.drawable.location), contentDescription = null, modifier = Modifier
+                    painterResource(R.drawable.jobs), contentDescription = null, modifier = Modifier
                         .padding(end = 5.dp)
                         .size(13.dp),
-                    tint = Color(0xFF1E90FF)
+                    tint = Color(0xFF666666)
                 )
-                Text("车间A区", fontSize = 13.sp, color = Color(0xFF666666))
+                Text(job.orderNo, fontSize = 13.sp, color = Color(0xFF666666))
             }
-            Row(Modifier.padding(end = 12.dp), verticalAlignment = Alignment.CenterVertically) {
+            // 负责人
+            if (job.initiatorName.isNotEmpty()) Row(Modifier.padding(end = 12.dp), verticalAlignment = Alignment.CenterVertically) {
                 Icon(
                     painterResource(R.drawable.user), contentDescription = null, modifier = Modifier
                         .padding(end = 5.dp)
                         .size(13.dp),
                     tint = Color(0xFF666666)
                 )
-                Text("负责人:张三", fontSize = 13.sp, color = Color(0xFF666666))
+                Text("负责人:${job.initiatorName}", fontSize = 13.sp, color = Color(0xFF666666))
             }
-            Row(Modifier.padding(end = 12.dp), verticalAlignment = Alignment.CenterVertically) {
+            // 岗位区间,暂时没有字段匹配,先隐藏
+            if (false) Row(Modifier.padding(end = 12.dp), verticalAlignment = Alignment.CenterVertically) {
                 Icon(
-                    painterResource(R.drawable.jobs), contentDescription = null, modifier = Modifier
+                    painterResource(R.drawable.location), contentDescription = null, modifier = Modifier
                         .padding(end = 5.dp)
                         .size(13.dp),
-                    tint = Color(0xFF666666)
+                    tint = Color(0xFF1E90FF)
                 )
-                Text(job.orderNo, fontSize = 13.sp, color = Color(0xFF666666))
+                Text("车间A区", fontSize = 13.sp, color = Color(0xFF666666))
             }
-            Text("作业内容:${job.description}", fontSize = 13.sp, color = Color(0xFF666666))
+            // Text("作业内容:${task.currentNodeName ?: "--"}", fontSize = 13.sp, color = Color(0xFF666666))
         }
-        Spacer(Modifier.weight(1f))
         Row(
             Modifier
+                .padding(top = 5.dp)
                 .fillMaxWidth()
-                .height(36.dp), verticalAlignment = Alignment.CenterVertically
+                .height(20.dp),
+            verticalAlignment = Alignment.CenterVertically
         ) {
-            Text("工作描述内容", fontSize = 12.sp, color = Color(0xFF999999))
-            Spacer(Modifier.weight(1f))
+            Text("当前任务:", fontSize = 13.sp, lineHeight = 13.sp, color = Text)
             Text(
-                "立即处理",
+                job.currentNodeName ?: "--",
                 modifier = Modifier
                     .fillMaxHeight()
+                    .height(18.dp)
                     .clip(RoundedCornerShape(4.dp))
                     .background(Color(0xFF1E90FF))
-                    .padding(horizontal = 10.dp),
-                fontSize = 13.sp,
-                lineHeight = 36.sp,
+                    .padding(horizontal = 6.dp),
+                fontSize = 12.sp,
+                lineHeight = 18.sp,
                 fontWeight = FontWeight.Bold,
                 color = Color.White,
                 textAlign = TextAlign.Center
             )
         }
     }
-}
+}
+
+/**
+ * 获取等级名称和背景色
+ */
+private fun getLevelNameAndColor(level: String): Pair<String, Color> {
+    return when (level) {
+        "1" -> "紧急" to Color(0xFFFF9800)
+        "2" -> "非常紧急" to Color(0xFFFF4500)
+        else -> "正常" to Color(0xFF1E90FF)
+    }
+}
+
+/**
+ * 获取任务状态名称
+ */
+fun getJobStatusName(approvalStatus: String): String {
+    return when (approvalStatus) {
+        "approved" -> "已审核"
+        "unaudited" -> "未审核"
+        "pending" -> "待处理"
+        else -> approvalStatus
+    }
+}
+

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

@@ -0,0 +1,152 @@
+package com.iscs.bozzys.ui.pages.compose
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.FlowRow
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Icon
+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.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+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 com.iscs.bozzys.R
+import com.iscs.bozzys.api.Task
+import com.iscs.bozzys.ui.pages.detail.task.openPageDetailTask
+import com.iscs.bozzys.ui.theme.Text
+
+@OptIn(ExperimentalLayoutApi::class)
+@Composable
+fun TaskListItem(task: Task) {
+    val ctx = LocalContext.current
+    // 获取等级名称和颜色
+    val levelInfo = getLevelNameAndColor(task.urgencyLevel)
+    Column(
+        Modifier
+            .padding(top = 1.dp, bottom = 15.dp)
+            .fillMaxWidth()
+            .heightIn(max = 200.dp)
+            .border(1.dp, color = Color(0xFFEEEEEE), shape = RoundedCornerShape(12.dp))
+            .clip(RoundedCornerShape(12.dp))
+            .background(Color(0xFFF8F9FA))
+            .clickable(onClick = { ctx.openPageDetailTask(task) })
+            .padding(16.dp)
+    ) {
+        Row {
+            Text(task.name, fontSize = 15.sp, fontWeight = FontWeight.Medium, color = Text)
+            Spacer(Modifier.weight(1f))
+            Text(
+                levelInfo.first, modifier = Modifier
+                    .height(24.dp)
+                    .clip(RoundedCornerShape(12.dp))
+                    .background(levelInfo.second)
+                    .padding(horizontal = 10.dp),
+                fontSize = 12.sp,
+                lineHeight = 24.sp,
+                color = Color.White
+            )
+        }
+        FlowRow(
+            Modifier
+                .padding(top = 10.dp)
+                .fillMaxWidth()
+        ) {
+            // 当前任务号
+            Row(Modifier.padding(end = 12.dp), verticalAlignment = Alignment.CenterVertically) {
+                Icon(
+                    painterResource(R.drawable.jobs), contentDescription = null, modifier = Modifier
+                        .padding(end = 5.dp)
+                        .size(13.dp),
+                    tint = Color(0xFF666666)
+                )
+                Text(task.orderNo, fontSize = 13.sp, color = Color(0xFF666666))
+            }
+            // 负责人
+            if (!task.workerUserName.isNullOrEmpty()) Row(Modifier.padding(end = 12.dp), verticalAlignment = Alignment.CenterVertically) {
+                Icon(
+                    painterResource(R.drawable.user), contentDescription = null, modifier = Modifier
+                        .padding(end = 5.dp)
+                        .size(13.dp),
+                    tint = Color(0xFF666666)
+                )
+                Text("负责人:${task.workerUserName}", fontSize = 13.sp, color = Color(0xFF666666))
+            }
+            // 岗位区间,暂时没有字段匹配,先隐藏
+            if (false) Row(Modifier.padding(end = 12.dp), verticalAlignment = Alignment.CenterVertically) {
+                Icon(
+                    painterResource(R.drawable.location), contentDescription = null, modifier = Modifier
+                        .padding(end = 5.dp)
+                        .size(13.dp),
+                    tint = Color(0xFF1E90FF)
+                )
+                Text("车间A区", fontSize = 13.sp, color = Color(0xFF666666))
+            }
+            // Text("作业内容:${task.currentNodeName ?: "--"}", fontSize = 13.sp, color = Color(0xFF666666))
+        }
+        Row(
+            Modifier
+                .padding(top = 5.dp)
+                .fillMaxWidth()
+                .height(20.dp),
+            verticalAlignment = Alignment.CenterVertically
+        ) {
+            Text("当前任务:", fontSize = 13.sp, lineHeight = 13.sp, color = Text)
+            Text(
+                task.currentNodeName ?: "--",
+                modifier = Modifier
+                    .fillMaxHeight()
+                    .height(18.dp)
+                    .clip(RoundedCornerShape(4.dp))
+                    .background(Color(0xFF1E90FF))
+                    .padding(horizontal = 6.dp),
+                fontSize = 12.sp,
+                lineHeight = 18.sp,
+                fontWeight = FontWeight.Bold,
+                color = Color.White,
+                textAlign = TextAlign.Center
+            )
+        }
+    }
+}
+
+/**
+ * 获取等级名称和背景色
+ */
+private fun getLevelNameAndColor(level: String): Pair<String, Color> {
+    return when (level) {
+        "1" -> "紧急" to Color(0xFFFF9800)
+        "2" -> "非常紧急" to Color(0xFFFF4500)
+        else -> "正常" to Color(0xFF1E90FF)
+    }
+}
+
+/**
+ * 获取任务状态名称
+ */
+ fun getTaskStatusName(approvalStatus: String): String {
+    return when (approvalStatus) {
+        "approved" -> "已审核"
+        "unaudited" -> "未审核"
+        "pending" -> "待处理"
+        else -> approvalStatus
+    }
+}
+

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

@@ -18,11 +18,8 @@ import androidx.compose.material3.Icon
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateListOf
-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
@@ -50,16 +47,16 @@ class PageCreateJob : PageBase() {
     @Composable
     override fun GetViews(pv: PaddingValues) {
         // 作业名称
-        var jobName by remember { mutableStateOf("") }
+        val jobName = remember { mutableStateListOf("") }
         // 作业分类
         val typeOptions =
             mutableStateListOf(FormOption("请选择分类", ""), FormOption("维修", "repair"), FormOption("维保", "check"), FormOption("其他", "other"))
-        var typeSelect by remember { mutableStateOf(typeOptions[0]) }
+        val typeSelect = remember { mutableStateListOf(typeOptions[0].value) }
         // 作业内容
-        var jobContent by remember { mutableStateOf("") }
+        val jobContent = remember { mutableStateListOf("") }
         // 作业紧急程度
         val levelOptions = mutableStateListOf(FormOption("普通", "Normal"), FormOption("紧急", "Middle"), FormOption("非常紧急", "High"))
-        var jobLevel by remember { mutableStateOf(levelOptions[0]) }
+        val jobLevel = remember { mutableStateListOf(levelOptions[0].value) }
         Column(
             Modifier
                 .fillMaxSize()
@@ -69,13 +66,25 @@ class PageCreateJob : PageBase() {
             // 表单内容
             Column(Modifier.padding(horizontal = 16.dp, vertical = 5.dp)) {
                 // 作业名称
-                FormInput("作业名称", jobName, { jobName = it }, required = true)
+                FormInput("作业名称", jobName, {
+                    jobName.clear()
+                    jobName.addAll(it)
+                }, required = true)
                 // 作业分类
-                FormSelect("作业分类", typeSelect.value, typeOptions, { typeSelect = it })
+                FormSelect("作业分类", typeSelect, typeOptions, {
+                    typeSelect.clear()
+                    typeSelect.addAll(it)
+                })
                 // 作业内容
-                FormTextarea("作业内容", jobContent, { jobContent = it })
+                FormTextarea("作业内容", jobContent, {
+                    jobContent.clear()
+                    jobContent.addAll(it)
+                })
                 // 作业紧急程度
-                FormRadio("紧急程度", jobLevel.value, levelOptions, { jobLevel = it })
+                FormRadio("紧急程度", jobLevel, levelOptions, {
+                    jobLevel.clear()
+                    jobLevel.addAll(it)
+                })
             }
             Spacer(Modifier.weight(1f))
             // 底部下一步按钮

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

@@ -0,0 +1,473 @@
+package com.iscs.bozzys.ui.pages.detail.task
+
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.FlowRow
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+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.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateListOf
+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.graphics.Color
+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.lifecycle.viewmodel.compose.viewModel
+import com.iscs.bozzys.R
+import com.iscs.bozzys.api.FormField
+import com.iscs.bozzys.api.Task
+import com.iscs.bozzys.ui.base.PageBase
+import com.iscs.bozzys.ui.base.Title
+import com.iscs.bozzys.ui.pages.compose.CardContainer
+import com.iscs.bozzys.ui.pages.compose.FormDateRangeSelect
+import com.iscs.bozzys.ui.pages.compose.FormDateSelect
+import com.iscs.bozzys.ui.pages.compose.FormInput
+import com.iscs.bozzys.ui.pages.compose.FormRadio
+import com.iscs.bozzys.ui.pages.compose.FormSelect
+import com.iscs.bozzys.ui.pages.compose.FormTextarea
+import com.iscs.bozzys.ui.pages.compose.getFormListByJsonList
+import com.iscs.bozzys.ui.pages.compose.getTaskStatusName
+import com.iscs.bozzys.ui.pages.vm.VMDetailTask
+import com.iscs.bozzys.ui.theme.Text
+
+/**
+ * 打开任务详情页面
+ */
+fun Context.openPageDetailTask(task: Task) {
+    startActivity(Intent(this, PageDetailTask::class.java).apply {
+        putExtra("task", task)
+    })
+}
+
+/**
+ * 任务详情页面
+ */
+class PageDetailTask : PageBase() {
+
+    // 页面携带数据对象
+    private lateinit var task: Task
+
+    /**
+     * 获取页面携带的数据
+     */
+    private fun getPageData(): Boolean {
+        task = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            intent.getSerializableExtra("task", Task::class.java) ?: return false
+        } else {
+            @Suppress("DEPRECATION")
+            (intent.getSerializableExtra("task") as? Task) ?: return false
+        }
+        return true
+    }
+
+    @Composable
+    override fun GetViews(pv: PaddingValues) {
+        if (!getPageData()) return
+        val vm: VMDetailTask = viewModel()
+        LaunchedEffect(Unit) {
+            vm.toast.showToast()
+            vm.loading.loading()
+            vm.getTaskFormInfo(task)
+        }
+        val state by vm.state.collectAsState()
+        Column(Modifier.fillMaxSize()) {
+            Title(pv, task.name)
+            Column(
+                modifier = Modifier
+                    .weight(1f)
+                    .verticalScroll(state = rememberScrollState())
+                    .padding(16.dp)
+            ) {
+                TaskInfo(task)
+                // 处理动态内容的条件
+                when (state.taskInfo.type) {
+                    "review",                       // 审核
+                    "inputInfo",                    // 录入信息
+                    "complete" -> TaskForm(vm)      // 完成操作,录入表单信息
+                    "releaseIsolation",             // 解除隔离
+                    "returnLock" -> TaskDevice(vm)  // 还锁
+                }
+            }
+            // 底部操作功能封装
+            TaskOptions(pv, vm)
+        }
+    }
+
+    /**
+     * 任务基础信息
+     */
+    @Composable
+    fun TaskInfo(task: Task) {
+        CardContainer(modifier = Modifier.fillMaxWidth()) {
+            Column(Modifier.padding(10.dp)) {
+                Row(verticalAlignment = Alignment.CenterVertically) {
+                    Icon(
+                        painterResource(R.drawable.tasks),
+                        contentDescription = null,
+                        modifier = Modifier
+                            .padding(end = 5.dp)
+                            .size(16.dp),
+                        tint = MaterialTheme.colorScheme.primary
+                    )
+                    Text("任务信息", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = Text)
+                }
+                FlowRow(
+                    verticalArrangement = Arrangement.Center, modifier = Modifier
+                        .padding(top = 6.dp)
+                        .offset(x = (-3).dp)
+                ) {
+                    // 任务编号
+                    Row(
+                        modifier = Modifier
+                            .padding(horizontal = 3.dp, vertical = 3.dp)
+                            .clip(RoundedCornerShape(50))
+                            .background(Color(0xFFFFF8E6))
+                            .padding(horizontal = 10.dp, vertical = 2.dp),
+                        verticalAlignment = Alignment.CenterVertically
+                    ) {
+                        Icon(
+                            painterResource(R.drawable.number),
+                            contentDescription = null,
+                            tint = MaterialTheme.colorScheme.primary,
+                            modifier = Modifier
+                                .padding(end = 3.dp)
+                                .size(14.dp)
+                        )
+                        Text(task.orderNo, fontSize = 12.sp, color = Text)
+                    }
+                    // 岗位区间
+                    if (false) Row(
+                        modifier = Modifier
+                            .padding(horizontal = 3.dp, vertical = 3.dp)
+                            .clip(RoundedCornerShape(50))
+                            .background(Color(0xFFFFF8E6))
+                            .padding(horizontal = 10.dp, vertical = 2.dp),
+                        verticalAlignment = Alignment.CenterVertically
+                    ) {
+                        Icon(
+                            painterResource(R.drawable.location),
+                            contentDescription = null,
+                            tint = MaterialTheme.colorScheme.primary,
+                            modifier = Modifier
+                                .padding(end = 3.dp)
+                                .size(14.dp)
+                        )
+                        Text("车间A区", fontSize = 12.sp, color = Text)
+                    }
+                    // 负责人
+                    if (!task.workerUserName.isNullOrEmpty()) Row(
+                        modifier = Modifier
+                            .padding(horizontal = 3.dp, vertical = 3.dp)
+                            .clip(RoundedCornerShape(50))
+                            .background(Color(0xFFFFF8E6))
+                            .padding(horizontal = 10.dp, vertical = 2.dp),
+                        verticalAlignment = Alignment.CenterVertically
+                    ) {
+                        Icon(
+                            painterResource(R.drawable.user),
+                            contentDescription = null,
+                            tint = MaterialTheme.colorScheme.primary,
+                            modifier = Modifier
+                                .padding(end = 3.dp)
+                                .size(14.dp)
+                        )
+                        Text(task.workerUserName, fontSize = 12.sp, color = Text)
+                    }
+                    // 任务状态
+                    Row(
+                        modifier = Modifier
+                            .padding(horizontal = 3.dp, vertical = 3.dp)
+                            .clip(RoundedCornerShape(50))
+                            .background(Color(0xFFFFF8E6))
+                            .padding(horizontal = 10.dp, vertical = 2.dp),
+                        verticalAlignment = Alignment.CenterVertically
+                    ) {
+                        Icon(
+                            painterResource(R.drawable.target),
+                            contentDescription = null,
+                            tint = MaterialTheme.colorScheme.primary,
+                            modifier = Modifier
+                                .padding(end = 3.dp)
+                                .size(14.dp)
+                        )
+                        Text(getTaskStatusName(task.approvalStatus), fontSize = 12.sp, color = Text)
+                    }
+                    // 任务创建时间
+                    if (false) Row(
+                        modifier = Modifier
+                            .padding(horizontal = 3.dp, vertical = 3.dp)
+                            .clip(RoundedCornerShape(50))
+                            .background(Color(0xFFFFF8E6))
+                            .padding(horizontal = 10.dp, vertical = 2.dp),
+                        verticalAlignment = Alignment.CenterVertically
+                    ) {
+                        Icon(
+                            painterResource(R.drawable.job_todo),
+                            contentDescription = null,
+                            tint = MaterialTheme.colorScheme.primary,
+                            modifier = Modifier
+                                .padding(end = 3.dp)
+                                .size(14.dp)
+                        )
+                        Text("8小时", fontSize = 12.sp, color = Text)
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * 表单信息
+     */
+    @Composable
+    fun TaskForm(vm: VMDetailTask) {
+        val state by vm.state.collectAsState()
+        val stateFormList = remember { mutableStateListOf<FormField>() }
+        LaunchedEffect(state.formInfo.fields) {
+            stateFormList.addAll(state.formInfo.fields.getFormListByJsonList())
+        }
+        CardContainer(Modifier.padding(top = 10.dp)) {
+            Column(
+                Modifier
+                    .fillMaxWidth()
+                    .padding(10.dp)
+            ) {
+                Row(verticalAlignment = Alignment.CenterVertically) {
+                    Icon(
+                        painterResource(R.drawable.jobs),
+                        contentDescription = null,
+                        modifier = Modifier
+                            .padding(end = 5.dp)
+                            .size(16.dp),
+                        tint = MaterialTheme.colorScheme.primary
+                    )
+                    Text("录入信息", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = Text)
+                }
+                stateFormList.forEach { form ->
+                    when (form.type) {
+                        // 单行文本输入
+                        "input" -> FormInput(
+                            form.label,
+                            form.value,
+                            { form.value = it },
+                            form.placeholder,
+                            required = form.required,
+                            enable = state.taskInfo.approvalStatus != "approved"
+                        )
+                        // 多行文本输入
+                        "textarea" -> FormTextarea(
+                            form.label,
+                            form.value,
+                            { form.value = it },
+                            form.placeholder,
+                            required = form.required,
+                            enable = state.taskInfo.approvalStatus != "approved"
+                        )
+                        // 选择器
+                        "select" -> FormSelect(
+                            form.label,
+                            form.value,
+                            form.options,
+                            { form.value = it }, required = form.required, enable = state.taskInfo.approvalStatus != "approved"
+                        )
+                        // 日期选择
+                        "date" -> FormDateSelect(
+                            form.label,
+                            form.value,
+                            { form.value = it },
+                            required = form.required, enable = state.taskInfo.approvalStatus != "approved"
+                        )
+                        // 起止日期选择
+                        "daterange" -> FormDateRangeSelect(
+                            form.label,
+                            form.value,
+                            { form.value = it },
+                            placeholder = form.placeholder,
+                            enable = state.taskInfo.approvalStatus != "approved"
+                        )
+                        // 单选
+                        "radio" -> FormRadio(
+                            form.label,
+                            form.value,
+                            form.options,
+                            { form.value = it }, required = form.required, enable = state.taskInfo.approvalStatus != "approved"
+                        )
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * 设备相关的操作
+     */
+    @Composable
+    fun TaskDevice(vm: VMDetailTask) {
+        CardContainer(
+            Modifier
+                .padding(top = 10.dp)
+                .fillMaxWidth()
+        ) {
+            Column(
+                Modifier
+                    .fillMaxWidth()
+                    .height(120.dp),
+                horizontalAlignment = Alignment.CenterHorizontally,
+                verticalArrangement = Arrangement.Center
+            ) {
+                Icon(
+                    painter = painterResource(R.drawable.job_warning),
+                    contentDescription = null,
+                    modifier = Modifier.size(60.dp),
+                    tint = MaterialTheme.colorScheme.primary
+                )
+                Text("请前往锁控柜进行取锁,取钥匙操作", fontSize = 14.sp, color = Text)
+            }
+        }
+    }
+
+    /**
+     * 底部多功能按钮
+     */
+    @Composable
+    fun TaskOptions(pv: PaddingValues, vm: VMDetailTask) {
+        val pb = pv.calculateBottomPadding()
+        val state by vm.state.collectAsState()
+        Column(
+            Modifier
+                .padding(bottom = if (pb.value <= 0) 10.dp else pb)
+                .fillMaxWidth()
+                .padding(top = 5.dp)
+        ) {
+            // 配置功能按键
+            Row(
+                modifier = Modifier
+                    .padding(horizontal = 16.dp)
+                    .fillMaxWidth()
+            ) {
+                if (listOf("complete", "inputInfo").contains(state.taskInfo.type)) {
+                    Button(
+                        { destroy() }, modifier = Modifier
+                            .padding(horizontal = 8.dp)
+                            .weight(1f)
+                            .height(50.dp)
+                            .clip(RoundedCornerShape(12.dp)),
+                        colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFFFF0E6)),
+                        shape = RoundedCornerShape(12.dp)
+                    ) {
+                        Row(verticalAlignment = Alignment.CenterVertically) {
+                            Icon(
+                                painter = painterResource(R.drawable.job_close),
+                                contentDescription = null,
+                                modifier = Modifier
+                                    .padding(end = 5.dp)
+                                    .size(16.dp),
+                                tint = Color(0xFFFF4D4F)
+                            )
+                            Text("取消", fontSize = 14.sp, lineHeight = 14.sp, color = Color(0xFFFF4D4F))
+                        }
+                    }
+                    Button(
+                        {}, modifier = Modifier
+                            .padding(horizontal = 8.dp)
+                            .weight(1f)
+                            .height(50.dp)
+                            .clip(RoundedCornerShape(12.dp)),
+                        shape = RoundedCornerShape(12.dp)
+                    ) {
+                        Row(verticalAlignment = Alignment.CenterVertically) {
+                            Icon(
+                                painter = painterResource(R.drawable.job_finish),
+                                contentDescription = null,
+                                modifier = Modifier
+                                    .padding(end = 5.dp)
+                                    .size(16.dp),
+                                tint = Color.White
+                            )
+                            Text("提交", fontSize = 14.sp, lineHeight = 14.sp, color = Color.White)
+                        }
+                    }
+                } else if (listOf("review").contains(state.taskInfo.type)) {
+                    Button(
+                        { destroy() },
+                        enabled = state.taskInfo.approvalStatus != "approved", // 审核通过禁用
+                        modifier = Modifier
+                            .padding(horizontal = 8.dp)
+                            .weight(1f)
+                            .height(50.dp)
+                            .clip(RoundedCornerShape(12.dp)),
+                        colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFFFF0E6)),
+                        shape = RoundedCornerShape(12.dp)
+                    ) {
+                        Row(verticalAlignment = Alignment.CenterVertically) {
+                            Icon(
+                                painter = painterResource(R.drawable.job_close),
+                                contentDescription = null,
+                                modifier = Modifier
+                                    .padding(end = 5.dp)
+                                    .size(16.dp),
+                                tint = if (state.taskInfo.approvalStatus == "approved") Color.White else Color(0xFFFF4D4F)
+                            )
+                            Text(
+                                "审核不通过",
+                                fontSize = 14.sp,
+                                lineHeight = 14.sp,
+                                color = if (state.taskInfo.approvalStatus == "approved") Color.White else Color(
+                                    0xFFFF4D4F
+                                )
+                            )
+                        }
+                    }
+                    Button(
+                        {},
+                        enabled = state.taskInfo.approvalStatus != "approved", // 审核通过禁用
+                        modifier = Modifier
+                            .padding(horizontal = 8.dp)
+                            .weight(1f)
+                            .height(50.dp)
+                            .clip(RoundedCornerShape(12.dp)),
+                        shape = RoundedCornerShape(12.dp)
+                    ) {
+                        Row(verticalAlignment = Alignment.CenterVertically) {
+                            Icon(
+                                painter = painterResource(R.drawable.job_finish),
+                                contentDescription = null,
+                                modifier = Modifier
+                                    .padding(end = 5.dp)
+                                    .size(16.dp),
+                                tint = Color.White
+                            )
+                            Text("审核通过", fontSize = 14.sp, lineHeight = 14.sp, color = Color.White)
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+}

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

@@ -63,12 +63,13 @@ import com.iscs.bozzys.api.FormField
 import com.iscs.bozzys.ui.base.PageBase
 import com.iscs.bozzys.ui.base.Title
 import com.iscs.bozzys.ui.pages.compose.CardContainer
+import com.iscs.bozzys.ui.pages.compose.FormDateRangeSelect
 import com.iscs.bozzys.ui.pages.compose.FormDateSelect
 import com.iscs.bozzys.ui.pages.compose.FormInput
 import com.iscs.bozzys.ui.pages.compose.FormRadio
 import com.iscs.bozzys.ui.pages.compose.FormSelect
 import com.iscs.bozzys.ui.pages.compose.FormTextarea
-import com.iscs.bozzys.ui.pages.compose.getFormList
+import com.iscs.bozzys.ui.pages.compose.getFormListByJsonList
 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.Node
@@ -100,7 +101,7 @@ class PageEditStep : PageBase() {
         val json =
             "[{\"id\":\"field_1766037799194\",\"type\":\"card\",\"label\":\"卡片容器\",\"name\":\"field1766037799194\",\"required\":false,\"placeholder\":\"\",\"options\":[],\"cardTitle\":\"卡片容器\",\"children\":[{\"id\":\"field_1766037802805\",\"type\":\"grid\",\"label\":\"双栏布局\",\"name\":\"field1766037802805\",\"required\":false,\"placeholder\":\"\",\"options\":[],\"gridColumns\":2,\"children\":[{\"id\":\"field_1766037804595\",\"type\":\"textarea\",\"label\":\"字段2\",\"name\":\"field1766037804595\",\"required\":false,\"placeholder\":\"请输入内容\",\"options\":[]},{\"id\":\"field_1766037807731\",\"type\":\"input\",\"label\":\"字段2\",\"name\":\"field1766037807731\",\"required\":false,\"placeholder\":\"请输入\",\"options\":[]}]}]},{\"id\":\"field_1766038059114\",\"type\":\"select\",\"label\":\"字段2\",\"name\":\"field1766038059114\",\"required\":false,\"placeholder\":\"请选择\",\"options\":[{\"label\":\"选项1\",\"value\":\"option1\"},{\"label\":\"选项2\",\"value\":\"option2\"}]},{\"id\":\"field_1766038062137\",\"type\":\"date\",\"label\":\"字段3\",\"name\":\"field1766038062137\",\"required\":false,\"placeholder\":\"请选择日期\",\"options\":[]},{\"id\":\"field_1766473806339\",\"type\":\"radio\",\"label\":\"字段4\",\"name\":\"field1766473806339\",\"required\":false,\"options\":[{\"label\":\"选项1\",\"value\":\"option1\"},{\"label\":\"选项2\",\"value\":\"option2\"}]}]"
         val fields = remember { mutableStateListOf<FormField>() }
-        fields.addAll(json.getFormList())
+        fields.addAll(json.getFormListByJsonList())
         LaunchedEffect(Unit) {
             nodes["A"] = Node("A", Offset(125f, -100f))
             nodes["B"] = Node("B", Offset(50f, 0f))
@@ -163,20 +164,37 @@ class PageEditStep : PageBase() {
                             ) {
                                 items(fields) {
                                     when (it.type) {
-                                        "textarea" -> FormTextarea(it.label, it.value, { sel -> it.value = sel }, it.placeholder)
-                                        "input" -> FormInput(it.label, it.value, { sel -> it.value = sel }, it.placeholder)
+                                        "input" -> FormInput(it.label, it.value, { sel -> it.value = sel }, it.placeholder, required = it.required)
+                                        "textarea" -> FormTextarea(
+                                            it.label,
+                                            it.value,
+                                            { sel -> it.value = sel },
+                                            it.placeholder,
+                                            required = it.required
+                                        )
+
                                         "select" -> FormSelect(
                                             it.label,
-                                            it.value.ifEmpty { it.options[0].value },
+                                            it.value,
                                             it.options,
-                                            { sel -> it.value = sel.value })
+                                            { sel -> it.value = sel }, required = it.required
+                                        )
 
-                                        "date" -> FormDateSelect(it.label, it.value.ifEmpty { "0" }.toLong(), { sel -> it.value = sel.toString() })
+                                        "date" -> FormDateSelect(it.label, it.value, { sel -> it.value = sel }, required = it.required)
+                                        // 起止日期选择
+                                        "daterange" -> FormDateRangeSelect(
+                                            it.label,
+                                            it.value,
+                                            { sel -> it.value = sel },
+                                            placeholder = it.placeholder
+                                        )
+                                        // 单选框
                                         "radio" -> FormRadio(
                                             it.label,
-                                            it.value.ifEmpty { it.options[0].value },
+                                            it.value,
                                             it.options,
-                                            { sel -> it.value = sel.value })
+                                            { sel -> it.value = sel }, required = it.required
+                                        )
                                     }
                                 }
                             }

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

@@ -38,6 +38,7 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.graphics.Brush
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.style.TextAlign
@@ -46,7 +47,9 @@ import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 import androidx.compose.ui.zIndex
 import com.iscs.bozzys.R
-import com.iscs.bozzys.ui.pages.compose.JobListItem
+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
 
@@ -67,7 +70,7 @@ fun HomeCompose(pv: PaddingValues, zIndex: Float, vmHome: VMHome) {
         }) {
             Column(Modifier.fillMaxSize()) {
                 Box {
-                    TODOList(pv, vmHome)
+                    TaskList(pv, vmHome)
                     TODO(vmHome)
                 }
             }
@@ -80,6 +83,7 @@ fun HomeCompose(pv: PaddingValues, zIndex: Float, vmHome: VMHome) {
  */
 @Composable
 private fun TopToolBar(pv: PaddingValues, vmHome: VMHome) {
+    val ctx = LocalContext.current
     val state by vmHome.state.collectAsState()
     Column(
         modifier = Modifier
@@ -116,7 +120,7 @@ private fun TopToolBar(pv: PaddingValues, vmHome: VMHome) {
                     modifier = Modifier
                         .size(36.dp)
                         .clip(RoundedCornerShape(6.dp))
-                        .clickable(onClick = { vmHome.updateNavIndex(2) })
+                        .clickable(onClick = { ctx.openPageMessage() })
                         .padding(9.dp),
                     tint = Color.White
                 )
@@ -140,7 +144,7 @@ private fun TopToolBar(pv: PaddingValues, vmHome: VMHome) {
                 modifier = Modifier
                     .size(36.dp)
                     .clip(RoundedCornerShape(6.dp))
-                    .clickable(onClick = { vmHome.updateNavIndex(3) })
+                    .clickable(onClick = { vmHome.navigationToId(3) })
                     .padding(10.dp),
                 tint = Color.White
             )
@@ -153,6 +157,7 @@ private fun TopToolBar(pv: PaddingValues, vmHome: VMHome) {
  */
 @Composable
 private fun TODO(vmHome: VMHome) {
+    val state by vmHome.state.collectAsState()
     Column(
         Modifier
             .fillMaxWidth()
@@ -160,7 +165,7 @@ private fun TODO(vmHome: VMHome) {
             .padding(top = 3.dp)
     ) {
         Text(
-            "中午好,今天有2个待办任务", Modifier
+            "中午好,今天有${state.todoCountPending}个待办任务", Modifier
                 .fillMaxWidth()
                 .height(51.dp)
                 .background(Color(0xFFE6F7FF)),
@@ -179,7 +184,7 @@ private fun TODO(vmHome: VMHome) {
                     .background(Color(0xFFFFF5EB)),
                 verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally
             ) {
-                Text("2", fontSize = 24.sp, fontWeight = FontWeight.Bold, color = Color(0xFFFF4500))
+                Text("${state.todoCountPending}", fontSize = 24.sp, fontWeight = FontWeight.Bold, color = Color(0xFFFF4500))
                 Text("待处理", fontSize = 12.sp, color = Text, modifier = Modifier.padding(top = 5.dp))
                 Icon(
                     painterResource(R.drawable.job_todo), contentDescription = null, modifier = Modifier
@@ -197,7 +202,7 @@ private fun TODO(vmHome: VMHome) {
                     .background(Color(0xFFFFF5EB)),
                 verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally
             ) {
-                Text("5", fontSize = 24.sp, fontWeight = FontWeight.Bold, color = Color(0xFFFFA500))
+                Text("${state.todoCountRunning}", fontSize = 24.sp, fontWeight = FontWeight.Bold, color = Color(0xFFFFA500))
                 Text("进行中", fontSize = 12.sp, color = Text, modifier = Modifier.padding(top = 5.dp))
                 Icon(
                     painterResource(R.drawable.job_ing), contentDescription = null, modifier = Modifier
@@ -215,7 +220,7 @@ private fun TODO(vmHome: VMHome) {
                     .background(Color(0xFFFFF5EB)),
                 verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally
             ) {
-                Text("10", fontSize = 24.sp, fontWeight = FontWeight.Bold, color = Color(0xFF32CD32))
+                Text("${state.todoCountFinish}", fontSize = 24.sp, fontWeight = FontWeight.Bold, color = Color(0xFF32CD32))
                 Text("本月完成", fontSize = 12.sp, color = Text, modifier = Modifier.padding(top = 5.dp))
                 Icon(
                     painterResource(R.drawable.job_finish), contentDescription = null, modifier = Modifier
@@ -233,23 +238,23 @@ private fun TODO(vmHome: VMHome) {
  * 待办列表
  */
 @Composable
-private fun TODOList(pv: PaddingValues, vmHome: VMHome) {
+private fun TaskList(pv: PaddingValues, vmHome: VMHome) {
+    val ctx = LocalContext.current
     val state by vmHome.state.collectAsState()
-    val messageList = listOf(1, 2, 3)
     Column(
         Modifier
             .padding(horizontal = 16.dp)
             .padding(bottom = pv.calculateBottomPadding())
             .fillMaxSize()
             .verticalScroll(state = rememberScrollState())
-            .padding(top = 190.dp)
+            .padding(top = 205.dp)
     ) {
         Row(
             Modifier
                 .fillMaxWidth()
-                .height(46.dp)
+                .height(36.dp)
                 .clickable(indication = null, interactionSource = remember { MutableInteractionSource() }, onClick = {
-                    vmHome.updateNavIndex(1)
+                    vmHome.navigationToId(2)
                 }),
             verticalAlignment = Alignment.CenterVertically
         ) {
@@ -257,32 +262,62 @@ private fun TODOList(pv: PaddingValues, vmHome: VMHome) {
             Spacer(Modifier.weight(1f))
             Text("查看全部", fontSize = 14.sp, color = Color(0xFF1E90FF))
         }
+        if (state.homeTasks.isEmpty()) Column(
+            Modifier
+                .padding(bottom = 15.dp)
+                .fillMaxWidth()
+                .height(102.dp)
+                .border(1.dp, color = Color(0xFFEEEEEE), shape = RoundedCornerShape(12.dp))
+                .clip(RoundedCornerShape(12.dp))
+                .background(Color(0xFFF8F9FA))
+                .padding(16.dp),
+            verticalArrangement = Arrangement.Center,
+            horizontalAlignment = Alignment.CenterHorizontally,
+        ) {
+            Text("您当前暂无任务", fontSize = 14.sp, color = Text)
+        }
         Column(Modifier.fillMaxWidth()) {
-            state.homeJobs.forEach { job -> key(job.id) { JobListItem(job) } }
+            state.homeTasks.forEachIndexed { idx, task ->
+                key(task.workId) {
+                    TaskListItem(task)
+                }
+            }
         }
         Row(
             Modifier
                 .fillMaxWidth()
-                .height(46.dp)
+                .height(36.dp)
                 .clickable(indication = null, interactionSource = remember { MutableInteractionSource() }, onClick = {
-                    vmHome.updateNavIndex(2)
+                    ctx.openPageMessage()
                 }),
-            verticalAlignment = Alignment.CenterVertically
+            verticalAlignment = Alignment.CenterVertically,
         ) {
-            Text("最新消息", modifier = Modifier.fillMaxHeight(), fontSize = 14.sp, fontWeight = FontWeight.Medium, color = Text)
+            Text("最新消息", fontSize = 14.sp, fontWeight = FontWeight.Medium, color = Text)
             Spacer(Modifier.weight(1f))
-            Text("全部消息", modifier = Modifier.fillMaxHeight(), fontSize = 14.sp, color = Color(0xFF1E90FF))
+            Text("全部消息", fontSize = 14.sp, color = Color(0xFF1E90FF))
+        }
+        if (state.messageList.isEmpty()) Column(
+            Modifier
+                .padding(bottom = 15.dp)
+                .fillMaxWidth()
+                .height(102.dp)
+                .border(1.dp, color = Color(0xFFEEEEEE), shape = RoundedCornerShape(12.dp))
+                .clip(RoundedCornerShape(12.dp))
+                .background(Color(0xFFF8F9FA))
+                .padding(16.dp),
+            verticalArrangement = Arrangement.Center,
+            horizontalAlignment = Alignment.CenterHorizontally,
+        ) {
+            Text("您当前暂无新消息", fontSize = 14.sp, color = Text)
         }
         Column(Modifier.fillMaxWidth()) {
-            messageList.forEach {
-                MessageListItem()
-            }
+            state.messageList.forEach { msg -> key(msg.id) { MessageListItem(msg) } }
         }
     }
 }
 
 @Composable
-fun MessageListItem() {
+fun MessageListItem(msg: Message) {
     Column(
         Modifier
             .padding(bottom = 15.dp)

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

@@ -23,6 +23,7 @@ import androidx.compose.material3.LocalTextStyle
 import androidx.compose.material3.Text
 import androidx.compose.material3.pulltorefresh.PullToRefreshBox
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
@@ -41,15 +42,15 @@ import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 import androidx.compose.ui.zIndex
 import com.iscs.bozzys.R
-import com.iscs.bozzys.api.Job
 import com.iscs.bozzys.ui.pages.compose.JobListItem
+import com.iscs.bozzys.ui.pages.create.job.openPageCreateJob
 import com.iscs.bozzys.ui.pages.select.job.openPageSelectJobType
 import com.iscs.bozzys.ui.pages.vm.VMHome
 import com.iscs.bozzys.ui.theme.Main
 import com.iscs.bozzys.ui.theme.Text
 
 @Composable
-fun JobsCompose(pv: PaddingValues, zIndex: Float, vmHome: VMHome) {
+fun JobsCompose(pv: PaddingValues, zIndex: Float, vm: VMHome) {
     Box(
         modifier = Modifier
             .fillMaxSize()
@@ -62,9 +63,9 @@ fun JobsCompose(pv: PaddingValues, zIndex: Float, vmHome: VMHome) {
                 .padding(bottom = pv.calculateBottomPadding())
                 .fillMaxSize()
         ) {
-            TopToolBar(pv, vmHome)
-            FilterBar()
-            MessageList()
+            TopToolBar(pv, vm)
+            FilterBar(vm)
+            TaskList(vm)
         }
     }
 }
@@ -96,7 +97,11 @@ private fun TopToolBar(pv: PaddingValues, vmHome: VMHome) {
                 modifier = Modifier
                     .size(36.dp)
                     .clip(RoundedCornerShape(6.dp))
-                    .clickable(onClick = { ctx.openPageSelectJobType() })
+                    .clickable(onClick = {
+                        // 前期SOP没有做流程选择,这里先直接跳转到创建页面
+                        // ctx.openPageSelectJobType()
+                        ctx.openPageCreateJob()
+                    })
                     .padding(6.dp),
                 tint = Color.White
             )
@@ -141,10 +146,12 @@ private fun TopToolBar(pv: PaddingValues, vmHome: VMHome) {
  * 过滤条
  */
 @Composable
-private fun FilterBar() {
+private fun FilterBar(vm: VMHome) {
+    val state by vm.state.collectAsState()
     var idx by remember { mutableStateOf(0) }
     Row(
         modifier = Modifier
+            .padding(vertical = 5.dp)
             .fillMaxWidth()
             .padding(horizontal = 16.dp)
             .padding(top = 6.dp)
@@ -157,7 +164,10 @@ private fun FilterBar() {
                 .background(
                     color = if (idx == 0) Main else Color(0xFFF0F0F0)
                 )
-                .clickable(onClick = { idx = 0 })
+                .clickable(onClick = {
+                    idx = 0
+                    vm.getJobList(state.jobPage.copy(page = 1, type = ""))
+                })
                 .padding(horizontal = 12.dp, vertical = 5.dp),
         )
         Text(
@@ -168,7 +178,10 @@ private fun FilterBar() {
                 .background(
                     color = if (idx == 1) Main else Color(0xFFF0F0F0)
                 )
-                .clickable(onClick = { idx = 1 })
+                .clickable(onClick = {
+                    idx = 1
+                    vm.getJobList(state.jobPage.copy(page = 1, type = "running"))
+                })
                 .padding(horizontal = 12.dp, vertical = 5.dp),
         )
         Text(
@@ -179,7 +192,10 @@ private fun FilterBar() {
                 .background(
                     color = if (idx == 2) Main else Color(0xFFF0F0F0)
                 )
-                .clickable(onClick = { idx = 2 })
+                .clickable(onClick = {
+                    idx = 2
+                    vm.getJobList(state.jobPage.copy(page = 1, type = "completed"))
+                })
                 .padding(horizontal = 12.dp, vertical = 5.dp),
         )
         Text(
@@ -190,20 +206,22 @@ private fun FilterBar() {
                 .background(
                     color = if (idx == 3) Main else Color(0xFFF0F0F0)
                 )
-                .clickable(onClick = { idx = 3 })
+                .clickable(onClick = {
+                    idx = 3
+                    vm.getJobList(state.jobPage.copy(page = 1, type = "cancelled"))
+                })
                 .padding(horizontal = 12.dp, vertical = 5.dp),
         )
     }
 }
 
 /**
- * 消息列表
+ * 任务列表数据
  */
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
-private fun MessageList() {
-    val isRefresh = remember { mutableStateOf(false) }
-    val list = listOf(Job(0, "", "", "", "", 0, "", "", 0, "", 0L, status = "", createTime = 0L))
+private fun TaskList(vm: VMHome) {
+    val state by vm.state.collectAsState()
     // 监听顶部Item
     val listState = rememberLazyListState()
     // 当前顶部Item
@@ -213,8 +231,8 @@ private fun MessageList() {
         }
     }
     Box {
-        PullToRefreshBox(isRefresh.value, onRefresh = {
-            isRefresh.value = true
+        PullToRefreshBox(state.jobPage.isRefresh, onRefresh = {
+            vm.getJobList(state.jobPage.copy(page = 1))
         }, modifier = Modifier.fillMaxSize()) {
             Box(
                 modifier = Modifier
@@ -226,18 +244,14 @@ private fun MessageList() {
                     state = listState,
                     modifier = Modifier.fillMaxSize()
                 ) {
-                    items(list) { item ->
-                        when (item) {
-                            // 1 -> MessageListItemTitle("今天")
-                            // 6 -> MessageListItemTitle("昨天")
-                            else -> JobListItem(item)
-                        }
+                    items(state.jobList) { item ->
+                        JobListItem(item)
                     }
                 }
                 // 悬浮在列表的顶部
-                Box(Modifier.background(Color.White)) {
-                    MessageListItemTitle("${list[topIdx]}")
-                }
+//                Box(Modifier.background(Color.White)) {
+//                    DateTitle("${list[topIdx]}")
+//                }
             }
         }
     }
@@ -247,7 +261,7 @@ private fun MessageList() {
  * 消息标题
  */
 @Composable
-private fun MessageListItemTitle(title: String) {
+private fun DateTitle(title: String) {
     Text(
         title,
         fontSize = 16.sp,

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

@@ -38,7 +38,6 @@ import androidx.compose.ui.unit.sp
 import androidx.lifecycle.viewmodel.compose.viewModel
 import com.iscs.bozzys.ui.base.PageBase
 import com.iscs.bozzys.ui.pages.vm.VMHome
-import kotlinx.coroutines.async
 
 /**
  * 打开主页面
@@ -62,8 +61,8 @@ class PageHome : PageBase() {
         val state by vm.state.collectAsState()
         LaunchedEffect(Unit) {
             // 处理基础Toast和Loading提示
-            vm.toastEvent.showToast()
-            vm.loadingEvent.loading()
+            vm.toast.showToast()
+            vm.loading.loading()
             vm.init()
         }
         Scaffold(
@@ -78,10 +77,10 @@ class PageHome : PageBase() {
             ) {
                 vm.state.value.navs.forEach { nav ->
                     when (nav.id) {
-                        0 -> HomeCompose(pv, if (state.navIndex == 0) 99f else 0f, vm)
-                        1 -> JobsCompose(pv, if (state.navIndex == 1) 99f else 0f, vm)
-                        2 -> MessageCompose(pv, if (state.navIndex == 2) 99f else 0f, vm)
-                        3 -> MyCompose(pv, if (state.navIndex == 3) 99f else 0f)
+                        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)
                     }
                 }
 
@@ -111,7 +110,7 @@ class PageHome : PageBase() {
                             .clickable(
                                 interactionSource = remember { MutableInteractionSource() },
                                 indication = null,
-                                onClick = { vm.updateNavIndex(nav.id) }),
+                                onClick = { vm.navigationToId(nav.id) }),
                     ) {
                         Column(
                             verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally,
@@ -121,14 +120,14 @@ class PageHome : PageBase() {
                                 painter = painterResource(nav.icon),
                                 contentDescription = null,
                                 modifier = Modifier.size(18.dp),
-                                tint = if (state.navIndex == nav.id) nav.selectedColor else nav.unselectedColor
+                                tint = if (state.navigationId == nav.id) nav.selectedColor else nav.unselectedColor
                             )
                             Text(
                                 nav.title,
                                 Modifier.padding(top = 5.dp),
                                 fontSize = 12.sp,
                                 lineHeight = 12.sp,
-                                color = if (state.navIndex == nav.id) nav.selectedColor else nav.unselectedColor
+                                color = if (state.navigationId == nav.id) nav.selectedColor else nav.unselectedColor
                             )
                         }
                         if (nav.count > 0) Text(

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

@@ -0,0 +1,257 @@
+package com.iscs.bozzys.ui.pages.home
+
+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.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.lazy.LazyColumn
+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.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.LocalTextStyle
+import androidx.compose.material3.Text
+import androidx.compose.material3.pulltorefresh.PullToRefreshBox
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.derivedStateOf
+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.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.platform.LocalContext
+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 com.iscs.bozzys.ui.pages.compose.TaskListItem
+import com.iscs.bozzys.ui.pages.vm.VMHome
+import com.iscs.bozzys.ui.theme.Main
+import com.iscs.bozzys.ui.theme.Text
+
+@Composable
+fun TasksCompose(pv: PaddingValues, zIndex: Float, vm: VMHome) {
+    Box(
+        modifier = Modifier
+            .fillMaxSize()
+            .zIndex(zIndex)
+            .background(Color.White)
+    ) {
+
+        // 顶部工具栏
+        Column(
+            modifier = Modifier
+                .padding(bottom = pv.calculateBottomPadding())
+                .fillMaxSize()
+        ) {
+            TopToolBar(pv, vm)
+            FilterBar(vm)
+            TaskList(vm)
+        }
+    }
+}
+
+/**
+ * 顶部工具栏
+ */
+@Composable
+private fun TopToolBar(pv: PaddingValues, vm: VMHome) {
+    val ctx = LocalContext.current
+    var keywords by remember { mutableStateOf("") }
+    Column(
+        modifier = Modifier
+            .fillMaxWidth()
+            .background(brush = Brush.horizontalGradient(listOf(Color(0xFFFF8C00), Color(0xFFFFA500))))
+            .padding(top = pv.calculateTopPadding() + 8.dp)
+    ) {
+        Row(
+            modifier = Modifier
+                .fillMaxWidth()
+                .padding(start = 16.dp, end = 10.dp),
+            verticalAlignment = Alignment.CenterVertically
+        ) {
+            Text("我的任务", fontSize = 18.sp, color = Color.White, fontWeight = FontWeight.Medium)
+            Spacer(Modifier.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
+                .background(brush = Brush.horizontalGradient(listOf(Color(0xFFFF8C00), Color(0xFFFFA500))))
+                .padding(horizontal = 16.dp, vertical = 8.dp)
+        ) {
+            BasicTextField(
+                keywords,
+                onValueChange = { keywords = it },
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .height(40.dp)
+                    .clip(RoundedCornerShape(50))
+                    .background(brush = Brush.horizontalGradient(listOf(Color(0xFFFFA126), Color(0xFFFFB126))))
+                    .padding(horizontal = 16.dp),
+                singleLine = true,
+                textStyle = LocalTextStyle.current.copy(fontSize = 16.sp, lineHeight = 18.sp, color = Color.White),
+                decorationBox = { innerTextField ->
+                    Box(contentAlignment = Alignment.CenterStart) {
+                        innerTextField()
+                        if (keywords.isEmpty()) {
+                            val text = "搜索任务名称..."
+                            Text(
+                                text,
+                                color = Color.White.copy(alpha = 0.8f),
+                                fontSize = 16.sp,
+                                lineHeight = 18.sp
+                            )
+                        }
+                    }
+                },
+                cursorBrush = SolidColor(Color.White),
+            )
+        }
+    }
+}
+
+/**
+ * 过滤条
+ */
+@Composable
+private fun FilterBar(vm: VMHome) {
+    var idx by remember { mutableStateOf(0) }
+    Row(
+        modifier = Modifier
+            .padding(vertical = 5.dp)
+            .fillMaxWidth()
+            .padding(horizontal = 16.dp)
+            .padding(top = 6.dp)
+    ) {
+        Text(
+            "全部", fontSize = 14.sp, color = if (idx == 0) Color.White else Color(0xFF666666),
+            modifier = Modifier
+                .padding(end = 10.dp)
+                .clip(RoundedCornerShape(50))
+                .background(
+                    color = if (idx == 0) Main else Color(0xFFF0F0F0)
+                )
+                .clickable(onClick = { idx = 0 })
+                .padding(horizontal = 12.dp, vertical = 5.dp),
+        )
+        Text(
+            "进行中", fontSize = 14.sp, color = if (idx == 1) Color.White else Color(0xFF666666),
+            modifier = Modifier
+                .padding(end = 10.dp)
+                .clip(RoundedCornerShape(50))
+                .background(
+                    color = if (idx == 1) Main else Color(0xFFF0F0F0)
+                )
+                .clickable(onClick = { idx = 1 })
+                .padding(horizontal = 12.dp, vertical = 5.dp),
+        )
+        Text(
+            "已完成", fontSize = 14.sp, color = if (idx == 2) Color.White else Color(0xFF666666),
+            modifier = Modifier
+                .padding(end = 10.dp)
+                .clip(RoundedCornerShape(50))
+                .background(
+                    color = if (idx == 2) Main else Color(0xFFF0F0F0)
+                )
+                .clickable(onClick = { idx = 2 })
+                .padding(horizontal = 12.dp, vertical = 5.dp),
+        )
+        Text(
+            "已取消", fontSize = 14.sp, color = if (idx == 3) Color.White else Color(0xFF666666),
+            modifier = Modifier
+                .padding(end = 10.dp)
+                .clip(RoundedCornerShape(50))
+                .background(
+                    color = if (idx == 3) Main else Color(0xFFF0F0F0)
+                )
+                .clickable(onClick = { idx = 3 })
+                .padding(horizontal = 12.dp, vertical = 5.dp),
+        )
+    }
+}
+
+/**
+ * 消息列表
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun TaskList(vm: VMHome) {
+    val state by vm.state.collectAsState()
+    // 监听顶部Item
+    val listState = rememberLazyListState()
+    // 当前顶部Item
+    val topIdx by remember {
+        derivedStateOf {
+            listState.firstVisibleItemIndex
+        }
+    }
+    Box {
+        PullToRefreshBox(state.taskPage.isRefresh, onRefresh = {
+            // 下拉刷新
+            vm.getTaskList(state.taskPage.copy(page = 1))
+        }, modifier = Modifier.fillMaxSize()) {
+            Box(
+                modifier = Modifier
+                    .padding(top = 5.dp)
+                    .fillMaxSize()
+                    .padding(horizontal = 16.dp)
+            ) {
+                LazyColumn(
+                    state = listState,
+                    modifier = Modifier.fillMaxSize()
+                ) {
+                    items(state.taskList) { item ->
+                        TaskListItem(item)
+                    }
+                }
+                // 悬浮在列表的顶部
+//                Box(Modifier.background(Color.White)) {
+//                    DateTitle("${list[topIdx]}")
+//                }
+            }
+        }
+    }
+}
+
+/**
+ * 消息标题
+ */
+@Composable
+private fun DateTitle(title: String) {
+    Text(
+        title,
+        fontSize = 16.sp,
+        lineHeight = 22.sp,
+        fontWeight = FontWeight.Medium,
+        color = Text,
+        modifier = Modifier
+            .padding(bottom = 10.dp)
+            .fillMaxWidth()
+            .height(22.dp),
+    )
+}

+ 2 - 5
app/src/main/java/com/iscs/bozzys/ui/pages/login/PageLogin.kt

@@ -11,7 +11,6 @@ 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.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
@@ -43,7 +42,6 @@ import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.input.KeyboardType
 import androidx.compose.ui.text.input.PasswordVisualTransformation
 import androidx.compose.ui.text.input.VisualTransformation
-import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 import androidx.lifecycle.viewmodel.compose.viewModel
@@ -52,7 +50,6 @@ import com.iscs.bozzys.ui.base.PageBase
 import com.iscs.bozzys.ui.pages.home.openPageHome
 import com.iscs.bozzys.ui.pages.vm.VMLogin
 import com.iscs.bozzys.ui.theme.Main
-import com.iscs.bozzys.ui.theme.SubMain
 import com.iscs.bozzys.ui.theme.Text
 import com.iscs.bozzys.ui.theme.TextDesc
 
@@ -121,8 +118,8 @@ class PageLogin : PageBase() {
         val state = vm.state
         LaunchedEffect(Unit) {
             // 处理基础Toast和Loading提示
-            vm.toastEvent.showToast()
-            vm.loadingEvent.loading()
+            vm.toast.showToast()
+            vm.loading.loading()
         }
         Column(
             Modifier

+ 94 - 54
app/src/main/java/com/iscs/bozzys/ui/pages/home/MessageCompose.kt → app/src/main/java/com/iscs/bozzys/ui/pages/message/PageMessage.kt

@@ -1,5 +1,7 @@
-package com.iscs.bozzys.ui.pages.home
+package com.iscs.bozzys.ui.pages.message
 
+import android.content.Context
+import android.content.Intent
 import androidx.compose.foundation.background
 import androidx.compose.foundation.border
 import androidx.compose.foundation.clickable
@@ -14,6 +16,7 @@ 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.layout.width
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.items
 import androidx.compose.foundation.lazy.rememberLazyListState
@@ -34,77 +37,114 @@ import androidx.compose.ui.graphics.Brush
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
 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 com.iscs.bozzys.R
-import com.iscs.bozzys.ui.pages.vm.VMHome
-import com.iscs.bozzys.ui.theme.Main
+import com.iscs.bozzys.ui.base.PageBase
 import com.iscs.bozzys.ui.theme.Text
 
-@Composable
-fun MessageCompose(pv: PaddingValues, zIndex: Float, vmHome: VMHome) {
-    Box(
-        modifier = Modifier
-            .fillMaxSize()
-            .zIndex(zIndex)
-            .background(Color.White)
-    ) {
-        // 顶部工具栏
-        Column(
+/**
+ * 打开消息中心页面
+ */
+fun Context.openPageMessage() {
+    startActivity(Intent(this, PageMessage::class.java))
+}
+
+/**
+ * 消息中心
+ */
+class PageMessage : PageBase() {
+    @Composable
+    override fun GetViews(pv: PaddingValues) {
+        Box(
             modifier = Modifier
-                .padding(bottom = pv.calculateBottomPadding())
                 .fillMaxSize()
+                .background(Color.White)
         ) {
-            TopToolBar(pv, vmHome)
-            MessageList()
+            // 顶部工具栏
+            Column(
+                modifier = Modifier
+                    .padding(bottom = pv.calculateBottomPadding())
+                    .fillMaxSize()
+            ) {
+                TopToolBar(pv)
+                MessageList()
+            }
         }
     }
-}
 
-/**
- * 顶部工具栏
- */
-@Composable
-private fun TopToolBar(pv: PaddingValues, vmHome: VMHome) {
-    Column(
-        modifier = Modifier
-            .fillMaxWidth()
-            .background(brush = Brush.horizontalGradient(listOf(Color(0xFFFF8C00), Color(0xFFFFA500))))
-            .padding(top = pv.calculateTopPadding())
-    ) {
-        Row(
+
+    /**
+     * 顶部工具栏
+     */
+    @Composable
+    private fun TopToolBar(pv: PaddingValues) {
+        Column(
             modifier = Modifier
                 .fillMaxWidth()
-                .height(66.dp)
-                .padding(start = 16.dp, end = 10.dp),
-            verticalAlignment = Alignment.CenterVertically
+                .background(brush = Brush.horizontalGradient(listOf(Color(0xFFFF8C00), Color(0xFFFFA500))))
+                .padding(top = pv.calculateTopPadding())
         ) {
-            Text("消息中心", fontSize = 18.sp, color = Color.White, fontWeight = FontWeight.Medium)
-            Spacer(Modifier.weight(1f))
-            Icon(
-                painter = painterResource(R.drawable.read_all),
-                contentDescription = null,
+            Row(
                 modifier = Modifier
-                    .size(36.dp)
-                    .clip(RoundedCornerShape(6.dp))
-                    .clickable(onClick = {})
-                    .padding(8.dp),
-                tint = Color.White
-            )
-            Icon(
-                painter = painterResource(R.drawable.delete_all),
-                contentDescription = null,
-                modifier = Modifier
-                    .size(36.dp)
-                    .clip(RoundedCornerShape(6.dp))
-                    .clickable(onClick = {})
-                    .padding(9.dp),
-                tint = Color.White
-            )
+                    .fillMaxWidth()
+                    .height(66.dp)
+                    .padding(horizontal = 10.dp),
+                verticalAlignment = Alignment.CenterVertically
+            ) {
+                Row(
+                    Modifier
+                        .width(80.dp)
+                        .padding(start = 6.dp)
+                ) {
+                    Icon(
+                        painter = painterResource(R.drawable.back),
+                        contentDescription = "",
+                        modifier = Modifier
+                            .size(32.dp)
+                            .clip(RoundedCornerShape(50))
+                            .background(Color(0xFFFFA126))
+                            .clickable(onClick = { destroy() })
+                            .padding(8.dp),
+                        tint = Color.White
+                    )
+                }
+                Text(
+                    "消息中心",
+                    fontSize = 18.sp,
+                    color = Color.White,
+                    fontWeight = FontWeight.Medium,
+                    modifier = Modifier.weight(1f),
+                    textAlign = TextAlign.Center
+                )
+                Row(Modifier.width(80.dp)) {
+                    Icon(
+                        painter = painterResource(R.drawable.read_all),
+                        contentDescription = null,
+                        modifier = Modifier
+                            .size(36.dp)
+                            .clip(RoundedCornerShape(6.dp))
+                            .clickable(onClick = {})
+                            .padding(8.dp),
+                        tint = Color.White
+                    )
+                    Icon(
+                        painter = painterResource(R.drawable.delete_all),
+                        contentDescription = null,
+                        modifier = Modifier
+                            .size(36.dp)
+                            .clip(RoundedCornerShape(6.dp))
+                            .clickable(onClick = {})
+                            .padding(9.dp),
+                        tint = Color.White
+                    )
+                }
+            }
         }
     }
+
 }
 
 /**

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

@@ -0,0 +1,61 @@
+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.Task
+import com.iscs.bozzys.api.TaskFormInfo
+import com.iscs.bozzys.api.TaskInfo
+import com.iscs.bozzys.ui.base.VMBase
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+
+/**
+ * 任务详情页面vm
+ */
+class VMDetailTask : VMBase() {
+
+    // 本类使用
+    private val _state = MutableStateFlow(StateDetailTask())
+
+    // 对Compose暴露
+    val state = _state.asStateFlow()
+
+    /**
+     * 获取任务表单信息
+     */
+    fun getTaskFormInfo(task: Task) {
+        viewModelScope.launch {
+            loading.emit(StateLoading(show = true))
+            ApiRequest.getTaskInfoByNodeId(task.nodeId).onSuccess {
+                val taskInfo = it.data
+                if (taskInfo == null) {
+                    loading.emit(StateLoading(show = false))
+                    toast.emit("任务数据异常")
+                    return@launch
+                }
+                ApiRequest.getTaskFormInfoByFormId(it.data?.formId ?: 0).onSuccess { formInfo ->
+                    loading.emit(StateLoading(show = false))
+                    val formInfo = formInfo.data
+                    if (formInfo == null) {
+                        toast.emit("表单数据异常")
+                        return@launch
+                    }
+                    _state.value = _state.value.copy(formInfo = formInfo, taskInfo = taskInfo)
+                }.onFailure { err ->
+                    loading.emit(StateLoading(show = false))
+                    toast.emit("获取表单异常:${err.getResponse().msg}")
+                }
+            }.onFailure {
+                loading.emit(StateLoading(show = false))
+                toast.emit("获取任务详情异常:${it.getResponse().msg}")
+            }
+        }
+    }
+}
+
+/**
+ * 页面状态类
+ */
+data class StateDetailTask(val formInfo: TaskFormInfo = TaskFormInfo(), val taskInfo: TaskInfo = TaskInfo())

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

@@ -1,12 +1,13 @@
 package com.iscs.bozzys.ui.pages.vm
 
-import android.util.Log
 import androidx.compose.ui.graphics.Color
 import androidx.lifecycle.viewModelScope
 import com.iscs.bozzys.R
 import com.iscs.bozzys.api.ApiRequest
 import com.iscs.bozzys.api.ApiRequest.getResponse
 import com.iscs.bozzys.api.Job
+import com.iscs.bozzys.api.Message
+import com.iscs.bozzys.api.Task
 import com.iscs.bozzys.ui.base.VMBase
 import com.iscs.bozzys.ui.theme.Text
 import com.iscs.bozzys.utils.Storage
@@ -25,16 +26,23 @@ class VMHome : VMBase() {
             val navs = listOf(
                 NavBarItem(0, "首页", R.drawable.home),
                 NavBarItem(1, "作业", R.drawable.jobs),
-                NavBarItem(2, "消息", R.drawable.message),
+                NavBarItem(2, "任务", R.drawable.tasks),
                 NavBarItem(3, "设置", R.drawable.settings)
             )
             _state.value = _state.value.copy(navs = navs, username = Storage.readUserName())
+            // 延时等待页面配置数据初步加载完成
+            delay(500)
+            getHomeTaskData(showLoading = true)
+            // 获取作业列表
+            getJobList(_state.value.jobPage)
+            // 获取任务列表数据
+            getTaskList(_state.value.taskPage)
         }
-        getJobsData()
     }
 
-    fun updateNavIndex(navIndex: Int) {
-        _state.value = _state.value.copy(navIndex = navIndex)
+    fun navigationToId(id: Int) {
+        val idx = _state.value.navs.indexOfFirst { it.id == id }
+        _state.value = _state.value.copy(navigationId = idx)
     }
 
     fun updateUnReadCount() {
@@ -45,24 +53,67 @@ class VMHome : VMBase() {
 
     fun onRefreshHomeTab() {
         _state.value = _state.value.copy(isHomeTabRefresh = true)
+        // 下拉刷新,刷新数据
+        getHomeTaskData()
+    }
+
+    /**
+     * 获取作业数据列表
+     */
+    fun getHomeTaskData(showLoading: Boolean = false) {
         viewModelScope.launch {
-            delay(3000)
-            _state.value = _state.value.copy(isHomeTabRefresh = false)
+            loading.emit(StateLoading(show = showLoading))
+            // pending      待处理
+            // unaudited    未审核
+            // approved     已审核
+            // "approvalStatus" to "unaudited"
+            ApiRequest.getTasks(hashMapOf("pageNo" to 1, "pageSize" to 3)).onSuccess {
+                loading.emit(StateLoading(show = false))
+                val list = it.data?.list ?: listOf()
+                // 后面看需求是否需要限制
+                // val maxLimitList = if (list.isEmpty()) list else list.subList(0, min(list.size, 3))
+                _state.value = _state.value.copy(homeTasks = ArrayList(list), todoCountPending = it.data?.total ?: 0, isHomeTabRefresh = false)
+            }.onFailure {
+                _state.value = _state.value.copy(isHomeTabRefresh = false)
+                loading.emit(StateLoading(show = false))
+                toast.emit(it.getResponse().msg)
+            }
         }
     }
 
     /**
-     * 获取作业数据列表
+     * 获取任务列表数据
      */
-    fun getJobsData() {
+    fun getTaskList(page: StatePage) {
         viewModelScope.launch {
-            loadingEvent.emit(StateLoading(show = true))
-            ApiRequest.getJobs(hashMapOf("pageNo" to 1, "pageSize" to 10, "status" to "pending")).onSuccess {
-                loadingEvent.emit(StateLoading(show = false))
-                _state.value = _state.value.copy(homeJobs = ArrayList(it.data?.list ?: listOf()))
+            // 下拉即刷新
+            _state.value = _state.value.copy(taskPage = _state.value.taskPage.copy(isRefresh = true))
+            ApiRequest.getTasks(hashMapOf("pageNo" to page.page, "pageSize" to page.pageSize)).onSuccess {
+                if (page.page == 1) _state.value.taskList.clear()
+                val tasks = it.data?.list ?: emptyList()
+                _state.value.taskList.addAll(tasks)
+                // 存储页面数据
+                _state.value = _state.value.copy(taskPage = page)
             }.onFailure {
-                loadingEvent.emit(StateLoading(show = false))
-                toastEvent.emit(it.getResponse().msg)
+                _state.value = _state.value.copy(taskPage = _state.value.taskPage.copy(isRefresh = false))
+                toast.emit(it.getResponse().msg)
+            }
+        }
+    }
+
+    fun getJobList(page: StatePage) {
+        viewModelScope.launch {
+            // 下拉即刷新
+            _state.value = _state.value.copy(jobPage = _state.value.jobPage.copy(isRefresh = true))
+            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)
+                // 存储页面数据
+                _state.value = _state.value.copy(jobPage = page)
+            }.onFailure {
+                _state.value = _state.value.copy(jobPage = _state.value.jobPage.copy(isRefresh = false))
+                toast.emit(it.getResponse().msg)
             }
         }
     }
@@ -71,17 +122,33 @@ class VMHome : VMBase() {
 
 /**
  * @param navs              当前底部的导航列表
- * @param navIndex          当前选中的底部导航
+ * @param navigationId      当前选中的底部导航Id
  * @param username          用户名
  * @param isHomeTabRefresh  是否HomeTab页面刷新数据
- * @param homeJobs          首页进行中的任务
+ * @param homeTasks         首页进行中的任务
+ * @param messageList       消息列表
+ * @param taskList          任务列表
+ * @param taskPage          任务列表分页器
+ * @param jobList           作业列表
+ * @param jobPage           作业列表分页器
+ * @param todoCountPending  待处理
+ * @param todoCountRunning  进行中
+ * @param todoCountFinish   已完成
  */
 data class StateHome(
     val navs: List<NavBarItem> = arrayListOf(),
-    val navIndex: Int = 0,
+    val navigationId: Int = 0,
     val username: String = "",
     val isHomeTabRefresh: Boolean = false,
-    val homeJobs: List<Job> = listOf(),
+    val homeTasks: List<Task> = listOf(),
+    val messageList: List<Message> = listOf(),
+    val taskPage: StatePage = StatePage(),
+    val taskList: MutableList<Task> = mutableListOf(),
+    val jobPage: StatePage = StatePage(),
+    val jobList: MutableList<Job> = mutableListOf(),
+    val todoCountPending: Int = 0,
+    val todoCountRunning: Int = 0,
+    val todoCountFinish: Int = 0,
 )
 
 /**
@@ -101,4 +168,14 @@ data class NavBarItem(
     val count: Int = 0,
     val selectedColor: Color = Color(0xFF1E90FF),
     val unselectedColor: Color = Text
-)
+)
+
+/**
+ * 页面数据
+ *
+ * @param page      当前页面
+ * @param pageSize  页面大小
+ * @param type      请求数据类型
+ * @param isRefresh 是否正在刷新
+ */
+data class StatePage(val page: Int = 1, val pageSize: Int = 10, val type: String = "", val isRefresh: Boolean = false)

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

@@ -9,6 +9,7 @@ import com.iscs.bozzys.api.ApiRequest
 import com.iscs.bozzys.api.ApiRequest.getResponse
 import com.iscs.bozzys.ui.base.VMBase
 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
@@ -56,22 +57,23 @@ class VMLogin : VMBase() {
         viewModelScope.launch {
             val check = checkCanLogin()
             if (check != 0) {
-                toastEvent.emit(check)
+                toast.emit(check)
                 return@launch
             }
-            loadingEvent.emit(StateLoading(show = true))
+            loading.emit(StateLoading(show = true))
             ApiRequest.login(state.account, state.code).onSuccess {
                 it.data?.accessToken.saveToken()
+                it.data?.refreshToken.saveRefreshToken()
                 it.data?.nickname.saveUserName()
                 Storage.saveLogin(true)
-                loadingEvent.emit(StateLoading(show = false))
+                loading.emit(StateLoading(show = false))
                 delay(500)
-                toastEvent.emit("登录成功")
+                toast.emit("登录成功")
                 done.invoke()
             }.onFailure {
                 delay(500)
-                loadingEvent.emit(StateLoading(show = false))
-                toastEvent.emit(it.getResponse().msg)
+                loading.emit(StateLoading(show = false))
+                toast.emit(it.getResponse().msg)
             }
         }
     }

+ 14 - 0
app/src/main/java/com/iscs/bozzys/utils/DateUtil.kt

@@ -1,6 +1,7 @@
 package com.iscs.bozzys.utils
 
 import java.time.Instant
+import java.time.LocalDate
 import java.time.ZoneId
 import java.time.format.DateTimeFormatter
 
@@ -15,4 +16,17 @@ object DateUtil {
             .format(DateTimeFormatter.ofPattern(pattern))
     }
 
+    /**
+     * 将传入的时间String格式化为pattern格式的时间戳
+     */
+    fun String.dateToTimestamp(pattern: String): Long {
+        val date = this.ifEmpty { System.currentTimeMillis().format(pattern) }
+        val formatter = DateTimeFormatter.ofPattern(pattern)
+        val localDate = LocalDate.parse(date, formatter)
+        return localDate
+            .atStartOfDay(ZoneId.systemDefault())
+            .toInstant()
+            .toEpochMilli()
+    }
+
 }

+ 62 - 0
app/src/main/java/com/iscs/bozzys/utils/SerializerUtil.kt

@@ -0,0 +1,62 @@
+package com.iscs.bozzys.utils
+
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.InternalSerializationApi
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.descriptors.SerialKind
+import kotlinx.serialization.descriptors.buildSerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.JsonArray
+import kotlinx.serialization.json.JsonDecoder
+import kotlinx.serialization.json.JsonEncoder
+import kotlinx.serialization.json.JsonNull
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.contentOrNull
+import kotlinx.serialization.json.jsonPrimitive
+
+/**
+ * 解析多类型字段
+ */
+object PlaceholderSerializer : KSerializer<List<String>> {
+
+    @OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class)
+    override val descriptor: SerialDescriptor = buildSerialDescriptor("Placeholder", SerialKind.CONTEXTUAL)
+
+    override fun deserialize(decoder: Decoder): List<String> {
+        val jsonDecoder = decoder as? JsonDecoder
+            ?: error("This serializer can be used only with Json")
+
+        val element = jsonDecoder.decodeJsonElement()
+
+        return when (element) {
+            is JsonPrimitive -> {
+                if (element.isString) {
+                    listOf(element.content)
+                } else {
+                    emptyList()
+                }
+            }
+
+            is JsonArray -> {
+                element.mapNotNull {
+                    it.jsonPrimitive.contentOrNull
+                }
+            }
+
+            JsonNull -> emptyList()
+
+            else -> emptyList()
+        }
+    }
+
+    override fun serialize(encoder: Encoder, value: List<String>) {
+        val jsonEncoder = encoder as? JsonEncoder
+            ?: error("This serializer can be used only with Json")
+
+        jsonEncoder.encodeJsonElement(
+            JsonArray(value.map { JsonPrimitive(it) })
+        )
+    }
+}

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

@@ -46,6 +46,20 @@ object Storage {
         return mmkv.decodeString("user_token", "") ?: ""
     }
 
+    /**
+     * 保存刷新token
+     */
+    fun String?.saveRefreshToken() {
+        mmkv.encode("refresh_token", this ?: "")
+    }
+
+    /**
+     * 读取刷新token
+     */
+    fun readRefreshToken(): String {
+        return mmkv.decodeString("refresh_token", "") ?: ""
+    }
+
     /**
      * 保存用户名
      */

+ 12 - 0
app/src/main/res/drawable/tasks.xml

@@ -0,0 +1,12 @@
+<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="M719.4,22.2C710,8.5 692.5,0 673.6,0H379.6c-19.7,0 -36.6,8.9 -45.9,22.2 -4.9,6.9 -7.4,14.6 -7.5,22.5v44.1c0,24.7 23.9,44.8 53.4,44.8h294c29.4,0 53.3,-20.1 53.3,-44.8v-44.1a38.4,38.4 0,0 0,-7.5 -22.4"
+      android:fillColor="#4484FF"/>
+  <path
+      android:pathData="M849.8,44.5h-75.7v111.4c0,40.9 -34.6,74.3 -76.9,74.3L311.5,230.2c-42.3,0 -76.9,-33.4 -76.9,-74.3L234.6,44.5L158.9,44.5c-26.6,0 -52.1,10.5 -70.8,29A97.1,97.1 0,0 0,59.1 143.2v782.2A99,99 0,0 0,158.9 1024h690.8c26.6,0 52.2,-10.5 70.8,-29 18.7,-18.5 29.1,-43.6 29,-69.6L949.5,142.3c0.1,-54.1 -44.5,-97.8 -99.7,-97.8zM714.6,505.3l-211.5,223.3a39.4,39.4 0,0 1,-28.4 12.1h-0.2a39.5,39.5 0,0 1,-28.6 -12.4l-103.2,-111.7a36.3,36.3 0,0 1,3.2 -52.6,39.6 39.6,0 0,1 54.4,2.9l74.8,81 182.6,-192.9a39.7,39.7 0,0 1,53.7 -1.5c15.4,13.5 16.8,36.5 3.2,51.8z"
+      android:fillColor="#4484FF"/>
+</vector>

+ 74 - 0
app/src/main/res/layout/date_range_picker.xml

@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="wrap_content"
+    android:layout_height="180dp"
+    android:layout_gravity="center"
+    android:orientation="horizontal">
+
+    <com.loper7.date_time_picker.number_picker.NumberPicker
+        android:id="@+id/np_datetime_year"
+        android:layout_width="60dp"
+        android:layout_height="wrap_content"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:np_dividerColor="#E5E5E5"
+        app:np_dividerThickness="0.6dp"
+        app:np_height="184dp"
+        app:np_selectedTextSize="15sp"
+        app:np_textSize="15sp"
+        app:np_wheelItemCount="3" />
+
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="/"
+        android:textColor="@color/black"
+        android:translationY="-2dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toStartOf="@+id/np_datetime_month"
+        app:layout_constraintStart_toEndOf="@+id/np_datetime_year"
+        app:layout_constraintTop_toTopOf="parent" />
+
+
+    <com.loper7.date_time_picker.number_picker.NumberPicker
+        android:id="@+id/np_datetime_month"
+        android:layout_width="60dp"
+        android:layout_height="wrap_content"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintStart_toEndOf="@+id/np_datetime_year"
+        app:layout_constraintTop_toTopOf="parent"
+        app:np_dividerColor="#E5E5E5"
+        app:np_dividerThickness="0.6dp"
+        app:np_height="184dp"
+        app:np_selectedTextSize="15sp"
+        app:np_textSize="15sp"
+        app:np_wheelItemCount="3" />
+
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="/"
+        android:textColor="@color/black"
+        android:translationY="-2dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toStartOf="@+id/np_datetime_day"
+        app:layout_constraintStart_toEndOf="@+id/np_datetime_month"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <com.loper7.date_time_picker.number_picker.NumberPicker
+        android:id="@+id/np_datetime_day"
+        android:layout_width="60dp"
+        android:layout_height="wrap_content"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintStart_toEndOf="@+id/np_datetime_month"
+        app:layout_constraintTop_toTopOf="parent"
+        app:np_dividerColor="#E5E5E5"
+        app:np_dividerThickness="0.6dp"
+        app:np_height="184dp"
+        app:np_selectedTextSize="15sp"
+        app:np_textSize="15sp"
+        app:np_wheelItemCount="3" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>