diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 0000000..5e2878b
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,108 @@
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.kotlin.compose)
+ id("com.google.devtools.ksp")
+ id("com.google.dagger.hilt.android")
+ id("kotlin-parcelize")
+}
+
+android {
+ namespace = "com.sw.inbound"
+ compileSdk = 35
+
+ defaultConfig {
+ applicationId = "com.sw.inbound"
+ minSdk = 24
+ targetSdk = 35
+ versionCode = 1
+ versionName = "1.0"
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+
+ ndk {
+ abiFilters.addAll(listOf("armeabi-v7a"/*, "arm64-v8a"*/))
+ }
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+ kotlinOptions {
+ jvmTarget = "11"
+ }
+ buildFeatures {
+ compose = true
+ }
+ sourceSets {
+ named("main") {
+ jniLibs.srcDirs("libs")
+ }
+ }
+}
+
+dependencies {
+
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation(libs.androidx.activity.compose)
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.androidx.ui)
+ implementation(libs.androidx.ui.graphics)
+ implementation(libs.androidx.ui.tooling.preview)
+ implementation(libs.androidx.material3)
+ implementation(
+ fileTree(
+ mapOf(
+ "dir" to "libs",
+ "include" to listOf("*.aar", "*.jar")
+ )
+ )
+ )
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+ androidTestImplementation(platform(libs.androidx.compose.bom))
+ androidTestImplementation(libs.androidx.ui.test.junit4)
+ debugImplementation(libs.androidx.ui.tooling)
+ debugImplementation(libs.androidx.ui.test.manifest)
+ implementation(libs.androidx.navigation.compose)
+
+ // camerax
+ implementation(libs.androidx.camera.core)
+ implementation(libs.androidx.camera.camera2)
+ implementation(libs.androidx.camera.lifecycle)
+ implementation(libs.androidx.camera.view)
+ implementation(libs.androidx.camera.extensions)
+
+ // 权限申请
+ implementation(libs.accompanist.permissions)
+
+ // hilt注入
+ implementation(libs.hilt.android)
+ ksp(libs.hilt.android.compiler)
+ implementation(libs.androidx.hilt.navigation.compose)
+
+ // retrofit网络请求
+ implementation(libs.retrofit)
+ implementation(libs.converter.gson)
+ // okhttp
+ implementation(libs.okhttp)
+ implementation(libs.logging.interceptor)
+ // gson
+ implementation(libs.gson)
+ // 日志打印
+ implementation(libs.timber)
+ // 图片显示
+ implementation("io.coil-kt:coil-compose:2.4.0")
+}
\ No newline at end of file
diff --git a/app/libs/WbIntelligentHardwareSDK-debug.aar b/app/libs/WbIntelligentHardwareSDK-debug.aar
new file mode 100644
index 0000000..c25490d
Binary files /dev/null and b/app/libs/WbIntelligentHardwareSDK-debug.aar differ
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/sw/inbound/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/sw/inbound/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..293ef91
--- /dev/null
+++ b/app/src/androidTest/java/com/sw/inbound/ExampleInstrumentedTest.kt
@@ -0,0 +1,22 @@
+package com.sw.inbound
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.sw.inbound", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..62ec878
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/GlobalData.kt b/app/src/main/java/com/sw/inbound/GlobalData.kt
new file mode 100644
index 0000000..99641f1
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/GlobalData.kt
@@ -0,0 +1,51 @@
+package com.sw.inbound
+
+import android.net.Uri
+import com.sw.inbound.model.response.DictType
+import com.sw.inbound.model.response.User
+
+object GlobalData {
+ /**
+ * 登录后的用户信息
+ */
+ var user: User? = null
+
+ /**
+ * 采集图片uri
+ */
+ var imageUri: Uri? = null
+
+ var storageTypeList: List = arrayListOf()
+ var goodsTypeList: List = arrayListOf()
+ var warehouseTypeList: List = arrayListOf()
+ var supplierTypeList: List = arrayListOf()
+ var unitTypeList: List = arrayListOf()
+
+ fun getStorageType(id: Int): String {
+ return storageTypeList.find { it.id == id }?.value ?: "默认"
+ }
+
+ fun getGoodsType(id: Int): String {
+ return goodsTypeList.find { it.id == id }?.value ?: "-"
+ }
+
+ fun getWarehouseType(id: Int): String {
+ return warehouseTypeList.find { it.id == id }?.value ?: "-"
+ }
+
+ fun getSupplierType(id: Int): String {
+ return supplierTypeList.find { it.id == id }?.value ?: "-"
+ }
+
+ fun getUnitType(id: Int): String {
+ return unitTypeList.find { it.id == id }?.value ?: "-"
+ }
+}
+
+/**
+ *
+ */
+object GlobalKey {
+ const val KEY_TOKEN = "tokenKey"
+ const val KEY_USER_INFO = "userInfoKey"
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/MainActivity.kt b/app/src/main/java/com/sw/inbound/MainActivity.kt
new file mode 100644
index 0000000..38b9d36
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/MainActivity.kt
@@ -0,0 +1,30 @@
+package com.sw.inbound
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import com.sw.inbound.sdk.SensorScaleUtils
+import com.sw.inbound.ui.AppScreen
+import dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ setContent {
+ AppScreen(onBackRequest = {
+ finish()
+ })
+ }
+ }
+
+ override fun onDestroy() {
+ SensorScaleUtils.closeScale()
+ super.onDestroy()
+ }
+}
+
+
+
diff --git a/app/src/main/java/com/sw/inbound/MyApp.kt b/app/src/main/java/com/sw/inbound/MyApp.kt
new file mode 100644
index 0000000..f84a83d
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/MyApp.kt
@@ -0,0 +1,22 @@
+package com.sw.inbound
+
+import android.app.Application
+import com.sw.inbound.utils.ContextUtils
+import dagger.hilt.android.HiltAndroidApp
+import timber.log.Timber
+
+@HiltAndroidApp
+class MyApp : Application() {
+ companion object {
+ const val DEBUG: Boolean = true
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ Timber.plant(Timber.DebugTree())
+ ContextUtils.initAppContext(this)
+
+ // 初始化崩溃处理器
+// CrashHandler.init(this)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/di/NetworkModule.kt b/app/src/main/java/com/sw/inbound/di/NetworkModule.kt
new file mode 100644
index 0000000..8b50ad3
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/di/NetworkModule.kt
@@ -0,0 +1,65 @@
+package com.sw.inbound.di
+
+import com.sw.inbound.MyApp
+import com.sw.inbound.network.api.ApiService
+import com.sw.inbound.network.interceptor.RequestInterceptor
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import okhttp3.OkHttpClient
+import okhttp3.logging.HttpLoggingInterceptor
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+import timber.log.Timber
+import java.util.concurrent.TimeUnit
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+object NetworkModule {
+
+ // private const val BASE_URL = "https://127.0.0.1"
+ private const val BASE_URL = "https://vip.shuziweidao.com"
+
+ // private const val BASE_URL = "http://192.168.1.8:9092"
+ private const val TIME_OUT = 30L // 超时时间(秒)
+
+ @Provides
+ @Singleton
+ fun provideHttpClient(): OkHttpClient {
+ return OkHttpClient.Builder()
+ .connectTimeout(TIME_OUT, TimeUnit.SECONDS)
+ .readTimeout(TIME_OUT, TimeUnit.SECONDS)
+ .writeTimeout(TIME_OUT, TimeUnit.SECONDS)
+ .addNetworkInterceptor(HttpLoggingInterceptor(logger = {
+ Timber.d("okhttp logger ==>${it}")
+ }).apply {
+ level = if (MyApp.DEBUG) {
+ HttpLoggingInterceptor.Level.BODY
+ } else {
+ HttpLoggingInterceptor.Level.NONE
+ }
+ })
+ .addInterceptor(RequestInterceptor())
+// .addInterceptor(ErrorInterceptor())
+ .build()
+ }
+
+ @Provides
+ @Singleton
+ fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
+ return Retrofit.Builder()
+ .baseUrl(BASE_URL)
+ .client(okHttpClient)
+ .addConverterFactory(GsonConverterFactory.create())
+ .build()
+ }
+
+ @Provides
+ @Singleton
+ fun provideApiService(retrofit: Retrofit): ApiService {
+ return retrofit.create(ApiService::class.java)
+ }
+}
+
diff --git a/app/src/main/java/com/sw/inbound/ext/BorderExt.kt b/app/src/main/java/com/sw/inbound/ext/BorderExt.kt
new file mode 100644
index 0000000..c92c76c
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/ext/BorderExt.kt
@@ -0,0 +1,44 @@
+package com.sw.inbound.ext
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.draw.drawWithCache
+import androidx.compose.ui.geometry.CornerRadius
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.PathEffect
+import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+
+/**
+ * 虚线边框
+ */
+fun Modifier.dashedBorder(
+ strokeWidth: Dp = 2.dp,
+ color: Color = Color(0xFFDCDCF0),
+ cornerRadiusDp: Dp = 10.dp
+) = composed(
+ factory = {
+ val density = LocalDensity.current
+ val strokeWidthPx = density.run { strokeWidth.toPx() }
+ val cornerRadiusPx = density.run { cornerRadiusDp.toPx() }
+
+ this.then(
+ Modifier.drawWithCache {
+ onDrawBehind {
+ val stroke = Stroke(
+ width = strokeWidthPx,
+ pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f)
+ )
+
+ drawRoundRect(
+ color = color,
+ style = stroke,
+ cornerRadius = CornerRadius(cornerRadiusPx)
+ )
+ }
+ }
+ )
+ }
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/ext/CommonExt.kt b/app/src/main/java/com/sw/inbound/ext/CommonExt.kt
new file mode 100644
index 0000000..d37547f
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/ext/CommonExt.kt
@@ -0,0 +1,64 @@
+package com.sw.inbound.ext
+
+import java.math.BigDecimal
+import java.math.RoundingMode
+import kotlin.math.pow
+
+fun Float.toFormattedString(decimalPlaces: Int = 2): String {
+ return when (this) {
+ 0f -> ""
+ else -> "%.${decimalPlaces}f".format(this)
+ }
+}
+
+fun Double.toFormattedString(decimalPlaces: Int = 2): String {
+ return when (this) {
+ 0.0 -> ""
+ else -> "%.${decimalPlaces}f".format(this)
+ }
+}
+
+fun Double.toSafeBigDecimal(): BigDecimal {
+ return when (this) {
+ 0.0 -> BigDecimal(0)
+ else -> toBigDecimal()
+ }
+}
+
+/**
+ * 保留指定小数位数
+ */
+private fun Double.roundToDouble(decimalPlaces: Int): Double {
+ val factor = 10.0.pow(decimalPlaces)
+ return kotlin.math.round(this * factor) / factor
+}
+
+fun Double?.toSafeDouble(unitName: String?): Double {
+ if (this == null || this == 0.0) return 0.0
+
+ val decimalPlaces = when (unitName) {
+ "斤", "克", "公斤", "升" -> 2
+ else -> 0
+ }
+
+ return roundToDouble(decimalPlaces)
+}
+
+fun Double?.toSafeFloat(unitName: String?): Float {
+ if (this == null || this == 0.0) return 0f
+
+ val decimalPlaces = when (unitName) {
+ "斤", "克", "公斤", "升" -> 2
+ else -> 0
+ }
+
+ return roundToDouble(decimalPlaces).toFloat()
+}
+
+fun BigDecimal.toFormattedString(): String {
+ return this.setScale(2, RoundingMode.HALF_UP).toString()
+}
+
+fun Int.toFormattedString(): String {
+ return this.toString()
+}
diff --git a/app/src/main/java/com/sw/inbound/ext/StringExt.kt b/app/src/main/java/com/sw/inbound/ext/StringExt.kt
new file mode 100644
index 0000000..9cb38b2
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/ext/StringExt.kt
@@ -0,0 +1,89 @@
+package com.sw.inbound.ext
+
+import java.math.BigDecimal
+import java.math.RoundingMode
+
+fun String.isValidAmount(): Boolean {
+ // 空字符串允许(用于删除所有字符)
+ if (this.isEmpty()) return true
+
+ // 检查是否只包含数字和小数点
+ if (!matches(Regex("^\\d*\\.?\\d*$"))) return false
+
+ // 检查小数点数量(最多一个)
+ if (count { it == '.' } > 1) return false
+
+ // 检查小数点后位数(最多2位)
+ if (contains('.')) {
+ val decimalPart = substringAfter('.')
+ if (decimalPart.length > 2) return false
+ }
+
+ // 检查不以小数点开头
+ if (startsWith('.')) return false
+
+ return true
+}
+
+fun String.isNumeric(): Boolean {
+ return this.matches("-?\\d+(\\.\\d+)?".toRegex())
+}
+
+fun String.toFormattedString(decimalPlaces: Int = 2): String {
+ return when (this) {
+ else -> "%.${decimalPlaces}f".format(this)
+ }
+}
+
+fun String.toSafeBigDecimal(
+ scale: Int = 2,
+ roundingMode: RoundingMode = RoundingMode.HALF_UP
+): BigDecimal {
+ return when {
+ this.isBlank() -> BigDecimal.ZERO.setScale(scale, roundingMode)
+ this == "." -> BigDecimal.ZERO.setScale(scale, roundingMode)
+ else -> try {
+ BigDecimal(this.trim())
+ .setScale(scale, roundingMode)
+ } catch (e: Exception) {
+ BigDecimal.ZERO.setScale(scale, roundingMode)
+ }
+ }
+}
+
+fun String.isValidNumber(): Boolean {
+ return matches(Regex("^\\d*$"))
+}
+
+fun String.toSafeInt(): Int {
+ return if (this.isEmpty()) {
+ 0
+ } else {
+ this.toInt()
+ }
+}
+
+fun String.toSafeDouble(): Double {
+ return if (this.isEmpty()) {
+ 0.0
+ } else {
+ this.toDouble()
+ }
+}
+
+fun String.isValidFloat(): Boolean {
+ if (startsWith(".")) return false
+ val decimalRegex = Regex("^\\d*\\.?\\d{0,2}$")
+ return this.matches(decimalRegex)
+// return input.matches(Regex("-?\\d+(\\.\\d+)?"))
+}
+
+fun String.toSafeFloat(maxDecimalDigits: Int = 2): Float {
+ return if (this.isEmpty()) {
+ 0f
+ } else if (isValidFloat()) {
+ toFloat()
+ } else {
+ 0f
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/ext/TextExt.kt b/app/src/main/java/com/sw/inbound/ext/TextExt.kt
new file mode 100644
index 0000000..521c3f0
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/ext/TextExt.kt
@@ -0,0 +1,33 @@
+package com.sw.inbound.ext
+
+import androidx.annotation.ColorRes
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.TextUnit
+import androidx.compose.ui.unit.sp
+
+// 快速修改颜色
+fun TextStyle.withColor(color: Color): TextStyle = this.copy(color = color)
+
+@Composable
+fun TextStyle.withColorRes(@ColorRes colorRes: Int): TextStyle {
+ return this.copy(color = colorResource(id = colorRes))
+}
+
+// 快速修改字体大小
+fun TextStyle.withSize(size: TextUnit): TextStyle = this.copy(fontSize = size)
+
+fun TextStyle.withSizeSp(sp: Float): TextStyle = this.copy(fontSize = sp.sp)
+
+// 快速修改字体粗细
+fun TextStyle.withWeight(weight: FontWeight): TextStyle = this.copy(fontWeight = weight)
+
+fun TextStyle.bold(): TextStyle = this.copy(fontWeight = FontWeight.Bold)
+fun TextStyle.medium(): TextStyle = this.copy(fontWeight = FontWeight.Medium)
+fun TextStyle.light(): TextStyle = this.copy(fontWeight = FontWeight.Light)
+
+fun TextStyle.textAlign(textAlign: TextAlign): TextStyle = this.copy(textAlign = textAlign)
diff --git a/app/src/main/java/com/sw/inbound/model/exception/ApiException.kt b/app/src/main/java/com/sw/inbound/model/exception/ApiException.kt
new file mode 100644
index 0000000..5658425
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/model/exception/ApiException.kt
@@ -0,0 +1,4 @@
+package com.sw.inbound.model.exception
+
+class ApiException {
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/model/exception/NetException.kt b/app/src/main/java/com/sw/inbound/model/exception/NetException.kt
new file mode 100644
index 0000000..d166d52
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/model/exception/NetException.kt
@@ -0,0 +1,4 @@
+package com.sw.inbound.model.exception
+
+
+class NetException(val code: Int, override val message: String) : Exception(message)
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/model/request/GoodsAddParam.kt b/app/src/main/java/com/sw/inbound/model/request/GoodsAddParam.kt
new file mode 100644
index 0000000..7f34fbc
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/model/request/GoodsAddParam.kt
@@ -0,0 +1,143 @@
+package com.sw.inbound.model.request
+
+import com.sw.inbound.GlobalData
+import com.sw.inbound.ext.toFormattedString
+import java.math.BigDecimal
+
+/**
+ * 添加物品参数
+ */
+data class GoodsAddParam(
+ /**
+ * 物品名称
+ */
+ var goodName: String? = null,
+
+ /**
+ * 物品编号 服务自动生成
+ */
+ var goodCode: String? = null,
+
+ /**
+ * 物品类型
+ */
+ var goodType: Int? = null,
+
+ /**
+ * 存储方式
+ */
+ var storageType: Int? = null,
+
+ /**
+ * 净材率
+ */
+ var netRate: Float? = null,
+
+ /**
+ * 库存单位id
+ */
+ var unitId: Int? = null,
+
+ /**
+ * 采购单位
+ */
+ var purchaseUnit: Int = 0,
+
+ /**
+ * 单位转换值
+ */
+ var purchaseValue: Float = 0F,
+
+ /**
+ * 采购单价
+ */
+ var purchasePrice: BigDecimal? = null,
+
+ /**
+ * 图片url
+ */
+ var relativeUrl: String? = null
+) {
+
+ val goodNameSr: String
+ get() {
+ if (goodName == null)
+ return ""
+ return goodName!!
+ }
+
+
+ var goodsTypeStr: String
+ set(value) {
+ goodsTypeStr = value
+ }
+ get() {
+ return GlobalData.goodsTypeList.find { it.id == goodType }?.value ?: ""
+ }
+
+ var storageTypeStr: String
+ set(value) {
+ storageTypeStr = value
+ }
+ get() {
+ return GlobalData.storageTypeList.find { it.id == storageType }?.value ?: ""
+ }
+
+ val netRateStr: String
+ get() {
+ if (netRate == null || netRate == 0f) {
+ return ""
+ } else {
+ return netRate!!.toFormattedString()
+ }
+ }
+
+ val unitIdStr: String
+ get() {
+ return GlobalData.unitTypeList.find { it.id == unitId }?.value ?: ""
+ }
+
+ val purchasePriceStr: String
+ get() {
+ if (purchasePrice == null) {
+ return ""
+ }
+ return purchasePrice!!.toFormattedString()
+ }
+
+
+ val purchaseUnitStr: String
+ get() {
+ if (purchaseUnit == 0) {
+ return ""
+ }
+ return GlobalData.unitTypeList.find { it.id == purchaseUnit }?.value ?: ""
+ }
+
+ fun hasNullField(): String? {
+ if (goodName == null || goodName!!.isEmpty())
+ return "请录入物品名称"
+ if (goodType == null || goodType == 0) {
+ return "请选择物品类型"
+ }
+ if (storageType == null || storageType == 0) {
+ return "请选择存储方式"
+ }
+ if (netRate == null || netRate == 0f) {
+ return "请录入净材率"
+ }
+ if (unitId == null || unitId == 0) {
+ return "请选择库存单位"
+ }
+ if (purchaseUnit == null || purchaseUnit == 0) {
+ return "请选择采购单位"
+ }
+ if (purchaseValue == null || purchaseValue == 0f) {
+ return "请输入采购单位对应的库存数量"
+ }
+ if (purchasePrice == null || purchasePrice == BigDecimal(0)) {
+ return "请输入单价"
+ }
+ return null
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/model/request/LoginParam.kt b/app/src/main/java/com/sw/inbound/model/request/LoginParam.kt
new file mode 100644
index 0000000..487af89
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/model/request/LoginParam.kt
@@ -0,0 +1,12 @@
+package com.sw.inbound.model.request
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data class LoginParam(
+ val userName: String,
+ val password: String
+) : Parcelable
+
+
diff --git a/app/src/main/java/com/sw/inbound/model/request/PurchaseWarehouseParam.kt b/app/src/main/java/com/sw/inbound/model/request/PurchaseWarehouseParam.kt
new file mode 100644
index 0000000..1fab293
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/model/request/PurchaseWarehouseParam.kt
@@ -0,0 +1,164 @@
+package com.sw.inbound.model.request
+
+import android.os.Parcelable
+import com.sw.inbound.GlobalData
+import com.sw.inbound.ext.toFormattedString
+import com.sw.inbound.model.response.DictType
+import com.sw.inbound.model.response.SearchGoodsInfo.Record.UnitVo
+import kotlinx.parcelize.Parcelize
+import java.math.BigDecimal
+
+/**
+ * 自采购入库参数
+ */
+@Parcelize
+data class PurchaseWarehouseParam(
+ /**
+ * 物品id
+ */
+ var goodsId: Int = 0,
+ /**
+ * 库存单位
+ */
+ var kcUnitId: Int = -1,
+ /**
+ * 入库数量
+ */
+ var goodsCount: Double = 0.0,
+ /**
+ * 入库单价
+ */
+ var goodsUnitPrice: Double = 0.0,
+ /**
+ * 入库金额
+ */
+ var goodsPrice: Double = 0.0,
+ /**
+ * (物品列表里 businessUnitId字段的值)
+ */
+ var goodPurId: Int = -1,
+ /**
+ * 仓库id
+ */
+ var warehouseId: Int = -1,
+
+ /**
+ * 采购库存转换关系
+ */
+ var buyToInventoryValue: String = "",
+
+ // 仓库名
+// var warehouseName: String? = null,
+
+ // 重量 收货入参
+ var goodsWeight: BigDecimal? = null,
+ // 物品名称
+ var goodsName: String? = null,
+ // 单位名称
+ var unitName: String? = null,
+ // 搜索的物品单位列表
+ var unitList: List? = null,
+ // 选中的采购单位
+ var selectUnitType: UnitVo? = null
+) : Parcelable {
+
+ val warehouseNameStr: String
+ get() {
+ if (warehouseId == -1) {
+ return ""
+ }
+ return GlobalData.warehouseTypeList.find { it.id == warehouseId }?.value ?: ""
+// if (warehouseName == null) {
+// return ""
+// }
+// return warehouseName!!
+ }
+
+ val goodsWeightStr: String
+ get() {
+ if (goodsWeight == null) return ""
+
+ // 小于10 kg 显示 g
+ if (goodsWeight!!.toInt() < 10) {
+ return goodsWeight!!.multiply(BigDecimal(1000)).toString()
+ }
+ return goodsWeight?.toFormattedString() ?: ""
+ }
+
+ val goodsWeightUnitStr: String
+ get() {
+ return if (goodsWeight == null || goodsWeight!!.toInt() < 10) {
+ "克"
+ } else {
+ "千克"
+ }
+ }
+
+ val goodsNameStr: String
+ get() {
+ if (goodsName == null) return "-"
+ return goodsName!!
+ }
+
+ val unitDictTypeList: List
+ get() {
+ return if (unitList == null) emptyList()
+ else {
+ unitList!!.map {
+ DictType(it.businessUnitId!!.toInt(), it.purchaseUnitName!!)
+ }
+ }
+ }
+
+
+ val unitNameStr: String
+ get() {
+ if (unitName == null) return ""
+ return unitName!!
+ }
+
+ val goodsCountStr: String
+ get() {
+ if (goodsCount == null || goodsCount == 0.0) {
+ return ""
+ }
+ return goodsCount.toFormattedString()
+ }
+
+ val goodsUnitPriceStr: String
+ get() {
+ if (goodsUnitPrice == null || goodsUnitPrice == 0.0) {
+ return ""
+ }
+ return goodsUnitPrice.toFormattedString()
+ }
+
+ val goodsPriceStr: String
+ get() {
+ if (goodsPrice == null || goodsPrice == 0.0) {
+ return ""
+ }
+ return goodsPrice.toFormattedString()
+ }
+
+ fun hasNullField(): String? {
+ if (goodsId == null || goodsId == 0 || goodsName == null || goodsName!!.isEmpty())
+ return "请选择物品"
+ if (warehouseId == null || warehouseId == -1) {
+ return "请选择仓库"
+ }
+ if (goodPurId == null || goodPurId == -1) {
+ return "请选择采购单位"
+ }
+ if (goodsCount == null || goodsCount == 0.0) {
+ return "请输入采购数量"
+ }
+ if (goodsUnitPrice == null || goodsUnitPrice == 0.0) {
+ return "请输入采购单价"
+ }
+ if (goodsPrice == null || goodsPrice == 0.0) {
+ return "请输入采购金额"
+ }
+ return null
+ }
+}
diff --git a/app/src/main/java/com/sw/inbound/model/request/UploadInfo.kt b/app/src/main/java/com/sw/inbound/model/request/UploadInfo.kt
new file mode 100644
index 0000000..c8efbf6
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/model/request/UploadInfo.kt
@@ -0,0 +1,90 @@
+package com.sw.inbound.model.request
+
+import android.os.Parcelable
+import com.sw.inbound.model.response.BaseBean
+import com.sw.inbound.model.response.GoodsInfo
+import kotlinx.parcelize.Parcelize
+import java.math.BigDecimal
+
+/**
+ * 确认收货结构
+ */
+@Parcelize
+data class UploadInfo(
+ /**
+ * 收货单id
+ */
+ var id: Int,
+ /**
+ * 供应商id
+ */
+ var supplierId: Int? = null,
+ /**
+ * 物品信息
+ */
+ var receiveGoodsInfos: List
+) : Parcelable
+
+/**
+ * 确认收货物品信息
+ */
+@Parcelize
+private data class UploadGoodsInfo(
+ /**
+ * 物品Id
+ */
+ var goodId: Int = 0,
+ /**
+ * 物品采购单位关系
+ */
+ var goodsPurId: Int = 0,
+ /**
+ * 数量
+ */
+ var receiveCount: Float = 0F,
+
+ /**
+ * 单价
+ */
+ var recUnitPriceTaxIn: Float = 0f,
+ /**
+ * 金额
+ */
+ var recPriceExItem: Float = 0f,
+
+ /**
+ * 仓库id
+ */
+ var warehouseId: Int = 0,
+
+
+ /**
+ * 物品名称
+ */
+ var goodName: String = "-",
+
+
+ /**
+ * 单位名称
+ */
+ var unitName: String = "-",
+
+ /**
+ * 采购单位id
+ */
+ var purchaseUnitId: Int = 0,
+
+ /**
+ * 收货数量
+ */
+ var receivedNum: Float = 0f,
+ /**
+ * 物品重量
+ */
+ var goodsWeight: BigDecimal = BigDecimal(0),
+
+ // 已调整
+ var isAdjusted: Boolean = false
+) : Parcelable, BaseBean() {
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/model/response/ApiResponse.kt b/app/src/main/java/com/sw/inbound/model/response/ApiResponse.kt
new file mode 100644
index 0000000..f3ee4d2
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/model/response/ApiResponse.kt
@@ -0,0 +1,10 @@
+package com.sw.inbound.model.response
+
+data class ApiResponse(
+ val code: Int,
+ val success: Boolean? = false,
+ val msg: String? = "",
+ val data: T? = null
+) {
+ fun isSuccess(): Boolean = code == 200
+}
diff --git a/app/src/main/java/com/sw/inbound/model/response/BaseBean.kt b/app/src/main/java/com/sw/inbound/model/response/BaseBean.kt
new file mode 100644
index 0000000..e4fad7c
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/model/response/BaseBean.kt
@@ -0,0 +1,5 @@
+package com.sw.inbound.model.response
+
+open class BaseBean() {
+
+}
diff --git a/app/src/main/java/com/sw/inbound/model/response/CommonDict.kt b/app/src/main/java/com/sw/inbound/model/response/CommonDict.kt
new file mode 100644
index 0000000..9194dc8
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/model/response/CommonDict.kt
@@ -0,0 +1,10 @@
+package com.sw.inbound.model.response
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data class DictType(
+ val id: Int,
+ val value: String
+) : Parcelable
diff --git a/app/src/main/java/com/sw/inbound/model/response/GoodsInfo.kt b/app/src/main/java/com/sw/inbound/model/response/GoodsInfo.kt
new file mode 100644
index 0000000..0e16aa9
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/model/response/GoodsInfo.kt
@@ -0,0 +1,181 @@
+package com.sw.inbound.model.response
+
+import android.os.Parcelable
+import com.sw.inbound.GlobalData
+import com.sw.inbound.ext.toFormattedString
+import com.sw.inbound.model.response.SearchGoodsInfo.Record.UnitVo
+import kotlinx.parcelize.Parcelize
+import java.math.BigDecimal
+
+/**
+ * 物品信息
+ */
+@Parcelize
+data class GoodsInfo(
+ /**
+ * 数据id
+ */
+ var id: Int? = 0,
+ /**
+ * 物品Id
+ */
+ var goodId: Int? = 0,
+ /**
+ * 物品名称
+ */
+ var goodName: String? = null,
+ /**
+ * 单价
+ */
+ var recUnitPriceTaxIn: Float? = null,
+
+ /**
+ * 单位名称
+ */
+ var unitName: String? = null,
+
+ /**
+ * 采购单位id
+ */
+ var purchaseUnitId: Int? = null,
+ /**
+ * 采购数量
+ */
+ var receiveCount: Float? = null,
+
+ /**
+ * 金额
+ */
+ var recPriceExItem: Float? = null,
+ /**
+ * 仓库id
+ */
+ var warehouseId: Int? = null,
+ /**
+ * 仓库名称
+ */
+ var warehouseName: String? = null,
+
+ /**
+ * 物品采购单位关系
+ */
+ var goodsPurId: Int? = 0,
+ /**
+ * 采购库存转换值
+ */
+ var purchaseValue: Int? = 0,
+ /**
+ * 库存单位
+ */
+ var kcUnitName: String? = "",
+ /**
+ * 消耗转换值
+ */
+ var consumeValue: String? = "",
+
+ // 收货数量 收货入参
+ var receivedNum: Float? = null,
+ // 重量 收货入参
+ var goodsWeight: BigDecimal? = null,
+
+ // 已调整
+ var isAdjusted: Boolean = false,
+
+ var unitList: List = emptyList()
+) : Parcelable, BaseBean() {
+
+ val unitDictTypeList: List
+ get() {
+ return unitList.map {
+ DictType(it.businessUnitId!!.toInt(), it.purchaseUnitName!!)
+ }
+ }
+
+
+ val goodNameStr: String
+ get() {
+ if (goodName == null) return "-"
+ return goodName!!
+ }
+ val receivedNumStr: String
+ get() {
+ if (receivedNum == null)
+ return ""
+ return receivedNum?.toFormattedString() ?: ""
+ }
+
+ val recUnitPriceTaxInStr: String
+ get() {
+ if (recUnitPriceTaxIn == 0f) return ""
+ return recUnitPriceTaxIn?.toFormattedString() ?: ""
+ }
+
+ var receiveCountStr: String
+ set(value) {
+// receiveCount = value.toSafeFloat()
+ receiveCountStr = value
+ }
+ get() {
+ return when (receiveCount) {
+ 0f -> ""
+ else -> receiveCount?.toFormattedString() ?: ""
+ }
+ }
+
+ val recPriceExItemStr: String
+ get() {
+ return when (recPriceExItem) {
+ 0f -> ""
+ else -> recPriceExItem?.toFormattedString() ?: ""
+ }
+ }
+
+ val unitNameStr: String
+ get() {
+ if (unitName == null) return "-"
+ return unitName!!
+ }
+
+ val goodsWeightStr: String
+ get() {
+ if (goodsWeight == null) return ""
+
+ // 小于10 kg 显示 g
+ if (goodsWeight!!.toInt() < 10) {
+ return goodsWeight!!.multiply(BigDecimal(1000)).toString()
+ }
+ return goodsWeight?.toFormattedString() ?: ""
+ }
+
+ val goodsWeightUnitStr: String
+ get() {
+ return if (goodsWeight == null || goodsWeight!!.toInt() < 10) {
+ "克"
+ } else {
+ "千克"
+ }
+ }
+
+
+ var purchaseUnitIdStr: String
+ set(value) {
+ purchaseUnitIdStr = value
+ }
+ get() {
+ return GlobalData.unitTypeList.find { it.id == purchaseUnitId }?.value ?: ""
+ }
+
+ val warehouseNameStr: String
+ get() {
+ if (warehouseName == null) {
+ return ""
+ }
+ return warehouseName!!
+ }
+
+ fun hasNull(): Boolean {
+ return goodName == null || warehouseId == null ||
+ purchaseUnitId == null || recUnitPriceTaxIn == null
+ || receiveCount == null || recPriceExItem == null
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/model/response/GoodsType.kt b/app/src/main/java/com/sw/inbound/model/response/GoodsType.kt
new file mode 100644
index 0000000..9dc9833
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/model/response/GoodsType.kt
@@ -0,0 +1,52 @@
+package com.sw.inbound.model.response
+
+
+import android.os.Parcelable
+import com.google.gson.annotations.SerializedName
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data class GoodsType(
+ @SerializedName("allType")
+ val allType: List? = listOf()
+) : Parcelable {
+ @Parcelize
+ data class AllType(
+ @SerializedName("createTime")
+ val createTime: String? = "",
+ @SerializedName("createUserNo")
+ val createUserNo: String? = "",
+ @SerializedName("eaId")
+ val eaId: Int? = 0,
+ @SerializedName("ftype")
+ val ftype: String? = "",
+ /**
+ * 物品id
+ */
+ @SerializedName("id")
+ val id: Int? = 0,
+ @SerializedName("idResult")
+ val idResult: String? = "",
+ @SerializedName("kidTypes")
+ val kidTypes: List? = listOf(),
+ @SerializedName("pageNum")
+ val pageNum: String? = "",
+ @SerializedName("pageSize")
+ val pageSize: String? = "",
+ @SerializedName("status")
+ val status: Int? = 0,
+ @SerializedName("superName")
+ val superName: String? = "",
+ @SerializedName("typeCode")
+ val typeCode: String? = "",
+ /**
+ * 物品类型
+ */
+ @SerializedName("typeName")
+ val typeName: String? = "",
+ @SerializedName("updateTime")
+ val updateTime: String? = "",
+ @SerializedName("updateUserNo")
+ val updateUserNo: String? = ""
+ ) : Parcelable
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/model/response/PurchaseInfo.kt b/app/src/main/java/com/sw/inbound/model/response/PurchaseInfo.kt
new file mode 100644
index 0000000..5df6756
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/model/response/PurchaseInfo.kt
@@ -0,0 +1,32 @@
+package com.sw.inbound.model.response
+
+data class PurchaseInfo(
+ /**
+ * 单据id
+ */
+ var id: Int = 0,
+ /**
+ * 供应商id
+ */
+ var supplierId: Int = 0,
+ /**
+ * 供应商名称
+ */
+ var supplierName: String = "",
+ /**
+ * 采购单号
+ */
+ var purCode: String = "",
+ /**
+ * 收货单号
+ */
+ var receiveCode: String = "",
+
+ /**
+ * 物品信息
+ */
+ var receiveGoodsInfoList: List = emptyList()
+) : BaseBean() {
+ // 自用
+ var warehouseName: String = "选择仓库"
+}
diff --git a/app/src/main/java/com/sw/inbound/model/response/SearchGoodsInfo.kt b/app/src/main/java/com/sw/inbound/model/response/SearchGoodsInfo.kt
new file mode 100644
index 0000000..78222b6
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/model/response/SearchGoodsInfo.kt
@@ -0,0 +1,105 @@
+package com.sw.inbound.model.response
+
+
+import android.os.Parcelable
+import com.google.gson.annotations.SerializedName
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data class SearchGoodsInfo(
+ @SerializedName("current")
+ val current: Int? = 0,
+ @SerializedName("hitCount")
+ val hitCount: Boolean? = false,
+ @SerializedName("optimizeCountSql")
+ val optimizeCountSql: Boolean? = false,
+ @SerializedName("orders")
+ val orders: List? = listOf(),
+ @SerializedName("pages")
+ val pages: Int? = 0,
+ @SerializedName("records")
+ val records: List? = listOf(),
+ @SerializedName("searchCount")
+ val searchCount: Boolean? = false,
+ @SerializedName("size")
+ val size: Int? = 0,
+ @SerializedName("total")
+ val total: Int? = 0
+) : Parcelable {
+ @Parcelize
+ data class Record(
+ @SerializedName("goodsCode")
+ val goodsCode: String? = "",
+ /**
+ * 物品id
+ */
+ @SerializedName("goodsId")
+ val goodsId: Int? = 0,
+ /**
+ * 物品名称
+ */
+ @SerializedName("goodsName")
+ val goodsName: String? = "",
+ /**
+ * 库存单位id
+ */
+ @SerializedName("kcUnitId")
+ val kcUnitId: Int? = 0,
+ /**
+ * 库存单位名称
+ */
+ @SerializedName("kcUnitName")
+ val kcUnitName: String? = "",
+ @SerializedName("specification")
+ val specification: String? = "",
+ @SerializedName("typeId")
+ val typeId: Int? = 0,
+ @SerializedName("typeName")
+ val typeName: String? = "",
+ /**
+ * 采购单位列表
+ */
+ @SerializedName("unitVoList")
+ val unitVoList: List? = listOf(),
+ @SerializedName("zjmCode")
+ val zjmCode: String? = ""
+ ) : Parcelable {
+ val goodsNameStr: String
+ get() {
+ if (goodsName == null) return ""
+ return goodsName
+ }
+
+
+ @Parcelize
+ data class UnitVo(
+ /**
+ * 单位关系id
+ */
+ @SerializedName("businessUnitId")
+ val businessUnitId: String? = "",
+ /**
+ * 转换值
+ */
+ @SerializedName("buyToInventoryValue")
+ val buyToInventoryValue: String? = "",
+ @SerializedName("isDefault")
+ val isDefault: Int? = 0,
+ /**
+ * 采购单位名称
+ */
+ @SerializedName("purchaseUnitName")
+ val purchaseUnitName: String? = "",
+ @SerializedName("value")
+ val value: String? = "",
+ /**
+ * 采购库存转换值
+ */
+ val consumeValue: String? = "",
+ /**
+ * 消耗转换值
+ */
+ val purchaseValue: Int? = 0,
+ ) : Parcelable
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/model/response/StorageType.kt b/app/src/main/java/com/sw/inbound/model/response/StorageType.kt
new file mode 100644
index 0000000..978a6fa
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/model/response/StorageType.kt
@@ -0,0 +1,53 @@
+package com.sw.inbound.model.response
+
+
+import android.os.Parcelable
+import com.google.gson.annotations.SerializedName
+import kotlinx.parcelize.Parcelize
+
+/**
+ * 存储方式
+ */
+@Parcelize
+data class StorageType(
+ @SerializedName("childList")
+ val childList: List? = listOf(),
+ @SerializedName("createTime")
+ val createTime: String? = "",
+ @SerializedName("createUserNo")
+ val createUserNo: String? = "",
+ @SerializedName("depth")
+ val depth: String? = "",
+ @SerializedName("description")
+ val description: String? = "",
+ @SerializedName("dictId")
+ val dictId: String? = "",
+ @SerializedName("eaId")
+ val eaId: Int? = 0,
+ @SerializedName("fid")
+ val fid: String? = "",
+ @SerializedName("id")
+ val id: Int? = 0,
+ @SerializedName("idc")
+ val idc: String? = "",
+ /**
+ * 名称
+ */
+ @SerializedName("itemText")
+ val itemText: String? = "",
+ /**
+ * 对应值
+ */
+ @SerializedName("itemValue")
+ val itemValue: String? = "",
+ @SerializedName("level")
+ val level: Int? = 0,
+ @SerializedName("sortOrder")
+ val sortOrder: Int? = 0,
+ @SerializedName("status")
+ val status: Int? = 0,
+ @SerializedName("updateTime")
+ val updateTime: String? = "",
+ @SerializedName("updateUserNo")
+ val updateUserNo: String? = ""
+) : Parcelable
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/model/response/SupplierInfo.kt b/app/src/main/java/com/sw/inbound/model/response/SupplierInfo.kt
new file mode 100644
index 0000000..c7b25e1
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/model/response/SupplierInfo.kt
@@ -0,0 +1,113 @@
+package com.sw.inbound.model.response
+
+
+import android.os.Parcelable
+import com.google.gson.annotations.SerializedName
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data class SupplierInfo(
+ @SerializedName("createTime")
+ val createTime: String? = "",
+ @SerializedName("createUserNo")
+ val createUserNo: String? = "",
+ @SerializedName("dateStart")
+ val dateStart: String? = "",
+ @SerializedName("dateStop")
+ val dateStop: String? = "",
+ @SerializedName("eaId")
+ val eaId: Int? = 0,
+ @SerializedName("eaIds")
+ val eaIds: String? = "",
+ @SerializedName("giftAll")
+ val giftAll: Double? = 0.0,
+ /**
+ * 物品项
+ */
+ @SerializedName("goodCount")
+ val goodCount: Int = 0,
+ /**
+ * 订单id
+ */
+ @SerializedName("id")
+ val id: Int = 0,
+ @SerializedName("pageNum")
+ val pageNum: String? = "",
+ @SerializedName("pageSize")
+ val pageSize: String? = "",
+ @SerializedName("purCode")
+ val purCode: String? = "",
+ @SerializedName("purDateStart")
+ val purDateStart: String? = "",
+ @SerializedName("purDateStop")
+ val purDateStop: String? = "",
+ /**
+ * 采购日期
+ */
+ @SerializedName("purchaseDate")
+ val purchaseDate: String? = "",
+ @SerializedName("purchaseDates")
+ val purchaseDates: String? = "",
+ @SerializedName("receiveAddress")
+ val receiveAddress: String? = "",
+ /**
+ * 单号
+ */
+ @SerializedName("receiveCode")
+ val receiveCode: String? = "",
+ @SerializedName("receiveCountAll")
+ val receiveCountAll: Double? = 0.0,
+ /**
+ * 收货时间
+ */
+ @SerializedName("receiveDate")
+ val receiveDate: String? = "",
+ @SerializedName("receiveDates")
+ val receiveDates: String? = "",
+ @SerializedName("receiveGoodsInfos")
+ val receiveGoodsInfos: String? = "",
+ @SerializedName("receiveLogs")
+ val receiveLogs: String? = "",
+ @SerializedName("receivePhone")
+ val receivePhone: String? = "",
+ @SerializedName("receivePriceEx")
+ val receivePriceEx: Double? = 0.0,
+ @SerializedName("receivePriceIn")
+ val receivePriceIn: Double? = 0.0,
+ @SerializedName("receiveRemark")
+ val receiveRemark: String? = "",
+ /**
+ * 收货状态1收货关闭/2收货完成/3待收货 /4部分收货
+ */
+ @SerializedName("receiveStatus")
+ val receiveStatus: Int? = 0,
+ /**
+ * 操作人
+ */
+ @SerializedName("receiveUser")
+ val receiveUser: String = "",
+ @SerializedName("receivedNum")
+ val receivedNum: String? = "",
+ @SerializedName("restId")
+ val restId: String? = "",
+ /**
+ * 供应商id
+ */
+ @SerializedName("supplierId")
+ val supplierId: Int = 0,
+ /**
+ * 供应商
+ */
+ @SerializedName("supplierName")
+ val supplierName: String = "",
+ @SerializedName("taxRateAll")
+ val taxRateAll: Double? = 0.0,
+ @SerializedName("updateTime")
+ val updateTime: String? = "",
+ @SerializedName("updateUserNo")
+ val updateUserNo: String? = "",
+ @SerializedName("userName")
+ val userName: String? = "",
+ @SerializedName("warehouseId")
+ val warehouseId: String? = ""
+) : Parcelable
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/model/response/SupplierResponse.kt b/app/src/main/java/com/sw/inbound/model/response/SupplierResponse.kt
new file mode 100644
index 0000000..19a832b
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/model/response/SupplierResponse.kt
@@ -0,0 +1,34 @@
+package com.sw.inbound.model.response
+
+
+import android.os.Parcelable
+import com.google.gson.annotations.SerializedName
+import kotlinx.parcelize.Parcelize
+
+/**
+ * 供应商列表返回
+ */
+@Parcelize
+data class SupplierResponse(
+ @SerializedName("current")
+ val current: Int? = 0,
+ @SerializedName("hitCount")
+ val hitCount: Boolean? = false,
+ @SerializedName("optimizeCountSql")
+ val optimizeCountSql: Boolean? = false,
+ @SerializedName("orders")
+ val orders: List? = listOf(),
+ @SerializedName("pages")
+ val pages: Int? = 0,
+ /**
+ * 供应商列表
+ */
+ @SerializedName("records")
+ val records: List? = listOf(),
+ @SerializedName("searchCount")
+ val searchCount: Boolean? = false,
+ @SerializedName("size")
+ val size: Int? = 0,
+ @SerializedName("total")
+ val total: Int? = 0
+) : Parcelable
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/model/response/User.kt b/app/src/main/java/com/sw/inbound/model/response/User.kt
new file mode 100644
index 0000000..c3dc3cf
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/model/response/User.kt
@@ -0,0 +1,49 @@
+package com.sw.inbound.model.response
+
+import android.os.Parcelable
+import com.google.gson.annotations.SerializedName
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data class User(
+ @SerializedName("eaCode")
+ val eaCode: String? = null,
+ @SerializedName("eaId")
+ val eaId: String? = null,
+ @SerializedName("eaIdList")
+ val eaIdList: String? = null,
+ @SerializedName("eaIdListName")
+ val eaIdListName: String? = null,
+ @SerializedName("eaLogoUrl")
+ val eaLogoUrl: String? = null,
+ @SerializedName("eaName")
+ val eaName: String? = null,
+ @SerializedName("errorCode")
+ val errorCode: String? = null,
+ @SerializedName("errorMsg")
+ val errorMsg: String? = null,
+ @SerializedName("id")
+ val id: Int? = 0,
+ @SerializedName("loginIp")
+ val loginIp: String? = null,
+ @SerializedName("name")
+ val name: String? = "",
+ @SerializedName("phone")
+ val phone: String? = "",
+ @SerializedName("token")
+ val token: String? = "",
+ @SerializedName("userCode")
+ val userCode: String? = null,
+ @SerializedName("userName")
+ val userName: String? = ""
+) : Parcelable
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/com/sw/inbound/network/LoadingState.kt b/app/src/main/java/com/sw/inbound/network/LoadingState.kt
new file mode 100644
index 0000000..d5a79ef
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/network/LoadingState.kt
@@ -0,0 +1,16 @@
+package com.sw.inbound.network
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import java.util.concurrent.atomic.AtomicInteger
+
+object LoadingState {
+ private var _isLoading by mutableStateOf(false)
+ val isLoading: Boolean get() = _isLoading
+
+ private val counter = AtomicInteger(0)
+
+ fun show() = counter.incrementAndGet().let { if (it == 1) _isLoading = true }
+ fun hide() = counter.decrementAndGet().let { if (it <= 0) _isLoading = false }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/network/api/ApiService.kt b/app/src/main/java/com/sw/inbound/network/api/ApiService.kt
new file mode 100644
index 0000000..191b740
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/network/api/ApiService.kt
@@ -0,0 +1,108 @@
+package com.sw.inbound.network.api
+
+import com.sw.inbound.model.request.GoodsAddParam
+import com.sw.inbound.model.request.LoginParam
+import com.sw.inbound.model.request.PurchaseWarehouseParam
+import com.sw.inbound.model.request.UploadInfo
+import com.sw.inbound.model.response.ApiResponse
+import com.sw.inbound.model.response.DictType
+import com.sw.inbound.model.response.GoodsType
+import com.sw.inbound.model.response.PurchaseInfo
+import com.sw.inbound.model.response.SearchGoodsInfo
+import com.sw.inbound.model.response.StorageType
+import com.sw.inbound.model.response.SupplierResponse
+import com.sw.inbound.model.response.User
+import okhttp3.MultipartBody
+import okhttp3.RequestBody
+import retrofit2.http.Body
+import retrofit2.http.GET
+import retrofit2.http.Multipart
+import retrofit2.http.POST
+import retrofit2.http.Part
+import retrofit2.http.Query
+
+interface ApiService {
+ /**
+ * 登录
+ */
+ @POST("/shuwei-user/swuserbase/loginPad")
+ suspend fun login(@Body body: LoginParam): ApiResponse
+
+ /**
+ * 采购单入库-列表
+ */
+ @GET("/shuwei-zhct/pad/receivePage")
+ suspend fun getReceiveList(
+ @Query("pageNo") pageNo: Int,
+ @Query("pageSize") pageSize: Int
+ ): ApiResponse
+
+ /**
+ * 采购单入库-详情
+ */
+ @GET("/shuwei-zhct/pad/receiveDetail")
+ suspend fun getReceiveDetail(@Query("id") id: Int): ApiResponse
+
+ /**
+ * 部分收货
+ */
+ @POST("/shuwei-zhct/pad/partialReceiptGoods")
+ suspend fun partialReceipt(@Body uploadInfo: UploadInfo): ApiResponse
+
+ /**
+ * 确认收货
+ */
+ @POST("/shuwei-zhct/pad/saveAndReceiveAndGoWare")
+ suspend fun confirmReceipt(@Body uploadInfo: UploadInfo): ApiResponse
+
+ /**
+ * 存储方式
+ */
+ @GET("/shuwei-zhct/swsysdictitem/getDictItemByF")
+ suspend fun getGoodsStorageType(@Query("dictCode") dictCode: String = "goods_storage_type"): ApiResponse>
+
+ /**
+ * 物品类型
+ */
+ @GET("/shuwei-zhct/swkcglgoodstype/notLimitList")
+ suspend fun getGoodsType(): ApiResponse
+
+ /**
+ * 获取字典数据
+ */
+ @GET("/shuwei-zhct/pad/dataList")
+ suspend fun getDictType(@Query("type") type: String): ApiResponse>
+
+ /**
+ * 上传图片
+ */
+ @Multipart
+ @POST("/shuwei-zhct/fileUpload/fileUpload/")
+ suspend fun uploadImage(
+ @Part("code") code: RequestBody,
+ @Part file: MultipartBody.Part
+ ): ApiResponse
+
+ /**
+ * 搜索物品列表
+ */
+ @GET("/shuwei-zhct/pad/goodsInfoList")
+ suspend fun searchGoodsInfoList(
+ @Query("goodsName") goodsName: String,
+ @Query("pageNo") pageNo: Int,
+ @Query("pageSize") pageSize: Int
+ ): ApiResponse
+
+ /**
+ * 自采添加商品
+ */
+ @POST("/shuwei-zhct/pad/goodsAdd")
+ suspend fun selfPurchaseGoodsAdd(@Body goodsAddParam: GoodsAddParam): ApiResponse
+
+ /**
+ * 自采入库
+ */
+ @POST("/shuwei-zhct/pad/SelfPurchasedGoods")
+ suspend fun selfPurchaseWarehousing(@Body list: List): ApiResponse
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/network/interceptor/ErrorInterceptor.kt b/app/src/main/java/com/sw/inbound/network/interceptor/ErrorInterceptor.kt
new file mode 100644
index 0000000..0b8d3cd
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/network/interceptor/ErrorInterceptor.kt
@@ -0,0 +1,29 @@
+package com.sw.inbound.network.interceptor
+
+import com.google.gson.JsonParseException
+import com.sw.inbound.model.exception.NetException
+import okhttp3.Interceptor
+import okhttp3.Response
+import retrofit2.HttpException
+import timber.log.Timber
+import java.io.IOException
+
+class ErrorInterceptor : Interceptor {
+ override fun intercept(chain: Interceptor.Chain): Response {
+ try {
+ val response = chain.proceed(chain.request())
+ if (!response.isSuccessful) {
+ throw NetException(response.code, "HTTP Error ${response.code}")
+ }
+ return response
+ } catch (e: Exception) {
+ Timber.e(e)
+ throw when (e) {
+ is IOException -> NetException(-1, "Network error: ${e.message}")
+ is JsonParseException -> NetException(-2, "Data parse error")
+ is HttpException -> NetException(-3, e.message ?: "未知错误")
+ else -> NetException(-100, e.message ?: "未知错误")
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/network/interceptor/RequestInterceptor.kt b/app/src/main/java/com/sw/inbound/network/interceptor/RequestInterceptor.kt
new file mode 100644
index 0000000..0ed71ed
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/network/interceptor/RequestInterceptor.kt
@@ -0,0 +1,29 @@
+package com.sw.inbound.network.interceptor
+
+import com.sw.inbound.GlobalKey
+import com.sw.inbound.utils.ContextUtils
+import com.sw.inbound.utils.SPUtil
+import okhttp3.Interceptor
+import okhttp3.Response
+
+class RequestInterceptor : Interceptor {
+ override fun intercept(chain: Interceptor.Chain): Response {
+ val originalRequest = chain.request()
+ val requestBuilder = originalRequest.newBuilder()
+ .header("Content-Type", "application/json")
+ .header("Accept", "application/json")
+// .header("Authorization", "Bearer ${getToken()}")
+ .header("Authorization", getToken())
+
+ val newRequest = requestBuilder.build()
+
+ return chain.proceed(newRequest)
+ }
+
+ private fun getToken(): String {
+ // 从本地获取token的逻辑
+ val spUtil = SPUtil.getInstance(context = ContextUtils.getAppContext())
+ return spUtil.get(GlobalKey.KEY_TOKEN, "") as String
+// return "eyJhbGciOiJIUzUxMiJ9.eyJpZCI6MTQ2LCJ1c2VyTmFtZSI6IjEzNjgxNDQ4ODU2IiwibmFtZSI6IuW-kOejiiIsInBhc3N3b3JkIjoiOTllOTQ1ZmVjZmZjNWIzNDI4MmUwNDRlODYyMzdjM2UxZjU5OWY5OCIsInNhbHQiOiI0NmEzMzUzYWU4OTA0MDYxYjMzODU5ZWNlYTBlMGE2NyIsInBob25lIjoiMTM2ODE0NDg4NTYiLCJzdGF0dXMiOjEsInVzZXJUeXBlIjoyLCJjcmVhdGVVc2VyTm8iOiIxNDEiLCJjcmVhdGVUaW1lIjoxNjI2OTQzNDE2MDAwLCJ1cGRhdGVVc2VyTm8iOiIxNDEiLCJ1cGRhdGVUaW1lIjoxNjI2OTQzNDE2MDAwLCJpc0RlbCI6ZmFsc2UsImVhSWQiOjk5LCJlYUlkTGlzdCI6Ijk5IiwiaXNTaG9wTWFuYWdlciI6dHJ1ZSwidXNlck5vIjoiMWY5Nzk5ZWMtODlkYi00MWYyLTk1YTEtY2UzNTA3Y2QyMTU2In0.f7wImPgBOYMV0AqRchnXGPkUWZN9dFJ9gLPsaB8uNldd21IfXLjJl8y-FiWVuVUvlwUvGpgqGDFR1JKj5H7amw"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/repository/BaseRepository.kt b/app/src/main/java/com/sw/inbound/repository/BaseRepository.kt
new file mode 100644
index 0000000..2f81754
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/repository/BaseRepository.kt
@@ -0,0 +1,58 @@
+package com.sw.inbound.repository
+
+import com.google.gson.JsonParseException
+import com.sw.inbound.model.response.ApiResponse
+import retrofit2.HttpException
+import timber.log.Timber
+import java.io.IOException
+import java.net.ConnectException
+import java.net.SocketTimeoutException
+import javax.net.ssl.SSLHandshakeException
+
+abstract class BaseRepository {
+ suspend fun safeApiCall(apiCall: suspend () -> ApiResponse): ApiResponse {
+ return try {
+ apiCall()
+ } catch (e: Exception) {
+ Timber.e("safeApiCall Exception: ${e.stackTraceToString()}")
+
+ when (e) {
+ is HttpException -> {
+ // 对于HTTP异常,尝试从响应体中获取错误信息
+ val errorBody = e.response()?.errorBody()?.string()
+ val errorMsg = if (!errorBody.isNullOrEmpty()) {
+ // 可以尝试解析errorBody为JSON获取更详细的错误信息
+ errorBody
+ } else {
+ "HTTP Error: ${e.code()} - ${e.message()}"
+ }
+ ApiResponse(code = e.code(), msg = errorMsg)
+ }
+
+ is SocketTimeoutException -> {
+ ApiResponse(code = -2, msg = "请求超时: ${e.message}")
+ }
+
+ is ConnectException -> {
+ ApiResponse(code = -3, msg = "连接失败: ${e.message}")
+ }
+
+ is SSLHandshakeException -> {
+ ApiResponse(code = -4, msg = "SSL握手失败: ${e.message}")
+ }
+
+ is JsonParseException -> {
+ ApiResponse(code = -5, msg = "JSON解析错误: ${e.message}")
+ }
+
+ is IOException -> {
+ ApiResponse(code = -6, msg = "网络IO错误: ${e.message}")
+ }
+
+ else -> {
+ ApiResponse(code = -1, msg = "未知错误: ${e.message ?: "无错误信息"}")
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/repository/RemoteRepository.kt b/app/src/main/java/com/sw/inbound/repository/RemoteRepository.kt
new file mode 100644
index 0000000..e50fe3b
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/repository/RemoteRepository.kt
@@ -0,0 +1,143 @@
+package com.sw.inbound.repository
+
+import android.net.Uri
+import com.sw.inbound.model.request.GoodsAddParam
+import com.sw.inbound.model.request.LoginParam
+import com.sw.inbound.model.request.PurchaseWarehouseParam
+import com.sw.inbound.model.request.UploadInfo
+import com.sw.inbound.model.response.ApiResponse
+import com.sw.inbound.model.response.DictType
+import com.sw.inbound.model.response.GoodsType
+import com.sw.inbound.model.response.PurchaseInfo
+import com.sw.inbound.model.response.SearchGoodsInfo
+import com.sw.inbound.model.response.StorageType
+import com.sw.inbound.model.response.SupplierResponse
+import com.sw.inbound.model.response.User
+import com.sw.inbound.network.api.ApiService
+import com.sw.inbound.utils.ContextUtils
+import com.sw.inbound.utils.ImageUtils
+import okhttp3.MultipartBody
+import okhttp3.RequestBody
+import timber.log.Timber
+import javax.inject.Inject
+
+class RemoteRepository @Inject constructor(
+ private val apiService: ApiService
+) : BaseRepository() {
+
+ suspend fun login(loginParam: LoginParam): ApiResponse {
+ return safeApiCall { apiService.login(loginParam) }
+ }
+
+ suspend fun getReceiveList(pageNum: Int, pageSize: Int): ApiResponse {
+ return safeApiCall {
+ apiService.getReceiveList(pageNum, pageSize)
+ }
+ }
+
+ suspend fun getReceiveDetail(id: Int): ApiResponse {
+ return safeApiCall {
+ apiService.getReceiveDetail(id)
+ }
+ }
+
+ suspend fun getGoodsStorageType(): ApiResponse> {
+ return safeApiCall {
+ apiService.getGoodsStorageType()
+ }
+ }
+
+ suspend fun getGoodsType(): ApiResponse {
+ return safeApiCall {
+ apiService.getGoodsType()
+ }
+ }
+
+ enum class TypeEnum(val string: String) {
+ /**
+ * 仓库
+ */
+ WAREHOUSE("warehouse"),
+
+ /**
+ * 供应商
+ */
+ SUPPLIER("supplier"),
+
+ /**
+ * 单位
+ */
+ UNIT("unit")
+ }
+
+ suspend fun getDictType(type: TypeEnum): ApiResponse> {
+ return safeApiCall {
+ apiService.getDictType(type.string)
+ }
+ }
+
+
+ suspend fun uploadImage(uri: Uri): ApiResponse {
+ Timber.d("uploadImage uri = $uri")
+ return safeApiCall {
+ val codeRequestBody = RequestBody.create(
+ MultipartBody.FORM,
+ "4"
+ )
+
+ val part =
+ ImageUtils.genRequestPart(context = ContextUtils.getAppContext(), imageUri = uri)
+ if (part == null) {
+ ApiResponse(code = -1, msg = "解析图片失败", data = "")
+ } else {
+ apiService.uploadImage(codeRequestBody, file = part)
+ }
+ }
+ }
+
+ /**
+ * 部分收货
+ */
+ suspend fun partialReceipt(uploadInfo: UploadInfo): ApiResponse {
+ return safeApiCall {
+ apiService.partialReceipt(uploadInfo)
+ }
+ }
+
+ /**
+ * 全部收货
+ */
+ suspend fun confirmReceipt(uploadInfo: UploadInfo): ApiResponse {
+ return safeApiCall {
+ apiService.confirmReceipt(uploadInfo)
+ }
+ }
+
+ suspend fun searchGoodsInfoList(
+ goodsName: String,
+ pageNo: Int,
+ pageSize: Int
+ ): ApiResponse {
+ return safeApiCall {
+ apiService.searchGoodsInfoList(goodsName, pageNo, pageSize)
+ }
+ }
+
+ /**
+ * 自采购-添加商品
+ */
+ suspend fun selfPurchaseGoodsAdd(goodsAddParam: GoodsAddParam): ApiResponse {
+ return safeApiCall {
+ apiService.selfPurchaseGoodsAdd(goodsAddParam)
+ }
+ }
+
+ /**
+ * 自采购-入库
+ */
+ suspend fun selfPurchaseWarehousing(list: List): ApiResponse {
+ return safeApiCall {
+ apiService.selfPurchaseWarehousing(list)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/sdk/SensorScaleUtils.kt b/app/src/main/java/com/sw/inbound/sdk/SensorScaleUtils.kt
new file mode 100644
index 0000000..f556aab
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/sdk/SensorScaleUtils.kt
@@ -0,0 +1,104 @@
+package com.sw.inbound.sdk
+
+import com.sw.inbound.utils.ThreadUtils
+import com.wabon.wbintelligenthardwaresdk.api.SensorScale
+import com.wabon.wbintelligenthardwaresdk.api.SensorScale.OnScaleResult
+import kotlinx.coroutines.delay
+import timber.log.Timber
+
+typealias Callback = (Double) -> Unit
+
+/**
+ * 称 传感器计算
+ */
+object SensorScaleUtils {
+
+ const val serialPort = "/dev/ttyS7"
+ const val baudRate = 115200
+ private var mSensorScale: SensorScale? = null
+ private var isOpened: Boolean = false
+ private var callback: Callback? = {}
+
+ private fun init() {
+ mSensorScale = SensorScale(object : OnScaleResult {
+ /**
+ * 读取重量
+ */
+ override fun readWeight(state: Int, value: Double) {
+ val stateStr = when (state) {
+ SensorScale.STATE_STABLE -> "稳定"
+ SensorScale.STATE_UNSTABLE -> "不稳定"
+ SensorScale.STATE_OVER_WEIGHT -> "量程溢出"
+ else -> "未知"
+ }
+// Timber.d("readWeight state = ${stateStr}, weight = $value")
+ callback?.invoke(value)
+ }
+
+ /**
+ * 读取鉴别率
+ */
+ override fun readIdentify(rate: Int) {
+ Timber.e("readIdentify rate = $rate")
+ }
+
+ override fun fail(errCode: Int) {
+ Timber.e("fail code = $errCode")
+ }
+ })
+ }
+
+ fun startScale(autoScale: Boolean = true, callback: Callback?) {
+ if (isOpened) {
+ startContinuousRead(callback = callback)
+ return
+ }
+ this.callback = callback
+ init()
+ mSensorScale?.openScale(serialPort, baudRate) { open ->
+ isOpened = open
+ Timber.d("isOpened = $isOpened")
+ if (open) {
+ if (autoScale) {
+ ThreadUtils.launchOnIo {
+ delay(2000)
+ // 打开后需要等待后才能调用,否则会 1001 SDK未初始化
+ mSensorScale?.startContinuousRead()
+ }
+ }
+ }
+ }
+ }
+
+ fun startContinuousRead(callback: Callback?) {
+ this.callback = callback
+ Timber.d("isOpened = $isOpened")
+ if (!isOpened) {
+ return
+ }
+ mSensorScale?.startContinuousRead()
+ }
+
+ fun readWeight(callback: Callback?) {
+ this.callback = callback
+ Timber.d("isOpened = $isOpened")
+ if (!isOpened) {
+ return
+ }
+ mSensorScale?.readWeight()
+ }
+
+ fun stopContinuousRead() {
+ Timber.d("isOpened = $isOpened")
+ if (!isOpened) {
+ return
+ }
+ mSensorScale?.stopContinuousRead()
+ }
+
+ fun closeScale() {
+ isOpened = false
+ mSensorScale?.closeScale()
+ mSensorScale == null
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/ui/NavController.kt b/app/src/main/java/com/sw/inbound/ui/NavController.kt
new file mode 100644
index 0000000..62f7e1b
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/ui/NavController.kt
@@ -0,0 +1,159 @@
+package com.sw.inbound.ui
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Scaffold
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.paint
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.navigation.NavHostController
+import androidx.navigation.NavType
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.currentBackStackEntryAsState
+import androidx.navigation.compose.rememberNavController
+import androidx.navigation.navArgument
+import com.sw.inbound.GlobalKey
+import com.sw.inbound.R
+import com.sw.inbound.ui.page.HomeScreen
+import com.sw.inbound.ui.page.LoginScreen
+import com.sw.inbound.ui.page.PurchaseOrderScreen
+import com.sw.inbound.ui.page.ReceiptProductScreen
+import com.sw.inbound.ui.page.SelfProcurementScreen
+import com.sw.inbound.ui.page.TestScreen
+import com.sw.inbound.ui.weight.GlobalLoading
+import com.sw.inbound.utils.SPUtil
+import com.sw.inbound.utils.ToastUtils
+
+@Composable
+fun AppScreen(
+ onBackRequest: () -> Unit
+) {
+ val navController = rememberNavController()
+ val currentBackStackEntry by navController.currentBackStackEntryAsState()
+ var canBack by remember { mutableStateOf(false) }
+
+ // 拦截物理返回键
+// BackHandler(
+// onBack = {
+// val currentRoute = currentBackStackEntry?.destination?.route
+// Timber.d("BackHandler currentRouter = ${currentRoute}")
+// if (currentRoute == Screen.Home.route && !canBack) {
+// ToastUtils.showToast("再次点击退出")
+// canBack = true
+// } else if (currentRoute == Screen.Login.route) {
+// onBackRequest()
+// } else {
+// navController.popBackStack()
+// }
+// }
+// )
+//
+// BackHandler(canBack) {
+// Timber.d("canBack = $canBack")
+// if (canBack) {
+// onBackRequest()
+// }
+// }
+
+ Scaffold(
+ modifier = Modifier
+ .fillMaxSize()
+ .paint(painter = painterResource(R.mipmap.bg)),
+ containerColor = Color.Transparent,
+ content = { padding ->
+ Box {
+// Image(
+// painter = painterResource(R.mipmap.bg),
+// contentDescription = null,
+// modifier = Modifier.fillMaxSize(),
+// contentScale = ContentScale.Crop
+// )
+
+ NavHost(padding, navController, onBackRequest)
+
+ }
+ }
+ )
+ ToastUtils.ToastComposable()
+ GlobalLoading() // 全局Loading
+
+}
+
+@Composable
+private fun NavHost(
+ padding: PaddingValues,
+ navController: NavHostController,
+ onBackRequest: () -> Unit = {}
+) {
+ val spUtil = SPUtil.getInstance()
+ val token = spUtil.get(GlobalKey.KEY_TOKEN, "")!!
+ NavHost(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(padding),
+ navController = navController,
+ startDestination = if (token.isEmpty()) Screen.Login.route else Screen.Home.route
+ ) {
+ composable(Screen.Test.route) {
+ TestScreen(modifier = Modifier, navController)
+ }
+ // 首页
+ composable(Screen.Home.route) {
+ HomeScreen(modifier = Modifier, navController, onBackRequest = onBackRequest)
+ }
+ // 登录
+ composable(Screen.Login.route) {
+ LoginScreen(modifier = Modifier, navController)
+ }
+ // 采购单入库
+ composable(Screen.PurchaseOrder.route) {
+ PurchaseOrderScreen(modifier = Modifier.fillMaxSize(), navController)
+ }
+ // 自采单
+ composable(Screen.SelfProcurement.route) {
+ SelfProcurementScreen(modifier = Modifier, navController)
+ }
+ // 收货
+ composable(
+ Screen.ReceiptProduct.route,
+ arguments = listOf(
+ navArgument(name = "id", builder = { type = NavType.IntType }),
+ navArgument(name = "supplierId", builder = { type = NavType.IntType })
+ )
+ ) { backStackEntry ->
+ val id = backStackEntry.arguments?.getInt("id") ?: 0
+ val supplierId = backStackEntry.arguments?.getInt("supplierId") ?: 0
+ ReceiptProductScreen(modifier = Modifier, navController, id, supplierId)
+ }
+ }
+}
+
+sealed class Screen(val route: String) {
+ data object Home : Screen("home")
+ data object Login : Screen("login")
+
+ // 采购单入库
+ data object PurchaseOrder : Screen("purchase_order")
+
+ // 自采单入库
+ data object SelfProcurement : Screen("self_procurement")
+
+ // 收货
+ data object ReceiptProduct : Screen("receipt_product?id={id}&supplierId={supplierId}") {
+ fun createRoute(id: Int, supplierId: Int) =
+ "receipt_product?id=${id}&supplierId=${supplierId}"
+ }
+
+ // 添加物品
+ data object AddProduct : Screen("addProduct")
+ data object Test : Screen("Test")
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/ui/page/HomeScreen.kt b/app/src/main/java/com/sw/inbound/ui/page/HomeScreen.kt
new file mode 100644
index 0000000..ade6ae0
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/ui/page/HomeScreen.kt
@@ -0,0 +1,89 @@
+package com.sw.inbound.ui.page
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+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.width
+import androidx.compose.runtime.Composable
+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.res.painterResource
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.rememberNavController
+import com.sw.inbound.R
+import com.sw.inbound.ui.Screen
+import com.sw.inbound.ui.weight.TopTitleBar
+import com.sw.inbound.viewmodel.UserViewModel
+import timber.log.Timber
+
+@Composable
+fun HomeScreen(
+ modifier: Modifier = Modifier,
+ navController: NavHostController = rememberNavController(),
+ viewModel: UserViewModel = hiltViewModel(),
+ onBackRequest: () -> Unit = {}
+) {
+ val user by viewModel.user.collectAsState()
+
+ // 避免每次进入都获取字典类型
+ LaunchedEffect(Unit) {
+ Timber.d("LaunchedEffect(Unit)")
+ viewModel.getInitInfo()
+ }
+
+ BackHandler {
+ onBackRequest()
+ }
+
+ Column(modifier = Modifier.fillMaxSize()) {
+ TopTitleBar(user = user, onLogoutClick = {
+ viewModel.logout()
+ navController.navigate(Screen.Login.route) {
+ popUpTo(Screen.Home.route) {
+ inclusive = true
+ }
+ }
+ })
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center,
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxWidth()
+ ) {
+ Image(
+ modifier = Modifier
+ .width(595.dp)
+ .height(477.dp)
+ .clickable() {
+ navController.navigate(Screen.PurchaseOrder.route)
+ },
+ painter = painterResource(R.mipmap.ic_order_purchase),
+ contentDescription = "采购单入库"
+ )
+ Spacer(modifier = Modifier.width(15.dp))
+ Image(
+ modifier = Modifier
+ .width(595.dp)
+ .height(477.dp)
+ .clickable {
+ navController.navigate(Screen.SelfProcurement.route)
+ },
+ painter = painterResource(R.mipmap.ic_order_self_procurement),
+ contentDescription = "自采入库"
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/ui/page/LoginScreen.kt b/app/src/main/java/com/sw/inbound/ui/page/LoginScreen.kt
new file mode 100644
index 0000000..54c1440
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/ui/page/LoginScreen.kt
@@ -0,0 +1,169 @@
+package com.sw.inbound.ui.page
+
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+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.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.material3.TextFieldDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.text.input.VisualTransformation
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.navigation.NavController
+import androidx.navigation.compose.rememberNavController
+import com.sw.inbound.GlobalData
+import com.sw.inbound.R
+import com.sw.inbound.ext.bold
+import com.sw.inbound.ext.textAlign
+import com.sw.inbound.ext.withSize
+import com.sw.inbound.ui.Screen
+import com.sw.inbound.ui.theme.AppTypography
+import com.sw.inbound.ui.weight.CustomButton
+import com.sw.inbound.utils.ToastUtils
+import com.sw.inbound.viewmodel.UserViewModel
+import timber.log.Timber
+
+@Composable
+fun LoginScreen(
+ modifier: Modifier = Modifier,
+ controller: NavController = rememberNavController(),
+ viewModel: UserViewModel = hiltViewModel()
+) {
+ var username by remember { mutableStateOf("padAdmin") }
+ var password by remember { mutableStateOf("123123") }
+
+// val user by viewModel.user.collectAsState()
+
+ LaunchedEffect(Unit) {
+ Timber.d(" user =")
+ viewModel.user.collect {
+ Timber.d(" user collect = ${it}")
+ if (it != null) {
+ GlobalData.user = it
+ controller.navigate(Screen.Home.route) {
+ Timber.d("登录成功,跳转到主界面")
+ controller.popBackStack()
+ }
+ }
+ }
+// if (user != null){
+// controller.navigate(Screen.Home.route) {
+// controller.popBackStack()
+// }
+// }
+ }
+
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(text = "出入库管理", style = AppTypography.black141428TextStyle.withSize(60.sp).bold())
+ Spacer(modifier = Modifier.height(91.dp))
+
+ UserInputView(value = username, placeholderValue = "请输入用户名", onValueChange = {
+ username = it
+ })
+
+ Spacer(modifier = Modifier.height(30.dp))
+
+ UserInputView(value = password, placeholderValue = "请输入密码", onValueChange = {
+ password = it
+ }, isPassword = true)
+
+ Spacer(modifier = Modifier.height(90.dp))
+ CustomButton(
+ modifier = modifier
+ .width(885.dp)
+ .height(120.dp),
+ text = "登录",
+ onClick = {
+ if (username.isEmpty() || password.isEmpty()) {
+ ToastUtils.showToast("用户名或密码不能为空")
+ return@CustomButton
+ }
+ viewModel.login(username, password)
+// viewModel.test()
+ },
+ borderColor = colorResource(R.color.blue),
+ textColor = colorResource(R.color.white),
+ fontSize = 36.sp
+ )
+ }
+}
+
+
+@Composable
+private fun UserInputView(
+ value: String,
+ placeholderValue: String,
+ onValueChange: (String) -> Unit,
+ isPassword: Boolean = false,
+ isShowPassword: Boolean = false
+) {
+ val backgroundColor = Color.White.copy(alpha = 0.37f)
+
+ TextField(
+ value = value,
+ onValueChange = onValueChange,
+ modifier = Modifier
+ .height(120.dp)
+ .width(885.dp)
+ .border(
+ width = 2.dp,
+ color = backgroundColor,
+ shape = RoundedCornerShape(10.dp)
+ ),
+ colors = TextFieldDefaults.colors(
+ focusedContainerColor = backgroundColor,
+ unfocusedContainerColor = backgroundColor,
+ disabledContainerColor = backgroundColor,
+ focusedIndicatorColor = Color.Transparent,
+ unfocusedIndicatorColor = Color.Transparent,
+ disabledIndicatorColor = Color.Transparent
+ ),
+ // 密码输入相关设置
+ visualTransformation = if (isPassword && !isShowPassword) {
+ PasswordVisualTransformation()
+ } else {
+ VisualTransformation.None
+ },
+ keyboardOptions = KeyboardOptions(
+ keyboardType = if (isPassword) KeyboardType.Password else KeyboardType.Text,
+ imeAction = if (isPassword) ImeAction.Done else ImeAction.Next
+ ),
+ shape = RoundedCornerShape(10.dp),
+ textStyle = AppTypography.gray96a0aaTextStyle.withSize(30.sp).textAlign(TextAlign.Center),
+ placeholder = {
+ Text(
+ text = placeholderValue,
+ modifier = Modifier.fillMaxWidth(),
+ textAlign = TextAlign.Center, // 占位符水平居中
+ style = AppTypography.gray96a0aaTextStyle.textAlign(TextAlign.Center)
+ )
+ },
+ singleLine = true // 确保单行输入(避免换行导致高度变化)
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/ui/page/PurchaseOrderScreen.kt b/app/src/main/java/com/sw/inbound/ui/page/PurchaseOrderScreen.kt
new file mode 100644
index 0000000..a9b4782
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/ui/page/PurchaseOrderScreen.kt
@@ -0,0 +1,283 @@
+package com.sw.inbound.ui.page
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+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.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.width
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.HorizontalDivider
+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.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.rememberNavController
+import com.sw.inbound.R
+import com.sw.inbound.ext.dashedBorder
+import com.sw.inbound.ext.toFormattedString
+import com.sw.inbound.model.response.SupplierInfo
+import com.sw.inbound.ui.Screen
+import com.sw.inbound.ui.theme.AppTypography.blackTextStyle
+import com.sw.inbound.ui.theme.AppTypography.grayTextStyle
+import com.sw.inbound.ui.weight.BottomActionBar
+import com.sw.inbound.ui.weight.TopTitleBar
+import com.sw.inbound.viewmodel.ProductViewModel
+
+@Preview(
+ widthDp = 1920,
+ heightDp = 1080,
+ showBackground = true
+)
+@Composable
+fun PurchaseOrderScreen(
+ modifier: Modifier = Modifier,
+ navController: NavHostController = rememberNavController(),
+ viewModel: ProductViewModel = hiltViewModel()
+) {
+ val products by viewModel.supplierList.collectAsState()
+
+ LaunchedEffect(Unit) {
+ viewModel.getOrderList()
+ }
+
+ Column {
+ TopTitleBar()
+ Box(
+ modifier = Modifier
+ .weight(1f)
+ .padding(horizontal = 10.dp)
+ ) {
+ Image(
+ painter = painterResource(id = R.mipmap.bg_listview),
+ contentDescription = "背景",
+ modifier = Modifier
+ .width(1900.dp)
+ .height(880.dp),
+ contentScale = ContentScale.FillBounds
+ )
+
+ Box(
+ modifier = Modifier.padding(
+ start = 30.dp,
+ end = 30.dp,
+ top = 30.dp,
+ bottom = 90.dp
+ )
+ ) {
+ if (products.isEmpty()) {
+ PurchaseEmptyItem()
+ } else {
+ LazyRow(
+ modifier = modifier
+// .paint(painterResource(R.mipmap.bg_listview))
+// .fillMaxSize()
+// .background(
+// color = Color(0x80FFFFFF),
+// shape = RoundedCornerShape(12.dp) // 圆角背景
+// )
+ .padding(10.dp),
+ contentPadding = PaddingValues(16.dp),
+ horizontalArrangement = Arrangement.spacedBy(30.dp)
+ ) {
+ items(items = products, key = { it!!.id }) {
+ PurchaseOrderItem(it!!) {
+ navController.navigate(
+ Screen.ReceiptProduct.createRoute(
+ it.id,
+ it.supplierId
+ )
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+
+ BottomActionBar(
+ modifier = Modifier, onLeftButtonClick = {
+ navController.popBackStack()
+ }, onRight2ButtonClick = {
+ navController.navigate(Screen.SelfProcurement.route)
+ })
+ }
+}
+
+@Composable
+fun PurchaseEmptyItem() {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Image(painter = painterResource(R.mipmap.ic_empty), contentDescription = "暂无数据")
+ Text(
+ text = "暂无数据", style = TextStyle(
+ fontSize = 30.sp,
+ fontWeight = FontWeight.Bold,
+ color = colorResource(R.color.title)
+ )
+ )
+ }
+}
+
+@Composable
+fun PurchaseOrderItem(product: SupplierInfo, onItemClick: (SupplierInfo) -> Unit) {
+
+ val (receiptStatusStyle, statusLabel) = when (product.receiveStatus) {
+ 1 -> blackTextStyle to "已关闭"
+ 2 -> blackTextStyle to "已完成"
+ 3 -> blackTextStyle.copy(
+ color = colorResource(R.color.red)
+ ) to "未收货"
+
+ 4 -> blackTextStyle.copy(
+ color = colorResource(R.color.origin)
+ ) to "部分收货"
+
+ else -> blackTextStyle to "已关闭"
+ }
+
+ val itemSpace = 29.dp
+ Box(
+ modifier = Modifier
+ .background(
+ color = Color.White.copy(alpha = 0.37f),
+ shape = RoundedCornerShape(10.dp) // 圆角背景
+ )
+ .clickable {
+ onItemClick(product)
+ }
+ ) {
+ Column(
+ modifier = Modifier
+ .width(580.dp)
+ .padding(horizontal = 30.dp, vertical = 30.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(0.dp)
+ ) {
+ Text(
+ text = product.supplierName,
+ modifier = Modifier.padding(top = 25.dp),
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ style = TextStyle(
+ fontWeight = FontWeight.Bold,
+ fontSize = 30.sp,
+ color = colorResource(R.color.black)
+ )
+ )
+ Spacer(modifier = Modifier.height(itemSpace))
+ Text(
+ text = "单号 ${product.receiveCode}",
+ maxLines = 1,
+ style = grayTextStyle
+ )
+ Spacer(modifier = Modifier.height(itemSpace))
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = colorResource(R.color.divider)
+ )
+ Spacer(modifier = Modifier.height(40.dp))
+ Text(
+ text = "物品(项)",
+ style = grayTextStyle
+ )
+ Spacer(modifier = Modifier.height(22.dp))
+ Text(
+ text = product.goodCount.toFormattedString(),
+ style = TextStyle(
+ color = colorResource(R.color.black),
+ fontSize = 90.sp,
+ fontWeight = FontWeight.Bold
+ )
+ )
+ Spacer(modifier = Modifier.height(22.dp))
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .dashedBorder(
+ strokeWidth = 2.dp,
+ color = Color(0xFFBDC5CE),
+ cornerRadiusDp = 10.dp
+ )
+ .padding(horizontal = 72.dp, vertical = 27.dp),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ Text(text = "采购日期", style = grayTextStyle)
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(text = product.purchaseDate ?: "", style = blackTextStyle)
+ }
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ Text(text = "到货日期", style = grayTextStyle)
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(text = product.receiveDate ?: "-", style = blackTextStyle)
+ }
+ }
+ Spacer(modifier = Modifier.height(28.dp))
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Row {
+ Text("收货状态:", style = grayTextStyle)
+ Spacer(Modifier.width(8.dp))
+ Text(statusLabel, style = receiptStatusStyle)
+ }
+
+ Row {
+ Text("采购人:", style = grayTextStyle)
+ Spacer(Modifier.width(8.dp))
+
+ Text(product.receiveUser, style = blackTextStyle)
+ }
+ }
+ Spacer(modifier = Modifier.height(itemSpace))
+ Button(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(80.dp),
+ shape = RoundedCornerShape(10.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Color(0xFF009632).copy(alpha = 1f),
+ contentColor = colorResource(R.color.white)
+ ), onClick = {
+ onItemClick(product)
+ }) {
+ Text(
+ if (product.receiveStatus == 4) "部分收货" else "收货",
+ style = TextStyle(fontWeight = FontWeight.Bold, fontSize = 30.sp)
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/ui/page/ReceiptProductScreen.kt b/app/src/main/java/com/sw/inbound/ui/page/ReceiptProductScreen.kt
new file mode 100644
index 0000000..b87ba8b
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/ui/page/ReceiptProductScreen.kt
@@ -0,0 +1,613 @@
+package com.sw.inbound.ui.page
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Tab
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.LocalLifecycleOwner
+import androidx.navigation.NavController
+import androidx.navigation.compose.rememberNavController
+import com.sw.inbound.GlobalData
+import com.sw.inbound.R
+import com.sw.inbound.ext.bold
+import com.sw.inbound.ext.toSafeFloat
+import com.sw.inbound.ext.withSize
+import com.sw.inbound.model.request.UploadInfo
+import com.sw.inbound.model.response.DictType
+import com.sw.inbound.model.response.GoodsInfo
+import com.sw.inbound.ui.theme.AppTypography
+import com.sw.inbound.ui.theme.AppTypography.black141428TextStyle
+import com.sw.inbound.ui.theme.AppTypography.gray96a0aaTextStyle
+import com.sw.inbound.ui.weight.BottomActionBar
+import com.sw.inbound.ui.weight.CustomDropdownTextField
+import com.sw.inbound.ui.weight.CustomSpinner
+import com.sw.inbound.ui.weight.CustomTextField
+import com.sw.inbound.ui.weight.IdentityView
+import com.sw.inbound.ui.weight.InputType
+import com.sw.inbound.ui.weight.ProductListView
+import com.sw.inbound.ui.weight.RowInputLayout
+import com.sw.inbound.ui.weight.TopTitleBar
+import com.sw.inbound.ui.weight.dialog.ReceiptTipDialog
+import com.sw.inbound.viewmodel.ReceiptViewModel
+import timber.log.Timber
+
+@Composable
+fun ReceiptProductScreen(
+ modifier: Modifier = Modifier,
+ controller: NavController = rememberNavController(),
+ id: Int,
+ supplierId: Int,
+ viewModel: ReceiptViewModel = hiltViewModel()
+) {
+ val adjustState by viewModel.adjustState.collectAsState()
+ val showDialog by viewModel.showReceiptDialog.collectAsState()
+ val orders by viewModel.orders.collectAsState()
+ val receiptResult by viewModel.receiptResult.collectAsState()
+
+ LaunchedEffect(receiptResult) {
+ if (receiptResult) {
+ controller.popBackStack()
+ }
+ }
+
+ Column(modifier = modifier) {
+ TopTitleBar()
+ Row(
+ modifier = modifier
+ .padding(30.dp)
+ .weight(1f)
+ ) {
+ Row(
+ modifier = modifier
+ .background(
+ color = Color(0x80FFFFFF),
+ shape = RoundedCornerShape(12.dp)
+ )
+ .fillMaxWidth()
+ .padding(30.dp)
+ ) {
+ // 左侧列表
+ ReceiptLeftView(modifier.weight(1f), viewModel, id)
+
+ // 未调整,显示右侧界面
+ if (!adjustState) {
+ Spacer(modifier = Modifier.width(30.dp))
+ // 右侧编辑
+ ReceiptRightView(modifier.width(580.dp), viewModel)
+ }
+ }
+ }
+ BottomActionBar(
+ modifier = Modifier,
+ right2ButtonText = "确认收货",
+ onLeftButtonClick = {
+ controller.popBackStack()
+ },
+ onRight2ButtonClick = {
+ viewModel.updateReceiptDialog(true)
+ })
+ }
+
+ if (showDialog) {
+ ReceiptTipDialog(isWarn = viewModel.hasWrongCount(), onCancelClick = {
+ Timber.d("返回")
+ viewModel.updateReceiptDialog(false)
+ }, onConfirmClick = { isFull ->
+ Timber.d(if (isFull) "确认收货" else "部分收货")
+ viewModel.updateReceiptDialog(false)
+ val uploadInfo = UploadInfo(id = id, receiveGoodsInfos = orders)
+ if (isFull) {
+ viewModel.confirmReceipt(uploadInfo)
+ } else {
+ uploadInfo.supplierId = supplierId
+ viewModel.partialReceipt(uploadInfo)
+ }
+
+ })
+ }
+}
+
+@Composable
+fun ReceiptLeftView(
+ modifier: Modifier,
+ viewModel: ReceiptViewModel,
+ id: Int
+) {
+ val warehouseTypeList = GlobalData.warehouseTypeList // receiptViewModel.getStoreList()
+ val currentPurchaseInfo by viewModel.currentPurchaseInfo.collectAsState()
+
+ LaunchedEffect(id) {
+ viewModel.getReceiveDetail(id)
+ }
+
+ currentPurchaseInfo?.let {
+ Column(modifier) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Column {
+ Text(
+ "供应商:${currentPurchaseInfo?.supplierName}",
+ style = black141428TextStyle.bold()
+ )
+ Spacer(modifier = Modifier.height(18.dp))
+ Row {
+ Text(
+ "采购单号:${currentPurchaseInfo?.purCode}",
+ style = gray96a0aaTextStyle.copy(fontSize = 20.sp)
+ )
+ Spacer(modifier = Modifier.width(120.dp))
+ Text(
+ "收货单号:${currentPurchaseInfo?.receiveCode}",
+ style = gray96a0aaTextStyle.copy(fontSize = 20.sp)
+ )
+ }
+ }
+ CustomSpinner(
+ items = warehouseTypeList,
+ selectedItem = currentPurchaseInfo!!.warehouseName,
+ onItemSelected = { value ->
+ val currentPurchaseInfo1 = currentPurchaseInfo!!.copy()
+ currentPurchaseInfo1.warehouseName = value.value
+ viewModel.updateCurrentPurchaseInfo(currentPurchaseInfo1)
+ viewModel.updateAllPurchaseStore(store = value)
+ })
+ }
+ Spacer(modifier = Modifier.height(30.dp))
+
+ TabScreen(viewModel)
+// LazyColumn(
+// verticalArrangement = Arrangement.spacedBy(30.dp)
+// ) {
+// items(items = purchaseOrderList, key = { it.id }) {
+// ReceiptItem(it, modifier)
+// }
+// }
+ }
+ }
+}
+
+@Composable
+fun TabScreen(viewModel: ReceiptViewModel) {
+ var selectedTabIndex by remember { mutableIntStateOf(0) }
+
+ val purchaseUnadjustedList by viewModel.unadjustedOrders.collectAsState()
+ val purchaseAdjustList by viewModel.adjustedOrders.collectAsState()
+
+ val tabs =
+ listOf("未调整(${purchaseUnadjustedList.size})", "已调整(${purchaseAdjustList.size})")
+
+ Column(
+ modifier = Modifier
+ .background(
+ color = Color.White.copy(alpha = 0.37f),
+ shape = RoundedCornerShape(10.dp) // 圆角背景
+ )
+ .fillMaxSize()
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 30.dp), // 左上角对齐
+ horizontalArrangement = Arrangement.Start,
+
+ ) {
+ tabs.forEachIndexed { index, title ->
+ val selected = selectedTabIndex == index
+ Tab(
+ selected = selected,
+ onClick = { selectedTabIndex = index },
+ modifier = Modifier
+ .width(150.dp)
+// .wrapContentWidth()
+ .padding(start = if (index == 1) 30.dp else 0.dp),
+ selectedContentColor = Color(0xFF0032C8),
+ unselectedContentColor = Color(0xFF14141E)
+ ) {
+ Text(
+ text = title,
+ style = if (selected) AppTypography.BlueTextStyle else AppTypography.black141428TextStyle.bold(),
+ textAlign = TextAlign.Center,
+ modifier = Modifier.padding(vertical = 12.dp)
+ )
+ }
+ }
+ }
+
+ HorizontalDivider(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 30.dp),
+ thickness = 1.dp,
+ color = colorResource(R.color.divider)
+ )
+
+ when (selectedTabIndex) {
+ // 未调整列表
+ 0 -> {
+ UnadjustedView(purchaseUnadjustedList, viewModel)
+ viewModel.updateAdjustState(false)
+ }
+ // 已调整列表
+ 1 -> {
+ AdjustedView(viewModel)
+ viewModel.updateAdjustState(true)
+ }
+ }
+ }
+}
+
+@Composable
+fun UnadjustedView(purchaseOrderList: List, viewModel: ReceiptViewModel) {
+ val checkedItem by viewModel.selectedItem.collectAsState()
+ // 未调整view
+ ProductListView(
+ modifier = Modifier.padding(horizontal = 30.dp),
+ productList = purchaseOrderList,
+ checkedItem = checkedItem,
+ onItemCheckedClick = {
+ viewModel.updateSelectedItem(it)
+ })
+}
+
+@Composable
+fun AdjustedView(viewModel: ReceiptViewModel) {
+ // 已调整view
+ val purchaseAdjustList by viewModel.adjustedOrders.collectAsState()
+
+ LazyColumn(
+ verticalArrangement = Arrangement.spacedBy(0.dp)
+ ) {
+ items(items = purchaseAdjustList, key = { it.goodId!! }) {
+// ReceiptItem(it)
+ AdjustedViewItem(it, viewModel)
+ }
+ }
+}
+
+@Composable
+private fun AdjustedViewItem(purchaseOrder: GoodsInfo, viewModel: ReceiptViewModel) {
+ val warehouseTypeList = GlobalData.warehouseTypeList // viewModel.getStoreList()
+
+ Column(modifier = Modifier.padding(horizontal = 30.dp)) {
+ Spacer(modifier = Modifier.height(30.dp))
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 30.dp),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(purchaseOrder.goodNameStr, style = AppTypography.blackTextStyle)
+ Row {
+ Text("采购量:", style = gray96a0aaTextStyle)
+ Text(
+ "${purchaseOrder.receiveCountStr}${purchaseOrder.unitNameStr}",
+ style = black141428TextStyle
+ )
+ }
+ }
+ Spacer(modifier = Modifier.height(30.dp))
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 30.dp)
+ ) {
+ AdjustedRowInputLayout(
+ label = "单价", value = purchaseOrder.recUnitPriceTaxInStr,
+ onValueChange = {
+ viewModel.updatePurchaseItem(
+ purchaseOrder = purchaseOrder.copy(
+ recUnitPriceTaxIn = it.toSafeFloat()
+ )
+ )
+ }, inputType = InputType.Decimal
+ )
+ Spacer(modifier = Modifier.width(12.dp))
+ Text(text = "元", style = AppTypography.gray96a0aaTextStyle)
+ Spacer(modifier = Modifier.width(50.dp))
+ Text(text = "x", style = AppTypography.black141428TextStyle.bold())
+ Spacer(modifier = Modifier.width(50.dp))
+ AdjustedRowInputLayout(
+ label = "实收", value = purchaseOrder.receivedNumStr,
+ onValueChange = {
+ viewModel.updatePurchaseItem(
+ purchaseOrder = purchaseOrder.copy(
+ receivedNum = it.toSafeFloat()
+ )
+ )
+ }, inputType = InputType.Decimal
+ )
+ Spacer(modifier = Modifier.width(12.dp))
+ Text(text = purchaseOrder.unitNameStr, style = AppTypography.gray96a0aaTextStyle)
+ Spacer(modifier = Modifier.width(50.dp))
+ Text(text = "=", style = AppTypography.black141428TextStyle.bold())
+ Spacer(modifier = Modifier.width(50.dp))
+ AdjustedRowInputLayout(
+ label = "金额", value = purchaseOrder.recPriceExItemStr,
+ onValueChange = {
+ viewModel.updatePurchaseItem(
+ purchaseOrder = purchaseOrder.copy(
+ recPriceExItem = it.toSafeFloat()
+ )
+ )
+ }, inputType = InputType.Decimal
+ )
+ Spacer(modifier = Modifier.width(12.dp))
+ Text(text = "元", style = AppTypography.gray96a0aaTextStyle)
+
+ Spacer(modifier = Modifier.weight(1f))
+
+ AdjustedRowInputLayout(
+ label = "仓库",
+ value = purchaseOrder.warehouseNameStr,
+ dropdownItems = warehouseTypeList,
+ onValueChange = { value ->
+ purchaseOrder.let {
+ viewModel.updatePurchaseItem(
+ purchaseOrder.copy(
+ warehouseName = value,
+ warehouseId = warehouseTypeList.find { it.value == value }?.id ?: 0
+ )
+ )
+ }
+ })
+ }
+ Spacer(modifier = Modifier.height(30.dp))
+ HorizontalDivider(thickness = 1.dp, color = colorResource(R.color.divider))
+ }
+}
+
+@Composable
+private fun AdjustedRowInputLayout(
+ label: String,
+ value: String,
+ onValueChange: (String) -> Unit,
+ dropdownItems: List = emptyList(),
+ textAlign: TextAlign = TextAlign.Center,
+ spinnerTextAlign: TextAlign = TextAlign.Start,
+ trailingLabel: String? = null,
+ inputType: InputType = InputType.Text
+) {
+ Row(
+ modifier = Modifier
+ .height(60.dp)
+// .fillMaxWidth()
+// .padding(horizontal = 30.dp)
+ ,
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(label, style = AppTypography.gray96a0aaTextStyle)
+ Spacer(modifier = Modifier.width(12.dp))
+ if (dropdownItems.isEmpty()) {
+ CustomTextField(
+ modifier = Modifier.width(180.dp),
+ value = value,
+ onValueChange = onValueChange,
+ textAlign = textAlign,
+ trailingLabel = trailingLabel, inputType = inputType
+ )
+ } else {
+ CustomDropdownTextField(
+ modifier = Modifier.width(300.dp),
+ value = value,
+ dropdownItems = dropdownItems,
+ onValueChange = onValueChange,
+ textAlign = spinnerTextAlign
+ )
+ }
+ }
+}
+
+@Composable
+fun ReceiptRightView(modifier: Modifier = Modifier, viewModel: ReceiptViewModel) {
+ val searchResultList by viewModel.searchListItems.collectAsState()
+ val selectedItem by viewModel.selectedItem.collectAsState()
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .background(
+ color = Color.White.copy(alpha = 0.37f),
+ shape = RoundedCornerShape(10.dp) // 圆角背景
+ )
+ .padding(horizontal = 30.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ if (selectedItem == null) {
+ Spacer(modifier = Modifier.height(30.dp))
+ IdentityView(list = searchResultList, showSearchView = false, onOptionSelected = {})
+ } else {
+ ReceiptProductEditView(viewModel)
+ }
+ }
+}
+
+/**
+ * 收货界面右侧编辑
+ */
+@Composable
+private fun ReceiptProductEditView(
+ viewModel: ReceiptViewModel,
+ onConfirmClick: () -> Unit = {},
+ onCancelClick: () -> Unit = {}
+) {
+ val selectedItem by viewModel.selectedItem.collectAsState()
+ val warehouseTypeList = GlobalData.warehouseTypeList
+ val lifecycleOwner = LocalLifecycleOwner.current
+
+ DisposableEffect(lifecycleOwner) {
+ Timber.d("开始传感器采集")
+ viewModel.startSensorScale()
+ onDispose {
+ Timber.d("停止传感器采集")
+ viewModel.stopSensorScale()
+ }
+ }
+
+ selectedItem?.let {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .fillMaxHeight()
+ .padding(top = 20.dp, bottom = 20.dp),
+ verticalArrangement = Arrangement.spacedBy(20.dp)
+ ) {
+ Spacer(modifier = Modifier.height(10.dp))
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ textAlign = TextAlign.Center,
+ text = selectedItem?.goodName ?: "-",
+ style = AppTypography.black141428TextStyle.bold().withSize(30.sp),
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ Spacer(modifier = Modifier.height(20.dp))
+
+ HorizontalDivider(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(),
+ thickness = 1.dp,
+ color = colorResource(R.color.divider)
+ )
+
+ RowInputLayout(
+ label = "仓库",
+ value = selectedItem!!.warehouseNameStr,
+ dropdownItems = warehouseTypeList,
+ onValueChange = { value ->
+ selectedItem?.let {
+ viewModel.updateSelectedItem(
+ selectedItem!!.copy(
+ warehouseName = value,
+ warehouseId =
+ warehouseTypeList.find { it.value == value }?.id ?: 0
+// storeList.indexOf(value)
+ )
+ )
+ }
+ })
+ HorizontalDivider(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(),
+ thickness = 1.dp,
+ color = colorResource(R.color.divider)
+ )
+
+ RowInputLayout(
+ label = "收货数量",
+ value = selectedItem!!.receivedNumStr,
+ onValueChange = { value ->
+ viewModel.updateCountInputState(value.isNotEmpty())
+ selectedItem?.let {
+ viewModel.updateSelectedItem(selectedItem!!.copy(receivedNum = value.toSafeFloat()))
+ }
+ },
+ trailingLabel = selectedItem?.unitName,
+ inputType = InputType.Decimal,
+ isInitUpdate = true
+ )
+
+ RowInputLayout(
+ label = "收货单价",
+ value = selectedItem!!.recUnitPriceTaxInStr,
+ trailingLabel = "元",
+ onValueChange = { value ->
+ selectedItem?.let {
+ viewModel.updateSelectedItem(selectedItem!!.copy(recUnitPriceTaxIn = value.toSafeFloat()))
+ }
+ }, inputType = InputType.Decimal
+ )
+ RowInputLayout(
+ label = "收货金额",
+ value = selectedItem!!.recPriceExItemStr,
+ trailingLabel = "元",
+ onValueChange = { value ->
+ selectedItem?.let {
+ viewModel.updateSelectedItem(selectedItem!!.copy(recPriceExItem = value.toSafeFloat()))
+ }
+ }, inputType = InputType.Decimal
+ )
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 30.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(text = "物品重量", style = AppTypography.black141428TextStyle)
+ CustomTextField(
+ modifier = Modifier
+ .width(290.dp)
+ .height(60.dp),
+ value = selectedItem!!.goodsWeightStr, onValueChange = {},
+ enabled = false,
+ isInitUpdate = true,
+ textAlign = TextAlign.End,
+ trailingLabel = selectedItem!!.goodsWeightUnitStr,
+ inputType = InputType.Decimal
+ )
+ }
+
+ HorizontalDivider(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(),
+ thickness = 1.dp,
+ color = colorResource(R.color.divider)
+ )
+ Spacer(modifier = Modifier.height(30.dp))
+ Row(
+ horizontalArrangement = Arrangement.SpaceAround,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(80.dp)
+ ) {
+ Image(modifier = Modifier.clickable {
+ viewModel.updateSelectedItem(null)
+ }, painter = painterResource(R.mipmap.ic_btn_back), contentDescription = "返回")
+ Image(modifier = Modifier.clickable {
+ viewModel.updatePurchaseItem(purchaseOrder = selectedItem!!.copy(isAdjusted = true))
+ viewModel.updateSelectedItem(null)
+ }, painter = painterResource(R.mipmap.ic_btn_confirm), contentDescription = "确定")
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/ui/page/SelfProcurementScreen.kt b/app/src/main/java/com/sw/inbound/ui/page/SelfProcurementScreen.kt
new file mode 100644
index 0000000..de32c0c
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/ui/page/SelfProcurementScreen.kt
@@ -0,0 +1,758 @@
+package com.sw.inbound.ui.page
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+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.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+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.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.window.Dialog
+import androidx.compose.ui.window.DialogProperties
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.LocalLifecycleOwner
+import androidx.navigation.NavController
+import androidx.navigation.compose.rememberNavController
+import com.sw.inbound.GlobalData
+import com.sw.inbound.R
+import com.sw.inbound.ext.bold
+import com.sw.inbound.ext.dashedBorder
+import com.sw.inbound.ext.isValidAmount
+import com.sw.inbound.ext.medium
+import com.sw.inbound.ext.toFormattedString
+import com.sw.inbound.ext.toSafeBigDecimal
+import com.sw.inbound.ext.toSafeDouble
+import com.sw.inbound.ext.toSafeFloat
+import com.sw.inbound.ext.withColor
+import com.sw.inbound.ext.withSize
+import com.sw.inbound.model.request.PurchaseWarehouseParam
+import com.sw.inbound.model.response.DictType
+import com.sw.inbound.ui.theme.AppTypography
+import com.sw.inbound.ui.theme.Black_141428
+import com.sw.inbound.ui.weight.BottomActionBar
+import com.sw.inbound.ui.weight.CameraCaptureLayout
+import com.sw.inbound.ui.weight.CustomButton
+import com.sw.inbound.ui.weight.CustomDropdownTextField
+import com.sw.inbound.ui.weight.CustomOutlinedButton
+import com.sw.inbound.ui.weight.CustomSpinner
+import com.sw.inbound.ui.weight.CustomTextField
+import com.sw.inbound.ui.weight.IdentityView
+import com.sw.inbound.ui.weight.InputType
+import com.sw.inbound.ui.weight.RowInputLayout
+import com.sw.inbound.ui.weight.SelfProcurementListItem
+import com.sw.inbound.ui.weight.TopTitleBar
+import com.sw.inbound.utils.ToastUtils
+import com.sw.inbound.viewmodel.SelfProcurementViewModel
+import timber.log.Timber
+
+@Preview(
+ widthDp = 1920,
+ heightDp = 1080,
+ showBackground = true
+)
+@Composable
+fun SelfProcurementScreen(
+ modifier: Modifier = Modifier,
+ controller: NavController = rememberNavController(),
+ viewModel: SelfProcurementViewModel = hiltViewModel()
+) {
+ val showDialog by viewModel.showAddProductDialog.collectAsState()
+ val addToWarehouseResult by viewModel.addToWarehouseResult.collectAsState()
+
+ LaunchedEffect(addToWarehouseResult) {
+ if (addToWarehouseResult) {
+ controller.popBackStack()
+ }
+ }
+
+ Column(modifier = modifier) {
+ TopTitleBar(title = "自采入库")
+ Box(
+ modifier = Modifier
+ .weight(1f)
+ .padding(horizontal = 20.dp)
+ ) {
+ Image(
+ painter = painterResource(id = R.mipmap.bg_listview),
+ contentDescription = "背景",
+ modifier = Modifier
+ .width(1900.dp)
+ .height(880.dp),
+ contentScale = ContentScale.FillBounds
+ )
+ ContentView(viewModel)
+ }
+ BottomActionBar(onLeftButtonClick = {
+ controller.popBackStack()
+ }, showRight1Button = true, right1ButtonText = "清空物品", onRight1ButtonClick = {
+ viewModel.clearPurchaseOrders()
+ }, right2ButtonText = "提交入库", onRight2ButtonClick = {
+ viewModel.addToWarehouse()
+ })
+ }
+
+ if (showDialog) {
+ AddProductDialog(
+ onDismiss = { viewModel.updateAddProductDialog(false) },
+ modifier = Modifier,
+ onCancelClick = {
+ viewModel.updateAddProductDialog(false)
+ viewModel.cleanGoodsAddParam()
+ },
+ onConfirmClick = {
+ viewModel.addGoodsInfo()
+ }
+ ) {
+ // 弹窗内容
+ DialogContentView(modifier, viewModel)
+ }
+ }
+}
+
+@Composable
+private fun ContentView(
+ viewModel: SelfProcurementViewModel,
+) {
+ Row(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(20.dp)
+ ) {
+ ContentLeftView(viewModel)
+ ContentRightView(modifier = Modifier.weight(1f), viewModel)
+ }
+}
+
+@Composable
+private fun ContentLeftView(
+ viewModel: SelfProcurementViewModel
+) {
+ val warehouseList = GlobalData.warehouseTypeList // productViewModel.getStoreList()
+ val productList by viewModel.purchaseList.collectAsState()
+ val globalWarehouseName by viewModel.globalWarehouse.collectAsState() // remember { mutableStateOf("选择仓库") }
+
+ Column(
+ modifier = Modifier
+ .width(793.dp)
+ .padding(30.dp)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(text = "物品(项)", style = AppTypography.black141428TextStyle.bold())
+ CustomSpinner(
+ modifier = Modifier
+ .height(70.dp)
+ .width(240.dp),
+ items = warehouseList,
+ selectedItem = globalWarehouseName.value,
+ onItemSelected = {
+// globalWarehouseName = it
+ viewModel.updateGlobalWarehouse(it)
+ })
+ }
+ Spacer(modifier = Modifier.height(30.dp))
+ SelfProcurementListItem(
+ modifier = Modifier
+ .fillMaxHeight()
+ .background(
+ color = Color.White.copy(alpha = 0.37f),
+ shape = RoundedCornerShape(10.dp) // 圆角背景
+ ), productList = productList, showClose = true, onCloseClick = {
+ viewModel.removePurchaseOrder(it.goodsId)
+ })
+ }
+}
+
+@Composable
+fun ContentRightView(
+ modifier: Modifier,
+ viewModel: SelfProcurementViewModel
+) {
+ Row(
+ modifier = modifier
+ .width(1037.dp)
+ .fillMaxHeight()
+ .padding(top = 30.dp, end = 30.dp, bottom = 30.dp)
+ .background(
+ color = Color.White.copy(alpha = 0.37f),
+ shape = RoundedCornerShape(10.dp) // 圆角背景
+ )
+ ) {
+ // 商品识别
+ ProductIdentification(viewModel)
+ Image(
+ painter = painterResource(R.mipmap.bg_divider),
+ modifier = Modifier
+ .fillMaxHeight()
+ .width(30.dp),
+ contentDescription = ""
+ )
+ SelfProductEditView(viewModel)
+ }
+}
+
+@Composable
+fun ProductIdentification(viewModel: SelfProcurementViewModel) {
+ val searchResultList by viewModel.searchListItems.collectAsState()
+ val selectItem by viewModel.selectedItem.collectAsState()
+ Column(
+ modifier = Modifier
+ .width(457.dp)
+ .fillMaxHeight()
+ .padding(30.dp)
+ ) {
+ Box(
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxWidth()
+ ) {
+ // 菜品识别
+ IdentityView(list = searchResultList, showSearchView = true, onSearchClick = {
+ viewModel.searchGoodsInfoList(it)
+ }, onOptionSelected = {
+ viewModel.updateSelectedItemWithSearch(it)
+ })
+ }
+ Spacer(modifier = Modifier.height(10.dp))
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(40.dp)
+ .clickable {
+ viewModel.updateAddProductDialog(true)
+ },
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center
+ ) {
+ Image(painter = painterResource(R.mipmap.ic_add), contentDescription = "添加")
+ Spacer(modifier = Modifier.width(11.dp))
+ Text(text = "快速添加", style = AppTypography.BlueTextStyle.medium())
+ }
+ }
+}
+
+@Composable
+private fun DialogContentView(
+ modifier: Modifier,
+ viewModel: SelfProcurementViewModel
+) {
+ Column(
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = modifier.fillMaxSize()
+ ) {
+ Text(
+ text = "新增物品", style = TextStyle().bold().withSize(36.sp).withColor(
+ Black_141428
+ )
+ )
+ Spacer(modifier = Modifier.height(56.dp))
+ HorizontalDivider(thickness = 1.dp, color = colorResource(R.color.divider))
+ Spacer(modifier = Modifier.height(58.dp))
+ Row(modifier = Modifier.fillMaxWidth()) {
+ // 采集窗口
+ CameraCaptureLayout(modifier = Modifier.width(428.dp), onCancelClick = {
+
+ }, onConfirmClick = {
+
+ })
+ Spacer(modifier = Modifier.width(60.dp))
+ // 右侧输入窗口
+
+ DialogRightEditView(viewModel)
+ }
+ Spacer(modifier = Modifier.height(60.dp))
+ HorizontalDivider(thickness = 1.dp, color = colorResource(R.color.divider))
+ }
+}
+
+@Composable
+private fun DialogRightEditView(viewModel: SelfProcurementViewModel) {
+ val itemTypeList = GlobalData.goodsTypeList
+ val unitTypeList = GlobalData.unitTypeList
+ val storageTypeList = GlobalData.storageTypeList
+ val goodsAddParam by viewModel.goodsAddParam.collectAsState()
+
+ Column(
+ modifier = Modifier
+ .fillMaxHeight()
+ .verticalScroll(rememberScrollState()),
+ verticalArrangement = Arrangement.spacedBy(29.dp)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(60.dp)
+ ) {
+ ColumnInputText(
+ modifier = Modifier
+ .width(474.dp),
+ label = "物品名称",
+ value = goodsAddParam.goodNameSr,
+ onValueChange = {
+ viewModel.updateGoodsAddParam(goodsAddParam.copy(goodName = it))
+ })
+ ColumnInputText(
+ modifier = Modifier.width(474.dp),
+ label = "物品编码(服务生成)",
+ value = "",
+ onValueChange = {
+// viewModel.updateFormState(formState.copy(goodCode = it))
+ })
+ }
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(60.dp)
+ ) {
+ ColumnInputText(
+ modifier = Modifier
+ .weight(1f)
+ .width(474.dp),
+ label = "物品类型",
+ dropdownItems = itemTypeList,
+ value = goodsAddParam.goodsTypeStr,
+ onValueChange = { value ->
+ viewModel.updateGoodsAddParam(
+ goodsAddParam.copy(
+ goodType =
+ itemTypeList.find { it.value == value }?.id ?: 0
+ )
+ )
+ }
+ )
+ ColumnInputText(
+ modifier = Modifier.width(474.dp),
+ label = "储存方式",
+ dropdownItems = storageTypeList,
+ value = goodsAddParam.storageTypeStr,
+ onValueChange = { value ->
+ viewModel.updateGoodsAddParam(
+ goodsAddParam.copy(
+ storageType = storageTypeList.find { it.value == value }?.id ?: 0
+ )
+ )
+ })
+ }
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(60.dp)
+ ) {
+ ColumnInputText(
+ modifier = Modifier
+ .weight(1f)
+ .width(474.dp),
+ label = "净材率",
+ value = goodsAddParam.netRateStr,
+ inputType = InputType.Decimal,
+ onValueChange = {
+ viewModel.updateGoodsAddParam(goodsAddParam.copy(netRate = it.toSafeFloat()))
+ })
+ ColumnInputText(
+ modifier = Modifier.width(474.dp),
+ value = goodsAddParam.unitIdStr,
+ label = "库存单位",
+ dropdownItems = unitTypeList,
+ onValueChange = { value ->
+ viewModel.updateGoodsAddParam(
+ goodsAddParam.copy(
+ unitId = unitTypeList.find { it.value == value }?.id ?: 0
+ )
+ )
+ },
+ )
+ }
+
+ Column {
+ Text(text = "采购单位", style = AppTypography.black141428TextStyle)
+ Spacer(modifier = Modifier.height(20.dp))
+ Row(
+ modifier = Modifier
+ .height(92.dp)
+ .fillMaxWidth()
+ .dashedBorder()
+ .padding(vertical = 16.dp, horizontal = 26.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text("1", style = AppTypography.black141428TextStyle.bold())
+ Spacer(modifier = Modifier.width(24.dp))
+ CustomDropdownTextField(
+ modifier = Modifier.width(200.dp),
+ value = goodsAddParam.purchaseUnitStr,
+ dropdownItems = unitTypeList,
+ placeholderValue = "请选择",
+ onValueChange = { value ->
+ viewModel.updateGoodsAddParam(
+ goodsAddParam.copy(
+ purchaseUnit = unitTypeList.find { it.value == value }?.id
+ ?: 0
+ )
+ )
+ })
+ Spacer(modifier = Modifier.width(24.dp))
+ Text(text = "=", style = AppTypography.gray96a0aaTextStyle)
+ Spacer(modifier = Modifier.width(24.dp))
+ CustomTextField(
+ modifier = Modifier.width(200.dp),
+ value = goodsAddParam.purchaseValue.toFormattedString(),
+ placeholderValue = "请录入",
+ onValueChange = {
+ viewModel.updateGoodsAddParam(goodsAddParam.copy(purchaseValue = it.toSafeFloat()))
+ }, inputType = InputType.Decimal
+ )
+ Spacer(modifier = Modifier.width(24.dp))
+ Text(goodsAddParam.unitIdStr, style = AppTypography.gray96a0aaTextStyle)
+
+ Spacer(modifier = Modifier.width(111.dp))
+
+ Text("单价", style = AppTypography.black141428TextStyle)
+ Spacer(modifier = Modifier.width(24.dp))
+ CustomTextField(
+ modifier = Modifier.width(200.dp),
+ placeholderValue = "请录入",
+ value = goodsAddParam.purchasePriceStr, onValueChange = {
+ if (it.isValidAmount()) {
+ viewModel.updateGoodsAddParam(goodsAddParam.copy(purchasePrice = it.toSafeBigDecimal()))
+ }
+ }, inputType = InputType.Decimal, hasNext = false
+ )
+ Spacer(modifier = Modifier.width(24.dp))
+ Text("元", style = AppTypography.gray96a0aaTextStyle)
+ }
+ }
+ }
+}
+
+
+@Composable
+fun ColumnInputText(
+ modifier: Modifier = Modifier,
+ label: String,
+ value: String,
+ onValueChange: (String) -> Unit = {},
+ dropdownItems: List = emptyList(),
+ trailingLabel: String? = null, inputType: InputType = InputType.Text
+) {
+ val placeholder = if (dropdownItems.isEmpty()) "请录入" else "请选择"
+ Column(modifier = modifier) {
+ Text(label, style = AppTypography.black141428TextStyle)
+ Spacer(modifier = Modifier.height(20.dp))
+ if (dropdownItems.isEmpty()) {
+ CustomTextField(
+ value = value,
+ onValueChange = onValueChange,
+ placeholderValue = placeholder,
+ trailingLabel = trailingLabel,
+ inputType = inputType
+ )
+ } else {
+ CustomDropdownTextField(
+ value = value,
+ onValueChange = onValueChange,
+ dropdownItems = dropdownItems,
+ placeholderValue = placeholder,
+ )
+ }
+ }
+}
+
+@Composable
+fun AddProductDialog(
+ modifier: Modifier = Modifier,
+ onCancelClick: () -> Unit,
+ onConfirmClick: () -> Unit,
+ onDismiss: () -> Unit,
+ content: @Composable () -> Unit
+) {
+ Dialog(
+ onDismissRequest = onDismiss,
+ properties = DialogProperties(
+ usePlatformDefaultWidth = false, // 不使用平台默认宽度
+ decorFitsSystemWindows = false // 允许内容延伸到系统窗口后面
+ )
+ ) {
+
+ Surface(
+ modifier = Modifier
+ .padding(start = 60.dp, end = 60.dp, bottom = 40.dp) // 设置弹窗距离边框60dp
+ .wrapContentSize(),
+ shape = RoundedCornerShape(30.dp),
+ color = Color.White,
+ shadowElevation = 0.dp
+ ) {
+ ToastUtils.ToastComposable()
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(start = 152.dp, end = 152.dp, top = 56.dp, bottom = 30.dp),
+ ) {
+ // 主要内容区域
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f)
+ ) {
+ content()
+ }
+ Spacer(modifier = Modifier.height(30.dp))
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.End
+ ) {
+ CustomOutlinedButton(
+ modifier = modifier
+ .width(300.dp)
+ .height(100.dp),
+ text = "取消",
+ fontSize = 36.sp,
+ onClick = onCancelClick
+ )
+ Spacer(modifier = Modifier.width(20.dp))
+
+ CustomButton(
+ modifier = modifier
+ .width(300.dp)
+ .height(100.dp),
+ text = "确定",
+ onClick = onConfirmClick,
+ borderColor = colorResource(R.color.blue),
+ textColor = colorResource(R.color.white),
+ fontSize = 36.sp
+ )
+ }
+ }
+ }
+ }
+}
+
+/**
+ * 商品编辑
+ */
+@Composable
+private fun SelfProductEditView(
+ viewModel: SelfProcurementViewModel,
+) {
+ val selectedItem by viewModel.selectedItem.collectAsState()
+ val storeList = GlobalData.warehouseTypeList
+ var unitTypeList = emptyList() // 界面中会重新获取
+ val lifecycleOwner = LocalLifecycleOwner.current
+
+ LaunchedEffect(selectedItem) {
+ Timber.d("界面显示")
+ if (selectedItem == null) {
+ viewModel.updateSelectedItem(purchaseOrder = PurchaseWarehouseParam())
+ }
+ }
+
+ DisposableEffect(lifecycleOwner) {
+ Timber.d("开始传感器采集")
+ viewModel.startSensorScale()
+ onDispose {
+ Timber.d("停止传感器采集")
+ viewModel.stopSensorScale()
+ }
+ }
+
+ selectedItem?.let {
+ unitTypeList = selectedItem!!.unitDictTypeList
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .fillMaxHeight()
+ .padding(top = 20.dp, bottom = 20.dp, end = 20.dp),
+ verticalArrangement = Arrangement.spacedBy(20.dp)
+ ) {
+ Spacer(modifier = Modifier.height(10.dp))
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ textAlign = TextAlign.Center,
+ text = selectedItem!!.goodsNameStr,
+ style = AppTypography.black141428TextStyle.bold().withSize(30.sp)
+ )
+ Spacer(modifier = Modifier.height(20.dp))
+
+ RowInputLayout(
+ label = "仓库",
+ value = selectedItem!!.warehouseNameStr,
+ dropdownItems = storeList,
+ onValueChange = { value ->
+ selectedItem.let {
+ viewModel.updateSelectedItem(
+ selectedItem!!.copy(
+// warehouseName = value,
+ warehouseId = storeList.find { it.value == value }?.id ?: 0
+ )
+ )
+ }
+ })
+
+ RowInputLayout(
+ label = "采购单位",
+ value = selectedItem!!.unitNameStr,
+ dropdownItems = unitTypeList,
+ onValueChange = { value ->
+ val selectUnitType =
+ selectedItem!!.unitList!!.find { it.purchaseUnitName == value }
+ if (selectUnitType != null) {
+ viewModel.updateSelectedItem(
+ selectedItem!!.copy(
+ unitName = value,
+ buyToInventoryValue = selectUnitType.buyToInventoryValue ?: "",
+ goodPurId = selectUnitType.businessUnitId?.toInt() ?: 0,
+ selectUnitType = selectUnitType
+ )
+ )
+ }
+ })
+
+ RowInputLayout(
+ label = "采购数量",
+ value = selectedItem!!.goodsCountStr,
+ inputType = InputType.Decimal,
+ isInitUpdate = true,
+ onValueChange = { value ->
+ Timber.d("采购数量 onValueChange value = $value")
+ viewModel.updateCountInputState(value.isNotEmpty())
+ selectedItem.let {
+ viewModel.updateSelectedItem(selectedItem!!.copy(goodsCount = value.toSafeDouble()))
+ }
+ },
+ trailingLabel = selectedItem!!.unitName,
+ )
+
+ RowInputLayout(
+ label = "采购单价",
+ value = selectedItem!!.goodsUnitPriceStr,
+ inputType = InputType.Decimal,
+ trailingLabel = "元",
+ onValueChange = { value ->
+ selectedItem.let {
+ viewModel.updateSelectedItem(selectedItem!!.copy(goodsUnitPrice = value.toSafeDouble()))
+ }
+ })
+ RowInputLayout(
+ label = "采购金额",
+ value = selectedItem!!.goodsPriceStr,
+ inputType = InputType.Decimal,
+ trailingLabel = "元",
+ onValueChange = { value ->
+ selectedItem.let {
+ viewModel.updateSelectedItem(selectedItem!!.copy(goodsPrice = value.toSafeDouble()))
+ }
+ })
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 30.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(text = "物品重量", style = AppTypography.black141428TextStyle)
+ CustomTextField(
+ modifier = Modifier
+ .width(290.dp)
+ .height(60.dp),
+ value = selectedItem!!.goodsWeightStr,
+ onValueChange = { },
+ enabled = false,
+ isInitUpdate = true,
+ textAlign = TextAlign.End,
+ trailingLabel = selectedItem!!.goodsWeightUnitStr,
+ leadingIcon = {
+ Button(
+ onClick = {},
+ modifier = Modifier
+ .padding(0.dp)
+ .fillMaxHeight(),
+ shape = RoundedCornerShape(10.dp),
+ border = BorderStroke(2.dp, color = Color.White),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Color.White,
+ contentColor = Black_141428
+ ),
+ ) { Text(text = "累计", style = AppTypography.black141428TextStyle) }
+ }
+ )
+ }
+
+ Spacer(modifier = Modifier.height(40.dp))
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(30.dp),
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(80.dp)
+ ) {
+ CustomOutlinedButton(
+ modifier = Modifier
+ .width(245.dp)
+ .height(80.dp),
+ text = "取消",
+ fontSize = 30.sp,
+ textColor = colorResource(R.color.green),
+ borderColor = colorResource(R.color.green),
+ onClick = {
+ viewModel.updateSelectedItem(null)
+ })
+ CustomButton(
+ modifier = Modifier
+ .width(245.dp)
+ .height(80.dp),
+ text = "确定",
+ fontSize = 30.sp,
+ borderColor = colorResource(R.color.green),
+ onClick = {
+ if (selectedItem == null) return@CustomButton
+ val errInfo = selectedItem!!.hasNullField()
+ if (errInfo != null) {
+ ToastUtils.showToast(errInfo)
+ return@CustomButton
+ }
+ viewModel.addPurchaseItem(selectedItem!!)
+ })
+ }
+ }
+ }
+
+}
+
+
diff --git a/app/src/main/java/com/sw/inbound/ui/page/TestScreen.kt b/app/src/main/java/com/sw/inbound/ui/page/TestScreen.kt
new file mode 100644
index 0000000..d9230b6
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/ui/page/TestScreen.kt
@@ -0,0 +1,15 @@
+package com.sw.inbound.ui.page
+
+
+import android.annotation.SuppressLint
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.navigation.NavHostController
+
+
+@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
+@Composable
+fun TestScreen(modifier: Modifier.Companion, navController: NavHostController) {
+
+}
+
diff --git a/app/src/main/java/com/sw/inbound/ui/theme/AppTypography.kt b/app/src/main/java/com/sw/inbound/ui/theme/AppTypography.kt
new file mode 100644
index 0000000..512346a
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/ui/theme/AppTypography.kt
@@ -0,0 +1,49 @@
+package com.sw.inbound.ui.theme
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+import com.sw.inbound.R
+
+object AppTypography {
+ val gray96a0aaTextStyle: TextStyle
+ @Composable get() = TextStyle(
+ fontSize = 24.sp,
+ fontWeight = FontWeight.Medium,
+ color = Color(0xFF96A0AA)
+ )
+
+ val black141428TextStyle: TextStyle
+ @Composable get() = TextStyle(
+ fontSize = 24.sp,
+ fontWeight = FontWeight.Medium,
+ color = Black_141428
+ )
+
+ val grayTextStyle: TextStyle
+ @Composable
+ get() = TextStyle(
+ color = colorResource(R.color.gray),
+ fontSize = 24.sp,
+ fontWeight = FontWeight.Medium
+ )
+ val blackTextStyle: TextStyle
+ @Composable
+ get() = TextStyle(
+ color = colorResource(R.color.black),
+ fontSize = 24.sp,
+ fontWeight = FontWeight.Bold
+ )
+
+ val BlueTextStyle: TextStyle
+ @Composable
+ get() = TextStyle(
+ color = colorResource(R.color.blue),
+ fontSize = 24.sp,
+ fontWeight = FontWeight.Bold
+ )
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/ui/theme/Color.kt b/app/src/main/java/com/sw/inbound/ui/theme/Color.kt
new file mode 100644
index 0000000..7f40fb7
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/ui/theme/Color.kt
@@ -0,0 +1,21 @@
+package com.sw.inbound.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val Purple80 = Color(0xFFD0BCFF)
+val PurpleGrey80 = Color(0xFFCCC2DC)
+val Pink80 = Color(0xFFEFB8C8)
+
+val Purple40 = Color(0xFF6650a4)
+val PurpleGrey40 = Color(0xFF625b71)
+val Pink40 = Color(0xFF7D5260)
+
+val Black_141428 = Color(0xFF141428)
+val Gray_A0A0B4 = Color(0XffA0A0B4)
+val Gray_DCDCF0 = Color(0xFFDCDCF0)
+val Gray_96A0AA = Color(0xFF96A0AA)
+val GrayDivider = Color(0XFFDCDCF0)
+
+val Green_009632 = Color(0xFF009632)
+val Blue_0032C8 = Color(0xFF0032C8)
+
diff --git a/app/src/main/java/com/sw/inbound/ui/theme/Theme.kt b/app/src/main/java/com/sw/inbound/ui/theme/Theme.kt
new file mode 100644
index 0000000..f44a1ab
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/ui/theme/Theme.kt
@@ -0,0 +1,57 @@
+package com.sw.inbound.ui.theme
+
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
+
+private val DarkColorScheme = darkColorScheme(
+ primary = Purple80,
+ secondary = PurpleGrey80,
+ tertiary = Pink80
+)
+
+private val LightColorScheme = lightColorScheme(
+ primary = Purple40,
+ secondary = PurpleGrey40,
+ tertiary = Pink40
+
+ /* Other default colors to override
+ background = Color(0xFFFFFBFE),
+ surface = Color(0xFFFFFBFE),
+ onPrimary = Color.White,
+ onSecondary = Color.White,
+ onTertiary = Color.White,
+ onBackground = Color(0xFF1C1B1F),
+ onSurface = Color(0xFF1C1B1F),
+ */
+)
+
+@Composable
+fun InboundTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ // Dynamic color is available on Android 12+
+ dynamicColor: Boolean = true,
+ content: @Composable () -> Unit
+) {
+ val colorScheme = when {
+ dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+ val context = LocalContext.current
+ if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+ }
+
+ darkTheme -> DarkColorScheme
+ else -> LightColorScheme
+ }
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = Typography,
+ content = content,
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/ui/theme/Type.kt b/app/src/main/java/com/sw/inbound/ui/theme/Type.kt
new file mode 100644
index 0000000..1fc68ee
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/ui/theme/Type.kt
@@ -0,0 +1,34 @@
+package com.sw.inbound.ui.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+// Set of Material typography styles to start with
+val Typography = Typography(
+ bodyLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.5.sp
+ )
+ /* Other default text styles to override
+ titleLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 22.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 0.sp
+ ),
+ labelSmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Medium,
+ fontSize = 11.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp
+ )
+ */
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/ui/weight/BottomActionBar.kt b/app/src/main/java/com/sw/inbound/ui/weight/BottomActionBar.kt
new file mode 100644
index 0000000..f54c2a8
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/ui/weight/BottomActionBar.kt
@@ -0,0 +1,91 @@
+package com.sw.inbound.ui.weight
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.sw.inbound.R
+
+@Composable
+fun BottomActionBar(
+ leftText: String = "返回",
+ right1ButtonText: String = "清空物品",
+ right2ButtonText: String = "新增收货",
+ showRight1Button: Boolean = false,
+ onLeftButtonClick: () -> Unit,
+ onRight1ButtonClick: () -> Unit = {},
+ onRight2ButtonClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(start = 30.dp, end = 30.dp, bottom = 20.dp)
+ .height(100.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ // 左侧图标+文字
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.clickable {
+ onLeftButtonClick()
+ }
+ ) {
+ Image(
+ painter = painterResource(R.mipmap.ic_home),
+ contentDescription = "返回",
+ modifier = Modifier.size(80.dp)
+ )
+ Spacer(modifier = Modifier.width(17.dp))
+ Text(
+ text = leftText,
+ style = TextStyle(
+ color = colorResource(R.color.black),
+ fontWeight = FontWeight.Bold,
+ fontSize = 30.sp
+ )
+ )
+ }
+
+ Row {
+ if (showRight1Button) {
+ CustomOutlinedButton(
+ modifier = modifier
+ .width(300.dp)
+ .height(100.dp),
+ text = right1ButtonText,
+ fontSize = 36.sp,
+ onClick = onRight1ButtonClick
+ )
+ Spacer(modifier = Modifier.width(20.dp))
+ }
+ CustomButton(
+ modifier = modifier
+ .width(300.dp)
+ .height(100.dp),
+ text = right2ButtonText,
+ onClick = onRight2ButtonClick,
+ borderColor = colorResource(R.color.blue),
+ textColor = colorResource(R.color.white),
+ fontSize = 36.sp
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/ui/weight/CameraCaptureLayout.kt b/app/src/main/java/com/sw/inbound/ui/weight/CameraCaptureLayout.kt
new file mode 100644
index 0000000..c5d8032
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/ui/weight/CameraCaptureLayout.kt
@@ -0,0 +1,264 @@
+package com.sw.inbound.ui.weight
+
+import android.net.Uri
+import android.view.ViewGroup
+import android.widget.LinearLayout
+import android.widget.Toast
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.ImageCaptureException
+import androidx.camera.view.CameraController
+import androidx.camera.view.LifecycleCameraController
+import androidx.camera.view.PreviewView
+import androidx.compose.foundation.Image
+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.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.width
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.core.content.ContextCompat
+import androidx.lifecycle.compose.LocalLifecycleOwner
+import coil.compose.AsyncImage
+import com.sw.inbound.GlobalData
+import com.sw.inbound.R
+import com.sw.inbound.ext.dashedBorder
+import com.sw.inbound.ui.theme.AppTypography
+import com.sw.inbound.utils.FileUtils
+import com.sw.inbound.utils.ToastUtils
+import timber.log.Timber
+import java.io.File
+
+@Composable
+fun CameraCaptureLayout(
+ modifier: Modifier = Modifier,
+ onCancelClick: () -> Unit = {},
+ onConfirmClick: () -> Unit = {}
+) {
+ val context = LocalContext.current
+ var showCamera by remember { mutableStateOf(false) }
+ val lifecycleOwner = LocalLifecycleOwner.current
+ var photoUri by remember { mutableStateOf(null) }
+
+ // CameraX 控制器
+ val cameraController = remember {
+ LifecycleCameraController(context).apply {
+ setEnabledUseCases(
+ CameraController.IMAGE_CAPTURE or
+ CameraController.VIDEO_CAPTURE
+ )
+ }
+ }
+
+ Column(
+ modifier = modifier
+ .width(428.dp)
+ .padding(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(24.dp)
+ ) {
+ if (showCamera) {
+// CameraPreview()
+ // 摄像头预览
+ CameraPreview(
+ modifier = Modifier
+ .width(428.dp)
+ .height(321.dp), controller = cameraController
+ )
+ } else {
+ if (photoUri == null) {
+ // 图片预览区域
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(321.dp)
+ .dashedBorder(strokeWidth = 2.dp, cornerRadiusDp = 15.dp)
+ .clickable {
+ showCamera = true
+ },
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Image(
+ painter = painterResource(R.mipmap.ic_camera),
+ contentDescription = "默认图片",
+ modifier = Modifier
+ .width(96.dp)
+ .height(76.dp)
+ )
+ Spacer(modifier = Modifier.height(24.dp))
+ Text(
+ "图片采集",
+ style = AppTypography.grayTextStyle.copy(color = Color(0xFFB4B4C8))
+ )
+ }
+ } else {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(321.dp)
+ .dashedBorder(strokeWidth = 2.dp, cornerRadiusDp = 15.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ AsyncImage(
+ model = photoUri,
+ contentDescription = "照片",
+ modifier = Modifier.fillMaxSize()
+ )
+ }
+ }
+ }
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.End
+ ) {
+ CustomOutlinedButton(
+ modifier = Modifier
+ .weight(1f)
+ .height(80.dp),
+ text = "取消",
+ fontSize = 30.sp,
+ borderColor = colorResource(R.color.green),
+ textColor = colorResource(R.color.green),
+ onClick = {
+ Timber.d("点击取消")
+ showCamera = true
+ photoUri?.let {
+ FileUtils.deleteFileWithUri(context, photoUri!!)
+ }
+ photoUri = null
+ }
+ )
+ Spacer(modifier = Modifier.width(20.dp))
+
+ CustomButton(
+ modifier = Modifier
+ .weight(1f)
+ .height(80.dp),
+ text = "采集",
+ onClick = {
+ Timber.d("点击采集")
+ if (!showCamera) {
+ ToastUtils.showToast("请先开启图片预览")
+ return@CustomButton
+ }
+ val executor = ContextCompat.getMainExecutor(context)
+ val cacheDir = context.cacheDir
+ val photoFile = File.createTempFile(
+ "IMG_${System.currentTimeMillis()}",
+ ".jpg",
+ cacheDir
+ )
+
+ val cacheOutputOptions =
+ ImageCapture.OutputFileOptions.Builder(photoFile).build()
+ cameraController.takePicture(
+ cacheOutputOptions,
+ executor,
+ object : ImageCapture.OnImageSavedCallback {
+ override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
+ val savedUri = outputFileResults.savedUri ?: Uri.fromFile(photoFile)
+ photoUri = savedUri
+ Timber.d("photoUri = $photoUri")
+ Toast.makeText(context, "照片已保存", Toast.LENGTH_SHORT).show()
+ showCamera = false
+ GlobalData.imageUri = photoUri
+ }
+
+ override fun onError(exception: ImageCaptureException) {
+ GlobalData.imageUri = null
+ Toast.makeText(
+ context,
+ "拍照失败: ${exception.message}",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ })
+ },
+ borderColor = colorResource(R.color.green),
+ textColor = colorResource(R.color.white),
+ fontSize = 30.sp,
+ showButtonIcon = true
+ )
+ }
+ }
+
+ // 生命周期管理
+ DisposableEffect(lifecycleOwner) {
+ Timber.d("cameraController 释放")
+ cameraController.bindToLifecycle(lifecycleOwner)
+ onDispose { }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun CameraContent(modifier: Modifier, cameraController: LifecycleCameraController) {
+ val lifecycleOwner = LocalLifecycleOwner.current
+
+ Box(
+ modifier = modifier,
+ contentAlignment = Alignment.Center,
+ ) {
+ //在Compose中使用View系统中的PreviewView
+ AndroidView(
+ modifier = Modifier
+ .fillMaxSize(),
+ factory = { context ->
+ PreviewView(context).apply {
+ //设置布局宽度和高度占据全屏
+ layoutParams = LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT
+ )
+ //设置背景颜色
+ setBackgroundColor(android.graphics.Color.BLACK)
+ //设置渲染的实现模式
+ implementationMode = PreviewView.ImplementationMode.COMPATIBLE
+ //设置缩放方式
+ scaleType = PreviewView.ScaleType.FILL_START
+ }.also {
+ it.controller = cameraController
+ cameraController.bindToLifecycle(lifecycleOwner)
+ }
+ },
+ onReset = {},
+ onRelease = {
+ Timber.d("cameraController.unbind()")
+ cameraController.unbind()
+ }
+ )
+
+ Image(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(30.dp),
+ painter = painterResource(R.mipmap.ic_scan),
+ contentDescription = "扫描"
+ )
+ }
+
+}
diff --git a/app/src/main/java/com/sw/inbound/ui/weight/CameraPreview.kt b/app/src/main/java/com/sw/inbound/ui/weight/CameraPreview.kt
new file mode 100644
index 0000000..b334903
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/ui/weight/CameraPreview.kt
@@ -0,0 +1,43 @@
+package com.sw.inbound.ui.weight
+
+import androidx.camera.view.LifecycleCameraController
+import androidx.camera.view.PreviewView
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import com.sw.inbound.R
+
+@Composable
+fun CameraPreview(
+ controller: LifecycleCameraController,
+ modifier: Modifier = Modifier
+) {
+ val context = LocalContext.current
+
+ Box(modifier = modifier) {
+ AndroidView(
+ factory = { ctx ->
+ PreviewView(ctx).apply {
+ this.controller = controller
+ scaleType = PreviewView.ScaleType.FILL_CENTER // 控制预览缩放
+ }
+ },
+ modifier = Modifier.fillMaxSize()
+ )
+ Image(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(30.dp),
+ painter = painterResource(R.mipmap.ic_scan),
+ contentDescription = "扫描"
+ )
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/ui/weight/CameraPreviewView.kt b/app/src/main/java/com/sw/inbound/ui/weight/CameraPreviewView.kt
new file mode 100644
index 0000000..2e29450
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/ui/weight/CameraPreviewView.kt
@@ -0,0 +1,60 @@
+package com.sw.inbound.ui.weight
+
+import android.Manifest
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.material3.Button
+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.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.google.accompanist.permissions.ExperimentalPermissionsApi
+import com.google.accompanist.permissions.PermissionState
+import com.google.accompanist.permissions.isGranted
+import com.google.accompanist.permissions.rememberPermissionState
+import com.google.accompanist.permissions.shouldShowRationale
+
+@OptIn(ExperimentalPermissionsApi::class)
+@Preview
+@Composable
+fun CameraScreen() {
+ val cameraPermissionState =
+ rememberPermissionState(permission = Manifest.permission.CAMERA)
+ LaunchedEffect(key1 = Unit) {
+ if (!cameraPermissionState.status.isGranted && !cameraPermissionState.status.shouldShowRationale) {
+ cameraPermissionState.launchPermissionRequest()
+ }
+ }
+
+ if (cameraPermissionState.status.isGranted) {
+ //接受拍照的授权
+// CameraContent()
+ } else {
+ //未授权,显示未授权的界面
+ NoPermissionScreen(cameraPermissionState)
+ }
+}
+
+@OptIn(ExperimentalPermissionsApi::class)
+@Composable
+fun NoPermissionScreen(cameraPermissionState: PermissionState) {
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ val message = if (cameraPermissionState.status.shouldShowRationale) {
+ "未获取照相机权限导致无法使用照相机功能"
+ } else {
+ "请授权照相机的权限"
+ }
+ Text(message)
+ Spacer(modifier = Modifier.height(8.dp))
+ Button(onClick = {
+ cameraPermissionState.launchPermissionRequest()
+ }) {
+ Text("请求授权")
+ }
+ }
+}
+
diff --git a/app/src/main/java/com/sw/inbound/ui/weight/CustomButton.kt b/app/src/main/java/com/sw/inbound/ui/weight/CustomButton.kt
new file mode 100644
index 0000000..5910b35
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/ui/weight/CustomButton.kt
@@ -0,0 +1,102 @@
+package com.sw.inbound.ui.weight
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.TextUnit
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.sw.inbound.R
+
+@Composable
+fun CustomOutlinedButton(
+ text: String,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ textColor: Color = colorResource(R.color.blue),
+ borderColor: Color = Color.Blue,
+ cornerRadius: Dp = 8.dp,
+ borderWidth: Dp = 1.dp,
+ fontWeight: FontWeight = FontWeight.Bold,
+ fontSize: TextUnit = 24.sp
+) {
+ OutlinedButton(
+ onClick = onClick,
+ modifier = modifier,
+ shape = RoundedCornerShape(cornerRadius),
+ border = BorderStroke(borderWidth, color = borderColor),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Color.Transparent,
+ contentColor = textColor
+ )
+ ) {
+ Text(
+ text = text,
+ style = TextStyle(
+ fontWeight = fontWeight,
+ fontSize = fontSize,
+ color = textColor
+ )
+ )
+ }
+}
+
+@Composable
+fun CustomButton(
+ text: String,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ textColor: Color = colorResource(R.color.white),
+ borderColor: Color = colorResource(R.color.green),
+ cornerRadius: Dp = 8.dp,
+ borderWidth: Dp = 1.dp,
+ fontWeight: FontWeight = FontWeight.Bold,
+ fontSize: TextUnit = 24.sp,
+ showButtonIcon: Boolean = false
+) {
+ Button(
+ onClick = onClick,
+ modifier = modifier,
+ shape = RoundedCornerShape(cornerRadius),
+ border = BorderStroke(borderWidth, color = borderColor),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = borderColor,
+ contentColor = textColor
+ )
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ if (showButtonIcon) {
+ Image(
+ painter = painterResource(R.mipmap.ic_camera_small),
+ contentDescription = "采集"
+ )
+ Spacer(modifier = Modifier.width(13.dp))
+ }
+
+ Text(
+ text = text,
+ style = TextStyle(
+ fontWeight = fontWeight,
+ fontSize = fontSize,
+ color = textColor
+ )
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/ui/weight/CustomDropdownTextField.kt b/app/src/main/java/com/sw/inbound/ui/weight/CustomDropdownTextField.kt
new file mode 100644
index 0000000..bc18de6
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/ui/weight/CustomDropdownTextField.kt
@@ -0,0 +1,109 @@
+package com.sw.inbound.ui.weight
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExposedDropdownMenuBox
+import androidx.compose.material3.ExposedDropdownMenuDefaults
+import androidx.compose.material3.LocalTextStyle
+import androidx.compose.material3.MenuAnchorType.Companion.PrimaryNotEditable
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.sw.inbound.R
+import com.sw.inbound.model.response.DictType
+import com.sw.inbound.ui.theme.AppTypography
+import com.sw.inbound.ui.theme.Black_141428
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun CustomDropdownTextField(
+ value: String,
+ onValueChange: (String) -> Unit,
+ dropdownItems: List,
+ modifier: Modifier = Modifier,
+ placeholderValue: String = "请选择",
+ textAlign: TextAlign = TextAlign.Start,
+ textStyle: TextStyle? = null,
+) {
+ var expanded by remember { mutableStateOf(false) }
+ var newTextStyle = textStyle
+ ?: LocalTextStyle.current.copy(
+ color = Black_141428,
+ fontSize = 24.sp,
+ fontWeight = FontWeight.Bold,
+ textAlign = textAlign
+ )
+
+ ExposedDropdownMenuBox(
+ modifier = modifier,
+ expanded = expanded,
+ onExpandedChange = { expanded = it },
+ ) {
+ TextField(
+ value = value,
+ onValueChange = onValueChange,
+ readOnly = true,
+ textStyle = newTextStyle,
+
+ shape = RoundedCornerShape(10.dp),
+ trailingIcon = {
+ ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
+ },
+ colors = ExposedDropdownMenuDefaults.textFieldColors(
+ focusedContainerColor = Color.Transparent,
+ unfocusedContainerColor = Color.Transparent,
+ disabledContainerColor = Color.Transparent,
+ focusedIndicatorColor = Color.Transparent,
+ unfocusedIndicatorColor = Color.Transparent
+ ),
+ placeholder = {
+ Text(
+ modifier = modifier.fillMaxWidth(),
+ text = placeholderValue, style = AppTypography.gray96a0aaTextStyle.copy(
+ textAlign = textAlign
+ )
+ )
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(Color.Transparent)
+ .border(
+ width = 2.dp,
+ color = colorResource(R.color.border_line),
+ shape = RoundedCornerShape(10.dp)
+ )
+ .menuAnchor(PrimaryNotEditable, true)
+ )
+
+ ExposedDropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false }
+ ) {
+ dropdownItems.forEach { item ->
+ DropdownMenuItem(
+ text = { Text(text = item.value, style = AppTypography.black141428TextStyle) },
+ onClick = {
+ onValueChange(item.value)
+ expanded = false
+ }
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/ui/weight/CustomSearchScreen.kt b/app/src/main/java/com/sw/inbound/ui/weight/CustomSearchScreen.kt
new file mode 100644
index 0000000..123b110
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/ui/weight/CustomSearchScreen.kt
@@ -0,0 +1,95 @@
+package com.sw.inbound.ui.weight
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.material3.TextFieldDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.unit.dp
+import com.sw.inbound.R
+import com.sw.inbound.ui.theme.AppTypography
+import timber.log.Timber
+
+@Composable
+fun CustomSearchView(onValueChange: (String) -> Unit = {}, onSearchClick: (String) -> Unit) {
+ var searchText by remember { mutableStateOf("") }
+ val borderColor = Color(0xFFDCDCF0)
+ val focusManager = LocalFocusManager.current
+ // 搜索框
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(60.dp)
+ .border(
+ width = 2.dp,
+ color = borderColor,
+ shape = RoundedCornerShape(10.dp) // 圆角边框
+ )
+ .background(Color.Transparent) // 透明背景
+ ) {
+ TextField(
+ value = searchText,
+ onValueChange = {
+ searchText = it
+ onValueChange
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(end = 25.dp), // 为图标留出空间
+ colors = TextFieldDefaults.colors(
+ focusedContainerColor = Color.Transparent,
+ unfocusedContainerColor = Color.Transparent,
+ disabledContainerColor = Color.Transparent,
+ focusedIndicatorColor = Color.Transparent,
+ unfocusedIndicatorColor = Color.Transparent,
+ disabledIndicatorColor = Color.Transparent
+ ),
+ placeholder = {
+ Text("输入物品名称", style = AppTypography.gray96a0aaTextStyle)
+ },
+ singleLine = true,
+ textStyle = AppTypography.black141428TextStyle,
+ keyboardOptions = KeyboardOptions.Default.copy(
+ imeAction = ImeAction.Search
+ ),
+ keyboardActions = KeyboardActions(onSearch = {
+ focusManager.clearFocus()
+ onSearchClick(searchText)
+ }),
+ trailingIcon = {
+ Image(
+ painter = painterResource(R.mipmap.ic_search),
+ contentDescription = "搜索",
+ modifier = Modifier
+ .width(32.dp)
+ .height(32.dp)
+ .clickable {
+ Timber.d("搜索点击")
+ onSearchClick(searchText)
+ }
+ )
+ },
+ )
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/ui/weight/CustomSpinner.kt b/app/src/main/java/com/sw/inbound/ui/weight/CustomSpinner.kt
new file mode 100644
index 0000000..662c6cd
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/ui/weight/CustomSpinner.kt
@@ -0,0 +1,102 @@
+package com.sw.inbound.ui.weight
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExposedDropdownMenuBox
+import androidx.compose.material3.ExposedDropdownMenuDefaults
+import androidx.compose.material3.MenuAnchorType.Companion.PrimaryNotEditable
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.sw.inbound.R
+import com.sw.inbound.model.response.DictType
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun CustomSpinner(
+ items: List,
+ selectedItem: String,
+ onItemSelected: (DictType) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ var expanded by remember { mutableStateOf(false) }
+
+ ExposedDropdownMenuBox(
+ expanded = expanded,
+ onExpandedChange = { expanded = !expanded },
+ modifier = modifier.background(
+ color = Color(0x80FFFFFF),
+ shape = RoundedCornerShape(10.dp) // 圆角背景
+ )
+ ) {
+ TextField(
+ value = selectedItem,
+ onValueChange = {},
+ readOnly = true,
+ textStyle = TextStyle(
+ color = Color.White,
+ fontSize = 24.sp,
+ fontWeight = FontWeight.Bold
+ ),
+ shape = RoundedCornerShape(10.dp),
+ trailingIcon = {
+ ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
+ },
+ colors = ExposedDropdownMenuDefaults.textFieldColors(
+ focusedContainerColor = colorResource(R.color.blue),
+ unfocusedContainerColor = colorResource(R.color.blue),
+ disabledContainerColor = colorResource(R.color.blue),
+ focusedTextColor = Color.White,
+ unfocusedTextColor = Color.White,
+ cursorColor = Color.White,
+ focusedIndicatorColor = Color.Transparent,
+ unfocusedIndicatorColor = Color.Transparent,
+ focusedLabelColor = Color.White.copy(alpha = 0.8f),
+ unfocusedLabelColor = Color.White.copy(alpha = 0.8f),
+ focusedTrailingIconColor = Color.White,
+ unfocusedTrailingIconColor = Color.White
+ ),
+ modifier = Modifier.menuAnchor(PrimaryNotEditable, true)
+ )
+
+ ExposedDropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false },
+ modifier = Modifier
+ .background(colorResource(R.color.blue))
+ .exposedDropdownSize(matchTextFieldWidth = true)
+ ) {
+ items.forEach { item ->
+ DropdownMenuItem(
+ text = {
+ Text(
+ text = item.value,
+ style = TextStyle(
+ color = Color.White,
+ fontSize = 24.sp,
+ fontWeight = FontWeight.Bold
+ ),
+ )
+ },
+ onClick = {
+ onItemSelected(item)
+ expanded = false
+ }
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/ui/weight/CustomTextField.kt b/app/src/main/java/com/sw/inbound/ui/weight/CustomTextField.kt
new file mode 100644
index 0000000..53c4441
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/ui/weight/CustomTextField.kt
@@ -0,0 +1,210 @@
+package com.sw.inbound.ui.weight
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.LocalTextStyle
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.material3.TextFieldDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusDirection
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.sw.inbound.ext.isValidFloat
+import com.sw.inbound.ext.isValidNumber
+import com.sw.inbound.ui.theme.AppTypography
+import com.sw.inbound.ui.theme.Black_141428
+import com.sw.inbound.ui.theme.Gray_DCDCF0
+import timber.log.Timber
+
+//@Preview(showBackground = true)
+//@Composable
+//fun testTextField() {
+// Column(verticalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier.padding(20.dp)) {
+// CustomTextField(value = "123", onValueChange = {}, textAlign = TextAlign.Start)
+// CustomTextField(value = "", onValueChange = {}, textAlign = TextAlign.Start)
+// CustomTextField(value = "", onValueChange = {}, textAlign = TextAlign.End)
+// CustomTextField(
+// value = "123",
+// onValueChange = {},
+// textAlign = TextAlign.End,
+// trailingLabel = "克"
+// )
+// CustomTextField(
+// enabled = false,
+// modifier = Modifier
+// .height(60.dp),
+// value = "123",
+// onValueChange = {},
+// textAlign = TextAlign.End,
+// leadingIcon =
+// {
+// Button(
+// onClick = {},
+// modifier = Modifier
+// .padding(0.dp)
+// .fillMaxHeight(),
+// shape = RoundedCornerShape(10.dp),
+// border = BorderStroke(2.dp, color = Color.White),
+// colors = ButtonDefaults.buttonColors(
+// containerColor = Color.White,
+// contentColor = Black_141428
+// ),
+// ) { Text(text = "累计", style = AppTypography.black141428TextStyle) }
+// }
+// )
+// }
+//
+//}
+
+enum class InputType {
+ Text, // 字符串
+ Number, // 数字
+ Decimal // 浮点
+}
+
+@Composable
+fun CustomTextField(
+ value: String,
+ onValueChange: (String) -> Unit,
+ modifier: Modifier = Modifier.height(60.dp),
+ placeholderValue: String = "请录入",
+ trailingLabel: String? = null,
+ textAlign: TextAlign = TextAlign.Start,
+ textStyle: TextStyle? = null,
+ leadingIcon: @Composable (() -> Unit)? = null,
+ enabled: Boolean = true,
+ inputType: InputType = InputType.Text,
+ hasNext: Boolean = true,
+ isInitUpdate: Boolean = false,
+ keyboardOptions: KeyboardOptions? = null,
+ keyboardActions: KeyboardActions? = null
+) {
+ val containerColor = if (enabled) Color.Transparent else Gray_DCDCF0
+// var inputValue by remember { mutableStateOf(value) }
+ var isUserInput by remember { mutableStateOf(false) }
+ var inputValue by remember(if (isInitUpdate && !isUserInput) value else null) {
+ mutableStateOf(value)
+ }
+ val focusManager = LocalFocusManager.current
+
+ fun onEditingComplete() {
+ Timber.d("onEditingComplete inputValue = $inputValue")
+ onValueChange(inputValue)
+ }
+
+ var newTextStyle = textStyle
+ ?: LocalTextStyle.current.copy(
+ color = Black_141428,
+ fontSize = 24.sp,
+ fontWeight = FontWeight.Bold,
+ textAlign = textAlign
+ )
+ Box(modifier = modifier) {
+ TextField(
+ enabled = enabled,
+ value = inputValue,
+ textStyle = newTextStyle,
+ onValueChange = { newValue ->
+ Timber.d("onValueChange newValue = $newValue")
+ isUserInput = newValue.isNotEmpty()
+ when (inputType) {
+ InputType.Number -> {
+ if (newValue.isEmpty() || newValue.isValidNumber()) {
+ inputValue = newValue
+ onEditingComplete()
+ }
+ }
+
+ InputType.Decimal -> {
+ if (newValue.isEmpty() || newValue.isValidFloat()) {
+ inputValue = newValue
+ onEditingComplete()
+ }
+ }
+
+ else -> {
+ inputValue = newValue
+ onEditingComplete()
+ }
+ }
+ },
+
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(Color.Transparent)
+ .border(
+ width = 2.dp,
+ color = Gray_DCDCF0,
+ shape = RoundedCornerShape(10.dp)
+ )
+ .onFocusChanged(onFocusChanged = { focusState ->
+ {
+ onEditingComplete()
+ }
+ }),
+ colors = TextFieldDefaults.colors(
+ focusedContainerColor = Color.Transparent,
+ unfocusedContainerColor = Color.Transparent,
+ disabledContainerColor = containerColor,
+ focusedIndicatorColor = Color.Transparent,
+ unfocusedIndicatorColor = Color.Transparent,
+ disabledIndicatorColor = Color.Transparent
+ ),
+ shape = RoundedCornerShape(10.dp),
+ placeholder = {
+ Text(
+ modifier = Modifier
+ .fillMaxWidth()
+ .fillMaxHeight()
+ .wrapContentHeight(Alignment.CenterVertically),
+ text = placeholderValue,
+ textAlign = textAlign,
+ style = AppTypography.gray96a0aaTextStyle
+ )
+ },
+ leadingIcon = leadingIcon,
+ trailingIcon = trailingLabel?.let { label ->
+ {
+ Text(
+ text = trailingLabel,
+ modifier = Modifier
+ .fillMaxHeight()
+ .wrapContentHeight(Alignment.CenterVertically),
+ style = AppTypography.gray96a0aaTextStyle
+ )
+ }
+ },
+ keyboardOptions = keyboardOptions
+ ?: KeyboardOptions.Default.copy(imeAction = if (hasNext) ImeAction.Next else ImeAction.Done),
+ keyboardActions = keyboardActions ?: KeyboardActions(onNext = {
+ focusManager.moveFocus(focusDirection = FocusDirection.Next)
+ onEditingComplete()
+ }, onDone = {
+ focusManager.clearFocus()
+ onEditingComplete()
+ })
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/ui/weight/GlobalLoading.kt b/app/src/main/java/com/sw/inbound/ui/weight/GlobalLoading.kt
new file mode 100644
index 0000000..3a58314
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/ui/weight/GlobalLoading.kt
@@ -0,0 +1,32 @@
+package com.sw.inbound.ui.weight
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import androidx.compose.ui.window.DialogProperties
+import com.sw.inbound.network.LoadingState
+
+@Composable
+fun GlobalLoading() {
+ if (LoadingState.isLoading) {
+ Dialog(
+ onDismissRequest = {},
+ properties = DialogProperties(
+ dismissOnBackPress = false,
+ dismissOnClickOutside = false
+ )
+ ) {
+ Column {
+ CircularProgressIndicator(modifier = Modifier)
+ Spacer(modifier = Modifier.height(20.dp))
+ Text(text = "请稍等...")
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/ui/weight/IdentityView.kt b/app/src/main/java/com/sw/inbound/ui/weight/IdentityView.kt
new file mode 100644
index 0000000..15f376f
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/ui/weight/IdentityView.kt
@@ -0,0 +1,77 @@
+package com.sw.inbound.ui.weight
+
+import androidx.camera.view.CameraController
+import androidx.camera.view.LifecycleCameraController
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.LocalLifecycleOwner
+import com.sw.inbound.ext.medium
+import com.sw.inbound.model.response.SearchGoodsInfo
+import com.sw.inbound.ui.theme.AppTypography
+
+/**
+ * 菜品识别组件
+ */
+@Composable
+fun IdentityView(
+ list: List,
+ showSearchView: Boolean = false,
+ onSearchClick: (String) -> Unit = {},
+ onOptionSelected: (SearchGoodsInfo.Record) -> Unit = {},
+) {
+ val context = LocalContext.current
+ val lifecycleOwner = LocalLifecycleOwner.current
+ // CameraX 控制器
+ val cameraController = remember {
+ LifecycleCameraController(context).apply {
+ setEnabledUseCases(CameraController.IMAGE_CAPTURE)
+ bindToLifecycle(lifecycleOwner)
+ }
+ }
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ CameraPreview(
+ modifier = Modifier
+ .width(397.dp)
+ .height(298.dp),
+ controller = cameraController
+ )
+ if (showSearchView) {
+ Spacer(modifier = Modifier.height(30.dp))
+ CustomSearchView(onSearchClick = onSearchClick)
+ }
+ Spacer(modifier = Modifier.height(30.dp))
+ if (showSearchView) {
+ SingleSelectButtonGroup(
+ modifier = Modifier
+ .height(64.dp)
+ .width(192.dp), options = list,
+ onOptionSelected = onOptionSelected
+ )
+ } else {
+ SingleSelectButtonGroup(
+ modifier = Modifier
+ .height(70.dp)
+ .width(215.dp),
+ options = list,
+ horizontalSpacing = 30.dp,
+ verticalSpacing = 20.dp,
+ onOptionSelected = onOptionSelected,
+ unSelectedTextStyle = AppTypography.blackTextStyle.medium()
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/ui/weight/ProductEdit.kt b/app/src/main/java/com/sw/inbound/ui/weight/ProductEdit.kt
new file mode 100644
index 0000000..d9eb247
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/ui/weight/ProductEdit.kt
@@ -0,0 +1,70 @@
+package com.sw.inbound.ui.weight
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import com.sw.inbound.model.response.DictType
+import com.sw.inbound.ui.theme.AppTypography
+
+/**
+ * 横向输入框 包含左侧文件+右侧输入框
+ */
+@Composable
+fun RowInputLayout(
+ label: String,
+ value: String,
+ onValueChange: (String) -> Unit,
+ dropdownItems: List? = null,
+ hasNext: Boolean = true,
+ inputType: InputType = InputType.Text,
+ trailingLabel: String? = null,
+ keyboardOptions: KeyboardOptions? = null,
+ keyboardActions: KeyboardActions? = null,
+ isInitUpdate: Boolean = false
+) {
+ Row(
+ modifier = Modifier
+ .height(60.dp)
+ .fillMaxWidth()
+ .padding(horizontal = 30.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(label, style = AppTypography.black141428TextStyle)
+// Spacer(modifier = Modifier.width(67.dp))
+ Spacer(modifier = Modifier.weight(1f))
+ if (dropdownItems == null) {
+ CustomTextField(
+ modifier = Modifier.width(290.dp),
+ value = value, onValueChange = onValueChange,
+ textAlign = TextAlign.End,
+ trailingLabel = trailingLabel,
+ hasNext = hasNext,
+ inputType = inputType,
+ keyboardOptions = keyboardOptions,
+ keyboardActions = keyboardActions,
+ isInitUpdate = isInitUpdate
+ )
+ } else {
+ CustomDropdownTextField(
+ modifier = Modifier.width(290.dp),
+ value = value,
+ dropdownItems = dropdownItems,
+ onValueChange = onValueChange,
+ textAlign = TextAlign.End
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/ui/weight/ProductListItem.kt b/app/src/main/java/com/sw/inbound/ui/weight/ProductListItem.kt
new file mode 100644
index 0000000..e4c7326
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/ui/weight/ProductListItem.kt
@@ -0,0 +1,161 @@
+package com.sw.inbound.ui.weight
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import com.sw.inbound.R
+import com.sw.inbound.ext.bold
+import com.sw.inbound.model.response.GoodsInfo
+import com.sw.inbound.ui.theme.AppTypography
+import com.sw.inbound.ui.theme.Gray_DCDCF0
+
+
+@Composable
+fun ProductListView(
+ modifier: Modifier = Modifier,
+ productList: List,
+ checkedItem: GoodsInfo? = null,
+ onItemCheckedClick: (GoodsInfo) -> Unit = {},
+ showClose: Boolean = false,
+ onCloseClick: (GoodsInfo) -> Unit = {},
+) {
+ Column(
+ modifier = modifier
+// .padding(horizontal = 30.dp)
+ ) {
+ // 左侧列表标题
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(30.dp)
+ ) {
+ CustomSingleRightText(
+ modifier = Modifier.weight(1f),
+ text = "名称",
+ style = AppTypography.gray96a0aaTextStyle.bold(),
+ textAlign = TextAlign.Start
+ )
+ Spacer(modifier = Modifier.width(10.dp))
+ CustomSingleRightText(
+ modifier = Modifier.width(100.dp),
+ text = "单价(元)",
+ style = AppTypography.gray96a0aaTextStyle.bold()
+ )
+ Spacer(modifier = Modifier.width(20.dp))
+ CustomSingleRightText(
+ modifier = Modifier.width(100.dp),
+ text = "数量",
+ style = AppTypography.gray96a0aaTextStyle.bold()
+ )
+ Spacer(modifier = Modifier.width(20.dp))
+ CustomSingleRightText(
+ modifier = Modifier.width(110.dp),
+ text = "金额(元)",
+ style = AppTypography.gray96a0aaTextStyle.bold()
+ )
+ if (showClose) {
+ Spacer(modifier = Modifier.width(78.dp))
+ }
+ }
+// Spacer(modifier = Modifier.height(31.dp))
+ HorizontalDivider(thickness = 1.dp, color = colorResource(R.color.divider))
+// Spacer(modifier = Modifier.height(31.dp))
+ // 左侧列表
+ LazyColumn() {
+ items(items = productList, key = { it.goodId!! }) { it ->
+
+ val checkedBg =
+ if (it.goodId == checkedItem?.goodId) Gray_DCDCF0 else Color.Transparent
+ Row(
+ modifier = Modifier
+ .background(color = checkedBg)
+ .padding(30.dp)
+ .clickable {
+ onItemCheckedClick(it)
+ }, verticalAlignment = Alignment.CenterVertically
+ ) {
+ CustomSingleRightText(
+ modifier = Modifier.weight(1f),
+ text = it.goodNameStr,
+ textAlign = TextAlign.Start
+ )
+ Spacer(modifier = Modifier.width(10.dp))
+ CustomSingleRightText(
+ modifier = Modifier.width(100.dp),
+ text = it.recUnitPriceTaxInStr,
+ style = AppTypography.blackTextStyle
+ )
+ Spacer(modifier = Modifier.width(20.dp))
+ CustomSingleRightText(
+ modifier = Modifier.width(100.dp),
+ text = "${it.receiveCountStr}${it.unitNameStr}",
+ style = AppTypography.blackTextStyle
+ )
+ Spacer(modifier = Modifier.width(20.dp))
+ CustomSingleRightText(
+ modifier = Modifier.width(110.dp),
+ text = it.recPriceExItemStr,
+ style = AppTypography.blackTextStyle
+ )
+ if (showClose) {
+ Spacer(modifier = Modifier.width(30.dp))
+ Image(
+ modifier = Modifier
+ .size(48.dp)
+ .clickable {
+ onCloseClick(it)
+ },
+ painter = painterResource(R.mipmap.ic_delete),
+ contentDescription = "删除"
+ )
+ }
+ }
+// Spacer(modifier = Modifier.height(33.dp))
+// if (productList.indexOf(it) != productList.lastIndex)
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = colorResource(R.color.divider)
+ )
+// Spacer(modifier = Modifier.height(33.dp))
+ }
+ }
+ }
+}
+
+@Composable
+private fun CustomSingleRightText(
+ modifier: Modifier,
+ text: String,
+ style: TextStyle = AppTypography.blackTextStyle,
+ textAlign: TextAlign = TextAlign.End
+) {
+ Text(
+ modifier = modifier,
+ text = text,
+ textAlign = textAlign,
+ style = style,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/ui/weight/SelfProcurementListItem.kt b/app/src/main/java/com/sw/inbound/ui/weight/SelfProcurementListItem.kt
new file mode 100644
index 0000000..9f3498d
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/ui/weight/SelfProcurementListItem.kt
@@ -0,0 +1,161 @@
+package com.sw.inbound.ui.weight
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import com.sw.inbound.R
+import com.sw.inbound.ext.bold
+import com.sw.inbound.model.request.PurchaseWarehouseParam
+import com.sw.inbound.ui.theme.AppTypography
+import com.sw.inbound.ui.theme.Gray_DCDCF0
+
+
+@Composable
+fun SelfProcurementListItem(
+ modifier: Modifier = Modifier,
+ productList: List,
+ checkedItem: PurchaseWarehouseParam? = null,
+ onItemCheckedClick: (PurchaseWarehouseParam) -> Unit = {},
+ showClose: Boolean = false,
+ onCloseClick: (PurchaseWarehouseParam) -> Unit = {},
+) {
+ Column(
+ modifier = modifier
+// .padding(horizontal = 30.dp)
+ ) {
+ // 左侧列表标题
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(30.dp)
+ ) {
+ CustomSingleRightText(
+ modifier = Modifier.weight(1f),
+ text = "名称",
+ style = AppTypography.gray96a0aaTextStyle.bold(),
+ textAlign = TextAlign.Start
+ )
+ Spacer(modifier = Modifier.width(10.dp))
+ CustomSingleRightText(
+ modifier = Modifier.width(100.dp),
+ text = "单价(元)",
+ style = AppTypography.gray96a0aaTextStyle.bold()
+ )
+ Spacer(modifier = Modifier.width(20.dp))
+ CustomSingleRightText(
+ modifier = Modifier.width(100.dp),
+ text = "数量",
+ style = AppTypography.gray96a0aaTextStyle.bold()
+ )
+ Spacer(modifier = Modifier.width(20.dp))
+ CustomSingleRightText(
+ modifier = Modifier.width(110.dp),
+ text = "金额(元)",
+ style = AppTypography.gray96a0aaTextStyle.bold()
+ )
+ if (showClose) {
+ Spacer(modifier = Modifier.width(78.dp))
+ }
+ }
+// Spacer(modifier = Modifier.height(31.dp))
+ HorizontalDivider(thickness = 1.dp, color = colorResource(R.color.divider))
+// Spacer(modifier = Modifier.height(31.dp))
+ // 左侧列表
+ LazyColumn() {
+ items(items = productList, key = { it.goodsId }) { it ->
+
+ val checkedBg =
+ if (it.goodsId == checkedItem?.goodsId) Gray_DCDCF0 else Color.Transparent
+ Row(
+ modifier = Modifier
+ .background(color = checkedBg)
+ .padding(30.dp)
+ .clickable {
+ onItemCheckedClick(it)
+ }, verticalAlignment = Alignment.CenterVertically
+ ) {
+ CustomSingleRightText(
+ modifier = Modifier.weight(1f),
+ text = it.goodsNameStr,
+ textAlign = TextAlign.Start
+ )
+ Spacer(modifier = Modifier.width(10.dp))
+ CustomSingleRightText(
+ modifier = Modifier.width(100.dp),
+ text = it.goodsUnitPriceStr,
+ style = AppTypography.blackTextStyle
+ )
+ Spacer(modifier = Modifier.width(20.dp))
+ CustomSingleRightText(
+ modifier = Modifier.width(100.dp),
+ text = "${it.goodsCountStr}${it.unitNameStr}",
+ style = AppTypography.blackTextStyle
+ )
+ Spacer(modifier = Modifier.width(20.dp))
+ CustomSingleRightText(
+ modifier = Modifier.width(110.dp),
+ text = it.goodsPriceStr,
+ style = AppTypography.blackTextStyle
+ )
+ if (showClose) {
+ Spacer(modifier = Modifier.width(30.dp))
+ Image(
+ modifier = Modifier
+ .size(48.dp)
+ .clickable {
+ onCloseClick(it)
+ },
+ painter = painterResource(R.mipmap.ic_delete),
+ contentDescription = "删除"
+ )
+ }
+ }
+// Spacer(modifier = Modifier.height(33.dp))
+// if (productList.indexOf(it) != productList.lastIndex)
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = colorResource(R.color.divider)
+ )
+// Spacer(modifier = Modifier.height(33.dp))
+ }
+ }
+ }
+}
+
+@Composable
+private fun CustomSingleRightText(
+ modifier: Modifier,
+ text: String,
+ style: TextStyle = AppTypography.blackTextStyle,
+ textAlign: TextAlign = TextAlign.End
+) {
+ Text(
+ modifier = modifier,
+ text = text,
+ textAlign = textAlign,
+ style = style,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/ui/weight/SingleSelectButtonGroup.kt b/app/src/main/java/com/sw/inbound/ui/weight/SingleSelectButtonGroup.kt
new file mode 100644
index 0000000..0e3d8fd
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/ui/weight/SingleSelectButtonGroup.kt
@@ -0,0 +1,112 @@
+package com.sw.inbound.ui.weight
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentWidth
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import com.sw.inbound.model.response.SearchGoodsInfo
+import com.sw.inbound.ui.theme.AppTypography
+
+
+@Preview(showBackground = true)
+@Composable
+fun testSingleGroup() {
+ val list = mutableListOf()
+ list.add(SearchGoodsInfo.Record(goodsName = "胶东大白菜胶东大白菜胶东大白菜胶东大白菜"))
+ for (i in 1..20) {
+ list.add(SearchGoodsInfo.Record(goodsName = "胶东大白菜$i"))
+ }
+
+// val list = arrayListOf(
+// "胶东大白菜",
+// "玉田尖白菜1",
+// "玉田尖白菜2",
+// "玉田尖白菜3",
+// "玉田尖白菜4"
+// )
+// SingleSelectButtonGroup(list)
+}
+
+@Composable
+fun SingleSelectButtonGroup(
+ options: List,
+ modifier: Modifier = Modifier,
+ onOptionSelected: (SearchGoodsInfo.Record) -> Unit,
+ horizontalSpacing: Dp = 13.dp,
+ verticalSpacing: Dp = 13.dp,
+ unSelectedTextStyle: TextStyle = AppTypography.gray96a0aaTextStyle
+) {
+ val context = LocalContext.current
+ var selectedOption by remember { mutableStateOf(options.firstOrNull() ?: "") }
+
+ // 定义颜色
+ val selectedColor = Color(0xFFD9E3F9) // 选中颜色 #D9E3F9
+ val unselectedColor = Color(0xFFDCDCF0) // 未选中颜色 #DCDCF0
+
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(2), // 每行2列
+ modifier = Modifier
+ .fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(horizontalSpacing), // 水平间距13dp
+ verticalArrangement = Arrangement.spacedBy(verticalSpacing) // 垂直间距13dp
+ ) {
+ items(items = options) { option ->
+ Box(
+ modifier = modifier
+ .border(
+ width = 2.dp,
+ shape = RoundedCornerShape(10.dp),
+ color = if (option == selectedOption) selectedColor else unselectedColor,
+ )
+ .clip(RoundedCornerShape(10.dp))
+ .background(if (option == selectedOption) selectedColor else Color.Transparent)
+ .clickable {
+ selectedOption = option
+ onOptionSelected(option)
+ },
+ contentAlignment = Alignment.Center
+// .padding(vertical = 20.dp) // 垂直内边距
+ ) {
+ Text(
+ text = option.goodsNameStr,
+ style = if (option == selectedOption) AppTypography.BlueTextStyle else unSelectedTextStyle,
+ modifier = Modifier
+// .width(192.dp)
+// .height(64.dp)
+ .wrapContentWidth()
+ .padding(horizontal = 10.dp), // 水平内边距
+ textAlign = TextAlign.Center,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ softWrap = false
+ )
+ }
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/ui/weight/TopTitleBar.kt b/app/src/main/java/com/sw/inbound/ui/weight/TopTitleBar.kt
new file mode 100644
index 0000000..491d7da
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/ui/weight/TopTitleBar.kt
@@ -0,0 +1,112 @@
+package com.sw.inbound.ui.weight
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.sw.inbound.R
+import com.sw.inbound.ext.medium
+import com.sw.inbound.model.response.User
+import com.sw.inbound.ui.theme.AppTypography
+import com.sw.inbound.utils.DateTimeUtils
+import kotlinx.coroutines.delay
+
+
+@OptIn(ExperimentalFoundationApi::class)
+@Preview(
+ widthDp = 1920,
+ heightDp = 1080,
+ showBackground = true
+)
+@Composable
+fun TopTitleBar(
+ modifier: Modifier = Modifier,
+ title: String = "采购单入库",
+ user: User? = null,
+ onLogoutClick: () -> Unit = {}
+) {
+ var currentTime by remember { mutableStateOf(DateTimeUtils.getChineseDateString()) }
+
+ // 每秒更新一次时间
+ LaunchedEffect(Unit) {
+ while (true) {
+ delay(1000) // 1秒间隔
+ currentTime = DateTimeUtils.getChineseDateString()
+ }
+ }
+
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(start = 60.dp, end = 60.dp, top = 22.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ text = title,
+ style = TextStyle(
+ fontWeight = FontWeight.Bold,
+ color = colorResource(R.color.title),
+ fontSize = 36.sp
+ ),
+ modifier = Modifier.weight(1f)
+ )
+
+ if (user != null) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .height(60.dp)
+ .clickable {
+ onLogoutClick()
+ }) {
+ Text(text = user.name ?: "用户", style = AppTypography.blackTextStyle.medium())
+ Spacer(modifier = Modifier.width(21.dp))
+ Image(
+ modifier = Modifier.size(60.dp),
+ painter = painterResource(R.mipmap.ic_logout),
+ contentDescription = "退出"
+ )
+ }
+ } else {
+ Text(
+ modifier = Modifier.combinedClickable(
+ onClick = {},
+ onLongClick = {
+ onLogoutClick()
+ }
+ ),
+ text = currentTime,
+ style = TextStyle(
+ fontWeight = FontWeight.Medium,
+ color = colorResource(R.color.black),
+ fontSize = 24.sp
+ )
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/ui/weight/dialog/ReceiptTipDialog.kt b/app/src/main/java/com/sw/inbound/ui/weight/dialog/ReceiptTipDialog.kt
new file mode 100644
index 0000000..6be7151
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/ui/weight/dialog/ReceiptTipDialog.kt
@@ -0,0 +1,142 @@
+package com.sw.inbound.ui.weight.dialog
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+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.width
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.window.Dialog
+import androidx.compose.ui.window.DialogProperties
+import com.sw.inbound.R
+import com.sw.inbound.ext.bold
+import com.sw.inbound.ext.withColor
+import com.sw.inbound.ext.withSize
+import com.sw.inbound.ui.theme.AppTypography
+import com.sw.inbound.ui.weight.CustomButton
+import com.sw.inbound.ui.weight.CustomOutlinedButton
+
+@Composable
+fun ReceiptTipDialog(
+ modifier: Modifier = Modifier,
+ isWarn: Boolean = false,
+ onCancelClick: () -> Unit,
+ onConfirmClick: (Boolean) -> Unit,
+ onDismiss: () -> Unit = {},
+) {
+ Dialog(
+ onDismissRequest = onDismiss,
+ properties = DialogProperties(
+ usePlatformDefaultWidth = false, // 不使用平台默认宽度
+ decorFitsSystemWindows = false // 允许内容延伸到系统窗口后面
+ )
+ ) {
+ Surface(
+ modifier = Modifier
+ .width(1190.dp)
+ .height(840.dp)
+ .wrapContentSize(),
+ shape = RoundedCornerShape(30.dp),
+ color = Color.White,
+ shadowElevation = 0.dp
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(30.dp),
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ Image(
+ painter = if (isWarn) painterResource(R.mipmap.ic_tip_warn) else painterResource(
+ R.mipmap.ic_tip_success
+ ), contentDescription = "提示"
+ )
+ Spacer(modifier = Modifier.height(60.dp))
+ Text(
+ text = if (isWarn) "收货数量与采购量不一致" else "核对无误,确认收货",
+ style = AppTypography.black141428TextStyle.bold().withSize(48.sp)
+ )
+ if (isWarn) {
+ Spacer(modifier = Modifier.height(29.dp))
+ Text(
+ text = "请确认实际收货数量,或选择部分收货处理",
+ style = AppTypography.black141428TextStyle.withColor(
+ color = Color(0xFF999999)
+ )
+ )
+ }
+ }
+ HorizontalDivider(thickness = 1.dp, color = colorResource(R.color.divider))
+ Spacer(modifier = Modifier.height(30.dp))
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.End
+ ) {
+ CustomOutlinedButton(
+ modifier = modifier
+ .width(180.dp)
+ .height(90.dp),
+ text = "返回",
+ fontSize = 36.sp,
+ onClick = onCancelClick
+ )
+ Spacer(modifier = Modifier.width(12.dp))
+
+ if (isWarn) {
+ CustomButton(
+ modifier = modifier
+ .width(260.dp)
+ .height(90.dp),
+ text = "部分收货",
+ onClick = {
+ onConfirmClick(false)
+ },
+ borderColor = colorResource(R.color.green),
+ textColor = colorResource(R.color.white),
+ fontSize = 36.sp
+ )
+
+ Spacer(modifier = Modifier.width(12.dp))
+ }
+
+ CustomButton(
+ modifier = modifier
+ .width(260.dp)
+ .height(90.dp),
+ text = "确定收货",
+ onClick = {
+ onConfirmClick(true)
+ },
+ borderColor = colorResource(R.color.blue),
+ textColor = colorResource(R.color.white),
+ fontSize = 36.sp
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/sw/inbound/utils/ContextUtils.kt b/app/src/main/java/com/sw/inbound/utils/ContextUtils.kt
new file mode 100644
index 0000000..bef33ec
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/utils/ContextUtils.kt
@@ -0,0 +1,50 @@
+package com.sw.inbound.utils
+
+import android.content.Context
+import android.view.View
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
+
+/**
+ * Context 工具类
+ */
+object ContextUtils {
+
+ // ========== Composable 内获取 ==========
+
+ /**
+ * 获取当前 Composable 的 Activity Context
+ * 只能在 @Composable 函数中调用
+ */
+ @Composable
+ fun getActivityContext(): Context {
+ return LocalContext.current
+ }
+
+ /**
+ * 获取当前 Composable 的 View
+ */
+ @Composable
+ fun getLocalView(): View {
+ return LocalView.current
+ }
+
+ // ========== 非 Composable 环境获取 ==========
+
+ /**
+ * 通过静态 Application 引用获取
+ * 需要在 Application 类中初始化
+ */
+ private var _applicationContext: Context? = null
+
+ fun initAppContext(context: Context) {
+ _applicationContext = context.applicationContext
+ }
+
+ fun getAppContext(): Context {
+ return _applicationContext ?: throw IllegalStateException(
+ "Application context not initialized. Call initAppContext() first."
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/utils/CrashHandler.kt b/app/src/main/java/com/sw/inbound/utils/CrashHandler.kt
new file mode 100644
index 0000000..54d9499
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/utils/CrashHandler.kt
@@ -0,0 +1,237 @@
+package com.sw.inbound.utils
+
+import android.app.ActivityManager
+import android.app.AlarmManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.os.Handler
+import android.os.Looper
+import android.os.Process
+import com.sw.inbound.MainActivity
+import timber.log.Timber
+import java.io.File
+import java.io.FileOutputStream
+import java.io.IOException
+import java.io.PrintWriter
+import java.io.StringWriter
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+import kotlin.system.exitProcess
+
+/**
+ * 崩溃处理
+ */
+class CrashHandler private constructor(private val context: Context) :
+ Thread.UncaughtExceptionHandler {
+
+ companion object {
+ private const val TAG = "CrashHandler"
+ private const val CRASH_REPORTS_DIR = "crash_reports"
+ private const val LOG_LINES = 500 // 收集最近500行日志
+
+ @Volatile
+ private var instance: CrashHandler? = null
+
+ fun init(context: Context) {
+ if (instance == null) {
+ synchronized(CrashHandler::class.java) {
+ if (instance == null) {
+ instance = CrashHandler(context.applicationContext)
+ }
+ }
+ }
+ }
+
+ fun getCrashReportFiles(context: Context): Array {
+ val crashDir = File(context.getExternalFilesDir(null), CRASH_REPORTS_DIR)
+ return if (crashDir.exists() && crashDir.isDirectory) {
+ crashDir.listFiles { _, name -> name.endsWith(".log") } ?: emptyArray()
+ } else {
+ emptyArray()
+ }
+ }
+
+ fun clearCrashReports(context: Context) {
+ getCrashReportFiles(context).forEach { it.delete() }
+ }
+ }
+
+ private val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
+
+ init {
+ Thread.setDefaultUncaughtExceptionHandler(this)
+ }
+
+ override fun uncaughtException(thread: Thread, ex: Throwable) {
+ handleException(thread, ex)
+
+ // 如果系统提供了默认的异常处理器,则交给系统去结束程序
+ // 否则自己结束程序
+ defaultHandler?.uncaughtException(thread, ex) ?: run {
+ Process.killProcess(Process.myPid())
+ exitProcess(1)
+ }
+ }
+
+ /**
+ * 自动重启app
+ */
+ private fun restartApp() {
+ // 延迟1秒后重启应用
+ Handler(Looper.getMainLooper()).postDelayed({
+ val intent = Intent(context, MainActivity::class.java).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+
+ val pendingIntent = PendingIntent.getActivity(
+ context, 0, intent,
+ PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
+ )
+
+ val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
+ alarmManager.set(AlarmManager.RTC, System.currentTimeMillis() + 100, pendingIntent)
+
+ Process.killProcess(Process.myPid())
+ exitProcess(1)
+ }, 1000)
+ }
+
+ private fun handleException(thread: Thread, ex: Throwable) {
+ // 收集设备信息和异常信息
+ val crashInfo = collectCrashInfo(thread, ex)
+
+ // 保存日志文件
+ saveCrashInfoToFile(crashInfo)
+
+ // 这里可以添加其他处理逻辑,比如上传到服务器等
+ }
+
+ private fun collectCrashInfo(thread: Thread, ex: Throwable): String {
+ return buildString {
+ // 收集设备信息
+ collectDeviceInfo(this)
+
+ // 收集应用日志
+ append("\n\n").append(collectLogs())
+
+ // 收集线程和异常信息
+ append("\n\n========== Thread & Exception Info ==========\n")
+ append("Thread: ${thread.name}\n")
+ append("Stack Trace:\n")
+
+ val sw = StringWriter()
+ val pw = PrintWriter(sw)
+ ex.printStackTrace(pw)
+ var cause: Throwable? = ex.cause
+ while (cause != null) {
+ cause.printStackTrace(pw)
+ cause = cause.cause
+ }
+ pw.close()
+ append(sw.toString())
+ }
+ }
+
+ private fun collectDeviceInfo(sb: StringBuilder) {
+ sb.append("========== Device Info ==========\n")
+ try {
+ // 应用信息
+ val pm = context.packageManager
+ val pi = pm.getPackageInfo(context.packageName, 0)
+
+ sb.append("App Version: ${pi.versionName}_${pi.versionCode}\n")
+
+ // Android 版本信息
+ sb.append("OS Version: ${Build.VERSION.RELEASE}_${Build.VERSION.SDK_INT}\n")
+
+ // 设备信息
+ sb.append("Vendor: ${Build.MANUFACTURER}\n")
+ sb.append("Model: ${Build.MODEL}\n")
+ sb.append("CPU ABI: ${Build.SUPPORTED_ABIS[0]}\n")
+
+ // 其他信息
+ sb.append("Locale: ${Locale.getDefault()}\n")
+ sb.append(
+ "Current Time: ${
+ SimpleDateFormat(
+ "yyyy-MM-dd HH:mm:ss",
+ Locale.getDefault()
+ ).format(Date())
+ }\n"
+ )
+
+ // 内存信息
+ val memoryInfo = ActivityManager.MemoryInfo()
+ val activityManager =
+ context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
+ activityManager.getMemoryInfo(memoryInfo)
+ sb.append("Available Memory: ${memoryInfo.availMem / (1024 * 1024)}MB\n")
+ sb.append("Total Memory: ${memoryInfo.totalMem / (1024 * 1024)}MB\n")
+ sb.append("Low Memory: ${memoryInfo.lowMemory}\n")
+
+ } catch (e: Exception) {
+ Timber.e(e, "Error while collecting device info")
+ sb.append("Error while collecting device info: ${e.message}\n")
+ }
+ }
+
+ private fun collectLogs(): String {
+ return buildString {
+ append("========== Application Logs ==========\n")
+ try {
+ val process = Runtime.getRuntime().exec("logcat -d -v threadtime")
+ val reader = process.inputStream.bufferedReader()
+
+ val logLines = reader.readLines()
+ val start = maxOf(0, logLines.size - LOG_LINES)
+
+ logLines.subList(start, logLines.size).forEach {
+ append(it).append("\n")
+ }
+ } catch (e: IOException) {
+ Timber.e(e, "Error collecting logs")
+ append("Error collecting logs: ${e.message}\n")
+ }
+ }
+ }
+
+ private fun saveCrashInfoToFile(crashInfo: String) {
+ try {
+ val time = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.getDefault()).format(Date())
+ val fileName = "crash_$time.log"
+
+ val crashDir = File(context.getExternalFilesDir(null), CRASH_REPORTS_DIR)
+ if (!crashDir.exists() && !crashDir.mkdirs()) {
+ Timber.tag(TAG).e("Failed to create crash report directory")
+ return
+ }
+
+ val crashFile = File(crashDir, fileName)
+ FileOutputStream(crashFile).use { it.write(crashInfo.toByteArray()) }
+
+ Timber.tag(TAG).d("Crash info saved to: ${crashFile.absolutePath}")
+ } catch (e: Exception) {
+ Timber.tag(TAG).e(e, "Error saving crash info to file")
+ }
+ }
+
+ /**
+ * 清理旧的崩溃日志
+ */
+ fun cleanupOldCrashReports(maxAgeDays: Int = 7) {
+ val crashDir = File(context.getExternalFilesDir(null), CRASH_REPORTS_DIR)
+ if (!crashDir.exists() || !crashDir.isDirectory) return
+
+ val now = System.currentTimeMillis()
+ val maxAgeMillis = maxAgeDays * 24 * 60 * 60 * 1000L
+
+ crashDir.listFiles()?.forEach { file ->
+ if (file.lastModified() < now - maxAgeMillis) {
+ file.delete()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/utils/DateTimeUtils.kt b/app/src/main/java/com/sw/inbound/utils/DateTimeUtils.kt
new file mode 100644
index 0000000..71d6b43
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/utils/DateTimeUtils.kt
@@ -0,0 +1,37 @@
+package com.sw.inbound.utils
+
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.flow
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+
+/**
+ * 时间格式化工具类
+ */
+object DateTimeUtils {
+
+ /**
+ * 获取完整中文日期格式(示例:2025年6月11日 星期三)
+ */
+ fun getChineseDateString(date: Date = Date()): String {
+ return SimpleDateFormat("yyyy年M月d日 EEEE", Locale.CHINA).format(date)
+ }
+
+ /**
+ * 获取带时间的完整中文格式(示例:2025年6月11日 星期三 14:30)
+ */
+ fun getChineseDateTimeString(date: Date = Date()): String {
+ return SimpleDateFormat("yyyy年M月d日 EEEE HH:mm:ss", Locale.CHINA).format(date)
+ }
+
+ /**
+ * 实时时间流(每秒更新)
+ */
+ fun realTimeChineseDateFlow() = flow {
+ while (true) {
+ emit(getChineseDateString())
+ delay(1000)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/utils/FileUtils.kt b/app/src/main/java/com/sw/inbound/utils/FileUtils.kt
new file mode 100644
index 0000000..3b3f3af
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/utils/FileUtils.kt
@@ -0,0 +1,90 @@
+package com.sw.inbound.utils
+
+import android.content.ContentUris
+import android.content.Context
+import android.net.Uri
+import android.os.Build
+import android.provider.MediaStore
+import androidx.annotation.RequiresApi
+import java.io.File
+
+object FileUtils {
+ /**
+ * 通过Uri删除文件
+ * @param context 上下文
+ * @param uri 文件Uri
+ * @return Boolean 是否删除成功
+ */
+ fun deleteFileWithUri(context: Context, uri: Uri): Boolean {
+ return when {
+ // 1. 处理 content:// 类型的Uri (MediaStore)
+ uri.scheme.equals("content", ignoreCase = true) -> {
+ deleteContentUriFile(context, uri)
+ }
+ // 2. 处理 file:// 类型的Uri
+ uri.scheme.equals("file", ignoreCase = true) -> {
+ deleteFileUriFile(uri)
+ }
+ // 3. 其他情况尝试直接解析路径
+ else -> {
+ deleteFileFromPath(uri.path ?: return false)
+ }
+ }
+ }
+
+ // 删除Content Uri文件
+ private fun deleteContentUriFile(context: Context, uri: Uri): Boolean {
+ return try {
+ context.contentResolver.delete(uri, null, null) > 0
+ } catch (e: SecurityException) {
+ // Android 10+需要特殊处理
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ deleteMediaStoreFile(context, uri)
+ } else {
+ false
+ }
+ } catch (e: Exception) {
+ false
+ }
+ }
+
+ // Android 10+删除MediaStore文件
+ @RequiresApi(Build.VERSION_CODES.Q)
+ private fun deleteMediaStoreFile(context: Context, uri: Uri): Boolean {
+ val contentResolver = context.contentResolver
+ val projection = arrayOf(MediaStore.MediaColumns._ID)
+
+ return try {
+ contentResolver.query(uri, projection, null, null, null)?.use { cursor ->
+ if (cursor.moveToFirst()) {
+ val id =
+ cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))
+ val contentUri = ContentUris.withAppendedId(uri, id)
+ contentResolver.delete(contentUri, null, null) > 0
+ } else {
+ false
+ }
+ } ?: false
+ } catch (e: Exception) {
+ false
+ }
+ }
+
+ // 删除File Uri文件
+ private fun deleteFileUriFile(uri: Uri): Boolean {
+ return try {
+ File(uri.path ?: return false).delete()
+ } catch (e: Exception) {
+ false
+ }
+ }
+
+ // 直接通过路径删除文件
+ private fun deleteFileFromPath(path: String): Boolean {
+ return try {
+ File(path).delete()
+ } catch (e: Exception) {
+ false
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/sw/inbound/utils/GsonUtils.kt b/app/src/main/java/com/sw/inbound/utils/GsonUtils.kt
new file mode 100644
index 0000000..90c00d1
--- /dev/null
+++ b/app/src/main/java/com/sw/inbound/utils/GsonUtils.kt
@@ -0,0 +1,123 @@
+package com.sw.inbound.utils
+
+import com.google.gson.Gson
+import com.google.gson.GsonBuilder
+import com.google.gson.reflect.TypeToken
+import java.lang.reflect.Type
+
+object GsonUtils {
+
+ // 默认的 Gson 实例
+ private val defaultGson: Gson by lazy {
+ GsonBuilder()
+ .setDateFormat("yyyy-MM-dd HH:mm:ss") // 设置日期格式
+// .disableHtmlEscaping() // 禁止转义HTML标签
+ .create()
+ }
+
+ /**
+ * 获取默认配置的 Gson 实例
+ */
+ fun getGson(): Gson = defaultGson
+
+ /**
+ * 将对象转换为 JSON 字符串
+ * @param obj 要转换的对象
+ * @return JSON 字符串
+ */
+ fun toJson(obj: Any?): String {
+ return if (obj == null) "" else defaultGson.toJson(obj)
+ }
+
+ /**
+ * 将 JSON 字符串转换为对象
+ * @param json JSON 字符串
+ * @param clazz 目标类
+ * @return 转换后的对象
+ */
+ fun fromJson(json: String?, clazz: Class): T? {
+ if (json.isNullOrEmpty()) {
+ return null
+ }
+ return try {
+ defaultGson.fromJson(json, clazz)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ null
+ }
+ }
+
+ /**
+ * 将 JSON 字符串转换为对象 (支持泛型)
+ * @param json JSON 字符串
+ * @param type 类型令牌,用于获取泛型类型
+ * @return 转换后的对象
+ */
+ fun fromJson(json: String?, type: Type): T? {
+ if (json.isNullOrEmpty()) {
+ return null
+ }
+ return try {
+ defaultGson.fromJson(json, type)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ null
+ }
+ }
+
+ /**
+ * 将 JSON 字符串转换为 List 对象
+ * @param json JSON 字符串
+ * @param clazz List 中的元素类型
+ * @return 转换后的 List 对象
+ */
+ fun fromJsonList(json: String?, clazz: Class): List? {
+ if (json.isNullOrEmpty()) {
+ return null
+ }
+ return try {
+ val type = TypeToken.getParameterized(List::class.java, clazz).type
+ defaultGson.fromJson>(json, type)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ null
+ }
+ }
+
+ /**
+ * 将 JSON 字符串转换为 Map 对象
+ * @param json JSON 字符串
+ * @param keyClazz Map 的 key 类型
+ * @param valueClazz Map 的 value 类型
+ * @return 转换后的 Map 对象
+ */
+ fun fromJsonMap(
+ json: String?,
+ keyClazz: Class,
+ valueClazz: Class
+ ): Map? {
+ if (json.isNullOrEmpty()) {
+ return null
+ }
+ return try {
+ val type = TypeToken.getParameterized(Map::class.java, keyClazz, valueClazz).type
+ defaultGson.fromJson