Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4720b103a | ||
|
|
9db8996d44 | ||
|
|
5ffdb3ea64 |
@@ -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
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ object GlobalData {
|
||||
// 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://api.dm.yixiong-tech.com:8443"
|
||||
const val PROD_BASE_URL = "https://platform-api.uat.shuziweidao.com"
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,12 @@ class FileLogger(context: Context) {
|
||||
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 logChannel = Channel<RawEntry>(capacity = Channel.UNLIMITED)
|
||||
private val writerScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
@@ -31,6 +37,9 @@ class FileLogger(context: Context) {
|
||||
for (entry in logChannel) {
|
||||
try {
|
||||
val fileName = "${FILE_PREFIX}${dateFormat.format(Date())}${FILE_EXTENSION}"
|
||||
// 单文件超过大小上限时丢弃本条日志,防止异常刷屏撑爆存储(单协程检查,无并发问题)
|
||||
val file = File(entry.appContext.filesDir, fileName)
|
||||
if (file.exists() && file.length() > MAX_LOG_FILE_SIZE) continue
|
||||
val timestamp = timeFormat.format(Date())
|
||||
val logLine = "[$timestamp] [${entry.level}] ${entry.message}\n"
|
||||
val output = entry.appContext.openFileOutput(
|
||||
@@ -49,6 +58,28 @@ class FileLogger(context: Context) {
|
||||
val message: String,
|
||||
val level: LogLevel
|
||||
)
|
||||
|
||||
/**
|
||||
* 清理过期的日志文件,保留最近 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 持有 ApplicationContext 避免 Activity 泄漏 */
|
||||
|
||||
@@ -11,6 +11,10 @@ import androidx.activity.addCallback
|
||||
import androidx.activity.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator
|
||||
import com.google.android.flexbox.FlexDirection
|
||||
import com.google.android.flexbox.FlexWrap
|
||||
import com.google.android.flexbox.FlexboxLayoutManager
|
||||
import com.scwang.smart.refresh.layout.constant.RefreshState
|
||||
import com.shuwei.intelligent.shelves.App
|
||||
import com.shuwei.intelligent.shelves.R
|
||||
import com.shuwei.intelligent.shelves.base.BaseActivity
|
||||
@@ -23,9 +27,6 @@ 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
|
||||
@@ -129,13 +130,50 @@ class HomeV3Activity : BaseActivity() {
|
||||
saveGoodsTask()
|
||||
overdueTask()
|
||||
|
||||
initRefreshLayout()
|
||||
|
||||
refreshSlots()
|
||||
}
|
||||
|
||||
/** 配置格口列表下拉刷新 */
|
||||
private fun initRefreshLayout() {
|
||||
binding.refreshLayout.run {
|
||||
setEnableRefresh(true)
|
||||
setEnableLoadMore(false)
|
||||
setOnRefreshListener { refreshSlots(fromPull = true) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取格口列表并刷新 UI。
|
||||
* @param fromPull true 表示由下拉刷新触发:不弹 Loading,失败时保留现有格口数据仅提示
|
||||
*/
|
||||
private fun refreshSlots(fromPull: Boolean = false) {
|
||||
viewModel.slots(
|
||||
onLoading = { showProgress() },
|
||||
onSuccess = { record -> updateUI(record) },
|
||||
onError = { msg -> showError(msg) }
|
||||
onLoading = { if (!fromPull) showProgress() },
|
||||
onSuccess = { record ->
|
||||
if (fromPull) finishRefresh()
|
||||
updateUI(record)
|
||||
},
|
||||
onError = { msg ->
|
||||
if (fromPull) {
|
||||
finishRefresh()
|
||||
// 列表已有数据时刷新失败仅提示,保留现有格口展示;无数据则走空视图重试
|
||||
if (list.isEmpty()) showError(msg) else toast(msg)
|
||||
} else {
|
||||
showError(msg)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/** 结束下拉刷新动画 */
|
||||
private fun finishRefresh() {
|
||||
binding.refreshLayout.let {
|
||||
if (it.state == RefreshState.Refreshing) it.finishRefresh(500)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showProgress() {
|
||||
Loading.show(this)
|
||||
}
|
||||
@@ -376,13 +414,7 @@ class HomeV3Activity : BaseActivity() {
|
||||
shelfAdapter.notifyDataSetChanged()
|
||||
binding.include?.let {
|
||||
it.root.visible()
|
||||
it.root.setOnClickListener {
|
||||
viewModel.slots(
|
||||
onLoading = { showProgress() },
|
||||
onSuccess = { record -> updateUI(record) },
|
||||
onError = { msg -> showError(msg) }
|
||||
)
|
||||
}
|
||||
it.root.setOnClickListener { refreshSlots() }
|
||||
it.ivEmptyIcon.setImageResource(R.drawable.ic_empty_white)
|
||||
it.tvEmptyContent.setTextColor(Color.WHITE)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.sw.scalefusion.shelf.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.core.content.ContextCompat
|
||||
@@ -26,13 +26,23 @@ class PendingInboundAdapter(list: MutableList<FoodItem>) :
|
||||
override fun onBindViewHolder(holder: VH, position: Int, item: FoodItem?) {
|
||||
item ?: return
|
||||
val isSelected = item.isSelected
|
||||
holder.binding.btnFoodName.run {
|
||||
val binding = holder.binding
|
||||
// 名称与编码:选中蓝字,未选中灰字
|
||||
val nameColor = getColor(if (isSelected) R.color.shelf_name_blue else R.color.black999)
|
||||
binding.tvFoodName.run {
|
||||
text = item.materName
|
||||
setTextColor(getColor(if (isSelected) R.color.shelf_name_blue else R.color.black999))
|
||||
strokeColor = ColorStateList.valueOf(
|
||||
(if (isSelected) "#4969F5" else "#E6E6E6").toColorInt()
|
||||
)
|
||||
setTextColor(nameColor)
|
||||
}
|
||||
binding.tvMaterCode.run {
|
||||
text = item.materCode
|
||||
setTextColor(nameColor)
|
||||
}
|
||||
// 选中蓝边框,未选中灰边框
|
||||
binding.cardFood.strokeColor =
|
||||
(if (isSelected) "#4969F5" else "#E6E6E6").toColorInt()
|
||||
// 仅今日已验收的食材显示右上角绿色圆点
|
||||
binding.viewAcceptedDot.visibility =
|
||||
if (item.todayAccepted == true) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
private fun getColor(@ColorRes id: Int) = ContextCompat.getColor(context, id)
|
||||
|
||||
@@ -6,6 +6,10 @@ import java.util.Date
|
||||
data class FoodItem(
|
||||
var materId:Long? = null,
|
||||
var materName:String? = null,
|
||||
/** 食材编码(一级分类+二级分类+自编码,全局唯一) */
|
||||
var materCode:String? = null,
|
||||
/** 今日是否在验收秤有验收记录 */
|
||||
var todayAccepted:Boolean? = null,
|
||||
var materUrl:String? = null,
|
||||
var vegTypeList:List<FoodType>? = null,
|
||||
var isSelected:Boolean = false,
|
||||
|
||||
@@ -6,16 +6,30 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:background="@color/bg_page">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvShelf"
|
||||
<!-- 格口列表支持下拉刷新:重新拉取 /init 格口数据 -->
|
||||
<com.scwang.smart.refresh.layout.SmartRefreshLayout
|
||||
android:id="@+id/refreshLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||
app:spanCount="2"
|
||||
android:overScrollMode="never"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
tools:listitem="@layout/list_item_shelf_v3"/>
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.scwang.smart.refresh.header.ClassicsHeader
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:srlAccentColor="@color/white"
|
||||
app:srlPrimaryColor="@color/bg_page"
|
||||
app:srlEnableLastTime="false" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvShelf"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||
app:spanCount="2"
|
||||
android:overScrollMode="never"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
tools:listitem="@layout/list_item_shelf_v3"/>
|
||||
</com.scwang.smart.refresh.layout.SmartRefreshLayout>
|
||||
|
||||
<include
|
||||
android:id="@+id/include"
|
||||
|
||||
@@ -1,48 +1,60 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"-->
|
||||
<!-- xmlns:tools="http://schemas.android.com/tools"-->
|
||||
<!-- android:layout_width="match_parent"-->
|
||||
<!-- android:layout_height="80dp"-->
|
||||
<!-- android:layout_margin="12dp">-->
|
||||
|
||||
<!-- <TextView-->
|
||||
<!-- android:id="@+id/btnFoodName"-->
|
||||
<!-- android:layout_width="wrap_content"-->
|
||||
<!-- android:layout_height="wrap_content"-->
|
||||
<!-- android:layout_gravity="center"-->
|
||||
<!-- android:ellipsize="end"-->
|
||||
<!-- android:gravity="center"-->
|
||||
<!-- android:maxLines="1"-->
|
||||
<!-- android:paddingStart="5dp"-->
|
||||
<!-- android:paddingEnd="5dp"-->
|
||||
<!-- android:fontFamily="sans-serif-medium"-->
|
||||
<!-- android:textColor="@color/black999"-->
|
||||
<!-- android:textSize="30sp"-->
|
||||
<!-- tools:text="土豆丝" />-->
|
||||
<!--</FrameLayout>-->
|
||||
<com.google.android.material.button.MaterialButton xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<!-- 搜索食材列表 item:食材名 + 编码副行,今日已验收时右上角显示绿色圆点 -->
|
||||
<com.google.android.material.card.MaterialCardView 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:id="@+id/btnFoodName"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:id="@+id/cardFood"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="80dp"
|
||||
android:layout_height="96dp"
|
||||
android:layout_margin="12dp"
|
||||
android:ellipsize="end"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:gravity="center"
|
||||
android:insetTop="0dp"
|
||||
android:insetBottom="0dp"
|
||||
android:maxLines="1"
|
||||
app:paddingStart="5dp"
|
||||
app:paddingEnd="5dp"
|
||||
android:insetRight="0dp"
|
||||
android:insetLeft="0dp"
|
||||
android:paddingHorizontal="8dp"
|
||||
android:textColor="@color/black999"
|
||||
android:textSize="30sp"
|
||||
app:cornerRadius="12dp"
|
||||
app:strokeWidth="2dp"
|
||||
tools:text="土豆丝土豆丝土豆丝土豆丝土豆丝土豆丝"
|
||||
/>
|
||||
app:cardBackgroundColor="@color/white"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="0dp"
|
||||
app:strokeColor="#E6E6E6"
|
||||
app:strokeWidth="2dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="5dp"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingEnd="5dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvFoodName"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:gravity="center"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/black999"
|
||||
android:textSize="30sp"
|
||||
tools:text="土豆丝土豆丝土豆丝" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvMaterCode"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/black999"
|
||||
android:textSize="16sp"
|
||||
tools:text="0102003" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 今日已验收标识圆点,默认隐藏,由 Adapter 控制显示 -->
|
||||
<View
|
||||
android:id="@+id/viewAcceptedDot"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_gravity="top|end"
|
||||
android:layout_margin="10dp"
|
||||
android:background="@drawable/shape_accepted_dot"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<color name="food_name_black">#0A143C</color>
|
||||
<color name="food_weight_blue">#C8C8FF</color>
|
||||
<color name="food_weight_orange">#FF6400</color>
|
||||
<color name="accepted_green">#34C77B</color>
|
||||
<color name="black999">#999999</color>
|
||||
<color name="black666">#666666</color>
|
||||
<color name="white_f6">#F6F6F6</color>
|
||||
|
||||
Reference in New Issue
Block a user