初始代码提交

This commit is contained in:
2026-01-14 10:36:27 +08:00
parent 600aa4dbb0
commit 196cacfe5a
268 changed files with 22545 additions and 2 deletions
+206 -2
View File
@@ -1,3 +1,207 @@
# Inbound20260114
# Inbound
入库2026新代码
入库客户端
## 服务接口
### 采购单入库-列表
```js
/shuwei-zhct/pad/receivePage
请求方式 get
入参
pageNo 页数 Integer
pageSize 条数 Integer
返回值
supplierId 供应商id Integer
supplierName 供应商 String
receiveCode 收货单号 String 废弃界面显示purCode
purCode 采购单号 String
goodCount 物品项 Integer
receiveDate 收货日期 Date
purchaseDate 采购日期 Date
receiveStatus 收货状态1收货关闭/2收货完成/3待收货 /4部分收货 Integer
receiveUser 操作人 String
```
### 采购单入库-详情
```
/shuwei-zhct/pad/receiveDetail
请求方式 get
入参 id (列表里返回的id)
返回值
id 单据id Integer
supplierId 供应商id Integer
supplierName 供应商名称
purCode 采购单号 String
receiveCode 收货单号 String
receiveGoodsInfoList 物品信息
{[
goodsPurId 物品采购单位关系(没有回显, 之后接口需要该参数)
goodId 物品Id Integer
goodName 物品名称 String
recUnitPriceTaxIn 单价 Float
receiveCount 数量 Float
unitName 采购单位名称 String
purchaseUnitId 采购单位id integer
recPriceExItem 金额 Float
warehouseId 仓库id integer
warehouseName 仓库名称 String
]}
```
### 存储方式
```
/shuwei-zhct/swsysdictitem/getDictItemByF?dictCode=goods_storage_type
入参:无
返回
itemText 名称
itemValue 对应值
```
### 物品类型
```
/shuwei-zhct/swkcglgoodstype/notLimitList
请求方式 get
入参:无
返回
id ,typeName 类型名称
```
### 字典类型
```
/shuwei-zhct/pad/dataList
请求方式 get
入参 type => warehouse 仓库; supplier 供应商; unit 单位
返回值 id, value
```
### 图片上传
```
https://vip.shuziweidao.com/shuwei-zhct/fileUpload/fileUpload
请求方式 post
入参
code 传 4
file 传图片文件
返回
data 里返回的是全路径串
```
### 物品列表
```
/shuwei-zhct/pad/goodsInfoList
请求方式 get
入参
goodsName 物品名称 String
pageNo 页数 Integer
pageSize 条数 Integer
返回
goodsId 物品id, integer
goodsName 物品名称 String
kcUnitId 库存单位id integer
kcUnitName 库存单位名称 String
unitVoList 采购单位列表 [
{
businessUnitId 单位关系id Integer
purchaseUnitName 采购单位名称 String
buyToInventoryValue 转换值 String
}
]
```
### 部分收货
```
/pad/partialReceiptGoods
请求方式 post
入参
{
id 收货单id Integer
supplierId 供应商id Integer
receiveGoodsInfoList 物品信息
[
{
goodsId 物品id Integer
goodsPurId Integer
warehouseId 仓库id Integer
recUnitPriceTaxIn 收货单价 Float
newRecUnitPriceTaxIn 采购单价 Float
receiveCount 收货数量 Float
recPriceInItem 收货金额 Float
receivedNum 实际收货数量 Float
}
]
}
```
### 确认收货
```
/shuwei-zhct/pad/saveAndReceiveAndGoWare
请求方式 post
入参
id 单据id
receiveGoodsInfos 物品列表
[
{
goodId 物品id integer
goodsPurId 物品采购单位关系 integer
receiveCount 采购数量(详情里返回的) float
recUnitPriceTaxIn 单价 float
recPriceExItem 金额 float
warehouseId 仓库id integer
receivedNum 收货数量 float
goodsWeight 物品重量 BigDecimal
}
]
返回 true/false
```
### 添加物品
```
/shuwei-zhct/pad/goodsAdd
请求方式 post
入参
String goodName 物品名称
Integer goodType 物品类型
Integer storageType 存储方式
Float netRate 净材率
String relativeUrl 图片url
BigDecimal purchasePrice 采购单价
Integer purchaseUnit 采购单位
Integer unitId 库存单位
Float purchaseValue 单位转换值
```
### 确认入库
```
/shuwei-zhct/pad/SelfPurchasedGoods
请求方式 post
入参
[
{
goodsId 物品id integer
kcUnitId 库存单位 integer
goodsCount 入库数量 Double
goodsUnitPrice 入库单价 Double
goodsPrice 入库金额 Double
goodPurId (物品列表里 businessUnitId字段的值) integer
warehouseId 仓库id integer
buyToInventoryValue 采购库存转换关系 String
}
]
```
### 通过重量计算数据
```
秤上的克重 / 消耗转换值/库存采购转换值
```
+1
View File
@@ -0,0 +1 @@
/build
+161
View File
@@ -0,0 +1,161 @@
import com.android.tools.r8.internal.fA
plugins {
// alias(libs.plugins.android.application)
// alias(libs.plugins.kotlin.android)
// alias(libs.plugins.kotlin.compose)
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("com.google.dagger.hilt.android")
id("com.google.devtools.ksp")
id("kotlin-parcelize")
// id("compose.compiler")
kotlin("kapt")
alias(libs.plugins.kotlin.compose)
}
android {
signingConfigs {
getByName("debug") {
storeFile = file("./swkey.jks")
storePassword = "123456"
keyAlias = "android"
keyPassword = "123456"
}
}
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"*/))
}
// 设置输出APK文件名格式
setProperty("archivesBaseName", "入库客户端_${versionName}")
}
buildTypes {
//debug {
// buildConfigField( "boolean", "DEBUG", "true")
//}
release {
//buildConfigField( "boolean", "DEBUG", "false")
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
viewBinding = true
buildConfig = true
}
// composeOptions {
// kotlinCompilerExtensionVersion = "1.5.4"
// }
sourceSets {
named("main") {
jniLibs.srcDirs("libs")
}
}
}
val isDebug = gradle.startParameter.taskNames.any {
it.contains("debug", ignoreCase = true)
}
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")
implementation(libs.core)
implementation(libs.android.core)
implementation(libs.material)
implementation("androidx.recyclerview:recyclerview:1.4.0")
implementation("androidx.compose.foundation:foundation:1.7.0")
implementation("androidx.compose.foundation:foundation-layout:1.7.0")
implementation (libs.pytorch.android)
implementation (libs.pytorch.android.torchvision)
implementation("io.github.scwang90:refresh-layout-kernel:3.0.0-alpha")
implementation("io.github.scwang90:refresh-header-classics:3.0.0-alpha")
val objectboxVersion = "5.0.1"
if (isDebug) {
debugImplementation("io.objectbox:objectbox-android-objectbrowser:$objectboxVersion")
} else {
releaseImplementation("io.objectbox:objectbox-android:$objectboxVersion")
}
implementation("org.greenrobot:eventbus:3.3.1")
}
apply(plugin = "io.objectbox")
Binary file not shown.
Binary file not shown.
+49
View File
@@ -0,0 +1,49 @@
{
"_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.",
"_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.",
"_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.",
"entities": [
{
"id": "1:5687655337808888744",
"lastPropertyId": "4:1848754684826631948",
"name": "Food",
"properties": [
{
"id": "1:5283273794898780585",
"name": "id",
"type": 6,
"flags": 1
},
{
"id": "2:6054189006589418860",
"name": "name",
"type": 9
},
{
"id": "3:4137038678535479448",
"name": "foodIdx",
"type": 5
},
{
"id": "4:1848754684826631948",
"name": "foodVector",
"indexId": "1:8444853059627692581",
"type": 28,
"flags": 8
}
],
"relations": []
}
],
"lastEntityId": "1:5687655337808888744",
"lastIndexId": "1:8444853059627692581",
"lastRelationId": "0:0",
"lastSequenceId": "0:0",
"modelVersion": 5,
"modelVersionParserMinimum": 5,
"retiredEntityUids": [],
"retiredIndexUids": [],
"retiredPropertyUids": [],
"retiredRelationUids": [],
"version": 1
}
+49
View File
@@ -0,0 +1,49 @@
{
"_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.",
"_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.",
"_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.",
"entities": [
{
"id": "1:5687655337808888744",
"lastPropertyId": "4:1848754684826631948",
"name": "Food",
"properties": [
{
"id": "1:5283273794898780585",
"name": "id",
"type": 6,
"flags": 1
},
{
"id": "2:6054189006589418860",
"name": "name",
"type": 9
},
{
"id": "3:4137038678535479448",
"name": "foodIdx",
"type": 5
},
{
"id": "4:1848754684826631948",
"name": "foodVector",
"indexId": "1:8444853059627692581",
"type": 28,
"flags": 8
}
],
"relations": []
}
],
"lastEntityId": "1:5687655337808888744",
"lastIndexId": "1:8444853059627692581",
"lastRelationId": "0:0",
"lastSequenceId": "0:0",
"modelVersion": 5,
"modelVersionParserMinimum": 5,
"retiredEntityUids": [],
"retiredIndexUids": [],
"retiredPropertyUids": [],
"retiredRelationUids": [],
"version": 1
}
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,22 @@
package com.sw.inbound
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.sw.inbound", appContext.packageName)
}
}
+76
View File
@@ -0,0 +1,76 @@
<?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" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:name=".MyApp"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_logo512"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_logo512"
android:supportsRtl="true"
android:theme="@style/Theme.Inbound"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<!-- 注册BootReceiver,监听开机完成广播 -->
<receiver
android:name=".receiver.BootReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<activity
android:name=".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>
<activity android:name="com.sw.inbound.activity.ReceiptActivity"
android:screenOrientation="landscape"
android:windowSoftInputMode="adjustPan" />
<activity android:name="com.sw.inbound.activity.SelfProcurementActivity"
android:windowSoftInputMode="adjustPan" />
<activity android:name="com.sw.inbound.activity.FoodCollectionActivity"
android:windowSoftInputMode="adjustPan" />
<activity android:name="com.sw.inbound.activity.LocalImagePreviewActivity" />
<!-- <activity-->
<!-- android:name=".InitActivity"-->
<!-- 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>
Binary file not shown.
+113
View File
@@ -0,0 +1,113 @@
{
"class_names": [
"北豆腐WP2025111900007",
"圆白菜[卷心菜,包菜]WP2025111000063",
"基围虾WP2025110700019",
"多宝鱼WP2025112600011",
"大白菜WP2025111000064",
"大葱WP2025111000060",
"姜[黄姜]WP2025112000071",
"娃娃菜WP2025120300018",
"广东菜心WP2025120100014",
"松茸菌WP2025120100016",
"洋葱(鲜)[葱头]WP2025111000061",
"牛腩WP2025110700043",
"猪腿肉WP2025110700040",
"玉米WP2025120100017",
"番茄WP2025110700016",
"红提子WP2025111900006",
"羊排WP2025120300021",
"胡萝卜WP2025110600008",
"芹菜WP2025111000065",
"茄子(紫皮,长)WP2025110700024",
"茼蒿WP2025111900005",
"莴笋(鲜)[莴苣]WP2025111000056",
"蒜WP2025111400003",
"西兰花[绿菜花]WP2025110700029",
"豆腐皮WP2025111000059",
"辣椒(红,小)WP2025111000066",
"辣椒(青辣椒,螺丝椒)WP2025110700017",
"金针菇WP2025111900010",
"马铃薯[土豆、洋芋]WP2025110600006",
"鸡腿肉WP2025110700048",
"鸡蛋(代表值)WP2025112000070",
"黄瓜(鲜)[胡瓜]WP2025110700031",
"黑袋子",
"黑鱼(整条鱼)WP2025110600012"
],
"class_to_idx": {
"北豆腐WP2025111900007": 0,
"圆白菜[卷心菜,包菜]WP2025111000063": 1,
"基围虾WP2025110700019": 2,
"多宝鱼WP2025112600011": 3,
"大白菜WP2025111000064": 4,
"大葱WP2025111000060": 5,
"姜[黄姜]WP2025112000071": 6,
"娃娃菜WP2025120300018": 7,
"广东菜心WP2025120100014": 8,
"松茸菌WP2025120100016": 9,
"洋葱(鲜)[葱头]WP2025111000061": 10,
"牛腩WP2025110700043": 11,
"猪腿肉WP2025110700040": 12,
"玉米WP2025120100017": 13,
"番茄WP2025110700016": 14,
"红提子WP2025111900006": 15,
"羊排WP2025120300021": 16,
"胡萝卜WP2025110600008": 17,
"芹菜WP2025111000065": 18,
"茄子(紫皮,长)WP2025110700024": 19,
"茼蒿WP2025111900005": 20,
"莴笋(鲜)[莴苣]WP2025111000056": 21,
"蒜WP2025111400003": 22,
"西兰花[绿菜花]WP2025110700029": 23,
"豆腐皮WP2025111000059": 24,
"辣椒(红,小)WP2025111000066": 25,
"辣椒(青辣椒,螺丝椒)WP2025110700017": 26,
"金针菇WP2025111900010": 27,
"马铃薯[土豆、洋芋]WP2025110600006": 28,
"鸡腿肉WP2025110700048": 29,
"鸡蛋(代表值)WP2025112000070": 30,
"黄瓜(鲜)[胡瓜]WP2025110700031": 31,
"黑袋子": 32,
"黑鱼(整条鱼)WP2025110600012": 33
},
"idx_to_class": {
"0": "北豆腐WP2025111900007",
"1": "圆白菜[卷心菜,包菜]WP2025111000063",
"2": "基围虾WP2025110700019",
"3": "多宝鱼WP2025112600011",
"4": "大白菜WP2025111000064",
"5": "大葱WP2025111000060",
"6": "姜[黄姜]WP2025112000071",
"7": "娃娃菜WP2025120300018",
"8": "广东菜心WP2025120100014",
"9": "松茸菌WP2025120100016",
"10": "洋葱(鲜)[葱头]WP2025111000061",
"11": "牛腩WP2025110700043",
"12": "猪腿肉WP2025110700040",
"13": "玉米WP2025120100017",
"14": "番茄WP2025110700016",
"15": "红提子WP2025111900006",
"16": "羊排WP2025120300021",
"17": "胡萝卜WP2025110600008",
"18": "芹菜WP2025111000065",
"19": "茄子(紫皮,长)WP2025110700024",
"20": "茼蒿WP2025111900005",
"21": "莴笋(鲜)[莴苣]WP2025111000056",
"22": "蒜WP2025111400003",
"23": "西兰花[绿菜花]WP2025110700029",
"24": "豆腐皮WP2025111000059",
"25": "辣椒(红,小)WP2025111000066",
"26": "辣椒(青辣椒,螺丝椒)WP2025110700017",
"27": "金针菇WP2025111900010",
"28": "马铃薯[土豆、洋芋]WP2025110600006",
"29": "鸡腿肉WP2025110700048",
"30": "鸡蛋(代表值)WP2025112000070",
"31": "黄瓜(鲜)[胡瓜]WP2025110700031",
"32": "黑袋子",
"33": "黑鱼(整条鱼)WP2025110600012"
},
"embedding_dim": 512,
"total_images": 2065,
"build_time": "2025-12-05T10:16:42.915631"
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,69 @@
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()
/**
* 具体业务baseurl
*/
//var appBaseUrl: String = "http://192.168.1.201:14801"
var appBaseUrl: String = "https://dev.yixiong-tech.com:8081"
/**
* 设备id
*/
var deviceId: String = ""
var restId: String = ""
var appVersion: String = "1"
var appId = "Hkz1rBk6PZXbS8KwKr67K2eZtsz8bRoMHLg64bUdgCZj"
var sdkKey = "AabXs3sHM8UhhE7oCGf4LVMLrdkGb1nJNksbjjTdVt7k"
var appToken = ""
}
/**
*
*/
object GlobalKey {
const val KEY_EQUIPMENT_INFO = "equipmentInfo"
const val KEY_TOKEN = "tokenKey"
const val KEY_USER_INFO = "userInfoKey"
const val KEY_USER_NAME = "userNameKey"
}
@@ -0,0 +1,183 @@
package com.sw.inbound
import android.content.Intent
import android.os.Bundle
import android.view.WindowManager
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandIn
import androidx.compose.animation.shrinkOut
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsBottomHeight
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
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.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
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 androidx.hilt.navigation.compose.hiltViewModel
import com.sw.inbound.utils.AppUtil
import com.sw.inbound.utils.QRCodeUtil
import com.sw.inbound.viewmodel.DeviceViewModel
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.StateFlow
/**
* 设备初始化界面
*/
@AndroidEntryPoint
class InitActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
window.setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
)
setContent {
InitScreen()
}
}
private var scale = 0.8F
private val qrCodeBmp by lazy {
QRCodeUtil.generateQRCode(
content = GlobalData.deviceId,
size = (scale * 150).toInt()
)
}
@Preview(widthDp = 1920, heightDp = 1080)
@Composable
fun InitScreen(
viewModel: DeviceViewModel = hiltViewModel<DeviceViewModel>()
) {
// val context = LocalContext.current
val hasDeviceData = viewModel.checkEquipmentInfo()
val deviceInfoResult = viewModel.deviceInfoResult.collectAsState()
if (hasDeviceData) {
LaunchedEffect(this) {
delay(1500)
goLoginActivity()
}
} else {
FirstPage(viewModel)
}
if (deviceInfoResult.value == true) {
goLoginActivity()
}
}
@Composable
fun FirstPage(viewModel: DeviceViewModel) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.size(
1920.dp, 1080.dp
)
) {
Spacer(
modifier = Modifier
.padding(top = (scale * 112).dp)
.background(Color.White.copy(alpha = 0.0F))
.size((scale * 337).dp, (scale * 322).dp)
)
Button(
onClick = {
viewModel.getDeviceToken()
},
colors = ButtonDefaults.buttonColors(
contentColor = Color.Transparent,
containerColor = Color(0xFF08C6DC)
),
shape = RoundedCornerShape((scale * 8).dp),
modifier = Modifier
.width((scale * 225).dp)
.padding(top = (scale * 75).dp)
.padding((scale * 10).dp)
) {
Text(
text = "设备初始化",
color = Color.White,
fontSize = (scale * 22).sp,
fontWeight = FontWeight.Bold,
)
}
Spacer(
modifier = Modifier
.wrapContentWidth()
.weight(1F)
)
Image(
bitmap = qrCodeBmp!!.asImageBitmap(),
contentDescription = "",
modifier = Modifier
.padding(bottom = 10.dp)
.size((scale * 150).dp, (scale * 150).dp)
)
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars))
}
}
// AnimatedVisibility(
// visible = isSuccess.not(),
// enter = expandIn(clip = false),
// exit = shrinkOut(clip = false)
// ) {}
// fun initialize() {
// var deviceId = AppUtil.getUDID(this)
// deviceId = "6ce7cd59-b875-38b2-b73d-88a570be3212"
// GlobalData.deviceId = deviceId
// val isSuccess = deviceViewModel.checkEquipmentInfo()
// if (isSuccess) {
// goLoginActivity()
// return
// }
// binding.ivQrCode.setImageBitmap(
// QRCodeUtil.generateQRCode(
// content = GlobalData.deviceId,
// size = 200
// )
// )
// binding.btnInit.setOnClickListener {
// deviceViewModel.getDeviceToken()
// }
// }
private fun goLoginActivity() {
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
finish()
}
}
@@ -0,0 +1,49 @@
package com.sw.inbound
import android.os.Bundle
import android.view.WindowManager
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import com.sw.inbound.objbox.FoodModule
import com.sw.inbound.sdk.SensorScaleUtils
import com.sw.inbound.ui.AppScreen
import com.sw.inbound.utils.ContextUtils
import com.sw.inbound.utils.SPUtil
import com.sw.inbound.utils.ThreadUtils
import com.sw.inbound.viewmodel.ReceiptViewModel
import dagger.hilt.android.AndroidEntryPoint
import kotlin.getValue
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
val viewModel: ReceiptViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
window.setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
)
setContent {
AppScreen(onBackRequest = {
finish()
})
}
SensorScaleUtils.startScale(autoScale = true)
FoodModule.init(this)
}
override fun onDestroy() {
SensorScaleUtils.closeScale()
ThreadUtils.release()
super.onDestroy()
}
}
+35
View File
@@ -0,0 +1,35 @@
package com.sw.inbound
import android.app.Application
import com.sw.inbound.objbox.ObjectBox
import com.sw.inbound.utils.AppUtil
import com.sw.inbound.utils.ContextUtils
import com.sw.inbound.utils.CrashHandler
import com.sw.inbound.utils.SPUtil
import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber
@HiltAndroidApp
class MyApp : Application() {
companion object {
const val DEBUG: Boolean = true
var instance: MyApp?=null
}
override fun onCreate() {
super.onCreate()
instance = this
Timber.plant(Timber.DebugTree())
ContextUtils.initAppContext(this)
GlobalData.appVersion = AppUtil.getAppVersionCode(this).toString()
var deviceId = AppUtil.getUDID(this)
///deviceId = "6ce7cd59-b875-38b2-b73d-88a570be3212"
GlobalData.deviceId = deviceId
val spUtil = SPUtil.getInstance(context = ContextUtils.getAppContext())
GlobalData.appToken = spUtil.get(GlobalKey.KEY_TOKEN, "") as String
ObjectBox.init(this)
// 初始化崩溃处理器
CrashHandler.init(this)
}
}
@@ -0,0 +1,693 @@
package com.sw.inbound.activity
import android.annotation.SuppressLint
import android.app.Dialog
import android.graphics.Bitmap
import android.net.Uri
import android.os.Bundle
import android.text.TextUtils
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.activity.ComponentActivity
import androidx.activity.viewModels
import androidx.camera.view.PreviewView
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import com.sw.inbound.R
import com.sw.inbound.adapter.FoodCollectionAdapter
import com.sw.inbound.adapter.GoodsSearchAdapter
import com.sw.inbound.databinding.ActivityFoodCollectionBinding
import com.sw.inbound.databinding.LayoutCameraPreviewBinding
import com.sw.inbound.databinding.LayoutEmptySearchBinding
import com.sw.inbound.dialog.CollectSearchDialog
import com.sw.inbound.dialog.CustomDialog
import com.sw.inbound.dialog.Loading
import com.sw.inbound.model.response.SearchGoodsInfo
import com.sw.inbound.objbox.Food
import com.sw.inbound.objbox.FoodCollectionBean
import com.sw.inbound.objbox.FoodModule
import com.sw.inbound.objbox.ObjectBox
import com.sw.inbound.utils.BitmapCropper
import com.sw.inbound.utils.BitmapSaver
import com.sw.inbound.utils.CameraUtils
import com.sw.inbound.utils.ImageUploader
import com.sw.inbound.utils.ImageUtil
import com.sw.inbound.utils.ext.addOnActionSearchListener
import com.sw.inbound.utils.ext.clickWithDebounce
import com.sw.inbound.utils.ext.dp
import com.sw.inbound.utils.ext.gone
import com.sw.inbound.utils.ext.hideKeyboard
import com.sw.inbound.utils.ext.invisible
import com.sw.inbound.utils.ext.startActivity
import com.sw.inbound.utils.ext.toObject
import com.sw.inbound.utils.ext.toast
import com.sw.inbound.utils.ext.visible
import com.sw.inbound.viewmodel.ReceiptViewModel
import dagger.hilt.android.AndroidEntryPoint
import io.objectbox.Box
import io.objectbox.kotlin.boxFor
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import timber.log.Timber
import java.io.File
@SuppressLint("NotifyDataSetChanged")
@AndroidEntryPoint
class FoodCollectionActivity : ComponentActivity() {
companion object {
const val TAG = "FoodCollectionActivity"
const val MAX_COUNT = 102
val PAGE_SIZE = 30
}
private var box: Box<Food>? = null
private val collectList: MutableList<FoodCollectionBean> = mutableListOf<FoodCollectionBean>()
private lateinit var binding: ActivityFoodCollectionBinding
private lateinit var previewView: PreviewView
val viewModel: ReceiptViewModel by viewModels()
private val cameraUtils: CameraUtils by lazy {
CameraUtils(this)
}
private val collectionAdapter: FoodCollectionAdapter by lazy {
FoodCollectionAdapter(collectList).apply {
addOnItemChildClickListener(R.id.ivDelete) { _, _, position ->
collectList[position].let {
it.imageFile = null
it.imageVector = null
it.bitmap = null
it.isFinish = false
it.isShowCamera = true
it.uploadSuccess = false
}
collectionAdapter.notifyItemChanged(position)
// val count = collectList.count { it.imageFile != null && it.uploadSuccess.not() }
// binding.btnUploadImage.text = "待上传图片${count}张"
}
}
}
override fun onResume() {
super.onResume()
cameraUtils.bind()
binding.llCameraFlag.run {
visible()
postDelayed({
gone()
}, 3000)
}
}
override fun onPause() {
super.onPause()
cameraUtils.unbind()
binding.llCameraFlag.visible()
}
@SuppressLint("NotifyDataSetChanged")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityFoodCollectionBinding.inflate(layoutInflater)
setContentView(binding.root)
cameraUtils.initCamera()
binding.root.setOnClickListener {
it.hideKeyboard()
}
val previewBinding =
LayoutCameraPreviewBinding.inflate(layoutInflater, binding.flCameraPreview)
previewView = previewBinding.previewView.also {
it.updateLayoutParams {
width = 420.dp
height = 420.dp
}
}
cameraUtils.setPreviewController(previewView)
binding.btnBack.setOnClickListener { finish() }
binding.rvFoodCollection.let {
it.layoutManager = GridLayoutManager(this, 3, GridLayoutManager.VERTICAL, false)
it.adapter = collectionAdapter
}
repeat(MAX_COUNT) {
collectList.add(FoodCollectionBean(isShowCamera = true))
}
collectionAdapter.notifyDataSetChanged()
binding.ivGoodsSearch.setOnClickListener { searchGoods() }
binding.etGoodsInput.let { v ->
v.addOnActionSearchListener {
searchGoods()
}
}
// binding.btnUploadImage.setOnClickListener {
// val count = collectList.count { it.imageFile!=null && it.uploadSuccess.not() }
// if (count == 0) {
// toast("已全部上传成功")
// return@setOnClickListener
// }
// uploadAllImage()
// }
binding.btnSave.setOnClickListener { _ ->
if (clickIndex == -1) {
toast("请选择物品名称")
return@setOnClickListener
}
val count = collectList.count { it.imageVector != null }
if (count == 0) {
toast("请拍摄物品照片")
return@setOnClickListener
}
// vectorThread()
// uploadAllImage()
upload()
}
binding.btnTakePhoto.clickWithDebounce {
takePhoto()
}
binding.btnCollectedGoods.setOnClickListener {
CollectSearchDialog(this@FoodCollectionActivity).show()
}
binding.btnClearData.setOnClickListener { clearData() }
binding.rvSearch.let {
it.layoutManager = GridLayoutManager(this, 2, LinearLayoutManager.VERTICAL, false)
it.adapter = searchAdapter
}
binding.refreshLayout.let {
it.setEnableRefresh(true)
it.setEnableLoadMore(false)
it.setOnRefreshListener {
pageNo = 1
searchGoods()
}
it.setOnLoadMoreListener {
searchGoods()
}
}
loadEmptyView()
binding.tvTitle.setOnClickListener {
// startActivity<LocalImagePreviewActivity> { }
// val goodsImage = GoodsImage(goodsId = "1993493094888140802", goodsName = "多宝鱼", startTime = 1764120531856L, endTime = 1764121842825L)
// uploadImage(goodsImage)
// val goodsImage = GoodsImage(goodsId = "1986366073649287170", goodsName = "黑鱼(整条鱼)", startTime = 1764122703093L, endTime = 1764123156022L)
// uploadImage(goodsImage)
// val goodsImage = GoodsImage(goodsId = "1986699063868801025", goodsName = "鸡腿肉", startTime = 1764123375841L, endTime = 1764124039162L)
// uploadImage(goodsImage)
}
}
private var pageNo = 1
private var clickIndex = -1
private val searchGoodsList: MutableList<SearchGoodsInfo.Record> = mutableListOf()
private val searchAdapter by lazy {
GoodsSearchAdapter(searchGoodsList).apply {
isStateViewEnable = true
setOnItemClickListener { adapter, view, position ->
clickIndex = position
if (list[position].isSelected) {
return@setOnItemClickListener
}
list.forEach { item ->
item.isSelected = false
}
list[position].isSelected = true
notifyDataSetChanged()
}
}
}
private fun finishRefresh() {
binding.refreshLayout.let {
if (pageNo == 1) {
it.finishRefresh(500)
} else {
it.finishLoadMore(500)
}
}
}
@SuppressLint("NotifyDataSetChanged")
private fun takePhoto() {
val count = collectList.count { it.imageFile != null }
if (count == MAX_COUNT) {
toast("每次只允许保存${MAX_COUNT}条数据")
return
}
cameraUtils.takePhoto(cameraCallback)
}
@SuppressLint("NotifyDataSetChanged")
private fun clearData() {
collectList.forEach {
try {
if (it.bitmap?.isRecycled?.not() == true) {
it.bitmap?.recycle()
}
it.bitmap = null
it.imageVector = null
it.imageFile = null
it.isShowCamera = true
it.isFinish = false
it.uploadSuccess = false
} catch (e: Exception) {
e.printStackTrace()
}
}
collectionAdapter.notifyDataSetChanged()
clickIndex = -1
binding.etGoodsInput.setText("")
searchGoodsList.clear()
searchAdapter.notifyDataSetChanged()
loadEmptyView()
binding.ivFinish.invisible()
// binding.btnUploadImage.text = "待上传图片${0}张"
}
private val cameraCallback: (Uri) -> Unit = { uri ->
val index = collectList.indexOfFirst { it.imageFile == null }
if (index == -1) {
toast("每次只允许保存${MAX_COUNT}条数据")
rerurn@ cameraCallback
}
// toast("index=$index")
try {
ImageUtil.uriToBitmap(this, uri)?.let { bitmap ->
val cropBitmap = BitmapCropper.cropCenter(
original = bitmap,
targetWidth = 1000, targetHeight = 1300,
offsetX = 30, offsetY = 100
)
initBox()
val imageVector = try {
FoodModule.bitmap2FloatArray(cropBitmap, false)
} catch (e: Exception) {
e.printStackTrace()
toast("操作失败")
return@let
}
val foodList = FoodModule.queryFoodNameScore(imageVector)
val filterList =
foodList.filter { it.score < FoodModule.BAG_RATE && it.name.contains("黑袋子") }
if (filterList.isNotEmpty()) {
toast("当前物品可能被袋子遮挡,请检查后重试")
return@let
}
val file = BitmapSaver.saveToAppFilesDir(
cropBitmap, this, "IMG_CROP_${System.currentTimeMillis()}.jpg"
)
Timber.d("${this.javaClass.simpleName}-searchFood-裁剪bitmap保存文件路径:${file?.absolutePath}")
collectList[index].let {
it.imageVector = imageVector
it.bitmap = null
it.imageFile = file
it.isShowCamera = false
it.isFinish = false
it.uploadSuccess = false
}
collectionAdapter.notifyItemChanged(index)
// val count = collectList.count { it.imageFile != null && it.uploadSuccess.not() }
// binding.btnUploadImage.text = "待上传图片${count}张"
if (cropBitmap.isRecycled.not()) {
cropBitmap.recycle()
}
if (bitmap.isRecycled.not()) {
bitmap.recycle()
}
}
} catch (e: Exception) {
e.printStackTrace()
toast("程序异常${e.message}")
}
}
private fun vectorThread() {
Loading.show(this)
Thread {
collectList
//.filter { it.bitmap != null }
// .filter { it.imageVector != null }
.forEachIndexed { index, it ->
//image2VectorTask(it.bitmap!!, index)
it.imageVector?.let{ imageVector ->
image2VectorTask(imageVector = imageVector, position = index)
}
}
runOnUiThread {
window.decorView.postDelayed({
Loading.dismiss()
toast("保存成功")
}, 1000)
}
}.start()
}
private fun image2VectorTask(
bitmap: Bitmap? = null,
imageVector: FloatArray? = null,
position: Int
) {
try {
initBox()
// val imageVector = try {
// FoodModule.bitmap2FloatArray(bitmap)
// } catch (e: Exception) {
// e.printStackTrace()
// //toast("保存失败")
// return
// }
val goods = searchGoodsList[clickIndex]
val saveName = goods.goodsName + goods.goodsCode
ObjectBox.boxStore.runInTx {
box?.put(Food(name = saveName, foodIdx = 0, foodVector = imageVector))
}
collectList[position].let {
// it.imageVector = imageVector
it.isFinish = true
}
runOnUiThread {
collectionAdapter.notifyItemChanged(position)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
@SuppressLint("NotifyDataSetChanged")
private fun searchGoods() {
val searchName = binding.etGoodsInput.text.toString().trim()
if (searchName.isBlank()) {
toast("请输入物品名称")
finishRefresh()
return
}
searchGoods(
searchName = searchName,
pageNo = pageNo
) { records ->
finishRefresh()
binding.etGoodsInput.hideKeyboard()
if (pageNo == 1 && records.isEmpty()) {
searchGoodsList.clear()
searchAdapter.notifyDataSetChanged()
loadEmptyView()
return@searchGoods
}
if (pageNo == 1) {
searchGoodsList.clear()
}
searchGoodsList.addAll(records)
val enableLoadMore = records.size >= PAGE_SIZE
binding.refreshLayout.setEnableLoadMore(enableLoadMore)
if (enableLoadMore) {
pageNo++
}
binding.rvSearch.layoutManager =
GridLayoutManager(this, 2, GridLayoutManager.VERTICAL, false)
searchAdapter.notifyDataSetChanged()
}
}
private var emptyBinding: LayoutEmptySearchBinding? = null
private fun loadEmptyView() {
binding.rvSearch.layoutManager =
LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
if (emptyBinding == null) {
emptyBinding = LayoutEmptySearchBinding.inflate(layoutInflater, binding.rvSearch, false)
}
emptyBinding?.tvContent?.text = "请检查物品名称,或稍后重新搜索"
emptyBinding?.root?.let { layout ->
layout.setOnClickListener { layout.hideKeyboard() }
searchAdapter.stateView = layout
}
}
fun searchGoods(
searchName: String,
pageNo: Int,
pageSize: Int = PAGE_SIZE,
callback: (List<SearchGoodsInfo.Record>) -> Unit
) {
runBlocking {
val records = viewModel.searchGoodsInfoList2(
goodsName = searchName,
pageNo = pageNo,
pageSize = pageSize
)
runOnUiThread {
callback(records)
}
}
}
private fun initBox() {
if (box == null) {
box = ObjectBox.boxStore.boxFor(Food::class)
}
}
// private fun uploadAllImage() {
// Loading.show(this)
// val files = collectList.filter { it.imageFile!=null }.map { it.imageFile!! }
// if (files.isEmpty()) {
// return
// }
// val goods = searchGoodsList[clickIndex]
// for (index in collectList.indices step 5) {
// val end = if(index + 5 < collectList.size - 1) index + 5 else collectList.size - 1
// val subList = collectList.subList(index, end)
// val subFiles = subList.map { it.imageFile }
// viewModel.uploadMultipleImages(goods.id!!, goods.goodsName!!, subFiles) {isSuccess->
// Timber.tag(TAG).d("uploadMultipleImages: ${isSuccess}")
// subList.filter { it.imageFile!=null }.forEach { it.uploadSuccess = isSuccess }
//
// val count = collectList.count { it.imageFile!=null && it.uploadSuccess.not() }
// runOnUiThread {
// binding.btnUploadImage.text = "待上传图片${count}张"
// if (count == 0) {
// Loading.dismiss()
// }
// }
// }
// }
// }
private fun upload() {
lifecycleScope.launch {
val totalFileCount = collectList.count { it.imageFile != null }
showWaitingDialog2("图片上传中0/$totalFileCount")
val goods = searchGoodsList[clickIndex]
val params = HashMap<String, RequestBody>()
params["goodsId"] = goods.id!!.toRequestBody()
params["goodsName"] = goods.goodsName!!.toRequestBody()
//params["foodVector"] = foodVector.toRequestBody()
ImageUploader(
totalList = collectList,
uploadImage = { batch ->
val files = batch.map { it.imageFile }
viewModel.uploadMultipleImages(files = files, params)
},
onProgress = { count, batch ->
runOnUiThread {
showWaitingDialog2("图片上传中$count/$totalFileCount")
batch.forEach {
it.uploadSuccess = true
}
}
},
onError = {
runOnUiThread {
hideWaitingDialog()
toast("上传失败,请稍后重试")
}
},
onComplete = {
hideWaitingDialog()
vectorThread()
}
).processUploads()
}
}
private val receiptViewModel: ReceiptViewModel by viewModels()
fun searchGoodsInfoList(goodsName: String, block: (List<SearchGoodsInfo.Record>) -> Unit) {
runBlocking {
val list = searchGoodsInfoList2(goodsName)
block(list)
}
}
suspend fun searchGoodsInfoList2(
goodsName: String,
pageNo: Int = 1,
pageSize: Int = 10
): List<SearchGoodsInfo.Record> {
return receiptViewModel.searchGoodsInfoList2(goodsName, pageNo, pageSize)
}
private var mDialogWaiting: CustomDialog? = null
/**
* 显示等待提示框
*/
fun showWaitingDialog(tip: String?): Dialog? {
hideWaitingDialog()
val view = View.inflate(this, R.layout.dialog_waiting, null)
if (!TextUtils.isEmpty(tip)) (view.findViewById<View?>(R.id.tvTip) as TextView).text = tip
mDialogWaiting = CustomDialog(this, view, R.style.MyDialog)
mDialogWaiting!!.show()
mDialogWaiting!!.setCancelable(true)
return mDialogWaiting
}
/**
* 隐藏等待提示框
*/
fun hideWaitingDialog() {
mDialogWaiting?.dismiss()
mDialogWaiting = null
}
fun showWaitingDialog2(tip: String?) {
if (mDialogWaiting == null) {
hideWaitingDialog()
val view = View.inflate(this, R.layout.dialog_waiting, null)
mDialogWaiting = CustomDialog(this, view, R.style.MyDialog)
mDialogWaiting!!.show()
mDialogWaiting!!.setCancelable(true)
}
val contentView = mDialogWaiting?.findViewById<ViewGroup>(android.R.id.content)
val tvTip = contentView?.findViewById<TextView>(R.id.tvTip)
tvTip?.text = tip
}
data class RespData(
var code: String? = null,
var data: String? = null,
var msg: String? = null,
)
data class GoodsImage(
var goodsId: String,
var goodsName: String,
var startTime: Long,
var endTime: Long
)
//
// val goodsImageList = mutableListOf<GoodsImage>().apply {
// add(
// GoodsImage(
// goodsId = "1986353555971297282",
// goodsName = "马铃薯[土豆、洋芋]",
// startTime = 1763692477786L,
// endTime = 1763693153825L
// )
// )
// add(GoodsImage(
// goodsId = "1986607476769869825",
// goodsName = "辣椒(青辣椒,螺丝椒)",
// startTime = 1763693182929L,
// endTime = 1763693621664L
// ))
// add(GoodsImage(
// goodsId = "1987880591453859842",
// goodsName = "辣椒(红,小)",
// startTime = 1763693683837L,
// endTime = 1763694154521L
// ))
// add(GoodsImage(
// goodsId = "1986699063868801025",
// goodsName = "鸡腿肉",
// startTime = 1763694221267L,
// endTime = 1763695052924L
// ))
//
// add(GoodsImage(
// goodsId = "1986689570300809217",
// goodsName = "猪腿肉",
// startTime = 1763695212428L,
// endTime = 1763695810578L
// ))
// add(GoodsImage(
// goodsId = "1986673470079029249",
// goodsName = "西兰花[绿菜花]",
// startTime = 1763695924575L,
// endTime = 1763696252669L
// ))
// add(GoodsImage(
// goodsId = "1991473021965070338",
// goodsName = "姜[黄姜]",
// startTime = 1763696258001L,
// endTime = 1763696294831L
// ))
// add(GoodsImage(
// goodsId = "00000",
// goodsName = "其它",
// startTime = 1763696442577L,
// endTime = 1763696990541L
// ))
// }
// private fun uploadImage(goodsImage: GoodsImage) {
// Loading.show(this)
// val cropFile = File(this.cacheDir, "crop")
// val list = mutableListOf<File>()
// val startTime = goodsImage.startTime
// val endTime = goodsImage.endTime
// cropFile.listFiles()?.forEach {
// if (it.isDirectory) {
// return@forEach
// }
// val start = "IMG_CROP_".length
// val end = it.name.indexOf(".jpg")
// val time = it.name.substring(start, end).toLong()
// if (time in startTime..endTime) {
// list.add(it)
// }
// if (list.size < 5) {
// return@forEach
// }
// viewModel.uploadMultipleImages(
// goodsId = goodsImage.goodsId,
// goodsName = goodsImage.goodsName,
// files = list
// ) { isSuccess ->
//// val resp = data?.toObject<RespData>()
//// val isSuccess = resp?.code == "00000"
// binding.ivFinish.run {
// if (isSuccess) visible() else invisible()
// }
// }
// list.clear()
// }
// if (list.isNotEmpty()) {
// viewModel.uploadMultipleImages(
// goodsId = goodsImage.goodsId,
// goodsName = goodsImage.goodsName,
// files = list
// ) { isSuccess ->
//// val resp = data?.toObject<RespData>()
//// val isSuccess = resp?.code == "00000"
// binding.ivFinish.run {
// if (isSuccess) visible() else invisible()
// }
// }
// }
// Loading.dismiss()
// }
}
@@ -0,0 +1,644 @@
package com.sw.inbound.activity
import android.net.Uri
import android.os.Bundle
import android.text.SpannableStringBuilder
import android.text.style.ForegroundColorSpan
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedCallback
import androidx.activity.viewModels
import androidx.camera.view.PreviewView
import androidx.core.graphics.toColorInt
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import com.sw.inbound.utils.ext.appendText
import com.sw.inbound.utils.ext.buildSpannableString
import com.sw.inbound.GlobalData
import com.sw.inbound.R
import com.sw.inbound.databinding.ActivityGoodsListBinding
import com.sw.inbound.databinding.LayoutCameraPreviewBinding
import com.sw.inbound.databinding.ListItemReceiptGoodsBinding
import com.sw.inbound.databinding.ListItemSelfProcurementBinding
import com.sw.inbound.dialog.DialogManager
import com.sw.inbound.dialog.DropdownPopup
import com.sw.inbound.dialog.GoodsRecognizeDialog
import com.sw.inbound.dialog.GoodsSearchDialog
import com.sw.inbound.dialog.Loading
import com.sw.inbound.dialog.WarnDialog
import com.sw.inbound.model.request.GoodsAddParam
import com.sw.inbound.model.request.PurchaseWarehouseParam
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.SearchGoodsInfo
import com.sw.inbound.objbox.FoodModule
import com.sw.inbound.sdk.SensorScaleUtils
import com.sw.inbound.utils.BitmapCropper
import com.sw.inbound.utils.CameraUtils
import com.sw.inbound.utils.ImageUtil
import com.sw.inbound.utils.PreciseDelayHandler
import com.sw.inbound.utils.ext.dp
import com.sw.inbound.utils.ext.gone
import com.sw.inbound.utils.ext.hideKeyboard
import com.sw.inbound.utils.ext.roundedDecimalPlace
import com.sw.inbound.utils.ext.toJsonString
import com.sw.inbound.utils.ext.toast
import com.sw.inbound.viewmodel.ReceiptViewModel
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import timber.log.Timber
import java.io.File
import kotlin.math.abs
import kotlin.math.min
typealias RecognizeCallback = (List<SearchGoodsInfo.Record>) -> Unit
@AndroidEntryPoint
abstract class GoodsListActivity : ComponentActivity() {
companion object {
private const val TAG = "GoodsListActivity"
public const val ID = "id"
public const val SUPPLIER_ID = "supplierId"
public const val IS_RECEIPT_PAGE = "isReceiptPage"
public const val IS_NEW_RECEIPT = "isNewReceipt"
const val PAGE_SIZE = 10
private const val DELAY_TIME: Long = 3 * 1000
}
var isReceiptPage: Boolean = false
var isNewReceipt: Boolean = false
var id: String = ""
var supplierId: String = ""
lateinit var binding: ActivityGoodsListBinding
val viewModel: ReceiptViewModel by viewModels()
val goodsList: MutableList<GoodsInfo> = mutableListOf()
abstract fun initRecyclerView()
abstract fun initView()
abstract fun getWarehouseList(): List<DictType>
private lateinit var backPressedCallback: OnBackPressedCallback
private lateinit var previewView: PreviewView
private val cameraUtils: CameraUtils by lazy {
CameraUtils(this@GoodsListActivity)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityGoodsListBinding.inflate(layoutInflater)
setContentView(binding.root)
initBackDispatcher()
isReceiptPage = intent.getBooleanExtra(IS_RECEIPT_PAGE, true)
isNewReceipt = intent.getBooleanExtra(IS_NEW_RECEIPT, false)
id = intent.getStringExtra(ID) ?: ""
supplierId = intent.getStringExtra(SUPPLIER_ID) ?: ""
cameraUtils.initCamera()
replaceIncludeContent()
initRecyclerView()
initView()
binding.root.setOnClickListener {
it.hideKeyboard()
}
binding.btnBack.setOnClickListener { handleBackEvent() }
binding.flWarehouseDropdown.setOnClickListener { v ->
if (GlobalData.warehouseTypeList.isEmpty()) {
toast("暂无仓库数据")
return@setOnClickListener
}
if (dropdownPopup == null) {
dropdownList.clear()
dropdownList.addAll(GlobalData.warehouseTypeList)
dropdownPopup = DropdownPopup(
context = this,
list = dropdownList,
popHeight = min(355.dp, dropdownList.size * 71.dp)
) {
binding.tvWareHouse.run {
text = it.value
tag = it.id
}
}.also {
it.arrowImage = binding.ivDropdown
it.bgLayout = binding.flWarehouseDropdown
it.defLayoutBgResId = R.drawable.bg_white_radius10_stroke
}
}
dropdownPopup?.showAsDropDown(v)
}
binding.tvPurchaseNo.setOnClickListener {
handleGoodsResult()
}
binding.tvUserName.text = GlobalData.deviceId
binding.tvUserName.gone()
binding.ivLogout.gone()
val previewBinding =
LayoutCameraPreviewBinding.inflate(layoutInflater, binding.flCameraPreview)
previewView = previewBinding.previewView.also {
it.updateLayoutParams {
width = 1
height = 1
}
}
cameraUtils.setPreviewController(previewView)
//startTime = System.currentTimeMillis()
binding.root.postDelayed({
SensorScaleUtils.addWeightListener {
runOnUiThread {
try {
weightCallback?.invoke(it)
recognizeWeight(weight = it)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}, 1 * 1000)
}
private var weightCallback: ((weight: Int) -> Unit)? = null
fun readWeightInfo(callback: (weight: Int) -> Unit) {
weightCallback = callback
}
private var lastWeight = 0
private fun recognizeWeight(weight: Int) {
Timber.tag(TAG).d("recognizeWeight,lastWeight=$lastWeight,weight=$weight")
//if (this.lastWeight - weight >= 0) {
// this.lastWeight = weight
// //从秤上取物品中
// return
//}
if (weight < 200) {
if (abs(weight) < 10) {
// val clazzName = DialogManager.getDialogList()
// .map { it.javaClass.simpleName }
// .joinToString(",")
// Timber.tag(TAG).d(clazzName)
if (DialogManager.getDialogList().size == 1 &&
DialogManager.contains("GoodsRecognizeDialog") &&
goodsRecognizeDialog?.isShowing == true
) {
goodsRecognizeDialog?.dismiss()
}
}
return
}
this.lastWeight = weight
if (DialogManager.hasShowDialog()) {
return
}
// val checkResult = isFirstRecognize.not()
// //&& System.currentTimeMillis() - startTime <= DELAY_TIME
// Timber.tag(TAG).d("recognizeWeight,checkResult=$checkResult")
// if (checkResult) {
// //Timber.tag(TAG).d("recognizeWeight,等待中:${(System.currentTimeMillis() - startTime)/1000}")
// Timber.tag(TAG).d("recognizeWeight, checkResult=true")
// return
// }
Timber.tag(TAG)
.d("recognizeWeight,recognizeFoodisShowRecognizeDialog=$isShowRecognizeDialog")
recognizeFood()
}
//var startTime = 0L
var isFirstRecognize = true
private val takePhotoSuccessCallback: (Uri) -> Unit = { uri ->
Thread {
try {
searchFood(uri)
} catch (e: Exception) {
e.printStackTrace()
}
}.start()
}
private val delayHandler by lazy {
PreciseDelayHandler()
}
fun delayRecognizeFood() {
if (abs(lastWeight) < 10) return
delayHandler.precisePostDelayed(DELAY_TIME) {
runOnUiThread {
recognizeFood()
}
}
}
fun recognizeFood() {
if (isShowRecognizeDialog.not()) {
if (goodsRecognizeDialog == null) {
isShowRecognizeDialog = true
}
return
}
Timber.tag(TAG).d("recognizeFood,takePhoto")
isShowRecognizeDialog = false
//bindCamera()
try {
cameraUtils.takePhoto(takePhotoSuccessCallback)
isFirstRecognize = false
} catch (e: Exception) {
Timber.tag(TAG).d("recognizeFood,takePhoto异常:${e.message}")
e.printStackTrace()
handleGoodsResult()
}
}
private var recognizeCallback: RecognizeCallback? = null
var isAgainRecognize = false
fun againRecognizeFood(callback: RecognizeCallback) {
Loading.show(this)
isAgainRecognize = true
this.recognizeCallback = callback
try {
cameraUtils.takePhoto(takePhotoSuccessCallback)
} catch (e: Exception) {
Timber.tag(TAG).d("againRecognizeFood,takePhoto异常:${e.message}")
e.printStackTrace()
Loading.dismiss()
runOnUiThread {
recognizeCallback?.invoke(mutableListOf())
}
}
}
private var dropdownPopup: DropdownPopup? = null
private val dropdownList: MutableList<DictType> = mutableListOf()
fun getSpannable(text: String, num: Int, color: String): SpannableStringBuilder {
return buildSpannableString {
appendText(text, ForegroundColorSpan("#FF141428".toColorInt()))
appendText(num.toString(), ForegroundColorSpan(color.toColorInt()))
}
}
var isShowRecognizeDialog = true
private fun searchFood(uri: Uri) {
ImageUtil.uriToBitmap(this, uri)?.let { bitmap ->
val newBmp = BitmapCropper.cropCenter(
original = bitmap,
targetWidth = 1000, targetHeight = 1300,
offsetX = 30, offsetY = 100
)
// val file = BitmapSaver.saveToAppFilesDir(
// newBmp,
// this,
// "IMG_CROP_${System.currentTimeMillis()}.jpg"
// )
// Timber.d("${this.javaClass.simpleName}-searchFood-裁剪bitmap保存文件路径:${file?.absolutePath}")
//var foodList = FoodModule.queryFood(newBmp)
//物品编号
//foodList = foodList.map { "WP"+it.split("WP")[1] }
//物品名称
//foodList = foodList.map { it.split("WP")[0] }
val foodList = FoodModule.getFoodScoreList(newBmp)
if (bitmap.isRecycled.not()) {
bitmap.recycle()
}
if (newBmp.isRecycled.not()) {
newBmp.recycle()
}
if (foodList.isEmpty()) {
recognizeFoodError()
return@let
}
foodList.forEach {
it.name = it.name.split("WP")[0]
}
val filterList =
foodList.filter { it.score < FoodModule.BAG_RATE && it.name.contains("黑袋子") }
if (filterList.isNotEmpty()) {
runOnUiThread {
toast("当前物品可能被袋子遮挡,请检查后重试")
if (isAgainRecognize) {
Loading.dismiss()
recognizeCallback?.invoke(mutableListOf())
isAgainRecognize = false
return@runOnUiThread
}
handleGoodsResult(mutableListOf())
}
return@let
}
viewModel.recognizeGoodsList(
//nameList = foodList,
scoreList = foodList,
) { goodsList ->
runOnUiThread {
if (isAgainRecognize) {
Loading.dismiss()
recognizeCallback?.invoke(goodsList)
isAgainRecognize = false
return@runOnUiThread
}
handleGoodsResult(
goodsList = if (goodsList.size == foodList.size) goodsList else mutableListOf(),
// file = null,
// name = foodList.joinToString(",")
)
}
}
}
}
private fun handleGoodsResult(
goodsList: List<SearchGoodsInfo.Record> = mutableListOf(),
file: File? = null,
name: String? = null,
) {
runOnUiThread {
if (goodsRecognizeDialog?.isShowing == true) {
//isShowRecognizeDialog = false
return@runOnUiThread
}
goodsRecognizeDialog = GoodsRecognizeDialog(
context = this,
list = goodsList.toMutableList(),
goodsFile = file,
goodsName = name,
) {
GoodsSearchDialog(this).show()
}.apply {
if (isFinishing.not() && isDestroyed.not()) {
//isShowRecognizeDialog = false
show()
}
}
}
}
var goodsRecognizeDialog: GoodsRecognizeDialog? = null
private fun replaceIncludeContent() {
binding.flContainer.removeAllViews()
if (isReceiptPage) {
val tempBinding =
ListItemReceiptGoodsBinding.inflate(layoutInflater, binding.flContainer, false)
binding.flContainer.addView(tempBinding.root)
} else {
val tempBinding =
ListItemSelfProcurementBinding.inflate(layoutInflater, binding.flContainer, false)
binding.flContainer.addView(tempBinding.root)
}
}
override fun onPause() {
super.onPause()
unBindCamera()
}
fun bindCamera() {
cameraUtils.bind()
}
fun unBindCamera() {
cameraUtils.unbind()
}
override fun onDestroy() {
try {
DialogManager.dismissAll()
} catch (e: Exception) {
e.printStackTrace()
}
super.onDestroy()
//cameraController?.unbind()
//isCameraReady = false
}
fun Double?.plus2(value: Double?): Double {
return (this ?: 0.0) + (value ?: 0.0)
}
fun addGoods(goods: GoodsInfo) {
if (!isReceiptPage) {
goods.isSelected = true
goodsList.add(goods)
binding.rvGoodsList.adapter?.notifyItemInserted(goodsList.size - 1)
return
}
val item = goodsList.firstOrNull { it.goodId == goods.goodId }
if (item == null || item.unitName != goods.unitName) {
//不存在物品或者物品单位不同
goods.isSelected = true
goodsList.add(goods)
binding.rvGoodsList.adapter?.notifyItemInserted(goodsList.size - 1)
return
}
item.isSelected = true
val finalWeight = item.goodsWeight?.toDouble().plus2(goods.goodsWeight?.toDouble())
val finalNum = item.receivedNum.plus2(goods.receivedNum)
val finalAmount = item.recPriceInItem.plus2(goods.recPriceInItem)
val finalPrice =
if (finalAmount == 0.toDouble() || finalNum == 0.toDouble()) 0.toDouble() else finalAmount / finalNum
item.run {
receivedNum = finalNum
recUnitPriceTaxIn = finalPrice.roundedDecimalPlace(2)
recPriceInItem = finalAmount.roundedDecimalPlace(2)
goodsWeight = finalWeight.toBigDecimal()
}
binding.rvGoodsList.adapter?.notifyItemChanged(goodsList.indexOf(item))
if (this is ReceiptActivity) {
this.setConfirmNum()
}
}
fun searchGoods(
searchName: String,
pageNo: Int,
pageSize: Int = PAGE_SIZE,
callback: (List<SearchGoodsInfo.Record>) -> Unit
) {
runBlocking {
val records = searchGoodsBlock(searchName, pageNo, pageSize)
runOnUiThread {
callback(records)
}
}
}
suspend fun searchGoodsBlock(
searchName: String,
pageNo: Int,
pageSize: Int = PAGE_SIZE
): List<SearchGoodsInfo.Record> {
return viewModel.searchGoodsInfoList2(
goodsName = searchName,
pageNo = pageNo,
pageSize = pageSize
)
}
fun searchMultipleGoods(
goodsNameList: List<String>,
callback: (MutableList<SearchGoodsInfo.Record>) -> Unit
) {
runBlocking {
val list = mutableListOf<SearchGoodsInfo.Record>()
for (goodsName in goodsNameList) {
val records = searchGoodsBlock(goodsName, 1, 1)
if (records.isNotEmpty()) {
list.add(records[0])
}
}
callback(list)
}
}
private fun initBackDispatcher() {
// 创建回调,true表示初始启用状态
backPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
handleBackEvent()
}
}
// 注册回调,使用lifecycleOwner确保生命周期安全
onBackPressedDispatcher.addCallback(this, backPressedCallback)
}
private fun handleBackEvent() {
val count = goodsList.count { it.isLocalGoods }
// 处理返回事件
if (count > 0) {
WarnDialog(
context = this@GoodsListActivity,
content = "存在未收货的物品,请确认是否放弃收货,若放弃则数据不会保存?"
) {
//onBackPressedDispatcher.onBackPressed()
finish()
}.show()
} else {
// 允许默认行为
backPressedCallback.isEnabled = false
//onBackPressedDispatcher.onBackPressed()
finish()
}
}
fun receiptSubmit() {
if (binding.tvWareHouse.tag == null) {
toast("请选择仓库")
binding.flWarehouseDropdown.setBackgroundResource(R.drawable.bg_white_stroke_red)
return
}
goodsList.forEach { goodsInfo ->
goodsInfo.warehouseId = binding.tvWareHouse.tag.toString()
goodsInfo.warehouseName = binding.tvWareHouse.text.toString()
}
if (goodsList.isEmpty()) {
toast("请选择收货物品")
return
}
val uploadInfo = UploadInfo(
id = id,
supplierId = supplierId,
receiveGoodsInfos = goodsList
)
Timber.tag("SelfProcurementActivity")
.d("receiptSubmit确认收货提交数据:${uploadInfo.toJsonString()}")
viewModel.confirmReceipt(uploadInfo) { success ->
if (success) {
toast("确认收货成功")
finish()
} else {
toast("确认收货失败")
}
}
}
fun selfProcurementSubmit() {
if (binding.tvWareHouse.tag == null) {
toast("请选择仓库")
binding.flWarehouseDropdown.setBackgroundResource(R.drawable.bg_white_stroke_red)
return
}
if (goodsList.isEmpty()) {
toast("暂无入库物品,请采集后操作")
return
}
val submitData = goodsList.map {
PurchaseWarehouseParam(
goodsId = it.goodId ?: "",
goodsName = it.goodName,
kcUnitId = it.kcUnitId,
goodsCount = it.goodsCount ?: 0.0,
goodsUnitPrice = it.goodsUnitPrice ?: 0.0,
goodsPrice = it.goodsPrice ?: 0.0,
//goodPurId = it.goodsPurId,
warehouseId = binding.tvWareHouse.tag.toString(),
buyToInventoryValue = it.buyToInventoryValue ?: ""
)
}
Timber.tag("SelfProcurementActivity")
.d("selfProcurementSubmit自采入库提交数据:${submitData.toJsonString()}")
viewModel.addToWarehouse(submitData) { success ->
if (success) {
toast("提交入库成功")
finish()
} else {
toast("提交入库失败")
}
}
}
// fun uploadImage(imageUri: Uri, callback: (String?) -> Unit) {
// viewModel.uploadImage(imageUri) { imgUrl ->
// if (imgUrl.isNullOrBlank()) {
// callback(null)
// return@uploadImage
// }
// callback(imgUrl)
// }
// }
fun createNewGoods(file: File, param: GoodsAddParam, callback: (Boolean) -> Unit) {
viewModel.uploadImage(file) { imgUrl ->
if (imgUrl.isNullOrBlank()) {
toast("图片上传失败")
return@uploadImage
}
param.relativeUrl = imgUrl
viewModel.createNewGoods(param = param, callback = callback)
}
}
private fun recognizeFoodError() {
if (isAgainRecognize) {
runOnUiThread {
Loading.dismiss()
//toast("识别失败,请稍候重试")
recognizeCallback?.invoke(mutableListOf())
}
isAgainRecognize = false
} else {
handleGoodsResult()
}
}
fun uploadImage(goodsId: String, goodsName: String, imageFile: File?) {
lifecycleScope.launch {
if (imageFile == null) {
return@launch
}
val list = mutableListOf<File>()
list.add(imageFile)
val map = hashMapOf<String, RequestBody>()
map["goodsId"] = goodsId.toRequestBody()
map["goodsName"] = goodsName.toRequestBody()
viewModel.uploadMultipleImages(files = list, params = map)
}
}
}
@@ -0,0 +1,97 @@
package com.sw.inbound.activity
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.activity.ComponentActivity
import androidx.recyclerview.widget.GridLayoutManager
import coil.load
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.sw.inbound.databinding.ActivityLocalImagePreviewBinding
import com.sw.inbound.databinding.ListItemImagePreviewBinding
import com.sw.inbound.dialog.Loading
import com.sw.inbound.utils.ext.copyText
import java.io.File
class LocalImagePreviewActivity : ComponentActivity() {
private lateinit var binding: ActivityLocalImagePreviewBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityLocalImagePreviewBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.recyclerView.let {
it.layoutManager = GridLayoutManager(this, 6, GridLayoutManager.VERTICAL, false)
it.adapter = imageAdapter
}
binding.btnBack.setOnClickListener { finish() }
loadData()
}
fun loadData() {
Thread {
runOnUiThread {
Loading.show(this)
}
val cropFile = File(this.cacheDir, "crop")
cropFile.listFiles()?.forEach {
if (it.isDirectory) {
return@forEach
}
val start = "IMG_CROP_".length
val end = it.name.indexOf(".jpg")
val time = it.name.substring(start, end).toLong()
if (time >= 1764086400L) {
imageList.add(ImageBean(file = it, name = "$time"))
}
}
imageList.sortBy { it.name.toLong() }
runOnUiThread {
imageAdapter.notifyDataSetChanged()
Loading.dismiss()
}
}.start()
}
private var isCycle = true
private val imageList = mutableListOf<ImageBean>()
private val imageAdapter: ImageAdapter by lazy {
ImageAdapter(imageList).apply {
setOnItemClickListener { _, _, position ->
imageList[position].name.copyText(this@LocalImagePreviewActivity)
}
}
}
data class ImageBean(
var file: File,
var name: String
)
inner class ImageAdapter(var list: MutableList<ImageBean>) :
BaseQuickAdapter<ImageBean, ImageAdapter.VH>(list) {
inner class VH(var binding: ListItemImagePreviewBinding) : QuickViewHolder(binding.root)
override fun onBindViewHolder(holder: VH, position: Int, item: ImageBean?) {
holder.binding.let {
it.imageView.load(item!!.file)
it.textView.text = item.name
}
}
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val inflater = LayoutInflater.from(context)
val binding = ListItemImagePreviewBinding.inflate(inflater, parent, false)
return VH(binding)
}
}
}
@@ -0,0 +1,101 @@
package com.sw.inbound.activity
import android.annotation.SuppressLint
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager
import com.sw.inbound.adapter.ReceiptGoodsAdapter
import com.sw.inbound.model.response.DictType
import com.sw.inbound.utils.ext.gone
import com.sw.inbound.utils.ext.toast
import com.sw.inbound.utils.ext.visible
import kotlinx.coroutines.launch
class ReceiptActivity : GoodsListActivity() {
private val adapter by lazy {
ReceiptGoodsAdapter(goodsList)
}
override fun initRecyclerView() {
binding.rvGoodsList.let {
it.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
it.adapter = adapter
}
}
@SuppressLint("NotifyDataSetChanged")
override fun initView() {
//initRvTestData()
binding.tvConfirmNum.run {
if (isNewReceipt) gone() else visible()
}
binding.tvNotConfirmNum.run {
if (isNewReceipt) gone() else visible()
}
binding.btnConfirmReceipt.setOnClickListener {
if (isNewReceipt) {
selfProcurementSubmit()
} else {
receiptSubmit()
}
}
if (isNewReceipt.not()) {
viewModel.getReceiveDetail(id) { detail ->
runOnUiThread {
if (detail == null) {
toast("未查询到数据")
return@runOnUiThread
}
binding.tvPurchaseNo.text = "采购单号:${detail.purCode}"
val list = detail.receiveGoodsInfoList
list.forEach { goodsInfo ->
goodsInfo.recUnitPriceTaxInBak = goodsInfo.recUnitPriceTaxIn
goodsInfo.recUnitPriceTaxIn = 0.0
goodsInfo.unitNameBak = goodsInfo.unitName
goodsInfo.receivedNumBak = goodsInfo.receivedNum
}
goodsList.addAll(list)
adapter.notifyDataSetChanged()
setConfirmNum()
}
}
} else {
binding.tvPurchaseNo.text = "新增收货"
}
// lifecycleScope.launch {
// repeatOnLifecycle(Lifecycle.State.CREATED) {
// launch {
// viewModel.adjustedOrders.collect {
// val confirmOrderCount = it.size
// binding.tvConfirmNum.text = getSpannable("已确认 ", num = confirmOrderCount, color = "#FF009632")
// val notConfirmOrderCount = goodsList.size - confirmOrderCount
// binding.tvNotConfirmNum.text = getSpannable("未确认 ", num = notConfirmOrderCount, color = "#FF009632")
// }
// }
// }
// }
}
override fun getWarehouseList(): List<DictType> {
return goodsList.filter {
it.warehouseId.isNullOrBlank().not() && it.warehouseName.isNullOrBlank().not()
}
.distinctBy { it.warehouseId }
.map {
DictType(id = it.warehouseId!!, value = it.warehouseName!!)
}
}
fun setConfirmNum() {
val confirmOrderCount = goodsList.count { it.isAdjusted }
binding.tvConfirmNum.text =
getSpannable("已确认 ", num = confirmOrderCount, color = "#FF009632")
val notConfirmOrderCount = goodsList.size - confirmOrderCount
binding.tvNotConfirmNum.text =
getSpannable("未确认 ", num = notConfirmOrderCount, color = "#FFFF0000")
}
}
@@ -0,0 +1,62 @@
package com.sw.inbound.activity
import android.util.Log
import androidx.recyclerview.widget.LinearLayoutManager
import com.sw.inbound.R
import com.sw.inbound.adapter.SelfProcurementAdapter
import com.sw.inbound.dialog.WarnDialog
import com.sw.inbound.model.request.PurchaseWarehouseParam
import com.sw.inbound.model.response.DictType
import com.sw.inbound.utils.ext.toJsonString
import com.sw.inbound.utils.ext.toast
import timber.log.Timber
import kotlin.getValue
class SelfProcurementActivity : GoodsListActivity() {
private val adapter by lazy {
SelfProcurementAdapter(goodsList).apply {
setOnItemClickListener { adapter, view, position ->
goodsList[position].let {
it.isSelected = it.isSelected.not()
}
notifyItemChanged(position)
}
addOnItemChildClickListener(R.id.ivState) { adapter, view, position ->
///删除行数据弹窗
showWarnDialog(content = "请确认是否删除该条入库数据,删除后将无法恢复?") {
removeAt(position)
}
}
}
}
private fun showWarnDialog(content:String, callback:()-> Unit) {
WarnDialog(context = this, content = content, confirmBlock = callback).show()
}
override fun initRecyclerView() {
binding.rvGoodsList.let {
it.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
it.adapter = adapter
}
}
override fun initView() {
binding.tvPurchaseNo.text = "自采入库"
binding.btnConfirmReceipt.setOnClickListener {
selfProcurementSubmit()
}
}
override fun getWarehouseList(): List<DictType> {
return goodsList.filter {
it.warehouseId.isNullOrBlank().not() && it.warehouseName.isNullOrBlank().not()
}
.distinctBy { it.warehouseId }
.map {
DictType(id = it.warehouseId!!, value = it.warehouseName!!)
}
}
}
@@ -0,0 +1,56 @@
package com.sw.inbound.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.graphics.toColorInt
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.sw.inbound.databinding.ListItemCollectSearchBinding
import com.sw.inbound.model.response.SearchGoodsInfo
import com.sw.inbound.utils.ext.dp
import com.sw.inbound.utils.ext.setShapeDrawable
class CollectSearchAdapter(var list: MutableList<SearchGoodsInfo.Record>) :
BaseQuickAdapter<SearchGoodsInfo.Record, CollectSearchAdapter.VH>(list) {
inner class VH(var binding: ListItemCollectSearchBinding) : QuickViewHolder(binding.root)
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val inflater = LayoutInflater.from(context)
val binding = ListItemCollectSearchBinding.inflate(inflater, parent, false)
return VH(binding)
}
override fun onBindViewHolder(holder: VH, position: Int, item: SearchGoodsInfo.Record?) {
holder.binding.let {
it.tvGoodsName.run {
text = item!!.goodsName
setTextColor(if (item!!.isSelected) "#FF0032C8".toColorInt() else "#FF666666".toColorInt())
//setTextColor("#FF666666".toColorInt())
}
it.root.run {
if (item!!.isSelected) {
setShapeDrawable(
solidColor = "#FFEAF0FF",
strokeColor = "#FF0033CC",
strokeWidth = 2.dp,
radius = 10.dp
)
} else {
setShapeDrawable(
solidColor = "#FFF5F5F5",
radius = 10.dp
)
}
}
// it.root.run {
// setShapeDrawable(
// solidColor = "#FFF5F5F5",
// radius = 10.dp
// )
// }
}
}
}
@@ -0,0 +1,31 @@
package com.sw.inbound.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.sw.inbound.databinding.ListItemDropdownBinding
import com.sw.inbound.model.response.DictType
import com.sw.inbound.utils.ext.gone
import com.sw.inbound.utils.ext.visible
class DropdownAdapter(var list: MutableList<DictType>) :
BaseQuickAdapter<DictType, DropdownAdapter.VH>(list) {
inner class VH(var binding: ListItemDropdownBinding) : QuickViewHolder(binding.root)
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val inflater = LayoutInflater.from(context)
val binding = ListItemDropdownBinding.inflate(inflater, parent, false)
return VH(binding)
}
override fun onBindViewHolder(holder: VH, position: Int, item: DictType?) {
holder.binding.tvDropdown.text = item?.value
holder.binding.divider.run {
if (position == list.size - 1) gone() else visible()
}
}
}
@@ -0,0 +1,50 @@
package com.sw.inbound.adapter
import android.content.Context
import android.net.Uri
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.core.net.toUri
import coil.load
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.sw.inbound.R
import com.sw.inbound.databinding.ListItemFoodCollectionBinding
import com.sw.inbound.objbox.FoodCollectionBean
import kotlin.let
import kotlin.run
class FoodCollectionAdapter (var list: MutableList<FoodCollectionBean>) :
BaseQuickAdapter<FoodCollectionBean, FoodCollectionAdapter.VH>(list) {
inner class VH(var binding: ListItemFoodCollectionBinding) : QuickViewHolder(binding.root)
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val inflater = LayoutInflater.from(context)
val binding = ListItemFoodCollectionBinding.inflate(inflater, parent, false)
return VH(binding)
}
override fun onBindViewHolder(holder: VH, position: Int, item: FoodCollectionBean?) {
val binding = holder.binding
item?.let {
binding.ivFinish.visibility = if (it.isFinish) View.VISIBLE else View.GONE
binding.ivDelete.visibility = if (it.isShowCamera) View.GONE else View.VISIBLE
binding.imageView.run {
if (it.isShowCamera) {
scaleType = ImageView.ScaleType.CENTER
setImageResource(R.mipmap.ic_camera256)
} else {
scaleType = ImageView.ScaleType.FIT_CENTER
//setImageURI(it.imageUri)
//load(it.bitmap)
// setImageBitmap(it.bitmap!!)
setImageURI(it.imageFile?.toUri())
}
}
}
}
}
@@ -0,0 +1,50 @@
package com.sw.inbound.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.graphics.toColorInt
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.sw.inbound.R
import com.sw.inbound.databinding.ListItemGoodsSearchBinding
import com.sw.inbound.model.response.SearchGoodsInfo
import com.sw.inbound.utils.ext.dp
import com.sw.inbound.utils.ext.setShapeDrawable
class GoodsSearchAdapter(var list: MutableList<SearchGoodsInfo.Record>) :
BaseQuickAdapter<SearchGoodsInfo.Record, GoodsSearchAdapter.VH>(list) {
inner class VH(var binding: ListItemGoodsSearchBinding) : QuickViewHolder(binding.root)
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val inflater = LayoutInflater.from(context)
val binding = ListItemGoodsSearchBinding.inflate(inflater, parent, false)
return VH(binding)
}
override fun onBindViewHolder(holder: VH, position: Int, item: SearchGoodsInfo.Record?) {
holder.binding.let {
it.tvGoodsName.run {
text = item!!.goodsName
setTextColor(if (item!!.isSelected) "#FF0032C8".toColorInt() else "#FF666666".toColorInt())
}
it.root.run {
if (item!!.isSelected) {
setShapeDrawable(
solidColor = "#FFEAF0FF",
strokeColor = "#FF0033CC",
strokeWidth = 2.dp,
radius = 10.dp
)
} else {
setShapeDrawable(
solidColor = "#FFF5F5F5",
radius = 10.dp
)
}
}
}
}
}
@@ -0,0 +1,221 @@
package com.sw.inbound.adapter
import android.content.Context
import android.text.InputType
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.graphics.toColorInt
import androidx.core.view.updateLayoutParams
import androidx.core.widget.addTextChangedListener
import androidx.recyclerview.widget.RecyclerView
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.sw.inbound.R
import com.sw.inbound.databinding.ListItemReceiptGoodsBinding
import com.sw.inbound.dialog.WarnDialog
import com.sw.inbound.ext.toFormattedString
import com.sw.inbound.ext.toSafeDouble
import com.sw.inbound.model.response.GoodsInfo
import com.sw.inbound.utils.ext.dp
import com.sw.inbound.utils.ext.gone
import com.sw.inbound.utils.ext.hideKeyboard
import com.sw.inbound.utils.ext.ifNullOrBlank
import com.sw.inbound.utils.ext.ifNullOrZero
import com.sw.inbound.utils.ext.roundedDecimalPlace
import com.sw.inbound.utils.ext.setShapeDrawable
import com.sw.inbound.utils.ext.toast
import com.sw.inbound.utils.ext.visible
import com.sw.inbound.utils.removeTrailingZeros
class ReceiptGoodsAdapter(var list: MutableList<GoodsInfo>) :
BaseQuickAdapter<GoodsInfo, ReceiptGoodsAdapter.VH>(list) {
inner class VH(var binding: ListItemReceiptGoodsBinding) : QuickViewHolder(binding.root)
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val inflater = LayoutInflater.from(context)
val binding = ListItemReceiptGoodsBinding.inflate(inflater, parent, false)
return VH(binding)
}
override fun onBindViewHolder(holder: VH, position: Int, item: GoodsInfo?) {
holder.binding.run {
root.updateLayoutParams<RecyclerView.LayoutParams> {
height = 100.dp
topMargin = 2.dp
bottomMargin = 3.dp
}
root.setShapeDrawable(
solidColor = if (item!!.isSelected) "#FFD5E1FF" else "#FFFFFFFF",
radius = 6.dp
)
root.setOnClickListener {
it.hideKeyboard()
}
updateTextView(
tvGoodsName, tvProcurementUnit,
tvProcurementCount, tvGoodsWeight,
etReceiptCount, etReceiptAmount
//tvReceiptCount, tvProcurementPrice, tvProcurementAmount,
)
tvGoodsName.text = item.goodName.ifNullOrBlank("-")
tvProcurementUnit.text = item.unitName.ifNullOrBlank("-")
val realCount = item.receiveCount?:0.toDouble()
tvProcurementCount.text = if (realCount == 0.toDouble()) "-" else realCount.removeTrailingZeros()
//tvProcurementPrice.text = item.recUnitPriceTaxInBak.toSafeDouble(2).ifNullOrZero("-")
//tvProcurementAmount.text = item.recPriceExItem.toSafeDouble(2).ifNullOrZero("-")
//tvReceiptCount.text = item.receivedNum.toSafeDouble(2).ifNullOrZero("-")
tvGoodsWeight.text = doubleFormat(item.goodsWeight?.toDouble())
tvReceiptPrice.text = doubleFormat(item.recUnitPriceTaxIn)
if (item.unitHint.isNullOrBlank().not()) {
unitDivider.visible()
tvUnitHint.run {
visible()
text = item.unitHint?.replace("", "\n")
}
}
tvState.gone()
ivState.run {
visible()
setImageResource(
if (item.isSelected.not()) R.mipmap.ic_receipt_no
else if (item.isLocalGoods) R.mipmap.ic_receipt_local
else if ((item.receivedNum?:0.toDouble()) < (item.receiveCount?:0.toDouble())) R.mipmap.ic_receipt_part
else R.mipmap.ic_receipt_all
)
}
etReceiptCount.run {
isEnabled = item.receivedNum != null && item.receivedNum != 0.toDouble()
updateLayoutParams<LinearLayout.LayoutParams> {
topMargin = if (isEnabled) 20.dp else 0
bottomMargin = if (isEnabled) 20.dp else 0
leftMargin = if (isEnabled) 20.dp else 0
rightMargin = if (isEnabled) 20.dp else 0
}
if (isEnabled) {
setShapeDrawable(strokeColor = "#FFDCDCF0", strokeWidth = 2.dp, radius = 8.dp)
val unit = item.unitName
inputType = if (unit == "千克" || unit == "公斤" || unit == "") {
InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL
} else {
InputType.TYPE_CLASS_NUMBER
}
setText(item.receivedNum?.removeTrailingZeros())
setSelection(length())
} else {
setText("-")
}
}
etReceiptAmount.run {
isEnabled = item.recPriceInItem != null && item.recPriceInItem != 0.toDouble()
updateLayoutParams<LinearLayout.LayoutParams> {
topMargin = if (isEnabled) 20.dp else 0
bottomMargin = if (isEnabled) 20.dp else 0
leftMargin = if (isEnabled) 20.dp else 0
rightMargin = if (isEnabled) 20.dp else 0
}
if (isEnabled) {
setShapeDrawable(strokeColor = "#FFDCDCF0", strokeWidth = 2.dp, radius = 6.dp)
setText(item.recPriceInItem?.removeTrailingZeros())
setSelection(length())
} else {
setText("-")
val colorString = if (item.isSelected) "#FFD5E1FF" else "#FFFFFFFF"
setBackgroundColor(colorString.toColorInt())
}
}
addEditListener(etReceiptCount) {
if (it == 0.toDouble()) {
context.toast("数量不能为0")
etReceiptCount.setText(item.receivedNum?.removeTrailingZeros())
return@addEditListener
}
item.receivedNum = it
item.recUnitPriceTaxIn = item.recPriceInItem!! / it
tvReceiptPrice.text = "${item.recUnitPriceTaxIn?.roundedDecimalPlace(2)}"
}
addEditListener(etReceiptAmount) {
if (it == 0.toDouble()) {
context.toast("金额不能为0")
etReceiptAmount.setText(item.recPriceInItem?.removeTrailingZeros())
return@addEditListener
}
item.recPriceInItem = it
item.recUnitPriceTaxIn = it / item.receivedNum!!
tvReceiptPrice.text = "${item.recUnitPriceTaxIn?.roundedDecimalPlace(2)}"
}
ivClear.run {
visible()
setOnClickListener {
if (item.isLocalGoods) {
//删除行数据弹窗
showWarnDialog(content = "请确认是否删除该条采购数据,删除后不可恢复?") {
list.removeAt(position)
notifyDataSetChanged()
}
return@setOnClickListener
}
//清除数据,恢复默认
showWarnDialog(content = "请确认是否清除添加的收货信息,清除后不可恢复?") {
item.receivedNum = item.receivedNumBak
item.recPriceInItem = 0.0
item.recUnitPriceTaxIn = 0.0
item.unitName = item.unitNameBak
notifyItemChanged(position)
}
}
}
}
}
private fun showWarnDialog(content:String, callback:()-> Unit) {
WarnDialog(context = context, content = content, confirmBlock = callback).show()
}
private fun updateTextView(vararg views: TextView) {
views.forEach {
it.setTextColor("#FF1E2832".toColorInt())
it.paint.isFakeBoldText = true
}
}
private fun addEditListener(editText: EditText, action: (Double) -> Unit) {
editText.run {
addTextChangedListener(onTextChanged = { it, _, _, _ ->
try {
if (editText.isFocused.not()) {
return@addTextChangedListener
}
val input = it.toString()
if (input.startsWith(".")) {
setText("0.")
setSelection(length())
} else if (input.contains(".")) {
val index = input.indexOf(".")
if (input.length - index > 3) {
setText(input.substring(0, index + 3))
setSelection(length())
}
}
val value = if (input == "0.") 0.0 else input.toDouble()
action(value)
} catch (e: Exception) {
e.printStackTrace()
}
})
}
}
private fun doubleFormat(num:Double?): String{
val realNum = num?:0.toDouble()
return if (realNum == 0.toDouble()) "-" else realNum.toFormattedString(2)
}
}
@@ -0,0 +1,57 @@
package com.sw.inbound.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.ImageView
import coil.load
import coil.transform.RoundedCornersTransformation
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.sw.inbound.databinding.ListItemRecognizeBinding
import com.sw.inbound.ext.toFormattedString
import com.sw.inbound.model.response.SearchGoodsInfo
import com.sw.inbound.utils.ext.dp
import com.sw.inbound.utils.ext.setShapeDrawable
import com.sw.inbound.utils.ext.setShapeDrawable2
class RecognizeAdapter(var list: MutableList<SearchGoodsInfo.Record>) :
BaseQuickAdapter<SearchGoodsInfo.Record, RecognizeAdapter.VH>(list) {
inner class VH(var binding: ListItemRecognizeBinding) : QuickViewHolder(binding.root)
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val inflater = LayoutInflater.from(context)
val binding = ListItemRecognizeBinding.inflate(inflater, parent, false)
return VH(binding)
}
override fun onBindViewHolder(holder: VH, position: Int, item: SearchGoodsInfo.Record?) {
holder.binding.let {
it.tvRecName.run {
text = if (item!!.score == 0) item.goodsName else "${item.goodsName}\n${((item.score)/100.0).toFormattedString(2)}%"
setShapeDrawable2(
solidColor = "#DEF5F5F5",
bottomRightRadius = 12.dp,
bottomLeftRadius = 12.dp
)
}
it.ivRecImage.run {
load(item!!.relativeUrl) {
transformations(RoundedCornersTransformation(radius = 12.dp.toFloat()))
}
scaleType = ImageView.ScaleType.CENTER_CROP
}
//it.root.run {
// setShapeDrawable(
// //strokeColor = if (item.isSelected) "#FF0033CC" else "#FFDCDCF0",
// strokeColor = "#FFDCDCF0",
// strokeWidth = 2.dp,
// radius = 12.dp
// )
// //setPadding(if (item.isSelected) 2.dp else 0)
//}
}
}
}
@@ -0,0 +1,160 @@
package com.sw.inbound.adapter
import android.content.Context
import android.text.InputType
import android.view.LayoutInflater
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.graphics.toColorInt
import androidx.core.view.updateLayoutParams
import androidx.core.widget.addTextChangedListener
import androidx.recyclerview.widget.RecyclerView
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.sw.inbound.databinding.ListItemSelfProcurementBinding
import com.sw.inbound.model.response.GoodsInfo
import com.sw.inbound.utils.ext.dp
import com.sw.inbound.utils.ext.gone
import com.sw.inbound.utils.ext.hideKeyboard
import com.sw.inbound.utils.ext.roundedDecimalPlace
import com.sw.inbound.utils.ext.setShapeDrawable
import com.sw.inbound.utils.ext.toast
import com.sw.inbound.utils.ext.visible
import com.sw.inbound.utils.removeTrailingZeros
class SelfProcurementAdapter(var list: MutableList<GoodsInfo>) :
BaseQuickAdapter<GoodsInfo, SelfProcurementAdapter.VH>(list) {
inner class VH(var binding: ListItemSelfProcurementBinding) : QuickViewHolder(binding.root)
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val inflater = LayoutInflater.from(context)
val binding = ListItemSelfProcurementBinding.inflate(inflater, parent, false)
return VH(binding)
}
override fun onBindViewHolder(holder: VH, position: Int, item: GoodsInfo?) {
holder.binding.run {
root.updateLayoutParams<RecyclerView.LayoutParams> {
height = 100.dp
topMargin = 2.dp
bottomMargin = 3.dp
}
root.setShapeDrawable(
solidColor = if (item!!.isSelected) "#FFD5E1FF" else "#FFFFFFFF",
radius = 6.dp
)
root.setOnClickListener {
it.hideKeyboard()
}
updateTextView(
tvGoodsName, tvGoodsUnit,
etGoodsCount, tvGoodsWeight, tvGoodsPrice,
etGoodsAmount
)
tvGoodsName.text = item.goodName
tvGoodsUnit.text = item.unitName
if (item.unitHint.isNullOrBlank().not()) {
unitDivider.visible()
tvUnitHint.run {
visible()
text = item.unitHint?.replace("", "\n")
}
}
etGoodsCount.run {
updateLayoutParams<LinearLayout.LayoutParams> {
topMargin = 20.dp
bottomMargin = 20.dp
}
setShapeDrawable(strokeColor = "#FFDCDCF0", strokeWidth = 2.dp, radius = 8.dp)
isEnabled = true
setText(item.goodsCount?.removeTrailingZeros())
val unit = item.unitName
inputType = if (unit == "千克" || unit == "公斤" || unit == "") {
InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL
} else {
InputType.TYPE_CLASS_NUMBER
}
if (isEnabled) {
setSelection(length())
}
}
tvGoodsWeight.text = item.goodsWeight?.toDouble()?.removeTrailingZeros()
tvGoodsPrice.text = "${item.goodsUnitPrice?.roundedDecimalPlace(2)}"
etGoodsAmount.run {
updateLayoutParams<LinearLayout.LayoutParams> {
topMargin = 20.dp
bottomMargin = 20.dp
}
setShapeDrawable(strokeColor = "#FFDCDCF0", strokeWidth = 2.dp, radius = 6.dp)
isEnabled = true
setText(item.goodsPrice?.removeTrailingZeros())
if (isEnabled) {
setSelection(length())
}
}
tvState.gone()
ivState.visible()
addEditListener(etGoodsCount) {
if (it == 0.toDouble()) {
context.toast("数量不能为0")
etGoodsCount.setText(item.goodsCount?.removeTrailingZeros())
return@addEditListener
}
item.goodsCount = it
item.goodsUnitPrice = item.goodsPrice!! / it
tvGoodsPrice.text = "${item.goodsUnitPrice?.roundedDecimalPlace(2)}"
}
addEditListener(etGoodsAmount) {
if (it == 0.toDouble()) {
context.toast("金额不能为0")
etGoodsAmount.setText(item.goodsPrice?.removeTrailingZeros())
return@addEditListener
}
item.goodsPrice = it
item.goodsUnitPrice = it / item.goodsCount!!
tvGoodsPrice.text = "${item.goodsUnitPrice?.roundedDecimalPlace(2)}"
}
}
}
private fun updateTextView(vararg views: TextView) {
views.forEach {
it.setTextColor("#FF1E2832".toColorInt())
it.paint.isFakeBoldText = true
}
}
private fun addEditListener(editText: EditText, action: (Double) -> Unit) {
editText.run {
addTextChangedListener(onTextChanged = { it, _, _, _ ->
try {
if (editText.isFocused.not()) {
return@addTextChangedListener
}
val input = it.toString()
if (input.startsWith(".")) {
setText("0.")
setSelection(length())
} else if (input.contains(".")) {
val index = input.indexOf(".")
if (input.length - index > 3) {
setText(input.substring(0, index + 3))
setSelection(length())
}
}
val value = if (input == "0.") 0.0 else input.toDouble()
action(value)
} catch (e: Exception) {
e.printStackTrace()
}
})
}
}
}
@@ -0,0 +1,68 @@
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 DEVICE_BASE_URL = "http://device.shuziweidao.com:8889/"
// private const val BASE_URL = "https://127.0.0.1"
private const val BASE_URL = "https://yyjk.shuziweidao.com/gateway/"
// private const val BASE_URL = "http://192.168.1.237:9002/"
// private const val BASE_URL = "https://vip.shuziweidao.com/gateway/"
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(DEVICE_BASE_URL)
.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,295 @@
package com.sw.inbound.dialog
import android.content.Context
import android.net.Uri
import android.view.LayoutInflater
import android.view.View
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import androidx.camera.view.PreviewView
import androidx.core.view.updateLayoutParams
import coil.load
import com.sw.inbound.GlobalData
import com.sw.inbound.R
import com.sw.inbound.activity.GoodsListActivity
import com.sw.inbound.databinding.DialogAddGoodsBinding
import com.sw.inbound.databinding.LayoutCameraPreviewBinding
import com.sw.inbound.ext.toSafeBigDecimal
import com.sw.inbound.ext.toSafeFloat
import com.sw.inbound.model.request.GoodsAddParam
import com.sw.inbound.model.response.DictType
import com.sw.inbound.utils.BitmapCropper
import com.sw.inbound.utils.BitmapSaver
import com.sw.inbound.utils.CameraUtils
import com.sw.inbound.utils.ImageUtil
import com.sw.inbound.utils.ext.dp
import com.sw.inbound.utils.ext.gone
import com.sw.inbound.utils.ext.toast
import com.sw.inbound.utils.ext.visible
import kotlin.math.min
class AddGoodsDialog(
context: Context,
var cancelBlock: () -> Unit = {},
var confirmBlock: (String) -> Unit = {},
) : BaseDialog(context) {
private lateinit var binding: DialogAddGoodsBinding
override fun getRootView(): View {
val inflater = LayoutInflater.from(context)
binding = DialogAddGoodsBinding.inflate(inflater)
return binding.root
}
private var dropdownPopup: DropdownPopup? = null
private val dropdownList: MutableList<DictType> = mutableListOf()
private var activity: GoodsListActivity? = null
private val cameraUtils: CameraUtils by lazy {
CameraUtils(activity!!)
}
override fun show() {
super.show()
cameraUtils.setPreviewController(previewView)
cameraUtils.bind()
window?.decorView?.postDelayed({
binding.llCameraFlag.gone()
}, 1000)
}
private val takePhotoCallback: (Uri) -> Unit = { uri ->
binding.flCameraPreview.gone()
imageUri = uri
binding.cvImageBg.visible()
binding.ivGoodsImage.load(uri)
}
private lateinit var previewView: PreviewView
override fun initView() {
activity = getReceiptActivity()
activity?.unBindCamera()
setOnDismissListener {
cameraUtils.unbind()
}
cameraUtils.initCamera()
val previewBinding =
LayoutCameraPreviewBinding.inflate(
LayoutInflater.from(context),
binding.flCameraPreview
)
previewBinding.previewView.let {
it.updateLayoutParams {
width = 428.dp
height = 320.dp
}
previewView = it
}
// binding.flCameraPreview.removeAllViews()
// binding.flCameraPreview.addView(previewView)
binding.flCameraPreview.visible()
binding.cvImageBg.gone()
binding.btnClose.setOnClickListener {
cancelBlock()
dismiss()
}
binding.btnConfirm.setOnClickListener {
submit()
}
binding.btnStartPreview.setOnClickListener {
binding.cvImageBg.gone()
binding.flCameraPreview.visible()
}
binding.btnTakePhoto.setOnClickListener {
cameraUtils.takePhoto(takePhotoCallback)
}
binding.btnUploadImg.gone()
// binding.btnUploadImg.setOnClickListener {
// uploadImage()
// }
binding.tvGoodsType.let { tv ->
tv.text = "原材料"
tv.tag = GlobalData.goodsTypeList.firstOrNull { it.value == "原材料" }?.id
?: "1980098911276191746"
}
showDropDownList(
binding.flProcurementUnit,
textView = binding.tvProcurementUnit,
arrowImage = binding.ivProcurementArrow,
list = GlobalData.unitTypeList,
height = 2 * 71.dp
)
// showDropDownList(
// binding.flGoodsType,
// textView = binding.tvGoodsType,
// list = GlobalData.goodsTypeList,
// )
showDropDownList(
binding.flStorageType,
textView = binding.tvStorageType,
arrowImage = binding.ivStorageArrow,
list = GlobalData.storageTypeList,
)
showDropDownList(
dropdownView = binding.flWarehouseUnit,
textView = binding.tvWarehouseUnit,
arrowImage = binding.ivWarehouseArrow,
list = GlobalData.unitTypeList,
height = 3 * 71.dp
) {
binding.tvSelectUseUnit.text = it.value
binding.tvSelectProcurementUnit.text = it.value
}
}
private fun showDropDownList(
dropdownView: FrameLayout,
textView: TextView,
arrowImage: ImageView,
list: List<DictType>,
height: Int = 355.dp,
block: (DictType) -> Unit = {}
) {
dropdownView.setOnClickListener { v ->
dropdownList.clear()
val tempList =
list.filter { it.id.isNullOrBlank().not() && it.value.isNullOrBlank().not() }
if (tempList.isEmpty()) {
context.toast("暂无下拉数据")
return@setOnClickListener
}
dropdownList.addAll(tempList)
val realHeight = min(height, dropdownList.size * 71.dp)
dropdownPopup = DropdownPopup(
context = context,
list = dropdownList,
popWidth = dropdownView.width,
popHeight = realHeight
) {
textView.run {
text = it.value
tag = it.id
}
block(it)
}.also {
it.arrowImage = arrowImage
it.bgLayout = dropdownView
}
dropdownPopup?.showAsDropDown(dropdownView)
dropdownPopup?.setOnDismissListener {
dropdownPopup = null
}
}
}
private fun submit() {
if (imageUri == null) {
context.toast("请采集图片")
return
}
val goodsName = binding.etGoodsName.text.trim().toString()
if (goodsName.isBlank()) {
context.toast("请输入物品名称")
return
}
val netRateText = binding.etNetMaterialRate.text.trim().toString()
if (netRateText.isBlank()) {
context.toast("请输入净材率")
return
}
val netRate = (netRateText.toFloatOrNull() ?: 0f) / 100f
val goodsType = binding.tvGoodsType.tag
if (goodsType == null || goodsType.toString().isBlank()) {
context.toast("请录入物品类型")
return
}
val storageType = binding.tvStorageType.tag
if (storageType == null || storageType.toString().isBlank()) {
context.toast("请录入存储方式")
return
}
val warehouseUnit = binding.tvWarehouseUnit.tag
if (warehouseUnit == null || warehouseUnit.toString().isBlank()) {
context.toast("请录入库存单位")
return
}
val purchaseUnit = binding.tvProcurementUnit.tag
if (purchaseUnit == null || purchaseUnit.toString().isBlank()) {
context.toast("请录采购单位")
return
}
val costValue = binding.etUseUnit.text.trim().toString()
if (costValue.isBlank()) {
context.toast("请输入物品名称")
return
}
val goodsCount = binding.etGoodsCount.text.trim().toString()
if (costValue.isBlank()) {
context.toast("请输入采购数量")
return
}
val goodsPrice = binding.etGoodsPrice.text.trim().toString()
if (goodsPrice.isBlank()) {
context.toast("请输入采购单价")
return
}
val param = GoodsAddParam(
goodName = goodsName,
netRate = netRate,
goodType = goodsType.toString(),
storageType = storageType.toString(),
unitId = warehouseUnit.toString(),
consumeValue = costValue,
purchaseUnit = purchaseUnit.toString(),
purchaseValue = goodsCount.toSafeFloat(),
purchasePrice = goodsPrice.toSafeBigDecimal(),
)
activity?.let {
Loading.show(it)
}
ImageUtil.uriToBitmap(context, imageUri!!)?.let { bitmap ->
val newBmp = BitmapCropper.cropCenter(
original = bitmap,
targetWidth = 1000, targetHeight = 1300,
offsetX = 30, offsetY = 100
)
val file = BitmapSaver.saveToAppFilesDir(
newBmp,
context,
"IMG_CROP_${System.currentTimeMillis()}.jpg"
)
if(file == null) {
context.toast("请重新拍照后再试")
Loading.dismiss()
return@let
}
activity?.createNewGoods(file, param) {
Loading.dismiss()
if (it.not()) {
context.toast("新增物品失败")
return@createNewGoods
}
context.toast("新增物品成功")
confirmBlock(goodsName)
dismiss()
}
}
}
private var imageUri: Uri? = null
// private fun uploadImage() {
// if (imageUri == null) {
// context.toast("请采集图片")
// return
// }
// activity?.uploadImage(imageUri!!) { imgUrl ->
// if (imgUrl.isNullOrBlank()) {
// context.toast("图片上传失败")
// return@uploadImage
// }
// context.toast(imgUrl)
// }
// }
}
@@ -0,0 +1,32 @@
//package com.sw.inbound.dialog
//
//import android.content.Context
//import android.view.LayoutInflater
//import android.view.View
//import com.sw.inbound.databinding.DialogAddProcurementUnitBinding
//
//class AddProcurementUnitDialog(
// context: Context,
// var cancelBlock: () -> Unit = {},
// var confirmBlock: () -> Unit = {},
//) : BaseDialog(context) {
//
// private lateinit var binding: DialogAddProcurementUnitBinding
// override fun getRootView(): View {
// val inflater = LayoutInflater.from(context)
// binding = DialogAddProcurementUnitBinding.inflate(inflater)
// return binding.root
// }
//
// override fun initView() {
// binding.tvSelectUnit.text = "箱"
// binding.btnCancel.setOnClickListener {
// cancelBlock()
// dismiss()
// }
// binding.btnConfirm.setOnClickListener {
// confirmBlock()
// dismiss()
// }
// }
//}
@@ -0,0 +1,72 @@
package com.sw.inbound.dialog
import android.app.Dialog
import android.content.Context
import android.content.ContextWrapper
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.view.Gravity
import android.view.View
import com.sw.inbound.R
import com.sw.inbound.activity.FoodCollectionActivity
import com.sw.inbound.activity.GoodsListActivity
import com.sw.inbound.utils.ext.dp
import com.sw.inbound.utils.ext.hideKeyboard
abstract class BaseDialog(
context: Context,
var defWidth: Int = 1500.dp,
var defHeight: Int = 900.dp
) : Dialog(context, R.style.DialogTheme) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val rootView = getRootView()
setContentView(rootView)
setCancelable(false)
setCanceledOnTouchOutside(false)
window?.apply {
setLayout(defWidth, defHeight)
setBackgroundDrawable(ColorDrawable())
setGravity(Gravity.CENTER)
}
rootView.setOnClickListener { it.hideKeyboard() }
initView()
}
abstract fun getRootView(): View
abstract fun initView()
fun getReceiptActivity(): GoodsListActivity? {
var ctx = context
while (ctx is ContextWrapper) {
ctx = ctx.baseContext
if (ctx is GoodsListActivity) {
return ctx
}
}
return null
}
fun getFoodCollectionActivity(): FoodCollectionActivity? {
var ctx = context
while (ctx is ContextWrapper) {
ctx = ctx.baseContext
if (ctx is FoodCollectionActivity) {
return ctx
}
}
return null
}
override fun show() {
super.show()
DialogManager.addDialog(this)
}
override fun dismiss() {
super.dismiss()
DialogManager.removeDialog(this)
}
}
@@ -0,0 +1,234 @@
package com.sw.inbound.dialog
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import androidx.activity.viewModels
import androidx.core.widget.addTextChangedListener
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import com.sw.inbound.R
import com.sw.inbound.activity.FoodCollectionActivity
import com.sw.inbound.adapter.CollectSearchAdapter
import com.sw.inbound.databinding.DialogCollectSearchBinding
import com.sw.inbound.databinding.LayoutEmptySearchBinding
import com.sw.inbound.model.response.SearchGoodsInfo
import com.sw.inbound.objbox.Food
import com.sw.inbound.objbox.ObjectBox
import com.sw.inbound.utils.ext.addOnActionSearchListener
import com.sw.inbound.utils.ext.hideKeyboard
import com.sw.inbound.utils.ext.toast
import com.sw.inbound.viewmodel.ReceiptViewModel
import io.objectbox.Box
import io.objectbox.kotlin.boxFor
@SuppressLint("NotifyDataSetChanged")
class CollectSearchDialog(
context: Context
) : BaseDialog(context) {
private lateinit var inflater: LayoutInflater
private var emptyBinding: LayoutEmptySearchBinding? = null
private lateinit var binding: DialogCollectSearchBinding
override fun getRootView(): View {
inflater = LayoutInflater.from(context)
binding = DialogCollectSearchBinding.inflate(inflater)
return binding.root
}
private var activity: FoodCollectionActivity? = null
override fun initView() {
activity = getFoodCollectionActivity()
binding.ivClose.setOnClickListener { dismiss() }
binding.ivGoodsSearch.setOnClickListener {
val searchName = binding.etInputGoods.text.toString().trim()
if (searchName.isBlank()) {
activity?.toast("请输入物品名称")
return@setOnClickListener
}
getCollectGoods(searchName)
}
binding.etInputGoods.let { v ->
v.addOnActionSearchListener {
val searchName = binding.etInputGoods.text.toString().trim()
if (searchName.isBlank()) {
activity?.toast("请输入物品名称")
return@addOnActionSearchListener
}
getCollectGoods(searchName)
}
v.addTextChangedListener {
if (it.isNullOrBlank()) {
getCollectGoods()
}
}
}
binding.btnConfirm.setOnClickListener {
if (clickIndex == -1) {
dismiss()
return@setOnClickListener
}
deleteGoods(clickIndex) {
it.postDelayed({dismiss()}, 500)
}
}
binding.rvSearch.let {
it.layoutManager = GridLayoutManager(context, 2, LinearLayoutManager.VERTICAL, false)
it.adapter = searchAdapter
}
binding.refreshLayout.let {
it.setEnableRefresh(true)
it.setEnableLoadMore(false)
it.setOnRefreshListener {
pageNo = 1
val searchName = binding.etInputGoods.text.toString().trim()
getCollectGoods(searchName)
}
//it.setOnLoadMoreListener {
// searchGoods()
//}
}
getCollectGoods()
//binding.root.clickWithDebounce {
// Thread {
// FoodModule.initDefFoodData(activity!!) {
// activity?.runOnUiThread {
// activity?.toast("添加完成")
// getCollectGoods()
// }
// }
// }.start()
//}
}
private var pageNo = 1
private fun loadEmptyView() {
binding.rvSearch.layoutManager =
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
if (emptyBinding == null) {
emptyBinding = LayoutEmptySearchBinding.inflate(inflater, binding.rvSearch, false)
}
emptyBinding?.tvContent?.text = "请检查物品名称,或稍后重新搜索"
emptyBinding?.root?.let { layout ->
layout.setOnClickListener { layout.hideKeyboard() }
searchAdapter.stateView = layout
}
}
private var clickIndex = -1
private val list: MutableList<SearchGoodsInfo.Record> = mutableListOf()
private val searchAdapter by lazy {
CollectSearchAdapter(list).apply {
isStateViewEnable = true
//setOnItemClickListener { _, _, position ->
// this@CollectSearchDialog.clickIndex = position
// if (list[position].isSelected) {
// return@setOnItemClickListener
// }
// list.forEach { item ->
// item.isSelected = false
// }
// list[position].isSelected = true
// notifyDataSetChanged()
//}
addOnItemChildClickListener(R.id.ivDelete) { _, _, position ->
this@CollectSearchDialog.clickIndex = position
list.forEach { item ->
item.isSelected = false
}
list[position].isSelected = true
notifyDataSetChanged()
deleteGoods(position)
}
}
}
private fun deleteGoods(position: Int, deleteAction:()-> Unit = {}) {
WarnDialog(
context = context,
content = "请确认是否删除物品:${list[position].goodsName}",
confirmBlock = {
Thread {
activity?.runOnUiThread {
Loading.show(activity!!)
}
val name = list[position].goodsName
val filterIdList = box?.all?.filter { it.name == name }?.map { it.id }
ObjectBox.boxStore.runInTx {
box?.removeByIds(filterIdList)
}
activity?.runOnUiThread {
Loading.dismiss()
//list.remove(list[position])
//searchAdapter.notifyDataSetChanged()
searchAdapter.removeAt(position)
activity?.toast("删除成功")
if (list.isEmpty()) {
loadEmptyView()
}
deleteAction()
}
}.start()
}).show()
}
private fun finishRefresh() {
binding.refreshLayout.let {
if (pageNo == 1) {
it.finishRefresh(500)
} else {
it.finishLoadMore(500)
}
}
}
private var box: Box<Food>? = null
@SuppressLint("NotifyDataSetChanged")
private fun getCollectGoods(searchName: String? = null) {
if (box == null) {
box = ObjectBox.boxStore.boxFor(Food::class)
}
list.clear()
var queryList = box?.all?.distinctBy { it.name }
if (searchName.isNullOrBlank().not()) {
queryList = queryList?.filter { it.name?.contains(searchName!!) == true }
}
queryList?.forEach {
list.add((SearchGoodsInfo.Record(goodsName = it.name)))
}
binding.rvSearch.layoutManager =
GridLayoutManager(activity, 2, LinearLayoutManager.VERTICAL, false)
searchAdapter.notifyDataSetChanged()
if (list.isEmpty()) {
loadEmptyView()
}
finishRefresh()
binding.root.hideKeyboard()
}
// fun test() {
// val goodsName = "多宝鱼,黑鱼(整条鱼),鸡腿肉"
// activity?.searchGoodsInfoList(goodsName = goodsName) { items->
// items.forEach { goods ->
// box?.all?.filter {
// it.name?.contains(goods.goodsName?:"-") == true
// }?.forEach {
// activity?.runOnUiThread {
// activity?.toast(goods.goodsName)
// }
// it.name = goods.goodsName + goods.goodsCode
// box?.put(it)
// }
// }
//// activity?.runOnUiThread {
//// activity?.toast("已保存")
//// }
// }
// }
}
@@ -0,0 +1,95 @@
package com.sw.inbound.dialog;
import android.app.Dialog;
import android.content.Context;
import android.content.res.Resources;
import android.os.Build;
import android.util.DisplayMetrics;
import android.view.Gravity;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.TextView;
import com.sw.inbound.R;
/**
* 加载进度条弹窗
*/
public class CustomDialog extends Dialog {
/**
* 宽高由布局文件中指定(但是最底层的宽度无效,可以多嵌套一层解决)
*/
public CustomDialog(Context context, View layout, int style) {
super(context, style);
setContentView(layout);
Window window = getWindow();
WindowManager.LayoutParams params = window.getAttributes();
params.gravity = Gravity.CENTER;
window.setAttributes(params);
}
/**
* 宽高由该方法的参数设置
*/
public CustomDialog(Context context, int width, int height, View layout,
int style) {
super(context, style);
// 设置内容
setContentView(layout);
// 设置窗口属性
Window window = getWindow();
WindowManager.LayoutParams params = window.getAttributes();
// 设置宽度、高度、密度、对齐方式
float density = getDensity(context);
params.width = (int) (width * density);
params.height = (int) (height * density);
params.gravity = Gravity.CENTER;
window.setAttributes(params);
}
/**
* 获取显示密度
*
* @param context
* @return
*/
public float getDensity(Context context) {
Resources res = context.getResources();
DisplayMetrics dm = res.getDisplayMetrics();
return dm.density;
}
private void fullScreenImmersive(View view) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
int uiOptions = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_FULLSCREEN;
view.setSystemUiVisibility(uiOptions);
}
}
@Override
public void show() {
this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
super.show();
// fullScreenImmersive(getWindow().getDecorView());
// this.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
}
}
@@ -0,0 +1,48 @@
package com.sw.inbound.dialog
object DialogManager {
private var dialogList: MutableList<BaseDialog> = mutableListOf()
fun addDialog(dialog: BaseDialog) {
if (dialogList.contains(dialog).not()) {
val className = dialog.javaClass.simpleName
val item = dialogList.firstOrNull { it.javaClass.simpleName == className }
item?.dismiss()
dialogList.add(dialog)
}
}
fun removeDialog(dialog: BaseDialog) {
dialogList.remove(dialog)
}
fun getDialogList(): MutableList<BaseDialog> {
return dialogList
}
fun dismissAll() {
dialogList.forEach {
it.dismiss()
}
}
fun dismissAllExcept(dialog: BaseDialog) {
dialogList.forEach {
if (it != dialog) {
it.dismiss()
}
}
}
fun contains(vararg className: String): Boolean {
val item = dialogList.firstOrNull { className.contains(it.javaClass.simpleName) }
return item != null
}
fun hasShowDialog(): Boolean {
val item = dialogList.firstOrNull { it.isShowing }
return item != null
}
}
@@ -0,0 +1,78 @@
package com.sw.inbound.dialog
import android.content.Context
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.PopupWindow
import androidx.core.graphics.drawable.toDrawable
import androidx.core.view.updateLayoutParams
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.sw.inbound.R
import com.sw.inbound.adapter.DropdownAdapter
import com.sw.inbound.model.response.DictType
import com.sw.inbound.utils.ext.dp
class DropdownPopup(
var context: Context,
var list: MutableList<DictType>,
var popWidth: Int = 280.dp,
var popHeight: Int = 284.dp,
var onPopupItemClick: (DictType) -> Unit
) : PopupWindow() {
init {
// 初始化PopupWindow
val popupView = LayoutInflater.from(context).inflate(R.layout.layout_popup_dropdown, null)
contentView = popupView
width = ViewGroup.LayoutParams.WRAP_CONTENT
height = ViewGroup.LayoutParams.WRAP_CONTENT
// 设置透明背景,避免覆盖圆角
setBackgroundDrawable(Color.TRANSPARENT.toDrawable())
isOutsideTouchable = true
isFocusable = true
// 设置动画样式
// animationStyle = R.style.PopupAnimation
// 初始化视图和事件
initViews(popupView)
}
private fun initViews(view: View) {
// 获取关闭按钮并设置点击事件
view.findViewById<RecyclerView>(R.id.rvWarehouse)?.let {
it.updateLayoutParams {
width = popWidth
height = popHeight
}
it.layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
it.adapter = DropdownAdapter(list).setOnItemClickListener { adapter, view, position ->
onPopupItemClick(list[position])
dismiss()
}
}
}
var arrowImage: ImageView? = null
var bgLayout: ViewGroup? = null
var defLayoutBgResId = R.drawable.bg_white_radius10_stroke2
override fun showAsDropDown(anchor: View?) {
super.showAsDropDown(anchor)
bgLayout?.setBackgroundResource(R.drawable.bg_white_stroke_blue)
arrowImage?.setImageResource(R.mipmap.ic_triangle_up)
}
override fun dismiss() {
super.dismiss()
bgLayout?.setBackgroundResource(defLayoutBgResId)
arrowImage?.setImageResource(R.mipmap.ic_triangle_down)
}
}
@@ -0,0 +1,147 @@
package com.sw.inbound.dialog
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager
import com.sw.inbound.activity.GoodsListActivity
import com.sw.inbound.adapter.RecognizeAdapter
import com.sw.inbound.databinding.DialogGoodsRecognizeBinding
import com.sw.inbound.databinding.LayoutEmptyRecognizeBinding
import com.sw.inbound.model.response.SearchGoodsInfo
import com.sw.inbound.utils.ext.gone
import com.sw.inbound.utils.ext.toast
import com.sw.inbound.utils.ext.visible
import java.io.File
@SuppressLint("NotifyDataSetChanged")
class GoodsRecognizeDialog(
context: Context,
var goodsFile: File? = null,
var goodsName: String? = null,
var list: MutableList<SearchGoodsInfo.Record>,
//var closeCallback:()->Unit,
var findGoods: () -> Unit
) : BaseDialog(context) {
private lateinit var binding: DialogGoodsRecognizeBinding
private lateinit var inflater: LayoutInflater
override fun getRootView(): View {
inflater = LayoutInflater.from(context)
binding = DialogGoodsRecognizeBinding.inflate(inflater)
return binding.root
}
// override fun show() {
// super.show()
// binding.ivGoods.load(goodsFile)
// binding.tvGoods.text = goodsName
// }
private val recognizeAdapter by lazy {
RecognizeAdapter(list).apply {
// isStateViewEnable = true
setOnItemClickListener { adapter, view, position ->
if (list[position].isSelected) {
showGoodsStoreDialog(position)
return@setOnItemClickListener
}
list.forEach { item ->
item.isSelected = false
}
list[position].isSelected = true
notifyDataSetChanged()
showGoodsStoreDialog(position)
}
}
}
private fun showGoodsStoreDialog(position: Int) {
GoodsStoreDialog(
context = context,
funcType = 0,
goods = list[position]
) {
activity?.addGoods(it)
//activity?.startTime = System.currentTimeMillis()
dismiss()
}.show()
}
private var activity: GoodsListActivity? = null
override fun initView() {
activity = getReceiptActivity()
binding.rvRecognize.let {
it.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
it.adapter = recognizeAdapter
}
val isEmpty = list.isEmpty()
if (isEmpty) {
// loadEmptyView()
binding.rvRecognize.gone()
binding.flEmptyLayout.visible()
} else {
binding.rvRecognize.visible()
binding.flEmptyLayout.gone()
}
// if (recognizeAdapter.list.isEmpty()) {
// loadEmptyView()
// }
binding.ivClose.setOnClickListener {
dismiss()
activity?.delayRecognizeFood()
//activity?.startTime = System.currentTimeMillis()
}
binding.btnManuallyFind.setOnClickListener {
findGoods()
//dismiss()
}
binding.btnAgainRecognize.setOnClickListener {
//dismiss()
activity?.againRecognizeFood { goodsList ->
//binding.rvRecognize.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
if (goodsList.isEmpty()) {
// loadEmptyView()
binding.rvRecognize.gone()
binding.flEmptyLayout.visible()
return@againRecognizeFood
}
list.clear()
list.addAll(goodsList)
// recognizeAdapter.isStateViewEnable = false
recognizeAdapter.notifyDataSetChanged()
binding.rvRecognize.visible()
binding.flEmptyLayout.gone()
}
}
}
// private var emptyBinding: LayoutEmptyRecognizeBinding? = null
private fun loadEmptyView() {
// if (emptyBinding == null) {
// emptyBinding = LayoutEmptyRecognizeBinding.inflate(inflater, binding.flEmptyLayout, false)
// }
// binding.flEmptyLayout.removeAllViews()
// emptyBinding?.root.let {
// binding.flEmptyLayout.addView(it)
// }
binding.rvRecognize.gone()
binding.flEmptyLayout.visible()
// emptyBinding?.root?.let {
// recognizeAdapter.isStateViewEnable = true
// recognizeAdapter.stateView = it
// }
}
override fun dismiss() {
super.dismiss()
activity?.run {
isShowRecognizeDialog = true
goodsRecognizeDialog = null
}
}
}
@@ -0,0 +1,176 @@
package com.sw.inbound.dialog
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import com.sw.inbound.activity.GoodsListActivity
import com.sw.inbound.adapter.GoodsSearchAdapter
import com.sw.inbound.databinding.DialogGoodsSearchBinding
import com.sw.inbound.databinding.LayoutEmptySearchBinding
import com.sw.inbound.model.response.SearchGoodsInfo
import com.sw.inbound.utils.ext.addOnActionSearchListener
import com.sw.inbound.utils.ext.hideKeyboard
import com.sw.inbound.utils.ext.toast
class GoodsSearchDialog(
context: Context
) : BaseDialog(context) {
private lateinit var inflater: LayoutInflater
private var emptyBinding: LayoutEmptySearchBinding? = null
private lateinit var binding: DialogGoodsSearchBinding
override fun getRootView(): View {
inflater = LayoutInflater.from(context)
binding = DialogGoodsSearchBinding.inflate(inflater)
return binding.root
}
private var activity: GoodsListActivity? = null
override fun initView() {
activity = getReceiptActivity()
binding.ivClose.setOnClickListener { dismiss() }
binding.ivGoodsSearch.setOnClickListener { searchGoods() }
binding.etInputGoods.let { v ->
v.addOnActionSearchListener {
searchGoods()
}
}
binding.btnAddGoods.setOnClickListener {
AddGoodsDialog(
context = context,
cancelBlock = {},
confirmBlock = { goodsName->
pageNo = 1
binding.etInputGoods.setText(goodsName)
searchGoods()
}
).apply {
setOnDismissListener {
//activity?.bindCamera()
}
show()
}
}
binding.btnConfirm.setOnClickListener {
if (list.isEmpty()) {
context.toast("请搜索物品")
return@setOnClickListener
}
if (clickIndex == -1) {
context.toast("请选择物品")
return@setOnClickListener
}
GoodsStoreDialog(
context = context,
funcType = 1,
goods = list[clickIndex]
) {
activity?.addGoods(it)
//activity?.startTime = System.currentTimeMillis()
activity?.goodsRecognizeDialog?.dismiss()
dismiss()
}.show()
}
binding.rvSearch.let {
it.layoutManager = GridLayoutManager(context, 2, LinearLayoutManager.VERTICAL, false)
it.adapter = searchAdapter
}
binding.refreshLayout.let {
it.setEnableRefresh(true)
it.setEnableLoadMore(false)
it.setOnRefreshListener {
pageNo = 1
searchGoods()
}
it.setOnLoadMoreListener {
searchGoods()
}
}
loadEmptyView()
}
private var pageNo = 1
companion object {
private const val PAGE_SIZE = 10
}
@SuppressLint("NotifyDataSetChanged")
private fun searchGoods() {
val searchName = binding.etInputGoods.text.toString().trim()
if (searchName.isBlank()) {
activity?.toast("请输入物品名称")
finishRefresh()
return
}
activity?.searchGoods(
searchName = searchName,
pageNo = pageNo,
pageSize = PAGE_SIZE
) { records ->
finishRefresh()
binding.etInputGoods.hideKeyboard()
if (pageNo == 1) {
list.clear()
}
list.addAll(records)
if (list.isEmpty()) {
loadEmptyView()
return@searchGoods
}
val enableLoadMore = records.size >= PAGE_SIZE
binding.refreshLayout.setEnableLoadMore(enableLoadMore)
if (enableLoadMore) {
pageNo++
}
binding.rvSearch.layoutManager =
GridLayoutManager(context, 2, GridLayoutManager.VERTICAL, false)
searchAdapter.notifyDataSetChanged()
}
}
private fun loadEmptyView() {
binding.rvSearch.layoutManager =
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
if (emptyBinding == null) {
emptyBinding = LayoutEmptySearchBinding.inflate(inflater, binding.rvSearch, false)
}
emptyBinding?.root?.let { layout ->
layout.setOnClickListener { layout.hideKeyboard() }
searchAdapter.stateView = layout
}
}
private var clickIndex = -1
private val list: MutableList<SearchGoodsInfo.Record> = mutableListOf()
private val searchAdapter by lazy {
GoodsSearchAdapter(list).apply {
isStateViewEnable = true
setOnItemClickListener { adapter, view, position ->
this@GoodsSearchDialog.clickIndex = position
if (list[position].isSelected) {
return@setOnItemClickListener
}
list.forEach { item ->
item.isSelected = false
}
list[position].isSelected = true
notifyDataSetChanged()
}
}
}
private fun finishRefresh() {
binding.refreshLayout.let {
if (pageNo == 1) {
it.finishRefresh(500)
} else {
it.finishLoadMore(500)
}
}
}
}
@@ -0,0 +1,471 @@
package com.sw.inbound.dialog
import android.content.Context
import android.graphics.Typeface
import android.net.Uri
import android.text.InputType
import android.text.SpannableStringBuilder
import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan
import android.view.LayoutInflater
import android.view.View
import android.widget.EditText
import android.widget.LinearLayout
import androidx.camera.view.PreviewView
import androidx.core.graphics.toColorInt
import androidx.core.view.updateLayoutParams
import androidx.core.widget.addTextChangedListener
import coil.load
import coil.transform.RoundedCornersTransformation
import com.sw.inbound.utils.ext.appendText
import com.sw.inbound.utils.ext.buildSpannableString
import com.sw.inbound.R
import com.sw.inbound.activity.GoodsListActivity
import com.sw.inbound.databinding.DialogGoodsStoreBinding
import com.sw.inbound.databinding.LayoutImagePreviewBinding
import com.sw.inbound.databinding.LayoutTakePhotoBinding
import com.sw.inbound.model.response.DictType
import com.sw.inbound.model.response.GoodsInfo
import com.sw.inbound.model.response.SearchGoodsInfo
import com.sw.inbound.objbox.Food
import com.sw.inbound.objbox.FoodModule
import com.sw.inbound.objbox.ObjectBox
import com.sw.inbound.sdk.SensorScaleUtils
import com.sw.inbound.utils.BitmapCropper
import com.sw.inbound.utils.BitmapSaver
import com.sw.inbound.utils.CameraUtils
import com.sw.inbound.utils.ImageUtil
import com.sw.inbound.utils.ext.dp
import com.sw.inbound.utils.ext.gone
import com.sw.inbound.utils.ext.roundedDecimalPlace
import com.sw.inbound.utils.ext.toast
import com.sw.inbound.utils.ext.visible
import com.sw.inbound.utils.removeTrailingZeros
import io.objectbox.Box
import io.objectbox.kotlin.boxFor
import timber.log.Timber
import java.io.File
import kotlin.Int
import kotlin.math.abs
import kotlin.math.min
class GoodsStoreDialog(
context: Context,
var goods: SearchGoodsInfo.Record,
var funcType: Int,
var cancelBlock: () -> Unit = {},
var confirmBlock: (GoodsInfo) -> Unit = {},
) : BaseDialog(context) {
private lateinit var binding: DialogGoodsStoreBinding
private lateinit var inflater: LayoutInflater
override fun getRootView(): View {
inflater = LayoutInflater.from(context)
binding = DialogGoodsStoreBinding.inflate(inflater)
return binding.root
}
private val dropdownList: MutableList<DictType> = mutableListOf()
private var dropdownPopup: DropdownPopup? = null
private var realWeight: Int = 0
private var activity: GoodsListActivity? = null
override fun initView() {
activity = getReceiptActivity()
if (goods.unitVoList.isNullOrEmpty()) {
activity?.toast("未匹配到合适的入库单位")
dismiss()
return
}
dropdownList.clear()
goods.unitVoList!!.forEach {
dropdownList.add(DictType(id = it.businessUnitId, value = it.purchaseUnitName))
}
//binding.etProcurementCount.isEnabled = false
binding.etGoodsName.run {
setText(goods.goodsName)
isEnabled = false
}
//binding.ivGoodsImage.load(goods.relativeUrl)
addChildToContainer()
//flContainer
binding.btnCancel.setOnClickListener {
cancelBlock()
dismiss()
}
binding.btnConfirm.setOnClickListener {
confirmInputData()
}
binding.tvTitle.setOnClickListener {
SensorScaleUtils.startScale(autoScale = true)
}
// binding.ivAddUnit.setOnClickListener {
// AddProcurementUnitDialog(context).show()
// }
binding.flUnitDropdown.setOnClickListener { v ->
if (dropdownPopup == null) {
dropdownPopup = DropdownPopup(
context = context,
list = dropdownList,
popWidth = 428.dp,
popHeight = min(dropdownList.size, 4) * 71.dp
) { dictType ->
try {
binding.tvProcurementUnit.run {
text = dictType.value
tag = dictType.id
}
val unit = dictType.value
binding.tvCountUnit.text = unit
binding.etProcurementCount.let {
//it.isEnabled = true
it.inputType = if (unit == "千克" || unit == "公斤" || unit == "") {
InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL
} else {
InputType.TYPE_CLASS_NUMBER
}
}
val first = goods.unitVoList!!.first { it.businessUnitId == dictType.id }
val stringBuilder =
StringBuilder().append("1").append(dictType.value).append("=")
.append(first.purchaseValue!!.toInt().toString())
.append(goods.kcUnitName).append("").append("1")
.append(goods.kcUnitName).append("=").append(first.consumeValue)
.append("")
binding.tvUnitCalculate.text = stringBuilder.toString()
//val count = getUnitCount(dictType.id!!)
//if (count.isBlank() || "0" == count) {
// //context.toast("入库数量不能为0")
// return@DropdownPopup
//}
//binding.etProcurementCount.let {
// it.setText(count)
// it.setSelection(it.length())
//}
} catch (e: Exception) {
e.printStackTrace()
}
}.also {
it.arrowImage = binding.ivDropdown
it.bgLayout = binding.flUnitDropdown
}
}
dropdownPopup?.showAsDropDown(v)
}
binding.btnClearZero.setOnClickListener {
SensorScaleUtils.tare()
}
//addPriceInputListener(binding.etProcurementPrice) {
// val countText = binding.etProcurementCount.text.trim().toString()
// val count = if (countText.isNotBlank()) countText.toInt() else 0
// val amount = (it * count).toFormattedString(2)
// binding.etProcurementAmount.setText(amount)
//}
addPriceInputListener(binding.etProcurementAmount) {
val countText = binding.etProcurementCount.text.trim().toString()
val count = if (countText.isNotBlank()) countText.toDouble() else 0.toDouble()
if (count == 0.toDouble()) {
//context.toast("入库数量不能为0")
return@addPriceInputListener
}
val price = (it / count).roundedDecimalPlace(2)
binding.tvProcurementPrice.text = price.removeTrailingZeros()
}
binding.etProcurementCount.run {
addTextChangedListener(onTextChanged = { it, _, _, _ ->
try {
if (it.isNullOrBlank()) {
if (it.isNullOrBlank()) {
binding.tvProcurementPrice.text = ""
}
return@addTextChangedListener
}
val amountText = binding.etProcurementAmount.text.toString().trim()
if (amountText.isBlank()) {
return@addTextChangedListener
}
val input = it.toString()
val count = input.toDouble()
if (count == 0.toDouble()) {
//context.toast("入库数量不能为0")
return@addTextChangedListener
}
val amount = amountText.toDouble()
val price = (amount / count).roundedDecimalPlace(2)
binding.tvProcurementPrice.text = price.removeTrailingZeros()
} catch (e: Exception) {
e.printStackTrace()
}
})
}
addWeightListener()
}
private fun addWeightListener() {
activity?.readWeightInfo { weight ->
if (abs(this.realWeight - weight) < 10 && binding.etProcurementCount.text.isNotBlank()) {
//两次数据差在10克内,认为无变化,不进行操作
return@readWeightInfo
}
this.realWeight = weight
binding.tvTotalWeight.text = getSpannable(weight)
if (binding.etProcurementCount.isFocused) {
return@readWeightInfo
}
val unitTag = binding.tvProcurementUnit.tag
if (unitTag == null || unitTag.toString().isBlank()) {
return@readWeightInfo
}
val id = unitTag.toString()
val count = getUnitCount(id)
if (count.isBlank() || "0" == count) {
binding.etProcurementCount.setText("")
return@readWeightInfo
}
val currentCount = binding.etProcurementCount.text.toString().trim()
if (currentCount == count) {
return@readWeightInfo
}
binding.etProcurementCount.let {
it.setText(count)
it.setSelection(it.length())
//it.isEnabled = true
}
}
}
private fun addPriceInputListener(editText: EditText, action: (Double) -> Unit) {
editText.run {
addTextChangedListener(onTextChanged = { it, _, _, _ ->
try {
if (it.isNullOrBlank()) {
return@addTextChangedListener
}
val input = it.toString()
if (input.startsWith(".")) {
setText("0.")
setSelection(length())
} else if (input.contains(".")) {
val index = input.indexOf(".")
if (input.length - index > 3) {
setText(input.substring(0, index + 3))
setSelection(length())
}
}
val value = if (input == "0.") 0.0 else input.toDouble()
action(value)
} catch (e: Exception) {
e.printStackTrace()
}
})
}
}
private fun getSpannable(num: Int): SpannableStringBuilder {
return buildSpannableString {
appendText(
num.toString(),
ForegroundColorSpan("#FF141428".toColorInt()),
StyleSpan(Typeface.BOLD)
)
appendText(
"", ForegroundColorSpan("#FF000033".toColorInt()), StyleSpan(Typeface.BOLD)
)
}
}
private fun confirmInputData() {
if (funcType == 1 && imageUri == null) {
context.toast("请采集图片")
return
}
val unit = binding.tvProcurementUnit.text.trim().toString()
if (unit.isBlank()) {
context.toast("请选择入库单位")
return
}
val countText = binding.etProcurementCount.text.trim().toString()
if (countText.isBlank() || "0" == countText) {
context.toast("请录入入库数量")
return
}
val amountText = binding.etProcurementAmount.text.trim().toString()
if (amountText.isBlank()) {
context.toast("请录入入库金额")
return
}
val priceText = binding.tvProcurementPrice.text.trim().toString()
if (priceText.isBlank()) {
context.toast("请录入入库单价")
return
}
if (realWeight <= 0) {
context.toast("物品重量需大于0")
return
}
val count = countText.toDoubleOrNull() ?: 0.toDouble()
if (count == 0.toDouble()) {
context.toast("入库数量不能为0")
return
}
val price = priceText.toDoubleOrNull() ?: 0.toDouble()
if (price == 0.toDouble()) {
context.toast("入库单价不能为0")
return
}
val totalPrice = amountText.toDoubleOrNull()
if (totalPrice == 0.toDouble()) {
context.toast("入库金额不能为0")
return
}
if (funcType == 1) {
collectGoodsVector()
}
val id = binding.tvProcurementUnit.tag.toString()
val unitVo = goods.unitVoList!!.first { it.businessUnitId == id }
activity?.uploadImage(goods.id!!, goods.goodsName!!, imageFile)
confirmBlock(
GoodsInfo(
goodId = goods.id,
goodName = goods.goodsName,
consumeValue = goods.purchaseValue,
//goodsPurId = binding.tvProcurementUnit.tag.toString(),
kcUnitId = binding.tvProcurementUnit.tag.toString(),
unitName = unit,
//收货接口-数量
receivedNum = count,
//收货接口-单价(含税)
recUnitPriceTaxIn = price,
//收货接口-金额(含税)
recPriceInItem = totalPrice,
goodsWeight = realWeight.toBigDecimal(),
isLocalGoods = true,
goodsCount = count,
goodsUnitPrice = price,
goodsPrice = totalPrice,
buyToInventoryValue = unitVo.buyToInventoryValue,
unitHint = binding.tvUnitCalculate.text.toString()
)
)
dismiss()
}
private fun getUnitCount(id: String): String {
val first = goods.unitVoList!!.first { it.businessUnitId == id }
val count = first.getPurchaseUnitValue(this.realWeight)
return count
}
private val cameraUtils: CameraUtils by lazy {
CameraUtils(activity!!)
}
override fun show() {
super.show()
if (funcType == 1) {
takePhotoBinding?.let {
cameraUtils.setPreviewController(previewView)
cameraUtils.bind()
it.llCameraFlag.run {
postDelayed({
gone()
}, 1000)
}
}
}
binding.root.postDelayed({
SensorScaleUtils.startScale()
}, 2600)
}
private var imageUri: Uri? = null
private val takePhotoCallback: (Uri) -> Unit = { uri ->
takePhotoBinding?.let {
imageUri = uri
it.flCameraPreview.gone()
it.cvImageBg.visible()
it.ivGoodsImage.load(uri)
}
}
private var previewView: PreviewView? = null
private var takePhotoBinding: LayoutTakePhotoBinding? = null
private var imagePreviewBinding: LayoutImagePreviewBinding? = null
private fun addChildToContainer() {
val inflater = LayoutInflater.from(context)
if (funcType == 1) {
//预览及拍照
takePhotoBinding = LayoutTakePhotoBinding.inflate(inflater, binding.flContainer, true)
activity?.unBindCamera()
setOnDismissListener {
cameraUtils.unbind()
}
cameraUtils.initCamera()
takePhotoBinding?.let {
previewView = it.previewView
it.flCameraPreview.visible()
it.cvImageBg.gone()
it.btnStartPreview.setOnClickListener { _ ->
it.cvImageBg.gone()
it.flCameraPreview.visible()
}
it.btnTakePhoto.setOnClickListener { _ ->
cameraUtils.takePhoto(takePhotoCallback)
}
}
return
}
//只显示原始图片
imagePreviewBinding = LayoutImagePreviewBinding.inflate(inflater, binding.flContainer)
imagePreviewBinding?.imageView?.load(goods.relativeUrl) {
transformations(RoundedCornersTransformation(radius = 10.dp.toFloat()))
}
binding.flContainer.let {
it.setBackgroundResource(R.drawable.bg_white_radius10_stroke2)
it.updateLayoutParams<LinearLayout.LayoutParams> {
width = 488.dp
height = 356.dp
bottomMargin = 35.dp
}
}
}
private var imageFile: File?=null
private var box: Box<Food>? = null
private fun collectGoodsVector() {
try {
ImageUtil.uriToBitmap(context, imageUri!!)?.let { bitmap ->
val cropBitmap = BitmapCropper.cropCenter(
original = bitmap,
targetWidth = 1000,
targetHeight = 1300,
offsetX = 30,
offsetY = 100
)
val file = BitmapSaver.saveToAppFilesDir(
cropBitmap, context, "IMG_CROP_${System.currentTimeMillis()}.jpg"
)
imageFile = file
Timber.d("${this.javaClass.simpleName}-collectGoodsVector-裁剪bitmap保存文件路径:${file?.absolutePath}")
if (box == null) {
box = ObjectBox.boxStore.boxFor(Food::class)
}
Thread {
try {
val imageVector = FoodModule.bitmap2FloatArray(bitmap, true)
if (imageVector != null) {
ObjectBox.boxStore.runInTx {
box?.put(Food(name = goods.goodsName + goods.goodsCode, foodIdx = 0, foodVector = imageVector))
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}.start()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
@@ -0,0 +1,73 @@
package com.sw.inbound.dialog
import android.app.Activity
import android.app.Dialog
import android.content.Context
import android.graphics.Color
import android.view.LayoutInflater
import androidx.core.graphics.drawable.toDrawable
import com.sw.inbound.R
import com.sw.inbound.databinding.DialogLoadingBinding
import kotlin.let
import kotlin.run
object Loading {
private var dialog: LoadingDialog? = null
fun show(activity: Activity) {
try {
if (activity.isFinishing || activity.isDestroyed) {
return
}
if (dialog?.isShowing == true) {
dialog?.dismiss()
}
if (dialog == null) {
dialog = LoadingDialog(activity)
}
dialog?.show()
} catch (e: Exception) {
e.printStackTrace()
}
}
fun dismiss() {
try {
dialog?.let {
if (it.isShowing) {
it.dismiss()
}
}
dialog = null
} catch (e: Exception) {
e.printStackTrace()
}
}
}
class LoadingDialog(
context: Context,
message: String = "加载中……",
private var onDismiss: () -> Unit = {}
) : Dialog(context, R.style.LoadingDialog) {
init {
val binding = DialogLoadingBinding.inflate(LayoutInflater.from(context))
binding.tvMessage.text = message
setContentView(binding.root)
setCancelable(true)
setCanceledOnTouchOutside(true)
window?.run {
setBackgroundDrawable(Color.TRANSPARENT.toDrawable())
// setFlags(
// WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
// WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
// )
}
setOnDismissListener {
onDismiss()
}
}
}
@@ -0,0 +1,34 @@
//package com.sw.inbound.dialog
//
//import android.content.Context
//import android.view.LayoutInflater
//import android.view.View
//import com.sw.inbound.databinding.DialogNotReceiptWarnBinding
//import com.sw.inbound.utils.ext.dp
//
//class NotReceiptWarnDialog(
// context: Context,
// var cancelBlock: () -> Unit = {},
// var confirmBlock: () -> Unit = {},
//) : BaseDialog(context = context, defWidth = 960.dp, defHeight = 540.dp) {
//
// private lateinit var binding: DialogNotReceiptWarnBinding
// override fun getRootView(): View {
// val inflater = LayoutInflater.from(context)
// binding = DialogNotReceiptWarnBinding.inflate(inflater)
// return binding.root
// }
//
// override fun initView() {
// binding.btnCancel.setOnClickListener {
// cancelBlock()
// dismiss()
// }
// binding.btnConfirm.setOnClickListener {
// confirmBlock()
// dismiss()
// }
// }
//
//
//}
@@ -0,0 +1,37 @@
package com.sw.inbound.dialog
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import com.sw.inbound.databinding.DialogNotReceiptWarnBinding
import com.sw.inbound.databinding.DialogWarnBinding
import com.sw.inbound.utils.ext.dp
class WarnDialog(
context: Context,
var content:String,
var cancelBlock: () -> Unit = {},
var confirmBlock: () -> Unit = {},
) : BaseDialog(context = context, defWidth = 660.dp, defHeight = 360.dp) {
private lateinit var binding: DialogWarnBinding
override fun getRootView(): View {
val inflater = LayoutInflater.from(context)
binding = DialogWarnBinding.inflate(inflater)
return binding.root
}
override fun initView() {
binding.tvWarnContent.text = content
binding.btnCancel.setOnClickListener {
cancelBlock()
dismiss()
}
binding.btnConfirm.setOnClickListener {
confirmBlock()
dismiss()
}
}
}
@@ -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,105 @@
package com.sw.inbound.ext
import java.math.BigDecimal
import java.math.RoundingMode
import kotlin.math.pow
import kotlin.math.round
fun Float.toFormattedString(decimalPlaces: Int = 2): String {
return when (this) {
0f -> ""
else -> "%.${decimalPlaces}f".format(this) // 先格式化为固定位数
// .replace(Regex("\\.?0+$"), "") // 移除末尾的0
}
}
fun Double.toFormattedString(decimalPlaces: Int = 2): String {
return when (this) {
0.0 -> ""
else -> "%.${decimalPlaces}f".format(this)
// .replace(Regex("\\.?0+$"), "")
}
}
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 round(this * factor) / factor
}
private fun Float.roundToDouble(decimalPlaces: Int): Double {
val factor = 10.0.pow(decimalPlaces)
return round(this * factor) / factor
}
fun Float?.toSafeString(unitName: String?): String {
if (this == null || this == 0f) return ""
val decimalPlaces = getDecimalPlaces(unitName)
return "%.${decimalPlaces}f".format(this)
}
fun Double?.toSafeString(unitName: String?): String {
if (this == null || this == 0.0) return ""
val decimalPlaces = getDecimalPlaces(unitName)
return "%.${decimalPlaces}f".format(this)
}
fun Double?.toSafeDouble(unitName: String?): Double {
if (this == null || this == 0.0) return 0.0
val decimalPlaces = getDecimalPlaces(unitName)
return roundToDouble(decimalPlaces)
}
fun Double?.toSafeDouble(decimalPlaces: Int = 2): Double {
if (this == null || this == 0.0) return 0.0
return roundToDouble(decimalPlaces)
}
fun Double?.toSafeFloat(unitName: String?): Float {
if (this == null || this == 0.0) return 0f
val decimalPlaces = getDecimalPlaces(unitName)
return roundToDouble(decimalPlaces).toFloat()
}
fun Double?.toSafeFloat(decimalPlaces: Int = 2): Float {
if (this == null || this == 0.0) return 0f
return roundToDouble(decimalPlaces).toFloat()
}
fun Float?.toSafeFloat(decimalPlaces: Int = 2): Float {
if (this == null || this == 0f) return 0f
return roundToDouble(decimalPlaces).toFloat()
}
/**
* 通过单位判断小数
*/
private fun getDecimalPlaces(unitName: String?): Int {
val decimalPlaces = when (unitName) {
"", "公斤", "", "千克" -> 2
else -> 0
}
return decimalPlaces
}
fun BigDecimal.toFormattedString(): String {
return this.setScale(2, RoundingMode.HALF_UP).toString()
}
fun Int.toFormattedString(): String {
return this.toString()
}
@@ -0,0 +1,104 @@
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) {
null -> ""
else -> "%.${decimalPlaces}s".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(maxFractionDigits: Int = 2): Boolean {
if (isEmpty()) return false
if (startsWith(".")) return false
// 处理前导零的情况
if (length > 1 && startsWith("0")) {
// 允许 "0" 和 "0.x" 但不允许 "00", "01", "00.1" 等
if (!startsWith("0.")) {
return false
}
}
// 构建动态正则表达式
val decimalRegex = when {
maxFractionDigits <= 0 -> Regex("^\\d+$") // 不允许小数
else -> Regex("^\\d+\\.?\\d{0,${maxFractionDigits}}$")
}
return matches(decimalRegex)
}
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,157 @@
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 goodType: String? = null,
/**
* 存储方式
*/
var storageType: String? = null,
/**
* 净材率
*/
var netRate: Float? = null,
/**
* 库存单位id
*/
var unitId: String? = null,
/**
* 采购单位
*/
var purchaseUnit: String? = null,
/**
* 单位转换值
*/
var purchaseValue: Float = 0F,
//var purchaseValueStr: String = "",
/**
* 采购单价
*/
var purchasePrice: BigDecimal? = null,
/**
* 图片url
*/
var relativeUrl: String? = null,
/**
* 库存与克的转换值
*/
val consumeValue: String = ""
) {
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.toString() }?.value ?: ""
}
val netRateStr: String
get() {
return if (netRate == null || netRate == 0f) {
""
} else {
netRate!!.toFormattedString(1)
}
}
val unitIdStr: String
get() {
return GlobalData.unitTypeList.find { it.id == unitId.toString() }?.value ?: ""
}
val unitIdStrPlace: String
get() {
return if (unitIdStr.isEmpty()) "-" else unitIdStr
}
val purchasePriceStr: String
get() {
if (purchasePrice == null) {
return ""
}
return purchasePrice!!.toFormattedString()
}
val purchaseUnitStr: String
get() {
if (purchaseUnit == null) {
return ""
}
return GlobalData.unitTypeList.find { it.id == purchaseUnit.toString() }?.value ?: ""
}
fun hasNullField(): String? {
if (goodName == null || goodName!!.isEmpty())
return "请录入物品名称"
if (goodType.isNullOrBlank() || goodType == "0") {
return "请选择物品类型"
}
if (storageType.isNullOrBlank()) {
return "请选择存储方式"
}
if (netRate == null || netRate == 0f) {
return "请录入净材率"
}
if (unitId.isNullOrBlank()) {
return "请选择库存单位"
}
if (purchaseUnit.isNullOrBlank()) {
return "请选择采购单位"
}
if (purchaseValue == null || purchaseValue == 0f) {
return "请输入采购单位对应的库存数量"
}
if (purchasePrice == null || purchasePrice!!.toFloat() <= 0) {
return "请输入单价"
}
if (consumeValue.isEmpty()) {
return "请输入库存单位对应的消耗值"
}
return null
}
}
@@ -0,0 +1,50 @@
package com.sw.inbound.model.request
import com.sw.inbound.GlobalData
/**
* 物品采购单位入参
*/
data class GoodsPurchaseUnitParam(
val goodsId: Int = 0,// 物品id integer ,
val purchaseUnit: Int = 0, // 采购单位 integer ,
val unitId: Int = 0, // 库存单位id integer,
val purchaseValue: Float = 0f, // 单位转换值 float
val purchaseValueStr: String = "", // 单位转换值
/**
* 库存与克的转换值
*/
var consumeValue: String = "",
) {
val unitIdStr: String
get() {
if (unitId == 0) return ""
return GlobalData.unitTypeList.find { it.id == unitId.toString() }?.value ?: ""
}
val purchaseUnitStr: String
get() {
if (purchaseUnit == 0) {
return ""
}
return GlobalData.unitTypeList.find { it.id == purchaseUnit.toString() }?.value ?: ""
}
fun hasNullField(): String? {
if (goodsId == null || goodsId == 0)
return "物品信息异常"
if (unitId == null || unitId == 0) {
return "请选择库存单位"
}
if (purchaseUnit == null || purchaseUnit == 0) {
return "请选择采购单位"
}
if (purchaseValue == null || purchaseValue == 0f) {
return "请输入采购单位对应的库存数量"
}
if (consumeValue.isEmpty()) {
return "请输入库存单位对应的消耗值"
}
return null
}
}
@@ -0,0 +1,15 @@
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,177 @@
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.ext.toSafeString
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: String = "",
/**
* 库存单位
*/
var kcUnitId: String? = "",
/**
* 入库数量
*/
var goodsCount: Double = 0.0,
// 用于数量输入框显示
var goodsCountTemp: String = "",
/**
* 入库单价
*/
var goodsUnitPrice: Double = 0.0,
// 单价 输入框显示
var goodsUnitPriceTemp: String = "",
/**
* 入库金额
*/
var goodsPrice: Double = 0.0,
// 用于界面显示的金额
var goodsPriceTemp: String = "",
/**
* (物品列表里 businessUnitId字段的值)
*/
var goodPurId: String?="",
/**
* 仓库id
*/
var warehouseId: String?="",
/**
* 采购库存转换关系
*/
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,
// 库存单位与克的转换值
val consumeValue: String = ""
) : Parcelable {
val warehouseNameStr: String
get() {
if (warehouseId.isNullOrBlank()) {
return ""
}
return GlobalData.warehouseTypeList.find { it.id == warehouseId.toString() }?.value ?: ""
// if (warehouseName == null) {
// return ""
// }
// return warehouseName!!
}
val goodsWeightStr: String
get() {
if (goodsWeight == null) return ""
// 小于10 kg 显示 g
if (goodsWeight!!.toInt() < 1) {
return goodsWeight!!.multiply(BigDecimal(1000)).toString()
}
return goodsWeight?.toFormattedString() ?: ""
}
val goodsWeightUnitStr: String
get() {
return if (goodsWeight == null || goodsWeight!!.toInt() < 1) {
""
} else {
"千克"
}
}
val goodsNameStr: String
get() {
if (goodsName == null) return "-"
return goodsName!!
}
// val unitDictTypeList: List<DictType>
// get() {
// return if (unitList == null) emptyList()
// else {
// unitList!!.filter { it.businessUnitId != null && it.purchaseUnitName != null }.map {
// DictType(it.businessUnitId?.toInt() ?: 0, 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.toSafeString(unitName)
}
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.isNullOrBlank() || goodsName == null || goodsName!!.isEmpty())
return "请选择物品"
if (warehouseId.isNullOrBlank()) {
return "请选择仓库"
}
if (goodPurId.isNullOrBlank()) {
return "请选择采购单位"
}
if (goodsCount == null || goodsCount == 0.0) {
return "请输入采购数量"
}
if (goodsUnitPrice == null || goodsUnitPrice == 0.0) {
return "请输入采购单价"
}
if (goodsPrice == null || goodsPrice == 0.0) {
return "请输入采购金额"
}
if (goodsWeight == null || goodsWeight!!.toFloat() <= 0) {
return "请放入要称重的物品"
}
return null
}
}
@@ -0,0 +1,95 @@
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: String,
/**
* 供应商id
*/
var supplierId: String? = null,
/**
*
* 物品信息 【全部收货使用】
*/
var receiveGoodsInfos: List<GoodsInfo> = emptyList<GoodsInfo>(),
/**
* 物品信息 【部分收货使用】
*/
var receiveGoodsInfoList: List<GoodsInfo> = emptyList<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: String = "",
/**
* 收货数量
*/
var receivedNum: Float = 0f,
/**
* 物品重量
*/
var goodsWeight: BigDecimal = BigDecimal(0),
// 已调整
var isAdjusted: Boolean = false
) : Parcelable, BaseBean() {
}
@@ -0,0 +1,24 @@
package com.sw.inbound.model.response
//data class ApiResponse<T>(
// val code: Int,
// val success: Boolean? = false,
// val message: String? = "",
// val result: T? = null,
// val timestamp: Long? = 0
//) {
// fun isSuccess(): Boolean = code == 200
//}
data class ApiResponse<T>(
val code: String,
val msg: String? = "",
val data: T? = null
) {
fun isSuccess(): Boolean = ("00000" == code)
val result: T?
get() = data
val message: String?
get() = msg
}
@@ -0,0 +1,5 @@
package com.sw.inbound.model.response
open class BaseBean() {
}
@@ -0,0 +1,13 @@
package com.sw.inbound.model.response
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
/**
* 字典类型 如仓库列表 物品类型 存储方式等
*/
@Parcelize
data class DictType(
val id: String?=null,
val value: String?=null
) : Parcelable
@@ -0,0 +1,72 @@
package com.sw.inbound.model.response
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
@Parcelize
data class EquipmentInfo(
@SerializedName("appPackageLocalUrl")
val appPackageLocalUrl: String? = "",
@SerializedName("appPackageUrl")
val appPackageUrl: String? = "",
@SerializedName("arcsoftActiveKey")
val arcsoftActiveKey: String? = "",
@SerializedName("arcsoftAppId")
val arcsoftAppId: String? = "",
@SerializedName("arcsoftSdkKey")
val arcsoftSdkKey: String? = "",
@SerializedName("arrayCross")
val arrayCross: Int? = 0,
@SerializedName("arrayMode")
val arrayMode: String? = "",
@SerializedName("arrayVertical")
val arrayVertical: Int? = 0,
@SerializedName("canteenId")
val canteenId: String? = "",
@SerializedName("canteenName")
val canteenName: String? = "",
@SerializedName("clientServerIp")
val clientServerIp: String? = "",
@SerializedName("createBy")
val createBy: String? = "",
@SerializedName("createTime")
val createTime: String? = "",
@SerializedName("customerName")
val customerName: String? = "",
@SerializedName("equipmentCode")
val equipmentCode: String? = "",
@SerializedName("equipmentName")
val equipmentName: String? = "",
@SerializedName("equipmentName_dictText")
val equipmentNameDictText: String? = "",
@SerializedName("id")
val id: String? = "",
@SerializedName("isSync")
val isSync: Int? = 0,
@SerializedName("mqName")
val mqName: String? = "",
@SerializedName("mqPassword")
val mqPassword: String? = "",
@SerializedName("orgCode")
val orgCode: String? = "",
@SerializedName("owningTrack")
val owningTrack: Int? = 0,
@SerializedName("owningTrackOrder")
val owningTrackOrder: Int? = 0,
@SerializedName("screenHtml")
val screenHtml: String? = "",
@SerializedName("serviceRequestAddress")
val serviceRequestAddress: String? = "",
@SerializedName("status")
val status: Int? = 0,
@SerializedName("sysOrgCode")
val sysOrgCode: String? = "",
@SerializedName("updateBy")
val updateBy: String? = "",
@SerializedName("updateTime")
val updateTime: String? = "",
@SerializedName("zhstServerIp")
val zhstServerIp: String? = ""
) : Parcelable
@@ -0,0 +1,267 @@
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.ext.toSafeString
import com.sw.inbound.model.response.SearchGoodsInfo.Record.UnitVo
import kotlinx.parcelize.Parcelize
import java.math.BigDecimal
/**
* 物品信息
*/
@Parcelize
data class GoodsInfo(
/**
* 数据id
*/
var id: String? = "",
/**
* 物品Id
*/
var goodId: String? = "",
/**
* 物品名称
*/
var goodName: String? = null,
/**
* 单价
*/
var recUnitPriceTaxIn: Double? = null,
var recUnitPriceTaxInBak: Double? = null,
// 单价 界面显示
var recUnitPriceTaxInTemp: String = "",
/**
* 新单价 部分收货需要
*/
var newRecUnitPriceTaxIn: Double? = null,
var newRecUnitPriceTaxInTemp: String = "",
/**
* 单位名称
*/
var unitName: String? = null,
var unitNameBak: String? = null,
/**
* 采购单位id
*/
var purchaseUnitId: String? = null,
/**
* 采购数量
*/
var receiveCount: Double? = null,
/**
* 金额
*/
var recPriceExItem: Double? = null,
// 金额 界面显示
var recPriceExItemTemp: String = "",
/**
* 仓库id
*/
var warehouseId: String? = null,
/**
* 仓库名称
*/
var warehouseName: String? = null,
/**
* 物品采购单位关系
*/
var goodsPurId: String? = "",
/**
* 采购库存转换值
*/
var purchaseValue: Int? = 0,
/**
* 库存单位
*/
var kcUnitName: String? = "",
/**
* 消耗转换值
*/
var consumeValue: String? = "",
// 收货数量 收货入参
var receivedNum: Double? = null,
var receivedNumBak: Double? = null,
var receiveCountAll: Double? = null,
// 收货数量显示
var receivedNumTemp: String = "",
// 重量 收货入参
var goodsWeight: BigDecimal? = null,
// 已调整
var isAdjusted: Boolean = false,
// 选中物品的可选单位
var unitList: List<UnitVo> = emptyList<UnitVo>(),
//--------------------------------------------------------
var isSelected: Boolean = false,
var isLocalGoods: Boolean = false,
var kcUnitId: String?="",
var recPriceInItem:Double?=null,
var goodsCount:Double?=null,
var goodsUnitPrice:Double?=null,
var goodsPrice:Double?=null,
var unitHint:String?="",
var buyToInventoryValue:String?="",
//--------------------------------------------------------
) : Parcelable, BaseBean() {
val goodNameStr: String
get() {
if (goodName == null) return "-"
return goodName!!
}
val receivedNumStr: String
get() {
if (receivedNum == null)
return ""
return receivedNum?.toFormattedString() ?: ""
}
val receivedNumListStr: String
get() {
if (receivedNum == null)
return "-"
return receivedNum?.toSafeString(unitName) ?: "-"
}
val receivedNumCount: String
get() {
if (receivedNum == null || receivedNum == 0.toDouble())
return "0"
return receivedNum?.toSafeString(unitName) ?: "0"
}
val dualCountStr: String
get() {
return if (receivedNum != null && receivedNum != 0.toDouble() && receiveCount != null && receiveCount != 0.toDouble()) {
"${receivedNum.toSafeString(unitName)}/${receiveCount.toSafeString(unitName)}"
} else if (receivedNum != null && receivedNum != 0.toDouble()) {
"${receivedNum.toSafeString(unitName)}/-"
} else {
receiveCountListStr
}
}
val recUnitPriceTaxInStr: String
get() {
if (recUnitPriceTaxIn == 0.toDouble()) return ""
return recUnitPriceTaxIn?.toFormattedString() ?: ""
}
val recUnitPriceTaxInListStr: String
get() {
if (recUnitPriceTaxIn == 0.toDouble()) return "-"
return recUnitPriceTaxIn?.toFormattedString() ?: "-"
}
val newRecUnitPriceTaxInListStr: String
get() {
if (newRecUnitPriceTaxIn == 0.toDouble()) return "-"
return newRecUnitPriceTaxIn?.toFormattedString() ?: "-"
}
val receiveCountStr: String
get() {
return when (receiveCount) {
0.toDouble() -> ""
else -> receiveCount?.toSafeString(unitName) ?: ""
}
}
val receiveCountListStr: String
get() {
return when (receiveCount) {
0.toDouble() -> "-"
else -> receiveCount?.toSafeString(unitName) ?: "-"
}
}
val recPriceExItemStr: String
get() {
return when (recPriceExItem) {
0.toDouble() -> ""
else -> recPriceExItem?.toFormattedString() ?: ""
}
}
val recPriceExItemListStr: String
get() {
return when (recPriceExItem) {
0.toDouble() -> "-"
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() < 1) {
return goodsWeight!!.multiply(BigDecimal(1000)).toString()
}
return goodsWeight?.toFormattedString() ?: ""
}
val goodsWeightUnitStr: String
get() {
return if (goodsWeight == null || goodsWeight!!.toInt() < 1) {
""
} else {
"千克"
}
}
var purchaseUnitIdStr: String
set(value) {
purchaseUnitIdStr = value
}
get() {
return GlobalData.unitTypeList.find { it.id == purchaseUnitId.toString() }?.value ?: ""
}
val warehouseNameStr: String
get() {
if (warehouseName == null) {
return ""
}
return warehouseName!!
}
fun hasNullField(): String? {
if (warehouseId.isNullOrBlank()) {
return "请选择仓库"
}
if (receivedNum == null || receivedNum == 0.toDouble()) {
return "请输入收货数量或进行称重"
}
if (newRecUnitPriceTaxIn == null || newRecUnitPriceTaxIn == 0.toDouble()) {
return "请输入收货单价"
}
if (recPriceExItem == null || recPriceExItem == 0.toDouble()) {
return "请输入收货金额"
}
if (goodsWeight == null || goodsWeight!!.toFloat() <= 0) {
return "请放入要称重的物品"
}
return null
}
}
@@ -0,0 +1,55 @@
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: String? = "",
@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: Boolean? = false,
@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,36 @@
package com.sw.inbound.model.response
/**
* 采购详情
*/
data class PurchaseInfo(
/**
* 单据id
*/
var id: String,
/**
* 供应商id
*/
var supplierId: String,
/**
* 供应商名称
*/
var supplierName: String? = "",
/**
* 采购单号
*/
var purCode: String? = "",
/**
* 收货单号
*/
var receiveCode: String? = "",
/**
* 物品信息
*/
var receiveGoodsInfoList: List<GoodsInfo> = emptyList<GoodsInfo>(),
// 自用
var warehouseName: String = "选择仓库"
) : BaseBean() {
}
@@ -0,0 +1,5 @@
package com.sw.inbound.model.response
data class RecGoodsName(
var nameList: List<String>
)
@@ -0,0 +1,149 @@
package com.sw.inbound.model.response
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import com.sw.inbound.utils.ext.roundedDecimalPlace
import com.sw.inbound.utils.removeTrailingZeros
import kotlinx.parcelize.IgnoredOnParcel
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("relativeUrl")
val relativeUrl: String? = "",
@SerializedName("goodsCode")
val goodsCode: String? = "",
@SerializedName("id")
val id: String? = "",
/**
* 物品id
*/
// @SerializedName("goodsId")
// val goodsId: String? = "",
/**
* 物品名称
*/
@SerializedName("goodsName")
val goodsName: String? = "",
/**
* 库存单位id
*/
@SerializedName("kcUnitId")
val kcUnitId: Int? = 0,
/**
* 库存单位名称
*/
@SerializedName("kcUnitName")
val kcUnitName: String? = "",
/**
* 库存与克的转换值
*/
val consumeValue: String? = "",
val purchaseValue: 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? = "",
var isSelected: Boolean = false,
var score: Int = 0
) : 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: Double? = 0.0,
) : Parcelable {
/**
* 购买单位数值
*/
fun getPurchaseUnitValue(value: Int): String {
val defZero = 0.toDouble()
val num = when (purchaseUnitName) {
"千克", "公斤" -> 2
"" -> 1
else -> 0
}
if (purchaseValue == null || purchaseValue == defZero || consumeValue.isNullOrBlank()) {
return ""
}
var consumeValueDouble: Double
try {
consumeValueDouble = consumeValue.toDouble()
} catch (e: Exception) {
e.printStackTrace()
return ""
}
val count = (value / purchaseValue / consumeValueDouble).roundedDecimalPlace(num)
return count.removeTrailingZeros()
}
}
}
}
@@ -0,0 +1,62 @@
package com.sw.inbound.model.response
//import android.os.Parcelable
//import com.google.gson.annotations.SerializedName
//import kotlinx.parcelize.Parcelize
/**
* 存储方式
*/
data class StorageType(
val id:String?=null,
val value:String?=null,
val text:String?=null,
val title:String?=null,
val label:String?=null,
)
//@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: String? = "",
// @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,119 @@
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: String? = "",
@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: String ? = "",
/**
* 供应商
*/
@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,52 @@
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,19 @@
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,213 @@
package com.sw.inbound.network.api
import com.sw.inbound.GlobalData
import com.sw.inbound.model.request.GoodsAddParam
import com.sw.inbound.model.request.GoodsPurchaseUnitParam
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.EquipmentInfo
import com.sw.inbound.model.response.GoodsType
import com.sw.inbound.model.response.PurchaseInfo
import com.sw.inbound.model.response.RecGoodsName
import com.sw.inbound.model.response.SearchGoodsInfo
import com.sw.inbound.model.response.StorageType
import com.sw.inbound.model.response.SupplierInfo
import com.sw.inbound.model.response.User
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
import retrofit2.http.PartMap
import retrofit2.http.Query
import retrofit2.http.Url
/**
* api接口
*/
interface ApiService {
private val apiPrefix: String
get() = "/terminal/neglect/inbound"
/**
* device获取token
*/
@GET("sys/getEquipmentToken")
suspend fun getDeviceToken(
@Query("qrcodeId") qrcodeId: String,
@Query("appVersion") appVersion: String = GlobalData.appVersion
): ApiResponse<String>
/**
*获取配置信息
*/
@GET("equipment/stEquipment/queryByEquipmentCode")
suspend fun getDeviceInfo(
@Query("equipmentCode") equipmentCode: String,
@Query("appVersion") appVersion: String = GlobalData.appVersion,
@Header("X-Access-Token") token: String
): ApiResponse<EquipmentInfo>
/**
* 登录
*/
@POST//("sys/loginPad")
suspend fun login(
@Url url: String = "${GlobalData.appBaseUrl}/sys/loginPad",
@Body body: LoginParam,
): ApiResponse<User>
/**
* 采购单入库-列表
*/
// @GET//("pad/receivePage")
// suspend fun getReceiveList(
// @Url url: String = "${GlobalData.appBaseUrl}${apiPrefix}/pad/receivePage",
// @Query("pageNo") pageNo: Int,
// @Query("pageSize") pageSize: Int,
//
// ): ApiResponse<SupplierResponse>
@POST
suspend fun getReceiveList(
@Url url: String = "${GlobalData.appBaseUrl}${apiPrefix}/pad/receivePage",
@Body param:Map<String, String>
): ApiResponse<List<SupplierInfo>?>
/**
* 采购单入库-详情
*/
@GET//("pad/receiveDetail")
suspend fun getReceiveDetail(
@Url url: String = "${GlobalData.appBaseUrl}${apiPrefix}/pad/receiveDetail",
@Query("id") id: String
): ApiResponse<PurchaseInfo>
/**
* 部分收货
*/
@POST//("pad/partialReceiptGoods")
suspend fun partialReceipt(
@Url url: String = "${GlobalData.appBaseUrl}${apiPrefix}/pad/partialReceiptGoods",
@Body uploadInfo: UploadInfo
): ApiResponse<Boolean>
/**
* 确认收货
*/
@POST//("pad/saveAndReceiveAndGoWare")
suspend fun confirmReceipt(
@Url url: String = "${GlobalData.appBaseUrl}${apiPrefix}/pad/saveAndReceiveAndGoWare",
@Body uploadInfo: UploadInfo,
): ApiResponse<Boolean>
/**
* 存储方式
*/
@GET//("sys/dict/getDictItemByF")
suspend fun getGoodsStorageType(
// @Url url: String = "${GlobalData.appBaseUrl}${apiPrefix}/sys/dict/getDictItemByF",
@Url url: String = "${GlobalData.appBaseUrl}${apiPrefix}/pad/getDictItemByF",
// @Query("dictCode") dictCode: String = "goods_storage_type",
@Query("dictCode") dictCode: String = "ccfs",
): ApiResponse<List<StorageType>>
/**
* 物品类型
*/
@GET//("pad/notLimitList")
suspend fun getGoodsType(
@Url url: String = "${GlobalData.appBaseUrl}${apiPrefix}/pad/notLimitList",
): ApiResponse<GoodsType>
/**
* 获取字典数据
*/
@GET//("pad/dataList")
suspend fun getDictType(
@Url url: String = "${GlobalData.appBaseUrl}${apiPrefix}/pad/dataList",
@Query("type") type: String
): ApiResponse<List<DictType>>
/**
* 上传图片
*/
// @Multipart
// @POST//("sys/common/upload")
// suspend fun uploadImage(
// @Url url: String = "${GlobalData.appBaseUrl}${apiPrefix}/sys/common/upload",
// @Part("code") code: RequestBody,
// @Part file: MultipartBody.Part,
// ): ApiResponse<String>
@Multipart
@POST//("sys/common/upload")
suspend fun uploadImage(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/upload",
//@Part("code") code: RequestBody,
@Part file: MultipartBody.Part,
): ApiResponse<String>
@Multipart
@POST
suspend fun uploadMultipleImages(
// @Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/inboundWeighingScale/add",
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/inbound/pad/inboundWeighingScale/add",
@PartMap params: HashMap<String, RequestBody>,
@Part goodsPics: List<MultipartBody.Part>
): ApiResponse<String>
/**
* 搜索物品列表
*/
@POST//("pad/goodsInfoList")
suspend fun searchGoodsInfoList(
@Url url: String = "${GlobalData.appBaseUrl}${apiPrefix}/pad/goodsInfoList",
@Body param:Map<String, String>
): ApiResponse<SearchGoodsInfo>
/**
* 物品列表(识别图片后根据名称查询,名称支持多个)
*/
@POST
suspend fun recognizeGoodsList(
@Url url: String = "${GlobalData.appBaseUrl}${apiPrefix}/pad/goodsList",
@Body recGoodsName: RecGoodsName
): ApiResponse<List<SearchGoodsInfo.Record>>
/**
* 自采添加商品
*/
@POST//("pad/goodsAdd")
suspend fun selfPurchaseGoodsAdd(
@Url url: String = "${GlobalData.appBaseUrl}${apiPrefix}/pad/goodsAdd",
@Body goodsAddParam: GoodsAddParam,
): ApiResponse<String>
/**
* 添加物品采购单位
*/
@POST//("pad/goodsAddUnit")
suspend fun goodsAddUnit(
@Url url: String = "${GlobalData.appBaseUrl}${apiPrefix}/pad/goodsAddUnit",
@Body purchaseUnitParam: GoodsPurchaseUnitParam,
): ApiResponse<Boolean>
/**
* 自采入库
*/
@POST//("pad/SelfPurchasedGoods")
suspend fun selfPurchaseWarehousing(
@Url url: String = "${GlobalData.appBaseUrl}${apiPrefix}/pad/selfPurchasedGoods",
@Body list: List<PurchaseWarehouseParam>,
): ApiResponse<Boolean>
}
@@ -0,0 +1,38 @@
package com.sw.inbound.network.interceptor
import com.sw.inbound.GlobalData
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("deviceId", GlobalData.deviceId)
// .header("X-DEVICE-CODE", GlobalData.deviceId)
.header("X-DEVICE-CODE", "bcf396ed-78f6-3864-9837-7c37c5b2ec41")
.header("x-access-token", GlobalData.appToken)
.header("authorization", "57ee87183f2a4fa59683ec9ef41c8f5d")
// .header("Authorization", "Bearer ${getToken()}")
// .header("x-access-token", 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,68 @@
package com.sw.inbound.objbox
import android.graphics.Bitmap
import android.net.Uri
import java.io.File
data class FoodClassInfo(
var class_names: List<String>,
var class_to_idx: Map<String, Int>,
var idx_to_class: Map<String, String>
)
data class FoodCollectionBean(
var imageUri: Uri? = null,
var bitmap: Bitmap? = null,
var imageFile: File? = null,
var imageVector: FloatArray? = null,
var isShowCamera: Boolean = false,
var isFinish:Boolean = false,
var uploadSuccess:Boolean = false
)
data class ReceiptGoodsInfo(
var goodsId: String? = "",
var goodsName: String? = "",
var procurementUnit: String? = "",
var procurementCount: Int? = 0,
var procurementPrice: Double? = 0.0,
var procurementAmount: Double? = 0.0,
var receiptCount: Int? = 0,
var goodsWeight: Int? = 0,
var goodsState: String? = "",
var isSelected: Boolean = false,
var isLocalGoods: Boolean = false
)
//data class DropdownInfo(
// var id: String,
// var name: String
//)
//data class RecognizeResult(
// var name: String? = null,
// var image: String? = null,
// var isSelected: Boolean = false
//)
//data class GoodsSearchInfo(
// var id: String? = null,
// var name: String? = null,
// var isSelected: Boolean = false
//)
data class OperateBean(
var obj: Any? = null,
var typeList: MutableList<OperateType> = mutableListOf(),
var isSelected: Boolean = false,
var isLocalGoods: Boolean = false,
var isReceiptPage: Boolean = true
)
data class OperateType(
var value: String = "",
var width: Int = 0,
var textColor: String = "#FF999999",
var isBoldFont: Boolean = false,
var isShowIcon: Boolean = false
)
@@ -0,0 +1,37 @@
package com.sw.inbound.objbox
import io.objectbox.annotation.Entity
import io.objectbox.annotation.HnswIndex
import io.objectbox.annotation.Id
import io.objectbox.annotation.VectorDistanceType
@Entity
data class Food(
@Id var id: Long = 0,
var name: String? = null,
var foodIdx: Int = 0,
@HnswIndex(dimensions = 512, distanceType = VectorDistanceType.DOT_PRODUCT)
var foodVector: FloatArray? = null
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Food
if (id != other.id) return false
if (foodIdx != other.foodIdx) return false
if (name != other.name) return false
if (!foodVector.contentEquals(other.foodVector)) return false
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + foodIdx
result = 31 * result + (name?.hashCode() ?: 0)
result = 31 * result + (foodVector?.contentHashCode() ?: 0)
return result
}
}
@@ -0,0 +1,282 @@
package com.sw.inbound.objbox
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import android.renderscript.Element.DataType
import androidx.core.graphics.scale
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.sw.inbound.MyApp
import com.sw.inbound.utils.AssetsTool
import com.sw.inbound.utils.ImageUtil
import com.sw.inbound.utils.ext.toJsonString
import io.objectbox.Box
import io.objectbox.kotlin.boxFor
import io.objectbox.query.Query
import org.pytorch.IValue
import org.pytorch.Module
import org.pytorch.torchvision.TensorImageUtils
import timber.log.Timber
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
object FoodModule {
private lateinit var module_mobile: Module
private lateinit var box: Box<Food>
private lateinit var embeddingsList: List<List<Float>>
private lateinit var labelsList: IntArray
private lateinit var classInfo: FoodClassInfo
private val NO_MEAN_RGB = floatArrayOf(0.0f, 0.0f, 0.0f)
private val NO_STD_RGB = floatArrayOf(1.0f, 1.0f, 1.0f)
// 1. 定义你的模型固定输入尺寸 (根据你的tflite模型修改,比如224x224)
private const val MODEL_INPUT_WIDTH = 300
private const val MODEL_INPUT_HEIGHT = 300
const val DEFAULT_FOOD_INDEX = -1
const val BAG_RATE = 0.05
fun init(context: Context) {
Thread {
module_mobile = Module.load(copyAssetToCache(context, "best_embedding_model_mobile.pt"))
box = ObjectBox.boxStore.boxFor(Food::class)
//if (box.all.isNotEmpty()) {
// box.removeAll()
//}
if (box.all.isEmpty()) {
initDefFoodData(context)
}
}.start()
}
// fun uri2FloatArray(uri: Uri): FloatArray? {
// return MyApp.instance?.let { context ->
// ImageUtil.uriToBitmap(context, uri)?.let {
// bitmap2FloatArray(it)
// }
// }
// }
fun bitmap2FloatArray(originBitmap: Bitmap, isRecycle: Boolean): FloatArray? {
var rgb565Bitmap: Bitmap?=null
try {
val scaledBitmap = originBitmap.scale(MODEL_INPUT_WIDTH, MODEL_INPUT_HEIGHT)
if (isRecycle) {
originBitmap.recycle()
}
rgb565Bitmap = scaledBitmap.copy(Bitmap.Config.RGB_565, false)
scaledBitmap.recycle()
val inputTensor = TensorImageUtils.bitmapToFloat32Tensor(
rgb565Bitmap,
NO_MEAN_RGB, // [0.485, 0.456, 0.406] TORCHVISION_NORM_MEAN_RGB
NO_STD_RGB // [0.229, 0.224, 0.225] TORCHVISION_NORM_STD_RGB
)
val outputTensor = module_mobile.forward(IValue.from(inputTensor)).toTensor()
return outputTensor.dataAsFloatArray
} catch (e: OutOfMemoryError) {
e.printStackTrace()
} finally {
if (rgb565Bitmap != null && rgb565Bitmap.isRecycled.not()) {
rgb565Bitmap.recycle()
}
//System.gc()
//System.runFinalization()
}
return null
}
// fun queryFood(uri: Uri, queryCount: Int = 15): List<String>? {
// return uri2FloatArray(uri)?.let {
// queryFood(it, queryCount)
// }
// }
// /**
// * 返回识别物品名称列表
// */
// fun queryFood(bitmap: Bitmap, queryCount: Int = 15): List<String> {
// val floatArray = bitmap2FloatArray(bitmap)
// return queryFood(floatArray, queryCount)
// }
// /**
// * 返回识别物品IdNameScore对象列表
// */
// fun queryFoodNameScore(bitmap: Bitmap, queryCount: Int = 15): List<IdNameScore> {
// val floatArray = bitmap2FloatArray(bitmap)
// return queryFoodNameScore(floatArray, queryCount)
// }
fun queryFoodNameScore(floatArray: FloatArray?, queryCount: Int = 15): List<IdNameScore> {
if (floatArray == null) return emptyList()
val query: Query<Food> =
box.query(Food_.foodVector.nearestNeighbors(floatArray, queryCount)).build()
//查询比较分数
// val tempList = query.findWithScores().sortedBy { it.score }.map { "${it.get().name}|${it.get().foodIdx}|${it.score}" }
val idScoreList = query.findIdsWithScores()
val nameScoreList = mutableListOf<IdNameScore>()
idScoreList.forEach {
nameScoreList.add(IdNameScore(id = it.id, name = box.get(it.id).name?:"", score = it.score))
}
Timber.tag("FoodModule").d("queryFood数据:${nameScoreList.toJsonString()}")
query.close()
return nameScoreList
}
fun getFoodScoreList(bitmap: Bitmap, queryCount: Int = 15): List<IdNameScore> {
val floatArray = bitmap2FloatArray(bitmap, false)
if (floatArray == null) return emptyList()
val nameScoreList = queryFoodNameScore(floatArray, queryCount)
if (nameScoreList.isEmpty()) {
return emptyList()
}
val maxScoreList = nameScoreList
.filter { it.score < 0.15 }
.groupBy { it.name }
.map { (_, value) -> value.minByOrNull { it.score }!! }
.toMutableList()
val map = mutableMapOf<String, Int>()
nameScoreList.forEach {
val key = it.name
val count = map[key] ?: 0
map[key] = count + 1
}
val orderList = map.entries.sortedByDescending { it.value }.map { it.key }.toMutableList()
val firstFood = nameScoreList[0].name
orderList.remove(firstFood)
orderList.add(0, firstFood)
val sortedScoreList = maxScoreList.sortedWith(compareBy {
orderList.indexOf(it.name)
})
Timber.tag("FoodModule").d("getFoodScoreList数据:${sortedScoreList.toJsonString()}")
return sortedScoreList
}
fun queryFood(floatArray: FloatArray, queryCount: Int = 15): List<String> {
val map = mutableMapOf<String, Int>()
val nameScoreList = queryFoodNameScore(floatArray, queryCount)
nameScoreList.filter { it.score < 0.05 }.forEach {
val count = map[it.name] ?: 0
map[it.name] = count + 1
}
val list = map.entries.sortedByDescending { it.value }.map { it.key }
return list
}
data class IdNameScore(
val id:Long,
var name:String,
val score: Double
)
fun initDefFoodData(context: Context, action:()-> Unit={}) {
val count = box.all.count { it.foodIdx == DEFAULT_FOOD_INDEX }
if (count > 0) {
return
}
val embeddingsJson = AssetsTool.readAssetsFile(context, "data/embeddings.json")
val labelsJson = AssetsTool.readAssetsFile(context, "data/labels.json")
val classInfoJson = AssetsTool.readAssetsFile(context, "data/class_info.json")
embeddingsList =
Gson().fromJson(embeddingsJson, object : TypeToken<List<List<Float>>>() {}.type)
labelsList = Gson().fromJson(labelsJson, IntArray::class.java)
classInfo =
Gson().fromJson(classInfoJson, FoodClassInfo::class.java)
val foodMap = classInfo.idx_to_class
embeddingsList.forEachIndexed { index, floatList ->
val classIdx = labelsList[index]
val foodName = foodMap["$classIdx"]
val array = floatList.toFloatArray()
ObjectBox.boxStore.runInTx {
box.put(Food(name = foodName, foodVector = array, foodIdx = DEFAULT_FOOD_INDEX))
}
}
action()
}
/**
* ,此方法的主要目的是:从assets 拷贝到 app的cache目录
* @param context
* @param fileName
* @return 例如是这样:/data/user/0/com.frizzle.pluginhookandroid9/cache/plugin-debug.apk
*
* 不可能反正SD
*/
// fun copyAssetToCache(context: Context, fileName: String): String? {
// // 此app的缓存目录 --> 会默认在 cache目录...,可以自己去看看哦
// val cacheDir = context.getCacheDir()
// if (!cacheDir.exists()) {
// cacheDir.mkdirs() // TODO 如果没有缓存目录,就创建
// }
// val outPath = File(cacheDir, fileName) // TODO 创建输出的文件位置
// if (outPath.exists()) {
// outPath.delete() // TODO 如果该文件已经存在,就删掉
// }
// var `is`: InputStream? = null // 读取
// var fos: FileOutputStream? = null // 写入
// try {
// // 创建文件,如果创建成功,就返回true
// val res = outPath.createNewFile()
// if (res) {
// `is` = context.getAssets().open(fileName) // 拿到main/assets目录的输入流,用于读取字节
// fos = FileOutputStream(outPath) // 读取出来的字节最终写到outPath
// val buf = ByteArray(`is`.available()) // 缓存区
// var byteCount: Int
//
// // 开始循环读取
// while ((`is`.read(buf).also { byteCount = it }) != -1) {
// fos.write(buf, 0, byteCount)
// }
// return outPath.getAbsolutePath()
// }
// } catch (e: IOException) {
// e.printStackTrace()
// } finally {
// try {
// // TODO 一定要记得关闭资源,为了不去性能的磨损
// fos?.flush()
// `is`?.close()
// fos?.close()
// } catch (e: IOException) {
// e.printStackTrace()
// }
// }
// return null
// }
fun copyAssetToCache(context: Context, fileName: String): String? {
val cacheFile = File(context.cacheDir, fileName)
val buffer = ByteArray(8 * 1024)
var inputStream: InputStream? = null
var outputStream: FileOutputStream? = null
try {
inputStream = context.assets.open(fileName)
outputStream = FileOutputStream(cacheFile)
var byteCount: Int
while (inputStream.read(buffer).also { byteCount = it } != -1) {
outputStream.write(buffer, 0, byteCount)
}
outputStream.channel.force(true) // 强制物理落盘,比flush更彻底
return cacheFile.absolutePath
} catch (e: IOException) {
e.printStackTrace()
Timber.tag("FoodModule").d("文件拷贝失败 fileName=$fileName, error=${e.message}")
// 拷贝失败时删除残缺文件,避免下次读取到损坏文件
if (cacheFile.exists()) {
cacheFile.delete()
}
} finally {
outputStream?.close()
inputStream?.close()
}
return null
}
}
@@ -0,0 +1,110 @@
/*
* Copyright 2024 ObjectBox Ltd. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.sw.inbound.objbox
import android.content.Context
import android.util.Log
import io.objectbox.BoxStore
import io.objectbox.BoxStoreBuilder
import io.objectbox.exception.DbException
import io.objectbox.exception.FileCorruptException
import io.objectbox.sync.Sync
import java.io.File
import java.util.zip.GZIPOutputStream
import kotlin.io.copyTo
import kotlin.io.inputStream
import kotlin.io.outputStream
import kotlin.io.use
import kotlin.jvm.java
import kotlin.jvm.javaClass
/**
* Singleton to keep BoxStore reference and provide current list of Notes Objects.
* Inserts demo data if no Objects are stored.
*/
object ObjectBox {
// 确保所有设备/版本使用相同的objectbox-models/default.json
// 避免直接修改自动生成的MyObjectBox类
// 跨版本升级时使用boxStore.runInTx执行数据迁移
private const val TAG = "ObjectBox"
lateinit var boxStore: BoxStore
private set
/**
* If building the [boxStore] failed, contains the thrown error message.
*/
var dbExceptionMessage: String? = null
private set
fun init(context: Context) {
// 优化:调大事务回收超时时间
System.setProperty("objectbox.finalizerTimeout", "30000");
// On Android make sure to pass a Context when building the Store.
boxStore = try {
MyObjectBox.builder()
.androidContext(context.applicationContext)
.build()
} catch (e: DbException) {
if (e.javaClass == DbException::class.java || e is FileCorruptException) {
// Failed to build BoxStore due to database file issue, store message;
// checked in NoteListActivity to notify user.
dbExceptionMessage = e.toString()
return
} else {
// Failed to build BoxStore due to developer error.
throw e
}
}
if (com.sw.inbound.BuildConfig.DEBUG) {
var syncAvailable = if (Sync.isAvailable()) "available" else "unavailable"
Log.d(TAG,"Using ObjectBox ${BoxStore.getVersion()} (${BoxStore.getVersionNative()}, sync $syncAvailable)")
// Enable ObjectBox Admin on debug builds.
// https://docs.objectbox.io/data-browser
io.objectbox.android.Admin(boxStore).start(context.applicationContext)
}
}
/**
* If the database file is not in use, compresses (GZIP) and copies it to the given [target].
*/
fun copyAndGzipDatabaseFileTo(target: File, context: Context): Boolean {
if (BoxStore.isDatabaseOpen(context, null)) {
// Do not copy if database file is still in use.
// If it would be open, the copy will likely get corrupted
// as BoxStore may currently write data to the file.
Log.e(TAG, "Database file is still in use, can not copy.")
return false
}
// If a name was given when building BoxStore use that instead of the default below.
val dbName = BoxStoreBuilder.DEFAULT_NAME
File(context.filesDir, "objectbox/$dbName/data.mdb").inputStream().use { input ->
target.parentFile?.mkdirs()
GZIPOutputStream(target.outputStream()).use { output ->
input.copyTo(output, DEFAULT_BUFFER_SIZE)
}
}
return true
}
}
@@ -0,0 +1,22 @@
package com.sw.inbound.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.sw.inbound.MainActivity
import timber.log.Timber
/**
* 开机广播监听
*/
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (Intent.ACTION_BOOT_COMPLETED == intent.action) {
Timber.d("设备启动完成,开始执行自启动逻辑")
val intent = Intent(context, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
}
}
}
@@ -0,0 +1,57 @@
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
// } else {
// "HTTP Error: ${e.code()} - ${e.message()}"
// }
ApiResponse(code = "${e.code()}", msg = e.message())
}
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,57 @@
//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 BaseRepositoryBak {
// 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
//// } else {
//// "HTTP Error: ${e.code()} - ${e.message()}"
//// }
// ApiResponse(code = e.code(), message = e.message())
// }
//
// is SocketTimeoutException -> {
// ApiResponse(code = -2, message = "请求超时: ${e.message}")
// }
//
// is ConnectException -> {
// ApiResponse(code = -3, message = "连接失败: ${e.message}")
// }
//
// is SSLHandshakeException -> {
// ApiResponse(code = -4, message = "SSL握手失败: ${e.message}")
// }
//
// is JsonParseException -> {
// ApiResponse(code = -5, message = "JSON解析错误: ${e.message}")
// }
//
// is IOException -> {
// ApiResponse(code = -6, message = "网络IO错误: ${e.message}")
// }
//
// else -> {
// ApiResponse(code = -1, message = "未知错误: ${e.message ?: "无错误信息"}")
// }
// }
// }
// }
//}
@@ -0,0 +1,238 @@
package com.sw.inbound.repository
import android.net.Uri
import com.sw.inbound.model.request.GoodsAddParam
import com.sw.inbound.model.request.GoodsPurchaseUnitParam
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.EquipmentInfo
import com.sw.inbound.model.response.GoodsType
import com.sw.inbound.model.response.PurchaseInfo
import com.sw.inbound.model.response.RecGoodsName
import com.sw.inbound.model.response.SearchGoodsInfo
import com.sw.inbound.model.response.StorageType
import com.sw.inbound.model.response.SupplierInfo
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.FileUtils
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import timber.log.Timber
import java.io.File
import javax.inject.Inject
/**
* 远程数据处理
*/
class RemoteRepository @Inject constructor(
private val apiService: ApiService
) : BaseRepository() {
/**
* 生成token
*/
suspend fun getDeviceToken(qrcodeId: String): ApiResponse<String> {
return safeApiCall {
apiService.getDeviceToken(
qrcodeId
)
}
}
/**
* 获取设备信息
*/
suspend fun getDeviceInfo(equipmentCode: String, token: String): ApiResponse<EquipmentInfo> {
return safeApiCall {
apiService.getDeviceInfo(
equipmentCode = equipmentCode,
token = token
)
}
}
suspend fun login(loginParam: LoginParam): ApiResponse<User> {
return safeApiCall { apiService.login(body = loginParam) }
}
suspend fun getReceiveList(pageNum: Int, pageSize: Int): ApiResponse<List<SupplierInfo>?> {
return safeApiCall {
apiService.getReceiveList(
param = mutableMapOf(
"pageNum" to pageNum.toString(),
"pageSize" to pageSize.toString(),
)
)
}
}
suspend fun getReceiveDetail(id: String): ApiResponse<PurchaseInfo> {
return safeApiCall {
apiService.getReceiveDetail(id = 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 = 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 =
FileUtils.genRequestPart(context = ContextUtils.getAppContext(), imageUri = uri)
if (part == null) {
ApiResponse(code = "-1", msg = "解析图片失败")
} else {
//apiService.uploadImage(code = codeRequestBody, file = part)
apiService.uploadImage(file = part)
}
}
}
suspend fun uploadImage(file:File): ApiResponse<String> {
return safeApiCall {
//val codeRequestBody = RequestBody.create(
// MultipartBody.FORM,
// "4"
//)
val part = FileUtils.genRequestPart(file)
if (part == null) {
ApiResponse(code = "-1", msg = "解析图片失败")
} else {
//apiService.uploadImage(code = codeRequestBody, file = part)
apiService.uploadImage(file = part)
}
}
}
/**
* 部分收货
*/
suspend fun partialReceipt(uploadInfo: UploadInfo): ApiResponse<Boolean> {
return safeApiCall {
apiService.partialReceipt(uploadInfo = uploadInfo)
}
}
/**
* 全部收货
*/
suspend fun confirmReceipt(uploadInfo: UploadInfo): ApiResponse<Boolean> {
return safeApiCall {
apiService.confirmReceipt(uploadInfo = uploadInfo)
}
}
suspend fun searchGoodsInfoList(
goodsName: String,
pageNo: Int = 1,
pageSize: Int = 10
): ApiResponse<SearchGoodsInfo> {
return safeApiCall {
val map = mutableMapOf(
"goodsName" to goodsName,
"pageNum" to pageNo.toString(),
"pageSize" to pageSize.toString()
)
apiService.searchGoodsInfoList(param = map)
}
}
suspend fun recognizeGoodsList(nameList: List<String>): ApiResponse<List<SearchGoodsInfo.Record>> {
return safeApiCall {
apiService.recognizeGoodsList(recGoodsName = RecGoodsName(nameList))
}
}
/**
* 自采购-添加商品
*/
suspend fun selfPurchaseGoodsAdd(goodsAddParam: GoodsAddParam): ApiResponse<String> {
return safeApiCall {
apiService.selfPurchaseGoodsAdd(goodsAddParam = goodsAddParam)
}
}
suspend fun selfPurchaseGoodsAddUnit(addParam: GoodsPurchaseUnitParam): ApiResponse<Boolean> {
return safeApiCall {
apiService.goodsAddUnit(purchaseUnitParam = addParam)
}
}
/**
* 自采购-入库
*/
suspend fun selfPurchaseWarehousing(list: List<PurchaseWarehouseParam>): ApiResponse<Boolean> {
return safeApiCall {
apiService.selfPurchaseWarehousing(list = list)
}
}
suspend fun uploadMultipleImages(fileList: List<File>, params: HashMap<String, RequestBody>): ApiResponse<String> {
// val paramsMap = hashMapOf<String, RequestBody>()
// param.forEach { (key, value) ->
// paramsMap[key] = value.toRequestBody("text/plain".toMediaTypeOrNull())
// }
// 准备文件参数
val fileParts = mutableListOf<MultipartBody.Part>()
fileList.forEachIndexed { index, file ->
val requestFile = file
.asRequestBody("multipart/form-data".toMediaTypeOrNull())
val filePart = MultipartBody.Part.createFormData(
"goodsPics",
file.name,
requestFile
)
fileParts.add(filePart)
}
return safeApiCall {
apiService.uploadMultipleImages(params = params, goodsPics = fileParts)
}
}
}
@@ -0,0 +1,136 @@
package com.sw.inbound.sdk
import android.os.Handler
import android.os.Looper
import com.sw.inbound.utils.ThreadUtils
import com.sw.inbound.utils.ToastUtils
import com.wabon.wbintelligenthardwaresdk.api.SensorScale
import com.wabon.wbintelligenthardwaresdk.api.SensorScale.OnScaleResult
import kotlinx.coroutines.delay
import timber.log.Timber
import kotlin.math.roundToInt
typealias WeightCallback = (Int) -> Unit
/**
* 称 传感器计算
*/
object SensorScaleUtils {
const val serialPort = "/dev/ttyS7"
const val baudRate = 115200
private var mSensorScale: SensorScale? = null
var isValidWeight = false
private fun init() {
if (mSensorScale != null) {
return
}
mSensorScale = SensorScale(object : OnScaleResult {
/**
* 读取重量
*/
override fun readWeight(state: Int, value: Double) {
isValidWeight = true
callbackList.forEach {
it.invoke((value * 1000).roundToInt())
}
// EventBus.getDefault().post(MessageEvent((value * 1000).roundToInt()))
}
/**
* 读取鉴别率
*/
override fun readIdentify(rate: Int) {
Timber.e("readIdentify rate = $rate")
}
override fun fail(errCode: Int) {
isValidWeight = false
Timber.e("fail code = $errCode")
Handler(Looper.getMainLooper()).post {
ToastUtils.showToast("秤读取数据异常")
startScale()
}
}
})
}
private var callbackList: MutableList<WeightCallback> = mutableListOf()
fun addWeightListener(callback: WeightCallback) {
callbackList.add(callback)
}
/**
* 开启称重
* @param autoScale 是否开启自动读取
*/
fun startScale(autoScale: Boolean = true) {
init()
mSensorScale?.openScale(serialPort, baudRate) { open ->
if (open && autoScale) {
// Handler(Looper.getMainLooper()).postDelayed({
// mSensorScale?.readWeight()
// mSensorScale?.startContinuousRead()
// }, 1500)
ThreadUtils.launchOnIo {
delay(1000)
// 打开后需要等待后才能调用,否则会 1001 SDK未初始化
mSensorScale?.readWeight()
mSensorScale?.startContinuousRead()
}
}
}
}
/**
* 开启自动读取重量
*/
fun startContinuousRead() {
mSensorScale?.startContinuousRead()
}
/**
* 手动读取重量
*/
fun readWeight() {
mSensorScale?.readWeight()
}
/**
* 零位标定
*/
fun zero() {
mSensorScale?.zero {
Timber.d("零位标定操作成功")
}
}
/**
* 去皮置零
*/
fun tare() {
mSensorScale?.tare {
Timber.d("去皮置零操作成功")
ToastUtils.showToast("去皮置零操作成功")
}
}
/**
* 停止自动读取重量
*/
fun stopContinuousRead() {
mSensorScale?.stopContinuousRead()
}
/**
* 关闭称重
*/
fun closeScale() {
mSensorScale?.closeScale()
mSensorScale = null
}
}
@@ -0,0 +1,159 @@
//package com.sw.inbound.sdk
//
//import android.os.Handler
//import android.os.Looper
//import com.sw.inbound.utils.ThreadUtils
//import com.sw.inbound.utils.ToastUtils
//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 SensorScaleUtils2 {
//
// 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 -> "未知"
// //}
//// if (state != SensorScale.STATE_STABLE) {
//// return
//// }
// Handler(Looper.getMainLooper()).post {
// weightCallback?.invoke((value * 1000).toInt())
// }
// //Timber.d("readWeight state = ${stateStr}, weight = $value")
// // 只使用稳定值
//// if (state == SensorScale.STATE_STABLE) {
// callback?.invoke(value)
//// }
// }
//
// /**
// * 读取鉴别率
// */
// override fun readIdentify(rate: Int) {
// Timber.e("readIdentify rate = $rate")
// }
//
// override fun fail(errCode: Int) {
// Timber.e("fail code = $errCode")
// }
// })
// }
//
// private var weightCallback: ((Int) -> Unit)? = null
// fun addWeightListener(callback: (Int) -> Unit) {
// weightCallback = callback
// }
//
// /**
// * 开启称重
// * @param autoScale 是否开启自动读取
// */
// 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(1000)
// // 打开后需要等待后才能调用,否则会 1001 SDK未初始化
// mSensorScale?.startContinuousRead()
// }
// }
// }
// }
// }
//
// /**
// * 开启自动读取重量
// */
// fun startContinuousRead(callback: Callback?) {
// this.callback = callback
// Timber.d("开启自动读取 = $isOpened")
// if (!isOpened) {
// return
// }
// mSensorScale?.startContinuousRead()
// }
//
// /**
// * 手动读取重量
// */
// fun readWeight(callback: Callback?) {
// this.callback = callback
// Timber.d("isOpened = $isOpened")
// if (!isOpened) {
// return
// }
// mSensorScale?.readWeight()
// }
//
// /**
// * 零位标定
// */
// fun zero() {
// if (!isOpened) return
// mSensorScale?.zero {
// Timber.d("零位标定操作成功")
// }
// }
//
// /**
// * 去皮置零
// */
// fun tare() {
// Timber.d("tare isOpened = $isOpened")
// if (!isOpened) return
// mSensorScale?.tare {
// Timber.d("去皮置零操作成功")
// ToastUtils.showToast("去皮置零操作成功")
// }
// }
//
// /**
// * 停止自动读取重量
// */
// fun stopContinuousRead() {
// Timber.d("isOpened = $isOpened")
// if (!isOpened) {
// return
// }
// mSensorScale?.stopContinuousRead()
// }
//
// /**
// * 关闭称重
// */
// fun closeScale() {
// isOpened = false
// mSensorScale?.closeScale()
// mSensorScale = null
// }
//}
@@ -0,0 +1,175 @@
package com.sw.inbound.ui
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.Image
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.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.sw.inbound.GlobalKey
import com.sw.inbound.R
import com.sw.inbound.ui.page.HomeScreen
import com.sw.inbound.ui.page.PurchaseOrderScreen
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
// )
Image(
painter = painterResource(id = R.mipmap.bg),
contentDescription = "背景图片",
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
startDestination = 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,
enterTransition = { fadeIn(animationSpec = tween(0)) }, // 无进入动画
exitTransition = { fadeOut(animationSpec = tween(0)) } // 无退出动画
) {
PurchaseOrderScreen(modifier = Modifier.fillMaxSize(), navController)
}
// // 自采单
// composable(
// Screen.SelfProcurement.route,
// enterTransition = { fadeIn(animationSpec = tween(0)) }, // 无进入动画
// exitTransition = { fadeOut(animationSpec = tween(0)) } // 无退出动画
// ) {
// SelfProcurementScreen(modifier = Modifier, navController)
// }
// // 收货
// composable(
// Screen.ReceiptProduct.route,
// enterTransition = { fadeIn(animationSpec = tween(0)) }, // 无进入动画
// exitTransition = { fadeOut(animationSpec = tween(0)) }, // 无退出动画
// arguments = listOf(
// navArgument(name = "id", builder = { type = NavType.IntType }),
// navArgument(name = "supplierId", builder = { type = NavType.IntType })
// )
// ) { backStackEntry ->
// val id = backStackEntry.arguments?.getString("id") ?: ""
// val supplierId = backStackEntry.arguments?.getString("supplierId") ?: ""
// 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,118 @@
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.interaction.MutableInteractionSource
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.runtime.remember
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.activity.GoodsListActivity
import com.sw.inbound.activity.SelfProcurementActivity
import com.sw.inbound.ui.Screen
import com.sw.inbound.ui.weight.TopTitleBar
import com.sw.inbound.utils.ext.startActivity
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(
interactionSource = remember { MutableInteractionSource() },
indication = null
) {
navController.navigate(Screen.PurchaseOrder.route)
//navController.context.startActivity<ReceiptActivity> {
// putExtra(GoodsListActivity.ID, "123123")
// putExtra(GoodsListActivity.SUPPLIER_ID, "123123123123")
// putExtra(GoodsListActivity.IS_RECEIPT_PAGE, true)
//}
},
painter = painterResource(R.mipmap.ic_order_purchase),
contentDescription = "采购单入库"
)
Spacer(modifier = Modifier.width(15.dp))
Image(
modifier = Modifier
.width(595.dp)
.height(477.dp)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) {
// navController.navigate(
// Screen.SelfProcurement.route, navOptions = NavOptions.Builder()
// .setEnterAnim(0)
// .setExitAnim(0)
// .setPopEnterAnim(0)
// .setPopExitAnim(0)
// .build()
// )
navController.context.startActivity<SelfProcurementActivity> {
putExtra(GoodsListActivity.IS_RECEIPT_PAGE, false)
}
},
painter = painterResource(R.mipmap.ic_order_self_procurement),
contentDescription = "自采入库"
)
}
}
}
@@ -0,0 +1,202 @@
//package com.sw.inbound.ui.page
//
//import androidx.compose.foundation.Image
//import androidx.compose.foundation.border
//import androidx.compose.foundation.layout.Arrangement
//import androidx.compose.foundation.layout.Box
//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.padding
//import androidx.compose.foundation.layout.size
//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.graphics.asImageBitmap
//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.GlobalKey
//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.QRCodeUtil
//import com.sw.inbound.utils.SPUtil
//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>("") }
// var password by remember { mutableStateOf<String>("") }
//
//// val user by viewModel.user.collectAsState()
//
// LaunchedEffect(Unit) {
// Timber.d(" user =")
// username = SPUtil.getInstance().get(GlobalKey.KEY_USER_NAME, "").toString()
// 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()
//// }
//// }
// }
//
// val qrCodeBmp by lazy {
// QRCodeUtil.generateQRCode(
// content = GlobalData.deviceId,
// size = 150
// )
// }
//
// Box(contentAlignment = Alignment.BottomStart) {
// 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)
// // TODO: 测试功能,临时打开新页面
// controller.navigate(Screen.Home.route)
//// controller.context.startActivity<ReceiptActivity> {
//// putExtra(GoodsListActivity.ID, "123123")
//// putExtra(GoodsListActivity.SUPPLIER_ID, "123123123123")
//// }
// },
// borderColor = colorResource(R.color.blue),
// textColor = colorResource(R.color.white),
// fontSize = 36.sp
// )
// }
// Image(
// bitmap = qrCodeBmp!!.asImageBitmap(),
// contentDescription = "",
// modifier = Modifier
// .padding(start = 10.dp, bottom = 10.dp)
// .size(80.dp, 80.dp)
// )
// }
//}
//
//
//@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,361 @@
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.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.HorizontalDivider
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.runtime.snapshotFlow
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.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import com.sw.inbound.R
import com.sw.inbound.activity.GoodsListActivity
import com.sw.inbound.activity.ReceiptActivity
import com.sw.inbound.activity.SelfProcurementActivity
import com.sw.inbound.ext.dashedBorder
import com.sw.inbound.ext.toFormattedString
import com.sw.inbound.model.response.SupplierInfo
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.utils.ext.startActivity
import com.sw.inbound.viewmodel.PurchaseOrderViewModel
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import timber.log.Timber
@Preview(
widthDp = 1920,
heightDp = 1080,
showBackground = true
)
/**
* 采购订单-供应商列表
*/
@Composable
fun PurchaseOrderScreen(
modifier: Modifier = Modifier,
navController: NavHostController = rememberNavController(),
viewModel: PurchaseOrderViewModel = hiltViewModel<PurchaseOrderViewModel>()
) {
val products by viewModel.supplierList.collectAsState()
val lazyListState = rememberLazyListState()
val canLoadMore by viewModel.canLoadMore.collectAsState()
val isMoreLoading by viewModel.isMoreLoading.collectAsState()
var pageNum by remember { mutableIntStateOf(1) }
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) {
Timber.d("ON_RESUME 触发")
pageNum = 0
viewModel.getOrderList()
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
}
// 检测是否滚动到底部
LaunchedEffect(lazyListState) {
snapshotFlow { lazyListState.layoutInfo }
.map { layoutInfo ->
val lastVisibleItem = layoutInfo.visibleItemsInfo.lastOrNull()
lastVisibleItem?.index == layoutInfo.totalItemsCount - 1
}
.distinctUntilChanged()
.collect { reachedEnd ->
Timber.d("加载更多 reachedEnd = $reachedEnd, isMoreLoading = $isMoreLoading, canLoadMore = $canLoadMore")
if (reachedEnd && !isMoreLoading && canLoadMore) {
pageNum++
viewModel.getOrderList(pageNum = pageNum)
}
}
}
Column {
TopTitleBar()
Box(
modifier = Modifier
.weight(1f)
.padding(horizontal = 10.dp)
) {
Image(
painter = painterResource(id = R.mipmap.bg_listview),
contentDescription = "背景",
modifier = Modifier
.fillMaxSize(),
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
.padding(10.dp),
state = lazyListState,
contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(30.dp)
) {
items(items = products, key = { it!!.id?:"" }) { product ->
PurchaseOrderItem(product!!) {
// navController.navigate(
// Screen.ReceiptProduct.createRoute(
// it.id,
// it.supplierId
// )
// )
navController.context.startActivity<ReceiptActivity> {
putExtra(GoodsListActivity.ID,product!!.id)
putExtra(GoodsListActivity.SUPPLIER_ID,product.supplierId)
putExtra(GoodsListActivity.IS_RECEIPT_PAGE,true)
putExtra(GoodsListActivity.IS_NEW_RECEIPT,false)
}
}
}
if (isMoreLoading) {
item {
Box(
modifier = Modifier
.fillParentMaxHeight()
.padding(16.dp),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
}
}
}
}
}
BottomActionBar(
modifier = Modifier, onLeftButtonClick = {
navController.popBackStack()
}, onRight2ButtonClick = {
//navController.navigate(Screen.SelfProcurement.route)
navController.context.startActivity<SelfProcurementActivity> {
putExtra(GoodsListActivity.IS_RECEIPT_PAGE, false)}
//navController.context.startActivity<ReceiptActivity> {
// //xtra(GoodsListActivity.ID,it!!.id)
// //xtra(GoodsListActivity.SUPPLIER_ID,it.supplierId)
// putExtra(GoodsListActivity.IS_RECEIPT_PAGE,true)
// putExtra(GoodsListActivity.IS_NEW_RECEIPT,true)
//}
})
}
}
/**
* 采购单缺省
*/
@Composable
fun PurchaseEmptyItem() {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Image(painter = painterResource(R.mipmap.ic_empty), contentDescription = "暂无数据")
Spacer(modifier = Modifier.height(30.dp))
Text(
text = "暂无数据", style = TextStyle(
fontSize = 30.sp,
fontWeight = FontWeight.Bold,
color = colorResource(R.color.title)
)
)
}
}
/**
* 列表item
*/
@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.purCode ?: "-"}",
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 = 26.sp)
)
}
}
}
}
@@ -0,0 +1,744 @@
//package com.sw.inbound.ui.page
//
//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.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.lazy.LazyColumn
//import androidx.compose.foundation.lazy.itemsIndexed
//import androidx.compose.foundation.shape.RoundedCornerShape
//import androidx.compose.material3.HorizontalDivider
//import androidx.compose.material3.LocalTextStyle
//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.layout.ContentScale
//import androidx.compose.ui.res.colorResource
//import androidx.compose.ui.res.painterResource
//import androidx.compose.ui.text.font.FontWeight
//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.withColor
//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.sdk.SensorScaleUtils
//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.theme.Black_141428
//import com.sw.inbound.ui.theme.Red_FF3232
//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.PageType
//import com.sw.inbound.ui.weight.ReceiptUnadjustedListView
//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.utils.InteractionUtils
//import com.sw.inbound.utils.ToastUtils
//import com.sw.inbound.viewmodel.ReceiptViewModel
//import timber.log.Timber
//import androidx.compose.foundation.text.KeyboardOptions
//import androidx.compose.ui.text.input.KeyboardType
//
///**
// * 收货界面
// */
//@Composable
//fun ReceiptProductScreen(
// modifier: Modifier = Modifier,
// controller: NavController = rememberNavController(),
// id: String,
// supplierId: String,
// 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) {
//// ToastUtils.showToast("收货成功")
// controller.popBackStack()
// }
// }
//
// val lifecycleOwner = LocalLifecycleOwner.current
// DisposableEffect(lifecycleOwner) {
// Timber.d("开始传感器采集")
// viewModel.startSensorScale()
// onDispose {
// Timber.d("停止传感器采集")
// viewModel.stopSensorScale()
// }
// }
//
// Column(modifier = modifier) {
// TopTitleBar()
// Box(
// modifier = modifier
// .weight(1f)
// .padding(horizontal = 10.dp)
// ) {
// Image(
// painter = painterResource(id = R.mipmap.bg_listview),
// contentDescription = "背景",
// modifier = Modifier
// .fillMaxSize(),
// contentScale = ContentScale.FillBounds
// )
// Row(
// modifier = Modifier.padding(
// start = 30.dp,
// end = 30.dp,
// top = 30.dp,
// bottom = 30.dp
// )
// ) {
// // 左侧列表
// ReceiptLeftView(
// modifier
// .weight(1f)
// .padding(start = 30.dp, top = 30.dp, bottom = 30.dp), viewModel, id
// )
//
// // 未调整,显示右侧界面
// if (!adjustState) {
//// Spacer(modifier = Modifier.width(30.dp))
// // 右侧编辑
// ReceiptRightView(
// modifier
// .width(580.dp)
// .padding(30.dp), viewModel
// )
// }
// }
// }
// BottomActionBar(
// modifier = Modifier,
// right2ButtonText = "确认收货",
// onLeftButtonClick = {
// controller.popBackStack()
// },
// onRight2ButtonClick = {
// viewModel.updateReceiptDialog(true)
// })
// }
//
// if (showDialog) {
// ReceiptTipDialog(
// isWarn = viewModel.hasWrongCount(),
// goodsList = orders.filter { it.receivedNum != it.receiveCount },
// onCancelClick = {
// Timber.d("返回")
// viewModel.updateReceiptDialog(false)
// },
// onConfirmClick = { isFull ->
// Timber.d(if (isFull) "确认收货" else "部分收货")
// viewModel.updateReceiptDialog(false)
// val uploadInfo = UploadInfo(id = id)
// if (isFull) {
// uploadInfo.receiveGoodsInfos = orders
// viewModel.confirmReceipt(uploadInfo)
// } else {
// uploadInfo.receiveGoodsInfoList = orders
// uploadInfo.supplierId = supplierId
// viewModel.partialReceipt(uploadInfo)
// }
// })
// }
//}
//
///**
// * 左侧列表
// */
//@Composable
//fun ReceiptLeftView(
// modifier: Modifier,
// viewModel: ReceiptViewModel,
// id: String
//) {
// val warehouseTypeList = GlobalData.warehouseTypeList
// 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(30.dp))
// Text(
// "收货单号:${currentPurchaseInfo?.receiveCode ?: "-"}",
// style = gray96a0aaTextStyle.copy(fontSize = 20.sp)
// )
// }
// }
// CustomSpinner(
// items = warehouseTypeList,
// selectedItem = currentPurchaseInfo!!.warehouseName,
// onItemSelected = { value ->
// val currentPurchaseInfo1 =
// currentPurchaseInfo!!.copy(warehouseName = value.value)
// viewModel.updateCurrentPurchaseInfo(currentPurchaseInfo1)
// viewModel.updateAllPurchaseStore(warehouse = value)
// })
// }
// Spacer(modifier = Modifier.height(30.dp))
//
// TabScreen(viewModel)
// }
// }
//}
//
///**
// * 已确认、未确认切换按钮
// */
//@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})")
//
// LaunchedEffect(purchaseUnadjustedList) {
// viewModel.updateReceiptList(purchaseUnadjustedList)
// }
//
// 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
// ReceiptUnadjustedListView(
// modifier = Modifier.padding(horizontal = 30.dp),
// productList = purchaseOrderList,
// checkedItem = checkedItem,
// onItemCheckedClick = { index, it ->
// viewModel.updateSelectedItemWithSwitch(it)
// })
//}
//
//@Composable
//fun AdjustedView(viewModel: ReceiptViewModel) {
// // 已调整view
// val purchaseAdjustList by viewModel.adjustedOrders.collectAsState()
//
//
// LazyColumn(
// verticalArrangement = Arrangement.spacedBy(0.dp)
// ) {
// itemsIndexed(
// items = purchaseAdjustList,
//// key = { index, it -> it.id ?: index }) { index, it ->
// key = { index, it -> index }) { index, it ->
//
// AdjustedViewItem(it, viewModel)
// }
// }
//}
//
///**
// * 已确认界面
// */
//@Composable
//private fun AdjustedViewItem(purchaseOrder: GoodsInfo, viewModel: ReceiptViewModel) {
// val warehouseTypeList = GlobalData.warehouseTypeList
// val isWarnItem = purchaseOrder.receivedNum != purchaseOrder.receiveCount
// var modifier: Modifier // = Modifier.padding(horizontal = 30.dp)
// if (isWarnItem) {
// modifier = Modifier
// .padding(horizontal = 30.dp)
// .background(Color(0xffFAF1F9))
// .border(width = 1.dp, color = Color.Red)
// } else {
// modifier = Modifier.padding(horizontal = 30.dp)
// }
//
// Column(modifier = modifier) {
// Spacer(modifier = Modifier.height(30.dp))
// Row(
// modifier = Modifier
// .fillMaxWidth()
// .padding(horizontal = 30.dp),
// horizontalArrangement = Arrangement.SpaceBetween
// ) {
// Text(
// purchaseOrder.goodNameStr,
// style = if (isWarnItem) AppTypography.blackTextStyle.withColor(Color.Red) else AppTypography.blackTextStyle
// )
// Row {
// Text("采购量:", style = gray96a0aaTextStyle)
// Text(
// "${purchaseOrder.receiveCountListStr}${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.newRecUnitPriceTaxInListStr,
// onValueChange = {
// viewModel.updatePurchaseItem(
// purchaseOrder = purchaseOrder.copy(
// newRecUnitPriceTaxIn = 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.receivedNumTemp,
// onValueChange = {
// viewModel.updatePurchaseItem(
// purchaseOrder = purchaseOrder.copy(
// receivedNumTemp = it,
// receivedNum = it.toSafeFloat()
// )
// )
// },
// inputType = InputType.Decimal,
// inputTextColor = if (isWarnItem) Red_FF3232 else Black_141428
// )
// 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,
// inputTextColor: Color = Black_141428
//) {
// 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,
// textStyle = LocalTextStyle.current.copy(
// color = inputTextColor,
// fontSize = 24.sp,
// fontWeight = FontWeight.Bold,
// textAlign = textAlign
// )
// )
// } else {
// CustomDropdownTextField(
// modifier = Modifier.width(300.dp),
// value = value,
// dropdownItems = dropdownItems,
// onValueChange = onValueChange,
// textAlign = spinnerTextAlign,
// )
// }
// }
//}
//
///**
// * 未调整,显示右侧界面
// */
//@Composable
//fun ReceiptRightView(modifier: Modifier = Modifier, viewModel: ReceiptViewModel) {
// 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(
// showSearchView = false,
// onOptionSelected = {},
// baseViewModel = viewModel,
// pageType = PageType.RECEIPT_PRODUCT
// )
// } else {
// ReceiptProductEditView(viewModel)
// }
// }
//}
//
///**
// * 收货界面右侧编辑
// */
//@Composable
//private fun ReceiptProductEditView(
// viewModel: ReceiptViewModel,
// onConfirmClick: () -> Unit = {},
// onCancelClick: () -> Unit = {}
//) {
// val selectedItem by viewModel.selectedItem.collectAsState()
// val warehouseTypeList = GlobalData.warehouseTypeList
//
// 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!!.receivedNumTemp,
// onValueChange = { value ->
// viewModel.updateCountInputState(true)
//
// selectedItem?.let {
// viewModel.updateSelectedItem(
// selectedItem!!.copy(
// receivedNumTemp = value,
// receivedNum = value.toSafeFloat()
// )
// )
// }
// },
// trailingLabel = selectedItem?.unitName,
// inputType = InputType.Decimal,
// isInitUpdate = true,
// keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
// )
//
// RowInputLayout(
// label = "收货单价",
// value = selectedItem!!.newRecUnitPriceTaxInTemp,
// trailingLabel = "元",
// isInitUpdate = true,
// onValueChange = { value ->
// viewModel.updatePriceInputState(true)
// viewModel.updateAmountInputState(false)
// selectedItem?.let {
// viewModel.updateSelectedItem(
// selectedItem!!.copy(
// recPriceExItemTemp = "",
// recPriceExItem = 0f,
// newRecUnitPriceTaxInTemp = value,
// newRecUnitPriceTaxIn = value.toSafeFloat()
// )
// )
// }
// }, inputType = InputType.Decimal,
// keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
// )
// RowInputLayout(
// label = "收货金额",
// value = selectedItem!!.recPriceExItemTemp,
// trailingLabel = "元",
// isInitUpdate = true,
// onValueChange = { value ->
// viewModel.updateAmountInputState(true)
// viewModel.updatePriceInputState(false)
// selectedItem?.let {
// viewModel.updateSelectedItem(
// selectedItem!!.copy(
// newRecUnitPriceTaxInTemp = "",
// newRecUnitPriceTaxIn = 0f,
//
// recPriceExItemTemp = value,
// recPriceExItem = value.toSafeFloat()
// )
// )
// }
// }, inputType = InputType.Decimal,
// keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
// )
//
// 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,
// onClick = InteractionUtils.rememberDoubleClickDetector {
// Timber.d("物品重量 双击")
// SensorScaleUtils.tare()
// }
// )
// }
//
// HorizontalDivider(
// modifier = Modifier
// .fillMaxWidth()
// .padding(),
// thickness = 1.dp,
// color = colorResource(R.color.divider)
// )
// Spacer(modifier = Modifier.height(20.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 {
// val errorInfo = selectedItem!!.hasNullField()
// if (errorInfo != null) {
// ToastUtils.showToast(errorInfo)
// return@clickable
// }
// val expectAmount =
// selectedItem!!.receivedNum!!.times(selectedItem!!.newRecUnitPriceTaxIn!!)
// .toSafeFloat()
// Timber.d("金额判断:期望金额 = $expectAmount, 实际金额 = ${selectedItem!!.recPriceExItem}")
//// if (expectAmount != selectedItem!!.recPriceExItem) {
//// ToastUtils.showToast("数量和金额不符,请检查后再试")
//// return@clickable
//// }
// viewModel.updatePurchaseItem(purchaseOrder = selectedItem!!.copy(isAdjusted = true))
// viewModel.updateSelectedItem(null)
// }, painter = painterResource(R.mipmap.ic_btn_confirm), contentDescription = "确定")
// }
// }
// }
//}
File diff suppressed because it is too large Load Diff
@@ -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,64 @@
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 gray999999TextStyle: TextStyle
@Composable get() = TextStyle(
fontSize = 24.sp,
fontWeight = FontWeight.Medium,
color = Color(0xFF999999)
)
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 blackMediumTextStyle: TextStyle
@Composable
get() = TextStyle(
color = colorResource(R.color.black),
fontSize = 24.sp,
fontWeight = FontWeight.Medium
)
val BlueTextStyle: TextStyle
@Composable
get() = TextStyle(
color = colorResource(R.color.blue),
fontSize = 24.sp,
fontWeight = FontWeight.Bold
)
}
@@ -0,0 +1,22 @@
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)
val Red_FF3232 = Color(0xFFFF3232)
@@ -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,105 @@
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.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.foundation.shape.RoundedCornerShape
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,
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.width(220.dp)
.height(100.dp)
.background(
color = colorResource(R.color.blue),
shape = RoundedCornerShape(10.dp)
)
.clickable {
onLeftButtonClick()
}
) {
Image(
painter = painterResource(R.mipmap.ic_back_white),
contentDescription = "返回",
modifier = Modifier.size(40.dp)
)
Spacer(modifier = Modifier.width(21.dp))
Text(
text = leftText,
style = TextStyle(
color = colorResource(R.color.white),
fontWeight = FontWeight.Bold,
fontSize = 36.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,193 @@
package com.sw.inbound.ui.weight
import android.net.Uri
import android.widget.Toast
import androidx.camera.view.CameraController
import androidx.camera.view.LifecycleCameraController
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.padding
import androidx.compose.foundation.layout.width
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.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 com.sw.inbound.utils.rememberPhotoCapture
import timber.log.Timber
/**
* 相机预览 -默认不预览,支持拍照
*/
@Composable
fun CameraCaptureLayout(
modifier: Modifier = Modifier
) {
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
)
}
}
// 创建拍照工具实例
val (photoCaptureHelper, takePhoto) = rememberPhotoCapture(
cameraController = cameraController,
onSuccess = { savedUri ->
// 处理拍照成功的逻辑
photoUri = savedUri
Timber.d("photoUri = $photoUri")
Toast.makeText(context, "图片已保存", Toast.LENGTH_SHORT).show()
showCamera = false
GlobalData.imageUri = photoUri
},
onError = { error ->
// 处理拍照失败的逻辑
Toast.makeText(context, error, Toast.LENGTH_SHORT).show()
GlobalData.imageUri = null
}
)
Column(
modifier = modifier
.width(428.dp)
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
if (showCamera) {
// 摄像头预览
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
}
takePhoto()
},
borderColor = colorResource(R.color.green),
textColor = colorResource(R.color.white),
fontSize = 30.sp,
showButtonIcon = true
)
}
}
// 生命周期管理
DisposableEffect(lifecycleOwner) {
Timber.d("cameraController")
cameraController.bindToLifecycle(lifecycleOwner)
onDispose { }
}
}
@@ -0,0 +1,52 @@
package com.sw.inbound.ui.weight
import android.net.Uri
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.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import coil.compose.AsyncImage
import com.sw.inbound.R
/**
* 相机预览
*/
@Composable
fun CameraPreview(
controller: LifecycleCameraController,
modifier: Modifier = Modifier,
photoUri: Uri? = null
) {
Box(modifier = modifier) {
AndroidView(
factory = { ctx ->
PreviewView(ctx).apply {
this.controller = controller
scaleType = PreviewView.ScaleType.FILL_CENTER // 控制预览缩放
}
},
modifier = Modifier.fillMaxSize()
)
if (photoUri != null) {
AsyncImage(
modifier = Modifier.fillMaxSize(),
model = photoUri,
contentDescription = "照片",
)
}
Image(
modifier = Modifier
.fillMaxSize()
.padding(30.dp),
painter = painterResource(R.mipmap.ic_scan),
contentDescription = "扫描"
)
}
}
@@ -0,0 +1,108 @@
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,113 @@
//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,
// enabled: Boolean = true,
//) {
// 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 || !enabled,
// 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,99 @@
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("搜索点击")
focusManager.clearFocus()
onSearchClick(searchText)
}
)
},
)
}
}
@@ -0,0 +1,29 @@
package com.sw.inbound.ui.weight
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import com.sw.inbound.ui.theme.AppTypography
/**
* 自定义单行文本
*/
@Composable
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,106 @@
//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) }
// var expanded by remember { mutableStateOf(true) }
//
// 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,226 @@
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.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
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, // 浮点
Percent, // 百分比
}
/**
* 自定义输入框
*/
@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,
onClick: () -> Unit = {}, // 点击
) {
val containerColor = if (enabled) Color.Transparent else Gray_DCDCF0
var inputValue by remember(if (isInitUpdate) value else null) {
mutableStateOf(value)
}
val focusManager = LocalFocusManager.current
fun onEditingComplete(isFocus: Boolean) {
Timber.d("onEditingComplete inputValue = $inputValue, isFocus = $isFocus")
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")
when (inputType) {
InputType.Number -> {
if (newValue.isEmpty() || newValue.isValidNumber()) {
inputValue = newValue
onEditingComplete(true)
}
}
InputType.Decimal -> {
if (newValue.isEmpty() || newValue.isValidFloat()) {
inputValue = newValue
onEditingComplete(true)
}
}
InputType.Percent -> {
val range: ClosedFloatingPointRange<Float> = 0f..100f
if (newValue.isEmpty() || (newValue.isValidFloat(1) && newValue.toFloat() in range)) {
inputValue = newValue
onEditingComplete(true)
}
}
else -> {
inputValue = newValue
onEditingComplete(true)
}
}
},
modifier = Modifier
.fillMaxWidth()
.background(Color.Transparent)
.border(
width = 2.dp,
color = Gray_DCDCF0,
shape = RoundedCornerShape(10.dp)
)
.clickable(onClick = onClick)
// .focusable()
.onFocusChanged(onFocusChanged = { focusState ->
{
onEditingComplete(focusState.isFocused)
}
}),
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 ->
{
Box(modifier = Modifier.padding(start = 10.dp, end = 20.dp)) {
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(false)
}, onDone = {
focusManager.clearFocus()
onEditingComplete(false)
})
)
}
}
@@ -0,0 +1,126 @@
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.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.foundation.shape.RoundedCornerShape
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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.sw.inbound.R
@Preview
@Composable
fun BackButton() {
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,
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.width(220.dp)
.height(100.dp)
.background(
color = colorResource(R.color.blue),
shape = RoundedCornerShape(10.dp)
)
.clickable {
}
) {
Image(
painter = painterResource(R.mipmap.ic_back_white),
contentDescription = "返回",
modifier = Modifier.size(40.dp)
)
Spacer(modifier = Modifier.width(21.dp))
Text(
text = "返回",
style = TextStyle(
color = colorResource(R.color.white),
fontWeight = FontWeight.Bold,
fontSize = 36.sp
)
)
}
// Row {
// if (false) {
// CustomOutlinedButton(
// modifier = Modifier
// .width(300.dp)
// .height(100.dp),
// text = "ok",
// fontSize = 36.sp,
// onClick = {}
// )
// Spacer(modifier = Modifier.width(20.dp))
// }
// CustomButton(
// modifier = Modifier
// .width(300.dp)
// .height(100.dp),
// text = "Button",
// onClick = {},
// borderColor = colorResource(R.color.blue),
// textColor = colorResource(R.color.white),
// fontSize = 36.sp
// )
// }
}
}
@Composable
fun BackButton2() {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.width(220.dp)
.height(100.dp)
.background(
color = colorResource(R.color.blue),
shape = RoundedCornerShape(10.dp)
)
.clickable {
}
) {
Image(
painter = painterResource(R.mipmap.ic_home2),
contentDescription = "返回",
modifier = Modifier.size(40.dp)
)
Spacer(modifier = Modifier.width(21.dp))
Text(
text = "返回",
style = TextStyle(
color = colorResource(R.color.white),
fontWeight = FontWeight.Bold,
fontSize = 36.sp
)
)
}
}

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