init
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 982 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 307 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 2.5 KiB |