Compare commits
46
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c76164fa66 | ||
|
|
74fcd65f1b | ||
|
|
0dfe84898b | ||
|
|
5284cebf5e | ||
|
|
15e1f87913 | ||
|
|
ee84189fdf | ||
|
|
285238ed9c | ||
|
|
aa24059caa | ||
|
|
e63078dfd0 | ||
|
|
8b0f850d27 | ||
|
|
080f505c5d | ||
|
|
37289d43f3 | ||
|
|
fabc4d780a | ||
|
|
ac9494a0f3 | ||
|
|
fa58304ca1 | ||
|
|
1092a7c43d | ||
|
|
0c0aba2aeb | ||
|
|
e81df4f55c | ||
|
|
07e0a1996f | ||
|
|
79f58a36e3 | ||
|
|
7c5278bc88 | ||
|
|
c995daf48d | ||
|
|
d95a00b8a9 | ||
|
|
13f893cd0c | ||
|
|
8eae8c2b3c | ||
|
|
532f840a78 | ||
|
|
21b9b03014 | ||
|
|
4047cd67d8 | ||
|
|
441d40b584 | ||
|
|
915e20cd23 | ||
|
|
d0fa46a8c8 | ||
|
|
8a83de3238 | ||
|
|
4b241ba81b | ||
|
|
75306af483 | ||
|
|
c697b4aa65 | ||
|
|
baee019444 | ||
|
|
e50d6ceb60 | ||
|
|
7563209cf1 | ||
|
|
05015a7a18 | ||
|
|
1cc2358d72 | ||
|
|
1206ecd575 | ||
|
|
f53669d048 | ||
|
|
244013f99f | ||
|
|
a14c2c3348 | ||
|
|
26d9eabc5f | ||
|
|
301b91581b |
@@ -0,0 +1,52 @@
|
|||||||
|
请按以下步骤为当前改动生成 Git 提交信息并执行提交:
|
||||||
|
|
||||||
|
## 步骤
|
||||||
|
|
||||||
|
1. 并行运行以下命令,了解当前改动:
|
||||||
|
- `git diff --staged` 查看已暂存的变更内容
|
||||||
|
- `git diff` 查看未暂存的变更内容
|
||||||
|
- `git status` 查看变更文件列表
|
||||||
|
|
||||||
|
2. 若存在未暂存的文件,询问用户是否需要一起提交。
|
||||||
|
|
||||||
|
3. 根据变更内容,按以下规范生成提交信息:
|
||||||
|
|
||||||
|
**格式:** `<类型>(<范围>): <简短描述>`
|
||||||
|
|
||||||
|
| 类型 | 适用场景 |
|
||||||
|
|------|---------|
|
||||||
|
| feat | 新增功能 |
|
||||||
|
| fix | 修复 bug |
|
||||||
|
| refactor | 代码重构(不改变行为) |
|
||||||
|
| chore | 构建/配置/依赖调整 |
|
||||||
|
| docs | 文档更新 |
|
||||||
|
| perf | 性能优化 |
|
||||||
|
|
||||||
|
**范围参考本项目模块:**
|
||||||
|
- `face` — 人脸识别相关(LoginByFaceActivity、RecognizeViewModel)
|
||||||
|
- `serial` — 串口/开柜相关(SerialApi、PlateUtils)
|
||||||
|
- `network` — 网络层(ApiService、RemoteRepository、RequestInterceptor)
|
||||||
|
- `activity` — Activity 界面逻辑
|
||||||
|
- `fragment` — Fragment 界面逻辑
|
||||||
|
- `viewmodel` — ViewModel 层
|
||||||
|
- `ui` — 布局/样式调整
|
||||||
|
- `global` — GlobalData、MyApp 等全局配置
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
- `feat(face): 新增识别失败后自动采集人脸流程`
|
||||||
|
- `fix(serial): 修复开柜编号解析为负数导致串口指令异常`
|
||||||
|
- `refactor(network): 统一 API 地址使用 GlobalData.appBaseUrl 前缀`
|
||||||
|
|
||||||
|
4. 向用户展示拟提交的信息,确认后执行:
|
||||||
|
```
|
||||||
|
git add <相关文件>
|
||||||
|
git commit -m "<提交信息>"
|
||||||
|
```
|
||||||
|
|
||||||
|
5. 提交完成后运行 `git log --oneline -3` 确认结果。
|
||||||
|
|
||||||
|
## 约束
|
||||||
|
|
||||||
|
- 提交信息必须使用**简体中文**描述
|
||||||
|
- 不要使用 `--no-verify` 跳过钩子
|
||||||
|
- 不要在未经用户确认的情况下执行 `git push`
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
在本项目中新增一个后端 API 接口,需要同时修改三层:ApiService → RemoteRepository → ViewModel。
|
||||||
|
|
||||||
|
## 步骤
|
||||||
|
|
||||||
|
### 1. 收集信息
|
||||||
|
询问用户以下信息(如已在指令中提供则跳过):
|
||||||
|
- 接口名称/功能描述
|
||||||
|
- 请求方式:GET / POST / POST + FormUrlEncoded / Multipart
|
||||||
|
- 接口路径(相对路径,如 `/sideboard/app/xxx`)
|
||||||
|
- 请求参数字段和类型
|
||||||
|
- 响应数据类型(已有类 or 需新建)
|
||||||
|
- 归属哪个 ViewModel:`SettingViewModel` / `UserViewModel` / `DeviceViewModel`
|
||||||
|
|
||||||
|
### 2. 创建数据模型(如需要)
|
||||||
|
**请求参数类:** `app/src/main/java/com/sw/platecabinet/model/request/<XxxParam>.kt`
|
||||||
|
**响应数据类:** `app/src/main/java/com/sw/platecabinet/model/response/<XxxModel>.kt`
|
||||||
|
|
||||||
|
数据类规范:
|
||||||
|
```kotlin
|
||||||
|
/**
|
||||||
|
* <描述>
|
||||||
|
*/
|
||||||
|
data class XxxParam(
|
||||||
|
val fieldName: String // 字段说明
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 在 ApiService 中添加接口方法
|
||||||
|
文件:`app/src/main/java/com/sw/platecabinet/network/api/ApiService.kt`
|
||||||
|
|
||||||
|
**关键规则:**
|
||||||
|
- 必须使用 `@Url` 注解,URL 格式为:`"${GlobalData.appBaseUrl}/terminal/neglect/<路径>"`
|
||||||
|
- 不要使用相对路径(baseUrl 拦截器不适用于本项目)
|
||||||
|
- 方法必须为 `suspend` 函数,返回 `ApiResponse<T>`
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
/**
|
||||||
|
* <接口功能描述>
|
||||||
|
*/
|
||||||
|
@POST // 或 @GET / @FormUrlEncoded @POST / @Multipart @POST
|
||||||
|
suspend fun <methodName>(
|
||||||
|
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/<路径>",
|
||||||
|
@Body param: XxxParam // 或 @Query / @Field / @Part
|
||||||
|
): ApiResponse<XxxModel?>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 在 RemoteRepository 中添加调用方法
|
||||||
|
文件:`app/src/main/java/com/sw/platecabinet/repository/RemoteRepository.kt`
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
/**
|
||||||
|
* <功能描述>
|
||||||
|
*/
|
||||||
|
suspend fun <methodName>(/* 参数 */): ApiResponse<XxxModel?> {
|
||||||
|
return safeApiCall {
|
||||||
|
apiService.<methodName>(/* 参数传递 */)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
约束:
|
||||||
|
- 必须用 `safeApiCall { }` 包裹,不要裸调用 apiService
|
||||||
|
- 参数直接透传,不在 Repository 层做业务逻辑
|
||||||
|
|
||||||
|
### 5. 在 ViewModel 中添加调用逻辑
|
||||||
|
文件:`app/src/main/java/com/sw/platecabinet/viewmodel/<XxxViewModel>.kt`
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
/**
|
||||||
|
* <功能描述>
|
||||||
|
* @param callback 成功时回调,参数为响应数据
|
||||||
|
*/
|
||||||
|
fun <methodName>(/* 业务参数 */, callback: (XxxModel?) -> Unit) {
|
||||||
|
launch { // 不需要 loading 时用 launch;需要 loading 时用 launchWithLoading
|
||||||
|
val response = repository.<methodName>(/* 参数 */)
|
||||||
|
if (parseResponse(response)) {
|
||||||
|
runOnMainThread { callback(response.result) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意:** `launch` / `launchWithLoading` 在协程中执行,回调若需更新 UI 需切换到主线程(使用 `withContext(Dispatchers.Main)` 或在 Activity 中用 `runOnUiThread`)。
|
||||||
|
|
||||||
|
### 6. 在 Activity/Fragment 中调用
|
||||||
|
```kotlin
|
||||||
|
viewModel.<methodName>(/* 参数 */) { result ->
|
||||||
|
// 处理结果
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
- 所有注释使用简体中文
|
||||||
|
- 向用户展示将要修改的文件清单(含 Diff 预览),等待确认后再写文件
|
||||||
|
- `RequestInterceptor` 已自动注入认证 header,无需在方法中手动添加
|
||||||
|
- 响应体统一为 `ApiResponse<T>`,通过 `response.isSuccess()` 和 `response.result` 取数据
|
||||||
@@ -26,7 +26,7 @@ android {
|
|||||||
compileSdk = 35
|
compileSdk = 35
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.sw.inbound"
|
applicationId = "com.sw.inbound.new"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
@@ -147,7 +147,7 @@ dependencies {
|
|||||||
|
|
||||||
implementation("io.github.scwang90:refresh-layout-kernel:3.0.0-alpha")
|
implementation("io.github.scwang90:refresh-layout-kernel:3.0.0-alpha")
|
||||||
implementation("io.github.scwang90:refresh-header-classics:3.0.0-alpha")
|
implementation("io.github.scwang90:refresh-header-classics:3.0.0-alpha")
|
||||||
val objectboxVersion = "5.0.1"
|
val objectboxVersion:String by rootProject.extra
|
||||||
if (isDebug) {
|
if (isDebug) {
|
||||||
debugImplementation("io.objectbox:objectbox-android-objectbrowser:$objectboxVersion")
|
debugImplementation("io.objectbox:objectbox-android-objectbrowser:$objectboxVersion")
|
||||||
} else {
|
} else {
|
||||||
@@ -156,6 +156,9 @@ dependencies {
|
|||||||
|
|
||||||
implementation("org.greenrobot:eventbus:3.3.1")
|
implementation("org.greenrobot:eventbus:3.3.1")
|
||||||
|
|
||||||
|
// flexbox 布局,用于动态表单两列排列
|
||||||
|
implementation("com.google.android.flexbox:flexbox:3.0.0")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apply(plugin = "io.objectbox")
|
apply(plugin = "io.objectbox")
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
android:maxSdkVersion="28" />
|
android:maxSdkVersion="28" />
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".MyApp"
|
android:name=".MyApp"
|
||||||
@@ -40,6 +41,35 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".activity.HomeActivity"
|
android:name=".activity.HomeActivity"
|
||||||
android:configChanges="orientation|screenSize"
|
android:configChanges="orientation|screenSize"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:screenOrientation="landscape"
|
||||||
|
android:theme="@style/Theme.Inbound" />
|
||||||
|
|
||||||
|
<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="com.sw.inbound.activity.PurchaseOrderActivity" />
|
||||||
|
<activity
|
||||||
|
android:name="com.sw.inbound.activity.StoreActivity"
|
||||||
|
android:screenOrientation="landscape"
|
||||||
|
android:windowSoftInputMode="adjustPan" />
|
||||||
|
<activity android:name="com.sw.inbound.activity.FunActivity"
|
||||||
|
android:screenOrientation="landscape" />
|
||||||
|
<!-- 首页 Activity(XML 版本,对应 HomeScreen.kt Compose 版本) -->
|
||||||
|
<activity
|
||||||
|
android:name="com.sw.inbound.MainActivity"
|
||||||
|
android:screenOrientation="landscape" />
|
||||||
|
|
||||||
|
<!-- 初始化页面 Activity -->
|
||||||
|
<activity
|
||||||
|
android:name=".activity.InitActivity"
|
||||||
|
android:configChanges="orientation|screenSize"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:screenOrientation="landscape"
|
android:screenOrientation="landscape"
|
||||||
@@ -50,33 +80,6 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</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="com.sw.inbound.activity.PurchaseOrderActivity" />
|
|
||||||
<!-- 首页 Activity(XML 版本,对应 HomeScreen.kt Compose 版本) -->
|
|
||||||
<activity
|
|
||||||
android:name="com.sw.inbound.MainActivity"
|
|
||||||
android:screenOrientation="landscape" />
|
|
||||||
|
|
||||||
<!-- <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>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
Binary file not shown.
@@ -2,268 +2,472 @@
|
|||||||
"class_names": [
|
"class_names": [
|
||||||
"三黄鸡",
|
"三黄鸡",
|
||||||
"上海青WP2026020200041",
|
"上海青WP2026020200041",
|
||||||
|
"丝瓜WP2026032300114",
|
||||||
|
"乳瓜WP2026032000107",
|
||||||
|
"冬瓜WP2026032300115",
|
||||||
|
"冰糖",
|
||||||
"冰草WP2026020900054",
|
"冰草WP2026020900054",
|
||||||
"刀豆",
|
"刀豆",
|
||||||
"北豆腐WP2025111900007",
|
"北豆腐WP2025111900007",
|
||||||
|
"南瓜[倭瓜,番瓜]",
|
||||||
"味精WP2026013000034",
|
"味精WP2026013000034",
|
||||||
"圆生菜WP2026020900053",
|
"圆生菜WP2026020900053",
|
||||||
"圆白菜[卷心菜,包菜]WP2025111000063",
|
|
||||||
"基围虾WP2025110700019",
|
"基围虾WP2025110700019",
|
||||||
"多宝鱼WP2025112600011",
|
"多宝鱼WP2025112600011",
|
||||||
"大白菜(代表值)WP2026022800638",
|
"大白菜(代表值)WP2026031700085",
|
||||||
"大葱WP2026013000025",
|
"大葱WP2026013000026",
|
||||||
|
"大蒜(去皮)WP2026020200011",
|
||||||
|
"大蒜(带皮)WP2026020300046",
|
||||||
|
"奶白菜",
|
||||||
|
"姜(子姜)[嫩姜]",
|
||||||
"姜[黄姜]WP2026022800956",
|
"姜[黄姜]WP2026022800956",
|
||||||
"娃娃菜WP2026020200005",
|
"娃娃菜WP2026020200005",
|
||||||
"小白菜WP2026022800031",
|
"小白菜WP2026022800031",
|
||||||
"小米辣椒WP2026013000027",
|
"小米辣椒WP2026013000027",
|
||||||
"小葱WP2026013000029",
|
"小葱WP2026013000029",
|
||||||
|
"小鸡腿(带骨)",
|
||||||
"山药(带皮)WP2026020200008",
|
"山药(带皮)WP2026020200008",
|
||||||
"巨人笋",
|
"巨人笋",
|
||||||
|
"平菇[糙皮侧耳,青蘑]WP2026031700086",
|
||||||
"广东菜心WP2025120100014",
|
"广东菜心WP2025120100014",
|
||||||
|
"新世纪哈密瓜",
|
||||||
|
"明虾",
|
||||||
"木耳(干)[黑木耳,云耳]WP2026022801182",
|
"木耳(干)[黑木耳,云耳]WP2026022801182",
|
||||||
"杏鲍菇WP2026022700030",
|
"杏鲍菇WP2026022700030",
|
||||||
"松花蛋",
|
"松花蛋",
|
||||||
"松茸菌WP2025120100016",
|
"松茸菌WP2025120100016",
|
||||||
|
"槐花[洋槐花,豆槐花]",
|
||||||
|
"橙",
|
||||||
"沙葱",
|
"沙葱",
|
||||||
"洋葱(鲜)[葱头]WP2026022800612",
|
"油麦菜WP2026031200077",
|
||||||
"海带(浸)[江白菜,昆布]WP2026022800035",
|
"海带(浸)[江白菜,昆布]WP2026022800035",
|
||||||
|
"淀粉(马铃薯)",
|
||||||
|
"湿米线过桥米线",
|
||||||
|
"火龙果(带皮)",
|
||||||
|
"炒饼丝",
|
||||||
|
"牛肉(腹部肉)[牛腩]",
|
||||||
|
"牛肚",
|
||||||
"牛腩WP2025110700043",
|
"牛腩WP2025110700043",
|
||||||
"猪五花肉WP2026020300048",
|
"猪五花肉WP2026020300048",
|
||||||
"猪腿肉WP2025110700040",
|
"猪小排(带骨)",
|
||||||
|
"猪排(带骨)WP2026031200078",
|
||||||
|
"猪肉(瘦)",
|
||||||
|
"猪肉(腿)WP2026022800033",
|
||||||
|
"猪肉(里脊)WP2026021000025",
|
||||||
|
"猪肝(鲜)WP2026032000105",
|
||||||
|
"猪肾(fat8g)[猪腰子]",
|
||||||
"玉米WP2025120100017",
|
"玉米WP2025120100017",
|
||||||
"玉米糁(黄)WP2026022800038",
|
"玉米糁(黄)WP2026022800038",
|
||||||
|
"甜椒[灯笼椒,柿子椒]WP2026020500018",
|
||||||
|
"生菜(叶用莴苣)WP2026020500020",
|
||||||
|
"生蚝",
|
||||||
"番茄[西红柿]WP2026021000028",
|
"番茄[西红柿]WP2026021000028",
|
||||||
"白吉饼WP2026013000024",
|
"白吉饼WP2026013000024",
|
||||||
|
"白玉菇WP2026031100072",
|
||||||
|
"白菜薹[菜薹,菜心]WP2026031100068",
|
||||||
|
"白萝卜WP2026031200079",
|
||||||
|
"石斑鱼(整条鱼)",
|
||||||
"笋",
|
"笋",
|
||||||
"粉丝",
|
"粉丝",
|
||||||
"紫甘蓝",
|
"素鸡WP2026031700096",
|
||||||
"红心萝卜[心里美]WP2026022800368",
|
"红心萝卜[心里美]WP2026022800368",
|
||||||
"红提子WP2025111900006",
|
"红提子WP2025111900006",
|
||||||
"红椒WP2026030300055",
|
"红椒WP2026030300055",
|
||||||
|
"红洋葱WP2026030600054",
|
||||||
"红美人椒WP2026020300049",
|
"红美人椒WP2026020300049",
|
||||||
"红薯",
|
"红薯",
|
||||||
|
"红薯粉条WP2026031700091",
|
||||||
"红薯粉条(袋)",
|
"红薯粉条(袋)",
|
||||||
|
"结球甘蓝(紫)[圆白菜]WP2026020500017",
|
||||||
|
"结球甘蓝(绿)[圆白菜]WP2026032300116",
|
||||||
|
"绿豆芽WP2026031100073",
|
||||||
"羊排WP2025120300021",
|
"羊排WP2025120300021",
|
||||||
"羊血WP2026013000038",
|
"羊血WP2026013000038",
|
||||||
"胡萝卜WP2025110600008",
|
"胡萝卜WP2025110600008",
|
||||||
|
"腰果(熟)",
|
||||||
"芋头",
|
"芋头",
|
||||||
|
"芥蓝[甘蓝菜,盖蓝菜]",
|
||||||
|
"芦笋(绿)[石刁柏,龙须菜]",
|
||||||
"花卷",
|
"花卷",
|
||||||
"芹菜WP2025111000065",
|
"花椒(粉)",
|
||||||
|
"花生仁(生)WP2026032000102",
|
||||||
|
"花螺",
|
||||||
|
"芹菜(茎)[旱芹,药芹]WP2026022800032",
|
||||||
"苦菊",
|
"苦菊",
|
||||||
|
"苹果(代表值)WP2026032300117",
|
||||||
"茄子(紫皮,长)WP2025110700024",
|
"茄子(紫皮,长)WP2025110700024",
|
||||||
"茼蒿WP2026020200045",
|
"茼蒿WP2026020200045",
|
||||||
|
"草鱼",
|
||||||
|
"荷兰豆WP2026032000104",
|
||||||
"莲藕(带皮)",
|
"莲藕(带皮)",
|
||||||
"莴笋(鲜)[莴苣]WP2026020200009",
|
"莴笋(鲜)[莴苣]WP2026020200009",
|
||||||
"菜花(白色)[花椰菜]",
|
"菜花(白色)[花椰菜]WP2026030600057",
|
||||||
"菠菜(鲜)[赤根菜]WP2026022800700",
|
"菠菜(鲜)[赤根菜]WP2026022800700",
|
||||||
"蒜WP2025111400003",
|
"菠萝[凤梨,地菠萝]",
|
||||||
"蒜苗WP2026013000028",
|
"蒜苗WP2026013000028",
|
||||||
"蒜薹(圆)",
|
"蒜薹(圆)",
|
||||||
"蒸鱼豉油WP2026013000032",
|
"蒸鱼豉油WP2026013000032",
|
||||||
|
"蕹菜[空心菜,藤藤菜]",
|
||||||
"薏米[薏仁米,苡米]WP2026022800037",
|
"薏米[薏仁米,苡米]WP2026022800037",
|
||||||
"西兰花[绿菜花]WP2025110700029",
|
"虫草花(干)WP2026032000108",
|
||||||
"西葫芦",
|
"蛏子",
|
||||||
"豆腐皮WP2025111000059",
|
"西兰花[绿菜花]WP2026031100071",
|
||||||
"豆角",
|
"西瓜(代表值)",
|
||||||
|
"西芹[西洋芹菜,美芹]WP2026031100069",
|
||||||
|
"西葫芦WP2026021000027",
|
||||||
|
"豆腐干(代表值)",
|
||||||
|
"豆腐皮WP2026031700097",
|
||||||
|
"豆角豇豆WP2026021000026",
|
||||||
|
"赤小豆[小豆,红小豆](干)WP2026032300118",
|
||||||
"辣椒(红,小)WP2025111000066",
|
"辣椒(红,小)WP2025111000066",
|
||||||
"辣椒(青,尖)WP2026022800504",
|
"辣椒(青,尖)WP2026030600056",
|
||||||
|
"辣椒段(干)",
|
||||||
"醪糟",
|
"醪糟",
|
||||||
"金针菇WP2025111900010",
|
"金针菇[(鲜)[智力菇]WP2026030600051",
|
||||||
"青线椒WP2026013000031",
|
"青线椒WP2026013000031",
|
||||||
|
"青美人椒辣椒(青,尖)",
|
||||||
|
"青花椒鲜花椒",
|
||||||
|
"面条(生,代表值)WP2026030600055",
|
||||||
|
"面条(生,炒细面",
|
||||||
"韭菜WP2026020200044",
|
"韭菜WP2026020200044",
|
||||||
|
"韭薹",
|
||||||
"韭黄",
|
"韭黄",
|
||||||
|
"馒头(代表值)WP2026030600056",
|
||||||
"香椿[香椿芽]WP2026022801086",
|
"香椿[香椿芽]WP2026022801086",
|
||||||
"香菇",
|
"香菇(鲜)[香蕈,冬菇]WP2026030600050",
|
||||||
"香菜WP2026013000030",
|
"香菜WP2026013000030",
|
||||||
"马铃薯[土豆、洋芋]WP2026012900001",
|
"马铃薯[土豆、洋芋]WP2026012900001",
|
||||||
|
"鱿鱼(鲜,中国枪乌贼)[枪乌贼]",
|
||||||
|
"鲍鱼(带壳)",
|
||||||
|
"鲑鱼[大马哈鱼,三文鱼]",
|
||||||
|
"鲜猪精排",
|
||||||
"鸡全腿(带骨)",
|
"鸡全腿(带骨)",
|
||||||
"鸡翅(带骨)",
|
"鸡翅(带骨)",
|
||||||
|
"鸡胸脯肉WP2026022802698",
|
||||||
"鸡腿肉WP2025110700048",
|
"鸡腿肉WP2025110700048",
|
||||||
"鸡腿(带骨)",
|
"鸡蛋WP2026013000039",
|
||||||
"鸡蛋(代表值)WP2026013000039",
|
"鸭(均值)",
|
||||||
"麦芹",
|
"鸭蛋黄",
|
||||||
"黄瓜(鲜)[胡瓜]WP2025110700031",
|
"黄瓜(鲜)[胡瓜]WP2025110700031",
|
||||||
|
"黄豆",
|
||||||
"黄豆芽WP2026020200042",
|
"黄豆芽WP2026020200042",
|
||||||
|
"黑米WP2026031200081",
|
||||||
"黑袋子",
|
"黑袋子",
|
||||||
"黑鱼(整条鱼)WP2025110600012"
|
"黑鱼(整条鱼)WP2025110600012"
|
||||||
],
|
],
|
||||||
"class_to_idx": {
|
"class_to_idx": {
|
||||||
"三黄鸡": 0,
|
"三黄鸡": 0,
|
||||||
"上海青WP2026020200041": 1,
|
"上海青WP2026020200041": 1,
|
||||||
"冰草WP2026020900054": 2,
|
"丝瓜WP2026032300114": 2,
|
||||||
"刀豆": 3,
|
"乳瓜WP2026032000107": 3,
|
||||||
"北豆腐WP2025111900007": 4,
|
"冬瓜WP2026032300115": 4,
|
||||||
"味精WP2026013000034": 5,
|
"冰糖": 5,
|
||||||
"圆生菜WP2026020900053": 6,
|
"冰草WP2026020900054": 6,
|
||||||
"圆白菜[卷心菜,包菜]WP2025111000063": 7,
|
"刀豆": 7,
|
||||||
"基围虾WP2025110700019": 8,
|
"北豆腐WP2025111900007": 8,
|
||||||
"多宝鱼WP2025112600011": 9,
|
"南瓜[倭瓜,番瓜]": 9,
|
||||||
"大白菜(代表值)WP2026022800638": 10,
|
"味精WP2026013000034": 10,
|
||||||
"大葱WP2026013000025": 11,
|
"圆生菜WP2026020900053": 11,
|
||||||
"姜[黄姜]WP2026022800956": 12,
|
"基围虾WP2025110700019": 12,
|
||||||
"娃娃菜WP2026020200005": 13,
|
"多宝鱼WP2025112600011": 13,
|
||||||
"小白菜WP2026022800031": 14,
|
"大白菜(代表值)WP2026031700085": 14,
|
||||||
"小米辣椒WP2026013000027": 15,
|
"大葱WP2026013000026": 15,
|
||||||
"小葱WP2026013000029": 16,
|
"大蒜(去皮)WP2026020200011": 16,
|
||||||
"山药(带皮)WP2026020200008": 17,
|
"大蒜(带皮)WP2026020300046": 17,
|
||||||
"巨人笋": 18,
|
"奶白菜": 18,
|
||||||
"广东菜心WP2025120100014": 19,
|
"姜(子姜)[嫩姜]": 19,
|
||||||
"木耳(干)[黑木耳,云耳]WP2026022801182": 20,
|
"姜[黄姜]WP2026022800956": 20,
|
||||||
"杏鲍菇WP2026022700030": 21,
|
"娃娃菜WP2026020200005": 21,
|
||||||
"松花蛋": 22,
|
"小白菜WP2026022800031": 22,
|
||||||
"松茸菌WP2025120100016": 23,
|
"小米辣椒WP2026013000027": 23,
|
||||||
"沙葱": 24,
|
"小葱WP2026013000029": 24,
|
||||||
"洋葱(鲜)[葱头]WP2026022800612": 25,
|
"小鸡腿(带骨)": 25,
|
||||||
"海带(浸)[江白菜,昆布]WP2026022800035": 26,
|
"山药(带皮)WP2026020200008": 26,
|
||||||
"牛腩WP2025110700043": 27,
|
"巨人笋": 27,
|
||||||
"猪五花肉WP2026020300048": 28,
|
"平菇[糙皮侧耳,青蘑]WP2026031700086": 28,
|
||||||
"猪腿肉WP2025110700040": 29,
|
"广东菜心WP2025120100014": 29,
|
||||||
"玉米WP2025120100017": 30,
|
"新世纪哈密瓜": 30,
|
||||||
"玉米糁(黄)WP2026022800038": 31,
|
"明虾": 31,
|
||||||
"番茄[西红柿]WP2026021000028": 32,
|
"木耳(干)[黑木耳,云耳]WP2026022801182": 32,
|
||||||
"白吉饼WP2026013000024": 33,
|
"杏鲍菇WP2026022700030": 33,
|
||||||
"笋": 34,
|
"松花蛋": 34,
|
||||||
"粉丝": 35,
|
"松茸菌WP2025120100016": 35,
|
||||||
"紫甘蓝": 36,
|
"槐花[洋槐花,豆槐花]": 36,
|
||||||
"红心萝卜[心里美]WP2026022800368": 37,
|
"橙": 37,
|
||||||
"红提子WP2025111900006": 38,
|
"沙葱": 38,
|
||||||
"红椒WP2026030300055": 39,
|
"油麦菜WP2026031200077": 39,
|
||||||
"红美人椒WP2026020300049": 40,
|
"海带(浸)[江白菜,昆布]WP2026022800035": 40,
|
||||||
"红薯": 41,
|
"淀粉(马铃薯)": 41,
|
||||||
"红薯粉条(袋)": 42,
|
"湿米线过桥米线": 42,
|
||||||
"羊排WP2025120300021": 43,
|
"火龙果(带皮)": 43,
|
||||||
"羊血WP2026013000038": 44,
|
"炒饼丝": 44,
|
||||||
"胡萝卜WP2025110600008": 45,
|
"牛肉(腹部肉)[牛腩]": 45,
|
||||||
"芋头": 46,
|
"牛肚": 46,
|
||||||
"花卷": 47,
|
"牛腩WP2025110700043": 47,
|
||||||
"芹菜WP2025111000065": 48,
|
"猪五花肉WP2026020300048": 48,
|
||||||
"苦菊": 49,
|
"猪小排(带骨)": 49,
|
||||||
"茄子(紫皮,长)WP2025110700024": 50,
|
"猪排(带骨)WP2026031200078": 50,
|
||||||
"茼蒿WP2026020200045": 51,
|
"猪肉(瘦)": 51,
|
||||||
"莲藕(带皮)": 52,
|
"猪肉(腿)WP2026022800033": 52,
|
||||||
"莴笋(鲜)[莴苣]WP2026020200009": 53,
|
"猪肉(里脊)WP2026021000025": 53,
|
||||||
"菜花(白色)[花椰菜]": 54,
|
"猪肝(鲜)WP2026032000105": 54,
|
||||||
"菠菜(鲜)[赤根菜]WP2026022800700": 55,
|
"猪肾(fat8g)[猪腰子]": 55,
|
||||||
"蒜WP2025111400003": 56,
|
"玉米WP2025120100017": 56,
|
||||||
"蒜苗WP2026013000028": 57,
|
"玉米糁(黄)WP2026022800038": 57,
|
||||||
"蒜薹(圆)": 58,
|
"甜椒[灯笼椒,柿子椒]WP2026020500018": 58,
|
||||||
"蒸鱼豉油WP2026013000032": 59,
|
"生菜(叶用莴苣)WP2026020500020": 59,
|
||||||
"薏米[薏仁米,苡米]WP2026022800037": 60,
|
"生蚝": 60,
|
||||||
"西兰花[绿菜花]WP2025110700029": 61,
|
"番茄[西红柿]WP2026021000028": 61,
|
||||||
"西葫芦": 62,
|
"白吉饼WP2026013000024": 62,
|
||||||
"豆腐皮WP2025111000059": 63,
|
"白玉菇WP2026031100072": 63,
|
||||||
"豆角": 64,
|
"白菜薹[菜薹,菜心]WP2026031100068": 64,
|
||||||
"辣椒(红,小)WP2025111000066": 65,
|
"白萝卜WP2026031200079": 65,
|
||||||
"辣椒(青,尖)WP2026022800504": 66,
|
"石斑鱼(整条鱼)": 66,
|
||||||
"醪糟": 67,
|
"笋": 67,
|
||||||
"金针菇WP2025111900010": 68,
|
"粉丝": 68,
|
||||||
"青线椒WP2026013000031": 69,
|
"素鸡WP2026031700096": 69,
|
||||||
"韭菜WP2026020200044": 70,
|
"红心萝卜[心里美]WP2026022800368": 70,
|
||||||
"韭黄": 71,
|
"红提子WP2025111900006": 71,
|
||||||
"香椿[香椿芽]WP2026022801086": 72,
|
"红椒WP2026030300055": 72,
|
||||||
"香菇": 73,
|
"红洋葱WP2026030600054": 73,
|
||||||
"香菜WP2026013000030": 74,
|
"红美人椒WP2026020300049": 74,
|
||||||
"马铃薯[土豆、洋芋]WP2026012900001": 75,
|
"红薯": 75,
|
||||||
"鸡全腿(带骨)": 76,
|
"红薯粉条WP2026031700091": 76,
|
||||||
"鸡翅(带骨)": 77,
|
"红薯粉条(袋)": 77,
|
||||||
"鸡腿肉WP2025110700048": 78,
|
"结球甘蓝(紫)[圆白菜]WP2026020500017": 78,
|
||||||
"鸡腿(带骨)": 79,
|
"结球甘蓝(绿)[圆白菜]WP2026032300116": 79,
|
||||||
"鸡蛋(代表值)WP2026013000039": 80,
|
"绿豆芽WP2026031100073": 80,
|
||||||
"麦芹": 81,
|
"羊排WP2025120300021": 81,
|
||||||
"黄瓜(鲜)[胡瓜]WP2025110700031": 82,
|
"羊血WP2026013000038": 82,
|
||||||
"黄豆芽WP2026020200042": 83,
|
"胡萝卜WP2025110600008": 83,
|
||||||
"黑袋子": 84,
|
"腰果(熟)": 84,
|
||||||
"黑鱼(整条鱼)WP2025110600012": 85
|
"芋头": 85,
|
||||||
|
"芥蓝[甘蓝菜,盖蓝菜]": 86,
|
||||||
|
"芦笋(绿)[石刁柏,龙须菜]": 87,
|
||||||
|
"花卷": 88,
|
||||||
|
"花椒(粉)": 89,
|
||||||
|
"花生仁(生)WP2026032000102": 90,
|
||||||
|
"花螺": 91,
|
||||||
|
"芹菜(茎)[旱芹,药芹]WP2026022800032": 92,
|
||||||
|
"苦菊": 93,
|
||||||
|
"苹果(代表值)WP2026032300117": 94,
|
||||||
|
"茄子(紫皮,长)WP2025110700024": 95,
|
||||||
|
"茼蒿WP2026020200045": 96,
|
||||||
|
"草鱼": 97,
|
||||||
|
"荷兰豆WP2026032000104": 98,
|
||||||
|
"莲藕(带皮)": 99,
|
||||||
|
"莴笋(鲜)[莴苣]WP2026020200009": 100,
|
||||||
|
"菜花(白色)[花椰菜]WP2026030600057": 101,
|
||||||
|
"菠菜(鲜)[赤根菜]WP2026022800700": 102,
|
||||||
|
"菠萝[凤梨,地菠萝]": 103,
|
||||||
|
"蒜苗WP2026013000028": 104,
|
||||||
|
"蒜薹(圆)": 105,
|
||||||
|
"蒸鱼豉油WP2026013000032": 106,
|
||||||
|
"蕹菜[空心菜,藤藤菜]": 107,
|
||||||
|
"薏米[薏仁米,苡米]WP2026022800037": 108,
|
||||||
|
"虫草花(干)WP2026032000108": 109,
|
||||||
|
"蛏子": 110,
|
||||||
|
"西兰花[绿菜花]WP2026031100071": 111,
|
||||||
|
"西瓜(代表值)": 112,
|
||||||
|
"西芹[西洋芹菜,美芹]WP2026031100069": 113,
|
||||||
|
"西葫芦WP2026021000027": 114,
|
||||||
|
"豆腐干(代表值)": 115,
|
||||||
|
"豆腐皮WP2026031700097": 116,
|
||||||
|
"豆角豇豆WP2026021000026": 117,
|
||||||
|
"赤小豆[小豆,红小豆](干)WP2026032300118": 118,
|
||||||
|
"辣椒(红,小)WP2025111000066": 119,
|
||||||
|
"辣椒(青,尖)WP2026030600056": 120,
|
||||||
|
"辣椒段(干)": 121,
|
||||||
|
"醪糟": 122,
|
||||||
|
"金针菇[(鲜)[智力菇]WP2026030600051": 123,
|
||||||
|
"青线椒WP2026013000031": 124,
|
||||||
|
"青美人椒辣椒(青,尖)": 125,
|
||||||
|
"青花椒鲜花椒": 126,
|
||||||
|
"面条(生,代表值)WP2026030600055": 127,
|
||||||
|
"面条(生,炒细面": 128,
|
||||||
|
"韭菜WP2026020200044": 129,
|
||||||
|
"韭薹": 130,
|
||||||
|
"韭黄": 131,
|
||||||
|
"馒头(代表值)WP2026030600056": 132,
|
||||||
|
"香椿[香椿芽]WP2026022801086": 133,
|
||||||
|
"香菇(鲜)[香蕈,冬菇]WP2026030600050": 134,
|
||||||
|
"香菜WP2026013000030": 135,
|
||||||
|
"马铃薯[土豆、洋芋]WP2026012900001": 136,
|
||||||
|
"鱿鱼(鲜,中国枪乌贼)[枪乌贼]": 137,
|
||||||
|
"鲍鱼(带壳)": 138,
|
||||||
|
"鲑鱼[大马哈鱼,三文鱼]": 139,
|
||||||
|
"鲜猪精排": 140,
|
||||||
|
"鸡全腿(带骨)": 141,
|
||||||
|
"鸡翅(带骨)": 142,
|
||||||
|
"鸡胸脯肉WP2026022802698": 143,
|
||||||
|
"鸡腿肉WP2025110700048": 144,
|
||||||
|
"鸡蛋WP2026013000039": 145,
|
||||||
|
"鸭(均值)": 146,
|
||||||
|
"鸭蛋黄": 147,
|
||||||
|
"黄瓜(鲜)[胡瓜]WP2025110700031": 148,
|
||||||
|
"黄豆": 149,
|
||||||
|
"黄豆芽WP2026020200042": 150,
|
||||||
|
"黑米WP2026031200081": 151,
|
||||||
|
"黑袋子": 152,
|
||||||
|
"黑鱼(整条鱼)WP2025110600012": 153
|
||||||
},
|
},
|
||||||
"idx_to_class": {
|
"idx_to_class": {
|
||||||
"0": "三黄鸡",
|
"0": "三黄鸡",
|
||||||
"1": "上海青WP2026020200041",
|
"1": "上海青WP2026020200041",
|
||||||
"2": "冰草WP2026020900054",
|
"2": "丝瓜WP2026032300114",
|
||||||
"3": "刀豆",
|
"3": "乳瓜WP2026032000107",
|
||||||
"4": "北豆腐WP2025111900007",
|
"4": "冬瓜WP2026032300115",
|
||||||
"5": "味精WP2026013000034",
|
"5": "冰糖",
|
||||||
"6": "圆生菜WP2026020900053",
|
"6": "冰草WP2026020900054",
|
||||||
"7": "圆白菜[卷心菜,包菜]WP2025111000063",
|
"7": "刀豆",
|
||||||
"8": "基围虾WP2025110700019",
|
"8": "北豆腐WP2025111900007",
|
||||||
"9": "多宝鱼WP2025112600011",
|
"9": "南瓜[倭瓜,番瓜]",
|
||||||
"10": "大白菜(代表值)WP2026022800638",
|
"10": "味精WP2026013000034",
|
||||||
"11": "大葱WP2026013000025",
|
"11": "圆生菜WP2026020900053",
|
||||||
"12": "姜[黄姜]WP2026022800956",
|
"12": "基围虾WP2025110700019",
|
||||||
"13": "娃娃菜WP2026020200005",
|
"13": "多宝鱼WP2025112600011",
|
||||||
"14": "小白菜WP2026022800031",
|
"14": "大白菜(代表值)WP2026031700085",
|
||||||
"15": "小米辣椒WP2026013000027",
|
"15": "大葱WP2026013000026",
|
||||||
"16": "小葱WP2026013000029",
|
"16": "大蒜(去皮)WP2026020200011",
|
||||||
"17": "山药(带皮)WP2026020200008",
|
"17": "大蒜(带皮)WP2026020300046",
|
||||||
"18": "巨人笋",
|
"18": "奶白菜",
|
||||||
"19": "广东菜心WP2025120100014",
|
"19": "姜(子姜)[嫩姜]",
|
||||||
"20": "木耳(干)[黑木耳,云耳]WP2026022801182",
|
"20": "姜[黄姜]WP2026022800956",
|
||||||
"21": "杏鲍菇WP2026022700030",
|
"21": "娃娃菜WP2026020200005",
|
||||||
"22": "松花蛋",
|
"22": "小白菜WP2026022800031",
|
||||||
"23": "松茸菌WP2025120100016",
|
"23": "小米辣椒WP2026013000027",
|
||||||
"24": "沙葱",
|
"24": "小葱WP2026013000029",
|
||||||
"25": "洋葱(鲜)[葱头]WP2026022800612",
|
"25": "小鸡腿(带骨)",
|
||||||
"26": "海带(浸)[江白菜,昆布]WP2026022800035",
|
"26": "山药(带皮)WP2026020200008",
|
||||||
"27": "牛腩WP2025110700043",
|
"27": "巨人笋",
|
||||||
"28": "猪五花肉WP2026020300048",
|
"28": "平菇[糙皮侧耳,青蘑]WP2026031700086",
|
||||||
"29": "猪腿肉WP2025110700040",
|
"29": "广东菜心WP2025120100014",
|
||||||
"30": "玉米WP2025120100017",
|
"30": "新世纪哈密瓜",
|
||||||
"31": "玉米糁(黄)WP2026022800038",
|
"31": "明虾",
|
||||||
"32": "番茄[西红柿]WP2026021000028",
|
"32": "木耳(干)[黑木耳,云耳]WP2026022801182",
|
||||||
"33": "白吉饼WP2026013000024",
|
"33": "杏鲍菇WP2026022700030",
|
||||||
"34": "笋",
|
"34": "松花蛋",
|
||||||
"35": "粉丝",
|
"35": "松茸菌WP2025120100016",
|
||||||
"36": "紫甘蓝",
|
"36": "槐花[洋槐花,豆槐花]",
|
||||||
"37": "红心萝卜[心里美]WP2026022800368",
|
"37": "橙",
|
||||||
"38": "红提子WP2025111900006",
|
"38": "沙葱",
|
||||||
"39": "红椒WP2026030300055",
|
"39": "油麦菜WP2026031200077",
|
||||||
"40": "红美人椒WP2026020300049",
|
"40": "海带(浸)[江白菜,昆布]WP2026022800035",
|
||||||
"41": "红薯",
|
"41": "淀粉(马铃薯)",
|
||||||
"42": "红薯粉条(袋)",
|
"42": "湿米线过桥米线",
|
||||||
"43": "羊排WP2025120300021",
|
"43": "火龙果(带皮)",
|
||||||
"44": "羊血WP2026013000038",
|
"44": "炒饼丝",
|
||||||
"45": "胡萝卜WP2025110600008",
|
"45": "牛肉(腹部肉)[牛腩]",
|
||||||
"46": "芋头",
|
"46": "牛肚",
|
||||||
"47": "花卷",
|
"47": "牛腩WP2025110700043",
|
||||||
"48": "芹菜WP2025111000065",
|
"48": "猪五花肉WP2026020300048",
|
||||||
"49": "苦菊",
|
"49": "猪小排(带骨)",
|
||||||
"50": "茄子(紫皮,长)WP2025110700024",
|
"50": "猪排(带骨)WP2026031200078",
|
||||||
"51": "茼蒿WP2026020200045",
|
"51": "猪肉(瘦)",
|
||||||
"52": "莲藕(带皮)",
|
"52": "猪肉(腿)WP2026022800033",
|
||||||
"53": "莴笋(鲜)[莴苣]WP2026020200009",
|
"53": "猪肉(里脊)WP2026021000025",
|
||||||
"54": "菜花(白色)[花椰菜]",
|
"54": "猪肝(鲜)WP2026032000105",
|
||||||
"55": "菠菜(鲜)[赤根菜]WP2026022800700",
|
"55": "猪肾(fat8g)[猪腰子]",
|
||||||
"56": "蒜WP2025111400003",
|
"56": "玉米WP2025120100017",
|
||||||
"57": "蒜苗WP2026013000028",
|
"57": "玉米糁(黄)WP2026022800038",
|
||||||
"58": "蒜薹(圆)",
|
"58": "甜椒[灯笼椒,柿子椒]WP2026020500018",
|
||||||
"59": "蒸鱼豉油WP2026013000032",
|
"59": "生菜(叶用莴苣)WP2026020500020",
|
||||||
"60": "薏米[薏仁米,苡米]WP2026022800037",
|
"60": "生蚝",
|
||||||
"61": "西兰花[绿菜花]WP2025110700029",
|
"61": "番茄[西红柿]WP2026021000028",
|
||||||
"62": "西葫芦",
|
"62": "白吉饼WP2026013000024",
|
||||||
"63": "豆腐皮WP2025111000059",
|
"63": "白玉菇WP2026031100072",
|
||||||
"64": "豆角",
|
"64": "白菜薹[菜薹,菜心]WP2026031100068",
|
||||||
"65": "辣椒(红,小)WP2025111000066",
|
"65": "白萝卜WP2026031200079",
|
||||||
"66": "辣椒(青,尖)WP2026022800504",
|
"66": "石斑鱼(整条鱼)",
|
||||||
"67": "醪糟",
|
"67": "笋",
|
||||||
"68": "金针菇WP2025111900010",
|
"68": "粉丝",
|
||||||
"69": "青线椒WP2026013000031",
|
"69": "素鸡WP2026031700096",
|
||||||
"70": "韭菜WP2026020200044",
|
"70": "红心萝卜[心里美]WP2026022800368",
|
||||||
"71": "韭黄",
|
"71": "红提子WP2025111900006",
|
||||||
"72": "香椿[香椿芽]WP2026022801086",
|
"72": "红椒WP2026030300055",
|
||||||
"73": "香菇",
|
"73": "红洋葱WP2026030600054",
|
||||||
"74": "香菜WP2026013000030",
|
"74": "红美人椒WP2026020300049",
|
||||||
"75": "马铃薯[土豆、洋芋]WP2026012900001",
|
"75": "红薯",
|
||||||
"76": "鸡全腿(带骨)",
|
"76": "红薯粉条WP2026031700091",
|
||||||
"77": "鸡翅(带骨)",
|
"77": "红薯粉条(袋)",
|
||||||
"78": "鸡腿肉WP2025110700048",
|
"78": "结球甘蓝(紫)[圆白菜]WP2026020500017",
|
||||||
"79": "鸡腿(带骨)",
|
"79": "结球甘蓝(绿)[圆白菜]WP2026032300116",
|
||||||
"80": "鸡蛋(代表值)WP2026013000039",
|
"80": "绿豆芽WP2026031100073",
|
||||||
"81": "麦芹",
|
"81": "羊排WP2025120300021",
|
||||||
"82": "黄瓜(鲜)[胡瓜]WP2025110700031",
|
"82": "羊血WP2026013000038",
|
||||||
"83": "黄豆芽WP2026020200042",
|
"83": "胡萝卜WP2025110600008",
|
||||||
"84": "黑袋子",
|
"84": "腰果(熟)",
|
||||||
"85": "黑鱼(整条鱼)WP2025110600012"
|
"85": "芋头",
|
||||||
|
"86": "芥蓝[甘蓝菜,盖蓝菜]",
|
||||||
|
"87": "芦笋(绿)[石刁柏,龙须菜]",
|
||||||
|
"88": "花卷",
|
||||||
|
"89": "花椒(粉)",
|
||||||
|
"90": "花生仁(生)WP2026032000102",
|
||||||
|
"91": "花螺",
|
||||||
|
"92": "芹菜(茎)[旱芹,药芹]WP2026022800032",
|
||||||
|
"93": "苦菊",
|
||||||
|
"94": "苹果(代表值)WP2026032300117",
|
||||||
|
"95": "茄子(紫皮,长)WP2025110700024",
|
||||||
|
"96": "茼蒿WP2026020200045",
|
||||||
|
"97": "草鱼",
|
||||||
|
"98": "荷兰豆WP2026032000104",
|
||||||
|
"99": "莲藕(带皮)",
|
||||||
|
"100": "莴笋(鲜)[莴苣]WP2026020200009",
|
||||||
|
"101": "菜花(白色)[花椰菜]WP2026030600057",
|
||||||
|
"102": "菠菜(鲜)[赤根菜]WP2026022800700",
|
||||||
|
"103": "菠萝[凤梨,地菠萝]",
|
||||||
|
"104": "蒜苗WP2026013000028",
|
||||||
|
"105": "蒜薹(圆)",
|
||||||
|
"106": "蒸鱼豉油WP2026013000032",
|
||||||
|
"107": "蕹菜[空心菜,藤藤菜]",
|
||||||
|
"108": "薏米[薏仁米,苡米]WP2026022800037",
|
||||||
|
"109": "虫草花(干)WP2026032000108",
|
||||||
|
"110": "蛏子",
|
||||||
|
"111": "西兰花[绿菜花]WP2026031100071",
|
||||||
|
"112": "西瓜(代表值)",
|
||||||
|
"113": "西芹[西洋芹菜,美芹]WP2026031100069",
|
||||||
|
"114": "西葫芦WP2026021000027",
|
||||||
|
"115": "豆腐干(代表值)",
|
||||||
|
"116": "豆腐皮WP2026031700097",
|
||||||
|
"117": "豆角豇豆WP2026021000026",
|
||||||
|
"118": "赤小豆[小豆,红小豆](干)WP2026032300118",
|
||||||
|
"119": "辣椒(红,小)WP2025111000066",
|
||||||
|
"120": "辣椒(青,尖)WP2026030600056",
|
||||||
|
"121": "辣椒段(干)",
|
||||||
|
"122": "醪糟",
|
||||||
|
"123": "金针菇[(鲜)[智力菇]WP2026030600051",
|
||||||
|
"124": "青线椒WP2026013000031",
|
||||||
|
"125": "青美人椒辣椒(青,尖)",
|
||||||
|
"126": "青花椒鲜花椒",
|
||||||
|
"127": "面条(生,代表值)WP2026030600055",
|
||||||
|
"128": "面条(生,炒细面",
|
||||||
|
"129": "韭菜WP2026020200044",
|
||||||
|
"130": "韭薹",
|
||||||
|
"131": "韭黄",
|
||||||
|
"132": "馒头(代表值)WP2026030600056",
|
||||||
|
"133": "香椿[香椿芽]WP2026022801086",
|
||||||
|
"134": "香菇(鲜)[香蕈,冬菇]WP2026030600050",
|
||||||
|
"135": "香菜WP2026013000030",
|
||||||
|
"136": "马铃薯[土豆、洋芋]WP2026012900001",
|
||||||
|
"137": "鱿鱼(鲜,中国枪乌贼)[枪乌贼]",
|
||||||
|
"138": "鲍鱼(带壳)",
|
||||||
|
"139": "鲑鱼[大马哈鱼,三文鱼]",
|
||||||
|
"140": "鲜猪精排",
|
||||||
|
"141": "鸡全腿(带骨)",
|
||||||
|
"142": "鸡翅(带骨)",
|
||||||
|
"143": "鸡胸脯肉WP2026022802698",
|
||||||
|
"144": "鸡腿肉WP2025110700048",
|
||||||
|
"145": "鸡蛋WP2026013000039",
|
||||||
|
"146": "鸭(均值)",
|
||||||
|
"147": "鸭蛋黄",
|
||||||
|
"148": "黄瓜(鲜)[胡瓜]WP2025110700031",
|
||||||
|
"149": "黄豆",
|
||||||
|
"150": "黄豆芽WP2026020200042",
|
||||||
|
"151": "黑米WP2026031200081",
|
||||||
|
"152": "黑袋子",
|
||||||
|
"153": "黑鱼(整条鱼)WP2025110600012"
|
||||||
},
|
},
|
||||||
"embedding_dim": 512,
|
"embedding_dim": 512,
|
||||||
"total_images": 2070,
|
"total_images": 3180,
|
||||||
"build_time": "2026-03-06T09:38:05.028631"
|
"build_time": "2026-04-27T14:13:11.594418"
|
||||||
}
|
}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -2,7 +2,10 @@ package com.sw.inbound
|
|||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.sw.inbound.model.response.DictType
|
import com.sw.inbound.model.response.DictType
|
||||||
|
import com.sw.inbound.model.response.SpecOption
|
||||||
|
import com.sw.inbound.model.response.SupplierOption
|
||||||
import com.sw.inbound.model.response.User
|
import com.sw.inbound.model.response.User
|
||||||
|
import com.sw.inbound.model.response.ZoneOption
|
||||||
|
|
||||||
object GlobalData {
|
object GlobalData {
|
||||||
/**
|
/**
|
||||||
@@ -41,10 +44,30 @@ object GlobalData {
|
|||||||
var unitTypeList: List<DictType> = arrayListOf()
|
var unitTypeList: List<DictType> = arrayListOf()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 具体业务baseurl
|
* 入库供应商列表(入库秤使用)
|
||||||
*/
|
*/
|
||||||
//var appBaseUrl: String = "http://192.168.1.201:14801"
|
var inboundSupplierList: List<SupplierOption> = emptyList()
|
||||||
var appBaseUrl: String = "https://dev.yixiong-tech.com:8081"
|
|
||||||
|
/**
|
||||||
|
* 入库仓库区域列表(入库秤使用)
|
||||||
|
*/
|
||||||
|
var inboundZoneList: List<ZoneOption> = emptyList()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规格
|
||||||
|
*/
|
||||||
|
var specOptionList: List<SpecOption> = emptyList()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 具体业务 BaseUrl
|
||||||
|
*/
|
||||||
|
// const val TEST_BASE_URL = "http://192.168.1.201:14801"
|
||||||
|
// const val LOCAL_BASE_URL = "http://192.168.10.118:14801"
|
||||||
|
const val LOCAL_BASE_URL = "http://192.168.10.101:24801";
|
||||||
|
// const val TEST_BASE_URL = "https://dev.yixiong-tech.com:8083"
|
||||||
|
const val TEST_BASE_URL = "https://dev.yixiong-tech.com:8081"
|
||||||
|
const val PROD_BASE_URL = "https://api.dm.yixiong-tech.com:8443"
|
||||||
|
var appBaseUrl: String = ""
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设备id
|
* 设备id
|
||||||
@@ -66,4 +89,5 @@ object GlobalKey {
|
|||||||
const val KEY_TOKEN = "tokenKey"
|
const val KEY_TOKEN = "tokenKey"
|
||||||
const val KEY_USER_INFO = "userInfoKey"
|
const val KEY_USER_INFO = "userInfoKey"
|
||||||
const val KEY_USER_NAME = "userNameKey"
|
const val KEY_USER_NAME = "userNameKey"
|
||||||
|
const val KEY_BASE_URL = "baseUrlKey"
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,12 @@
|
|||||||
package com.sw.inbound.activity
|
package com.sw.inbound.activity
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.graphics.Bitmap
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import com.sw.inbound.base.BaseActivity
|
import com.sw.inbound.base.BaseActivity
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
@@ -17,8 +15,8 @@ import androidx.core.view.updateLayoutParams
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import coil.ImageLoader
|
|
||||||
import coil.annotation.ExperimentalCoilApi
|
import coil.annotation.ExperimentalCoilApi
|
||||||
|
import coil.imageLoader
|
||||||
import com.sw.inbound.R
|
import com.sw.inbound.R
|
||||||
import com.sw.inbound.adapter.FoodCollectionAdapter
|
import com.sw.inbound.adapter.FoodCollectionAdapter
|
||||||
import com.sw.inbound.adapter.GoodsSearchAdapter
|
import com.sw.inbound.adapter.GoodsSearchAdapter
|
||||||
@@ -39,6 +37,8 @@ import com.sw.inbound.utils.BitmapSaver
|
|||||||
import com.sw.inbound.utils.CameraUtils
|
import com.sw.inbound.utils.CameraUtils
|
||||||
import com.sw.inbound.utils.ImageUploader
|
import com.sw.inbound.utils.ImageUploader
|
||||||
import com.sw.inbound.utils.ImageUtil
|
import com.sw.inbound.utils.ImageUtil
|
||||||
|
import com.sw.inbound.utils.LogSaveUtil
|
||||||
|
import com.sw.inbound.utils.NetworkUtils
|
||||||
import com.sw.inbound.utils.ext.addOnActionSearchListener
|
import com.sw.inbound.utils.ext.addOnActionSearchListener
|
||||||
import com.sw.inbound.utils.ext.clickWithDebounce
|
import com.sw.inbound.utils.ext.clickWithDebounce
|
||||||
import com.sw.inbound.utils.ext.dp
|
import com.sw.inbound.utils.ext.dp
|
||||||
@@ -47,21 +47,20 @@ import com.sw.inbound.utils.ext.hideKeyboard
|
|||||||
import com.sw.inbound.utils.ext.invisible
|
import com.sw.inbound.utils.ext.invisible
|
||||||
import com.sw.inbound.utils.ext.startActivity
|
import com.sw.inbound.utils.ext.startActivity
|
||||||
import com.sw.inbound.utils.ext.toJsonString
|
import com.sw.inbound.utils.ext.toJsonString
|
||||||
import com.sw.inbound.utils.ext.toObject
|
|
||||||
import com.sw.inbound.utils.ext.toast
|
import com.sw.inbound.utils.ext.toast
|
||||||
import com.sw.inbound.utils.ext.visible
|
import com.sw.inbound.utils.ext.visible
|
||||||
import com.sw.inbound.viewmodel.ReceiptViewModel
|
import com.sw.inbound.viewmodel.ReceiptViewModel
|
||||||
|
import com.sw.inbound.viewmodel.UserViewModel
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import io.objectbox.Box
|
|
||||||
import io.objectbox.exception.DbException
|
import io.objectbox.exception.DbException
|
||||||
import io.objectbox.exception.FileCorruptException
|
import io.objectbox.exception.FileCorruptException
|
||||||
import io.objectbox.kotlin.boxFor
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 采集菜品
|
* 采集菜品
|
||||||
@@ -76,13 +75,12 @@ class FoodCollectionActivity : BaseActivity() {
|
|||||||
const val PAGE_SIZE = 30
|
const val PAGE_SIZE = 30
|
||||||
}
|
}
|
||||||
|
|
||||||
// private var box: Box<Food>? = null
|
private val collectList: MutableList<FoodCollectionBean> = mutableListOf()
|
||||||
private val collectList: MutableList<FoodCollectionBean> = mutableListOf<FoodCollectionBean>()
|
|
||||||
private lateinit var binding: ActivityFoodCollectionBinding
|
private lateinit var binding: ActivityFoodCollectionBinding
|
||||||
private lateinit var previewView: PreviewView
|
private lateinit var previewView: PreviewView
|
||||||
|
|
||||||
val viewModel: ReceiptViewModel by viewModels()
|
private val receiptViewModel: ReceiptViewModel by viewModels()
|
||||||
|
val userViewModel: UserViewModel by viewModels()
|
||||||
private val cameraUtils: CameraUtils by lazy {
|
private val cameraUtils: CameraUtils by lazy {
|
||||||
CameraUtils(this)
|
CameraUtils(this)
|
||||||
}
|
}
|
||||||
@@ -90,9 +88,14 @@ class FoodCollectionActivity : BaseActivity() {
|
|||||||
FoodCollectionAdapter(collectList).apply {
|
FoodCollectionAdapter(collectList).apply {
|
||||||
setOnItemClickListener { adapter, view, position ->
|
setOnItemClickListener { adapter, view, position ->
|
||||||
val file = collectList[position].imageFile ?: return@setOnItemClickListener
|
val file = collectList[position].imageFile ?: return@setOnItemClickListener
|
||||||
|
// 磁盘读取和 TFLite 推理均为耗时操作,切换到 IO 线程执行
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
ImageUtil.decodeFromFile(file)?.let { bitmap ->
|
ImageUtil.decodeFromFile(file)?.let { bitmap ->
|
||||||
val imageVector = FoodModule.bitmap2FloatArray(bitmap, false)
|
val imageVector = FoodModule.bitmap2FloatArray(bitmap, false)
|
||||||
Timber.tag(TAG).d("向量:${imageVector.toJsonString()}")
|
Timber.tag(TAG).d("向量:${imageVector.toJsonString()}")
|
||||||
|
// 向量提取完毕后立即回收,避免内存泄漏
|
||||||
|
if (bitmap.isRecycled.not()) bitmap.recycle()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
addOnItemChildClickListener(R.id.ivDelete) { _, _, position ->
|
addOnItemChildClickListener(R.id.ivDelete) { _, _, position ->
|
||||||
@@ -105,8 +108,6 @@ class FoodCollectionActivity : BaseActivity() {
|
|||||||
it.uploadSuccess = false
|
it.uploadSuccess = false
|
||||||
}
|
}
|
||||||
collectionAdapter.notifyItemChanged(position)
|
collectionAdapter.notifyItemChanged(position)
|
||||||
// val count = collectList.count { it.imageFile != null && it.uploadSuccess.not() }
|
|
||||||
// binding.btnUploadImage.text = "待上传图片${count}张"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -128,7 +129,6 @@ class FoodCollectionActivity : BaseActivity() {
|
|||||||
binding.llCameraFlag.visible()
|
binding.llCameraFlag.visible()
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
binding = ActivityFoodCollectionBinding.inflate(layoutInflater)
|
binding = ActivityFoodCollectionBinding.inflate(layoutInflater)
|
||||||
@@ -163,14 +163,6 @@ class FoodCollectionActivity : BaseActivity() {
|
|||||||
searchGoods()
|
searchGoods()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// binding.btnUploadImage.setOnClickListener {
|
|
||||||
// val count = collectList.count { it.imageFile!=null && it.uploadSuccess.not() }
|
|
||||||
// if (count == 0) {
|
|
||||||
// toast("已全部上传成功")
|
|
||||||
// return@setOnClickListener
|
|
||||||
// }
|
|
||||||
// uploadAllImage()
|
|
||||||
// }
|
|
||||||
binding.btnSave.setOnClickListener { _ ->
|
binding.btnSave.setOnClickListener { _ ->
|
||||||
if (clickIndex == -1) {
|
if (clickIndex == -1) {
|
||||||
toast("请选择物品名称")
|
toast("请选择物品名称")
|
||||||
@@ -181,8 +173,6 @@ class FoodCollectionActivity : BaseActivity() {
|
|||||||
toast("请拍摄物品照片")
|
toast("请拍摄物品照片")
|
||||||
return@setOnClickListener
|
return@setOnClickListener
|
||||||
}
|
}
|
||||||
// vectorThread()
|
|
||||||
// uploadAllImage()
|
|
||||||
showUploadDialog()
|
showUploadDialog()
|
||||||
}
|
}
|
||||||
binding.btnTakePhoto.clickWithDebounce(800) {
|
binding.btnTakePhoto.clickWithDebounce(800) {
|
||||||
@@ -210,18 +200,11 @@ class FoodCollectionActivity : BaseActivity() {
|
|||||||
|
|
||||||
loadEmptyView()
|
loadEmptyView()
|
||||||
|
|
||||||
binding.tvTitle.setOnClickListener {
|
// 点击"图片"按钮,跳转至本地图片预览页
|
||||||
// startActivity<LocalImagePreviewActivity> { }
|
binding.tvImagePreview.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 fun showUploadDialog() {
|
private fun showUploadDialog() {
|
||||||
@@ -233,6 +216,10 @@ class FoodCollectionActivity : BaseActivity() {
|
|||||||
|
|
||||||
},
|
},
|
||||||
confirmBlock = {
|
confirmBlock = {
|
||||||
|
if (NetworkUtils.isNetworkConnected(this).not()) {
|
||||||
|
toast("网络异常,请稍后重试")
|
||||||
|
return@WarnDialog
|
||||||
|
}
|
||||||
upload()
|
upload()
|
||||||
}
|
}
|
||||||
).show()
|
).show()
|
||||||
@@ -240,6 +227,8 @@ class FoodCollectionActivity : BaseActivity() {
|
|||||||
|
|
||||||
private var pageNo = 1
|
private var pageNo = 1
|
||||||
private var clickIndex = -1
|
private var clickIndex = -1
|
||||||
|
// 保存当前搜索任务,用于在新搜索发起时取消上一次未完成的请求
|
||||||
|
private var searchJob: Job? = null
|
||||||
|
|
||||||
private val searchGoodsList: MutableList<SearchGoodsInfo.Record> = mutableListOf()
|
private val searchGoodsList: MutableList<SearchGoodsInfo.Record> = mutableListOf()
|
||||||
private val searchAdapter by lazy {
|
private val searchAdapter by lazy {
|
||||||
@@ -269,7 +258,6 @@ class FoodCollectionActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
|
||||||
private fun takePhoto() {
|
private fun takePhoto() {
|
||||||
val count = collectList.count { it.imageFile != null }
|
val count = collectList.count { it.imageFile != null }
|
||||||
if (count == MAX_COUNT) {
|
if (count == MAX_COUNT) {
|
||||||
@@ -280,10 +268,12 @@ class FoodCollectionActivity : BaseActivity() {
|
|||||||
cameraUtils.takePhoto(cameraCallback)
|
cameraUtils.takePhoto(cameraCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
|
||||||
private fun clearData() {
|
private fun clearData() {
|
||||||
val count = collectList.count { it.imageFile != null }
|
val count = collectList.count { it.imageFile != null }
|
||||||
if (count == 0) return
|
if (count == 0) return
|
||||||
|
// 取消正在进行的搜索,防止清空后回调重新填充数据
|
||||||
|
searchJob?.cancel()
|
||||||
|
searchJob = null
|
||||||
clearCoilCache(this)
|
clearCoilCache(this)
|
||||||
collectList.forEach {
|
collectList.forEach {
|
||||||
try {
|
try {
|
||||||
@@ -308,7 +298,6 @@ class FoodCollectionActivity : BaseActivity() {
|
|||||||
searchAdapter.notifyDataSetChanged()
|
searchAdapter.notifyDataSetChanged()
|
||||||
loadEmptyView()
|
loadEmptyView()
|
||||||
binding.ivFinish.invisible()
|
binding.ivFinish.invisible()
|
||||||
// binding.btnUploadImage.text = "待上传图片${0}张"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val cameraCallback: (Uri) -> Unit = { uri ->
|
private val cameraCallback: (Uri) -> Unit = { uri ->
|
||||||
@@ -321,48 +310,77 @@ class FoodCollectionActivity : BaseActivity() {
|
|||||||
val index = collectList.indexOfFirst { it.imageFile == null }
|
val index = collectList.indexOfFirst { it.imageFile == null }
|
||||||
if (index == -1) {
|
if (index == -1) {
|
||||||
toast("每次只允许保存${MAX_COUNT}条数据")
|
toast("每次只允许保存${MAX_COUNT}条数据")
|
||||||
showWaitingDialog2("加载中")
|
hideWaitingDialog()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// toast("index=$index")
|
// 将 bitmap/cropBitmap 提到外层,确保任意退出路径都能在 finally 中回收,避免内存泄漏
|
||||||
|
var bitmap: Bitmap? = null
|
||||||
|
var cropBitmap: Bitmap? = null
|
||||||
try {
|
try {
|
||||||
ImageUtil.uriToBitmap(this, uri)?.let { bitmap ->
|
// 所有耗时操作(磁盘 I/O、图像裁剪、TFLite 推理)切换到 IO 线程,避免阻塞主线程
|
||||||
val cropBitmap = BitmapCropper.cropCenter(
|
bitmap = withContext(Dispatchers.IO) {
|
||||||
|
ImageUtil.uriToBitmap(this@FoodCollectionActivity, uri)
|
||||||
|
}
|
||||||
|
if (bitmap == null) {
|
||||||
|
// uriToBitmap 返回 null 时关闭 loading,否则转圈永不消失
|
||||||
|
hideWaitingDialog()
|
||||||
|
toast("图片读取失败,请重试")
|
||||||
|
LogSaveUtil.saveLogFile("图片读取失败,请重试")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cropBitmap = withContext(Dispatchers.IO) {
|
||||||
|
BitmapCropper.cropCenter(
|
||||||
original = bitmap,
|
original = bitmap,
|
||||||
targetWidth = 1000, targetHeight = 1300,
|
targetWidth = 1000, targetHeight = 1300,
|
||||||
offsetX = 30, offsetY = 100
|
offsetX = 30, offsetY = 100
|
||||||
)
|
)
|
||||||
initBox()
|
}
|
||||||
val imageVector = try {
|
val imageVector = try {
|
||||||
|
// TFLite 推理为 CPU 密集型,切换到 IO 线程执行
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
FoodModule.bitmap2FloatArray(cropBitmap, false)
|
FoodModule.bitmap2FloatArray(cropBitmap, false)
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
hideWaitingDialog()
|
hideWaitingDialog()
|
||||||
toast("操作失败")
|
toast("操作失败")
|
||||||
return@let
|
LogSaveUtil.saveLogFile("操作失败:${e.message}")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
Timber.tag(TAG).d("向量:${imageVector.toJsonString()}")
|
Timber.tag(TAG).d("向量:${imageVector.toJsonString()}")
|
||||||
val foodList = try {
|
val foodList = try {
|
||||||
|
// 数据库查询切换到 IO 线程
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
FoodModule.queryFoodNameScore(imageVector)
|
FoodModule.queryFoodNameScore(imageVector)
|
||||||
|
}
|
||||||
} catch (e: DbException) {
|
} catch (e: DbException) {
|
||||||
|
hideWaitingDialog()
|
||||||
try {
|
try {
|
||||||
ObjectBox.init(this)
|
// ObjectBox 重新初始化为磁盘 I/O,切换到 IO 线程
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
ObjectBox.init(this@FoodCollectionActivity)
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
toast("数据库异常,请稍后重试")
|
toast("数据库异常,请稍后重试")
|
||||||
return@let
|
LogSaveUtil.saveLogFile("数据库异常,请稍后重试:${e.message}")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
val filterList =
|
val filterList =
|
||||||
foodList.filter { it.score < FoodModule.BAG_RATE && it.name.contains("黑袋子") }
|
foodList.filter { it.score < FoodModule.BAG_RATE && it.name.contains("黑袋子") }
|
||||||
if (filterList.isNotEmpty()) {
|
if (filterList.isNotEmpty()) {
|
||||||
hideWaitingDialog()
|
hideWaitingDialog()
|
||||||
toast("当前物品可能被袋子遮挡,请检查后重试")
|
toast("当前物品可能被袋子遮挡,请检查后重试")
|
||||||
return@let
|
LogSaveUtil.saveLogFile("当前物品可能被袋子遮挡,请检查后重试")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
val file = BitmapSaver.saveToAppFilesDir(
|
// 图片保存为磁盘 I/O,切换到 IO 线程
|
||||||
cropBitmap, this, "IMG_CROP_${System.currentTimeMillis()}.jpg"
|
val file = withContext(Dispatchers.IO) {
|
||||||
|
BitmapSaver.saveToAppFilesDir(
|
||||||
|
cropBitmap, this@FoodCollectionActivity, "IMG_CROP_${System.currentTimeMillis()}.jpg"
|
||||||
)
|
)
|
||||||
|
}
|
||||||
Timber.d("${this.javaClass.simpleName}-searchFood-裁剪bitmap保存文件路径:${file?.absolutePath}")
|
Timber.d("${this.javaClass.simpleName}-searchFood-裁剪bitmap保存文件路径:${file?.absolutePath}")
|
||||||
collectList[index].let {
|
collectList[index].let {
|
||||||
it.imageVector = imageVector
|
it.imageVector = imageVector
|
||||||
@@ -373,16 +391,7 @@ class FoodCollectionActivity : BaseActivity() {
|
|||||||
it.uploadSuccess = false
|
it.uploadSuccess = false
|
||||||
}
|
}
|
||||||
collectionAdapter.notifyItemChanged(index)
|
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()
|
|
||||||
}
|
|
||||||
hideWaitingDialog()
|
hideWaitingDialog()
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
hideWaitingDialog()
|
hideWaitingDialog()
|
||||||
@@ -392,14 +401,23 @@ class FoodCollectionActivity : BaseActivity() {
|
|||||||
e is DbException
|
e is DbException
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
ObjectBox.init(this)
|
// ObjectBox 重新初始化为磁盘 I/O,切换到 IO 线程
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
ObjectBox.init(this@FoodCollectionActivity)
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
toast("请重试")
|
toast("请重试")
|
||||||
|
LogSaveUtil.saveLogFile("请重试:${e.message}")
|
||||||
} else {
|
} else {
|
||||||
toast("程序异常$errMsg")
|
toast("程序异常$errMsg")
|
||||||
|
LogSaveUtil.saveLogFile("程序异常$errMsg:${e.message}")
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
// 无论成功、失败还是提前返回,均在此回收 bitmap,彻底消除内存泄漏
|
||||||
|
if (cropBitmap?.isRecycled == false) cropBitmap.recycle()
|
||||||
|
if (bitmap?.isRecycled == false) bitmap.recycle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,15 +425,13 @@ class FoodCollectionActivity : BaseActivity() {
|
|||||||
Loading.show(this)
|
Loading.show(this)
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
collectList
|
collectList
|
||||||
//.filter { it.bitmap != null }
|
|
||||||
// .filter { it.imageVector != null }
|
|
||||||
.forEachIndexed { index, it ->
|
.forEachIndexed { index, it ->
|
||||||
//image2VectorTask(it.bitmap!!, index)
|
|
||||||
it.imageVector?.let { imageVector ->
|
it.imageVector?.let { imageVector ->
|
||||||
image2VectorTask(imageVector = imageVector, position = index)
|
image2VectorTask(imageVector = imageVector, position = index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
runOnUiThread {
|
// lifecycleScope 默认在主线程,直接切换到主线程调度器更新 UI
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
window.decorView.postDelayed({
|
window.decorView.postDelayed({
|
||||||
Loading.dismiss()
|
Loading.dismiss()
|
||||||
toast("保存成功")
|
toast("保存成功")
|
||||||
@@ -425,30 +441,19 @@ class FoodCollectionActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun image2VectorTask(
|
private suspend fun image2VectorTask(
|
||||||
bitmap: Bitmap? = null,
|
imageVector: FloatArray,
|
||||||
imageVector: FloatArray? = null,
|
|
||||||
position: Int
|
position: Int
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
initBox()
|
|
||||||
// val imageVector = try {
|
|
||||||
// FoodModule.bitmap2FloatArray(bitmap)
|
|
||||||
// } catch (e: Exception) {
|
|
||||||
// e.printStackTrace()
|
|
||||||
// //toast("保存失败")
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
val goods = searchGoodsList[clickIndex]
|
val goods = searchGoodsList[clickIndex]
|
||||||
val saveName = goods.goodsName + goods.goodsCode
|
val saveName = goods.goodsName + goods.goodsCode
|
||||||
// ObjectBox.boxStore.runInTx {
|
// ObjectBox 写入为磁盘 I/O,切换到 IO 线程避免主线程卡顿
|
||||||
// box?.put(Food(name = saveName, foodIdx = 0, foodVector = imageVector))
|
withContext(Dispatchers.IO) {
|
||||||
// }
|
|
||||||
ObjectBox.put(Food(name = saveName, foodIdx = 0, foodVector = imageVector))
|
ObjectBox.put(Food(name = saveName, foodIdx = 0, foodVector = imageVector))
|
||||||
collectList[position].let {
|
|
||||||
// it.imageVector = imageVector
|
|
||||||
it.isFinish = true
|
|
||||||
}
|
}
|
||||||
runOnUiThread {
|
collectList[position].isFinish = true
|
||||||
|
// 切换到主线程刷新列表项,与协程调度器保持一致
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
collectionAdapter.notifyItemChanged(position)
|
collectionAdapter.notifyItemChanged(position)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -456,7 +461,6 @@ class FoodCollectionActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
|
||||||
private fun searchGoods() {
|
private fun searchGoods() {
|
||||||
val searchName = binding.etGoodsInput.text.toString().trim()
|
val searchName = binding.etGoodsInput.text.toString().trim()
|
||||||
if (searchName.isBlank()) {
|
if (searchName.isBlank()) {
|
||||||
@@ -511,77 +515,54 @@ class FoodCollectionActivity : BaseActivity() {
|
|||||||
pageSize: Int = PAGE_SIZE,
|
pageSize: Int = PAGE_SIZE,
|
||||||
callback: (List<SearchGoodsInfo.Record>) -> Unit
|
callback: (List<SearchGoodsInfo.Record>) -> Unit
|
||||||
) {
|
) {
|
||||||
runBlocking {
|
// 取消上一次未完成的搜索,避免并发竞争导致结果乱序
|
||||||
val records = viewModel.searchGoodsInfoList2(
|
searchJob?.cancel()
|
||||||
|
searchJob = lifecycleScope.launch {
|
||||||
|
val records = receiptViewModel.searchGoodsInfoList2(
|
||||||
goodsName = searchName,
|
goodsName = searchName,
|
||||||
pageNo = pageNo,
|
pageNo = pageNo,
|
||||||
pageSize = pageSize
|
pageSize = pageSize
|
||||||
)
|
)
|
||||||
runOnUiThread {
|
|
||||||
callback(records)
|
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() {
|
private fun upload() {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val totalFileCount = collectList.count { it.imageFile != null }
|
val totalFileCount = collectList.count { it.imageFile != null }
|
||||||
showWaitingDialog2("图片上传中0/$totalFileCount")
|
showWaitingDialog2("图片上传中0/$totalFileCount")
|
||||||
val goods = searchGoodsList[clickIndex]
|
val goods = searchGoodsList[clickIndex]
|
||||||
val params = HashMap<String, RequestBody>()
|
val params = HashMap<String, RequestBody>()
|
||||||
params["goodsId"] = goods.id!!.toRequestBody()
|
// 使用安全调用避免 id/goodsName 为 null 时崩溃,字段缺失则跳过上传
|
||||||
params["goodsName"] = goods.goodsName!!.toRequestBody()
|
val goodsId = goods.id ?: run {
|
||||||
//params["foodVector"] = foodVector.toRequestBody()
|
hideWaitingDialog()
|
||||||
|
toast("商品数据异常,请重新选择")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
val goodsName = goods.goodsName ?: run {
|
||||||
|
hideWaitingDialog()
|
||||||
|
toast("商品数据异常,请重新选择")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
params["goodsId"] = goodsId.toRequestBody()
|
||||||
|
params["goodsName"] = goodsName.toRequestBody()
|
||||||
ImageUploader(
|
ImageUploader(
|
||||||
totalList = collectList,
|
totalList = collectList,
|
||||||
uploadImage = { batch ->
|
uploadImage = { batch ->
|
||||||
val files = batch.map { it.imageFile }
|
val files = batch.map { it.imageFile }
|
||||||
viewModel.uploadMultipleImages(files = files, params)
|
receiptViewModel.uploadMultipleImages(files = files, params)
|
||||||
},
|
},
|
||||||
onProgress = { count, batch ->
|
onProgress = { count, batch ->
|
||||||
runOnUiThread {
|
// lifecycleScope 默认在主线程执行,无需 runOnUiThread
|
||||||
showWaitingDialog2("图片上传中$count/$totalFileCount")
|
showWaitingDialog2("图片上传中$count/$totalFileCount")
|
||||||
batch.forEach {
|
batch.forEach {
|
||||||
it.uploadSuccess = true
|
it.uploadSuccess = true
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onError = {
|
onError = {
|
||||||
runOnUiThread {
|
// lifecycleScope 默认在主线程执行,无需 runOnUiThread
|
||||||
hideWaitingDialog()
|
hideWaitingDialog()
|
||||||
toast("上传失败,请稍后重试")
|
toast("上传失败,请稍后重试")
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onComplete = {
|
onComplete = {
|
||||||
hideWaitingDialog()
|
hideWaitingDialog()
|
||||||
@@ -591,10 +572,9 @@ class FoodCollectionActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val receiptViewModel: ReceiptViewModel by viewModels()
|
|
||||||
|
|
||||||
fun searchGoodsInfoList(goodsName: String, block: (List<SearchGoodsInfo.Record>) -> Unit) {
|
fun searchGoodsInfoList(goodsName: String, block: (List<SearchGoodsInfo.Record>) -> Unit) {
|
||||||
runBlocking {
|
// 使用协程异步执行,避免阻塞主线程
|
||||||
|
lifecycleScope.launch {
|
||||||
val list = searchGoodsInfoList2(goodsName)
|
val list = searchGoodsInfoList2(goodsName)
|
||||||
block(list)
|
block(list)
|
||||||
}
|
}
|
||||||
@@ -605,25 +585,35 @@ class FoodCollectionActivity : BaseActivity() {
|
|||||||
pageNo: Int = 1,
|
pageNo: Int = 1,
|
||||||
pageSize: Int = 10
|
pageSize: Int = 10
|
||||||
): List<SearchGoodsInfo.Record> {
|
): List<SearchGoodsInfo.Record> {
|
||||||
|
// 统一使用类顶部声明的 viewModel,避免重复创建 ViewModel 实例
|
||||||
return receiptViewModel.searchGoodsInfoList2(goodsName, pageNo, pageSize)
|
return receiptViewModel.searchGoodsInfoList2(goodsName, pageNo, pageSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var mDialogWaiting: CustomDialog? = null
|
private var mDialogWaiting: CustomDialog? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 显示等待提示框
|
* 内部方法:创建并显示等待提示框(调用前须确保已在主线程)
|
||||||
|
* showWaitingDialog 与 showWaitingDialog2 共用此创建逻辑,避免重复代码
|
||||||
|
*/
|
||||||
|
private fun createWaitingDialog(tip: String?) {
|
||||||
|
mDialogWaiting?.dismiss()
|
||||||
|
mDialogWaiting = null
|
||||||
|
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).apply {
|
||||||
|
show()
|
||||||
|
setCanceledOnTouchOutside(true)
|
||||||
|
setCancelable(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示等待提示框(每次调用都重新创建对话框)
|
||||||
*/
|
*/
|
||||||
fun showWaitingDialog(tip: String?) {
|
fun showWaitingDialog(tip: String?) {
|
||||||
runOnUiThread {
|
runOnUiThread { createWaitingDialog(tip) }
|
||||||
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!!.setCanceledOnTouchOutside(true)
|
|
||||||
mDialogWaiting!!.setCancelable(true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -636,19 +626,18 @@ class FoodCollectionActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示或更新等待提示框
|
||||||
|
* 若对话框已存在则仅更新文字,否则创建新对话框
|
||||||
|
*/
|
||||||
fun showWaitingDialog2(tip: String?) {
|
fun showWaitingDialog2(tip: String?) {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
if (mDialogWaiting == null) {
|
if (mDialogWaiting == null) {
|
||||||
hideWaitingDialog()
|
createWaitingDialog(tip)
|
||||||
val view = View.inflate(this, R.layout.dialog_waiting, null)
|
} else {
|
||||||
mDialogWaiting = CustomDialog(this, view, R.style.MyDialog)
|
// 对话框已显示,仅更新提示文字
|
||||||
mDialogWaiting!!.show()
|
mDialogWaiting?.findViewById<TextView>(R.id.tvTip)?.text = tip
|
||||||
mDialogWaiting!!.setCanceledOnTouchOutside(true)
|
|
||||||
mDialogWaiting!!.setCancelable(true)
|
|
||||||
}
|
}
|
||||||
val contentView = mDialogWaiting?.findViewById<ViewGroup>(android.R.id.content)
|
|
||||||
val tvTip = contentView?.findViewById<TextView>(R.id.tvTip)
|
|
||||||
tvTip?.text = tip
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -657,128 +646,9 @@ class FoodCollectionActivity : BaseActivity() {
|
|||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalCoilApi::class)
|
@OptIn(ExperimentalCoilApi::class)
|
||||||
fun clearCoilCache(context: Context) {
|
fun clearCoilCache(context: Context) {
|
||||||
val imageLoader = ImageLoader.Builder(context).build()
|
// 使用 Coil 全局单例,避免每次新建 ImageLoader 实例导致清除无效
|
||||||
// 清除内存缓存
|
context.imageLoader.memoryCache?.clear()
|
||||||
imageLoader.memoryCache?.clear()
|
context.imageLoader.diskCache?.clear()
|
||||||
// 清除磁盘缓存
|
|
||||||
imageLoader.diskCache?.clear()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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,298 @@
|
|||||||
|
package com.sw.inbound.activity
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.reflect.TypeToken
|
||||||
|
import com.sw.inbound.base.BaseActivity
|
||||||
|
import com.sw.inbound.databinding.ActivityFunBinding
|
||||||
|
import com.sw.inbound.objbox.Food
|
||||||
|
import com.sw.inbound.objbox.FoodClassInfo
|
||||||
|
import com.sw.inbound.objbox.ObjectBox
|
||||||
|
import com.sw.inbound.utils.ext.toast
|
||||||
|
import com.sw.inbound.utils.ZipDownloadUtils
|
||||||
|
import com.sw.inbound.utils.ZipExtractUtils
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 趣味页面 Activity
|
||||||
|
* 用于测试 Zip 文件下载、解压和数据库操作
|
||||||
|
*/
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class FunActivity : BaseActivity() {
|
||||||
|
|
||||||
|
private lateinit var binding: ActivityFunBinding
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding = ActivityFunBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
initViews()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化各控件的点击事件
|
||||||
|
*/
|
||||||
|
private fun initViews() {
|
||||||
|
// 返回按钮点击事件
|
||||||
|
binding.ivBack.setOnClickListener {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载按钮
|
||||||
|
binding.btnDownload.setOnClickListener {
|
||||||
|
downloadZipFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解压按钮
|
||||||
|
binding.btnExtract.setOnClickListener {
|
||||||
|
extractZipFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取并保存按钮
|
||||||
|
binding.btnLoadAndSave.setOnClickListener {
|
||||||
|
loadAndSaveToDatabase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载 Zip 文件
|
||||||
|
*/
|
||||||
|
private fun downloadZipFile() {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
updateProgress("开始下载...")
|
||||||
|
binding.btnDownload.isEnabled = false
|
||||||
|
|
||||||
|
val zipFile = File(cacheDir, "model_resources.zip")
|
||||||
|
// 假的下载地址
|
||||||
|
val downloadUrl = "https://dev.yixiong-tech.com:8081/common/files/20260313/embedding_model_resources.zip"
|
||||||
|
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
ZipDownloadUtils.downloadZip(
|
||||||
|
downloadUrl = downloadUrl,
|
||||||
|
targetFile = zipFile,
|
||||||
|
listener = object : ZipDownloadUtils.DownloadProgressListener {
|
||||||
|
override fun onProgress(currentSize: Long, totalSize: Long, progress: Int) {
|
||||||
|
updateProgress("下载中... $progress%")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSuccess(file: File) {
|
||||||
|
updateProgress("下载完成!文件大小: ${formatFileSize(file.length())}")
|
||||||
|
Timber.d("Zip 文件下载完成: ${file.absolutePath}")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(exception: Exception) {
|
||||||
|
updateProgress("下载失败: ${exception.message}")
|
||||||
|
Timber.e(exception, "下载失败")
|
||||||
|
this@FunActivity.toast("下载失败: ${exception.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "下载异常")
|
||||||
|
updateProgress("异常: ${e.message}")
|
||||||
|
this@FunActivity.toast("异常: ${e.message}")
|
||||||
|
} finally {
|
||||||
|
binding.btnDownload.isEnabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解压 Zip 文件
|
||||||
|
*/
|
||||||
|
private fun extractZipFile() {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
updateProgress("开始解压...")
|
||||||
|
binding.btnExtract.isEnabled = false
|
||||||
|
|
||||||
|
val zipFile = File(cacheDir, "model_resources.zip")
|
||||||
|
val targetDir = File(cacheDir, "model_resources")
|
||||||
|
|
||||||
|
if (!zipFile.exists()) {
|
||||||
|
updateProgress("Zip 文件不存在,请先下载")
|
||||||
|
this@FunActivity.toast("Zip 文件不存在,请先下载")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
ZipExtractUtils.extractZip(
|
||||||
|
zipFile = zipFile,
|
||||||
|
targetDir = targetDir,
|
||||||
|
listener = object : ZipExtractUtils.ExtractProgressListener {
|
||||||
|
override fun onProgress(
|
||||||
|
currentFile: String,
|
||||||
|
currentIndex: Int,
|
||||||
|
totalFiles: Int,
|
||||||
|
progress: Int
|
||||||
|
) {
|
||||||
|
updateProgress("解压中... $progress% ($currentIndex/$totalFiles)")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSuccess(targetDir: File) {
|
||||||
|
updateProgress("解压完成!目录: ${targetDir.absolutePath}")
|
||||||
|
Timber.d("Zip 文件解压完成: ${targetDir.absolutePath}")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(exception: Exception) {
|
||||||
|
updateProgress("解压失败: ${exception.message}")
|
||||||
|
Timber.e(exception, "解压失败")
|
||||||
|
this@FunActivity.toast("解压失败: ${exception.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "解压异常")
|
||||||
|
updateProgress("异常: ${e.message}")
|
||||||
|
this@FunActivity.toast("异常: ${e.message}")
|
||||||
|
} finally {
|
||||||
|
binding.btnExtract.isEnabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取解压后的文件并保存到 ObjectBox 数据库
|
||||||
|
*/
|
||||||
|
private fun loadAndSaveToDatabase() {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
updateProgress("开始读取数据...")
|
||||||
|
binding.btnLoadAndSave.isEnabled = false
|
||||||
|
|
||||||
|
val resourcesDir = File(cacheDir, "model_resources")
|
||||||
|
|
||||||
|
if (!resourcesDir.exists()) {
|
||||||
|
updateProgress("资源目录不存在,请先解压")
|
||||||
|
this@FunActivity.toast("资源目录不存在,请先解压")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
// 清除已有数据
|
||||||
|
updateProgress("清除已有数据...")
|
||||||
|
clearFoodData()
|
||||||
|
|
||||||
|
// 读取 JSON 文件
|
||||||
|
updateProgress("读取 JSON 文件...")
|
||||||
|
val embeddingsJson = readFileContent(File(resourcesDir, "data/embeddings.json"))
|
||||||
|
val labelsJson = readFileContent(File(resourcesDir, "data/labels.json"))
|
||||||
|
val classInfoJson = readFileContent(File(resourcesDir, "data/class_info.json"))
|
||||||
|
|
||||||
|
if (embeddingsJson.isEmpty() || labelsJson.isEmpty() || classInfoJson.isEmpty()) {
|
||||||
|
updateProgress("JSON 文件读取失败")
|
||||||
|
this@FunActivity.toast("JSON 文件读取失败")
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析 JSON 数据
|
||||||
|
updateProgress("解析 JSON 数据...")
|
||||||
|
val embeddingsList: List<List<Float>> =
|
||||||
|
Gson().fromJson(embeddingsJson, object : TypeToken<List<List<Float>>>() {}.type)
|
||||||
|
val labelsList: IntArray = Gson().fromJson(labelsJson, IntArray::class.java)
|
||||||
|
val classInfo: FoodClassInfo =
|
||||||
|
Gson().fromJson(classInfoJson, FoodClassInfo::class.java)
|
||||||
|
|
||||||
|
// 构建 Food 对象列表
|
||||||
|
updateProgress("构建数据对象...")
|
||||||
|
val foodMap = classInfo.idx_to_class
|
||||||
|
val foodList = mutableListOf<Food>()
|
||||||
|
val size = embeddingsList.size
|
||||||
|
|
||||||
|
Timber.d("开始构建 Food 对象,总数: $size")
|
||||||
|
|
||||||
|
embeddingsList.forEachIndexed { index, floatList ->
|
||||||
|
val classIdx = labelsList[index]
|
||||||
|
val foodName = foodMap["$classIdx"]
|
||||||
|
val array = floatList.toFloatArray()
|
||||||
|
val food = Food(name = foodName, foodVector = array, foodIdx = -1)
|
||||||
|
foodList.add(food)
|
||||||
|
|
||||||
|
// 每 100 条更新一次进度
|
||||||
|
if ((index + 1) % 100 == 0) {
|
||||||
|
val progress = ((index + 1) * 100) / size
|
||||||
|
updateProgress("构建数据中... $progress% (${index + 1}/$size)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到数据库
|
||||||
|
updateProgress("保存到数据库...")
|
||||||
|
ObjectBox.putAll(foodList)
|
||||||
|
|
||||||
|
updateProgress("完成!已保存 ${foodList.size} 条数据到数据库")
|
||||||
|
Timber.d("数据保存完成,总数: ${foodList.size}")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "读取和保存异常")
|
||||||
|
updateProgress("异常: ${e.message}")
|
||||||
|
this@FunActivity.toast("异常: ${e.message}")
|
||||||
|
} finally {
|
||||||
|
binding.btnLoadAndSave.isEnabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除 Food 表中的所有数据
|
||||||
|
*/
|
||||||
|
private suspend fun clearFoodData() {
|
||||||
|
try {
|
||||||
|
ObjectBox.safeDbOp {
|
||||||
|
val box = ObjectBox.getBox<Food>()
|
||||||
|
box?.removeAll()
|
||||||
|
Timber.d("Food 表数据已清除")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "清除数据失败")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取文件内容
|
||||||
|
*/
|
||||||
|
private fun readFileContent(file: File): String {
|
||||||
|
return try {
|
||||||
|
if (!file.exists()) {
|
||||||
|
Timber.w("文件不存在: ${file.absolutePath}")
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
file.readText()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "读取文件失败: ${file.absolutePath}")
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新进度显示
|
||||||
|
*/
|
||||||
|
private fun updateProgress(message: String) {
|
||||||
|
runOnUiThread {
|
||||||
|
binding.tvProgress.text = message
|
||||||
|
Timber.d("进度: $message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化文件大小
|
||||||
|
*/
|
||||||
|
private fun formatFileSize(bytes: Long): String {
|
||||||
|
return when {
|
||||||
|
bytes <= 0 -> "0 B"
|
||||||
|
bytes < 1024 -> "$bytes B"
|
||||||
|
bytes < 1024 * 1024 -> String.format("%.2f KB", bytes / 1024.0)
|
||||||
|
bytes < 1024 * 1024 * 1024 -> String.format("%.2f MB", bytes / (1024.0 * 1024))
|
||||||
|
else -> String.format("%.2f GB", bytes / (1024.0 * 1024 * 1024))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,6 +41,7 @@ import com.sw.inbound.model.response.SearchGoodsInfo
|
|||||||
import com.sw.inbound.objbox.FoodModule
|
import com.sw.inbound.objbox.FoodModule
|
||||||
import com.sw.inbound.objbox.FoodModule.IdNameScore
|
import com.sw.inbound.objbox.FoodModule.IdNameScore
|
||||||
import com.sw.inbound.sdk.SensorScaleUtils
|
import com.sw.inbound.sdk.SensorScaleUtils
|
||||||
|
import com.sw.inbound.sdk.WeightCallback
|
||||||
import com.sw.inbound.utils.BitmapCropper
|
import com.sw.inbound.utils.BitmapCropper
|
||||||
import com.sw.inbound.utils.BitmapSaver
|
import com.sw.inbound.utils.BitmapSaver
|
||||||
import com.sw.inbound.utils.CameraUtils
|
import com.sw.inbound.utils.CameraUtils
|
||||||
@@ -52,6 +53,7 @@ import com.sw.inbound.utils.ext.dp
|
|||||||
import com.sw.inbound.utils.ext.gone
|
import com.sw.inbound.utils.ext.gone
|
||||||
import com.sw.inbound.utils.ext.hideKeyboard
|
import com.sw.inbound.utils.ext.hideKeyboard
|
||||||
import com.sw.inbound.utils.ext.roundedDecimalPlace
|
import com.sw.inbound.utils.ext.roundedDecimalPlace
|
||||||
|
import com.sw.inbound.utils.ext.startActivity
|
||||||
import com.sw.inbound.utils.ext.toJsonString
|
import com.sw.inbound.utils.ext.toJsonString
|
||||||
import com.sw.inbound.utils.ext.toast
|
import com.sw.inbound.utils.ext.toast
|
||||||
import com.sw.inbound.viewmodel.ReceiptViewModel
|
import com.sw.inbound.viewmodel.ReceiptViewModel
|
||||||
@@ -76,6 +78,7 @@ abstract class GoodsListActivity : BaseActivity() {
|
|||||||
private const val TAG = "GoodsListActivity"
|
private const val TAG = "GoodsListActivity"
|
||||||
public const val ID = "id"
|
public const val ID = "id"
|
||||||
public const val SUPPLIER_ID = "supplierId"
|
public const val SUPPLIER_ID = "supplierId"
|
||||||
|
public const val PUR_CODE = "purCode"
|
||||||
|
|
||||||
public const val IS_RECEIPT_PAGE = "isReceiptPage"
|
public const val IS_RECEIPT_PAGE = "isReceiptPage"
|
||||||
// public const val IS_NEW_RECEIPT = "isNewReceipt"
|
// public const val IS_NEW_RECEIPT = "isNewReceipt"
|
||||||
@@ -89,6 +92,7 @@ abstract class GoodsListActivity : BaseActivity() {
|
|||||||
// var isNewReceipt: Boolean = false
|
// var isNewReceipt: Boolean = false
|
||||||
var id: String = ""
|
var id: String = ""
|
||||||
var supplierId: String = ""
|
var supplierId: String = ""
|
||||||
|
var purCode: String = ""
|
||||||
lateinit var binding: ActivityGoodsListBinding
|
lateinit var binding: ActivityGoodsListBinding
|
||||||
val receiptViewModel: ReceiptViewModel by viewModels()
|
val receiptViewModel: ReceiptViewModel by viewModels()
|
||||||
val userViewModel: UserViewModel by viewModels()
|
val userViewModel: UserViewModel by viewModels()
|
||||||
@@ -108,12 +112,14 @@ abstract class GoodsListActivity : BaseActivity() {
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
binding = ActivityGoodsListBinding.inflate(layoutInflater)
|
binding = ActivityGoodsListBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
cameraUtils.initCamera()
|
||||||
initBackDispatcher()
|
initBackDispatcher()
|
||||||
isReceiptPage = intent.getBooleanExtra(IS_RECEIPT_PAGE, true)
|
isReceiptPage = intent.getBooleanExtra(IS_RECEIPT_PAGE, true)
|
||||||
//isNewReceipt = intent.getBooleanExtra(IS_NEW_RECEIPT, false)
|
//isNewReceipt = intent.getBooleanExtra(IS_NEW_RECEIPT, false)
|
||||||
id = intent.getStringExtra(ID) ?: ""
|
id = intent.getStringExtra(ID) ?: ""
|
||||||
supplierId = intent.getStringExtra(SUPPLIER_ID) ?: ""
|
supplierId = intent.getStringExtra(SUPPLIER_ID) ?: ""
|
||||||
cameraUtils.initCamera()
|
purCode = intent.getStringExtra(PUR_CODE) ?: ""
|
||||||
|
|
||||||
replaceIncludeContent()
|
replaceIncludeContent()
|
||||||
initRecyclerView()
|
initRecyclerView()
|
||||||
initView()
|
initView()
|
||||||
@@ -121,7 +127,9 @@ abstract class GoodsListActivity : BaseActivity() {
|
|||||||
it.hideKeyboard()
|
it.hideKeyboard()
|
||||||
}
|
}
|
||||||
binding.btnBack.setOnClickListener { handleBackEvent() }
|
binding.btnBack.setOnClickListener { handleBackEvent() }
|
||||||
|
binding.tvTitle.setOnClickListener {
|
||||||
|
startActivity<FoodCollectionActivity>()
|
||||||
|
}
|
||||||
binding.flWarehouseDropdown.setOnClickListener { v ->
|
binding.flWarehouseDropdown.setOnClickListener { v ->
|
||||||
if (GlobalData.warehouseTypeList.isEmpty()) {
|
if (GlobalData.warehouseTypeList.isEmpty()) {
|
||||||
toast("暂无仓库数据")
|
toast("暂无仓库数据")
|
||||||
@@ -161,10 +169,26 @@ abstract class GoodsListActivity : BaseActivity() {
|
|||||||
height = 1
|
height = 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
binding.btnClearZero.setOnClickListener {
|
||||||
|
SensorScaleUtils.tare()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (this !is StoreActivity) {
|
||||||
|
userViewModel.getInitInfo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
cameraUtils.bind()
|
||||||
cameraUtils.setPreviewController(previewView)
|
cameraUtils.setPreviewController(previewView)
|
||||||
//startTime = System.currentTimeMillis()
|
|
||||||
binding.root.postDelayed({
|
binding.root.postDelayed({
|
||||||
SensorScaleUtils.addWeightListener {
|
SensorScaleUtils.addWeightListener(weightIntCallback)
|
||||||
|
}, 1 * 1500)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var weightIntCallback: WeightCallback = {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
try {
|
try {
|
||||||
weightCallback?.invoke(it)
|
weightCallback?.invoke(it)
|
||||||
@@ -174,52 +198,6 @@ abstract class GoodsListActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 1 * 1000)
|
|
||||||
binding.btnClearZero.setOnClickListener {
|
|
||||||
SensorScaleUtils.tare()
|
|
||||||
}
|
|
||||||
|
|
||||||
//val array = arrayOf("1","2","qwe")
|
|
||||||
//Timber.tag(TAG).d("测试:${array[2].toInt()}")
|
|
||||||
|
|
||||||
userViewModel.getInitInfo()
|
|
||||||
|
|
||||||
// lifecycleScope.launch {
|
|
||||||
// ObjectBox.put(
|
|
||||||
// Food(
|
|
||||||
// name = "测试",
|
|
||||||
// foodIdx = -100,
|
|
||||||
// foodVector = floatArrayOf(
|
|
||||||
// 0.015068196f,0.0f,0.0f,0.0f,0.0037743463f,0.0f,0.0f,0.07893528f,0.0f,0.0f,0.0f,0.0f,0.052832354f,0.0f,0.045785826f,0.0f,6.156802E-4f,0.04242764f,0.0f,0.0f,0.0f,0.0f,0.11469748f,0.13930011f,0.005731758f,0.03159248f,0.018799406f,0.028927676f,0.03472497f,0.01896616f,0.078822434f,0.0f,0.015187703f,0.0f,0.033177234f,0.07078301f,0.0f,0.0f,0.012238876f,0.0f,0.0f,0.0f,0.0f,0.0f,0.10691055f,0.008628067f,0.0f,0.0f,0.0f,0.0f,0.0f,0.06886323f,0.0f,0.0f,0.10460233f,0.0f,0.0f,0.0f,0.0f,0.0f,0.012850592f,0.0f,0.0f,0.103946045f,0.0f,0.018065454f,0.046292588f,0.063853905f,0.0f,0.10074362f,0.110680245f,0.10660499f,0.0f,0.06775707f,0.0f,0.0f,0.0f,0.0f,0.01639176f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.024850294f,0.0f,0.05308442f,0.0f,0.0f,0.0f,0.028708044f,0.02787582f,0.0f,0.0f,0.0f,0.0f,0.0f,0.027483912f,0.11719905f,0.04781422f,0.09033941f,0.09655397f,0.0032244714f,0.0f,0.0f,0.09580024f,0.07075415f,0.0876085f,0.0f,0.08756652f,2.2919098E-4f,0.019845076f,0.0f,0.0f,0.0f,0.04727701f,0.007276267f,0.0f,0.12660357f,0.03222255f,0.00182945f,0.0f,0.0f,0.0f,0.0f,0.0f,0.09080463f,0.0043369643f,0.0f,0.0f,0.0f,0.0f,0.0f,0.073994264f,0.0f,0.030294247f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.04890032f,0.0f,0.05318369f,0.0f,0.0f,0.0510094f,0.0f,0.0f,0.0f,0.12696978f,0.0f,0.21870367f,0.0f,0.0f,0.0f,0.0f,0.022256335f,0.13343737f,0.0f,0.0f,0.0f,0.0f,0.025192281f,0.1621841f,0.031685397f,0.0f,0.0f,0.03057069f,0.011333849f,0.0f,0.0f,0.11482361f,0.10645431f,0.0f,0.0f,0.0f,0.039949007f,0.0f,0.018850718f,0.0f,0.0f,0.0f,0.026081126f,0.0f,0.07934742f,0.0f,0.0f,0.0f,0.0f,0.0f,0.007998445f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.098366655f,0.0f,0.01606297f,0.16557153f,0.0f,0.0f,0.0f,0.0f,0.042638786f,0.024310581f,0.0f,0.027677892f,5.940642E-4f,0.0f,0.0f,0.0f,0.0f,0.05605889f,0.0f,0.0f,0.05216231f,0.080610394f,0.0f,0.0f,0.0f,0.0f,0.0f,0.07294793f,0.0f,0.02029506f,0.0f,0.0f,0.0f,0.12797904f,0.16668096f,0.083295606f,0.07768397f,0.07725612f,0.0f,0.0f,0.07259326f,0.110221304f,0.0f,0.0f,0.0f,0.0f,0.0f,0.027140126f,0.0f,0.053474665f,0.0f,0.018286237f,0.0f,0.0f,0.12152021f,0.0f,0.0f,0.0822232f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.04146023f,0.0f,0.034810524f,0.0f,0.00393797f,0.0f,0.032150663f,0.0f,0.0f,0.0f,0.0f,0.097777024f,0.0f,0.0f,0.0f,0.078797914f,0.0f,0.1292709f,0.0f,0.010623558f,0.0f,0.08360209f,0.0f,0.0f,0.122683614f,0.0f,0.0f,0.0f,0.0f,0.10111214f,0.10537941f,0.0f,0.0f,0.0f,0.0f,0.06272928f,0.0f,0.029668529f,0.049171254f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.032665383f,0.0f,0.0f,0.044802308f,0.0f,0.0f,0.05388319f,0.0f,0.0f,0.079517856f,0.012373011f,0.0f,0.01629987f,0.014890053f,0.0f,0.027798127f,0.0f,0.051028676f,0.15331483f,0.024111472f,0.0f,0.0f,0.0f,0.0f,0.0f,0.013699274f,0.0f,0.0f,0.07011116f,0.03022409f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.10939847f,0.0f,0.0f,0.0f,0.030306451f,0.0f,0.0f,0.12335773f,0.0f,0.0f,0.09099765f,0.0f,0.0f,0.0073862355f,0.0f,0.027847463f,0.0840776f,0.04207977f,0.046206567f,0.06679146f,0.0f,0.0f,0.01625976f,0.022998374f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.059722353f,0.0f,0.012246756f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.04984296f,0.0f,0.1435546f,0.0f,0.14667304f,0.079418436f,0.1305641f,0.014777453f,0.0f,0.020115053f,0.05619832f,0.011520876f,0.0f,0.11993748f,0.009492612f,0.019984039f,0.0f,0.0f,0.0f,0.0f,0.043548502f,0.0f,0.0f,0.0f,0.0f,0.15310751f,0.0f,0.0f,0.0f,0.0f,0.0f,0.018038996f,0.0021733972f,0.0f,0.0f,0.0f,0.0f,0.060995787f,0.06326715f,0.0f,0.06896745f,0.013745607f,5.018817E-4f,0.0f,0.0f,0.14184067f,0.019447664f,0.047488734f,0.13301876f,0.0f,0.0f,0.06715185f,0.0f,0.0f,0.0025059544f,0.0013816518f,0.0f,0.00415278f,0.17231767f,0.0073697185f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0017814273f,0.040113796f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.016738398f,0.0f,0.10465313f,0.0f,0.057283692f,0.0f,0.05406501f,0.0f,0.012632326f,0.0f,0.0f,0.0f,0.10440692f,0.0f,0.0047152108f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.11544036f
|
|
||||||
// )
|
|
||||||
// )
|
|
||||||
// )
|
|
||||||
// ObjectBox.remove("测试")
|
|
||||||
// ObjectBox.safeDbOp {
|
|
||||||
// val vector2 = floatArrayOf(
|
|
||||||
// 0.029274145f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.04849013f,0.0f,0.0f,0.0030461443f,0.0f,0.04333778f,0.0f,0.036458552f,0.0f,0.0f,0.032146808f,0.0f,0.0f,0.0f,0.0f,0.12563168f,0.16337962f,0.0086008925f,0.028651662f,0.05676712f,0.038951095f,0.035215054f,0.023515426f,0.09426005f,0.0f,0.0f,0.0f,0.019520408f,0.09774056f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.12273409f,0.03317896f,0.0f,0.0f,0.0f,0.0f,0.0f,0.056036845f,0.0f,0.005601604f,0.09232295f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.10639726f,0.017117623f,0.0f,0.04106111f,0.06582154f,0.0f,0.10570599f,0.10395363f,0.11154216f,0.0f,0.09019618f,0.0f,0.0f,0.0f,0.0f,0.008780864f,0.0f,0.0f,0.01997713f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.016066022f,0.020096382f,0.0f,0.05578338f,0.0f,0.0f,0.0f,0.048834037f,0.031760734f,0.0f,0.0f,0.0f,0.0f,0.0f,0.02459963f,0.11993096f,0.047805987f,0.070974134f,0.10536186f,0.0f,0.0f,0.0f,0.111894555f,0.08973926f,0.071053386f,0.0f,0.071883f,0.005135076f,0.0f,0.0f,0.0f,0.0f,0.023139987f,0.027882056f,0.0f,0.12620124f,0.05076331f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.06944067f,0.0146693f,0.0f,0.0f,0.0f,0.0f,0.00978479f,0.04965295f,0.0f,0.03953311f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.05496083f,0.0f,0.05957852f,0.0f,0.0f,0.05285405f,0.0f,0.0f,0.0f,0.10977843f,0.0f,0.21329264f,0.0f,0.0f,0.0f,0.0f,0.022029493f,0.12511873f,0.0f,0.0f,0.0f,0.0f,0.015391711f,0.1697028f,0.035269253f,0.0f,0.0f,0.0070471396f,0.0f,0.0f,0.0f,0.123943165f,0.10272677f,0.0f,0.0f,0.0f,0.06351426f,0.0f,0.009808158f,0.0f,0.0f,0.0f,0.02582221f,0.0f,0.0745801f,0.0f,0.0f,0.023285847f,0.0f,0.0f,0.017162288f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.09181752f,0.0f,0.007880242f,0.15574618f,0.0f,0.0f,0.0f,0.0f,0.02478441f,0.039601766f,0.0f,0.049401764f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0793202f,0.0f,0.0f,0.071133174f,0.09675928f,0.0f,0.015621623f,0.0f,0.0f,0.0f,0.08347399f,0.009147923f,0.0f,0.0f,0.0f,0.0f,0.14187747f,0.15219086f,0.12055488f,0.07175866f,0.07192898f,0.0f,0.0f,0.04634941f,0.12534219f,0.0f,0.0f,0.0f,0.008665879f,0.0f,0.040081337f,0.0f,0.06729515f,0.0f,0.058273762f,0.0020215393f,0.0f,0.10878004f,0.0f,0.0f,0.09762441f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.03849448f,0.0f,0.03300536f,0.0f,0.0f,0.013231625f,0.018584426f,0.0f,0.0f,0.0f,0.0f,0.08124468f,0.0f,0.0f,0.0f,0.060708806f,0.0f,0.11115098f,0.0f,0.0f,0.0f,0.0803424f,0.0f,0.0f,0.115781434f,0.0f,0.00764197f,0.0f,0.0f,0.121340856f,0.0869794f,0.0f,0.0f,0.0f,0.0f,0.045324527f,0.0f,0.015057986f,0.048937548f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.030864827f,0.0f,0.0029875587f,0.03196471f,0.0f,0.0f,0.054534424f,0.0f,0.0f,0.07863297f,0.019617077f,0.0f,0.026181744f,0.0f,0.0f,0.021914937f,0.0f,0.063720085f,0.16607703f,0.006567324f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0017898319f,0.0f,0.0f,0.04841979f,0.0044351136f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.15329078f,0.0f,0.0f,0.0f,0.030429695f,0.0f,0.0f,0.11840538f,0.0f,0.0f,0.107368395f,0.014730752f,0.0f,0.0f,0.0f,0.030180579f,0.039591953f,0.03209204f,0.048180148f,0.0676186f,0.0f,0.0f,0.03614289f,0.030257396f,1.5626202E-4f,0.018185573f,0.0f,0.0f,0.0f,0.0f,0.055325508f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.051894154f,0.0f,0.12869093f,0.0f,0.122797504f,0.083724305f,0.12840301f,0.0f,0.0f,0.0f,0.05686797f,0.0f,0.0f,0.11015593f,0.018203156f,0.020094728f,0.0f,0.0f,0.0f,0.0f,0.058597155f,0.0f,0.0f,0.0f,0.0f,0.12797132f,0.0f,0.006606713f,0.0f,0.0f,0.0f,0.028628595f,0.0f,0.0f,0.0f,0.0f,0.0f,0.050383586f,0.07132696f,0.0f,0.092209496f,0.0f,0.0f,0.0f,0.0f,0.14604862f,0.01660271f,0.057229653f,0.13104743f,0.0f,0.0f,0.06136706f,0.0f,0.0f,0.0f,0.0011910667f,0.0f,0.0f,0.15921566f,0.0f,0.0f,0.0f,0.0f,0.007558685f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0f,0.007532647f,0.05110198f,0.00667856f,0.0f,0.0f,0.0f,0.0f,0.0f,0.0016389234f,0.0f,0.0f,0.0f,0.0f,0.018518167f,0.0f,0.08580791f,0.0f,0.06099585f,0.0f,0.0366169f,0.0f,0.0f,0.0f,0.0f,0.0f,0.073218584f,0.0f,0.0f,0.0f,0.008933658f,0.0f,0.0f,0.0f,0.0f,0.0f,0.116691895f
|
|
||||||
// )
|
|
||||||
// ObjectBox.getBox<Food>()?.let {
|
|
||||||
// val query: Query<Food>? = it.query(
|
|
||||||
// Food_.foodVector
|
|
||||||
// .nearestNeighbors(vector2, 5)
|
|
||||||
// )?.build()
|
|
||||||
// try {
|
|
||||||
// val list = query?.findIdsWithScores()
|
|
||||||
// val resultList = mutableListOf<String>()
|
|
||||||
// list?.forEach { idWithScore ->
|
|
||||||
// val food = it.get(idWithScore.id)
|
|
||||||
// resultList.add("${food.name}|${food.id}|${idWithScore.score}")
|
|
||||||
// }
|
|
||||||
// Timber.tag(TAG).d("比较结果:${resultList.joinToString(separator = ",")}")
|
|
||||||
// } finally {
|
|
||||||
// // 先关闭Query,释放Cursor
|
|
||||||
// query?.close()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
private var weightCallback: ((weight: Int) -> Unit)? = null
|
private var weightCallback: ((weight: Int) -> Unit)? = null
|
||||||
fun readWeightInfo(callback: (weight: Int) -> Unit) {
|
fun readWeightInfo(callback: (weight: Int) -> Unit) {
|
||||||
@@ -238,13 +216,13 @@ abstract class GoodsListActivity : BaseActivity() {
|
|||||||
binding.tvTotalWeight.text = "重量:${weight}克"
|
binding.tvTotalWeight.text = "重量:${weight}克"
|
||||||
}
|
}
|
||||||
if (weight < 200) {
|
if (weight < 200) {
|
||||||
if (weight < 10) {
|
if (weight < 20) {
|
||||||
// val clazzName = DialogManager.getDialogList()
|
// val clazzName = DialogManager.getDialogList()
|
||||||
// .map { it.javaClass.simpleName }
|
// .map { it.javaClass.simpleName }
|
||||||
// .joinToString(",")
|
// .joinToString(",")
|
||||||
// Timber.tag(TAG).d(clazzName)
|
// Timber.tag(TAG).d(clazzName)
|
||||||
if (DialogManager.getDialogList().size == 1 &&
|
// 无其他弹窗正在显示时(过滤幽灵条目),自动关闭识别弹窗
|
||||||
DialogManager.contains("GoodsRecognizeDialog") &&
|
if (DialogManager.getDialogList().none { it.isShowing && it != goodsRecognizeDialog } &&
|
||||||
goodsRecognizeDialog?.isShowing == true
|
goodsRecognizeDialog?.isShowing == true
|
||||||
) {
|
) {
|
||||||
goodsRecognizeDialog?.dismiss()
|
goodsRecognizeDialog?.dismiss()
|
||||||
@@ -281,8 +259,9 @@ abstract class GoodsListActivity : BaseActivity() {
|
|||||||
searchFood(uri)
|
searchFood(uri)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
|
Loading.dismiss()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.start()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val delayHandler by lazy {
|
private val delayHandler by lazy {
|
||||||
@@ -298,7 +277,7 @@ abstract class GoodsListActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun recognizeFood() {
|
open fun recognizeFood() {
|
||||||
if (isShowRecognizeDialog.not()) {
|
if (isShowRecognizeDialog.not()) {
|
||||||
if (goodsRecognizeDialog == null) {
|
if (goodsRecognizeDialog == null) {
|
||||||
isShowRecognizeDialog = true
|
isShowRecognizeDialog = true
|
||||||
@@ -423,7 +402,11 @@ abstract class GoodsListActivity : BaseActivity() {
|
|||||||
// readBitmap(it)
|
// readBitmap(it)
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
ImageUtil.uriToBitmap(this, uri)?.let { bitmap ->
|
val bitmap = ImageUtil.uriToBitmap(this, uri)
|
||||||
|
if (bitmap == null) {
|
||||||
|
Loading.dismiss()
|
||||||
|
return
|
||||||
|
}
|
||||||
val newBmp = BitmapCropper.cropCenter(
|
val newBmp = BitmapCropper.cropCenter(
|
||||||
original = bitmap,
|
original = bitmap,
|
||||||
targetWidth = 1000, targetHeight = 1300,
|
targetWidth = 1000, targetHeight = 1300,
|
||||||
@@ -437,7 +420,6 @@ abstract class GoodsListActivity : BaseActivity() {
|
|||||||
bitmap.recycle()
|
bitmap.recycle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun recognizeGoodsList(foodList: List<IdNameScore>) {
|
private fun recognizeGoodsList(foodList: List<IdNameScore>) {
|
||||||
receiptViewModel.recognizeGoodsList(
|
receiptViewModel.recognizeGoodsList(
|
||||||
@@ -478,7 +460,7 @@ abstract class GoodsListActivity : BaseActivity() {
|
|||||||
goodsFile = file,
|
goodsFile = file,
|
||||||
goodsName = name
|
goodsName = name
|
||||||
) {
|
) {
|
||||||
GoodsSearchDialog(this).show()
|
onGoodsSearchRequested()
|
||||||
}.apply {
|
}.apply {
|
||||||
if (isFinishing.not() && isDestroyed.not()) {
|
if (isFinishing.not() && isDestroyed.not()) {
|
||||||
//isShowRecognizeDialog = false
|
//isShowRecognizeDialog = false
|
||||||
@@ -490,6 +472,11 @@ abstract class GoodsListActivity : BaseActivity() {
|
|||||||
|
|
||||||
var goodsRecognizeDialog: GoodsRecognizeDialog? = null
|
var goodsRecognizeDialog: GoodsRecognizeDialog? = null
|
||||||
|
|
||||||
|
/** 子类可重写以替换物品检索弹窗 */
|
||||||
|
protected open fun onGoodsSearchRequested() {
|
||||||
|
GoodsSearchDialog(this).show()
|
||||||
|
}
|
||||||
|
|
||||||
private fun replaceIncludeContent() {
|
private fun replaceIncludeContent() {
|
||||||
binding.flContainer.removeAllViews()
|
binding.flContainer.removeAllViews()
|
||||||
//isReceiptPage
|
//isReceiptPage
|
||||||
@@ -497,15 +484,18 @@ abstract class GoodsListActivity : BaseActivity() {
|
|||||||
val tempBinding =
|
val tempBinding =
|
||||||
ListItemReceiptGoodsBinding.inflate(layoutInflater, binding.flContainer, false)
|
ListItemReceiptGoodsBinding.inflate(layoutInflater, binding.flContainer, false)
|
||||||
binding.flContainer.addView(tempBinding.root)
|
binding.flContainer.addView(tempBinding.root)
|
||||||
} else {
|
} else if (this is SelfProcurementActivity) {
|
||||||
val tempBinding =
|
val tempBinding =
|
||||||
ListItemSelfProcurementBinding.inflate(layoutInflater, binding.flContainer, false)
|
ListItemSelfProcurementBinding.inflate(layoutInflater, binding.flContainer, false)
|
||||||
binding.flContainer.addView(tempBinding.root)
|
binding.flContainer.addView(tempBinding.root)
|
||||||
|
} else {
|
||||||
|
binding.flContainer.gone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
|
SensorScaleUtils.removeWeightListener(weightIntCallback)
|
||||||
unBindCamera()
|
unBindCamera()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -670,11 +660,12 @@ abstract class GoodsListActivity : BaseActivity() {
|
|||||||
val uploadInfo = UploadInfo(
|
val uploadInfo = UploadInfo(
|
||||||
id = id,
|
id = id,
|
||||||
supplierId = supplierId,
|
supplierId = supplierId,
|
||||||
|
purCode = purCode,
|
||||||
receiveGoodsInfos = goodsList
|
receiveGoodsInfos = goodsList
|
||||||
)
|
)
|
||||||
logInfo("receiptSubmit确认收货提交数据:${uploadInfo.toJsonString()}")
|
logInfo("receiptSubmit确认收货提交数据:${uploadInfo.toJsonString()}")
|
||||||
showWaitingDialog2("加载中")
|
showWaitingDialog2("加载中")
|
||||||
receiptViewModel.confirmReceipt(uploadInfo) { success ->
|
receiptViewModel.confirmReceipt(uploadInfo) { success, msg ->
|
||||||
hideWaitingDialog()
|
hideWaitingDialog()
|
||||||
if (success) {
|
if (success) {
|
||||||
userViewModel.deleteKey(localGoodsKey)
|
userViewModel.deleteKey(localGoodsKey)
|
||||||
@@ -683,7 +674,7 @@ abstract class GoodsListActivity : BaseActivity() {
|
|||||||
finish()
|
finish()
|
||||||
}, 200)
|
}, 200)
|
||||||
} else {
|
} else {
|
||||||
toast("确认收货失败")
|
toast("确认收货失败:$msg")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -713,7 +704,7 @@ abstract class GoodsListActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
logInfo("selfProcurementSubmit自采入库提交数据:${submitData.toJsonString()}")
|
logInfo("selfProcurementSubmit自采入库提交数据:${submitData.toJsonString()}")
|
||||||
showWaitingDialog2("加载中")
|
showWaitingDialog2("加载中")
|
||||||
receiptViewModel.addToWarehouse(submitData) { success ->
|
receiptViewModel.addToWarehouse(submitData) { success, msg ->
|
||||||
hideWaitingDialog()
|
hideWaitingDialog()
|
||||||
if (success) {
|
if (success) {
|
||||||
toast("提交入库成功")
|
toast("提交入库成功")
|
||||||
@@ -722,7 +713,7 @@ abstract class GoodsListActivity : BaseActivity() {
|
|||||||
finish()
|
finish()
|
||||||
}, 200)
|
}, 200)
|
||||||
} else {
|
} else {
|
||||||
toast("提交入库失败")
|
toast("提交入库失败:$msg")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,19 +3,24 @@ package com.sw.inbound.activity
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import android.view.KeyEvent
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import com.sw.inbound.base.BaseActivity
|
import com.sw.inbound.base.BaseActivity
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import com.sw.inbound.databinding.ActivityHomeBinding
|
import com.sw.inbound.databinding.ActivityHomeBinding
|
||||||
|
import com.sw.inbound.dialog.EnvSwitchDialog
|
||||||
|
import com.sw.inbound.dialog.Loading
|
||||||
import com.sw.inbound.model.response.User
|
import com.sw.inbound.model.response.User
|
||||||
import com.sw.inbound.objbox.FoodModule
|
import com.sw.inbound.objbox.FoodModule
|
||||||
|
import com.sw.inbound.objbox.ObjectBox
|
||||||
import com.sw.inbound.sdk.SensorScaleUtils
|
import com.sw.inbound.sdk.SensorScaleUtils
|
||||||
import com.sw.inbound.utils.DateTimeUtils
|
import com.sw.inbound.utils.DateTimeUtils
|
||||||
import com.sw.inbound.utils.ThreadUtils
|
import com.sw.inbound.utils.ThreadUtils
|
||||||
import com.sw.inbound.utils.ext.gone
|
import com.sw.inbound.utils.ext.gone
|
||||||
import com.sw.inbound.utils.ext.startActivity
|
import com.sw.inbound.utils.ext.startActivity
|
||||||
|
import com.sw.inbound.utils.ext.toast
|
||||||
import com.sw.inbound.utils.ext.visible
|
import com.sw.inbound.utils.ext.visible
|
||||||
import com.sw.inbound.viewmodel.UserViewModel
|
import com.sw.inbound.viewmodel.UserViewModel
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
@@ -45,6 +50,10 @@ class HomeActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 用于检测双击返回键的时间戳 */
|
||||||
|
private var lastBackPressTime = 0L
|
||||||
|
private val backPressInterval = 2000L // 2 秒内连续按下返回键视为双击
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
binding = ActivityHomeBinding.inflate(layoutInflater)
|
binding = ActivityHomeBinding.inflate(layoutInflater)
|
||||||
@@ -68,19 +77,48 @@ class HomeActivity : BaseActivity() {
|
|||||||
*/
|
*/
|
||||||
private fun initViews() {
|
private fun initViews() {
|
||||||
// 标题点击:跳转菜品采集页面
|
// 标题点击:跳转菜品采集页面
|
||||||
binding.tvTitle.setOnClickListener {
|
// binding.tvTitle.setOnClickListener {
|
||||||
startActivity<FoodCollectionActivity>()
|
// startActivity<FoodCollectionActivity>()
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// 时间点击:打开环境切换弹窗
|
||||||
|
binding.tvTime.setOnClickListener { v ->
|
||||||
|
EnvSwitchDialog(context = this, onEnvChanged = {
|
||||||
|
Loading.show(this)
|
||||||
|
lifecycleScope.launch {
|
||||||
|
// ObjectBox.removeAll()
|
||||||
|
// FoodModule.init(this@HomeActivity)
|
||||||
|
v.postDelayed({
|
||||||
|
Loading.dismiss()
|
||||||
|
toast("服务重置完成")
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
}).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 采购单入库入口
|
// // 采购单入库入口
|
||||||
binding.ivOrderPurchase.setOnClickListener {
|
// binding.ivOrderPurchase.setOnClickListener {
|
||||||
startActivity<PurchaseOrderActivity>()
|
// startActivity<PurchaseOrderActivity>()
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // 自采入库入口,传入非采购单标志
|
||||||
|
// binding.ivOrderSelfProcurement.setOnClickListener {
|
||||||
|
// startActivity<SelfProcurementActivity> {
|
||||||
|
// putExtra(GoodsListActivity.IS_RECEIPT_PAGE, false)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 净菜按钮点击提示
|
||||||
|
binding.btnCleanDishStore.setOnClickListener {
|
||||||
|
startActivity<StoreActivity> {
|
||||||
|
putExtra(StoreActivity.EXTRA_STORE_TYPE, 0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自采入库入口,传入非采购单标志
|
// 毛菜按钮点击提示
|
||||||
binding.ivOrderSelfProcurement.setOnClickListener {
|
binding.btnRawDishStore.setOnClickListener {
|
||||||
startActivity<SelfProcurementActivity> {
|
startActivity<StoreActivity> {
|
||||||
putExtra(GoodsListActivity.IS_RECEIPT_PAGE, false)
|
putExtra(StoreActivity.EXTRA_STORE_TYPE, 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -128,4 +166,27 @@ class HomeActivity : BaseActivity() {
|
|||||||
// 页面不可见时停止刷新,避免内存泄漏
|
// 页面不可见时停止刷新,避免内存泄漏
|
||||||
handler.removeCallbacks(timeRunnable)
|
handler.removeCallbacks(timeRunnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理返回键事件
|
||||||
|
* 双击返回键退出应用,单击返回键提示再次点击退出
|
||||||
|
*/
|
||||||
|
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||||
|
return if (keyCode == KeyEvent.KEYCODE_BACK) {
|
||||||
|
val currentTime = System.currentTimeMillis()
|
||||||
|
if (currentTime - lastBackPressTime < backPressInterval) {
|
||||||
|
// 双击返回键,退出应用
|
||||||
|
finish()
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
// 单击返回键,显示提示
|
||||||
|
lastBackPressTime = currentTime
|
||||||
|
this.toast("再按一次返回键退出应用")
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
super.onKeyDown(keyCode, event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
package com.sw.inbound.activity
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.os.SystemClock
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import android.view.animation.RotateAnimation
|
||||||
|
import com.sw.inbound.GlobalData
|
||||||
|
import com.sw.inbound.GlobalKey
|
||||||
|
import com.sw.inbound.base.BaseActivity
|
||||||
|
import com.sw.inbound.databinding.ActivityInitBinding
|
||||||
|
import com.sw.inbound.utils.NetworkUtils
|
||||||
|
import com.sw.inbound.utils.SPUtil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化页面 Activity
|
||||||
|
* 用于应用启动时的初始化流程
|
||||||
|
* - 显示 60 秒倒计时
|
||||||
|
* - 每 10 秒检测一次网络连接
|
||||||
|
* - 网络连接成功则跳转到 HomeActivity
|
||||||
|
* - 60 秒后仍未连接则显示"连接网络"按钮
|
||||||
|
*/
|
||||||
|
class InitActivity : BaseActivity() {
|
||||||
|
|
||||||
|
private lateinit var binding: ActivityInitBinding
|
||||||
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
|
private var startTime = 0L // 倒计时开始时间
|
||||||
|
private var lastNetworkCheckTime = 0L // 上次检测网络的时间
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
binding = ActivityInitBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
initViews()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化控件事件
|
||||||
|
*/
|
||||||
|
private fun initViews() {
|
||||||
|
// 连接网络按钮点击事件
|
||||||
|
binding.btnConnectNetwork.setOnClickListener {
|
||||||
|
openNetworkSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动加载动画 - 持续旋转
|
||||||
|
startLoadingAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动加载动画 - ImageView 持续旋转
|
||||||
|
*/
|
||||||
|
private fun startLoadingAnimation() {
|
||||||
|
val rotateAnimation = RotateAnimation(
|
||||||
|
0f, 360f,
|
||||||
|
RotateAnimation.RELATIVE_TO_SELF, 0.5f,
|
||||||
|
RotateAnimation.RELATIVE_TO_SELF, 0.5f
|
||||||
|
).apply {
|
||||||
|
duration = 1200 // 旋转周期 1.2 秒
|
||||||
|
repeatCount = RotateAnimation.INFINITE // 无限循环
|
||||||
|
repeatMode = RotateAnimation.RESTART
|
||||||
|
}
|
||||||
|
binding.ivLoading.startAnimation(rotateAnimation)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
if (NetworkUtils.isNetworkConnected(this)) {
|
||||||
|
navigateToHome()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
startCountdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
// 页面不可见时停止倒计时
|
||||||
|
handler.removeCallbacksAndMessages(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动 60 秒倒计时
|
||||||
|
*/
|
||||||
|
private fun startCountdown() {
|
||||||
|
startTime = SystemClock.elapsedRealtime()
|
||||||
|
lastNetworkCheckTime = 0
|
||||||
|
binding.llLoading.visibility = android.view.View.VISIBLE
|
||||||
|
binding.llNetworkButton.visibility = android.view.View.GONE
|
||||||
|
binding.tvCountdown.text = "60秒"
|
||||||
|
scheduleCountdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定时更新倒计时
|
||||||
|
* 使用 SystemClock.elapsedRealtime() 确保精确计时
|
||||||
|
*/
|
||||||
|
private fun scheduleCountdown() {
|
||||||
|
handler.postDelayed({
|
||||||
|
val elapsedTime = SystemClock.elapsedRealtime() - startTime
|
||||||
|
val remainingTime = 60 - (elapsedTime / 1000).toInt()
|
||||||
|
val currentNetworkCheckTime = (elapsedTime / 1000).toInt()
|
||||||
|
|
||||||
|
// 每 10 秒检测一次网络
|
||||||
|
if (currentNetworkCheckTime > 0 && currentNetworkCheckTime % 10 == 0 && currentNetworkCheckTime != lastNetworkCheckTime.toInt()) {
|
||||||
|
lastNetworkCheckTime = currentNetworkCheckTime.toLong()
|
||||||
|
checkNetworkConnection()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 倒计时未结束,继续更新 UI
|
||||||
|
if (remainingTime > 0) {
|
||||||
|
binding.tvCountdown.text = "${remainingTime}秒"
|
||||||
|
scheduleCountdown()
|
||||||
|
} else {
|
||||||
|
// 倒计时结束,最后检测一次网络
|
||||||
|
if (NetworkUtils.isNetworkConnected(this)) {
|
||||||
|
navigateToHome()
|
||||||
|
} else {
|
||||||
|
// 网络未连接,显示"连接网络"按钮
|
||||||
|
showNetworkButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 100) // 每 100ms 检查一次,确保精确性
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测网络连接
|
||||||
|
*/
|
||||||
|
private fun checkNetworkConnection() {
|
||||||
|
if (NetworkUtils.isNetworkConnected(this)) {
|
||||||
|
// 网络连接成功,停止倒计时并跳转到 HomeActivity
|
||||||
|
handler.removeCallbacksAndMessages(null)
|
||||||
|
navigateToHome()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳转到 HomeActivity
|
||||||
|
*/
|
||||||
|
private fun navigateToHome() {
|
||||||
|
// 优先读取用户手动切换后持久化的 url
|
||||||
|
val savedUrl = SPUtil.getInstance().get(GlobalKey.KEY_BASE_URL, "")
|
||||||
|
if (!savedUrl.isNullOrEmpty()) {
|
||||||
|
GlobalData.appBaseUrl = savedUrl
|
||||||
|
} else {
|
||||||
|
// 未保存过则使用默认逻辑:特定设备走测试环境,其余走 UAT
|
||||||
|
GlobalData.appBaseUrl = GlobalData.PROD_BASE_URL
|
||||||
|
val list = listOf(
|
||||||
|
//"5a41a2cf-3cee-3731-911d-a28b10164f7a",
|
||||||
|
"f0bcabbb-0d58-3b14-8bed-9863b7ec61ef"
|
||||||
|
)
|
||||||
|
if (list.contains(GlobalData.deviceId)) {
|
||||||
|
GlobalData.appBaseUrl = GlobalData.LOCAL_BASE_URL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val intent = Intent(this, HomeActivity::class.java)
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示"连接网络"按钮
|
||||||
|
*/
|
||||||
|
private fun showNetworkButton() {
|
||||||
|
binding.llLoading.visibility = android.view.View.GONE
|
||||||
|
binding.llNetworkButton.visibility = android.view.View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开系统网络设置页面
|
||||||
|
*/
|
||||||
|
private fun openNetworkSettings() {
|
||||||
|
val intent = Intent(android.provider.Settings.ACTION_WIFI_SETTINGS)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 禁用返回键
|
||||||
|
*/
|
||||||
|
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||||
|
return if (keyCode == KeyEvent.KEYCODE_BACK) {
|
||||||
|
// 禁用返回键,不执行任何操作
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
super.onKeyDown(keyCode, event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,19 +1,31 @@
|
|||||||
package com.sw.inbound.activity
|
package com.sw.inbound.activity
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Dialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import com.sw.inbound.base.BaseActivity
|
import android.view.WindowManager
|
||||||
|
import androidx.core.graphics.drawable.toDrawable
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import coil.load
|
import coil.load
|
||||||
import com.chad.library.adapter4.BaseQuickAdapter
|
import com.chad.library.adapter4.BaseQuickAdapter
|
||||||
import com.chad.library.adapter4.viewholder.QuickViewHolder
|
import com.chad.library.adapter4.viewholder.QuickViewHolder
|
||||||
|
import com.sw.inbound.base.BaseActivity
|
||||||
import com.sw.inbound.databinding.ActivityLocalImagePreviewBinding
|
import com.sw.inbound.databinding.ActivityLocalImagePreviewBinding
|
||||||
|
import com.sw.inbound.databinding.DialogImageFullPreviewBinding
|
||||||
import com.sw.inbound.databinding.ListItemImagePreviewBinding
|
import com.sw.inbound.databinding.ListItemImagePreviewBinding
|
||||||
import com.sw.inbound.dialog.Loading
|
import com.sw.inbound.dialog.Loading
|
||||||
import com.sw.inbound.utils.ext.copyText
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
class LocalImagePreviewActivity : BaseActivity() {
|
class LocalImagePreviewActivity : BaseActivity() {
|
||||||
|
|
||||||
@@ -32,39 +44,99 @@ class LocalImagePreviewActivity : BaseActivity() {
|
|||||||
loadData()
|
loadData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 从缓存 crop 目录加载所有图片文件 */
|
||||||
fun loadData() {
|
fun loadData() {
|
||||||
Thread {
|
lifecycleScope.launch {
|
||||||
runOnUiThread {
|
// 主线程显示加载框
|
||||||
Loading.show(this)
|
Loading.show(this@LocalImagePreviewActivity)
|
||||||
}
|
// 切换到 IO 线程执行文件读取
|
||||||
val cropFile = File(this.cacheDir, "crop")
|
withContext(Dispatchers.IO) {
|
||||||
|
val cropFile = File(this@LocalImagePreviewActivity.cacheDir, "crop")
|
||||||
cropFile.listFiles()?.forEach {
|
cropFile.listFiles()?.forEach {
|
||||||
if (it.isDirectory) {
|
// 跳过子目录,只处理文件
|
||||||
return@forEach
|
if (it.isDirectory) return@forEach
|
||||||
|
// 跳过非 jpg 图片
|
||||||
|
if (!it.name.endsWith(".jpg", ignoreCase = true)) return@forEach
|
||||||
|
val nameWithoutExt = it.nameWithoutExtension
|
||||||
|
// 兼容 IMG_CROP_{时间戳} 和其他命名格式
|
||||||
|
val displayName = if (nameWithoutExt.startsWith("IMG_CROP_")) {
|
||||||
|
df.format(Date(nameWithoutExt.removePrefix("IMG_CROP_").toLong()))
|
||||||
|
} else {
|
||||||
|
nameWithoutExt
|
||||||
}
|
}
|
||||||
val start = "IMG_CROP_".length
|
imageList.add(ImageBean(file = it, name = displayName))
|
||||||
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 }
|
||||||
}
|
}
|
||||||
imageList.sortBy { it.name.toLong() }
|
// 回到主线程刷新列表
|
||||||
runOnUiThread {
|
|
||||||
imageAdapter.notifyDataSetChanged()
|
imageAdapter.notifyDataSetChanged()
|
||||||
|
// 延迟 500ms 后关闭加载框
|
||||||
|
delay(500)
|
||||||
Loading.dismiss()
|
Loading.dismiss()
|
||||||
}
|
}
|
||||||
}.start()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var isCycle = true
|
@SuppressLint("SimpleDateFormat")
|
||||||
|
private val df = SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||||
|
|
||||||
|
/** 弹出全屏大图预览 Dialog,支持左右切换 */
|
||||||
|
private fun showFullImagePreview(startPosition: Int) {
|
||||||
|
var currentIndex = startPosition
|
||||||
|
|
||||||
|
val dialogBinding = DialogImageFullPreviewBinding.inflate(layoutInflater)
|
||||||
|
val dialog = Dialog(this, android.R.style.Theme_Black_NoTitleBar_Fullscreen)
|
||||||
|
dialog.setContentView(dialogBinding.root)
|
||||||
|
dialog.setCancelable(true)
|
||||||
|
dialog.setCanceledOnTouchOutside(true)
|
||||||
|
dialog.window?.apply {
|
||||||
|
setBackgroundDrawable(Color.TRANSPARENT.toDrawable())
|
||||||
|
setLayout(
|
||||||
|
WindowManager.LayoutParams.MATCH_PARENT,
|
||||||
|
WindowManager.LayoutParams.MATCH_PARENT
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据 currentIndex 刷新图片、文件名、页码及按钮状态 */
|
||||||
|
fun updateContent() {
|
||||||
|
val item = imageList[currentIndex]
|
||||||
|
dialogBinding.ivFullImage.load(item.file)
|
||||||
|
dialogBinding.tvFileName.text = item.file.name
|
||||||
|
dialogBinding.tvPageIndicator.text = "${currentIndex + 1} / ${imageList.size}"
|
||||||
|
// 到达边界时隐藏对应方向的按钮
|
||||||
|
dialogBinding.btnPrev.alpha = if (currentIndex > 0) 1f else 0.2f
|
||||||
|
dialogBinding.btnNext.alpha = if (currentIndex < imageList.size - 1) 1f else 0.2f
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击图片本身关闭弹窗
|
||||||
|
dialogBinding.ivFullImage.setOnClickListener { dialog.dismiss() }
|
||||||
|
// 关闭按钮
|
||||||
|
dialogBinding.btnClose.setOnClickListener { dialog.dismiss() }
|
||||||
|
// 上一张
|
||||||
|
dialogBinding.btnPrev.setOnClickListener {
|
||||||
|
if (currentIndex > 0) {
|
||||||
|
currentIndex--
|
||||||
|
updateContent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 下一张
|
||||||
|
dialogBinding.btnNext.setOnClickListener {
|
||||||
|
if (currentIndex < imageList.size - 1) {
|
||||||
|
currentIndex++
|
||||||
|
updateContent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateContent()
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
private val imageList = mutableListOf<ImageBean>()
|
private val imageList = mutableListOf<ImageBean>()
|
||||||
private val imageAdapter: ImageAdapter by lazy {
|
private val imageAdapter: ImageAdapter by lazy {
|
||||||
ImageAdapter(imageList).apply {
|
ImageAdapter(imageList).apply {
|
||||||
setOnItemClickListener { _, _, position ->
|
setOnItemClickListener { _, _, position ->
|
||||||
imageList[position].name.copyText(this@LocalImagePreviewActivity)
|
// 点击缩略图,弹出大图预览,传入当前索引以支持左右切换
|
||||||
|
showFullImagePreview(position)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ class PurchaseOrderActivity : BaseActivity() {
|
|||||||
startActivity<ReceiptActivity> {
|
startActivity<ReceiptActivity> {
|
||||||
putExtra(GoodsListActivity.ID, item.id)
|
putExtra(GoodsListActivity.ID, item.id)
|
||||||
putExtra(GoodsListActivity.SUPPLIER_ID, item.supplierId)
|
putExtra(GoodsListActivity.SUPPLIER_ID, item.supplierId)
|
||||||
|
putExtra(GoodsListActivity.PUR_CODE, item.purCode)
|
||||||
putExtra(GoodsListActivity.IS_RECEIPT_PAGE, true)
|
putExtra(GoodsListActivity.IS_RECEIPT_PAGE, true)
|
||||||
//putExtra(GoodsListActivity.IS_NEW_RECEIPT, false)
|
//putExtra(GoodsListActivity.IS_NEW_RECEIPT, false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||||||
import com.sw.inbound.R
|
import com.sw.inbound.R
|
||||||
import com.sw.inbound.adapter.ReceiptGoodsAdapter
|
import com.sw.inbound.adapter.ReceiptGoodsAdapter
|
||||||
import com.sw.inbound.model.response.DictType
|
import com.sw.inbound.model.response.DictType
|
||||||
|
import com.sw.inbound.model.response.GoodsInfo
|
||||||
import com.sw.inbound.utils.ext.clickWithDebounce
|
import com.sw.inbound.utils.ext.clickWithDebounce
|
||||||
import com.sw.inbound.utils.ext.toJsonString
|
import com.sw.inbound.utils.ext.toJsonString
|
||||||
import com.sw.inbound.utils.ext.toast
|
import com.sw.inbound.utils.ext.toast
|
||||||
@@ -83,12 +84,7 @@ class ReceiptActivity : GoodsListActivity() {
|
|||||||
binding.tvPurchaseNo.text = "采购单号:${detail.purCode}"
|
binding.tvPurchaseNo.text = "采购单号:${detail.purCode}"
|
||||||
val list = detail.receiveGoodsInfoList
|
val list = detail.receiveGoodsInfoList
|
||||||
list.forEach { goodsInfo ->
|
list.forEach { goodsInfo ->
|
||||||
goodsInfo.recUnitPriceTaxInBak = goodsInfo.recUnitPriceTaxIn
|
resetGoods(goodsInfo)
|
||||||
goodsInfo.recUnitPriceTaxIn = 0.0
|
|
||||||
goodsInfo.recPriceExItemBak = goodsInfo.recPriceExItem
|
|
||||||
goodsInfo.receiveCountBak = goodsInfo.receiveCount
|
|
||||||
goodsInfo.unitNameBak = goodsInfo.unitName
|
|
||||||
goodsInfo.receivedNumBak = goodsInfo.receivedNum
|
|
||||||
}
|
}
|
||||||
goodsList.addAll(list)
|
goodsList.addAll(list)
|
||||||
adapter.notifyDataSetChanged()
|
adapter.notifyDataSetChanged()
|
||||||
@@ -97,6 +93,15 @@ class ReceiptActivity : GoodsListActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun resetGoods(item: GoodsInfo) {
|
||||||
|
item.recUnitPriceTaxInBak = item.recUnitPriceTaxIn
|
||||||
|
item.recUnitPriceTaxIn = 0.0
|
||||||
|
item.recPriceExItemBak = item.recPriceExItem
|
||||||
|
item.receiveCountBak = item.receiveCount
|
||||||
|
item.unitNameBak = item.unitName
|
||||||
|
item.receivedNumBak = item.receivedNum
|
||||||
|
}
|
||||||
|
|
||||||
override fun getWarehouseList(): List<DictType> {
|
override fun getWarehouseList(): List<DictType> {
|
||||||
return goodsList.filter {
|
return goodsList.filter {
|
||||||
it.warehouseId.isNullOrBlank().not() && it.warehouseName.isNullOrBlank().not()
|
it.warehouseId.isNullOrBlank().not() && it.warehouseName.isNullOrBlank().not()
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package com.sw.inbound.activity
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import com.sw.inbound.GlobalData
|
||||||
|
import com.sw.inbound.dialog.InboundGoodsSearchDialog
|
||||||
|
import com.sw.inbound.model.response.DictType
|
||||||
|
import com.sw.inbound.utils.ext.gone
|
||||||
|
import com.sw.inbound.viewmodel.StoreViewModel
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 入库称重页面
|
||||||
|
* 继承 GoodsListActivity 以复用秤监听、相机拍照、菜品识别弹窗等核心能力,
|
||||||
|
* 通过隐藏不需要的 View 精简界面,避免大量重复代码。
|
||||||
|
*/
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class StoreActivity : GoodsListActivity() {
|
||||||
|
|
||||||
|
val storeViewModel: StoreViewModel by viewModels()
|
||||||
|
|
||||||
|
/** 0=净菜,1=毛菜,由 HomeActivity 通过 Intent 传入 */
|
||||||
|
var storeType: Int = 0
|
||||||
|
|
||||||
|
/** 防止 onResume 重复弹窗 */
|
||||||
|
private var dialogShown = false
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
storeType = intent.getIntExtra(EXTRA_STORE_TYPE, 0)
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
hideUnusedViews()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
preloadInboundOptions()
|
||||||
|
if (!dialogShown) {
|
||||||
|
dialogShown = true
|
||||||
|
showEntryDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 进入页面统一打开检索弹窗,dismiss 时关闭页面 */
|
||||||
|
private fun showEntryDialog() {
|
||||||
|
InboundGoodsSearchDialog(this).apply {
|
||||||
|
setOnDismissListener { finish() }
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 预加载供应商和仓库区域列表到 GlobalData,供弹窗直接使用 */
|
||||||
|
private fun preloadInboundOptions() {
|
||||||
|
storeViewModel.loadSupplierOptions(callback = { GlobalData.inboundSupplierList = it })
|
||||||
|
storeViewModel.loadZoneOptions(callback = { GlobalData.inboundZoneList = it })
|
||||||
|
storeViewModel.loadSpecOptions(callback = { GlobalData.specOptionList = it })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 隐藏采购单列表、仓库下拉、提交按钮等本页面不需要的控件 */
|
||||||
|
private fun hideUnusedViews() {
|
||||||
|
binding.flWarehouseDropdown.gone()
|
||||||
|
binding.cvDropdownBg.gone()
|
||||||
|
binding.tvPurchaseNo.gone()
|
||||||
|
binding.btnConfirmReceipt.gone()
|
||||||
|
binding.tvConfirmNum.gone()
|
||||||
|
binding.tvNotConfirmNum.gone()
|
||||||
|
binding.flContainer.gone()
|
||||||
|
binding.rvGoodsList.gone()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun initRecyclerView() {}
|
||||||
|
|
||||||
|
override fun initView() {}
|
||||||
|
|
||||||
|
/** 入库页面暂不需要识别功能 */
|
||||||
|
override fun recognizeFood() {}
|
||||||
|
|
||||||
|
override fun getWarehouseList(): List<DictType> = emptyList()
|
||||||
|
|
||||||
|
/** 使用入库专用检索弹窗替代原有 GoodsSearchDialog */
|
||||||
|
override fun onGoodsSearchRequested() {
|
||||||
|
InboundGoodsSearchDialog(this).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val EXTRA_STORE_TYPE = "storeType"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,12 +8,13 @@ import androidx.core.graphics.toColorInt
|
|||||||
import com.chad.library.adapter4.BaseQuickAdapter
|
import com.chad.library.adapter4.BaseQuickAdapter
|
||||||
import com.chad.library.adapter4.viewholder.QuickViewHolder
|
import com.chad.library.adapter4.viewholder.QuickViewHolder
|
||||||
import com.sw.inbound.databinding.ListItemCollectSearchBinding
|
import com.sw.inbound.databinding.ListItemCollectSearchBinding
|
||||||
|
import com.sw.inbound.model.response.CollectedFoodInfo
|
||||||
import com.sw.inbound.model.response.SearchGoodsInfo
|
import com.sw.inbound.model.response.SearchGoodsInfo
|
||||||
import com.sw.inbound.utils.ext.dp
|
import com.sw.inbound.utils.ext.dp
|
||||||
import com.sw.inbound.utils.ext.setShapeDrawable
|
import com.sw.inbound.utils.ext.setShapeDrawable
|
||||||
|
|
||||||
class CollectSearchAdapter(var list: MutableList<SearchGoodsInfo.Record>) :
|
class CollectSearchAdapter(var list: MutableList<CollectedFoodInfo>) :
|
||||||
BaseQuickAdapter<SearchGoodsInfo.Record, CollectSearchAdapter.VH>(list) {
|
BaseQuickAdapter<CollectedFoodInfo, CollectSearchAdapter.VH>(list) {
|
||||||
|
|
||||||
inner class VH(var binding: ListItemCollectSearchBinding) : QuickViewHolder(binding.root)
|
inner class VH(var binding: ListItemCollectSearchBinding) : QuickViewHolder(binding.root)
|
||||||
|
|
||||||
@@ -23,19 +24,20 @@ class CollectSearchAdapter(var list: MutableList<SearchGoodsInfo.Record>) :
|
|||||||
return VH(binding)
|
return VH(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: VH, position: Int, item: SearchGoodsInfo.Record?) {
|
override fun onBindViewHolder(holder: VH, position: Int, item: CollectedFoodInfo?) {
|
||||||
holder.binding.let {
|
holder.binding.let {
|
||||||
var goodsName = item!!.goodsName?.split("WP")?.get(0)
|
val goodsName = item!!.foodName.split("WP")[0]
|
||||||
// if (goodsName?.contains("\n") == true) {
|
// if (goodsName?.contains("\n") == true) {
|
||||||
// goodsName = goodsName?.split("\n")?.get(0)
|
// goodsName = goodsName?.split("\n")?.get(0)
|
||||||
// }
|
// }
|
||||||
it.tvGoodsName.run {
|
it.tvGoodsName.run {
|
||||||
text = Html.fromHtml(goodsName)
|
text = Html.fromHtml(goodsName)
|
||||||
setTextColor(if (item!!.isSelected) "#FF0032C8".toColorInt() else "#FF666666".toColorInt())
|
setTextColor(if (item.isSelected) "#FF0032C8".toColorInt() else "#FF666666".toColorInt())
|
||||||
//setTextColor("#FF666666".toColorInt())
|
//setTextColor("#FF666666".toColorInt())
|
||||||
}
|
}
|
||||||
|
it.tvGoodsNum.text = "已采集${item.foodNum}"
|
||||||
it.root.run {
|
it.root.run {
|
||||||
if (item!!.isSelected) {
|
if (item.isSelected) {
|
||||||
setShapeDrawable(
|
setShapeDrawable(
|
||||||
solidColor = "#FFEAF0FF",
|
solidColor = "#FFEAF0FF",
|
||||||
strokeColor = "#FF0033CC",
|
strokeColor = "#FF0033CC",
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
package com.sw.inbound.adapter
|
||||||
|
|
||||||
|
import android.app.DatePickerDialog
|
||||||
|
import android.content.Context
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.InputType
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
|
import android.text.TextWatcher
|
||||||
|
import android.text.style.ForegroundColorSpan
|
||||||
|
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.ListItemFormFieldBinding
|
||||||
|
import com.sw.inbound.dialog.DropdownPopup
|
||||||
|
import com.sw.inbound.model.FieldType
|
||||||
|
import com.sw.inbound.model.FormField
|
||||||
|
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.visible
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Locale
|
||||||
|
import kotlin.math.min
|
||||||
|
import kotlin.text.clear
|
||||||
|
import kotlin.text.get
|
||||||
|
import kotlin.text.set
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动态表单字段 Adapter,配合 FlexboxLayoutManager 实现两列自动换行布局
|
||||||
|
* 单一 item 布局,通过 visibility 控制 TEXT/NUMBER/FIXED/DROPDOWN/DATE_PICKER 五种类型渲染
|
||||||
|
*
|
||||||
|
* @param activityContext Activity context,用于创建 DatePickerDialog
|
||||||
|
* @param fields 表单字段列表(hidden=false 的字段,由外部过滤后传入)
|
||||||
|
*/
|
||||||
|
class FormFieldAdapter(
|
||||||
|
private val activityContext: Context,
|
||||||
|
fields: MutableList<FormField>
|
||||||
|
) : BaseQuickAdapter<FormField, FormFieldAdapter.VH>(fields) {
|
||||||
|
|
||||||
|
private val isoFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
|
||||||
|
private val displayDateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
|
||||||
|
|
||||||
|
inner class VH(val binding: ListItemFormFieldBinding) : QuickViewHolder(binding.root)
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
|
||||||
|
val binding = ListItemFormFieldBinding.inflate(LayoutInflater.from(context), parent, false)
|
||||||
|
return VH(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: VH, position: Int, item: FormField?) {
|
||||||
|
item ?: return
|
||||||
|
val b = holder.binding
|
||||||
|
|
||||||
|
// 设置标签,必填项追加红色星号
|
||||||
|
b.tvLabel.text = buildLabelSpannable(item.label, item.required)
|
||||||
|
|
||||||
|
// 先全部隐藏,再按类型显示对应控件
|
||||||
|
b.etInput.gone()
|
||||||
|
b.flDropdown.gone()
|
||||||
|
b.flDatePicker.gone()
|
||||||
|
|
||||||
|
// 移除旧的 TextWatcher,防止 RecyclerView 复用时错误触发
|
||||||
|
(b.etInput.tag as? TextWatcher)?.let { b.etInput.removeTextChangedListener(it) }
|
||||||
|
b.etInput.tag = null
|
||||||
|
|
||||||
|
when (item.type) {
|
||||||
|
FieldType.TEXT -> bindEditText(b, item, InputType.TYPE_CLASS_TEXT)
|
||||||
|
FieldType.NUMBER -> bindEditText(
|
||||||
|
b, item,
|
||||||
|
InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL or InputType.TYPE_NUMBER_FLAG_SIGNED
|
||||||
|
)
|
||||||
|
FieldType.FIXED -> bindFixed(b, item)
|
||||||
|
FieldType.DROPDOWN -> bindDropdown(b, item)
|
||||||
|
FieldType.DATE_PICKER -> bindDatePicker(b, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 绑定可编辑文本框(TEXT / NUMBER) */
|
||||||
|
private fun bindEditText(b: ListItemFormFieldBinding, item: FormField, inputType: Int) {
|
||||||
|
b.etInput.visible()
|
||||||
|
b.etInput.isEnabled = true
|
||||||
|
b.etInput.inputType = inputType
|
||||||
|
b.etInput.hint = item.hint.ifBlank { item.label }
|
||||||
|
b.etInput.setBackgroundResource(R.drawable.bg_white_radius10_stroke2)
|
||||||
|
b.etInput.setText(item.value)
|
||||||
|
b.etInput.setSelection(b.etInput.text.length)
|
||||||
|
|
||||||
|
val watcher = object : TextWatcher {
|
||||||
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||||
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||||
|
override fun afterTextChanged(s: Editable?) {
|
||||||
|
item.value = s?.toString() ?: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.etInput.addTextChangedListener(watcher)
|
||||||
|
b.etInput.tag = watcher
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 绑定固定值(不可编辑,灰色背景区分) */
|
||||||
|
private fun bindFixed(b: ListItemFormFieldBinding, item: FormField) {
|
||||||
|
b.etInput.visible()
|
||||||
|
b.etInput.isEnabled = false
|
||||||
|
b.etInput.inputType = InputType.TYPE_CLASS_TEXT
|
||||||
|
b.etInput.hint = ""
|
||||||
|
b.etInput.setBackgroundResource(R.drawable.bg_gray_radius10)
|
||||||
|
b.etInput.setText(item.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 绑定下拉选择框,点击时创建 DropdownPopup */
|
||||||
|
private fun bindDropdown(b: ListItemFormFieldBinding, item: FormField) {
|
||||||
|
b.flDropdown.visible()
|
||||||
|
b.tvDropdownValue.hint = item.hint.ifBlank { "请选择${item.label}" }
|
||||||
|
b.tvDropdownValue.text = item.value.ifBlank { null }
|
||||||
|
|
||||||
|
b.flDropdown.setOnClickListener { v ->
|
||||||
|
v.hideKeyboard()
|
||||||
|
val popHeight = min(item.options.size.coerceAtLeast(1), 4) * 71.dp
|
||||||
|
DropdownPopup(
|
||||||
|
context = activityContext,
|
||||||
|
list = item.options.toMutableList(),
|
||||||
|
popWidth = v.width,
|
||||||
|
popHeight = popHeight
|
||||||
|
) { dictType ->
|
||||||
|
item.value = dictType.value ?: ""
|
||||||
|
b.tvDropdownValue.text = item.value
|
||||||
|
// 将 extraApiKeys 映射的 DictType 字段值写入 extraValues
|
||||||
|
item.extraValues.clear()
|
||||||
|
item.extraApiKeys.forEach { (apiKey, dictField) ->
|
||||||
|
item.extraValues[apiKey] = dictType[dictField]
|
||||||
|
}
|
||||||
|
}.also {
|
||||||
|
it.bgLayout = b.flDropdown
|
||||||
|
it.showAsDropDown(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 绑定日期选择器,点击时弹出 DatePickerDialog */
|
||||||
|
private fun bindDatePicker(b: ListItemFormFieldBinding, item: FormField) {
|
||||||
|
b.flDatePicker.visible()
|
||||||
|
b.tvDateValue.hint = item.hint.ifBlank { "请选择日期" }
|
||||||
|
b.tvDateValue.text = item.value.ifBlank { null }
|
||||||
|
|
||||||
|
b.flDatePicker.setOnClickListener { v ->
|
||||||
|
v.hideKeyboard()
|
||||||
|
val cal = Calendar.getInstance()
|
||||||
|
DatePickerDialog(
|
||||||
|
activityContext,
|
||||||
|
{ _, year, month, dayOfMonth ->
|
||||||
|
val selected = Calendar.getInstance().apply {
|
||||||
|
set(year, month, dayOfMonth, 0, 0, 0)
|
||||||
|
set(Calendar.MILLISECOND, 0)
|
||||||
|
}
|
||||||
|
// value 存显示格式,valueId 存 ISO 格式供提交使用
|
||||||
|
item.value = displayDateFormat.format(selected.time)
|
||||||
|
b.tvDateValue.text = item.value
|
||||||
|
},
|
||||||
|
cal.get(Calendar.YEAR),
|
||||||
|
cal.get(Calendar.MONTH),
|
||||||
|
cal.get(Calendar.DAY_OF_MONTH)
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 构建带红色星号的必填标签 */
|
||||||
|
private fun buildLabelSpannable(label: String, required: Boolean): SpannableStringBuilder {
|
||||||
|
return SpannableStringBuilder().apply {
|
||||||
|
append(label)
|
||||||
|
if (required) {
|
||||||
|
val start = length
|
||||||
|
append(" *")
|
||||||
|
setSpan(
|
||||||
|
ForegroundColorSpan("#FFCC0000".toColorInt()),
|
||||||
|
start, length,
|
||||||
|
android.text.Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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 androidx.recyclerview.widget.DiffUtil
|
||||||
|
import com.chad.library.adapter4.BaseDifferAdapter
|
||||||
|
import com.chad.library.adapter4.viewholder.QuickViewHolder
|
||||||
|
import com.sw.inbound.databinding.ListItemGoodsSearchBinding
|
||||||
|
import com.sw.inbound.model.response.GoodsSearchModel
|
||||||
|
import com.sw.inbound.utils.ext.dp
|
||||||
|
import com.sw.inbound.utils.ext.setShapeDrawable
|
||||||
|
|
||||||
|
class InboundSearchAdapter :
|
||||||
|
BaseDifferAdapter<GoodsSearchModel, InboundSearchAdapter.VH>(DIFF_CALLBACK) {
|
||||||
|
|
||||||
|
inner class VH(val binding: ListItemGoodsSearchBinding) : QuickViewHolder(binding.root)
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
|
||||||
|
val binding = ListItemGoodsSearchBinding.inflate(LayoutInflater.from(context), parent, false)
|
||||||
|
return VH(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: VH, position: Int, item: GoodsSearchModel?) {
|
||||||
|
holder.binding.tvGoodsName.run {
|
||||||
|
text = item?.materName
|
||||||
|
setTextColor(
|
||||||
|
if (item?.isSelected == true) "#FF0032C8".toColorInt()
|
||||||
|
else "#FF666666".toColorInt()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
holder.binding.root.run {
|
||||||
|
if (item?.isSelected == true) {
|
||||||
|
setShapeDrawable(
|
||||||
|
solidColor = "#FFEAF0FF",
|
||||||
|
strokeColor = "#FF0033CC",
|
||||||
|
strokeWidth = 2.dp,
|
||||||
|
radius = 10.dp
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
setShapeDrawable(solidColor = "#FFF5F5F5", radius = 10.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<GoodsSearchModel>() {
|
||||||
|
override fun areItemsTheSame(old: GoodsSearchModel, new: GoodsSearchModel) =
|
||||||
|
old.materId == new.materId
|
||||||
|
|
||||||
|
override fun areContentsTheSame(old: GoodsSearchModel, new: GoodsSearchModel) =
|
||||||
|
old == new
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -176,6 +176,7 @@ class ReceiptGoodsAdapter(var receiptActivity: ReceiptActivity, var list: Mutabl
|
|||||||
item.recPriceInItem = 0.0
|
item.recPriceInItem = 0.0
|
||||||
item.recUnitPriceTaxIn = 0.0
|
item.recUnitPriceTaxIn = 0.0
|
||||||
item.unitName = item.unitNameBak
|
item.unitName = item.unitNameBak
|
||||||
|
item.goodsWeight = null
|
||||||
notifyItemChanged(position)
|
notifyItemChanged(position)
|
||||||
receiptActivity.run {
|
receiptActivity.run {
|
||||||
userViewModel.putGoodsData(localGoodsKey, list.toJsonString())
|
userViewModel.putGoodsData(localGoodsKey, list.toJsonString())
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.sw.inbound.base
|
package com.sw.inbound.base
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.WindowInsetsControllerCompat
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
@@ -12,7 +12,7 @@ import androidx.core.view.WindowInsetsControllerCompat
|
|||||||
* - 自动隐藏状态栏和导航栏,实现全屏沉浸式体验
|
* - 自动隐藏状态栏和导航栏,实现全屏沉浸式体验
|
||||||
* - 在屏幕底部上滑时可临时唤出导航栏,松手后自动收回
|
* - 在屏幕底部上滑时可临时唤出导航栏,松手后自动收回
|
||||||
*/
|
*/
|
||||||
abstract class BaseActivity : ComponentActivity() {
|
abstract class BaseActivity : AppCompatActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package com.sw.inbound.di
|
||||||
|
|
||||||
|
import com.google.gson.GsonBuilder
|
||||||
|
import com.sw.inbound.GlobalData
|
||||||
|
import com.sw.inbound.MyApp
|
||||||
|
import com.sw.inbound.network.api.InboundApiService
|
||||||
|
import com.sw.inbound.network.interceptor.InboundInterceptor
|
||||||
|
import com.sw.inbound.network.interceptor.RequestInterceptor
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Response
|
||||||
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.inject.Named
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
object InboundNetworkModule {
|
||||||
|
|
||||||
|
// private const val INBOUND_BASE_URL = "http://192.168.10.101:24801/"
|
||||||
|
private const val TIME_OUT = 30L
|
||||||
|
|
||||||
|
/** 动态域名拦截器:将 Retrofit 请求的 baseUrl 实时替换为 [GlobalData.appBaseUrl] */
|
||||||
|
class DynamicBaseUrlInterceptor : Interceptor {
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val request = chain.request()
|
||||||
|
val newUrl = GlobalData.appBaseUrl.toHttpUrlOrNull() ?: return chain.proceed(request)
|
||||||
|
val url = request.url.newBuilder()
|
||||||
|
.scheme(newUrl.scheme)
|
||||||
|
.host(newUrl.host)
|
||||||
|
.port(newUrl.port)
|
||||||
|
.build()
|
||||||
|
return chain.proceed(request.newBuilder().url(url).build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
@Named("inbound")
|
||||||
|
fun provideInboundHttpClient(): OkHttpClient {
|
||||||
|
return OkHttpClient.Builder()
|
||||||
|
.connectTimeout(TIME_OUT, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(TIME_OUT, TimeUnit.SECONDS)
|
||||||
|
.writeTimeout(TIME_OUT, TimeUnit.SECONDS)
|
||||||
|
.addNetworkInterceptor(HttpLoggingInterceptor(logger = {
|
||||||
|
Timber.d("inbound okhttp ==> $it")
|
||||||
|
}).apply {
|
||||||
|
level = if (MyApp.DEBUG) HttpLoggingInterceptor.Level.BODY
|
||||||
|
else HttpLoggingInterceptor.Level.NONE
|
||||||
|
})
|
||||||
|
.addInterceptor(DynamicBaseUrlInterceptor())
|
||||||
|
// .addInterceptor(InboundInterceptor())
|
||||||
|
.addInterceptor(RequestInterceptor())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
@Named("inbound")
|
||||||
|
fun provideInboundRetrofit(@Named("inbound") okHttpClient: OkHttpClient): Retrofit {
|
||||||
|
val gson = GsonBuilder().setLenient().create()
|
||||||
|
return Retrofit.Builder()
|
||||||
|
.baseUrl(GlobalData.appBaseUrl)
|
||||||
|
.client(okHttpClient)
|
||||||
|
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideInboundApiService(@Named("inbound") retrofit: Retrofit): InboundApiService {
|
||||||
|
return retrofit.create(InboundApiService::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.sw.inbound.di
|
package com.sw.inbound.di
|
||||||
|
|
||||||
|
import com.google.gson.GsonBuilder
|
||||||
import com.sw.inbound.MyApp
|
import com.sw.inbound.MyApp
|
||||||
import com.sw.inbound.network.api.ApiService
|
import com.sw.inbound.network.api.ApiService
|
||||||
import com.sw.inbound.network.interceptor.RequestInterceptor
|
import com.sw.inbound.network.interceptor.RequestInterceptor
|
||||||
@@ -51,11 +52,15 @@ object NetworkModule {
|
|||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
|
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
|
||||||
|
val gson = GsonBuilder()
|
||||||
|
.setLenient()
|
||||||
|
.create()
|
||||||
|
|
||||||
return Retrofit.Builder()
|
return Retrofit.Builder()
|
||||||
// .baseUrl(DEVICE_BASE_URL)
|
// .baseUrl(DEVICE_BASE_URL)
|
||||||
.baseUrl(BASE_URL)
|
.baseUrl(BASE_URL)
|
||||||
.client(okHttpClient)
|
.client(okHttpClient)
|
||||||
.addConverterFactory(GsonConverterFactory.create())
|
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,11 +14,17 @@ import com.sw.inbound.activity.GoodsListActivity
|
|||||||
import com.sw.inbound.utils.ext.dp
|
import com.sw.inbound.utils.ext.dp
|
||||||
import com.sw.inbound.utils.ext.hideKeyboard
|
import com.sw.inbound.utils.ext.hideKeyboard
|
||||||
|
|
||||||
|
//测试设备,小屏幕
|
||||||
|
private val devices = listOf(
|
||||||
|
"f0bcabbb-0d58-3b14-8bed-9863b7ec61ef",
|
||||||
|
"3dff871a-c66a-3fd8-93ce-81b57ead80f4"
|
||||||
|
)
|
||||||
|
|
||||||
abstract class BaseDialog(
|
abstract class BaseDialog(
|
||||||
context: Context,
|
context: Context,
|
||||||
var defWidth: Int = 1500.dp,
|
var defWidth: Int = 1500.dp,
|
||||||
// var defHeight: Int = 900.dp
|
// var defHeight: Int = 900.dp
|
||||||
var defHeight: Int = if ("f0bcabbb-0d58-3b14-8bed-9863b7ec61ef" == GlobalData.deviceId) 750.dp else 900.dp
|
var defHeight: Int = if (devices.contains(GlobalData.deviceId)) 750.dp else 900.dp
|
||||||
) : Dialog(context, R.style.DialogTheme) {
|
) : Dialog(context, R.style.DialogTheme) {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import android.content.Context
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.core.widget.addTextChangedListener
|
import androidx.core.widget.addTextChangedListener
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.sw.inbound.R
|
import com.sw.inbound.R
|
||||||
@@ -13,6 +15,7 @@ import com.sw.inbound.activity.FoodCollectionActivity
|
|||||||
import com.sw.inbound.adapter.CollectSearchAdapter
|
import com.sw.inbound.adapter.CollectSearchAdapter
|
||||||
import com.sw.inbound.databinding.DialogCollectSearchBinding
|
import com.sw.inbound.databinding.DialogCollectSearchBinding
|
||||||
import com.sw.inbound.databinding.LayoutEmptySearchBinding
|
import com.sw.inbound.databinding.LayoutEmptySearchBinding
|
||||||
|
import com.sw.inbound.model.response.CollectedFoodInfo
|
||||||
import com.sw.inbound.model.response.SearchGoodsInfo
|
import com.sw.inbound.model.response.SearchGoodsInfo
|
||||||
import com.sw.inbound.objbox.ObjectBox
|
import com.sw.inbound.objbox.ObjectBox
|
||||||
import com.sw.inbound.utils.ext.addOnActionSearchListener
|
import com.sw.inbound.utils.ext.addOnActionSearchListener
|
||||||
@@ -28,6 +31,32 @@ class CollectSearchDialog(
|
|||||||
private lateinit var inflater: LayoutInflater
|
private lateinit var inflater: LayoutInflater
|
||||||
private var emptyBinding: LayoutEmptySearchBinding? = null
|
private var emptyBinding: LayoutEmptySearchBinding? = null
|
||||||
private lateinit var binding: DialogCollectSearchBinding
|
private lateinit var binding: DialogCollectSearchBinding
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 收集 searchFoodState
|
||||||
|
*/
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
private fun initObserver() {
|
||||||
|
activity?.run {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
userViewModel.collectedFoodListState.collect { items ->
|
||||||
|
binding.rvSearch.layoutManager =
|
||||||
|
GridLayoutManager(activity, 2, LinearLayoutManager.VERTICAL, false)
|
||||||
|
list.clear()
|
||||||
|
list.addAll(items)
|
||||||
|
searchAdapter.notifyDataSetChanged()
|
||||||
|
finishRefresh()
|
||||||
|
binding.root.hideKeyboard()
|
||||||
|
if (items.isEmpty()) {
|
||||||
|
loadEmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun getRootView(): View {
|
override fun getRootView(): View {
|
||||||
inflater = LayoutInflater.from(context)
|
inflater = LayoutInflater.from(context)
|
||||||
binding = DialogCollectSearchBinding.inflate(inflater)
|
binding = DialogCollectSearchBinding.inflate(inflater)
|
||||||
@@ -82,23 +111,14 @@ class CollectSearchDialog(
|
|||||||
this@CollectSearchDialog.clickIndex = -1
|
this@CollectSearchDialog.clickIndex = -1
|
||||||
val searchName = binding.etInputGoods.text.toString().trim()
|
val searchName = binding.etInputGoods.text.toString().trim()
|
||||||
getCollectGoods(searchName)
|
getCollectGoods(searchName)
|
||||||
}
|
|
||||||
//it.setOnLoadMoreListener {
|
|
||||||
// searchGoods()
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
getCollectGoods()
|
|
||||||
//binding.root.clickWithDebounce {
|
|
||||||
// Thread {
|
|
||||||
// FoodModule.initDefFoodData(activity!!) {
|
|
||||||
// activity?.runOnUiThread {
|
|
||||||
// activity?.toast("添加完成")
|
|
||||||
// getCollectGoods()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }.start()
|
|
||||||
//}
|
|
||||||
|
|
||||||
|
binding.root.postDelayed({
|
||||||
|
finishRefresh()
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
initObserver()
|
||||||
|
getCollectGoods()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var pageNo = 1
|
private var pageNo = 1
|
||||||
@@ -118,17 +138,17 @@ class CollectSearchDialog(
|
|||||||
|
|
||||||
private var clickIndex = -1
|
private var clickIndex = -1
|
||||||
|
|
||||||
private val list: MutableList<SearchGoodsInfo.Record> = mutableListOf()
|
private val list: MutableList<CollectedFoodInfo> = mutableListOf()
|
||||||
private val searchAdapter by lazy {
|
private val searchAdapter by lazy {
|
||||||
CollectSearchAdapter(list).apply {
|
CollectSearchAdapter(list).apply {
|
||||||
isStateViewEnable = true
|
isStateViewEnable = true
|
||||||
setOnItemClickListener { _, _, position ->
|
// setOnItemClickListener { _, _, position ->
|
||||||
activity?.lifecycleScope?.launch {
|
// activity?.lifecycleScope?.launch {
|
||||||
val goodsName = list[position].goodsName?:""
|
// val goodsName = list[position].goodsName?:""
|
||||||
val count = ObjectBox.count(goodsName)
|
// val count = ObjectBox.countNameField(goodsName)
|
||||||
activity?.toast("已采集数据${count}条")
|
// activity?.toast("已采集数据${count}条")
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
addOnItemChildClickListener(R.id.ivDelete) { _, _, position ->
|
addOnItemChildClickListener(R.id.ivDelete) { _, _, position ->
|
||||||
this@CollectSearchDialog.clickIndex = position
|
this@CollectSearchDialog.clickIndex = position
|
||||||
list.forEach { item ->
|
list.forEach { item ->
|
||||||
@@ -144,44 +164,22 @@ class CollectSearchDialog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun showDeleteDialog(position: Int, deleteAction: () -> Unit = {}) {
|
private fun showDeleteDialog(position: Int, deleteAction: () -> Unit = {}) {
|
||||||
|
val foodName = list[position].foodName
|
||||||
WarnDialog(
|
WarnDialog(
|
||||||
context = context,
|
context = context,
|
||||||
content = "请确认是否删除物品:${list[position].goodsName?.split("WP")?.get(0)}?",
|
content = "请确认是否删除物品:${foodName.split("WP").first()}?",
|
||||||
cancelBlock = {
|
cancelBlock = {
|
||||||
this@CollectSearchDialog.clickIndex = -1
|
this@CollectSearchDialog.clickIndex = -1
|
||||||
list[position].isSelected = false
|
list[position].isSelected = false
|
||||||
searchAdapter.notifyItemChanged(position)
|
searchAdapter.notifyItemChanged(position)
|
||||||
},
|
},
|
||||||
confirmBlock = {
|
confirmBlock = {
|
||||||
// Thread {
|
activity?.userViewModel?.removeCollectedFood(
|
||||||
// }.start()
|
foodName = foodName,
|
||||||
activity?.lifecycleScope?.launch {
|
nameFilter = binding.etInputGoods.text.toString().trim()
|
||||||
deleteGoods(position, deleteAction)
|
)
|
||||||
}
|
|
||||||
}).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun deleteGoods(position: Int, deleteAction: () -> Unit = {}) {
|
|
||||||
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)
|
|
||||||
// }
|
|
||||||
ObjectBox.remove(name)
|
|
||||||
activity?.runOnUiThread {
|
|
||||||
Loading.dismiss()
|
|
||||||
//list.remove(list[position])
|
|
||||||
//searchAdapter.notifyDataSetChanged()
|
|
||||||
searchAdapter.removeAt(position)
|
|
||||||
activity?.toast("删除成功")
|
|
||||||
if (list.isEmpty()) {
|
|
||||||
loadEmptyView()
|
|
||||||
}
|
|
||||||
deleteAction()
|
deleteAction()
|
||||||
}
|
}).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun finishRefresh() {
|
private fun finishRefresh() {
|
||||||
@@ -197,27 +195,8 @@ class CollectSearchDialog(
|
|||||||
// private var box: Box<Food>? = null
|
// private var box: Box<Food>? = null
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
private fun getCollectGoods(searchName: String? = null) = activity?.lifecycleScope?.launch {
|
private fun getCollectGoods(name: String? = null) {
|
||||||
// if (box == null) {
|
activity?.userViewModel?.loadCollectedFoodList(nameFilter = name)
|
||||||
// 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 }
|
|
||||||
// }
|
|
||||||
val queryList = ObjectBox.filter(searchName)
|
|
||||||
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() {
|
// fun test() {
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package com.sw.inbound.dialog
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.WindowManager
|
||||||
|
import com.sw.inbound.GlobalData
|
||||||
|
import com.sw.inbound.GlobalKey
|
||||||
|
import com.sw.inbound.databinding.DialogEnvSwitchBinding
|
||||||
|
import com.sw.inbound.utils.SPUtil
|
||||||
|
import com.sw.inbound.utils.ext.dp
|
||||||
|
import com.sw.inbound.utils.ext.toast
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 环境切换弹窗
|
||||||
|
*
|
||||||
|
* 提供 TEST / UAT / PROD 三套环境的切换功能,
|
||||||
|
* 初始化时自动根据 [GlobalData.appBaseUrl] 选中当前环境,
|
||||||
|
* 确认后更新 [GlobalData.appBaseUrl] 并通过 [onEnvChanged] 回调通知外部。
|
||||||
|
*
|
||||||
|
* @param context 上下文
|
||||||
|
* @param onEnvChanged 确认切换后的回调,参数为新的 baseUrl
|
||||||
|
*/
|
||||||
|
class EnvSwitchDialog(
|
||||||
|
context: Context,
|
||||||
|
private val onEnvChanged: (newBaseUrl: String) -> Unit = {}
|
||||||
|
) : BaseDialog(
|
||||||
|
context,
|
||||||
|
defWidth = 600.dp,
|
||||||
|
defHeight = WindowManager.LayoutParams.WRAP_CONTENT
|
||||||
|
) {
|
||||||
|
|
||||||
|
private lateinit var binding: DialogEnvSwitchBinding
|
||||||
|
|
||||||
|
override fun getRootView(): View {
|
||||||
|
binding = DialogEnvSwitchBinding.inflate(LayoutInflater.from(context))
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun initView() {
|
||||||
|
// 根据当前 appBaseUrl 预选对应 RadioButton,不匹配则不选
|
||||||
|
when (GlobalData.appBaseUrl) {
|
||||||
|
GlobalData.LOCAL_BASE_URL -> binding.rbTest.isChecked = true
|
||||||
|
GlobalData.TEST_BASE_URL -> binding.rbUat.isChecked = true
|
||||||
|
GlobalData.PROD_BASE_URL -> binding.rbProd.isChecked = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消按钮
|
||||||
|
binding.btnCancel.setOnClickListener { dismiss() }
|
||||||
|
|
||||||
|
// 确认按钮
|
||||||
|
binding.btnConfirm.setOnClickListener {
|
||||||
|
// 未选中任何选项时提示用户
|
||||||
|
if (binding.rgEnv.checkedRadioButtonId == -1) {
|
||||||
|
context.toast("请选择环境")
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
val newUrl = when (binding.rgEnv.checkedRadioButtonId) {
|
||||||
|
binding.rbTest.id -> GlobalData.LOCAL_BASE_URL
|
||||||
|
binding.rbUat.id -> GlobalData.TEST_BASE_URL
|
||||||
|
binding.rbProd.id -> GlobalData.PROD_BASE_URL
|
||||||
|
else -> return@setOnClickListener
|
||||||
|
}
|
||||||
|
GlobalData.appBaseUrl = newUrl
|
||||||
|
SPUtil.getInstance().put(GlobalKey.KEY_BASE_URL, newUrl)
|
||||||
|
onEnvChanged(newUrl)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,8 +6,10 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.sw.inbound.activity.GoodsListActivity
|
import com.sw.inbound.activity.GoodsListActivity
|
||||||
|
import com.sw.inbound.activity.StoreActivity
|
||||||
import com.sw.inbound.adapter.RecognizeAdapter
|
import com.sw.inbound.adapter.RecognizeAdapter
|
||||||
import com.sw.inbound.databinding.DialogGoodsRecognizeBinding
|
import com.sw.inbound.databinding.DialogGoodsRecognizeBinding
|
||||||
|
import com.sw.inbound.model.response.GoodsSearchModel
|
||||||
import com.sw.inbound.model.response.SearchGoodsInfo
|
import com.sw.inbound.model.response.SearchGoodsInfo
|
||||||
import com.sw.inbound.utils.ext.clickWithDebounce
|
import com.sw.inbound.utils.ext.clickWithDebounce
|
||||||
import com.sw.inbound.utils.ext.gone
|
import com.sw.inbound.utils.ext.gone
|
||||||
@@ -57,6 +59,17 @@ class GoodsRecognizeDialog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun showGoodsStoreDialog(position: Int) {
|
private fun showGoodsStoreDialog(position: Int) {
|
||||||
|
if (activity is StoreActivity) {
|
||||||
|
// 入库称重页面:识别结果无溯源码,直接用物品名称构造 GoodsSearchModel 打开入库录入弹窗
|
||||||
|
val goods = list[position]
|
||||||
|
InboundGoodsStoreDialog(
|
||||||
|
context = context,
|
||||||
|
goods = GoodsSearchModel(materName = goods.goodsName ?: "")
|
||||||
|
) {
|
||||||
|
dismiss()
|
||||||
|
}.show()
|
||||||
|
return
|
||||||
|
}
|
||||||
GoodsStoreDialog(
|
GoodsStoreDialog(
|
||||||
context = context,
|
context = context,
|
||||||
funcType = 0,
|
funcType = 0,
|
||||||
|
|||||||
@@ -225,17 +225,17 @@ class GoodsStoreDialog(
|
|||||||
val item = activity?.goodsList?.firstOrNull { it.goodId == goods.id }
|
val item = activity?.goodsList?.firstOrNull { it.goodId == goods.id }
|
||||||
if (item == null || dropdownList.isEmpty()) return
|
if (item == null || dropdownList.isEmpty()) return
|
||||||
val unit = item.unitNameBak
|
val unit = item.unitNameBak
|
||||||
val price = item.recPriceExItemBak
|
val amount = item.recPriceExItemBak.removeTrailingZeros()
|
||||||
val amount = item.recUnitPriceTaxInBak
|
val price = item.recUnitPriceTaxInBak.removeTrailingZeros()
|
||||||
val count = item.receiveCountBak
|
val count = item.receiveCountBak.removeTrailingZeros()
|
||||||
|
|
||||||
val dictType = dropdownList.firstOrNull { it.value == unit }
|
val dictType = dropdownList.firstOrNull { it.value == unit }
|
||||||
if (dictType == null) return
|
if (dictType == null) return
|
||||||
loadDropdownMenu(dictType)
|
loadDropdownMenu(dictType)
|
||||||
|
|
||||||
count?.let { binding.etProcurementCount.setText("$count") }
|
binding.etProcurementCount.setText(count)
|
||||||
price?.let { binding.tvProcurementPrice.text = "$price" }
|
binding.tvProcurementPrice.text = price
|
||||||
amount?.let { binding.etProcurementAmount.setText("$amount") }
|
binding.etProcurementAmount.setText(amount)
|
||||||
isLoadPurchaseOrderInfo = true
|
isLoadPurchaseOrderInfo = true
|
||||||
}
|
}
|
||||||
private var isLoadPurchaseOrderInfo = true
|
private var isLoadPurchaseOrderInfo = true
|
||||||
@@ -357,7 +357,8 @@ class GoodsStoreDialog(
|
|||||||
context.toast("入库金额不能为0")
|
context.toast("入库金额不能为0")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (funcType == 1) {
|
// if (funcType == 1) {
|
||||||
|
//无论是否识别到物品,都上传图片增加样本数据
|
||||||
activity?.let { Loading.show(it) }
|
activity?.let { Loading.show(it) }
|
||||||
cameraUtils.takePhoto { uri ->
|
cameraUtils.takePhoto { uri ->
|
||||||
//imageUri = uri
|
//imageUri = uri
|
||||||
@@ -375,9 +376,9 @@ class GoodsStoreDialog(
|
|||||||
confirm(unitName, count, price, totalPrice)
|
confirm(unitName, count, price, totalPrice)
|
||||||
}, 500)
|
}, 500)
|
||||||
}
|
}
|
||||||
} else {
|
// } else {
|
||||||
confirm(unitName, count, price, totalPrice)
|
// confirm(unitName, count, price, totalPrice)
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun confirm(unitName: String, count: Double, price: Double, totalPrice: Double?) {
|
private fun confirm(unitName: String, count: Double, price: Double, totalPrice: Double?) {
|
||||||
@@ -423,7 +424,7 @@ class GoodsStoreDialog(
|
|||||||
|
|
||||||
override fun show() {
|
override fun show() {
|
||||||
super.show()
|
super.show()
|
||||||
if (funcType == 1) {
|
// if (funcType == 1) {
|
||||||
takePhotoBinding?.let {
|
takePhotoBinding?.let {
|
||||||
cameraUtils.setPreviewController(previewView)
|
cameraUtils.setPreviewController(previewView)
|
||||||
cameraUtils.bind()
|
cameraUtils.bind()
|
||||||
@@ -433,7 +434,7 @@ class GoodsStoreDialog(
|
|||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
// }
|
||||||
binding.root.postDelayed({
|
binding.root.postDelayed({
|
||||||
SensorScaleUtils.startScale()
|
SensorScaleUtils.startScale()
|
||||||
}, 2600)
|
}, 2600)
|
||||||
@@ -454,7 +455,7 @@ class GoodsStoreDialog(
|
|||||||
private var imagePreviewBinding: LayoutImagePreviewBinding? = null
|
private var imagePreviewBinding: LayoutImagePreviewBinding? = null
|
||||||
private fun addChildToContainer() {
|
private fun addChildToContainer() {
|
||||||
val inflater = LayoutInflater.from(context)
|
val inflater = LayoutInflater.from(context)
|
||||||
if (funcType == 1) {
|
// if (funcType == 1) {
|
||||||
//预览及拍照
|
//预览及拍照
|
||||||
takePhotoBinding = LayoutTakePhotoBinding.inflate(inflater, binding.flContainer, true)
|
takePhotoBinding = LayoutTakePhotoBinding.inflate(inflater, binding.flContainer, true)
|
||||||
activity?.unBindCamera()
|
activity?.unBindCamera()
|
||||||
@@ -465,7 +466,7 @@ class GoodsStoreDialog(
|
|||||||
takePhotoBinding?.let {
|
takePhotoBinding?.let {
|
||||||
previewView = it.previewView
|
previewView = it.previewView
|
||||||
it.flCameraPreview.visible()
|
it.flCameraPreview.visible()
|
||||||
it.cvImageBg.gone()
|
// it.cvImageBg.gone()
|
||||||
// it.btnStartPreview.setOnClickListener { _ ->
|
// it.btnStartPreview.setOnClickListener { _ ->
|
||||||
// it.cvImageBg.gone()
|
// it.cvImageBg.gone()
|
||||||
// it.flCameraPreview.visible()
|
// it.flCameraPreview.visible()
|
||||||
@@ -474,8 +475,7 @@ class GoodsStoreDialog(
|
|||||||
// cameraUtils.takePhoto(takePhotoCallback)
|
// cameraUtils.takePhoto(takePhotoCallback)
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
return
|
if(funcType != 1){
|
||||||
}
|
|
||||||
//只显示原始图片
|
//只显示原始图片
|
||||||
imagePreviewBinding = LayoutImagePreviewBinding.inflate(inflater, binding.flContainer)
|
imagePreviewBinding = LayoutImagePreviewBinding.inflate(inflater, binding.flContainer)
|
||||||
imagePreviewBinding?.imageView?.load(goods.relativeUrl) {
|
imagePreviewBinding?.imageView?.load(goods.relativeUrl) {
|
||||||
@@ -490,6 +490,7 @@ class GoodsStoreDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var imageFile: File? = null
|
private var imageFile: File? = null
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
package com.sw.inbound.dialog
|
||||||
|
|
||||||
|
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.StoreActivity
|
||||||
|
import com.sw.inbound.adapter.InboundSearchAdapter
|
||||||
|
import com.sw.inbound.databinding.DialogInboundGoodsSearchBinding
|
||||||
|
import com.sw.inbound.databinding.LayoutEmptySearchBinding
|
||||||
|
import com.sw.inbound.model.response.GoodsSearchModel
|
||||||
|
import com.sw.inbound.utils.ext.addOnActionSearchListener
|
||||||
|
import com.sw.inbound.utils.ext.hideKeyboard
|
||||||
|
import com.sw.inbound.utils.ext.toast
|
||||||
|
|
||||||
|
/** 入库物品检索弹窗,使用溯源码查询食材,供 StoreActivity 使用 */
|
||||||
|
class InboundGoodsSearchDialog(
|
||||||
|
context: Context
|
||||||
|
) : BaseDialog(context) {
|
||||||
|
|
||||||
|
private lateinit var inflater: LayoutInflater
|
||||||
|
private var emptyBinding: LayoutEmptySearchBinding? = null
|
||||||
|
private lateinit var binding: DialogInboundGoodsSearchBinding
|
||||||
|
|
||||||
|
override fun getRootView(): View {
|
||||||
|
inflater = LayoutInflater.from(context)
|
||||||
|
binding = DialogInboundGoodsSearchBinding.inflate(inflater)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
private var storeActivity: StoreActivity? = null
|
||||||
|
|
||||||
|
override fun initView() {
|
||||||
|
storeActivity = getReceiptActivity() as? StoreActivity
|
||||||
|
binding.tvDialogTitle.text = if (storeActivity?.storeType == 1) "毛菜检索" else "净菜检索"
|
||||||
|
binding.ivClose.setOnClickListener { dismiss() }
|
||||||
|
binding.ivGoodsSearch.setOnClickListener { searchGoods() }
|
||||||
|
binding.etInputGoods.addOnActionSearchListener { searchGoods() }
|
||||||
|
binding.btnAddGoods.setOnClickListener {
|
||||||
|
InboundGoodsStoreDialog(context = context, goods = null) {
|
||||||
|
dismiss()
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
|
binding.btnConfirm.setOnClickListener {
|
||||||
|
if (list.isEmpty()) {
|
||||||
|
context.toast("请搜索物品")
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
if (clickIndex == -1) {
|
||||||
|
context.toast("请选择物品")
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
InboundGoodsStoreDialog(
|
||||||
|
context = context,
|
||||||
|
goods = list[clickIndex]
|
||||||
|
) {
|
||||||
|
storeActivity?.goodsRecognizeDialog?.dismiss()
|
||||||
|
dismiss()
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
|
binding.rvSearch.let {
|
||||||
|
it.layoutManager = GridLayoutManager(context, 2, LinearLayoutManager.VERTICAL, false)
|
||||||
|
it.adapter = searchAdapter
|
||||||
|
}
|
||||||
|
binding.refreshLayout.let {
|
||||||
|
it.setEnableRefresh(false)
|
||||||
|
it.setEnableLoadMore(false)
|
||||||
|
}
|
||||||
|
loadEmptyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun searchGoods() {
|
||||||
|
val traceCode = binding.etInputGoods.text.toString().trim()
|
||||||
|
if (traceCode.isBlank()) {
|
||||||
|
context.toast("请输入溯源码")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
storeActivity?.storeViewModel?.getIngredientByTrace(traceCode) { items ->
|
||||||
|
binding.etInputGoods.hideKeyboard()
|
||||||
|
val models = items.map { GoodsSearchModel.from(it) }
|
||||||
|
if (models.isEmpty()) {
|
||||||
|
loadEmptyView()
|
||||||
|
searchAdapter.submitList(emptyList())
|
||||||
|
return@getIngredientByTrace
|
||||||
|
}
|
||||||
|
list.clear()
|
||||||
|
list.addAll(models)
|
||||||
|
binding.rvSearch.layoutManager =
|
||||||
|
GridLayoutManager(context, 2, GridLayoutManager.VERTICAL, false)
|
||||||
|
searchAdapter.submitList(list.toList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadEmptyView() {
|
||||||
|
binding.rvSearch.layoutManager =
|
||||||
|
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||||
|
if (emptyBinding == null) {
|
||||||
|
emptyBinding = LayoutEmptySearchBinding.inflate(inflater, binding.rvSearch, false)
|
||||||
|
}
|
||||||
|
emptyBinding?.run {
|
||||||
|
tvContent.text = "请检查溯源码或稍后重试"
|
||||||
|
root.setOnClickListener { root.hideKeyboard() }
|
||||||
|
searchAdapter.stateView = root
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var clickIndex = -1
|
||||||
|
private val list: MutableList<GoodsSearchModel> = mutableListOf()
|
||||||
|
|
||||||
|
private val searchAdapter by lazy {
|
||||||
|
InboundSearchAdapter().apply {
|
||||||
|
isStateViewEnable = true
|
||||||
|
setOnItemClickListener { _, _, position ->
|
||||||
|
clickIndex = position
|
||||||
|
if (list[position].isSelected) return@setOnItemClickListener
|
||||||
|
list.forEach { it.isSelected = false }
|
||||||
|
list[position].isSelected = true
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,393 @@
|
|||||||
|
package com.sw.inbound.dialog
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Typeface
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
|
import android.text.style.ForegroundColorSpan
|
||||||
|
import android.text.style.StyleSpan
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import androidx.camera.view.PreviewView
|
||||||
|
import androidx.core.graphics.toColorInt
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import com.sw.inbound.GlobalData
|
||||||
|
import com.sw.inbound.activity.StoreActivity
|
||||||
|
import com.sw.inbound.adapter.FormFieldAdapter
|
||||||
|
import com.sw.inbound.databinding.DialogInboundGoodsStoreBinding
|
||||||
|
import com.sw.inbound.databinding.LayoutTakePhotoBinding
|
||||||
|
import com.sw.inbound.model.FieldType
|
||||||
|
import com.sw.inbound.model.FormField
|
||||||
|
import com.sw.inbound.model.response.DictType
|
||||||
|
import com.sw.inbound.model.response.GoodsSearchModel
|
||||||
|
import com.sw.inbound.sdk.SensorScaleUtils
|
||||||
|
import com.sw.inbound.utils.CameraUtils
|
||||||
|
import com.sw.inbound.utils.ext.appendText
|
||||||
|
import com.sw.inbound.utils.ext.buildSpannableString
|
||||||
|
import com.sw.inbound.utils.ext.toast
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
/** 入库输入弹窗,使用 RecyclerView加载 动态表单,根据 storeType 展示净菜/毛菜对应字段 */
|
||||||
|
class InboundGoodsStoreDialog(
|
||||||
|
context: Context,
|
||||||
|
private val goods: GoodsSearchModel?,
|
||||||
|
private val confirmBlock: () -> Unit = {}
|
||||||
|
) : BaseDialog(context) {
|
||||||
|
|
||||||
|
private lateinit var binding: DialogInboundGoodsStoreBinding
|
||||||
|
private lateinit var inflater: LayoutInflater
|
||||||
|
|
||||||
|
private var storeActivity: StoreActivity? = null
|
||||||
|
|
||||||
|
/** 0=净菜,1=毛菜 */
|
||||||
|
private var storeType: Int = 0
|
||||||
|
|
||||||
|
private var realWeight: Int = 0
|
||||||
|
|
||||||
|
private val isoFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
|
||||||
|
|
||||||
|
/** 当前渲染的表单字段列表 */
|
||||||
|
private lateinit var fields: MutableList<FormField>
|
||||||
|
|
||||||
|
private var previewView: PreviewView? = null
|
||||||
|
private var takePhotoBinding: LayoutTakePhotoBinding? = null
|
||||||
|
private val cameraUtils: CameraUtils by lazy { CameraUtils(storeActivity!!) }
|
||||||
|
|
||||||
|
override fun getRootView(): View {
|
||||||
|
inflater = LayoutInflater.from(context)
|
||||||
|
binding = DialogInboundGoodsStoreBinding.inflate(inflater)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun initView() {
|
||||||
|
storeActivity = getReceiptActivity() as? StoreActivity
|
||||||
|
storeType = storeActivity?.storeType ?: 0
|
||||||
|
|
||||||
|
buildFormFields()
|
||||||
|
setupRecyclerView()
|
||||||
|
setupButtons()
|
||||||
|
addChildToContainer()
|
||||||
|
addWeightListener()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildFormFields() {
|
||||||
|
fields = if (storeType == 1) buildRawFields() else buildCleanFields()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 毛菜字段列表
|
||||||
|
* 接口字段:traceCode, supplierType(额外注入), materialName, supplierId, spec, quantity,
|
||||||
|
* weight(额外注入), inboundTime, zoneId, operator
|
||||||
|
* 注:来源类型无对应接口字段,apiKey="" 不提交
|
||||||
|
*/
|
||||||
|
private fun buildRawFields(): MutableList<FormField> {
|
||||||
|
val supplierOptions = GlobalData.inboundSupplierList.mapNotNull { opt ->
|
||||||
|
val id = opt.id?.toString() ?: return@mapNotNull null
|
||||||
|
DictType(id = id, value = opt.name, type = opt.type?.toString() ?: "")
|
||||||
|
}
|
||||||
|
val zoneOptions = GlobalData.inboundZoneList.mapNotNull { zone ->
|
||||||
|
val id = zone.id?.toString() ?: return@mapNotNull null
|
||||||
|
DictType(id = id, value = zone.zoneName)
|
||||||
|
}
|
||||||
|
return mutableListOf(
|
||||||
|
FormField(
|
||||||
|
"溯源码",
|
||||||
|
FieldType.TEXT,
|
||||||
|
apiKey = "traceCode",
|
||||||
|
value = goods?.traceCode ?: "",
|
||||||
|
required = true,
|
||||||
|
hint = "请输入溯源码"
|
||||||
|
),
|
||||||
|
FormField(
|
||||||
|
"食材名称",
|
||||||
|
FieldType.TEXT,
|
||||||
|
apiKey = "materialName",
|
||||||
|
required = true,
|
||||||
|
value = goods?.materName ?: "",
|
||||||
|
hint = "请输入食材名称"
|
||||||
|
),
|
||||||
|
FormField(
|
||||||
|
"供应商", FieldType.DROPDOWN,
|
||||||
|
extraApiKeys = mapOf("supplierId" to "id", "supplierType" to "type"),
|
||||||
|
required = true,
|
||||||
|
hint = "请选择供应商", options = supplierOptions
|
||||||
|
),
|
||||||
|
FormField(
|
||||||
|
"入库规格",
|
||||||
|
FieldType.TEXT,
|
||||||
|
apiKey = "spec",
|
||||||
|
required = true,
|
||||||
|
hint = "请输入入库规格"
|
||||||
|
),
|
||||||
|
FormField(
|
||||||
|
"入库数量",
|
||||||
|
FieldType.NUMBER,
|
||||||
|
apiKey = "quantity",
|
||||||
|
required = true,
|
||||||
|
hint = "请输入入库数量"
|
||||||
|
),
|
||||||
|
FormField(
|
||||||
|
"入库时间",
|
||||||
|
FieldType.DATE_PICKER,
|
||||||
|
apiKey = "inboundTime",
|
||||||
|
required = true,
|
||||||
|
hint = "请选择入库时间"
|
||||||
|
),
|
||||||
|
FormField(
|
||||||
|
"存放区域", FieldType.DROPDOWN,
|
||||||
|
extraApiKeys = mapOf("zoneId" to "id", "storageArea" to "value"),
|
||||||
|
required = true,
|
||||||
|
hint = "请选择存放区域",
|
||||||
|
options = zoneOptions
|
||||||
|
),
|
||||||
|
FormField(
|
||||||
|
"操作人",
|
||||||
|
FieldType.TEXT,
|
||||||
|
apiKey = "operator",
|
||||||
|
value = GlobalData.user?.name ?: "",
|
||||||
|
required = true,
|
||||||
|
hint = "请输入操作人"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 净菜字段列表
|
||||||
|
* 接口字段:traceCode, ingredientName, cleanType, spec, quantity,
|
||||||
|
* weight(额外注入), inboundTime(额外注入当前时间), zoneId, operator,
|
||||||
|
* expiryDate, storageTemp
|
||||||
|
*/
|
||||||
|
private fun buildCleanFields(): MutableList<FormField> {
|
||||||
|
val zoneOptions = GlobalData.inboundZoneList.mapNotNull { zone ->
|
||||||
|
val id = zone.id?.toString() ?: return@mapNotNull null
|
||||||
|
DictType(id = id, value = zone.zoneName)
|
||||||
|
}
|
||||||
|
val specOptions = GlobalData.specOptionList.mapNotNull { spec ->
|
||||||
|
if (spec.dictCode.isNullOrEmpty() || spec.dictLabel.isNullOrEmpty()) {
|
||||||
|
return@mapNotNull null
|
||||||
|
}
|
||||||
|
DictType(id = spec.dictCode, value = spec.dictLabel)
|
||||||
|
}
|
||||||
|
return mutableListOf(
|
||||||
|
FormField(
|
||||||
|
"溯源码",
|
||||||
|
FieldType.TEXT,
|
||||||
|
apiKey = "traceCode",
|
||||||
|
value = goods?.traceCode ?: "",
|
||||||
|
required = true,
|
||||||
|
hint = "请输入溯源码"
|
||||||
|
),
|
||||||
|
FormField(
|
||||||
|
"食材名称",
|
||||||
|
type = if (goods?.materName.isNullOrEmpty()) FieldType.TEXT else FieldType.FIXED,
|
||||||
|
apiKey = "ingredientName",
|
||||||
|
required = true,
|
||||||
|
value = goods?.materName ?: ""
|
||||||
|
),
|
||||||
|
FormField(
|
||||||
|
"净菜类型",
|
||||||
|
type = if (goods?.materName.isNullOrEmpty()) FieldType.TEXT else FieldType.FIXED,
|
||||||
|
apiKey = "cleanType",
|
||||||
|
value = goods?.vegTypes ?: ""
|
||||||
|
),
|
||||||
|
FormField(
|
||||||
|
"入库规格",
|
||||||
|
FieldType.DROPDOWN,
|
||||||
|
extraApiKeys = mapOf("spec" to "id", "spec_dictText" to "value"),
|
||||||
|
required = true,
|
||||||
|
hint = "请选择入库规格",
|
||||||
|
options = specOptions
|
||||||
|
),
|
||||||
|
FormField(
|
||||||
|
"入库数量",
|
||||||
|
FieldType.NUMBER,
|
||||||
|
apiKey = "quantity",
|
||||||
|
required = true,
|
||||||
|
hint = "请输入入库数量"
|
||||||
|
),
|
||||||
|
FormField(
|
||||||
|
"操作时间",
|
||||||
|
FieldType.DATE_PICKER,
|
||||||
|
apiKey = "inboundTime",
|
||||||
|
hint = "请选择操作时间"
|
||||||
|
),
|
||||||
|
FormField(
|
||||||
|
"保质期至",
|
||||||
|
FieldType.DATE_PICKER,
|
||||||
|
apiKey = "expiryDate",
|
||||||
|
hint = "请选择保质期"
|
||||||
|
),
|
||||||
|
FormField(
|
||||||
|
"存放区域", FieldType.DROPDOWN,
|
||||||
|
extraApiKeys = mapOf("zoneId" to "id", "storageArea" to "value"),
|
||||||
|
required = true,
|
||||||
|
hint = "请选择存放区域",
|
||||||
|
options = zoneOptions
|
||||||
|
),
|
||||||
|
FormField(
|
||||||
|
"存放温度(℃)",
|
||||||
|
FieldType.NUMBER,
|
||||||
|
apiKey = "storageTemp"
|
||||||
|
),
|
||||||
|
FormField(
|
||||||
|
"操作人",
|
||||||
|
FieldType.TEXT,
|
||||||
|
apiKey = "operator",
|
||||||
|
value = GlobalData.user?.name ?: "",
|
||||||
|
required = true,
|
||||||
|
hint = "请输入操作人"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupRecyclerView() {
|
||||||
|
binding.rvFormFields.run {
|
||||||
|
layoutManager = GridLayoutManager(context, 2)
|
||||||
|
adapter = FormFieldAdapter(storeActivity ?: context, fields)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupButtons() {
|
||||||
|
binding.btnCancel.setOnClickListener {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
binding.btnConfirm.setOnClickListener { confirmInputData() }
|
||||||
|
binding.tvTitle.run {
|
||||||
|
text = if (storeType == 1) "毛菜入库" else "净菜入库"
|
||||||
|
setOnClickListener { SensorScaleUtils.startScale(autoScale = true) }
|
||||||
|
}
|
||||||
|
binding.btnClearZero.setOnClickListener { SensorScaleUtils.tare() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addWeightListener() {
|
||||||
|
storeActivity?.readWeightInfo { weight ->
|
||||||
|
if (abs(this.realWeight - weight) < 10) return@readWeightInfo
|
||||||
|
this.realWeight = weight
|
||||||
|
binding.tvTotalWeight.text = buildWeightSpannable(weight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildWeightSpannable(num: Int): SpannableStringBuilder {
|
||||||
|
return buildSpannableString {
|
||||||
|
appendText(
|
||||||
|
num.toString(),
|
||||||
|
ForegroundColorSpan("#FF141428".toColorInt()),
|
||||||
|
StyleSpan(Typeface.BOLD)
|
||||||
|
)
|
||||||
|
appendText(
|
||||||
|
" 克",
|
||||||
|
ForegroundColorSpan("#FF000033".toColorInt()),
|
||||||
|
StyleSpan(Typeface.BOLD)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 校验必填字段,通过后将 fields 转为 Map 提交 */
|
||||||
|
private fun confirmInputData() {
|
||||||
|
val invalid = fields.firstOrNull { it.required && it.value.isBlank() }
|
||||||
|
if (invalid != null) {
|
||||||
|
context.toast(invalid.hint.ifBlank { "${invalid.label}不能为空" })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (realWeight <= 0) {
|
||||||
|
context.toast("物品重量需大于0")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (storeType == 1) submitRawInbound() else submitCleanInbound()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 fields 转为接口 Map:
|
||||||
|
* - extraApiKeys 非空时从 extraValues 读取多字段值
|
||||||
|
* - apiKey 非空时按原逻辑提交单字段值
|
||||||
|
* - quantity → Int,storageTemp → Double,其余 → String
|
||||||
|
*/
|
||||||
|
private fun buildParamMap(): MutableMap<String, Any> {
|
||||||
|
val map = mutableMapOf<String, Any>()
|
||||||
|
for (field in fields) {
|
||||||
|
if (field.extraApiKeys.isNotEmpty()) {
|
||||||
|
field.extraValues.forEach { (apiKey, value) ->
|
||||||
|
map[apiKey] = value
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (field.apiKey.isBlank()) continue
|
||||||
|
val raw = field.value
|
||||||
|
if (raw.isBlank()) continue
|
||||||
|
val converted: Any = when (field.apiKey) {
|
||||||
|
"quantity" -> raw.toIntOrNull() ?: continue
|
||||||
|
"storageTemp" -> raw.toDoubleOrNull() ?: continue
|
||||||
|
else -> raw
|
||||||
|
}
|
||||||
|
map[field.apiKey] = converted
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun submitRawInbound() {
|
||||||
|
val param = buildParamMap()
|
||||||
|
// weight 来自秤,不在表单中,额外注入
|
||||||
|
param["weight"] = realWeight / 1000.0
|
||||||
|
param["materId"] = goods?.materId ?: ""
|
||||||
|
Loading.show(storeActivity ?: return)
|
||||||
|
storeActivity?.storeViewModel?.submitRawInbound(param) { success, msg ->
|
||||||
|
Loading.dismiss()
|
||||||
|
if (success) {
|
||||||
|
context.toast("入库成功")
|
||||||
|
confirmBlock()
|
||||||
|
dismiss()
|
||||||
|
} else {
|
||||||
|
context.toast(msg ?: "入库失败")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun submitCleanInbound() {
|
||||||
|
val param = buildParamMap()
|
||||||
|
// weight 来自秤,不在表单中,额外注入
|
||||||
|
param["weight"] = realWeight / 1000.0
|
||||||
|
// inboundTime 净菜取当前时间,不在表单中
|
||||||
|
val inboundTime = isoFormat.format(Date())
|
||||||
|
param["inboundTime"] = inboundTime
|
||||||
|
param["materId"] = goods?.materId ?: ""
|
||||||
|
|
||||||
|
val expiryDate = param["expiryDate"] as? String
|
||||||
|
param["expiryDate"] = expiryDate + " " + inboundTime.split(" ").getOrNull(1)
|
||||||
|
|
||||||
|
Loading.show(storeActivity ?: return)
|
||||||
|
storeActivity?.storeViewModel?.submitCleanInbound(param) { success, msg ->
|
||||||
|
Loading.dismiss()
|
||||||
|
if (success) {
|
||||||
|
context.toast("入库成功")
|
||||||
|
confirmBlock()
|
||||||
|
dismiss()
|
||||||
|
} else {
|
||||||
|
context.toast(msg ?: "入库失败")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addChildToContainer() {
|
||||||
|
takePhotoBinding = LayoutTakePhotoBinding.inflate(inflater, binding.flContainer, true)
|
||||||
|
storeActivity?.unBindCamera()
|
||||||
|
setOnDismissListener { cameraUtils.unbind() }
|
||||||
|
cameraUtils.initCamera()
|
||||||
|
takePhotoBinding?.let {
|
||||||
|
previewView = it.previewView
|
||||||
|
it.flCameraPreview.visibility = View.VISIBLE
|
||||||
|
it.cvImageBg.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun show() {
|
||||||
|
super.show()
|
||||||
|
takePhotoBinding?.let {
|
||||||
|
cameraUtils.setPreviewController(previewView)
|
||||||
|
cameraUtils.bind()
|
||||||
|
it.llCameraFlag.postDelayed({ it.llCameraFlag.visibility = View.GONE }, 1000)
|
||||||
|
}
|
||||||
|
binding.root.postDelayed({ SensorScaleUtils.startScale() }, 2600)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.sw.inbound.model
|
||||||
|
|
||||||
|
import com.sw.inbound.model.response.DictType
|
||||||
|
|
||||||
|
/** 表单字段类型枚举 */
|
||||||
|
enum class FieldType {
|
||||||
|
/** 普通文本输入 */
|
||||||
|
TEXT,
|
||||||
|
/** 数字输入(支持小数和负数) */
|
||||||
|
NUMBER,
|
||||||
|
/** 下拉选择 */
|
||||||
|
DROPDOWN,
|
||||||
|
/** 日期选择弹窗 */
|
||||||
|
DATE_PICKER,
|
||||||
|
/** 固定值,不可编辑 */
|
||||||
|
FIXED
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动态表单字段数据模型
|
||||||
|
* @param label 字段标签名称
|
||||||
|
* @param type 字段类型,决定渲染哪种输入控件
|
||||||
|
* @param apiKey 非下拉字段对应的接口参数名,空字符串表示不提交
|
||||||
|
* @param extraApiKeys 下拉字段的多接口参数映射,key=接口参数名,value=DictType 字段名(id/value/type)
|
||||||
|
* @param required 是否必填,提交时校验
|
||||||
|
* @param hidden 是否隐藏,hidden=true 时不渲染该字段
|
||||||
|
* @param hint 校验失败提示文案,同时作为输入框 hint
|
||||||
|
* @param options 下拉框选项列表(DROPDOWN 类型使用)
|
||||||
|
* @param value 当前显示值/输入值;DATE_PICKER 存 yyyy-MM-dd 显示格式
|
||||||
|
* @param extraValues 下拉选中后按 extraApiKeys 映射存储的提交值,key=接口参数名
|
||||||
|
*/
|
||||||
|
data class FormField(
|
||||||
|
val label: String,
|
||||||
|
val type: FieldType,
|
||||||
|
val apiKey: String = "",
|
||||||
|
val extraApiKeys: Map<String, String> = emptyMap(),
|
||||||
|
val required: Boolean = false,
|
||||||
|
val hidden: Boolean = false,
|
||||||
|
val hint: String = "",
|
||||||
|
val options: List<DictType> = emptyList(),
|
||||||
|
var value: String = "",
|
||||||
|
val extraValues: MutableMap<String, String> = mutableMapOf()
|
||||||
|
)
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.sw.inbound.model.request
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
/** 净菜入库提交参数 */
|
||||||
|
data class CleanInboundParam(
|
||||||
|
@SerializedName("traceCode") val traceCode: String,
|
||||||
|
@SerializedName("ingredientName") val ingredientName: String,
|
||||||
|
/** 净菜类型,如"切丁"、"切片",参考 IngredientByTrace.vegTypes */
|
||||||
|
@SerializedName("cleanType") val cleanType: String,
|
||||||
|
/** 入库规格,如"袋装/500g"、"盒装/300g" */
|
||||||
|
@SerializedName("spec") val spec: String,
|
||||||
|
@SerializedName("quantity") val quantity: Int,
|
||||||
|
/** 入库重量(kg) */
|
||||||
|
@SerializedName("weight") val weight: Double,
|
||||||
|
/** 入库时间,格式:yyyy-MM-dd'T'HH:mm:ss */
|
||||||
|
@SerializedName("inboundTime") val inboundTime: String,
|
||||||
|
@SerializedName("zoneId") val zoneId: Long,
|
||||||
|
@SerializedName("operator") val operator: String? = null,
|
||||||
|
/** 保质期至,格式同 inboundTime */
|
||||||
|
@SerializedName("expiryDate") val expiryDate: String? = null,
|
||||||
|
@SerializedName("storageTemp") val storageTemp: Double? = null,
|
||||||
|
@SerializedName("cameraCode") val cameraCode: String? = null,
|
||||||
|
@SerializedName("videoUrl") val videoUrl: String? = null,
|
||||||
|
@SerializedName("remark") val remark: String? = null
|
||||||
|
)
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.sw.inbound.model.request
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
/** 毛菜入库提交参数 */
|
||||||
|
data class RawInboundParam(
|
||||||
|
@SerializedName("traceCode") val traceCode: String,
|
||||||
|
/** 供应商类型:1-内部,2-外部 */
|
||||||
|
@SerializedName("supplierType") val supplierType: Short,
|
||||||
|
@SerializedName("materialName") val materialName: String,
|
||||||
|
@SerializedName("supplierId") val supplierId: Long,
|
||||||
|
/** 入库规格,如"25kg/件"、"散装(kg)" */
|
||||||
|
@SerializedName("spec") val spec: String,
|
||||||
|
@SerializedName("quantity") val quantity: Int,
|
||||||
|
/** 入库重量(kg) */
|
||||||
|
@SerializedName("weight") val weight: Double,
|
||||||
|
/** 入库时间,格式:yyyy-MM-dd'T'HH:mm:ss */
|
||||||
|
@SerializedName("inboundTime") val inboundTime: String,
|
||||||
|
@SerializedName("zoneId") val zoneId: Long,
|
||||||
|
@SerializedName("operator") val operator: String,
|
||||||
|
@SerializedName("storageArea") val storageArea: String? = null,
|
||||||
|
@SerializedName("reportNo") val reportNo: String? = null,
|
||||||
|
@SerializedName("cameraCode") val cameraCode: String? = null,
|
||||||
|
@SerializedName("videoUrl") val videoUrl: String? = null,
|
||||||
|
@SerializedName("remark") val remark: String? = null
|
||||||
|
)
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.sw.inbound.model.request
|
package com.sw.inbound.model.request
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.sw.inbound.model.response.BaseBean
|
|
||||||
import com.sw.inbound.model.response.GoodsInfo
|
import com.sw.inbound.model.response.GoodsInfo
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
@@ -19,6 +18,10 @@ data class UploadInfo(
|
|||||||
* 供应商id
|
* 供应商id
|
||||||
*/
|
*/
|
||||||
var supplierId: String? = null,
|
var supplierId: String? = null,
|
||||||
|
/**
|
||||||
|
* 采购单号
|
||||||
|
*/
|
||||||
|
var purCode: String? = null,
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* 物品信息 【全部收货使用】
|
* 物品信息 【全部收货使用】
|
||||||
@@ -90,6 +93,6 @@ private data class UploadGoodsInfo(
|
|||||||
|
|
||||||
// 已调整
|
// 已调整
|
||||||
var isAdjusted: Boolean = false
|
var isAdjusted: Boolean = false
|
||||||
) : Parcelable, BaseBean() {
|
) : Parcelable {
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package com.sw.inbound.model.response
|
|
||||||
|
|
||||||
open class BaseBean() {
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.sw.inbound.model.response
|
||||||
|
|
||||||
|
data class CollectedFoodInfo(
|
||||||
|
var foodName: String,
|
||||||
|
var foodNum: Int,
|
||||||
|
var isSelected: Boolean = false
|
||||||
|
)
|
||||||
@@ -9,5 +9,16 @@ import kotlinx.parcelize.Parcelize
|
|||||||
@Parcelize
|
@Parcelize
|
||||||
data class DictType(
|
data class DictType(
|
||||||
val id: String? = null,
|
val id: String? = null,
|
||||||
val value: String?=null
|
val value: String? = null,
|
||||||
) : Parcelable
|
val type: String = ""
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按字段名取值,供 extraApiKeys 映射直接使用
|
||||||
|
*/
|
||||||
|
operator fun get(fieldName: String): String = when (fieldName) {
|
||||||
|
"id" -> id ?: ""
|
||||||
|
"value" -> value ?: ""
|
||||||
|
else -> type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ data class GoodsInfo(
|
|||||||
var buyToInventoryValue:String?="",
|
var buyToInventoryValue:String?="",
|
||||||
|
|
||||||
//--------------------------------------------------------
|
//--------------------------------------------------------
|
||||||
) : Parcelable, BaseBean() {
|
) : Parcelable {
|
||||||
|
|
||||||
val goodNameStr: String
|
val goodNameStr: String
|
||||||
get() {
|
get() {
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.sw.inbound.model.response
|
||||||
|
|
||||||
|
/** 溯源码查食材结果,用于 InboundGoodsSearchDialog 列表展示 */
|
||||||
|
data class GoodsSearchModel(
|
||||||
|
val materId: Long = 0L,
|
||||||
|
val materName: String = "",
|
||||||
|
/** 净菜类型,逗号分隔,如"切丁,切片" */
|
||||||
|
val vegTypes: String = "",
|
||||||
|
val traceCode: String = "",
|
||||||
|
var isSelected: Boolean = false
|
||||||
|
) {
|
||||||
|
/** 将 vegTypes 拆分为列表,供净菜类型下拉使用 */
|
||||||
|
fun vegTypeList(): List<String> =
|
||||||
|
if (vegTypes.isBlank()) emptyList()
|
||||||
|
else vegTypes.split(",").map { it.trim() }.filter { it.isNotBlank() }
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun from(item: IngredientByTrace): GoodsSearchModel = GoodsSearchModel(
|
||||||
|
materId = item.materId ?: 0L,
|
||||||
|
materName = item.materName ?: "",
|
||||||
|
vegTypes = item.vegTypes ?: "",
|
||||||
|
traceCode = item.traceCode ?: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.sw.inbound.model.response
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 入库秤设备端接口统一响应包装
|
||||||
|
* 与旧 ApiResponse 区别:code 为 Int,200 表示成功
|
||||||
|
*/
|
||||||
|
data class InboundApiResponse<T>(
|
||||||
|
@SerializedName("code") val code: String? = null,
|
||||||
|
@SerializedName("msg") val msg: String? = null,
|
||||||
|
@SerializedName("data") val data: T? = null
|
||||||
|
) {
|
||||||
|
fun isSuccess(): Boolean = code == "00000"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.sw.inbound.model.response
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
/** 按溯源码查食材的响应条目 */
|
||||||
|
data class IngredientByTrace(
|
||||||
|
@SerializedName("traceCode") val traceCode: String? = null,
|
||||||
|
@SerializedName("materId") val materId: Long? = null,
|
||||||
|
@SerializedName("materName") val materName: String? = null,
|
||||||
|
/** 净菜类型,逗号分隔,如"切丁,切片" */
|
||||||
|
@SerializedName("vegTypes") val vegTypes: String? = null
|
||||||
|
)
|
||||||
@@ -31,6 +31,6 @@ data class PurchaseInfo(
|
|||||||
var receiveGoodsInfoList: List<GoodsInfo> = emptyList<GoodsInfo>(),
|
var receiveGoodsInfoList: List<GoodsInfo> = emptyList<GoodsInfo>(),
|
||||||
// 自用
|
// 自用
|
||||||
var warehouseName: String = "选择仓库"
|
var warehouseName: String = "选择仓库"
|
||||||
) : BaseBean() {
|
) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.sw.inbound.model.response
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字典下拉,dictLabel 用于界面展示,dictCode 用于提交入参
|
||||||
|
*/
|
||||||
|
data class SpecOption(
|
||||||
|
var dictLabel:String?,
|
||||||
|
var dictCode:String?,
|
||||||
|
)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.sw.inbound.model.response
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
/** 供应商下拉选项 */
|
||||||
|
data class SupplierOption(
|
||||||
|
@SerializedName("id") val id: Long? = null,
|
||||||
|
@SerializedName("name") val name: String? = null,
|
||||||
|
/** 1-内部供应商,2-外部供应商 */
|
||||||
|
@SerializedName("type") val type: Int? = null
|
||||||
|
)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.sw.inbound.model.response
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
/** 仓库区域下拉选项 */
|
||||||
|
data class ZoneOption(
|
||||||
|
@SerializedName("id") val id: Long? = null,
|
||||||
|
@SerializedName("zoneNo") val zoneNo: String? = null,
|
||||||
|
@SerializedName("zoneName") val zoneName: String? = null,
|
||||||
|
@SerializedName("warehouseId") val warehouseId: Long? = null
|
||||||
|
)
|
||||||
@@ -39,31 +39,30 @@ interface ApiService {
|
|||||||
/**
|
/**
|
||||||
* device获取token
|
* device获取token
|
||||||
*/
|
*/
|
||||||
@GET("sys/getEquipmentToken")
|
// @GET("sys/getEquipmentToken")
|
||||||
suspend fun getDeviceToken(
|
// suspend fun getDeviceToken(
|
||||||
@Query("qrcodeId") qrcodeId: String,
|
// @Query("qrcodeId") qrcodeId: String,
|
||||||
@Query("appVersion") appVersion: String = GlobalData.appVersion
|
// @Query("appVersion") appVersion: String = GlobalData.appVersion
|
||||||
): ApiResponse<String>
|
// ): ApiResponse<String>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*获取配置信息
|
*获取配置信息
|
||||||
*/
|
*/
|
||||||
@GET("equipment/stEquipment/queryByEquipmentCode")
|
// @GET("equipment/stEquipment/queryByEquipmentCode")
|
||||||
suspend fun getDeviceInfo(
|
// suspend fun getDeviceInfo(
|
||||||
@Query("equipmentCode") equipmentCode: String,
|
// @Query("equipmentCode") equipmentCode: String,
|
||||||
@Query("appVersion") appVersion: String = GlobalData.appVersion,
|
// @Query("appVersion") appVersion: String = GlobalData.appVersion,
|
||||||
@Header("X-Access-Token") token: String
|
// @Header("X-Access-Token") token: String
|
||||||
): ApiResponse<EquipmentInfo>
|
// ): ApiResponse<EquipmentInfo>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 登录
|
* 登录
|
||||||
*/
|
*/
|
||||||
@POST//("sys/loginPad")
|
// @POST//("sys/loginPad")
|
||||||
suspend fun login(
|
// suspend fun login(
|
||||||
@Url url: String = "${GlobalData.appBaseUrl}/sys/loginPad",
|
// @Url url: String = "${GlobalData.appBaseUrl}/sys/loginPad",
|
||||||
@Body body: LoginParam,
|
// @Body body: LoginParam,
|
||||||
|
// ): ApiResponse<User>
|
||||||
): ApiResponse<User>
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 采购单入库-列表
|
* 采购单入库-列表
|
||||||
@@ -94,11 +93,11 @@ interface ApiService {
|
|||||||
/**
|
/**
|
||||||
* 部分收货
|
* 部分收货
|
||||||
*/
|
*/
|
||||||
@POST//("pad/partialReceiptGoods")
|
// @POST//("pad/partialReceiptGoods")
|
||||||
suspend fun partialReceipt(
|
// suspend fun partialReceipt(
|
||||||
@Url url: String = "${GlobalData.appBaseUrl}${apiPrefix}/pad/partialReceiptGoods",
|
// @Url url: String = "${GlobalData.appBaseUrl}${apiPrefix}/pad/partialReceiptGoods",
|
||||||
@Body uploadInfo: UploadInfo
|
// @Body uploadInfo: UploadInfo
|
||||||
): ApiResponse<Boolean>
|
// ): ApiResponse<Boolean>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 确认收货
|
* 确认收货
|
||||||
@@ -195,11 +194,11 @@ interface ApiService {
|
|||||||
/**
|
/**
|
||||||
* 添加物品采购单位
|
* 添加物品采购单位
|
||||||
*/
|
*/
|
||||||
@POST//("pad/goodsAddUnit")
|
// @POST//("pad/goodsAddUnit")
|
||||||
suspend fun goodsAddUnit(
|
// suspend fun goodsAddUnit(
|
||||||
@Url url: String = "${GlobalData.appBaseUrl}${apiPrefix}/pad/goodsAddUnit",
|
// @Url url: String = "${GlobalData.appBaseUrl}${apiPrefix}/pad/goodsAddUnit",
|
||||||
@Body purchaseUnitParam: GoodsPurchaseUnitParam,
|
// @Body purchaseUnitParam: GoodsPurchaseUnitParam,
|
||||||
): ApiResponse<Boolean>
|
// ): ApiResponse<Boolean>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 自采入库
|
* 自采入库
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package com.sw.inbound.network.api
|
||||||
|
|
||||||
|
import com.sw.inbound.GlobalData
|
||||||
|
import com.sw.inbound.model.response.SpecOption
|
||||||
|
import com.sw.inbound.model.response.IngredientByTrace
|
||||||
|
import com.sw.inbound.model.response.InboundApiResponse
|
||||||
|
import com.sw.inbound.model.response.SupplierOption
|
||||||
|
import com.sw.inbound.model.response.ZoneOption
|
||||||
|
import retrofit2.http.Body
|
||||||
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.POST
|
||||||
|
import retrofit2.http.Query
|
||||||
|
import retrofit2.http.Url
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 入库秤设备端接口
|
||||||
|
* Base URL: http://192.168.10.101:24801/
|
||||||
|
* 鉴权:authorization header 由 InboundInterceptor 统一注入
|
||||||
|
*/
|
||||||
|
interface InboundApiService {
|
||||||
|
|
||||||
|
/** 按溯源码查食材,用于毛菜/净菜入库时联动选择食材 */
|
||||||
|
@GET
|
||||||
|
suspend fun getIngredientByTrace(
|
||||||
|
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/inbound/ingredient-by-trace",
|
||||||
|
@Query("traceCode") traceCode: String
|
||||||
|
): InboundApiResponse<List<IngredientByTrace>>
|
||||||
|
|
||||||
|
/** 仓库区域下拉列表 */
|
||||||
|
@GET
|
||||||
|
suspend fun getZoneOptions(
|
||||||
|
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/inbound/zone-options",
|
||||||
|
@Query("warehouseId") warehouseId: Long? = null,
|
||||||
|
@Query("keyword") keyword: String? = null
|
||||||
|
): InboundApiResponse<List<ZoneOption>>
|
||||||
|
|
||||||
|
/** 供应商下拉列表,type: 1-内部,2-外部,不传返回全部 */
|
||||||
|
@GET
|
||||||
|
suspend fun getSupplierOptions(
|
||||||
|
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/inbound/supplier-options",
|
||||||
|
@Query("type") type: Int? = null,
|
||||||
|
@Query("keyword") keyword: String? = null
|
||||||
|
): InboundApiResponse<List<SupplierOption>>
|
||||||
|
|
||||||
|
/** 规格下拉列表,dictType=nut_spec_type */
|
||||||
|
@GET
|
||||||
|
suspend fun getSpecOptions(
|
||||||
|
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/dict-items",
|
||||||
|
@Query("dictType") dictType: String = "nut_spec_type"
|
||||||
|
): InboundApiResponse<List<SpecOption>?>
|
||||||
|
|
||||||
|
/** 毛菜入库提交,入参为 Map 以便动态组装字段 */
|
||||||
|
@POST
|
||||||
|
suspend fun submitRawInbound(
|
||||||
|
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/inbound/raw-inbound",
|
||||||
|
@Body param: Map<String, @JvmSuppressWildcards Any>
|
||||||
|
): InboundApiResponse<Unit>
|
||||||
|
|
||||||
|
/** 净菜入库提交,入参为 Map 以便动态组装字段 */
|
||||||
|
@POST
|
||||||
|
suspend fun submitCleanInbound(
|
||||||
|
@Url url: String = "${GlobalData.appBaseUrl}/nutrition/neglect/inbound/clean-inbound",
|
||||||
|
@Body param: Map<String, @JvmSuppressWildcards Any>
|
||||||
|
): InboundApiResponse<Unit>
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.sw.inbound.network.interceptor
|
||||||
|
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Response
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 入库秤设备端接口拦截器
|
||||||
|
* 统一注入 authorization 固定鉴权值及 Content-Type
|
||||||
|
*/
|
||||||
|
class InboundInterceptor : Interceptor {
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val request = chain.request().newBuilder()
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.header("authorization", "57ee87183f2a4fa59683ec9ef41c8f5d")
|
||||||
|
.build()
|
||||||
|
return chain.proceed(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,10 +36,6 @@ object FoodModule {
|
|||||||
|
|
||||||
private lateinit var module_mobile: Module
|
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_MEAN_RGB = floatArrayOf(0.0f, 0.0f, 0.0f)
|
||||||
private val NO_STD_RGB = floatArrayOf(1.0f, 1.0f, 1.0f)
|
private val NO_STD_RGB = floatArrayOf(1.0f, 1.0f, 1.0f)
|
||||||
|
|
||||||
@@ -47,6 +43,7 @@ object FoodModule {
|
|||||||
private const val MODEL_INPUT_WIDTH = 300
|
private const val MODEL_INPUT_WIDTH = 300
|
||||||
private const val MODEL_INPUT_HEIGHT = 300
|
private const val MODEL_INPUT_HEIGHT = 300
|
||||||
const val DEFAULT_FOOD_INDEX = -1
|
const val DEFAULT_FOOD_INDEX = -1
|
||||||
|
const val DEFAULT_QUERY_COUNT = 50
|
||||||
|
|
||||||
const val BAG_RATE = 0.05
|
const val BAG_RATE = 0.05
|
||||||
|
|
||||||
@@ -94,7 +91,7 @@ object FoodModule {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// fun queryFood(uri: Uri, queryCount: Int = 15): List<String>? {
|
// fun queryFood(uri: Uri, queryCount: Int = DEFAULT_QUERY_COUNT): List<String>? {
|
||||||
// return uri2FloatArray(uri)?.let {
|
// return uri2FloatArray(uri)?.let {
|
||||||
// queryFood(it, queryCount)
|
// queryFood(it, queryCount)
|
||||||
// }
|
// }
|
||||||
@@ -103,7 +100,7 @@ object FoodModule {
|
|||||||
// /**
|
// /**
|
||||||
// * 返回识别物品名称列表
|
// * 返回识别物品名称列表
|
||||||
// */
|
// */
|
||||||
// fun queryFood(bitmap: Bitmap, queryCount: Int = 15): List<String> {
|
// fun queryFood(bitmap: Bitmap, queryCount: Int = DEFAULT_QUERY_COUNT): List<String> {
|
||||||
// val floatArray = bitmap2FloatArray(bitmap)
|
// val floatArray = bitmap2FloatArray(bitmap)
|
||||||
// return queryFood(floatArray, queryCount)
|
// return queryFood(floatArray, queryCount)
|
||||||
// }
|
// }
|
||||||
@@ -111,14 +108,14 @@ object FoodModule {
|
|||||||
// /**
|
// /**
|
||||||
// * 返回识别物品IdNameScore对象列表
|
// * 返回识别物品IdNameScore对象列表
|
||||||
// */
|
// */
|
||||||
// fun queryFoodNameScore(bitmap: Bitmap, queryCount: Int = 15): List<IdNameScore> {
|
// fun queryFoodNameScore(bitmap: Bitmap, queryCount: Int = DEFAULT_QUERY_COUNT): List<IdNameScore> {
|
||||||
// val floatArray = bitmap2FloatArray(bitmap)
|
// val floatArray = bitmap2FloatArray(bitmap)
|
||||||
// return queryFoodNameScore(floatArray, queryCount)
|
// return queryFoodNameScore(floatArray, queryCount)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
suspend fun queryFoodNameScore(
|
suspend fun queryFoodNameScore(
|
||||||
floatArray: FloatArray?,
|
floatArray: FloatArray?,
|
||||||
queryCount: Int = 15
|
queryCount: Int = DEFAULT_QUERY_COUNT
|
||||||
): List<IdNameScore> {
|
): List<IdNameScore> {
|
||||||
if (floatArray == null) return emptyList()
|
if (floatArray == null) return emptyList()
|
||||||
// val query: Query<Food> = box.query(Food_.foodVector.nearestNeighbors(floatArray, queryCount)).build()
|
// val query: Query<Food> = box.query(Food_.foodVector.nearestNeighbors(floatArray, queryCount)).build()
|
||||||
@@ -134,16 +131,18 @@ object FoodModule {
|
|||||||
|
|
||||||
logInfo("queryFood向量:${floatArray.slice(0 until 50).toJsonString()}")
|
logInfo("queryFood向量:${floatArray.slice(0 until 50).toJsonString()}")
|
||||||
val idScoreList = ObjectBox.query(floatArray, queryCount)
|
val idScoreList = ObjectBox.query(floatArray, queryCount)
|
||||||
|
// 批量加载所有实体,一次DB操作替代循环单条查询
|
||||||
|
val foodMap = ObjectBox.getByIds(idScoreList.map { it.id }).associateBy { it.id }
|
||||||
val nameScoreList = mutableListOf<IdNameScore>()
|
val nameScoreList = mutableListOf<IdNameScore>()
|
||||||
idScoreList.forEach {
|
idScoreList.forEach {
|
||||||
val name = ObjectBox.get(it.id)?.name ?: ""
|
val name = foodMap[it.id]?.name ?: ""
|
||||||
nameScoreList.add(IdNameScore(id = it.id, name = name, score = it.score))
|
nameScoreList.add(IdNameScore(id = it.id, name = name, score = it.score))
|
||||||
}
|
}
|
||||||
logInfo("queryFood数据:${nameScoreList.toJsonString()}")
|
logInfo("queryFood数据:${nameScoreList.toJsonString()}")
|
||||||
return nameScoreList
|
return nameScoreList
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getFoodScoreList(bitmap: Bitmap, queryCount: Int = 15): List<IdNameScore> {
|
suspend fun getFoodScoreList(bitmap: Bitmap, queryCount: Int = DEFAULT_QUERY_COUNT): List<IdNameScore> {
|
||||||
val floatArray = bitmap2FloatArray(bitmap, false) ?: return emptyList()
|
val floatArray = bitmap2FloatArray(bitmap, false) ?: return emptyList()
|
||||||
logInfo("getFoodScoreList向量:${floatArray.toJsonString()}")
|
logInfo("getFoodScoreList向量:${floatArray.toJsonString()}")
|
||||||
val nameScoreList = queryFoodNameScore(floatArray, queryCount)
|
val nameScoreList = queryFoodNameScore(floatArray, queryCount)
|
||||||
@@ -156,34 +155,35 @@ object FoodModule {
|
|||||||
.groupBy { it.name }
|
.groupBy { it.name }
|
||||||
.map { (_, value) -> value.minByOrNull { it.score }!! }
|
.map { (_, value) -> value.minByOrNull { it.score }!! }
|
||||||
.toMutableList()
|
.toMutableList()
|
||||||
val map = mutableMapOf<String, Int>()
|
// val map = mutableMapOf<String, Int>()
|
||||||
nameScoreList.forEach {
|
// nameScoreList.forEach {
|
||||||
val key = it.name
|
// val key = it.name
|
||||||
val count = map[key] ?: 0
|
// val count = map[key] ?: 0
|
||||||
map[key] = count + 1
|
// map[key] = count + 1
|
||||||
}
|
// }
|
||||||
val orderList = map.entries.sortedByDescending { it.value }.map { it.key }.toMutableList()
|
// val orderList = map.entries.sortedByDescending { it.value }.map { it.key }.toMutableList()
|
||||||
val firstFood = nameScoreList[0].name
|
// val firstFood = nameScoreList[0].name
|
||||||
orderList.remove(firstFood)
|
// orderList.remove(firstFood)
|
||||||
orderList.add(0, firstFood)
|
// orderList.add(0, firstFood)
|
||||||
|
//
|
||||||
val sortedScoreList = maxScoreList.sortedWith(compareBy {
|
// val sortedScoreList = maxScoreList.sortedWith(compareBy {
|
||||||
orderList.indexOf(it.name)
|
// orderList.indexOf(it.name)
|
||||||
})
|
// })
|
||||||
|
val sortedScoreList = maxScoreList.sortedBy { it.score }
|
||||||
logInfo("getFoodScoreList数据:${sortedScoreList.toJsonString()}")
|
logInfo("getFoodScoreList数据:${sortedScoreList.toJsonString()}")
|
||||||
return sortedScoreList
|
return sortedScoreList
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun queryFood(floatArray: FloatArray, queryCount: Int = 15): List<String> {
|
// suspend fun queryFood(floatArray: FloatArray, queryCount: Int = DEFAULT_QUERY_COUNT): List<String> {
|
||||||
val map = mutableMapOf<String, Int>()
|
// val map = mutableMapOf<String, Int>()
|
||||||
val nameScoreList = queryFoodNameScore(floatArray, queryCount)
|
// val nameScoreList = queryFoodNameScore(floatArray, queryCount)
|
||||||
nameScoreList.filter { it.score < 0.05 }.forEach {
|
// nameScoreList.filter { it.score < 0.05 }.forEach {
|
||||||
val count = map[it.name] ?: 0
|
// val count = map[it.name] ?: 0
|
||||||
map[it.name] = count + 1
|
// map[it.name] = count + 1
|
||||||
}
|
// }
|
||||||
val list = map.entries.sortedByDescending { it.value }.map { it.key }
|
// val list = map.entries.sortedByDescending { it.value }.map { it.key }
|
||||||
return list
|
// return list
|
||||||
}
|
// }
|
||||||
|
|
||||||
data class IdNameScore(
|
data class IdNameScore(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
@@ -193,8 +193,7 @@ object FoodModule {
|
|||||||
|
|
||||||
suspend fun initDefFoodData(context: Context, action: () -> Unit = {}) {
|
suspend fun initDefFoodData(context: Context, action: () -> Unit = {}) {
|
||||||
try {
|
try {
|
||||||
val count =
|
val count = ObjectBox.countIndexField(DEFAULT_FOOD_INDEX)
|
||||||
ObjectBox.getBox<Food>()?.all?.count { it.foodIdx == DEFAULT_FOOD_INDEX } ?: 0
|
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -202,10 +201,10 @@ object FoodModule {
|
|||||||
val labelsJson = AssetsTool.readAssetsFile(context, "data/labels.json")
|
val labelsJson = AssetsTool.readAssetsFile(context, "data/labels.json")
|
||||||
val classInfoJson = AssetsTool.readAssetsFile(context, "data/class_info.json")
|
val classInfoJson = AssetsTool.readAssetsFile(context, "data/class_info.json")
|
||||||
|
|
||||||
embeddingsList =
|
val embeddingsList: List<List<Float>> =
|
||||||
Gson().fromJson(embeddingsJson, object : TypeToken<List<List<Float>>>() {}.type)
|
Gson().fromJson(embeddingsJson, object : TypeToken<List<List<Float>>>() {}.type)
|
||||||
labelsList = Gson().fromJson(labelsJson, IntArray::class.java)
|
val labelsList: IntArray = Gson().fromJson(labelsJson, IntArray::class.java)
|
||||||
classInfo =
|
val classInfo: FoodClassInfo =
|
||||||
Gson().fromJson(classInfoJson, FoodClassInfo::class.java)
|
Gson().fromJson(classInfoJson, FoodClassInfo::class.java)
|
||||||
|
|
||||||
val foodMap = classInfo.idx_to_class
|
val foodMap = classInfo.idx_to_class
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ package com.sw.inbound.objbox
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Environment
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.sw.inbound.MyApp
|
import com.sw.inbound.MyApp
|
||||||
import com.sw.inbound.utils.LogSaveUtil
|
import com.sw.inbound.utils.LogSaveUtil
|
||||||
@@ -32,7 +31,6 @@ import io.objectbox.query.IdWithScore
|
|||||||
import io.objectbox.query.Query
|
import io.objectbox.query.Query
|
||||||
import io.objectbox.sync.Sync
|
import io.objectbox.sync.Sync
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -41,13 +39,6 @@ import java.io.FileInputStream
|
|||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.zip.GZIPOutputStream
|
import java.util.zip.GZIPOutputStream
|
||||||
import kotlin.collections.emptyList
|
|
||||||
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.
|
* Singleton to keep BoxStore reference and provide current list of Notes Objects.
|
||||||
@@ -60,9 +51,10 @@ object ObjectBox {
|
|||||||
// 避免直接修改自动生成的MyObjectBox类
|
// 避免直接修改自动生成的MyObjectBox类
|
||||||
// 跨版本升级时使用boxStore.runInTx执行数据迁移
|
// 跨版本升级时使用boxStore.runInTx执行数据迁移
|
||||||
|
|
||||||
|
|
||||||
private const val TAG = "ObjectBox"
|
private const val TAG = "ObjectBox"
|
||||||
|
|
||||||
|
// 添加 @Volatile 保证多线程下的可见性
|
||||||
|
@Volatile
|
||||||
var boxStore: BoxStore? = null
|
var boxStore: BoxStore? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -111,6 +103,7 @@ object ObjectBox {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* If the database file is not in use, compresses (GZIP) and copies it to the given [target].
|
* If the database file is not in use, compresses (GZIP) and copies it to the given [target].
|
||||||
|
* 增加 IOException 捕获,避免文件异常导致崩溃。
|
||||||
*/
|
*/
|
||||||
fun copyAndGzipDatabaseFileTo(target: File, context: Context): Boolean {
|
fun copyAndGzipDatabaseFileTo(target: File, context: Context): Boolean {
|
||||||
if (BoxStore.isDatabaseOpen(context, null)) {
|
if (BoxStore.isDatabaseOpen(context, null)) {
|
||||||
@@ -123,13 +116,18 @@ object ObjectBox {
|
|||||||
|
|
||||||
// If a name was given when building BoxStore use that instead of the default below.
|
// If a name was given when building BoxStore use that instead of the default below.
|
||||||
val dbName = BoxStoreBuilder.DEFAULT_NAME
|
val dbName = BoxStoreBuilder.DEFAULT_NAME
|
||||||
|
return try {
|
||||||
File(context.filesDir, "objectbox/$dbName/data.mdb").inputStream().use { input ->
|
File(context.filesDir, "objectbox/$dbName/data.mdb").inputStream().use { input ->
|
||||||
target.parentFile?.mkdirs()
|
target.parentFile?.mkdirs()
|
||||||
GZIPOutputStream(target.outputStream()).use { output ->
|
GZIPOutputStream(target.outputStream()).use { output ->
|
||||||
input.copyTo(output, DEFAULT_BUFFER_SIZE)
|
input.copyTo(output, DEFAULT_BUFFER_SIZE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
true
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.e(TAG, "备份数据库文件失败", e)
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -145,14 +143,17 @@ object ObjectBox {
|
|||||||
// 操作中触发损坏,尝试重建数据库
|
// 操作中触发损坏,尝试重建数据库
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
LogSaveUtil.saveLogFile("safeDbOp出现FileCorruptException异常:${e.message},${e.errorCode}")
|
LogSaveUtil.saveLogFile("safeDbOp出现FileCorruptException异常:${e.message},${e.errorCode}")
|
||||||
//val context = boxStore.context
|
// 使用安全调用替代 !! 断言,避免 NPE
|
||||||
//deleteDbFiles(context)
|
val app = MyApp.instance ?: return@withContext null
|
||||||
init(MyApp.instance!!)
|
init(app)
|
||||||
null
|
null
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
LogSaveUtil.saveLogFile("safeDbOp出现其它异常:${e.message}")
|
LogSaveUtil.saveLogFile("safeDbOp出现其它异常:${e.message}")
|
||||||
null
|
null
|
||||||
|
} finally {
|
||||||
|
// 线程资源清理
|
||||||
|
boxStore?.closeThreadResources()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -174,6 +175,12 @@ object ObjectBox {
|
|||||||
getBox<Food>()?.get(id)
|
getBox<Food>()?.get(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 批量按ID加载,一次DB事务替代循环单条查询
|
||||||
|
suspend fun getByIds(ids: List<Long>): List<Food> = safeDbOp {
|
||||||
|
if (ids.isEmpty()) return@safeDbOp emptyList<Food>()
|
||||||
|
getBox<Food>()?.get(ids.toLongArray()) ?: emptyList<Food>()
|
||||||
|
} ?: emptyList()
|
||||||
|
|
||||||
suspend fun put(entity: Food) = safeDbOp {
|
suspend fun put(entity: Food) = safeDbOp {
|
||||||
getBox<Food>()?.put(entity)
|
getBox<Food>()?.put(entity)
|
||||||
}
|
}
|
||||||
@@ -183,36 +190,68 @@ object ObjectBox {
|
|||||||
box?.put(entities)
|
box?.put(entities)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun count(name: String) = safeDbOp {
|
// 使用 QueryBuilder 在数据库层计数,避免全量加载到内存
|
||||||
getBox<Food>()?.all?.count() { it.name == name }
|
suspend fun countNameField(name: String) = safeDbOp {
|
||||||
} ?: 0
|
getBox<Food>()?.query(Food_.name.equal(name))?.build()?.count()
|
||||||
|
} ?: 0L
|
||||||
|
|
||||||
|
suspend fun countIndexField(idx: Int) = safeDbOp {
|
||||||
|
getBox<Food>()?.query(Food_.foodIdx.equal(idx))?.build()?.count()
|
||||||
|
} ?: 0L
|
||||||
|
|
||||||
|
// 使用 QueryBuilder 在数据库层过滤,避免全量加载到内存
|
||||||
suspend fun filter(name: String?) = safeDbOp {
|
suspend fun filter(name: String?) = safeDbOp {
|
||||||
val list = getBox<Food>()?.all?.distinctBy { it.name }
|
if (!name.isNullOrBlank()) {
|
||||||
if (name.isNullOrBlank().not()) {
|
getBox<Food>()?.query(Food_.name.contains(name))?.build()?.find()
|
||||||
list?.filter { it.name?.contains(name) == true }
|
?.distinctBy { it.name }
|
||||||
} else {
|
} else {
|
||||||
list
|
getBox<Food>()?.all?.distinctBy { it.name }
|
||||||
} ?: emptyList()
|
} ?: emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun remove(name: String) = safeDbOp {
|
/**
|
||||||
getBox<Food>()?.run {
|
* 按名称分组统计采集数量
|
||||||
val filterIdList = all.filter { it.name == name }.map { it.id }
|
* 使用属性查询只加载 name 字段,不加载 foodVector,节省大量内存
|
||||||
removeByIds(filterIdList)
|
* @param nameFilter 名称过滤关键词,为空时查询全部
|
||||||
|
* @return 按名称分组后的 (名称, 数量) 列表,按名称排序
|
||||||
|
*/
|
||||||
|
suspend fun queryFoodNameCounts(nameFilter: String? = null) = safeDbOp {
|
||||||
|
val box = getBox<Food>() ?: return@safeDbOp emptyList<Pair<String, Int>>()
|
||||||
|
val query = if (!nameFilter.isNullOrBlank()) {
|
||||||
|
box.query(Food_.name.contains(nameFilter)).build()
|
||||||
|
} else {
|
||||||
|
box.query().build()
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
// 只查 name 字段,完全不加载 foodVector
|
||||||
|
query.property(Food_.name).findStrings()
|
||||||
|
.groupingBy { it }
|
||||||
|
.eachCount()
|
||||||
|
.map { (name, count) -> Pair(name, count) }
|
||||||
|
.sortedBy { it.first }
|
||||||
|
} finally {
|
||||||
|
query.close()
|
||||||
|
}
|
||||||
|
} ?: emptyList()
|
||||||
|
|
||||||
|
// 使用 QueryBuilder 在数据库层删除,避免全量加载到内存
|
||||||
|
suspend fun remove(name: String) = safeDbOp {
|
||||||
|
getBox<Food>()?.query(Food_.name.equal(name))?.build()?.remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun removeAll() = safeDbOp {
|
||||||
|
getBox<Food>()?.removeAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val dbMutex = Mutex() // 协程并发锁,保证写入操作原子性
|
private val dbMutex = Mutex() // 协程并发锁,保证写入操作原子性
|
||||||
private const val DB_DIR_NAME = "objectbox" // ObjectBox 默认数据库目录
|
private const val DB_DIR_NAME = "objectbox" // ObjectBox 默认数据库目录
|
||||||
private const val BACKUP_DIR_NAME = "objectbox_backup" // 备份目录
|
private const val BACKUP_DIR_NAME = "objectbox_backup" // 备份目录
|
||||||
|
|
||||||
// 检查存储是否可读写(操作数据库前调用)
|
// 检查内部存储是否可读写(操作数据库前调用)
|
||||||
|
// 修复:原逻辑错误地检查外部存储状态,数据库存于内部存储,与外部存储无关
|
||||||
fun isStorageAvailable(context: Context): Boolean {
|
fun isStorageAvailable(context: Context): Boolean {
|
||||||
return try {
|
return try {
|
||||||
val state = Environment.getExternalStorageState()
|
context.filesDir.canRead() && context.filesDir.canWrite()
|
||||||
val innerDir = context.filesDir
|
|
||||||
Environment.MEDIA_MOUNTED == state && innerDir.canRead() && innerDir.canWrite()
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@@ -290,5 +329,4 @@ object ObjectBox {
|
|||||||
boxStore = null
|
boxStore = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@ package com.sw.inbound.receiver
|
|||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.sw.inbound.activity.HomeActivity
|
import com.sw.inbound.activity.InitActivity
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -14,7 +14,7 @@ class BootReceiver : BroadcastReceiver() {
|
|||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
if (Intent.ACTION_BOOT_COMPLETED == intent.action) {
|
if (Intent.ACTION_BOOT_COMPLETED == intent.action) {
|
||||||
Timber.d("设备启动完成,开始执行自启动逻辑")
|
Timber.d("设备启动完成,开始执行自启动逻辑")
|
||||||
val intent = Intent(context, HomeActivity::class.java)
|
val intent = Intent(context, InitActivity::class.java)
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.sw.inbound.repository
|
|||||||
|
|
||||||
import com.google.gson.JsonParseException
|
import com.google.gson.JsonParseException
|
||||||
import com.sw.inbound.model.response.ApiResponse
|
import com.sw.inbound.model.response.ApiResponse
|
||||||
|
import com.sw.inbound.utils.LogSaveUtil
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@@ -15,7 +16,7 @@ abstract class BaseRepository {
|
|||||||
apiCall()
|
apiCall()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e("safeApiCall Exception: ${e.stackTraceToString()}")
|
Timber.e("safeApiCall Exception: ${e.stackTraceToString()}")
|
||||||
|
LogSaveUtil.saveLogFile("safeApiCall Exception: ${e.stackTraceToString()}")
|
||||||
when (e) {
|
when (e) {
|
||||||
is HttpException -> {
|
is HttpException -> {
|
||||||
// 对于HTTP异常,尝试从响应体中获取错误信息
|
// 对于HTTP异常,尝试从响应体中获取错误信息
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package com.sw.inbound.repository
|
||||||
|
|
||||||
|
import com.google.gson.JsonParseException
|
||||||
|
import com.sw.inbound.model.response.IngredientByTrace
|
||||||
|
import com.sw.inbound.model.response.InboundApiResponse
|
||||||
|
import com.sw.inbound.model.response.SpecOption
|
||||||
|
import com.sw.inbound.model.response.SupplierOption
|
||||||
|
import com.sw.inbound.model.response.ZoneOption
|
||||||
|
import com.sw.inbound.network.api.InboundApiService
|
||||||
|
import com.sw.inbound.utils.LogSaveUtil
|
||||||
|
import retrofit2.HttpException
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.ConnectException
|
||||||
|
import java.net.SocketTimeoutException
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 入库秤设备端接口数据层
|
||||||
|
* 对应接口文档 Base URL: http://192.168.10.101:24801/nutrition/neglect/inbound
|
||||||
|
*/
|
||||||
|
class InboundRepository @Inject constructor(
|
||||||
|
private val apiService: InboundApiService
|
||||||
|
) {
|
||||||
|
|
||||||
|
/** 按溯源码查食材 */
|
||||||
|
suspend fun getIngredientByTrace(traceCode: String): InboundApiResponse<List<IngredientByTrace>> {
|
||||||
|
return safeCall { apiService.getIngredientByTrace(traceCode = traceCode) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 仓库区域下拉列表 */
|
||||||
|
suspend fun getZoneOptions(
|
||||||
|
warehouseId: Long? = null,
|
||||||
|
keyword: String? = null
|
||||||
|
): InboundApiResponse<List<ZoneOption>> {
|
||||||
|
return safeCall { apiService.getZoneOptions(warehouseId = warehouseId, keyword = keyword) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 供应商下拉列表 */
|
||||||
|
suspend fun getSupplierOptions(
|
||||||
|
type: Int? = null,
|
||||||
|
keyword: String? = null
|
||||||
|
): InboundApiResponse<List<SupplierOption>> {
|
||||||
|
return safeCall { apiService.getSupplierOptions(type = type, keyword = keyword) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 规格下拉列表 */
|
||||||
|
suspend fun getSpecOptions(): InboundApiResponse<List<SpecOption>?> {
|
||||||
|
return safeCall { apiService.getSpecOptions() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 毛菜入库提交 */
|
||||||
|
suspend fun submitRawInbound(param: Map<String, Any>): InboundApiResponse<Unit> {
|
||||||
|
return safeCall { apiService.submitRawInbound(param = param) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 净菜入库提交 */
|
||||||
|
suspend fun submitCleanInbound(param: Map<String, Any>): InboundApiResponse<Unit> {
|
||||||
|
return safeCall { apiService.submitCleanInbound(param = param) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun <T> safeCall(call: suspend () -> InboundApiResponse<T>): InboundApiResponse<T> {
|
||||||
|
return try {
|
||||||
|
call()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e("InboundRepository safeCall Exception: ${e.stackTraceToString()}")
|
||||||
|
LogSaveUtil.saveLogFile("InboundRepository safeCall Exception: ${e.stackTraceToString()}")
|
||||||
|
val msg = when (e) {
|
||||||
|
is HttpException -> "HTTP错误: ${e.code()} - ${e.message()}"
|
||||||
|
is SocketTimeoutException -> "请求超时: ${e.message}"
|
||||||
|
is ConnectException -> "连接失败: ${e.message}"
|
||||||
|
is JsonParseException -> "JSON解析错误: ${e.message}"
|
||||||
|
is IOException -> "网络IO错误: ${e.message}"
|
||||||
|
else -> "未知错误: ${e.message}"
|
||||||
|
}
|
||||||
|
InboundApiResponse(code = "-1", msg = msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,29 +38,29 @@ class RemoteRepository @Inject constructor(
|
|||||||
/**
|
/**
|
||||||
* 生成token
|
* 生成token
|
||||||
*/
|
*/
|
||||||
suspend fun getDeviceToken(qrcodeId: String): ApiResponse<String> {
|
// suspend fun getDeviceToken(qrcodeId: String): ApiResponse<String> {
|
||||||
return safeApiCall {
|
// return safeApiCall {
|
||||||
apiService.getDeviceToken(
|
// apiService.getDeviceToken(
|
||||||
qrcodeId
|
// qrcodeId
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取设备信息
|
* 获取设备信息
|
||||||
*/
|
*/
|
||||||
suspend fun getDeviceInfo(equipmentCode: String, token: String): ApiResponse<EquipmentInfo> {
|
// suspend fun getDeviceInfo(equipmentCode: String, token: String): ApiResponse<EquipmentInfo> {
|
||||||
return safeApiCall {
|
// return safeApiCall {
|
||||||
apiService.getDeviceInfo(
|
// apiService.getDeviceInfo(
|
||||||
equipmentCode = equipmentCode,
|
// equipmentCode = equipmentCode,
|
||||||
token = token
|
// token = token
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
suspend fun login(loginParam: LoginParam): ApiResponse<User> {
|
// suspend fun login(loginParam: LoginParam): ApiResponse<User> {
|
||||||
return safeApiCall { apiService.login(body = loginParam) }
|
// return safeApiCall { apiService.login(body = loginParam) }
|
||||||
}
|
// }
|
||||||
|
|
||||||
suspend fun getReceiveList(pageNum: Int, pageSize: Int): ApiResponse<List<SupplierInfo>?> {
|
suspend fun getReceiveList(pageNum: Int, pageSize: Int): ApiResponse<List<SupplierInfo>?> {
|
||||||
return safeApiCall {
|
return safeApiCall {
|
||||||
@@ -153,11 +153,11 @@ class RemoteRepository @Inject constructor(
|
|||||||
/**
|
/**
|
||||||
* 部分收货
|
* 部分收货
|
||||||
*/
|
*/
|
||||||
suspend fun partialReceipt(uploadInfo: UploadInfo): ApiResponse<Boolean> {
|
// suspend fun partialReceipt(uploadInfo: UploadInfo): ApiResponse<Boolean> {
|
||||||
return safeApiCall {
|
// return safeApiCall {
|
||||||
apiService.partialReceipt(uploadInfo = uploadInfo)
|
// apiService.partialReceipt(uploadInfo = uploadInfo)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 全部收货
|
* 全部收货
|
||||||
@@ -198,11 +198,11 @@ class RemoteRepository @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun selfPurchaseGoodsAddUnit(addParam: GoodsPurchaseUnitParam): ApiResponse<Boolean> {
|
// suspend fun selfPurchaseGoodsAddUnit(addParam: GoodsPurchaseUnitParam): ApiResponse<Boolean> {
|
||||||
return safeApiCall {
|
// return safeApiCall {
|
||||||
apiService.goodsAddUnit(purchaseUnitParam = addParam)
|
// apiService.goodsAddUnit(purchaseUnitParam = addParam)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 自采购-入库
|
* 自采购-入库
|
||||||
|
|||||||
@@ -61,8 +61,16 @@ object SensorScaleUtils {
|
|||||||
|
|
||||||
private var callbackList: MutableList<WeightCallback> = mutableListOf()
|
private var callbackList: MutableList<WeightCallback> = mutableListOf()
|
||||||
fun addWeightListener(callback: WeightCallback) {
|
fun addWeightListener(callback: WeightCallback) {
|
||||||
|
if (callbackList.contains(callback).not()) {
|
||||||
callbackList.add(callback)
|
callbackList.add(callback)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeWeightListener(callback: WeightCallback) {
|
||||||
|
if (callbackList.contains(callback)) {
|
||||||
|
callbackList.remove(callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 开启称重
|
* 开启称重
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ class CameraUtils(private var activity: ComponentActivity) {
|
|||||||
photoCaptureHelper = PhotoCaptureHelper(
|
photoCaptureHelper = PhotoCaptureHelper(
|
||||||
context = activity,
|
context = activity,
|
||||||
cameraController = cameraController!!,
|
cameraController = cameraController!!,
|
||||||
onSuccess = {},
|
|
||||||
onError = { msg ->
|
onError = { msg ->
|
||||||
//toast(msg)
|
//toast(msg)
|
||||||
failCallback?.invoke()
|
failCallback?.invoke()
|
||||||
@@ -77,11 +76,19 @@ class CameraUtils(private var activity: ComponentActivity) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun bind() {
|
fun bind() {
|
||||||
|
try {
|
||||||
cameraController?.bindToLifecycle(activity)
|
cameraController?.bindToLifecycle(activity)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unbind() {
|
fun unbind() {
|
||||||
|
try {
|
||||||
cameraController?.unbind()
|
cameraController?.unbind()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
package com.sw.inbound.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模型资源管理器
|
||||||
|
* 负责从接口下载模型资源包(Zip),并解压到应用缓存目录
|
||||||
|
* 替代原有的 assets 中写死的 data 文件夹和 .pt 文件
|
||||||
|
*/
|
||||||
|
object ModelResourceManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 资源下载和解压的回调接口
|
||||||
|
*/
|
||||||
|
interface ResourceCallback {
|
||||||
|
/**
|
||||||
|
* 下载进度回调
|
||||||
|
* @param progress 进度百分比 (0-100)
|
||||||
|
* @param message 进度信息
|
||||||
|
*/
|
||||||
|
fun onDownloadProgress(progress: Int, message: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解压进度回调
|
||||||
|
* @param progress 进度百分比 (0-100)
|
||||||
|
* @param message 进度信息
|
||||||
|
*/
|
||||||
|
fun onExtractProgress(progress: Int, message: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 完成回调
|
||||||
|
* @param resourceDir 资源所在目录
|
||||||
|
*/
|
||||||
|
fun onSuccess(resourceDir: File)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 失败回调
|
||||||
|
* @param errorMessage 错误信息
|
||||||
|
*/
|
||||||
|
fun onFailure(errorMessage: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载并解压模型资源
|
||||||
|
* @param context 应用上下文
|
||||||
|
* @param downloadUrl 资源包下载链接
|
||||||
|
* @param callback 进度和结果回调
|
||||||
|
*/
|
||||||
|
fun downloadAndExtractResources(
|
||||||
|
context: Context,
|
||||||
|
downloadUrl: String,
|
||||||
|
callback: ResourceCallback
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
// 获取缓存目录
|
||||||
|
val cacheDir = context.cacheDir
|
||||||
|
val resourcesDir = File(cacheDir, "model_resources")
|
||||||
|
val zipFile = File(cacheDir, "model_resources.zip")
|
||||||
|
|
||||||
|
Timber.d("资源缓存目录: ${resourcesDir.absolutePath}")
|
||||||
|
Timber.d("Zip 文件路径: ${zipFile.absolutePath}")
|
||||||
|
|
||||||
|
// Step 1: 下载 Zip 文件
|
||||||
|
Timber.d("开始下载资源包: $downloadUrl")
|
||||||
|
ZipDownloadUtils.downloadZip(
|
||||||
|
downloadUrl = downloadUrl,
|
||||||
|
targetFile = zipFile,
|
||||||
|
listener = object : ZipDownloadUtils.DownloadProgressListener {
|
||||||
|
override fun onProgress(currentSize: Long, totalSize: Long, progress: Int) {
|
||||||
|
val message = "下载中... ${formatFileSize(currentSize)}/${formatFileSize(totalSize)}"
|
||||||
|
callback.onDownloadProgress(progress, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSuccess(file: File) {
|
||||||
|
Timber.d("资源包下载完成")
|
||||||
|
callback.onDownloadProgress(100, "下载完成")
|
||||||
|
|
||||||
|
// Step 2: 解压 Zip 文件
|
||||||
|
extractResources(resourcesDir, zipFile, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(exception: Exception) {
|
||||||
|
val errorMsg = "资源包下载失败: ${exception.message}"
|
||||||
|
Timber.e(exception, errorMsg)
|
||||||
|
callback.onFailure(errorMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
val errorMsg = "资源下载和解压过程异常: ${e.message}"
|
||||||
|
Timber.e(e, errorMsg)
|
||||||
|
callback.onFailure(errorMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解压资源包
|
||||||
|
*/
|
||||||
|
private fun extractResources(
|
||||||
|
resourcesDir: File,
|
||||||
|
zipFile: File,
|
||||||
|
callback: ResourceCallback
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
Timber.d("开始解压资源包")
|
||||||
|
ZipExtractUtils.extractZip(
|
||||||
|
zipFile = zipFile,
|
||||||
|
targetDir = resourcesDir,
|
||||||
|
listener = object : ZipExtractUtils.ExtractProgressListener {
|
||||||
|
override fun onProgress(
|
||||||
|
currentFile: String,
|
||||||
|
currentIndex: Int,
|
||||||
|
totalFiles: Int,
|
||||||
|
progress: Int
|
||||||
|
) {
|
||||||
|
val message = "解压中... ($currentIndex/$totalFiles) $currentFile"
|
||||||
|
callback.onExtractProgress(progress, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSuccess(targetDir: File) {
|
||||||
|
Timber.d("资源包解压完成: ${targetDir.absolutePath}")
|
||||||
|
callback.onExtractProgress(100, "解压完成")
|
||||||
|
|
||||||
|
// 清理 Zip 文件
|
||||||
|
zipFile.delete()
|
||||||
|
Timber.d("临时 Zip 文件已删除")
|
||||||
|
|
||||||
|
callback.onSuccess(targetDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(exception: Exception) {
|
||||||
|
val errorMsg = "资源包解压失败: ${exception.message}"
|
||||||
|
Timber.e(exception, errorMsg)
|
||||||
|
callback.onFailure(errorMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
val errorMsg = "解压过程异常: ${e.message}"
|
||||||
|
Timber.e(e, errorMsg)
|
||||||
|
callback.onFailure(errorMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取已缓存的资源目录
|
||||||
|
* @param context 应用上下文
|
||||||
|
* @return 资源目录,如果不存在则返回 null
|
||||||
|
*/
|
||||||
|
fun getCachedResourcesDir(context: Context): File? {
|
||||||
|
val resourcesDir = File(context.cacheDir, "model_resources")
|
||||||
|
return if (resourcesDir.exists() && resourcesDir.isDirectory) {
|
||||||
|
resourcesDir
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理缓存的资源
|
||||||
|
* @param context 应用上下文
|
||||||
|
* @return 是否清理成功
|
||||||
|
*/
|
||||||
|
fun clearCachedResources(context: Context): Boolean {
|
||||||
|
return try {
|
||||||
|
val resourcesDir = File(context.cacheDir, "model_resources")
|
||||||
|
val zipFile = File(context.cacheDir, "model_resources.zip")
|
||||||
|
|
||||||
|
val dirDeleted = resourcesDir.deleteRecursively()
|
||||||
|
val zipDeleted = zipFile.delete()
|
||||||
|
|
||||||
|
Timber.d("清理缓存资源: 目录=$dirDeleted, Zip=$zipDeleted")
|
||||||
|
dirDeleted || zipDeleted
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "清理缓存资源失败")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化文件大小为可读字符串
|
||||||
|
*/
|
||||||
|
private fun formatFileSize(bytes: Long): String {
|
||||||
|
return when {
|
||||||
|
bytes <= 0 -> "0 B"
|
||||||
|
bytes < 1024 -> "$bytes B"
|
||||||
|
bytes < 1024 * 1024 -> String.format("%.2f KB", bytes / 1024.0)
|
||||||
|
bytes < 1024 * 1024 * 1024 -> String.format("%.2f MB", bytes / (1024.0 * 1024))
|
||||||
|
else -> String.format("%.2f GB", bytes / (1024.0 * 1024 * 1024))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.sw.inbound.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 网络连接检测工具类
|
||||||
|
* 提供检测设备网络连接状态的功能
|
||||||
|
*/
|
||||||
|
object NetworkUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测设备是否连接到网络
|
||||||
|
* @param context 应用上下文
|
||||||
|
* @return true 表示已连接网络,false 表示未连接
|
||||||
|
*/
|
||||||
|
fun isNetworkConnected(context: Context): Boolean {
|
||||||
|
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
|
val network = connectivityManager.activeNetwork ?: return false
|
||||||
|
val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
|
||||||
|
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,13 +16,11 @@ import java.io.File
|
|||||||
* 拍照工具类
|
* 拍照工具类
|
||||||
* @param context Context 上下文
|
* @param context Context 上下文
|
||||||
* @param cameraController CameraController 相机控制器
|
* @param cameraController CameraController 相机控制器
|
||||||
* @param onSuccess (Uri) -> Unit 拍照成功回调
|
|
||||||
* @param onError (String) -> Unit 拍照失败回调
|
* @param onError (String) -> Unit 拍照失败回调
|
||||||
*/
|
*/
|
||||||
class PhotoCaptureHelper(
|
class PhotoCaptureHelper(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val cameraController: CameraController,
|
private val cameraController: CameraController,
|
||||||
private val onSuccess: (Uri) -> Unit = {},
|
|
||||||
private val onError: (String) -> Unit = {}
|
private val onError: (String) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
private val callbackList: MutableList<(Uri) -> Unit> = mutableListOf()
|
private val callbackList: MutableList<(Uri) -> Unit> = mutableListOf()
|
||||||
@@ -66,7 +64,6 @@ class PhotoCaptureHelper(
|
|||||||
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
|
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
|
||||||
val photoUri = outputFileResults.savedUri ?: Uri.fromFile(photoFile)
|
val photoUri = outputFileResults.savedUri ?: Uri.fromFile(photoFile)
|
||||||
Timber.d("照片保存成功: $photoUri")
|
Timber.d("照片保存成功: $photoUri")
|
||||||
onSuccess(photoUri)
|
|
||||||
callbackList.forEach {
|
callbackList.forEach {
|
||||||
it(photoUri)
|
it(photoUri)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package com.sw.inbound.utils
|
||||||
|
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zip 文件下载工具类
|
||||||
|
* 支持大文件下载、进度回调、断点续传等功能
|
||||||
|
*/
|
||||||
|
object ZipDownloadUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载进度回调接口
|
||||||
|
*/
|
||||||
|
interface DownloadProgressListener {
|
||||||
|
/**
|
||||||
|
* 下载进度回调
|
||||||
|
* @param currentSize 已下载字节数
|
||||||
|
* @param totalSize 总字节数
|
||||||
|
* @param progress 进度百分比 (0-100)
|
||||||
|
*/
|
||||||
|
fun onProgress(currentSize: Long, totalSize: Long, progress: Int)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载完成回调
|
||||||
|
* @param file 下载完成的文件
|
||||||
|
*/
|
||||||
|
fun onSuccess(file: File)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载失败回调
|
||||||
|
* @param exception 异常信息
|
||||||
|
*/
|
||||||
|
fun onFailure(exception: Exception)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载 Zip 文件
|
||||||
|
* @param downloadUrl 下载链接
|
||||||
|
* @param targetFile 目标文件路径
|
||||||
|
* @param listener 进度监听器
|
||||||
|
* @param okHttpClient OkHttp 客户端(可选,默认创建新实例)
|
||||||
|
*/
|
||||||
|
fun downloadZip(
|
||||||
|
downloadUrl: String,
|
||||||
|
targetFile: File,
|
||||||
|
listener: DownloadProgressListener? = null,
|
||||||
|
okHttpClient: OkHttpClient? = null
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
// 确保目标文件的父目录存在
|
||||||
|
targetFile.parentFile?.mkdirs()
|
||||||
|
|
||||||
|
val client = okHttpClient ?: OkHttpClient()
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(downloadUrl)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val response = client.newCall(request).execute()
|
||||||
|
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
throw IOException("下载失败,HTTP 状态码: ${response.code}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val responseBody = response.body
|
||||||
|
?: throw IOException("响应体为空")
|
||||||
|
|
||||||
|
val totalSize = responseBody.contentLength()
|
||||||
|
Timber.d("开始下载 Zip 文件,总大小: ${formatFileSize(totalSize)}")
|
||||||
|
|
||||||
|
var currentSize = 0L
|
||||||
|
val buffer = ByteArray(8192) // 8KB 缓冲区
|
||||||
|
var bytesRead: Int
|
||||||
|
|
||||||
|
FileOutputStream(targetFile).use { fileOutput ->
|
||||||
|
responseBody.byteStream().use { inputStream ->
|
||||||
|
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
|
||||||
|
fileOutput.write(buffer, 0, bytesRead)
|
||||||
|
currentSize += bytesRead
|
||||||
|
|
||||||
|
// 计算进度百分比
|
||||||
|
val progress = if (totalSize > 0) {
|
||||||
|
((currentSize * 100) / totalSize).toInt()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
listener?.onProgress(currentSize, totalSize, progress)
|
||||||
|
Timber.d("下载进度: $progress% (${formatFileSize(currentSize)}/${formatFileSize(totalSize)})")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timber.d("Zip 文件下载完成: ${targetFile.absolutePath}")
|
||||||
|
listener?.onSuccess(targetFile)
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "下载 Zip 文件失败")
|
||||||
|
targetFile.delete() // 删除不完整的文件
|
||||||
|
listener?.onFailure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化文件大小为可读字符串
|
||||||
|
* @param bytes 字节数
|
||||||
|
* @return 格式化后的字符串(如 "10.5 MB")
|
||||||
|
*/
|
||||||
|
private fun formatFileSize(bytes: Long): String {
|
||||||
|
return when {
|
||||||
|
bytes <= 0 -> "0 B"
|
||||||
|
bytes < 1024 -> "$bytes B"
|
||||||
|
bytes < 1024 * 1024 -> String.format("%.2f KB", bytes / 1024.0)
|
||||||
|
bytes < 1024 * 1024 * 1024 -> String.format("%.2f MB", bytes / (1024.0 * 1024))
|
||||||
|
else -> String.format("%.2f GB", bytes / (1024.0 * 1024 * 1024))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
package com.sw.inbound.utils
|
||||||
|
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.util.zip.ZipEntry
|
||||||
|
import java.util.zip.ZipInputStream
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zip 文件解压工具类
|
||||||
|
* 支持解压进度回调、自动创建目录等功能
|
||||||
|
*/
|
||||||
|
object ZipExtractUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解压进度回调接口
|
||||||
|
*/
|
||||||
|
interface ExtractProgressListener {
|
||||||
|
/**
|
||||||
|
* 解压进度回调
|
||||||
|
* @param currentFile 当前正在解压的文件名
|
||||||
|
* @param currentIndex 当前解压文件索引
|
||||||
|
* @param totalFiles 总文件数
|
||||||
|
* @param progress 进度百分比 (0-100)
|
||||||
|
*/
|
||||||
|
fun onProgress(currentFile: String, currentIndex: Int, totalFiles: Int, progress: Int)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解压完成回调
|
||||||
|
* @param targetDir 解压目标目录
|
||||||
|
*/
|
||||||
|
fun onSuccess(targetDir: File)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解压失败回调
|
||||||
|
* @param exception 异常信息
|
||||||
|
*/
|
||||||
|
fun onFailure(exception: Exception)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解压 Zip 文件
|
||||||
|
* @param zipFile 待解压的 Zip 文件
|
||||||
|
* @param targetDir 解压目标目录
|
||||||
|
* @param listener 进度监听器
|
||||||
|
*/
|
||||||
|
fun extractZip(
|
||||||
|
zipFile: File,
|
||||||
|
targetDir: File,
|
||||||
|
listener: ExtractProgressListener? = null
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
if (!zipFile.exists() || !zipFile.isFile) {
|
||||||
|
throw IllegalArgumentException("Zip 文件不存在或不是文件: ${zipFile.absolutePath}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保目标目录存在
|
||||||
|
targetDir.mkdirs()
|
||||||
|
|
||||||
|
Timber.d("开始解压 Zip 文件: ${zipFile.absolutePath}")
|
||||||
|
Timber.d("解压目标目录: ${targetDir.absolutePath}")
|
||||||
|
|
||||||
|
// 第一次遍历:计算总文件数
|
||||||
|
val totalFiles = countZipEntries(zipFile)
|
||||||
|
Timber.d("Zip 文件总条目数: $totalFiles")
|
||||||
|
|
||||||
|
var currentIndex = 0
|
||||||
|
|
||||||
|
ZipInputStream(zipFile.inputStream()).use { zipInputStream ->
|
||||||
|
var entry: ZipEntry? = zipInputStream.nextEntry
|
||||||
|
|
||||||
|
while (entry != null) {
|
||||||
|
currentIndex++
|
||||||
|
val entryName = entry.name
|
||||||
|
val progress = ((currentIndex * 100) / totalFiles).coerceIn(0, 100)
|
||||||
|
|
||||||
|
Timber.d("解压进度: $progress% ($currentIndex/$totalFiles) - $entryName")
|
||||||
|
listener?.onProgress(entryName, currentIndex, totalFiles, progress)
|
||||||
|
|
||||||
|
if (entry.isDirectory) {
|
||||||
|
// 创建目录
|
||||||
|
val dir = File(targetDir, entryName)
|
||||||
|
dir.mkdirs()
|
||||||
|
} else {
|
||||||
|
// 解压文件
|
||||||
|
val file = File(targetDir, entryName)
|
||||||
|
file.parentFile?.mkdirs()
|
||||||
|
|
||||||
|
FileOutputStream(file).use { fileOutput ->
|
||||||
|
val buffer = ByteArray(8192) // 8KB 缓冲区
|
||||||
|
var bytesRead: Int
|
||||||
|
while (zipInputStream.read(buffer).also { bytesRead = it } != -1) {
|
||||||
|
fileOutput.write(buffer, 0, bytesRead)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
zipInputStream.closeEntry()
|
||||||
|
entry = zipInputStream.nextEntry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timber.d("Zip 文件解压完成: ${targetDir.absolutePath}")
|
||||||
|
listener?.onSuccess(targetDir)
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "解压 Zip 文件失败")
|
||||||
|
listener?.onFailure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算 Zip 文件中的条目总数
|
||||||
|
* @param zipFile Zip 文件
|
||||||
|
* @return 条目总数
|
||||||
|
*/
|
||||||
|
private fun countZipEntries(zipFile: File): Int {
|
||||||
|
var count = 0
|
||||||
|
try {
|
||||||
|
ZipInputStream(zipFile.inputStream()).use { zipInputStream ->
|
||||||
|
while (zipInputStream.nextEntry != null) {
|
||||||
|
count++
|
||||||
|
zipInputStream.closeEntry()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "计算 Zip 条目数失败")
|
||||||
|
}
|
||||||
|
return count.coerceAtLeast(1) // 至少返回 1,避免除以 0
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
package com.sw.inbound.utils
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zip 文件下载和解压工具使用示例
|
||||||
|
*
|
||||||
|
* ============================================
|
||||||
|
* 方案一:使用高层管理器(推荐)
|
||||||
|
* ============================================
|
||||||
|
*
|
||||||
|
* 在 Activity 或 ViewModel 中使用:
|
||||||
|
*
|
||||||
|
* ```kotlin
|
||||||
|
* // 下载并解压模型资源
|
||||||
|
* ModelResourceManager.downloadAndExtractResources(
|
||||||
|
* context = this,
|
||||||
|
* downloadUrl = "https://example.com/model_resources.zip",
|
||||||
|
* callback = object : ModelResourceManager.ResourceCallback {
|
||||||
|
* override fun onDownloadProgress(progress: Int, message: String) {
|
||||||
|
* // 更新下载进度 UI
|
||||||
|
* runOnUiThread {
|
||||||
|
* tvProgress.text = "$progress% - $message"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* override fun onExtractProgress(progress: Int, message: String) {
|
||||||
|
* // 更新解压进度 UI
|
||||||
|
* runOnUiThread {
|
||||||
|
* tvProgress.text = "$progress% - $message"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* override fun onSuccess(resourceDir: File) {
|
||||||
|
* // 资源解压完成,可以开始使用
|
||||||
|
* Timber.d("资源已准备好: ${resourceDir.absolutePath}")
|
||||||
|
* // 例如:加载 data 文件夹和 .pt 文件
|
||||||
|
* val dataDir = File(resourceDir, "data")
|
||||||
|
* val modelFile = File(resourceDir, "model.pt")
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* override fun onFailure(errorMessage: String) {
|
||||||
|
* // 处理错误
|
||||||
|
* runOnUiThread {
|
||||||
|
* ToastUtils.show("资源下载失败: $errorMessage")
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* // 获取已缓存的资源目录
|
||||||
|
* val cachedDir = ModelResourceManager.getCachedResourcesDir(this)
|
||||||
|
* if (cachedDir != null) {
|
||||||
|
* // 资源已缓存,直接使用
|
||||||
|
* val dataDir = File(cachedDir, "data")
|
||||||
|
* val modelFile = File(cachedDir, "model.pt")
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* // 清理缓存的资源
|
||||||
|
* ModelResourceManager.clearCachedResources(this)
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ============================================
|
||||||
|
* 方案二:分别使用下载和解压工具
|
||||||
|
* ============================================
|
||||||
|
*
|
||||||
|
* 如果需要更细粒度的控制,可以分别使用:
|
||||||
|
*
|
||||||
|
* ```kotlin
|
||||||
|
* // 1. 下载 Zip 文件
|
||||||
|
* val zipFile = File(context.cacheDir, "model_resources.zip")
|
||||||
|
* ZipDownloadUtils.downloadZip(
|
||||||
|
* downloadUrl = "https://example.com/model_resources.zip",
|
||||||
|
* targetFile = zipFile,
|
||||||
|
* listener = object : ZipDownloadUtils.DownloadProgressListener {
|
||||||
|
* override fun onProgress(currentSize: Long, totalSize: Long, progress: Int) {
|
||||||
|
* Timber.d("下载进度: $progress%")
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* override fun onSuccess(file: File) {
|
||||||
|
* Timber.d("下载完成: ${file.absolutePath}")
|
||||||
|
* // 继续解压
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* override fun onFailure(exception: Exception) {
|
||||||
|
* Timber.e(exception, "下载失败")
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* // 2. 解压 Zip 文件
|
||||||
|
* val targetDir = File(context.cacheDir, "model_resources")
|
||||||
|
* ZipExtractUtils.extractZip(
|
||||||
|
* zipFile = zipFile,
|
||||||
|
* targetDir = targetDir,
|
||||||
|
* listener = object : ZipExtractUtils.ExtractProgressListener {
|
||||||
|
* override fun onProgress(
|
||||||
|
* currentFile: String,
|
||||||
|
* currentIndex: Int,
|
||||||
|
* totalFiles: Int,
|
||||||
|
* progress: Int
|
||||||
|
* ) {
|
||||||
|
* Timber.d("解压进度: $progress% - $currentFile")
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* override fun onSuccess(targetDir: File) {
|
||||||
|
* Timber.d("解压完成: ${targetDir.absolutePath}")
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* override fun onFailure(exception: Exception) {
|
||||||
|
* Timber.e(exception, "解压失败")
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ============================================
|
||||||
|
* 关键特性
|
||||||
|
* ============================================
|
||||||
|
*
|
||||||
|
* 1. 下载特性:
|
||||||
|
* - 支持大文件下载(120MB+ 无压力)
|
||||||
|
* - 实时进度回调
|
||||||
|
* - 自动错误处理和清理
|
||||||
|
* - 使用 8KB 缓冲区,内存占用低
|
||||||
|
*
|
||||||
|
* 2. 解压特性:
|
||||||
|
* - 支持大型 Zip 文件
|
||||||
|
* - 自动创建目录结构
|
||||||
|
* - 实时进度回调
|
||||||
|
* - 使用 Java 内置 ZipInputStream,无额外依赖
|
||||||
|
*
|
||||||
|
* 3. 资源管理:
|
||||||
|
* - 自动缓存到应用缓存目录
|
||||||
|
* - 支持检查缓存是否存在
|
||||||
|
* - 支持清理缓存
|
||||||
|
* - 完整的日志记录
|
||||||
|
*
|
||||||
|
* ============================================
|
||||||
|
* 建议的集成方式
|
||||||
|
* ============================================
|
||||||
|
*
|
||||||
|
* 1. 在 Application 初始化时检查缓存:
|
||||||
|
* ```kotlin
|
||||||
|
* class MyApp : Application() {
|
||||||
|
* override fun onCreate() {
|
||||||
|
* super.onCreate()
|
||||||
|
* // 检查模型资源是否已缓存
|
||||||
|
* val cachedDir = ModelResourceManager.getCachedResourcesDir(this)
|
||||||
|
* if (cachedDir == null) {
|
||||||
|
* // 需要下载资源,可以在首次启动时提示用户
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* 2. 在需要使用资源的 Activity/Fragment 中:
|
||||||
|
* ```kotlin
|
||||||
|
* val cachedDir = ModelResourceManager.getCachedResourcesDir(context)
|
||||||
|
* if (cachedDir != null) {
|
||||||
|
* // 资源已准备好,直接使用
|
||||||
|
* useModelResources(cachedDir)
|
||||||
|
* } else {
|
||||||
|
* // 资源不存在,触发下载
|
||||||
|
* downloadResources()
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* 3. 在后台线程中执行下载和解压:
|
||||||
|
* ```kotlin
|
||||||
|
* Thread {
|
||||||
|
* ModelResourceManager.downloadAndExtractResources(
|
||||||
|
* context = this,
|
||||||
|
* downloadUrl = resourceUrl,
|
||||||
|
* callback = callback
|
||||||
|
* )
|
||||||
|
* }.start()
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ============================================
|
||||||
|
* 注意事项
|
||||||
|
* ============================================
|
||||||
|
*
|
||||||
|
* 1. 下载和解压操作会阻塞线程,建议在后台线程执行
|
||||||
|
* 2. 确保应用有网络权限和文件读写权限
|
||||||
|
* 3. 缓存目录会占用应用缓存空间,大文件需要足够的存储空间
|
||||||
|
* 4. 下载失败时会自动删除不完整的文件
|
||||||
|
* 5. 所有操作都有详细的日志记录,可通过 Timber 查看
|
||||||
|
*/
|
||||||
|
object ZipToolsUsageExample
|
||||||
@@ -14,6 +14,7 @@ import android.os.Bundle
|
|||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
|
import android.view.TouchDelegate
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
@@ -270,3 +271,24 @@ fun String.copyText(context: Context, showToast: Boolean = true) {
|
|||||||
Toast.makeText(context, "文本已复制到剪贴板", Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, "文本已复制到剪贴板", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扩大 View 的点击区域
|
||||||
|
* @param horizontal 水平方向扩大距离(dp)→ 左右各扩大一半
|
||||||
|
* @param vertical 垂直方向扩大距离(dp)→ 上下各扩大一半
|
||||||
|
*/
|
||||||
|
fun View.expandClickArea(horizontal: Int, vertical: Int) {
|
||||||
|
val density = resources.displayMetrics.density
|
||||||
|
val hPx = (horizontal * density).toInt()
|
||||||
|
val vPx = (vertical * density).toInt()
|
||||||
|
|
||||||
|
post {
|
||||||
|
val rect = android.graphics.Rect()
|
||||||
|
getHitRect(rect)
|
||||||
|
|
||||||
|
// 扩大区域:负值 = 向外扩大
|
||||||
|
rect.inset(-hPx, -vPx)
|
||||||
|
//rect.bottom += downPx // 只往下扩大
|
||||||
|
(parent as View).touchDelegate = TouchDelegate(rect, this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
import com.sw.inbound.GlobalData
|
import com.sw.inbound.GlobalData
|
||||||
|
import com.sw.inbound.MyApp
|
||||||
import com.sw.inbound.model.response.ApiResponse
|
import com.sw.inbound.model.response.ApiResponse
|
||||||
import com.sw.inbound.model.response.DictType
|
import com.sw.inbound.model.response.DictType
|
||||||
import com.sw.inbound.model.response.EquipmentInfo
|
import com.sw.inbound.model.response.EquipmentInfo
|
||||||
@@ -16,6 +17,7 @@ import com.sw.inbound.repository.RemoteRepository
|
|||||||
import com.sw.inbound.utils.SPUtil
|
import com.sw.inbound.utils.SPUtil
|
||||||
//import com.sw.inbound.utils.ToastUtils
|
//import com.sw.inbound.utils.ToastUtils
|
||||||
import com.sw.inbound.utils.ext.toType
|
import com.sw.inbound.utils.ext.toType
|
||||||
|
import com.sw.inbound.utils.ext.toast
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -103,6 +105,7 @@ abstract class BaseViewModel(
|
|||||||
}
|
}
|
||||||
Timber.d("msg = ${response.msg}, code = ${response.code}")
|
Timber.d("msg = ${response.msg}, code = ${response.code}")
|
||||||
// ToastUtils.showToast("${response.msg}(${response.code})")
|
// ToastUtils.showToast("${response.msg}(${response.code})")
|
||||||
|
MyApp.instance?.toast("${response.msg}(${response.code})")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,7 +212,11 @@ abstract class BaseViewModel(
|
|||||||
callback: (List<SearchGoodsInfo.Record>) -> Unit
|
callback: (List<SearchGoodsInfo.Record>) -> Unit
|
||||||
) {
|
) {
|
||||||
launch {
|
launch {
|
||||||
if (nameList.isNullOrEmpty() && scoreList.isNullOrEmpty()) return@launch
|
try {
|
||||||
|
if (nameList.isNullOrEmpty() && scoreList.isNullOrEmpty()) {
|
||||||
|
callback(listOf())
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
val paramList = if (nameList.isNullOrEmpty()) scoreList!!.map { it.name } else nameList
|
val paramList = if (nameList.isNullOrEmpty()) scoreList!!.map { it.name } else nameList
|
||||||
val response = repository.recognizeGoodsList(paramList)
|
val response = repository.recognizeGoodsList(paramList)
|
||||||
val list = if (parseResponse(response)) response.data ?: listOf() else listOf()
|
val list = if (parseResponse(response)) response.data ?: listOf() else listOf()
|
||||||
@@ -220,6 +227,10 @@ abstract class BaseViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
callback(list)
|
callback(list)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
callback(listOf())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,29 +27,29 @@ class DeviceViewModel @Inject constructor(
|
|||||||
/**
|
/**
|
||||||
* 获取token
|
* 获取token
|
||||||
*/
|
*/
|
||||||
fun getDeviceToken(deviceId: String = GlobalData.deviceId) {
|
// fun getDeviceToken(deviceId: String = GlobalData.deviceId) {
|
||||||
launchWithLoading {
|
// launchWithLoading {
|
||||||
val response = repository.getDeviceToken(deviceId)
|
// val response = repository.getDeviceToken(deviceId)
|
||||||
if (parseResponse(response)) {
|
// if (parseResponse(response)) {
|
||||||
val response1 = repository.getDeviceInfo(deviceId, response.result!!)
|
// val response1 = repository.getDeviceInfo(deviceId, response.result!!)
|
||||||
|
//
|
||||||
if (parseResponse(response1)) {
|
// if (parseResponse(response1)) {
|
||||||
val equipmentInfo = response1.result
|
// val equipmentInfo = response1.result
|
||||||
if (equipmentInfo == null) return@launchWithLoading
|
// if (equipmentInfo == null) return@launchWithLoading
|
||||||
try {
|
// try {
|
||||||
parseEquipmentInfo(equipmentInfo)
|
// parseEquipmentInfo(equipmentInfo)
|
||||||
SPUtil.getInstance()
|
// SPUtil.getInstance()
|
||||||
.put(GlobalKey.KEY_EQUIPMENT_INFO, GsonUtils.toJson(equipmentInfo))
|
// .put(GlobalKey.KEY_EQUIPMENT_INFO, GsonUtils.toJson(equipmentInfo))
|
||||||
_deviceInfoResult.value = true
|
// _deviceInfoResult.value = true
|
||||||
} catch (e: Exception) {
|
// } catch (e: Exception) {
|
||||||
Timber.e(e)
|
// Timber.e(e)
|
||||||
}
|
// }
|
||||||
} else {
|
// } else {
|
||||||
//ToastUtils.showToast("获取设备信息失败")
|
// //ToastUtils.showToast("获取设备信息失败")
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查缓存数据
|
* 检查缓存数据
|
||||||
|
|||||||
@@ -323,17 +323,17 @@ class ReceiptViewModel @Inject constructor(
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
fun confirmReceipt(uploadInfo: UploadInfo, callback: (Boolean) -> Unit) {
|
fun confirmReceipt(uploadInfo: UploadInfo, callback: (Boolean, String) -> Unit) {
|
||||||
launchWithLoading {
|
launchWithLoading {
|
||||||
val resp = repository.confirmReceipt(uploadInfo)
|
val resp = repository.confirmReceipt(uploadInfo)
|
||||||
callback(resp.data == true)
|
callback(resp.isSuccess(), resp.msg?:"")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addToWarehouse(list: List<PurchaseWarehouseParam>, callback: (Boolean) -> Unit) {
|
fun addToWarehouse(list: List<PurchaseWarehouseParam>, callback: (Boolean, String) -> Unit) {
|
||||||
launchWithLoading {
|
launchWithLoading {
|
||||||
val resp = repository.selfPurchaseWarehousing(list)
|
val resp = repository.selfPurchaseWarehousing(list)
|
||||||
callback(resp.data == true)
|
callback(resp.isSuccess(), resp.msg?:"")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,11 +409,7 @@ class ReceiptViewModel @Inject constructor(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
val resp = repository.uploadMultipleImages(fileList = fileList, params = params)
|
val resp = repository.uploadMultipleImages(fileList = fileList, params = params)
|
||||||
// if (!parseResponse(resp)) {
|
return parseResponse(resp)
|
||||||
// callback(false)
|
|
||||||
// return@launchWithLoading
|
|
||||||
// }
|
|
||||||
return resp.code == "00000"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
package com.sw.inbound.viewmodel
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.sw.inbound.model.response.IngredientByTrace
|
||||||
|
import com.sw.inbound.model.response.SpecOption
|
||||||
|
import com.sw.inbound.model.response.SupplierOption
|
||||||
|
import com.sw.inbound.model.response.ZoneOption
|
||||||
|
import com.sw.inbound.repository.InboundRepository
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class StoreViewModel @Inject constructor(
|
||||||
|
private val repository: InboundRepository
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val _zoneOptions = MutableStateFlow<List<ZoneOption>>(emptyList())
|
||||||
|
val zoneOptions: StateFlow<List<ZoneOption>> = _zoneOptions
|
||||||
|
|
||||||
|
private val _supplierOptions = MutableStateFlow<List<SupplierOption>>(emptyList())
|
||||||
|
val supplierOptions: StateFlow<List<SupplierOption>> = _supplierOptions
|
||||||
|
|
||||||
|
private val _specOptions = MutableStateFlow<List<SpecOption>>(emptyList())
|
||||||
|
val specOptions: StateFlow<List<SpecOption>> = _specOptions
|
||||||
|
|
||||||
|
/** 按溯源码查食材,结果通过 callback 返回 */
|
||||||
|
fun getIngredientByTrace(
|
||||||
|
traceCode: String,
|
||||||
|
callback: (List<IngredientByTrace>) -> Unit
|
||||||
|
) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val response = repository.getIngredientByTrace(traceCode)
|
||||||
|
if (response.isSuccess()) {
|
||||||
|
callback(response.data ?: emptyList())
|
||||||
|
} else {
|
||||||
|
Timber.e("getIngredientByTrace failed: ${response.msg}")
|
||||||
|
callback(emptyList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 加载仓库区域列表,结果同时更新 StateFlow 并通过 callback 返回 */
|
||||||
|
fun loadZoneOptions(
|
||||||
|
warehouseId: Long? = null,
|
||||||
|
keyword: String? = null,
|
||||||
|
callback: ((List<ZoneOption>) -> Unit)? = null
|
||||||
|
) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val response = repository.getZoneOptions(warehouseId, keyword)
|
||||||
|
if (response.isSuccess()) {
|
||||||
|
val list = response.data ?: emptyList()
|
||||||
|
_zoneOptions.value = list
|
||||||
|
callback?.invoke(list)
|
||||||
|
} else {
|
||||||
|
Timber.e("loadZoneOptions failed: ${response.msg}")
|
||||||
|
callback?.invoke(emptyList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 加载供应商列表,结果同时更新 StateFlow 并通过 callback 返回 */
|
||||||
|
fun loadSupplierOptions(
|
||||||
|
type: Int? = null,
|
||||||
|
keyword: String? = null,
|
||||||
|
callback: ((List<SupplierOption>) -> Unit)? = null
|
||||||
|
) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val response = repository.getSupplierOptions(type, keyword)
|
||||||
|
if (response.isSuccess()) {
|
||||||
|
val list = response.data ?: emptyList()
|
||||||
|
_supplierOptions.value = list
|
||||||
|
callback?.invoke(list)
|
||||||
|
} else {
|
||||||
|
Timber.e("loadSupplierOptions failed: ${response.msg}")
|
||||||
|
callback?.invoke(emptyList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 加载规格列表,结果同时更新 StateFlow 并通过 callback 返回 */
|
||||||
|
fun loadSpecOptions(callback: ((List<SpecOption>) -> Unit)? = null) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val response = repository.getSpecOptions()
|
||||||
|
if (response.isSuccess()) {
|
||||||
|
val list = response.data ?: emptyList()
|
||||||
|
_specOptions.value = list
|
||||||
|
callback?.invoke(list)
|
||||||
|
} else {
|
||||||
|
Timber.e("loadSpecOptions failed: ${response.msg}")
|
||||||
|
callback?.invoke(emptyList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 毛菜入库提交 */
|
||||||
|
fun submitRawInbound(param: Map<String, Any>, callback: (Boolean, String?) -> Unit) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val response = repository.submitRawInbound(param)
|
||||||
|
callback(response.isSuccess(), response.msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 净菜入库提交 */
|
||||||
|
fun submitCleanInbound(param: Map<String, Any>, callback: (Boolean, String?) -> Unit) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val response = repository.submitCleanInbound(param)
|
||||||
|
callback(response.isSuccess(), response.msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,23 @@
|
|||||||
package com.sw.inbound.viewmodel
|
package com.sw.inbound.viewmodel
|
||||||
|
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.sw.inbound.GlobalData
|
import com.sw.inbound.GlobalData
|
||||||
import com.sw.inbound.GlobalKey
|
import com.sw.inbound.GlobalKey
|
||||||
import com.sw.inbound.model.request.LoginParam
|
import com.sw.inbound.model.request.LoginParam
|
||||||
|
import com.sw.inbound.model.response.CollectedFoodInfo
|
||||||
import com.sw.inbound.model.response.User
|
import com.sw.inbound.model.response.User
|
||||||
|
import com.sw.inbound.objbox.ObjectBox
|
||||||
import com.sw.inbound.repository.RemoteRepository
|
import com.sw.inbound.repository.RemoteRepository
|
||||||
import com.sw.inbound.utils.GsonUtils
|
import com.sw.inbound.utils.GsonUtils
|
||||||
import com.sw.inbound.utils.SPUtil
|
import com.sw.inbound.utils.SPUtil
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.collections.remove
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class UserViewModel @Inject constructor(
|
class UserViewModel @Inject constructor(
|
||||||
@@ -33,16 +39,16 @@ class UserViewModel @Inject constructor(
|
|||||||
//}
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun login(userName: String, password: String) = launchWithLoading {
|
// fun login(userName: String, password: String) = launchWithLoading {
|
||||||
val loginParam = LoginParam(userName = userName, password = password)
|
// val loginParam = LoginParam(userName = userName, password = password)
|
||||||
val response = repo.login(loginParam)
|
// val response = repo.login(loginParam)
|
||||||
if (parseResponse(response)) {
|
// if (parseResponse(response)) {
|
||||||
SPUtil.getInstance().put(GlobalKey.KEY_USER_NAME, userName)
|
// SPUtil.getInstance().put(GlobalKey.KEY_USER_NAME, userName)
|
||||||
_user.value = response.result
|
// _user.value = response.result
|
||||||
saveToken(response.result)
|
// saveToken(response.result)
|
||||||
response.result?.token
|
// response.result?.token
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
fun getUserInfo() {
|
fun getUserInfo() {
|
||||||
val userInfo = SPUtil.getInstance().get(GlobalKey.KEY_USER_INFO, "")
|
val userInfo = SPUtil.getInstance().get(GlobalKey.KEY_USER_INFO, "")
|
||||||
@@ -64,6 +70,39 @@ class UserViewModel @Inject constructor(
|
|||||||
SPUtil.getInstance().remove(GlobalKey.KEY_TOKEN)
|
SPUtil.getInstance().remove(GlobalKey.KEY_TOKEN)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已采集食材列表 UI 状态流
|
||||||
|
* 元素为按名称分组后的(名称,数量)数据
|
||||||
|
*/
|
||||||
|
private val _collectedFoodListState = MutableStateFlow<List<CollectedFoodInfo>>(emptyList())
|
||||||
|
val collectedFoodListState: StateFlow<List<CollectedFoodInfo>> = _collectedFoodListState.asStateFlow()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载已采集食材列表,按名称分组统计数量
|
||||||
|
* 使用属性查询只读 name 字段,不加载 foodVector,节省内存
|
||||||
|
* @param nameFilter 名称过滤关键词,为空时加载全部
|
||||||
|
*/
|
||||||
|
fun loadCollectedFoodList(nameFilter: String? = null) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val result = ObjectBox.queryFoodNameCounts(nameFilter)
|
||||||
|
_collectedFoodListState.value = result.map { (name, count) ->
|
||||||
|
CollectedFoodInfo(foodName = name, foodNum = count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除指定名称的所有采集记录,删除完成后刷新列表
|
||||||
|
* @param foodName 食材名称
|
||||||
|
* @param nameFilter 当前搜索关键词,用于删除后刷新列表
|
||||||
|
*/
|
||||||
|
fun removeCollectedFood(foodName: String, nameFilter: String? = null) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
ObjectBox.remove(foodName)
|
||||||
|
loadCollectedFoodList(nameFilter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun handleSensorScale(weight: Double) {
|
override fun handleSensorScale(weight: Double) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<!-- 从上到下:浅蓝(占比多)→ 深蓝,angle=270 表示从上到下 -->
|
||||||
|
<gradient
|
||||||
|
android:angle="270"
|
||||||
|
android:startColor="#88BBFF"
|
||||||
|
android:centerColor="#1A6EFF"
|
||||||
|
android:centerX="0.65"
|
||||||
|
android:endColor="#0032C8"
|
||||||
|
android:type="linear" />
|
||||||
|
<corners android:radius="16dp" />
|
||||||
|
</shape>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<!-- 从上到下:浅橙(占比多)→ 深橙,angle=270 表示从上到下 -->
|
||||||
|
<gradient
|
||||||
|
android:angle="270"
|
||||||
|
android:startColor="#FFD08A"
|
||||||
|
android:centerColor="#FF9632"
|
||||||
|
android:centerX="0.65"
|
||||||
|
android:endColor="#CC6400"
|
||||||
|
android:type="linear" />
|
||||||
|
<corners android:radius="16dp" />
|
||||||
|
</shape>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="#4ECDC4" />
|
||||||
|
<corners android:radius="12dp" />
|
||||||
|
<stroke
|
||||||
|
android:width="2dp"
|
||||||
|
android:color="#2BA39F" />
|
||||||
|
</shape>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="80dp"
|
||||||
|
android:height="80dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<!-- 网络断开的图标 -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF6B6B"
|
||||||
|
android:pathData="M1,9l2,2c4.97,-4.97 13.03,-4.97 18,0l2,-2C16.93,2.93 7.08,2.93 1,9zM5,13l2,2c2.76,-2.76 7.24,-2.76 10,0l2,-2c-4.42,-4.42 -11.58,-4.42 -16,0zM9,17h6v2h-6z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="240dp"
|
||||||
|
android:height="240dp"
|
||||||
|
android:viewportWidth="1024"
|
||||||
|
android:viewportHeight="1024">
|
||||||
|
|
||||||
|
<!-- 由小到大排列的圆点,构成旋转加载指示器 -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#262626"
|
||||||
|
android:pathData="M876.864,782.592c3.264,0 6.272,-3.2 6.272,-6.656c0,-3.456 -3.008,-6.592 -6.272,-6.592c-3.264,0 -6.272,3.2 -6.272,6.592c0,3.456 3.008,6.656 6.272,6.656z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#262626"
|
||||||
|
android:pathData="M736.32,935.936c2.304,2.432 5.568,3.84 8.768,3.84a12.16,12.16 0,0 0,8.832 -3.84a13.76,13.76 0,0 0,0 -18.56a12.224,12.224 0,0 0,-8.832 -3.84a12.16,12.16 0,0 0,-8.768 3.84a13.696,13.696 0,0 0,0 18.56z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#262626"
|
||||||
|
android:pathData="M552.32,1018.24c3.456,3.648 8.32,5.76 13.184,5.76a18.368,18.368 0,0 0,13.184 -5.76a20.608,20.608 0,0 0,0 -27.968a18.368,18.368 0,0 0,-13.184 -5.824a18.368,18.368 0,0 0,-13.184 5.76a20.608,20.608 0,0 0,0 28.032z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#262626"
|
||||||
|
android:pathData="M353.984,1012.48c4.608,4.8 11.072,7.68 17.6,7.68a24.448,24.448 0,0 0,17.536 -7.68a27.456,27.456 0,0 0,0 -37.248a24.448,24.448 0,0 0,-17.536 -7.68a24.448,24.448 0,0 0,-17.6 7.68a27.52,27.52 0,0 0,0 37.184z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#262626"
|
||||||
|
android:pathData="M178.304,920.64c5.76,6.08 13.824,9.6 21.952,9.6a30.592,30.592 0,0 0,22.016 -9.6a34.368,34.368 0,0 0,0 -46.592a30.592,30.592 0,0 0,-22.016 -9.6a30.592,30.592 0,0 0,-21.952 9.6a34.368,34.368 0,0 0,0 46.592z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#262626"
|
||||||
|
android:pathData="M57.152,761.28c6.912,7.36 16.64,11.648 26.368,11.648a36.736,36.736 0,0 0,26.432 -11.584a41.28,41.28 0,0 0,0 -55.936a36.736,36.736 0,0 0,-26.432 -11.584a36.8,36.8 0,0 0,-26.368 11.52a41.28,41.28 0,0 0,0 56z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#262626"
|
||||||
|
android:pathData="M12.736,564.672a42.88,42.88 0,0 0,30.784 13.44a42.88,42.88 0,0 0,30.784 -13.44a48.128,48.128 0,0 0,0 -65.216a42.88,42.88 0,0 0,-30.72 -13.44a42.88,42.88 0,0 0,-30.848 13.44a48.128,48.128 0,0 0,0 65.216z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#262626"
|
||||||
|
android:pathData="M52.544,369.28a48.96,48.96 0,0 0,35.2 15.36a48.96,48.96 0,0 0,35.2 -15.36a54.976,54.976 0,0 0,0 -74.56a48.96,48.96 0,0 0,-35.2 -15.424a48.96,48.96 0,0 0,-35.2 15.424a54.976,54.976 0,0 0,0 74.56z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#262626"
|
||||||
|
android:pathData="M168.32,212.48c10.368,11.008 24.96,17.408 39.68,17.408c14.592,0 29.184,-6.4 39.552,-17.408a61.888,61.888 0,0 0,0 -83.84a55.104,55.104 0,0 0,-39.616 -17.408c-14.656,0 -29.248,6.4 -39.616,17.408a61.888,61.888 0,0 0,0 83.84z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#262626"
|
||||||
|
android:pathData="M337.344,124.8c11.52,12.16 27.712,19.264 43.968,19.264c16.256,0 32.448,-7.04 43.968,-19.264a68.672,68.672 0,0 0,0 -93.184a61.248,61.248 0,0 0,-43.968 -19.264a61.248,61.248 0,0 0,-43.968 19.264a68.736,68.736 0,0 0,0 93.184z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#262626"
|
||||||
|
android:pathData="M526.976,123.712c12.672,13.44 30.528,21.248 48.448,21.248s35.712,-7.808 48.384,-21.248a75.584,75.584 0,0 0,0 -102.464A67.392,67.392 0,0 0,575.36 0c-17.92,0 -35.776,7.808 -48.448,21.248a75.584,75.584 0,0 0,0 102.464z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#262626"
|
||||||
|
android:pathData="M700.8,210.304c13.824,14.592 33.28,23.104 52.736,23.104c19.584,0 39.04,-8.512 52.8,-23.104a82.432,82.432 0,0 0,0 -111.744a73.472,73.472 0,0 0,-52.8 -23.168c-19.52,0 -38.912,8.512 -52.736,23.168a82.432,82.432 0,0 0,0 111.744z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#262626"
|
||||||
|
android:pathData="M824.832,368.832c14.976,15.872 36.032,25.088 57.216,25.088c21.12,0 42.24,-9.216 57.152,-25.088a89.344,89.344 0,0 0,0 -121.088a79.616,79.616 0,0 0,-57.152 -25.088c-21.184,0 -42.24,9.216 -57.216,25.088a89.344,89.344 0,0 0,0 121.088z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#262626"
|
||||||
|
android:pathData="M875.264,572.864c16.128,17.088 38.784,27.008 61.632,27.008c22.784,0 45.44,-9.92 61.568,-27.008a96.256,96.256 0,0 0,0 -130.432a85.76,85.76 0,0 0,-61.568 -27.072c-22.848,0 -45.44,9.984 -61.632,27.072a96.192,96.192 0,0 0,0 130.432z" />
|
||||||
|
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="ring"
|
||||||
|
android:innerRadiusRatio="3"
|
||||||
|
android:thicknessRatio="8"
|
||||||
|
android:useLevel="false">
|
||||||
|
<gradient
|
||||||
|
android:type="sweep"
|
||||||
|
android:startColor="#FF6B6B"
|
||||||
|
android:centerColor="#4ECDC4"
|
||||||
|
android:endColor="#FF6B6B"
|
||||||
|
android:angle="0" />
|
||||||
|
</shape>
|
||||||
|
|
||||||
@@ -20,6 +20,19 @@
|
|||||||
android:textColor="#141428"
|
android:textColor="#141428"
|
||||||
android:textSize="36sp"
|
android:textSize="36sp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvImagePreview"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_gravity="center_vertical|end"
|
||||||
|
android:layout_marginEnd="40dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:paddingHorizontal="20dp"
|
||||||
|
android:text="图片"
|
||||||
|
android:textColor="#141428"
|
||||||
|
android:textSize="32sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@mipmap/bg"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<!-- 标题栏区域 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingStart="60dp"
|
||||||
|
android:paddingEnd="60dp"
|
||||||
|
android:paddingTop="22dp">
|
||||||
|
|
||||||
|
<!-- 返回按钮 -->
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/ivBack"
|
||||||
|
android:layout_width="60dp"
|
||||||
|
android:layout_height="60dp"
|
||||||
|
android:contentDescription="返回"
|
||||||
|
android:src="@mipmap/ic_back" />
|
||||||
|
|
||||||
|
<!-- 标题文字 -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="30dp"
|
||||||
|
android:text="趣味页面"
|
||||||
|
android:textColor="@color/title"
|
||||||
|
android:textSize="36sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<!-- 占位弹性空间 -->
|
||||||
|
<Space
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- 内容区域 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center"
|
||||||
|
android:padding="60dp">
|
||||||
|
|
||||||
|
<!-- 下载按钮 -->
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnDownload"
|
||||||
|
android:layout_width="400dp"
|
||||||
|
android:layout_height="80dp"
|
||||||
|
android:text="1. 下载 Zip 文件"
|
||||||
|
android:textSize="24sp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:backgroundTint="@color/title"
|
||||||
|
android:layout_marginBottom="30dp" />
|
||||||
|
|
||||||
|
<!-- 解压按钮 -->
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnExtract"
|
||||||
|
android:layout_width="400dp"
|
||||||
|
android:layout_height="80dp"
|
||||||
|
android:text="2. 解压 Zip 文件"
|
||||||
|
android:textSize="24sp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:backgroundTint="@color/title"
|
||||||
|
android:layout_marginBottom="30dp" />
|
||||||
|
|
||||||
|
<!-- 读取并保存按钮 -->
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnLoadAndSave"
|
||||||
|
android:layout_width="400dp"
|
||||||
|
android:layout_height="80dp"
|
||||||
|
android:text="3. 读取并保存到数据库"
|
||||||
|
android:textSize="24sp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:backgroundTint="@color/title"
|
||||||
|
android:layout_marginBottom="30dp" />
|
||||||
|
|
||||||
|
<!-- 进度/状态显示 -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvProgress"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="就绪"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:layout_marginTop="30dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -19,16 +19,22 @@
|
|||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="0dp"
|
android:id="@+id/tvTitle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="40dp"
|
android:layout_marginStart="30dp"
|
||||||
android:layout_weight="1"
|
android:padding="10dp"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:text="出入库管理"
|
android:text="出入库管理"
|
||||||
android:textColor="#141428"
|
android:textColor="#141428"
|
||||||
android:textSize="36sp"
|
android:textSize="36sp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_height="wrap_content"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tvUserName"
|
android:id="@+id/tvUserName"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="@mipmap/bg"
|
android:background="@mipmap/bg"
|
||||||
@@ -37,9 +38,11 @@
|
|||||||
android:id="@+id/tvTime"
|
android:id="@+id/tvTime"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingVertical="10dp"
|
||||||
android:textColor="@color/black"
|
android:textColor="@color/black"
|
||||||
android:textSize="24sp"
|
android:textSize="24sp"
|
||||||
android:visibility="visible" />
|
android:visibility="visible"
|
||||||
|
tools:text="2026-03-13 星期五"/>
|
||||||
|
|
||||||
<!-- 用户信息区(已登录时显示,未登录隐藏) -->
|
<!-- 用户信息区(已登录时显示,未登录隐藏) -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
@@ -73,13 +76,14 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<!-- 功能入口区域,水平居中排列两个入口图标 -->
|
<!-- 功能入口区域(原始,已隐藏) -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
<!-- 采购单入库入口 -->
|
<!-- 采购单入库入口 -->
|
||||||
<ImageView
|
<ImageView
|
||||||
@@ -103,4 +107,44 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- 功能入口区域(新版,按钮样式) -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<!-- 净菜入库入口 -->
|
||||||
|
<androidx.appcompat.widget.AppCompatButton
|
||||||
|
android:id="@+id/btnCleanDishStore"
|
||||||
|
android:layout_width="595dp"
|
||||||
|
android:layout_height="477dp"
|
||||||
|
android:background="@drawable/bg_gradient_blue_btn"
|
||||||
|
android:foreground="?android:attr/selectableItemBackground"
|
||||||
|
android:text="净菜"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="100sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:stateListAnimator="@null" />
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="15dp"
|
||||||
|
android:layout_height="1dp" />
|
||||||
|
|
||||||
|
<!-- 毛菜入库入口 -->
|
||||||
|
<androidx.appcompat.widget.AppCompatButton
|
||||||
|
android:id="@+id/btnRawDishStore"
|
||||||
|
android:layout_width="595dp"
|
||||||
|
android:layout_height="477dp"
|
||||||
|
android:background="@drawable/bg_gradient_orange_btn"
|
||||||
|
android:foreground="?android:attr/selectableItemBackground"
|
||||||
|
android:text="毛菜"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="100sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:stateListAnimator="@null" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@mipmap/bg"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
|
<!-- Loading 动画容器 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/llLoading"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center"
|
||||||
|
android:visibility="visible">
|
||||||
|
|
||||||
|
<!-- 加载动画 -->
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/ivLoading"
|
||||||
|
android:layout_width="160dp"
|
||||||
|
android:layout_height="160dp"
|
||||||
|
android:src="@drawable/loading"
|
||||||
|
android:contentDescription="加载中" />
|
||||||
|
|
||||||
|
<!-- 倒计时文字 -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvCountdown"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="60秒"
|
||||||
|
android:textSize="40sp"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_marginTop="50dp" />
|
||||||
|
|
||||||
|
<!-- 提示文字 -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="网络连接检测中,请稍后……"
|
||||||
|
android:textSize="22sp"
|
||||||
|
android:textColor="#555555"
|
||||||
|
android:layout_marginTop="20dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- 连接网络按钮容器 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/llNetworkButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<!-- 网络错误图标 -->
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="140dp"
|
||||||
|
android:layout_height="140dp"
|
||||||
|
android:src="@drawable/ic_network_error"
|
||||||
|
android:contentDescription="网络连接失败"
|
||||||
|
android:layout_marginBottom="30dp" />
|
||||||
|
|
||||||
|
<!-- 连接失败提示 -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="网络连接失败"
|
||||||
|
android:textSize="36sp"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_marginBottom="40dp" />
|
||||||
|
|
||||||
|
<!-- 连接网络按钮 -->
|
||||||
|
<androidx.appcompat.widget.AppCompatButton
|
||||||
|
android:id="@+id/btnConnectNetwork"
|
||||||
|
android:layout_width="350dp"
|
||||||
|
android:layout_height="70dp"
|
||||||
|
android:text="连接网络"
|
||||||
|
android:textSize="24sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:background="@drawable/btn_connect_network" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="600dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/bg_white_radius12"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingHorizontal="40dp"
|
||||||
|
android:paddingVertical="36dp">
|
||||||
|
|
||||||
|
<!-- 标题 -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="环境切换"
|
||||||
|
android:textColor="#ff141428"
|
||||||
|
android:textSize="36sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<!-- 分割线 -->
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="2dp"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:background="#FFDCDCF0" />
|
||||||
|
|
||||||
|
<!-- 环境选项 -->
|
||||||
|
<RadioGroup
|
||||||
|
android:id="@+id/rgEnv"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="32dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/rbTest"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="72dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:paddingStart="16dp"
|
||||||
|
android:text="本地环境"
|
||||||
|
android:textColor="#ff141428"
|
||||||
|
android:textSize="28sp" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/rbUat"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="72dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:paddingStart="16dp"
|
||||||
|
android:text="测试环境"
|
||||||
|
android:textColor="#ff141428"
|
||||||
|
android:textSize="28sp" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/rbProd"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="72dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:paddingStart="16dp"
|
||||||
|
android:text="生产环境"
|
||||||
|
android:textColor="#ff141428"
|
||||||
|
android:textSize="28sp" />
|
||||||
|
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
|
<!-- 分割线 -->
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="2dp"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:background="#FFDCDCF0" />
|
||||||
|
|
||||||
|
<!-- 底部按钮 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="88dp"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatButton
|
||||||
|
android:id="@+id/btnCancel"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginEnd="20dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@drawable/bg_white_radius10_stroke2"
|
||||||
|
android:text="取消"
|
||||||
|
android:textColor="#ff141428"
|
||||||
|
android:textSize="28sp" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatButton
|
||||||
|
android:id="@+id/btnConfirm"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginStart="20dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@drawable/bg_blue_radius10"
|
||||||
|
android:text="确认"
|
||||||
|
android:textColor="#ffffff"
|
||||||
|
android:textSize="28sp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/rootLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="#CC000000">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/ivFullImage"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginBottom="120dp"
|
||||||
|
android:scaleType="fitCenter" />
|
||||||
|
|
||||||
|
<!-- 左侧翻页按钮 -->
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/btnPrev"
|
||||||
|
android:layout_width="80dp"
|
||||||
|
android:layout_height="80dp"
|
||||||
|
android:layout_gravity="center_vertical|start"
|
||||||
|
android:layout_marginBottom="120dp"
|
||||||
|
android:layout_marginStart="20dp"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:src="@mipmap/ic_back_white2"
|
||||||
|
android:background="#55000000" />
|
||||||
|
|
||||||
|
<!-- 右侧翻页按钮 -->
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/btnNext"
|
||||||
|
android:layout_width="80dp"
|
||||||
|
android:layout_height="80dp"
|
||||||
|
android:layout_gravity="center_vertical|end"
|
||||||
|
android:layout_marginBottom="120dp"
|
||||||
|
android:layout_marginEnd="20dp"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:src="@mipmap/ic_back_white2"
|
||||||
|
android:rotation="180"
|
||||||
|
android:background="#55000000" />
|
||||||
|
|
||||||
|
<!-- 底部工具栏:文件名 + 页码指示器 + 关闭按钮 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="120dp"
|
||||||
|
android:layout_gravity="bottom"
|
||||||
|
android:background="#99000000"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingHorizontal="30dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvFileName"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:ellipsize="middle"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textColor="#FFFFFFFF"
|
||||||
|
android:textSize="26sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvPageIndicator"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="30dp"
|
||||||
|
android:textColor="#CCFFFFFF"
|
||||||
|
android:textSize="26sp" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/btnClose"
|
||||||
|
android:layout_width="200dp"
|
||||||
|
android:layout_height="80dp"
|
||||||
|
android:background="@drawable/bg_blue_ripple">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:text="关闭"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="30sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="1500dp"
|
||||||
|
android:layout_height="900dp"
|
||||||
|
android:background="@drawable/bg_white_radius12"
|
||||||
|
tools:ignore="HardcodedText">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/ivClose"
|
||||||
|
android:layout_width="80dp"
|
||||||
|
android:layout_height="80dp"
|
||||||
|
android:layout_marginTop="30dp"
|
||||||
|
android:layout_marginEnd="30dp"
|
||||||
|
android:padding="10dp"
|
||||||
|
android:src="@mipmap/ic_close"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvDialogTitle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="物品检索"
|
||||||
|
android:textColor="#ff141428"
|
||||||
|
android:textSize="36sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/flSearchBlock"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/flSearchBlock"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="80dp"
|
||||||
|
android:layout_marginHorizontal="150dp"
|
||||||
|
android:layout_marginTop="180dp"
|
||||||
|
android:background="@drawable/bg_white_radius10_stroke2"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/etInputGoods"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="#00000000"
|
||||||
|
android:gravity="center"
|
||||||
|
android:hint="输入溯源码"
|
||||||
|
android:imeOptions="actionSearch"
|
||||||
|
android:inputType="text"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:paddingStart="10dp"
|
||||||
|
android:paddingEnd="80dp"
|
||||||
|
android:textColor="#FF666666"
|
||||||
|
android:textColorHint="#FFC7C8DC"
|
||||||
|
android:textSize="30sp" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/ivGoodsSearch"
|
||||||
|
android:layout_width="60dp"
|
||||||
|
android:layout_height="60dp"
|
||||||
|
android:layout_gravity="end|center_vertical"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:padding="14dp"
|
||||||
|
android:src="@mipmap/ic_goods_search"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<com.scwang.smart.refresh.layout.SmartRefreshLayout
|
||||||
|
android:id="@+id/refreshLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_marginHorizontal="130dp"
|
||||||
|
android:layout_marginTop="40dp"
|
||||||
|
android:layout_marginBottom="58dp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/dividerLine"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/flSearchBlock">
|
||||||
|
|
||||||
|
<com.scwang.smart.refresh.header.ClassicsHeader
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/rvSearch"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:overScrollMode="never"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||||
|
app:spanCount="2"
|
||||||
|
tools:itemCount="8"
|
||||||
|
tools:listitem="@layout/list_item_goods_search" />
|
||||||
|
|
||||||
|
<com.scwang.smart.refresh.footer.ClassicsFooter
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
</com.scwang.smart.refresh.layout.SmartRefreshLayout>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/dividerLine"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_marginHorizontal="40dp"
|
||||||
|
android:layout_marginBottom="160dp"
|
||||||
|
android:background="#FFDCDCF0"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatButton
|
||||||
|
android:id="@+id/btnAddGoods"
|
||||||
|
android:layout_width="240dp"
|
||||||
|
android:layout_height="80dp"
|
||||||
|
android:background="@drawable/bg_white_stroke_blue_ripple"
|
||||||
|
android:text="新增入库"
|
||||||
|
android:textColor="#FF0033CC"
|
||||||
|
android:textSize="30sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:visibility="visible"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/btnConfirm"
|
||||||
|
app:layout_constraintHorizontal_chainStyle="packed"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/dividerLine" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatButton
|
||||||
|
android:id="@+id/btnConfirm"
|
||||||
|
android:layout_width="240dp"
|
||||||
|
android:layout_height="80dp"
|
||||||
|
android:background="@drawable/bg_blue_ripple"
|
||||||
|
android:text="确定"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="30sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_marginStart="100dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/btnAddGoods"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/dividerLine" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="1500dp"
|
||||||
|
android:layout_height="900dp"
|
||||||
|
android:background="@drawable/bg_white_radius12"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:orientation="vertical"
|
||||||
|
tools:ignore="HardcodedText">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvTitle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="14"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="物品入库"
|
||||||
|
android:textColor="#ff141428"
|
||||||
|
android:textSize="36sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="2dp"
|
||||||
|
android:layout_marginHorizontal="40dp"
|
||||||
|
android:background="#FFDCDCF0" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="70">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Guideline
|
||||||
|
android:id="@+id/guideline"
|
||||||
|
android:layout_width="1dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:layout_constraintGuide_percent="0.70" />
|
||||||
|
|
||||||
|
<!-- 左侧:FlexboxLayoutManager 驱动的动态表单,两列自动换行 -->
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/rvFormFields"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:descendantFocusability="afterDescendants"
|
||||||
|
android:paddingHorizontal="10dp"
|
||||||
|
android:paddingVertical="10dp"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/guideline"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
tools:listitem="@layout/list_item_form_field" />
|
||||||
|
|
||||||
|
<!-- 右侧:相机预览 + 秤重显示 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
android:layout_marginHorizontal="20dp"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/guideline">
|
||||||
|
|
||||||
|
<!-- 相机预览容器,由代码动态 inflate LayoutTakePhotoBinding -->
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/flContainer"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:layout_marginBottom="15dp" />
|
||||||
|
|
||||||
|
<!-- 秤重显示条 -->
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="70dp"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:background="@drawable/bg_gray_radius10">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="120dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_gravity="start"
|
||||||
|
android:background="@drawable/bg_white_radius10_stroke2"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="累计"
|
||||||
|
android:textColor="#FF141428"
|
||||||
|
android:textSize="24sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvTotalWeight"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginHorizontal="120dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textColor="#FF141428"
|
||||||
|
android:textSize="24sp"
|
||||||
|
tools:text="6300 克" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/btnClearZero"
|
||||||
|
android:layout_width="120dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_gravity="end"
|
||||||
|
android:background="@drawable/bg_white_radius10_stroke2"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="清零"
|
||||||
|
android:textColor="#FF141428"
|
||||||
|
android:textSize="24sp" />
|
||||||
|
</FrameLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="2dp"
|
||||||
|
android:layout_marginHorizontal="40dp"
|
||||||
|
android:background="#FFDCDCF0" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="16"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingHorizontal="50dp">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatButton
|
||||||
|
android:id="@+id/btnCancel"
|
||||||
|
android:layout_width="240dp"
|
||||||
|
android:layout_height="80dp"
|
||||||
|
android:background="@drawable/bg_white_stroke_blue_ripple"
|
||||||
|
android:text="取消"
|
||||||
|
android:textColor="#FF0033CC"
|
||||||
|
android:textSize="30sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatButton
|
||||||
|
android:id="@+id/btnConfirm"
|
||||||
|
android:layout_width="240dp"
|
||||||
|
android:layout_height="80dp"
|
||||||
|
android:layout_marginStart="30dp"
|
||||||
|
android:background="@drawable/bg_blue_ripple"
|
||||||
|
android:text="确定"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="30sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -7,8 +7,10 @@
|
|||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<!-- android:layout_width="488dp"-->
|
||||||
|
<!-- android:layout_height="325dp"-->
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:layout_width="488dp"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="325dp"
|
android:layout_height="325dp"
|
||||||
android:background="@drawable/bg_white_radius10_stroke2_dash">
|
android:background="@drawable/bg_white_radius10_stroke2_dash">
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="82dp"
|
android:layout_height="100dp"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_marginVertical="10dp"
|
android:layout_marginVertical="10dp"
|
||||||
android:layout_marginHorizontal="20dp"
|
android:layout_marginHorizontal="20dp"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
@@ -11,23 +12,41 @@
|
|||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tvGoodsName"
|
android:id="@+id/tvGoodsName"
|
||||||
android:layout_width="0dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_weight="1"
|
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
|
||||||
tools:text="高杆白菜"
|
tools:text="高杆白菜"
|
||||||
android:textColor="#ff666666"
|
android:textColor="#ff333333"
|
||||||
android:textSize="30sp"
|
android:textSize="30sp"
|
||||||
|
app:layout_constraintVertical_chainStyle="packed"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/tvGoodsNum"
|
||||||
android:layout_marginStart="30dp"
|
android:layout_marginStart="30dp"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:layout_marginHorizontal="10dp"
|
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvGoodsNum"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
tools:text="已采集100"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/tvGoodsName"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/tvGoodsName"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
android:textColor="#ff666666"
|
||||||
|
android:layout_marginTop="5dp"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/ivDelete"
|
android:id="@+id/ivDelete"
|
||||||
android:layout_width="80dp"
|
android:layout_width="80dp"
|
||||||
android:layout_height="80dp"
|
android:layout_height="80dp"
|
||||||
android:padding="25dp"
|
android:padding="25dp"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
android:src="@mipmap/ic_close"/>
|
android:src="@mipmap/ic_close"/>
|
||||||
</LinearLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingVertical="10dp">
|
||||||
|
|
||||||
|
<!-- 字段标签,必填项红色星号由 Adapter 通过 SpannableString 追加 -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvLabel"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="12dp"
|
||||||
|
android:layout_marginBottom="6dp"
|
||||||
|
android:textColor="#FF141428"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
tools:text="食材名称" />
|
||||||
|
|
||||||
|
<!-- 输入区域容器,三种控件叠放,按 FieldType 切换 visibility -->
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="60dp"
|
||||||
|
android:layout_marginHorizontal="12dp">
|
||||||
|
|
||||||
|
<!-- TEXT / NUMBER / FIXED 类型:EditText -->
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/etInput"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@drawable/bg_white_radius10_stroke2"
|
||||||
|
android:focusableInTouchMode="true"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:paddingHorizontal="20dp"
|
||||||
|
android:textColor="#FF141428"
|
||||||
|
android:textColorHint="#FF96A0AA"
|
||||||
|
android:textSize="22sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:ignore="Autofill,TextFields" />
|
||||||
|
|
||||||
|
<!-- DROPDOWN 类型:点击触发 DropdownPopup -->
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/flDropdown"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@drawable/bg_white_radius10_stroke2"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvDropdownValue"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginEnd="44dp"
|
||||||
|
android:layout_marginStart="20dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textColor="#FF141428"
|
||||||
|
android:textColorHint="#FF96A0AA"
|
||||||
|
android:textSize="22sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="end|center_vertical"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:src="@mipmap/ic_triangle_down"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<!-- DATE_PICKER 类型:点击触发 DatePickerDialog -->
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/flDatePicker"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@drawable/bg_white_radius10_stroke2"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvDateValue"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginEnd="44dp"
|
||||||
|
android:layout_marginStart="20dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:textColor="#FF141428"
|
||||||
|
android:textColorHint="#FF96A0AA"
|
||||||
|
android:textSize="22sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="end|center_vertical"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:src="@mipmap/ic_triangle_down"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
android:layout_height="82dp"
|
android:layout_height="82dp"
|
||||||
android:layout_marginVertical="10dp"
|
android:layout_marginVertical="10dp"
|
||||||
android:layout_marginHorizontal="20dp"
|
android:layout_marginHorizontal="20dp"
|
||||||
tools:background="@drawable/bg_white_stroke_blue_ripple">
|
android:background="@drawable/bg_gray_radius10">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tvGoodsName"
|
android:id="@+id/tvGoodsName"
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">入库客户端</string>
|
<string name="app_name">入库新</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<style name="Theme.Inbound" parent="android:Theme.Material.Light.NoActionBar">
|
<style name="Theme.Inbound" parent="Theme.MaterialComponents.Light.NoActionBar">
|
||||||
<!-- <item name="android:windowBackground">@drawable/bg_splash</item>-->
|
<!-- <item name="android:windowBackground">@drawable/bg_splash</item>-->
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
@@ -41,4 +41,5 @@
|
|||||||
<item name="windowNoTitle">true</item>
|
<item name="windowNoTitle">true</item>
|
||||||
<item name="windowActionBar">false</item>
|
<item name="windowActionBar">false</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
+3
-3
@@ -28,9 +28,9 @@ buildscript {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
plugins {
|
//plugins {
|
||||||
alias(libs.plugins.kotlin.compose) apply false
|
// alias(libs.plugins.kotlin.compose) apply false
|
||||||
}
|
//}
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
|
|||||||
+487
@@ -0,0 +1,487 @@
|
|||||||
|
# 入库系统接口文档
|
||||||
|
|
||||||
|
> 生成时间:2026-05-26
|
||||||
|
> 项目:Inbound20260114
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、通用说明
|
||||||
|
|
||||||
|
### 1.1 服务器环境
|
||||||
|
|
||||||
|
| 环境 | 地址 |
|
||||||
|
|------|------|
|
||||||
|
| 本地环境 | `http://192.168.1.201:14801` |
|
||||||
|
| 测试环境 | `https://dev.yixiong-tech.com:8083` |
|
||||||
|
| 生产环境(PROD) | `https://api.dm.yixiong-tech.com:8443` |
|
||||||
|
|
||||||
|
> 代码中使用 `GlobalData.appBaseUrl` 动态切换,启动时由设备配置决定当前所用环境。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.2 通用请求头
|
||||||
|
|
||||||
|
以下请求头由拦截器(`RequestInterceptor`)统一注入,**无需在每个接口中单独传递**:
|
||||||
|
|
||||||
|
| Header 字段 | 说明 | 示例值 |
|
||||||
|
|---|---|---|
|
||||||
|
| `Content-Type` | 请求体格式 | `application/json` |
|
||||||
|
| `Accept` | 响应体格式 | `application/json` |
|
||||||
|
| `deviceId` | 设备唯一标识符(UUID) | `bcf396ed-78f6-3864-...` |
|
||||||
|
| `X-DEVICE-CODE` | 设备编码(同 deviceId) | `bcf396ed-78f6-3864-...` |
|
||||||
|
| `x-access-token` | 用户登录 Token(登录后获取) | `{token}` |
|
||||||
|
| `authorization` | 固定鉴权值 | `57ee87183f2a4fa59683ec9ef41c8f5d` |
|
||||||
|
|
||||||
|
> **特殊说明**:`getDeviceInfo` 接口额外通过 `@Header("X-Access-Token")` 传入设备 Token(设备初始化阶段使用,区别于用户登录 Token)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.3 通用响应格式
|
||||||
|
|
||||||
|
所有接口返回结构统一如下:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "00000",
|
||||||
|
"msg": "success",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `code` | String | 业务状态码,`00000` 表示成功 |
|
||||||
|
| `msg` | String | 提示信息 |
|
||||||
|
| `data` | T / null | 业务数据,各接口不同 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.4 接口路径前缀
|
||||||
|
|
||||||
|
业务接口通用前缀(简称 `{prefix}`):
|
||||||
|
|
||||||
|
```
|
||||||
|
/terminal/neglect/inbound
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、接口列表
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.1 获取存储方式列表
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 请求方式 | `GET` |
|
||||||
|
| 接口路径 | `{baseUrl}{prefix}/pad/getDictItemByF` |
|
||||||
|
|
||||||
|
**Query 参数:**
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| `dictCode` | String | 否 | 字典编码,固定值 `ccfs`(存储方式) |
|
||||||
|
|
||||||
|
**响应 data(`List<StorageType>`):**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `id` | String | 存储方式 ID |
|
||||||
|
| `value` | String | 存储方式值 |
|
||||||
|
| `text` | String | 展示文本 |
|
||||||
|
| `title` | String | 标题 |
|
||||||
|
| `label` | String | 标签 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.2 获取物品类型列表
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 请求方式 | `GET` |
|
||||||
|
| 接口路径 | `{baseUrl}{prefix}/pad/notLimitList` |
|
||||||
|
|
||||||
|
**Query 参数:** 无
|
||||||
|
|
||||||
|
**响应 data(`GoodsType`):**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `allType` | List\<AllType\> | 物品类型列表 |
|
||||||
|
|
||||||
|
**AllType 结构:**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `id` | String | 类型 ID |
|
||||||
|
| `typeName` | String | 类型名称 |
|
||||||
|
| `typeCode` | String | 类型编码 |
|
||||||
|
| `ftype` | String | 父类型 |
|
||||||
|
| `superName` | String | 父类型名称 |
|
||||||
|
| `status` | Boolean | 是否启用 |
|
||||||
|
| `eaId` | Int | 企业 ID |
|
||||||
|
| `kidTypes` | List\<String\> | 子类型列表 |
|
||||||
|
| `createTime` | String | 创建时间 |
|
||||||
|
| `updateTime` | String | 更新时间 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.3 获取字典数据列表
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 请求方式 | `GET` |
|
||||||
|
| 接口路径 | `{baseUrl}{prefix}/pad/dataList` |
|
||||||
|
|
||||||
|
**Query 参数:**
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| `type` | String | 是 | 字典类型,如仓库列表、单位类型等 |
|
||||||
|
|
||||||
|
**响应 data(`List<DictType>`):**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `id` | String | 字典项 ID |
|
||||||
|
| `value` | String | 字典项名称/值 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.4 采购单入库列表
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 请求方式 | `POST` |
|
||||||
|
| 接口路径 | `{baseUrl}{prefix}/pad/receivePage` |
|
||||||
|
| Content-Type | `application/json` |
|
||||||
|
|
||||||
|
**请求体(JSON,`Map<String, String>`):**
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| `pageNo` | String | 是 | 页码,从 1 开始 |
|
||||||
|
| `pageSize` | String | 是 | 每页条数 |
|
||||||
|
| *(其他筛选条件)* | String | 否 | 如供应商ID、日期范围等 |
|
||||||
|
|
||||||
|
**响应 data(`List<SupplierInfo>`):**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `id` | String | 收货单 ID |
|
||||||
|
| `purCode` | String | 采购单号 |
|
||||||
|
| `receiveCode` | String | 收货单号 |
|
||||||
|
| `supplierId` | String | 供应商 ID |
|
||||||
|
| `supplierName` | String | 供应商名称 |
|
||||||
|
| `purchaseDate` | String | 采购日期 |
|
||||||
|
| `receiveDate` | String | 收货时间 |
|
||||||
|
| `receiveStatus` | Int | 收货状态:1-收货关闭 / 2-收货完成 / 3-待收货 / 4-部分收货 |
|
||||||
|
| `receiveUser` | String | 操作人 |
|
||||||
|
| `goodCount` | Int | 物品项数 |
|
||||||
|
| `receiveCountAll` | Double | 总收货数量 |
|
||||||
|
| `receivePriceEx` | Double | 收货金额(不含税) |
|
||||||
|
| `receivePriceIn` | Double | 收货金额(含税) |
|
||||||
|
| `taxRateAll` | Double | 税率合计 |
|
||||||
|
| `giftAll` | Double | 赠品合计 |
|
||||||
|
| `warehouseId` | String | 仓库 ID |
|
||||||
|
| `eaId` | Int | 企业 ID |
|
||||||
|
| `createTime` | String | 创建时间 |
|
||||||
|
| `updateTime` | String | 更新时间 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.5 采购单入库详情
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 请求方式 | `GET` |
|
||||||
|
| 接口路径 | `{baseUrl}{prefix}/pad/receiveDetail` |
|
||||||
|
|
||||||
|
**Query 参数:**
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| `id` | String | 是 | 收货单 ID |
|
||||||
|
|
||||||
|
**响应 data(`PurchaseInfo`):**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `id` | String | 收货单 ID |
|
||||||
|
| `supplierId` | String | 供应商 ID |
|
||||||
|
| `supplierName` | String | 供应商名称 |
|
||||||
|
| `purCode` | String | 采购单号 |
|
||||||
|
| `receiveCode` | String | 收货单号 |
|
||||||
|
| `receiveGoodsInfoList` | List\<GoodsInfo\> | 物品明细列表(见 **GoodsInfo 结构**) |
|
||||||
|
|
||||||
|
**GoodsInfo 结构:**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `id` | String | 明细 ID |
|
||||||
|
| `goodId` | String | 物品 ID |
|
||||||
|
| `goodName` | String | 物品名称 |
|
||||||
|
| `unitName` | String | 采购单位名称 |
|
||||||
|
| `purchaseUnitId` | String | 采购单位 ID |
|
||||||
|
| `goodsPurId` | String | 物品采购单位关系 ID |
|
||||||
|
| `receiveCount` | Double | 应收数量 |
|
||||||
|
| `receivedNum` | Double | 实收数量 |
|
||||||
|
| `receiveCountAll` | Double | 总收货数量 |
|
||||||
|
| `recUnitPriceTaxIn` | Double | 含税单价 |
|
||||||
|
| `newRecUnitPriceTaxIn` | Double | 调整后单价(部分收货使用) |
|
||||||
|
| `recPriceExItem` | Double | 不含税金额 |
|
||||||
|
| `recPriceInItem` | Double | 含税金额 |
|
||||||
|
| `warehouseId` | String | 仓库 ID |
|
||||||
|
| `warehouseName` | String | 仓库名称 |
|
||||||
|
| `kcUnitId` | String | 库存单位 ID |
|
||||||
|
| `kcUnitName` | String | 库存单位名称 |
|
||||||
|
| `purchaseValue` | Int | 采购库存转换值 |
|
||||||
|
| `consumeValue` | String | 库存与克的转换值 |
|
||||||
|
| `goodsWeight` | BigDecimal | 物品重量(千克) |
|
||||||
|
| `goodsCount` | Double | 物品数量 |
|
||||||
|
| `goodsUnitPrice` | Double | 物品单价 |
|
||||||
|
| `goodsPrice` | Double | 物品总价 |
|
||||||
|
| `buyToInventoryValue` | String | 采购转库存换算值 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.6 上传单张图片
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 请求方式 | `POST` |
|
||||||
|
| 接口路径 | `{baseUrl}/terminal/neglect/upload` |
|
||||||
|
| Content-Type | `multipart/form-data` |
|
||||||
|
|
||||||
|
**Form-Data 参数:**
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| `file` | File(MultipartBody.Part) | 是 | 图片文件 |
|
||||||
|
|
||||||
|
**响应 data:**
|
||||||
|
|
||||||
|
```
|
||||||
|
String // 图片访问路径(相对 URL)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.7 上传多张图片
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 请求方式 | `POST` |
|
||||||
|
| 接口路径 | `{baseUrl}/terminal/neglect/inbound/pad/inboundWeighingScale/add` |
|
||||||
|
| Content-Type | `multipart/form-data` |
|
||||||
|
|
||||||
|
**Form-Data 参数:**
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| `goodsId` | String(RequestBody) | 是 | 物品 ID |
|
||||||
|
| `goodsName` | String(RequestBody) | 是 | 物品名称 |
|
||||||
|
| `goodsPics` | List\<MultipartBody.Part\> | 是 | 多张图片文件列表,字段名固定为 `goodsPics` |
|
||||||
|
|
||||||
|
**响应 data:**
|
||||||
|
|
||||||
|
```
|
||||||
|
String // 操作结果
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.8 搜索物品列表
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 请求方式 | `POST` |
|
||||||
|
| 接口路径 | `{baseUrl}{prefix}/pad/goodsInfoList` |
|
||||||
|
| Content-Type | `application/json` |
|
||||||
|
|
||||||
|
**请求体(JSON,`Map<String, String>`):**
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| `goodsName` | String | 否 | 物品名称关键词 |
|
||||||
|
| `pageNo` | String | 是 | 页码 |
|
||||||
|
| `pageSize` | String | 是 | 每页条数 |
|
||||||
|
| *(其他筛选条件)* | String | 否 | 按需传入 |
|
||||||
|
|
||||||
|
**响应 data(`SearchGoodsInfo`,分页结构):**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `current` | Int | 当前页码 |
|
||||||
|
| `total` | Int | 总记录数 |
|
||||||
|
| `size` | Int | 每页条数 |
|
||||||
|
| `pages` | Int | 总页数 |
|
||||||
|
| `records` | List\<Record\> | 物品记录列表(见 **Record 结构**) |
|
||||||
|
|
||||||
|
**Record 结构:**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `id` | String | 物品 ID |
|
||||||
|
| `goodsCode` | String | 物品编码 |
|
||||||
|
| `goodsName` | String | 物品名称 |
|
||||||
|
| `zjmCode` | String | 助记码 |
|
||||||
|
| `specification` | String | 规格 |
|
||||||
|
| `typeId` | Int | 物品类型 ID |
|
||||||
|
| `typeName` | String | 物品类型名称 |
|
||||||
|
| `kcUnitId` | Int | 库存单位 ID |
|
||||||
|
| `kcUnitName` | String | 库存单位名称 |
|
||||||
|
| `consumeValue` | String | 库存与克的转换值 |
|
||||||
|
| `purchaseValue` | String | 采购库存转换值 |
|
||||||
|
| `unit` | String | 单位 |
|
||||||
|
| `relativeUrl` | String | 物品图片相对路径 |
|
||||||
|
| `unitVoList` | List\<UnitVo\> | 可选采购单位列表(见 **UnitVo 结构**) |
|
||||||
|
|
||||||
|
**UnitVo 结构:**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `businessUnitId` | String | 单位关系 ID |
|
||||||
|
| `purchaseUnitName` | String | 采购单位名称 |
|
||||||
|
| `buyToInventoryValue` | String | 采购转库存换算值 |
|
||||||
|
| `consumeValue` | String | 消耗转换值 |
|
||||||
|
| `purchaseValue` | Double | 购买单位对应库存数量 |
|
||||||
|
| `isDefault` | Int | 是否默认单位(1-是) |
|
||||||
|
| `value` | String | 单位展示值 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.9 图像识别物品列表查询
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 请求方式 | `POST` |
|
||||||
|
| 接口路径 | `{baseUrl}{prefix}/pad/goodsList` |
|
||||||
|
| Content-Type | `application/json` |
|
||||||
|
|
||||||
|
> 用于图片识别后,根据识别到的物品名称列表批量查询物品信息。
|
||||||
|
|
||||||
|
**请求体(`RecGoodsName`):**
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| `nameList` | List\<String\> | 是 | 识别到的物品名称列表 |
|
||||||
|
|
||||||
|
**响应 data:**
|
||||||
|
|
||||||
|
```
|
||||||
|
List<SearchGoodsInfo.Record> // 物品记录列表(字段同 2.8 的 Record 结构)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.10 自采添加新物品
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 请求方式 | `POST` |
|
||||||
|
| 接口路径 | `{baseUrl}{prefix}/pad/goodsAdd` |
|
||||||
|
| Content-Type | `application/json` |
|
||||||
|
|
||||||
|
**请求体(`GoodsAddParam`):**
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| `goodName` | String | 是 | 物品名称 |
|
||||||
|
| `goodCode` | String | 否 | 物品编号(服务端自动生成) |
|
||||||
|
| `goodType` | String | 是 | 物品类型 ID |
|
||||||
|
| `storageType` | String | 是 | 存储方式 ID |
|
||||||
|
| `netRate` | Float | 是 | 净材率(0~1 之间) |
|
||||||
|
| `unitId` | String | 是 | 库存单位 ID |
|
||||||
|
| `purchaseUnit` | String | 是 | 采购单位 ID |
|
||||||
|
| `purchaseValue` | Float | 是 | 采购单位对应库存数量(转换值) |
|
||||||
|
| `purchasePrice` | BigDecimal | 是 | 采购单价 |
|
||||||
|
| `relativeUrl` | String | 否 | 物品图片相对路径 |
|
||||||
|
| `consumeValue` | String | 是 | 库存单位对应消耗值(库存与克的转换值) |
|
||||||
|
|
||||||
|
**响应 data:**
|
||||||
|
|
||||||
|
```
|
||||||
|
String // 新增物品 ID 或操作结果
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.11 确认收货(全部收货)
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 请求方式 | `POST` |
|
||||||
|
| 接口路径 | `{baseUrl}{prefix}/pad/saveAndReceiveAndGoWare` |
|
||||||
|
| Content-Type | `application/json` |
|
||||||
|
|
||||||
|
**请求体(`UploadInfo`):**
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| `id` | String | 是 | 收货单 ID |
|
||||||
|
| `supplierId` | String | 否 | 供应商 ID |
|
||||||
|
| `purCode` | String | 否 | 采购单号 |
|
||||||
|
| `receiveGoodsInfos` | List\<GoodsInfo\> | 是 | 全部收货物品列表(字段同 GoodsInfo) |
|
||||||
|
| `receiveGoodsInfoList` | List\<GoodsInfo\> | 否 | 部分收货列表(全量收货时传空列表) |
|
||||||
|
|
||||||
|
**响应 data:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Boolean // true 表示操作成功
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.12 自采入库
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 请求方式 | `POST` |
|
||||||
|
| 接口路径 | `{baseUrl}{prefix}/pad/selfPurchasedGoods` |
|
||||||
|
| Content-Type | `application/json` |
|
||||||
|
|
||||||
|
**请求体(`List<PurchaseWarehouseParam>`):**
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| `goodsId` | String | 是 | 物品 ID |
|
||||||
|
| `kcUnitId` | String | 否 | 库存单位 ID |
|
||||||
|
| `goodsCount` | Double | 是 | 入库数量 |
|
||||||
|
| `goodsUnitPrice` | Double | 是 | 入库单价 |
|
||||||
|
| `goodsPrice` | Double | 是 | 入库金额 |
|
||||||
|
| `goodPurId` | String | 是 | 物品采购单位关系 ID(`businessUnitId`) |
|
||||||
|
| `warehouseId` | String | 是 | 仓库 ID |
|
||||||
|
| `buyToInventoryValue` | String | 否 | 采购转库存换算值 |
|
||||||
|
| `goodsWeight` | BigDecimal | 是 | 物品重量(千克) |
|
||||||
|
| `consumeValue` | String | 否 | 库存单位对应消耗值 |
|
||||||
|
|
||||||
|
> 注:`goodsName`、`unitName`、`unitList`、`selectUnitType` 等字段为本地 UI 展示用,**不上传至服务端**。
|
||||||
|
|
||||||
|
**响应 data:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Boolean // true 表示入库成功
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、接口速查表
|
||||||
|
|
||||||
|
| # | 接口名称 | 方法 | 路径 |
|
||||||
|
|---|----------|------|------|
|
||||||
|
| 2.1 | 获取存储方式列表 | GET | `{prefix}/pad/getDictItemByF` |
|
||||||
|
| 2.2 | 获取物品类型列表 | GET | `{prefix}/pad/notLimitList` |
|
||||||
|
| 2.3 | 获取字典数据列表 | GET | `{prefix}/pad/dataList` |
|
||||||
|
| 2.4 | 采购单入库列表 | POST | `{prefix}/pad/receivePage` |
|
||||||
|
| 2.5 | 采购单入库详情 | GET | `{prefix}/pad/receiveDetail` |
|
||||||
|
| 2.6 | 上传单张图片 | POST | `/terminal/neglect/upload` |
|
||||||
|
| 2.7 | 上传多张图片 | POST | `/terminal/neglect/inbound/pad/inboundWeighingScale/add` |
|
||||||
|
| 2.8 | 搜索物品列表 | POST | `{prefix}/pad/goodsInfoList` |
|
||||||
|
| 2.9 | 图像识别物品查询 | POST | `{prefix}/pad/goodsList` |
|
||||||
|
| 2.10 | 自采添加新物品 | POST | `{prefix}/pad/goodsAdd` |
|
||||||
|
| 2.11 | 确认收货(全部) | POST | `{prefix}/pad/saveAndReceiveAndGoWare` |
|
||||||
|
| 2.12 | 自采入库 | POST | `{prefix}/pad/selfPurchasedGoods` |
|
||||||
|
|
||||||
|
> `{prefix}` = `/terminal/neglect/inbound`
|
||||||
+289
@@ -0,0 +1,289 @@
|
|||||||
|
# 入库秤设备端接口文档
|
||||||
|
|
||||||
|
**Base URL**:`http://192.168.10.101:24801/nutrition/neglect/inbound`
|
||||||
|
**鉴权**:无需 Sa-Token(Nacos 白名单 `/nutrition/neglect/**`)
|
||||||
|
|
||||||
|
| Header 字段 | 说明 | 示例值 |
|
||||||
|
|---|---|---|
|
||||||
|
| `authorization` | 固定鉴权值 | `57ee87183f2a4fa59683ec9ef41c8f5d` |
|
||||||
|
**Content-Type**:`application/json`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 通用响应结构
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"data": { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| code | int | 200 成功,其他为业务错误码 |
|
||||||
|
| msg | string | 提示信息 |
|
||||||
|
| data | any | 响应数据,无数据时为 `null` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 接口列表
|
||||||
|
|
||||||
|
### 1. 按溯源码查食材
|
||||||
|
|
||||||
|
**GET** `/nutrition/neglect/inbound/ingredient-by-trace`
|
||||||
|
|
||||||
|
按溯源码反查命中的食材列表,返回食材 id、名称、净菜类型,用于毛菜/净菜入库时联动选择食材。
|
||||||
|
|
||||||
|
**Query 参数**
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| traceCode | string | ✅ | 溯源码 |
|
||||||
|
|
||||||
|
**响应示例**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"traceCode": "TC20240501001",
|
||||||
|
"materId": 1001,
|
||||||
|
"materName": "西红柿",
|
||||||
|
"vegTypes": "切丁,切片"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应字段说明**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| traceCode | string | 溯源码 |
|
||||||
|
| materId | long | 食材 id(关联 nut_mater_base.id) |
|
||||||
|
| materName | string | 食材名称 |
|
||||||
|
| vegTypes | string | 净菜类型,逗号分隔,如 `切丁,切片` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 仓库区域下拉列表
|
||||||
|
|
||||||
|
**GET** `/nutrition/neglect/inbound/zone-options`
|
||||||
|
|
||||||
|
获取仓库区域选项,用于入库时选择存放区域。
|
||||||
|
|
||||||
|
**Query 参数**
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| warehouseId | long | ❌ | 仓库 id,不传则返回所有区域 |
|
||||||
|
| keyword | string | ❌ | 关键字模糊搜索(区域编号/名称) |
|
||||||
|
|
||||||
|
**响应示例**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 101,
|
||||||
|
"zoneNo": "WH-A-01",
|
||||||
|
"zoneName": "A仓-蔬菜区-01",
|
||||||
|
"warehouseId": 10
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应字段说明**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| id | long | 区域主键 id |
|
||||||
|
| zoneNo | string | 区域编号,如 `WH-A-01` |
|
||||||
|
| zoneName | string | 区域名称,如 `A仓-蔬菜区-01` |
|
||||||
|
| warehouseId | long | 关联仓库 id |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. 供应商下拉列表
|
||||||
|
|
||||||
|
**GET** `/nutrition/neglect/inbound/supplier-options`
|
||||||
|
|
||||||
|
获取供应商选项,内部与外部供应商合并返回。
|
||||||
|
|
||||||
|
**Query 参数**
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| type | int | ❌ | 供应商类型:`1`-内部,`2`-外部,不传则返回全部 |
|
||||||
|
| keyword | string | ❌ | 关键字模糊搜索(供应商名称) |
|
||||||
|
|
||||||
|
**响应示例**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 201,
|
||||||
|
"name": "绿源农场",
|
||||||
|
"type": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 202,
|
||||||
|
"name": "鲜达食品有限公司",
|
||||||
|
"type": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应字段说明**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| id | long | 供应商 id |
|
||||||
|
| name | string | 供应商名称 |
|
||||||
|
| type | int | `1`-内部供应商,`2`-外部供应商 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. 毛菜入库提交
|
||||||
|
|
||||||
|
**POST** `/nutrition/neglect/inbound/raw-inbound`
|
||||||
|
|
||||||
|
入库秤设备提交毛菜入库记录,`opType` 由后端固定为 `1`(设备自动),无需传入。
|
||||||
|
|
||||||
|
**Request Body**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"traceCode": "TC20240501001",
|
||||||
|
"supplierType": 1,
|
||||||
|
"materialName": "西红柿",
|
||||||
|
"supplierId": 201,
|
||||||
|
"spec": "25kg/件",
|
||||||
|
"quantity": 2,
|
||||||
|
"weight": 49.8,
|
||||||
|
"zoneId": 101,
|
||||||
|
"operator": "张三",
|
||||||
|
"storageArea": "A仓-蔬菜区-01",
|
||||||
|
"reportNo": "RPT-2024-001",
|
||||||
|
"cameraCode": "CAM-001",
|
||||||
|
"videoUrl": "http://example.com/video/001.mp4",
|
||||||
|
"remark": ""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**字段说明**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| traceCode | string | ✅ | 溯源码 |
|
||||||
|
| supplierType | short | ✅ | 供应商类型:`1`-内部,`2`-外部 |
|
||||||
|
| materialName | string | ✅ | 食材名称 |
|
||||||
|
| supplierId | long | ✅ | 供应商 id |
|
||||||
|
| spec | string | ✅ | 入库规格,如 `25kg/件`、`散装(kg)` |
|
||||||
|
| quantity | int | ✅ | 入库数量 |
|
||||||
|
| weight | decimal | ✅ | 入库重量(kg) |
|
||||||
|
| zoneId | long | ✅ | 存放区域 id |
|
||||||
|
| operator | string | ✅ | 操作人姓名 |
|
||||||
|
| storageArea | string | ❌ | 存放区域名称(冗余字段,可与 zoneId 对应名称一致) |
|
||||||
|
| reportNo | string | ❌ | 卫生检测报告编号 |
|
||||||
|
| cameraCode | string | ❌ | 摄像头编号 |
|
||||||
|
| videoUrl | string | ❌ | 监控视频地址 |
|
||||||
|
| remark | string | ❌ | 备注 |
|
||||||
|
|
||||||
|
**响应示例**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "保存成功",
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. 净菜入库提交
|
||||||
|
|
||||||
|
**POST** `/nutrition/neglect/inbound/clean-inbound`
|
||||||
|
|
||||||
|
入库秤设备提交净菜入库记录,`opType` 由后端固定为 `1`(设备自动),无需传入。
|
||||||
|
|
||||||
|
**Request Body**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"traceCode": "TC20240501002",
|
||||||
|
"ingredientName": "西红柿",
|
||||||
|
"cleanType": "切丁",
|
||||||
|
"spec": "袋装/500g",
|
||||||
|
"quantity": 10,
|
||||||
|
"weight": 5.0,
|
||||||
|
"inboundTime": "2024-05-01T08:30:00",
|
||||||
|
"zoneId": 102,
|
||||||
|
"operator": "李四",
|
||||||
|
"expiryDate": "2024-05-03T00:00:00",
|
||||||
|
"storageTemp": 4.5,
|
||||||
|
"cameraCode": "CAM-002",
|
||||||
|
"videoUrl": "http://example.com/video/002.mp4",
|
||||||
|
"remark": ""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**字段说明**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| traceCode | string | ✅ | 溯源码 |
|
||||||
|
| ingredientName | string | ✅ | 食材名称 |
|
||||||
|
| cleanType | string | ✅ | 净菜类型,如 `切丁`、`切片`(参考接口1返回的 `vegTypes`) |
|
||||||
|
| spec | string | ✅ | 入库规格,如 `袋装/500g`、`盒装/300g` |
|
||||||
|
| quantity | int | ✅ | 入库数量(包/盒/份) |
|
||||||
|
| weight | decimal | ✅ | 入库重量(kg) |
|
||||||
|
| inboundTime | datetime | ✅ | 入库时间,格式:`yyyy-MM-dd'T'HH:mm:ss` |
|
||||||
|
| zoneId | long | ✅ | 仓库区域 id |
|
||||||
|
| operator | string | ❌ | 操作人姓名 |
|
||||||
|
| expiryDate | datetime | ❌ | 保质期至,格式同 `inboundTime` |
|
||||||
|
| storageTemp | decimal | ❌ | 存放温度(℃) |
|
||||||
|
| cameraCode | string | ❌ | 摄像头编号 |
|
||||||
|
| videoUrl | string | ❌ | 监控视频地址 |
|
||||||
|
| remark | string | ❌ | 备注 |
|
||||||
|
|
||||||
|
**响应示例**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "保存成功",
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 典型业务流程
|
||||||
|
|
||||||
|
```
|
||||||
|
设备扫溯源码
|
||||||
|
↓
|
||||||
|
GET /ingredient-by-trace?traceCode=xxx → 获取食材列表
|
||||||
|
↓
|
||||||
|
GET /zone-options → 获取区域列表
|
||||||
|
GET /supplier-options → 获取供应商列表(毛菜需要)
|
||||||
|
↓
|
||||||
|
用户确认信息 / 秤自动称重
|
||||||
|
↓
|
||||||
|
POST /raw-inbound (毛菜)
|
||||||
|
或
|
||||||
|
POST /clean-inbound(净菜)
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user