From 967b001d460f6a30b42625baad8a06ae3034f686 Mon Sep 17 00:00:00 2001 From: dongliang Date: Tue, 28 Apr 2026 19:23:48 +0930 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=80=83=E5=8B=A4=E6=94=B9=E4=B8=BANFC?= =?UTF-8?q?=E6=89=93=E5=8D=A1=EF=BC=88=E6=9B=BF=E4=BB=A3=E8=93=9D=E7=89=99?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - API: onAndOffPunch → nfcOnAndOffPunch(v1.2.5新增) - 参数: {beaconMacs, punchType} → {nfcId, punchType} - 流程: 点按钮→开NFC→贴信标读卡→提交 - 10秒超时自动关闭NFC - 离开页面自动关闭NFC Co-Authored-By: Claude Opus 4.6 (1M context) --- .../com/xiaoqu/watch/network/api/PunchApi.kt | 9 +- .../xiaoqu/watch/ui/punch/PunchFragment.kt | 144 +++++++++--------- 2 files changed, 75 insertions(+), 78 deletions(-) diff --git a/app/src/main/java/com/xiaoqu/watch/network/api/PunchApi.kt b/app/src/main/java/com/xiaoqu/watch/network/api/PunchApi.kt index ce1ad33..56aecc6 100644 --- a/app/src/main/java/com/xiaoqu/watch/network/api/PunchApi.kt +++ b/app/src/main/java/com/xiaoqu/watch/network/api/PunchApi.kt @@ -8,8 +8,7 @@ import retrofit2.http.POST /** * 考勤打卡 API 接口 - * 来源:discovery-map.md 考勤章节 - * 注意:myCurrentAttendance 标注为 GET,如返回 405 需改为 POST + * 来源:v1.2.5 punchApis.js + 用户确认NFC考勤流程 */ interface PunchApi { @@ -17,9 +16,9 @@ interface PunchApi { @GET("watchTask/myCurrentAttendance") suspend fun getAttendance(): ApiResponse - /** 上班/下班打卡 */ - @POST("watchTask/onAndOffPunch") - suspend fun onAndOffPunch(@Body params: HashMap): ApiResponse + /** NFC 上班/下班打卡(v1.2.5 新增) */ + @POST("watchTask/nfcOnAndOffPunch") + suspend fun nfcOnAndOffPunch(@Body params: HashMap): ApiResponse /** 撤销打卡 */ @POST("watchTask/revokePunch") 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 846d618..c97d1d6 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 @@ -6,10 +6,10 @@ import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import androidx.lifecycle.lifecycleScope -import androidx.navigation.fragment.findNavController import com.xiaoqu.watch.R 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.event.AppEvent import com.xiaoqu.watch.event.EventBus @@ -20,31 +20,36 @@ import com.xiaoqu.watch.ui.common.BaseFragment import com.xiaoqu.watch.ui.widget.QuConfirmDialog import com.xiaoqu.watch.ui.widget.QuTipDialog import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.delay import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject /** - * 考勤打卡页面 - * 入口:首页下拉手势 - * 3 种状态:未上班→上班打卡→已上班→下班打卡/撤销 + * 考勤打卡页面(NFC 方式) * - * 流程(基于 discovery-map.md 考勤章节 + baseline/05 流程6): - * 上班:蓝牙识别1.5s → 确认弹窗 → POST onAndOffPunch → 成功 - * 下班:蓝牙识别1.5s → 直接提交 → POST onAndOffPunch → 成功+低耗电 - * 撤销:确认弹窗 → POST revokePunch → 恢复上班状态 + * 流程: + * 1. 进入页面 → GET myCurrentAttendance → 显示考勤状态 + * 2. 点击「上班打卡」→ 开启 NFC → "请将手表贴近打卡信标" + * 3. NFC 读到卡号 → 上班弹确认 / 下班直接提交 + * 4. POST nfcOnAndOffPunch {nfcId, punchType} + * 5. 成功 → 提示 + 更新工作状态 + 屏幕亮度 + * + * 来源:v1.2.5 punchApis.js nfcOnAndOffPunch + 用户确认 */ @AndroidEntryPoint class PunchFragment : BaseFragment() { @Inject lateinit var punchApi: PunchApi + @Inject lateinit var nfcController: NfcController @Inject lateinit var screenController: ScreenController @Inject lateinit var eventBus: EventBus /** 当前考勤状态 */ private var punchStatus: PunchStatus? = null + /** 当前打卡类型(0=上班, 1=下班) */ + private var currentPunchType = 0 + /** 提示弹窗 */ private lateinit var tipDialog: QuTipDialog @@ -66,10 +71,19 @@ class PunchFragment : BaseFragment() { // 获取考勤状态 fetchAttendance() - // 监听系统状态事件(电量更新状态栏) + // 监听系统状态事件 observeEvents() } + override fun onDestroyView() { + super.onDestroyView() + // 离开页面时关闭 NFC + nfcController.stopScan() + if (nfcController.isOpen()) { + nfcController.close() + } + } + // ===== 数据获取 ===== /** 获取当前考勤状态 */ @@ -83,7 +97,6 @@ class PunchFragment : BaseFragment() { } is ApiResult.Error -> { Timber.w("考勤: API 错误 ${result.code}") - // 默认显示未上班 displayStatus(PunchStatus()) } is ApiResult.NetworkError -> { @@ -96,105 +109,103 @@ class PunchFragment : BaseFragment() { // ===== UI 显示 ===== - /** 根据考勤状态更新页面显示(基于业务逻辑矩阵) */ + /** 根据考勤状态更新页面(基于业务逻辑矩阵) */ private fun displayStatus(status: PunchStatus) { - // 重置所有可选元素 binding.btnRevoke.visibility = View.GONE binding.lowPowerHint.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 { startPunch(0) } + binding.btnPunch.setOnClickListener { startNfcPunch(0) } } - - // 已上班 + 已下班 → 显示「撤销」+「下班打卡」+ 低耗电提示 + // 已上班 + 已下班 status.isOnDuty && status.isOffDuty -> { 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 { startPunch(1) } + binding.btnPunch.setOnClickListener { startNfcPunch(1) } binding.btnRevoke.visibility = View.VISIBLE binding.btnRevoke.setOnClickListener { doRevoke() } binding.lowPowerHint.visibility = View.VISIBLE } - - // 已上班 + 未下班 → 显示「下班打卡」 + // 已上班 + 未下班 status.isOnDuty && !status.isOffDuty -> { binding.tvPunchStatus.text = "已上班" binding.tvPunchStatus.setTextColor(requireContext().getColor(R.color.success)) binding.btnPunch.text = "下班打卡" binding.btnPunch.setBackgroundColor(requireContext().getColor(R.color.primary)) - binding.btnPunch.setOnClickListener { startPunch(1) } + binding.btnPunch.setOnClickListener { startNfcPunch(1) } } } } - // ===== 打卡操作 ===== + // ===== NFC 打卡操作 ===== /** - * 开始打卡流程 - * 1. 显示蓝牙识别 Loading(1.5秒) - * 2. 读取 beacons(当前用模拟数据) - * 3. 上班:弹确认框;下班:直接提交 + * 开始 NFC 打卡 + * 1. 开启 NFC + * 2. 显示"请将手表贴近打卡信标" + * 3. NFC 读到卡号后回调 */ - private fun startPunch(punchType: Int) { - // 显示蓝牙识别提示 + private fun startNfcPunch(punchType: Int) { + currentPunchType = punchType + + // 开启 NFC + nfcController.open() + + // 显示扫描提示 tipDialog.show( status = QuTipDialog.Status.LOCATION, - title = "蓝牙正在识别…", - back = false + title = "请将手表贴近打卡信标", + back = true, + step = 0, + countdown = 10, // 10秒超时自动关闭 + onBack = { + // 用户取消或超时 → 关闭 NFC + nfcController.stopScan() + nfcController.close() + } ) - viewLifecycleOwner.lifecycleScope.launch { - // 等待 1.5 秒(模拟蓝牙扫描采集) - delay(1500) + // 开始 NFC 扫描,读到卡号后回调 + nfcController.startScan { nfcId -> + Timber.d("考勤: NFC 读到卡号 $nfcId") + // 关闭 NFC 和提示 + nfcController.stopScan() + nfcController.close() tipDialog.dismiss() - // 读取蓝牙信标(TODO: 后续从 store.beacons 读取,当前用模拟数据) - val beaconMacs = getMockBeaconMacs() - - if (beaconMacs.isEmpty()) { - // 无信标 → 提示失败 - tipDialog.show( - status = QuTipDialog.Status.WARNING, - title = "打卡失败", - desc = "未搜索到蓝牙信标,请重试", - back = true, step = 0, countdown = 3 - ) - return@launch - } - - if (punchType == 0) { - // 上班 → 弹确认弹窗 + // 上班弹确认 / 下班直接提交 + if (currentPunchType == 0) { confirmDialog.showText( text = "确定上班打卡?", - onConfirm = { doPunch(punchType, beaconMacs) } + onConfirm = { doPunch(nfcId) }, + onCancel = { /* 取消,不打卡 */ } ) } else { - // 下班 → 不弹确认,直接提交 - doPunch(punchType, beaconMacs) + doPunch(nfcId) } } } - /** 执行打卡 API 调用 */ - private fun doPunch(punchType: Int, beaconMacs: List) { + /** 执行 NFC 打卡 API */ + private fun doPunch(nfcId: String) { viewLifecycleOwner.lifecycleScope.launch { val params = hashMapOf( - "beaconMacs" to beaconMacs, - "punchType" to punchType + "nfcId" to nfcId, + "punchType" to currentPunchType ) - val result = safeApiCall { punchApi.onAndOffPunch(params) } + val result = safeApiCall { punchApi.nfcOnAndOffPunch(params) } when (result) { is ApiResult.Success -> { - Timber.d("考勤: 打卡成功 punchType=$punchType") + Timber.d("考勤: NFC 打卡成功 punchType=$currentPunchType") tipDialog.show( status = QuTipDialog.Status.SUCCESS, title = "打卡成功", @@ -202,17 +213,14 @@ class PunchFragment : BaseFragment() { ) // 副作用:更新工作状态和屏幕亮度 - if (punchType == 0) { - // 上班 → 屏幕正常亮度 + if (currentPunchType == 0) { screenController.turnOn() emitWorkState(true) } else { - // 下班 → 低耗电(熄屏) screenController.turnOff() emitWorkState(false) } - // 刷新考勤状态 fetchAttendance() } is ApiResult.Error -> { @@ -245,13 +253,11 @@ class PunchFragment : BaseFragment() { when (result) { is ApiResult.Success -> { - Timber.d("考勤: 撤销成功") tipDialog.show( status = QuTipDialog.Status.SUCCESS, title = "撤销成功", back = true, step = 0, countdown = 2 ) - // 恢复上班状态 screenController.turnOn() emitWorkState(true) fetchAttendance() @@ -277,15 +283,7 @@ class PunchFragment : BaseFragment() { ) } - // ===== 辅助方法 ===== - - /** - * 模拟蓝牙信标 MAC 列表 - * TODO: 蓝牙扫描模块完成后,改为从 store.beacons 读取 - */ - private fun getMockBeaconMacs(): List { - return listOf("AA:BB:CC:DD:EE:FF", "11:22:33:44:55:66") - } + // ===== 辅助 ===== /** 发送工作状态变更事件 */ private fun emitWorkState(isWorking: Boolean) {