|
|
@@ -6,6 +6,7 @@ import android.os.Build
|
|
|
import android.util.Log
|
|
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
|
import androidx.activity.result.contract.ActivityResultContracts
|
|
|
+import androidx.compose.foundation.background
|
|
|
import androidx.compose.foundation.border
|
|
|
import androidx.compose.foundation.clickable
|
|
|
import androidx.compose.foundation.layout.Arrangement
|
|
|
@@ -14,9 +15,13 @@ import androidx.compose.foundation.layout.Column
|
|
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
|
|
import androidx.compose.foundation.layout.FlowRow
|
|
|
import androidx.compose.foundation.layout.Row
|
|
|
+import androidx.compose.foundation.layout.Spacer
|
|
|
+import androidx.compose.foundation.layout.aspectRatio
|
|
|
+import androidx.compose.foundation.layout.fillMaxSize
|
|
|
import androidx.compose.foundation.layout.fillMaxWidth
|
|
|
import androidx.compose.foundation.layout.height
|
|
|
import androidx.compose.foundation.layout.heightIn
|
|
|
+import androidx.compose.foundation.layout.offset
|
|
|
import androidx.compose.foundation.layout.padding
|
|
|
import androidx.compose.foundation.layout.size
|
|
|
import androidx.compose.foundation.layout.width
|
|
|
@@ -35,6 +40,7 @@ import androidx.compose.material3.RadioButton
|
|
|
import androidx.compose.material3.Text
|
|
|
import androidx.compose.runtime.Composable
|
|
|
import androidx.compose.runtime.LaunchedEffect
|
|
|
+import androidx.compose.runtime.collectAsState
|
|
|
import androidx.compose.runtime.getValue
|
|
|
import androidx.compose.runtime.key
|
|
|
import androidx.compose.runtime.mutableStateListOf
|
|
|
@@ -44,10 +50,16 @@ import androidx.compose.runtime.setValue
|
|
|
import androidx.compose.runtime.snapshots.SnapshotStateList
|
|
|
import androidx.compose.ui.Alignment
|
|
|
import androidx.compose.ui.Modifier
|
|
|
+import androidx.compose.ui.draw.alpha
|
|
|
import androidx.compose.ui.draw.clip
|
|
|
+import androidx.compose.ui.draw.drawBehind
|
|
|
import androidx.compose.ui.draw.rotate
|
|
|
+import androidx.compose.ui.geometry.CornerRadius
|
|
|
import androidx.compose.ui.graphics.Color
|
|
|
+import androidx.compose.ui.graphics.PathEffect
|
|
|
import androidx.compose.ui.graphics.SolidColor
|
|
|
+import androidx.compose.ui.graphics.drawscope.Stroke
|
|
|
+import androidx.compose.ui.layout.ContentScale
|
|
|
import androidx.compose.ui.layout.onSizeChanged
|
|
|
import androidx.compose.ui.platform.LocalContext
|
|
|
import androidx.compose.ui.platform.LocalDensity
|
|
|
@@ -57,14 +69,22 @@ import androidx.compose.ui.text.input.KeyboardType
|
|
|
import androidx.compose.ui.unit.DpOffset
|
|
|
import androidx.compose.ui.unit.dp
|
|
|
import androidx.compose.ui.unit.sp
|
|
|
+import androidx.lifecycle.viewmodel.compose.viewModel
|
|
|
+import coil.compose.AsyncImage
|
|
|
+import coil.compose.rememberAsyncImagePainter
|
|
|
import com.iscs.bozzys.R
|
|
|
import com.iscs.bozzys.api.FormField
|
|
|
import com.iscs.bozzys.api.FormOption
|
|
|
import com.iscs.bozzys.api.Job
|
|
|
+import com.iscs.bozzys.api.UploadFile
|
|
|
+import com.iscs.bozzys.ui.common.PageBase
|
|
|
+import com.iscs.bozzys.ui.pages.vm.VMFormUploadFile
|
|
|
import com.iscs.bozzys.ui.theme.Main
|
|
|
import com.iscs.bozzys.ui.theme.Text
|
|
|
import com.iscs.bozzys.utils.DateUtil.dateToTimestamp
|
|
|
import com.iscs.bozzys.utils.DateUtil.format
|
|
|
+import com.iscs.bozzys.utils.SystemUtil
|
|
|
+import com.iscs.bozzys.utils.SystemUtil.uriToFile
|
|
|
import com.loper7.date_time_picker.DateTimeConfig
|
|
|
import com.loper7.date_time_picker.dialog.CardDatePickerDialog
|
|
|
import kotlinx.serialization.json.Json
|
|
|
@@ -177,8 +197,8 @@ fun FormBox(forms: List<FormField>, onValueChange: (FormField) -> Unit, modifier
|
|
|
required = form.required,
|
|
|
enable = form.enabled && enabled
|
|
|
)
|
|
|
- // 图片上传
|
|
|
- "image" -> FormImage(
|
|
|
+ // 上传文件
|
|
|
+ "upload" -> FormUpload(
|
|
|
form.label,
|
|
|
form.value,
|
|
|
form.options,
|
|
|
@@ -187,7 +207,9 @@ fun FormBox(forms: List<FormField>, onValueChange: (FormField) -> Unit, modifier
|
|
|
onValueChange(form)
|
|
|
},
|
|
|
required = form.required,
|
|
|
- enable = form.enabled && enabled
|
|
|
+ enable = form.enabled && enabled,
|
|
|
+ maxCount = form.maxCount ?: 1,
|
|
|
+ uploadType = form.uploadType ?: "file"
|
|
|
)
|
|
|
}
|
|
|
}
|
|
|
@@ -414,7 +436,7 @@ fun FormSelect(
|
|
|
Text(
|
|
|
values.data2String()
|
|
|
.ifEmpty { (placeholder.getOrNull(0) ?: "").ifEmpty { "请选择$label" } },
|
|
|
- color = Text.copy(alpha = if (enable) 1f else 0.6f),
|
|
|
+ color = Text.copy(alpha = if (enable && values.data2String().isNotEmpty()) 1f else 0.6f),
|
|
|
fontSize = 14.sp,
|
|
|
lineHeight = 18.sp,
|
|
|
modifier = Modifier.weight(1f),
|
|
|
@@ -628,38 +650,55 @@ fun FormCheckbox(
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 照片上传组件
|
|
|
+ * 文件上传组件
|
|
|
*
|
|
|
* @param label 标题
|
|
|
* @param value 当前选中
|
|
|
* @param options 可选列表
|
|
|
* @param onSelectChange 当前选中
|
|
|
+ * @param maxCount 上传最大限制
|
|
|
+ * @param uploadType 上传类型
|
|
|
*/
|
|
|
@OptIn(ExperimentalLayoutApi::class)
|
|
|
@Composable
|
|
|
-fun FormImage(
|
|
|
+fun FormUpload(
|
|
|
label: String,
|
|
|
value: List<String>,
|
|
|
options: List<FormOption>,
|
|
|
onSelectChange: (List<String>) -> Unit,
|
|
|
required: Boolean = false,
|
|
|
enable: Boolean = true,
|
|
|
+ maxCount: Int = 1,
|
|
|
+ uploadType: String = "file",
|
|
|
+ vm: VMFormUploadFile = viewModel()
|
|
|
) {
|
|
|
val ctx = LocalContext.current
|
|
|
+ val state by vm.state.collectAsState()
|
|
|
+ val type = if (uploadType == "image") "image" else "*"
|
|
|
+ LaunchedEffect(Unit) {
|
|
|
+ if (ctx is PageBase) {
|
|
|
+ ctx.apply {
|
|
|
+ vm.toast.initToast()
|
|
|
+ vm.loading.initLoading()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
// 启动页面
|
|
|
val activityLauncher = rememberLauncherForActivityResult(contract = ActivityResultContracts.GetContent()) { uri ->
|
|
|
-
|
|
|
+ if (uri != null) {
|
|
|
+ val file = ctx.uriToFile(uri)
|
|
|
+ val fileType = ctx.contentResolver.getType(uri) ?: ""
|
|
|
+ vm.uploadFile(UploadFile(file = file, type = fileType)) { onSelectChange(it) }
|
|
|
+ }
|
|
|
}
|
|
|
// 权限请求
|
|
|
val permissionLauncher = rememberLauncherForActivityResult(contract = ActivityResultContracts.RequestPermission()) {
|
|
|
if (it) {
|
|
|
- activityLauncher.launch("image/*")
|
|
|
+ activityLauncher.launch("${type}/*")
|
|
|
}
|
|
|
}
|
|
|
- val values = remember { mutableStateListOf<String>() }
|
|
|
LaunchedEffect(Unit) {
|
|
|
- values.clear()
|
|
|
- values.addAll(value)
|
|
|
+ vm.init(value)
|
|
|
}
|
|
|
Column(
|
|
|
Modifier
|
|
|
@@ -684,58 +723,151 @@ fun FormImage(
|
|
|
modifier = Modifier.padding(start = 3.dp)
|
|
|
)
|
|
|
}
|
|
|
- FlowRow(
|
|
|
- Modifier
|
|
|
+
|
|
|
+ Column(
|
|
|
+ modifier = Modifier
|
|
|
.fillMaxWidth()
|
|
|
.heightIn(max = 120.dp)
|
|
|
+ .border(1.dp, shape = RoundedCornerShape(6.dp), color = Color(0xFFE5E6EB))
|
|
|
+ .padding(vertical = 10.dp)
|
|
|
+ .verticalScroll(rememberScrollState()),
|
|
|
+ verticalArrangement = Arrangement.Top,
|
|
|
+ horizontalAlignment = Alignment.CenterHorizontally
|
|
|
) {
|
|
|
- if (options.isEmpty()) {
|
|
|
- // 没有默认数据时,显示点击上传
|
|
|
- Column(
|
|
|
+ if (state.files.isEmpty()) Column(
|
|
|
+ horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier
|
|
|
+ .fillMaxSize()
|
|
|
+ .padding(horizontal = 10.dp)
|
|
|
+ .clip(RoundedCornerShape(6.dp))
|
|
|
+ .clickable {
|
|
|
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
|
|
+ if (ctx.checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
|
|
+ permissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
|
|
|
+ } else {
|
|
|
+ activityLauncher.launch("${type}/*")
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ activityLauncher.launch("${type}/*")
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .padding(vertical = 10.dp)) {
|
|
|
+ Icon(
|
|
|
+ painter = painterResource(R.drawable.upload),
|
|
|
+ contentDescription = null,
|
|
|
+ modifier = Modifier
|
|
|
+ .size(50.dp),
|
|
|
+ tint = Text.copy(alpha = if (enable) 0.3f else 0.1f)
|
|
|
+ )
|
|
|
+ Text(if (enable) "点击上传" else "暂无上传内容", fontSize = 14.sp, color = Text.copy(alpha = if (enable) 0.3f else 0.1f))
|
|
|
+ }
|
|
|
+ FlowRow(
|
|
|
+ modifier = Modifier
|
|
|
+ .padding(horizontal = 10.dp)
|
|
|
+ .fillMaxWidth()
|
|
|
+ ) {
|
|
|
+ // 显示添加的文件列表
|
|
|
+ state.files.forEach { item ->
|
|
|
+ Box(
|
|
|
+ modifier = Modifier
|
|
|
+ .fillMaxWidth(1 / 4f)
|
|
|
+ .aspectRatio(1f)
|
|
|
+ .padding(5.dp),
|
|
|
+ contentAlignment = Alignment.Center
|
|
|
+ ) {
|
|
|
+ val src = SystemUtil.getFileTypeIconBySuffix(item.url.ifEmpty { item.file?.name ?: "" })
|
|
|
+ AsyncImage(
|
|
|
+ model = item.url.ifEmpty { item.file },
|
|
|
+ placeholder = rememberAsyncImagePainter(model = item.file),
|
|
|
+ contentDescription = null,
|
|
|
+ modifier = Modifier
|
|
|
+ .fillMaxSize()
|
|
|
+ .clip(RoundedCornerShape(10))
|
|
|
+ .alpha(if (enable) 1f else 0.5f),
|
|
|
+ contentScale = ContentScale.Crop
|
|
|
+ )
|
|
|
+ if (!listOf("jpg", "png").contains(src.second)) {
|
|
|
+ Box(
|
|
|
+ Modifier
|
|
|
+ .fillMaxSize()
|
|
|
+ .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.3f), shape = RoundedCornerShape(10.dp))
|
|
|
+ .alpha(if (enable) 1f else 0.5f)
|
|
|
+ ) {
|
|
|
+ Spacer(
|
|
|
+ modifier = Modifier
|
|
|
+ .padding(20.dp)
|
|
|
+ .fillMaxSize()
|
|
|
+ .background(Color.White)
|
|
|
+ )
|
|
|
+ Icon(
|
|
|
+ painter = painterResource(src.first),
|
|
|
+ contentDescription = null,
|
|
|
+ modifier = Modifier
|
|
|
+ .padding(15.dp)
|
|
|
+ .fillMaxSize(),
|
|
|
+ tint = MaterialTheme.colorScheme.primary
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 右上角删除操作
|
|
|
+ if (enable) Icon(
|
|
|
+ painterResource(R.drawable.delete_all),
|
|
|
+ contentDescription = null,
|
|
|
+ modifier = Modifier
|
|
|
+ .align(Alignment.TopEnd)
|
|
|
+ .offset(x = 6.dp, y = (-3).dp)
|
|
|
+ .size(20.dp)
|
|
|
+ .background(Color.Black.copy(alpha = 0.6f), shape = RoundedCornerShape(6.dp))
|
|
|
+ .clickable { vm.delete(item) }
|
|
|
+ .padding(3.dp),
|
|
|
+ tint = MaterialTheme.colorScheme.primary
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 上传按钮
|
|
|
+ if (state.files.isNotEmpty() && enable) Box(
|
|
|
modifier = Modifier
|
|
|
- .fillMaxWidth()
|
|
|
- .height(120.dp)
|
|
|
- .border(1.dp, shape = RoundedCornerShape(6.dp), color = Color(0xFFE5E6EB))
|
|
|
- .clip(RoundedCornerShape(6.dp))
|
|
|
+ .fillMaxWidth(1 / 4f)
|
|
|
+ .aspectRatio(1f)
|
|
|
+ .padding(5.dp)
|
|
|
+ .clip(RoundedCornerShape(10.dp))
|
|
|
.clickable {
|
|
|
+ if (!vm.checkCanSelect(maxCount)) return@clickable
|
|
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
|
|
if (ctx.checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
|
|
permissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
|
|
|
} else {
|
|
|
- activityLauncher.launch("image/*")
|
|
|
+ activityLauncher.launch("${type}/*")
|
|
|
}
|
|
|
} else {
|
|
|
- activityLauncher.launch("image/*")
|
|
|
+ activityLauncher.launch("${type}/*")
|
|
|
}
|
|
|
},
|
|
|
- verticalArrangement = Arrangement.Center,
|
|
|
- horizontalAlignment = Alignment.CenterHorizontally
|
|
|
+ contentAlignment = Alignment.Center
|
|
|
) {
|
|
|
Icon(
|
|
|
painter = painterResource(R.drawable.upload),
|
|
|
contentDescription = null,
|
|
|
modifier = Modifier
|
|
|
- .padding(bottom = 5.dp)
|
|
|
- .size(50.dp),
|
|
|
- tint = Text.copy(alpha = 0.3f)
|
|
|
- )
|
|
|
- Text("点击上传", fontSize = 14.sp, color = Text.copy(alpha = 0.3f))
|
|
|
- }
|
|
|
- }
|
|
|
- options.forEach { item ->
|
|
|
- Row(
|
|
|
- modifier = Modifier
|
|
|
- .clip(RoundedCornerShape(12.dp))
|
|
|
- .clickable(onClick = {
|
|
|
- if (values.contains(item.value))
|
|
|
- values.remove(item.value)
|
|
|
- else values.add(item.value)
|
|
|
- onSelectChange(values)
|
|
|
- }, enabled = enable)
|
|
|
- .padding(horizontal = 10.dp, vertical = 5.dp),
|
|
|
- verticalAlignment = Alignment.CenterVertically
|
|
|
- ) {
|
|
|
+ .padding(2.dp)
|
|
|
+ .fillMaxSize()
|
|
|
+ .drawBehind {
|
|
|
+ val strokeWidth = 2.dp.toPx()
|
|
|
+ val dashWidth = 6.dp.toPx()
|
|
|
+ val dashGap = 4.dp.toPx()
|
|
|
+ val radius = 10.dp.toPx()
|
|
|
|
|
|
+ drawRoundRect(
|
|
|
+ color = Text.copy(alpha = 0.5f),
|
|
|
+ cornerRadius = CornerRadius(radius),
|
|
|
+ style = Stroke(
|
|
|
+ width = strokeWidth,
|
|
|
+ pathEffect = PathEffect.dashPathEffect(floatArrayOf(dashWidth, dashGap))
|
|
|
+ )
|
|
|
+ )
|
|
|
+ }
|
|
|
+ .padding(12.dp),
|
|
|
+ tint = Text.copy(alpha = 0.5f)
|
|
|
+ )
|
|
|
}
|
|
|
}
|
|
|
}
|