Author SHA1 Message Date
mazengfeiandClaude Fable 5 03e6c9a6bb feat(log): 增加日志定期清理机制,防止长期运行日志无限累加
- FileLogger 新增过期清理(保留7天)及单文件100MB写入上限
- CrashHandler 激活崩溃日志清理并补充异常保护
- App 启动清理 + 运行中每24小时定时清理,覆盖设备长期不断电场景

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 14:57:32 +08:00
mazengfei c77c32fde1 refactor(model): 删除已废弃的分页响应包装类PageData
- 移除PageData数据类及其相关注释
- 精简代码库,减少不必要的数据封装
- 为后续分页功能重构做好准备
- 保持代码结构清晰,提升可维护性
2026-08-09 17:59:22 +08:00
mazengfei dec5ecc100 refactor(api): 优化净菜包和餐品净菜包接口返回结构
- 移除净菜包和餐品净菜包接口中分页包装PageData,接口直接返回列表数组
- 修改相关调用处,取消对PageData的类型转换,直接处理列表数据
- 清理无用的PageData导入语句
- 保持接口功能和调用逻辑一致,提升代码简洁性和易维护性
2026-08-09 17:59:04 +08:00
mazengfei 5dc0bb2ec5 feat(model): 添加分页响应包装和分页请求体数据类
- 新增 PageData 泛型数据类,封装分页响应数据结构
- 增加 list、total、pageNum、pageSize 四个分页字段注释说明
- 新增 PageOptionsReq 数据类,用于分页请求参数封装
- pageNum、pageSize 必填,keyword 用于搜索或列表返回
- 适配净菜包及餐品净菜包下拉接口请求需求
2026-08-09 17:42:10 +08:00
mazengfei c3682d1ed3 refactor(api): 更新净菜包和餐品净菜包接口支持分页请求
- 使用 POST 请求和 JSON Body 替换 GET 请求,支持分页参数传递
- 更新接口返回类型为分页结构 PageData
- 在对应数据模型中新增分页请求参数类 PageOptionsReq 引入
- 增加 CleanPackageOption 和 MealPackageOption 模型字段以支持新需求
- 调整 NetViewModelV2 中调用接口逻辑,传递分页参数
- 修改 ShelfV2Activity 中数据处理逻辑,适配分页返回数据结构
- 更新全局常量 PROD_BASE_URL 地址
2026-08-09 17:07:13 +08:00
lvmeng e902f2869f fix(home): 修复行列数量赋值错误问题
- 纠正了 rows 和 columns 变量的赋值逻辑
- 将 rows 正确赋值为 cabinet.horizontalRows
- 将 columns 正确赋值为 cabinet.verticalCount
- 修正了因后端返回数据结构导致的显示错误
2026-06-26 09:27:50 +08:00
lvmengandClaude Opus 4.7 867cbb77be refactor(home): 用FlexboxLayoutManager替换GridLayoutManager,支持动态行列布局
FlexboxLayoutManager(COLUMN+WRAP)天然列优先排列,数据无需重排。
item宽高在拿到接口行列数后直接计算,适配任意rows×cols配置。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-26 09:23:10 +08:00
mazengfei 8aaba07cb6 perf(home): 调整货架商品同步到服务器的时间间隔
- 将定时任务的执行间隔从5秒调整为2分钟
- 保持初始延迟时间不变(10秒)
- 优化服务器同步频率以减少资源消耗
2026-06-04 18:10:17 +08:00
mazengfei 4f1a9dd0a1 refactor(home): 重构货架重量数据模型和显示逻辑
- 将实际重量计算统一到weight字段,移除actualQty和totalWeightG字段
- 修改HomeV2Activity中的重量数据处理逻辑,统一转换为千克单位
- 更新ShelfV2Activity中重量显示格式,优化单位转换逻辑
- 调整ShelfV2Adapter中重量显示逻辑,统一使用weight字段进行展示
- 在日志记录中增加温度信息输出
- 移除废弃的条件判断逻辑,简化代码结构
2026-06-04 17:28:23 +08:00
lvmeng 694e20023a refactor(layout): 优化网格布局工具和货架界面排序逻辑
- 新增 getLayoutManagerV2 方法用于创建垂直方向的网格布局管理器
- 修改 initListSortV2 方法支持动态行列参数控制排序逻辑
- 添加 submitListSortV2 方法用于提交时按槽位号排序
- 重构 HomeV2Activity 中的 UI 更新逻辑,支持动态行列配置
- 修复 RecyclerView 初始化时机问题,避免重复设置适配器
- 优化 ShelfV2Activity 中的清空操作,添加加载提示和自动关闭功能
- 完善槽位状态管理,支持空置和存放中状态标识
2026-06-04 14:32:08 +08:00
mazengfei ac17fca325 fix(home): 解决格口数据更新和权重计算问题
- 将超时时间从5分钟调整为1分钟
- 优化格口数据匹配逻辑,提高查找效率
- 根据物品类型分别处理重量数据存储
- 添加JSON序列化支持用于调试日志
- 修复itemType字段类型不一致问题
- 优化重量显示计算逻辑并简化条件判断
- 修复代码格式化问题
2026-06-04 11:19:25 +08:00
lvmeng 4b8e5f84e6 refactor(net): 重构网络请求配置以支持动态域名切换
- 在 ApiServiceV2 中使用 @Url 注解实现动态 URL 配置
- 添加 DynamicBaseUrlInterceptor 拦截器用于实时替换基础 URL
- 更新环境切换对话框中的环境名称和对应的 URL 映射关系
- 修改 UrlConfig 中的常量为变量并包含完整的基础 URL 路径
- 在 HttpManager 中为两个 API 服务实例都添加动态域名拦截器
- 调整环境配置常量定义顺序以匹配新的环境切换逻辑
2026-06-03 16:30:05 +08:00
lvmeng d6ba6cf527 feat(model): 更新货柜初始化结果和净菜包选项数据结构
- 在 CabinetInitResult 中添加 horizontalRows 和 verticalCount 字段
- 为 CleanPackageOption 和 MealPackageOption 添加 orderNo、foodId、needCount、needQtyKg 等字段
- 修改 CleanPackageOption 的 itemType 为 1,MealPackageOption 的 itemType 为 2
- 在 ShelfModelV2 中添加 cleanOrderNo 字段用于关联净菜订单号
- 更新 SyncItem 数据结构,添加 cleanOrderNo 字段
- 移除 HomeV2Activity 中的测试代码和硬编码逻辑
- 修改格口编号显示格式,补零显示两位数
- 调整定时任务执行频率,缩短同步间隔时间
2026-05-28 17:39:04 +08:00
lvmeng 4408acfca4 refactor(net): 注释掉设备令牌和配置相关功能
- 注释掉 getDeviceToken 方法及其相关逻辑
- 注释掉 getDeviceConfig 方法及其相关逻辑
- 注释掉 getAccessToken 方法及其相关逻辑
- 保留了 UiState 相关的状态流定义
- 维持了其他网络请求方法的正常功能
2026-05-27 19:55:02 +08:00
lvmengandClaude Sonnet 4.6 b0739605b9 feat(v2): 新增 V2 货柜接口层、页面及数据同步功能
- 新增 ApiServiceV2 / NetViewModelV2 承载 v1.1 接口(初始化、净菜包/餐品净菜包选项、数据同步)
- 新增 CabinetRequestInterceptor 独立 Retrofit 实例
- 新增 HomeV2Activity + ShelfV2Activity 及对应布局
- 新增 ShelfV2Adapter / CleanPackageAdapter / MealPackageAdapter
- 新增 ShelfModelV2 / CabinetInitResult / SyncBody / SyncItem / CleanPackageOption / MealPackageOption 数据模型
- HomeV2Activity 实现 syncShelfGoodsToServer 定时同步逻辑
- GridLayoutTool 新增 initListSortV2 支持 ShelfModelV2 排序

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 19:54:06 +08:00
lvmeng d524da1af3 refactor(net): 注释掉设备token和配置信息相关API接口
- 将getDeviceToken接口注释掉
- 将getDeviceInfo接口注释掉
- 将getAccessToken接口注释掉
- 保留了原有的文档注释
- 维持了文件结构完整性
2026-05-27 13:51:23 +08:00
33 changed files with 2033 additions and 150 deletions
+2
View File
@@ -87,6 +87,8 @@ dependencies {
implementation("org.greenrobot:eventbus:3.3.1")
implementation("com.google.android.flexbox:flexbox:3.0.0")
// implementation("io.github.jeremyliao:live-event-bus-x:1.8.0")
// implementation("com.hoho:android-usb-serial:1.3.0")
// implementation("io.github.jeadyx:jserialport:1.5")
+2
View File
@@ -45,6 +45,8 @@
tools:ignore="DiscouragedApi,LockedOrientationActivity" />
<!-- <activity android:name="com.shuwei.intelligent.shelves.activity.SettingActivity" />-->
<activity android:name="com.shuwei.intelligent.shelves.activity.LogActivity" />
<activity android:name="com.shuwei.intelligent.shelves.activity.HomeV2Activity" />
<activity android:name="com.shuwei.intelligent.shelves.activity.ShelfV2Activity" />
<receiver
android:name=".utils.BootReceiver"
@@ -5,8 +5,11 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.SharedPreferences
import android.os.Handler
import android.os.Looper
import com.shuwei.intelligent.shelves.utils.BootReceiver
import com.shuwei.intelligent.shelves.utils.CrashHandler
import com.shuwei.intelligent.shelves.utils.FileLogger
/**
* @author: star
@@ -19,10 +22,22 @@ class App : Application() {
super.onCreate()
app = this
CrashHandler.init(this)
// 启动时立即清理一次过期日志
cleanupLogs()
// 设备长期不断电、进程不会重新启动,运行中每24小时定期清理一次
logCleanupHandler.postDelayed(logCleanupRunnable, LOG_CLEANUP_INTERVAL)
val filter = IntentFilter(Intent.ACTION_BOOT_COMPLETED)
registerReceiver(BootReceiver(), filter)
}
/**
* 清理过期的日志文件:文件日志与崩溃日志均保留最近7天
*/
private fun cleanupLogs() {
FileLogger.cleanupExpiredLogs(this)
CrashHandler.getInstance()?.cleanupOldCrashReports()
}
companion object {
private lateinit var app: App
var canteenId = "0"
@@ -36,6 +51,19 @@ class App : Application() {
@Volatile
private var sharedPref: SharedPreferences? = null
/** 日志定期清理间隔:24小时,与日志文件按天滚动对齐 */
private const val LOG_CLEANUP_INTERVAL = 24L * 60 * 60 * 1000
private val logCleanupHandler = Handler(Looper.getMainLooper())
/** 日志定期清理任务:执行后重新调度自身,形成循环 */
private val logCleanupRunnable = object : Runnable {
override fun run() {
getInstance().cleanupLogs()
logCleanupHandler.postDelayed(this, LOG_CLEANUP_INTERVAL)
}
}
fun getInstance(): App {
return app
}
@@ -17,9 +17,10 @@ object GlobalData {
/**
* 具体业务 BaseUrl
*/
const val TEST_BASE_URL = "http://192.168.1.201:14801"
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 LOCAL_BASE_URL = "http://192.168.1.201:14801"
const val LOCAL_BASE_URL = "http://192.168.10.101:24801"
const val TEST_BASE_URL = "https://dev.yixiong-tech.com:8081"
const val PROD_BASE_URL = "https://platform-api.uat.shuziweidao.com"
}
@@ -0,0 +1,465 @@
package com.shuwei.intelligent.shelves.activity
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import androidx.activity.addCallback
import androidx.activity.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.GridLayoutManager
import com.shuwei.intelligent.shelves.App
import com.shuwei.intelligent.shelves.R
import com.shuwei.intelligent.shelves.adapter.ShelfV2Adapter
import com.shuwei.intelligent.shelves.base.BaseActivity
import com.shuwei.intelligent.shelves.databinding.ActivityHomeBinding
import com.shuwei.intelligent.shelves.dialog.CommonDialog
import com.shuwei.intelligent.shelves.model.CabinetInitResult
import com.shuwei.intelligent.shelves.model.ClearShelfEvent
import com.shuwei.intelligent.shelves.model.ShelfModelV2
import com.shuwei.intelligent.shelves.model.SendWeightEvent
import com.shuwei.intelligent.shelves.model.ShelfModel
import com.shuwei.intelligent.shelves.model.SyncBody
import com.shuwei.intelligent.shelves.net.Loading
import com.shuwei.intelligent.shelves.net.NetViewModel
import com.shuwei.intelligent.shelves.net.NetViewModelV2
import com.shuwei.intelligent.shelves.net.RespData
import com.shuwei.intelligent.shelves.net.UiState
import com.shuwei.intelligent.shelves.serial.ProtocolConstants
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.C_TEMP_CMD
import com.shuwei.intelligent.shelves.serial.ProtocolConstants.START_TEMP_CTRL_CMD
import com.shuwei.intelligent.shelves.serial.ScaleManager
import com.google.android.flexbox.FlexDirection
import com.google.android.flexbox.FlexWrap
import com.google.android.flexbox.FlexboxLayoutManager
import com.shuwei.intelligent.shelves.utils.IntervalExecutor
import com.shuwei.intelligent.shelves.utils.ext.dp
import com.shuwei.intelligent.shelves.utils.ext.gone
import com.shuwei.intelligent.shelves.utils.ext.toJsonString
import com.shuwei.intelligent.shelves.utils.ext.toast
import com.shuwei.intelligent.shelves.utils.ext.visible
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
class HomeV2Activity : BaseActivity() {
companion object {}
override val enableRightStatusMenu: Boolean = true
// 货架列表数据
private val list: MutableList<ShelfModelV2> = mutableListOf()
// 当前选中货架下标
private var shelfIndex = 0
// 当前温度(摄氏)
private var showTemperatureC = "0"
// 当前湿度
private var showHumidity = "0"
// 仅特定 deviceId 的部署点需要发送开锁指令
private fun isNeedOpenLock() = App.deviceId == "4787e213-90ab-3e32-88e0-ac271a937751"
private var deviceName = ""
private lateinit var binding: ActivityHomeBinding
private val viewModel: NetViewModel by viewModels()
private val viewModelV2: NetViewModelV2 by viewModels()
// 串口协议解析器,通过回调通知本 Activity
private fun initScaleManager() {
lifecycleScope.launch {
val ok = ScaleManager
.init(activateCode = ProtocolConstants.ACTIVE_MAP[App.deviceId] ?: "")
.onHeartbeat { _, data ->
data.weights.forEach { weight ->
val shelfNo = weight.index
// if (shelfNo in 1..list.size) {
// 用 slotNo 末尾数字与串口编号匹配
val pos = list.indexOfFirst {
it.slotNo?.filter { c -> c.isDigit() }?.toIntOrNull() == shelfNo
}
val model = list.getOrNull(pos)?:return@forEach
model.weight = (1.0 * weight.grams / 1000.0).toBigDecimal()
// if (model.itemType == 1) {
// model.actualQty = (weight.grams / 1000.0).toBigDecimal()
// } else {
// model.totalWeightG = weight.grams.toBigDecimal()
// }
shelfAdapter.notifyItemChanged(pos)
log("getWeightInfo: slotNo=${model.slotNo}, realWeight=${weight.grams}, temp=${data.temperature}, item:${model.toJsonString()}")
EventBus.getDefault().post(SendWeightEvent(shelfNo, weight.grams))
// }
}
}
.onTempReport { frame ->
log("onTempReport: $frame")
updateLeftStatus(deviceName)
}
.onLog { message -> log(message) }
.start { data ->
lastDataReceivedTime = System.currentTimeMillis()
noDataWarningDialog?.takeIf { it.isShowing }?.dismiss()
log("receiveSerialPortData: $data")
}
log("onCreate: openState=$ok")
}
}
@SuppressLint("NotifyDataSetChanged")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityHomeBinding.inflate(layoutInflater)
EventBus.getDefault().register(this)
setBackground()
setContentView(binding.root)
onBackPressedDispatcher.addCallback(this) {
}
updateLeftStatus("")
// 初始化 ScaleManager:注册回调、打开串口、启动接收
initScaleManager()
// 网络状态监听,独立协程,与串口初始化并行执行
lifecycleScope.launch {
viewModelV2.initCabinetUiState.collect { state ->
when (state) {
is UiState.Loading -> showProgress()
is UiState.Success<*> -> updateUI(state.data)
is UiState.Error -> showError(state.msg)
else -> {}
}
}
}
binding.root.postDelayed({
sendCmd(START_TEMP_CTRL_CMD)
sendCmd(C_TEMP_CMD)
}, 5000)
saveGoodsTask()
overdueTask()
viewModelV2.initCabinet(cabinetId = App.deviceId)
}
private val defDeviceList = listOf(
//原410货柜
"7a991439-3a12-3ef7-809b-c0258b839473",
//原1楼餐厅货柜
"4787e213-90ab-3e32-88e0-ac271a937751"
)
private fun getTokenSuccess(data: RespData<*>) {
log("getTokenSuccess: $data")
data.data?.let {
App.accessToken = it.toString()
viewModelV2.initCabinet(cabinetId = App.deviceId)
}
}
private fun showProgress() {
Loading.show(this)
}
private var cabinetInitResult: CabinetInitResult? = null
@SuppressLint("NotifyDataSetChanged")
private fun updateUI(data: RespData<*>) {
binding.include.root.gone()
window?.decorView?.postDelayed({ Loading.dismiss() }, 500)
cabinetInitResult = data.data as? CabinetInitResult
if (cabinetInitResult == null) {
log("updateUI: 数据类型错误,期望 CabinetInitResult,实际类型:${data.data?.javaClass?.simpleName}")
loadEmptyView()
return
}
val cabinet = cabinetInitResult!!
//行列数量后端返回是反着的
val rows = cabinet.horizontalRows
val columns = cabinet.verticalCount
log("updateUI: rows=$rows, columns=$columns")
deviceName = cabinet.deviceName
updateLeftStatus(deviceName)
App.canteenId = cabinet.canteenId.toString()
var tempList = cabinet.containerGoodsList
if (tempList.isNullOrEmpty()) {
loadEmptyView()
return
}
list.clear()
list.addAll(tempList)
val displayMetrics = resources.displayMetrics
shelfAdapter.itemWidth = (displayMetrics.widthPixels - 24.dp) / columns - 24.dp
shelfAdapter.itemHeight = (displayMetrics.heightPixels - 68.dp) / rows - 24.dp
if (binding.rvShelf.adapter == null) {
initRecyclerView()
} else {
shelfAdapter.notifyDataSetChanged()
}
}
private fun showError(message: String) {
Loading.dismiss()
toast(message)
loadEmptyView()
}
private var tipDialog: CommonDialog? = null
/** 最后一次成功收到串口数据的时间戳(毫秒) */
private var lastDataReceivedTime = System.currentTimeMillis()
/** 无数据超时提示弹窗 */
private var noDataWarningDialog: CommonDialog? = null
/**
* 无数据看门狗:每30秒检查一次,若超过5分钟未收到数据则弹出提示。
* 收到数据后会自动重置计时并关闭弹窗。
*/
private val noDataWatchdogTask = object : Runnable {
override fun run() {
// 生命周期检查,防止 Activity 销毁后继续执行
if (isFinishing || isDestroyed) return
val elapsed = System.currentTimeMillis() - lastDataReceivedTime
if (elapsed >= 5 * 60 * 1000L) {
if (noDataWarningDialog == null) {
noDataWarningDialog = CommonDialog(this@HomeV2Activity).apply {
dialogTitle = "设备提示"
dialogContent =
"长时间未收到数据,若无法开门或秤重量不更新情况,请考虑断电重启设备"
}
}
if (noDataWarningDialog?.isShowing == false) {
noDataWarningDialog?.show()
}
}
handler.postDelayed(this, 30 * 1000L)
}
}
/**
* 用户超时无操作时关灯
*/
override fun onUserInactive() {
switchLight(false)
}
override fun onDestroy() {
// 清理 Handler 所有回调,防止内存泄漏
handler.removeCallbacksAndMessages(null)
// 关闭并释放 Dialog 引用,防止内存泄漏
tipDialog?.dismiss()
tipDialog = null
noDataWarningDialog?.dismiss()
noDataWarningDialog = null
runBlocking { ScaleManager.release() }
saveTaskJob?.cancel()
overdueTaskJob?.cancel()
EventBus.getDefault().unregister(this)
super.onDestroy()
}
private val shelfAdapter by lazy {
ShelfV2Adapter(list).apply {
setOnItemClickListener { _, _, position ->
onShelfItemClicked(position)
}
}
}
/**
* 处理货架格子点击事件:按需开锁,然后跳转 ShelfV2Activity
*/
private fun onShelfItemClicked(position: Int) {
shelfIndex = position
lifecycleScope.launch {
if (isNeedOpenLock()) {
// 解析 slotNo 末尾数字判断锁区(1-5 号格口走锁1,其余走锁2)
val slotNum = list[position].slotNo?.filter { it.isDigit() }?.toIntOrNull() ?: 0
val openCmd = if (slotNum in 1..5)
ScaleManager.buildOpenLock1Cmd()
else
ScaleManager.buildOpenLock2Cmd()
ScaleManager.sendCmd(openCmd)
}
launch(Intent(this@HomeV2Activity, ShelfV2Activity::class.java).also {
it.putExtra(ShelfV2Activity.SHELF_MODEL, list[position])
}) { result ->
result?.let { onShelfActivityResult(position, it) }
}
}
}
/**
* 处理 ShelfV2Activity 返回结果
* TODO: 待 ShelfV2Activity 迁移到 RecordItem 后重新实现字段更新逻辑
*/
private fun onShelfActivityResult(position: Int, intent: Intent) {
@Suppress("DEPRECATION")
val model = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getSerializableExtra(ShelfV2Activity.SHELF_MODEL, ShelfModelV2::class.java)
} else {
intent.getSerializableExtra(ShelfV2Activity.SHELF_MODEL) as? ShelfModelV2
} ?: return
list[position].let {
it.itemType = model.itemType
it.itemName = model.itemName
it.traceCode = model.traceCode
it.cleanOrderNo = model.cleanOrderNo
it.spec = model.spec
it.foodId = model.foodId
it.storeTime = model.storeTime
it.slotStatus = model.slotStatus
}
shelfAdapter.notifyItemChanged(position)
// syncShelfGoodsToServer()
}
private fun initRecyclerView() {
val layoutManager = FlexboxLayoutManager(this).apply {
flexDirection = FlexDirection.COLUMN
flexWrap = FlexWrap.WRAP
}
binding.rvShelf.let {
it.layoutManager = layoutManager
it.adapter = shelfAdapter
it.itemAnimator.let { animator ->
if (animator is DefaultItemAnimator) {
animator.supportsChangeAnimations = false
}
}
}
}
private val handler = Handler(Looper.getMainLooper())
override fun onResume() {
super.onResume()
hideStatusBar()
// 启动无数据看门狗(每30秒检查一次)
handler.postDelayed(noDataWatchdogTask, 30 * 1000L)
}
override fun onPause() {
super.onPause()
// 停止无数据看门狗
handler.removeCallbacks(noDataWatchdogTask)
}
@SuppressLint("NotifyDataSetChanged")
private fun loadEmptyView() {
list.clear()
shelfAdapter.notifyDataSetChanged()
binding.include?.let {
it.root.visible()
it.root.setOnClickListener {
viewModelV2.initCabinet(cabinetId = App.deviceId)
}
it.ivEmptyIcon.setImageResource(R.drawable.ic_empty_white)
it.tvEmptyContent.setTextColor(Color.WHITE)
}
// loadTestData()
}
// @SuppressLint("NotifyDataSetChanged")
// private fun loadTestData() {
// binding.rvShelf.layoutManager = GridLayoutManager(this, 2)
// binding.rvShelf.adapter = shelfAdapter
// binding.include.root.gone()
// list.clear()
// var tempList = (1..10).map { i -> ShelfModelV2(slotNo = i.toString()) }
// tempList = GridLayoutTool.initListSortV2(tempList)
// list.addAll(tempList)
// shelfAdapter.notifyDataSetChanged()
// }
@Subscribe(threadMode = ThreadMode.MAIN)
fun clearEmptyShelf(event: ClearShelfEvent) {
// 用 slotNo 末尾数字与串口编号匹配
val index = list.indexOfFirst {
it.slotNo?.filter { c -> c.isDigit() }?.toIntOrNull() == event.shelfNo
}
if (index < 0) return
list[index] = list[index].copy(itemName = "")
shelfAdapter.notifyItemChanged(index)
}
private val taskExecutor by lazy { IntervalExecutor() }
private var saveTaskJob: Job? = null
/**
* 保存定时任务
*/
private fun saveGoodsTask() {
saveTaskJob =
// taskExecutor.startIntervalTaskWithInitialDelay(1 * 60 * 1000L, 5 * 60 * 1000L) {
taskExecutor.startIntervalTaskWithInitialDelay(10 * 1000L, 2 * 60 * 1000L) {
syncShelfGoodsToServer()
}
}
private var overdueTaskJob: Job? = null
/**
* 过期时间定时任务
*/
private fun overdueTask() {
overdueTaskJob = taskExecutor.startIntervalTaskWithInitialDelay(5 * 1000L, 5 * 60 * 1000L) {
viewModelV2.initCabinet(cabinetId = App.deviceId)
}
}
/**
* 将接口返回的临期状态应用到本地列表,仅在状态变化时刷新对应 item
*/
private fun applyOverdueState(items: List<ShelfModelV2>) {
if (items.size != list.size) return
items.forEachIndexed { index, newItem ->
val oldItem = list[index]
// slotStatus=2 表示临期预警
if (oldItem.slotStatus != newItem.slotStatus) {
list[index] = newItem
shelfAdapter.notifyItemChanged(index)
log("更新格口 ${newItem.slotNo} 物品:${newItem.itemName} 的状态:${newItem.slotStatus}")
}
}
}
/**
* 构建同步请求并提交到服务端
*/
private fun syncShelfGoodsToServer() {
if (cabinetInitResult == null) {
return
}
val submitList = list.sortedBy { it.slotNo?.toIntOrNull() ?: 0 }
viewModelV2.syncCabinetData(SyncBody().apply {
// cabinetId = list[0].cabinetId
cabinetId = App.deviceId
temperature = null
humidity = null
goodsList = submitList
})
}
}
@@ -59,7 +59,6 @@ class InitActivity : BaseActivity() {
binding = ActivityInitBinding.inflate(layoutInflater)
setContentView(binding.root)
App.deviceId = AppUtil.getUDID( this)
// App.deviceId = "4787e213-90ab-3e32-88e0-ac271a937751"
Log.d(TAG, "onCreate: deviceId = ${App.deviceId}")
@@ -112,60 +111,60 @@ class InitActivity : BaseActivity() {
}
}
private fun initConfig() {
binding.ivQrCode.setImageBitmap(
QRCodeUtil.generateQRCode(
content = App.deviceId,
size = 200.dp
)
)
binding.btnInit.setOnClickListener {
viewModel.getDeviceToken(App.deviceId)
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
launch {
viewModel.getDeviceTokenUiState.collect { state ->
when (state) {
is UiState.Loading -> {}
is UiState.Success<*> -> {
state.data.data?.let { deviceToken ->
Log.d(TAG, "initConfig: $deviceToken")
viewModel.getDeviceConfig(
deviceId = App.deviceId,
deviceToken = deviceToken.toString()
)
}
}
is UiState.Error -> toast(state.msg)
else -> {}
}
}
}
launch {
viewModel.getDeviceConfigUiState.collect { state ->
when (state) {
is UiState.Loading -> {}
is UiState.Success<*> -> {
state.data.data?.let {
if (it is DeviceConfigInfo) {
SpTool.put(SpTool.DEVICE_CONFIG_CACHE, it.toJsonString())
GlobalData.appBaseUrl = it.appPackageUrl?:""
App.canteenId = it.canteenId?:""
startActivity<HomeActivity>()
finish()
}
}
}
is UiState.Error -> toast(message = state.msg)
else -> {}
}
}
}
}
}
}
// private fun initConfig() {
// binding.ivQrCode.setImageBitmap(
// QRCodeUtil.generateQRCode(
// content = App.deviceId,
// size = 200.dp
// )
// )
//// binding.btnInit.setOnClickListener {
//// viewModel.getDeviceToken(App.deviceId)
//// }
// lifecycleScope.launch {
// repeatOnLifecycle(Lifecycle.State.CREATED) {
//// launch {
//// viewModel.getDeviceTokenUiState.collect { state ->
//// when (state) {
//// is UiState.Loading -> {}
//// is UiState.Success<*> -> {
//// state.data.data?.let { deviceToken ->
//// Log.d(TAG, "initConfig: $deviceToken")
//// viewModel.getDeviceConfig(
//// deviceId = App.deviceId,
//// deviceToken = deviceToken.toString()
//// )
//// }
//// }
//// is UiState.Error -> toast(state.msg)
//// else -> {}
//// }
//// }
//// }
//// launch {
//// viewModel.getDeviceConfigUiState.collect { state ->
//// when (state) {
//// is UiState.Loading -> {}
//// is UiState.Success<*> -> {
//// state.data.data?.let {
//// if (it is DeviceConfigInfo) {
//// SpTool.put(SpTool.DEVICE_CONFIG_CACHE, it.toJsonString())
//// GlobalData.appBaseUrl = it.appPackageUrl?:""
//// App.canteenId = it.canteenId?:""
////
//// startActivity<HomeActivity>()
//// finish()
//// }
//// }
//// }
//// is UiState.Error -> toast(message = state.msg)
//// else -> {}
//// }
//// }
//// }
// }
// }
// }
private fun checkConfigData(data: String): Boolean {
if (data.isBlank()) {
@@ -285,7 +284,8 @@ class InitActivity : BaseActivity() {
* 跳转到 HomeActivity
*/
private fun navigateToHome() {
startActivity<HomeActivity>()
// startActivity<HomeActivity>()
startActivity<HomeV2Activity>()
finish()
}
@@ -0,0 +1,384 @@
package com.shuwei.intelligent.shelves.activity
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.widget.LinearLayout
import androidx.activity.viewModels
import androidx.core.view.updateLayoutParams
import androidx.core.widget.addTextChangedListener
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.GridLayoutManager
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.util.setOnDebouncedItemClick
import com.scwang.smart.refresh.layout.constant.RefreshState
import com.shuwei.intelligent.shelves.R
import com.shuwei.intelligent.shelves.adapter.CleanPackageAdapter
import com.shuwei.intelligent.shelves.adapter.MealPackageAdapter
import com.shuwei.intelligent.shelves.base.BaseActivity
import com.shuwei.intelligent.shelves.databinding.ActivityShelfV2Binding
import com.shuwei.intelligent.shelves.model.CleanPackageOption
import com.shuwei.intelligent.shelves.model.MealPackageOption
import com.shuwei.intelligent.shelves.model.ShelfModelV2
import com.shuwei.intelligent.shelves.model.SendWeightEvent
import com.shuwei.intelligent.shelves.net.Loading
import com.shuwei.intelligent.shelves.net.NetViewModelV2
import com.shuwei.intelligent.shelves.net.RespData
import com.shuwei.intelligent.shelves.net.UiState
import com.shuwei.intelligent.shelves.serial.ScaleManager
import com.shuwei.intelligent.shelves.utils.KeyboardUtil
import com.shuwei.intelligent.shelves.utils.ext.addOnActionSearchListener
import com.shuwei.intelligent.shelves.utils.ext.clickWithDebounce
import com.shuwei.intelligent.shelves.utils.ext.dp
import com.shuwei.intelligent.shelves.utils.ext.gone
import com.shuwei.intelligent.shelves.utils.ext.toast
import com.shuwei.intelligent.shelves.utils.ext.visible
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@SuppressLint("NotifyDataSetChanged")
class ShelfV2Activity : BaseActivity() {
companion object {
const val SHELF_MODEL = "shelfModel"
const val YYYY_MM_DD__EEEE_HH_MM_SS = "yyyy年MM月dd日 EEEE***HH:mm:ss"
/** itemType=1:净菜包 */
private const val TYPE_CLEAN = 1
/** itemType=2:餐品净菜包 */
private const val TYPE_MEAL = 2
}
private lateinit var binding: ActivityShelfV2Binding
private var recordItem: ShelfModelV2? = null
private var realWeight: Int = 0
private var startTime = 0L
private val viewModel: NetViewModelV2 by viewModels()
// 当前选中类型,默认净菜包
private var currentType = TYPE_CLEAN
// 净菜包列表与 Adapter
private val cleanList: MutableList<CleanPackageOption> = mutableListOf()
private val cleanAdapter by lazy {
CleanPackageAdapter(cleanList).apply {
setOnDebouncedItemClick { _, _, position ->
cleanList.forEachIndexed { index, option -> option.isSelected = index == position }
notifyDataSetChanged()
binding.tvFoodName.text = cleanList[position].itemName
KeyboardUtil.hideKeyboard(this@ShelfV2Activity.window.decorView)
}
}
}
// 餐品净菜包列表与 Adapter
private val mealList: MutableList<MealPackageOption> = mutableListOf()
private val mealAdapter by lazy {
MealPackageAdapter(mealList).apply {
setOnDebouncedItemClick { _, _, position ->
mealList.forEachIndexed { index, option -> option.isSelected = index == position }
notifyDataSetChanged()
binding.tvFoodName.text = mealList[position].itemName
KeyboardUtil.hideKeyboard(this@ShelfV2Activity.window.decorView)
}
}
}
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityShelfV2Binding.inflate(layoutInflater)
setBackground()
setContentView(binding.root)
EventBus.getDefault().register(this)
recordItem = intent.getSerializableExtra(SHELF_MODEL) as? ShelfModelV2
recordItem?.let {
binding.tvShelfName.text = it.slotNo
binding.tvFoodName.text = if (it.itemName.isNullOrBlank()) "-" else it.itemName
val weightG = it.weight?.toDouble() ?: 0.0
realWeight = (weightG * 1000.0).toInt()
binding.tvFoodWeight.text = if (weightG >= 1.0) "%.3f千克".format(weightG)
else "${realWeight}"
startTime = System.currentTimeMillis()
}
initChipGroup()
initRecyclerView()
initSearchBox()
initButtons()
observeViewModel()
sendCmd(ScaleManager.buildGetStatusCmd())
loadList()
}
private fun initChipGroup() {
binding.chipGroupType.setOnCheckedStateChangeListener { _, checkedIds ->
currentType = when {
checkedIds.contains(R.id.chipClean) -> TYPE_CLEAN
checkedIds.contains(R.id.chipMeal) -> TYPE_MEAL
else -> currentType
}
// 切换时清空搜索框并重新加载列表
binding.etInputFood.setText("")
switchAdapter()
loadList()
}
}
private fun switchAdapter() {
val adapter: BaseQuickAdapter<*, *> =
if (currentType == TYPE_CLEAN) cleanAdapter else mealAdapter
binding.rvSearch.adapter = adapter
}
private fun initRecyclerView() {
binding.rvSearch.run {
layoutManager = GridLayoutManager(this@ShelfV2Activity, 2)
adapter = cleanAdapter
}
binding.refreshLayout.run {
setEnableRefresh(true)
setEnableLoadMore(false)
setOnRefreshListener { loadList() }
}
}
private fun initSearchBox() {
binding.etInputFood.run {
addOnActionSearchListener {
loadList()
KeyboardUtil.hideKeyboard(context, this)
}
addTextChangedListener(afterTextChanged = { editable ->
if (editable.isNullOrBlank()) loadList()
})
}
binding.root.setOnClickListener { KeyboardUtil.hideKeyboard(it) }
}
private fun initButtons() {
binding.ivBack.setOnClickListener { finish() }
binding.btnClearZero.setOnClickListener {
Loading.show(this@ShelfV2Activity)
clearZero()
window.decorView.postDelayed({ Loading.dismiss() }, 10000)
}
binding.btnClearEmpty.setOnClickListener { v ->
clearZero()
binding.tvFoodName.text = "-"
Loading.show(this@ShelfV2Activity)
v.postDelayed({
Loading.dismiss()
// 3 秒后自动关闭 Activity 并回传空值
val emptyRecord = recordItem?.also {
it.itemName = ""
//4-空置
it.slotStatus = 4
// it.totalWeightG = null
// it.actualQty = null
it.weight = null
it.itemType = 0
it.traceCode = null
it.storeTime = null
it.spec = null
it.specLabel = null
it.cleanOrderNo = null
}
val result = Intent().apply {
putExtra(SHELF_MODEL, emptyRecord)
}
setResult(RESULT_OK, result)
finish()
}, 3000)
}
binding.btnConfirm.clickWithDebounce {
var shelfModelV2: ShelfModelV2? = null
if (currentType == TYPE_CLEAN) {
val item = cleanList.firstOrNull { it.isSelected }
if (item == null) {
toast("请选择净菜包"); return@clickWithDebounce
}
shelfModelV2 = item.toShelfModelV2(recordItem).also {
//1-存放中
it.slotStatus = 1
}
} else {
val item = mealList.firstOrNull { it.isSelected }
if (item == null) {
toast("请选择餐品净菜包"); return@clickWithDebounce
}
shelfModelV2 = item.toShelfModelV2(recordItem).also {
//1-存放中
it.slotStatus = 1
}
}
// 将选中结果回传给上层 Activity
val result = Intent().apply {
putExtra(SHELF_MODEL, shelfModelV2)
}
setResult(RESULT_OK, result)
finish()
}
}
private fun observeViewModel() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.cleanPackageOptionsUiState.collect { state ->
when (state) {
is UiState.Loading -> showProgress()
is UiState.Success<*> -> updateCleanList(state.data)
is UiState.Error -> showError(state.msg)
else -> {}
}
}
}
launch {
viewModel.mealPackageOptionsUiState.collect { state ->
when (state) {
is UiState.Loading -> showProgress()
is UiState.Success<*> -> updateMealList(state.data)
is UiState.Error -> showError(state.msg)
else -> {}
}
}
}
}
}
}
private fun loadList() {
val keyword = binding.etInputFood.text.trim().toString().ifBlank { null }
if (currentType == TYPE_CLEAN) {
viewModel.getCleanPackageOptions(keyword = keyword)
} else {
viewModel.getMealPackageOptions(keyword = keyword)
}
}
private fun showProgress() {
Loading.show(this)
}
@SuppressLint("NotifyDataSetChanged")
private fun updateCleanList(data: RespData<*>) {
finishRefresh()
binding.include?.root?.gone()
binding.root.postDelayed({ Loading.dismiss() }, 200)
@Suppress("UNCHECKED_CAST")
val items = data.data as? List<CleanPackageOption>
if (items.isNullOrEmpty()) {
loadEmptyView(); return
}
cleanList.clear()
cleanList.addAll(items)
cleanAdapter.notifyDataSetChanged()
}
@SuppressLint("NotifyDataSetChanged")
private fun updateMealList(data: RespData<*>) {
finishRefresh()
binding.include?.root?.gone()
binding.root.postDelayed({ Loading.dismiss() }, 200)
@Suppress("UNCHECKED_CAST")
val items = data.data as? List<MealPackageOption>
if (items.isNullOrEmpty()) {
loadEmptyView(); return
}
mealList.clear()
mealList.addAll(items)
mealAdapter.notifyDataSetChanged()
}
private fun showError(message: String) {
finishRefresh()
Loading.dismiss()
toast(message)
loadEmptyView()
}
@SuppressLint("NotifyDataSetChanged")
private fun loadEmptyView() {
cleanList.clear()
mealList.clear()
cleanAdapter.notifyDataSetChanged()
mealAdapter.notifyDataSetChanged()
binding.include?.let {
it.root.visible()
it.root.setOnClickListener { loadList() }
it.ivEmptyIcon.setImageResource(R.drawable.ic_empty_gray)
it.ivEmptyIcon.updateLayoutParams<LinearLayout.LayoutParams> {
val swDpValue = resources?.configuration?.smallestScreenWidthDp ?: 0
width = if (swDpValue > 600) 200.dp else 120.dp
}
}
binding.refreshLayout.run {
setEnableRefresh(false)
setEnableLoadMore(false)
}
}
private fun finishRefresh() {
binding.refreshLayout.let {
if (it.state == RefreshState.Refreshing) it.finishRefresh(500)
else if (it.state == RefreshState.Loading) it.finishLoadMore(500)
}
}
private fun clearZero() {
lifecycleScope.launch {
val slotNum = recordItem?.slotNo?.filter { it.isDigit() }?.toIntOrNull() ?: 0
val zeroClearingCmd = ScaleManager.buildScaleZeroCmd(slotNum)
log("格口${recordItem?.slotNo}执行清零指令:$zeroClearingCmd")
sendCmd(zeroClearingCmd)
realWeight = 0
sendCmd(ScaleManager.buildGetStatusCmd())
startTime = System.currentTimeMillis()
}
KeyboardUtil.hideKeyboard(window.decorView)
}
override fun onClockTick() {
val sdf = SimpleDateFormat(YYYY_MM_DD__EEEE_HH_MM_SS, Locale.CHINA)
val arr = sdf.format(Date()).split("***")
updateLeftStatus(arr[0])
updateRightStatus(arr[1])
}
override fun onResume() {
super.onResume()
hideStatusBar()
}
override fun onDestroy() {
EventBus.getDefault().unregister(this)
super.onDestroy()
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun receiveWeightEvent(event: SendWeightEvent) {
val slotNum = recordItem?.slotNo?.filter { it.isDigit() }?.toIntOrNull() ?: return
if (event.shelfNo != slotNum) return
val intervalTime = System.currentTimeMillis() - startTime
val weight = event.weight
log("格口${recordItem?.slotNo}获取重量:${weight}克,间隔:${intervalTime}ms")
binding.tvFoodWeight.text = if (weight < 1000) "${weight}"
else "%.3f千克".format(weight / 1000.0)
startTime = System.currentTimeMillis()
window.decorView.postDelayed({ Loading.dismiss() }, 1000)
}
}
@@ -0,0 +1,39 @@
package com.shuwei.intelligent.shelves.adapter
import android.content.Context
import android.content.res.ColorStateList
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.ColorRes
import androidx.core.content.ContextCompat
import androidx.core.graphics.toColorInt
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.shuwei.intelligent.shelves.R
import com.shuwei.intelligent.shelves.databinding.ListItemSearchBinding
import com.shuwei.intelligent.shelves.model.CleanPackageOption
class CleanPackageAdapter(list: MutableList<CleanPackageOption>) :
BaseQuickAdapter<CleanPackageOption, CleanPackageAdapter.VH>(list) {
inner class VH(val binding: ListItemSearchBinding) : QuickViewHolder(binding.root)
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val binding = ListItemSearchBinding.inflate(LayoutInflater.from(context), parent, false)
return VH(binding)
}
override fun onBindViewHolder(holder: VH, position: Int, item: CleanPackageOption?) {
item ?: return
val isSelected = item.isSelected
holder.binding.btnFoodName.run {
text = " ${item.itemName}"
setTextColor(getColor(if (isSelected) R.color.shelf_name_blue else R.color.black999))
strokeColor = ColorStateList.valueOf(
(if (isSelected) "#4969F5" else "#E6E6E6").toColorInt()
)
}
}
private fun getColor(@ColorRes id: Int) = ContextCompat.getColor(context, id)
}
@@ -0,0 +1,41 @@
package com.shuwei.intelligent.shelves.adapter
import android.content.Context
import android.content.res.ColorStateList
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.ColorRes
import androidx.core.content.ContextCompat
import androidx.core.graphics.toColorInt
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.shuwei.intelligent.shelves.R
import com.shuwei.intelligent.shelves.databinding.ListItemSearchBinding
import com.shuwei.intelligent.shelves.model.MealPackageOption
class MealPackageAdapter(list: MutableList<MealPackageOption>) :
BaseQuickAdapter<MealPackageOption, MealPackageAdapter.VH>(list) {
inner class VH(val binding: ListItemSearchBinding) : QuickViewHolder(binding.root)
var selectedId: Long = -1L
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val binding = ListItemSearchBinding.inflate(LayoutInflater.from(context), parent, false)
return VH(binding)
}
override fun onBindViewHolder(holder: VH, position: Int, item: MealPackageOption?) {
item ?: return
val isSelected = item.isSelected
holder.binding.btnFoodName.run {
text = " ${item.itemName}"
setTextColor(getColor(if (isSelected) R.color.shelf_name_blue else R.color.black999))
strokeColor = ColorStateList.valueOf(
(if (isSelected) "#4969F5" else "#E6E6E6").toColorInt()
)
}
}
private fun getColor(@ColorRes id: Int) = ContextCompat.getColor(context, id)
}
@@ -0,0 +1,90 @@
package com.shuwei.intelligent.shelves.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.ColorRes
import androidx.core.content.ContextCompat
import com.chad.library.adapter4.BaseQuickAdapter
import com.chad.library.adapter4.viewholder.QuickViewHolder
import com.google.android.flexbox.FlexboxLayoutManager
import com.shuwei.intelligent.shelves.R
import com.shuwei.intelligent.shelves.databinding.ListItemShelfBinding
import com.shuwei.intelligent.shelves.model.ShelfModelV2
import com.shuwei.intelligent.shelves.utils.ext.gone
import com.shuwei.intelligent.shelves.utils.ext.visible
import kotlin.math.abs
class ShelfV2Adapter(list: MutableList<ShelfModelV2>) :
BaseQuickAdapter<ShelfModelV2, ShelfV2Adapter.VH>(list) {
/** 每个格口 item 的高度(像素),由 Activity 动态计算 */
var itemHeight: Int = 0
/** 每个格口 item 的宽度(像素),由 Activity 动态计算 */
var itemWidth: Int = 0
inner class VH(val binding: ListItemShelfBinding) : QuickViewHolder(binding.root)
override fun onCreateViewHolder(context: Context, parent: ViewGroup, viewType: Int): VH {
val binding = ListItemShelfBinding.inflate(LayoutInflater.from(context), parent, false)
return VH(binding)
}
override fun onBindViewHolder(holder: VH, position: Int, item: ShelfModelV2?) {
item ?: return
// 动态设置 item 宽高
val lp = holder.itemView.layoutParams as? FlexboxLayoutManager.LayoutParams
lp?.apply {
if (itemWidth > 0) width = itemWidth
if (itemHeight > 0) height = itemHeight
}
val binding = holder.binding
// 格口编号
binding.tvShelfName.text = item.slotNo?.padStart(2, '0')
// 临期预警(slotStatus=2)显示过期图标
if (item.slotStatus.toInt() == 2) binding.ivStaleFood.visible()
else binding.ivStaleFood.gone()
// // 重量:itemType=2 用 totalWeightG(g)itemType=1 用 actualQty×1000(g)
// val weightG = if (item.itemType == 1) (item.actualQty?.toDouble() ?: 0.0) * 1000.0
// else item.totalWeightG?.toDouble() ?: 0.0
val weightG = item.weight?.toDouble() ?: 0.0
binding.tvFoodWeight.run {
setTextColor(getColor(R.color.food_weight_orange))
text = if (abs(weightG) >= 1.0) "%.3f千克".format(weightG)
else "${(weightG * 1000.0).toInt()}"
}
val isBlank = item.itemName.isNullOrBlank()
if (isBlank) {
binding.layoutCard.setCardBackgroundColor(getColor(R.color.bg_card_blue))
binding.tvShelfName.setTextColor(getColor(R.color.shelf_name_white))
binding.tvFoodName.run {
setTextColor(getColor(R.color.white))
text = ""
}
binding.tvStoreDate.run {
setTextColor(getColor(R.color.food_weight_blue))
text = "-"
}
return
}
binding.layoutCard.setCardBackgroundColor(getColor(R.color.bg_card_white))
binding.tvShelfName.setTextColor(getColor(R.color.shelf_name_blue))
binding.tvFoodName.run {
setTextColor(getColor(R.color.food_name_black))
text = item.itemName
}
binding.tvStoreDate.run {
setTextColor(getColor(R.color.black999))
text = item.storeTime
}
}
private fun getColor(@ColorRes id: Int) = ContextCompat.getColor(context, id)
}
@@ -112,7 +112,7 @@ open class BaseActivity : AppCompatActivity() {
}
// 5分钟
private val TIME_OUT: Long = (5 * 60 * 1000).toLong()
private val TIME_OUT: Long = (1 * 60 * 1000).toLong()
private val mHandler by lazy { Handler(Looper.getMainLooper()) }
private val mTimeoutRunnable = Runnable {
// 用户长时间无操作时回调,子类可覆写实现具体逻辑(如关灯)
@@ -39,8 +39,8 @@ class EnvSwitchDialog(
override fun initView() {
// 根据当前 appBaseUrl 预选对应 RadioButton,不匹配则不选
when (GlobalData.appBaseUrl) {
GlobalData.TEST_BASE_URL -> binding.rbTest.isChecked = true
GlobalData.UAT_BASE_URL -> binding.rbUat.isChecked = true
GlobalData.LOCAL_BASE_URL -> binding.rbTest.isChecked = true
GlobalData.TEST_BASE_URL -> binding.rbUat.isChecked = true
GlobalData.PROD_BASE_URL -> binding.rbProd.isChecked = true
}
@@ -55,8 +55,8 @@ class EnvSwitchDialog(
return@setOnClickListener
}
val newUrl = when (binding.rgEnv.checkedRadioButtonId) {
binding.rbTest.id -> GlobalData.TEST_BASE_URL
binding.rbUat.id -> GlobalData.UAT_BASE_URL
binding.rbTest.id -> GlobalData.LOCAL_BASE_URL
binding.rbUat.id -> GlobalData.TEST_BASE_URL
binding.rbProd.id -> GlobalData.PROD_BASE_URL
else -> return@setOnClickListener
}
@@ -0,0 +1,14 @@
package com.shuwei.intelligent.shelves.model
/**
* 货柜初始化接口(4.1)响应体
*/
data class CabinetInitResult(
val canteenId: Long = 0,
val canteenName: String = "",
val deviceName: String = "",
val area: String = "",
val horizontalRows:Int = 0,
val verticalCount:Int = 0,
val containerGoodsList: List<ShelfModelV2>? = null,
)
@@ -0,0 +1,43 @@
package com.shuwei.intelligent.shelves.model
import com.shuwei.intelligent.shelves.utils.DateTimeUtil
import java.math.BigDecimal
import java.util.Date
/**
* 净菜包下拉选项,对应接口 4.2.1itemType=1
*/
data class CleanPackageOption(
val id: Long = 0,
val itemName: String = "",
/** 净菜订单号,sync 时作为 cleanOrderNo 回传 */
val orderNo: String = "",
val packageSpec: String? = null,
/** 关联菜品 id */
val foodId: Long? = null,
/** 需求份数 */
val needCount: Int? = null,
/** 需求计量(kg */
val needQtyKg: BigDecimal? = null,
/** 保质期至,格式 yyyy-MM-dd */
val expiryDate: String? = null,
/** 申领时间,格式 yyyy-MM-dd HH:mm:ss,仅订单有 */
val applyTime: String? = null,
/** 数据来源:1=净菜订单,2=食材库 */
val source: Short? = null,
/** 净菜类型 id,仅订单有 */
val vegTypeId: Long? = null,
/** 食材库 idsource=2 时有值,sync 时回传 materId */
val materId: Long? = null,
var isSelected: Boolean = false
) {
/** 将净菜包选项转换为格口记录 */
fun toShelfModelV2(model: ShelfModelV2? = null) = (model ?: ShelfModelV2()).also {
it.itemType = 1
it.itemName = itemName
it.cleanOrderNo = orderNo
it.spec = packageSpec
it.foodId = foodId
it.storeTime = DateTimeUtil.formatDateTime(dateTime = Date())
}
}
@@ -0,0 +1,37 @@
package com.shuwei.intelligent.shelves.model
import com.shuwei.intelligent.shelves.utils.DateTimeUtil
import java.math.BigDecimal
import java.util.Date
/**
* 餐品净菜包下拉选项,对应接口 4.2.2(itemType=2
*/
data class MealPackageOption(
val id: Long = 0,
val itemName: String = "",
/** 净菜订单号,sync 时作为 cleanOrderNo 回传 */
val orderNo: String = "",
val packageSpec: String? = null,
/** 关联菜品 id */
val foodId: Long? = null,
/** 需求份数 */
val needCount: Int? = null,
/** 需求计量(kg */
val needQtyKg: BigDecimal? = null,
/** 保质期至,格式 yyyy-MM-dd(包装完成后有值) */
val expiryDate: String? = null,
/** 数据来源:1=净菜订单,2=餐品库 */
val source: Short? = null,
var isSelected: Boolean = false
) {
/** 将餐品净菜包选项转换为格口记录 */
fun toShelfModelV2(model: ShelfModelV2? = null) = (model ?: ShelfModelV2()).also {
it.itemType = 2
it.itemName = itemName
it.cleanOrderNo = orderNo
it.spec = packageSpec
it.foodId = foodId
it.storeTime = DateTimeUtil.formatDateTime(dateTime = Date())
}
}
@@ -0,0 +1,18 @@
package com.shuwei.intelligent.shelves.model
/**
* 下拉列表分页请求体
*
* 用于净菜包/餐品净菜包下拉接口(POST),
* pageNum、pageSize 必填,keyword 可选:
* - 不传 keyword:返回净菜订单列表
* - 传 keyword:搜索食材库/餐品库
*/
data class PageOptionsReq(
/** 页码,从 1 开始 */
val pageNum: Long = 1,
/** 每页条数 */
val pageSize: Long = 50,
/** 搜索关键字,不传返回订单列表,传值搜索库表 */
val keyword: String? = null
)
@@ -0,0 +1,58 @@
package com.shuwei.intelligent.shelves.model
import java.io.Serializable
import java.math.BigDecimal
/**
* 格口存放记录,对应接口 4.1 货柜初始化返回的 containerGoodsList 元素
*/
data class ShelfModelV2(
var id: Long = 0,
/** 类型:1-食材净菜 / 2-餐品净菜包 */
var itemType: Int = 0,
/** 格口编号 */
var slotNo: String? = "",
/** 格口所在柜子编号 */
var cabinetId: String? = "",
/** 智能柜名称 */
var cabinetName: String? = "",
var area: String = "",
/** 溯源码 */
var traceCode: String? = "",
/** 关联净菜订单号 */
var cleanOrderNo: String? = "",
/** 物料名称 */
var itemName: String? = "",
/** 订单量 kgitemType=1 */
var orderQty: BigDecimal? = null,
/** 实收量 kgitemType=1 */
// var actualQty: BigDecimal? = null,
/** 差异量 kgitemType=1,可为负) */
var diffQty: BigDecimal? = null,
/** 规格(itemType=2,如:标准份) */
var spec: String? = null,
var specLabel: String? = null,
/** 包数(itemType=2 */
var pkgCount: Int? = null,
/** 总重量 gitemType=2 */
// var totalWeightG: BigDecimal? = null,
/** 统一重量字段,单位千克 */
var weight: BigDecimal? = null,
/** 验收结果:1-合格 / 2-轻微差异 / 3-不合格 / 4-待验 */
var checkResult: Short? = null,
/** 存储温度(℃) */
var temperature: BigDecimal? = null,
/** 存储湿度(% */
var humidity: BigDecimal? = null,
/** 剩余保质期(小时) */
var shelfLifeRemain: Int? = null,
var storeTime: String? = null,
/** 格口状态:1-存放中 / 2-临期预警 / 3-温度超标 / 4-空置 / 5-异常 */
var slotStatus: Short = 0,
/** 绑定菜品 iditemType=2 */
var foodId: Long? = null,
var unit: String? = null,
var remark: String? = null,
var canteenId: Long? = null,
var isClicked: Boolean = false
) : Serializable
@@ -0,0 +1,14 @@
package com.shuwei.intelligent.shelves.model
import java.math.BigDecimal
/**
* 全量同步整柜数据请求体,对应接口 4.3
* 注意:goodsList 为空或不传时,后端会软删除整柜所有格口数据
*/
data class SyncBody(
var cabinetId: String?=null,
var temperature: BigDecimal? = null,
var humidity: BigDecimal? = null,
var goodsList: List<ShelfModelV2>? = null,
)
@@ -0,0 +1,35 @@
package com.shuwei.intelligent.shelves.model
import java.math.BigDecimal
/**
* 全量同步单格口数据,对应接口 4.3 goodsList 元素
*/
data class SyncItem(
val slotNo: String,
/** 类型:1-食材净菜 / 2-餐品净菜包 */
val itemType: Short? = null,
val itemName: String? = null,
/** 关联净菜订单号(从下拉选项 orderNo 取值回传) */
val cleanOrderNo: String? = null,
/** 绑定菜品 iditemType=2 */
val foodId: Long? = null,
/** 订单量 kgitemType=1 */
val orderQty: BigDecimal? = null,
/** 实收量 kgitemType=1,与 totalWeightG 二选一) */
val actualQty: BigDecimal? = null,
/** 总重量 gitemType=2,后端自动换算为 kg 存储) */
val totalWeightG: BigDecimal? = null,
/** 规格:1-标准份 / 2-大份 / 3-小份(itemType=2 */
val spec: Int? = null,
val pkgCount: Int? = null,
/** 验收结果:1-合格 / 2-轻微差异 / 3-不合格 / 4-待验 */
val checkResult: Short? = null,
/** 格口状态:1-存放中 / 2-临期预警 / 3-温度超标 / 4-空置 / 5-异常 */
val slotStatus: Short? = null,
/** 剩余保质期(小时) */
val shelfLifeRemain: Int? = null,
/** 存入时间,仅新增时生效,不传则取当前时间 */
val storeTime: String? = null,
val remark: String? = null,
)
@@ -13,29 +13,29 @@ import retrofit2.http.Query
import retrofit2.http.Url
interface ApiService {
/**
* device获取token
*/
@GET("/sys/getEquipmentToken")
suspend fun getDeviceToken(
@Query("qrcodeId") qrcodeId: String,
@Query("appVersion") appVersion: String = App.appVersion
): RespData<String>
/**
*获取配置信息
*/
@GET("/equipment/stEquipment/queryByEquipmentCode")
suspend fun getDeviceInfo(
@Query("equipmentCode") equipmentCode: String,
@Query("appVersion") appVersion: String = App.appVersion
): RespData<DeviceConfigInfo>
@GET
suspend fun getAccessToken(
@Url url: String = UrlConfig.GET_ACCESS_TOKEN,
@Query("qrcodeId") qrcodeId: String
): RespData<String?>
// /**
// * device获取token
// */
// @GET("/sys/getEquipmentToken")
// suspend fun getDeviceToken(
// @Query("qrcodeId") qrcodeId: String,
// @Query("appVersion") appVersion: String = App.appVersion
// ): RespData<String>
//
// /**
// *获取配置信息
// */
// @GET("/equipment/stEquipment/queryByEquipmentCode")
// suspend fun getDeviceInfo(
// @Query("equipmentCode") equipmentCode: String,
// @Query("appVersion") appVersion: String = App.appVersion
// ): RespData<DeviceConfigInfo>
//
// @GET
// suspend fun getAccessToken(
// @Url url: String = UrlConfig.GET_ACCESS_TOKEN,
// @Query("qrcodeId") qrcodeId: String
// ): RespData<String?>
@GET
suspend fun getShelfList(
@@ -0,0 +1,55 @@
package com.shuwei.intelligent.shelves.net
import com.shuwei.intelligent.shelves.model.CabinetInitResult
import com.shuwei.intelligent.shelves.model.CleanPackageOption
import com.shuwei.intelligent.shelves.model.MealPackageOption
import com.shuwei.intelligent.shelves.model.PageOptionsReq
import com.shuwei.intelligent.shelves.model.SyncBody
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Query
import retrofit2.http.Url
interface ApiServiceV2 {
/**
* 4.1 货柜初始化
* 设备启动、Token 刷新后、首页重试时调用
*/
@GET
suspend fun initCabinet(
@Url url: String = UrlConfig.CABINET_INIT,
@Query("cabinetId") cabinetId: String
): RespData<CabinetInitResult>
/**
* 4.2.1 净菜包下拉列表(itemType=1
* POST + JSON Body 分页请求,data 直接为列表数组
*/
@POST
suspend fun getCleanPackageOptions(
@Url url: String = UrlConfig.CLEAN_PACKAGE_OPTIONS,
@Body body: PageOptionsReq
): RespData<List<CleanPackageOption>>
/**
* 4.2.2 餐品净菜包下拉列表(itemType=2)
* POST + JSON Body 分页请求,data 直接为列表数组
*/
@POST
suspend fun getMealPackageOptions(
@Url url: String = UrlConfig.MEAL_PACKAGE_OPTIONS,
@Body body: PageOptionsReq
): RespData<List<MealPackageOption>>
/**
* 4.3 全量同步整柜数据(定时上报)
* 警告:body.goodsList 为 null 或空列表时,后端会软删除整柜所有格口数据
*/
@POST
suspend fun syncCabinetData(
@Url url: String = UrlConfig.CABINET_RECORD_SYNC,
@Body body: SyncBody
): RespData<Any?>
}
@@ -0,0 +1,21 @@
package com.shuwei.intelligent.shelves.net
import com.shuwei.intelligent.shelves.App
import okhttp3.Interceptor
import okhttp3.Response
/**
* v1.1 接口专用拦截器
* /nutrition/neglect/ 路径无需鉴权,仅注入基础请求头
*/
class CabinetRequestInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request().newBuilder()
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.header("X-DEVICE-CODE", App.deviceId)
.header("authorization", "57ee87183f2a4fa59683ec9ef41c8f5d")
.build()
return chain.proceed(request)
}
}
@@ -4,6 +4,8 @@ import android.annotation.SuppressLint
import android.util.Log
import com.shuwei.intelligent.shelves.BuildConfig
import com.shuwei.intelligent.shelves.GlobalData
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
@@ -17,14 +19,35 @@ import java.security.SecureRandom
import java.security.cert.X509Certificate
import kotlin.apply
/** 动态域名拦截器:将 Retrofit 请求的 baseUrl 实时替换为 [GlobalData.appBaseUrl] */
class DynamicBaseUrlInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val newUrl = GlobalData.appBaseUrl.toHttpUrlOrNull() ?: return chain.proceed(request)
val url = request.url.newBuilder()
.scheme(newUrl.scheme)
.host(newUrl.host)
.port(newUrl.port)
.build()
return chain.proceed(request.newBuilder().url(url).build())
}
}
val apiService: ApiService = Retrofit.Builder()
// .baseUrl(UrlConfig.BASE_URL)
.baseUrl(GlobalData.appBaseUrl)
.baseUrl("http://placeholder/")
.client(HttpManager.instance.client)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiService::class.java)
/** v1.1 接口专用实例,使用轻量拦截器,不携带旧鉴权 Token */
val apiServiceV2: ApiServiceV2 = Retrofit.Builder()
.baseUrl("http://placeholder/")
.client(HttpManager.instance.cabinetClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiServiceV2::class.java)
class HttpManager private constructor() {
val client: OkHttpClient by lazy {
OkHttpClient.Builder()
@@ -35,6 +58,7 @@ class HttpManager private constructor() {
sslSocketFactory(createSSLSocketFactory(), TrustAllCerts())
hostnameVerifier { _, _ -> true }
// addInterceptor(LoggingInterceptor())
addInterceptor(DynamicBaseUrlInterceptor())
addNetworkInterceptor(HttpLoggingInterceptor(logger = {
Log.d("HttpManager","okhttp logger ==>${it}")
}).apply {
@@ -56,6 +80,27 @@ class HttpManager private constructor() {
.build()
}
/** v1.1 接口专用 OkHttpClient,仅注入 Content-Type、Accept、X-DEVICE-CODE */
val cabinetClient: OkHttpClient by lazy {
OkHttpClient.Builder()
.apply {
connectTimeout(30, TimeUnit.SECONDS)
readTimeout(30, TimeUnit.SECONDS)
writeTimeout(30, TimeUnit.SECONDS)
sslSocketFactory(createSSLSocketFactory(), TrustAllCerts())
hostnameVerifier { _, _ -> true }
addInterceptor(DynamicBaseUrlInterceptor())
addNetworkInterceptor(HttpLoggingInterceptor(logger = {
Log.d("HttpManager", "v2 okhttp ==>${it}")
}).apply {
level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY
else HttpLoggingInterceptor.Level.NONE
})
addInterceptor(CabinetRequestInterceptor())
}
.build()
}
companion object {
val instance by lazy { HttpManager() }
}
@@ -30,53 +30,53 @@ class NetViewModel : ViewModel() {
private val _updateOverdueStateUiState = MutableStateFlow<UiState>(UiState.Initial)
val updateOverdueStateUiState: StateFlow<UiState> = _updateOverdueStateUiState
fun getDeviceToken(deviceId: String) {
viewModelScope.launch {
_getDeviceTokenUiState.value = UiState.Loading
runCatching {
val response = apiService.getDeviceToken(qrcodeId = deviceId)
if (response.isSuccess()) {
_getDeviceTokenUiState.value = UiState.Success(response)
} else {
_getDeviceTokenUiState.value = UiState.Error(response.msg ?: "请求失败")
}
}.onFailure {
_getDeviceTokenUiState.value = UiState.Error(it.message ?: "请求异常")
}
}
}
// fun getDeviceToken(deviceId: String) {
// viewModelScope.launch {
// _getDeviceTokenUiState.value = UiState.Loading
// runCatching {
// val response = apiService.getDeviceToken(qrcodeId = deviceId)
// if (response.isSuccess()) {
// _getDeviceTokenUiState.value = UiState.Success(response)
// } else {
// _getDeviceTokenUiState.value = UiState.Error(response.msg ?: "请求失败")
// }
// }.onFailure {
// _getDeviceTokenUiState.value = UiState.Error(it.message ?: "请求异常")
// }
// }
// }
fun getDeviceConfig(deviceId: String, deviceToken: String) {
viewModelScope.launch {
_getDeviceConfigUiState.value = UiState.Loading
runCatching {
val response = apiService.getDeviceInfo(equipmentCode = deviceId)
if (response.isSuccess()) {
_getDeviceConfigUiState.value = UiState.Success(response)
} else {
_getDeviceConfigUiState.value = UiState.Error(response.msg ?: "请求失败")
}
}.onFailure {
_getDeviceConfigUiState.value = UiState.Error(it.message ?: "请求异常")
}
}
}
// fun getDeviceConfig(deviceId: String, deviceToken: String) {
// viewModelScope.launch {
// _getDeviceConfigUiState.value = UiState.Loading
// runCatching {
// val response = apiService.getDeviceInfo(equipmentCode = deviceId)
// if (response.isSuccess()) {
// _getDeviceConfigUiState.value = UiState.Success(response)
// } else {
// _getDeviceConfigUiState.value = UiState.Error(response.msg ?: "请求失败")
// }
// }.onFailure {
// _getDeviceConfigUiState.value = UiState.Error(it.message ?: "请求异常")
// }
// }
// }
fun getAccessToken(deviceId: String) {
viewModelScope.launch {
_getAccessTokenUiState.value = UiState.Loading
runCatching {
val response = apiService.getAccessToken(qrcodeId = deviceId)
if (response.isSuccess()) {
_getAccessTokenUiState.value = UiState.Success(response)
} else {
_getAccessTokenUiState.value = UiState.Error(response.msg ?: "请求失败")
}
}.onFailure {
_getAccessTokenUiState.value = UiState.Error(it.message ?: "请求异常")
}
}
}
// fun getAccessToken(deviceId: String) {
// viewModelScope.launch {
// _getAccessTokenUiState.value = UiState.Loading
// runCatching {
// val response = apiService.getAccessToken(qrcodeId = deviceId)
// if (response.isSuccess()) {
// _getAccessTokenUiState.value = UiState.Success(response)
// } else {
// _getAccessTokenUiState.value = UiState.Error(response.msg ?: "请求失败")
// }
// }.onFailure {
// _getAccessTokenUiState.value = UiState.Error(it.message ?: "请求异常")
// }
// }
// }
fun getShelfList(deviceId: String) {
viewModelScope.launch {
@@ -0,0 +1,104 @@
package com.shuwei.intelligent.shelves.net
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.shuwei.intelligent.shelves.model.CabinetInitResult
import com.shuwei.intelligent.shelves.model.CleanPackageOption
import com.shuwei.intelligent.shelves.model.MealPackageOption
import com.shuwei.intelligent.shelves.model.PageOptionsReq
import com.shuwei.intelligent.shelves.model.SyncBody
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
class NetViewModelV2 : ViewModel() {
private val _initCabinetUiState = MutableStateFlow<UiState>(UiState.Initial)
val initCabinetUiState: StateFlow<UiState> = _initCabinetUiState
private val _cleanPackageOptionsUiState = MutableStateFlow<UiState>(UiState.Initial)
val cleanPackageOptionsUiState: StateFlow<UiState> = _cleanPackageOptionsUiState
private val _mealPackageOptionsUiState = MutableStateFlow<UiState>(UiState.Initial)
val mealPackageOptionsUiState: StateFlow<UiState> = _mealPackageOptionsUiState
private val _syncCabinetUiState = MutableStateFlow<UiState>(UiState.Initial)
val syncCabinetUiState: StateFlow<UiState> = _syncCabinetUiState
/** 4.1 货柜初始化 */
fun initCabinet(cabinetId: String) {
viewModelScope.launch {
_initCabinetUiState.value = UiState.Loading
runCatching {
val response = apiServiceV2.initCabinet(cabinetId = cabinetId)
if (response.isSuccess()) {
_initCabinetUiState.value = UiState.Success(response)
} else {
_initCabinetUiState.value = UiState.Error(response.msg ?: "请求失败")
}
}.onFailure {
_initCabinetUiState.value = UiState.Error(it.message ?: "请求异常")
}
}
}
/** 4.2.1 净菜包下拉列表(itemType=1 */
fun getCleanPackageOptions(keyword: String? = null) {
viewModelScope.launch {
_cleanPackageOptionsUiState.value = UiState.Loading
runCatching {
val response = apiServiceV2.getCleanPackageOptions(
body = PageOptionsReq(pageNum = 1, pageSize = 50, keyword = keyword)
)
if (response.isSuccess()) {
_cleanPackageOptionsUiState.value = UiState.Success(response)
} else {
_cleanPackageOptionsUiState.value = UiState.Error(response.msg ?: "请求失败")
}
}.onFailure {
_cleanPackageOptionsUiState.value = UiState.Error(it.message ?: "请求异常")
}
}
}
/** 4.2.2 餐品净菜包下拉列表(itemType=2 */
fun getMealPackageOptions(keyword: String? = null) {
viewModelScope.launch {
_mealPackageOptionsUiState.value = UiState.Loading
runCatching {
val response = apiServiceV2.getMealPackageOptions(
body = PageOptionsReq(pageNum = 1, pageSize = 50, keyword = keyword)
)
if (response.isSuccess()) {
_mealPackageOptionsUiState.value = UiState.Success(response)
} else {
_mealPackageOptionsUiState.value = UiState.Error(response.msg ?: "请求失败")
}
}.onFailure {
_mealPackageOptionsUiState.value = UiState.Error(it.message ?: "请求异常")
}
}
}
/**
* 4.3 全量同步整柜数据
* 警告:body.goodsList 为 null 或空列表时,后端会软删除整柜所有格口数据
*/
fun syncCabinetData(body: SyncBody) {
viewModelScope.launch {
_syncCabinetUiState.value = UiState.Loading
runCatching {
val response = apiServiceV2.syncCabinetData(body = body)
if (response.isSuccess()) {
_syncCabinetUiState.value = UiState.Success(response)
} else {
_syncCabinetUiState.value = UiState.Error(response.msg ?: "请求失败")
}
}.onFailure {
Log.e("NetViewModelV2", "syncCabinetData 异常:${it.message}")
_syncCabinetUiState.value = UiState.Error(it.message ?: "请求异常")
}
}
}
}
@@ -27,5 +27,11 @@ object UrlConfig {
var SAVE_SHELF_GOODS_LIST =
"${GlobalData.appBaseUrl}/terminal/neglect/smartShelves/app/smartShelves/app/saveShelvesGoodsList"
// v1.1 新接口
var CABINET_INIT = "${GlobalData.appBaseUrl}/nutrition/neglect/cabinet/init"
var CLEAN_PACKAGE_OPTIONS = "${GlobalData.appBaseUrl}/nutrition/neglect/cabinet/clean-package-options"
var MEAL_PACKAGE_OPTIONS = "${GlobalData.appBaseUrl}/nutrition/neglect/cabinet/meal-package-options"
var CABINET_RECORD_SYNC = "${GlobalData.appBaseUrl}/nutrition/neglect/cabinet/record/sync"
}
@@ -144,7 +144,9 @@ object ProtocolConstants {
//原410货柜
"7a991439-3a12-3ef7-809b-c0258b839473",
//原1楼餐厅货柜
"4787e213-90ab-3e32-88e0-ac271a937751"
"4787e213-90ab-3e32-88e0-ac271a937751",
// TODO: 测试设备----------
"2987f0c5-5754-33e9-b00a-251db5e2e55f"
)
/**
@@ -39,11 +39,19 @@ class CrashHandler private constructor(private val context: Context) :
synchronized(CrashHandler::class.java) {
if (instance == null) {
instance = CrashHandler(context.applicationContext)
// 初始化时清理过期的崩溃日志(默认保留7天)
instance?.cleanupOldCrashReports()
}
}
}
}
/**
* 获取单例实例
* @return 已通过 init() 初始化时返回实例,否则返回 null
*/
fun getInstance(): CrashHandler? = instance
//fun getCrashReportFiles(context: Context): Array<File> {
// val crashDir = getCrashDir()
// return if (crashDir.exists() && crashDir.isDirectory) {
@@ -222,16 +230,21 @@ class CrashHandler private constructor(private val context: Context) :
* 清理旧的崩溃日志
*/
fun cleanupOldCrashReports(maxAgeDays: Int = 7) {
val crashDir = getCrashDir()
if (!crashDir.exists() || !crashDir.isDirectory) return
// 该方法会在 Application 启动及定时任务中调用,异常必须内部消化,避免影响主流程
try {
val crashDir = getCrashDir()
if (!crashDir.exists() || !crashDir.isDirectory) return
val now = System.currentTimeMillis()
val maxAgeMillis = maxAgeDays * 24 * 60 * 60 * 1000L
val now = System.currentTimeMillis()
val maxAgeMillis = maxAgeDays * 24 * 60 * 60 * 1000L
crashDir.listFiles()?.forEach { file ->
if (file.lastModified() < now - maxAgeMillis) {
file.delete()
crashDir.listFiles()?.forEach { file ->
if (file.lastModified() < now - maxAgeMillis) {
file.delete()
}
}
} catch (e: Exception) {
Log.e(TAG, "清理崩溃日志失败", e)
}
}
@@ -17,8 +17,37 @@ class FileLogger(private val context: Context) {
private const val LOG_DIR = "logs"
private const val FILE_PREFIX = "log_"
private const val FILE_EXTENSION = ".txt"
/** 日志保留天数,超过该天数的日志文件在应用启动时清理 */
private const val RETENTION_DAYS = 7L
/** 单个日志文件大小上限(100MB),超过后当天不再写入,防止异常刷屏撑爆存储 */
private const val MAX_LOG_FILE_SIZE = 100L * 1024 * 1024
private val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
private val timeFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
/**
* 清理过期的日志文件,保留最近 retentionDays 天
* 按文件最后修改时间判断,最近仍在写入的文件不会被误删
* 建议在 Application 启动时调用一次
* @param context 上下文,用于定位 filesDir
* @param retentionDays 日志保留天数
*/
fun cleanupExpiredLogs(context: Context, retentionDays: Long = RETENTION_DAYS) {
try {
val cutoff = System.currentTimeMillis() - retentionDays * 24 * 60 * 60 * 1000L
context.filesDir.listFiles { _, name ->
name.startsWith(FILE_PREFIX) && name.endsWith(FILE_EXTENSION)
}?.forEach { file ->
if (file.lastModified() < cutoff) {
file.delete()
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
/**
@@ -45,6 +74,7 @@ class FileLogger(private val context: Context) {
/**
* 写入文件
* 单文件超过大小上限(100MB)时当天不再写入,防止异常刷屏撑爆存储
*/
private fun writeToFile(logEntry: String) {
// val path = File(context.filesDir, LOG_DIR)
@@ -52,10 +82,10 @@ class FileLogger(private val context: Context) {
// path.mkdirs()
// }
val fileName = getCurrentDateFileName()
// val file = File(path, fileName)
// if (file.exists().not()) {
// file.createNewFile()
// }
val file = File(context.filesDir, fileName)
if (file.exists() && file.length() > MAX_LOG_FILE_SIZE) {
return
}
val output = context.openFileOutput(fileName, Context.MODE_APPEND)
val writer = BufferedWriter(OutputStreamWriter(output))
@@ -5,6 +5,7 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.shuwei.intelligent.shelves.App
import com.shuwei.intelligent.shelves.model.ShelfModel
import com.shuwei.intelligent.shelves.model.ShelfModelV2
import com.shuwei.intelligent.shelves.serial.ProtocolConstants
object GridLayoutTool {
@@ -29,6 +30,8 @@ object GridLayoutTool {
}
}
fun getLayoutManagerV2(context: Context, columns: Int) = GridLayoutManager(context, columns, GridLayoutManager.VERTICAL, false)
/**
* 初始化list显示顺序
*/
@@ -44,6 +47,28 @@ object GridLayoutTool {
}
}
fun initListSortV2(rows:Int, columns:Int, source: List<ShelfModelV2>): List<ShelfModelV2> {
val map = source.associateBy { it.slotNo?.toIntOrNull()?:0 }
if (rows != 5) return source
return when (columns) {
3 -> {
// 3列顺序
ProtocolConstants.device3ColumnsOrder.mapNotNull { map[it] }
}
2 -> {
// 2列顺序
ProtocolConstants.device2ColumnsOrder.mapNotNull { map[it] }
}
else -> {
//没有则默认
source
}
}
}
fun submitListSortV2(source: List<ShelfModelV2>): List<ShelfModelV2> {
return source.sortedBy { it.slotNo?.toIntOrNull()?:0 }
}
/**
* 提交接口list顺序
*/
@@ -0,0 +1,311 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:background="@color/bg_page">
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="376dp"
android:layout_marginStart="24dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="24dp"
app:cardBackgroundColor="@color/white"
app:cardCornerRadius="12dp"
app:cardElevation="0dp">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:gravity="center_horizontal"
android:orientation="vertical">
<TextView
android:id="@+id/tvShelfName"
android:layout_width="wrap_content"
android:layout_height="60dp"
android:layout_marginTop="24dp"
android:fontFamily="sans-serif-medium"
android:gravity="center"
android:textColor="@color/bg_page"
android:textSize="30sp"
tools:text="货架 - 03" />
<Space
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1" />
<TextView
android:id="@+id/tvFoodWeight"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:includeFontPadding="false"
android:maxLines="1"
android:textColor="@color/food_weight_orange"
android:textSize="60sp"
android:textStyle="bold"
tools:text="1506克" />
<Space
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1" />
<TextView
android:id="@+id/tvFoodName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/food_name_black"
android:textSize="36sp"
android:textStyle="bold"
tools:text="金针菇" />
<Space
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnClearZero"
android:layout_width="120dp"
android:layout_height="60dp"
android:layout_gravity="top|end"
android:layout_marginTop="24dp"
android:layout_marginEnd="24dp"
android:backgroundTint="@color/bg_page"
android:ellipsize="end"
android:fontFamily="sans-serif-medium"
android:insetTop="0dp"
android:insetBottom="0dp"
android:maxLines="1"
android:text="清零"
android:textColor="@color/white"
android:textSize="30sp"
app:cornerRadius="12dp"
app:elevation="0dp"
tools:ignore="HardcodedText" />
<ImageView
android:id="@+id/ivBack"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_gravity="top|start"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
android:src="@drawable/ic_back_512"
tools:ignore="ContentDescription" />
</FrameLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="24dp"
app:cardBackgroundColor="@color/white"
app:cardCornerRadius="12dp"
app:cardElevation="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- 类型单选:净菜包 / 餐品净菜包 -->
<com.google.android.material.chip.ChipGroup
android:id="@+id/chipGroupType"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="48dp"
android:layout_marginTop="36dp"
android:layout_marginEnd="48dp"
app:selectionRequired="true"
android:layout_gravity="center_horizontal"
app:singleSelection="true">
<com.google.android.material.chip.Chip
android:id="@+id/chipClean"
style="@style/Widget.MaterialComponents.Chip.Choice"
android:layout_width="180dp"
android:layout_height="64dp"
android:checked="true"
android:textAlignment="center"
android:fontFamily="sans-serif-medium"
android:text="净菜包"
android:textSize="24sp"
app:chipMinHeight="64dp"
tools:ignore="HardcodedText" />
<com.google.android.material.chip.Chip
android:id="@+id/chipMeal"
style="@style/Widget.MaterialComponents.Chip.Choice"
android:layout_width="180dp"
android:layout_height="64dp"
android:layout_marginStart="50dp"
android:textAlignment="center"
android:fontFamily="sans-serif-medium"
android:text="餐品净菜包"
android:textSize="24sp"
app:chipMinHeight="64dp"
tools:ignore="HardcodedText" />
</com.google.android.material.chip.ChipGroup>
<!-- 搜索框 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_marginStart="48dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="48dp"
android:background="@drawable/shape_search"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:src="@drawable/ic_search_food"
tools:ignore="ContentDescription" />
<EditText
android:id="@+id/etInputFood"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginEnd="24dp"
android:background="@null"
android:fontFamily="sans-serif-medium"
android:hint="输入名称搜索"
android:imeOptions="actionSearch"
android:inputType="text"
android:maxLines="1"
android:paddingStart="1dp"
android:paddingEnd="1dp"
android:text=""
android:textColor="@color/food_name_black"
android:textColorHint="#B4BEC8"
android:textSize="30sp"
tools:ignore="Autofill,HardcodedText,TextFields" />
</LinearLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="24dp"
android:layout_weight="1">
<com.scwang.smart.refresh.layout.SmartRefreshLayout
android:id="@+id/refreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.scwang.smart.refresh.header.ClassicsHeader
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvSearch"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:overScrollMode="never"
android:paddingStart="36dp"
android:paddingTop="12dp"
android:paddingEnd="36dp"
android:paddingBottom="12dp"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="2"
tools:itemCount="10"
tools:listitem="@layout/list_item_search" />
<com.scwang.smart.refresh.footer.ClassicsFooter
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.scwang.smart.refresh.layout.SmartRefreshLayout>
<include
android:id="@+id/include"
layout="@layout/layout_empty_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnClearEmpty"
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_marginStart="48dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="24dp"
android:layout_weight="1"
android:backgroundTint="@color/bg_page"
android:ellipsize="end"
android:insetTop="0dp"
android:insetBottom="0dp"
android:maxLines="1"
android:text="清空"
android:textColor="@color/white"
android:textSize="30sp"
android:textStyle="bold"
app:cornerRadius="12dp"
app:elevation="0dp"
tools:ignore="HardcodedText" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnConfirm"
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="48dp"
android:layout_marginBottom="24dp"
android:layout_weight="1"
android:backgroundTint="@color/bg_page"
android:ellipsize="end"
android:insetTop="0dp"
android:insetBottom="0dp"
android:maxLines="1"
android:text="确定"
android:textColor="@color/white"
android:textSize="30sp"
android:textStyle="bold"
app:cornerRadius="12dp"
app:elevation="0dp"
tools:ignore="HardcodedText" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
@@ -38,7 +38,7 @@
android:layout_height="72dp"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:text="测试环境"
android:text="本地环境"
android:textColor="#ff141428"
android:textSize="28sp" />
@@ -48,7 +48,7 @@
android:layout_height="72dp"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:text="UAT 环境"
android:text="测试环境"
android:textColor="#ff141428"
android:textSize="28sp" />
+2 -2
View File
@@ -3,8 +3,8 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/layoutCard"
android:layout_width="match_parent"
android:layout_height="216dp"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="12dp"
app:cardBackgroundColor="@color/bg_card_blue"
app:cardCornerRadius="12dp"