Pārlūkot izejas kodu

1. 支持创建作业
2. 支持作业流程回显
3. 支持token刷新

bjb 4 mēneši atpakaļ
vecāks
revīzija
8c66e5c215

+ 2 - 2
.idea/deploymentTargetSelector.xml

@@ -4,10 +4,10 @@
     <selectionStates>
       <SelectionState runConfigName="app">
         <option name="selectionMode" value="DROPDOWN" />
-        <DropdownSelection timestamp="2025-12-26T04:01:18.101228300Z">
+        <DropdownSelection timestamp="2025-12-26T06:50:06.824288800Z">
           <Target type="DEFAULT_BOOT">
             <handle>
-              <DeviceId pluginId="PhysicalDevice" identifier="serial=d8d12db95670c08" />
+              <DeviceId pluginId="PhysicalDevice" identifier="serial=TPG5T17C05011309" />
             </handle>
           </Target>
         </DropdownSelection>

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

@@ -1,7 +1,13 @@
 package com.iscs.bozzys.api
 
+import androidx.compose.runtime.snapshots.SnapshotStateMap
+import androidx.compose.ui.geometry.Offset
+import com.iscs.bozzys.ui.pages.edit.step.compose.Anchor
+import com.iscs.bozzys.ui.pages.edit.step.compose.Connection
+import com.iscs.bozzys.ui.pages.edit.step.compose.NodeUI
 import com.iscs.bozzys.utils.PlaceholderSerializer
 import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
 
 
 /**
@@ -32,10 +38,32 @@ class LoginRsp(val userId: Int, val username: String, val nickname: String, val
  */
 class PageRsp<T>(val total: Int, val list: List<T>)
 
+/**
+ * 流程模板
+ */
+@Serializable
+data class Sop(
+    val id: Int,
+    val name: String,
+    val content: String,
+    val nodeCount: Int,
+    val status: Int,
+    val description: String,
+    val createTime: Long,
+    val updateTime: Long
+)
+
+/**
+ * 字典结构
+ */
+@Serializable
+data class Dict(val id: Int, val sort: Int, val label: String, val value: String, val status: Int)
+
 /**
  * 任务信息
  */
-class TaskInfo(
+@Serializable
+class Node(
     val id: Int = 0,
     val workId: Int = 0,
     val uuid: String = "",
@@ -78,21 +106,68 @@ data class Job(
     val orderNo: String,                // 作业编号
     val name: String,                   // 作业名称
     val type: String,                   // 作业类型
-    val urgencyLevel: String,           // 安全等级
-    val designId: Int,
-    val designContent: String,
-    val description: String,
-    val initiatorId: Int,
-    val initiatorName: String,
-    val initiatorTime: Long,
+    val urgencyLevel: String,           // 紧急等级
+    val designId: Int,                  // sopId
+    val designContent: String,          // sopInfo
+    val description: String,            // 描述
+    val initiatorId: Int,               // 创建者id
+    val initiatorName: String,          // 创建者名称
+    val initiatorTime: Long,            // 创建者创建时间
     val status: String,                 // 作业状态
     val createTime: Long,               // 创建时间
     val currentNodeId: String? = null,
     val currentNodeName: String? = null,
     val completionTime: Long? = null,
     val cancellationTime: Long? = null,
-    val cancellationReason: String? = null
-)
+    val cancellationReason: String? = null,
+    val workflowWorkNodeDOList: List<Node> = listOf()
+) {
+    /**
+     * 将接口返回的node转换为可显示的Node
+     */
+    fun toUINodeMap(): SnapshotStateMap<String, NodeUI> {
+        val json = Json { ignoreUnknownKeys = true }
+        val map = SnapshotStateMap<String, NodeUI>()
+        workflowWorkNodeDOList.forEach {
+            val pos = json.decodeFromString<NodePositon>(it.position)
+            map[it.id.toString()] = NodeUI(it.id.toString(), Offset(pos.x, pos.y), node = it)
+        }
+        return map
+    }
+
+    fun String.toAnchor(): Anchor {
+        return when (this) {
+            "left-source" -> Anchor.LEFT
+            "right-source" -> Anchor.RIGHT
+            "top-source" -> Anchor.TOP
+            "bottom-source" -> Anchor.BOTTOM
+            "left-target" -> Anchor.LEFT
+            "right-target" -> Anchor.RIGHT
+            "top-target" -> Anchor.TOP
+            "bottom-target" -> Anchor.BOTTOM
+            else -> if (this.contains("source")) Anchor.LEFT else Anchor.RIGHT
+        }
+    }
+
+    /**
+     * 将api连线数据转化为UI层连线数据
+     */
+    fun toUIConnectList(): List<Connection> {
+        val connections = ArrayList<Connection>()
+        val json = Json { ignoreUnknownKeys = true }
+        val nodeInfo: NodeInfo = json.decodeFromString(designContent.ifEmpty { "{}" })
+        nodeInfo.edges.forEach {
+            val fromUuid = it.target
+            val toUuid = it.source
+            val fromId = workflowWorkNodeDOList.find { node -> node.uuid == fromUuid }?.id ?: 0
+            val fromAnchor = it.targetHandle.toAnchor()
+            val toId = workflowWorkNodeDOList.find { node -> node.uuid == toUuid }?.id ?: 0
+            val toAnchor = it.sourceHandle.toAnchor()
+            connections.add(Connection(fromId.toString(), toId.toString(), fromAnchor, toAnchor))
+        }
+        return connections
+    }
+}
 
 /**
  * Task任务
@@ -135,7 +210,7 @@ data class Message(val id: Int = 0)
  * @param children
  */
 @Serializable
-class FormField(
+data class FormField(
     val id: String = "",
     val type: String = "",
     val label: String = "",
@@ -144,9 +219,10 @@ class FormField(
     @Serializable(with = PlaceholderSerializer::class)
     val placeholder: List<String> = listOf(""),
     var value: List<String> = listOf(""),
-    val options: List<FormOption> = listOf(),
+    var options: List<FormOption> = listOf(),
     val cardTitle: String = "",
     val gridColumns: Int = 0,
+    val enabled: Boolean = true,
     val children: List<FormField> = listOf()
 )
 
@@ -157,4 +233,29 @@ class FormField(
  * @param value 数值
  */
 @Serializable
-data class FormOption(val label: String = "", val value: String = "")
+data class FormOption(val label: String = "", val value: String = "")
+
+/**
+ * 节点位置信息
+ */
+@Serializable
+data class NodePositon(val x: Float, val y: Float)
+
+/**
+ * 节点信息
+ */
+@Serializable
+data class NodeInfo(val edgeCount: Int, val edges: List<NodeConnection>)
+
+/**
+ * 节点连接线信息
+ */
+@Serializable
+data class NodeConnection(
+    val id: String,
+    val source: String,
+    val target: String,
+    val sourceHandle: String,
+    val targetHandle: String,
+    val type: String
+)

+ 44 - 8
app/src/main/java/com/iscs/bozzys/api/ApiRequest.kt

@@ -21,7 +21,7 @@ object ApiRequest {
      * @param headers 用于自定义请求头参数
      * @return 默认请求头参数,如果用户携带请求头参数与默认一致,优先使用用户携带
      */
-    private fun getUserHeaders(headers: Map<String, String> = emptyMap()): Map<String, String> {
+    fun getUserHeaders(headers: Map<String, String> = emptyMap()): Map<String, String> {
         val map = HashMap<String, String>()
         map["tenant-id"] = "1"
         map["Authorization"] = "Bearer ${Storage.readToken()}"
@@ -107,7 +107,7 @@ object ApiRequest {
         val params = JsonObject()
         params.addProperty("username", username)
         params.addProperty("password", pwd)
-        return requestApi { api.login(getUserHeaders(), params) }
+        return requestApi { api.login(params) }
     }
 
     /**
@@ -116,7 +116,7 @@ object ApiRequest {
      * @param refreshToken
      */
     suspend fun refreshToken(refreshToken: String): Result<Response<LoginRsp>> {
-        return requestApi { api.refreshToken(getUserHeaders(), refreshToken) }
+        return requestApi { api.refreshToken(refreshToken) }
     }
 
     /**
@@ -125,7 +125,7 @@ object ApiRequest {
      * @param params
      */
     suspend fun getJobs(params: MutableMap<String, Any>): Result<Response<PageRsp<Job>>> {
-        return requestApi { api.getJobs(getUserHeaders(), params) }
+        return requestApi { api.getJobs(params) }
     }
 
     /**
@@ -134,7 +134,7 @@ object ApiRequest {
      * @param params
      */
     suspend fun getTasks(params: MutableMap<String, Any>): Result<Response<PageRsp<Task>>> {
-        return requestApi { api.getTasks(getUserHeaders(), params) }
+        return requestApi { api.getTasks(params) }
     }
 
     /**
@@ -142,8 +142,8 @@ object ApiRequest {
      *
      * @param nodeId    节点Id
      */
-    suspend fun getTaskInfoByNodeId(nodeId: Int): Result<Response<TaskInfo>> {
-        return requestApi { api.getTaskInfo(getUserHeaders(), mutableMapOf("nodeId" to nodeId)) }
+    suspend fun getTaskInfoByNodeId(nodeId: Int): Result<Response<Node>> {
+        return requestApi { api.getTaskInfo(mutableMapOf("nodeId" to nodeId)) }
     }
 
     /**
@@ -152,7 +152,43 @@ object ApiRequest {
      * @param formId    表单id
      */
     suspend fun getTaskFormInfoByFormId(formId: Int): Result<Response<TaskFormInfo>> {
-        return requestApi { api.getTaskFormInfo(getUserHeaders(), mutableMapOf("id" to formId)) }
+        return requestApi { api.getTaskFormInfo(mutableMapOf("id" to formId)) }
+    }
+
+    /**
+     * 获取字典数据
+     *
+     * @param params 请求数据
+     */
+    suspend fun getDictData(params: MutableMap<String, Any>): Result<Response<PageRsp<Dict>>> {
+        return requestApi { api.getDictData(params) }
+    }
+
+    /**
+     * 获取sop流程模板列表
+     *
+     * @param params
+     */
+    suspend fun getSopList(params: MutableMap<String, Any>): Result<Response<PageRsp<Sop>>> {
+        return requestApi { api.getSopList(params) }
+    }
+
+    /**
+     * 创建作业
+     *
+     * @param params 请求参数
+     */
+    suspend fun createJob(params: MutableMap<String, Any>): Result<Response<Int>> {
+        return requestApi { api.createJob(params) }
+    }
+
+    /**
+     * 查询作业详情
+     *
+     * @param id    作业详情
+     */
+    suspend fun getJobById(id: Int): Result<Response<Job>> {
+        return requestApi { api.getJobById(id) }
     }
 
 }

+ 64 - 6
app/src/main/java/com/iscs/bozzys/api/ApiService.kt

@@ -19,41 +19,99 @@ interface ApiService {
      */
     @Headers("Content-Type: application/json")
     @POST("/admin-api/system/auth/login")
-    suspend fun login(@HeaderMap headers: Map<String, String>, @Body body: JsonObject): Response<LoginRsp>
+    suspend fun login(
+        @Body body: JsonObject,
+        @HeaderMap headers: Map<String, String> = ApiRequest.getUserHeaders()
+    ): 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>
+    suspend fun refreshToken(
+        @Query("refreshToken") refreshToken: String,
+        @HeaderMap headers: Map<String, String> = ApiRequest.getUserHeaders()
+    ): 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<PageRsp<Job>>
+    suspend fun getJobs(
+        @QueryMap params: MutableMap<String, Any>,
+        @HeaderMap headers: Map<String, String> = ApiRequest.getUserHeaders()
+    ): 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>>
+    suspend fun getTasks(
+        @QueryMap params: MutableMap<String, Any>,
+        @HeaderMap headers: Map<String, String> = ApiRequest.getUserHeaders()
+    ): 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>
+    suspend fun getTaskInfo(
+        @QueryMap params: MutableMap<String, Any>,
+        @HeaderMap headers: Map<String, String> = ApiRequest.getUserHeaders()
+    ): Response<Node>
 
     /**
      * 获取当前任务的表单信息
      */
     @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>
+    suspend fun getTaskFormInfo(
+        @QueryMap params: MutableMap<String, Any>,
+        @HeaderMap headers: Map<String, String> = ApiRequest.getUserHeaders()
+    ): Response<TaskFormInfo>
+
+    /**
+     * 获取字典数据
+     */
+    @Headers("Content-Type: application/x-www-form-urlencoded")
+    @GET("/admin-api/system/dict-data/page")
+    suspend fun getDictData(
+        @QueryMap params: MutableMap<String, Any>,
+        @HeaderMap headers: Map<String, String> = ApiRequest.getUserHeaders()
+    ): Response<PageRsp<Dict>>
+
+    /**
+     * 获取SOP模板列表
+     */
+    @Headers("Content-Type: application/x-www-form-urlencoded")
+    @GET("/admin-api/iscs/workflow-design/getWorkflowDesignPage")
+    suspend fun getSopList(
+        @QueryMap params: MutableMap<String, Any>,
+        @HeaderMap headers: Map<String, String> = ApiRequest.getUserHeaders()
+    ): Response<PageRsp<Sop>>
+
+    /**
+     * 创建作业
+     */
+    @Headers("Content-Type: application/json")
+    @POST("/admin-api/iscs/workflow-work/insertWorkflowWork")
+    suspend fun createJob(
+        @Body body: MutableMap<String, Any>,
+        @HeaderMap headers: Map<String, String> = ApiRequest.getUserHeaders()
+    ): Response<Int>
+
+    /**
+     * 查询作业详情
+     */
+    @Headers("Content-Type: application/x-www-form-urlencoded")
+    @GET("/admin-api/iscs/workflow-work/selectWorkflowWorkById")
+    suspend fun getJobById(
+        @Query("id") id: Int,
+        @HeaderMap headers: Map<String, String> = ApiRequest.getUserHeaders()
+    ): Response<Job>
 
 }

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

@@ -193,11 +193,11 @@ abstract class PageBase(
     }
 
     /**
-     * 显示toast,Flow方式
+     * 显示loading,Flow方式
      */
-    fun MutableSharedFlow<StateLoading>.loading() {
+    fun MutableSharedFlow<StateLoading>.showLoading() {
         lifecycleScope.async {
-            this@loading.collect { stateLoading ->
+            this@showLoading.collect { stateLoading ->
                 if (stateLoading.show) {
                     loading?.show(stateLoading)
                 } else {

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

@@ -28,6 +28,7 @@ import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
@@ -57,6 +58,70 @@ import com.loper7.date_time_picker.DateTimeConfig
 import com.loper7.date_time_picker.dialog.CardDatePickerDialog
 import kotlinx.serialization.json.Json
 
+
+@Composable
+fun FormContainer(forms: List<FormField>, modifier: Modifier = Modifier) {
+    Column(modifier = modifier) {
+        forms.forEach { form ->
+            key(form.id) {
+                when (form.type) {
+                    // 单行文本输入
+                    "input" -> FormInput(
+                        form.label,
+                        form.value,
+                        { form.value = it },
+                        form.placeholder,
+                        required = form.required,
+                        enable = form.enabled
+                    )
+                    // 多行文本输入
+                    "textarea" -> FormTextarea(
+                        form.label,
+                        form.value,
+                        { form.value = it },
+                        form.placeholder,
+                        required = form.required,
+                        enable = form.enabled
+                    )
+                    // 选择器
+                    "select" -> FormSelect(
+                        form.label,
+                        form.value,
+                        form.options,
+                        { form.value = it },
+                        required = form.required,
+                        enable = form.enabled
+                    )
+                    // 日期选择
+                    "date" -> FormDateSelect(
+                        form.label,
+                        form.value,
+                        { form.value = it },
+                        required = form.required, enable = form.enabled
+                    )
+                    // 起止日期选择
+                    "daterange" -> FormDateRangeSelect(
+                        form.label,
+                        form.value,
+                        { form.value = it },
+                        placeholder = form.placeholder,
+                        enable = form.enabled
+                    )
+                    // 单选
+                    "radio" -> FormRadio(
+                        form.label,
+                        form.value,
+                        form.options,
+                        { form.value = it },
+                        required = form.required,
+                        enable = form.enabled
+                    )
+                }
+            }
+        }
+    }
+}
+
 /**
  * 表单输入框
  *
@@ -110,7 +175,7 @@ fun FormInput(
                 Box(contentAlignment = Alignment.CenterStart) {
                     innerTextField()
                     if (text.isEmpty()) {
-                        val ph = placeholder.getOrNull(0) ?: "请输入$label"
+                        val ph = (placeholder.getOrNull(0) ?: "").ifEmpty { "请输入$label" }
                         Text(ph, color = Color(0xFF9CA3AF), fontSize = 14.sp, lineHeight = 18.sp)
                     }
                 }
@@ -175,8 +240,8 @@ fun FormTextarea(
             decorationBox = { innerTextField ->
                 Box(contentAlignment = Alignment.TopStart) {
                     innerTextField()
-                    if (value.isEmpty()) {
-                        val ph = placeholder.getOrNull(0) ?: "请输入$label"
+                    if (text.isEmpty()) {
+                        val ph = (placeholder.getOrNull(0) ?: "").ifEmpty { "请输入$label" }
                         Text(ph, color = Color(0xFF9CA3AF), fontSize = 14.sp, lineHeight = 18.sp)
                     }
                 }

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

@@ -6,20 +6,22 @@ import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
 import androidx.compose.material3.Button
 import androidx.compose.material3.Icon
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.mutableStateListOf
-import androidx.compose.runtime.remember
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
@@ -28,15 +30,14 @@ 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.FormOption
 import com.iscs.bozzys.ui.base.PageBase
 import com.iscs.bozzys.ui.base.Title
-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.CardContainer
+import com.iscs.bozzys.ui.pages.compose.FormContainer
 import com.iscs.bozzys.ui.pages.edit.step.openPageEditStep
+import com.iscs.bozzys.ui.pages.vm.VMCreateJob
 
 fun Context.openPageCreateJob() {
     startActivity(Intent(this, PageCreateJob::class.java))
@@ -46,47 +47,33 @@ class PageCreateJob : PageBase() {
 
     @Composable
     override fun GetViews(pv: PaddingValues) {
-        // 作业名称
-        val jobName = remember { mutableStateListOf("") }
-        // 作业分类
-        val typeOptions =
-            mutableStateListOf(FormOption("请选择分类", ""), FormOption("维修", "repair"), FormOption("维保", "check"), FormOption("其他", "other"))
-        val typeSelect = remember { mutableStateListOf(typeOptions[0].value) }
-        // 作业内容
-        val jobContent = remember { mutableStateListOf("") }
-        // 作业紧急程度
-        val levelOptions = mutableStateListOf(FormOption("普通", "Normal"), FormOption("紧急", "Middle"), FormOption("非常紧急", "High"))
-        val jobLevel = remember { mutableStateListOf(levelOptions[0].value) }
+        val vm: VMCreateJob = viewModel()
+        val state by vm.state.collectAsState()
+        LaunchedEffect(Unit) {
+            vm.toast.showToast()
+            vm.loading.showLoading()
+            vm.init()
+        }
         Column(
             Modifier
                 .fillMaxSize()
                 .background(Color(0xFFF8F9FA))
         ) {
-            Title(pv, "新建作业-电气隔离")
-            // 表单内容
-            Column(Modifier.padding(horizontal = 16.dp, vertical = 5.dp)) {
-                // 作业名称
-                FormInput("作业名称", jobName, {
-                    jobName.clear()
-                    jobName.addAll(it)
-                }, required = true)
-                // 作业分类
-                FormSelect("作业分类", typeSelect, typeOptions, {
-                    typeSelect.clear()
-                    typeSelect.addAll(it)
-                })
-                // 作业内容
-                FormTextarea("作业内容", jobContent, {
-                    jobContent.clear()
-                    jobContent.addAll(it)
-                })
-                // 作业紧急程度
-                FormRadio("紧急程度", jobLevel, levelOptions, {
-                    jobLevel.clear()
-                    jobLevel.addAll(it)
-                })
+            Title(pv, "新建作业")
+            Column(
+                modifier = Modifier
+                    .weight(1f)
+                    .verticalScroll(state = rememberScrollState())
+            ) {
+                // 表单内容
+                if (state.forms.isNotEmpty()) CardContainer(Modifier.padding(horizontal = 16.dp, vertical = 16.dp)) {
+                    FormContainer(
+                        state.forms, modifier = Modifier
+                            .padding(horizontal = 10.dp)
+                            .padding(top = 5.dp, bottom = 16.dp)
+                    )
+                }
             }
-            Spacer(Modifier.weight(1f))
             // 底部下一步按钮
             Column(
                 Modifier
@@ -95,7 +82,7 @@ class PageCreateJob : PageBase() {
                     .padding(top = 15.dp, bottom = (15f + pv.calculateBottomPadding().value).dp)
             ) {
                 Button(
-                    onClick = { openPageEditStep() }, modifier = Modifier
+                    onClick = { vm.onNext { openPageEditStep(it) } }, modifier = Modifier
                         .padding(horizontal = 16.dp)
                         .fillMaxWidth()
                         .height(50.dp)

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

@@ -91,7 +91,7 @@ class PageDetailTask : PageBase() {
         val vm: VMDetailTask = viewModel()
         LaunchedEffect(Unit) {
             vm.toast.showToast()
-            vm.loading.loading()
+            vm.loading.showLoading()
             vm.getTaskFormInfo(task)
         }
         val state by vm.state.collectAsState()
@@ -105,7 +105,7 @@ class PageDetailTask : PageBase() {
             ) {
                 TaskInfo(task)
                 // 处理动态内容的条件
-                when (state.taskInfo.type) {
+                when (state.node.type) {
                     "review",                       // 审核
                     "inputInfo",                    // 录入信息
                     "complete" -> TaskForm(vm)      // 完成操作,录入表单信息
@@ -277,7 +277,7 @@ class PageDetailTask : PageBase() {
                             { form.value = it },
                             form.placeholder,
                             required = form.required,
-                            enable = state.taskInfo.approvalStatus != "approved"
+                            enable = state.node.approvalStatus != "approved"
                         )
                         // 多行文本输入
                         "textarea" -> FormTextarea(
@@ -286,21 +286,21 @@ class PageDetailTask : PageBase() {
                             { form.value = it },
                             form.placeholder,
                             required = form.required,
-                            enable = state.taskInfo.approvalStatus != "approved"
+                            enable = state.node.approvalStatus != "approved"
                         )
                         // 选择器
                         "select" -> FormSelect(
                             form.label,
                             form.value,
                             form.options,
-                            { form.value = it }, required = form.required, enable = state.taskInfo.approvalStatus != "approved"
+                            { form.value = it }, required = form.required, enable = state.node.approvalStatus != "approved"
                         )
                         // 日期选择
                         "date" -> FormDateSelect(
                             form.label,
                             form.value,
                             { form.value = it },
-                            required = form.required, enable = state.taskInfo.approvalStatus != "approved"
+                            required = form.required, enable = state.node.approvalStatus != "approved"
                         )
                         // 起止日期选择
                         "daterange" -> FormDateRangeSelect(
@@ -308,14 +308,14 @@ class PageDetailTask : PageBase() {
                             form.value,
                             { form.value = it },
                             placeholder = form.placeholder,
-                            enable = state.taskInfo.approvalStatus != "approved"
+                            enable = state.node.approvalStatus != "approved"
                         )
                         // 单选
                         "radio" -> FormRadio(
                             form.label,
                             form.value,
                             form.options,
-                            { form.value = it }, required = form.required, enable = state.taskInfo.approvalStatus != "approved"
+                            { form.value = it }, required = form.required, enable = state.node.approvalStatus != "approved"
                         )
                     }
                 }
@@ -370,7 +370,7 @@ class PageDetailTask : PageBase() {
                     .padding(horizontal = 16.dp)
                     .fillMaxWidth()
             ) {
-                if (listOf("complete", "inputInfo").contains(state.taskInfo.type)) {
+                if (listOf("complete", "inputInfo").contains(state.node.type)) {
                     Button(
                         { destroy() }, modifier = Modifier
                             .padding(horizontal = 8.dp)
@@ -412,10 +412,10 @@ class PageDetailTask : PageBase() {
                             Text("提交", fontSize = 14.sp, lineHeight = 14.sp, color = Color.White)
                         }
                     }
-                } else if (listOf("review").contains(state.taskInfo.type)) {
+                } else if (listOf("review").contains(state.node.type)) {
                     Button(
                         { destroy() },
-                        enabled = state.taskInfo.approvalStatus != "approved", // 审核通过禁用
+                        enabled = state.node.approvalStatus != "approved", // 审核通过禁用
                         modifier = Modifier
                             .padding(horizontal = 8.dp)
                             .weight(1f)
@@ -431,13 +431,13 @@ class PageDetailTask : PageBase() {
                                 modifier = Modifier
                                     .padding(end = 5.dp)
                                     .size(16.dp),
-                                tint = if (state.taskInfo.approvalStatus == "approved") Color.White else Color(0xFFFF4D4F)
+                                tint = if (state.node.approvalStatus == "approved") Color.White else Color(0xFFFF4D4F)
                             )
                             Text(
                                 "审核不通过",
                                 fontSize = 14.sp,
                                 lineHeight = 14.sp,
-                                color = if (state.taskInfo.approvalStatus == "approved") Color.White else Color(
+                                color = if (state.node.approvalStatus == "approved") Color.White else Color(
                                     0xFFFF4D4F
                                 )
                             )
@@ -445,7 +445,7 @@ class PageDetailTask : PageBase() {
                     }
                     Button(
                         {},
-                        enabled = state.taskInfo.approvalStatus != "approved", // 审核通过禁用
+                        enabled = state.node.approvalStatus != "approved", // 审核通过禁用
                         modifier = Modifier
                             .padding(horizontal = 8.dp)
                             .weight(1f)

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

@@ -25,7 +25,6 @@ import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material3.Button
 import androidx.compose.material3.Icon
@@ -34,17 +33,13 @@ import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateListOf
-import androidx.compose.runtime.mutableStateMapOf
-import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
-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.geometry.Offset
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.platform.LocalContext
@@ -58,23 +53,15 @@ import androidx.compose.ui.window.Dialog
 import androidx.compose.ui.window.DialogProperties
 import androidx.compose.ui.zIndex
 import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.viewmodel.compose.viewModel
 import com.iscs.bozzys.R
-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.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
+import com.iscs.bozzys.ui.pages.compose.FormContainer
 import com.iscs.bozzys.ui.pages.edit.step.compose.NodeItem
 import com.iscs.bozzys.ui.pages.edit.step.compose.ZoomPanContainer
+import com.iscs.bozzys.ui.pages.vm.VMEditStep
 import com.iscs.bozzys.ui.theme.Text
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
@@ -82,140 +69,124 @@ import kotlinx.coroutines.launch
 /**
  * 跳转到编辑流程页面
  */
-fun Context.openPageEditStep() {
-    startActivity(Intent(this, PageEditStep::class.java))
+fun Context.openPageEditStep(jobId: Int) {
+    // 传递作业票id进入页面
+    startActivity(Intent(this, PageEditStep::class.java).apply {
+        putExtra("job_id", jobId)
+    })
 }
 
 class PageEditStep : PageBase() {
     @Composable
     override fun GetViews(pv: PaddingValues) {
-        val nodes = remember { mutableStateMapOf<String, Node>() }
-        val selectNode = remember { mutableStateOf(Node("-1", Offset(0f, 0f))) }
-        val lines = listOf(
-            Connection("A", "B", fromAnchor = Anchor.BOTTOM, toAnchor = Anchor.TOP),
-            Connection("A", "D", fromAnchor = Anchor.BOTTOM, toAnchor = Anchor.TOP),
-            Connection("B", "C", fromAnchor = Anchor.BOTTOM, toAnchor = Anchor.TOP)
-        )
-        var showFormDialog by remember { mutableStateOf(false) }
-        // 处理显示的表单
-        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.getFormListByJsonList())
+        val vm: VMEditStep = viewModel()
+        val state by vm.state.collectAsState()
         LaunchedEffect(Unit) {
-            nodes["A"] = Node("A", Offset(125f, -100f))
-            nodes["B"] = Node("B", Offset(50f, 0f))
-            nodes["D"] = Node("D", Offset(200f, 0f))
-            nodes["C"] = Node("C", Offset(50f, 100f))
-            // 默认选中第一个节点
-            nodes["A"]?.let { selectNode.value = it }
+            vm.toast.showToast()
+            vm.loading.showLoading()
+            vm.init(intent.getIntExtra("job_id", 0))
         }
         Column(Modifier.fillMaxSize()) {
             Title(pv, "作业流程管理")
-            ZoomPanContainer(nodes, lines, modifier = Modifier.fillMaxSize()) { scale, toTopCenter, toCenter, toLast ->
-                // 控件显示
-                Box(
-                    modifier = Modifier
-                        .fillMaxSize()
-                        .zIndex(3f),
-                    contentAlignment = Alignment.CenterStart
-                ) {
-                    nodes.forEach {
-                        NodeItem(it.key, nodes, selectNode, { node ->
-                            selectNode.value = node
-                            // 将当前Node平移到屏幕中间
-                            toTopCenter(node)
-                            lifecycleScope.launch {
-                                // 给移动位置增加点动画效果
-                                delay(280)
-                                // 弹出Dialog
-                                showFormDialog = true
-                            }
-                        })
+            Box(Modifier.weight(1f)) {
+                ZoomPanContainer(state.nodes, state.connections, modifier = Modifier.fillMaxSize()) { scale, toTopCenter, toCenter, toLast ->
+                    // 控件显示
+                    Box(
+                        modifier = Modifier
+                            .fillMaxSize()
+                            .zIndex(3f),
+                        contentAlignment = Alignment.CenterStart
+                    ) {
+                        state.nodes.forEach {
+                            NodeItem(it.key, state.nodes, state.node, { node ->
+                                vm.updateNode(node)
+                                // 将当前Node平移到屏幕中间
+                                toTopCenter(node)
+                                lifecycleScope.launch {
+                                    // 给移动位置增加点动画效果
+                                    delay(280)
+                                    // 弹出Dialog
+                                    vm.formDialogShow(true)
+                                }
+                            })
+                        }
                     }
-                }
-                // 底部弹出式的Dialog
-                FormDialog(showFormDialog, {
-                    // 回到移动中心位置前的位置,默认不添加 等反馈
-                    toLast()
-                    showFormDialog = false
-                }, pv) {
-
-                    // 监听键盘弹出事件
+                    // 底部弹出式的Dialog
+                    FormDialog(state.showFormDialog, {
+                        // 回到移动中心位置前的位置,默认不添加 等反馈
+                        toLast()
+                        vm.formDialogShow(false)
+                    }, pv) {
 
-                    val density = LocalDensity.current
-                    val imeInsets = WindowInsets.ime
-                    val imeBottom = imeInsets.getBottom(density)
+                        // 监听键盘弹出事件
+                        val density = LocalDensity.current
+                        val imeInsets = WindowInsets.ime
+                        val imeBottom = imeInsets.getBottom(density)
 
-                    Column(
-                        Modifier
-                            .fillMaxHeight(4 / 7f)
-                            .padding(horizontal = 16.dp)
-                    ) {
-                        Box(
+                        Column(
                             Modifier
-                                .fillMaxSize()
-                                .weight(1f)
+                                .fillMaxHeight(4 / 7f)
+                                .padding(horizontal = 16.dp)
                         ) {
-                            LazyColumn(
+                            Box(
                                 Modifier
-                                    .imePadding()
-                                    .padding(vertical = 5.dp)
+                                    .fillMaxSize()
+                                    .weight(1f)
                             ) {
-                                items(fields) {
-                                    when (it.type) {
-                                        "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,
-                                            it.options,
-                                            { sel -> it.value = sel }, required = it.required
-                                        )
-
-                                        "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,
-                                            it.options,
-                                            { sel -> it.value = sel }, required = it.required
-                                        )
-                                    }
+                                LazyColumn(
+                                    Modifier
+                                        .imePadding()
+                                        .padding(vertical = 5.dp)
+                                ) {
+                                    item { FormContainer(state.nodeForms) }
                                 }
                             }
-                        }
-                        if (imeBottom <= 0) {
-                            Spacer(Modifier.height(10.dp))
-                            Button(
-                                {},
-                                modifier = Modifier
-                                    .fillMaxWidth()
-                                    .height(50.dp)
-                                    .clip(RoundedCornerShape(12.dp))
-                                    .background(MaterialTheme.colorScheme.primary),
-                                shape = RoundedCornerShape(12.dp)
-                            ) {
-                                Text("保存", fontSize = 16.sp, fontWeight = FontWeight.Bold)
+                            if (imeBottom <= 0) {
+                                Spacer(Modifier.height(10.dp))
+                                Button(
+                                    {},
+                                    modifier = Modifier
+                                        .fillMaxWidth()
+                                        .height(50.dp)
+                                        .clip(RoundedCornerShape(12.dp))
+                                        .background(MaterialTheme.colorScheme.primary),
+                                    shape = RoundedCornerShape(12.dp)
+                                ) {
+                                    Text("保存", fontSize = 16.sp, fontWeight = FontWeight.Bold)
+                                }
                             }
                         }
                     }
                 }
             }
+            // 底部下一步按钮
+            Column(
+                Modifier
+                    .fillMaxWidth()
+                    .background(MaterialTheme.colorScheme.background)
+                    .padding(top = 15.dp, bottom = (15f + pv.calculateBottomPadding().value).dp)
+            ) {
+                Button(
+                    onClick = { }, modifier = Modifier
+                        .padding(horizontal = 16.dp)
+                        .fillMaxWidth()
+                        .height(50.dp)
+                        .clip(RoundedCornerShape(12.dp))
+                        .background(MaterialTheme.colorScheme.primary),
+                    shape = RoundedCornerShape(12.dp)
+                ) {
+                    Row(verticalAlignment = Alignment.CenterVertically) {
+                        Text("下一步", fontSize = 16.sp, fontWeight = FontWeight.Bold)
+                        Icon(
+                            painter = painterResource(R.drawable.next),
+                            contentDescription = "",
+                            modifier = Modifier
+                                .padding(start = 5.dp)
+                                .size(16.dp)
+                        )
+                    }
+                }
+            }
         }
     }
 

+ 27 - 18
app/src/main/java/com/iscs/bozzys/ui/pages/edit/step/compose/Node.kt → app/src/main/java/com/iscs/bozzys/ui/pages/edit/step/compose/NodeUI.kt

@@ -35,6 +35,7 @@ import androidx.compose.ui.unit.sp
 import androidx.compose.ui.unit.toSize
 import androidx.compose.ui.zIndex
 import com.iscs.bozzys.R
+import com.iscs.bozzys.api.Node
 import com.iscs.bozzys.ui.pages.compose.CardContainer
 import com.iscs.bozzys.ui.theme.Main
 import com.iscs.bozzys.ui.theme.Text
@@ -47,7 +48,7 @@ import com.iscs.bozzys.ui.theme.Text
  * @param scale         缩放比例
  */
 @Composable
-fun ConnectionLayer(nodes: Map<String, Node>, connections: List<Connection>, scale: Float) {
+fun ConnectionLayer(nodes: Map<String, NodeUI>, connections: List<Connection>, scale: Float) {
     val modifier = Modifier
         .fillMaxSize()
         .zIndex(2f)
@@ -60,7 +61,7 @@ fun ConnectionLayer(nodes: Map<String, Node>, connections: List<Connection>, sca
 
             if (from != null && to != null) {
                 // 处理需要绘制的所有点位
-                val pathPoints = orthogonalPath(fromNode = from, fromAnchor = conn.fromAnchor, toNode = to, toAnchor = conn.toAnchor)
+                val pathPoints = orthogonalPath(fromNodeUI = from, fromAnchor = conn.fromAnchor, toNodeUI = to, toAnchor = conn.toAnchor)
                 // 连接所有线段
                 val path = Path().apply {
                     moveTo(pathPoints.first().x, pathPoints.first().y)
@@ -80,26 +81,32 @@ fun ConnectionLayer(nodes: Map<String, Node>, connections: List<Connection>, sca
  *
  * @param id            当前节点唯一标识
  * @param nodes         所有节点
- * @param selectNode    当前选中的节点
+ * @param selectNodeUI  当前选中的节点
  * @param onNodeClick   当前节点被点击
  * @param modifier      节点样式
  */
 @Composable
 fun NodeItem(
     id: String,
-    nodes: SnapshotStateMap<String, Node>,
-    selectNode: MutableState<Node>,
-    onNodeClick: (Node) -> Unit,
+    nodes: SnapshotStateMap<String, NodeUI>,
+    selectNodeUI: MutableState<NodeUI>,
+    onNodeClick: (NodeUI) -> Unit,
     modifier: Modifier = Modifier
 ) {
     val node = nodes[id] ?: return
-    val isSelect = selectNode.value.id == node.id
+    val isSelect = selectNodeUI.value.id == node.id
     CardContainer(
         modifier = modifier
             .size(node.baseSize.width.dp, node.baseSize.height.dp)
             .offset(node.position.x.dp, node.position.y.dp)
             .onGloballyPositioned { coordinates ->
-                nodes[id] = Node(id = id, position = node.position, offset = coordinates.positionInParent(), size = coordinates.size.toSize())
+                nodes[id] = NodeUI(
+                    id = id,
+                    position = node.position,
+                    offset = coordinates.positionInParent(),
+                    size = coordinates.size.toSize(),
+                    node = node.node
+                )
             },
         shadowColor = if (isSelect) MaterialTheme.colorScheme.primary.copy(alpha = 0.25f) else Color.Black.copy(alpha = 0.15f)
     ) {
@@ -121,9 +128,9 @@ fun NodeItem(
                 tint = Color.White
             )
             Spacer(Modifier.weight(2f))
-            Text(node.id, fontSize = 12.sp, lineHeight = 12.sp, fontWeight = FontWeight.Bold, color = Text)
+            Text(node.node?.nodeName ?: "", fontSize = 12.sp, lineHeight = 12.sp, fontWeight = FontWeight.Bold, color = Text)
             Spacer(Modifier.weight(1f))
-            Text("节点描述", fontSize = 10.sp, lineHeight = 10.sp, color = Color(0xFF999999))
+            Text("ID:${node.node?.id ?: 0}", fontSize = 10.sp, lineHeight = 10.sp, color = Color(0xFF999999))
         }
     }
 }
@@ -133,7 +140,7 @@ fun NodeItem(
  *
  * @param anchor    连接的点位信息
  */
-fun Node.anchor(anchor: Anchor): Offset = when (anchor) {
+fun NodeUI.anchor(anchor: Anchor): Offset = when (anchor) {
     Anchor.LEFT -> Offset(offset.x, offset.y + size.height / 2)
     Anchor.RIGHT -> Offset(offset.x + size.width, offset.y + size.height / 2)
     Anchor.TOP -> Offset(offset.x + size.width / 2, offset.y)
@@ -143,15 +150,15 @@ fun Node.anchor(anchor: Anchor): Offset = when (anchor) {
 /**
  * 绘制正交连接线
  *
- * @param fromNode      起始位置节点信息
+ * @param fromNodeUI      起始位置节点信息
  * @param fromAnchor    起始位置的连接点位置
- * @param toNode        终点位置节点信息
+ * @param toNodeUI        终点位置节点信息
  * @param toAnchor      终点位置的连接点位置
  * @param padding       起始/终点位置添加padding值
  */
-private fun orthogonalPath(fromNode: Node, fromAnchor: Anchor, toNode: Node, toAnchor: Anchor, padding: Float = 40f): List<Offset> {
-    val from = fromNode.anchor(fromAnchor)
-    val to = toNode.anchor(toAnchor)
+private fun orthogonalPath(fromNodeUI: NodeUI, fromAnchor: Anchor, toNodeUI: NodeUI, toAnchor: Anchor, padding: Float = 40f): List<Offset> {
+    val from = fromNodeUI.anchor(fromAnchor)
+    val to = toNodeUI.anchor(toAnchor)
     // 处理连接的点位
     val points = mutableListOf<Offset>()
     // 起点出线
@@ -229,13 +236,15 @@ private fun orthogonalPath(fromNode: Node, fromAnchor: Anchor, toNode: Node, toA
  * @param offset    在父控件中的位置
  * @param size      在父控件中的尺寸
  * @param baseSize  控件原始尺寸
+ * @param node      携带当前节点对象
  */
-data class Node(
+data class NodeUI(
     val id: String,
     val position: Offset,
     val offset: Offset = Offset(0f, 0f),
     val size: Size = Size(100f, 80f),
-    val baseSize: Size = Size(100f, 80f)
+    val baseSize: Size = Size(100f, 80f),
+    val node: Node? = null,
 )
 
 /**

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

@@ -52,13 +52,13 @@ import kotlin.math.floor
  */
 @Composable
 fun ZoomPanContainer(
-    nodes: Map<String, Node>,
+    nodes: Map<String, NodeUI>,
     connections: List<Connection>,
     defaultScale: Float = 1f,
     scaleMin: Float = 0.5f,
     scaleMax: Float = 1.5f,
     modifier: Modifier = Modifier,
-    content: @Composable BoxScope.(Float, (Node) -> Unit, (Node) -> Unit, () -> Unit) -> Unit
+    content: @Composable BoxScope.(Float, (NodeUI) -> Unit, (NodeUI) -> Unit, () -> Unit) -> Unit
 ) {
     val scope = rememberCoroutineScope()
     val scale = remember { Animatable(defaultScale) }
@@ -82,16 +82,16 @@ fun ZoomPanContainer(
     /**
      * 动画移动当前节点到屏幕中间
      */
-    fun animateNodeToTopCenter(node: Node) {
+    fun animateNodeToTopCenter(nodeUI: NodeUI) {
         if (containerSize == IntSize.Zero) return
         // 找到容器的中心点位置,高度为控件可用空间的1/5处
         val center = Offset(containerSize.width / 2f, (containerSize.height / 4f) / 2f)
         // 计算偏移量
-        val targetOffset = center - node.offset * scale.value
+        val targetOffset = center - nodeUI.offset * scale.value
 
         // 执行动画移动到中心点
-        val ox = targetOffset.x - (node.size.width / 2f) * scale.value
-        val oy = targetOffset.y - (node.size.height / 2f) * scale.value
+        val ox = targetOffset.x - (nodeUI.size.width / 2f) * scale.value
+        val oy = targetOffset.y - (nodeUI.size.height / 2f) * scale.value
         scope.launch {
             launch { offsetX.animateTo(ox, spring()) }
             launch { offsetY.animateTo(oy, spring()) }
@@ -101,16 +101,16 @@ fun ZoomPanContainer(
     /**
      * 动画移动当前节点到屏幕中间
      */
-    fun animateNodeToCenter(node: Node) {
+    fun animateNodeToCenter(nodeUI: NodeUI) {
         if (containerSize == IntSize.Zero) return
         // 找到容器的中心点位置,高度为控件可用空间的1/5处
         val center = Offset(containerSize.width / 2f, containerSize.height / 2f)
         // 计算偏移量
-        val targetOffset = center - node.offset * scale.value
+        val targetOffset = center - nodeUI.offset * scale.value
 
         // 执行动画移动到中心点
-        val ox = targetOffset.x - (node.size.width / 2f) * scale.value
-        val oy = targetOffset.y - (node.size.height / 2f) * scale.value
+        val ox = targetOffset.x - (nodeUI.size.width / 2f) * scale.value
+        val oy = targetOffset.y - (nodeUI.size.height / 2f) * scale.value
         scope.launch {
             launch { offsetX.animateTo(ox, spring()) }
             launch { offsetY.animateTo(oy, spring()) }

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

@@ -43,8 +43,7 @@ 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.ui.pages.create.job.openPageCreateJob
-import com.iscs.bozzys.ui.pages.select.job.openPageSelectJobType
+import com.iscs.bozzys.ui.pages.edit.step.openPageEditStep
 import com.iscs.bozzys.ui.pages.vm.VMHome
 import com.iscs.bozzys.ui.theme.Main
 import com.iscs.bozzys.ui.theme.Text
@@ -100,7 +99,9 @@ private fun TopToolBar(pv: PaddingValues, vmHome: VMHome) {
                     .clickable(onClick = {
                         // 前期SOP没有做流程选择,这里先直接跳转到创建页面
                         // ctx.openPageSelectJobType()
-                        ctx.openPageCreateJob()
+                        // ctx.openPageCreateJob()
+                        // 测试填写表单数据
+                        ctx.openPageEditStep(19)
                     })
                     .padding(6.dp),
                 tint = Color.White

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

@@ -62,7 +62,7 @@ class PageHome : PageBase() {
         LaunchedEffect(Unit) {
             // 处理基础Toast和Loading提示
             vm.toast.showToast()
-            vm.loading.loading()
+            vm.loading.showLoading()
             vm.init()
         }
         Scaffold(

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

@@ -119,7 +119,7 @@ class PageLogin : PageBase() {
         LaunchedEffect(Unit) {
             // 处理基础Toast和Loading提示
             vm.toast.showToast()
-            vm.loading.loading()
+            vm.loading.showLoading()
         }
         Column(
             Modifier

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

@@ -0,0 +1,110 @@
+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.FormField
+import com.iscs.bozzys.api.FormOption
+import com.iscs.bozzys.ui.base.VMBase
+import com.iscs.bozzys.ui.pages.compose.getFormListByJsonList
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+
+class VMCreateJob : VMBase() {
+
+    // 内部维护
+    private val _state = MutableStateFlow(StateCreateJob())
+
+    // 对Compose暴露
+    val state = _state.asStateFlow()
+
+    // 表单基准数据
+    val json =
+        "[{\"id\":\"job_name\",\"label\":\"作业名称\",\"value\":[\"\"],\"type\":\"input\",\"placeholder\":[\"请输入作业名称\"],\"required\":true},{\"id\":\"job_sop\",\"label\":\"流程模板\",\"value\":[\"\"],\"type\":\"select\",\"placeholder\":[\"\"],\"required\":true,\"options\":[]},{\"id\":\"job_type\",\"label\":\"作业分类\",\"value\":[\"\"],\"type\":\"select\",\"placeholder\":[\"\"],\"options\":[],\"required\":true},{\"id\":\"job_content\",\"label\":\"作业内容\",\"value\":[\"\"],\"type\":\"textarea\",\"placeholder\":[\"\"],\"required\":true},{\"id\":\"job_urgency_level\",\"label\":\"紧急程度\",\"value\":[\"0\"],\"type\":\"radio\",\"placeholder\":[\"\"],\"required\":false,\"options\":[{\"label\":\"普通\",\"value\":\"0\"},{\"label\":\"紧急\",\"value\":\"1\"},{\"label\":\"非常紧急\",\"value\":\"2\"}]}]"
+
+    /**
+     * 初始化
+     */
+    fun init() {
+        viewModelScope.launch {
+            val forms = json.getFormListByJsonList()
+            _state.value = _state.value.copy(forms = forms)
+            // 流程模板
+            ApiRequest.getSopList(mutableMapOf("pageNo" to 1, "pageSize" to -1)).onSuccess {
+                val data = it.data?.list ?: emptyList()
+                val options = data.map { sop -> FormOption(sop.name, sop.id.toString()) }
+                val newForms = _state.value.forms.map { form -> if (form.id == "job_sop") form.copy(options = options) else form }
+                _state.value = _state.value.copy(forms = newForms)
+            }
+            // 作业类型
+            ApiRequest.getDictData(mutableMapOf("pageNo" to 1, "pageSize" to -1, "dictType" to "work_type")).onSuccess {
+                val data = it.data?.list ?: emptyList()
+                val options = data.map { dict -> FormOption(dict.label, dict.value) }
+                val newForms = _state.value.forms.map { form -> if (form.id == "job_type") form.copy(options = options) else form }
+                _state.value = _state.value.copy(forms = newForms)
+            }
+            // 紧急程度
+            ApiRequest.getDictData(mutableMapOf("pageNo" to 1, "pageSize" to -1, "dictType" to "urgency_level")).onSuccess {
+                val data = it.data?.list ?: emptyList()
+                val options = data.map { dict -> FormOption(dict.label, dict.value) }
+                val newForms = _state.value.forms.map { form -> if (form.id == "job_urgency_level") form.copy(options = options) else form }
+                _state.value = _state.value.copy(forms = newForms)
+            }
+        }
+    }
+
+    /**
+     * 创建作业操作
+     */
+    fun onNext(done: (Int) -> Unit) {
+        viewModelScope.launch {
+            // 校验必要参数的值是否有效
+            _state.value.forms.forEach {
+                if (it.required) {
+                    if (it.value.isEmpty() || (it.value.getOrNull(0) ?: "").isEmpty()) {
+                        if (listOf("input", "textarea").contains(it.type)) {
+                            toast.emit("请输入${it.label}")
+                        } else if (listOf("select", "date", "radio").contains(it.type)) {
+                            toast.emit("请选择${it.label}")
+                        }
+                        return@launch
+                    }
+                }
+            }
+            // 这里调用创建接口进行作业的创建
+            loading.emit(StateLoading(show = true))
+            val name = _state.value.forms.find { it.id == "job_name" }?.value?.getOrNull(0) ?: ""
+            val sop = _state.value.forms.find { it.id == "job_sop" }?.value?.getOrNull(0) ?: "0"
+            val type = _state.value.forms.find { it.id == "job_type" }?.value?.getOrNull(0) ?: ""
+            val content = _state.value.forms.find { it.id == "job_content" }?.value?.getOrNull(0) ?: ""
+            val level = _state.value.forms.find { it.id == "job_urgency_level" }?.value?.getOrNull(0) ?: ""
+            ApiRequest.createJob(
+                mutableMapOf(
+                    "name" to name,
+                    "designId" to sop.toInt(),
+                    "type" to type,
+                    "description" to content,
+                    "urgencyLevel" to level
+                )
+            ).onSuccess {
+                delay(500)
+                loading.emit(StateLoading())
+                toast.emit("创建成功")
+                delay(500)
+                done(it.data ?: 0)
+            }.onFailure {
+                delay(500)
+                loading.emit(StateLoading())
+                toast.emit(it.getResponse().msg)
+            }
+        }
+    }
+
+}
+
+/**
+ * 表单数据
+ */
+data class StateCreateJob(val forms: List<FormField> = listOf())

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

@@ -5,7 +5,7 @@ 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.api.Node
 import com.iscs.bozzys.ui.base.VMBase
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asStateFlow
@@ -42,7 +42,7 @@ class VMDetailTask : VMBase() {
                         toast.emit("表单数据异常")
                         return@launch
                     }
-                    _state.value = _state.value.copy(formInfo = formInfo, taskInfo = taskInfo)
+                    _state.value = _state.value.copy(formInfo = formInfo, node = taskInfo)
                 }.onFailure { err ->
                     loading.emit(StateLoading(show = false))
                     toast.emit("获取表单异常:${err.getResponse().msg}")
@@ -58,4 +58,4 @@ class VMDetailTask : VMBase() {
 /**
  * 页面状态类
  */
-data class StateDetailTask(val formInfo: TaskFormInfo = TaskFormInfo(), val taskInfo: TaskInfo = TaskInfo())
+data class StateDetailTask(val formInfo: TaskFormInfo = TaskFormInfo(), val node: Node = Node())

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

@@ -0,0 +1,74 @@
+package com.iscs.bozzys.ui.pages.vm
+
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateMapOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.snapshots.SnapshotStateMap
+import androidx.compose.ui.geometry.Offset
+import androidx.lifecycle.viewModelScope
+import com.iscs.bozzys.api.ApiRequest
+import com.iscs.bozzys.api.FormField
+import com.iscs.bozzys.ui.base.VMBase
+import com.iscs.bozzys.ui.pages.edit.step.compose.Connection
+import com.iscs.bozzys.ui.pages.edit.step.compose.NodeUI
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+
+/**
+ * 作业编辑流程
+ */
+class VMEditStep : VMBase() {
+
+    private val _state = MutableStateFlow(StateEditStep())
+    val state = _state.asStateFlow()
+
+    fun init(id: Int) {
+        viewModelScope.launch {
+            ApiRequest.getJobById(id).onSuccess {
+                val nodes = it.data?.toUINodeMap() ?: mutableStateMapOf()
+                val connections = it.data?.toUIConnectList() ?: emptyList()
+                val nodeId = it.data?.workflowWorkNodeDOList?.getOrNull(0)?.id ?: 0
+                val node = nodes[nodeId.toString()]
+                _state.value = _state.value.copy(
+                    nodes = nodes,
+                    connections = connections,
+                    node = if (node == null) _state.value.node else mutableStateOf(node)
+                )
+            }.onFailure { }
+        }
+    }
+
+    /**
+     * 更新当前选中的节点
+     *
+     * @param nodeUI
+     */
+    fun updateNode(nodeUI: NodeUI) {
+        _state.value = _state.value.copy(node = mutableStateOf(nodeUI))
+    }
+
+    /**
+     * 是否显示表单Dialog
+     */
+    fun formDialogShow(show: Boolean) {
+        _state.value = _state.value.copy(showFormDialog = show)
+    }
+}
+
+/**
+ * 页面驱动数据
+ *
+ * @param nodes             节点数据
+ * @param node              当前选中的节点
+ * @param nodeForms         当前节点表单列表
+ * @param connections       连接线
+ * @param showFormDialog    是否显示表单Dialog
+ */
+data class StateEditStep(
+    val nodes: SnapshotStateMap<String, NodeUI> = mutableStateMapOf(),
+    val node: MutableState<NodeUI> = mutableStateOf(NodeUI("", Offset(0f, 0f))),
+    val nodeForms: List<FormField> = listOf(),
+    val connections: List<Connection> = emptyList(),
+    val showFormDialog: Boolean = false,
+)