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>(json, type) + } catch (e: Exception) { + e.printStackTrace() + null + } + } + + /** + * 将对象转换为另一种类型的对象 + * @param obj 源对象 + * @param clazz 目标类型 + * @return 转换后的对象 + */ + fun convert(obj: Any?, clazz: Class): T? { + if (obj == null) { + return null + } + return fromJson(toJson(obj), clazz) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/inbound/utils/ImageUtils.kt b/app/src/main/java/com/sw/inbound/utils/ImageUtils.kt new file mode 100644 index 0000000..567a5bc --- /dev/null +++ b/app/src/main/java/com/sw/inbound/utils/ImageUtils.kt @@ -0,0 +1,61 @@ +package com.sw.inbound.utils + +import android.content.Context +import android.net.Uri +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.RequestBody +import timber.log.Timber +import java.io.File + +/** + * file:///data/user/0/com.sw.inbound/cache/IMG_17515262353556856678814444882273.jpg + */ +object ImageUtils { + // 从Uri获取File + private fun getFileFromUri(context: Context, uri: Uri): File? { + return when (uri.scheme) { + "file" -> File(uri.path ?: return null) + "content" -> { + try { + val inputStream = context.contentResolver.openInputStream(uri) ?: return null + val cacheDir = context.cacheDir + val file = File.createTempFile( + "upload_${System.currentTimeMillis()}", + ".jpg", + cacheDir + ) + file.outputStream().use { output -> + inputStream.copyTo(output) + } + file + } catch (e: Exception) { + null + } + } + + else -> null + } + } + + fun genRequestPart(context: Context, imageUri: Uri): MultipartBody.Part? { + Timber.d("genRequestPart imageUri = $imageUri") + // 1. 从Uri获取文件 + val file = getFileFromUri(context, imageUri) + if (file == null) { + Timber.e("getFileFromUri file is null") + return null + } + // 2. 创建请求体 + val requestFile = RequestBody.create( + "application/octet-stream".toMediaTypeOrNull(), + file + ) + val imagePart = MultipartBody.Part.createFormData( + "file", + file.name, + requestFile + ) + return imagePart + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/inbound/utils/InteractionUtils.kt b/app/src/main/java/com/sw/inbound/utils/InteractionUtils.kt new file mode 100644 index 0000000..a33bf4a --- /dev/null +++ b/app/src/main/java/com/sw/inbound/utils/InteractionUtils.kt @@ -0,0 +1,259 @@ +package com.sw.inbound.utils + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +/** + * Jetpack Compose 交互工具集 + * 包含快速点击过滤、防抖、节流、双击检测、长按检测等功能 + */ +object InteractionUtils { + + // ======================== 点击过滤 ======================== + + /** + * 快速点击过滤器 + * @param minInterval 最小点击间隔时间(毫秒),默认500ms + */ + class ClickFilter(private val minInterval: Long = 500L) { + private var lastClickTime: Long = 0 + + /** + * 处理点击事件 + * @return Boolean 是否允许此次点击(true=允许,false=拦截) + */ + fun processClick(): Boolean { + val currentTime = System.currentTimeMillis() + return if (currentTime - lastClickTime > minInterval) { + lastClickTime = currentTime + true + } else { + false + } + } + + /** + * 处理点击事件(带回调) + */ + fun processClick(block: () -> Unit) { + if (processClick()) { + block() + } + } + } + + /** + * 记住点击过滤器 + */ + @Composable + fun rememberClickFilter(minInterval: Long = 500L): ClickFilter { + return remember { ClickFilter(minInterval) } + } + + // ======================== 防抖处理 ======================== + + /** + * 防抖处理器 + */ + class Debouncer( + private val delayMillis: Long = 300L, + private val coroutineScope: CoroutineScope + ) { + private var debounceJob: Job? = null + + /** + * 执行防抖操作 + */ + fun debounce(value: T, action: (T) -> Unit) { + debounceJob?.cancel() + debounceJob = coroutineScope.launch { + delay(delayMillis) + action(value) + } + } + } + + /** + * 记住防抖处理器 + */ + @Composable + fun rememberDebouncer( + delayMillis: Long = 300L, + coroutineScope: CoroutineScope = rememberCoroutineScope() + ): Debouncer { + return remember { Debouncer(delayMillis, coroutineScope) } + } + + // ======================== 节流处理 ======================== + + /** + * 节流处理器 + */ + class Throttler(private val timeoutMs: Long = 300L) { + private var lastRunTime: Long = 0 + + /** + * 执行节流操作 + */ + fun throttle(block: () -> Unit) { + val now = System.currentTimeMillis() + if (now - lastRunTime > timeoutMs) { + lastRunTime = now + block() + } + } + } + + /** + * 记住节流处理器 + */ + @Composable + fun rememberThrottler(timeoutMs: Long = 300L): Throttler { + return remember { Throttler(timeoutMs) } + } + + // ======================== 双击检测 ======================== + + /** + * 双击检测器 + */ + class DoubleClickDetector( + private val timeout: Long = 300L, + private val onSingleClick: () -> Unit = {}, + private val onDoubleClick: () -> Unit + ) { + private var clickCount by mutableStateOf(0) + private var lastClickTime by mutableStateOf(0L) + + /** + * 处理点击事件 + */ + fun processClick(coroutineScope: CoroutineScope) { + val currentTime = System.currentTimeMillis() + if (currentTime - lastClickTime < timeout) { + clickCount++ + if (clickCount == 2) { + onDoubleClick() + clickCount = 0 + } + } else { + clickCount = 1 + coroutineScope.launch { + delay(timeout) + if (clickCount == 1) { + onSingleClick() + } + clickCount = 0 + } + } + lastClickTime = currentTime + } + } + + /** + * 记住双击检测器 + */ + @Composable + fun rememberDoubleClickDetector( + timeout: Long = 300L, + onSingleClick: () -> Unit = {}, + onDoubleClick: () -> Unit + ): () -> Unit { + val detector = remember { DoubleClickDetector(timeout, onSingleClick, onDoubleClick) } + val scope = rememberCoroutineScope() + + return { + detector.processClick(scope) + } + } + + // ======================== 长按检测 ======================== + + /** + * 长按检测器 + */ + class LongPressDetector( + private val delay: Long = 1000L, + private val onLongPress: () -> Unit, + private val onClick: () -> Unit = {} + ) { + private var pressJob: Job? = null + + /** + * 处理按压事件 + */ + fun handlePress(coroutineScope: CoroutineScope) { + pressJob = coroutineScope.launch { + delay(delay) + onLongPress() + } + } + + /** + * 处理释放事件 + */ + fun handleRelease() { + pressJob?.cancel() + pressJob = null + onClick() + } + } + + /** + * 记住长按检测器 + */ + @Composable + fun rememberLongPressDetector( + delay: Long = 1000L, + onLongPress: () -> Unit, + onClick: () -> Unit = {} + ): Pair<() -> Unit, () -> Unit> { + val detector = remember { LongPressDetector(delay, onLongPress, onClick) } + val scope = rememberCoroutineScope() + + return Pair( + first = { detector.handlePress(scope) }, + second = { detector.handleRelease() } + ) + } + + // ======================== 组合工具 ======================== + + /** + * 带状态的按钮控制器 + */ + class StatefulButtonController { + var isLoading by mutableStateOf(false) + private val clickFilter = ClickFilter() + + /** + * 处理按钮点击 + */ + suspend fun handleClick(block: suspend () -> Unit) { + if (clickFilter.processClick()) { + isLoading = true + try { + block() + } finally { + isLoading = false + } + } + } + } + + /** + * 记住带状态的按钮控制器 + */ + @Composable + fun rememberStatefulButtonController(): StatefulButtonController { + return remember { StatefulButtonController() } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/inbound/utils/SPUtil.kt b/app/src/main/java/com/sw/inbound/utils/SPUtil.kt new file mode 100644 index 0000000..3b81934 --- /dev/null +++ b/app/src/main/java/com/sw/inbound/utils/SPUtil.kt @@ -0,0 +1,110 @@ +package com.sw.inbound.utils + +import android.content.Context +import androidx.core.content.edit +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.map +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +class SPUtil private constructor(context: Context, private val spName: String) { + + companion object { + @Volatile + private var instance: SPUtil? = null + + fun getInstance( + context: Context = ContextUtils.getAppContext(), + spName: String = "default_sp" + ): SPUtil { + return instance ?: synchronized(this) { + instance ?: SPUtil(context.applicationContext, spName).also { instance = it } + } + } + } + + private val sharedPreferences by lazy { + context.getSharedPreferences(spName, Context.MODE_PRIVATE) + } + + // 基础存储方法 + fun put(key: String, value: Any?) { + when (value) { + null -> remove(key) // 存入null视为删除 + is String -> sharedPreferences.edit { putString(key, value) } + is Int -> sharedPreferences.edit { putInt(key, value) } + is Long -> sharedPreferences.edit { putLong(key, value) } + is Float -> sharedPreferences.edit { putFloat(key, value) } + is Boolean -> sharedPreferences.edit { putBoolean(key, value) } + is Set<*> -> sharedPreferences.edit { putStringSet(key, value as Set) } + else -> throw IllegalArgumentException("Unsupported type: ${value.javaClass.name}") + } + notifyDataChanged(key) + } + + @Suppress("UNCHECKED_CAST") + fun get(key: String, defaultValue: T? = null): T? { + return when (defaultValue) { + is String -> sharedPreferences.getString(key, defaultValue) as T + is Int -> sharedPreferences.getInt(key, defaultValue) as T + is Long -> sharedPreferences.getLong(key, defaultValue) as T + is Float -> sharedPreferences.getFloat(key, defaultValue) as T + is Boolean -> sharedPreferences.getBoolean(key, defaultValue) as T + is Set<*> -> sharedPreferences.getStringSet(key, defaultValue as Set) as T + null -> when { + sharedPreferences.contains(key) -> get(key, "") as? T // 尝试作为String获取 + else -> null + } + + else -> throw IllegalArgumentException("Unsupported type: ${defaultValue.javaClass.name}") + } + } + + fun remove(key: String) { + if (sharedPreferences.contains(key)) { + sharedPreferences.edit { remove(key) } + notifyDataChanged(key) + } + } + + fun clear() { + sharedPreferences.edit { clear() } + notifyDataChanged(null) + } + + fun contains(key: String): Boolean { + return sharedPreferences.contains(key) + } + + // 监听变化 + private val dataChangeFlow = MutableStateFlow(0) + + private fun notifyDataChanged(key: String?) { + dataChangeFlow.value++ + } + + fun observeKey(key: String): Flow { + return dataChangeFlow.map { get(key) } + } + + // 属性委托支持 + fun int(key: String, default: Int = 0) = SpProperty(key, default) + fun long(key: String, default: Long = 0L) = SpProperty(key, default) + fun float(key: String, default: Float = 0f) = SpProperty(key, default) + fun boolean(key: String, default: Boolean = false) = SpProperty(key, default) + fun string(key: String, default: String = "") = SpProperty(key, default) + fun stringSet(key: String, default: Set = emptySet()) = SpProperty(key, default) + + inner class SpProperty(private val key: String, private val defaultValue: T) : + ReadWriteProperty { + + override fun getValue(thisRef: Any?, property: KProperty<*>): T { + return get(key, defaultValue) ?: defaultValue + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { + put(key, value) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/inbound/utils/TextFieldState.kt b/app/src/main/java/com/sw/inbound/utils/TextFieldState.kt new file mode 100644 index 0000000..31f4d6c --- /dev/null +++ b/app/src/main/java/com/sw/inbound/utils/TextFieldState.kt @@ -0,0 +1,105 @@ +package com.sw.inbound.utils + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue + +open class TextFieldState( + initialText: String = "", + initialSelection: TextRange = TextRange(initialText.length) +) { + private var _value by mutableStateOf(TextFieldValue(initialText, initialSelection)) + + open var value: TextFieldValue + get() = _value + set(newValue) { + _value = newValue + } + + open fun updateFromString(text: String) { + _value = TextFieldValue(text, TextRange(text.length)) + } + + val text: String get() = _value.text +} + +class AmountFieldState(initialAmount: String = "") : TextFieldState(initialAmount) { + // 获取原始数字字符串(不含格式字符) + fun getRawAmount(): String = value.text.filter { it.isDigit() } + + // 获取Double类型的金额值 + fun getAmountValue(): Double = value.text + .filter { it.isDigit() || it == '.' } + .toDoubleOrNull() ?: 0.0 + + // 重写value的setter以实现金额格式化 + override var value: TextFieldValue + get() = super.value + set(newValue) { + super.value = formatAmountValue(newValue) + } + + // 从外部更新金额(如从数据库加载) + override fun updateFromString(amount: String) { + super.updateFromString(formatAmount(amount)) + } + + private fun formatAmountValue(input: TextFieldValue): TextFieldValue { + val filtered = input.text.filter { it.isDigit() } + val formatted = formatAmount(filtered) + + // 计算新光标位置 + val newCursorPos = calculateNewCursorPosition( + originalText = input.text, + originalSelection = input.selection, + filteredText = filtered, + formattedText = formatted + ) + + return TextFieldValue( + text = formatted, + selection = TextRange(newCursorPos) + ) + } + + private fun formatAmount(amount: String): String { + val filtered = amount.filter { it.isDigit() } + return when { + filtered.isEmpty() -> "0.00" + filtered.length <= 2 -> "0.${filtered.padStart(2, '0')}" + else -> "${filtered.dropLast(2)}.${filtered.takeLast(2)}" + } + } + + private fun calculateNewCursorPosition( + originalText: String, + originalSelection: TextRange, + filteredText: String, + formattedText: String + ): Int { + // 如果在末尾添加,保持光标在末尾 + if (originalSelection.start >= originalText.length) { + return formattedText.length + } + + // 计算原始文本中光标前的数字个数 + val digitsBeforeCursor = originalText + .substring(0, originalSelection.start) + .count { it.isDigit() } + + // 在格式化文本中找到对应位置 + var digitCount = 0 + formattedText.forEachIndexed { index, char -> + if (char.isDigit()) { + digitCount++ + if (digitCount > digitsBeforeCursor) { + return index + } + } + } + + return formattedText.length + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/inbound/utils/ThreadUtils.kt b/app/src/main/java/com/sw/inbound/utils/ThreadUtils.kt new file mode 100644 index 0000000..eb28660 --- /dev/null +++ b/app/src/main/java/com/sw/inbound/utils/ThreadUtils.kt @@ -0,0 +1,124 @@ +package com.sw.inbound.utils + +import android.os.Handler +import android.os.Looper +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import kotlin.coroutines.CoroutineContext + +/** + * 多功能线程工具类 + * 结合协程、Handler和线程池实现线程切换 + */ +object ThreadUtils : CoroutineScope { + // 主线程Handler + private val mainHandler by lazy { Handler(Looper.getMainLooper()) } + + // 后台线程池(IO密集型任务) + private val ioThreadPool: ExecutorService by lazy { + Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2) + } + + // CPU密集型线程池 + private val cpuThreadPool: ExecutorService by lazy { + Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()) + } + + // 协程Job管理 + private val job = Job() + + override val coroutineContext: CoroutineContext + get() = Dispatchers.Main + job + + // ========== Handler相关方法 ========== + + /** + * 在主线程执行任务 + * @param delayMillis 延迟时间(毫秒) + */ + fun runOnUiThread(delayMillis: Long = 0, block: () -> Unit) { + if (delayMillis > 0) { + mainHandler.postDelayed(block, delayMillis) + } else { + if (isOnMainThread()) { + block() + } else { + mainHandler.post(block) + } + } + } + + /** + * 移除主线程任务 + */ + fun removeUiThreadTask(block: () -> Unit) { + mainHandler.removeCallbacks(block) + } + + // ========== 线程池相关方法 ========== + + /** + * 在IO线程执行任务 + */ + fun runOnIoThread(block: () -> Unit) { + ioThreadPool.execute(block) + } + + /** + * 在CPU计算线程执行任务 + */ + fun runOnCpuThread(block: () -> Unit) { + cpuThreadPool.execute(block) + } + + // ========== 协程相关方法 ========== + + /** + * 启动协程(默认在主线程) + */ + fun launch(block: suspend CoroutineScope.() -> Unit): Job { + return launch(coroutineContext, block = block) + } + + /** + * 在IO线程启动协程 + */ + fun launchOnIo(block: suspend CoroutineScope.() -> Unit): Job { + return launch(Dispatchers.IO, block = block) + } + + /** + * 切换到主线程(协程环境) + */ + suspend fun switchToMain(block: suspend CoroutineScope.() -> T): T { + return withContext(Dispatchers.Main, block) + } + + /** + * 切换到IO线程(协程环境) + */ + suspend fun switchToIo(block: suspend CoroutineScope.() -> T): T { + return withContext(Dispatchers.IO, block) + } + + /** + * 是否在主线程 + */ + fun isOnMainThread(): Boolean { + return Looper.myLooper() == Looper.getMainLooper() + } + + /** + * 释放资源 + */ + fun release() { + job.cancel() + ioThreadPool.shutdown() + cpuThreadPool.shutdown() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/inbound/utils/ToastUtils.kt b/app/src/main/java/com/sw/inbound/utils/ToastUtils.kt new file mode 100644 index 0000000..1f2f43f --- /dev/null +++ b/app/src/main/java/com/sw/inbound/utils/ToastUtils.kt @@ -0,0 +1,56 @@ +package com.sw.inbound.utils + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +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.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import kotlinx.coroutines.delay + +object ToastUtils { + private var show by mutableStateOf(false) + private var message by mutableStateOf("") + + fun showToast(msg: String) { + message = msg + show = true +// Toast.makeText(ContextUtils.getAppContext(), msg, Toast.LENGTH_LONG).show() + } + + @Composable + fun ToastComposable() { + if (show) { + LaunchedEffect(Unit) { + delay(2000) // 自动2秒后消失 + show = false + } + Box( + modifier = Modifier +// .fillMaxWidth() + .fillMaxSize() + .padding(bottom = 56.dp), + contentAlignment = Alignment.BottomCenter + ) { + Text( + text = message, + modifier = Modifier + .background(Color.Black.copy(alpha = 0.7f), RoundedCornerShape(8.dp)) + .padding(horizontal = 24.dp, vertical = 12.dp), + color = Color.White, + fontSize = 24.sp + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/inbound/viewmodel/BaseViewModel.kt b/app/src/main/java/com/sw/inbound/viewmodel/BaseViewModel.kt new file mode 100644 index 0000000..4df2f61 --- /dev/null +++ b/app/src/main/java/com/sw/inbound/viewmodel/BaseViewModel.kt @@ -0,0 +1,151 @@ +package com.sw.inbound.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.sw.inbound.GlobalData +import com.sw.inbound.model.response.ApiResponse +import com.sw.inbound.model.response.DictType +import com.sw.inbound.network.LoadingState +import com.sw.inbound.repository.RemoteRepository +import com.sw.inbound.utils.ToastUtils +import kotlinx.coroutines.launch +import retrofit2.HttpException +import timber.log.Timber +import java.io.IOException + +abstract class BaseViewModel( + private val repository: RemoteRepository +) : ViewModel() { + protected fun launchWithLoading(block: suspend () -> Unit) { + viewModelScope.launch { + try { + LoadingState.show() + block() + } catch (e: Exception) { + // 错误处理可被子类重写 + handleError(e) + } finally { + LoadingState.hide() + } + } + } + + protected fun launch(block: suspend () -> Unit) { + viewModelScope.launch() { + try { + block() + } catch (e: Exception) { + // 错误处理可被子类重写 + handleError(e) + } + } + } + + protected open fun parseResponse(response: ApiResponse<*>): Boolean { + if (response.isSuccess()) { + return true + } + Timber.d("msg = ${response.msg}, code = ${response.code}") + ToastUtils.showToast("${response.msg}(${response.code})") + return false + } + + protected open fun handleError(e: Exception) = { + Timber.e(e) + val message = when (e) { + is IOException -> "网络连接异常" + is HttpException -> "服务器错误: ${e.code()}" + else -> "操作失败: ${e.message}" + } + ToastUtils.showToast(message) + } + + fun getDictType() { + Timber.d("获取所有字典列表") + launchWithLoading { + val response = repository.getGoodsStorageType() + if (response.isSuccess()) { + Timber.d("getGoodsStorageType data = ${response.data}") + response.data?.let { +// val dictType = DictType(response.data) + val list = mutableListOf() + for (type in response.data) { + list.add(DictType(type.itemValue!!.toInt(), type.itemText!!)) + } + GlobalData.storageTypeList = list + } + } else { + Timber.e("getGoodsStorageType msg = ${response.msg}, code = ${response.code}") + } + val response1 = repository.getGoodsType() + if (response1.isSuccess()) { + Timber.d("getGoodsType data = ${response1.data?.allType}") + response1.data?.let { + if (response1.data.allType != null) { + val list = mutableListOf() + for (type in response1.data.allType) { + list.add(DictType(type!!.id!!, type.typeName!!)) + } + GlobalData.goodsTypeList = list + } + } + } else { + Timber.e("getGoodsType msg = ${response1.msg}, code = ${response1.code}") + } + val response2 = repository.getDictType(RemoteRepository.TypeEnum.WAREHOUSE) + if (response2.isSuccess()) { + Timber.d("getDictType WAREHOUSE data = ${response2.data}") + response2.data?.let { + GlobalData.warehouseTypeList = response2.data + } + } else { + Timber.e("getDictType WAREHOUSE msg = ${response2.msg}, code = ${response2.code}") + } + val response3 = repository.getDictType(RemoteRepository.TypeEnum.SUPPLIER) + if (response3.isSuccess()) { + Timber.d("getDictType SUPPLIER data = ${response3.data}") + response3.data?.let { + GlobalData.supplierTypeList = response3.data + } + } else { + Timber.e("getDictType SUPPLIER msg = ${response3.msg}, code = ${response3.code}") + } + val response4 = repository.getDictType(RemoteRepository.TypeEnum.UNIT) + if (response4.isSuccess()) { + Timber.d("getDictType UNIT data = ${response4.data}") + response4.data?.let { + GlobalData.unitTypeList = response4.data + } + } else { + Timber.e("getDictType UNIT msg = ${response4.msg}, code = ${response4.code}") + } + } + } + + fun getStoreList(): ArrayList { + return arrayListOf( + "默认仓库", + "仓库1", + "仓库2", + "仓库3", + ) + } + + fun getPurchasingUnit(): ArrayList { + return arrayListOf( + "斤", + "瓶", + "箱" + ) + } + + fun getProductList(): ArrayList { + return arrayListOf( + "胶东大白菜", + "玉田尖白菜1", + "玉田尖白菜2", + "玉田尖白菜3", + "玉田尖白菜4" + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/inbound/viewmodel/ProductViewModel.kt b/app/src/main/java/com/sw/inbound/viewmodel/ProductViewModel.kt new file mode 100644 index 0000000..f556a58 --- /dev/null +++ b/app/src/main/java/com/sw/inbound/viewmodel/ProductViewModel.kt @@ -0,0 +1,31 @@ +package com.sw.inbound.viewmodel + +import com.sw.inbound.model.response.SupplierInfo +import com.sw.inbound.repository.RemoteRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@HiltViewModel +class ProductViewModel @Inject constructor( + private val repository: RemoteRepository +) : BaseViewModel(repository) { + + private val _supplierList = MutableStateFlow>(emptyList()) + val supplierList: StateFlow> = _supplierList + + + fun getOrderList(pageNum: Int = 0, pageSize: Int = 20) { + launchWithLoading { + val response = repository.getReceiveList(pageNum, pageSize) + if (response.isSuccess()) { + response.data?.records?.let { + _supplierList.value = it + } + } + } + } + +} + diff --git a/app/src/main/java/com/sw/inbound/viewmodel/ReceiptViewModel.kt b/app/src/main/java/com/sw/inbound/viewmodel/ReceiptViewModel.kt new file mode 100644 index 0000000..0d755b3 --- /dev/null +++ b/app/src/main/java/com/sw/inbound/viewmodel/ReceiptViewModel.kt @@ -0,0 +1,237 @@ +package com.sw.inbound.viewmodel + +import androidx.lifecycle.viewModelScope +import com.sw.inbound.ext.toSafeBigDecimal +import com.sw.inbound.ext.toSafeFloat +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.model.response.PurchaseInfo +import com.sw.inbound.model.response.SearchGoodsInfo +import com.sw.inbound.repository.RemoteRepository +import com.sw.inbound.sdk.SensorScaleUtils +import com.sw.inbound.utils.ToastUtils +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import timber.log.Timber +import javax.inject.Inject + +@HiltViewModel +class ReceiptViewModel @Inject constructor( + private val repository: RemoteRepository +) : BaseViewModel(repository) { + // 调整状态 + private val _adjustState = MutableStateFlow(true) + val adjustState: StateFlow = _adjustState + + private val _showReceiptDialog = MutableStateFlow(false) + val showReceiptDialog: StateFlow = _showReceiptDialog + + // 当前要收货的供应商 + private val _currentPurchaseInfo = MutableStateFlow(null) + val currentPurchaseInfo: StateFlow = _currentPurchaseInfo + + // 自动从 currentPurchaseInfo 派生 orders +// private val _orders: StateFlow> = currentPurchaseInfo +// .map { purchaseInfo -> +// purchaseInfo?.receiveGoodsInfoList ?: emptyList() +// } +// .stateIn( +// viewModelScope, +// SharingStarted.WhileSubscribed(5000), // 或者使用 Lazily/Eagerly 根据需求 +// emptyList() +// ) + private val _orders = MutableStateFlow>(emptyList()) + val orders: StateFlow> = _orders.asStateFlow() + + fun initOrders() { + _orders.value = _currentPurchaseInfo.value?.receiveGoodsInfoList ?: emptyList() + } + + // 已调整列表 + val adjustedOrders: StateFlow> = _orders + .map { orders -> orders.filter { it.isAdjusted && it.goodId != null } } + .stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) + + // 未调整列表 + val unadjustedOrders: StateFlow> = _orders + .map { orders -> orders.filter { !it.isAdjusted && it.goodId != null } } + .stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) + + val mergedOrders: StateFlow> = _orders + + // + private val _selectedItem = MutableStateFlow(null) + val selectedItem: StateFlow = _selectedItem + + // 搜索物品列表 + private val _searchListItems = MutableStateFlow>(emptyList()) + val searchListItems: StateFlow> = _searchListItems + + private val _receiptResult = MutableStateFlow(false) + val receiptResult: StateFlow = _receiptResult + + private val _countUserInput = MutableStateFlow(false) + + /** + * 更新调整状态 + * @param isAdjustState true 已调整 false 未调整 + */ + fun updateAdjustState(isAdjustState: Boolean) { + _adjustState.value = isAdjustState + } + + /** + * 更新收货提示弹窗 + */ + fun updateReceiptDialog(showDialog: Boolean) { + _showReceiptDialog.value = showDialog + } + + /** + * 更新选中的item + */ + fun updateSelectedItem(purchaseOrder: GoodsInfo?) { + _selectedItem.value = purchaseOrder + } + + fun getReceiveDetail(id: Int) { + launchWithLoading { + val response = repository.getReceiveDetail(id) + if (response.isSuccess()) { + _currentPurchaseInfo.value = response.data + initOrders() + } + } + } + + /** + * 更新当前供应商信息 + */ + fun updateCurrentPurchaseInfo(purchaseInfo: PurchaseInfo?) { + _currentPurchaseInfo.value = purchaseInfo + } + + // 添加订单 + fun addPurchaseItem(purchaseOrder: GoodsInfo) { + _orders.update { currentList -> + // 当已经添加过则忽略 + if (currentList.any { it.goodId == purchaseOrder.goodId }) { + currentList + } else { + currentList + purchaseOrder + } + } + } + + // 更新订单 + fun updatePurchaseItem(purchaseOrder: GoodsInfo) { + _orders.update { currentList -> + currentList.map { order -> + if (order.goodId == purchaseOrder.goodId) purchaseOrder else order + } + } + } + + /** + * 有异常的订单数量 + */ + fun hasWrongCount(): Boolean { + return _orders.value.any { + it.receiveCount != it.receivedNum + } + } + + /** + * 更新所有商品仓库 + */ + fun updateAllPurchaseStore(store: DictType, predicate: (GoodsInfo) -> Boolean = { true }) { + _orders.update { currentList -> + currentList.map { order -> + if (predicate(order)) order.copy( + warehouseId = store.id, + warehouseName = store.value + ) else order + } + } + } + + // 删除订单 + fun removePurchaseOrder(orderId: Int) { + _orders.update { current -> + current.filterNot { it.goodId == orderId } + } + } + + // 清空列表 + fun clearPurchaseOrders() { + _orders.value = emptyList() + } + + fun updateCountInputState(boolean: Boolean) { + _countUserInput.value = boolean + } + + fun startSensorScale() { + Timber.d("开始称重") + SensorScaleUtils.startScale(callback = { weight -> + val currentItem = _selectedItem.value + + currentItem?.let { info -> + val consumeValue = currentItem.consumeValue?.toDouble() ?: 1.0 + val purchaseValue = currentItem.purchaseValue?.toDouble() ?: 1.0 + val count = + weight * 1000 / consumeValue / purchaseValue + val finalCount = count.toSafeFloat(currentItem.unitName) + Timber.d("startSensorScale weight = $weight, count = $count, finalCount = $finalCount, consumeValue = $consumeValue, purchaseValue = $purchaseValue") + _selectedItem.value = info.copy( + goodsWeight = weight.toSafeBigDecimal(), + receivedNum = if (_countUserInput.value) info.receivedNum else finalCount + ) + } + }) + } + + fun stopSensorScale() { + Timber.d("关闭称重") + SensorScaleUtils.stopContinuousRead() + } + + fun partialReceipt(uploadInfo: UploadInfo) { + launchWithLoading { + val response = repository.partialReceipt(uploadInfo) + if (!response.isSuccess()) { + parseResponse(response) + return@launchWithLoading + } + if (response.success == true) { + ToastUtils.showToast("部分收货成功") + _receiptResult.value = true + } else { + ToastUtils.showToast("部分收货失败") + } + } + } + + fun confirmReceipt(uploadInfo: UploadInfo) { + launchWithLoading { + val response = repository.confirmReceipt(uploadInfo) + if (!response.isSuccess()) { + parseResponse(response) + return@launchWithLoading + } + if (response.success == true) { + ToastUtils.showToast("收货成功") + _receiptResult.value = true + } else { + ToastUtils.showToast("收货失败") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/inbound/viewmodel/SelfProcurementViewModel.kt b/app/src/main/java/com/sw/inbound/viewmodel/SelfProcurementViewModel.kt new file mode 100644 index 0000000..820cbc0 --- /dev/null +++ b/app/src/main/java/com/sw/inbound/viewmodel/SelfProcurementViewModel.kt @@ -0,0 +1,231 @@ +package com.sw.inbound.viewmodel + +import androidx.lifecycle.viewModelScope +import com.sw.inbound.GlobalData +import com.sw.inbound.ext.toSafeBigDecimal +import com.sw.inbound.ext.toSafeDouble +import com.sw.inbound.model.request.GoodsAddParam +import com.sw.inbound.model.request.PurchaseWarehouseParam +import com.sw.inbound.model.response.DictType +import com.sw.inbound.model.response.SearchGoodsInfo +import com.sw.inbound.repository.RemoteRepository +import com.sw.inbound.sdk.SensorScaleUtils +import com.sw.inbound.utils.ToastUtils +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@HiltViewModel +class SelfProcurementViewModel @Inject constructor( + private val repository: RemoteRepository +) : BaseViewModel(repository) { + + private val _addGoodsResult = MutableStateFlow(false) + val addGoodsResult: StateFlow = _addGoodsResult + + private val _addToWarehouseResult = MutableStateFlow(false) + val addToWarehouseResult: StateFlow = _addToWarehouseResult + + private val _selectedItem = MutableStateFlow(null) + val selectedItem: StateFlow = _selectedItem + + // 全局仓库信息 + private val _globalWarehouse = MutableStateFlow(DictType(-1, "选择仓库")) + val globalWarehouse: StateFlow = _globalWarehouse + + private val _purchaseList = MutableStateFlow>(emptyList()) + val purchaseList: StateFlow> = _purchaseList + + // 表单状态 + private val _goodsAddParam = MutableStateFlow(GoodsAddParam()) + val goodsAddParam: StateFlow = _goodsAddParam + + // 快速添加弹窗状态 + private val _showAddProductDialog = MutableStateFlow(false) + val showAddProductDialog: StateFlow = _showAddProductDialog + + // 搜索物品列表 + private val _searchListItems = MutableStateFlow>(emptyList()) + val searchListItems: StateFlow> = _searchListItems + + // 称重结果 + private val _weightInfo = MutableStateFlow(0.0) + val weightInfo: StateFlow = _weightInfo + + /** + * 数量是否是用户输入的值 + */ + private val _countUserInput = MutableStateFlow(false) + + /** + * 更新全局仓库 + */ + fun updateGlobalWarehouse(dictType: DictType) { + _globalWarehouse.value = dictType + } + + fun startSensorScale() { + Timber.d("开始称重") + SensorScaleUtils.startScale(callback = { weight -> + val currentItem = _selectedItem.value + + currentItem?.let { info -> + val selectUnitType = currentItem.selectUnitType + if (selectUnitType == null) return@startScale + + val consumeValue = selectUnitType.consumeValue?.toDouble() ?: 1.0 + val purchaseValue = selectUnitType.purchaseValue?.toDouble() ?: 1.0 + val count = + weight * 1000 / consumeValue / purchaseValue + val finalCount = count.toSafeDouble(currentItem.unitName) + Timber.d("startSensorScale weight = $weight, count = $count, finalCount = $finalCount, consumeValue = $consumeValue, purchaseValue = $purchaseValue") + _selectedItem.value = info.copy( + goodsWeight = weight.toSafeBigDecimal(), + goodsCount = if (_countUserInput.value) info.goodsCount else finalCount + ) + } + }) + } + + fun stopSensorScale() { + Timber.d("关闭称重") + SensorScaleUtils.stopContinuousRead() + } + + fun updateAddProductDialog(show: Boolean) { + _showAddProductDialog.value = show + } + + fun updateSelectedItem(purchaseOrder: PurchaseWarehouseParam?) { + _selectedItem.value = purchaseOrder + } + + fun updateGoodsAddParam(newState: GoodsAddParam) { + _goodsAddParam.value = newState + } + + fun cleanGoodsAddParam() { + _goodsAddParam.value = GoodsAddParam() + } + + // 添加订单 + fun addPurchaseItem(purchaseOrder: PurchaseWarehouseParam) { + _purchaseList.update { currentList -> + // 当已经添加过则忽略 + if (currentList.any { it.goodsId == purchaseOrder.goodsId }) { + currentList + } else { + currentList + purchaseOrder + } + } + updateSelectedItem(null) + } + + // 更新订单 + fun updatePurchaseItem(purchaseOrder: PurchaseWarehouseParam) { + _purchaseList.update { currentList -> + currentList.map { order -> + if (order.goodsId == purchaseOrder.goodsId) purchaseOrder else order + } + } + } + + // 删除订单 + fun removePurchaseOrder(orderId: Int) { + _purchaseList.update { current -> + current.filterNot { it.goodsId == orderId } + } + } + + // 清空列表 + fun clearPurchaseOrders() { + _purchaseList.value = emptyList() + } + + fun searchGoodsInfoList( + goodsName: String, + pageNo: Int = 0, + pageSize: Int = 10 + ) { + launch { + val response = repository.searchGoodsInfoList(goodsName, pageNo, pageSize) + if (response.isSuccess()) { + val data = response.data + if (data != null) { + _searchListItems.value = data.records ?: emptyList() + } + } else { + _searchListItems.value = emptyList() + } + } + } + + fun updateSelectedItemWithSearch(searchFirst: SearchGoodsInfo.Record) { + val warehouse = _globalWarehouse.value + viewModelScope.launch { + updateSelectedItem(null) + delay(50) + _selectedItem.value = PurchaseWarehouseParam( + warehouseId = warehouse.id, + goodsId = searchFirst.goodsId!!, + goodsName = searchFirst.goodsName, + kcUnitId = searchFirst.kcUnitId!!, + unitList = searchFirst.unitVoList, + ) + } + } + + fun updateCountInputState(boolean: Boolean) { + _countUserInput.value = boolean + } + + fun addGoodsInfo() { + val imageUri = GlobalData.imageUri + if (imageUri == null) { + ToastUtils.showToast("请先进行图片采集") + return + } + val errInfo = _goodsAddParam.value.hasNullField() + if (errInfo != null) { + ToastUtils.showToast(errInfo) + return + } + + launchWithLoading { + val uploadResponse = repository.uploadImage(imageUri) + if (!uploadResponse.isSuccess()) { + parseResponse(uploadResponse) + return@launchWithLoading + } +// ToastUtils.showToast("图片上传成功,${uploadResponse.data}") + GlobalData.imageUri = null + _goodsAddParam.value.relativeUrl = uploadResponse.data + val response = repository.selfPurchaseGoodsAdd(_goodsAddParam.value) + if (!response.isSuccess()) { + parseResponse(response) + return@launchWithLoading + } + _showAddProductDialog.value = false + val searchFirst = response.data!!.records?.get(0) + searchFirst?.let { + updateSelectedItemWithSearch(searchFirst) + } + _addGoodsResult.value = true + } + } + + fun addToWarehouse() { + if (_purchaseList.value.isEmpty()) return + launchWithLoading { + val response = repository.selfPurchaseWarehousing(_purchaseList.value) + if (parseResponse(response)) { + _addToWarehouseResult.value = true + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/inbound/viewmodel/UserViewModel.kt b/app/src/main/java/com/sw/inbound/viewmodel/UserViewModel.kt new file mode 100644 index 0000000..3265248 --- /dev/null +++ b/app/src/main/java/com/sw/inbound/viewmodel/UserViewModel.kt @@ -0,0 +1,66 @@ +package com.sw.inbound.viewmodel + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import com.sw.inbound.GlobalKey +import com.sw.inbound.model.request.LoginParam +import com.sw.inbound.model.response.User +import com.sw.inbound.repository.RemoteRepository +import com.sw.inbound.utils.GsonUtils +import com.sw.inbound.utils.SPUtil +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import timber.log.Timber +import javax.inject.Inject + +@HiltViewModel +class UserViewModel @Inject constructor( + private val repo: RemoteRepository +) : BaseViewModel(repo) { + + private var isFirstLaunch by mutableStateOf(true) + + private val _user = MutableStateFlow(null) + val user = _user.asStateFlow() + + fun getInitInfo() { + Timber.d("getInitInfo isFirstLaunch = $isFirstLaunch") + if (isFirstLaunch) { + + getUserInfo() + getDictType() + isFirstLaunch = false + } + } + + fun login(userName: String, password: String) = launchWithLoading { + val loginParam = LoginParam(userName = userName, password = password) + val response = repo.login(loginParam) + if (response.isSuccess()) { + _user.value = response.data + saveToken(response.data) + response.data?.token + } + } + + fun getUserInfo() { + val userInfo = SPUtil.getInstance().get(GlobalKey.KEY_USER_INFO, "") + if (userInfo != null) { + _user.value = GsonUtils.fromJson(userInfo, User::class.java) + } + } + + private fun saveToken(user: User?) { + user?.let { + SPUtil.getInstance().put(GlobalKey.KEY_USER_INFO, GsonUtils.toJson(it)) + SPUtil.getInstance().put(GlobalKey.KEY_TOKEN, it.token) + } + } + + fun logout() { + SPUtil.getInstance().remove(GlobalKey.KEY_USER_INFO) + SPUtil.getInstance().remove(GlobalKey.KEY_TOKEN) + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/bg.png b/app/src/main/res/mipmap-xxhdpi/bg.png new file mode 100644 index 0000000..7bef335 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/bg.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/bg_divider.png b/app/src/main/res/mipmap-xxhdpi/bg_divider.png new file mode 100644 index 0000000..8b31716 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/bg_divider.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/bg_listview.png b/app/src/main/res/mipmap-xxhdpi/bg_listview.png new file mode 100644 index 0000000..7f8c8a5 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/bg_listview.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_add.png b/app/src/main/res/mipmap-xxhdpi/ic_add.png new file mode 100644 index 0000000..e5704eb Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_add.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_btn_back.png b/app/src/main/res/mipmap-xxhdpi/ic_btn_back.png new file mode 100644 index 0000000..5687452 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_btn_back.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_btn_confirm.png b/app/src/main/res/mipmap-xxhdpi/ic_btn_confirm.png new file mode 100644 index 0000000..25e1c88 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_btn_confirm.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_camera.png b/app/src/main/res/mipmap-xxhdpi/ic_camera.png new file mode 100644 index 0000000..3eaa632 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_camera.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_camera_small.png b/app/src/main/res/mipmap-xxhdpi/ic_camera_small.png new file mode 100644 index 0000000..f846c12 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_camera_small.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_delete.png b/app/src/main/res/mipmap-xxhdpi/ic_delete.png new file mode 100644 index 0000000..bd7efcf Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_delete.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_empty.png b/app/src/main/res/mipmap-xxhdpi/ic_empty.png new file mode 100644 index 0000000..ae06733 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_empty.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_home.png b/app/src/main/res/mipmap-xxhdpi/ic_home.png new file mode 100644 index 0000000..05478f7 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_home.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_logout.png b/app/src/main/res/mipmap-xxhdpi/ic_logout.png new file mode 100644 index 0000000..28f07dc Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_logout.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_order_purchase.png b/app/src/main/res/mipmap-xxhdpi/ic_order_purchase.png new file mode 100644 index 0000000..ed6cb00 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_order_purchase.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_order_self_procurement.png b/app/src/main/res/mipmap-xxhdpi/ic_order_self_procurement.png new file mode 100644 index 0000000..2a38695 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_order_self_procurement.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_receipt_empty.png b/app/src/main/res/mipmap-xxhdpi/ic_receipt_empty.png new file mode 100644 index 0000000..4d22cfe Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_receipt_empty.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_scan.png b/app/src/main/res/mipmap-xxhdpi/ic_scan.png new file mode 100644 index 0000000..1d05718 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_scan.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_search.png b/app/src/main/res/mipmap-xxhdpi/ic_search.png new file mode 100644 index 0000000..e9bb761 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_search.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_switch_checked.png b/app/src/main/res/mipmap-xxhdpi/ic_switch_checked.png new file mode 100644 index 0000000..89f4e57 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_switch_checked.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_switch_normal.png b/app/src/main/res/mipmap-xxhdpi/ic_switch_normal.png new file mode 100644 index 0000000..33fceaa Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_switch_normal.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_tip_success.png b/app/src/main/res/mipmap-xxhdpi/ic_tip_success.png new file mode 100644 index 0000000..be0f0b2 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_tip_success.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_tip_warn.png b/app/src/main/res/mipmap-xxhdpi/ic_tip_warn.png new file mode 100644 index 0000000..385d7e5 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_tip_warn.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..975a495 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,21 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + #DCDCF0 + #78788C + #FF3232 + #FF9632 + #0032C8 + #009632 + #DCDCF0 + #D9E3F9 + #141428 + #96A0AA + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..9940c0d --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + 入库客户端 + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..68db9bb --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +