Author SHA1 Message Date
lvmengandClaude Sonnet 4.6 9ae0d709f2 fix(scale): 修复子设备IP收到数据后仍显示"未知"的问题
setDeviceIp 新增缓存回填逻辑:若设备已有缓存数据且 ip 为空,
立即用新 IP 更新所有相关条目并触发 StateFlow,
解决早期数据包在 IP 到达前入缓存导致 UI 无法更新的时序问题

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 18:19:07 +08:00
lvmengandClaude Sonnet 4.6 34adc0fe56 fix(scale): 修复主从设备IP显示异常及RecyclerView滑动崩溃
- 新增 NetworkUtil 统一获取本机IP,兼容 Android 12+(ConnectivityManager)和低版本(WifiManager)
- 主设备IP改为实时读取,修复App启动时网络未就绪导致IP缓存为空的问题
- MasterScaleActivity/SlaveActivity 关闭RecyclerView默认ItemAnimator,消除高频刷新时的闪烁
- BaseActivity.hideSystemBars 加防重入标志,避免重复调用setDecorFitsSystemWindows触发布局重绘导致滑动崩溃
- 新增 CrashHandler,崩溃时记录日志并自动重启应用

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 18:09:41 +08:00
lvmengandClaude Sonnet 4.6 29228b069b feat(scale): 新增主从设备秤数据联网同步功能
- 新增 ScaleWebSocketServer/Client、ScaleDataAggregator、ScaleServiceManager 等秤服务模块
- 新增 MasterScaleActivity(主设备秤数据总览,按设备分组展示IP+秤列表)
- 新增 SlaveActivity(子设备专属页面,显示IP、主设备连接状态及本机秤数据)
- SlaveActivity 改为横屏,MasterScaleActivity 保持竖屏
- 子设备连接状态通过 WebSocket onOpen/onClose 回调实时更新
- GlobalData 新增 DeviceRole 枚举,BaseApp 启动时初始化角色并启动秤服务
- SettingActivity 新增设备角色展示和秤数据监控入口
- Weigher2 支持外部设置串口路径,适配不同硬件子设备

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 17:15:48 +08:00
lvmeng d6a9c3562d refactor(net): 优化API请求错误处理和基础组件功能
- 添加JsonSyntaxException异常处理,提供更友好的错误提示
- 修复API响应判断逻辑,移除不必要的空值检查
- 优化HttpException转换为ApiException时的数据解析
- 移除BaseActivity中废弃的launch方法和相关变量
- 重构BaseActivity中的权限请求和页面跳转回调机制
- 更新Loading对话框的显示方法,支持自定义消息和内容更新
- 修复CollectFragment中的语法错误和日志记录功能
- 优化DishSamplingActivity中的页面跳转实现方式
- 完善SettingActivity中的相机权限检查逻辑
2026-03-30 15:28:52 +08:00
lvmeng 28c7628321 提交昨天的修改:优化多个模块和UI效果 2026-03-17 13:42:09 +08:00
lvmeng 3cf9818dad 优化SettingActivity底部菜单按钮效果:选中状态显示白色字体和圆角下划线 2026-03-17 13:40:54 +08:00
lvmeng c8eb2b8f24 ttyS4改为ttyS7,删除manifest中sharedUserId配置 2026-03-16 11:07:15 +08:00
lvmeng 795deae228 优化 2026-02-09 16:28:30 +08:00
lvmeng 873ae95c0b 优化 2026-02-06 17:31:52 +08:00
lvmeng a4f3f9527c 优化 2026-02-05 15:06:45 +08:00
lvmeng eaa1b1eb00 新接口调试 2026-01-30 18:01:07 +08:00
lvmeng a4905e7226 新接口调试 2026-01-29 18:51:42 +08:00
173 changed files with 8070 additions and 4280 deletions
+2 -2
View File
@@ -4,10 +4,10 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-09-10T07:28:30.961571900Z">
<DropdownSelection timestamp="2026-03-16T07:12:10.978585600Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="Default" identifier="serial=192.168.1.21:5555;connection=bc7af9a5" />
<DeviceId pluginId="Default" identifier="serial=192.168.1.115:5555;connection=8c23341a" />
</handle>
</Target>
</DropdownSelection>
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MaterialThemeProjectNewConfig">
<option name="metadata">
<MTProjectMetadataState>
<option name="migrated" value="true" />
<option name="pristineConfig" value="false" />
<option name="userId" value="4ab8791:19ce5d9ffb1:-7ffe" />
</MTProjectMetadataState>
</option>
</component>
</project>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+4
View File
@@ -0,0 +1,4 @@
kotlin version: 2.0.21
error message: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0. The daemon process output:
1. Kotlin compile daemon is ready
+61 -33
View File
@@ -1,8 +1,14 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
id("com.google.devtools.ksp") version "2.0.21-1.0.27"
// alias(libs.plugins.android.application)
// alias(libs.plugins.kotlin.android)
// id("com.google.devtools.ksp") version "2.0.21-1.0.27"
id("com.android.application")
id("org.jetbrains.kotlin.android")
// id("com.google.dagger.hilt.android")
id("com.google.devtools.ksp")
id("kotlin-parcelize")
kotlin("kapt")
}
android {
@@ -18,8 +24,9 @@ android {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
ndk {
//设置支持的SO库架构x86_64 仅用于在 Windows 模拟器上调试(秤功能不可用)
abiFilters.addAll(listOf("armeabi-v7a", "x86_64"))
//设置支持的SO库架构
abiFilters.addAll(listOf("armeabi-v7a"))
//, 'x86', 'armeabi-v7a', 'x86_64', 'arm64-v8a'
}
// 设置输出APK文件名格式
@@ -41,13 +48,12 @@ android {
"proguard-rules.pro"
)
signingConfig = signingConfigs.getByName("debug_507")
buildConfigField("Boolean", "IS_TEST_DEVICE", "false")
// buildConfigField("boolean", "DEBUG", "false")
}
debug {
//507扫码秤主板在android.buildTypes{}内增加
signingConfig = signingConfigs.getByName("debug_507")
// 调试构建视为测试设备,使用固定的 DEVICE_ID_1 代替真实 UDID
buildConfigField("Boolean", "IS_TEST_DEVICE", "false")
// buildConfigField("boolean", "DEBUG", "true")
}
}
compileOptions {
@@ -63,7 +69,7 @@ android {
}
packagingOptions {
pickFirst("lib/armeabi-v7a/libserial_port.so")
pickFirst("lib/arm64-v8a/libserial_port.so")
pickFirst("lib/arme64-v8a/libserial_port.so")
}
sourceSets {
named("main") {
@@ -77,7 +83,9 @@ android {
}
// li
}
val isDebug = gradle.startParameter.taskNames.any {
it.contains("debug", ignoreCase = true)
}
dependencies {
implementation(fileTree("libs") { include("*.jar", "*.aar") })
implementation(libs.androidx.core.ktx)
@@ -89,35 +97,55 @@ dependencies {
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
implementation(libs.androidx.activity.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation("com.squareup.okhttp3:okhttp:4.9.1")
implementation("com.squareup.okhttp3:logging-interceptor:4.9.1")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.google.code.gson:gson:2.10.1")
// implementation("com.github.CymChad:BaseRecyclerViewAdapterHelper:2.9.30")
implementation(libs.gson)
implementation(libs.okhttp)
implementation(libs.logging.interceptor)
implementation(libs.retrofit)
implementation(libs.java.websocket)
implementation(libs.converter.gson)
implementation("io.github.cymchad:BaseRecyclerViewAdapterHelper4:4.1.2")
implementation(libs.baserecyclerviewadapterhelper4)
implementation("androidx.room:room-runtime:2.5.2")
ksp("androidx.room:room-compiler:2.5.2")
implementation("androidx.room:room-ktx:2.5.2") // Kotlin扩展
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7")
implementation("androidx.activity:activity-ktx:1.8.0")
// room
implementation(libs.androidx.room.runtime)
ksp(libs.androidx.room.compiler)
implementation(libs.androidx.room.ktx)
// implementation("biz.laenger.android:vpbs:0.0.6")
//必须 1.0.0 以上
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-radar:3.0.0-alpha")
implementation("io.github.scwang90:refresh-footer-classics:3.0.0-alpha")
//经典加载
// refresh-layout
implementation(libs.refresh.layout.kernel)
implementation(libs.refresh.header.classics)
implementation(libs.refresh.header.radar)
implementation(libs.refresh.footer.classics)
//implementation("com.geyifeng.immersionbar:immersionbar:3.2.2")
implementation("com.google.android.flexbox:flexbox:3.0.0")
}
// glide
implementation(libs.glide)
annotationProcessor(libs.compiler)
// camerax
implementation(libs.androidx.camera.core)
implementation(libs.androidx.camera.camera2)
implementation(libs.androidx.camera.lifecycle)
implementation(libs.androidx.camera.view)
implementation(libs.androidx.camera.extensions)
//pytorch
implementation (libs.pytorch.android)
implementation (libs.pytorch.android.torchvision)
// objectbox
if (isDebug) {
debugImplementation(libs.objectbox.android.objectbrowser)
} else {
releaseImplementation(libs.objectbox.android)
}
}
apply(plugin = "io.objectbox")
+49
View File
@@ -0,0 +1,49 @@
{
"_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.",
"_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.",
"_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.",
"entities": [
{
"id": "1:6465153867413579585",
"lastPropertyId": "4:3660727688686055762",
"name": "Food",
"properties": [
{
"id": "1:3688350375714699356",
"name": "id",
"type": 6,
"flags": 1
},
{
"id": "2:6503828433160216329",
"name": "name",
"type": 9
},
{
"id": "3:1726525909323356793",
"name": "foodIdx",
"type": 5
},
{
"id": "4:3660727688686055762",
"name": "foodVector",
"indexId": "1:5497952887792054610",
"type": 28,
"flags": 8
}
],
"relations": []
}
],
"lastEntityId": "1:6465153867413579585",
"lastIndexId": "1:5497952887792054610",
"lastRelationId": "0:0",
"lastSequenceId": "0:0",
"modelVersion": 5,
"modelVersionParserMinimum": 5,
"retiredEntityUids": [],
"retiredIndexUids": [],
"retiredPropertyUids": [],
"retiredRelationUids": [],
"version": 1
}
Binary file not shown.
Binary file not shown.
+18 -3
View File
@@ -1,11 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:sharedUserId="android.uid.system">
xmlns:tools="http://schemas.android.com/tools">
<!-- android:sharedUserId="android.uid.system"-->
<uses-feature android:name="android.hardware.usb.host" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE"
tools:ignore="ProtectedPermissions" />
@@ -85,7 +89,18 @@
android:screenOrientation="portrait"
tools:ignore="DiscouragedApi,LockedOrientationActivity" />
<activity android:name="com.shuwei.dish.match.ui.DeviceConfigActivity" />
<activity android:name="com.shuwei.dish.match.ui.SettingActivity"
android:screenOrientation="portrait"
tools:ignore="DiscouragedApi,LockedOrientationActivity" />
<activity android:name="com.shuwei.dish.match.ui.SubmitFoodActivity" />
<activity
android:name="com.shuwei.dish.match.ui.MasterScaleActivity"
android:screenOrientation="portrait"
tools:ignore="DiscouragedApi,LockedOrientationActivity" />
<activity
android:name="com.shuwei.dish.match.ui.SlaveActivity"
android:screenOrientation="landscape"
tools:ignore="DiscouragedApi,LockedOrientationActivity" />
<receiver
android:name=".utils.BootReceiver"
Binary file not shown.
+272
View File
@@ -0,0 +1,272 @@
{
"class_names": [
"三黄鸡",
"上海青WP2026020200041",
"冰草WP2026020900054",
"刀豆",
"北豆腐WP2025111900007",
"味精WP2026013000034",
"圆生菜WP2026020900053",
"圆白菜[卷心菜,包菜]WP2025111000063",
"基围虾WP2025110700019",
"多宝鱼WP2025112600011",
"大白菜(代表值)WP2026022800638",
"大葱WP2026013000025",
"姜[黄姜]WP2026022800956",
"娃娃菜WP2026020200005",
"小白菜WP2026022800031",
"小米辣椒WP2026013000027",
"小葱WP2026013000029",
"山药(带皮)WP2026020200008",
"巨人笋",
"广东菜心WP2025120100014",
"木耳(干)[黑木耳,云耳]WP2026022801182",
"杏鲍菇WP2026022700030",
"松花蛋",
"松茸菌WP2025120100016",
"沙葱",
"海带(浸)[江白菜,昆布]WP2026022800035",
"牛腩WP2025110700043",
"猪五花肉WP2026020300048",
"猪腿肉WP2025110700040",
"玉米WP2025120100017",
"玉米糁(黄)WP2026022800038",
"番茄[西红柿]WP2026021000028",
"白吉饼WP2026013000024",
"笋",
"粉丝",
"紫甘蓝",
"红心萝卜[心里美]WP2026022800368",
"红提子WP2025111900006",
"红椒WP2026030300055",
"红洋葱WP2026030600054",
"红美人椒WP2026020300049",
"红薯",
"红薯粉条(袋)",
"羊排WP2025120300021",
"羊血WP2026013000038",
"胡萝卜WP2025110600008",
"芋头",
"花卷",
"芹菜WP2025111000065",
"苦菊",
"茄子(紫皮,长)WP2025110700024",
"茼蒿WP2026020200045",
"莲藕(带皮)",
"莴笋(鲜)[莴苣]WP2026020200009",
"菜花(白色)[花椰菜]",
"菠菜(鲜)[赤根菜]WP2026022800700",
"蒜WP2025111400003",
"蒜苗WP2026013000028",
"蒜薹(圆)",
"蒸鱼豉油WP2026013000032",
"薏米[薏仁米,苡米]WP2026022800037",
"西兰花[绿菜花]WP2025110700029",
"西葫芦",
"豆腐皮WP2025111000059",
"豆角",
"辣椒(红,小)WP2025111000066",
"辣椒(青,尖)WP2026022800504",
"醪糟",
"金针菇[(鲜)[智力菇]WP2026030600051",
"青线椒WP2026013000031",
"韭菜WP2026020200044",
"韭黄",
"馒头(代表值)WP2026030600056",
"香椿[香椿芽]WP2026022801086",
"香菇(鲜)[香蕈,冬菇]WP2026030600050",
"香菜WP2026013000030",
"马铃薯[土豆、洋芋]WP2026012900001",
"鸡全腿(带骨)",
"鸡翅(带骨)",
"鸡腿肉WP2025110700048",
"鸡腿(带骨)",
"鸡蛋WP2026013000039",
"麦芹",
"黄瓜(鲜)[胡瓜]WP2025110700031",
"黄豆芽WP2026020200042",
"黑袋子",
"黑鱼(整条鱼)WP2025110600012"
],
"class_to_idx": {
"三黄鸡": 0,
"上海青WP2026020200041": 1,
"冰草WP2026020900054": 2,
"刀豆": 3,
"北豆腐WP2025111900007": 4,
"味精WP2026013000034": 5,
"圆生菜WP2026020900053": 6,
"圆白菜[卷心菜,包菜]WP2025111000063": 7,
"基围虾WP2025110700019": 8,
"多宝鱼WP2025112600011": 9,
"大白菜(代表值)WP2026022800638": 10,
"大葱WP2026013000025": 11,
"姜[黄姜]WP2026022800956": 12,
"娃娃菜WP2026020200005": 13,
"小白菜WP2026022800031": 14,
"小米辣椒WP2026013000027": 15,
"小葱WP2026013000029": 16,
"山药(带皮)WP2026020200008": 17,
"巨人笋": 18,
"广东菜心WP2025120100014": 19,
"木耳(干)[黑木耳,云耳]WP2026022801182": 20,
"杏鲍菇WP2026022700030": 21,
"松花蛋": 22,
"松茸菌WP2025120100016": 23,
"沙葱": 24,
"海带(浸)[江白菜,昆布]WP2026022800035": 25,
"牛腩WP2025110700043": 26,
"猪五花肉WP2026020300048": 27,
"猪腿肉WP2025110700040": 28,
"玉米WP2025120100017": 29,
"玉米糁(黄)WP2026022800038": 30,
"番茄[西红柿]WP2026021000028": 31,
"白吉饼WP2026013000024": 32,
"笋": 33,
"粉丝": 34,
"紫甘蓝": 35,
"红心萝卜[心里美]WP2026022800368": 36,
"红提子WP2025111900006": 37,
"红椒WP2026030300055": 38,
"红洋葱WP2026030600054": 39,
"红美人椒WP2026020300049": 40,
"红薯": 41,
"红薯粉条(袋)": 42,
"羊排WP2025120300021": 43,
"羊血WP2026013000038": 44,
"胡萝卜WP2025110600008": 45,
"芋头": 46,
"花卷": 47,
"芹菜WP2025111000065": 48,
"苦菊": 49,
"茄子(紫皮,长)WP2025110700024": 50,
"茼蒿WP2026020200045": 51,
"莲藕(带皮)": 52,
"莴笋(鲜)[莴苣]WP2026020200009": 53,
"菜花(白色)[花椰菜]": 54,
"菠菜(鲜)[赤根菜]WP2026022800700": 55,
"蒜WP2025111400003": 56,
"蒜苗WP2026013000028": 57,
"蒜薹(圆)": 58,
"蒸鱼豉油WP2026013000032": 59,
"薏米[薏仁米,苡米]WP2026022800037": 60,
"西兰花[绿菜花]WP2025110700029": 61,
"西葫芦": 62,
"豆腐皮WP2025111000059": 63,
"豆角": 64,
"辣椒(红,小)WP2025111000066": 65,
"辣椒(青,尖)WP2026022800504": 66,
"醪糟": 67,
"金针菇[(鲜)[智力菇]WP2026030600051": 68,
"青线椒WP2026013000031": 69,
"韭菜WP2026020200044": 70,
"韭黄": 71,
"馒头(代表值)WP2026030600056": 72,
"香椿[香椿芽]WP2026022801086": 73,
"香菇(鲜)[香蕈,冬菇]WP2026030600050": 74,
"香菜WP2026013000030": 75,
"马铃薯[土豆、洋芋]WP2026012900001": 76,
"鸡全腿(带骨)": 77,
"鸡翅(带骨)": 78,
"鸡腿肉WP2025110700048": 79,
"鸡腿(带骨)": 80,
"鸡蛋WP2026013000039": 81,
"麦芹": 82,
"黄瓜(鲜)[胡瓜]WP2025110700031": 83,
"黄豆芽WP2026020200042": 84,
"黑袋子": 85,
"黑鱼(整条鱼)WP2025110600012": 86
},
"idx_to_class": {
"0": "三黄鸡",
"1": "上海青WP2026020200041",
"2": "冰草WP2026020900054",
"3": "刀豆",
"4": "北豆腐WP2025111900007",
"5": "味精WP2026013000034",
"6": "圆生菜WP2026020900053",
"7": "圆白菜[卷心菜,包菜]WP2025111000063",
"8": "基围虾WP2025110700019",
"9": "多宝鱼WP2025112600011",
"10": "大白菜(代表值)WP2026022800638",
"11": "大葱WP2026013000025",
"12": "姜[黄姜]WP2026022800956",
"13": "娃娃菜WP2026020200005",
"14": "小白菜WP2026022800031",
"15": "小米辣椒WP2026013000027",
"16": "小葱WP2026013000029",
"17": "山药(带皮)WP2026020200008",
"18": "巨人笋",
"19": "广东菜心WP2025120100014",
"20": "木耳(干)[黑木耳,云耳]WP2026022801182",
"21": "杏鲍菇WP2026022700030",
"22": "松花蛋",
"23": "松茸菌WP2025120100016",
"24": "沙葱",
"25": "海带(浸)[江白菜,昆布]WP2026022800035",
"26": "牛腩WP2025110700043",
"27": "猪五花肉WP2026020300048",
"28": "猪腿肉WP2025110700040",
"29": "玉米WP2025120100017",
"30": "玉米糁(黄)WP2026022800038",
"31": "番茄[西红柿]WP2026021000028",
"32": "白吉饼WP2026013000024",
"33": "笋",
"34": "粉丝",
"35": "紫甘蓝",
"36": "红心萝卜[心里美]WP2026022800368",
"37": "红提子WP2025111900006",
"38": "红椒WP2026030300055",
"39": "红洋葱WP2026030600054",
"40": "红美人椒WP2026020300049",
"41": "红薯",
"42": "红薯粉条(袋)",
"43": "羊排WP2025120300021",
"44": "羊血WP2026013000038",
"45": "胡萝卜WP2025110600008",
"46": "芋头",
"47": "花卷",
"48": "芹菜WP2025111000065",
"49": "苦菊",
"50": "茄子(紫皮,长)WP2025110700024",
"51": "茼蒿WP2026020200045",
"52": "莲藕(带皮)",
"53": "莴笋(鲜)[莴苣]WP2026020200009",
"54": "菜花(白色)[花椰菜]",
"55": "菠菜(鲜)[赤根菜]WP2026022800700",
"56": "蒜WP2025111400003",
"57": "蒜苗WP2026013000028",
"58": "蒜薹(圆)",
"59": "蒸鱼豉油WP2026013000032",
"60": "薏米[薏仁米,苡米]WP2026022800037",
"61": "西兰花[绿菜花]WP2025110700029",
"62": "西葫芦",
"63": "豆腐皮WP2025111000059",
"64": "豆角",
"65": "辣椒(红,小)WP2025111000066",
"66": "辣椒(青,尖)WP2026022800504",
"67": "醪糟",
"68": "金针菇[(鲜)[智力菇]WP2026030600051",
"69": "青线椒WP2026013000031",
"70": "韭菜WP2026020200044",
"71": "韭黄",
"72": "馒头(代表值)WP2026030600056",
"73": "香椿[香椿芽]WP2026022801086",
"74": "香菇(鲜)[香蕈,冬菇]WP2026030600050",
"75": "香菜WP2026013000030",
"76": "马铃薯[土豆、洋芋]WP2026012900001",
"77": "鸡全腿(带骨)",
"78": "鸡翅(带骨)",
"79": "鸡腿肉WP2025110700048",
"80": "鸡腿(带骨)",
"81": "鸡蛋WP2026013000039",
"82": "麦芹",
"83": "黄瓜(鲜)[胡瓜]WP2025110700031",
"84": "黄豆芽WP2026020200042",
"85": "黑袋子",
"86": "黑鱼(整条鱼)WP2025110600012"
},
"embedding_dim": 512,
"total_images": 2093,
"build_time": "2026-03-10T10:33:47.104848"
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -8,11 +8,12 @@ import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.shuwei.dish.match.R
import com.shuwei.dish.match.databinding.ListItemDishCookBinding
import com.shuwei.dish.match.model.GoodsItem
import com.shuwei.dish.match.entity.CookFoodGoodsEntity
import com.shuwei.dish.match.utils.ext.roundedOneDecimalPlace
import java.text.DecimalFormat
class DishPartAdapter(list: MutableList<GoodsItem>) :
BaseQuickAdapter<GoodsItem, DishPartAdapter.VH>(list) {
class DishPartAdapter(list: MutableList<CookFoodGoodsEntity>) :
BaseQuickAdapter<CookFoodGoodsEntity, DishPartAdapter.VH>(list) {
inner class VH(var binding: ListItemDishCookBinding) : QuickViewHolder(binding.root)
@@ -22,10 +23,11 @@ class DishPartAdapter(list: MutableList<GoodsItem>) :
return VH(binding)
}
override fun onBindViewHolder(holder: VH, position: Int, item: GoodsItem?) {
override fun onBindViewHolder(holder: VH, position: Int, item: CookFoodGoodsEntity?) {
holder.binding.run {
tvDishName.text = item!!.goodsName
tvDishType.text = if (item.materialType == 1) "主辅材:主材" else if (item.materialType == 2) "主辅材:辅材" else ""
//"${DecimalFormat("#").format(item.useWeight)}克"
tvDishWeight.text =
if (item.useWeight == null || item.useWeight == 0.toDouble()) "" else "${item.useWeight!!.roundedOneDecimalPlace()}"
tvDishWeight.setTextColor(
@@ -36,16 +38,20 @@ class DishPartAdapter(list: MutableList<GoodsItem>) :
)
ivOperateIcon.setImageResource(
if (item.isSamplingPage)
//采集页面
R.drawable.ic_delete
else
//制作页面
if (item.isSetFinished) R.drawable.ic_dish_selected
else R.drawable.ic_dish_unselected
)
root.setBackgroundResource(
if (item.isClicked) R.drawable.shape_item_cook_dish
else R.drawable.shape_white_fb_15_corners
)
root.run {
setBackgroundResource(
if (item.isItemClicked) R.drawable.shape_item_cook_dish
else R.drawable.shape_white_fb_15_corners
)
}
}
}
}
}
@@ -7,7 +7,7 @@ import androidx.core.content.ContextCompat
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.shuwei.dish.match.R
import com.shuwei.dish.match.model.FoodRecord
import com.shuwei.dish.match.entity.FoodRecord
import com.shuwei.dish.match.databinding.ListItemDishBinding
import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.visible
@@ -0,0 +1,58 @@
package com.shuwei.dish.match.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.shuwei.dish.match.R
import com.shuwei.dish.match.databinding.ListItemFoodCollectionBinding
import com.shuwei.dish.match.objbox.FoodCollectionBean
import com.shuwei.dish.match.utils.ext.load
class FoodCollectionAdapter (var list: MutableList<FoodCollectionBean>) :
BaseQuickAdapter<FoodCollectionBean, FoodCollectionAdapter.VH>(list) {
inner class VH(var binding: ListItemFoodCollectionBinding) : QuickViewHolder(binding.root)
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val inflater = LayoutInflater.from(context)
val binding = ListItemFoodCollectionBinding.inflate(inflater, parent, false)
return VH(binding)
}
override fun onBindViewHolder(holder: VH, position: Int, item: FoodCollectionBean?) {
val binding = holder.binding
item?.let {
binding.ivFinish.visibility = if (it.isFinish) View.VISIBLE else View.GONE
binding.ivDelete.visibility = if (it.isShowCamera) View.GONE else View.VISIBLE
binding.imageView.run {
if (it.isShowCamera) {
scaleType = ImageView.ScaleType.CENTER
setImageResource(R.drawable.ic_camera_default)
} else {
//scaleType = ImageView.ScaleType.FIT_CENTER
scaleType = ImageView.ScaleType.CENTER_CROP
//setImageURI(it.imageUri)
//setImageBitmap(it.bitmap)
if (it.imageUri!=null) {
setImageURI(it.imageUri)
} else {
load(it.imageFile)
}
// it.bitmap?.let { bitmap ->
// GlideUtils.loadRoundCornerWitBitmap(
// context,
// url = bitmap,
// imageView = this,
// radius = 8.dp
// )
// }
}
}
}
}
}
@@ -6,7 +6,7 @@ import android.view.ViewGroup
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.shuwei.dish.match.databinding.ListItemDishRecordBinding
import com.shuwei.dish.match.model.FoodRecord
import com.shuwei.dish.match.entity.FoodRecord
class FoodRecordAdapter(list: MutableList<FoodRecord>) :
BaseQuickAdapter<FoodRecord, FoodRecordAdapter.VH>(list) {
@@ -8,15 +8,15 @@ import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.shuwei.dish.match.R
import com.shuwei.dish.match.databinding.ListItemSearchGoodsInfoBinding
import com.shuwei.dish.match.model.GoodsItem
import com.shuwei.dish.match.entity.CookFoodGoodsEntity
class GoodsInfoSearchAdapter(private var list: MutableList<GoodsItem>) :
BaseQuickAdapter<GoodsItem, GoodsInfoSearchAdapter.VH>(list) {
class GoodsInfoSearchAdapter(private var list: MutableList<CookFoodGoodsEntity>) :
BaseQuickAdapter<CookFoodGoodsEntity, GoodsInfoSearchAdapter.VH>(list) {
override fun onBindViewHolder(
holder: VH,
position: Int,
item: GoodsItem?
item: CookFoodGoodsEntity?
) {
holder.binding.tvGoodsInfo.run {
text = item?.goodsName
@@ -46,4 +46,4 @@ class GoodsInfoSearchAdapter(private var list: MutableList<GoodsItem>) :
inner class VH(var binding: ListItemSearchGoodsInfoBinding) : QuickViewHolder(binding.root)
}
}
@@ -8,7 +8,7 @@ import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.shuwei.dish.match.R
import com.shuwei.dish.match.databinding.ListItemHomeModeBinding
import com.shuwei.dish.match.model.HomeModeBean
import com.shuwei.dish.match.entity.HomeModeBean
import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.visible
@@ -7,9 +7,8 @@ import androidx.core.content.ContextCompat
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.shuwei.dish.match.R
import com.shuwei.dish.match.model.FoodRecord
import com.shuwei.dish.match.entity.FoodRecord
import com.shuwei.dish.match.databinding.ListItemDishBinding
import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.visible
import java.text.DecimalFormat
@@ -28,9 +27,9 @@ class SamplingAdapter(list: MutableList<FoodRecord>) :
holder.binding.run {
val isCooking = item!!.isCooking
clBlock.setBackgroundResource(R.drawable.shape_white_fb_15_corners)
val totalWeight = item!!.totalWeight ?: 0.toDouble()
val totalWeight = item.totalWeight ?: 0.toDouble()
tvDishName.run {
text = item!!.foodName
text = (item.foodName?:"").ifBlank { "--" }
setTextColor(
ContextCompat.getColor(
holder.itemView.context,
@@ -41,11 +40,16 @@ class SamplingAdapter(list: MutableList<FoodRecord>) :
tvDishCount.visible()
if (isCooking) {
tvDishCount.text = "制作统计:-kg"
tvShowState.text = "烹饪中"
tvShowState.text = "烹饪中"
} else {
val useWeight = if (totalWeight > 0.toDouble()) totalWeight else (item.foodWeight ?: 0.toDouble())
tvDishCount.text = "累计统计:${df.format(useWeight / 1000.0F)}kg(${item.count}次)"
tvShowState.text = "${df.format((item.foodWeight ?: 0.toDouble()) / 1000.0F)}kg"
val realTotalWeight = if (totalWeight > 0.toDouble()) totalWeight else (item.foodWeight
?: 0.toDouble())
val showTotalWeight =
if (realTotalWeight == 0.toDouble()) "-" else df.format(realTotalWeight / 1000.0F)
tvDishCount.text = "累计统计:${showTotalWeight}kg(${item.count ?: "-"}次)"
val foodWeight = item.foodWeight ?: 0.toDouble()
val showFoodWeight = if (foodWeight == 0.toDouble()) "-" else df.format(foodWeight / 1000.0F)
tvShowState.text = "${showFoodWeight}kg"
}
}
}
@@ -1,38 +0,0 @@
//package com.shuwei.dish.match.adapter
//
//import android.content.Context
//import android.view.LayoutInflater
//import android.view.ViewGroup
//import androidx.core.view.updateLayoutParams
//import com.chad.library.adapter4.BaseQuickAdapter
//import com.chad.library.adapter4.viewholder.QuickViewHolder
//import com.shuwei.dish.match.databinding.ListItemCellBinding
//import com.shuwei.dish.match.model.GoodsItem
//import com.shuwei.dish.match.utils.ext.dp
//
///**
// * 调料格子列表适配器,使用 list_item_cell 布局展示调料名称与用量
// */
//class SeasoningAdapter(list: MutableList<GoodsItem>) :
// BaseQuickAdapter<GoodsItem, SeasoningAdapter.VH>(list) {
//
// override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
// val binding = ListItemCellBinding.inflate(LayoutInflater.from(context), parent, false)
// return VH(binding)
// }
//
// override fun onBindViewHolder(holder: VH, position: Int, item: GoodsItem?) {
// item ?: return
// holder.binding.root.updateLayoutParams<ViewGroup.MarginLayoutParams> {
// width = 226.dp
// height = 120.dp
// leftMargin = 7.dp
// rightMargin = 8.dp
// topMargin = 7.dp
// bottomMargin = 8.dp
// }
// holder.binding.tvCell.text = TextCellAdapter.getTextSpan(item.toSeasoningEntity())
// }
//
// inner class VH(val binding: ListItemCellBinding) : QuickViewHolder(binding.root)
//}
@@ -1,134 +0,0 @@
//package com.shuwei.dish.match.adapter
//
//import android.annotation.SuppressLint
//import android.widget.TextView
//import androidx.recyclerview.widget.GridLayoutManager
//import androidx.recyclerview.widget.RecyclerView
//import com.shuwei.dish.match.base.BaseActivity
//import com.shuwei.dish.match.dialog.BottomDialog2
//import com.shuwei.dish.match.db.entity.SeasoningEntity
//import com.shuwei.dish.match.ui.DeviceSettingActivity
//import com.shuwei.dish.match.ui.SubmitDishActivity
//import com.shuwei.dish.match.utils.DataUtil
//
//object SeasoningCellTool {
// private val TAG = "SeasoningCellTool"
//
// private val list = mutableListOf<SeasoningEntity>()
// private lateinit var activity: BaseActivity
// private var pageType: Int = 0
//
// @SuppressLint("StaticFieldLeak")
// private lateinit var cellAdapter: TextCellAdapter
//
// @SuppressLint("NotifyDataSetChanged")
// fun loadData(
// activity: BaseActivity,
// initList: MutableList<SeasoningEntity>? = null,
// recyclerView: RecyclerView,
// tvCell: TextView,
// pageType: Int = 0
// ) {
// this.activity = activity
// this.pageType = pageType
// list.clear()
// val tempList = buildData(initList, pageType)
// list.addAll(tempList)
// recyclerView.run {
// if (adapter == null) {
// layoutManager = GridLayoutManager(
// activity,
// 3,
// GridLayoutManager.VERTICAL,
// false
// )
// cellAdapter = TextCellAdapter(list).apply {
// onCellClick = { position ->
// val currentAddress = DataUtil.getWeighAddressArray().get(position)
// BottomDialog2(
// weighIndex = position,
// weighAddress = currentAddress,
// clickName = list[position].goodsName
// ).show(activity) { item ->
// list[position].let { current ->
// syncItem(current, item)
// //3-调料
// current.materialType = 3
// current.sort = position
// }
// notifyItemChanged(position)
// if (position == 14) {
// syncItem(list[10], list[14])
// syncItem(list[11], list[14])
// syncItem(list[13], list[14])
// TextCellAdapter.loadCell(tvCell, list[14])
// notifyItemChanged(10)
// notifyItemChanged(11)
// notifyItemChanged(13)
// notifyItemChanged(14)
// }
// if (activity is DeviceSettingActivity) {
// //activity.list = list
// activity.saveSeasoning(list[position])
// }
// }
// }
// }
// adapter = cellAdapter
// } else {
// cellAdapter.notifyDataSetChanged()
// }
// if (activity is SubmitDishActivity) {
// activity.updateSeasoningList(list)
// }
// }
// if (pageType == 1) {
// tvCell.setOnClickListener {
// cellAdapter.onCellClick?.invoke(14)
// }
// }
// TextCellAdapter.loadCell(tvCell, list[list.size - 1])
// }
//
// private fun buildData(
// initList: MutableList<SeasoningEntity>?,
// pageType: Int = 0
// ): MutableList<SeasoningEntity> {
// val list = mutableListOf<SeasoningEntity>()
// repeat(15) {
// list.add(SeasoningEntity().apply { this.pageType = pageType })
// }
// if (initList.isNullOrEmpty().not()) {
// initList.forEach {
// if (it.sort < list.size) {
// list[it.sort] = it.apply { this.pageType = pageType }
// }
// }
// var lastItem = initList.firstOrNull { it.sort == 14 }
// lastItem = lastItem ?: list[list.size - 1]
// list[10] = lastItem
// list[11] = lastItem
// list[13] = lastItem
// }
// return list
// }
//
// private fun syncItem(item: SeasoningEntity, newItem: SeasoningEntity) {
// item.run {
// goodsId = newItem.goodsId
// goodsName = newItem.goodsName
// goodsOrRelationCode = newItem.goodsOrRelationCode
// relateionType = newItem.relateionType
// materialType = newItem.materialType
// allEdible = newItem.allEdible
// useWeight = newItem.useWeight
// popularName = newItem.popularName
// canteenId = newItem.canteenId
// relateionType_dictText = newItem.relateionType_dictText
//
// foodId = newItem.foodId
// sort = newItem.sort
// }
// }
//
//}
@@ -8,15 +8,15 @@ import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.shuwei.dish.match.R
import com.shuwei.dish.match.databinding.ListItemSearchGoodsInfoBinding
import com.shuwei.dish.match.model.GoodsItem
import com.shuwei.dish.match.entity.SeasoningEntity
class SeasoningSearchAdapter(private var list: MutableList<GoodsItem>) :
BaseQuickAdapter<GoodsItem, SeasoningSearchAdapter.VH>(list) {
class SeasoningSearchAdapter(private var list: MutableList<SeasoningEntity>) :
BaseQuickAdapter<SeasoningEntity, SeasoningSearchAdapter.VH>(list) {
override fun onBindViewHolder(
holder: VH,
position: Int,
item: GoodsItem?
item: SeasoningEntity?
) {
holder.binding.tvGoodsInfo.run {
text = item?.goodsName
@@ -46,4 +46,4 @@ class SeasoningSearchAdapter(private var list: MutableList<GoodsItem>) :
inner class VH(var binding: ListItemSearchGoodsInfoBinding) : QuickViewHolder(binding.root)
}
}
@@ -1,137 +1,136 @@
//package com.shuwei.dish.match.adapter
//
//import android.graphics.Typeface
//import android.icu.text.DecimalFormat
//import android.text.SpannableStringBuilder
//import android.text.style.AbsoluteSizeSpan
//import android.text.style.ForegroundColorSpan
//import android.text.style.LineHeightSpan
//import android.text.style.StyleSpan
//import android.view.LayoutInflater
//import android.view.ViewGroup
//import android.widget.FrameLayout
//import android.widget.TextView
//import androidx.core.content.ContextCompat
//import androidx.core.graphics.toColorInt
//import androidx.recyclerview.widget.RecyclerView
//import com.chad.library.adapter4.viewholder.QuickViewHolder
//import com.shuwei.dish.match.R
//import com.shuwei.dish.match.databinding.ListItemCellBinding
//import com.shuwei.dish.match.db.entity.SeasoningEntity
//import com.shuwei.dish.match.model.GoodsItem
//import com.shuwei.dish.match.utils.ext.appendText
//import com.shuwei.dish.match.utils.ext.buildSpannableString
//import com.shuwei.dish.match.utils.ext.clickWithDebounce
//import com.shuwei.dish.match.utils.ext.dp
//import com.shuwei.dish.match.utils.ext.invisible
//import com.shuwei.dish.match.utils.ext.roundedOneDecimalPlace
//import com.shuwei.dish.match.utils.ext.visible
//
//class TextCellAdapter(var list: MutableList<SeasoningEntity>) :
// RecyclerView.Adapter<TextCellAdapter.VH>() {
//
// inner class VH(var binding: ListItemCellBinding) : QuickViewHolder(binding.root)
//
// override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
// val inflater = LayoutInflater.from(parent.context)
// val binding = ListItemCellBinding.inflate(inflater, parent, false)
// return VH(binding)
// }
//
// override fun onBindViewHolder(holder: VH, position: Int) {
// val item = list[position]
// holder.binding.run {
// if (position == 10 || position == 11 || position == 13 || position == 14) {
// root.invisible()
package com.shuwei.dish.match.adapter
import android.graphics.Typeface
import android.icu.text.DecimalFormat
import android.text.SpannableStringBuilder
import android.text.style.AbsoluteSizeSpan
import android.text.style.ForegroundColorSpan
import android.text.style.LineHeightSpan
import android.text.style.StyleSpan
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.graphics.toColorInt
import androidx.recyclerview.widget.RecyclerView
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.shuwei.dish.match.R
import com.shuwei.dish.match.databinding.ListItemCellBinding
import com.shuwei.dish.match.entity.SeasoningEntity
import com.shuwei.dish.match.utils.ext.appendText
import com.shuwei.dish.match.utils.ext.buildSpannableString
import com.shuwei.dish.match.utils.ext.clickWithDebounce
import com.shuwei.dish.match.utils.ext.dp
import com.shuwei.dish.match.utils.ext.invisible
import com.shuwei.dish.match.utils.ext.roundedOneDecimalPlace
import com.shuwei.dish.match.utils.ext.visible
class TextCellAdapter(var list: MutableList<SeasoningEntity>) :
RecyclerView.Adapter<TextCellAdapter.VH>() {
inner class VH(var binding: ListItemCellBinding) : QuickViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
val inflater = LayoutInflater.from(parent.context)
val binding = ListItemCellBinding.inflate(inflater, parent, false)
return VH(binding)
}
override fun onBindViewHolder(holder: VH, position: Int) {
val item = list[position]
holder.binding.run {
if (position == 10 || position == 11 || position == 13 || position == 14) {
root.invisible()
} else {
root.visible()
}
loadCell(tvCell, item)
//root.setBackgroundResource(R.color.dish_green)
(root.layoutParams as RecyclerView.LayoutParams).let {
// it.bottomMargin = 15.dp
it.width = 226.dp
it.height = 120.dp
// it.rightMargin = 0
// it.leftMargin = if (position % 3 == 0) 0 else 15.dp
it.leftMargin = 7.dp
it.rightMargin = 8.dp
it.topMargin = 7.dp
it.bottomMargin = 8.dp
}
if (item.pageType == 1) {
root.clickWithDebounce { onCellClick?.invoke(position) }
}
}
}
override fun getItemCount() = list.size
var onCellClick: ((positon: Int) -> Unit)? = null
companion object {
fun loadLayout(frameLayout: FrameLayout, item: SeasoningEntity) {
val child: TextView = frameLayout.getChildAt(0) as TextView
child.text = getTextSpan(item)
}
fun loadCell(tvCell: TextView, item: SeasoningEntity) {
tvCell.text = getTextSpan(item)
}
// fun loadCell2(tvCell: TextView, item: SeasoningEntity) {
// if (item.goodsName.isNullOrBlank()) {
// tvCell.run {
// text = buildSpannableString {
// appendText(
// "未配置",
// ForegroundColorSpan("#999999".toColorInt()),
// StyleSpan(Typeface.BOLD),
// AbsoluteSizeSpan(28, true),
// LineHeightSpan { text, start, end, spanstartv, v, fm ->
// fm.descent += 10.dp // 增加行间距
// }
// )}
// //setTextColor(ContextCompat.getColor(context, R.color.black999))
// }
// } else {
// root.visible()
// }
// loadCell(tvCell, item)
// //root.setBackgroundResource(R.color.dish_green)
// (root.layoutParams as RecyclerView.LayoutParams).let {
//// it.bottomMargin = 15.dp
// it.width = 226.dp
// it.height = 120.dp
//// it.rightMargin = 0
//// it.leftMargin = if (position % 3 == 0) 0 else 15.dp
//
// it.leftMargin = 7.dp
// it.rightMargin = 8.dp
// it.topMargin = 7.dp
// it.bottomMargin = 8.dp
// }
// if (item.pageType == 1) {
// root.clickWithDebounce { onCellClick?.invoke(position) }
// }
// }
// }
//
// override fun getItemCount() = list.size
//
// var onCellClick: ((positon: Int) -> Unit)? = null
//
// companion object {
// fun loadLayout(frameLayout: FrameLayout, item: SeasoningEntity) {
// val child: TextView = frameLayout.getChildAt(0) as TextView
// child.text = getTextSpan(item)
// }
// fun loadCell(tvCell: TextView, item: SeasoningEntity) {
// tvCell.text = getTextSpan(item)
// }
//// fun loadCell2(tvCell: TextView, item: SeasoningEntity) {
//// if (item.goodsName.isNullOrBlank()) {
//// tvCell.run {
//// text = buildSpannableString {
//// appendText(
//// "未配置",
//// ForegroundColorSpan("#999999".toColorInt()),
//// StyleSpan(Typeface.BOLD),
//// AbsoluteSizeSpan(28, true),
//// LineHeightSpan { text, start, end, spanstartv, v, fm ->
//// fm.descent += 10.dp // 增加行间距
//// }
//// )}
//// //setTextColor(ContextCompat.getColor(context, R.color.black999))
//// if (item.pageType == 1) {
//// tvCell.run {
//// text = item.goodsName
//// setTextColor(ContextCompat.getColor(context, R.color.black))
//// }
//// } else {
// tvCell.text = getTextSpan(item)
//// }
//// } else {
////// if (item.pageType == 1) {
////// tvCell.run {
////// text = item.goodsName
////// setTextColor(ContextCompat.getColor(context, R.color.black))
////// }
////// } else {
//// tvCell.text = getTextSpan(item)
////// }
//// }
//// }
//
// fun getTextSpan(item: SeasoningEntity): SpannableStringBuilder {
// val endIndex = if (item.sort == 9) 10 else 6
// var name = if (item.goodsName.isNullOrBlank()) "未配置" else item.goodsName!!
// if (name.length > endIndex) {
// name = "${name.substring(0, endIndex)}…"
// }
// return buildSpannableString {
// appendText(
// name,
// ForegroundColorSpan(if (item.goodsName.isNullOrBlank()) "#999999".toColorInt() else "#000000".toColorInt()),
// StyleSpan(Typeface.BOLD),
// AbsoluteSizeSpan(28, true),
// LineHeightSpan { text, start, end, spanstartv, v, fm ->
// fm.descent += 10.dp // 增加行间距
// }
// )
// append("\n")
// val weight = item.useWeight ?: 0.toDouble()
// val weightColor = if (weight == 0.toDouble()) "#999999" else "#00BC71"
// appendText(
//// "${DecimalFormat("#").format(weight)}g",
// "${weight.roundedOneDecimalPlace()}g",
// ForegroundColorSpan(weightColor.toColorInt()),
// AbsoluteSizeSpan(30, true)
// )
// }
// }
// }
//
//}
fun getTextSpan(item: SeasoningEntity): SpannableStringBuilder {
val endIndex = if (item.sort == 9) 10 else 6
var name = if (item.goodsName.isNullOrBlank()) "未配置" else item.goodsName!!
if (name.length > endIndex) {
name = "${name.substring(0, endIndex)}"
}
return buildSpannableString {
appendText(
name,
ForegroundColorSpan(if (item.goodsName.isNullOrBlank()) "#999999".toColorInt() else "#000000".toColorInt()),
StyleSpan(Typeface.BOLD),
AbsoluteSizeSpan(28, true),
LineHeightSpan { text, start, end, spanstartv, v, fm ->
fm.descent += 10.dp // 增加行间距
}
)
append("\n")
val weight = item.useWeight ?: 0.toDouble()
val weightColor = if (weight == 0.toDouble()) "#999999" else "#00BC71"
appendText(
// "${DecimalFormat("#").format(weight)}g",
"${weight.roundedOneDecimalPlace()}g",
ForegroundColorSpan(weightColor.toColorInt()),
AbsoluteSizeSpan(30, true)
)
}
}
}
}
@@ -4,7 +4,6 @@ import android.content.Intent
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.View
import android.widget.FrameLayout
import android.widget.ImageView
@@ -15,45 +14,32 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import com.shuwei.dish.match.R
import com.shuwei.dish.match.databinding.ActivityBaseBinding
import com.shuwei.dish.match.db.DbViewModel
import com.shuwei.dish.match.dialog.Loading
import com.shuwei.dish.match.entity.CookFoodGoodsEntity
import com.shuwei.dish.match.entity.SeasoningEntity
import com.shuwei.dish.match.net.NetViewModel
import com.shuwei.dish.match.viewmodel.AppViewModel
import com.shuwei.dish.match.ui.HomeActivity.Companion.TAG
import com.shuwei.dish.match.ui.InitActivity
import com.shuwei.dish.match.utils.ActivityManager
import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.invisible
import com.shuwei.dish.match.utils.ext.put
import com.shuwei.dish.match.utils.ext.toast
import com.shuwei.dish.match.utils.ext.visible
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Locale
import kotlin.getValue
@Suppress("DEPRECATION")
open class BaseActivity : AppCompatActivity() {
val netViewModel: NetViewModel by viewModels()
val dbViewModel: DbViewModel by viewModels()
val appViewModel: AppViewModel by viewModels()
private lateinit var binding: ActivityBaseBinding
private var launcher: ActivityResultLauncher<Intent>? = null
private var launchCallback: ((Intent?) -> Unit)? = null
public fun launch(cls: Class<*>, launchCallback: (Intent?) -> Unit) {
this.launchCallback = launchCallback
launcher?.launch(Intent(this, cls))
}
public fun launch(intent: Intent, launchCallback: (Intent?) -> Unit) {
this.launchCallback = launchCallback
launcher?.launch(intent)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -64,12 +50,6 @@ open class BaseActivity : AppCompatActivity() {
}
setContentView(binding.root)
statusBarDarkFont(enable = false)
launcher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
launchCallback?.invoke(result.data)
}
}
// window.setDecorFitsSystemWindows(false) // 启用 Edge-to-Edge
// window.insetsController?.apply {
@@ -93,16 +73,17 @@ open class BaseActivity : AppCompatActivity() {
}
fun hideStatusBar() {
enableEdgeToEdge()
// val uiOptions = (View.SYSTEM_UI_FLAG_FULLSCREEN // 隐藏状态栏
// or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) // 隐藏导航栏(可选)
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN
// enableEdgeToEdge()
//// val uiOptions = (View.SYSTEM_UI_FLAG_FULLSCREEN // 隐藏状态栏
//// or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) // 隐藏导航栏(可选)
// window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN
hideSystemBars()
}
fun statusBarDarkFont(enable: Boolean) {
//window.decorView.systemUiVisibility = if (enable) View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR else 0
val color = ContextCompat.getColor(this,
if (enable) R.color.black else R.color.white
val color = ContextCompat.getColor(
this, if (enable) R.color.black else R.color.white
)
binding.tvLeftTime.setTextColor(color)
binding.tvRightTime.setTextColor(color)
@@ -167,8 +148,11 @@ open class BaseActivity : AppCompatActivity() {
handler.removeCallbacks(updateTask)
}
fun showLoading() {
Loading.show(this)
fun showLoading(msg: String = "加载中……") {
Loading.show(
context = this,
message = msg
)
}
fun dismissLoading() {
@@ -178,9 +162,107 @@ open class BaseActivity : AppCompatActivity() {
fun delayDismissLoading() {
window.decorView.postDelayed({
Loading.dismiss()
},500)
}, 500)
}
val netViewModel: NetViewModel by viewModels()
fun queryGoodsList(
param: MutableMap<String, Any>,
onSuccess: (MutableList<CookFoodGoodsEntity>?) -> Unit,
onFailure: (String, String) -> Unit
) {
netViewModel.queryGoodsList(param, onSuccess, onFailure)
}
fun querySeasoningList(
param: MutableMap<String, Any>,
onSuccess: (MutableList<SeasoningEntity>?) -> Unit,
onFailure: (String, String) -> Unit
) {
netViewModel.querySeasoningList(param, onSuccess, onFailure)
}
private var permissionCallback: ((isGranted: Boolean) -> Unit)? = null
private var activityCallback: ((intent: Intent?) -> Unit)? = null
fun requestMultiplePermissions(
permissions: Array<String>,
callback: (isGranted: Boolean) -> Unit
) {
this.permissionCallback = callback
requestMultiplePermissionsLauncher.launch(permissions)
}
fun requestPermission(permission: String, callback: (isGranted: Boolean) -> Unit) {
this.permissionCallback = callback
requestPermissionLauncher.launch(permission)
}
val requestMultiplePermissionsLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
var isGranted = true
permissions.entries.forEach {
if (!it.value) {
isGranted = false
}
}
permissionCallback?.invoke(isGranted)
}
// 权限请求回调
val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
permissionCallback?.invoke(isGranted)
}
fun startActivity(intent: Intent, callback: (Intent?) -> Unit) {
this.activityCallback = callback
startActivityLauncher.launch(intent)
}
// activity页面返回的回调
private val startActivityLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
activityCallback?.invoke(it.data)
}
/**
* 窗口焦点变化时回调
* 场景:弹出 Dialog 后焦点丢失,Dialog 消失后焦点恢复,此时需重新隐藏系统栏,
* 避免系统栏在 Dialog 关闭后意外"复活"
*/
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) {
hideSystemBars()
}
}
/**
* 隐藏状态栏和导航栏
* 使用 WindowInsetsControllerCompatAndroidx 推荐方式,向下兼容性好)
* - setDecorFitsSystemWindows 只在首次调用时设置,避免反复触发布局重绘
* (重绘会导致 RecyclerView 滑动时抛出 "view is not a child, cannot hide" 异常)
* - 每次调用仍重新执行 hide(),保证系统栏在滑出后能被重新隐藏
*/
private var systemBarsConfigured = false
private fun hideSystemBars() {
if (!systemBarsConfigured) {
// 只执行一次:允许布局延伸到系统栏区域
WindowCompat.setDecorFitsSystemWindows(window, false)
systemBarsConfigured = true
}
WindowInsetsControllerCompat(window, window.decorView).apply {
hide(WindowInsetsCompat.Type.systemBars())
systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
}
}
@@ -5,9 +5,16 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.SharedPreferences
import android.util.Log
import com.shuwei.dish.match.R
import com.shuwei.dish.match.db.DatabaseProvider
import com.shuwei.dish.match.objbox.FoodModule
import com.shuwei.dish.match.objbox.ObjectBox
import com.shuwei.dish.match.scale.ScaleServiceManager
import com.shuwei.dish.match.ui.InitActivity.Companion.TAG
import com.shuwei.dish.match.utils.AppUtil
import com.shuwei.dish.match.utils.BootReceiver
import com.shuwei.dish.match.utils.CrashHandler
class BaseApp : Application() {
@@ -17,18 +24,40 @@ class BaseApp : Application() {
override fun onCreate() {
super.onCreate()
instance = this
CrashHandler.init(this)
val deviceId = AppUtil.getUDID(this)
Log.d(TAG, "onCreate: deviceId=$deviceId")
GlobalData.deviceId = deviceId
// 根据设备 ID 判断角色:指定 ID 为主设备,其余为子设备
GlobalData.deviceRole = if (deviceId == "8fc2ab34-2137-3112-acca-f884ea8736d4") {
DeviceRole.MASTER
} else {
DeviceRole.SLAVE
}
Log.d(TAG, "设备角色: ${GlobalData.deviceRole}")
// 启动秤服务(主设备多启动发现+聚合,子设备只启动服务端+注册)
ScaleServiceManager.start(this)
// GlobalData.deviceId = "39a7abdd06b3c7ab"
val filter = IntentFilter(Intent.ACTION_BOOT_COMPLETED)
registerReceiver(BootReceiver(), filter)
ObjectBox.init(this)
}
override fun onTerminate() {
ScaleServiceManager.stop()
super.onTerminate()
}
companion object {
// const val canteenId = "1678234139391512577"
var canteenId = "0"
var configUrl = ""
// var configUrl = ""
var token: String? = null
// var token: String? = null
// var deviceId: String? = null
var appVersion: String = "1"
@Volatile
private var sharedPref: SharedPreferences? = null
@@ -18,8 +18,16 @@ abstract class BaseFragment<VB : ViewBinding> : Fragment() {
abstract fun inflateBinding(inflater: LayoutInflater, container: ViewGroup?): VB
abstract fun initialize()
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initialize()
}
}
@@ -14,12 +14,8 @@ object GlobalData {
* 具体业务 BaseUrl
*/
const val TEST_BASE_URL = "http://192.168.1.201:14801"
const val UAT_BASE_URL = "https://dev.yixiong-tech.com:8083"
const val UAT_BASE_URL = "https://dev.yixiong-tech.com:8081"
const val PROD_BASE_URL = "https://api.dm.yixiong-tech.com:8443"
//临时用于测试
const val DEVICE_ID_2 = "8fc2ab34-2137-3112-acca-f884ea8736d4"
/**
* 设备id
*/
@@ -29,6 +25,8 @@ object GlobalData {
* 当前设备角色,启动时从 SpTool 读取,默认为 SLAVE
*/
var deviceRole: DeviceRole = DeviceRole.SLAVE
var appId = "Hkz1rBk6PZXbS8KwKr67K2eZtsz8bRoMHLg64bUdgCZj"
var sdkKey = "AabXs3sHM8UhhE7oCGf4LVMLrdkGb1nJNksbjjTdVt7k"
}
/**
@@ -39,5 +37,4 @@ object GlobalKey {
const val KEY_TOKEN = "tokenKey"
const val KEY_USER_INFO = "userInfoKey"
const val KEY_USER_NAME = "userNameKey"
const val KEY_BASE_URL = "baseUrl"
}
}
@@ -6,14 +6,10 @@ import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import com.shuwei.dish.match.db.dao.CookFoodDao
import com.shuwei.dish.match.db.dao.CookFoodGoodsDao
import com.shuwei.dish.match.db.dao.SeasoningDao
import com.shuwei.dish.match.db.dao.SeasoningSlotDao
import com.shuwei.dish.match.db.entity.CookFoodEntity
import com.shuwei.dish.match.db.entity.CookFoodGoodsEntity
import com.shuwei.dish.match.db.entity.SeasoningEntity
import com.shuwei.dish.match.db.entity.SeasoningSlotEntity
import com.shuwei.dish.match.db.dao.AppDao
import com.shuwei.dish.match.entity.CookFoodEntity
import com.shuwei.dish.match.entity.CookFoodGoodsEntity
import com.shuwei.dish.match.entity.SeasoningEntity
// 步骤1:更新版本号
@@ -23,20 +19,16 @@ import com.shuwei.dish.match.db.entity.SeasoningSlotEntity
SeasoningEntity::class,
CookFoodEntity::class,
CookFoodGoodsEntity::class,
SeasoningSlotEntity::class,
],
version = 13,
version = 7,
exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
// abstract fun seasoningDao(): SeasoningDao
// abstract fun cookFoodDao(): CookFoodDao
// abstract fun cookFoodGoodsDao(): CookFoodGoodsDao
abstract fun cookFoodDao(): CookFoodDao
abstract fun cookFoodGoodsDao(): CookFoodGoodsDao
abstract fun seasoningDao(): SeasoningDao
abstract fun seasoningSlotDao(): SeasoningSlotDao
abstract fun appDao(): AppDao
}
@@ -148,20 +140,6 @@ val MIGRATION_5_6 = MigrationImpl(5, 6) { db ->
db.endTransaction()
}
val MIGRATION_7_8 = MigrationImpl(7, 8) { db ->
db.execSQL(
"""
CREATE TABLE IF NOT EXISTS `dm_seasoning_slot` (
`deviceId` TEXT NOT NULL,
`address` INTEGER NOT NULL,
`goodsId` TEXT NOT NULL,
`goodsName` TEXT NOT NULL,
PRIMARY KEY(`deviceId`, `address`)
)
""".trimIndent()
)
}
val MIGRATION_6_7 = MigrationImpl(6, 7) {db ->
runCatching {
@@ -214,48 +192,6 @@ val MIGRATION_6_7 = MigrationImpl(6, 7) {db ->
}
}
/**
* 版本8→9:为 dm_cook_food_goods 和 dm_seasoning 表新增 zjmCode、materId 字段
*/
val MIGRATION_8_9 = MigrationImpl(8, 9) { db ->
db.execSQL("ALTER TABLE dm_cook_food_goods ADD COLUMN zjmCode TEXT DEFAULT ''")
db.execSQL("ALTER TABLE dm_cook_food_goods ADD COLUMN materId TEXT DEFAULT ''")
db.execSQL("ALTER TABLE dm_seasoning ADD COLUMN zjmCode TEXT DEFAULT ''")
db.execSQL("ALTER TABLE dm_seasoning ADD COLUMN materId TEXT DEFAULT ''")
}
/**
* 版本9→10:删除 dm_cook_food 表 foodId 唯一索引
* 改为由应用层保证同一 foodId 且 isDel=0 只有一条记录,以支持逻辑删除
*/
val MIGRATION_9_10 = MigrationImpl(9, 10) { db ->
db.execSQL("DROP INDEX IF EXISTS dm_cook_food_foodId_IDX")
}
/**
* 版本10→11:为 dm_cook_food_goods 和 dm_seasoning 表新增 goodsCode(物料编码)字段
*/
val MIGRATION_10_11 = MigrationImpl(10, 11) { db ->
db.execSQL("ALTER TABLE dm_cook_food_goods ADD COLUMN goodsCode TEXT DEFAULT ''")
db.execSQL("ALTER TABLE dm_seasoning ADD COLUMN goodsCode TEXT DEFAULT ''")
}
/**
* 版本11→12:为 dm_cook_food_goods 和 dm_seasoning 表新增 rawMaterialsType(净材种类)字段
*/
val MIGRATION_11_12 = MigrationImpl(11, 12) { db ->
db.execSQL("ALTER TABLE dm_cook_food_goods ADD COLUMN rawMaterialsType TEXT DEFAULT NULL")
db.execSQL("ALTER TABLE dm_seasoning ADD COLUMN rawMaterialsType TEXT DEFAULT NULL")
}
/**
* 版本12→13:为 dm_cook_food 表新增 submitState(接口提交状态)字段
* 0-未提交(本地草稿/手动删除),1-接口提交成功后删除
*/
val MIGRATION_12_13 = MigrationImpl(12, 13) { db ->
db.execSQL("ALTER TABLE dm_cook_food ADD COLUMN submitState INTEGER NOT NULL DEFAULT 0")
}
class DatabaseProvider(private val context: Context) {
// 单例模式确保全局唯一实例‌
val instance: AppDatabase by lazy {
@@ -274,12 +210,6 @@ class DatabaseProvider(private val context: Context) {
// .addMigrations(MIGRATION_4_5)
// .addMigrations(MIGRATION_5_6)
// .addMigrations(MIGRATION_6_7)
.addMigrations(MIGRATION_7_8)
.addMigrations(MIGRATION_8_9)
.addMigrations(MIGRATION_9_10)
.addMigrations(MIGRATION_10_11)
.addMigrations(MIGRATION_11_12)
.addMigrations(MIGRATION_12_13)
.build()
}
}
@@ -0,0 +1,142 @@
package com.shuwei.dish.match.db
import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.db.dao.AppDao
import com.shuwei.dish.match.entity.CookFoodEntity
import com.shuwei.dish.match.entity.CookFoodGoodsEntity
import com.shuwei.dish.match.entity.SeasoningEntity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class AppRepository(val appDao: AppDao) {
suspend fun insertCookFood(item: CookFoodEntity) = withContext(Dispatchers.IO) {
appDao.insertCookFood(item)
}
suspend fun updateCookFood(item: CookFoodEntity) = withContext(Dispatchers.IO) {
appDao.updateCookFood(item)
}
suspend fun getCookFoodById(foodId: String, cookMode: Int) = withContext(Dispatchers.IO) {
appDao.getCookFoodById(BaseApp.canteenId, foodId, cookMode)
}
suspend fun getCookFoodList(cookMode: Int, dinnerType: String) = withContext(Dispatchers.IO) {
appDao.getCookFoodList(BaseApp.canteenId, cookMode, dinnerType)
}
suspend fun countCookFood(cookMode: Int) = withContext(Dispatchers.IO) {
appDao.countCookFood(BaseApp.canteenId, cookMode)
}
suspend fun deleteCookFood(foodId: String, cookMode: Int) = withContext(Dispatchers.IO) {
appDao.deleteCookFood(BaseApp.canteenId, foodId, cookMode)
}
suspend fun deleteCookFoodGoodsList(pid:Long, foodId: String) = withContext(Dispatchers.IO) {
appDao.deleteCookFoodGoodsList(pid, foodId)
}
//--------------------------------------------------------------------------------------------
// suspend fun insertGoods(item: CookFoodGoodsEntity) = withContext(Dispatchers.IO) {
// appDao.insertGoods(item)
// }
suspend fun insertGoodsList(items: MutableList<CookFoodGoodsEntity>) =
withContext(Dispatchers.IO) {
appDao.insertGoodsList(items)
}
// suspend fun updateGoods(item: CookFoodGoodsEntity) = withContext(Dispatchers.IO) {
// appDao.updateGoods(item)
// }
suspend fun getCookFoodGoodsList(pid:Long, foodId: String) = withContext(Dispatchers.IO) {
appDao.getCookFoodGoodsList(pid, foodId)
}
suspend fun getCookFoodGoodsListByType(
foodId: String,
materialType: Int
) = withContext(Dispatchers.IO) {
appDao.getCookFoodGoodsListByType(foodId, materialType)
}
//--------------------------------------------------------------------------------------------
suspend fun getSeasoningById(id: Long) = withContext(Dispatchers.IO) {
appDao.getSeasoningById(id)
}
suspend fun getSeasoningBySort(sort:Int) = withContext(Dispatchers.IO) {
appDao.getSeasoningBySort(sort)
}
suspend fun getSeasoningByGoodsId(goodsId: Int) = withContext(Dispatchers.IO) {
appDao.getSeasoningByGoodsId(goodsId)
}
suspend fun getAllStream() = withContext(Dispatchers.IO) {
appDao.getAllStream()
}
suspend fun search(query: String) = withContext(Dispatchers.IO) {
appDao.search(query)
}
suspend fun clearAllSeasoning() = withContext(Dispatchers.IO) {
appDao.clearAllSeasoning()
}
suspend fun deleteSeasoningBySort(sort: Int) = withContext(Dispatchers.IO) {
appDao.deleteSeasoningBySort(sort)
}
suspend fun insertSeasoningList(items: MutableList<SeasoningEntity>) =
withContext(Dispatchers.IO) {
appDao.insertSeasoningList(items)
}
suspend fun updateSeasoning(item: SeasoningEntity) = withContext(Dispatchers.IO) {
appDao.updateSeasoning(item)
}
suspend fun insertSeasoning(item: SeasoningEntity) = withContext(Dispatchers.IO) {
appDao.insertSeasoning(item)
}
}
//
//class SeasoningRepository(val seasoningDao: SeasoningDao) {
// val seasoningList: Flow<List<SeasoningEntity>> = seasoningDao.getAllStream()
//
// fun addSeasoning(item: SeasoningEntity) = withContext(Dispatchers.IO) {
// seasoningDao.insert(item)
// }
//
// fun searchSeasoning(query: String) = withContext(Dispatchers.IO) {
// seasoningDao.search(query)
// }
//}
//
//class CookFoodRepository(val cookFoodDao: CookFoodDao) {
// fun getCookFood(foodId: String, cookMode:Int = 0) = withContext(Dispatchers.IO) {
// cookFoodDao.getCookFoodById(foodId, cookMode)
// }
//
// fun getCookFoodByList(cookMode:Int = 0) = withContext(Dispatchers.IO) {
// cookFoodDao.getCookFoodList(cookMode)
// }
//
// fun countCookFood(cookMode:Int = 0)= withContext(Dispatchers.IO) {
// cookFoodDao.countCookFood(cookMode)
// }
//}
//
//class CookFoodGoodsRepository(val cookFoodGoodsDao: CookFoodGoodsDao) {
// fun getFoodGoodsList(foodId: String) = withContext(Dispatchers.IO) {
// cookFoodGoodsDao.getCookFoodGoodsList(foodId)
// }
//
//}
@@ -1,315 +0,0 @@
package com.shuwei.dish.match.db
import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.db.entity.CookFoodEntity
import com.shuwei.dish.match.db.entity.CookFoodGoodsEntity
import com.shuwei.dish.match.db.entity.SeasoningEntity
import com.shuwei.dish.match.db.entity.SeasoningSlotEntity
import androidx.sqlite.db.SupportSQLiteQuery
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class DbRepository {
private val db = BaseApp.instance!!.database
suspend fun insertCookFood(item: CookFoodEntity) = withContext(Dispatchers.IO) {
db.cookFoodDao().insertCookFood(item)
}
suspend fun updateCookFood(item: CookFoodEntity) = withContext(Dispatchers.IO) {
db.cookFoodDao().updateCookFood(item)
}
suspend fun getCookFoodById(foodId: String, cookMode: Int, dinnerType: String) = withContext(Dispatchers.IO) {
db.cookFoodDao().getCookFoodById(BaseApp.canteenId, foodId, cookMode, dinnerType)
}
suspend fun getCookFoodList(cookMode: Int, dinnerType: String) = withContext(Dispatchers.IO) {
db.cookFoodDao().getCookFoodList(BaseApp.canteenId, cookMode, dinnerType)
}
suspend fun countCookFood(cookMode: Int) = withContext(Dispatchers.IO) {
db.cookFoodDao().countCookFood(BaseApp.canteenId, cookMode)
}
suspend fun deleteCookFood(foodId: String, cookMode: Int, dinnerType: String) = withContext(Dispatchers.IO) {
db.cookFoodDao().deleteCookFood(BaseApp.canteenId, foodId, cookMode, dinnerType)
}
/** 接口提交成功后逻辑删除菜品,同时把 submitState 置 1 */
suspend fun markSubmittedAndDelete(foodId: String, cookMode: Int, dinnerType: String) = withContext(Dispatchers.IO) {
db.cookFoodDao().markSubmittedAndDelete(BaseApp.canteenId, foodId, cookMode, dinnerType)
}
suspend fun deleteCookFoodGoodsList(pid: Long, foodId: String) = withContext(Dispatchers.IO) {
db.cookFoodGoodsDao().deleteCookFoodGoodsList(pid, foodId)
}
/** 仅逻辑删除调料行(materialType = 3),主辅材不动 */
suspend fun deleteSeasoningOnly(pid: Long, foodId: String) = withContext(Dispatchers.IO) {
db.cookFoodGoodsDao().deleteSeasoningOnly(pid, foodId)
}
//--------------------------------------------------------------------------------------------
suspend fun insertGoodsList(items: MutableList<CookFoodGoodsEntity>) =
withContext(Dispatchers.IO) {
db.cookFoodGoodsDao().insertGoodsList(items)
}
suspend fun getCookFoodGoodsList(pid: Long, foodId: String) = withContext(Dispatchers.IO) {
db.cookFoodGoodsDao().getCookFoodGoodsList(pid, foodId)
}
suspend fun getCookFoodGoodsListByType(
foodId: String,
materialType: Int
) = withContext(Dispatchers.IO) {
db.cookFoodGoodsDao().getCookFoodGoodsListByType(foodId, materialType)
}
//--------------------------------------------------------------------------------------------
suspend fun getSeasoningById(id: Long) = withContext(Dispatchers.IO) {
db.seasoningDao().getSeasoningById(id)
}
suspend fun getSeasoningBySort(sort: Int) = withContext(Dispatchers.IO) {
db.seasoningDao().getSeasoningBySort(sort)
}
suspend fun getSeasoningByGoodsId(goodsId: String) = withContext(Dispatchers.IO) {
db.seasoningDao().getSeasoningByGoodsId(goodsId)
}
suspend fun getAllStream() = withContext(Dispatchers.IO) {
db.seasoningDao().getAllStream()
}
suspend fun search(query: String) = withContext(Dispatchers.IO) {
db.seasoningDao().search(query)
}
suspend fun clearAllSeasoning() = withContext(Dispatchers.IO) {
db.seasoningDao().clearAllSeasoning()
}
suspend fun deleteSeasoningBySort(sort: Int) = withContext(Dispatchers.IO) {
db.seasoningDao().deleteSeasoningBySort(sort)
}
suspend fun insertSeasoningList(items: MutableList<SeasoningEntity>) =
withContext(Dispatchers.IO) {
db.seasoningDao().insertSeasoningList(items)
}
suspend fun updateSeasoning(item: SeasoningEntity) = withContext(Dispatchers.IO) {
db.seasoningDao().updateSeasoning(item)
}
suspend fun insertSeasoning(item: SeasoningEntity) = withContext(Dispatchers.IO) {
db.seasoningDao().insertSeasoning(item)
}
/**
* 按 goodsId 查重后 upsert:存在则更新,不存在则插入
* 确保同一个 goodsId 在 dm_seasoning 中只有一条记录
*/
suspend fun upsertSeasoningByGoodsId(item: SeasoningEntity) = withContext(Dispatchers.IO) {
val existing = db.seasoningDao().getSeasoningByGoodsId(item.goodsId)
if (existing != null) {
db.seasoningDao().updateSeasoning(item.also { it.id = existing.id })
} else {
item.id = 0
db.seasoningDao().insertSeasoning(item)
}
}
/**
* 查询指定设备是否已配置调料槽位
* @param deviceId 目标设备 ID
* @return true 表示已有配置,false 表示未配置
*/
suspend fun hasSeasoningSlotConfig(deviceId: String) = withContext(Dispatchers.IO) {
db.seasoningSlotDao().queryByDeviceId(deviceId).isNotEmpty()
}
/**
* 查询所有设备的调料槽位配置
*/
suspend fun getAllSeasoningSlots() = withContext(Dispatchers.IO) {
db.seasoningSlotDao().queryAll()
}
/**
* 查询指定设备的调料槽位配置
* @param deviceId 目标设备 ID
*/
suspend fun getSeasoningSlotsByDeviceId(deviceId: String) = withContext(Dispatchers.IO) {
db.seasoningSlotDao().queryByDeviceId(deviceId)
}
/**
* 插入或更新单个调料槽位配置
*/
suspend fun upsertSeasoningSlot(slot: SeasoningSlotEntity) = withContext(Dispatchers.IO) {
db.seasoningSlotDao().upsert(slot)
}
/**
* 批量插入或更新调料槽位配置
*/
suspend fun upsertAllSeasoningSlots(slots: List<SeasoningSlotEntity>) = withContext(Dispatchers.IO) {
db.seasoningSlotDao().upsertAll(slots)
}
/**
* 删除指定设备的指定槽位配置
* @param deviceId 目标设备 ID
* @param address 秤硬件地址
*/
suspend fun deleteSeasoningSlot(deviceId: String, address: Int) = withContext(Dispatchers.IO) {
db.seasoningSlotDao().deleteByDeviceIdAndAddress(deviceId, address)
}
/**
* 删除指定设备的全部槽位配置
* @param deviceId 目标设备 ID
*/
suspend fun deleteAllSlotsByDeviceId(deviceId: String) = withContext(Dispatchers.IO) {
db.seasoningSlotDao().deleteAllByDeviceId(deviceId)
}
// ---- 调试用:查询各表全量数据(含已删除记录)----
/** 查询 dm_cook_food 全部记录 */
suspend fun getAllCookFood() = withContext(Dispatchers.IO) {
db.cookFoodDao().getAllCookFood()
}
/** 查询 dm_cook_food_goods 全部记录 */
suspend fun getAllCookFoodGoods() = withContext(Dispatchers.IO) {
db.cookFoodGoodsDao().getAllCookFoodGoods()
}
/** 查询 dm_seasoning 全部记录 */
suspend fun getAllSeasoning() = withContext(Dispatchers.IO) {
db.seasoningDao().getAllSeasoning()
}
// ---- 调试用:分页查询(含 isDel 过滤)----
suspend fun getCookFoodPaged(showDel: Boolean, limit: Int, offset: Int) =
withContext(Dispatchers.IO) {
db.cookFoodDao().getCookFoodPaged(if (showDel) 1 else 0, limit, offset)
}
suspend fun countCookFoodAll(showDel: Boolean) = withContext(Dispatchers.IO) {
db.cookFoodDao().countCookFoodAll(if (showDel) 1 else 0)
}
suspend fun getCookFoodGoodsPaged(showDel: Boolean, limit: Int, offset: Int) =
withContext(Dispatchers.IO) {
db.cookFoodGoodsDao().getCookFoodGoodsPaged(if (showDel) 1 else 0, limit, offset)
}
suspend fun countCookFoodGoodsAll(showDel: Boolean) = withContext(Dispatchers.IO) {
db.cookFoodGoodsDao().countCookFoodGoodsAll(if (showDel) 1 else 0)
}
suspend fun getSeasoningPaged(showDel: Boolean, limit: Int, offset: Int) =
withContext(Dispatchers.IO) {
db.seasoningDao().getSeasoningPaged(if (showDel) 1 else 0, limit, offset)
}
suspend fun countSeasoningAll(showDel: Boolean) = withContext(Dispatchers.IO) {
db.seasoningDao().countSeasoningAll(if (showDel) 1 else 0)
}
suspend fun getSlotPaged(limit: Int, offset: Int) = withContext(Dispatchers.IO) {
db.seasoningSlotDao().getSlotPaged(limit, offset)
}
suspend fun countSlotAll() = withContext(Dispatchers.IO) {
db.seasoningSlotDao().countSlotAll()
}
// ---- 调试用:动态条件过滤分页查询 ----
suspend fun getCookFoodPagedFiltered(query: SupportSQLiteQuery) = withContext(Dispatchers.IO) {
db.cookFoodDao().queryPagedFiltered(query)
}
suspend fun countCookFoodFiltered(query: SupportSQLiteQuery) = withContext(Dispatchers.IO) {
db.cookFoodDao().countFiltered(query)
}
suspend fun getCookFoodGoodsPagedFiltered(query: SupportSQLiteQuery) = withContext(Dispatchers.IO) {
db.cookFoodGoodsDao().queryPagedFiltered(query)
}
suspend fun countCookFoodGoodsFiltered(query: SupportSQLiteQuery) = withContext(Dispatchers.IO) {
db.cookFoodGoodsDao().countFiltered(query)
}
suspend fun getSeasoningPagedFiltered(query: SupportSQLiteQuery) = withContext(Dispatchers.IO) {
db.seasoningDao().queryPagedFiltered(query)
}
suspend fun countSeasoningFiltered(query: SupportSQLiteQuery) = withContext(Dispatchers.IO) {
db.seasoningDao().countFiltered(query)
}
suspend fun getSlotPagedFiltered(query: SupportSQLiteQuery) = withContext(Dispatchers.IO) {
db.seasoningSlotDao().queryPagedFiltered(query)
}
suspend fun countSlotFiltered(query: SupportSQLiteQuery) = withContext(Dispatchers.IO) {
db.seasoningSlotDao().countFiltered(query)
}
/**
* 物理清空全部 4 张表的测试数据
* 清除顺序:先删子表(食材),再删主表(菜品),最后删配置表(调料、槽位)
*/
suspend fun clearAllData() = withContext(Dispatchers.IO) {
db.cookFoodGoodsDao().deleteAll()
db.cookFoodDao().deleteAll()
db.seasoningDao().deleteAll()
db.seasoningSlotDao().deleteAll()
}
}
//
//class SeasoningRepository(val seasoningDao: SeasoningDao) {
// val seasoningList: Flow<List<SeasoningEntity>> = seasoningDao.getAllStream()
//
// fun addSeasoning(item: SeasoningEntity) = withContext(Dispatchers.IO) {
// seasoningDao.insert(item)
// }
//
// fun searchSeasoning(query: String) = withContext(Dispatchers.IO) {
// seasoningDao.search(query)
// }
//}
//
//class CookFoodRepository(val cookFoodDao: CookFoodDao) {
// fun getCookFood(foodId: String, cookMode:Int = 0) = withContext(Dispatchers.IO) {
// cookFoodDao.getCookFoodById(foodId, cookMode)
// }
//
// fun getCookFoodByList(cookMode:Int = 0) = withContext(Dispatchers.IO) {
// cookFoodDao.getCookFoodList(cookMode)
// }
//
// fun countCookFood(cookMode:Int = 0)= withContext(Dispatchers.IO) {
// cookFoodDao.countCookFood(cookMode)
// }
//}
//
//class CookFoodGoodsRepository(val cookFoodGoodsDao: CookFoodGoodsDao) {
// fun getFoodGoodsList(foodId: String) = withContext(Dispatchers.IO) {
// cookFoodGoodsDao.getCookFoodGoodsList(foodId)
// }
//
//}
@@ -1,623 +0,0 @@
package com.shuwei.dish.match.db
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.sqlite.db.SimpleSQLiteQuery
import com.shuwei.dish.match.model.CollectedFoodItem
import com.shuwei.dish.match.db.entity.CookFoodEntity
import com.shuwei.dish.match.db.entity.CookFoodGoodsEntity
import com.shuwei.dish.match.db.entity.SeasoningEntity
import com.shuwei.dish.match.db.entity.SeasoningSlotEntity
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
class DbViewModel : ViewModel() {
private val rep = DbRepository()
fun updateCookFood(entity: CookFoodEntity) {
viewModelScope.launch {
rep.updateCookFood(entity)
}
}
private val _cookFoodDetailState = MutableStateFlow<CookFoodEntity?>(null)
val cookFoodDetailState: StateFlow<CookFoodEntity?> = _cookFoodDetailState.asStateFlow()
fun getCookFoodById(foodId: String, cookMode: Int, dinnerType: String) {
viewModelScope.launch {
_cookFoodDetailState.value = rep.getCookFoodById(foodId, cookMode, dinnerType)
}
}
private val _cookFoodListState = MutableStateFlow<MutableList<CookFoodEntity>?>(null)
val cookFoodListState: StateFlow<MutableList<CookFoodEntity>?> = _cookFoodListState.asStateFlow()
fun resetCookFoodList() {
_cookFoodListState.value = null
}
fun getCookFoodList(
cookMode: Int,
dinnerType: String = "0"
) {
viewModelScope.launch {
_cookFoodListState.value = rep.getCookFoodList(cookMode, dinnerType)
}
}
/**
* 查询本地烹饪中数据(回调版本),结果通过 onResult 直接回调,不经过 StateFlow
* 适合不需要跨生命周期持续监听的一次性查询场景
*/
fun getCookFoodListWithCallback(
cookMode: Int,
dinnerType: String = "0",
onResult: (MutableList<CookFoodEntity>?) -> Unit
) {
viewModelScope.launch {
onResult(rep.getCookFoodList(cookMode, dinnerType))
}
}
/**
* 直接返回本地烹饪中数据,供需要串行等待结果的场景使用(如先查库再发网络请求)
*/
suspend fun getCookFoodListDirect(
cookMode: Int,
dinnerType: String = "0"
): MutableList<CookFoodEntity>? {
return rep.getCookFoodList(cookMode, dinnerType)
}
suspend fun countCookFood(cookMode: Int): Int {
return rep.countCookFood(cookMode)
}
// fun saveCookFood(cookMode: Int, entity: CookFoodEntity) {
// viewModelScope.launch {
// val data = rep.getCookFoodById(entity.foodId, cookMode)
// if (data == null) {
// rep.insertCookFood(entity)
// return@launch
// }
// rep.updateCookFood(entity)
// }
// }
suspend fun saveCookFoodAndGoods(
cookMode: Int,
entity: CookFoodEntity,
list: MutableList<CookFoodGoodsEntity>?
) {
val data = rep.getCookFoodById(entity.foodId, cookMode, entity.dinnerType ?: "0")
if (data == null) {
// 首次保存:主表 + 主辅材 + 调料 全量 insert
rep.insertCookFood(entity)
val newData = rep.getCookFoodById(entity.foodId, cookMode, entity.dinnerType ?: "0")
// 回填 id,防止下次调用时 getCookFoodById 查不到而重复插入主辅材
newData?.let { entity.id = it.id }
list?.let {
it.forEach { goods ->
goods.id = 0
goods.pid = newData?.id ?: 0
}
rep.insertGoodsList(it)
}
} else {
// 已存在:主表与主辅材保持不变,仅重建调料部分,避免主表/主辅材累积逻辑删除脏数据
rep.deleteSeasoningOnly(data.id, entity.foodId)
val seasoningList = list?.filter { it.materialType == 3 }?.toMutableList()
if (!seasoningList.isNullOrEmpty()) {
seasoningList.forEach { goods ->
goods.id = 0
goods.pid = data.id
goods.foodId = entity.foodId
}
rep.insertGoodsList(seasoningList)
}
}
}
suspend fun deleteCookFoodAndGoods(cookMode: Int, foodId: String, dinnerType: String) {
val data = rep.getCookFoodById(foodId, cookMode, dinnerType)
if (data != null) {
rep.deleteCookFood(foodId = foodId, cookMode = cookMode, dinnerType = dinnerType)
rep.deleteCookFoodGoodsList(pid = data.id, foodId = foodId)
}
}
/**
* 接口提交成功后删除本地菜品草稿(异步版本,绑定 viewModelScope
* 与 [markSubmittedAndDelete] 的区别:不是 suspend,内部用 viewModelScope 启动协程,
* 不受调用方 Activity 生命周期影响,适合 singleTask 跳转前触发删除的场景
*/
fun markSubmittedAndDeleteAsync(cookMode: Int, foodId: String, dinnerType: String) {
viewModelScope.launch {
markSubmittedAndDelete(cookMode, foodId, dinnerType)
}
}
/**
* 接口提交成功后删除本地菜品草稿
* 与 [deleteCookFoodAndGoods] 的区别:主表 submitState 会被置 1,便于审计区分手动删除与接口删除
*/
suspend fun markSubmittedAndDelete(cookMode: Int, foodId: String, dinnerType: String) {
val data = rep.getCookFoodById(foodId, cookMode, dinnerType)
if (data != null) {
rep.markSubmittedAndDelete(foodId = foodId, cookMode = cookMode, dinnerType = dinnerType)
rep.deleteCookFoodGoodsList(pid = data.id, foodId = foodId)
}
}
//--------------------------------------------------------------------------------------------------------
suspend fun getCookFoodGoodsList(
foodId: String,
cookMode: Int,
dinnerType: String
): MutableList<CookFoodGoodsEntity> {
val cookFood = rep.getCookFoodById(foodId, cookMode, dinnerType)
return if (cookFood != null) rep.getCookFoodGoodsList(pid = cookFood.id, foodId = cookFood.foodId)
else mutableListOf()
}
// fun updateGoods(entity: CookFoodGoodsEntity) {
// viewModelScope.launch {
// rep.updateGoods(entity)
// }
// }
// fun saveCookFoodGoods(list: MutableList<CookFoodGoodsEntity>) {
// viewModelScope.launch {
// val foodId = list[0].foodId
// val data: MutableList<CookFoodGoodsEntity>? =
// rep.getCookFoodGoodsList(foodId!!)
// if (data.isNullOrEmpty()) {
// rep.insertGoodsList(list)
// return@launch
// }
// val localDbSeasoningList = rep.getCookFoodGoodsListByType(foodId, 3)
// localDbSeasoningList.forEach {
// it.isDel = 1
// //更新本地调料数据为已删除的状态
// rep.updateGoods(it)
// }
// //筛选出调料数据保存,主辅材不会变更不需要再次处理
// val saveSeasoningList = list.filter { it.materialType == 3 }
// rep.insertGoodsList(saveSeasoningList.toMutableList())
// }
// }
//--------------------------------------------------------------------------------------------------------
// fun addSeasoning(list: MutableList<SeasoningEntity>, callback: () -> Unit) {
// viewModelScope.launch {
// rep.deleteBatch()
// rep.insertSeasoningList(items = list)
// callback()
// }
// }
private val loadSeasoningMutex = Mutex()
/**
* 本地调料列表状态流
*/
private val _seasoningState = MutableStateFlow<MutableList<SeasoningEntity>>(mutableListOf())
val seasoningState: StateFlow<MutableList<SeasoningEntity>> = _seasoningState.asStateFlow()
fun loadSeasoning() {
if (loadSeasoningMutex.isLocked) return
viewModelScope.launch {
loadSeasoningMutex.withLock {
_seasoningState.value = rep.getAllStream()
}
}
}
/**
* 所有设备调料槽位配置的 UI 状态流
*/
private val _seasoningSlotState = MutableStateFlow<List<SeasoningSlotEntity>>(emptyList())
val seasoningSlotState: StateFlow<List<SeasoningSlotEntity>> = _seasoningSlotState.asStateFlow()
/**
* 查询所有调料槽位配置,更新 seasoningSlotState 并返回结果
* 声明为 suspend,调用方可在同一协程中直接使用返回值,无需等待 StateFlow 更新
*/
suspend fun loadSeasoningSlot(): List<SeasoningSlotEntity> {
val slots = rep.getAllSeasoningSlots()
_seasoningSlotState.value = slots
return slots
}
fun deleteSeasoningBySort(sort: Int) {
viewModelScope.launch {
rep.deleteSeasoningBySort(sort)
}
}
private val _clearAllSeasoningState = MutableStateFlow(false)
val clearAllSeasoningState: StateFlow<Boolean> = _clearAllSeasoningState.asStateFlow()
fun clearAllSeasoning() {
viewModelScope.launch {
rep.clearAllSeasoning()
_clearAllSeasoningState.value = true
}
}
private val _hasSeasoningSlotConfigState = MutableStateFlow(false)
val hasSeasoningSlotConfigState: StateFlow<Boolean> = _hasSeasoningSlotConfigState.asStateFlow()
/**
* 查询指定设备是否已配置调料槽位
* @param deviceId 目标设备 ID
*/
fun hasSeasoningSlotConfig(deviceId: String) {
viewModelScope.launch {
_hasSeasoningSlotConfigState.value = rep.hasSeasoningSlotConfig(deviceId)
}
}
/**
* 查询指定设备的调料槽位配置
*/
suspend fun getSeasoningSlotsByDeviceId(deviceId: String) =
rep.getSeasoningSlotsByDeviceId(deviceId)
/**
* 插入或更新单个调料槽位配置
*/
suspend fun upsertSeasoningSlot(slot: SeasoningSlotEntity) =
rep.upsertSeasoningSlot(slot)
/**
* 批量插入或更新调料槽位配置
*/
suspend fun upsertAllSeasoningSlots(slots: List<SeasoningSlotEntity>) =
rep.upsertAllSeasoningSlots(slots)
/**
* 删除指定设备的指定槽位配置
* @param deviceId 目标设备 ID
* @param address 秤硬件地址
*/
suspend fun deleteSeasoningSlot(deviceId: String, address: Int) =
rep.deleteSeasoningSlot(deviceId, address)
/**
* 删除指定设备的全部槽位配置(子设备接收全量广播时先清空再写入)
* @param deviceId 目标设备 ID
*/
suspend fun deleteAllSlotsByDeviceId(deviceId: String) =
rep.deleteAllSlotsByDeviceId(deviceId)
// ---- 数据库调试查看 ----
/**
* 物理清空全部 4 张表的测试数据,完成后通过回调通知 UI
* @param onDone 清除完成后在主线程执行的回调
*/
fun clearAllData(onDone: () -> Unit) {
viewModelScope.launch {
rep.clearAllData()
onDone()
}
}
companion object {
/** 每页加载条数 */
const val DB_INSPECT_PAGE_SIZE = 50
}
/**
* 单条数据库记录的展示模型
* @param isDel 是否已逻辑删除
* @param fields 字段名-值对列表,按顺序排列
* @param columnsPerRow 每行显示的列数,默认 3;槽位表传 1 保持单列
*/
data class DbRecord(
val isDel: Boolean,
val fields: List<Pair<String, String>>,
val columnsPerRow: Int = 3
)
/**
* 数据库调试页面 UI 状态
* @param items 当前已加载的记录列表
* @param totalCount 当前过滤条件下的总记录数
* @param hasMore 是否还有更多数据可加载
* @param isLoading 是否正在加载(首次加载或加载更多)
* @param filter 当前生效的筛选条件,供"加载更多"复用
*/
data class DbInspectUiState(
val items: List<DbRecord> = emptyList(),
val totalCount: Int = 0,
val hasMore: Boolean = false,
val isLoading: Boolean = false,
val filter: DbInspectFilter = DbInspectFilter()
)
/**
* 数据库调试页面筛选条件
* @param showDel 是否包含已删除记录
* @param exactFields 精确匹配字段:列名 → 值
* @param likeFields 模糊匹配字段:列名 → 值(自动加 %value%)
*/
data class DbInspectFilter(
val showDel: Boolean = true,
val exactFields: Map<String, String> = emptyMap(),
val likeFields: Map<String, String> = emptyMap()
)
private val _dbInspectState = MutableStateFlow(DbInspectUiState())
val dbInspectState: StateFlow<DbInspectUiState> = _dbInspectState.asStateFlow()
/**
* 加载指定表的第一页数据(切换 Tab / 修改筛选条件时调用)
* @param tableIndex 0=菜品 1=食材 2=调料 3=槽位
* @param filter 筛选条件,包含 showDel 和各字段过滤值
*/
fun loadDbInspect(tableIndex: Int, filter: DbInspectFilter) {
viewModelScope.launch {
// 立即清空旧数据并显示加载状态,避免切换 Tab 时旧数据闪现
_dbInspectState.value = DbInspectUiState(isLoading = true, filter = filter)
val (items, total) = queryPage(tableIndex, filter, offset = 0)
_dbInspectState.value = DbInspectUiState(
items = items,
totalCount = total,
hasMore = items.size < total,
isLoading = false,
filter = filter
)
}
}
/**
* 追加加载下一页数据(点击"加载更多"时调用)
* 筛选条件从当前 state 中复用,无需重新传入
* @param tableIndex 0=菜品 1=食材 2=调料 3=槽位
*/
fun loadMoreDbInspect(tableIndex: Int) {
viewModelScope.launch {
val current = _dbInspectState.value
// 已在加载中或无更多数据时直接返回,防止重复请求
if (!current.hasMore || current.isLoading) return@launch
_dbInspectState.value = current.copy(isLoading = true)
val (newItems, total) = queryPage(tableIndex, current.filter, offset = current.items.size)
val merged = current.items + newItems
_dbInspectState.value = DbInspectUiState(
items = merged,
totalCount = total,
hasMore = merged.size < total,
isLoading = false,
filter = current.filter
)
}
}
/**
* 执行分页查询,返回 DbRecord 列表和总记录数
* 通过 buildQueries 将 filter 转换为动态 SQL
*/
private suspend fun queryPage(
tableIndex: Int,
filter: DbInspectFilter,
offset: Int
): Pair<List<DbRecord>, Int> {
val limit = DB_INSPECT_PAGE_SIZE
return when (tableIndex) {
0 -> {
val (pageQ, countQ) = buildQueries("dm_cook_food", "createTime DESC", true, filter, limit, offset)
val total = rep.countCookFoodFiltered(countQ)
val rows = rep.getCookFoodPagedFiltered(pageQ).map { e ->
DbRecord(
isDel = e.isDel == 1,
fields = listOf(
"id" to "${e.id}", "foodId" to "${e.foodId}", "foodName" to "${e.foodName}",
"canteenId" to "${e.canteenId}", "foodWeight" to "${e.foodWeight}", "cookMode" to "${e.cookMode}",
"dinnerType" to "${e.dinnerType}", "isDel" to "${e.isDel}", "submitState" to "${e.submitState}",
"createTime" to "${e.createTime}"
)
)
}
rows to total
}
1 -> {
val (pageQ, countQ) = buildQueries("dm_cook_food_goods", "createTime DESC", true, filter, limit, offset)
val total = rep.countCookFoodGoodsFiltered(countQ)
val rows = rep.getCookFoodGoodsPagedFiltered(pageQ).map { e ->
DbRecord(
isDel = e.isDel == 1,
fields = listOf(
"id" to "${e.id}", "pid" to "${e.pid}", "goodsId" to "${e.goodsId}",
"goodsName" to "${e.goodsName}", "foodId" to "${e.foodId}", "materialType" to "${e.materialType}",
"relateionType" to "${e.relateionType}", "allEdible" to "${e.allEdible}", "useWeight" to "${e.useWeight}",
"sort" to "${e.sort}", "zjmCode" to "${e.zjmCode}", "materId" to "${e.materId}",
"isDel" to "${e.isDel}", "createTime" to "${e.createTime}"
)
)
}
rows to total
}
2 -> {
val (pageQ, countQ) = buildQueries("dm_seasoning", "sort ASC", true, filter, limit, offset)
val total = rep.countSeasoningFiltered(countQ)
val rows = rep.getSeasoningPagedFiltered(pageQ).map { e ->
DbRecord(
isDel = e.isDel == 1,
fields = listOf(
"id" to "${e.id}", "goodsId" to "${e.goodsId}", "goodsName" to "${e.goodsName}",
"sort" to "${e.sort}", "materialType" to "${e.materialType}", "useWeight" to "${e.useWeight}",
"zjmCode" to "${e.zjmCode}", "materId" to "${e.materId}", "isDel" to "${e.isDel}",
"createTime" to "${e.createTime}"
)
)
}
rows to total
}
3 -> {
val (pageQ, countQ) = buildQueries("dm_seasoning_slot", "deviceId ASC, address ASC", false, filter, limit, offset)
val total = rep.countSlotFiltered(countQ)
val rows = rep.getSlotPagedFiltered(pageQ).map { e ->
DbRecord(
isDel = false,
fields = listOf(
"deviceId" to e.deviceId, "address" to "${e.address}",
"goodsId" to e.goodsId, "goodsName" to e.goodsName
),
columnsPerRow = 1
)
}
rows to total
}
else -> emptyList<DbRecord>() to 0
}
}
/**
* 根据筛选条件动态构建分页查询和计数查询的 SQL
* @param tableName 表名
* @param orderBy ORDER BY 子句(不含关键字)
* @param hasIsDel 是否有 isDel 字段,无则忽略 showDel 参数
* @param filter 筛选条件
* @param limit 分页大小
* @param offset 偏移量
* @return Pair<分页查询, 计数查询>
*/
private fun buildQueries(
tableName: String,
orderBy: String,
hasIsDel: Boolean,
filter: DbInspectFilter,
limit: Int,
offset: Int
): Pair<SimpleSQLiteQuery, SimpleSQLiteQuery> {
val conditions = mutableListOf<String>()
val args = mutableListOf<Any>()
if (hasIsDel && !filter.showDel) {
conditions.add("isDel = 0")
}
filter.exactFields.forEach { (col, value) ->
if (value.isNotBlank()) {
conditions.add("$col = ?")
args.add(value)
}
}
filter.likeFields.forEach { (col, value) ->
if (value.isNotBlank()) {
conditions.add("$col LIKE ?")
args.add("%$value%")
}
}
val where = if (conditions.isEmpty()) "" else "WHERE ${conditions.joinToString(" AND ")}"
val pageQuery = SimpleSQLiteQuery(
"SELECT * FROM $tableName $where ORDER BY $orderBy LIMIT ? OFFSET ?",
(args + limit + offset).toTypedArray()
)
val countQuery = SimpleSQLiteQuery(
"SELECT COUNT(*) FROM $tableName $where",
args.toTypedArray()
)
return pageQuery to countQuery
}
//--------------------------------------------------------------------------------------------------------
// viewModelScope.launch {
// list.forEach {
// rep.updateSeasoning(it)
// }
// }
// }
fun saveSeasoning(entity: SeasoningEntity, block: () -> Unit) {
viewModelScope.launch {
val list = rep.getSeasoningBySort(entity.sort)
val size = list?.size?:0
if (size == 1) {
rep.updateSeasoning(entity.also { it.id = list!![0].id })
block()
return@launch
}
if (size > 1) {
list?.forEach {
rep.deleteSeasoningBySort(it.sort)
}
}
entity.id = 0
val id = rep.insertSeasoning(entity)
entity.id = id
block()
}
}
/**
* 按 goodsId 查重后 upsert 调料数据,确保同一 goodsId 只有一条记录
*/
suspend fun upsertSeasoningByGoodsId(entity: SeasoningEntity) {
rep.upsertSeasoningByGoodsId(entity)
}
/**
* 按 goodsId 查询调料详情
*/
suspend fun getSeasoningByGoodsId(goodsId: String): SeasoningEntity? {
return rep.getSeasoningByGoodsId(goodsId)
}
//--------------------------------------------------------------------------------------------------------
/**
* 已采集食材列表 UI 状态流
* 元素为按名称分组后的(名称,数量)数据
*/
private val _collectedFoodListState = MutableStateFlow<List<CollectedFoodItem>>(emptyList())
val collectedFoodListState: StateFlow<List<CollectedFoodItem>> = _collectedFoodListState.asStateFlow()
// /**
// * 加载已采集食材列表,按名称分组统计数量
// * 使用属性查询只读 name 字段,不加载 foodVector,节省内存
// * @param nameFilter 名称过滤关键词,为空时加载全部
// */
// fun loadCollectedFoodList(nameFilter: String? = null) {
// viewModelScope.launch {
// val result = ObjectBox.queryFoodNameCounts(nameFilter)
// _collectedFoodListState.value = result.map { (name, count) ->
// CollectedFoodItem(foodName = name, collectCount = count)
// }
// }
// }
// /**
// * 删除指定名称的所有采集记录,删除完成后刷新列表
// * @param foodName 食材名称
// * @param nameFilter 当前搜索关键词,用于删除后刷新列表
// */
// fun removeCollectedFood(foodName: String, nameFilter: String? = null) {
// viewModelScope.launch {
// ObjectBox.remove(foodName)
// loadCollectedFoodList(nameFilter)
// }
// }
// /**
// * 清除所有向量采集数据,完成后刷新列表
// * @param onDone 清除完成后在主线程执行的回调
// */
// fun clearAllCollectedFood(onDone: (() -> Unit)? = null) {
// viewModelScope.launch {
// ObjectBox.removeAll()
// loadCollectedFoodList()
// onDone?.invoke()
// }
// }
//--------------------------------------------------------------------------------------------------------
}
@@ -0,0 +1,92 @@
package com.shuwei.dish.match.db.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import com.shuwei.dish.match.entity.CookFoodEntity
import com.shuwei.dish.match.entity.CookFoodGoodsEntity
import com.shuwei.dish.match.entity.SeasoningEntity
import kotlinx.coroutines.flow.Flow
@Dao
interface AppDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertCookFood(item: CookFoodEntity): Long
@Update
suspend fun updateCookFood(item: CookFoodEntity)
@Query("SELECT * FROM dm_cook_food WHERE isDel = 0 AND canteenId = :canteenId AND foodId = :foodId AND cookMode = :cookMode")
suspend fun getCookFoodById(canteenId:String, foodId: String, cookMode: Int): CookFoodEntity?
@Query("SELECT * FROM dm_cook_food WHERE isDel = 0 AND canteenId = :canteenId AND cookMode = :cookMode AND dinnerType = :dinnerType ORDER BY createTime DESC")
suspend fun getCookFoodList(canteenId:String, cookMode: Int, dinnerType:String): MutableList<CookFoodEntity>?
@Query("SELECT count(1) FROM dm_cook_food WHERE isDel = 0 AND canteenId = :canteenId AND cookMode = :cookMode")
suspend fun countCookFood(canteenId:String, cookMode: Int): Int
@Query("DELETE FROM dm_cook_food WHERE canteenId = :canteenId AND foodId = :foodId AND cookMode = :cookMode")
suspend fun deleteCookFood(canteenId:String, foodId: String, cookMode: Int)
//--------------------------------------------------------------------------------------------
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertGoods(item: CookFoodGoodsEntity): Long
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertGoodsList(items: MutableList<CookFoodGoodsEntity>): Array<Long>
@Update
suspend fun updateGoods(item: CookFoodGoodsEntity)
@Query("SELECT * FROM dm_cook_food_goods WHERE isDel = 0 AND pid = :pid AND foodId = :foodId")
suspend fun getCookFoodGoodsList(pid:Long, foodId: String): MutableList<CookFoodGoodsEntity>
@Query("SELECT * FROM dm_cook_food_goods WHERE isDel = 0 AND foodId = :foodId AND materialType = :materialType")
suspend fun getCookFoodGoodsListByType(
foodId: String,
materialType: Int
): MutableList<CookFoodGoodsEntity>
@Query("DELETE FROM dm_cook_food_goods WHERE pid = :pid AND foodId = :foodId")
suspend fun deleteCookFoodGoodsList(pid:Long, foodId: String)
//--------------------------------------------------------------------------------------------
@Query("SELECT * FROM dm_seasoning WHERE isDel = 0 AND id = :id")
suspend fun getSeasoningById(id: Long): SeasoningEntity?
@Query("SELECT * FROM dm_seasoning WHERE isDel = 0 AND sort = :sort")
suspend fun getSeasoningBySort(sort: Int): MutableList<SeasoningEntity>?
@Query("SELECT * FROM dm_seasoning WHERE isDel = 0 AND goodsId = :goodsId")
suspend fun getSeasoningByGoodsId(goodsId: Int): SeasoningEntity?
// @Query("SELECT * FROM dm_seasoning WHERE isDel = 0 AND goodsId != -1 AND goodsName != '' ORDER BY sort ASC")
// fun getAllStream(): Flow<MutableList<SeasoningEntity>>
@Query("SELECT * FROM dm_seasoning WHERE isDel = 0 AND goodsId != -1 AND goodsName != '' ORDER BY sort ASC")
fun getAllStream(): MutableList<SeasoningEntity>
@Query("SELECT * FROM dm_seasoning WHERE isDel = 0 AND goodsName LIKE '%' || :query || '%'")
suspend fun search(query: String): MutableList<SeasoningEntity>
@Query("DELETE FROM dm_seasoning WHERE isDel = 0 AND sort = :sort")
suspend fun deleteSeasoningBySort(sort:Int): Int
@Query("DELETE FROM dm_seasoning WHERE isDel = 0")
suspend fun clearAllSeasoning(): Int
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertSeasoningList(items: MutableList<SeasoningEntity>): Array<Long>
@Update
suspend fun updateSeasoning(item: SeasoningEntity)
@Insert
suspend fun insertSeasoning(item: SeasoningEntity): Long
//--------------------------------------------------------------------------------------------
}
@@ -1,61 +1,20 @@
package com.shuwei.dish.match.db.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.RawQuery
import androidx.room.Update
import androidx.sqlite.db.SupportSQLiteQuery
import com.shuwei.dish.match.db.entity.CookFoodEntity
@Dao
interface CookFoodDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertCookFood(item: CookFoodEntity): Long
@Update
suspend fun updateCookFood(item: CookFoodEntity)
@Query("SELECT * FROM dm_cook_food WHERE isDel = 0 AND canteenId = :canteenId AND foodId = :foodId AND cookMode = :cookMode AND dinnerType = :dinnerType")
suspend fun getCookFoodById(canteenId: String, foodId: String, cookMode: Int, dinnerType: String): CookFoodEntity?
@Query("SELECT * FROM dm_cook_food WHERE isDel = 0 AND canteenId = :canteenId AND cookMode = :cookMode AND dinnerType = :dinnerType ORDER BY createTime DESC")
suspend fun getCookFoodList(canteenId: String, cookMode: Int, dinnerType: String): MutableList<CookFoodEntity>?
@Query("SELECT count(1) FROM dm_cook_food WHERE isDel = 0 AND canteenId = :canteenId AND cookMode = :cookMode")
suspend fun countCookFood(canteenId: String, cookMode: Int): Int
// @Query("DELETE FROM dm_cook_food WHERE canteenId = :canteenId AND foodId = :foodId AND cookMode = :cookMode")
@Query("UPDATE dm_cook_food SET isDel = 1 WHERE isDel = 0 AND canteenId = :canteenId AND foodId = :foodId AND cookMode = :cookMode AND dinnerType = :dinnerType")
suspend fun deleteCookFood(canteenId: String, foodId: String, cookMode: Int, dinnerType: String)
/** 接口提交成功后逻辑删除菜品,同时把 submitState 置 1 以便在数据库审计中区分手动删除 */
@Query("UPDATE dm_cook_food SET isDel = 1, submitState = 1 WHERE isDel = 0 AND canteenId = :canteenId AND foodId = :foodId AND cookMode = :cookMode AND dinnerType = :dinnerType")
suspend fun markSubmittedAndDelete(canteenId: String, foodId: String, cookMode: Int, dinnerType: String)
/** 查询全部菜品记录(含已删除),用于数据库调试查看 */
@Query("SELECT * FROM dm_cook_food ORDER BY createTime DESC")
suspend fun getAllCookFood(): MutableList<CookFoodEntity>
/** 分页查询菜品记录,showDel=true 时包含已删除记录 */
@Query("SELECT * FROM dm_cook_food WHERE (:showDel = 1 OR isDel = 0) ORDER BY createTime DESC LIMIT :limit OFFSET :offset")
suspend fun getCookFoodPaged(showDel: Int, limit: Int, offset: Int): List<CookFoodEntity>
/** 统计菜品记录总数,showDel=true 时包含已删除记录 */
@Query("SELECT COUNT(*) FROM dm_cook_food WHERE (:showDel = 1 OR isDel = 0)")
suspend fun countCookFoodAll(showDel: Int): Int
/** 物理清空全表,用于清除测试数据 */
@Query("DELETE FROM dm_cook_food")
suspend fun deleteAll()
/** 动态条件分页查询菜品记录,由 ViewModel 构建 SQL */
@RawQuery
suspend fun queryPagedFiltered(query: SupportSQLiteQuery): List<CookFoodEntity>
/** 动态条件统计菜品记录总数,由 ViewModel 构建 SQL */
@RawQuery
suspend fun countFiltered(query: SupportSQLiteQuery): Int
}
//package com.shuwei.dish.match.db.dao
//
//import androidx.room.Dao
//import androidx.room.Query
//import com.shuwei.dish.match.db.BaseDao
//import com.shuwei.dish.match.entity.CookFoodEntity
//
//@Dao
//interface CookFoodDao : BaseDao<CookFoodEntity> {
//
// @Query("SELECT * FROM dm_cook_food WHERE isDel = 0 AND foodId = :foodId AND cookMode = :cookMode")
// suspend fun getCookFoodById(foodId: String, cookMode:Int): CookFoodEntity?
//
// @Query("SELECT * FROM dm_cook_food WHERE isDel = 0 AND cookMode = :cookMode")
// suspend fun getCookFoodList(cookMode:Int): MutableList<CookFoodEntity>?
//
// @Query("SELECT count(1) FROM dm_cook_food WHERE isDel = 0 AND cookMode = :cookMode")
// suspend fun countCookFood(cookMode:Int): Int
//
//}
@@ -1,61 +1,29 @@
package com.shuwei.dish.match.db.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.RawQuery
import androidx.room.Update
import androidx.sqlite.db.SupportSQLiteQuery
import com.shuwei.dish.match.db.entity.CookFoodGoodsEntity
@Dao
interface CookFoodGoodsDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertGoods(item: CookFoodGoodsEntity): Long
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertGoodsList(items: MutableList<CookFoodGoodsEntity>): Array<Long>
@Update
suspend fun updateGoods(item: CookFoodGoodsEntity)
@Query("SELECT * FROM dm_cook_food_goods WHERE isDel = 0 AND pid = :pid AND foodId = :foodId")
suspend fun getCookFoodGoodsList(pid: Long, foodId: String): MutableList<CookFoodGoodsEntity>
@Query("SELECT * FROM dm_cook_food_goods WHERE isDel = 0 AND foodId = :foodId AND materialType = :materialType")
suspend fun getCookFoodGoodsListByType(foodId: String, materialType: Int): MutableList<CookFoodGoodsEntity>
// @Query("DELETE FROM dm_cook_food_goods WHERE pid = :pid AND foodId = :foodId")
@Query("UPDATE dm_cook_food_goods SET isDel = 1 WHERE isDel = 0 AND pid = :pid AND foodId = :foodId")
suspend fun deleteCookFoodGoodsList(pid: Long, foodId: String)
/** 仅逻辑删除调料行(materialType = 3),主辅材不动,用于 cook 时只重建调料部分 */
@Query("UPDATE dm_cook_food_goods SET isDel = 1 WHERE isDel = 0 AND pid = :pid AND foodId = :foodId AND materialType = 3")
suspend fun deleteSeasoningOnly(pid: Long, foodId: String)
/** 查询全部食材记录(含已删除),用于数据库调试查看 */
@Query("SELECT * FROM dm_cook_food_goods ORDER BY createTime DESC")
suspend fun getAllCookFoodGoods(): MutableList<CookFoodGoodsEntity>
/** 分页查询食材记录,showDel=true 时包含已删除记录 */
@Query("SELECT * FROM dm_cook_food_goods WHERE (:showDel = 1 OR isDel = 0) ORDER BY createTime DESC LIMIT :limit OFFSET :offset")
suspend fun getCookFoodGoodsPaged(showDel: Int, limit: Int, offset: Int): List<CookFoodGoodsEntity>
/** 统计食材记录总数,showDel=true 时包含已删除记录 */
@Query("SELECT COUNT(*) FROM dm_cook_food_goods WHERE (:showDel = 1 OR isDel = 0)")
suspend fun countCookFoodGoodsAll(showDel: Int): Int
/** 物理清空全表,用于清除测试数据 */
@Query("DELETE FROM dm_cook_food_goods")
suspend fun deleteAll()
/** 动态条件分页查询食材记录,由 ViewModel 构建 SQL */
@RawQuery
suspend fun queryPagedFiltered(query: SupportSQLiteQuery): List<CookFoodGoodsEntity>
/** 动态条件统计食材记录总数,由 ViewModel 构建 SQL */
@RawQuery
suspend fun countFiltered(query: SupportSQLiteQuery): Int
}
//package com.shuwei.dish.match.db.dao
//
//import androidx.room.Dao
//import androidx.room.Query
//import com.shuwei.dish.match.db.BaseDao
//import com.shuwei.dish.match.entity.CookFoodGoodsEntity
//
//@Dao
//interface CookFoodGoodsDao : BaseDao<CookFoodGoodsEntity> {
//
// @Query("SELECT * FROM dm_cook_food_goods WHERE isDel = 0 AND foodId = :foodId")
// suspend fun getCookFoodGoodsList(foodId: String): MutableList<CookFoodGoodsEntity>
//
// @Query("SELECT * FROM dm_cook_food_goods WHERE isDel = 0 AND foodId = :foodId AND materialType = :materialType")
// suspend fun getCookFoodGoodsListByType(foodId: String, materialType:Int): MutableList<CookFoodGoodsEntity>
//
//// @Query(
//// """
//// UPDATE dm_cook_food_goods
//// SET goodsName=:entity.goodsName, foodId=:entity.foodId, relateionType=:entity.relateionType, sort=:entity.sort
//// allEdible=:entity.allEdible, useWeight=:entity.useWeight, isDel=:entity.isDel, createTime=:entity.createTime
//// WHERE isDel = 0
//// AND foodId=:entity.foodId
//// AND goodsId=:goodsId
//// """
//// )
//// fun updateByGoodsId(entity: CookFoodGoodsEntity)
//
//}
@@ -1,70 +1,25 @@
package com.shuwei.dish.match.db.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.RawQuery
import androidx.room.Update
import androidx.sqlite.db.SupportSQLiteQuery
import com.shuwei.dish.match.db.entity.SeasoningEntity
@Dao
interface SeasoningDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertSeasoning(item: SeasoningEntity): Long
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertSeasoningList(items: MutableList<SeasoningEntity>): Array<Long>
@Update
suspend fun updateSeasoning(item: SeasoningEntity)
@Query("SELECT * FROM dm_seasoning WHERE isDel = 0 AND id = :id")
suspend fun getSeasoningById(id: Long): SeasoningEntity?
@Query("SELECT * FROM dm_seasoning WHERE isDel = 0 AND sort = :sort")
suspend fun getSeasoningBySort(sort: Int): MutableList<SeasoningEntity>?
@Query("SELECT * FROM dm_seasoning WHERE isDel = 0 AND goodsId = :goodsId")
suspend fun getSeasoningByGoodsId(goodsId: String): SeasoningEntity?
@Query("SELECT * FROM dm_seasoning WHERE isDel = 0 AND goodsId != -1 AND goodsName != '' ORDER BY sort ASC")
fun getAllStream(): MutableList<SeasoningEntity>
@Query("SELECT * FROM dm_seasoning WHERE isDel = 0 AND goodsName LIKE '%' || :query || '%'")
suspend fun search(query: String): MutableList<SeasoningEntity>
// @Query("DELETE FROM dm_seasoning WHERE isDel = 0 AND sort = :sort")
@Query("UPDATE dm_seasoning SET isDel = 1 WHERE isDel = 0 AND sort = :sort")
suspend fun deleteSeasoningBySort(sort: Int): Int
// @Query("DELETE FROM dm_seasoning WHERE isDel = 0")
@Query("UPDATE dm_seasoning SET isDel = 1 WHERE isDel = 0")
suspend fun clearAllSeasoning(): Int
/** 查询全部调料记录(含已删除),用于数据库调试查看 */
@Query("SELECT * FROM dm_seasoning ORDER BY sort ASC")
suspend fun getAllSeasoning(): MutableList<SeasoningEntity>
/** 分页查询调料记录,showDel=true 时包含已删除记录 */
@Query("SELECT * FROM dm_seasoning WHERE (:showDel = 1 OR isDel = 0) ORDER BY sort ASC LIMIT :limit OFFSET :offset")
suspend fun getSeasoningPaged(showDel: Int, limit: Int, offset: Int): List<SeasoningEntity>
/** 统计调料记录总数,showDel=true 时包含已删除记录 */
@Query("SELECT COUNT(*) FROM dm_seasoning WHERE (:showDel = 1 OR isDel = 0)")
suspend fun countSeasoningAll(showDel: Int): Int
/** 物理清空全表,用于清除测试数据(比 clearAllSeasoning 的逻辑删除更彻底) */
@Query("DELETE FROM dm_seasoning")
suspend fun deleteAll()
/** 动态条件分页查询调料记录,由 ViewModel 构建 SQL */
@RawQuery
suspend fun queryPagedFiltered(query: SupportSQLiteQuery): List<SeasoningEntity>
/** 动态条件统计调料记录总数,由 ViewModel 构建 SQL */
@RawQuery
suspend fun countFiltered(query: SupportSQLiteQuery): Int
}
//package com.shuwei.dish.match.db.dao
//
//import androidx.room.Dao
//import androidx.room.Query
//import com.shuwei.dish.match.db.BaseDao
//import com.shuwei.dish.match.entity.SeasoningEntity
//import kotlinx.coroutines.flow.Flow
//
//@Dao
//interface SeasoningDao : BaseDao<SeasoningEntity> {
//
//// @Query("SELECT * FROM dm_seasoning WHERE isDel = 0 AND id = :id")
//// suspend fun getSeasoningById(id: Long): SeasoningEntity?
////
//// @Query("SELECT * FROM dm_seasoning WHERE isDel = 0 ORDER BY sort ASC")
//// fun getAllStream(): Flow<MutableList<SeasoningEntity>>
////
//// @Query("SELECT * FROM dm_seasoning WHERE isDel = 0 AND name LIKE '%' || :query || '%'")
//// suspend fun search(query: String): MutableList<SeasoningEntity>
////
//// @Query("DELETE FROM dm_seasoning WHERE rowid IN " +
//// "(SELECT rowid FROM dm_seasoning LIMIT 1000)")
//// suspend fun deleteBatch(): Int
//
//}
@@ -1,77 +0,0 @@
package com.shuwei.dish.match.db.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.RawQuery
import androidx.sqlite.db.SupportSQLiteQuery
import com.shuwei.dish.match.db.entity.SeasoningSlotEntity
/**
* 调料槽位配置 DAO
* 以 (deviceId, address) 为主键做 upsert,保证同一槽位只保留最新配置
*/
@Dao
interface SeasoningSlotDao {
/**
* 插入或更新单个槽位配置(主键冲突时覆盖)
*/
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsert(slot: SeasoningSlotEntity)
/**
* 批量插入或更新槽位配置(主设备广播全量时使用)
*/
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsertAll(slots: List<SeasoningSlotEntity>)
/**
* 查询指定设备的全部槽位配置
* @param deviceId 目标设备 ID
*/
@Query("SELECT * FROM dm_seasoning_slot WHERE deviceId = :deviceId")
suspend fun queryByDeviceId(deviceId: String): List<SeasoningSlotEntity>
/**
* 查询所有设备的全部槽位配置(主设备广播时使用)
*/
@Query("SELECT * FROM dm_seasoning_slot")
suspend fun queryAll(): List<SeasoningSlotEntity>
/** 分页查询槽位记录(槽位表无 isDel,showDel 参数忽略) */
@Query("SELECT * FROM dm_seasoning_slot ORDER BY deviceId ASC, address ASC LIMIT :limit OFFSET :offset")
suspend fun getSlotPaged(limit: Int, offset: Int): List<SeasoningSlotEntity>
/** 统计槽位记录总数 */
@Query("SELECT COUNT(*) FROM dm_seasoning_slot")
suspend fun countSlotAll(): Int
/**
* 删除指定设备的指定槽位配置
* @param deviceId 目标设备 ID
* @param address 秤硬件地址
*/
@Query("DELETE FROM dm_seasoning_slot WHERE deviceId = :deviceId AND address = :address")
suspend fun deleteByDeviceIdAndAddress(deviceId: String, address: Int)
/**
* 删除指定设备的全部槽位配置(子设备接收全量广播时先清空再写入)
* @param deviceId 目标设备 ID
*/
@Query("DELETE FROM dm_seasoning_slot WHERE deviceId = :deviceId")
suspend fun deleteAllByDeviceId(deviceId: String)
/** 物理清空全表,用于清除测试数据 */
@Query("DELETE FROM dm_seasoning_slot")
suspend fun deleteAll()
/** 动态条件分页查询槽位记录,由 ViewModel 构建 SQL */
@RawQuery
suspend fun queryPagedFiltered(query: SupportSQLiteQuery): List<SeasoningSlotEntity>
/** 动态条件统计槽位记录总数,由 ViewModel 构建 SQL */
@RawQuery
suspend fun countFiltered(query: SupportSQLiteQuery): Int
}
@@ -1,6 +0,0 @@
package com.shuwei.dish.match.db.entity
/** 默认调料配置 JSON 反序列化的包装类 */
data class ResetReasoningRecord(
val list: MutableList<SeasoningEntity> = mutableListOf()
)
@@ -1,23 +0,0 @@
package com.shuwei.dish.match.db.entity
import androidx.room.Entity
/**
* 调料槽位配置表,记录每个格子对应哪种调料
* 主键为 (deviceId, address) 组合,唯一标识一个物理秤槽位
*
* @param deviceId 所属设备 ID,用于区分 22格/18格 设备
* @param address 秤硬件地址,对应 ScaleDeviceConfig.SCALE_ORDER_22/18 中的值
* @param goodsId 调料 ID
* @param goodsName 调料名称,用于 adapter 展示
*/
@Entity(
tableName = "dm_seasoning_slot",
primaryKeys = ["deviceId", "address"]
)
data class SeasoningSlotEntity(
val deviceId: String,
val address: Int,
val goodsId: String,
val goodsName: String
)
@@ -1,193 +0,0 @@
package com.shuwei.dish.match.dialog
import android.annotation.SuppressLint
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import androidx.core.view.isEmpty
import androidx.core.widget.addTextChangedListener
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.shuwei.dish.match.R
import com.shuwei.dish.match.adapter.GoodsInfoSearchAdapter
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.databinding.DialogBottomSheetBinding
import com.shuwei.dish.match.model.GoodsItem
import com.shuwei.dish.match.net.RemoteRepository
import com.shuwei.dish.match.net.UiState
import com.shuwei.dish.match.utils.KeyboardUtil
import com.shuwei.dish.match.utils.ext.addOnActionSearchListener
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import com.shuwei.dish.match.utils.ext.toast
/**
* @param goodsType 1-调料,0-食材
*/
class BottomDialog(
private var goodsType: Int = 1,
private var defGoodsName:String? = null
) {
companion object {
const val TAG = "BottomDialog"
}
private var list = mutableListOf<GoodsItem>()
private lateinit var binding: DialogBottomSheetBinding
private lateinit var goodsInfoSearchAdapter: GoodsInfoSearchAdapter
private var goodsName: String? = null
private var activity: BaseActivity? = null
fun show(activity: BaseActivity, callback: (item: GoodsItem) -> Unit) {
this.activity = activity
//直接使用BottomSheetDialog
var dialog: BottomSheetDialog? = null
val inflater = LayoutInflater.from(activity)
binding = DialogBottomSheetBinding.inflate(inflater)
binding.tvSheetName.text = if (goodsType == 1) "调料检索" else "食材检索"
binding.etSheetInput.hint = if (goodsType == 1) "输入调料名称" else "输入食材名称或速记码"
goodsInfoSearchAdapter = GoodsInfoSearchAdapter(list).apply {
setOnItemClickListener { _, _, position ->
list[position].isClicked = true
notifyItemChanged(position)
Handler(Looper.getMainLooper()).postDelayed({
callback(list[position])
dialog?.dismiss()
}, 300)
}
}
binding.recyclerView.run {
layoutManager = GridLayoutManager(activity, 2, GridLayoutManager.VERTICAL, false)
adapter = goodsInfoSearchAdapter
addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(rv: RecyclerView, newState: Int) {
super.onScrollStateChanged(rv, newState);
}
override fun onScrolled(rv: RecyclerView, dx: Int, dy: Int) {
//解决RecyclerView和smartRefreshLayout滑动冲突问题
super.onScrolled(rv, dx, dy)
val topRowVerticalPosition = if (rv.isEmpty()) 0 else rv.getChildAt(0).top
binding.refreshLayout.setNestedScrollingEnabled(topRowVerticalPosition >= 0)
}
})
}
dialog = BottomSheetDialog(activity, R.style.BottomSheet).apply {
setContentView(binding.root)
setCancelable(true)
behavior.skipCollapsed = false // 跳过折叠状态
// behavior.peekHeight = 880.dp
// window?.setLayout(-1, 880.dp)
show()
setOnDismissListener {
activity.hideStatusBar()
}
}
binding.refreshLayout.setOnRefreshListener {
pageNo = 1
getGoodsList()
}
binding.refreshLayout.setOnLoadMoreListener {
getGoodsList()
}
binding.etSheetInput.addTextChangedListener(
onTextChanged = { text, start, before, count ->
goodsName = text.toString()
})
binding.ivSearch.setOnClickListener { v ->
searchGoods(v)
}
binding.root.setOnClickListener { v ->
KeyboardUtil.hideKeyboard(v.context, v)
}
binding.etSheetInput.let { v ->
v.addOnActionSearchListener {
searchGoods(v)
}
}
binding.refreshLayout.setEnableRefresh(false)
binding.refreshLayout.setEnableLoadMore(false)
if (defGoodsName.isNullOrBlank().not()) {
binding.etSheetInput.let {
it.setText(defGoodsName)
searchGoods(it)
}
}
}
private fun searchGoods(v: View) {
if (goodsName.isNullOrBlank()) {
activity?.toast("${binding.etSheetInput.hint}")
return
}
pageNo = 1
getGoodsList()
KeyboardUtil.hideKeyboard(v.context, v)
}
private var pageNo = 1
private val pageSize = 120
/**
*/
@SuppressLint("NotifyDataSetChanged")
private fun getGoodsList() {
val param = mutableMapOf<String, Any>(
"goodsType" to "$goodsType",
"placeId" to BaseApp.canteenId,
"pageNum" to pageNo,
"pageSize" to pageSize
)
if (goodsName.isNullOrBlank().not()) {
param["goodsName"] = goodsName!!
}
Log.d(TAG, "getGoodsList: param=$param")
activity?.lifecycleScope?.launch {
val state = RemoteRepository().queryGoodsList(param)
when (state) {
is UiState.Success -> {
val records = state.data
if (records.isNullOrEmpty()) {
activity?.toast(if (goodsType == 1) "暂未搜索到调料信息" else "暂未搜索到食材信息")
return@launch
}
if (pageNo == 1) {
list.clear()
}
list.addAll(records)
goodsInfoSearchAdapter.notifyDataSetChanged()
if (pageNo == 1) {
binding.refreshLayout.finishRefresh(1200)
} else {
binding.refreshLayout.finishLoadMore(1200)
}
val isLoadMoreEnable = records.size >= pageSize
binding.refreshLayout.setEnableLoadMore(isLoadMoreEnable)
if (isLoadMoreEnable) {
pageNo++
}
}
is UiState.Error -> {
activity?.toast(state.msg)
if (pageNo == 1) {
binding.refreshLayout.finishRefresh(1200)
} else {
binding.refreshLayout.finishLoadMore(1200)
}
}
else -> {}
}
}
}
}
@@ -0,0 +1,245 @@
package com.shuwei.dish.match.dialog
import android.annotation.SuppressLint
import android.os.Handler
import android.os.Looper
import android.text.Editable
import android.view.LayoutInflater
import android.view.View
import androidx.core.view.isEmpty
import androidx.core.widget.addTextChangedListener
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.shuwei.dish.match.R
import com.shuwei.dish.match.adapter.GoodsInfoSearchAdapter
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.databinding.DialogFoodSearchBinding
import com.shuwei.dish.match.entity.CookFoodGoodsEntity
import com.shuwei.dish.match.utils.KeyboardUtil
import com.shuwei.dish.match.utils.ext.addOnActionSearchListener
import com.shuwei.dish.match.utils.ext.toast
/**
* 食材搜索弹窗
*/
class FoodSearchDialog(
private var defGoodsName: String? = null
) {
companion object {
const val TAG = "BottomDialog"
}
private var list = mutableListOf<CookFoodGoodsEntity>()
private lateinit var binding: DialogFoodSearchBinding
private lateinit var searchAdapter: GoodsInfoSearchAdapter
private var goodsName: String? = null
private var activity: BaseActivity? = null
fun show(activity: BaseActivity, callback: (item: CookFoodGoodsEntity) -> Unit) {
this.activity = activity
//直接使用BottomSheetDialog
var dialog: BottomSheetDialog? = null
val inflater = LayoutInflater.from(activity)
binding = DialogFoodSearchBinding.inflate(inflater)
dialog = BottomSheetDialog(activity, R.style.BottomSheet).apply {
setContentView(binding.root)
setCancelable(true)
behavior.skipCollapsed = false // 跳过折叠状态
// behavior.peekHeight = 880.dp
// window?.setLayout(-1, 880.dp)
show()
setOnDismissListener {
activity.hideStatusBar()
}
binding.etSheetInput.requestFocus()
}
binding.tvSheetName.text = "食材检索"
binding.etSheetInput.run {
hint = "输入食材名称或速记码"
addTextChangedListener(afterTextChanged = { it: Editable? ->
if (it.isNullOrBlank()) {
list.clear()
searchAdapter.notifyDataSetChanged()
}
})
}
searchAdapter = GoodsInfoSearchAdapter(list).apply {
setOnItemClickListener { _, _, position ->
list[position].isClicked = true
notifyItemChanged(position)
Handler(Looper.getMainLooper()).postDelayed({
callback(list[position])
dialog?.dismiss()
}, 300)
}
}
binding.recyclerView.run {
layoutManager = GridLayoutManager(activity, 2, GridLayoutManager.VERTICAL, false)
adapter = searchAdapter
addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(rv: RecyclerView, newState: Int) {
super.onScrollStateChanged(rv, newState);
}
override fun onScrolled(rv: RecyclerView, dx: Int, dy: Int) {
//解决RecyclerView和smartRefreshLayout滑动冲突问题
super.onScrolled(rv, dx, dy)
val topRowVerticalPosition = if (rv.isEmpty()) 0 else rv.getChildAt(0).top
binding.refreshLayout.setNestedScrollingEnabled(topRowVerticalPosition >= 0)
}
})
}
binding.refreshLayout.setOnRefreshListener {
pageNo = 1
getGoodsList()
}
binding.refreshLayout.setOnLoadMoreListener {
getGoodsList()
}
binding.etSheetInput.addTextChangedListener(
onTextChanged = { text, start, before, count ->
goodsName = text.toString()
})
binding.ivSearch.setOnClickListener { v ->
searchGoods(v)
}
binding.root.setOnClickListener { v ->
KeyboardUtil.hideKeyboard(v.context, v)
}
binding.etSheetInput.let { v ->
v.addOnActionSearchListener {
searchGoods(v)
}
}
binding.refreshLayout.setEnableRefresh(false)
binding.refreshLayout.setEnableLoadMore(false)
if (defGoodsName.isNullOrBlank().not()) {
binding.etSheetInput.let {
it.setText(defGoodsName)
searchGoods(it)
}
}
}
private fun searchGoods(v: View) {
if (goodsName.isNullOrBlank()) {
activity?.toast("${binding.etSheetInput.hint}")
return
}
pageNo = 1
getGoodsList()
KeyboardUtil.hideKeyboard(v.context, v)
}
private var pageNo = 1
private val pageSize = 50
/**
*/
@SuppressLint("NotifyDataSetChanged")
private fun getGoodsList() {
// val pathSegmentList = UrlConfig.QUERY_GOODS_LIST.split(delimiters = arrayOf("/"), limit = 1)
// val queryParams = mutableMapOf(
// "goodsType" to "0",
// "canteenId" to BaseApp.canteenId,
// "pageNo" to "$pageNo",
// "pageSize" to "$pageSize"
// )
// if (goodsName.isNullOrBlank().not()) {
// queryParams.put("name", goodsName!!)
// }
// val sb = StringBuilder().apply {
// append(UrlConfig.QUERY_GOODS_LIST)
// append("?")
// queryParams.forEach { (key, value) ->
// append("$key=$value&")
// }
// }
// sb.deleteCharAt(sb.length - 1)
// Log.d(TAG, "getGoodsList: url = $sb")
// HttpUtil.get(
// url = sb.toString(),
// doSuccess = {
// val json = it.toJsonString()
// Log.d(TAG, "getGoodsList: json=$json")
// val recordBean: GoodsRecordBean? = json.toObject<GoodsRecordBean>()
// if (recordBean == null || recordBean.records.isNullOrEmpty()) {
// activity?.toast("暂未搜索到食材信息")
// return@get
// }
// val records = recordBean.records
// if (pageNo == 1) {
// list.clear()
// }
// list.addAll(records!!)
// searchAdapter.notifyDataSetChanged()
// if (pageNo == 1) {
// binding.refreshLayout.finishRefresh(1200)
// } else {
// binding.refreshLayout.finishLoadMore(1200)
// }
// val isLoadMoreEnable = records.size >= pageSize
// binding.refreshLayout.setEnableLoadMore(isLoadMoreEnable)
// if (isLoadMoreEnable) {
// pageNo++
// }
// }, doFailure = { code, msg ->
// activity?.toast(msg)
// if (pageNo == 1) {
// binding.refreshLayout.finishRefresh(1200)
// } else {
// binding.refreshLayout.finishLoadMore(1200)
// }
// })
val param = mutableMapOf<String, Any>(
"goodsType" to "0",
"placeId" to BaseApp.canteenId,
"pageNum" to pageNo,
"pageSize" to pageSize
)
if (goodsName.isNullOrBlank().not()) {
param.put("goodsName", goodsName!!)
}
activity?.queryGoodsList(param = param, onSuccess = {
loadGoodsList(it)
}, onFailure = { code, msg ->
activity?.toast(msg)
finishRefresh()
})
}
private fun finishRefresh() {
if (pageNo == 1) {
binding.refreshLayout.finishRefresh()
} else {
binding.refreshLayout.finishLoadMore()
}
}
@SuppressLint("NotifyDataSetChanged")
private fun loadGoodsList(records: MutableList<CookFoodGoodsEntity>?) {
finishRefresh()
if (records.isNullOrEmpty()) {
activity?.toast("暂未搜索到食材信息")
return
}
if (pageNo == 1) {
list.clear()
}
list.addAll(records)
searchAdapter.notifyDataSetChanged()
val isLoadMoreEnable = records.size >= pageSize
binding.refreshLayout.setEnableLoadMore(isLoadMoreEnable)
if (isLoadMoreEnable) {
pageNo++
}
}
}
@@ -13,14 +13,22 @@ object Loading {
private var dialog: LoadingDialog? = null
fun show(context: Context) {
fun isShowing(): Boolean {
return dialog != null && dialog!!.isShowing
}
fun updateContent(content: String) {
if (isShowing().not()) return
dialog!!.updateContent(content)
}
fun show(context: Context, message: String = "加载中……", onDismiss: () -> Unit = {}) {
try {
if (dialog?.isShowing == true) {
dialog?.dismiss()
}
if (dialog == null) {
dialog = LoadingDialog(context)
dialog = LoadingDialog(context = context, message = message, onDismiss = onDismiss)
}
dialog?.show()
} catch (e: Exception) {
@@ -45,11 +53,14 @@ object Loading {
class LoadingDialog(
context: Context,
message: String = "加载中……",
private var onDismiss: () -> Unit = {}
message: String,
private var onDismiss: () -> Unit
) : Dialog(context, R.style.LoadingDialog) {
private val binding: DialogLoadingBinding by lazy {
DialogLoadingBinding.inflate(LayoutInflater.from(context))
}
init {
val binding = DialogLoadingBinding.inflate(LayoutInflater.from(context))
binding.tvMessage.text = message
setContentView(binding.root)
setCancelable(true)
@@ -64,6 +75,10 @@ class LoadingDialog(
onDismiss()
}
}
fun updateContent(content: String) {
binding.tvMessage.text = content
}
}
//class LoadingDialog3(
@@ -4,14 +4,18 @@ import android.annotation.SuppressLint
import android.graphics.Typeface
import android.os.Handler
import android.os.Looper
import android.text.Editable
import android.text.SpannableStringBuilder
import android.text.style.AbsoluteSizeSpan
import android.text.style.ForegroundColorSpan
import android.text.style.LineHeightSpan
import android.text.style.StyleSpan
import android.util.Log
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import androidx.core.graphics.toColorInt
import androidx.core.view.isEmpty
import androidx.core.widget.addTextChangedListener
@@ -22,26 +26,21 @@ import com.shuwei.dish.match.R
import com.shuwei.dish.match.adapter.SeasoningSearchAdapter
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.databinding.DialogBottomSheet2Binding
import com.shuwei.dish.match.model.GoodsItem
import com.shuwei.dish.match.net.RemoteRepository
import com.shuwei.dish.match.net.UiState
import com.shuwei.dish.match.databinding.DialogSeasoningSearchBinding
import com.shuwei.dish.match.entity.SeasoningEntity
import com.shuwei.dish.match.utils.KeyboardUtil
import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.addOnActionSearchListener
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import com.shuwei.dish.match.utils.ext.appendText
import com.shuwei.dish.match.utils.ext.buildSpannableString
import com.shuwei.dish.match.utils.ext.dp
import com.shuwei.dish.match.utils.ext.roundedDecimalPlace
import com.shuwei.dish.match.utils.ext.roundedOneDecimalPlace
import com.shuwei.dish.match.utils.ext.toast
/**
* @param goodsType 1-调料0-食材
* 调料搜索弹窗
*/
class BottomDialog2(
class SeasoningSearchDialog(
private var weighIndex: Int,
private var weighAddress: Int,
private var clickName: String? = null
@@ -50,24 +49,30 @@ class BottomDialog2(
const val TAG = "BottomDialog2"
}
private val goodsType: Int = 1
private var list = mutableListOf<GoodsItem>()
private var list = mutableListOf<SeasoningEntity>()
private lateinit var binding: DialogBottomSheet2Binding
private lateinit var binding: DialogSeasoningSearchBinding
private lateinit var searchAdapter: SeasoningSearchAdapter
private var goodsName: String? = null
private var activity: BaseActivity? = null
fun show(activity: BaseActivity, callback: (item: GoodsItem) -> Unit) {
fun show(activity: BaseActivity, callback: (item: SeasoningEntity) -> Unit) {
this.activity = activity
//直接使用BottomSheetDialog
var dialog: BottomSheetDialog? = null
val inflater = LayoutInflater.from(activity)
binding = DialogBottomSheet2Binding.inflate(inflater)
binding = DialogSeasoningSearchBinding.inflate(inflater)
//binding.tvSheetName.text = if (goodsType == 1) "调料检索" else "食材检索"
binding.etSheetInput.hint = if (goodsType == 1) "输入调料名称" else "输入食材名称或速记码"
binding.etSheetInput.run {
hint = "输入调料名称"
addTextChangedListener(afterTextChanged = { it: Editable? ->
if (it.isNullOrBlank()) {
list.clear()
searchAdapter.notifyDataSetChanged()
}
})
}
searchAdapter = SeasoningSearchAdapter(list).apply {
setOnItemClickListener { _, _, position ->
list[position].isClicked = true
@@ -98,12 +103,45 @@ class BottomDialog2(
setContentView(binding.root)
setCancelable(true)
behavior.skipCollapsed = false // 跳过折叠状态
// behavior.peekHeight = 880.dp
// window?.setLayout(-1, 880.dp)
//// behavior.peekHeight = 880.dp
//// window?.setLayout(-1, 880.dp)
// window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN)
//or WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN
//or WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING // 禁用额外平移
window?.setGravity(Gravity.BOTTOM)
window?.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
window?.setWindowAnimations(R.style.DialogSoftInputAnimation)
window?.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
show()
// val onGlobalLayoutListener = ViewTreeObserver.OnGlobalLayoutListener {
// val r = Rect()
// binding.root.getWindowVisibleDisplayFrame(r)
// val screenHeight = binding.root.rootView.height
// val keyboardHeight = screenHeight - r.bottom
//
// // 软键盘弹出(高度>200dp
// if (keyboardHeight > 200.dp) {
// // 调整 Dialog 底部边距,避开软键盘
// val layoutParams = binding.root.layoutParams as ViewGroup.MarginLayoutParams
//// layoutParams.bottomMargin = keyboardHeight
// layoutParams.bottomMargin = 200.dp
// binding.root.layoutParams = layoutParams
// } else {
// // 软键盘收起,恢复边距
// val layoutParams = binding.root.layoutParams as ViewGroup.MarginLayoutParams
// layoutParams.bottomMargin = 0
// binding.root.layoutParams = layoutParams
// }
// }
setOnDismissListener {
activity.hideStatusBar()
// binding.root.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalLayoutListener)
}
// binding.root.viewTreeObserver.addOnGlobalLayoutListener(onGlobalLayoutListener)
}
binding.refreshLayout.setOnRefreshListener {
pageNo = 1
@@ -129,6 +167,7 @@ class BottomDialog2(
}
}
binding.refreshLayout.setEnableRefresh(false)
binding.refreshLayout.setEnableLoadMore(false)
binding.tvClear.setOnClickListener {
Log.d(TAG, "show: weighAddress=$weighAddress")
WeightUtil.tareTwo(weighAddress)
@@ -137,7 +176,7 @@ class BottomDialog2(
// WeightUtil.tareTwo(num+1)
// }
}
WeightUtil.addWeightListener (
WeightUtil.addWeightListener(
weightKey = TAG,
getWeight = { address, state, weight ->
if (weighAddress == address) {
@@ -166,57 +205,111 @@ class BottomDialog2(
private var currentWeight = 0.toDouble()
private var pageNo = 1
private val pageSize = 120
private val pageSize = 50
/**
*/
@SuppressLint("NotifyDataSetChanged")
private fun getGoodsList() {
// val queryParams = mutableMapOf(
// "goodsType" to "1",
// "canteenId" to BaseApp.canteenId,
// "pageNo" to "$pageNo",
// "pageSize" to "$pageSize"
// )
// if (goodsName.isNullOrBlank().not()) {
// queryParams.put("name", goodsName!!)
// }
// val sb = StringBuilder().apply {
// append(UrlConfig.QUERY_GOODS_LIST)
// append("?")
// queryParams.forEach { (key, value) ->
// append("$key=$value&")
// }
// }
// sb.deleteCharAt(sb.length - 1)
// Log.d(TAG, "getGoodsList: url = $sb")
// HttpUtil.get(
// url = sb.toString(),
// doSuccess = {
// val json = it.toJsonString()
// Log.d(TAG, "getGoodsList: json=$json")
// val recordBean: SeasoningRecordBean? = json.toObject<SeasoningRecordBean>()
// if (recordBean == null || recordBean.records.isNullOrEmpty()) {
// activity?.toast( "暂未搜索到调料信息")
// return@get
// }
// val records = recordBean.records
// if (pageNo == 1) {
// list.clear()
// }
// list.addAll(records!!)
// searchAdapter.notifyDataSetChanged()
// if (pageNo == 1) {
// binding.refreshLayout.finishRefresh(1200)
// } else {
// binding.refreshLayout.finishLoadMore(1200)
// }
// val isLoadMoreEnable = records.size >= pageSize
// binding.refreshLayout.setEnableLoadMore(isLoadMoreEnable)
// if (isLoadMoreEnable) {
// pageNo++
// }
// }, doFailure = { code, msg ->
// activity?.toast(msg)
// if (pageNo == 1) {
// binding.refreshLayout.finishRefresh(1200)
// } else {
// binding.refreshLayout.finishLoadMore(1200)
// }
// })
val param = mutableMapOf<String, Any>(
"goodsType" to "$goodsType",
"goodsType" to "1",
"placeId" to BaseApp.canteenId,
"pageNum" to pageNo,
"pageSize" to pageSize
)
if (goodsName.isNullOrBlank().not()) {
param["goodsName"] = goodsName!!
param.put("goodsName", goodsName!!)
}
Log.d(TAG, "getGoodsList: param=$param")
activity?.lifecycleScope?.launch {
val state = RemoteRepository().queryGoodsList(param)
when (state) {
is UiState.Success -> {
val records = state.data
if (records.isNullOrEmpty()) {
activity?.toast(if (goodsType == 1) "暂未搜索到调料信息" else "暂未搜索到食材信息")
return@launch
}
if (pageNo == 1) {
list.clear()
}
list.addAll(records)
searchAdapter.notifyDataSetChanged()
if (pageNo == 1) {
binding.refreshLayout.finishRefresh(1200)
} else {
binding.refreshLayout.finishLoadMore(1200)
}
val isLoadMoreEnable = records.size >= pageSize
binding.refreshLayout.setEnableLoadMore(isLoadMoreEnable)
if (isLoadMoreEnable) {
pageNo++
}
}
is UiState.Error -> {
activity?.toast(state.msg)
if (pageNo == 1) {
binding.refreshLayout.finishRefresh(1200)
} else {
binding.refreshLayout.finishLoadMore(1200)
}
}
else -> {}
activity?.querySeasoningList(
param = param,
onSuccess = {
loadGoodsList(it)
},
onFailure = { code, msg ->
activity?.toast(msg)
finishRefresh()
}
)
}
@SuppressLint("NotifyDataSetChanged")
private fun loadGoodsList(records: MutableList<SeasoningEntity>?) {
finishRefresh()
if (records.isNullOrEmpty()) {
activity?.toast("暂未搜索到调料信息")
return
}
if (pageNo == 1) {
list.clear()
}
list.addAll(records)
searchAdapter.notifyDataSetChanged()
val isLoadMoreEnable = records.size >= pageSize
binding.refreshLayout.setEnableLoadMore(isLoadMoreEnable)
if (isLoadMoreEnable) {
pageNo++
}
}
private fun finishRefresh() {
if (pageNo == 1) {
binding.refreshLayout.finishRefresh()
} else {
binding.refreshLayout.finishLoadMore()
}
}
@@ -224,7 +317,7 @@ class BottomDialog2(
var topWeight = "$weight"
var bottomUnit = ""
if (weight >= 1000) {
topWeight = "${(weight/1000).roundedDecimalPlace(3)}"
topWeight = "${(weight / 1000).roundedDecimalPlace(3)}"
bottomUnit = "千克"
}
return buildSpannableString {
@@ -1,4 +1,4 @@
package com.shuwei.dish.match.db.entity
package com.shuwei.dish.match.entity
import androidx.room.Entity
import androidx.room.Ignore
@@ -18,17 +18,15 @@ data class CookFoodEntity(
//0-制作模式,1-采样模式,2-品控模式
var cookMode:Int = 0,
var isDel: Int = 0,
/** 接口提交状态:0-未提交(本地草稿或手动删除),1-接口提交成功后删除 */
var submitState: Int = 0,
var createTime: String = DateTimeUtil.formatDateTime(LocalDateTime.now()),
var dinnerType: String? = "0",
) : BaseEntity {
// @Ignore
// var stFoodInfoConstituteList: MutableList<CookFoodGoodsEntity>? = null
@Ignore
var stFoodInfoConstituteList: MutableList<CookFoodGoodsEntity>? = null
@Ignore
var matchingConstituteInfoList: MutableList<CookFoodGoodsEntity>? = null
@Ignore
var foodConstituteList: MutableList<CookFoodGoodsEntity>? = null
constructor() : this(foodId = "") // 必需的空构造
}
}
@@ -1,9 +1,9 @@
package com.shuwei.dish.match.db.entity
package com.shuwei.dish.match.entity
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
import com.shuwei.dish.match.db.BaseEntity
import com.shuwei.dish.match.model.GoodsItem
import com.shuwei.dish.match.utils.DateTimeUtil
import java.io.Serializable
import java.time.LocalDateTime
@@ -63,36 +63,27 @@ class CookFoodGoodsEntity(
var relateionType_dictText: String? = "",
var zjmCode: String? = "",
var materId: String? = "",
/** 物料编码 */
var goodsCode: String? = "",
/** 净材种类 */
var rawMaterialsType: String? = null,
var isDel: Int = 0,
var createTime: String = DateTimeUtil.formatDateTime(LocalDateTime.now())
) : BaseEntity, Serializable {
constructor() : this(goodsId = "") // 必需的空构造
constructor() : this(goodsId = "") // 必需的空构造,
/**
* 转换为 GoodsItem用于 UI DishPartAdapterPrepareFoodActivity
*/
fun toGoodsItem(): GoodsItem = GoodsItem(
goodsId = goodsId,
goodsName = goodsName,
popularName = popularName,
zjmCode = zjmCode,
materId = materId,
materCode = goodsCode,
materialType = materialType,
useWeight = useWeight,
relateionType = relateionType,
allEdible = allEdible,
goodsOrRelationCode = goodsOrRelationCode,
rawMaterialsType = rawMaterialsType
)
}
@Ignore
var isSetFinished: Boolean = false
@Ignore
var isItemClicked: Boolean = false
@Ignore
var isSamplingPage: Boolean = false
@Ignore
var isNewDishType: Boolean = false
@Ignore
var isClicked: Boolean = false
//true-原始数据,false-非原始数据
@Ignore
var isOriginalData: Boolean = true
}
@@ -0,0 +1,62 @@
package com.shuwei.dish.match.entity
data class HomeModeBean(
var modeName: String = "",
var modeDesc: String = "",
var modeIconId: Int = 0,
var isLocked: Boolean = false,
var onClick: (() -> Unit)? = null
)
data class FoodRecordBean(
var records: MutableList<FoodRecord>? = null,
var total: Int,
var size: Int,
var current: Int,
var page: Int
)
data class GoodsRecordBean(
var records: MutableList<CookFoodGoodsEntity>? = null,
var total: Int,
var size: Int,
var current: Int,
var page: Int
)
data class SeasoningRecordBean(
var records: MutableList<SeasoningEntity>? = null,
var total: Int,
var size: Int,
var current: Int,
var page: Int
)
data class ResetReasoningRecord(
val list: MutableList<SeasoningEntity>
)
//data class DishDetailEntity(
// var foodId: String? = null,
// var foodWeight: Double? = null,
// var foodRealWeight: Double? = null,
// var voList: List<DishPartEntity>? = null
//)
//data class DishPartEntity(
// var foodId: String? = null,
// var foodName: String? = null,
// var foodWeight: Int? = null,
// var useWeight: Int? = null,
// var useRealWeight: Int? = null,
// var goodsId: Int? = null,
// var goodsName: String? = null,
// var materialType: Int? = null,
// var isSetFinished: Boolean = false,
// var isItemClicked: Boolean = false,
// var isSamplingPage: Boolean = false
//)
@@ -0,0 +1,25 @@
package com.shuwei.dish.match.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.shuwei.dish.match.db.BaseEntity
//@Entity(tableName = "dm_dish")
//data class DishEntity (
// /**
// * 菜品id
// */
// @PrimaryKey var id: Long,
// /**
// * 菜品名称
// */
// var name: String? = "",
// /**
// * 菜品重量
// */
// var weight: Int? = 0,
//
// var sort: Int = 0,
// @ColumnInfo(name = "create_time") val createTime: Long = System.currentTimeMillis()
//): BaseEntity
@@ -1,4 +1,4 @@
package com.shuwei.dish.match.model
package com.shuwei.dish.match.entity
import java.io.Serializable
@@ -17,3 +17,4 @@ data class FoodRecord(
var isOriginalData: Boolean = true,
var sort: Int = 0
) : Serializable
@@ -0,0 +1,38 @@
//package com.shuwei.dish.match.entity
//
//import androidx.room.Ignore
//import androidx.room.PrimaryKey
//import com.shuwei.dish.match.db.BaseEntity
//
//data class GoodsInfoEntity(
// @PrimaryKey(autoGenerate = true) var id: Long = 0,
// var goodsId: Int,
// var goodsName: String? = "",
// var popularName: String? = "",
// var canteenId: String? = "",
// var goodsOrRelationCode: String? = "",
// var relateionType: Int? = -1,
// var relateionType_dictText: String? = "",
//
// var sort: Int = 0,
//
// var swMaterBaseMaterial: String? = "",
// var preMaterClass: String? = "",
// var preUseWeight: Int? = 0,
// var preWeight: Int? = 0,
// var oil: String? = "",
// var sugar: String? = "",
// /**
// * isDel表示数据是否已删除,1-已删除,为无效数据,0-正常使用,有效数据
// */
// var isDel: Int = 0,
// val createTime: Long = System.currentTimeMillis()
//) : BaseEntity {
// @Ignore
// var pageType: Int = 0
//
// @Ignore
// var isClicked: Boolean = false
//
// constructor() : this(id = -1, goodsId = -1) // 必需的空构造
//}
@@ -1,10 +1,9 @@
package com.shuwei.dish.match.db.entity
package com.shuwei.dish.match.entity
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
import com.shuwei.dish.match.db.BaseEntity
import com.shuwei.dish.match.model.GoodsItem
import com.shuwei.dish.match.utils.DateTimeUtil
import java.time.LocalDateTime
@@ -56,16 +55,6 @@ data class SeasoningEntity(
var relateionType_dictText: String? = "",
var zjmCode: String? = "",
var materId: String? = "",
/** 物料编码 */
var goodsCode: String? = "",
/** 净材种类 */
var rawMaterialsType: String? = null,
var sort: Int = 9999,
/**
* isDel表示数据是否已删除1-已删除为无效数据0-正常使用有效数据
@@ -73,25 +62,11 @@ data class SeasoningEntity(
var isDel: Int = 0,
var createTime: String = DateTimeUtil.formatDateTime(LocalDateTime.now())
) : BaseEntity {
/** UI 状态:格子类型,非数据库字段 */
@Ignore var pageType: Int = 0
/** UI 状态:是否选中,非数据库字段 */
@Ignore var isClicked: Boolean = false
@Ignore
var pageType: Int = 0
@Ignore
var isClicked: Boolean = false
constructor() : this(goodsId = "") // 必需的空构造
/** 转换为 GoodsItem,用于 UI 层展示 */
fun toGoodsItem(): GoodsItem = GoodsItem(
goodsId = goodsId,
goodsName = goodsName,
popularName = popularName,
zjmCode = zjmCode,
materId = materId,
materialType = materialType,
useWeight = useWeight,
relateionType = relateionType,
allEdible = allEdible,
goodsOrRelationCode = goodsOrRelationCode,
rawMaterialsType = rawMaterialsType
)
}
}
@@ -0,0 +1,70 @@
//package com.shuwei.dish.match.http
//
//import android.annotation.SuppressLint
//import android.util.Log
//import okhttp3.Call
//import okhttp3.Interceptor
//import okhttp3.OkHttpClient
//import okhttp3.Request
//import okhttp3.Response
//import java.util.concurrent.TimeUnit
//
//import javax.net.ssl.*
//import java.security.SecureRandom
//import java.security.cert.X509Certificate
//
//class HttpClient private constructor() {
// private val client: OkHttpClient by lazy {
// OkHttpClient.Builder()
// .apply {
// connectTimeout(15, TimeUnit.SECONDS)
// readTimeout(30, TimeUnit.SECONDS)
// writeTimeout(15, TimeUnit.SECONDS)
// sslSocketFactory(createSSLSocketFactory(), TrustAllCerts())
// hostnameVerifier { _, _ -> true }
// //if (BuildConfig.Debug) {
// addInterceptor(LoggingInterceptor())
// // }
// }
// .build()
// }
//
// companion object {
// val instance by lazy { HttpClient() }
// }
//
// fun newCall(request: Request): Call = client.newCall(request)
//
// inner class LoggingInterceptor : Interceptor {
// override fun intercept(chain: Interceptor.Chain): Response {
// val request = chain.request()
// // 打印请求日志
// Log.d("OkHttp", "--> ${request.method} ${request.url}")
//
// val response = chain.proceed(request)
// // 打印响应日志
// Log.d("OkHttp", "<-- ${response.code} ${response.request.url}")
//
// return response
// }
// }
//
// // 信任所有证书的TrustManager实现
// @SuppressLint("CustomX509TrustManager")
// class TrustAllCerts : X509TrustManager {
// @SuppressLint("TrustAllX509TrustManager")
// override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {}
// @SuppressLint("TrustAllX509TrustManager")
// override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {}
// override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf()
// }
//
// // 创建信任所有证书的SSLSocketFactory
// fun createSSLSocketFactory(): SSLSocketFactory {
// val sslContext = SSLContext.getInstance("TLS").apply {
// init(null, arrayOf(TrustAllCerts()), SecureRandom())
// }
// return sslContext.socketFactory
// }
//
//}
@@ -0,0 +1,199 @@
//package com.shuwei.dish.match.http
//
//import android.os.Handler
//import android.os.Looper
//import android.util.Log
//import com.google.gson.reflect.TypeToken
//import com.shuwei.dish.match.base.BaseApp
//import com.shuwei.dish.match.base.BaseReq
//import com.shuwei.dish.match.utils.SpTool
//import com.shuwei.dish.match.utils.ext.toType
//import okhttp3.Call
//import okhttp3.Callback
//import okhttp3.MediaType.Companion.toMediaTypeOrNull
//import okhttp3.Request
//import okhttp3.RequestBody.Companion.toRequestBody
//import okhttp3.Response
//import java.io.IOException
//import java.util.concurrent.Executors
//import java.util.concurrent.TimeUnit
//
//
//object HttpUtil {
//
// var loopGetToken = true
//
// fun runMainThread(action: () -> Unit) {
// Handler(Looper.getMainLooper()).post {
// action()
// }
// }
//
// // GET请求(HTTPS
// fun get(
// url: String,
// header: Map<String, String>?=null,
// doSuccess: (data: Any) -> Unit,
// doFailure: (code: Int?, msg: String?) -> Unit
// ) {
// val request = Request.Builder()
// .url(if (url.isHttpApi()) url else UrlConfig.BASE_URL + url)
// .apply {
// if (header != null) {
// header.forEach {
// addHeader(it.key, it.value)
// }
// } else {
// val token = getToken()
// if (token.isNotBlank()) {
// addHeader("X-Access-Token", token)
// }
// }
// }
// .get()
// .build()
// HttpClient.instance.newCall(request).enqueue(CallbackImpl(doSuccess, doFailure))
// }
////
//// fun get(
//// isHttps: Boolean = false,
//// host: String = "vip.shuziweidao.com",
//// pathSegmentList: List<String>,
//// queryParams: Map<String, String>,
//// doSuccess: (data: Any) -> Unit,
//// doFailure: (code: Int?, msg: String?) -> Unit
//// ) {
//// val token = getToken()
//// val url = HttpUrl.Builder()
//// .scheme(if (isHttps) "https" else "http")
//// .host(host)
//// .apply {
//// pathSegmentList.forEach { addPathSegment(it) }
//// queryParams.forEach { (key, value) -> addQueryParameter(key, value) }
//// }
//// .build()
//// val request = Request.Builder()
//// .url(url)
//// .apply {
//// if (token.isNotBlank()) {
//// addHeader("Authorization", token)
//// }
//// }
//// .get()
//// .build()
//// HttpClient.instance.newCall(request).enqueue(CallbackImpl(doSuccess, doFailure))
//// }
//
// // POST表单(HTTPS
//// fun postForm(url: String, params: Map<String, String>, callback: Callback) {
//// val formBody = FormBody.Builder().apply {
//// params.forEach { (k, v) -> add(k, v) }
//// }.build()
////
//// Request.Builder()
//// .url(url)
//// .post(formBody)
//// .build().let { HttpClient.instance.newCall(it).enqueue(callback) }
//// }
//
// // POST JSONHTTPS
// fun postJson(
// url: String,
// json: String,
// doSuccess: (data: Any) -> Unit,
// doFailure: (code: Int?, msg: String?) -> Unit
// ) {
// val body = json
// .toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
// val token = getToken()
// Request.Builder()
// .url(if (url.isHttpApi()) url else UrlConfig.BASE_URL + url)
// .apply {
// if (token.isNotBlank()) {
// addHeader("X-Access-Token", token)
// }
// }
// .post(body)
// .build()
// .let { HttpClient.instance.newCall(it).enqueue(CallbackImpl(doSuccess, doFailure)) }
// }
//
//
// val executor = Executors.newSingleThreadScheduledExecutor()
// var getTokenIsRunning = false
// private fun exeTokenThread() {
// if (getTokenIsRunning.not()) {
// executor.scheduleWithFixedDelay(
// task, 0, 1, TimeUnit.MINUTES
// )
// getTokenIsRunning = true
// }
// }
//
// val task = Runnable {
// if (BaseApp.token.isNullOrBlank()) {
// getAppToken()
// } else {
// executor.shutdown()
// }
// }
//
// fun getToken() = SpTool.getString(SpTool.TOKEN)
////BaseApp.instance?.token ?:""
////BaseApp.getSharedPref()?.getString("token", "") ?: ""
//
// fun getAppToken() {
// val url = "${UrlConfig.GET_TOKEN}?qrcodeId=${SpTool.getString(SpTool.DEVICE_ID)}"
//// Log.d(TAG, "getToken: url = $url")
// get(url = url, doSuccess = {
// SpTool.put(SpTool.TOKEN, it)
// BaseApp.token = it.toString()
// }) { code, msg ->
// //toast(msg)
// }
// }
//
// class CallbackImpl(
// private val doSuccess: (data: Any) -> Unit,
// private val doFailure: (code: Int?, msg: String?) -> Unit
// ) :
// Callback {
// override fun onFailure(call: Call, e: IOException) {
// runMainThread {
// e.printStackTrace()
// doFailure(-1, "服务异常,${e.message}")
// }
// }
//
// override fun onResponse(call: Call, response: Response) {
// val respData = response.body?.string()
// runMainThread {
// Log.d("HttpUtil", respData ?: "")
// runCatching {
// val typeToken = object : TypeToken<BaseReq<Any>>() {}
// val baseReq = respData?.toType<BaseReq<Any>>(typeToken = typeToken)
// if (baseReq == null) {
// doFailure(-1, "查询数据失败")
// return@runCatching
// }
// if (baseReq.code != 200) {
// doFailure(baseReq.code, baseReq.message)
// //baseReq.code == 500 ||
// if (baseReq.code == 401 && loopGetToken) {
// exeTokenThread()
// }
// return@runCatching
// }
// doSuccess(baseReq.result ?: "")
// }.onFailure {
// it.printStackTrace()
// doFailure(-1, "解析异常")
// }
// }
// }
//
// }
//
//}
//
//fun String.isHttpApi() = this.startsWith("http://") || this.startsWith("https://")
@@ -0,0 +1,37 @@
package com.shuwei.dish.match.http
//import com.shuwei.dish.match.base.BaseApp
object UrlConfig {
// private const val DEVICE_BASE_URL = "http://device.shuziweidao.com:8889"
// /**
// * device获取token
// */
// const val DEVICE_TOKEN = "${DEVICE_BASE_URL}/sys/getEquipmentToken"
// /**
// *获取配置信息
// */
// const val DEVICE_CONFIG = "${DEVICE_BASE_URL}/equipment/stEquipment/queryByEquipmentCode"
// const val BASE_URL = "http://vip.shuziweidao.com/shuwei-zhct"
// const val DISH_DETAIL = "$BASE_URL/scales/goodsUseList?foodId={foodId}&foodWeight={foodWeight}"
// const val DISH_LIST = "$BASE_URL/scales/getRestInfoFoods?eaId=99&type=0&foodName"
// const val BASE_URL = "http://192.168.1.207:9102"
// const val BASE_URL = "https://yyjk.shuziweidao.com/gateway"
const val BASE_URL = "https://yyjk.shuziweidao.com/gateway/"
// const val DISH_LIST = "$BASE_URL/scales/getRestInfoFoods?eaId=99&type=0&foodName"
// var DISH_DETAIL = "${BaseApp.configUrl}/food/stFoodInfoMatching/queryById"
//
// // "$BASE_URL/scales/generateToken?deviceId=1111111111111111111111111111"
//
// var GET_TOKEN = "${BaseApp.configUrl}/restaurant/equipment/stEquipment/getEquipmentToken"
// var SUBMIT_DISH = "${BaseApp.configUrl}/food/stFoodInfoMatching/saveoredit"
//
// var QUERY_GOODS_LIST = "${BaseApp.configUrl}/food/stFoodInfoMatching/queryGoodsInfoList"
// var QUERY_FOOD_LIST = "${BaseApp.configUrl}/food/stFoodInfoMatching/list"
// var SAMPLING_LIST = "${BaseApp.configUrl}/food/stFoodInfoMatching/queryHistorical/goodsInfoList"
}
@@ -1,11 +0,0 @@
package com.shuwei.dish.match.model
/**
* 已采集食材列表项数据模型
* @param foodName 食材名称
* @param collectCount 已采集数量
*/
data class CollectedFoodItem(
val foodName: String = "",
val collectCount: Int = 0
)
@@ -1,43 +0,0 @@
package com.shuwei.dish.match.model
import com.shuwei.dish.match.db.entity.CookFoodEntity
import com.shuwei.dish.match.db.entity.CookFoodGoodsEntity
import java.io.Serializable
/**
* 菜品详情接口传输对象(DTO),不依赖 Room
* 对应接口:getConstituteByFoodId / saveConstitute
*/
data class CookFoodDTO(
var foodId: String = "",
var foodName: String? = null,
var canteenId: String? = null,
var foodWeight: Double = 0.0,
// 0-制作模式,1-采样模式,2-品控模式
var cookMode: Int = 0,
var dinnerType: String? = "0",
// 接口返回的食材构成列表
var matchingConstituteInfoList: MutableList<CookFoodGoodsDTO>? = null,
var foodConstituteList: MutableList<CookFoodGoodsDTO>? = null
) : Serializable {
/**
* 转换为 Room 实体,用于写入本地数据库
* id/isDel/createTime 由 Room 自行管理,不从 DTO 携带
*/
fun toEntity(): CookFoodEntity = CookFoodEntity(
foodId = foodId,
foodName = foodName,
canteenId = canteenId,
foodWeight = foodWeight,
cookMode = cookMode,
dinnerType = dinnerType
).also { entity ->
entity.matchingConstituteInfoList = matchingConstituteInfoList
?.map { it.toEntity() }
?.toMutableList()
entity.foodConstituteList = foodConstituteList
?.map { it.toEntity() }
?.toMutableList()
}
}
@@ -1,119 +0,0 @@
package com.shuwei.dish.match.model
import com.shuwei.dish.match.db.entity.CookFoodEntity
import com.shuwei.dish.match.db.entity.CookFoodGoodsEntity
import java.io.Serializable
/**
* 菜品构成(主辅料/调料)接口传输对象(DTO),不依赖 Room
* 对应 CookFoodDTO 中 foodConstituteList、matchingConstituteInfoList 的元素
*/
data class CookFoodGoodsDTO(
var goodsId: String = "",
var goodsName: String? = null,
var foodId: String? = "",
var goodsOrRelationCode: String? = "",
// 调料数据顺序
var sort: Int = 0,
// 食材原材料:1,预制品:2
var relateionType: Int = 0,
// 物品类型:1主料 2辅料 3调料
var materialType: Int = 0,
// 物品 是否全部可食:true-全部可食,false-部分可食
var allEdible: Boolean = true,
// 物品 用料重量
var useWeight: Double? = 0.0,
var popularName: String? = "",
var canteenId: String? = "",
var relateionType_dictText: String? = "",
var zjmCode: String? = "",
var materId: String? = "",
// 物料编码
var goodsCode: String? = "",
// 净材种类
var rawMaterialsType: String? = null
) : Serializable {
/**
* 转换为 GoodsItem,用于 UI 层
*/
fun toGoodsItem(): GoodsItem = GoodsItem(
goodsId = goodsId,
goodsName = goodsName,
popularName = popularName,
zjmCode = zjmCode,
materId = materId,
materCode = goodsCode,
materialType = materialType,
useWeight = useWeight,
relateionType = relateionType,
allEdible = allEdible,
goodsOrRelationCode = goodsOrRelationCode,
rawMaterialsType = rawMaterialsType
)
/**
* 转换为 Room 实体,用于写入本地数据库
* id/pid/isDel/createTime 由 Room 自行管理,不从 DTO 携带
*/
fun toEntity(): CookFoodGoodsEntity = CookFoodGoodsEntity(
goodsId = goodsId,
goodsName = goodsName,
foodId = foodId,
goodsOrRelationCode = goodsOrRelationCode,
sort = sort,
relateionType = relateionType,
materialType = materialType,
allEdible = allEdible,
useWeight = useWeight,
popularName = popularName,
canteenId = canteenId,
relateionType_dictText = relateionType_dictText,
zjmCode = zjmCode,
materId = materId,
goodsCode = goodsCode,
rawMaterialsType = rawMaterialsType
)
}
/**
* Room 实体 → DTO 反向映射,用于将本地数据转为接口提交格式
* id/pid/isDel/createTime 为本地数据库字段,不参与提交
*/
fun CookFoodGoodsEntity.toDTO(): CookFoodGoodsDTO = CookFoodGoodsDTO(
goodsId = goodsId,
goodsName = goodsName,
foodId = foodId,
goodsOrRelationCode = goodsOrRelationCode,
sort = sort,
relateionType = relateionType,
materialType = materialType,
allEdible = allEdible,
useWeight = useWeight,
popularName = popularName,
canteenId = canteenId,
relateionType_dictText = relateionType_dictText,
zjmCode = zjmCode,
materId = materId,
goodsCode = goodsCode,
rawMaterialsType = rawMaterialsType
)
/**
* Room 实体 → DTO 反向映射,用于将本地数据转为接口提交格式
* id/isDel/createTime 为本地数据库字段,不参与提交
*/
fun CookFoodEntity.toDTO(): CookFoodDTO = CookFoodDTO(
foodId = foodId,
foodName = foodName,
canteenId = canteenId,
foodWeight = foodWeight,
cookMode = cookMode,
dinnerType = dinnerType,
matchingConstituteInfoList = matchingConstituteInfoList
?.map { it.toDTO() }
?.toMutableList(),
foodConstituteList = foodConstituteList
?.map { it.toDTO() }
?.toMutableList()
)
@@ -1,52 +0,0 @@
package com.shuwei.dish.match.model
import android.graphics.Bitmap
import android.net.Uri
import java.io.File
data class HomeModeBean(
var modeName: String = "",
var modeDesc: String = "",
var modeIconId: Int = 0,
var isLocked: Boolean = false,
var onClick: (() -> Unit)? = null
)
data class FoodCollectionBean(
var imageUri: Uri? = null,
var bitmap: Bitmap? = null,
var imageFile: File? = null,
var imageVector: FloatArray? = null,
var isShowCamera: Boolean = false,
var isFinish:Boolean = false,
var uploadSuccess:Boolean = false
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as FoodCollectionBean
if (isShowCamera != other.isShowCamera) return false
if (isFinish != other.isFinish) return false
if (uploadSuccess != other.uploadSuccess) return false
if (imageUri != other.imageUri) return false
if (bitmap != other.bitmap) return false
if (imageFile != other.imageFile) return false
if (!imageVector.contentEquals(other.imageVector)) return false
return true
}
override fun hashCode(): Int {
var result = isShowCamera.hashCode()
result = 31 * result + isFinish.hashCode()
result = 31 * result + uploadSuccess.hashCode()
result = 31 * result + (imageUri?.hashCode() ?: 0)
result = 31 * result + (bitmap?.hashCode() ?: 0)
result = 31 * result + (imageFile?.hashCode() ?: 0)
result = 31 * result + (imageVector?.contentHashCode() ?: 0)
return result
}
}
@@ -1,83 +0,0 @@
package com.shuwei.dish.match.model
import com.shuwei.dish.match.db.entity.CookFoodGoodsEntity
import com.shuwei.dish.match.db.entity.SeasoningEntity
import java.io.Serializable
/**
* 物品信息接口返回的数据类(DTO),不依赖 Room,同时承载 UI 状态字段
* 对应接口:queryGoodsInfoList,食材(goodsType=0)和调料(goodsType=1)共用此结构
*/
data class GoodsItem(
// 接口返回字段
var goodsId: String = "",
val goodsName: String? = null,
val popularName: String? = null,
val zjmCode: String? = null,
var materId: String? = null,
val materCode: String? = null,
// 从 CookFoodGoodsEntity 迁移的数据字段(DishPartAdapter 显示及转换用)
var materialType: Int = 0,
var useWeight: Double? = 0.0,
var relateionType: Int = 0,
var allEdible: Boolean = true,
var goodsOrRelationCode: String? = "",
// 从 CookFoodGoodsEntity @Ignore 迁移的 UI 状态字段
var isSetFinished: Boolean = false,
// var isItemClicked: Boolean = false,
var isSamplingPage: Boolean = false,
var isNewDishType: Boolean = false,
var isClicked: Boolean = false,
// true-原始数据,false-非原始数据(识图新增)
var isOriginalData: Boolean = true,
// 识图匹配分数,-1 表示不显示
var foodScore: Int = -1,
// 净材种类(当前选中值)
var rawMaterialsType: String? = null,
// 净材种类候选列表(临时字段,逗号分隔,不持久化)
var rawMaterialsTypes: String? = null,
var oldGoodsId: String? = null
) : Serializable {
/**
* 物料编码,与查询接口返回的 materCode 同值,统一对外暴露为 goodsCode 供下游使用
*/
val goodsCode: String? get() = materCode
/**
* 转换为 CookFoodGoodsEntity,用于写入数据库或传给 SubmitFoodActivity
*/
fun toCookFoodGoodsEntity(): CookFoodGoodsEntity = CookFoodGoodsEntity(
goodsId = goodsId,
goodsName = goodsName,
popularName = popularName,
zjmCode = zjmCode,
materId = materId,
goodsCode = goodsCode,
materialType = materialType,
useWeight = useWeight,
relateionType = relateionType,
allEdible = allEdible,
goodsOrRelationCode = goodsOrRelationCode,
rawMaterialsType = rawMaterialsType
)
/**
* 转换为 SeasoningEntity,用于保存调料完整信息到 dm_seasoning
*/
fun toSeasoningEntity(): SeasoningEntity = SeasoningEntity(
goodsId = goodsId,
goodsName = goodsName,
popularName = popularName,
zjmCode = zjmCode,
materId = materId,
goodsCode = goodsCode,
materialType = materialType,
relateionType = relateionType,
allEdible = allEdible,
goodsOrRelationCode = goodsOrRelationCode,
rawMaterialsType = rawMaterialsType
)
}
@@ -1,12 +0,0 @@
package com.shuwei.dish.match.model
/**
* 物品名称批量查询入参 DTO
* 用于 queryGoodsInfoList 接口 goodsNames 字段的元素类型
* @param goodsName 物品名称
* @param rawMaterialsType 净材种类
*/
data class GoodsNameQueryDTO(
val goodsName: String,
val rawMaterialsType: String
)
@@ -1,40 +0,0 @@
package com.shuwei.dish.match.model
/**
* 设置页面的列表项实体类
*
* @param type 菜单项类型,用于唯一标识每一项
* @param title 主标题文字
* @param subtitle 副标题文字,仅部分项目(如菜品模式)有值
* @param isHidden 是否为隐藏项,隐藏项需触发开发者模式后才显示
* @param onClick 点击回调,初始化时直接注入对应的业务逻辑
*/
data class SettingItem(
val type: Type,
val title: String,
val subtitle: String? = null,
val isHidden: Boolean = false,
val onClick: () -> Unit
) {
/** 设置菜单项类型枚举 */
enum class Type {
/** 菜品模式切换 */
COOK_MODE,
/** 调料区设置 */
SEASONING_CONFIG,
/** 食材采集 */
FOOD_COLLECT,
/** 减重配置 */
WEIGHT_CONFIG,
/** 数据库查看(隐藏项) */
DB_INSPECT,
/** 秤数据监控(隐藏项) */
SCALE_OBSERVE,
/** 切换环境(隐藏项) */
ENV_SWITCH,
/** 数据清除(隐藏项) */
CLEAR_DATA,
/** 测试调料拿取(隐藏项) */
TEST_SEASONING,
}
}
@@ -0,0 +1,129 @@
package com.shuwei.dish.match.net
import com.google.gson.JsonSyntaxException
import com.shuwei.dish.match.utils.ext.toJsonString
import com.shuwei.dish.match.utils.ext.toObject
import com.shuwei.dish.match.utils.ext.toType
import org.apache.http.conn.ConnectTimeoutException
import retrofit2.HttpException
import java.io.IOException
import java.net.SocketTimeoutException
import java.net.UnknownHostException
///**
// * 通用网络请求封装,统一处理异常
// * @param onRequest 实际的 Retrofit 接口请求
// * @param onSuccess 成功回调
// * @param onFailure 自定义错误回调(可选,不传则用默认提示)
// */
//suspend fun <T> apiRequest(
// onRequest: suspend () -> T,
// onSuccess: (T) -> Unit,
// onFailure: ((ApiException) -> Unit)? = null
//) {
// try {
// // 执行实际的网络请求
// val result = onRequest()
// onSuccess(result)
// } catch (e: Exception) {
// val apiException = getApiException(e)
// // 优先执行自定义错误回调,没有则用默认提示
// if (onFailure != null) {
// onFailure(apiException)
// } else {
// // 默认的错误提示(可根据 ErrorType 定制)
// val tip = apiException.errorMsg
// // 切换到UI线程弹提示(用 MainScope 确保在主线程)
// MainScope().launch {
// BaseApp.instance?.toast(tip)
// }
// }
// }
//}
/**
* 通用网络请求封装,统一处理异常
* @param onRequest 实际的 Retrofit 接口请求
* @param onSuccess 成功回调
* @param onFailure 自定义错误回调(可选,不传则用默认提示)
*/
suspend fun <T> request(
onRequest: suspend () -> ApiResponse<T>,
onSuccess: (T) -> Unit,
onFailure: (String, String) -> Unit
) {
var response: ApiResponse<T>?
try {
// 执行实际的网络请求
response = onRequest()
} catch (e: Exception) {
val apiException = getApiException(e)
onFailure("-1", apiException.errorMsg)
return
}
if (response.isSuccess()) {
onSuccess(response.data as T)
} else {
onFailure(response.code, response.msg ?: "")
}
}
data class CodeMsg(
var code: String? = null,
var msg: String? = null
)
fun getApiException(e: Exception): ApiException {
// 捕获各类网络异常并转换为自定义异常
return when (e) {
is HttpException -> httpException2ApiException(e)
is JsonSyntaxException -> ApiException(
ErrorType.NETWORK_ERROR,
errorMsg = "服务器响应异常,请稍后重试"
)
is UnknownHostException -> ApiException(
ErrorType.NETWORK_ERROR,
errorMsg = "网络未连接,请检查网络"
)
is SocketTimeoutException -> ApiException(
ErrorType.TIMEOUT_ERROR,
errorMsg = "请求超时,请稍后重试"
)
is ConnectTimeoutException -> ApiException(
ErrorType.TIMEOUT_ERROR,
errorMsg = "连接超时,请检查网络"
)
is IOException -> ApiException(
ErrorType.NETWORK_ERROR,
errorMsg = "网络异常:${e.message ?: "未知IO错误"}"
)
is ApiException -> e
else -> ApiException(
ErrorType.UNKNOWN_ERROR,
errorMsg = "未知错误:${e.message ?: "未知"}",
throwable = e
)
}
}
fun httpException2ApiException(e: HttpException): ApiException {
val body = e.response()?.errorBody()?.string()
val codeMsg: CodeMsg? = try {
body?.toObject<CodeMsg>()
} catch (ex: Exception) {
null
}
return ApiException(
ErrorType.NETWORK_ERROR,
errorMsg = if (codeMsg == null) "数据解析异常(${e.code()})"
else codeMsg.msg ?: "服务器错误(${e.code()})"
)
}
@@ -1,17 +1,13 @@
package com.shuwei.dish.match.net
import com.shuwei.dish.match.base.GlobalData
import com.shuwei.dish.match.model.CookFoodDTO
import com.shuwei.dish.match.model.FoodRecord
import com.shuwei.dish.match.model.GoodsItem
import okhttp3.MultipartBody
import okhttp3.RequestBody
import com.shuwei.dish.match.entity.CookFoodEntity
import com.shuwei.dish.match.entity.CookFoodGoodsEntity
import com.shuwei.dish.match.entity.FoodRecord
import com.shuwei.dish.match.entity.SeasoningEntity
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
import retrofit2.http.PartMap
import retrofit2.http.Query
import retrofit2.http.Url
@@ -24,7 +20,7 @@ interface ApiService {
suspend fun getFoodDetail(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/matching/app/getConstituteByFoodId",
@Query("foodId") foodId: String
): ApiResponse<CookFoodDTO?>
): ApiResponse<CookFoodEntity?>
/**
@@ -33,7 +29,7 @@ interface ApiService {
@POST
suspend fun submitCookFood(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/matching/app/saveConstitute",
@Body param: CookFoodDTO
@Body param: CookFoodEntity
): ApiResponse<Any?>
/**
@@ -41,7 +37,7 @@ interface ApiService {
*/
@POST
suspend fun searchFoodList(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/matching/app/queryPageList",
@Url url:String = "${GlobalData.appBaseUrl}/terminal/neglect/matching/app/queryPageList",
@Body param: MutableMap<String, Any>
): ApiResponse<MutableList<FoodRecord>?>
@@ -50,27 +46,68 @@ interface ApiService {
*/
@POST
suspend fun getSamplingList(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/matching/app/queryHistoryGoodsInfoList",
@Url url:String = "${GlobalData.appBaseUrl}/terminal/neglect/matching/app/queryHistoryGoodsInfoList",
@Body param: MutableMap<String, Any>
): ApiResponse<MutableList<FoodRecord>?>
/**
* 物品信息(食材 goodsType=0,调料 goodsType=1 共用此接口)
* 物品信息
*/
@POST
suspend fun queryGoodsList(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/matching/app/queryGoodsInfoList",
@Url url:String = "${GlobalData.appBaseUrl}/terminal/neglect/matching/app/queryGoodsInfoList",
@Body param: MutableMap<String, Any>
): ApiResponse<MutableList<GoodsItem>?>
): ApiResponse<MutableList<CookFoodGoodsEntity>?>
/**
* 提交采集图片数据
* 调料信息
*/
@Multipart
@POST
suspend fun uploadFoodVectorData(
@Url url: String = "${GlobalData.appBaseUrl}/terminal/neglect/matching/app/addWeighingCollectionData",
@PartMap params: MutableMap<String, RequestBody>,
@Part foodPics: List<MultipartBody.Part>
): ApiResponse<List<String>?>
suspend fun querySeasoningList(
@Url url:String = "${GlobalData.appBaseUrl}/terminal/neglect/matching/app/queryGoodsInfoList",
@Body param: MutableMap<String, Any>
): ApiResponse<MutableList<SeasoningEntity>?>
// /**
// *获取配置信息
// */
// @GET("/equipment/stEquipment/queryByEquipmentCode")
// suspend fun getDeviceInfo(
// @Query("equipmentCode") equipmentCode: String,
// @Query("appVersion") appVersion: String = App.appVersion
// ): ApiResponse<DeviceConfigInfo>
//
// @GET
// suspend fun getAccessToken(
// @Url url: String = UrlConfig.GET_ACCESS_TOKEN,
// @Query("qrcodeId") qrcodeId: String
// ): ApiResponse<String?>
//
// @GET
// suspend fun getShelfList(
// @Url url: String = UrlConfig.GET_SHELF_LIST,
// @Query("deviceId") deviceId: String
// ): ApiResponse<ShelfResult>
//
// @GET
// suspend fun getGoodsList(
// @Url url: String = UrlConfig.GET_GOODS_LIST,
// @Query("canteenId") canteenId: String = "0",
// @Query("goodsName") goodsName: String? = null,
// @Query("pageNo") pageNo: Int = 1,
// @Query("pageSize") pageSize: Int = 50
// ): ApiResponse<MutableList<GoodsModel>?>
//
// @POST
// suspend fun saveShelfGoodsList(
// @Url url: String = UrlConfig.SAVE_SHELF_GOODS_LIST,
// @Body body: ShelfBody
// ): ApiResponse<Any?>
}
@@ -1,10 +1,5 @@
package com.shuwei.dish.match.net
import java.io.IOException
import java.net.ConnectException
import java.net.SocketTimeoutException
import java.net.UnknownHostException
// 1. 网络异常类型枚举
enum class ErrorType {
NETWORK_ERROR, // 网络错误(无网)
@@ -21,14 +16,4 @@ class ApiException(
val errorCode: Int = -1,
val errorMsg: String,
val throwable: Throwable? = null
) : Exception(errorMsg, throwable)
/** 将任意异常转换为 ApiException,供 safeApiCall 统一使用 */
fun getApiException(e: Exception): ApiException = when (e) {
is ApiException -> e
is UnknownHostException -> ApiException(ErrorType.NETWORK_ERROR, errorMsg = "网络未连接,请检查网络")
is SocketTimeoutException -> ApiException(ErrorType.TIMEOUT_ERROR, errorMsg = "请求超时,请稍后重试")
is ConnectException -> ApiException(ErrorType.TIMEOUT_ERROR, errorMsg = "连接超时,请检查网络")
is IOException -> ApiException(ErrorType.NETWORK_ERROR, errorMsg = "网络异常:${e.message ?: "未知IO错误"}")
else -> ApiException(ErrorType.UNKNOWN_ERROR, errorMsg = "未知错误:${e.message ?: "未知"}", throwable = e)
}
) : Exception(errorMsg, throwable)
@@ -2,7 +2,7 @@ package com.shuwei.dish.match.net
import android.annotation.SuppressLint
import android.util.Log
import com.shuwei.dish.match.base.GlobalData
import com.shuwei.dish.match.http.UrlConfig
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
@@ -16,7 +16,7 @@ import kotlin.apply
import kotlin.jvm.java
val apiService: ApiService = Retrofit.Builder()
.baseUrl(GlobalData.appBaseUrl)
.baseUrl(UrlConfig.BASE_URL)
// .baseUrl(UrlConfig.DEVICE_BASE_URL)
.client(HttpManager.instance.client)
.addConverterFactory(GsonConverterFactory.create())
@@ -2,257 +2,110 @@ package com.shuwei.dish.match.net
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.model.CookFoodDTO
import com.shuwei.dish.match.model.FoodRecord
import com.shuwei.dish.match.model.GoodsItem
import com.shuwei.dish.match.model.GoodsNameQueryDTO
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import com.shuwei.dish.match.entity.CookFoodEntity
import com.shuwei.dish.match.entity.CookFoodGoodsEntity
import com.shuwei.dish.match.entity.FoodRecord
import com.shuwei.dish.match.entity.SeasoningEntity
import kotlinx.coroutines.launch
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody
import java.io.File
/**
* 网络请求 ViewModel,所有网络操作通过 RemoteRepository 发起,不直接依赖 apiService
* @param repository 网络数据仓库,默认使用单例实例
*/
class NetViewModel(
private val repository: RemoteRepository = RemoteRepository()
) : ViewModel() {
class NetViewModel : ViewModel() {
/**
* 提交制作菜品的 UI 状态流,UI 层通过 collect 监听
*/
private val _submitCookFoodState = MutableStateFlow<UiState<Any?>>(UiState.Idle)
val submitCookFoodState: StateFlow<UiState<Any?>> = _submitCookFoodState.asStateFlow()
/**
* 提交制作菜品
*/
fun submitCookFood(entity: CookFoodDTO) {
viewModelScope.launch {
_submitCookFoodState.value = UiState.Loading
_submitCookFoodState.value = repository.submitCookFood(entity)
}
}
/**
* 查询菜品详情的 UI 状态流,UI 层通过 collect 监听
*/
private val _foodDetailState = MutableStateFlow<UiState<CookFoodDTO?>>(UiState.Idle)
val foodDetailState: StateFlow<UiState<CookFoodDTO?>> = _foodDetailState.asStateFlow()
/**
* 查询菜品详情
*/
fun getFoodDetail(foodId: String) {
viewModelScope.launch {
_foodDetailState.value = UiState.Loading
_foodDetailState.value = repository.getFoodDetail(foodId)
}
}
fun resetFoodDetailState() {
_foodDetailState.value = UiState.Idle
}
/**
* 搜索菜品列表的 UI 状态流。
* 使用 replay=1 保证 Fragment 重建后能收到最近一次结果;
* emit 时包一层 Event wrapper 打破 equals 去重,确保相同数据也能触发 collect。
*/
private val _searchFoodState = MutableSharedFlow<UiState<MutableList<FoodRecord>?>>(
replay = 1,
extraBufferCapacity = 1
)
val searchFoodState: SharedFlow<UiState<MutableList<FoodRecord>?>> = _searchFoodState.asSharedFlow()
/**
* 搜索菜品列表
*/
fun searchFoodList(param: MutableMap<String, Any>) {
viewModelScope.launch {
// pageNum=1 时先 emit Idle 重置 replay 缓存,防止相同数据导致 SharedFlow 去重不触发
if (param["pageNum"] == 1) {
_searchFoodState.emit(UiState.Idle)
}
_searchFoodState.emit(UiState.Loading)
_searchFoodState.emit(repository.searchFoodList(param))
}
}
/**
* 搜索菜品列表(回调版本),结果通过 onResult 直接回调,不经过 StateFlow/SharedFlow
* 适合需要每次都能触发的场景(下拉刷新、上拉加载更多)
*/
fun searchFoodListWithCallback(
param: MutableMap<String, Any>,
onLoading: () -> Unit = {},
onResult: (UiState<MutableList<FoodRecord>?>) -> Unit
fun submitCookFood(
entity: CookFoodEntity,
onSuccess: (Any?) -> Unit,
onFailure: (String, String) -> Unit
) {
viewModelScope.launch {
onLoading()
onResult(repository.searchFoodList(param))
request(
onRequest = {
apiService.submitCookFood(param = entity)
},
onSuccess = onSuccess,
onFailure = onFailure
)
}
}
/**
*/
private val _samplingListState = MutableStateFlow<UiState<MutableList<FoodRecord>?>>(UiState.Idle)
val samplingListState: StateFlow<UiState<MutableList<FoodRecord>?>> = _samplingListState.asStateFlow()
fun resetSamplingListState() {
_samplingListState.value = UiState.Idle
}
/**
* 查询采样数据列表
*/
fun getSamplingList(param: MutableMap<String, Any>) {
viewModelScope.launch {
_samplingListState.value = UiState.Loading
_samplingListState.value = repository.getSamplingList(param)
}
}
/**
* 查询采样数据列表(回调版本),结果通过 onResult 直接回调,不经过 StateFlow
* 适合不需要跨生命周期持续监听的一次性查询场景
*/
fun getSamplingListWithCallback(
param: MutableMap<String, Any>,
onLoading: () -> Unit = {},
onResult: (UiState<MutableList<FoodRecord>?>) -> Unit
fun getFoodDetail(
foodId: String,
onSuccess: (CookFoodEntity?) -> Unit,
onFailure: (String, String) -> Unit
) {
viewModelScope.launch {
onLoading()
onResult(repository.getSamplingList(param))
request(
onRequest = {
apiService.getFoodDetail(foodId = foodId)
},
onSuccess = onSuccess,
onFailure = onFailure
)
}
}
/**
* 查询物品信息列表的 UI 状态流(食材/调料共用),UI 层通过 collect 监听
*/
private val _goodsListState = MutableStateFlow<UiState<MutableList<GoodsItem>?>>(UiState.Idle)
val goodsListState: StateFlow<UiState<MutableList<GoodsItem>?>> = _goodsListState.asStateFlow()
/**
* 食材搜索弹窗专用的 UI 状态流,与 goodsListState 隔离,避免弹窗请求影响其他页面
*/
private val _foodSearchGoodsListState = MutableStateFlow<UiState<MutableList<GoodsItem>?>>(UiState.Idle)
val foodSearchGoodsListState: StateFlow<UiState<MutableList<GoodsItem>?>> = _foodSearchGoodsListState.asStateFlow()
fun searchFoodList(
param: MutableMap<String, Any>,
onSuccess: (MutableList<FoodRecord>?) -> Unit,
onFailure: (String, String) -> Unit
) {
viewModelScope.launch {
request(
onRequest = {
apiService.searchFoodList(param = param)
},
onSuccess = onSuccess,
onFailure = onFailure
)
}
}
fun resetGoodsListState() {
_goodsListState.value = UiState.Idle
fun getSamplingList(
param: MutableMap<String, Any>,
onSuccess: (MutableList<FoodRecord>?) -> Unit,
onFailure: (String, String) -> Unit
) {
viewModelScope.launch {
request(
onRequest = {
apiService.getSamplingList(param = param)
},
onSuccess = onSuccess,
onFailure = onFailure
)
}
}
/**
* 查询物品信息列表(食材 goodsType=0,调料 goodsType=1 共用)
* @param goodsType 物品类型:0=食材,1=调料
* @param placeId 场所 ID,默认取当前食堂 ID
* @param pageNum 页码,默认第 1 页
* @param pageSize 每页条数,默认 50
* @param goodsName 按名称模糊搜索,为 null 时不传该字段
* @param goodsNames 按名称+净材种类列表批量查询,为 null 时不传该字段
*/
fun queryGoodsList(
goodsType: String,
placeId: String = BaseApp.canteenId,
pageNum: Int = 1,
pageSize: Int = 50,
goodsName: String? = null,
goodsNames: List<GoodsNameQueryDTO>? = null
param: MutableMap<String, Any>,
onSuccess: (MutableList<CookFoodGoodsEntity>?) -> Unit,
onFailure: (String, String) -> Unit
) {
val param = mutableMapOf<String, Any>(
"goodsType" to goodsType,
"placeId" to placeId,
"pageNum" to pageNum,
"pageSize" to pageSize
)
goodsName?.let { param["goodsName"] = it }
goodsNames?.let { param["goodsNames"] = it }
viewModelScope.launch {
_goodsListState.value = UiState.Loading
_goodsListState.value = repository.queryGoodsList(param)
request(
onRequest = {
apiService.queryGoodsList(param = param)
},
onSuccess = onSuccess,
onFailure = onFailure
)
}
}
/**
* 食材搜索弹窗专用的查询方法,结果写入 foodSearchGoodsListState,不影响 goodsListState
* @param goodsType 物品类型:0=食材,1=调料
* @param placeId 场所 ID,默认取当前食堂 ID
* @param pageNum 页码,默认第 1 页
* @param pageSize 每页条数,默认 50
* @param goodsName 按名称模糊搜索,为 null 时不传该字段
* @param goodsNames 按名称+净材种类列表批量查询,为 null 时不传该字段
*/
fun queryFoodSearchGoodsList(
goodsType: String,
placeId: String = BaseApp.canteenId,
pageNum: Int = 1,
pageSize: Int = 50,
goodsName: String? = null,
goodsNames: List<GoodsNameQueryDTO>? = null
fun querySeasoningList(
param: MutableMap<String, Any>,
onSuccess: (MutableList<SeasoningEntity>?) -> Unit,
onFailure: (String, String) -> Unit
) {
val param = mutableMapOf<String, Any>(
"goodsType" to goodsType,
"placeId" to placeId,
"pageNum" to pageNum,
"pageSize" to pageSize
)
goodsName?.let { param["goodsName"] = it }
goodsNames?.let { param["goodsNames"] = it }
viewModelScope.launch {
_foodSearchGoodsListState.value = UiState.Loading
_foodSearchGoodsListState.value = repository.queryGoodsList(param)
request(
onRequest = {
apiService.querySeasoningList(param = param)
},
onSuccess = onSuccess,
onFailure = onFailure
)
}
}
/**
* 上传采集菜品信息 UI 状态流,UI 层通过 collect 监听
*/
private val _uploadCollectState = MutableStateFlow<UiState<List<String>?>>(UiState.Idle)
val uploadCollectState: StateFlow<UiState<List<String>?>> = _uploadCollectState.asStateFlow()
/**
* 上传采集菜品信息(触发 StateFlow,适合单次上传场景)
*/
fun uploadFoodVectorData(
fileList: List<File>,
params: MutableMap<String, RequestBody>
) {
val fileParts = fileList.map { file ->
file.asRequestBody("multipart/form-data".toMediaTypeOrNull())
.let { MultipartBody.Part.createFormData("foodPics", file.name, it) }
}
viewModelScope.launch {
_uploadCollectState.value = UiState.Loading
_uploadCollectState.value = repository.uploadFoodVectorData(params, fileParts)
}
}
/**
* 上传采集菜品信息(直接返回结果,适合分批循环上传场景)
* @param fileList 图片文件列表
* @param params 附加参数
* @return UiState 包装的结果
*/
suspend fun uploadFoodVectorDataBatch(
fileList: List<File>,
params: MutableMap<String, RequestBody>
): UiState<List<String>?> {
val fileParts = fileList.map { file ->
//multipart/form-data image/jpeg
file.asRequestBody("multipart/form-data".toMediaTypeOrNull())
.let { MultipartBody.Part.createFormData("foodPics", file.name, it) }
}
return repository.uploadFoodVectorData(params, fileParts)
}
}
@@ -1,53 +0,0 @@
package com.shuwei.dish.match.net
import com.shuwei.dish.match.model.CookFoodDTO
import com.shuwei.dish.match.model.FoodRecord
import com.shuwei.dish.match.model.GoodsItem
import okhttp3.MultipartBody
import okhttp3.RequestBody
/**
* 网络数据仓库,封装所有 ApiService 调用
* ViewModel 不直接接触 apiService,统一通过此类发起网络请求
*/
class RemoteRepository {
/** 统一封装 API 调用:处理响应映射和异常转换,返回 UiState */
private suspend fun <T> safeApiCall(call: suspend () -> ApiResponse<T>): UiState<T?> {
return try {
val resp = call()
if (resp.isSuccess()) UiState.Success(resp.data)
else UiState.Error(resp.code, resp.msg ?: "")
} catch (e: Exception) {
UiState.Error("-1", getApiException(e).errorMsg)
}
}
/** 提交制作菜品 */
suspend fun submitCookFood(entity: CookFoodDTO): UiState<Any?> =
safeApiCall { apiService.submitCookFood(param = entity) }
/** 查询菜品详情 */
suspend fun getFoodDetail(foodId: String): UiState<CookFoodDTO?> =
safeApiCall { apiService.getFoodDetail(foodId = foodId) }
/** 搜索菜品列表 */
suspend fun searchFoodList(param: MutableMap<String, Any>): UiState<MutableList<FoodRecord>?> =
safeApiCall { apiService.searchFoodList(param = param) }
/** 查询采样数据列表 */
suspend fun getSamplingList(param: MutableMap<String, Any>): UiState<MutableList<FoodRecord>?> =
safeApiCall { apiService.getSamplingList(param = param) }
/** 查询物品信息列表(食材 goodsType=0,调料 goodsType=1 共用) */
suspend fun queryGoodsList(param: MutableMap<String, Any>): UiState<MutableList<GoodsItem>?> =
safeApiCall { apiService.queryGoodsList(param = param) }
/** 上传采集数据 */
suspend fun uploadFoodVectorData(
params: MutableMap<String, RequestBody>,
foodPics: List<MultipartBody.Part>
): UiState<List<String>?> =
safeApiCall { apiService.uploadFoodVectorData(params = params, foodPics = foodPics) }
}
@@ -1,15 +0,0 @@
package com.shuwei.dish.match.net
/**
* 统一 UI 状态模型,用于 ViewModel 向 UI 层传递请求状态
* Idle — 初始状态,未发起请求
* Loading — 请求进行中
* Success — 请求成功,携带数据
* Error — 请求失败,携带错误码和错误信息
*/
sealed class UiState<out T> {
object Idle : UiState<Nothing>()
object Loading : UiState<Nothing>()
data class Success<T>(val data: T) : UiState<T>()
data class Error(val code: String, val msg: String) : UiState<Nothing>()
}
@@ -0,0 +1,68 @@
package com.shuwei.dish.match.objbox
import android.graphics.Bitmap
import android.net.Uri
import java.io.File
data class FoodClassInfo(
var class_names: List<String>,
var class_to_idx: Map<String, Int>,
var idx_to_class: Map<String, String>
)
data class FoodCollectionBean(
var imageUri: Uri? = null,
var bitmap: Bitmap? = null,
var imageFile: File? = null,
var imageVector: FloatArray? = null,
var isShowCamera: Boolean = false,
var isFinish:Boolean = false,
var uploadSuccess:Boolean = false
)
data class ReceiptGoodsInfo(
var goodsId: String? = "",
var goodsName: String? = "",
var procurementUnit: String? = "",
var procurementCount: Int? = 0,
var procurementPrice: Double? = 0.0,
var procurementAmount: Double? = 0.0,
var receiptCount: Int? = 0,
var goodsWeight: Int? = 0,
var goodsState: String? = "",
var isSelected: Boolean = false,
var isLocalGoods: Boolean = false
)
//data class DropdownInfo(
// var id: String,
// var name: String
//)
//data class RecognizeResult(
// var name: String? = null,
// var image: String? = null,
// var isSelected: Boolean = false
//)
//data class GoodsSearchInfo(
// var id: String? = null,
// var name: String? = null,
// var isSelected: Boolean = false
//)
data class OperateBean(
var obj: Any? = null,
var typeList: MutableList<OperateType> = mutableListOf(),
var isSelected: Boolean = false,
var isLocalGoods: Boolean = false,
var isReceiptPage: Boolean = true
)
data class OperateType(
var value: String = "",
var width: Int = 0,
var textColor: String = "#FF999999",
var isBoldFont: Boolean = false,
var isShowIcon: Boolean = false
)
@@ -0,0 +1,37 @@
package com.shuwei.dish.match.objbox
import io.objectbox.annotation.Entity
import io.objectbox.annotation.HnswIndex
import io.objectbox.annotation.Id
import io.objectbox.annotation.VectorDistanceType
@Entity
data class Food(
@Id var id: Long = 0,
var name: String? = null,
var foodIdx: Int = 0,
@HnswIndex(dimensions = 512, distanceType = VectorDistanceType.DOT_PRODUCT)
var foodVector: FloatArray? = null
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Food
if (id != other.id) return false
if (foodIdx != other.foodIdx) return false
if (name != other.name) return false
if (!foodVector.contentEquals(other.foodVector)) return false
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + foodIdx
result = 31 * result + (name?.hashCode() ?: 0)
result = 31 * result + (foodVector?.contentHashCode() ?: 0)
return result
}
}
@@ -0,0 +1,308 @@
package com.shuwei.dish.match.objbox
import android.content.Context
import android.graphics.Bitmap
import android.util.Log
import androidx.core.graphics.scale
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.shuwei.dish.match.utils.AssetsTool
import com.shuwei.dish.match.utils.LogSaveUtil
import com.shuwei.dish.match.utils.ext.toJsonString
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.pytorch.IValue
import org.pytorch.Module
import org.pytorch.torchvision.TensorImageUtils
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
object FoodModule {
private lateinit var module_mobile: Module
private val NO_MEAN_RGB = floatArrayOf(0.0f, 0.0f, 0.0f)
private val NO_STD_RGB = floatArrayOf(1.0f, 1.0f, 1.0f)
// 1. 定义你的模型固定输入尺寸 (根据你的tflite模型修改,比如224x224)
private const val MODEL_INPUT_WIDTH = 300
private const val MODEL_INPUT_HEIGHT = 300
const val DEFAULT_FOOD_INDEX = -1
const val DEFAULT_QUERY_COUNT = 50
const val BAG_RATE = 0.05
suspend fun init(context: Context) {
withContext(Dispatchers.IO) {
module_mobile = Module.load(copyAssetToCache(context, "best_embedding_model_mobile.pt"))
initDefFoodData(context)
}
}
// fun uri2FloatArray(uri: Uri): FloatArray? {
// return MyApp.instance?.let { context ->
// ImageUtil.uriToBitmap(context, uri)?.let {
// bitmap2FloatArray(it)
// }
// }
// }
fun bitmap2FloatArray(originBitmap: Bitmap, isRecycle: Boolean): FloatArray? {
var rgb565Bitmap: Bitmap? = null
try {
val scaledBitmap = originBitmap.scale(MODEL_INPUT_WIDTH, MODEL_INPUT_HEIGHT)
if (isRecycle) {
originBitmap.recycle()
}
rgb565Bitmap = scaledBitmap.copy(Bitmap.Config.RGB_565, false)
scaledBitmap.recycle()
val inputTensor = TensorImageUtils.bitmapToFloat32Tensor(
rgb565Bitmap,
NO_MEAN_RGB, // [0.485, 0.456, 0.406] TORCHVISION_NORM_MEAN_RGB
NO_STD_RGB // [0.229, 0.224, 0.225] TORCHVISION_NORM_STD_RGB
)
val outputTensor = module_mobile.forward(IValue.from(inputTensor)).toTensor()
return outputTensor.dataAsFloatArray
} catch (e: OutOfMemoryError) {
e.printStackTrace()
logInfo("bitmap2FloatArray异常:${e.message}")
} finally {
if (rgb565Bitmap != null && rgb565Bitmap.isRecycled.not()) {
rgb565Bitmap.recycle()
}
//System.gc()
//System.runFinalization()
}
return null
}
// fun queryFood(uri: Uri, queryCount: Int = DEFAULT_QUERY_COUNT): List<String>? {
// return uri2FloatArray(uri)?.let {
// queryFood(it, queryCount)
// }
// }
// /**
// * 返回识别物品名称列表
// */
// fun queryFood(bitmap: Bitmap, queryCount: Int = DEFAULT_QUERY_COUNT): List<String> {
// val floatArray = bitmap2FloatArray(bitmap)
// return queryFood(floatArray, queryCount)
// }
// /**
// * 返回识别物品IdNameScore对象列表
// */
// fun queryFoodNameScore(bitmap: Bitmap, queryCount: Int = DEFAULT_QUERY_COUNT): List<IdNameScore> {
// val floatArray = bitmap2FloatArray(bitmap)
// return queryFoodNameScore(floatArray, queryCount)
// }
suspend fun queryFoodNameScore(
floatArray: FloatArray?,
queryCount: Int = DEFAULT_QUERY_COUNT
): List<IdNameScore> {
if (floatArray == null) return emptyList()
// val query: Query<Food> = box.query(Food_.foodVector.nearestNeighbors(floatArray, queryCount)).build()
// //查询比较分数
//// val tempList = query.findWithScores().sortedBy { it.score }.map { "${it.get().name}|${it.get().foodIdx}|${it.score}" }
// var idScoreList: List<IdWithScore>
// try {
// idScoreList = query.findIdsWithScores();
// } finally {
// // 先关闭Query,释放Cursor
// query.close()
// }
logInfo("queryFood向量:${floatArray.slice(0 until 50).toJsonString()}")
val idScoreList = ObjectBox.query(floatArray, queryCount)
// 批量加载所有实体,一次DB操作替代循环单条查询
val foodMap = ObjectBox.getByIds(idScoreList.map { it.id }).associateBy { it.id }
val nameScoreList = mutableListOf<IdNameScore>()
idScoreList.forEach {
val name = foodMap[it.id]?.name ?: ""
nameScoreList.add(IdNameScore(id = it.id, name = name, score = it.score))
}
logInfo("queryFood数据:${nameScoreList.toJsonString()}")
return nameScoreList
}
suspend fun getFoodScoreList(bitmap: Bitmap, queryCount: Int = DEFAULT_QUERY_COUNT): List<IdNameScore> {
val floatArray = bitmap2FloatArray(bitmap, false) ?: return emptyList()
logInfo("getFoodScoreList向量:${floatArray.toJsonString()}")
val nameScoreList = queryFoodNameScore(floatArray, queryCount)
if (nameScoreList.isEmpty()) {
return emptyList()
}
//暂时不过滤
val maxScoreList = nameScoreList
//.filter { it.score < 0.15 }
.groupBy { it.name }
.map { (_, value) -> value.minByOrNull { it.score }!! }
.toMutableList()
// val map = mutableMapOf<String, Int>()
// nameScoreList.forEach {
// val key = it.name
// val count = map[key] ?: 0
// map[key] = count + 1
// }
// val orderList = map.entries.sortedByDescending { it.value }.map { it.key }.toMutableList()
// val firstFood = nameScoreList[0].name
// orderList.remove(firstFood)
// orderList.add(0, firstFood)
//
// val sortedScoreList = maxScoreList.sortedWith(compareBy {
// orderList.indexOf(it.name)
// })
val sortedScoreList = maxScoreList.sortedBy { it.score }
logInfo("getFoodScoreList数据:${sortedScoreList.toJsonString()}")
return sortedScoreList
}
// suspend fun queryFood(floatArray: FloatArray, queryCount: Int = DEFAULT_QUERY_COUNT): List<String> {
// val map = mutableMapOf<String, Int>()
// val nameScoreList = queryFoodNameScore(floatArray, queryCount)
// nameScoreList.filter { it.score < 0.05 }.forEach {
// val count = map[it.name] ?: 0
// map[it.name] = count + 1
// }
// val list = map.entries.sortedByDescending { it.value }.map { it.key }
// return list
// }
data class IdNameScore(
val id: Long,
var name: String,
val score: Double
)
suspend fun initDefFoodData(context: Context, action: () -> Unit = {}) {
try {
val count = ObjectBox.countIndexField(DEFAULT_FOOD_INDEX)
if (count > 0) {
return
}
val embeddingsJson = AssetsTool.readAssetsFile(context, "data/embeddings.json")
val labelsJson = AssetsTool.readAssetsFile(context, "data/labels.json")
val classInfoJson = AssetsTool.readAssetsFile(context, "data/class_info.json")
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)
val foodMap = classInfo.idx_to_class
val list = mutableListOf<Food>()
val size = embeddingsList.size
val size2 = labelsList.size
val size3 = foodMap.size
logInfo("initDefFoodData: $size,$size2,$size3")
embeddingsList.forEachIndexed { index, floatList ->
val classIdx = labelsList[index]
val foodName = foodMap["$classIdx"]
val array = floatList.toFloatArray()
// ObjectBox.boxStore.runInTx {
// box.put(Food(name = foodName, foodVector = array, foodIdx = DEFAULT_FOOD_INDEX))
// }
val food = Food(name = foodName, foodVector = array, foodIdx = DEFAULT_FOOD_INDEX)
list.add(food)
}
// val tempList = list.chunked(10)
ObjectBox.putAll(list)
action()
} catch (e: Exception) {
e.printStackTrace()
logInfo("initDefFoodData: ---${e.message}--")
}
}
/**
* ,此方法的主要目的是:从assets 拷贝到 app的cache目录
* @param context
* @param fileName
* @return 例如是这样:/data/user/0/com.frizzle.pluginhookandroid9/cache/plugin-debug.apk
*
* 不可能反正SD
*/
// fun copyAssetToCache(context: Context, fileName: String): String? {
// // 此app的缓存目录 --> 会默认在 cache目录...,可以自己去看看哦
// val cacheDir = context.getCacheDir()
// if (!cacheDir.exists()) {
// cacheDir.mkdirs() // TODO 如果没有缓存目录,就创建
// }
// val outPath = File(cacheDir, fileName) // TODO 创建输出的文件位置
// if (outPath.exists()) {
// outPath.delete() // TODO 如果该文件已经存在,就删掉
// }
// var `is`: InputStream? = null // 读取
// var fos: FileOutputStream? = null // 写入
// try {
// // 创建文件,如果创建成功,就返回true
// val res = outPath.createNewFile()
// if (res) {
// `is` = context.getAssets().open(fileName) // 拿到main/assets目录的输入流,用于读取字节
// fos = FileOutputStream(outPath) // 读取出来的字节最终写到outPath
// val buf = ByteArray(`is`.available()) // 缓存区
// var byteCount: Int
//
// // 开始循环读取
// while ((`is`.read(buf).also { byteCount = it }) != -1) {
// fos.write(buf, 0, byteCount)
// }
// return outPath.getAbsolutePath()
// }
// } catch (e: IOException) {
// e.printStackTrace()
// } finally {
// try {
// // TODO 一定要记得关闭资源,为了不去性能的磨损
// fos?.flush()
// `is`?.close()
// fos?.close()
// } catch (e: IOException) {
// e.printStackTrace()
// }
// }
// return null
// }
fun copyAssetToCache(context: Context, fileName: String): String? {
val cacheFile = File(context.cacheDir, fileName)
val buffer = ByteArray(8 * 1024)
var inputStream: InputStream? = null
var outputStream: FileOutputStream? = null
try {
inputStream = context.assets.open(fileName)
outputStream = FileOutputStream(cacheFile)
var byteCount: Int
while (inputStream.read(buffer).also { byteCount = it } != -1) {
outputStream.write(buffer, 0, byteCount)
}
outputStream.channel.force(true) // 强制物理落盘,比flush更彻底
return cacheFile.absolutePath
} catch (e: IOException) {
e.printStackTrace()
logInfo("文件拷贝失败 fileName=$fileName, error=${e.message}")
// 拷贝失败时删除残缺文件,避免下次读取到损坏文件
if (cacheFile.exists()) {
cacheFile.delete()
}
} finally {
outputStream?.close()
inputStream?.close()
}
return null
}
private fun logInfo(msg:String) {
Log.d("FoodModule", msg)
LogSaveUtil.saveLogFile(msg)
}
}
@@ -0,0 +1,302 @@
/*
* Copyright 2024 ObjectBox Ltd. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.shuwei.dish.match.objbox
import android.annotation.SuppressLint
import android.content.Context
import android.util.Log
import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.utils.LogSaveUtil
import io.objectbox.Box
import io.objectbox.BoxStore
import io.objectbox.BoxStoreBuilder
import io.objectbox.exception.DbException
import io.objectbox.exception.FileCorruptException
import io.objectbox.kotlin.boxFor
import io.objectbox.query.Query
import io.objectbox.sync.Sync
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.util.zip.GZIPOutputStream
import com.shuwei.dish.match.BuildConfig
/**
* Singleton to keep BoxStore reference and provide current list of Notes Objects.
* Inserts demo data if no Objects are stored.
*/
object ObjectBox {
// 确保所有设备/版本使用相同的objectbox-models/default.json
// 避免直接修改自动生成的MyObjectBox类
// 跨版本升级时使用boxStore.runInTx执行数据迁移
private const val TAG = "ObjectBox"
// 添加 @Volatile 保证多线程下的可见性
@Volatile
var boxStore: BoxStore? = null
/**
* If building the [boxStore] failed, contains the thrown error message.
*/
var dbExceptionMessage: String? = null
private set
fun init(context: Context) {
if (boxStore != null && boxStore!!.isClosed.not()) return
// 优化:调大事务回收超时时间
System.setProperty("objectbox.finalizerTimeout", "30000");
// On Android make sure to pass a Context when building the Store.
boxStore = try {
MyObjectBox.builder()
.androidContext(context.applicationContext)
//.disableMultiProcess() // 开启多进程支持(按需关闭,单进程可删)
//.disableFileCompression() // 禁用文件压缩,避免存储异常
.build()
} catch (e: DbException) {
if (e.javaClass == DbException::class.java || e is FileCorruptException) {
// Failed to build BoxStore due to database file issue, store message;
// checked in NoteListActivity to notify user.
dbExceptionMessage = e.toString()
LogSaveUtil.saveLogFile("ObjectBox.init异常1:$dbExceptionMessage")
return
} else {
LogSaveUtil.saveLogFile("ObjectBox.init异常2:${e.message}")
// Failed to build BoxStore due to developer error.
throw e
}
}
if (BuildConfig.DEBUG) {
var syncAvailable = if (Sync.isAvailable()) "available" else "unavailable"
Log.d(
TAG,
"Using ObjectBox ${BoxStore.getVersion()} (${BoxStore.getVersionNative()}, sync $syncAvailable)"
)
// Enable ObjectBox Admin on debug builds.
// https://docs.objectbox.io/data-browser
io.objectbox.android.Admin(boxStore).start(context.applicationContext)
}
}
/**
* If the database file is not in use, compresses (GZIP) and copies it to the given [target].
* 增加 IOException 捕获,避免文件异常导致崩溃。
*/
fun copyAndGzipDatabaseFileTo(target: File, context: Context): Boolean {
if (BoxStore.isDatabaseOpen(context, null)) {
// Do not copy if database file is still in use.
// If it would be open, the copy will likely get corrupted
// as BoxStore may currently write data to the file.
Log.e(TAG, "Database file is still in use, can not copy.")
return false
}
// If a name was given when building BoxStore use that instead of the default below.
val dbName = BoxStoreBuilder.DEFAULT_NAME
return try {
File(context.filesDir, "objectbox/$dbName/data.mdb").inputStream().use { input ->
target.parentFile?.mkdirs()
GZIPOutputStream(target.outputStream()).use { output ->
input.copyTo(output, DEFAULT_BUFFER_SIZE)
}
}
true
} catch (e: IOException) {
Log.e(TAG, "备份数据库文件失败", e)
false
}
}
// 获取指定实体的Box,自动关联BoxStore
inline fun <reified T> getBox(): Box<T>? = boxStore?.boxFor()
// 协程安全执行数据库操作(推荐所有操作使用此方法)
suspend fun <T> safeDbOp(operation: suspend () -> T): T? {
return withContext(Dispatchers.IO) {
try {
dbMutex.withLock { operation() }
} catch (e: FileCorruptException) {
// 操作中触发损坏,尝试重建数据库
e.printStackTrace()
LogSaveUtil.saveLogFile("safeDbOp出现FileCorruptException异常:${e.message},${e.errorCode}")
// 使用安全调用替代 !! 断言,避免 NPE
val app = BaseApp.instance ?: return@withContext null
init(app)
null
} catch (e: Exception) {
e.printStackTrace()
LogSaveUtil.saveLogFile("safeDbOp出现其它异常:${e.message}")
null
} finally {
// 线程资源清理
boxStore?.closeThreadResources()
}
}
}
suspend fun query(floatArray: FloatArray, queryCount: Int) = safeDbOp {
val query: Query<Food>? = getBox<Food>()?.query(
Food_.foodVector
.nearestNeighbors(floatArray, queryCount)
)?.build()
try {
query?.findIdsWithScores()
} finally {
// 先关闭Query,释放Cursor
query?.close()
}
} ?: emptyList()
suspend fun get(id: Long) = safeDbOp {
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 {
getBox<Food>()?.put(entity)
}
suspend fun putAll(entities: List<Food>) = safeDbOp {
val box = getBox<Food>()
box?.put(entities)
}
// 使用 QueryBuilder 在数据库层计数,避免全量加载到内存
suspend fun countNameField(name: String) = safeDbOp {
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 {
if (!name.isNullOrBlank()) {
getBox<Food>()?.query(Food_.name.contains(name))?.build()?.find()
?.distinctBy { it.name }
} else {
getBox<Food>()?.all?.distinctBy { it.name }
} ?: emptyList()
}
// 使用 QueryBuilder 在数据库层删除,避免全量加载到内存
suspend fun remove(name: String) = safeDbOp {
getBox<Food>()?.query(Food_.name.equal(name))?.build()?.remove()
}
private val dbMutex = Mutex() // 协程并发锁,保证写入操作原子性
private const val DB_DIR_NAME = "objectbox" // ObjectBox 默认数据库目录
private const val BACKUP_DIR_NAME = "objectbox_backup" // 备份目录
// 检查内部存储是否可读写(操作数据库前调用)
// 修复:原逻辑错误地检查外部存储状态,数据库存于内部存储,与外部存储无关
fun isStorageAvailable(context: Context): Boolean {
return try {
context.filesDir.canRead() && context.filesDir.canWrite()
} catch (e: Exception) {
false
}
}
// 备份数据库到应用私有目录(无权限要求,推荐)
suspend fun backupDb(context: Context): Boolean = withContext(Dispatchers.IO) {
if (!isStorageAvailable(context)) return@withContext false
val dbDir = File(context.filesDir, DB_DIR_NAME)
val backupDir = File(context.filesDir, BACKUP_DIR_NAME)
return@withContext copyDir(dbDir, backupDir)
}
// 从备份恢复数据库(恢复后会重建BoxStore)
suspend fun restoreDb(context: Context): Boolean = withContext(Dispatchers.IO) {
if (!isStorageAvailable(context)) return@withContext false
val dbDir = File(context.filesDir, DB_DIR_NAME)
val backupDir = File(context.filesDir, BACKUP_DIR_NAME)
if (!backupDir.exists()) return@withContext false
// 先关闭旧的BoxStore,删除损坏文件,再恢复备份
boxStore?.close()
deleteDbFiles(context)
val isSuccess = copyDir(backupDir, dbDir)
// 重新初始化
init(context)
return@withContext isSuccess
}
// 递归删除数据库文件
private fun deleteDbFiles(context: Context) {
val dbDir = File(context.filesDir, DB_DIR_NAME)
if (dbDir.exists()) deleteDirRecursively(dbDir)
}
// 递归删除目录
private fun deleteDirRecursively(file: File) {
if (file.isDirectory) {
file.listFiles()?.forEach { deleteDirRecursively(it) }
}
file.delete()
}
// 递归复制目录(核心备份/恢复逻辑)
private fun copyDir(srcDir: File, destDir: File): Boolean {
return try {
if (!srcDir.exists()) return false
if (!destDir.exists()) destDir.mkdirs()
srcDir.listFiles()?.forEach { srcFile ->
val destFile = File(destDir, srcFile.name)
if (srcFile.isDirectory) {
copyDir(srcFile, destFile)
} else {
copyFile(srcFile, destFile)
}
}
true
} catch (e: IOException) {
e.printStackTrace()
false
}
}
// 复制单个文件(使用NIO,高效稳定)
private fun copyFile(srcFile: File, destFile: File) {
FileInputStream(srcFile).channel.use { srcChannel ->
FileOutputStream(destFile).channel.use { destChannel ->
destChannel.transferFrom(srcChannel, 0, srcChannel.size())
}
}
}
// 关闭BoxStore(应用退出时调用,可选)
fun close() {
boxStore?.close()
boxStore = null
}
}
@@ -0,0 +1,116 @@
package com.shuwei.dish.match.scale
import android.content.Context
import android.net.nsd.NsdManager
import android.net.nsd.NsdServiceInfo
import android.util.Log
/**
* mDNS 设备发现管理器(仅主设备运行)
* 持续扫描局域网,发现或丢失 DishMatch 子设备时通过回调通知连接管理器
*/
class MdnsDiscoveryManager(private val context: Context) {
companion object {
private const val TAG = "MdnsDiscoveryManager"
}
private val nsdManager: NsdManager by lazy {
context.getSystemService(Context.NSD_SERVICE) as NsdManager
}
private var discoveryListener: NsdManager.DiscoveryListener? = null
private var isDiscovering = false
/**
* 发现新设备时的回调:(deviceId, host, port)
*/
var onDeviceFound: ((deviceId: String, host: String, port: Int) -> Unit)? = null
/**
* 设备离线时的回调:(deviceId)
*/
var onDeviceLost: ((deviceId: String) -> Unit)? = null
/**
* 开始扫描局域网中的 DishMatch 设备
*/
fun startDiscovery() {
if (isDiscovering) return
discoveryListener = object : NsdManager.DiscoveryListener {
override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {
Log.e(TAG, "开始扫描失败: errorCode=$errorCode")
isDiscovering = false
}
override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) {
Log.e(TAG, "停止扫描失败: errorCode=$errorCode")
}
override fun onDiscoveryStarted(serviceType: String) {
Log.d(TAG, "mDNS 扫描已启动")
isDiscovering = true
}
override fun onDiscoveryStopped(serviceType: String) {
Log.d(TAG, "mDNS 扫描已停止")
isDiscovering = false
}
override fun onServiceFound(service: NsdServiceInfo) {
// 只处理 DishMatch 服务
if (!service.serviceName.startsWith("DishMatch-")) return
Log.d(TAG, "发现设备: ${service.serviceName}")
resolveService(service)
}
override fun onServiceLost(service: NsdServiceInfo) {
if (!service.serviceName.startsWith("DishMatch-")) return
val deviceId = service.serviceName.removePrefix("DishMatch-")
Log.d(TAG, "设备离线: $deviceId")
onDeviceLost?.invoke(deviceId)
}
}
nsdManager.discoverServices(
MdnsRegisterManager.SERVICE_TYPE,
NsdManager.PROTOCOL_DNS_SD,
discoveryListener
)
}
/**
* 解析服务获取 IP 和端口
*/
private fun resolveService(service: NsdServiceInfo) {
nsdManager.resolveService(service, object : NsdManager.ResolveListener {
override fun onResolveFailed(info: NsdServiceInfo, errorCode: Int) {
Log.e(TAG, "解析设备地址失败: ${info.serviceName}, errorCode=$errorCode")
}
override fun onServiceResolved(info: NsdServiceInfo) {
val deviceId = info.serviceName.removePrefix("DishMatch-")
val host = info.host?.hostAddress ?: return
val port = info.port
Log.d(TAG, "设备地址已解析: $deviceId -> $host:$port")
onDeviceFound?.invoke(deviceId, host, port)
}
})
}
/**
* 停止扫描,在应用退出时调用
*/
fun stopDiscovery() {
if (!isDiscovering) return
discoveryListener?.let {
try {
nsdManager.stopServiceDiscovery(it)
} catch (e: Exception) {
Log.e(TAG, "停止扫描异常: ${e.message}")
}
}
discoveryListener = null
}
}
@@ -0,0 +1,80 @@
package com.shuwei.dish.match.scale
import android.content.Context
import android.net.nsd.NsdManager
import android.net.nsd.NsdServiceInfo
import android.util.Log
/**
* mDNS 服务注册管理器(主设备和子设备均运行)
* 将本机 WebSocket 服务以 mDNS 形式广播到局域网,让主设备能发现本机 IP:PORT
*/
class MdnsRegisterManager(private val context: Context) {
companion object {
private const val TAG = "MdnsRegisterManager"
/** mDNS 服务类型,所有 DishMatch 设备使用统一类型 */
const val SERVICE_TYPE = "_dishmatch._tcp."
/** WebSocket 服务监听端口 */
const val WS_PORT = 8765
}
private val nsdManager: NsdManager by lazy {
context.getSystemService(Context.NSD_SERVICE) as NsdManager
}
private var registrationListener: NsdManager.RegistrationListener? = null
private var isRegistered = false
/**
* 向局域网注册 mDNS 服务
* @param deviceId 当前设备 ID,作为服务名称后缀,便于主设备识别
*/
fun register(deviceId: String) {
if (isRegistered) return
val serviceInfo = NsdServiceInfo().apply {
serviceName = "DishMatch-$deviceId"
serviceType = SERVICE_TYPE
port = WS_PORT
}
registrationListener = object : NsdManager.RegistrationListener {
override fun onRegistrationFailed(info: NsdServiceInfo, errorCode: Int) {
Log.e(TAG, "mDNS 注册失败, errorCode=$errorCode")
isRegistered = false
}
override fun onUnregistrationFailed(info: NsdServiceInfo, errorCode: Int) {
Log.e(TAG, "mDNS 注销失败, errorCode=$errorCode")
}
override fun onServiceRegistered(info: NsdServiceInfo) {
Log.d(TAG, "mDNS 注册成功: ${info.serviceName}")
isRegistered = true
}
override fun onServiceUnregistered(info: NsdServiceInfo) {
Log.d(TAG, "mDNS 已注销: ${info.serviceName}")
isRegistered = false
}
}
nsdManager.registerService(serviceInfo, NsdManager.PROTOCOL_DNS_SD, registrationListener)
}
/**
* 注销 mDNS 服务,在应用退出时调用
*/
fun unregister() {
if (!isRegistered) return
registrationListener?.let {
try {
nsdManager.unregisterService(it)
} catch (e: Exception) {
Log.e(TAG, "注销 mDNS 异常: ${e.message}")
}
}
registrationListener = null
}
}
@@ -0,0 +1,19 @@
package com.shuwei.dish.match.scale
/**
* 单个秤的数据快照
* @param deviceId 所属设备 ID
* @param address 秤地址编号(硬件 address,对应 WeightUtil 回调的 address 参数)
* @param weight 重量,单位克
* @param state 秤状态:1=稳定,0=不稳定,2=量程溢出(对应 WeightUtil 中的 STATE_* 常量)
* @param ts 数据时间戳(毫秒)
* @param ip 所属设备的 IP 地址,用于 UI 展示;网络传输数据中可能为空
*/
data class ScaleData(
val deviceId: String,
val address: Int,
val weight: Double,
val state: Int,
val ts: Long,
val ip: String = ""
)
@@ -0,0 +1,122 @@
package com.shuwei.dish.match.scale
import android.util.Log
import com.shuwei.dish.match.utils.WeightUtil
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import java.util.concurrent.ConcurrentHashMap
/**
* 秤数据聚合器(仅主设备运行)
* 统一管理本机秤(直接读取 WeightUtil)和远端子设备秤(通过 WebSocket 接收)的数据
* 通过 StateFlow 将所有秤的最新快照暴露给 UI 层
*
* 数据 Map 的 key 格式:"{deviceId}#{address}",便于 UI 按设备分组展示
*/
class ScaleDataAggregator(private val localDeviceId: String) {
companion object {
private const val TAG = "ScaleDataAggregator"
/** 本机秤监听器的 key,防止与其他监听器冲突 */
private const val LOCAL_LISTENER_KEY = "ScaleDataAggregator_local"
}
/** 所有秤的最新数据,UI 层通过此 Flow 订阅更新 */
private val _allScales = MutableStateFlow<Map<String, ScaleData>>(emptyMap())
val allScales: StateFlow<Map<String, ScaleData>> = _allScales.asStateFlow()
/** 内部缓存,并发安全 */
private val cache = ConcurrentHashMap<String, ScaleData>()
/** deviceId → IP 地址映射,用于写入 ScaleData.ip 字段 */
private val deviceIpMap = ConcurrentHashMap<String, String>()
/**
* 设置指定设备的 IP 地址
* 若缓存中已有该设备的数据且 ip 为空,立即回填并触发 StateFlow 更新,
* 解决早期数据包在 IP 到达前入缓存导致一直显示"未知"的问题
* @param deviceId 设备 ID
* @param ip IP 地址字符串
*/
fun setDeviceIp(deviceId: String, ip: String) {
if (ip.isEmpty()) return
deviceIpMap[deviceId] = ip
// 回填该设备所有 ip 为空的缓存条目
val keys = cache.keys.filter { it.startsWith("$deviceId#") }
if (keys.isEmpty()) return
var updated = false
keys.forEach { key ->
val old = cache[key] ?: return@forEach
if (old.ip.isEmpty()) {
cache[key] = old.copy(ip = ip)
updated = true
}
}
if (updated) {
_allScales.value = HashMap(cache)
Log.d(TAG, "已回填设备IP: $deviceId -> $ip, 共 ${keys.size}")
}
}
/**
* 启动本机秤数据监听
* 主设备本机秤直接读取 WeightUtil,无需经过网络
*/
fun startLocalCollection() {
WeightUtil.addWeightListener(LOCAL_LISTENER_KEY) { address, state, weight ->
val data = ScaleData(
deviceId = localDeviceId,
address = address,
weight = weight,
state = state,
ts = System.currentTimeMillis(),
ip = deviceIpMap[localDeviceId] ?: ""
)
updateCache(data)
}
Log.d(TAG, "本机秤监听已启动, deviceId=$localDeviceId")
}
/**
* 处理来自 WebSocket 客户端的远端秤数据
* 由 ScaleWebSocketClient.onScaleData 回调触发
* 自动补充该设备已记录的 IP 信息
*/
fun onRemoteScaleData(data: ScaleData) {
val ip = deviceIpMap[data.deviceId] ?: data.ip
updateCache(if (ip.isNotEmpty() && data.ip.isEmpty()) data.copy(ip = ip) else data)
}
/**
* 移除指定设备的所有秤数据(设备离线时调用)
*/
fun removeDevice(deviceId: String) {
val keysToRemove = cache.keys.filter { it.startsWith("$deviceId#") }
if (keysToRemove.isEmpty()) return
keysToRemove.forEach { cache.remove(it) }
deviceIpMap.remove(deviceId)
_allScales.value = HashMap(cache)
Log.d(TAG, "已移除设备数据: $deviceId, 共 ${keysToRemove.size} 个秤")
}
/**
* 停止本机秤监听并清空所有数据
*/
fun stop() {
WeightUtil.removeWeightListener(LOCAL_LISTENER_KEY)
cache.clear()
deviceIpMap.clear()
_allScales.value = emptyMap()
Log.d(TAG, "ScaleDataAggregator 已停止")
}
/**
* 更新缓存并发布新快照到 StateFlow
*/
private fun updateCache(data: ScaleData) {
val key = "${data.deviceId}#${data.address}"
cache[key] = data
_allScales.value = HashMap(cache)
}
}
@@ -0,0 +1,133 @@
package com.shuwei.dish.match.scale
import android.content.Context
import android.util.Log
import com.shuwei.dish.match.base.DeviceRole
import com.shuwei.dish.match.base.GlobalData
import com.shuwei.dish.match.utils.NetworkUtil
import kotlinx.coroutines.flow.StateFlow
/**
* 秤服务统一门面(单例)
* 根据当前设备角色(MASTER/SLAVE)启动对应的服务组合:
*
* 所有设备均运行:
* - MdnsRegisterManager:将本机 WebSocket 服务广播到局域网
* - ScaleWebSocketServer:对外提供本机秤数据推送服务
*
* 仅主设备(MASTER)额外运行:
* - MdnsDiscoveryManager:发现局域网内子设备
* - ScaleWebSocketClient:连接子设备 WebSocket
* - ScaleDataAggregator:聚合本机 + 远端所有秤数据
*
* 使用方式:
* 在 Application.onCreate() 中调用 ScaleServiceManager.start(context)
* 在 Application.onTerminate() 中调用 ScaleServiceManager.stop()
* 主设备 UI 通过 ScaleServiceManager.allScales 订阅全量秤数据
*/
object ScaleServiceManager {
private const val TAG = "ScaleServiceManager"
private var mdnsRegister: MdnsRegisterManager? = null
private var wsServer: ScaleWebSocketServer? = null
// 以下仅主设备使用
private var mdnsDiscovery: MdnsDiscoveryManager? = null
private var wsClient: ScaleWebSocketClient? = null
private var aggregator: ScaleDataAggregator? = null
/**
* 所有秤数据的实时流,仅主设备有效;子设备返回 null
* UI 层通过 lifecycleScope.launch { allScales?.collect { ... } } 订阅
*/
val allScales: StateFlow<Map<String, ScaleData>>?
get() = aggregator?.allScales
/**
* 主设备连接状态回调(仅子设备使用)
* 设置后会立即转发给 wsServer;需在 start() 之后设置才能生效
*/
var onMasterConnectionChanged: ((connected: Boolean) -> Unit)?
get() = wsServer?.onConnectionChanged
set(value) { wsServer?.onConnectionChanged = value }
/**
* 启动所有秤服务,在 Application.onCreate() 中调用
*/
fun start(context: Context) {
val deviceId = GlobalData.deviceId
val role = GlobalData.deviceRole
Log.d(TAG, "启动秤服务, deviceId=$deviceId, role=$role")
// 所有设备:启动 WebSocket 服务端 + mDNS 注册
wsServer = ScaleWebSocketServer(deviceId).also { it.start() }
mdnsRegister = MdnsRegisterManager(context).also { it.register(deviceId) }
if (role == DeviceRole.MASTER) {
startMasterServices(context, deviceId)
}
}
/**
* 获取本机局域网 IP,委托给 NetworkUtil 统一处理版本兼容
*/
private fun getLocalIp(context: Context): String {
return NetworkUtil.getLocalIpAddress(context)
}
/**
* 启动主设备专属服务
*/
private fun startMasterServices(context: Context, deviceId: String) {
// 聚合器:统一管理本机 + 远端秤数据
aggregator = ScaleDataAggregator(deviceId).also {
// 记录本机 IP,供本机秤数据展示使用
it.setDeviceIp(deviceId, getLocalIp(context))
it.startLocalCollection()
}
// WebSocket 客户端
val client = ScaleWebSocketClient().also {
it.onScaleData = { data -> aggregator?.onRemoteScaleData(data) }
}
wsClient = client
// mDNS 发现:发现子设备 → 建立 WebSocket 连接;设备离线 → 断开连接并清除数据
mdnsDiscovery = MdnsDiscoveryManager(context).also { discovery ->
discovery.onDeviceFound = { remoteId, host, port ->
// 不连接自身
if (remoteId != deviceId) {
Log.d(TAG, "发现子设备 $remoteId, 建立连接")
// 记录子设备 IP,供 UI 展示使用
aggregator?.setDeviceIp(remoteId, host)
client.connect(remoteId, host, port)
}
}
discovery.onDeviceLost = { remoteId ->
Log.d(TAG, "子设备离线 $remoteId, 断开连接并清除数据")
client.disconnect(remoteId)
aggregator?.removeDevice(remoteId)
}
discovery.startDiscovery()
}
}
/**
* 停止所有秤服务,在 Application.onTerminate() 或退出时调用
*/
fun stop() {
mdnsDiscovery?.stopDiscovery()
wsClient?.shutdown()
aggregator?.stop()
wsServer?.stop()
mdnsRegister?.unregister()
mdnsDiscovery = null
wsClient = null
aggregator = null
wsServer = null
mdnsRegister = null
Log.d(TAG, "所有秤服务已停止")
}
}
@@ -0,0 +1,145 @@
package com.shuwei.dish.match.scale
import android.util.Log
import com.google.gson.Gson
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
/**
* 秤数据 WebSocket 客户端连接管理器(仅主设备运行)
* 管理与多台子设备的 WebSocket 长连接,支持断线自动重连(指数退避)
*/
class ScaleWebSocketClient {
companion object {
private const val TAG = "ScaleWebSocketClient"
/** 初始重连间隔(毫秒) */
private const val RECONNECT_BASE_MS = 2_000L
/** 最大重连间隔(毫秒) */
private const val RECONNECT_MAX_MS = 30_000L
}
private val gson = Gson()
private val httpClient = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(0, TimeUnit.SECONDS) // WebSocket 长连接不设置读取超时
.pingInterval(20, TimeUnit.SECONDS) // 心跳保活
.build()
/** 各设备当前 WebSocket 连接,key = deviceId */
private val connections = ConcurrentHashMap<String, WebSocket>()
/** 各设备当前重连任务,key = deviceId */
private val reconnectTasks = ConcurrentHashMap<String, ScheduledFuture<*>>()
/** 各设备当前重连延迟,key = deviceId */
private val reconnectDelays = ConcurrentHashMap<String, Long>()
/** 已主动移除的设备集合,不对其触发重连 */
private val removedDevices = ConcurrentHashMap.newKeySet<String>()
private val scheduler: ScheduledExecutorService = Executors.newSingleThreadScheduledExecutor()
/** 收到秤数据时的回调,在子线程调用 */
var onScaleData: ((data: ScaleData) -> Unit)? = null
/**
* 连接到指定子设备
* @param deviceId 子设备 ID
* @param host 子设备 IP
* @param port 子设备 WebSocket 端口
*/
fun connect(deviceId: String, host: String, port: Int) {
removedDevices.remove(deviceId)
reconnectDelays[deviceId] = RECONNECT_BASE_MS
doConnect(deviceId, host, port)
}
private fun doConnect(deviceId: String, host: String, port: Int) {
val url = "ws://$host:$port"
val request = Request.Builder().url(url).build()
val ws = httpClient.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
Log.d(TAG, "已连接子设备: $deviceId ($url)")
connections[deviceId] = webSocket
reconnectDelays[deviceId] = RECONNECT_BASE_MS
reconnectTasks.remove(deviceId)?.cancel(false)
}
override fun onMessage(webSocket: WebSocket, text: String) {
try {
val data = gson.fromJson(text, ScaleData::class.java)
onScaleData?.invoke(data)
} catch (e: Exception) {
Log.w(TAG, "解析秤数据失败: ${e.message}")
}
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
Log.w(TAG, "连接子设备失败: $deviceId, ${t.message}")
connections.remove(deviceId)
scheduleReconnect(deviceId, host, port)
}
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
Log.d(TAG, "子设备连接已关闭: $deviceId, reason=$reason")
connections.remove(deviceId)
scheduleReconnect(deviceId, host, port)
}
})
connections[deviceId] = ws
}
/**
* 指数退避重连
*/
private fun scheduleReconnect(deviceId: String, host: String, port: Int) {
if (removedDevices.contains(deviceId)) return
val delay = reconnectDelays[deviceId] ?: RECONNECT_BASE_MS
Log.d(TAG, "将在 ${delay}ms 后重连子设备: $deviceId")
val future = scheduler.schedule({
if (!removedDevices.contains(deviceId)) {
doConnect(deviceId, host, port)
}
}, delay, TimeUnit.MILLISECONDS)
reconnectTasks[deviceId]?.cancel(false)
reconnectTasks[deviceId] = future
// 下次重连延迟翻倍,上限 30s
reconnectDelays[deviceId] = minOf(delay * 2, RECONNECT_MAX_MS)
}
/**
* 断开指定子设备连接(不再重连)
* @param deviceId 子设备 ID
*/
fun disconnect(deviceId: String) {
removedDevices.add(deviceId)
reconnectTasks.remove(deviceId)?.cancel(false)
connections.remove(deviceId)?.close(1000, "设备离线")
reconnectDelays.remove(deviceId)
Log.d(TAG, "已断开子设备: $deviceId")
}
/**
* 关闭所有连接并释放资源,在应用退出时调用
*/
fun shutdown() {
val ids = connections.keys.toList()
ids.forEach { disconnect(it) }
scheduler.shutdownNow()
httpClient.dispatcher.executorService.shutdown()
Log.d(TAG, "ScaleWebSocketClient 已关闭")
}
}
@@ -0,0 +1,134 @@
package com.shuwei.dish.match.scale
import android.util.Log
import com.google.gson.Gson
import com.shuwei.dish.match.utils.WeightUtil
import org.java_websocket.WebSocket
import org.java_websocket.handshake.ClientHandshake
import org.java_websocket.server.WebSocketServer
import java.net.InetSocketAddress
/**
* 秤数据 WebSocket 服务端(主设备和子设备均运行)
* 监听 WeightUtil 回调,将本机秤数据实时推送给所有已连接的主设备客户端
*/
class ScaleWebSocketServer(private val deviceId: String) {
companion object {
private const val TAG = "ScaleWebSocketServer"
/** 节流间隔:两次推送之间的最小间隔(毫秒) */
private const val THROTTLE_MS = 100L
}
private val gson = Gson()
private var server: InternalServer? = null
/** 主设备连接状态变化回调:true=已连接,false=已断开;在主线程外调用,需自行切换线程 */
var onConnectionChanged: ((connected: Boolean) -> Unit)? = null
/** 每个秤地址的最近一次推送时间,用于节流 */
private val lastPushTime = mutableMapOf<Int, Long>()
/** 缓存各秤最新数据,新客户端连接时立即同步当前状态 */
private val latestData = mutableMapOf<Int, ScaleData>()
/**
* 启动 WebSocket 服务器,并注册 WeightUtil 回调
*/
fun start() {
server = InternalServer(MdnsRegisterManager.WS_PORT)
server?.isReuseAddr = true
server?.start()
Log.d(TAG, "WebSocket 服务端已启动,端口 ${MdnsRegisterManager.WS_PORT}")
// 注册本机秤数据监听
WeightUtil.addWeightListener(TAG) { address, state, weight ->
val now = System.currentTimeMillis()
// 节流:同一地址 100ms 内只推送一次
if (now - (lastPushTime[address] ?: 0L) < THROTTLE_MS) return@addWeightListener
lastPushTime[address] = now
val data = ScaleData(
deviceId = deviceId,
address = address,
weight = weight,
state = state,
ts = now
)
latestData[address] = data
broadcast(gson.toJson(data))
}
}
/**
* 停止 WebSocket 服务器,移除秤监听
*/
fun stop() {
WeightUtil.removeWeightListener(TAG)
try {
server?.stop(1000)
} catch (e: Exception) {
Log.e(TAG, "停止服务端异常: ${e.message}")
}
server = null
latestData.clear()
lastPushTime.clear()
Log.d(TAG, "WebSocket 服务端已停止")
}
/**
* 向所有已连接客户端广播消息
*/
private fun broadcast(json: String) {
server?.connections?.forEach { conn ->
if (conn.isOpen) {
try {
conn.send(json)
} catch (e: Exception) {
Log.w(TAG, "推送失败: ${e.message}")
}
}
}
}
/**
* 内部 WebSocket 服务器实现
*/
private inner class InternalServer(port: Int) :
WebSocketServer(InetSocketAddress(port)) {
override fun onOpen(conn: WebSocket, handshake: ClientHandshake) {
Log.d(TAG, "主设备已连接: ${conn.remoteSocketAddress}")
onConnectionChanged?.invoke(true)
// 新客户端连接后,立即推送所有秤的最新数据
latestData.values.forEach { data ->
try {
conn.send(gson.toJson(data))
} catch (e: Exception) {
Log.w(TAG, "初始推送失败: ${e.message}")
}
}
}
override fun onClose(conn: WebSocket, code: Int, reason: String, remote: Boolean) {
Log.d(TAG, "主设备已断开: ${conn.remoteSocketAddress}, reason=$reason")
// 若已无任何连接,通知子设备 UI 更新为未连接状态
if (server?.connections.isNullOrEmpty()) {
onConnectionChanged?.invoke(false)
}
}
override fun onMessage(conn: WebSocket, message: String) {
// 服务端无需处理来自主设备的消息
}
override fun onError(conn: WebSocket?, ex: Exception) {
Log.e(TAG, "WebSocket 服务端错误: ${ex.message}")
}
override fun onStart() {
Log.d(TAG, "WebSocket 服务端 onStart")
connectionLostTimeout = 60
}
}
}
@@ -12,11 +12,17 @@ import android.util.SparseArray
import android.widget.FrameLayout
import androidx.core.graphics.toColorInt
import androidx.core.view.forEach
import androidx.core.content.ContextCompat
import com.shuwei.dish.match.R
import androidx.lifecycle.ViewModelProvider
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.db.entity.SeasoningEntity
import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.databinding.LayoutFoodRemindBinding
import com.shuwei.dish.match.db.AppRepository
import com.shuwei.dish.match.dialog.CommonDialog
import com.shuwei.dish.match.entity.SeasoningEntity
import com.shuwei.dish.match.entity.ResetReasoningRecord
import com.shuwei.dish.match.utils.AddressUtil
import com.shuwei.dish.match.utils.JsonAssetsLoader
import com.shuwei.dish.match.utils.MultiClickDetector
import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.appendText
import com.shuwei.dish.match.utils.ext.buildSpannableString
@@ -24,11 +30,12 @@ import com.shuwei.dish.match.utils.ext.dp
import com.shuwei.dish.match.utils.ext.startActivity
import com.shuwei.dish.match.utils.ext.toast
import com.shuwei.dish.match.utils.ext.visible
import com.shuwei.dish.match.viewmodel.AppViewModel
import com.shuwei.dish.match.viewmodel.factory.AppFactory
import com.shuwei.dish.match.adapter.TextCellAdapter
import com.shuwei.dish.match.databinding.ActivityDeviceConfigBinding
import com.shuwei.dish.match.dialog.BottomDialog2
import com.shuwei.dish.match.model.GoodsItem
import com.shuwei.dish.match.dialog.SeasoningSearchDialog
import com.shuwei.dish.match.utils.SpTool
import com.shuwei.dish.match.utils.TextCellTool
import com.shuwei.dish.match.utils.ext.clickWithDebounce
import com.shuwei.dish.match.utils.ext.gone
@@ -36,7 +43,7 @@ class DeviceConfigActivity : BaseActivity() {
companion object {
const val TAG = "DeviceSettingActivity"
const val TAG = "DeviceConfigActivity"
const val COOK_MODE = "cookMode"
@@ -54,9 +61,10 @@ class DeviceConfigActivity : BaseActivity() {
private lateinit var binding: ActivityDeviceConfigBinding
private var cookMode: Int = 0
private lateinit var appViewModel: AppViewModel
// private val weightArray = SparseIntArray()
private var cookMode: Int = 0
// private val weightArray = SparseIntArray()
private val weightArray = SparseArray<Double>()
private val addressArray = AddressUtil.getWeighAddressArray()
@@ -65,6 +73,7 @@ class DeviceConfigActivity : BaseActivity() {
binding = ActivityDeviceConfigBinding.inflate(layoutInflater)
setContentView(binding.root)
cookMode = intent.getIntExtra(COOK_MODE, 0)
initViewModel()
setHeaderBackground()
setTitleBar(titleBarAction = {
it.visible()
@@ -72,6 +81,13 @@ class DeviceConfigActivity : BaseActivity() {
it.text = "设备配置"
}, rightIconActon = {
it.gone()
// it.alpha = 0.0F
// it.setImageResource(R.drawable.ic_setting)
// it.setOnClickListener {_->
// detector.setOnDelayedMultiClickListener(it) {
// defaultDataSettingDialog()
// }
// }
})
loadQualitySpan(isEnable = false)
@@ -104,7 +120,9 @@ class DeviceConfigActivity : BaseActivity() {
binding.rbModeSampling.isChecked = false
loadCookSpan(isClicked = true)
loadSamplingSpan()
startActivity<SelectDishActivity> { }
startActivity<SelectDishActivity> {
putExtra(SelectDishActivity.IS_CONFIG_PAGE, true)
}
finish()
}
binding.rbModeSampling.setOnClickListener {
@@ -113,7 +131,8 @@ class DeviceConfigActivity : BaseActivity() {
binding.rbModeSampling.isChecked = true
loadCookSpan()
loadSamplingSpan(isClicked = true)
startActivity<SamplingListActivity> { }
startActivity<SamplingListActivity> {
putExtra(SamplingListActivity.IS_CONFIG_PAGE, true) }
finish()
}
addWeighListener()
@@ -125,84 +144,131 @@ class DeviceConfigActivity : BaseActivity() {
WeightUtil.addWeightListener(
weightKey = TAG,
getWeight = { address, state, weight ->
Log.d(TAG, "addWeighListener: address=$address,stat=$state,weight=$weight")
weightArray.put(address, weight)
val item = seasoningItems.firstOrNull { address == addressArray[it.sort] }
item?.let {
it.useWeight = weight
updateGridData(it)
// 根据是否量程溢出切换格子背景色
val frameLayout = binding.include.root.findViewWithTag<FrameLayout>(it.sort.toString())
frameLayout?.background = if (state == WeightUtil.STATE_OVER_WEIGHT) {
ContextCompat.getDrawable(this, R.drawable.shape_red_10_corners)
} else {
ContextCompat.getDrawable(this, R.drawable.selector_text_cell)
}
}
Log.d(TAG, "addWeighListener: update-----------------------")
})
Log.d(TAG, "addWeighListener: address=$address,stat=$state,weight=$weight")
weightArray.put(address, weight)
val item = seasoningItems.firstOrNull { address == addressArray[it.sort] }
item?.let {
it.useWeight = weight.toDouble()
updateGridData(it)
}
Log.d(TAG, "addWeighListener: update-----------------------")
})
}
private var seasoningItems = mutableListOf<SeasoningEntity>()
private fun updateGridData(entity: SeasoningEntity) {
val gridLayout = binding.include.root
val tag = entity.sort.toString()
val frameLayout = gridLayout.findViewWithTag<FrameLayout>(tag)
TextCellTool.loadLayout(frameLayout, entity)
}
private fun addGridItemListener() {
binding.include.root.forEach { child ->
child.clickWithDebounce {
val sort = child.tag.toString().toInt()
val entity = seasoningItems.firstOrNull { it.sort == sort }
?: SeasoningEntity().also { it.sort = sort }
val entity = seasoningItems.firstOrNull { it.sort == sort } ?: SeasoningEntity()
clickGridItem(sort, entity)
}
}
}
private fun loadSeasoning() {
Log.d(TAG, "loadSeasoning,start-----------------------")
appViewModel.loadSeasoning { list ->
Log.d(TAG, "loadSeasoning,list.size=${list.size}-----------------------")
seasoningItems.clear()
// 补全缺失的格子:对 0~11 中没有对应 sort 的位置,追加空实体
val existingSorts = list.map { it.sort }.toSet()
for (index in 0 until 12) {
if (index !in existingSorts) {
list.add(SeasoningEntity().also { it.sort = index })
}
val detector = MultiClickDetector(targetCount = 10, intervalMs = 800)
private fun defaultDataSettingDialog() {
val remindBinding = LayoutFoodRemindBinding.inflate(layoutInflater)
remindBinding.tvDialogTitle.text = "温馨提示"
remindBinding.tvDialogContent.text =
"您好,使用默认配置作为您的调料数据,则已有配置将被清除,确认吗?"
CommonDialog(this).apply {
messageView = remindBinding.root
leftText = "取消"
rightText = "确认"
onLeftClick = { dismiss() }
onRightClick = {
dismiss()
resetList()
}
seasoningItems.addAll(list)
// 按 sort 字段找到对应 FrameLayout,填充数据
list.forEach { entity ->
val tag = entity.sort.toString()
val frameLayout = binding.include.root.findViewWithTag<FrameLayout>(tag)
frameLayout?.let { TextCellTool.loadLayout(it, entity) }
onDismiss = { hideStatusBar() }
}.show()
}
private fun resetList() {
runCatching {
val record = JsonAssetsLoader.parseJsonFromAssets<ResetReasoningRecord>(
this,
"default_seasoning_list.json"
)
// val weightArray = WeightUtil.weightArray
appViewModel.clearAllSeasoning {
record?.list?.forEach { entity ->
entity.useWeight = weightArray[addressArray[entity.sort]].toDouble()
appViewModel.saveSeasoning(entity) {
updateGridData(entity)
}
}
// loadSeasoning()
}
}.onFailure { it.printStackTrace() }
}
private fun initViewModel() {
val db = BaseApp.instance!!.database
val factory =
AppFactory(AppRepository(db.appDao()))
appViewModel =
ViewModelProvider(this, factory)[AppViewModel::class.java]
}
private fun loadSeasoning() {
appViewModel.loadSeasoning {
seasoningItems.clear()
seasoningItems.addAll(it)
initConfigData()
setGridData(seasoningItems)
}
}
private fun initConfigData() {
repeat(12) { num ->
val firstOne = seasoningItems.firstOrNull { it.sort == num }
if (firstOne == null) {
seasoningItems.add(SeasoningEntity().also { it.sort = num })
}
}
}
private fun setGridData(list: MutableList<SeasoningEntity>) {
val gridLayout = binding.include.root
list.forEach { entity ->
val tag = entity.sort.toString()
val frameLayout = gridLayout.findViewWithTag<FrameLayout>(tag)
TextCellAdapter.loadLayout(frameLayout, entity)
}
}
private fun updateGridData(entity: SeasoningEntity) {
val gridLayout = binding.include.root
val tag = entity.sort.toString()
val frameLayout = gridLayout.findViewWithTag<FrameLayout>(tag)
TextCellAdapter.loadLayout(frameLayout, entity)
}
private fun clickGridItem(sort: Int, entity: SeasoningEntity) {
val currentAddress = AddressUtil.getWeighAddressArray().get(sort)
BottomDialog2(
// val seasoningEntity = list.first{it.sort == sort}
SeasoningSearchDialog(
weighIndex = sort,
weighAddress = currentAddress,
clickName = entity.goodsName
).show(this) { item ->
syncItem(entity, item)
//3-调料
entity.materialType = 3
entity.sort = sort
saveSeasoning(entity) {
val frameLayout = binding.include.root.findViewWithTag<FrameLayout>(sort.toString())
frameLayout?.let { TextCellTool.loadLayout(it, entity) }
val tag = sort.toString()
val gridLayout = binding.include.root
val frameLayout = gridLayout.findViewWithTag<FrameLayout>(tag)
TextCellAdapter.loadLayout(frameLayout, entity)
}
}
}
private fun syncItem(oldItem: SeasoningEntity, newItem: GoodsItem) {
private fun syncItem(oldItem: SeasoningEntity, newItem: SeasoningEntity) {
oldItem.run {
goodsId = newItem.goodsId
goodsName = newItem.goodsName
@@ -212,6 +278,11 @@ class DeviceConfigActivity : BaseActivity() {
allEdible = newItem.allEdible
useWeight = newItem.useWeight
popularName = newItem.popularName
canteenId = newItem.canteenId
relateionType_dictText = newItem.relateionType_dictText
foodId = newItem.foodId
sort = newItem.sort
}
}
@@ -5,20 +5,26 @@ import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.activity.viewModels
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import com.shuwei.dish.match.R
import com.shuwei.dish.match.adapter.DishPartAdapter
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.databinding.ActivityDishSamplingBinding
import com.shuwei.dish.match.databinding.LayoutFoodRemindBinding
import com.shuwei.dish.match.dialog.BottomDialog
import com.shuwei.dish.match.db.AppRepository
import com.shuwei.dish.match.dialog.FoodSearchDialog
import com.shuwei.dish.match.dialog.CommonDialog
import com.shuwei.dish.match.db.entity.CookFoodEntity
import com.shuwei.dish.match.model.GoodsItem
import com.shuwei.dish.match.model.FoodRecord
import com.shuwei.dish.match.entity.CookFoodEntity
import com.shuwei.dish.match.entity.CookFoodGoodsEntity
import com.shuwei.dish.match.entity.FoodRecord
import com.shuwei.dish.match.net.NetViewModel
import com.shuwei.dish.match.utils.AddressUtil
import com.shuwei.dish.match.utils.KeyboardUtil
import com.shuwei.dish.match.utils.SwipeCallback
import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.addOnActionSearchListener
import com.shuwei.dish.match.utils.ext.clickWithDebounce
@@ -27,6 +33,8 @@ import com.shuwei.dish.match.utils.ext.startActivity
import com.shuwei.dish.match.utils.ext.toJsonString
import com.shuwei.dish.match.utils.ext.toast
import com.shuwei.dish.match.utils.ext.visible
import com.shuwei.dish.match.viewmodel.AppViewModel
import com.shuwei.dish.match.viewmodel.factory.AppFactory
import java.io.Serializable
class DishSamplingActivity : BaseActivity() {
@@ -54,6 +62,7 @@ class DishSamplingActivity : BaseActivity() {
setTitleBar()
setHeaderBackground()
addViewClickListener()
initViewModel()
foodName = intent.getStringExtra(FOOD_NAME)
binding.etInputDish.setText(foodName)
@@ -87,6 +96,16 @@ class DishSamplingActivity : BaseActivity() {
})
}
private lateinit var appViewModel: AppViewModel
private fun initViewModel() {
val db = BaseApp.instance!!.database
val factory =
AppFactory(AppRepository(db.appDao()))
appViewModel =
ViewModelProvider(this, factory)[AppViewModel::class.java]
}
fun getCookFoodList(action: (MutableList<CookFoodEntity>?) -> Unit) {
appViewModel.getCookFoodList(cookMode = 1) { action(it) }
}
@@ -127,9 +146,9 @@ class DishSamplingActivity : BaseActivity() {
}
val list: MutableList<GoodsItem> = mutableListOf()
val list: MutableList<CookFoodGoodsEntity> = mutableListOf()
private var tempDishEntity: GoodsItem? = null
private var tempDishEntity: CookFoodGoodsEntity? = null
private var dishMaterialType = 1
@@ -150,6 +169,13 @@ class DishSamplingActivity : BaseActivity() {
layoutManager =
LinearLayoutManager(this@DishSamplingActivity, LinearLayoutManager.VERTICAL, false)
adapter = dishAdapter
val itemTouchHelper = ItemTouchHelper(SwipeCallback(dishAdapter) { position ->
list.removeAt(position)
dishAdapter.notifyItemRemoved(position)
dishAdapter.notifyItemRangeChanged(position, list.size - position)
toast("已删除")
})
itemTouchHelper.attachToRecyclerView(this)
}
binding.etInputDishType.addOnActionSearchListener {
searchDishType()
@@ -212,8 +238,8 @@ class DishSamplingActivity : BaseActivity() {
}
val food = FoodRecord()
binding.etInputDish.let {
if (it.tag!=null && it.tag.toString() == it.text.toString()) {
food.foodId = foodId
if (it.tag != null && it.tag.toString() == it.text.toString()) {
food.foodId = foodId
}
}
food.foodName = showFoodName
@@ -221,7 +247,7 @@ class DishSamplingActivity : BaseActivity() {
Log.d(TAG, "goToSubmit: goodsList:${list.toJsonString()}")
// startActivity<SubmitDishActivity> {
startActivity<SubmitFoodActivity> {
putExtra(SubmitFoodActivity.GOODS_LIST, list.map { it.toCookFoodGoodsEntity() }.toMutableList() as java.io.Serializable)
putExtra(SubmitFoodActivity.GOODS_LIST, list as java.io.Serializable)
putExtra(SubmitFoodActivity.FOOD_ITEM, food as Serializable)
}
}
@@ -233,17 +259,16 @@ class DishSamplingActivity : BaseActivity() {
}
private fun jumpSearch(v: View) {
launch(Intent(this, FoodSearchActivity::class.java).apply {
val intent = Intent(this, FoodSearchActivity::class.java).apply {
putExtra(FoodSearchActivity.FOOD_NAME, binding.etInputDish.text.toString().trim())
}) { intent ->
intent?.let {
foodId = it.getStringExtra(FoodSearchActivity.FOOD_ID)
foodName = it.getStringExtra(FoodSearchActivity.FOOD_NAME)
binding.etInputDish.run {
setText(foodName)
tag = foodName
setSelection(text.length)
}
}
startActivity(intent) {
foodId = it?.getStringExtra(FoodSearchActivity.FOOD_ID)
foodName = it?.getStringExtra(FoodSearchActivity.FOOD_NAME)
binding.etInputDish.run {
setText(foodName)
tag = foodName
setSelection(text.length)
}
}
KeyboardUtil.hideKeyboard(v.context, v)
@@ -251,7 +276,7 @@ class DishSamplingActivity : BaseActivity() {
private fun searchDishType() {
val inputText = binding.etInputDishType.text.toString().trim()
BottomDialog(goodsType = 0, defGoodsName = inputText).show(this@DishSamplingActivity) { item ->
FoodSearchDialog(defGoodsName = inputText).show(this@DishSamplingActivity) { item ->
val filterResult = list.firstOrNull { it.goodsName == item.goodsName }
if (filterResult != null) {
toast("不允许重复添加同一食材")
@@ -261,7 +286,17 @@ class DishSamplingActivity : BaseActivity() {
setText(item.goodsName)
setSelection(length())
}
tempDishEntity = item
tempDishEntity = CookFoodGoodsEntity().apply {
goodsId = item.goodsId
goodsName = item.goodsName
relateionType = item.relateionType ?: 0
goodsOrRelationCode = item.goodsOrRelationCode
isNewDishType = true
// TODO: 待定
//allEdible = item.allEdible
}
}
}
@@ -11,9 +11,7 @@ import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.databinding.ActivityFoodSearchBinding
import com.shuwei.dish.match.databinding.LayoutEmptyViewBinding
import com.shuwei.dish.match.model.FoodRecord
import com.shuwei.dish.match.net.UiState
import com.shuwei.dish.match.entity.FoodRecord
import com.shuwei.dish.match.utils.KeyboardUtil
import com.shuwei.dish.match.utils.ext.addOnActionSearchListener
import com.shuwei.dish.match.utils.ext.gone
@@ -36,7 +34,7 @@ class FoodSearchActivity : BaseActivity() {
private var list: MutableList<FoodRecord> = mutableListOf()
private var pageType = 0
private var dinnerType = "0"
private var dinnerType = "1"
private val recordAdapter by lazy {
FoodRecordAdapter(list).apply {
isStateViewEnable = true
@@ -64,7 +62,7 @@ class FoodSearchActivity : BaseActivity() {
binding = ActivityFoodSearchBinding.inflate(layoutInflater)
setContentView(binding.root)
pageType = intent.getIntExtra(PAGE_TYPE, 0)
dinnerType = intent.getStringExtra(DINNER_TYPE)?:"0"
dinnerType = intent.getStringExtra(DINNER_TYPE) ?: "0"
setHeaderBackground()
foodName = intent.getStringExtra(FOOD_NAME)
binding.etInputDish.setText(foodName)
@@ -130,7 +128,7 @@ class FoodSearchActivity : BaseActivity() {
}
private var pageNo = 1
private var pageSize = 100
private var pageSize = 10
fun getDinnerTypeText(): String {
return when (dinnerType) {
"1" -> "早餐"
@@ -139,58 +137,107 @@ class FoodSearchActivity : BaseActivity() {
else -> "早餐"
}
}
@SuppressLint("NotifyDataSetChanged")
private fun queryListInfo(input: String) {
showLoading()
val param = mutableMapOf<String, Any>(
// val map = mutableMapOf(
// "foodName" to input.trim(),
// "pageNo" to "$pageNo",
// "pageSize" to "$pageSize",
// "canteenId" to BaseApp.canteenId
// "dinnerType" to getDinnerTypeText()
// )
// val sb = StringBuilder(UrlConfig.QUERY_FOOD_LIST).apply {
// append("?")
// map.forEach { (key, value) -> append("$key=$value&") }
// }
// sb.deleteCharAt(sb.length - 1)
// HttpUtil.get(
// url = sb.toString(),
// doSuccess = {
// delayDismissLoading()
// finishRefresh()
// binding.refreshLayout.run {
// setEnableRefresh(true)
// }
// val json = it.toJsonString()
// val recordBean: FoodRecordBean? = json.toObject<FoodRecordBean>()
// if (recordBean == null || recordBean.records.isNullOrEmpty()) {
//// toast("暂未搜索到相关菜品信息")
// if (pageNo == 1) {
// loadEmptyView()
// }
// return@get
// }
// val records = recordBean.records
// if (pageNo == 1) {
// list.clear()
// }
// list.addAll(records!!)
// recordAdapter.notifyDataSetChanged()
// val isLoadMoreEnable = records.size >= pageSize
// binding.refreshLayout.setEnableLoadMore(isLoadMoreEnable)
// if (isLoadMoreEnable) {
// pageNo++
// }
// },
// doFailure = { code, msg ->
// binding.refreshLayout.run {
// setEnableRefresh(true)
// }
// toast(msg)
// delayDismissLoading()
// finishRefresh()
// if (pageNo == 1) {
// loadEmptyView()
// }
// }
// )
val map = mutableMapOf<String, Any>(
"foodName" to input.trim(),
"pageNo" to pageNo,
"pageNum" to pageNo,
"pageSize" to pageSize,
"canteenId" to BaseApp.canteenId
"placeId" to BaseApp.canteenId,
"dinnerType" to getDinnerTypeText()
)
if (dinnerType.toInt() > 0) {
param["dinnerType"] = getDinnerTypeText()
}
netViewModel.searchFoodListWithCallback(
param = param,
onResult = { state ->
when (state) {
is UiState.Success -> {
delayDismissLoading()
finishRefresh()
binding.refreshLayout.setEnableRefresh(true)
val records = state.data
if (records.isNullOrEmpty()) {
if (pageNo == 1) {
loadEmptyView()
}
return@searchFoodListWithCallback
}
if (pageNo == 1) {
list.clear()
}
list.addAll(records)
recordAdapter.notifyDataSetChanged()
val isLoadMoreEnable = records.size >= pageSize
binding.refreshLayout.setEnableLoadMore(isLoadMoreEnable)
if (isLoadMoreEnable) {
pageNo++
}
netViewModel.searchFoodList(
param = map,
onSuccess = {
delayDismissLoading()
finishRefresh()
binding.refreshLayout.setEnableRefresh(true)
if (it.isNullOrEmpty()) {
// toast("暂未搜索到相关菜品信息")
if (pageNo == 1) {
loadEmptyView()
}
is UiState.Error -> {
binding.refreshLayout.setEnableRefresh(true)
toast(state.msg)
delayDismissLoading()
finishRefresh()
if (pageNo == 1) {
loadEmptyView()
}
}
else -> {}
return@searchFoodList
}
}
)
if (pageNo == 1) {
list.clear()
}
list.addAll(it)
recordAdapter.notifyDataSetChanged()
val isLoadMoreEnable = it.size >= pageSize
binding.refreshLayout.setEnableLoadMore(isLoadMoreEnable)
if (isLoadMoreEnable) {
pageNo++
}
}, onFailure = { code, msg ->
binding.refreshLayout.run {
setEnableRefresh(true)
}
toast(msg)
delayDismissLoading()
finishRefresh()
if (pageNo == 1) {
loadEmptyView()
}
})
}
private fun finishRefresh() {
if (pageNo == 1) {
binding.refreshLayout.finishRefresh(1200)
@@ -200,12 +247,17 @@ class FoodSearchActivity : BaseActivity() {
}
private var emptyViewBinding: LayoutEmptyViewBinding? = null
@SuppressLint("NotifyDataSetChanged")
private fun loadEmptyView() {
list.clear()
recordAdapter.notifyDataSetChanged()
if (emptyViewBinding == null) {
emptyViewBinding = LayoutEmptyViewBinding.inflate(LayoutInflater.from(this), binding.rvDishSearchList, false)
emptyViewBinding = LayoutEmptyViewBinding.inflate(
LayoutInflater.from(this),
binding.rvDishSearchList,
false
)
}
emptyViewBinding!!.tvContent.text = "暂无数据"
emptyViewBinding!!.tvSubContent.text = "可以尝试换个名称重新搜索"
@@ -2,18 +2,20 @@ package com.shuwei.dish.match.ui
import android.annotation.SuppressLint
import android.os.Bundle
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import com.shuwei.dish.match.R
import com.shuwei.dish.match.adapter.HomeModeAdapter
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.databinding.ActivityHomeBinding
import com.shuwei.dish.match.model.HomeModeBean
import com.shuwei.dish.match.db.AppRepository
import com.shuwei.dish.match.entity.HomeModeBean
import com.shuwei.dish.match.utils.SpTool
import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.startActivity
import com.shuwei.dish.match.utils.ext.toast
import com.shuwei.dish.match.viewmodel.AppViewModel
import com.shuwei.dish.match.viewmodel.factory.AppFactory
class HomeActivity : BaseActivity() {
@@ -29,6 +31,7 @@ class HomeActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
statusBarDarkFont(enable = true)
initViewModel()
// // 获取 ANDROID_ID
// val deviceId =
// Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID)
@@ -38,7 +41,7 @@ class HomeActivity : BaseActivity() {
binding = ActivityHomeBinding.inflate(layoutInflater)
setContentView(binding.root)
setHeaderBackground(isHomePage = true)
binding.radioGroup.setOnCheckedChangeListener { _, checkedId ->
binding.radioGroup.setOnCheckedChangeListener { group, checkedId ->
if (checkedId == R.id.radioSw) {
BaseApp.canteenId = "1678234139391512577"
} else {
@@ -129,6 +132,14 @@ class HomeActivity : BaseActivity() {
)
}
private lateinit var appViewModel: AppViewModel
private fun initViewModel() {
val db = BaseApp.instance!!.database
val factory = AppFactory(AppRepository(db.appDao()))
appViewModel = ViewModelProvider(this, factory)[AppViewModel::class.java]
}
private fun goSampling() {
appViewModel.loadSeasoning {
if (it.isEmpty()) {
@@ -3,24 +3,29 @@ package com.shuwei.dish.match.ui
import android.annotation.SuppressLint
import android.os.Bundle
import android.provider.Settings
import android.util.Log
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import com.shuwei.dish.match.BuildConfig
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.databinding.ActivityInitBinding
import com.shuwei.dish.match.base.DeviceRole
import com.shuwei.dish.match.base.GlobalData
import com.shuwei.dish.match.databinding.ActivityInitBinding
import com.shuwei.dish.match.db.AppRepository
import com.shuwei.dish.match.http.UrlConfig
import com.shuwei.dish.match.objbox.FoodModule
import com.shuwei.dish.match.utils.AppUtil
import com.shuwei.dish.match.utils.QRCodeUtil
import com.shuwei.dish.match.utils.SpTool
import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.invisible
import com.shuwei.dish.match.utils.Weigher2
import com.shuwei.dish.match.utils.ext.dp
import com.shuwei.dish.match.utils.ext.startActivity
import com.shuwei.dish.match.utils.ext.toast
import com.shuwei.dish.match.utils.ext.visible
import com.shuwei.dish.match.utils.ext.toObject
import com.shuwei.dish.match.viewmodel.AppViewModel
import com.shuwei.dish.match.viewmodel.factory.AppFactory
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import kotlin.toString
class InitActivity : BaseActivity() {
companion object {
@@ -28,6 +33,8 @@ class InitActivity : BaseActivity() {
}
private lateinit var binding: ActivityInitBinding
private lateinit var appViewModel: AppViewModel
@SuppressLint("HardwareIds")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -35,20 +42,19 @@ class InitActivity : BaseActivity() {
// useBackground(false)
binding = ActivityInitBinding.inflate(layoutInflater)
setContentView(binding.root)
lifecycleScope.launch {
FoodModule.init(this@InitActivity)
}
// setHeaderBackground(isHomePage = true)
setHeaderBgVisible(false)
BaseApp.appVersion = AppUtil.getAppVersionCode(this).toString()
// 获取 ANDROID_ID
// var androidId =
// Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID)
// var androidId = Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID)
// androidId = "39a7abdd06b3c7ab"
val deviceId = if (BuildConfig.IS_TEST_DEVICE) GlobalData.DEVICE_ID_2 else AppUtil.getUDID(this)
GlobalData.deviceId = deviceId
SpTool.put(SpTool.DEVICE_ID, deviceId)
// GlobalData.appBaseUrl = GlobalData.TEST_BASE_URL
GlobalData.appBaseUrl = GlobalData.PROD_BASE_URL
// BaseApp.deviceId = androidId
// SpTool.put(SpTool.DEVICE_ID, androidId)
// BaseApp.configUrl = UrlConfig.BASE_URL
BaseApp.canteenId = "0"
// // TODO: 以上保存deviceId用于临时使用,后续改为下面注释方式
@@ -71,16 +77,42 @@ class InitActivity : BaseActivity() {
// return
// }
binding.ivQrCode.invisible()
binding.btnInit.invisible()
// binding.ivQrCode.invisible()
// binding.btnInit.invisible()
initViewModel()
// 若有保存的串口配置则先应用(子设备串口可能与主设备不同)
// val savedPort = SpTool.getString(SpTool.SCALE_DEVICE_PORT, "")
// if (savedPort.isNotEmpty()) {
// Weigher2.setDevicePort(savedPort)
// }
// TODO: 测试不同的串口配置---------------------2、5、3
Weigher2.setDevicePort(
if (GlobalData.deviceId == "8fc2ab34-2137-3112-acca-f884ea8736d4") {
"/dev/ttyS7"
} else {
"/dev/ttyS4"
}
)
WeightUtil.init()
WeightUtil.getWeight()
WeightUtil.startContinuousRead()
countDown()
}
private fun initViewModel() {
val db = BaseApp.instance!!.database
val factory = AppFactory(AppRepository(db.appDao()))
appViewModel = ViewModelProvider(this, factory)[AppViewModel::class.java]
}
private fun startNextPage() {
// 子设备:直接进入小屏专属页面,不走原有大屏业务流程
if (GlobalData.deviceRole == DeviceRole.SLAVE) {
startActivity<SlaveActivity>()
return
}
// 主设备:走原有路由逻辑
val launchPageType = SpTool.getInt(SpTool.LAUNCH_PAGE_TYPE, -1)
when (launchPageType) {
0 -> {
@@ -139,6 +171,72 @@ class InitActivity : BaseActivity() {
}
}
// private fun initConfig() {
// binding.ivQrCode.setImageBitmap(
// QRCodeUtil.generateQRCode(
// content = BaseApp.deviceId ?: "",
// size = 200.dp
// )
// )
// binding.btnInit.setOnClickListener {
//// HttpUtil.loopGetToken = false
//// getDeviceConfig()
// }
// }
// private fun getDeviceConfig() {
// val tokenUrl =
// "${UrlConfig.DEVICE_TOKEN}?qrcodeId=${BaseApp.deviceId}&appVersion=${BaseApp.appVersion}"
// HttpUtil.get(url = tokenUrl, doSuccess = { token ->
// Log.d(TAG, "initConfig: $token")
// getConfig(token.toString())
// }, doFailure = { code, msg ->
// Log.d(TAG, "initConfig: $code,$msg")
// })
// }
// private fun getConfig(token: String) {
// val deviceConfigUrl =
// "${UrlConfig.DEVICE_CONFIG}?equipmentCode=${BaseApp.deviceId}&&appVersion=${BaseApp.appVersion}"
// HttpUtil.get(
// url = deviceConfigUrl, header = mutableMapOf(
// "X-Access-Token" to token
// ), doSuccess = {
// Log.d(TAG, "getDeviceConfig: $it")
// val data = it.toJsonString()
// val checkResult = checkConfigData(data)
// if (checkResult.not()) {
// toast("初始化设备失败,请稍后重试")
// return@get
// }
// HttpUtil.loopGetToken = true
// SpTool.put(SpTool.DEVICE_CONFIG_CACHE, data)
// startActivity<HomeActivity>()
//// finish()
// }, doFailure = { code, msg ->
// Log.d(TAG, "getDeviceConfig: $code,$msg")
// toast("初始化设备失败,请稍后重试,code=${code},msg=${msg}")
// })
// }
data class DeviceConfigBean(
var appPackageUrl: String? = null,
var canteenId: String? = null
)
// private fun checkConfigData(data: String): Boolean {
// if (data.isBlank()) {
// return false
// }
// val config = data.toObject<DeviceConfigBean?>()
// if (config == null) {
// return false
// }
// BaseApp.configUrl = config.appPackageUrl ?: ""
// BaseApp.canteenId = config.canteenId ?: ""
// return true
// }
override fun onDestroy() {
WeightUtil.stopContinuousRead()
super.onDestroy()
@@ -0,0 +1,172 @@
package com.shuwei.dish.match.ui
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.databinding.ActivityMasterScaleBinding
import com.shuwei.dish.match.databinding.ListItemScaleDataBinding
import com.shuwei.dish.match.scale.ScaleData
import com.shuwei.dish.match.scale.ScaleServiceManager
import com.shuwei.dish.match.utils.NetworkUtil
import com.shuwei.dish.match.utils.WeightUtil
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
/**
* 主设备秤数据总览页面(仅 MASTER 角色显示)
* 聚合展示所有设备(本机 + 远端子设备)的实时秤数据
* 数据按设备分组展示:设备号 + IP + 该设备下所有秤数据
* 数据来源:ScaleServiceManager.allScalesStateFlow
*/
class MasterScaleActivity : BaseActivity() {
private lateinit var binding: ActivityMasterScaleBinding
/**
* 按设备分组后的数据模型
* @param deviceId 设备 ID
* @param ip 设备 IP 地址
* @param scales 该设备下所有秤数据,按 address 排序
*/
private data class DeviceGroup(
val deviceId: String,
val ip: String,
val scales: List<ScaleData>
)
private val groupList = mutableListOf<DeviceGroup>()
private val adapter = DeviceGroupAdapter(groupList)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMasterScaleBinding.inflate(layoutInflater)
setContentView(binding.root)
setHeaderBackground()
binding.rvScaleList.layoutManager = LinearLayoutManager(this)
binding.rvScaleList.itemAnimator = null // 关闭默认动画,避免数据频繁更新时出现闪烁错乱
binding.rvScaleList.adapter = adapter
observeScaleData()
}
/**
* 订阅 ScaleServiceManager 的全量秤数据 StateFlow
* 每次有任一秤数据更新时,重新按设备分组并刷新列表
*/
private fun observeScaleData() {
val flow = ScaleServiceManager.allScales ?: return
lifecycleScope.launch {
flow.collectLatest { scaleMap ->
// 按 deviceId 分组,本机设备排最前,其余按 deviceId 排序
val localId = com.shuwei.dish.match.base.GlobalData.deviceId
// 本机 IP 实时读取,避免 App 启动时网络未就绪导致缓存为空
val localIp = NetworkUtil.getLocalIpAddress(this@MasterScaleActivity)
val groups = scaleMap.values
.groupBy { it.deviceId }
.entries
.sortedWith(compareBy({ if (it.key == localId) 0 else 1 }, { it.key }))
.map { (deviceId, scales) ->
DeviceGroup(
deviceId = deviceId,
ip = if (deviceId == localId) localIp
else scales.firstOrNull { it.ip.isNotEmpty() }?.ip ?: "",
scales = scales.sortedBy { it.address }
)
}
updateList(groups)
updateStatusBar(scaleMap)
}
}
}
@SuppressLint("NotifyDataSetChanged")
private fun updateList(groups: List<DeviceGroup>) {
groupList.clear()
groupList.addAll(groups)
adapter.notifyDataSetChanged()
}
@SuppressLint("SetTextI18n")
private fun updateStatusBar(scaleMap: Map<String, ScaleData>) {
val deviceCount = scaleMap.values.map { it.deviceId }.distinct().size
val scaleCount = scaleMap.size
binding.tvConnectionStatus.text = "已发现设备:$deviceCount 台 / 共 $scaleCount 个秤"
}
/** 设备分组适配器,每项展示一台设备及其所有秤数据 */
private inner class DeviceGroupAdapter(private val data: List<DeviceGroup>) :
RecyclerView.Adapter<DeviceGroupAdapter.VH>() {
inner class VH(val binding: ListItemScaleDataBinding) :
RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = VH(
ListItemScaleDataBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
override fun getItemCount() = data.size
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: VH, position: Int) {
val group = data[position]
holder.binding.tvDeviceLabel.text = "设备:${group.deviceId}"
holder.binding.tvDeviceIp.text = "IP${group.ip.ifEmpty { "未知" }}"
// 清空旧秤行,重新填充
val container: LinearLayout = holder.binding.llScaleContainer
container.removeAllViews()
group.scales.forEach { scale ->
val stateStr = when (scale.state) {
WeightUtil.STATE_STABLE -> "稳定"
WeightUtil.STATE_UNSTABLE -> "不稳定"
WeightUtil.STATE_OVER_WEIGHT -> "超量"
else -> "${scale.state}"
}
// 每行:秤编号 重量 状态
val row = LinearLayout(holder.itemView.context).apply {
orientation = LinearLayout.HORIZONTAL
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).also { it.topMargin = 4.dpToPx() }
}
row.addView(makeTextView("${scale.address}", 22, weight = 1f))
row.addView(makeTextView("${scale.weight} g", 26, bold = true, weight = 2f))
row.addView(makeTextView(stateStr, 18, weight = 1f))
container.addView(row)
}
}
/** 创建秤行内的 TextView */
private fun makeTextView(
text: String,
spSize: Int,
bold: Boolean = false,
weight: Float = 1f
): TextView = TextView(this@MasterScaleActivity).apply {
this.text = text
textSize = spSize.toFloat()
if (bold) setTypeface(null, android.graphics.Typeface.BOLD)
setTextColor(
if (bold) getColor(com.shuwei.dish.match.R.color.home_title)
else getColor(com.shuwei.dish.match.R.color.home_sub_title)
)
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, weight)
}
}
/** dp 转 px 工具 */
private fun Int.dpToPx(): Int =
(this * resources.displayMetrics.density + 0.5f).toInt()
}
@@ -1,10 +1,10 @@
package com.shuwei.dish.match.ui
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Typeface
import android.os.Bundle
import android.util.Log
import androidx.activity.viewModels
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import com.shuwei.dish.match.R
@@ -12,17 +12,12 @@ import com.shuwei.dish.match.adapter.DishPartAdapter
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.databinding.ActivityPrepareCookBinding
import com.shuwei.dish.match.databinding.LayoutFoodRemindBinding
import com.shuwei.dish.match.dialog.BottomDialog
import com.shuwei.dish.match.dialog.FoodSearchDialog
import com.shuwei.dish.match.dialog.CommonDialog
import com.shuwei.dish.match.model.GoodsItem
import com.shuwei.dish.match.db.entity.CookFoodGoodsEntity
import com.shuwei.dish.match.model.CookFoodDTO
import com.shuwei.dish.match.model.FoodRecord
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.shuwei.dish.match.net.UiState
import kotlinx.coroutines.launch
import com.shuwei.dish.match.entity.CookFoodEntity
import com.shuwei.dish.match.entity.CookFoodGoodsEntity
import com.shuwei.dish.match.entity.FoodRecord
import com.shuwei.dish.match.net.NetViewModel
import com.shuwei.dish.match.utils.AddressUtil
import com.shuwei.dish.match.utils.SwipeCallback
import com.shuwei.dish.match.utils.WeightUtil
@@ -33,6 +28,7 @@ import com.shuwei.dish.match.utils.ext.toJsonString
import com.shuwei.dish.match.utils.ext.toast
import com.shuwei.dish.match.utils.ext.visible
import java.io.Serializable
import kotlin.getValue
@SuppressLint("NotifyDataSetChanged")
class PrepareCookActivity : BaseActivity() {
@@ -47,6 +43,8 @@ class PrepareCookActivity : BaseActivity() {
private var food: FoodRecord? = null
private var goodsList: MutableList<CookFoodGoodsEntity>? = null
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityPrepareCookBinding.inflate(layoutInflater)
@@ -70,7 +68,9 @@ class PrepareCookActivity : BaseActivity() {
food = intent.extras?.getSerializable(FOOD_ITEM) as FoodRecord?
binding.tvDishName.text = food?.foodName ?: ""
addViewClickListener()
initObserver()
initRecyclerView()
getDishDetail()
}
@@ -86,50 +86,18 @@ class PrepareCookActivity : BaseActivity() {
binding.ivWeightClear.setOnClickListener {
WeightUtil.tareTwo(AddressUtil.ONE)
}
binding.ivWeightAdd.setOnClickListener {
if (clickIndex == -1) {
toast("请选择菜品构成")
return@setOnClickListener
}
val clickItem = list[clickIndex]
clickItem.run {
if (isNewDishType && materialType != 1 && materialType != 2) {
toast("请选择主辅材类型")
return@setOnClickListener
}
}
val weight = binding.tvDishPartWeight.text.toString().toDoubleOrNull() ?:0.toDouble()
if (weight <= 0.toDouble()) {
toast("食材用量需要大于0")
return@setOnClickListener
}
Log.d(TAG, "addViewClickListener->useRealWeight=$weight")
clickItem.run {
useWeight = weight.toDouble()
isSetFinished = true
dishPartAdapter.notifyItemChanged(clickIndex)
}
}
binding.tvAddFood.clickWithDebounce {
BottomDialog(goodsType = 0).show(this) { item ->
val filterResult = list.firstOrNull { it.goodsId == item.goodsId }
if (filterResult != null) {
toast("不允许重复添加同一食材")
return@show
}
list.add(GoodsItem(goodsId = item.goodsId, goodsName = item.goodsName, isNewDishType = true))
onItemClick(list.size - 1)
}
}
binding.ivWeightAdd.setOnClickListener { addWeight() }
binding.tvAddFood.clickWithDebounce { addFood() }
binding.btnCook.clickWithDebounce {
val count = list.count { it.isSetFinished.not() }
if (count > 0) {
showRemindDialog()
return@clickWithDebounce
}
goToSubmit()
openSubmitPage()
}
binding.rgCook.setOnCheckedChangeListener { group, checkedId ->
if (clickIndex < 0 || clickIndex >= list.size) return@setOnCheckedChangeListener
val tempType =
if (checkedId == R.id.rbDishTypeFirst) 1 else if (checkedId == R.id.rbDishTypeSecond) 2 else -1
list[clickIndex].materialType = tempType
@@ -140,14 +108,58 @@ class PrepareCookActivity : BaseActivity() {
}
}
private fun goToSubmit() {
private fun addWeight() {
if (clickIndex == -1) {
toast("请选择菜品构成")
return
}
val clickItem = list[clickIndex]
clickItem.run {
if (isNewDishType && materialType != 1 && materialType != 2) {
toast("请选择主辅材类型")
return
}
}
val weight = binding.tvDishPartWeight.text.toString().toDouble()
if (weight <= 0.toDouble()) {
toast("食材用量需要大于0")
return
}
Log.d(TAG, "addViewClickListener->useRealWeight=$weight")
clickItem.run {
useWeight = weight
isOriginalData = false
isSetFinished = true
dishPartAdapter.notifyItemChanged(clickIndex)
}
}
private fun addFood() {
FoodSearchDialog().show(this) { item ->
val filterResult = list.firstOrNull { it.goodsId == item.goodsId }
if (filterResult != null) {
toast("不允许重复添加同一食材")
return@show
}
list.add(CookFoodGoodsEntity().apply {
goodsId = item.goodsId
goodsName = item.goodsName
// materialType =
isNewDishType = true
isOriginalData = false
})
onItemClick(list.size - 1)
binding.rvDishPartList.smoothScrollToPosition(list.size - 1)
}
}
private fun openSubmitPage() {
getGoodsList()
if (goodsList.isNullOrEmpty()) {
toast("菜品构成信息未设置")
toast("菜品构成信息")
return
}
Log.d(TAG, "goToSubmit: goodsList:${goodsList?.toJsonString()}")
// startActivity<SubmitDishActivity> {
startActivity<SubmitFoodActivity> {
putExtra(SubmitFoodActivity.GOODS_LIST, goodsList as Serializable)
putExtra(SubmitFoodActivity.FOOD_ITEM, food as Serializable)
@@ -158,12 +170,17 @@ class PrepareCookActivity : BaseActivity() {
if (goodsList == null) {
goodsList = mutableListOf()
}
list.forEach {
if (it.isSetFinished) {
goodsList?.add(it.toCookFoodGoodsEntity().also { entity ->
entity.foodId = food!!.foodId
})
}
list.filter { it.isSetFinished }.forEach {
goodsList?.add(CookFoodGoodsEntity().apply {
foodId = food!!.foodId
goodsId = it.goodsId
relateionType = it.relateionType
allEdible = it.allEdible
goodsName = it.goodsName
materialType = it.materialType
useWeight = it.useWeight ?: 0.toDouble()
goodsOrRelationCode = it.goodsOrRelationCode
})
}
}
@@ -175,7 +192,7 @@ class PrepareCookActivity : BaseActivity() {
rightText = "确认无误"
onLeftClick = { dismiss() }
onRightClick = {
goToSubmit()
openSubmitPage()
dismiss()
}
onDismiss = { hideStatusBar() }
@@ -183,53 +200,57 @@ class PrepareCookActivity : BaseActivity() {
}
private fun initObserver() {
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
netViewModel.foodDetailState.collect { state ->
when (state) {
is UiState.Loading -> showLoading()
is UiState.Success -> {
dismissLoading()
netViewModel.resetFoodDetailState()
val detail = state.data
if (detail == null) {
toast("查询菜品信息为空")
return@collect
}
loadDishDetail(detail)
}
is UiState.Error -> {
dismissLoading()
toast(state.msg)
}
else -> {}
}
}
}
}
}
private fun getDishDetail() {
netViewModel.getFoodDetail(food?.foodId ?: "")
// //649
// val url = "${UrlConfig.DISH_DETAIL}?foodId=${food?.foodId}"
// HttpUtil.get(
// url = url,
// doSuccess = {
// val json = it.toJsonString()
// Log.d(TAG, "getDishDetail: json:$json")
// val detail: CookFoodEntity? = json.toObject<CookFoodEntity>()
// if (detail == null) {
// toast("查询菜品信息为空")
// return@get
// }
// loadDishDetail(detail)
// }) { code, msg ->
// toast(msg)
// }
netViewModel.getFoodDetail(
food?.foodId ?: "",
onSuccess = { detail ->
if (detail == null) {
toast("查询菜品信息为空")
return@getFoodDetail
}
loadDishDetail(detail)
}, onFailure = { code, msg ->
toast(msg)
})
}
private fun loadDishDetail(detail: CookFoodDTO) {
private fun loadDishDetail(detail: CookFoodEntity) {
// val voList = detail.stFoodInfoConstituteList
val voList = detail.foodConstituteList
if (voList.isNullOrEmpty()) {
return
}
//筛选出主材和辅材
val tempDate = voList.filter { it.materialType == 1 || it.materialType == 2 }
.map { it.toGoodsItem().also { item -> item.useWeight = 0.0 } }
val tempData = voList.filter { it.materialType == 1 || it.materialType == 2 }
.apply {
forEach { it.useWeight = 0.toDouble() }
}
list.clear()
list.addAll(tempDate)
firstReqSize = list.size
initRecyclerView()
list.addAll(tempData)
dishPartAdapter.notifyDataSetChanged()
}
private var firstReqSize = 0
private val list = mutableListOf<GoodsItem>()
private val list = mutableListOf<CookFoodGoodsEntity>()
private val dishPartAdapter by lazy {
DishPartAdapter(list).apply {
setOnItemClickListener { _, _, positon ->
@@ -245,10 +266,28 @@ class PrepareCookActivity : BaseActivity() {
text = it.goodsName
setTypeface(typeface, Typeface.BOLD)
}
binding.tvDishType.text =
if (it.materialType == 1) "主材" else if (it.materialType == 2) "辅材" else ""
list.forEach { bean -> bean.isClicked = false }
it.isClicked = true
var materialType: String
when (it.materialType) {
1 -> {
materialType = "主材"
binding.rgCook.check(R.id.rbDishTypeFirst)
}
2 -> {
materialType = "辅材"
binding.rgCook.check(R.id.rbDishTypeSecond)
}
else -> {
materialType = ""
binding.rgCook.clearCheck()
}
}
binding.tvDishType.text = materialType
list.forEach { bean -> bean.isItemClicked = false }
it.isItemClicked = true
dishPartAdapter.notifyDataSetChanged()
if (it.isNewDishType) {
@@ -279,6 +318,9 @@ class PrepareCookActivity : BaseActivity() {
dishPartAdapter.notifyItemRemoved(position)
dishPartAdapter.notifyItemRangeChanged(position, list.size - position)
toast("已删除")
if (list.isEmpty()) {
binding.tvDishShowName.text = ""
}
})
itemTouchHelper.attachToRecyclerView(this)
}
@@ -288,17 +330,23 @@ class PrepareCookActivity : BaseActivity() {
@SuppressLint("GestureBackNavigation")
@Deprecated("Deprecated in Java")
override fun onBackPressed() {
if (list.size > firstReqSize) {
//说明有新增的食材
saveDataRemindDialog()
return
}
val filterResult = list.firstOrNull { (it.useWeight ?: 0.toDouble()) > 0.toDouble() }
if (filterResult != null) {
//说明有称重的数据
val count = list.count { !it.isOriginalData }
if (count > 0) {
//说明有新增或者设置重量
saveDataRemindDialog()
return
}
// if (list.size > firstReqSize) {
// //说明有新增的食材
// saveDataRemindDialog()
// return
// }
// val filterResult = list.firstOrNull { (it.useWeight ?: 0.toDouble()) > 0.toDouble() }
// if (filterResult != null) {
// //说明有称重的数据
// saveDataRemindDialog()
// return
// }
super.onBackPressed()
}
@@ -3,9 +3,8 @@ package com.shuwei.dish.match.ui
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.MotionEvent
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import com.shuwei.dish.match.R
@@ -14,17 +13,18 @@ import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.databinding.ActivitySamplingBinding
import com.shuwei.dish.match.databinding.LayoutEmptyViewBinding
import com.shuwei.dish.match.db.AppRepository
import com.shuwei.dish.match.dialog.DialogTool
import com.shuwei.dish.match.db.entity.CookFoodEntity
import com.shuwei.dish.match.model.FoodRecord
import com.shuwei.dish.match.net.UiState
import com.shuwei.dish.match.entity.CookFoodEntity
import com.shuwei.dish.match.entity.FoodRecord
import com.shuwei.dish.match.utils.DateTimeUtil
import com.shuwei.dish.match.utils.SwipeCallback
import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.startActivity
import com.shuwei.dish.match.utils.ext.toast
import com.shuwei.dish.match.utils.ext.visible
import com.shuwei.dish.match.viewmodel.AppViewModel
import com.shuwei.dish.match.viewmodel.factory.AppFactory
import java.io.Serializable
class SamplingListActivity : BaseActivity() {
@@ -34,6 +34,7 @@ class SamplingListActivity : BaseActivity() {
companion object {
private const val TAG = "SamplingListActivity"
const val IS_COOKING = "isCooking"
const val IS_CONFIG_PAGE = "isConfigPage"
}
override fun onCreate(savedInstanceState: Bundle?) {
@@ -42,6 +43,7 @@ class SamplingListActivity : BaseActivity() {
setContentView(binding.root)
setTitleBar()
setHeaderBackground()
initViewModel()
initRecyclerView()
addViewListener()
requestData(true)
@@ -57,9 +59,7 @@ class SamplingListActivity : BaseActivity() {
it.visible()
it.setImageResource(R.drawable.ic_setting)
it.setOnClickListener {
startActivity<DeviceConfigActivity> {
putExtra(DeviceConfigActivity.COOK_MODE, 2)
}
startActivity<SettingActivity>()
}
}, backAction = {
it.gone()
@@ -68,6 +68,14 @@ class SamplingListActivity : BaseActivity() {
}
private lateinit var appViewModel: AppViewModel
private fun initViewModel() {
val db = BaseApp.instance!!.database
val factory = AppFactory(AppRepository(db.appDao()))
appViewModel = ViewModelProvider(this, factory)[AppViewModel::class.java]
}
fun getCookFoodList(action: (MutableList<CookFoodEntity>?) -> Unit) {
appViewModel.getCookFoodList(cookMode = 1) { action(it) }
}
@@ -76,8 +84,12 @@ class SamplingListActivity : BaseActivity() {
appViewModel.deleteCookFoodAndGoods(cookMode = 1, foodId = foodId, onFinish = action)
}
override fun onNewIntent(intent: Intent?) {
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
val isConfigPage = intent?.getBooleanExtra(IS_CONFIG_PAGE, false) ?: false
if (isConfigPage) {
return
}
val isCooking = intent?.getBooleanExtra(IS_COOKING, false) ?: false
//toast("isCooking=$isCooking")
refreshPage(isCooking)
@@ -103,14 +115,13 @@ class SamplingListActivity : BaseActivity() {
}
}
private fun onItemClick(position:Int) {
private fun onItemClick(position: Int) {
val item = list[position]
if (item.isCooking) {
//烹饪中,跳到称熟重页面
startActivity<SubmitFoodActivity> {
putExtra(
SubmitFoodActivity.FOOD_ITEM,
item as Serializable
SubmitFoodActivity.FOOD_ITEM, item as Serializable
)
}
return
@@ -148,7 +159,7 @@ class SamplingListActivity : BaseActivity() {
@SuppressLint("ClickableViewAccessibility")
private fun addViewListener() {
binding.btnAddSampling.setOnClickListener {
judgeDeviceConfig{
judgeDeviceConfig {
startActivity<DishSamplingActivity>()
}
}
@@ -186,26 +197,50 @@ class SamplingListActivity : BaseActivity() {
@SuppressLint("NotifyDataSetChanged")
fun getSamplingList() {
showLoading()
val param = mutableMapOf<String, Any>(
"pageNo" to pageNo,
"pageSize" to pageSize,
"canteenId" to BaseApp.canteenId
)
netViewModel.getSamplingListWithCallback(
param = param,
onResult = { state ->
when (state) {
is UiState.Success -> loadDishList(state.data)
is UiState.Error -> {
binding.refreshLayout.setEnableRefresh(true)
toast(state.msg)
finishRefresh()
delayDismissLoading()
if (pageNo == 1) {
loadEmptyView()
}
}
else -> {}
// val map = mapOf(
//// "foodName" to input,
// "pageNo" to "$pageNo",
// "pageSize" to "$pageSize",
// "canteenId" to BaseApp.canteenId
// )
// val sb = StringBuilder(UrlConfig.SAMPLING_LIST).apply {
// append("?")
// map.forEach { (key, value) -> append("$key=$value&") }
// }
// sb.deleteCharAt(sb.length - 1)
// HttpUtil.get(
// url = sb.toString(),
// doSuccess = {
// loadDishList(it)
// }, doFailure = { code, msg ->
// binding.refreshLayout.run {
// setEnableRefresh(true)
// }
// toast(msg)
// finishRefresh()
// binding.refreshLayout.setEnableRefresh(true)
// delayDismissLoading()
// if (pageNo == 1) {
// loadEmptyView()
// }
// })
netViewModel.getSamplingList(
param = mutableMapOf(
"pageNum" to pageNo,
"pageSize" to pageSize,
"placeId" to BaseApp.canteenId
),
onSuccess = {
loadDishList(it)
},
onFailure = { code, msg ->
binding.refreshLayout.setEnableRefresh(true)
toast(msg)
finishRefresh()
delayDismissLoading()
if (pageNo == 1) {
loadEmptyView()
}
}
)
@@ -227,9 +262,7 @@ class SamplingListActivity : BaseActivity() {
dishAdapter.notifyDataSetChanged()
if (emptyViewBinding == null) {
emptyViewBinding = LayoutEmptyViewBinding.inflate(
LayoutInflater.from(this),
binding.rvSamplingList,
false
LayoutInflater.from(this), binding.rvSamplingList, false
)
}
emptyViewBinding!!.tvContent.text = "暂无数据"
@@ -280,8 +313,7 @@ class SamplingListActivity : BaseActivity() {
}
val tempList = mutableListOf<FoodRecord>()
cookFoodEntities
.sortedByDescending { DateTimeUtil.convert(dateStr = it.createTime) }
cookFoodEntities.sortedByDescending { DateTimeUtil.convert(dateStr = it.createTime) }
.forEach { entity ->
tempList.add(FoodRecord().apply {
foodId = entity.foodId
@@ -315,6 +347,7 @@ class SamplingListActivity : BaseActivity() {
delayDismissLoading()
finishRefresh()
if (records.isNullOrEmpty()) {
//toast("暂未搜索到相关菜品信息")
if (pageNo == 1) {
loadEmptyView()
}
@@ -335,7 +368,7 @@ class SamplingListActivity : BaseActivity() {
private var configFinished = false
fun judgeDeviceConfig(block:()-> Unit) {
fun judgeDeviceConfig(block: () -> Unit) {
if (configFinished) {
block()
return
@@ -359,11 +392,8 @@ class SamplingListActivity : BaseActivity() {
rightBtnText = "去设置",
leftBtnClick = {},
rightBtnClick = {
startActivity<DeviceConfigActivity> {
putExtra(DeviceConfigActivity.COOK_MODE, 2)
}
}
)
startActivity<SettingActivity>()
})
}
private var isVisible = false
@@ -1,16 +1,20 @@
package com.shuwei.dish.match.ui
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.activity.viewModels
import androidx.lifecycle.ViewModelProvider
import com.shuwei.dish.match.R
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.databinding.ActivitySelectDishBinding
import com.shuwei.dish.match.db.AppRepository
import com.shuwei.dish.match.dialog.DialogTool
import com.shuwei.dish.match.db.entity.CookFoodEntity
import com.shuwei.dish.match.entity.CookFoodEntity
import com.shuwei.dish.match.entity.FoodRecord
import com.shuwei.dish.match.entity.FoodRecordBean
import com.shuwei.dish.match.net.NetViewModel
import com.shuwei.dish.match.ui.fragment.DishListFragment
import com.shuwei.dish.match.utils.KeyboardUtil
import com.shuwei.dish.match.utils.ext.addOnActionSearchListener
@@ -18,16 +22,22 @@ import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.startActivity
import com.shuwei.dish.match.utils.ext.toast
import com.shuwei.dish.match.utils.ext.visible
import com.shuwei.dish.match.viewmodel.AppViewModel
import com.shuwei.dish.match.viewmodel.factory.AppFactory
import kotlin.getValue
class SelectDishActivity : BaseActivity() {
companion object {
const val IS_COOKING = "isCooking"
const val DINNER_TYPE = "dinnerType"
const val IS_CONFIG_PAGE = "isConfigPage"
}
private lateinit var binding: ActivitySelectDishBinding
private lateinit var appViewModel: AppViewModel
private var dinnerType: String = "1"
private val fragmentList = mutableListOf<DishListFragment>().apply {
@@ -49,17 +59,22 @@ class SelectDishActivity : BaseActivity() {
it.visible()
it.setImageResource(R.drawable.ic_setting)
it.setOnClickListener {
startActivity<DeviceConfigActivity> {
putExtra(DeviceConfigActivity.COOK_MODE, 1)
}
startActivity<SettingActivity>()
}
}, backAction = {
it.gone()
})
initViewModel()
addViewListener()
loadFragment()
}
private fun initViewModel() {
val db = BaseApp.instance!!.database
val factory = AppFactory(AppRepository(db.appDao()))
appViewModel = ViewModelProvider(this, factory)[AppViewModel::class.java]
}
fun getCookFoodList(action: (MutableList<CookFoodEntity>?) -> Unit) {
appViewModel.getCookFoodList(cookMode = 0, dinnerType = dinnerType) { action(it) }
}
@@ -90,9 +105,7 @@ class SelectDishActivity : BaseActivity() {
rightBtnText = "去设置",
leftBtnClick = {},
rightBtnClick = {
startActivity<DeviceConfigActivity> {
putExtra(DeviceConfigActivity.COOK_MODE, 1)
}
startActivity<SettingActivity>()
}
)
}
@@ -150,9 +163,13 @@ class SelectDishActivity : BaseActivity() {
appViewModel.deleteCookFoodAndGoods(cookMode = 0, foodId = foodId, onFinish = action)
}
override fun onNewIntent(intent: Intent?) {
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
runCatching {
val isConfigPage = intent?.getBooleanExtra(IS_CONFIG_PAGE, false) ?: false
if (isConfigPage) {
return
}
val isCooking = intent?.getBooleanExtra(IS_COOKING, false) ?: false
val lastDinnerType = intent?.getStringExtra(DINNER_TYPE) ?: "0"
if (lastDinnerType == "1" || lastDinnerType == "2" || lastDinnerType == "3") {
@@ -187,4 +204,16 @@ class SelectDishActivity : BaseActivity() {
isVisible = false
}
public fun getFoodList(
param: MutableMap<String, Any>,
onSuccess: (MutableList<FoodRecord>?) -> Unit,
onFailure: (String, String) -> Unit
) {
netViewModel.searchFoodList(
param = param,
onSuccess = onSuccess,
onFailure = onFailure
)
}
}
@@ -0,0 +1,264 @@
package com.shuwei.dish.match.ui
import android.Manifest
import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.graphics.toColorInt
import androidx.fragment.app.Fragment
import androidx.camera.view.PreviewView
import androidx.core.view.updateLayoutParams
import com.shuwei.dish.match.R
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.base.DeviceRole
import com.shuwei.dish.match.base.GlobalData
import com.shuwei.dish.match.databinding.ActivitySettingBinding
import com.shuwei.dish.match.databinding.LayoutCameraPreviewBinding
import com.shuwei.dish.match.ui.fragment.CollectFragment
import com.shuwei.dish.match.ui.fragment.DeviceConfigFragment
import com.shuwei.dish.match.utils.CameraUtils
import com.shuwei.dish.match.utils.SpTool
import com.shuwei.dish.match.utils.ext.dp
import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.startActivity
import com.shuwei.dish.match.utils.ext.toast
import com.shuwei.dish.match.utils.ext.visible
/**
* 设置Activity
* 功能:管理多个设置相关的Fragment,支持Fragment切换
*/
class SettingActivity : BaseActivity() {
companion object {
const val TAG = "SettingActivity"
}
private lateinit var binding: ActivitySettingBinding
private val cameraUtils: CameraUtils by lazy {
CameraUtils(this)
}
private lateinit var previewView: PreviewView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySettingBinding.inflate(layoutInflater)
setContentView(binding.root)
setHeaderBackground()
setTitleBar(titleBarAction = {
it.visible()
}, titleAction = {
it.text = "设置"
}, rightIconActon = {
it.gone()
})
// 默认显示设备配置Fragment
if (savedInstanceState == null) {
showDeviceConfigFragment()
}
initUI()
}
/**
* 初始化UI和事件监听
*/
private fun initUI() {
// 设备配置按钮点击事件
binding.btnDeviceConfig.setOnClickListener {
showDeviceConfigFragment()
updateButtonColors(isDeviceConfigSelected = true)
}
// 菜品采集按钮点击事件
binding.btnFoodCollect.setOnClickListener {
checkCameraPermissionAndShowFragment()
updateButtonColors(isDeviceConfigSelected = false)
}
// 设备角色只读展示,角色由设备 ID 决定,不可手动切换
updateRoleButtonText()
binding.btnDeviceRole.isEnabled = false
// 主设备显示「秤数据监控」入口
if (GlobalData.deviceRole == DeviceRole.MASTER) {
binding.btnScaleMonitor.visible()
binding.btnScaleMonitor.setOnClickListener {
startActivity<MasterScaleActivity>()
}
}
// 初始化按钮颜色(默认设备配置为选中状态)
updateButtonColors(isDeviceConfigSelected = true)
}
/** 刷新角色按钮文字 */
private fun updateRoleButtonText() {
val roleLabel = if (GlobalData.deviceRole == DeviceRole.MASTER) "主设备" else "子设备"
binding.btnDeviceRole.text = "设备角色:$roleLabel"
}
/**
* 更新底部菜单按钮的颜色状态
* @param isDeviceConfigSelected 设备配置按钮是否被选中
*/
private fun updateButtonColors(isDeviceConfigSelected: Boolean) {
val selectedColor = "#FFFFFF".toColorInt() // 选中颜色(白色)
val unselectedColor = "#5E7585".toColorInt() // 未选中颜色(灰色)
if (isDeviceConfigSelected) {
// 设备配置按钮:选中状态
binding.btnDeviceConfig.apply {
setTextColor(selectedColor)
textSize = 28f
setTypeface(null, android.graphics.Typeface.BOLD)
background = ContextCompat.getDrawable(this@SettingActivity, R.drawable.bg_btn_underline)
}
// 采集食材按钮:未选中状态
binding.btnFoodCollect.apply {
setTextColor(unselectedColor)
textSize = 26f
setTypeface(null, android.graphics.Typeface.NORMAL)
background = null
}
} else {
// 设备配置按钮:未选中状态
binding.btnDeviceConfig.apply {
setTextColor(unselectedColor)
textSize = 26f
setTypeface(null, android.graphics.Typeface.NORMAL)
background = null
}
// 采集食材按钮:选中状态
binding.btnFoodCollect.apply {
setTextColor(selectedColor)
textSize = 28f
setTypeface(null, android.graphics.Typeface.BOLD)
background = ContextCompat.getDrawable(this@SettingActivity, R.drawable.bg_btn_underline)
}
}
}
/**
* 检查相机权限并显示 CollectFragment
*/
@SuppressLint("ObsoleteSdkInt")
private fun checkCameraPermissionAndShowFragment() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
// Android 6.0 以下,直接显示 Fragment
showFragment(fragmentList[1])
return
}
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED
) {
// 权限已授予,直接显示 Fragment
showFragment(fragmentList[1])
} else {
// 权限未授予,申请权限
requestPermission(Manifest.permission.CAMERA) { isGranted ->
if (isGranted) {
// 权限已授予,显示 CollectFragment
showFragment(fragmentList[1])
} else {
// 权限被拒绝
toast("暂无相机权限,无法使用菜品采集功能")
}
}
}
}
/**
* 初始化相机
* @param container 相机预览容器
*/
fun initCamera(container: ViewGroup) {
cameraUtils.initCamera()
val previewBinding =
LayoutCameraPreviewBinding.inflate(layoutInflater, container)
previewView = previewBinding.previewView.also {
it.updateLayoutParams {
width = 456.dp
height = 342.dp
}
}
cameraUtils.setPreviewController(previewView)
}
/**
* Activity 恢复时绑定相机
*/
override fun onResume() {
super.onResume()
cameraUtils.bind()
}
/**
* Activity 暂停时解绑相机
*/
override fun onPause() {
super.onPause()
cameraUtils.unbind()
}
/**
* 提供给 Fragment 调用的拍照方法
*/
fun takePhoto(succCallback: (Uri) -> Unit, failCallback: (msg: String) -> Unit = {}) {
cameraUtils.takePhoto(succCallback = succCallback, failCallback = failCallback)
}
/**
* 显示设备配置Fragment
*/
private fun showDeviceConfigFragment() {
// val fragment = DeviceConfigFragment.newInstance()
// supportFragmentManager.beginTransaction()
// .replace(binding.fragmentContainer.id, fragment)
// .addToBackStack(null)
// .commit()
showFragment(fragmentList[0])
}
private val fragmentList = mutableListOf<Fragment>().apply {
add(DeviceConfigFragment.newInstance())
add(CollectFragment())
}
/**
* 显示指定的 Fragment
* 使用 add/show/hide 方式管理 Fragment,避免重复创建
* 已添加的 Fragment 直接显示,未添加的 Fragment 先添加再显示
*/
private fun showFragment(fragment: Fragment) {
runCatching {
supportFragmentManager.beginTransaction().apply {
// 如果 Fragment 还未添加到容器中,则添加
if (!fragment.isAdded) {
add(binding.fragmentContainer.id, fragment)
} else {
// 已添加过,直接显示
show(fragment)
}
// 隐藏其他 Fragment
fragmentList.forEach { otherFragment ->
if (otherFragment != fragment && otherFragment.isAdded) {
hide(otherFragment)
}
}
commit()
}
}.onFailure {
it.printStackTrace()
}
}
}
@@ -0,0 +1,137 @@
package com.shuwei.dish.match.ui
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.base.GlobalData
import com.shuwei.dish.match.databinding.ActivitySlaveBinding
import com.shuwei.dish.match.databinding.ListItemScaleRowBinding
import com.shuwei.dish.match.scale.ScaleServiceManager
import com.shuwei.dish.match.utils.NetworkUtil
import com.shuwei.dish.match.utils.WeightUtil
/**
* 子设备专属页面(小屏幕设备使用)
* 仅展示本机秤的实时数据和主设备连接状态
* 布局简洁,适配小屏幕
*/
class SlaveActivity : BaseActivity() {
private lateinit var binding: ActivitySlaveBinding
/** 本机秤数据列表:address → (weight, state) */
private data class ScaleItem(val address: Int, val weight: Double, val state: Int)
private val scaleList = mutableListOf<ScaleItem>()
private val adapter = ScaleAdapter(scaleList)
/** 已连接的主设备数量(有连接即代表主设备在线) */
private var masterConnected = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySlaveBinding.inflate(layoutInflater)
setContentView(binding.root)
setHeaderBackground()
binding.tvDeviceId.text = "设备:${GlobalData.deviceId}"
binding.tvDeviceIp.text = "IP${getLocalIpAddress()}"
binding.rvScaleList.layoutManager = LinearLayoutManager(this)
binding.rvScaleList.itemAnimator = null // 关闭默认动画,避免数据频繁更新时出现闪烁错乱
binding.rvScaleList.adapter = adapter
updateMasterStatus(false)
listenLocalScales()
listenMasterConnection()
}
/**
* 订阅主设备连接状态,回调在非主线程,切换到主线程更新 UI
*/
private fun listenMasterConnection() {
ScaleServiceManager.onMasterConnectionChanged = { connected ->
runOnUiThread { updateMasterStatus(connected) }
}
}
/**
* 监听本机 WeightUtil 回调,实时刷新秤列表
*/
private fun listenLocalScales() {
WeightUtil.addWeightListener(TAG) { address, state, weight ->
runOnUiThread {
val idx = scaleList.indexOfFirst { it.address == address }
val item = ScaleItem(address, weight, state)
if (idx >= 0) {
scaleList[idx] = item
adapter.notifyItemChanged(idx)
} else {
scaleList.add(item)
scaleList.sortBy { it.address }
adapter.notifyDataSetChanged()
}
}
}
}
/**
* 更新主设备连接状态文字
* 由 ScaleWebSocketServer 的 onOpen/onClose 回调驱动(可在后续扩展中接入)
*/
@SuppressLint("SetTextI18n")
fun updateMasterStatus(connected: Boolean) {
masterConnected = connected
binding.tvMasterStatus.text = if (connected) "主设备:已连接 ✓" else "主设备:未连接"
binding.tvMasterStatus.setTextColor(
getColor(if (connected) android.R.color.holo_green_dark else android.R.color.holo_red_light)
)
}
override fun onDestroy() {
ScaleServiceManager.onMasterConnectionChanged = null
WeightUtil.removeWeightListener(TAG)
super.onDestroy()
}
companion object {
const val TAG = "SlaveActivity"
}
/**
* 获取本机局域网 IP,委托给 NetworkUtil 统一处理版本兼容
* 获取失败时返回"未知"
*/
private fun getLocalIpAddress(): String {
return NetworkUtil.getLocalIpAddress(this).ifEmpty { "未知" }
}
private class ScaleAdapter(private val data: List<ScaleItem>) :
RecyclerView.Adapter<ScaleAdapter.VH>() {
inner class VH(val binding: ListItemScaleRowBinding) :
RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = VH(
ListItemScaleRowBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
override fun getItemCount() = data.size
override fun onBindViewHolder(holder: VH, position: Int) {
val item = data[position]
val stateStr = when (item.state) {
WeightUtil.STATE_STABLE -> "稳定"
WeightUtil.STATE_UNSTABLE -> "不稳定"
WeightUtil.STATE_OVER_WEIGHT -> "超量"
else -> "${item.state}"
}
holder.binding.tvScaleLabel.text = "${item.address}"
holder.binding.tvWeight.text = "${item.weight} g"
holder.binding.tvState.text = stateStr
}
}
}
@@ -6,26 +6,23 @@ import android.util.Log
import android.util.SparseArray
import android.util.SparseIntArray
import android.widget.FrameLayout
import androidx.activity.viewModels
import androidx.core.util.forEach
import androidx.lifecycle.ViewModelProvider
import com.google.gson.reflect.TypeToken
import com.shuwei.dish.match.R
import com.shuwei.dish.match.adapter.TextCellAdapter
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.databinding.ActivitySubmitFoodBinding
import com.shuwei.dish.match.db.AppRepository
import com.shuwei.dish.match.db.entity.CookFoodEntity
import com.shuwei.dish.match.db.entity.CookFoodGoodsEntity
import com.shuwei.dish.match.model.FoodRecord
import com.shuwei.dish.match.db.entity.SeasoningEntity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.shuwei.dish.match.model.toDTO
import com.shuwei.dish.match.net.UiState
import com.shuwei.dish.match.entity.CookFoodEntity
import com.shuwei.dish.match.entity.CookFoodGoodsEntity
import com.shuwei.dish.match.entity.FoodRecord
import com.shuwei.dish.match.entity.SeasoningEntity
import com.shuwei.dish.match.net.NetViewModel
import com.shuwei.dish.match.utils.AddressUtil
import com.shuwei.dish.match.utils.TextCellTool
import kotlinx.coroutines.launch
import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.clickWithDebounce
import com.shuwei.dish.match.utils.ext.gone
@@ -35,12 +32,15 @@ import com.shuwei.dish.match.utils.ext.toJsonString
import com.shuwei.dish.match.utils.ext.toType
import com.shuwei.dish.match.utils.ext.toast
import com.shuwei.dish.match.utils.ext.visible
import com.shuwei.dish.match.viewmodel.AppViewModel
import com.shuwei.dish.match.viewmodel.factory.AppFactory
import kotlin.getValue
@SuppressLint("UseSparseArrays")
class SubmitFoodActivity : BaseActivity() {
companion object {
const val TAG = "SubmitDishActivity"
const val TAG = "SubmitFoodActivity"
const val FOOD_ITEM = "foodItem"
const val GOODS_LIST = "goodsList"
}
@@ -69,6 +69,7 @@ class SubmitFoodActivity : BaseActivity() {
super.onCreate(savedInstanceState)
binding = ActivitySubmitFoodBinding.inflate(layoutInflater)
setContentView(binding.root)
initViewModel()
setHeaderBackground()
intent.extras?.apply {
food = getSerializable(FOOD_ITEM) as FoodRecord?
@@ -94,11 +95,13 @@ class SubmitFoodActivity : BaseActivity() {
}
})
addViewListener()
initObserver()
}
private fun loadSeasoningFromLocal() {
appViewModel.getCookFoodGoodsList(foodId = food!!.foodId!!, cookMode = food!!.cookMode, dinnerType = food!!.dinnerType) { it ->
appViewModel.getCookFoodGoodsList(
foodId = food!!.foodId!!,
cookMode = food!!.cookMode
) { it ->
goodsList = mutableListOf()
val goodsIdList = mutableListOf<String>()
//设置主辅材数据
@@ -175,33 +178,34 @@ class SubmitFoodActivity : BaseActivity() {
WeightUtil.addWeightListener(
weightKey = TAG,
getWeight = { address, state, weight ->
if (address == AddressUtil.TWO) {
cookFoodEntity.foodWeight = weight.toDouble()
binding.tvTotalWeight.text = "${weight / 1000f}"
}
val firstWeight = firstGoodsArray.get(address, null)
if (firstWeight == null) {
//未设置数据
if (state != WeightUtil.STATE_STABLE) {
//首次记录数据需要稳定数据
if (address == AddressUtil.TWO) {
cookFoodEntity.foodWeight = weight.toDouble()
binding.tvTotalWeight.text = "${weight / 1000f}"
}
val firstWeight = firstGoodsArray.get(address, null)
if (firstWeight == null) {
//未设置数据
if (state != WeightUtil.STATE_STABLE) {
//首次记录数据需要稳定数据
return@addWeightListener
}
firstGoodsArray.put(address, weight)
return@addWeightListener
}
firstGoodsArray.put(address, weight)
return@addWeightListener
}
var realUseWeight = firstWeight - weight
realUseWeight = if (realUseWeight > 0) realUseWeight else 0.0
seasoningArray.put(address, realUseWeight.toDouble())
var realUseWeight = firstWeight - weight
realUseWeight = if (realUseWeight > 0) realUseWeight else 0.0
seasoningArray.put(address, realUseWeight.toDouble())
// refreshSeasoningWeight()
val item = seasoningItems.firstOrNull { address == addressArray[it.sort]}
item?.let {
val lastWeight = seasoningArray.get(address) ?: 0.toDouble()
it.useWeight = (lastWeight + getCookingSeasoning(address)).roundedOneDecimalPlace()
updateGridData(it)
}
})
val item = seasoningItems.firstOrNull { address == addressArray[it.sort] }
item?.let {
val lastWeight = seasoningArray.get(address) ?: 0.toDouble()
it.useWeight =
(lastWeight + getCookingSeasoning(address)).roundedOneDecimalPlace()
updateGridData(it)
}
})
}
// private fun refreshSeasoningWeight() {
@@ -217,17 +221,18 @@ class SubmitFoodActivity : BaseActivity() {
// setGridData(seasoningItems)
// }
private fun updateGridData(entity:SeasoningEntity) {
private fun updateGridData(entity: SeasoningEntity) {
val gridLayout = binding.include.root
val tag = entity.sort.toString()
val frameLayout = gridLayout.findViewWithTag<FrameLayout>(tag)
TextCellTool.loadLayout(frameLayout , entity)
TextCellAdapter.loadLayout(frameLayout, entity)
}
private fun getCookingSeasoning(address: Int): Double {
if (isCooking.not()) return 0.toDouble()
if (seasoningItems.isEmpty()) return 0.toDouble()
val weight = seasoningCookingItems?.get(weightRelateArray2.get(address))?.useWeight ?: 0.toDouble()
val weight =
seasoningCookingItems?.get(weightRelateArray2.get(address))?.useWeight ?: 0.toDouble()
return weight
}
@@ -260,12 +265,12 @@ class SubmitFoodActivity : BaseActivity() {
return
}
val realSeasoningData = seasoningItems
//(binding.rvSeasoning.adapter as TextCellAdapter).list
//(binding.rvSeasoning.adapter as TextCellAdapter).list
if (realSeasoningData.isEmpty() && !isCooking) {
toast("未获取到调料信息")
return
}
if (realSeasoningData.isNotEmpty()&&realSeasoningData[0].goodsId.isBlank()) {
if (realSeasoningData.isNotEmpty() && realSeasoningData[0].goodsId.isNullOrBlank()) {
toast("还未设置调料信息,请去设置页面操作")
return
}
@@ -316,7 +321,7 @@ class SubmitFoodActivity : BaseActivity() {
toast("未获取到调料信息")
return
}
if (realSeasoningData.isNotEmpty()&&realSeasoningData[0].goodsId.isBlank()) {
if (realSeasoningData.isNotEmpty() && realSeasoningData[0].goodsId.isNullOrBlank()) {
toast("还未设置调料信息,请去设置页面操作")
return
}
@@ -335,8 +340,9 @@ class SubmitFoodActivity : BaseActivity() {
}
cookFoodEntity.let {
// it.stFoodInfoConstituteList = this@SubmitFoodActivity.goodsList
it.matchingConstituteInfoList = this@SubmitFoodActivity.goodsList
it.dinnerType = when(it.dinnerType) {
it.dinnerType = when (it.dinnerType) {
"1" -> "早餐"
"2" -> "午餐"
"3" -> "晚餐"
@@ -350,20 +356,46 @@ class SubmitFoodActivity : BaseActivity() {
cookFoodEntity.foodId = ""
goodsList?.forEach { it.foodId = "" }
}
netViewModel.submitCookFood(entity = cookFoodEntity.toDTO())
val json = cookFoodEntity.toJsonString()
Log.d(TAG, "submit: json=$json")
// HttpUtil.postJson(
// url = UrlConfig.SUBMIT_DISH,
// json = json,
// doSuccess = {
// Log.d(TAG, "submit: postJson=$it")
// submitSuccess(isSamplingData)
// }, doFailure = { code, msg ->
// Log.d(TAG, "submit: postJson:code=$code,msg=$msg")
// toast(msg)
// dismissLoading()
// })
netViewModel.submitCookFood(
entity = cookFoodEntity,
onSuccess = {
submitSuccess(isSamplingData)
},
onFailure = { code, msg ->
toast(msg)
dismissLoading()
})
}
private fun submitSuccess(isSamplingData: Boolean) {
if (food?.foodId.isNullOrBlank().not()) {
//接口提交成功后异步删除本地草稿
dbViewModel.markSubmittedAndDeleteAsync(
cookMode = food!!.cookMode,
foodId = food!!.foodId!!,
dinnerType = food!!.dinnerType ?: "0"
)
if (food?.foodId.isNullOrBlank()) {
//本地无删除数据直接跳转页面
dismissLoading()
jumpPage(isSamplingData, false)
return
}
//删除本地数据
appViewModel.deleteCookFoodAndGoods(
cookMode = food!!.cookMode,
foodId = food!!.foodId!!
) {
dismissLoading()
jumpPage(isSamplingData, false)
}
dismissLoading()
jumpPage(isSamplingData, false)
}
private fun jumpPage(isSampling: Boolean, isCooking: Boolean) {
@@ -379,26 +411,14 @@ class SubmitFoodActivity : BaseActivity() {
}
}
private fun initObserver() {
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
netViewModel.submitCookFoodState.collect { state ->
when (state) {
is UiState.Loading -> showLoading()
is UiState.Success -> {
Log.d(TAG, "submit: success")
submitSuccess(food?.cookMode == 1)
}
is UiState.Error -> {
Log.d(TAG, "submit: error code=${state.code}, msg=${state.msg}")
toast(state.msg)
dismissLoading()
}
else -> {}
}
}
}
}
private lateinit var appViewModel: AppViewModel
private fun initViewModel() {
val db = BaseApp.instance!!.database
val factory =
AppFactory(AppRepository(db.appDao()))
appViewModel =
ViewModelProvider(this, factory)[AppViewModel::class.java]
}
override fun onResume() {
@@ -413,7 +433,7 @@ class SubmitFoodActivity : BaseActivity() {
}
private var seasoningItems = mutableListOf<SeasoningEntity>()
private var seasoningCookingItems: List<SeasoningEntity> ?= null
private var seasoningCookingItems: List<SeasoningEntity>? = null
private fun loadTextCell(list: MutableList<SeasoningEntity>) {
seasoningItems.clear()
seasoningItems.addAll(list)
@@ -430,8 +450,8 @@ class SubmitFoodActivity : BaseActivity() {
}
private fun initConfigData() {
repeat(12) {num->
val firstOne = seasoningItems.firstOrNull {it.sort == num}
repeat(12) { num ->
val firstOne = seasoningItems.firstOrNull { it.sort == num }
if (firstOne == null) {
seasoningItems.add(SeasoningEntity().also { it.sort = num })
}
@@ -443,7 +463,7 @@ class SubmitFoodActivity : BaseActivity() {
list.forEach { entity ->
val tag = entity.sort.toString()
val frameLayout = gridLayout.findViewWithTag<FrameLayout>(tag)
TextCellTool.loadLayout(frameLayout, entity)
TextCellAdapter.loadLayout(frameLayout, entity)
}
}
}
@@ -0,0 +1,374 @@
package com.shuwei.dish.match.ui.fragment
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import androidx.recyclerview.widget.GridLayoutManager
import com.shuwei.dish.match.R
import com.shuwei.dish.match.adapter.FoodCollectionAdapter
import com.shuwei.dish.match.base.BaseFragment
import com.shuwei.dish.match.databinding.FragmentCollectBinding
import com.shuwei.dish.match.dialog.Loading
import com.shuwei.dish.match.objbox.FoodCollectionBean
import com.shuwei.dish.match.objbox.FoodModule
import com.shuwei.dish.match.ui.SettingActivity
import com.shuwei.dish.match.utils.BitmapSaver
import com.shuwei.dish.match.utils.Debouncer
import com.shuwei.dish.match.utils.ImageUtil
import com.shuwei.dish.match.utils.LogSaveUtil
import com.shuwei.dish.match.utils.ext.clickWithDebounce
import com.shuwei.dish.match.utils.ext.toast
@SuppressLint("NotifyDataSetChanged")
class CollectFragment : BaseFragment<FragmentCollectBinding>() {
companion object {
private const val TAG = "CollectFragment"
const val MAX_COUNT = 9
}
private var selectedFoodId: String? = ""
private var selectedFoodName: String? = ""
private val foodCollectionList = mutableListOf<FoodCollectionBean>().apply {
repeat(MAX_COUNT) {
add(FoodCollectionBean(isShowCamera = true))
}
}
// private val searchFoodList = mutableListOf<FoodInfo>()
// private var settingActivity: SettingActivity? = null
// private var checkedItem: FoodInfo? = null
// private val searchFoodAdapter by lazy {
// CollectFoodListAdapter(searchFoodList).apply {
// setOnItemClickListener { adapter, view, position ->
// searchFoodList.forEachIndexed { index, item -> item.isChecked = index == position }
// checkedItem = searchFoodList[position]
// notifyDataSetChanged()
// selectedFoodId = checkedItem!!.foodId
// selectedFoodName = checkedItem!!.foodName
// }
// }
// }
private val debouncer = Debouncer(2000)
private lateinit var settingActivity: SettingActivity
private val collectionAdapter: FoodCollectionAdapter by lazy {
FoodCollectionAdapter(foodCollectionList).apply {
addOnItemChildClickListener(R.id.ivDelete) { _, _, position ->
foodCollectionList[position].let {
it.bitmap = null
it.imageVector = null
it.imageFile = null
it.imageUri = null
it.isShowCamera = true
it.isFinish = false
it.uploadSuccess = false
}
notifyItemChanged(position)
}
}
}
override fun inflateBinding(
inflater: LayoutInflater,
container: ViewGroup?
): FragmentCollectBinding {
return FragmentCollectBinding.inflate(LayoutInflater.from(context))
}
//typealias CameraCallback = (Uri) -> Unit
private val cameraCallback: (Uri) -> Unit = { uri ->
try {
val index = foodCollectionList.indexOfFirst { it.imageFile == null }
if (index == -1) {
toast("每次只允许保存${MAX_COUNT}条数据")
hideWaitingDialog()
rerurn@ cameraCallback
}
activity?.runOnUiThread {
foodCollectionList[index].let {
it.imageVector = null
it.bitmap = null
it.isShowCamera = false
it.imageFile = null
it.imageUri = uri
}
collectionAdapter.notifyItemChanged(index)
}
// hideWaitingDialog()
Thread {
ImageUtil.uriToBitmap(requireActivity(), uri)?.let { bitmap ->
getImageVector(index, bitmap)
}
}.start()
} catch (e: Exception) {
e.printStackTrace()
hideWaitingDialog()
toast("程序异常${e.message}")
log("程序异常${e.message}")
}
}
private fun getImageVector(index: Int, bitmap: Bitmap) {
// val bitmap = BitmapCropper.cropCenter(
// original = srcBmp,
// targetWidth = 900, targetHeight = 900,
//// offsetX = 30, offsetY = 100
// )
val imageVector = try {
FoodModule.bitmap2FloatArray(bitmap, false)
} catch (e: Exception) {
e.printStackTrace()
toast("操作失败")
log("操作失败:${e.message}")
hideWaitingDialog()
return
}
val file = BitmapSaver.saveToAppFilesDir(
bitmap, requireActivity(), "IMG_CROP_${System.currentTimeMillis()}.jpg"
)
log("${this.javaClass.simpleName}-cameraCallback-裁剪bitmap保存文件路径:${file?.absolutePath}")
activity?.runOnUiThread {
foodCollectionList[index].let {
it.imageVector = imageVector
it.bitmap = null
it.isShowCamera = false
it.imageFile = file
}
collectionAdapter.notifyItemChanged(index)
}
if (bitmap.isRecycled.not()) {
bitmap.recycle()
}
hideWaitingDialog()
}
@SuppressLint("NotifyDataSetChanged")
private fun takePhoto() {
val count = foodCollectionList.count { it.bitmap != null }
if (count >= MAX_COUNT) {
toast("每次只允许保存${MAX_COUNT}条数据")
return
}
val index = foodCollectionList.indexOfFirst { it.imageFile == null }
if (index == -1) {
toast("每次只允许保存${MAX_COUNT}条数据")
hideWaitingDialog()
return
}
showWaitingDialog("采集中……")
settingActivity.takePhoto(succCallback = cameraCallback, failCallback = { errMsg ->
hideWaitingDialog()
// toast("拍照异常,请重新操作")
// if (cameraErrorCount >= 10) {
// toast("拍照异常,请重新操作")
// log("拍照异常,请重新操作:$errMsg")
// cameraErrorCount = 0
// return@takePhoto
// }
// cameraErrorCount++
// takePhoto()
})
}
override fun initialize() {
settingActivity = activity as SettingActivity
// 首次显示 CollectFragment 时初始化相机
settingActivity.initCamera(binding.flCameraPreview)
binding.rvFoodList.let {
it.layoutManager =
GridLayoutManager(requireActivity(), 3, GridLayoutManager.VERTICAL, false)
it.adapter = collectionAdapter
}
binding.btnFoodSearch.setOnClickListener {
searchFood()
}
binding.btnSave.setOnClickListener {
// if (checkedItem == null || checkedItem!!.isChecked.not()) {
// toast("请选择菜品名称")
// return@setOnClickListener
// }
// val count = foodCollectionList.count { it.imageFile != null }
// if (count == 0) {
// toast("请拍摄菜品照片")
// return@setOnClickListener
// }
// upload()
}
binding.editFoodName.setOnEditorActionListener { v, actionId, event ->
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
searchFood()
val imm =
v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(v.windowToken, 0)
true
} else {
false
}
}
binding.btnCollectedFood.setOnClickListener {
// startActivity(Intent(requireActivity(), CollectedFoodActivity::class.java))
}
binding.btnTakePhoto.clickWithDebounce {
val count = foodCollectionList.count { it.bitmap != null }
if (count >= MAX_COUNT) {
toast("每次只允许保存${MAX_COUNT}条数据")
return@clickWithDebounce
}
takePhoto()
}
binding.btnClearData.setOnClickListener { clearData() }
// binding.rvSearchFood.let {
// it.layoutManager = GridLayoutManager(context, 2)
// it.adapter = searchFoodAdapter
// }
searchFood()
}
@SuppressLint("NotifyDataSetChanged")
private fun upload() {
// lifecycleScope.launch {
// val totalFileCount = foodCollectionList.count { it.imageFile != null }
// showWaitingDialog2("图片上传中0/$totalFileCount")
// val params = HashMap<String, RequestBody>()
// //params["placeId"] = restId.toRequestBody()
// params["foodId"] = checkedItem!!.foodId.toRequestBody()
// params["foodName"] = checkedItem!!.foodName!!.toRequestBody()
// params["version"] = GlobalData.foodModelVersion.toRequestBody()
//
// ImageUploader(totalList = foodCollectionList, uploadImage = { batch ->
// val files = batch.map { it.imageFile }
// val foodVectorList = batch.filter { it.imageVector != null }.map {
// it.imageVector!!.joinToString(
// separator = ",", prefix = "[", postfix = "]"
// )
// }
// val foodVectorJson =
// foodVectorList.joinToString(separator = ",", prefix = "[", postfix = "]")
// log("json=$foodVectorJson")
// params["foodVector"] = foodVectorJson.toRequestBody()
// viewModel?.uploadCollectFoodPics(files, params)
// }, onProgress = { count, batch, idList ->
// //batch.forEach {
// // it.uploadSuccess = true
// //}
// runBlocking {
// activity?.runOnUiThread {
// showWaitingDialog("图片上传中$count/$totalFileCount")
// }
// val foodList = batch.mapIndexed { index, it ->
// Food(
// collectId = if (index < idList.size) idList[index] else null,
// foodId = checkedItem!!.foodId,
// foodName = checkedItem!!.foodName,
// foodVector = it.imageVector,
// version = GlobalData.foodModelVersion
// )
// }
// ObjectBox.putAll(foodList)
// activity?.runOnUiThread {
// batch.forEach { it.isFinish = true }
// collectionAdapter.notifyDataSetChanged()
// }
// }
// }, onError = {
// activity?.runOnUiThread {
// binding.root.postDelayed({
// hideWaitingDialog()
// toast("上传失败,请稍后重试")
// }, 1000)
// }
// }, onComplete = {
// //vectorThread()
// binding.root.postDelayed({
// hideWaitingDialog()
// toast("上传成功")
// }, 1000)
// }).processUploads()
// }
}
@SuppressLint("NotifyDataSetChanged")
fun searchFood() {
debouncer.debounce {
searchByFoodName(binding.editFoodName.text.toString()) {
// searchFoodList.clear()
// searchFoodList.addAll(it)
// searchFoodAdapter.notifyDataSetChanged()
}
}
}
var clickIndex = -1
@SuppressLint("NotifyDataSetChanged")
fun clearData() {
foodCollectionList.forEach {
it.bitmap = null
it.imageVector = null
it.imageFile = null
it.imageUri = null
it.isShowCamera = true
it.isFinish = false
it.uploadSuccess = false
}
collectionAdapter.notifyDataSetChanged()
clickIndex = -1
binding.editFoodName.setText("")
searchFood()
}
private var isFirstOpen = true
override fun onResume() {
super.onResume()
if (isVisible.not()) return
binding.llCameraFlag.run {
visibility = View.VISIBLE
postDelayed({
visibility = View.GONE
}, 1000)
}
}
override fun onPause() {
super.onPause()
binding.llCameraFlag.visibility = View.VISIBLE
}
private fun log(msg: String) {
Log.d(TAG, "log: $msg")
LogSaveUtil.saveLogFile(msg)
}
private fun searchByFoodName(name: String, block: (String) -> Unit) {}
private fun hideWaitingDialog() {
settingActivity.dismissLoading()
}
private fun showWaitingDialog(msg: String) {
if (Loading.isShowing()) {
Loading.updateContent(msg)
return
}
settingActivity.showLoading(msg)
}
}
@@ -0,0 +1,365 @@
package com.shuwei.dish.match.ui.fragment
import android.graphics.Typeface
import android.os.Bundle
import android.text.SpannableStringBuilder
import android.text.style.AbsoluteSizeSpan
import android.text.style.ForegroundColorSpan
import android.text.style.LineHeightSpan
import android.text.style.StyleSpan
import android.util.Log
import android.util.SparseArray
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.core.graphics.toColorInt
import androidx.core.view.forEach
import androidx.lifecycle.ViewModelProvider
import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.base.BaseFragment
import com.shuwei.dish.match.databinding.FragmentDeviceConfigBinding
import com.shuwei.dish.match.db.AppRepository
import com.shuwei.dish.match.dialog.CommonDialog
import com.shuwei.dish.match.dialog.SeasoningSearchDialog
import com.shuwei.dish.match.entity.SeasoningEntity
import com.shuwei.dish.match.entity.ResetReasoningRecord
import com.shuwei.dish.match.adapter.TextCellAdapter
import com.shuwei.dish.match.base.BaseActivity
import com.shuwei.dish.match.utils.AddressUtil
import com.shuwei.dish.match.utils.JsonAssetsLoader
import com.shuwei.dish.match.utils.MultiClickDetector
import com.shuwei.dish.match.utils.WeightUtil
import com.shuwei.dish.match.utils.ext.appendText
import com.shuwei.dish.match.utils.ext.buildSpannableString
import com.shuwei.dish.match.utils.ext.dp
import com.shuwei.dish.match.utils.ext.clickWithDebounce
import com.shuwei.dish.match.utils.ext.gone
import com.shuwei.dish.match.utils.ext.toast
import com.shuwei.dish.match.utils.ext.visible
import com.shuwei.dish.match.viewmodel.AppViewModel
import com.shuwei.dish.match.viewmodel.factory.AppFactory
import com.shuwei.dish.match.databinding.LayoutFoodRemindBinding
import com.shuwei.dish.match.ui.SamplingListActivity
import com.shuwei.dish.match.ui.SelectDishActivity
import com.shuwei.dish.match.utils.SpTool
import com.shuwei.dish.match.utils.ext.startActivity
/**
* 设备配置Fragment
* 功能:支持三种模式选择(制作、采样、品控),调料网格配置管理
*/
class DeviceConfigFragment : BaseFragment<FragmentDeviceConfigBinding>() {
companion object {
const val TAG = "DeviceConfigFragment"
const val COOK_MODE = "cookMode"
val modeList = listOf<ModeBean>(
ModeBean("制作模式", "菜品快速制作,记录熟重"),
ModeBean("采样模式", "菜品快速制作,记录熟重"),
ModeBean("品控模式", "菜品快速制作,记录熟重")
)
fun newInstance(cookMode: Int = 0): DeviceConfigFragment {
return DeviceConfigFragment().apply {
arguments = Bundle().apply {
putInt(COOK_MODE, cookMode)
}
}
}
}
data class ModeBean(
var modeName: String,
var modeDesc: String
)
private lateinit var appViewModel: AppViewModel
private var cookMode: Int = 0
private val weightArray = SparseArray<Double>()
private val addressArray = AddressUtil.getWeighAddressArray()
private var seasoningItems = mutableListOf<SeasoningEntity>()
private val detector = MultiClickDetector(targetCount = 10, intervalMs = 800)
override fun inflateBinding(
inflater: LayoutInflater,
container: ViewGroup?
): FragmentDeviceConfigBinding {
return FragmentDeviceConfigBinding.inflate(inflater, container, false)
}
override fun initialize(){
cookMode = arguments?.getInt(COOK_MODE, 0) ?: 0
initViewModel()
initUI()
addWeighListener()
addGridItemListener()
loadSeasoning()
}
/**
* 初始化ViewModel
*/
private fun initViewModel() {
val db = BaseApp.instance!!.database
val factory = AppFactory(AppRepository(db.appDao()))
appViewModel = ViewModelProvider(this, factory)[AppViewModel::class.java]
}
/**
* 初始化UI
*/
private fun initUI() {
loadQualitySpan(isEnable = false)
when (cookMode) {
1 -> {
binding.rbModeCook.isChecked = true
binding.rbModeSampling.isChecked = false
loadCookSpan(isClicked = true)
loadSamplingSpan()
}
2 -> {
binding.rbModeCook.isChecked = false
binding.rbModeSampling.isChecked = true
loadCookSpan()
loadSamplingSpan(isClicked = true)
}
else -> {
binding.rbModeCook.isChecked = true
binding.rbModeSampling.isChecked = false
loadCookSpan(isClicked = true)
loadSamplingSpan()
}
}
binding.rbModeCook.setOnClickListener {
SpTool.put(SpTool.LAUNCH_PAGE_TYPE, 0)
binding.rbModeCook.isChecked = true
binding.rbModeSampling.isChecked = false
loadCookSpan(isClicked = true)
loadSamplingSpan()
activity?.startActivity<SelectDishActivity> {
putExtra(SelectDishActivity.IS_CONFIG_PAGE, true)
}
activity?.finish()
}
binding.rbModeSampling.setOnClickListener {
SpTool.put(SpTool.LAUNCH_PAGE_TYPE, 1)
binding.rbModeCook.isChecked = false
binding.rbModeSampling.isChecked = true
loadCookSpan()
loadSamplingSpan(isClicked = true)
activity?.startActivity<SamplingListActivity> {
putExtra(SamplingListActivity.IS_CONFIG_PAGE, true) }
activity?.finish()
}
}
/**
* 添加称重监听
*/
private fun addWeighListener() {
WeightUtil.addWeightListener(
weightKey = TAG,
getWeight = { address, state, weight ->
Log.d(TAG, "addWeighListener: address=$address,stat=$state,weight=$weight")
weightArray.put(address, weight)
val item = seasoningItems.firstOrNull { address == addressArray[it.sort] }
item?.let {
it.useWeight = weight.toDouble()
updateGridData(it)
}
Log.d(TAG, "addWeighListener: update-----------------------")
})
}
/**
* 添加网格项点击监听
*/
private fun addGridItemListener() {
binding.include.root.forEach { child ->
child.clickWithDebounce {
val sort = child.tag.toString().toInt()
val entity = seasoningItems.firstOrNull { it.sort == sort } ?: SeasoningEntity()
clickGridItem(sort, entity)
}
}
}
/**
* 加载调料数据
*/
private fun loadSeasoning() {
appViewModel.loadSeasoning {
seasoningItems.clear()
seasoningItems.addAll(it)
initConfigData()
setGridData(seasoningItems)
}
}
/**
* 初始化配置数据,确保有12个调料位置
*/
private fun initConfigData() {
repeat(12) { num ->
val firstOne = seasoningItems.firstOrNull { it.sort == num }
if (firstOne == null) {
seasoningItems.add(SeasoningEntity().also { it.sort = num })
}
}
}
/**
* 设置网格数据
*/
private fun setGridData(list: MutableList<SeasoningEntity>) {
val gridLayout = binding.include.root
list.forEach { entity ->
val tag = entity.sort.toString()
val frameLayout = gridLayout.findViewWithTag<FrameLayout>(tag)
TextCellAdapter.loadLayout(frameLayout, entity)
}
}
/**
* 更新网格数据
*/
private fun updateGridData(entity: SeasoningEntity) {
val gridLayout = binding.include.root
val tag = entity.sort.toString()
val frameLayout = gridLayout.findViewWithTag<FrameLayout>(tag)
TextCellAdapter.loadLayout(frameLayout, entity)
}
/**
* 点击网格项,打开调料搜索对话框
*/
private fun clickGridItem(sort: Int, entity: SeasoningEntity) {
val currentAddress = AddressUtil.getWeighAddressArray().get(sort)
SeasoningSearchDialog(
weighIndex = sort,
weighAddress = currentAddress,
clickName = entity.goodsName
).show(requireActivity() as BaseActivity) { item ->
syncItem(entity, item)
// 3-调料
entity.materialType = 3
entity.sort = sort
saveSeasoning(entity) {
val tag = sort.toString()
val gridLayout = binding.include.root
val frameLayout = gridLayout.findViewWithTag<FrameLayout>(tag)
TextCellAdapter.loadLayout(frameLayout, entity)
}
}
}
/**
* 同步调料项数据
*/
private fun syncItem(oldItem: SeasoningEntity, newItem: SeasoningEntity) {
oldItem.run {
goodsId = newItem.goodsId
goodsName = newItem.goodsName
goodsOrRelationCode = newItem.goodsOrRelationCode
relateionType = newItem.relateionType
materialType = newItem.materialType
allEdible = newItem.allEdible
useWeight = newItem.useWeight
popularName = newItem.popularName
canteenId = newItem.canteenId
relateionType_dictText = newItem.relateionType_dictText
foodId = newItem.foodId
sort = newItem.sort
}
}
/**
* 保存调料数据
*/
fun saveSeasoning(entity: SeasoningEntity, action: () -> Unit = {}) {
appViewModel.saveSeasoning(entity) {
action()
toast("保存成功")
}
}
/**
* 加载品控模式文本样式
*/
private fun loadQualitySpan(isClicked: Boolean = false, isEnable: Boolean = true) {
binding.rbModeQuality.text =
getModeTextSpan(
topText = modeList[2].modeName,
bottomText = modeList[2].modeDesc,
topColor = if (isEnable) if (isClicked) "#00BC71" else "#000000" else "#B4B4B4",
bottomColor = if (isEnable) "#8B8B8B" else "#B4B4B4"
)
}
/**
* 加载采样模式文本样式
*/
private fun loadSamplingSpan(isClicked: Boolean = false, isEnable: Boolean = true) {
binding.rbModeSampling.text =
getModeTextSpan(
topText = modeList[1].modeName,
bottomText = modeList[1].modeDesc,
topColor = if (isEnable) if (isClicked) "#00BC71" else "#000000" else "#B4B4B4",
bottomColor = if (isEnable) "#8B8B8B" else "#B4B4B4"
)
}
/**
* 加载制作模式文本样式
*/
private fun loadCookSpan(isClicked: Boolean = false, isEnable: Boolean = true) {
binding.rbModeCook.text =
getModeTextSpan(
topText = modeList[0].modeName,
bottomText = modeList[0].modeDesc,
topColor = if (isEnable) if (isClicked) "#00BC71" else "#000000" else "#B4B4B4",
bottomColor = if (isEnable) "#8B8B8B" else "#B4B4B4"
)
}
/**
* 生成模式文本样式
*/
private fun getModeTextSpan(
topText: String,
topColor: String,
bottomText: String,
bottomColor: String
): SpannableStringBuilder {
return buildSpannableString {
appendText(
topText,
ForegroundColorSpan(topColor.toColorInt()),
StyleSpan(Typeface.BOLD),
AbsoluteSizeSpan(36, true),
LineHeightSpan { text, start, end, spanstartv, v, fm ->
fm.descent += 10.dp // 增加行间距
}
)
append("\n")
appendText(
bottomText,
ForegroundColorSpan(bottomColor.toColorInt()),
AbsoluteSizeSpan(26, true)
)
}
}
override fun onDestroyView() {
super.onDestroyView()
// 移除称重监听
WeightUtil.removeWeightListener(TAG)
}
}
@@ -13,9 +13,7 @@ import com.shuwei.dish.match.base.BaseApp
import com.shuwei.dish.match.base.BaseFragment
import com.shuwei.dish.match.databinding.FragmentDishListBinding
import com.shuwei.dish.match.databinding.LayoutEmptyViewBinding
import com.shuwei.dish.match.model.FoodRecord
import com.shuwei.dish.match.net.UiState
import com.shuwei.dish.match.entity.FoodRecord
import com.shuwei.dish.match.ui.PrepareCookActivity
import com.shuwei.dish.match.ui.SelectDishActivity
import com.shuwei.dish.match.ui.SubmitFoodActivity
@@ -86,13 +84,12 @@ class DishListFragment : BaseFragment<FragmentDishListBinding>() {
inflater: LayoutInflater,
container: ViewGroup?
): FragmentDishListBinding {
return FragmentDishListBinding.inflate(inflater, container, false)
return FragmentDishListBinding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
override fun initialize() {
activity = requireActivity() as SelectDishActivity
dinnerType = arguments?.getString(DINNER_TYPE, "0")?:"0"
dinnerType = arguments?.getString(DINNER_TYPE, "0") ?: "0"
binding.rvDishList.run {
layoutManager =
LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
@@ -115,6 +112,7 @@ class DishListFragment : BaseFragment<FragmentDishListBinding>() {
pageNo = 1
activity.showLoading()
getDishList()
binding.refreshLayout.setEnableRefresh(true)
}
private fun addViewListener() {
@@ -130,6 +128,7 @@ class DishListFragment : BaseFragment<FragmentDishListBinding>() {
}
}
}
fun getDishList(pageNo: Int, dinnerType: String) {
this.pageNo = pageNo
this.dinnerType = dinnerType
@@ -149,35 +148,71 @@ class DishListFragment : BaseFragment<FragmentDishListBinding>() {
@Suppress("unchecked_cast")
fun getDishList() {
activity.showLoading()
val param = mutableMapOf<String, Any>(
"dinnerType" to getDinnerTypeText(),
"pageNo" to pageNo,
"pageSize" to pageSize,
"canteenId" to BaseApp.canteenId
)
activity.netViewModel.searchFoodListWithCallback(
param = param,
onResult = { state ->
try {
if (isAdded.not()) return@searchFoodListWithCallback
when (state) {
is UiState.Success -> loadDishList(state.data)
is UiState.Error -> {
binding.refreshLayout.setEnableRefresh(true)
toast(state.msg)
finishRefresh()
if (pageNo == 1) {
loadEmptyView()
}
activity.delayDismissLoading()
}
else -> {}
}
} catch (e: Exception) {
e.printStackTrace()
activity.getFoodList(
param = mutableMapOf(
"pageNum" to pageNo,
"pageSize" to pageSize,
"placeId" to BaseApp.canteenId,
"dinnerType" to getDinnerTypeText()
),
onSuccess = { records ->
activity.delayDismissLoading()
finishRefresh()
loadDishList(records)
},
onFailure = { code, msg ->
if (isAdded.not()) {
return@getFoodList
}
toast(msg)
finishRefresh()
if (pageNo == 1) {
loadEmptyView()
}
activity.delayDismissLoading()
}
)
// val map = mapOf(
//// "foodName" to input,
// "dinnerType" to getDinnerTypeText(),
// "pageNo" to "$pageNo",
// "pageSize" to "$pageSize",
// "canteenId" to BaseApp.canteenId
// )
// val sb = StringBuilder(UrlConfig.QUERY_FOOD_LIST).apply {
// append("?")
// map.forEach { (key, value) -> append("$key=$value&") }
// }
// sb.deleteCharAt(sb.length - 1)
// HttpUtil.get(
// url = sb.toString(),
// doSuccess = { data ->
// binding.refreshLayout.run {
// setEnableRefresh(true)
// }
// activity.delayDismissLoading()
// finishRefresh()
// val json = data.toJsonString()
// val recordBean: FoodRecordBean? = json.toObject<FoodRecordBean>()
// loadDishList(recordBean)
// }, doFailure = { code, msg ->
// try {
// if (isAdded.not()) {
// return@get
// }
// binding.refreshLayout.run {
// setEnableRefresh(true)
// }
// toast(msg)
// finishRefresh()
// if (pageNo == 1) {
// loadEmptyView()
// }
// activity.delayDismissLoading()
// } catch (e: Exception) {
// e.printStackTrace()
// }
// })
}
private var emptyViewBinding: LayoutEmptyViewBinding? = null
@@ -185,6 +220,8 @@ class DishListFragment : BaseFragment<FragmentDishListBinding>() {
@SuppressLint("NotifyDataSetChanged")
private fun loadEmptyView() {
try {
binding.refreshLayout.setEnableRefresh(true)
binding.refreshLayout.setEnableLoadMore(false)
list.clear()
dishAdapter.notifyDataSetChanged()
if (emptyViewBinding == null) {
@@ -224,7 +261,7 @@ class DishListFragment : BaseFragment<FragmentDishListBinding>() {
if (food != null) {
food.isCooking = true
food.sort = index
food.dinnerType = entity.dinnerType?:"0"
food.dinnerType = entity.dinnerType ?: "0"
tempList.add(food)
} else {
//当前查询到的list不包括本地数据id,可能数据在很多页以后了,暂时只能直接构造数据
@@ -233,7 +270,7 @@ class DishListFragment : BaseFragment<FragmentDishListBinding>() {
foodId = entity.foodId,
foodName = entity.foodName,
sort = index,
dinnerType = entity.dinnerType?:"0",
dinnerType = entity.dinnerType ?: "0",
isCooking = true
)
)
@@ -248,11 +285,11 @@ class DishListFragment : BaseFragment<FragmentDishListBinding>() {
@SuppressLint("NotifyDataSetChanged")
private fun loadDishList(records: MutableList<FoodRecord>?) {
try {
if (isAdded.not()) return
binding.refreshLayout.setEnableRefresh(true)
activity.delayDismissLoading()
finishRefresh()
if (isAdded.not()) {
return
}
if (records.isNullOrEmpty()) {
//toast("暂未搜索到相关菜品信息")
if (pageNo == 1) {
loadEmptyView()
}
@@ -0,0 +1,76 @@
package com.shuwei.dish.match.utils
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader
object AssetsTool {
// fun readJson(context: Context, fileName: String): List<String> {
// val json = readAssetsFile(context, fileName)
// val json2 = json.replace("[[", "[").replaceAfterLast("]]", "]")
// val list = json2.split("],")
// val resultList = mutableListOf<String>()
// var count = 0
// val tempList = mutableListOf<String>()
// list.forEachIndexed { index, text ->
// if (count >= 500) {
// val tempJson = "[${tempList.joinToString (",")}]"
// resultList.add(tempJson)
// tempList.clear()
// count = 0
// }
// val newText = if (index == list.size - 1) text else "${text}]"
// tempList.add(newText)
// count++
// }
// if (count < 500) {
// val tempJson = "[${tempList.joinToString(",")}]"
// resultList.add(tempJson)
// }
// return resultList
// }
fun readAssetsFile(context: Context, fileName: String): String {
val stringBuilder = StringBuilder()
try {
val bf = BufferedReader(InputStreamReader(context.assets.open(fileName)))
bf.useLines { lines -> lines.forEach { stringBuilder.append(it) } }
} catch (e: IOException) {
e.printStackTrace()
}
return stringBuilder.toString()
}
fun loadImagesFromAssets(context: Context, subPath: String): MutableList<Bitmap> {
val bitmaps: MutableList<Bitmap> = mutableListOf()
val assetManager = context.assets
try {
val files = assetManager.list(subPath)
files?.forEach { file ->
assetManager.open("$subPath/$file").use { `is` ->
val bitmap = BitmapFactory.decodeStream(`is`)
if (bitmap != null) {
bitmaps.add(bitmap)
}
}
}
} catch (e: IOException) {
e.printStackTrace()
}
return bitmaps
}
fun loadImageBitmapFromAssets(context: Context, imagePath:String, action:(bmp: Bitmap)-> Unit) {
context.assets.open(imagePath).use { `is` ->
val bitmap = BitmapFactory.decodeStream(`is`)
action(bitmap)
}
}
}
@@ -0,0 +1,57 @@
package com.shuwei.dish.match.utils
import android.content.Context
import android.graphics.Bitmap
import android.os.Environment
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
object BitmapSaver {
// 保存到公共目录(需WRITE_EXTERNAL_STORAGE权限)
fun saveToPublicDirectory(
bitmap: Bitmap,
folderName: String = Environment.DIRECTORY_PICTURES,
fileName: String,
format: Bitmap.CompressFormat = Bitmap.CompressFormat.PNG,
quality: Int = 100
): File? {
val dir = Environment.getExternalStoragePublicDirectory(folderName)
if (!dir.exists()) dir.mkdirs()
return saveBitmap(bitmap, File(dir, fileName), format, quality)
}
// 保存到应用私有目录(无需权限)
fun saveToAppFilesDir(
bitmap: Bitmap,
context: Context,
fileName: String,
format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG,
quality: Int = 100
): File? {
//val dir = context.getExternalFilesDir(null)
val dir = File(context.cacheDir, "crop")
if (dir.exists().not()) {
dir.mkdirs()
}
return saveBitmap(bitmap, File(dir, fileName), format, quality)
}
private fun saveBitmap(
bitmap: Bitmap,
outputFile: File,
format: Bitmap.CompressFormat,
quality: Int
): File? {
return try {
FileOutputStream(outputFile).use { fos ->
bitmap.compress(format, quality, fos)
fos.flush()
}
outputFile
} catch (e: IOException) {
e.printStackTrace()
null
}
}
}
@@ -0,0 +1,84 @@
package com.shuwei.dish.match.utils
import android.net.Uri
import androidx.activity.ComponentActivity
import androidx.camera.core.CameraSelector
import androidx.camera.view.CameraController
import androidx.camera.view.LifecycleCameraController
import androidx.camera.view.PreviewView
class CameraUtils(private var activity: ComponentActivity) {
private var cameraController: LifecycleCameraController? = null
private var photoCaptureHelper: PhotoCaptureHelper? = null
private var failCallback: ((msg: String) -> Unit)? = null
// private var isCameraReady = false
fun takePhoto(succCallback: (Uri) -> Unit, failCallback: (msg: String) -> Unit = { }) {
this.failCallback = failCallback
cameraController?.let {
if (photoCaptureHelper == null) {
initCaptureHelper()
}
}
photoCaptureHelper?.let {
it.addSuccessCallback(succCallback)
it.bindCameraCallback {
bind()
}
it.takePhoto()
}
}
private fun initCaptureHelper() {
photoCaptureHelper = PhotoCaptureHelper(
context = activity,
cameraController = cameraController!!,
onSuccess = {},
onError = { msg ->
//toast(msg)
failCallback?.invoke(msg)
}
)
}
fun setPreviewController(previewView: PreviewView?) {
if (previewView?.controller == null) {
previewView?.controller = cameraController
}
}
fun initCamera() {
if (cameraController == null) {
cameraController = LifecycleCameraController(activity).apply {
// 必须设置有效的用例
setEnabledUseCases(
CameraController.IMAGE_CAPTURE
// or CameraController.VIDEO_CAPTURE
)
cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
}
bind()
}
// if (isCameraReady.not()) {
// try {
// cameraController!!.initializationFuture.addListener({
// isCameraReady = true
// Timber.d("Camera initialized successfully")
// }, ContextCompat.getMainExecutor(this))
// } catch (e: Exception) {
// Timber.d("Camera initialized error = ${e.message}")
// }
// }
}
fun bind() {
cameraController?.bindToLifecycle(activity)
}
fun unbind() {
cameraController?.unbind()
}
}
@@ -0,0 +1,235 @@
package com.shuwei.dish.match.utils
import android.app.ActivityManager
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.Process
import android.util.Log
import com.shuwei.dish.match.ui.HomeActivity
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.PrintWriter
import java.io.StringWriter
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import kotlin.system.exitProcess
/**
* 崩溃处理
*/
class CrashHandler private constructor(private val context: Context) :
Thread.UncaughtExceptionHandler {
companion object {
private const val TAG = "CrashHandler"
private const val CRASH_REPORTS_DIR = "crash_reports"
private const val LOG_LINES = 500 // 收集最近500行日志
@Volatile
private var instance: CrashHandler? = null
fun init(context: Context) {
if (instance == null) {
synchronized(CrashHandler::class.java) {
if (instance == null) {
instance = CrashHandler(context.applicationContext)
}
}
}
}
fun getCrashReportFiles(context: Context): Array<File> {
val crashDir = File(context.getExternalFilesDir(null), CRASH_REPORTS_DIR)
return if (crashDir.exists() && crashDir.isDirectory) {
crashDir.listFiles { _, name -> name.endsWith(".log") } ?: emptyArray()
} else {
emptyArray()
}
}
fun clearCrashReports(context: Context) {
getCrashReportFiles(context).forEach { it.delete() }
}
}
private val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
init {
Thread.setDefaultUncaughtExceptionHandler(this)
}
override fun uncaughtException(thread: Thread, ex: Throwable) {
handleException(thread, ex)
}
/**
* 自动重启app
*/
private fun restartApp() {
// 延迟1秒后重启应用
Handler(Looper.getMainLooper()).postDelayed({
val intent = Intent(context, HomeActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
val pendingIntent = PendingIntent.getActivity(
context, 0, intent,
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
)
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmManager.set(AlarmManager.RTC, System.currentTimeMillis() + 100, pendingIntent)
Process.killProcess(Process.myPid())
exitProcess(1)
}, 1000)
}
private fun handleException(thread: Thread, ex: Throwable) {
// 收集设备信息和异常信息
val crashInfo = collectCrashInfo(thread, ex)
// 保存日志文件
saveCrashInfoToFile(crashInfo)
// 这里可以添加其他处理逻辑,比如上传到服务器等
restartApp()
}
private fun collectCrashInfo(thread: Thread, ex: Throwable): String {
return buildString {
// 收集设备信息
collectDeviceInfo(this)
// 收集应用日志
try {
append("\n\n").append(collectLogs())
} catch (e: OutOfMemoryError) {
append("collectLogs发生OutOfMemoryError:${e.message}")
}
// 收集线程和异常信息
append("\n\n========== Thread & Exception Info ==========\n")
append("Thread: ${thread.name}\n")
append("Stack Trace:\n")
val sw = StringWriter()
val pw = PrintWriter(sw)
ex.printStackTrace(pw)
var cause: Throwable? = ex.cause
while (cause != null) {
cause.printStackTrace(pw)
cause = cause.cause
}
pw.close()
append(sw.toString())
}
}
private fun collectDeviceInfo(sb: StringBuilder) {
sb.append("========== Device Info ==========\n")
try {
// 应用信息
val pm = context.packageManager
val pi = pm.getPackageInfo(context.packageName, 0)
sb.append("App Version: ${pi.versionName}_${pi.versionCode}\n")
// Android 版本信息
sb.append("OS Version: ${Build.VERSION.RELEASE}_${Build.VERSION.SDK_INT}\n")
// 设备信息
sb.append("Vendor: ${Build.MANUFACTURER}\n")
sb.append("Model: ${Build.MODEL}\n")
sb.append("CPU ABI: ${Build.SUPPORTED_ABIS.firstOrNull() ?: "unknown"}\n")
// 其他信息
sb.append("Locale: ${Locale.getDefault()}\n")
sb.append(
"Current Time: ${
SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss",
Locale.getDefault()
).format(Date())
}\n"
)
// 内存信息
val memoryInfo = ActivityManager.MemoryInfo()
val activityManager =
context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
activityManager.getMemoryInfo(memoryInfo)
sb.append("Available Memory: ${memoryInfo.availMem / (1024 * 1024)}MB\n")
sb.append("Total Memory: ${memoryInfo.totalMem / (1024 * 1024)}MB\n")
sb.append("Low Memory: ${memoryInfo.lowMemory}\n")
} catch (e: Exception) {
Log.e(TAG, "Error while collecting device info", e)
sb.append("Error while collecting device info: ${e.message}\n")
}
}
private fun collectLogs(): String {
return buildString {
append("========== Application Logs ==========\n")
try {
val process = Runtime.getRuntime().exec("logcat -d -v threadtime")
val reader = process.inputStream.bufferedReader()
val logLines = reader.readLines()
val start = kotlin.comparisons.maxOf(0, logLines.size - LOG_LINES)
logLines.subList(start, logLines.size).forEach {
append(it).append("\n")
}
} catch (e: IOException) {
Log.e(TAG, "Error collecting logs", e)
append("Error collecting logs: ${e.message}\n")
}
}
}
private fun saveCrashInfoToFile(crashInfo: String) {
try {
val time = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.getDefault()).format(Date())
val fileName = "crash_$time.log"
val crashDir = File(context.getExternalFilesDir(null), CRASH_REPORTS_DIR)
if (!crashDir.exists() && !crashDir.mkdirs()) {
Log.d(TAG,"Failed to create crash report directory")
return
}
val crashFile = File(crashDir, fileName)
FileOutputStream(crashFile).use { it.write(crashInfo.toByteArray()) }
Log.d(TAG,"Crash info saved to: ${crashFile.absolutePath}")
} catch (e: Exception) {
Log.e(TAG, "Error saving crash info to file", e)
}
}
/**
* 清理旧的崩溃日志
*/
fun cleanupOldCrashReports(maxAgeDays: Int = 7) {
val crashDir = File(context.getExternalFilesDir(null), CRASH_REPORTS_DIR)
if (!crashDir.exists() || !crashDir.isDirectory) return
val now = System.currentTimeMillis()
val maxAgeMillis = maxAgeDays * 24 * 60 * 60 * 1000L
crashDir.listFiles()?.forEach { file ->
if (file.lastModified() < now - maxAgeMillis) {
file.delete()
}
}
}
}
@@ -0,0 +1,28 @@
package com.shuwei.dish.match.utils
/**
* 防抖工具
*/
class Debouncer(private val delayMillis: Long = 2000) {
private var lastActionTime = 0L
/**
* 执行防抖操作
* @param action 要执行的操作
* @return Boolean 是否执行了操作 (true=已执行, false=被防抖)
*/
fun debounce(action: () -> Unit): Boolean {
val currentTime = System.currentTimeMillis()
if (currentTime - lastActionTime >= delayMillis) {
lastActionTime = currentTime
action()
return true
}
return false
}
// 重置防抖计时
fun reset() {
lastActionTime = 0L
}
}

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