feat: 考勤功能补全

1. 首页加载时检查工作状态(fetchWorkState)
2. MQTT type=4 工作状态推送处理(上班→亮屏,下班→关NFC+熄屏)
3. PunchStatus 增加打卡时间字段(onPunchTime/offPunchTime)
4. 考勤页显示打卡时间("已上班 07:02"/"已下班 17:05")
5. 首页注入 PunchApi/ScreenController/NfcController

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
dongliang
2026-04-28 19:30:29 +09:30
parent 967b001d46
commit b59fee4bb7
3 changed files with 72 additions and 5 deletions

View File

@@ -11,7 +11,11 @@ data class PunchStatus(
/** 上班打卡状态0=未上班, 1=已上班 */ /** 上班打卡状态0=未上班, 1=已上班 */
@SerializedName("onPunchState") val onPunchState: Int = 0, @SerializedName("onPunchState") val onPunchState: Int = 0,
/** 下班打卡状态1=已下班, 其他=未下班 */ /** 下班打卡状态1=已下班, 其他=未下班 */
@SerializedName("offPunchState") val offPunchState: Int = 0 @SerializedName("offPunchState") val offPunchState: Int = 0,
/** 上班打卡时间API 可能返回,待验证字段名) */
@SerializedName("onPunchTime") val onPunchTime: String? = null,
/** 下班打卡时间 */
@SerializedName("offPunchTime") val offPunchTime: String? = null
) { ) {
/** 是否已上班 */ /** 是否已上班 */
val isOnDuty: Boolean get() = onPunchState == 1 val isOnDuty: Boolean get() = onPunchState == 1

View File

@@ -15,9 +15,13 @@ import com.xiaoqu.watch.data.prefs.UserPrefs
import com.xiaoqu.watch.databinding.FragmentHomeBinding import com.xiaoqu.watch.databinding.FragmentHomeBinding
import com.xiaoqu.watch.event.AppEvent import com.xiaoqu.watch.event.AppEvent
import com.xiaoqu.watch.event.EventBus import com.xiaoqu.watch.event.EventBus
import com.xiaoqu.watch.device.nfc.NfcController
import com.xiaoqu.watch.device.screen.ScreenController
import com.xiaoqu.watch.network.ApiResult import com.xiaoqu.watch.network.ApiResult
import com.xiaoqu.watch.network.api.PunchApi
import com.xiaoqu.watch.network.api.TaskApi import com.xiaoqu.watch.network.api.TaskApi
import com.xiaoqu.watch.network.safeApiCall import com.xiaoqu.watch.network.safeApiCall
import org.json.JSONObject
import com.xiaoqu.watch.ui.common.BaseFragment import com.xiaoqu.watch.ui.common.BaseFragment
import com.xiaoqu.watch.ui.widget.StatusBarView import com.xiaoqu.watch.ui.widget.StatusBarView
import com.xiaoqu.watch.util.DateUtil import com.xiaoqu.watch.util.DateUtil
@@ -39,6 +43,9 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
@Inject lateinit var userPrefs: UserPrefs @Inject lateinit var userPrefs: UserPrefs
@Inject lateinit var eventBus: EventBus @Inject lateinit var eventBus: EventBus
@Inject lateinit var taskApi: TaskApi @Inject lateinit var taskApi: TaskApi
@Inject lateinit var punchApi: PunchApi
@Inject lateinit var screenController: ScreenController
@Inject lateinit var nfcController: NfcController
// ===== 固定状态栏(不随 ViewPager 滑动) ===== // ===== 固定状态栏(不随 ViewPager 滑动) =====
private lateinit var statusBar: StatusBarView private lateinit var statusBar: StatusBarView
@@ -93,6 +100,9 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
// 加载任务统计数据 // 加载任务统计数据
fetchStatistics() fetchStatistics()
// 检查当前工作状态(考勤)
fetchWorkState()
// 监听 MQTT 事件 // 监听 MQTT 事件
observeEvents() observeEvents()
@@ -251,9 +261,9 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
findNavController().navigate(R.id.action_home_to_bind) findNavController().navigate(R.id.action_home_to_bind)
} }
4 -> { 4 -> {
// 工作状态变更 // 工作状态变更MQTT type=4/5 推送)
Timber.d("首页: 收到工作状态变更") Timber.d("首页: 收到工作状态变更")
// TODO: 更新考勤状态,控制蓝牙扫描 handleWorkStateChange(event.rawJson)
} }
} }
} }
@@ -273,6 +283,57 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
findNavController().navigate(R.id.action_home_to_taskList, bundle) findNavController().navigate(R.id.action_home_to_taskList, bundle)
} }
// ===== 工作状态(考勤联动) =====
/** 首页加载时检查当前工作状态 */
private fun fetchWorkState() {
viewLifecycleOwner.lifecycleScope.launch {
val result = safeApiCall { punchApi.getAttendance() }
if (result is ApiResult.Success && result.data != null) {
val status = result.data
val isWorking = status.isOnDuty && !status.isOffDuty
Timber.d("首页: 工作状态 isWorking=$isWorking (on=${status.onPunchState}, off=${status.offPunchState})")
// 发送工作状态事件(其他模块可监听)
eventBus.emit(AppEvent.WorkStateChanged(isWorking))
}
}
}
/**
* 处理 MQTT 工作状态变更type=4/5
* 服务端推送上班/下班状态
* v1.2.5: msg.action = 0(下班) / 1(上班)
*/
private fun handleWorkStateChange(rawJson: String) {
try {
val json = JSONObject(rawJson)
val action = json.optInt("action", -1)
val isWorking = action == 1
Timber.d("首页: MQTT 工作状态变更 action=$action isWorking=$isWorking")
// 发送工作状态事件
viewLifecycleOwner.lifecycleScope.launch {
eventBus.emit(AppEvent.WorkStateChanged(isWorking))
}
// 屏幕亮度联动
if (isWorking) {
screenController.turnOn()
} else {
// 下班 → 低耗电关闭NFC + 熄屏
nfcController.stopScan()
if (nfcController.isOpen()) nfcController.close()
screenController.turnOff()
}
// 刷新统计数据
fetchStatistics()
} catch (e: Exception) {
Timber.w(e, "首页: 解析工作状态变更异常")
}
}
/** 设置下拉手势 → 进入考勤打卡页(在状态栏区域检测) */ /** 设置下拉手势 → 进入考勤打卡页(在状态栏区域检测) */
@android.annotation.SuppressLint("ClickableViewAccessibility") @android.annotation.SuppressLint("ClickableViewAccessibility")
private fun setupPullDownGesture() { private fun setupPullDownGesture() {

View File

@@ -125,7 +125,8 @@ class PunchFragment : BaseFragment<FragmentPunchBinding>() {
} }
// 已上班 + 已下班 // 已上班 + 已下班
status.isOnDuty && status.isOffDuty -> { status.isOnDuty && status.isOffDuty -> {
binding.tvPunchStatus.text = "已下班" val timeText = if (!status.offPunchTime.isNullOrEmpty()) "已下班 ${status.offPunchTime}" else "已下班"
binding.tvPunchStatus.text = timeText
binding.tvPunchStatus.setTextColor(requireContext().getColor(R.color.text_secondary)) binding.tvPunchStatus.setTextColor(requireContext().getColor(R.color.text_secondary))
binding.btnPunch.text = "下班打卡" binding.btnPunch.text = "下班打卡"
binding.btnPunch.setBackgroundColor(requireContext().getColor(R.color.primary)) binding.btnPunch.setBackgroundColor(requireContext().getColor(R.color.primary))
@@ -136,7 +137,8 @@ class PunchFragment : BaseFragment<FragmentPunchBinding>() {
} }
// 已上班 + 未下班 // 已上班 + 未下班
status.isOnDuty && !status.isOffDuty -> { status.isOnDuty && !status.isOffDuty -> {
binding.tvPunchStatus.text = "已上班" val timeText = if (!status.onPunchTime.isNullOrEmpty()) "已上班 ${status.onPunchTime}" else "已上班"
binding.tvPunchStatus.text = timeText
binding.tvPunchStatus.setTextColor(requireContext().getColor(R.color.success)) binding.tvPunchStatus.setTextColor(requireContext().getColor(R.color.success))
binding.btnPunch.text = "下班打卡" binding.btnPunch.text = "下班打卡"
binding.btnPunch.setBackgroundColor(requireContext().getColor(R.color.primary)) binding.btnPunch.setBackgroundColor(requireContext().getColor(R.color.primary))