revert: 删除考勤打卡模块代码,准备重新开发

删除:PunchStatus.kt, PunchApi.kt, PunchFragment.kt, fragment_punch.xml
恢复:HomeFragment(去掉下拉手势/PunchApi/工作状态处理)
恢复:fragment_home.xml(去掉pullDownArea覆盖层)
恢复:nav_main.xml(去掉action_home_to_punch)
恢复:NetworkModule(去掉PunchApi提供者)

考勤模块将按NFC打卡流程重新从需求开始设计开发。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
dongliang
2026-04-28 20:23:00 +09:30
parent e44a25e841
commit 98de3f0b46
8 changed files with 20 additions and 548 deletions

View File

@@ -1,24 +0,0 @@
package com.xiaoqu.watch.data.punch
import com.google.gson.annotations.SerializedName
/**
* 考勤状态数据类
* 对应 watchTask/myCurrentAttendance API 返回
* 来源discovery-map.md 考勤章节(已验证字段名)
*/
data class PunchStatus(
/** 上班打卡状态0=未上班, 1=已上班 */
@SerializedName("onPunchState") val onPunchState: Int = 0,
/** 下班打卡状态1=已下班, 其他=未下班 */
@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 isOffDuty: Boolean get() = offPunchState == 1
}

View File

@@ -6,7 +6,6 @@ import com.xiaoqu.watch.network.EnvConfig
import com.xiaoqu.watch.network.SignatureInterceptor
import com.xiaoqu.watch.network.UnbindInterceptor
import com.xiaoqu.watch.network.api.CommonApi
import com.xiaoqu.watch.network.api.PunchApi
import com.xiaoqu.watch.network.api.TaskApi
import dagger.Module
import dagger.Provides
@@ -70,10 +69,4 @@ object NetworkModule {
fun provideTaskApi(retrofit: Retrofit): TaskApi {
return retrofit.create(TaskApi::class.java)
}
@Provides
@Singleton
fun providePunchApi(retrofit: Retrofit): PunchApi {
return retrofit.create(PunchApi::class.java)
}
}

View File

@@ -1,26 +0,0 @@
package com.xiaoqu.watch.network.api
import com.xiaoqu.watch.data.punch.PunchStatus
import com.xiaoqu.watch.network.ApiResponse
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
/**
* 考勤打卡 API 接口
* 来源v1.2.5 punchApis.js + 用户确认NFC考勤流程
*/
interface PunchApi {
/** 查询当前考勤状态 */
@GET("watchTask/myCurrentAttendance")
suspend fun getAttendance(): ApiResponse<PunchStatus>
/** NFC 上班/下班打卡v1.2.5 新增) */
@POST("watchTask/nfcOnAndOffPunch")
suspend fun nfcOnAndOffPunch(@Body params: HashMap<String, Any>): ApiResponse<Any>
/** 撤销打卡 */
@POST("watchTask/revokePunch")
suspend fun revokePunch(@Body params: HashMap<String, Any>): ApiResponse<Any>
}

View File

@@ -15,13 +15,9 @@ import com.xiaoqu.watch.data.prefs.UserPrefs
import com.xiaoqu.watch.databinding.FragmentHomeBinding
import com.xiaoqu.watch.event.AppEvent
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.api.PunchApi
import com.xiaoqu.watch.network.api.TaskApi
import com.xiaoqu.watch.network.safeApiCall
import org.json.JSONObject
import com.xiaoqu.watch.ui.common.BaseFragment
import com.xiaoqu.watch.ui.widget.StatusBarView
import com.xiaoqu.watch.util.DateUtil
@@ -43,9 +39,6 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
@Inject lateinit var userPrefs: UserPrefs
@Inject lateinit var eventBus: EventBus
@Inject lateinit var taskApi: TaskApi
@Inject lateinit var punchApi: PunchApi
@Inject lateinit var screenController: ScreenController
@Inject lateinit var nfcController: NfcController
// ===== 固定状态栏(不随 ViewPager 滑动) =====
private lateinit var statusBar: StatusBarView
@@ -100,14 +93,8 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
// 加载任务统计数据
fetchStatistics()
// 检查当前工作状态(考勤)
fetchWorkState()
// 监听 MQTT 事件
observeEvents()
// 下拉手势 → 进入考勤页
setupPullDownGesture()
}
// ===== 主页 =====
@@ -261,9 +248,9 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
findNavController().navigate(R.id.action_home_to_bind)
}
4 -> {
// 工作状态变更MQTT type=4/5 推送)
// 工作状态变更
Timber.d("首页: 收到工作状态变更")
handleWorkStateChange(event.rawJson)
// TODO: 考勤模块重新开发时实现
}
}
}
@@ -283,101 +270,4 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
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, "首页: 解析工作状态变更异常")
}
}
/**
* 设置下拉手势 → 进入考勤打卡页
* 触摸区域上半屏140dp只拦截垂直下拉水平滑动透传给 ViewPager2
*/
@android.annotation.SuppressLint("ClickableViewAccessibility")
private fun setupPullDownGesture() {
var startY = 0f
var startX = 0f
binding.pullDownArea.setOnTouchListener { _, event ->
when (event.action) {
android.view.MotionEvent.ACTION_DOWN -> {
startY = event.y
startX = event.x
true // 拦截 DOWN
}
android.view.MotionEvent.ACTION_MOVE -> {
val dy = event.y - startY
val dx = event.x - startX
// 如果水平滑动幅度 > 垂直 → 不拦截,透传给 ViewPager2
if (kotlin.math.abs(dx) > kotlin.math.abs(dy) + 10) {
false
} else {
true
}
}
android.view.MotionEvent.ACTION_UP -> {
val dy = event.y - startY
val dx = event.x - startX
// 下拉dy > 50且垂直幅度 > 水平
if (dy > 50 && kotlin.math.abs(dy) > kotlin.math.abs(dx)) {
navigateToPunch()
}
true
}
else -> false
}
}
}
/** 跳转到考勤打卡页 */
private fun navigateToPunch() {
val currentDest = findNavController().currentDestination?.id
if (currentDest != R.id.homeFragment) return
findNavController().navigate(R.id.action_home_to_punch)
}
}

View File

@@ -1,256 +0,0 @@
package com.xiaoqu.watch.ui.punch
import android.os.Bundle
import android.view.LayoutInflater
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.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
import com.xiaoqu.watch.network.api.PunchApi
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 扫描时按钮变状态,不弹窗
* - 打卡有语音+振动反馈
*/
@AndroidEntryPoint
class PunchFragment : BaseFragment<FragmentPunchBinding>() {
@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 currentPunchType = 0
private var isScanning = false
private lateinit var tipDialog: QuTipDialog
private lateinit var confirmDialog: QuConfirmDialog
override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentPunchBinding {
return FragmentPunchBinding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val dialogContainer = requireActivity().findViewById<FrameLayout>(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()
}
override fun onDestroyView() {
super.onDestroyView()
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 -> displayStatus(result.data ?: PunchStatus())
else -> displayStatus(PunchStatus())
}
}
}
/** 按钮即状态 */
private fun displayStatus(status: PunchStatus) {
binding.btnRevoke.visibility = View.GONE
binding.tvLowPower.visibility = View.GONE
when {
!status.isOnDuty -> {
binding.btnPunch.text = "上班打卡"
binding.btnPunch.setBackgroundColor(requireContext().getColor(R.color.primary))
binding.btnPunch.setOnClickListener { startNfcPunch(0) }
}
status.isOnDuty && status.isOffDuty -> {
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.tvLowPower.visibility = View.VISIBLE
}
status.isOnDuty && !status.isOffDuty -> {
binding.btnPunch.text = "下班打卡"
binding.btnPunch.setBackgroundColor(requireContext().getColor(R.color.primary))
binding.btnPunch.setOnClickListener { startNfcPunch(1) }
}
}
}
// ===== NFC 打卡 =====
private fun startNfcPunch(punchType: Int) {
if (isScanning) return
currentPunchType = punchType
isScanning = true
// 按钮变为扫描状态(不弹窗)
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 扫描(加日志排查)
Timber.d("考勤: 开启 NFC")
nfcController.open()
Timber.d("考勤: NFC isOpen=${nfcController.isOpen()}, 开始扫描")
nfcController.startScan { nfcId ->
Timber.d("考勤: NFC 读到卡号 $nfcId")
stopNfc()
// 不弹确认,直接打卡
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()
}
private fun doPunch(nfcId: String) {
viewLifecycleOwner.lifecycleScope.launch {
val params = hashMapOf<String, Any>("nfcId" to nfcId, "punchType" to currentPunchType)
val result = safeApiCall { punchApi.nfcOnAndOffPunch(params) }
when (result) {
is ApiResult.Success -> {
// 成功语音+振动planId=4
VibrationDefaults.getPattern(4)?.let { vibrationController.executePattern(it) }
tipDialog.show(
status = QuTipDialog.Status.SUCCESS, title = "打卡成功",
back = true, step = 0, countdown = 2
)
if (currentPunchType == 0) {
screenController.turnOn(); emitWorkState(true)
} else {
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
)
resetButton()
}
is ApiResult.NetworkError -> {
tipDialog.show(
status = QuTipDialog.Status.ERROR, title = "网络异常",
back = true, step = 0, countdown = 3
)
resetButton()
}
}
}
}
private fun doRevoke() {
confirmDialog.showText(
text = "确定撤销打卡?",
onConfirm = {
viewLifecycleOwner.lifecycleScope.launch {
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 = "撤销成功",
back = true, step = 0, countdown = 2
)
screenController.turnOn(); emitWorkState(true); fetchAttendance()
}
is ApiResult.Error -> {
tipDialog.show(
status = QuTipDialog.Status.ERROR, title = "撤销失败",
desc = result.message, back = true, step = 0, countdown = 3
)
}
is ApiResult.NetworkError -> {
tipDialog.show(
status = QuTipDialog.Status.ERROR, title = "网络异常",
back = true, step = 0, countdown = 3
)
}
}
}
}
)
}
private fun emitWorkState(isWorking: Boolean) {
viewLifecycleOwner.lifecycleScope.launch {
eventBus.emit(AppEvent.WorkStateChanged(isWorking))
}
}
}

View File

@@ -1,40 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 首页容器:状态栏 + ViewPager2 + 下拉触摸层(上半屏) -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<!-- 首页容器:固定状态栏 + ViewPager2 左右滑动 -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background">
android:background="@color/background"
android:orientation="vertical"
android:paddingStart="21dp"
android:paddingTop="27dp"
android:paddingEnd="21dp">
<!-- 底层:状态栏 + ViewPager2 -->
<LinearLayout
<!-- 固定状态栏 -->
<com.xiaoqu.watch.ui.widget.StatusBarView
android:id="@+id/statusBar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingStart="21dp"
android:paddingTop="27dp"
android:paddingEnd="21dp">
android:layout_height="24dp"
android:layout_marginBottom="3dp" />
<!-- 固定状态栏 -->
<com.xiaoqu.watch.ui.widget.StatusBarView
android:id="@+id/statusBar"
android:layout_width="match_parent"
android:layout_height="24dp"
android:layout_marginBottom="3dp" />
<!-- ViewPager2 -->
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
<!-- 上层:下拉触摸区域(上半屏,透明,不拦截水平滑动) -->
<View
android:id="@+id/pullDownArea"
<!-- ViewPager2 -->
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="140dp"
android:layout_gravity="top" />
android:layout_height="0dp"
android:layout_weight="1" />
</FrameLayout>
</LinearLayout>

View File

@@ -1,88 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 考勤打卡(半屏下拉面板)
上半:时间+按钮(黑色背景)
下半:透明,点击收回 -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#00000000">
<!-- 点击空白收回 -->
<View
android:id="@+id/dismissArea"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!-- 上半屏面板 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:background="@color/background"
android:orientation="vertical"
android:paddingStart="21dp"
android:paddingTop="27dp"
android:paddingEnd="21dp"
android:paddingBottom="21dp">
<!-- 时间 -->
<TextView
android:id="@+id/tvTime"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="@color/text_primary"
android:textSize="48sp"
android:fontFamily="sans-serif-medium"
android:layout_marginBottom="5dp" />
<!-- 日期 -->
<TextView
android:id="@+id/tvDate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="@color/text_secondary"
android:textSize="20sp"
android:layout_marginBottom="16dp" />
<!-- 主按钮 -->
<TextView
android:id="@+id/btnPunch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="16dp"
android:textColor="@color/text_primary"
android:textSize="26sp"
android:textStyle="bold" />
<!-- 撤销按钮 -->
<TextView
android:id="@+id/btnRevoke"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="14dp"
android:background="@color/grey_button"
android:textColor="@color/error"
android:textSize="22sp"
android:text="撤销打卡"
android:layout_marginTop="8dp"
android:visibility="gone" />
<!-- 低耗电提示 -->
<TextView
android:id="@+id/tvLowPower"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="低耗电模式已开启"
android:textColor="@color/warning"
android:textSize="18sp"
android:layout_marginTop="11dp"
android:visibility="gone" />
</LinearLayout>
</FrameLayout>

View File

@@ -28,9 +28,6 @@
<!-- 首页 → 任务列表 -->
<action android:id="@+id/action_home_to_taskList"
app:destination="@id/taskListFragment" />
<!-- 首页 → 考勤打卡(下拉入口) -->
<action android:id="@+id/action_home_to_punch"
app:destination="@id/punchFragment" />
</fragment>
<!-- 设备绑定页 -->