Преглед на файлове

1. 表单支持按照接口表单字段进行显示
2. 配置apk的签名文件

bjb преди 4 месеца
родител
ревизия
4aef850ebd

+ 2 - 2
.idea/deploymentTargetSelector.xml

@@ -4,10 +4,10 @@
     <selectionStates>
       <SelectionState runConfigName="app">
         <option name="selectionMode" value="DROPDOWN" />
-        <DropdownSelection timestamp="2025-12-22T09:03:34.905640300Z">
+        <DropdownSelection timestamp="2025-12-23T00:54:45.591141200Z">
           <Target type="DEFAULT_BOOT">
             <handle>
-              <DeviceId pluginId="PhysicalDevice" identifier="serial=32923c41" />
+              <DeviceId pluginId="PhysicalDevice" identifier="serial=d8d12db95670c08" />
             </handle>
           </Target>
         </DropdownSelection>

+ 23 - 0
app/build.gradle.kts

@@ -1,7 +1,11 @@
+import java.io.FileInputStream
+import java.util.Properties
+
 plugins {
     alias(libs.plugins.android.application)
     alias(libs.plugins.kotlin.android)
     alias(libs.plugins.kotlin.compose)
+    alias(libs.plugins.kotlin.serialization)
 }
 
 android {
@@ -20,10 +24,23 @@ android {
         testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
     }
 
+    signingConfigs {
+        create("release") {
+            storeFile = file("../store.jks")
+            storePassword = "bozzys2025"
+            keyAlias = "bozzys"
+            keyPassword = "bozzys2025"
+        }
+    }
+
     buildTypes {
         release {
             isMinifyEnabled = false
             proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
+            signingConfig = signingConfigs.getByName("release")
+        }
+        getByName("debug") {
+            signingConfig = signingConfigs.getByName("release")
         }
     }
     compileOptions {
@@ -63,6 +80,12 @@ dependencies {
     implementation(libs.push)
     implementation(libs.push.third)
     implementation(libs.push.third.xiaomi)
+    // 三方库
+    implementation(libs.third.json)
+    // 日期和时间选择器 下面第二行的依赖是为了引入Dialog
+    implementation(libs.third.date.picker)
+    implementation("com.google.android.material:material:1.1.0")
+
     implementation(libs.androidx.ui)
     implementation(libs.androidx.ui.graphics)
 

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

@@ -1,5 +1,10 @@
 package com.iscs.bozzys.api
 
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import kotlinx.serialization.Serializable
+
 
 /**
  * 请求基础响应对象
@@ -12,4 +17,44 @@ open class Response(var code: Int = 0, var msg: String = "")
  * @param nickName  用户昵称
  * @param token     token
  */
-class LoginRsp(val nickName: String, val token: String) : Response()
+class LoginRsp(val nickName: String, val token: String) : Response()
+
+/**
+ * 表单需要的组件
+ *
+ * @param id
+ * @param type
+ * @param label
+ * @param name
+ * @param required
+ * @param placeholder
+ * @param options
+ * @param cardTitle
+ * @param gridColumns
+ * @param children
+ */
+@Serializable
+class FormField(
+    val id: String = "",
+    val type: String = "",
+    val label: String = "",
+    val name: String = "",
+    val required: Boolean = false,
+    val placeholder: String = "",
+    val options: List<FormOption> = listOf(),
+    val cardTitle: String = "",
+    val gridColumns: Int = 0,
+    val children: List<FormField> = listOf()
+) {
+    // 可变参数,参数变化能监听到
+    var value by mutableStateOf("")
+}
+
+/**
+ * 表单Option字段
+ *
+ * @param label 标题
+ * @param value 数值
+ */
+@Serializable
+data class FormOption(val label: String = "", val value: String = "")

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

@@ -4,6 +4,7 @@ import androidx.compose.foundation.border
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Box
 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.fillMaxWidth
@@ -35,6 +36,7 @@ import androidx.compose.ui.draw.rotate
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.SolidColor
 import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.text.font.FontWeight
@@ -43,7 +45,13 @@ import androidx.compose.ui.unit.DpOffset
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 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.utils.DateUtil.format
+import com.loper7.date_time_picker.DateTimeConfig
+import com.loper7.date_time_picker.dialog.CardDatePickerDialog
+import kotlinx.serialization.json.Json
 
 /**
  * 表单输入框
@@ -53,15 +61,19 @@ import com.iscs.bozzys.ui.theme.Main
  * @param onValueChange 表单内容发生变化
  */
 @Composable
-fun FormInput(label: String, value: String, onValueChange: (String) -> Unit) {
-    Column(Modifier.fillMaxWidth()) {
+fun FormInput(label: String, value: String, onValueChange: (String) -> Unit, placeholder: String = "", required: Boolean = false) {
+    Column(
+        Modifier
+            .fillMaxWidth()
+            .heightIn(max = 90.dp)
+    ) {
         Row(
             Modifier
                 .fillMaxWidth()
                 .height(40.dp), verticalAlignment = Alignment.CenterVertically
         ) {
             Text(label, fontSize = 14.sp, fontWeight = FontWeight.Bold)
-            Text("*", fontSize = 14.sp, color = Color(0xFFFF4D4F), modifier = Modifier.padding(start = 3.dp))
+            if (required) Text("*", fontSize = 14.sp, color = Color(0xFFFF4D4F), modifier = Modifier.padding(start = 3.dp))
         }
         BasicTextField(
             value,
@@ -77,7 +89,7 @@ fun FormInput(label: String, value: String, onValueChange: (String) -> Unit) {
                 Box(contentAlignment = Alignment.CenterStart) {
                     innerTextField()
                     if (value.isEmpty()) {
-                        val text = "请输入$label"
+                        val text = placeholder.ifEmpty { "请输入$label" }
                         Text(text, color = Color(0xFF9CA3AF), fontSize = 14.sp, lineHeight = 18.sp)
                     }
                 }
@@ -97,13 +109,15 @@ fun FormInput(label: String, value: String, onValueChange: (String) -> Unit) {
  * @param onSelectChange    选择变化监听
  */
 @Composable
-fun <T> FormSelect(label: String, value: Pair<String, T>, options: List<Pair<String, T>>, onSelectChange: (Pair<String, T>) -> Unit) {
+fun FormSelect(label: String, value: String, options: List<FormOption>, onSelectChange: (FormOption) -> Unit, required: Boolean = false) {
     var expanded by remember { mutableStateOf(false) }
     var width by remember { mutableStateOf(0) }
     val density = LocalDensity.current
+    val find = options.find { it.value == value }
     Column(
         Modifier
             .fillMaxWidth()
+            .heightIn(max = 90.dp)
             .onSizeChanged({ width = it.width })
     ) {
         Row(
@@ -112,7 +126,7 @@ fun <T> FormSelect(label: String, value: Pair<String, T>, options: List<Pair<Str
                 .height(40.dp), verticalAlignment = Alignment.CenterVertically
         ) {
             Text(label, fontSize = 14.sp, fontWeight = FontWeight.Bold)
-            Text("*", fontSize = 14.sp, color = Color(0xFFFF4D4F), modifier = Modifier.padding(start = 3.dp))
+            if (required) Text("*", fontSize = 14.sp, color = Color(0xFFFF4D4F), modifier = Modifier.padding(start = 3.dp))
         }
         Row(
             Modifier
@@ -125,7 +139,7 @@ fun <T> FormSelect(label: String, value: Pair<String, T>, options: List<Pair<Str
             verticalAlignment = Alignment.CenterVertically
         ) {
             Text(
-                value.first,
+                find?.label ?: "请选择$label",
                 color = Color.Black,
                 fontSize = 14.sp,
                 lineHeight = 18.sp,
@@ -151,7 +165,7 @@ fun <T> FormSelect(label: String, value: Pair<String, T>, options: List<Pair<Str
                 options.forEach { option ->
                     DropdownMenuItem(
                         modifier = Modifier.fillMaxWidth(),
-                        text = { Text(option.first) },
+                        text = { Text(option.label) },
                         onClick = {
                             onSelectChange(option)
                             expanded = false
@@ -172,15 +186,19 @@ fun <T> FormSelect(label: String, value: Pair<String, T>, options: List<Pair<Str
  * @param placeholder   占位内容
  */
 @Composable
-fun FormTextarea(label: String, value: String, onValueChange: (String) -> Unit, placeholder: String = "") {
-    Column(Modifier.fillMaxWidth()) {
+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)
-            Text("*", fontSize = 14.sp, color = Color(0xFFFF4D4F), modifier = Modifier.padding(start = 3.dp))
+            if (required) Text("*", fontSize = 14.sp, color = Color(0xFFFF4D4F), modifier = Modifier.padding(start = 3.dp))
         }
         BasicTextField(
             value,
@@ -215,18 +233,33 @@ fun FormTextarea(label: String, value: String, onValueChange: (String) -> Unit,
  * @param options           可选列表
  * @param onSelectChange    当前选中
  */
+@OptIn(ExperimentalLayoutApi::class)
 @Composable
-fun <T> FormRadio(label: String, value: Pair<String, T>, options: List<Pair<String, T>>, onSelectChange: (Pair<String, T>) -> Unit) {
-    Column(Modifier.fillMaxWidth()) {
+fun FormRadio(
+    label: String,
+    value: String,
+    options: List<FormOption>,
+    onSelectChange: (FormOption) -> Unit,
+    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)
-            Text("*", fontSize = 14.sp, color = Color(0xFFFF4D4F), modifier = Modifier.padding(start = 3.dp))
+            if (required) Text("*", fontSize = 14.sp, color = Color(0xFFFF4D4F), modifier = Modifier.padding(start = 3.dp))
         }
-        FlowRow(Modifier.fillMaxWidth()) {
+        FlowRow(
+            Modifier
+                .fillMaxWidth()
+                .heightIn(max = 120.dp)
+        ) {
             options.forEach { item ->
                 Row(
                     modifier = Modifier
@@ -235,10 +268,85 @@ fun <T> FormRadio(label: String, value: Pair<String, T>, options: List<Pair<Stri
                         .padding(horizontal = 10.dp, vertical = 5.dp),
                     verticalAlignment = Alignment.CenterVertically
                 ) {
-                    RadioButton(selected = value == item, onClick = null, modifier = Modifier.size(14.dp))
-                    Text(text = item.first, fontSize = 15.sp, modifier = Modifier.padding(start = 10.dp))
+                    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))
                 }
             }
         }
     }
+}
+
+/**
+ * 表单单选组件
+ *
+ * @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
+    val picker = CardDatePickerDialog.builder(LocalContext.current)
+        .setTitle("请选择日期")
+        .setDefaultTime(date)
+        .setDisplayType(mutableListOf(DateTimeConfig.YEAR, DateTimeConfig.MONTH, DateTimeConfig.DAY, DateTimeConfig.HOUR, DateTimeConfig.MIN))
+        .setPickerLayout(R.layout.date_picker)
+        .showBackNow(false)
+        .showFocusDateInfo(false)
+        .showDateLabel(false)
+        .setOnChoose { millisecond ->
+            onSelectChange(millisecond)
+        }.build()
+
+    Column(
+        Modifier
+            .fillMaxWidth()
+            .heightIn(max = 90.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))
+        }
+
+        Text(
+            date.format("yyyy/MM/dd HH:mm"),
+            modifier = Modifier
+                .fillMaxWidth()
+                .height(46.dp)
+                .border(1.dp, shape = RoundedCornerShape(6.dp), color = Color(0xFFE5E6EB))
+                .clip(RoundedCornerShape(6.dp))
+                .clickable(onClick = {
+                    picker.show()
+                })
+                .padding(10.dp),
+            fontSize = 14.sp,
+            lineHeight = 26.sp
+        )
+    }
+}
+
+/**
+ * 转换表单列表
+ */
+fun String.getFormList(): List<FormField> {
+    val json = Json { ignoreUnknownKeys = true }
+    val fields = json.decodeFromString<List<FormField>>(this)
+    val list = ArrayList<FormField>()
+
+    fun List<FormField>.forEach() {
+        this.forEach { ch ->
+            if (ch.children.isNotEmpty()) ch.children.forEach()
+            else list.add(ch)
+        }
+    }
+
+    // 执行递归遍历
+    fields.forEach()
+
+    return list
 }

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

@@ -32,6 +32,7 @@ import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 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
@@ -51,12 +52,13 @@ class PageCreateJob : PageBase() {
         // 作业名称
         var jobName by remember { mutableStateOf("") }
         // 作业分类
-        val typeOptions = mutableStateListOf("请选择分类" to "", "维修" to "repair", "维保" to "check", "其他" to "other")
-        var typeSelect by remember { mutableStateOf("请选择分类" to "") }
+        val typeOptions =
+            mutableStateListOf(FormOption("请选择分类", ""), FormOption("维修", "repair"), FormOption("维保", "check"), FormOption("其他", "other"))
+        var typeSelect by remember { mutableStateOf(typeOptions[0]) }
         // 作业内容
         var jobContent by remember { mutableStateOf("") }
         // 作业紧急程度
-        val levelOptions = mutableStateListOf("普通" to "Normal", "紧急" to "Middle", "非常紧急" to "High")
+        val levelOptions = mutableStateListOf(FormOption("普通", "Normal"), FormOption("紧急", "Middle"), FormOption("非常紧急", "High"))
         var jobLevel by remember { mutableStateOf(levelOptions[0]) }
         Column(
             Modifier
@@ -67,13 +69,13 @@ class PageCreateJob : PageBase() {
             // 表单内容
             Column(Modifier.padding(horizontal = 16.dp, vertical = 5.dp)) {
                 // 作业名称
-                FormInput("作业名称", jobName, { jobName = it })
+                FormInput("作业名称", jobName, { jobName = it }, required = true)
                 // 作业分类
-                FormSelect("作业分类", typeSelect, typeOptions, { typeSelect = it })
+                FormSelect("作业分类", typeSelect.value, typeOptions, { typeSelect = it })
                 // 作业内容
                 FormTextarea("作业内容", jobContent, { jobContent = it })
                 // 作业紧急程度
-                FormRadio("紧急程度", jobLevel, levelOptions, { jobLevel = it })
+                FormRadio("紧急程度", jobLevel.value, levelOptions, { jobLevel = it })
             }
             Spacer(Modifier.weight(1f))
             // 底部下一步按钮

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

@@ -3,12 +3,9 @@ package com.iscs.bozzys.ui.pages.edit.step
 import android.content.Context
 import android.content.Intent
 import android.view.WindowManager
-import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.animation.core.Animatable
 import androidx.compose.animation.core.spring
 import androidx.compose.animation.core.tween
-import androidx.compose.animation.slideInVertically
-import androidx.compose.animation.slideOutVertically
 import androidx.compose.foundation.background
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -17,13 +14,18 @@ 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.WindowInsets
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.ime
+import androidx.compose.foundation.layout.imePadding
 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
@@ -33,6 +35,7 @@ import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.SideEffect
 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
@@ -45,6 +48,7 @@ import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.style.TextAlign
@@ -55,11 +59,16 @@ import androidx.compose.ui.window.DialogProperties
 import androidx.compose.ui.zIndex
 import androidx.lifecycle.lifecycleScope
 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.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.edit.step.compose.Anchor
 import com.iscs.bozzys.ui.pages.edit.step.compose.Connection
 import com.iscs.bozzys.ui.pages.edit.step.compose.Node
@@ -82,22 +91,27 @@ class PageEditStep : PageBase() {
         val nodes = remember { mutableStateMapOf<String, Node>() }
         val selectNode = remember { mutableStateOf(Node("-1", Offset(0f, 0f))) }
         val lines = listOf(
-            Connection("A", "B"),
-            Connection("A", "D", fromAnchor = Anchor.RIGHT, toAnchor = Anchor.LEFT),
-            Connection("B", "C")
+            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.getFormList())
         LaunchedEffect(Unit) {
-            nodes["A"] = Node("A", Offset(50f, 0f))
-            nodes["B"] = Node("B", Offset(250f, 0f))
-            nodes["D"] = Node("D", Offset(250f, 100f))
-            nodes["C"] = Node("C", Offset(450f, 0f))
+            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 }
         }
         Column(Modifier.fillMaxSize()) {
             Title(pv, "作业流程管理")
-            ZoomPanContainer(nodes, lines, modifier = Modifier.fillMaxSize()) { scale, toTopCenter, toCenter ->
+            ZoomPanContainer(nodes, lines, modifier = Modifier.fillMaxSize()) { scale, toTopCenter, toCenter, toLast ->
                 // 控件显示
                 Box(
                     modifier = Modifier
@@ -121,49 +135,65 @@ class PageEditStep : PageBase() {
                 }
                 // 底部弹出式的Dialog
                 FormDialog(showFormDialog, {
-                    // 回到中心位置,默认不添加 等反馈
-                    // toCenter(selectNode.value)
+                    // 回到移动中心位置前的位置,默认不添加 等反馈
+                    toLast()
                     showFormDialog = false
                 }, pv) {
+
+                    // 监听键盘弹出事件
+
+                    val density = LocalDensity.current
+                    val imeInsets = WindowInsets.ime
+                    val imeBottom = imeInsets.getBottom(density)
+
                     Column(
                         Modifier
-                            .fillMaxWidth()
-                            .fillMaxHeight(5 / 7f)
+                            .fillMaxHeight(4 / 7f)
                             .padding(horizontal = 16.dp)
                     ) {
-                        Column(
+                        Box(
                             Modifier
-                                .fillMaxWidth()
+                                .fillMaxSize()
                                 .weight(1f)
                         ) {
-                            FormInput("节点名称", "", {})
-                            FormSelect("负责人", "张三" to "zhangsan", listOf("张三" to "zhangsan"), {})
-                            FormSelect(
-                                "关联表单",
-                                "通用作业申请单" to "normal2",
-                                listOf(
-                                    "通用作业申请单" to "normal2",
-                                    "通用作业申请单" to "normal3",
-                                    "通用作业申请单" to "normal4",
-                                    "通用作业申请单" to "normal5",
-                                    "通用作业申请单" to "normal6",
-                                    "通用作业申请单" to "normal7",
-                                    "通用作业申请单" to "normal8",
-                                    "通用作业申请单" to "normal",
-                                    "通用作业申请单" to "normal9"
-                                ),
-                                {})
+                            LazyColumn(
+                                Modifier
+                                    .imePadding()
+                                    .padding(vertical = 5.dp)
+                            ) {
+                                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)
+                                        "select" -> FormSelect(
+                                            it.label,
+                                            it.value.ifEmpty { it.options[0].value },
+                                            it.options,
+                                            { sel -> it.value = sel.value })
+
+                                        "date" -> FormDateSelect(it.label, it.value.ifEmpty { "0" }.toLong(), { sel -> it.value = sel.toString() })
+                                        "radio" -> FormRadio(
+                                            it.label,
+                                            it.value.ifEmpty { it.options[0].value },
+                                            it.options,
+                                            { sel -> it.value = sel.value })
+                                    }
+                                }
+                            }
                         }
-                        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)
+                            }
                         }
                     }
                 }
@@ -174,34 +204,6 @@ class PageEditStep : PageBase() {
 
 }
 
-/**
- * 底部弹出式的表单录入弹窗,嵌入页面式
- */
-@Composable
-fun FormInPage(show: Boolean, onDismiss: () -> Unit, content: @Composable () -> Unit) {
-    AnimatedVisibility(
-        visible = show,
-        enter = slideInVertically { it },
-        exit = slideOutVertically { it }
-    ) {
-        Box(Modifier.fillMaxSize()) {
-            Box(
-                modifier = Modifier
-                    .align(Alignment.BottomCenter)
-                    .fillMaxWidth()
-                    .background(Color.White)
-            ) {
-                Column(modifier = Modifier.fillMaxWidth()) {
-                    Row() {
-
-                    }
-                    content()
-                }
-            }
-        }
-    }
-}
-
 @Composable
 fun FormDialog(
     show: Boolean,
@@ -242,7 +244,7 @@ fun FormDialog(
         Box(
             modifier = Modifier
                 .fillMaxSize()
-                .background(Color.Black.copy(alpha = 0.6f))
+                .background(Color.Black.copy(alpha = 0.4f))
                 .clickable(
                     indication = null,
                     interactionSource = remember { MutableInteractionSource() }) {
@@ -300,6 +302,12 @@ fun FormDialog(
                                 .padding(10.dp)
                         )
                     }
+                    Spacer(
+                        Modifier
+                            .fillMaxWidth()
+                            .height(0.5.dp)
+                            .background(Color.Black.copy(alpha = 0.05f))
+                    )
                     content()
                 }
             }

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

@@ -41,6 +41,10 @@ import com.iscs.bozzys.ui.theme.Text
 
 /**
  * 布局之间的连线操作(贝塞尔曲线)
+ *
+ * @param nodes         所有节点列表
+ * @param connections   所有连接的信息
+ * @param scale         缩放比例
  */
 @Composable
 fun ConnectionLayer(nodes: Map<String, Node>, connections: List<Connection>, scale: Float) {
@@ -65,7 +69,7 @@ fun ConnectionLayer(nodes: Map<String, Node>, connections: List<Connection>, sca
                     }
                 }
                 // 绘制线段
-                drawPath(path = path, color = Main, style = Stroke(width = 2f * scale, cap = StrokeCap.Round, join = StrokeJoin.Round))
+                drawPath(path = path, color = Main, style = Stroke(width = 3f * scale, cap = StrokeCap.Round, join = StrokeJoin.Round))
             }
         }
     }
@@ -76,8 +80,8 @@ fun ConnectionLayer(nodes: Map<String, Node>, connections: List<Connection>, sca
  *
  * @param id            当前节点唯一标识
  * @param nodes         所有节点
- * @param onNodeClick   当前节点被点击
  * @param selectNode    当前选中的节点
+ * @param onNodeClick   当前节点被点击
  * @param modifier      节点样式
  */
 @Composable
@@ -117,13 +121,18 @@ fun NodeItem(
                 tint = Color.White
             )
             Spacer(Modifier.weight(2f))
-            Text("节点名称", fontSize = 12.sp, lineHeight = 12.sp, fontWeight = FontWeight.Bold, color = Text)
+            Text(node.id, 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))
         }
     }
 }
 
+/**
+ * 通过节点获取四面连接点的位置信息
+ *
+ * @param anchor    连接的点位信息
+ */
 fun Node.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)
@@ -133,6 +142,12 @@ fun Node.anchor(anchor: Anchor): Offset = when (anchor) {
 
 /**
  * 绘制正交连接线
+ *
+ * @param fromNode      起始位置节点信息
+ * @param fromAnchor    起始位置的连接点位置
+ * @param toNode        终点位置节点信息
+ * @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)
@@ -232,6 +247,11 @@ enum class Anchor {
 
 /**
  * 定义连线信息
+ *
+ * @param fromId        起始ID
+ * @param toId          终点ID
+ * @param fromAnchor    来源起始位置
+ * @param toAnchor      终点结束位置
  */
 data class Connection(
     val fromId: String,

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

@@ -58,12 +58,14 @@ fun ZoomPanContainer(
     scaleMin: Float = 0.5f,
     scaleMax: Float = 1.5f,
     modifier: Modifier = Modifier,
-    content: @Composable BoxScope.(Float, (Node) -> Unit, (Node) -> Unit) -> Unit
+    content: @Composable BoxScope.(Float, (Node) -> Unit, (Node) -> Unit, () -> Unit) -> Unit
 ) {
     val scope = rememberCoroutineScope()
     val scale = remember { Animatable(defaultScale) }
     val offsetX = remember { Animatable(0f) }
     val offsetY = remember { Animatable(0f) }
+    // 记录上一次位置,手动触发和缩放控件触发有效
+    var lastOffset by remember { mutableStateOf(Offset(0f, 0f)) }
     // 当前容器大小
     var containerSize by remember { mutableStateOf(IntSize.Zero) }
 
@@ -72,6 +74,8 @@ fun ZoomPanContainer(
             scale.snapTo(targetValue = (scale.value * zoomChange).coerceIn(scaleMin, scaleMax))
             offsetX.snapTo(offsetX.value + panChange.x)
             offsetY.snapTo(offsetY.value + panChange.y)
+            // 先保存上一次的位置
+            lastOffset = Offset(offsetX.value, offsetY.value)
         }
     }
 
@@ -81,7 +85,7 @@ fun ZoomPanContainer(
     fun animateNodeToTopCenter(node: Node) {
         if (containerSize == IntSize.Zero) return
         // 找到容器的中心点位置,高度为控件可用空间的1/5处
-        val center = Offset(containerSize.width / 2f, (containerSize.height / 5f) / 2f)
+        val center = Offset(containerSize.width / 2f, (containerSize.height / 4f) / 2f)
         // 计算偏移量
         val targetOffset = center - node.offset * scale.value
 
@@ -113,6 +117,16 @@ fun ZoomPanContainer(
         }
     }
 
+    /**
+     * 移动到上一次位置
+     */
+    fun animateLastPosition() {
+        scope.launch {
+            launch { offsetX.animateTo(lastOffset.x, spring()) }
+            launch { offsetY.animateTo(lastOffset.y, spring()) }
+        }
+    }
+
     Box(
         modifier = modifier
             .clipToBounds()
@@ -176,6 +190,7 @@ fun ZoomPanContainer(
                 scaleY = scale.value
                 translationX = offsetX.value
                 translationY = offsetY.value
+                // lastOffset = Offset(offsetX.value, offsetY.value)
             }
         ) {
             // 点阵背景
@@ -183,7 +198,7 @@ fun ZoomPanContainer(
             // 连接线操作
             if (nodes.isNotEmpty()) ConnectionLayer(nodes, connections, scale = scale.value)
             // 外部自定义子控件
-            content(scale.value, ::animateNodeToTopCenter, ::animateNodeToCenter)
+            content(scale.value, ::animateNodeToTopCenter, ::animateNodeToCenter, ::animateLastPosition)
         }
     }
 }

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

@@ -115,7 +115,7 @@ private fun TopToolBar(pv: PaddingValues, vmHome: VMHome) {
                     modifier = Modifier
                         .size(36.dp)
                         .clip(RoundedCornerShape(6.dp))
-                        .clickable(onClick = {})
+                        .clickable(onClick = { vmHome.updateNavIndex(2) })
                         .padding(9.dp),
                     tint = Color.White
                 )
@@ -139,7 +139,7 @@ private fun TopToolBar(pv: PaddingValues, vmHome: VMHome) {
                 modifier = Modifier
                     .size(36.dp)
                     .clip(RoundedCornerShape(6.dp))
-                    .clickable(onClick = {})
+                    .clickable(onClick = { vmHome.updateNavIndex(3) })
                     .padding(10.dp),
                 tint = Color.White
             )

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

@@ -85,7 +85,7 @@ private fun TopToolBar(pv: PaddingValues, vmHome: VMHome) {
                 contentDescription = null,
                 modifier = Modifier
                     .size(36.dp)
-                    .clip(RoundedCornerShape(12.dp))
+                    .clip(RoundedCornerShape(6.dp))
                     .clickable(onClick = { ctx.openPageSelectJobType() })
                     .padding(6.dp),
                 tint = Color.White

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

@@ -1,13 +1,13 @@
 package com.iscs.bozzys.ui.pages.home
 
-import android.util.Log
 import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.zIndex
@@ -18,7 +18,9 @@ fun MyCompose(pv: PaddingValues, zIndex: Float) {
         modifier = Modifier
             .fillMaxSize()
             .zIndex(zIndex)
-            .background(Color.White)
+            .background(Color.White),
+        verticalArrangement = Arrangement.Center,
+        horizontalAlignment = Alignment.CenterHorizontally
     ) {
         Text("设置")
     }

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

@@ -251,7 +251,12 @@ class PageLogin : PageBase() {
                     // 隐藏输入框
                     kb?.hide()
                     // 执行登录操作
-                    vm.login { openPageHome() }
+                    vm.login {
+                        // 打开主页
+                        openPageHome()
+                        // 延时关闭当前页面
+                        destroyDelay(500)
+                    }
                 }, Modifier
                     .padding(top = 30.dp)
                     .size(335.dp, 52.dp)

+ 3 - 0
app/src/main/java/com/iscs/bozzys/ui/pages/select/job/PageSelectJobType.kt

@@ -7,6 +7,7 @@ import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
 import androidx.compose.foundation.layout.FlowRow
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Row
@@ -51,6 +52,8 @@ fun Context.openPageSelectJobType() {
 }
 
 class PageSelectJobType : PageBase() {
+
+    @OptIn(ExperimentalLayoutApi::class)
     @Composable
     override fun GetViews(pv: PaddingValues) {
         var keywords by remember { mutableStateOf("") }

+ 4 - 0
app/src/main/java/com/iscs/bozzys/ui/theme/Theme.kt

@@ -23,6 +23,8 @@ private val DarkColorScheme = darkColorScheme(
     primary = Main,
     secondary = Main,
     tertiary = Main,
+    onSurfaceVariant = Main,
+    surfaceContainerHigh = Color.Black,
     background = Color.Black,
     surfaceContainer = Color.White
 )
@@ -32,6 +34,8 @@ private val LightColorScheme = lightColorScheme(
     primary = Main,
     secondary = Main,
     tertiary = Main,
+    onSurfaceVariant = Main,
+    surfaceContainerHigh = Color.White,
     background = Color.White,
     surfaceContainer = Color.White
 )

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

@@ -0,0 +1,18 @@
+package com.iscs.bozzys.utils
+
+import java.time.Instant
+import java.time.ZoneId
+import java.time.format.DateTimeFormatter
+
+object DateUtil {
+
+    /**
+     * 格式化时间戳为字符串
+     */
+    fun Long.format(pattern: String): String {
+        return Instant.ofEpochMilli(this)
+            .atZone(ZoneId.systemDefault())
+            .format(DateTimeFormatter.ofPattern(pattern))
+    }
+
+}

+ 115 - 0
app/src/main/res/layout/date_picker.xml

@@ -0,0 +1,115 @@
+<?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" />
+
+    <com.loper7.date_time_picker.number_picker.NumberPicker
+        android:id="@+id/np_datetime_hour"
+        android:layout_width="60dp"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="16dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintStart_toEndOf="@+id/np_datetime_day"
+        app:layout_constraintTop_toTopOf="parent"
+        app:np_dividerColor="#E5E5E5"
+        app:np_dividerThickness="0.6dp"
+        app:np_height="184dp"
+        app:np_selectedTextSize="13sp"
+        app:np_textSize="13sp"
+        app:np_wheelItemCount="3" />
+
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text=":"
+        android:textColor="@color/black"
+        android:textSize="16sp"
+        android:translationY="-2dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toStartOf="@+id/np_datetime_minute"
+        app:layout_constraintStart_toEndOf="@+id/np_datetime_hour"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <com.loper7.date_time_picker.number_picker.NumberPicker
+        android:id="@+id/np_datetime_minute"
+        android:layout_width="60dp"
+        android:layout_height="wrap_content"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintStart_toEndOf="@+id/np_datetime_hour"
+        app:layout_constraintTop_toTopOf="parent"
+        app:np_dividerColor="#E5E5E5"
+        app:np_dividerThickness="0.6dp"
+        app:np_height="184dp"
+        app:np_selectedTextSize="13sp"
+        app:np_textSize="13sp"
+        app:np_wheelItemCount="3" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 6 - 0
gradle/libs.versions.toml

@@ -47,9 +47,15 @@ push_third_xiaomi = { group = "com.aliyun.ams", name = "alicloud-android-third-p
 androidx-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "ui" }
 androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics", version.ref = "uiGraphics" }
 # 三方Compose库
+# JSON对象转换
+third-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version = "1.6.0" }
+# 时间选择器
+third-date-picker = { group = "com.github.loper7", name = "DateTimePicker", version = "0.6.3" }
 
 [plugins]
 android-application = { id = "com.android.application", version.ref = "agp" }
 kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
 kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
+
 

+ 3 - 0
settings.gradle.kts

@@ -20,6 +20,9 @@ dependencyResolutionManagement {
         maven("https://maven.aliyun.com/nexus/content/repositories/releases/")
         // 配置HMS Core SDK的Maven仓地址,集成华为通道需要。
         maven("https://developer.huawei.com/repo/")
+        // jitpack
+        // 目前使用到的库有WheelPickerCompose
+        maven("https://jitpack.io")
     }
 }
 

BIN
store.jks