diff --git a/app/src/main/java/com/xiaoqu/watch/ui/punch/PunchFragment.kt b/app/src/main/java/com/xiaoqu/watch/ui/punch/PunchFragment.kt index 16f2deb..7480ff3 100644 --- a/app/src/main/java/com/xiaoqu/watch/ui/punch/PunchFragment.kt +++ b/app/src/main/java/com/xiaoqu/watch/ui/punch/PunchFragment.kt @@ -12,6 +12,8 @@ import com.xiaoqu.watch.data.punch.PunchStatus import com.xiaoqu.watch.databinding.FragmentPunchBinding import com.xiaoqu.watch.device.nfc.NfcController import com.xiaoqu.watch.device.screen.ScreenController +import com.xiaoqu.watch.device.sensor.VibrationController +import com.xiaoqu.watch.device.sensor.VibrationDefaults import com.xiaoqu.watch.event.AppEvent import com.xiaoqu.watch.event.EventBus import com.xiaoqu.watch.network.ApiResult @@ -20,22 +22,19 @@ import com.xiaoqu.watch.network.safeApiCall import com.xiaoqu.watch.ui.common.BaseFragment import com.xiaoqu.watch.ui.widget.QuConfirmDialog import com.xiaoqu.watch.ui.widget.QuTipDialog +import com.xiaoqu.watch.util.DateUtil import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject /** - * 考勤打卡页面(NFC 方式) - * - * 流程: - * 1. 进入页面 → GET myCurrentAttendance → 显示考勤状态 - * 2. 点击「上班打卡」→ 开启 NFC → "请将手表贴近打卡信标" - * 3. NFC 读到卡号 → 上班弹确认 / 下班直接提交 - * 4. POST nfcOnAndOffPunch {nfcId, punchType} - * 5. 成功 → 提示 + 更新工作状态 + 屏幕亮度 - * - * 来源:v1.2.5 punchApis.js nfcOnAndOffPunch + 用户确认 + * 考勤打卡(半屏下拉面板) + * - 点击空白收回 + * - NFC 扫描时按钮变状态,不弹窗 + * - 打卡有语音+振动反馈 */ @AndroidEntryPoint class PunchFragment : BaseFragment() { @@ -43,18 +42,12 @@ class PunchFragment : BaseFragment() { @Inject lateinit var punchApi: PunchApi @Inject lateinit var nfcController: NfcController @Inject lateinit var screenController: ScreenController + @Inject lateinit var vibrationController: VibrationController @Inject lateinit var eventBus: EventBus - /** 当前考勤状态 */ - private var punchStatus: PunchStatus? = null - - /** 当前打卡类型(0=上班, 1=下班) */ private var currentPunchType = 0 - - /** 提示弹窗 */ + private var isScanning = false private lateinit var tipDialog: QuTipDialog - - /** 确认弹窗 */ private lateinit var confirmDialog: QuConfirmDialog override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentPunchBinding { @@ -64,86 +57,70 @@ class PunchFragment : BaseFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - // 初始化弹窗 val dialogContainer = requireActivity().findViewById(R.id.dialog_container) tipDialog = QuTipDialog(dialogContainer) confirmDialog = QuConfirmDialog(dialogContainer) + // 点击空白收回 + binding.dismissArea.setOnClickListener { + if (!isScanning) findNavController().popBackStack() + } + + // 显示时间并每秒更新 + updateTime() + viewLifecycleOwner.lifecycleScope.launch { + while (isActive) { delay(1000); updateTime() } + } + // 获取考勤状态 fetchAttendance() - - // 上滑返回手势 - setupSwipeUpToBack() - - // 监听系统状态事件 - observeEvents() } override fun onDestroyView() { super.onDestroyView() - // 离开页面时关闭 NFC - nfcController.stopScan() - if (nfcController.isOpen()) { - nfcController.close() - } + stopNfc() } - // ===== 数据获取 ===== + // ===== 时间 ===== + + private fun updateTime() { + binding.tvTime.text = DateUtil.formatTimeShort() + val info = DateUtil.getDateInfo() + binding.tvDate.text = "${info.month}\u6708${info.day}\u65E5 ${info.week}" + } + + // ===== 考勤状态 ===== - /** 获取当前考勤状态 */ private fun fetchAttendance() { viewLifecycleOwner.lifecycleScope.launch { val result = safeApiCall { punchApi.getAttendance() } when (result) { - is ApiResult.Success -> { - punchStatus = result.data - displayStatus(result.data ?: PunchStatus()) - } - is ApiResult.Error -> { - Timber.w("考勤: API 错误 ${result.code}") - displayStatus(PunchStatus()) - } - is ApiResult.NetworkError -> { - Timber.w("考勤: 网络异常") - displayStatus(PunchStatus()) - } + is ApiResult.Success -> displayStatus(result.data ?: PunchStatus()) + else -> displayStatus(PunchStatus()) } } } - // ===== UI 显示 ===== - - /** 根据考勤状态更新页面(基于业务逻辑矩阵) */ + /** 按钮即状态 */ private fun displayStatus(status: PunchStatus) { binding.btnRevoke.visibility = View.GONE - binding.lowPowerHint.visibility = View.GONE + binding.tvLowPower.visibility = View.GONE when { - // 未上班 !status.isOnDuty -> { - binding.tvPunchStatus.text = "未上班" - binding.tvPunchStatus.setTextColor(requireContext().getColor(R.color.text_secondary)) binding.btnPunch.text = "上班打卡" binding.btnPunch.setBackgroundColor(requireContext().getColor(R.color.primary)) binding.btnPunch.setOnClickListener { startNfcPunch(0) } } - // 已上班 + 已下班 status.isOnDuty && status.isOffDuty -> { - val timeText = if (!status.offPunchTime.isNullOrEmpty()) "已下班 ${status.offPunchTime}" else "已下班" - binding.tvPunchStatus.text = timeText - binding.tvPunchStatus.setTextColor(requireContext().getColor(R.color.text_secondary)) binding.btnPunch.text = "下班打卡" binding.btnPunch.setBackgroundColor(requireContext().getColor(R.color.primary)) binding.btnPunch.setOnClickListener { startNfcPunch(1) } binding.btnRevoke.visibility = View.VISIBLE binding.btnRevoke.setOnClickListener { doRevoke() } - binding.lowPowerHint.visibility = View.VISIBLE + binding.tvLowPower.visibility = View.VISIBLE } - // 已上班 + 未下班 status.isOnDuty && !status.isOffDuty -> { - val timeText = if (!status.onPunchTime.isNullOrEmpty()) "已上班 ${status.onPunchTime}" else "已上班" - binding.tvPunchStatus.text = timeText - binding.tvPunchStatus.setTextColor(requireContext().getColor(R.color.success)) binding.btnPunch.text = "下班打卡" binding.btnPunch.setBackgroundColor(requireContext().getColor(R.color.primary)) binding.btnPunch.setOnClickListener { startNfcPunch(1) } @@ -151,135 +128,123 @@ class PunchFragment : BaseFragment() { } } - // ===== NFC 打卡操作 ===== + // ===== NFC 打卡 ===== - /** - * 开始 NFC 打卡 - * 1. 开启 NFC - * 2. 显示"请将手表贴近打卡信标" - * 3. NFC 读到卡号后回调 - */ private fun startNfcPunch(punchType: Int) { + if (isScanning) return currentPunchType = punchType + isScanning = true - // 开启 NFC + // 按钮变为扫描状态(不弹窗) + binding.btnPunch.text = "NFC 扫描中..." + binding.btnPunch.setBackgroundColor(requireContext().getColor(R.color.warning)) + binding.btnPunch.setOnClickListener { stopNfc(); resetButton() } + + // NFC 开启语音+振动(planId=8) + VibrationDefaults.getPattern(8)?.let { vibrationController.executePattern(it) } + + // 开启 NFC 扫描 nfcController.open() - - // 显示扫描提示 - tipDialog.show( - status = QuTipDialog.Status.LOCATION, - title = "请将手表贴近打卡信标", - back = true, - step = 0, - countdown = 10, // 10秒超时自动关闭 - onBack = { - // 用户取消或超时 → 关闭 NFC - nfcController.stopScan() - nfcController.close() - } - ) - - // 开始 NFC 扫描,读到卡号后回调 nfcController.startScan { nfcId -> - Timber.d("考勤: NFC 读到卡号 $nfcId") - // 关闭 NFC 和提示 - nfcController.stopScan() - nfcController.close() - tipDialog.dismiss() + Timber.d("考勤: NFC 读到 $nfcId") + stopNfc() - // 上班弹确认 / 下班直接提交 if (currentPunchType == 0) { confirmDialog.showText( text = "确定上班打卡?", onConfirm = { doPunch(nfcId) }, - onCancel = { /* 取消,不打卡 */ } + onCancel = { resetButton() } ) } else { doPunch(nfcId) } } + + // 10秒超时 + viewLifecycleOwner.lifecycleScope.launch { + delay(10000) + if (isScanning) { + stopNfc() + resetButton() + VibrationDefaults.getPattern(7)?.let { vibrationController.executePattern(it) } + } + } + } + + private fun stopNfc() { + isScanning = false + nfcController.stopScan() + if (nfcController.isOpen()) nfcController.close() + } + + private fun resetButton() { + isScanning = false + fetchAttendance() } - /** 执行 NFC 打卡 API */ private fun doPunch(nfcId: String) { viewLifecycleOwner.lifecycleScope.launch { - val params = hashMapOf( - "nfcId" to nfcId, - "punchType" to currentPunchType - ) + val params = hashMapOf("nfcId" to nfcId, "punchType" to currentPunchType) val result = safeApiCall { punchApi.nfcOnAndOffPunch(params) } when (result) { is ApiResult.Success -> { - Timber.d("考勤: NFC 打卡成功 punchType=$currentPunchType") + // 成功语音+振动(planId=4) + VibrationDefaults.getPattern(4)?.let { vibrationController.executePattern(it) } tipDialog.show( - status = QuTipDialog.Status.SUCCESS, - title = "打卡成功", + status = QuTipDialog.Status.SUCCESS, title = "打卡成功", back = true, step = 0, countdown = 2 ) - - // 副作用:更新工作状态和屏幕亮度 if (currentPunchType == 0) { - screenController.turnOn() - emitWorkState(true) + screenController.turnOn(); emitWorkState(true) } else { - screenController.turnOff() - emitWorkState(false) + screenController.turnOff(); emitWorkState(false) } - fetchAttendance() } is ApiResult.Error -> { + VibrationDefaults.getPattern(7)?.let { vibrationController.executePattern(it) } tipDialog.show( - status = QuTipDialog.Status.ERROR, - title = "打卡失败", - desc = result.message, - back = true, step = 0, countdown = 3 + status = QuTipDialog.Status.ERROR, title = "打卡失败", + desc = result.message, back = true, step = 0, countdown = 3 ) + resetButton() } is ApiResult.NetworkError -> { tipDialog.show( - status = QuTipDialog.Status.ERROR, - title = "网络异常", + status = QuTipDialog.Status.ERROR, title = "网络异常", back = true, step = 0, countdown = 3 ) + resetButton() } } } } - /** 撤销打卡 */ private fun doRevoke() { confirmDialog.showText( text = "确定撤销打卡?", onConfirm = { viewLifecycleOwner.lifecycleScope.launch { - val params = hashMapOf() - val result = safeApiCall { punchApi.revokePunch(params) } - + val result = safeApiCall { punchApi.revokePunch(hashMapOf()) } when (result) { is ApiResult.Success -> { + VibrationDefaults.getPattern(4)?.let { vibrationController.executePattern(it) } tipDialog.show( - status = QuTipDialog.Status.SUCCESS, - title = "撤销成功", + status = QuTipDialog.Status.SUCCESS, title = "撤销成功", back = true, step = 0, countdown = 2 ) - screenController.turnOn() - emitWorkState(true) - fetchAttendance() + screenController.turnOn(); emitWorkState(true); fetchAttendance() } is ApiResult.Error -> { tipDialog.show( - status = QuTipDialog.Status.ERROR, - title = "撤销失败", - desc = result.message, - back = true, step = 0, countdown = 3 + status = QuTipDialog.Status.ERROR, title = "撤销失败", + desc = result.message, back = true, step = 0, countdown = 3 ) } is ApiResult.NetworkError -> { tipDialog.show( - status = QuTipDialog.Status.ERROR, - title = "网络异常", + status = QuTipDialog.Status.ERROR, title = "网络异常", back = true, step = 0, countdown = 3 ) } @@ -289,53 +254,9 @@ class PunchFragment : BaseFragment() { ) } - // ===== 辅助 ===== - - /** 发送工作状态变更事件 */ private fun emitWorkState(isWorking: Boolean) { viewLifecycleOwner.lifecycleScope.launch { eventBus.emit(AppEvent.WorkStateChanged(isWorking)) } } - - /** 监听系统状态事件 */ - private fun observeEvents() { - viewLifecycleOwner.lifecycleScope.launch { - eventBus.events.collect { event -> - when (event) { - is AppEvent.BatteryChanged -> { - binding.statusBar.updateBattery(event.level, event.isCharging) - } - is AppEvent.BluetoothStateChanged -> { - binding.statusBar.updateBluetooth(event.isOn) - } - else -> {} - } - } - } - } - - /** 上滑返回首页 */ - @android.annotation.SuppressLint("ClickableViewAccessibility") - private fun setupSwipeUpToBack() { - var startY = 0f - - binding.contentArea.setOnTouchListener { _, event -> - when (event.action) { - android.view.MotionEvent.ACTION_DOWN -> { - startY = event.y - true - } - android.view.MotionEvent.ACTION_UP -> { - val dy = event.y - startY - // 上滑(dy < -50)→ 返回 - if (dy < -50) { - findNavController().popBackStack() - } - true - } - else -> true - } - } - } } diff --git a/app/src/main/res/layout/fragment_punch.xml b/app/src/main/res/layout/fragment_punch.xml index 25d1bea..a234c0b 100644 --- a/app/src/main/res/layout/fragment_punch.xml +++ b/app/src/main/res/layout/fragment_punch.xml @@ -1,106 +1,88 @@ - - + + android:background="#00000000"> - - + + android:layout_height="match_parent" /> - + + android:paddingTop="27dp" + android:paddingEnd="21dp" + android:paddingBottom="21dp"> - + - - - + android:textColor="@color/text_primary" + android:textSize="48sp" + android:fontFamily="sans-serif-medium" + android:layout_marginBottom="5dp" /> - - - - - - - - - - + + android:textColor="@color/text_secondary" + android:textSize="20sp" + android:layout_marginBottom="16dp" /> - + + - + + - + + - +