This commit is contained in:
zxj
2025-07-08 15:40:39 +08:00
parent d2698ed60f
commit 4501b6d5fc
132 changed files with 8998 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
/build
+108
View File
@@ -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")
}
Binary file not shown.
+21
View File
@@ -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
@@ -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)
}
}
+45
View File
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-feature
android:name="android.hardware.camera"
android:required="true" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_LOGS" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<!-- android:networkSecurityConfig="@xml/network_security_config"-->
<application
android:name="com.sw.inbound.MyApp"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:usesCleartextTraffic="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Inbound"
tools:targetApi="31">
<activity
android:name="com.sw.inbound.MainActivity"
android:configChanges="orientation|screenSize"
android:exported="true"
android:label="@string/app_name"
android:screenOrientation="landscape"
android:theme="@style/Theme.Inbound">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -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<DictType> = arrayListOf()
var goodsTypeList: List<DictType> = arrayListOf()
var warehouseTypeList: List<DictType> = arrayListOf()
var supplierTypeList: List<DictType> = arrayListOf()
var unitTypeList: List<DictType> = 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"
}
@@ -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()
}
}
+22
View File
@@ -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)
}
}
@@ -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)
}
}
@@ -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)
)
}
}
)
}
)
@@ -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()
}
@@ -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
}
}
@@ -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)
@@ -0,0 +1,4 @@
package com.sw.inbound.model.exception
class ApiException {
}
@@ -0,0 +1,4 @@
package com.sw.inbound.model.exception
class NetException(val code: Int, override val message: String) : Exception(message)
@@ -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
}
}
@@ -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
@@ -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<UnitVo>? = 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<DictType>
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
}
}
@@ -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<GoodsInfo>
) : 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() {
}
@@ -0,0 +1,10 @@
package com.sw.inbound.model.response
data class ApiResponse<T>(
val code: Int,
val success: Boolean? = false,
val msg: String? = "",
val data: T? = null
) {
fun isSuccess(): Boolean = code == 200
}
@@ -0,0 +1,5 @@
package com.sw.inbound.model.response
open class BaseBean() {
}
@@ -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
@@ -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<UnitVo> = emptyList<UnitVo>()
) : Parcelable, BaseBean() {
val unitDictTypeList: List<DictType>
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
}
}
@@ -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<AllType?>? = 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<String?>? = 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
}
@@ -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<GoodsInfo> = emptyList<GoodsInfo>()
) : BaseBean() {
// 自用
var warehouseName: String = "选择仓库"
}
@@ -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<String?>? = listOf(),
@SerializedName("pages")
val pages: Int? = 0,
@SerializedName("records")
val records: List<Record>? = 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<UnitVo>? = 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
}
}
@@ -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<String?>? = 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
@@ -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
@@ -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<SupplierInfo?>? = listOf(),
@SerializedName("pages")
val pages: Int? = 0,
/**
* 供应商列表
*/
@SerializedName("records")
val records: List<SupplierInfo?>? = listOf(),
@SerializedName("searchCount")
val searchCount: Boolean? = false,
@SerializedName("size")
val size: Int? = 0,
@SerializedName("total")
val total: Int? = 0
) : Parcelable
@@ -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
@@ -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 }
}
@@ -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<User>
/**
* 采购单入库-列表
*/
@GET("/shuwei-zhct/pad/receivePage")
suspend fun getReceiveList(
@Query("pageNo") pageNo: Int,
@Query("pageSize") pageSize: Int
): ApiResponse<SupplierResponse>
/**
* 采购单入库-详情
*/
@GET("/shuwei-zhct/pad/receiveDetail")
suspend fun getReceiveDetail(@Query("id") id: Int): ApiResponse<PurchaseInfo>
/**
* 部分收货
*/
@POST("/shuwei-zhct/pad/partialReceiptGoods")
suspend fun partialReceipt(@Body uploadInfo: UploadInfo): ApiResponse<Boolean>
/**
* 确认收货
*/
@POST("/shuwei-zhct/pad/saveAndReceiveAndGoWare")
suspend fun confirmReceipt(@Body uploadInfo: UploadInfo): ApiResponse<Boolean>
/**
* 存储方式
*/
@GET("/shuwei-zhct/swsysdictitem/getDictItemByF")
suspend fun getGoodsStorageType(@Query("dictCode") dictCode: String = "goods_storage_type"): ApiResponse<List<StorageType>>
/**
* 物品类型
*/
@GET("/shuwei-zhct/swkcglgoodstype/notLimitList")
suspend fun getGoodsType(): ApiResponse<GoodsType>
/**
* 获取字典数据
*/
@GET("/shuwei-zhct/pad/dataList")
suspend fun getDictType(@Query("type") type: String): ApiResponse<List<DictType>>
/**
* 上传图片
*/
@Multipart
@POST("/shuwei-zhct/fileUpload/fileUpload/")
suspend fun uploadImage(
@Part("code") code: RequestBody,
@Part file: MultipartBody.Part
): ApiResponse<String>
/**
* 搜索物品列表
*/
@GET("/shuwei-zhct/pad/goodsInfoList")
suspend fun searchGoodsInfoList(
@Query("goodsName") goodsName: String,
@Query("pageNo") pageNo: Int,
@Query("pageSize") pageSize: Int
): ApiResponse<SearchGoodsInfo>
/**
* 自采添加商品
*/
@POST("/shuwei-zhct/pad/goodsAdd")
suspend fun selfPurchaseGoodsAdd(@Body goodsAddParam: GoodsAddParam): ApiResponse<SearchGoodsInfo>
/**
* 自采入库
*/
@POST("/shuwei-zhct/pad/SelfPurchasedGoods")
suspend fun selfPurchaseWarehousing(@Body list: List<PurchaseWarehouseParam>): ApiResponse<Boolean>
}
@@ -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 ?: "未知错误")
}
}
}
}
@@ -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"
}
}
@@ -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 <T> safeApiCall(apiCall: suspend () -> ApiResponse<T>): ApiResponse<T> {
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 ?: "无错误信息"}")
}
}
}
}
}
@@ -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<User> {
return safeApiCall { apiService.login(loginParam) }
}
suspend fun getReceiveList(pageNum: Int, pageSize: Int): ApiResponse<SupplierResponse> {
return safeApiCall {
apiService.getReceiveList(pageNum, pageSize)
}
}
suspend fun getReceiveDetail(id: Int): ApiResponse<PurchaseInfo> {
return safeApiCall {
apiService.getReceiveDetail(id)
}
}
suspend fun getGoodsStorageType(): ApiResponse<List<StorageType>> {
return safeApiCall {
apiService.getGoodsStorageType()
}
}
suspend fun getGoodsType(): ApiResponse<GoodsType> {
return safeApiCall {
apiService.getGoodsType()
}
}
enum class TypeEnum(val string: String) {
/**
* 仓库
*/
WAREHOUSE("warehouse"),
/**
* 供应商
*/
SUPPLIER("supplier"),
/**
* 单位
*/
UNIT("unit")
}
suspend fun getDictType(type: TypeEnum): ApiResponse<List<DictType>> {
return safeApiCall {
apiService.getDictType(type.string)
}
}
suspend fun uploadImage(uri: Uri): ApiResponse<String> {
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<Boolean> {
return safeApiCall {
apiService.partialReceipt(uploadInfo)
}
}
/**
* 全部收货
*/
suspend fun confirmReceipt(uploadInfo: UploadInfo): ApiResponse<Boolean> {
return safeApiCall {
apiService.confirmReceipt(uploadInfo)
}
}
suspend fun searchGoodsInfoList(
goodsName: String,
pageNo: Int,
pageSize: Int
): ApiResponse<SearchGoodsInfo> {
return safeApiCall {
apiService.searchGoodsInfoList(goodsName, pageNo, pageSize)
}
}
/**
* 自采购-添加商品
*/
suspend fun selfPurchaseGoodsAdd(goodsAddParam: GoodsAddParam): ApiResponse<SearchGoodsInfo> {
return safeApiCall {
apiService.selfPurchaseGoodsAdd(goodsAddParam)
}
}
/**
* 自采购-入库
*/
suspend fun selfPurchaseWarehousing(list: List<PurchaseWarehouseParam>): ApiResponse<Boolean> {
return safeApiCall {
apiService.selfPurchaseWarehousing(list)
}
}
}
@@ -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
}
}
@@ -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")
}
@@ -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<UserViewModel>(),
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 = "自采入库"
)
}
}
}
@@ -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<UserViewModel>()
) {
var username by remember { mutableStateOf<String>("padAdmin") }
var password by remember { mutableStateOf<String>("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 // 确保单行输入(避免换行导致高度变化)
)
}
@@ -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<ProductViewModel>()
) {
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)
)
}
}
}
}
@@ -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<ReceiptViewModel>()
) {
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<GoodsInfo>, 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<DictType> = 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 = "确定")
}
}
}
}
@@ -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<SelfProcurementViewModel>()
) {
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<String>("选择仓库") }
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<DictType> = 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<DictType>() // 界面中会重新获取
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!!)
})
}
}
}
}
@@ -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) {
}
@@ -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
)
}
@@ -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)
@@ -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,
)
}
@@ -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
)
*/
)
@@ -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
)
}
}
}
@@ -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<Uri?>(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 = "扫描"
)
}
}
@@ -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 = "扫描"
)
}
}
@@ -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("请求授权")
}
}
}
@@ -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
)
)
}
}
}
@@ -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<DictType>,
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
}
)
}
}
}
}
@@ -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)
}
)
},
)
}
}
@@ -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<DictType>,
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
}
)
}
}
}
}
@@ -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()
})
)
}
}
@@ -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 = "请稍等...")
}
}
}
}
@@ -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<SearchGoodsInfo.Record>,
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()
)
}
}
}
@@ -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<DictType>? = 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
)
}
}
}
@@ -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<GoodsInfo>,
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,
)
}
@@ -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<PurchaseWarehouseParam>,
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,
)
}
@@ -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<SearchGoodsInfo.Record>()
list.add(SearchGoodsInfo.Record(goodsName = "胶东大白菜胶东大白菜胶东大白菜胶东大白菜"))
for (i in 1..20) {
list.add(SearchGoodsInfo.Record(goodsName = "胶东大白菜$i"))
}
// val list = arrayListOf<String>(
// "胶东大白菜",
// "玉田尖白菜1",
// "玉田尖白菜2",
// "玉田尖白菜3",
// "玉田尖白菜4"
// )
// SingleSelectButtonGroup(list)
}
@Composable
fun SingleSelectButtonGroup(
options: List<SearchGoodsInfo.Record>,
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
)
}
}
}
}
@@ -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
)
)
}
}
}
@@ -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
)
}
}
}
}
}
@@ -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."
)
}
}
@@ -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<File> {
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()
}
}
}
}
@@ -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)
}
}
}
@@ -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
}
}
}
@@ -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 <T> fromJson(json: String?, clazz: Class<T>): 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 <T> 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 <T> fromJsonList(json: String?, clazz: Class<T>): List<T>? {
if (json.isNullOrEmpty()) {
return null
}
return try {
val type = TypeToken.getParameterized(List::class.java, clazz).type
defaultGson.fromJson<List<T>>(json, type)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
/**
* 将 JSON 字符串转换为 Map 对象
* @param json JSON 字符串
* @param keyClazz Map 的 key 类型
* @param valueClazz Map 的 value 类型
* @return 转换后的 Map 对象
*/
fun <K, V> fromJsonMap(
json: String?,
keyClazz: Class<K>,
valueClazz: Class<V>
): Map<K, V>? {
if (json.isNullOrEmpty()) {
return null
}
return try {
val type = TypeToken.getParameterized(Map::class.java, keyClazz, valueClazz).type
defaultGson.fromJson<Map<K, V>>(json, type)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
/**
* 将对象转换为另一种类型的对象
* @param obj 源对象
* @param clazz 目标类型
* @return 转换后的对象
*/
fun <T> convert(obj: Any?, clazz: Class<T>): T? {
if (obj == null) {
return null
}
return fromJson(toJson(obj), clazz)
}
}
@@ -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
}
}
@@ -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 <T> 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() }
}
}
@@ -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<String>) }
else -> throw IllegalArgumentException("Unsupported type: ${value.javaClass.name}")
}
notifyDataChanged(key)
}
@Suppress("UNCHECKED_CAST")
fun <T> 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<String>) 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<Any?> {
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<String> = emptySet()) = SpProperty(key, default)
inner class SpProperty<T>(private val key: String, private val defaultValue: T) :
ReadWriteProperty<Any?, T> {
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)
}
}
}
@@ -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
}
}
@@ -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 <T> switchToMain(block: suspend CoroutineScope.() -> T): T {
return withContext(Dispatchers.Main, block)
}
/**
* 切换到IO线程(协程环境)
*/
suspend fun <T> 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()
}
}
@@ -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
)
}
}
}
}
@@ -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<DictType>()
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<DictType>()
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<String> {
return arrayListOf(
"默认仓库",
"仓库1",
"仓库2",
"仓库3",
)
}
fun getPurchasingUnit(): ArrayList<String> {
return arrayListOf(
"",
"",
""
)
}
fun getProductList(): ArrayList<String> {
return arrayListOf<String>(
"胶东大白菜",
"玉田尖白菜1",
"玉田尖白菜2",
"玉田尖白菜3",
"玉田尖白菜4"
)
}
}
@@ -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<List<SupplierInfo?>>(emptyList())
val supplierList: StateFlow<List<SupplierInfo?>> = _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
}
}
}
}
}
@@ -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<Boolean>(true)
val adjustState: StateFlow<Boolean> = _adjustState
private val _showReceiptDialog = MutableStateFlow(false)
val showReceiptDialog: StateFlow<Boolean> = _showReceiptDialog
// 当前要收货的供应商
private val _currentPurchaseInfo = MutableStateFlow<PurchaseInfo?>(null)
val currentPurchaseInfo: StateFlow<PurchaseInfo?> = _currentPurchaseInfo
// 自动从 currentPurchaseInfo 派生 orders
// private val _orders: StateFlow<List<GoodsInfo>> = currentPurchaseInfo
// .map { purchaseInfo ->
// purchaseInfo?.receiveGoodsInfoList ?: emptyList()
// }
// .stateIn(
// viewModelScope,
// SharingStarted.WhileSubscribed(5000), // 或者使用 Lazily/Eagerly 根据需求
// emptyList()
// )
private val _orders = MutableStateFlow<List<GoodsInfo>>(emptyList())
val orders: StateFlow<List<GoodsInfo>> = _orders.asStateFlow()
fun initOrders() {
_orders.value = _currentPurchaseInfo.value?.receiveGoodsInfoList ?: emptyList()
}
// 已调整列表
val adjustedOrders: StateFlow<List<GoodsInfo>> = _orders
.map { orders -> orders.filter { it.isAdjusted && it.goodId != null } }
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
// 未调整列表
val unadjustedOrders: StateFlow<List<GoodsInfo>> = _orders
.map { orders -> orders.filter { !it.isAdjusted && it.goodId != null } }
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
val mergedOrders: StateFlow<List<GoodsInfo>> = _orders
//
private val _selectedItem = MutableStateFlow<GoodsInfo?>(null)
val selectedItem: StateFlow<GoodsInfo?> = _selectedItem
// 搜索物品列表
private val _searchListItems = MutableStateFlow<List<SearchGoodsInfo.Record>>(emptyList())
val searchListItems: StateFlow<List<SearchGoodsInfo.Record>> = _searchListItems
private val _receiptResult = MutableStateFlow<Boolean>(false)
val receiptResult: StateFlow<Boolean> = _receiptResult
private val _countUserInput = MutableStateFlow<Boolean>(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("收货失败")
}
}
}
}
@@ -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<Boolean>(false)
val addGoodsResult: StateFlow<Boolean> = _addGoodsResult
private val _addToWarehouseResult = MutableStateFlow<Boolean>(false)
val addToWarehouseResult: StateFlow<Boolean> = _addToWarehouseResult
private val _selectedItem = MutableStateFlow<PurchaseWarehouseParam?>(null)
val selectedItem: StateFlow<PurchaseWarehouseParam?> = _selectedItem
// 全局仓库信息
private val _globalWarehouse = MutableStateFlow<DictType>(DictType(-1, "选择仓库"))
val globalWarehouse: StateFlow<DictType> = _globalWarehouse
private val _purchaseList = MutableStateFlow<List<PurchaseWarehouseParam>>(emptyList())
val purchaseList: StateFlow<List<PurchaseWarehouseParam>> = _purchaseList
// 表单状态
private val _goodsAddParam = MutableStateFlow<GoodsAddParam>(GoodsAddParam())
val goodsAddParam: StateFlow<GoodsAddParam> = _goodsAddParam
// 快速添加弹窗状态
private val _showAddProductDialog = MutableStateFlow<Boolean>(false)
val showAddProductDialog: StateFlow<Boolean> = _showAddProductDialog
// 搜索物品列表
private val _searchListItems = MutableStateFlow<List<SearchGoodsInfo.Record>>(emptyList())
val searchListItems: StateFlow<List<SearchGoodsInfo.Record>> = _searchListItems
// 称重结果
private val _weightInfo = MutableStateFlow<Double?>(0.0)
val weightInfo: StateFlow<Double?> = _weightInfo
/**
* 数量是否是用户输入的值
*/
private val _countUserInput = MutableStateFlow<Boolean>(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<SearchGoodsInfo.Record>()
}
} else {
_searchListItems.value = emptyList<SearchGoodsInfo.Record>()
}
}
}
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
}
}
}
}
@@ -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<User?>(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)
}
}
@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Some files were not shown because too many files have changed in this diff Show More