feat: 考勤面板UI优化(5项改进)
1. 打卡状态文字:显示"已上班 07:02"/"已下班 17:05" 2. NFC扫描倒计时:显示"请贴近信标(8s)" 3. 底部收回指示条:灰色短横线,点击可收回面板 4. 药丸形按钮:大圆角28dp + 蓝色渐变(源码#4CBAF1→#339AFB) 5. 撤销按钮主次分明:窄灰底红字(0.4) + 宽蓝色下班(0.6) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -172,9 +172,12 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
punchViewModel.uiState.collect { state ->
|
||||
// 更新面板按钮
|
||||
// 更新面板按钮和状态文字
|
||||
punchPanel.updateButtons(state)
|
||||
|
||||
// 更新 NFC 倒计时
|
||||
punchPanel.updateNfcCountdown(state.nfcCountdown)
|
||||
|
||||
// 处理打卡结果(一次性事件)
|
||||
state.punchResult?.let { result ->
|
||||
punchViewModel.consumePunchResult()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.xiaoqu.watch.ui.punch
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
@@ -17,13 +16,13 @@ import com.xiaoqu.watch.util.DateUtil
|
||||
*
|
||||
* 交互方式:
|
||||
* - 调用 show() 展开面板
|
||||
* - 点击遮罩区域收回
|
||||
* - 点击遮罩 / 底部指示条收回
|
||||
* - 打卡完成后自动收回
|
||||
*
|
||||
* 按钮显示规则(来自源码分析,已评审修正):
|
||||
* - onPunchState=0 → "上班打卡"
|
||||
* - onPunchState=1, offPunchState=0 → "下班打卡"
|
||||
* - onPunchState=1, offPunchState=1 → "撤销打卡" + "下班打卡"
|
||||
* - onPunchState=1, offPunchState=0 → "下班打卡" + 状态文字"已上班 HH:MM"
|
||||
* - onPunchState=1, offPunchState=1 → "撤销" + "下班打卡" + 状态文字"已下班 HH:MM"
|
||||
*/
|
||||
class PunchPanelView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
@@ -35,10 +34,12 @@ class PunchPanelView @JvmOverloads constructor(
|
||||
private val overlay: View
|
||||
private val panelContent: LinearLayout
|
||||
private val tvPunchTime: TextView
|
||||
private val tvPunchStatus: TextView
|
||||
private val tvNfcHint: TextView
|
||||
private val btnPunchIn: TextView
|
||||
private val btnPunchOut: TextView
|
||||
private val btnRevoke: TextView
|
||||
private val dismissBar: View
|
||||
|
||||
/** 面板是否正在显示 */
|
||||
var isShowing = false
|
||||
@@ -48,27 +49,27 @@ class PunchPanelView @JvmOverloads constructor(
|
||||
var onPunchInClick: (() -> Unit)? = null
|
||||
var onPunchOutClick: (() -> Unit)? = null
|
||||
var onRevokeClick: (() -> Unit)? = null
|
||||
/** 面板关闭回调(用于通知 HomeFragment 恢复 ViewPager2 滑动) */
|
||||
/** 面板关闭回调 */
|
||||
var onDismiss: (() -> Unit)? = null
|
||||
|
||||
init {
|
||||
// 加载布局
|
||||
LayoutInflater.from(context).inflate(R.layout.view_punch_panel, this, true)
|
||||
|
||||
// 默认隐藏
|
||||
visibility = GONE
|
||||
|
||||
// 绑定 View
|
||||
overlay = findViewById(R.id.overlay)
|
||||
panelContent = findViewById(R.id.panelContent)
|
||||
tvPunchTime = findViewById(R.id.tvPunchTime)
|
||||
tvPunchStatus = findViewById(R.id.tvPunchStatus)
|
||||
tvNfcHint = findViewById(R.id.tvNfcHint)
|
||||
btnPunchIn = findViewById(R.id.btnPunchIn)
|
||||
btnPunchOut = findViewById(R.id.btnPunchOut)
|
||||
btnRevoke = findViewById(R.id.btnRevoke)
|
||||
dismissBar = findViewById(R.id.dismissBar)
|
||||
|
||||
// 点击遮罩收回面板
|
||||
// 点击遮罩 / 指示条 收回面板
|
||||
overlay.setOnClickListener { dismiss() }
|
||||
dismissBar.setOnClickListener { dismiss() }
|
||||
|
||||
// 按钮点击
|
||||
btnPunchIn.setOnClickListener { onPunchInClick?.invoke() }
|
||||
@@ -80,11 +81,8 @@ class PunchPanelView @JvmOverloads constructor(
|
||||
fun show() {
|
||||
if (isShowing) return
|
||||
isShowing = true
|
||||
|
||||
// 更新时间
|
||||
updateClock()
|
||||
|
||||
// 显示并播放动画
|
||||
visibility = VISIBLE
|
||||
panelContent.translationY = -panelContent.height.toFloat().coerceAtLeast(300f)
|
||||
panelContent.animate()
|
||||
@@ -94,10 +92,7 @@ class PunchPanelView @JvmOverloads constructor(
|
||||
.start()
|
||||
|
||||
overlay.alpha = 0f
|
||||
overlay.animate()
|
||||
.alpha(1f)
|
||||
.setDuration(200)
|
||||
.start()
|
||||
overlay.animate().alpha(1f).setDuration(200).start()
|
||||
}
|
||||
|
||||
/** 收回面板 */
|
||||
@@ -105,7 +100,6 @@ class PunchPanelView @JvmOverloads constructor(
|
||||
if (!isShowing) return
|
||||
isShowing = false
|
||||
|
||||
// 收回动画
|
||||
panelContent.animate()
|
||||
.translationY(-panelContent.height.toFloat().coerceAtLeast(300f))
|
||||
.setDuration(200)
|
||||
@@ -113,49 +107,61 @@ class PunchPanelView @JvmOverloads constructor(
|
||||
.withEndAction {
|
||||
visibility = GONE
|
||||
tvNfcHint.visibility = GONE
|
||||
tvPunchStatus.visibility = GONE
|
||||
onDismiss?.invoke()
|
||||
}
|
||||
.start()
|
||||
|
||||
overlay.animate()
|
||||
.alpha(0f)
|
||||
.setDuration(200)
|
||||
.start()
|
||||
overlay.animate().alpha(0f).setDuration(200).start()
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新按钮显示状态
|
||||
* 更新面板显示状态
|
||||
* @param state 考勤状态
|
||||
*/
|
||||
fun updateButtons(state: PunchUiState) {
|
||||
val onState = state.onPunchState
|
||||
val offState = state.offPunchState
|
||||
|
||||
// 重置所有按钮
|
||||
// 重置按钮
|
||||
btnPunchIn.visibility = GONE
|
||||
btnPunchOut.visibility = GONE
|
||||
btnRevoke.visibility = GONE
|
||||
|
||||
// 重置按钮权重(单按钮时 weight=1,双按钮时撤销0.4+下班0.6)
|
||||
when {
|
||||
// 未上班打卡 → 显示"上班打卡"
|
||||
// 未上班打卡 → "上班打卡"
|
||||
onState == 0 -> {
|
||||
btnPunchIn.visibility = VISIBLE
|
||||
setWeight(btnPunchIn, 1f)
|
||||
// 无状态文字
|
||||
tvPunchStatus.visibility = GONE
|
||||
}
|
||||
// 已上班,已下班 → 显示"撤销打卡" + "下班打卡"
|
||||
// 已上班,已下班 → "撤销" + "下班打卡"
|
||||
onState == 1 && offState == 1 -> {
|
||||
btnRevoke.visibility = VISIBLE
|
||||
btnPunchOut.visibility = VISIBLE
|
||||
setWeight(btnRevoke, 0.4f)
|
||||
setWeight(btnPunchOut, 0.6f)
|
||||
// 状态:已下班 HH:MM
|
||||
tvPunchStatus.visibility = VISIBLE
|
||||
tvPunchStatus.text = "已下班 ${state.actualOffTime ?: ""}"
|
||||
tvPunchStatus.setTextColor(context.getColor(R.color.text_secondary))
|
||||
}
|
||||
// 已上班,未下班 → 仅"下班打卡"
|
||||
// 已上班,未下班 → "下班打卡"
|
||||
onState == 1 && offState == 0 -> {
|
||||
btnPunchOut.visibility = VISIBLE
|
||||
setWeight(btnPunchOut, 1f)
|
||||
// 状态:已上班 HH:MM
|
||||
tvPunchStatus.visibility = VISIBLE
|
||||
tvPunchStatus.text = "已上班 ${state.actualOnTime ?: ""}"
|
||||
tvPunchStatus.setTextColor(context.getColor(R.color.success))
|
||||
}
|
||||
}
|
||||
|
||||
// NFC 扫描中 → 显示提示
|
||||
// NFC 扫描中
|
||||
if (state.isNfcScanning) {
|
||||
tvNfcHint.visibility = VISIBLE
|
||||
// 扫描中禁用按钮
|
||||
btnPunchIn.isEnabled = false
|
||||
btnPunchOut.isEnabled = false
|
||||
btnRevoke.isEnabled = false
|
||||
@@ -167,8 +173,28 @@ class PunchPanelView @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
|
||||
/** 更新时钟显示 */
|
||||
/**
|
||||
* 更新 NFC 扫描倒计时
|
||||
* @param secondsLeft 剩余秒数,-1 表示不显示
|
||||
*/
|
||||
fun updateNfcCountdown(secondsLeft: Int) {
|
||||
if (secondsLeft > 0) {
|
||||
tvNfcHint.text = "请贴近信标(${secondsLeft}s)"
|
||||
tvNfcHint.visibility = VISIBLE
|
||||
} else {
|
||||
tvNfcHint.visibility = GONE
|
||||
}
|
||||
}
|
||||
|
||||
/** 更新时钟 */
|
||||
fun updateClock() {
|
||||
tvPunchTime.text = DateUtil.formatTimeShort()
|
||||
}
|
||||
|
||||
/** 设置 View 的 layout_weight */
|
||||
private fun setWeight(view: View, weight: Float) {
|
||||
val lp = view.layoutParams as LinearLayout.LayoutParams
|
||||
lp.weight = weight
|
||||
view.layoutParams = lp
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,12 +107,16 @@ class PunchViewModel @Inject constructor(
|
||||
handleNfcResult(nfcId, punchType)
|
||||
}
|
||||
|
||||
// 3. 超时自动关闭
|
||||
// 3. 倒计时 + <20><>时自动关闭
|
||||
nfcTimeoutJob = viewModelScope.launch {
|
||||
delay(nfcTimeoutMs)
|
||||
val totalSeconds = (nfcTimeoutMs / 1000).toInt()
|
||||
for (i in totalSeconds downTo 1) {
|
||||
_uiState.update { it.copy(nfcCountdown = i) }
|
||||
delay(1000)
|
||||
}
|
||||
Timber.d("考勤: NFC超时自动关闭")
|
||||
closeNfc()
|
||||
_uiState.update { it.copy(isNfcScanning = false, scanningPunchType = -1) }
|
||||
_uiState.update { it.copy(isNfcScanning = false, scanningPunchType = -1, nfcCountdown = 0) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,6 +134,8 @@ class PunchViewModel @Inject constructor(
|
||||
// 关闭 NFC 硬件(不播关闭音效,成功/失败音效由 API 结果决定)
|
||||
nfcController.stopScan()
|
||||
nfcController.close()
|
||||
// 清除倒计时
|
||||
_uiState.update { it.copy(nfcCountdown = 0) }
|
||||
|
||||
// 调用打卡 API
|
||||
viewModelScope.launch {
|
||||
@@ -266,6 +272,7 @@ data class PunchUiState(
|
||||
val actualOffTime: String? = null,
|
||||
val isNfcScanning: Boolean = false,
|
||||
val scanningPunchType: Int = -1,
|
||||
val nfcCountdown: Int = 0,
|
||||
val punchResult: PunchResult? = null,
|
||||
val errorMessage: String? = null
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user