feat: 任务管理模块 - 列表+详情+抢单/打卡/完成

新增:
- TaskListFragment 分段控件+RecyclerView+下拉刷新
- TaskDetailFragment 信息展示+底部固定操作按钮(foot-btn)
- TaskListAdapter 任务卡片适配器
- TaskItem/TaskDetail 数据类
- TaskApi 新增5个接口(pageList/detail/grab/assign/complete)
- 分段控件/底部按钮/状态标签 drawable

修改:
- nav_main.xml 添加参数和action
- HomeFragment 快捷区点击→任务列表(传tableStatus)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
dongliang
2026-04-27 20:19:35 +09:30
parent 3c9d74f16c
commit 1056386af8
19 changed files with 889 additions and 43 deletions

View File

@@ -0,0 +1,33 @@
package com.xiaoqu.watch.data.task
import com.google.gson.annotations.SerializedName
/**
* 任务详情数据类
* 对应 watchTask/lookTaskDetail API 返回
*/
data class TaskDetail(
@SerializedName("id") val id: Long = 0,
/** 任务编号 */
@SerializedName("no") val no: String = "",
/** 任务名称 */
@SerializedName("taskName") val taskName: String = "",
/** 地点名称 */
@SerializedName("positionName") val positionName: String = "",
/** 积分 */
@SerializedName("point") val point: Int = 0,
/** 状态2=待抢单, 3=待打卡, 4=进行中, 1=已完成 */
@SerializedName("status") val status: Int = 0,
/** 任务类型 */
@SerializedName("taskType") val taskType: Int = 0,
/** 开始时间 */
@SerializedName("beginTime") val beginTime: String = "",
/** 结束时间 */
@SerializedName("endTime") val endTime: String = "",
/** 任务描述 */
@SerializedName("description") val description: String = "",
/** 协作人 */
@SerializedName("executorName") val executorName: String = "",
/** 是否有打卡地点(决定打卡方式:有=NFC打卡无=直接确认) */
@SerializedName("hasPosition") val hasPosition: Boolean = false
)

View File

@@ -0,0 +1,27 @@
package com.xiaoqu.watch.data.task
import com.google.gson.annotations.SerializedName
/**
* 任务列表项数据类
* 对应 watchTask/pageList API 返回
*/
data class TaskItem(
@SerializedName("id") val id: Long = 0,
/** 任务编号 */
@SerializedName("no") val no: String = "",
/** 任务名称 */
@SerializedName("taskName") val taskName: String = "",
/** 地点名称 */
@SerializedName("positionName") val positionName: String = "",
/** 积分 */
@SerializedName("point") val point: Int = 0,
/** 状态2=待抢单, 3=待打卡, 4=进行中, 1=已完成 */
@SerializedName("status") val status: Int = 0,
/** 任务类型0=计划, 1=监控, 2=指派, 3=用户上报, 4=巡检上报, 5=巡检任务 */
@SerializedName("taskType") val taskType: Int = 0,
/** 开始时间 */
@SerializedName("beginTime") val beginTime: String = "",
/** 结束时间 */
@SerializedName("endTime") val endTime: String = ""
)

View File

@@ -1,9 +1,14 @@
package com.xiaoqu.watch.network.api
import com.xiaoqu.watch.data.task.AttendanceInfo
import com.xiaoqu.watch.data.task.TaskDetail
import com.xiaoqu.watch.data.task.TaskItem
import com.xiaoqu.watch.data.task.TaskStatistics
import com.xiaoqu.watch.network.ApiResponse
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Query
/**
* 任务相关 API 接口
@@ -17,4 +22,24 @@ interface TaskApi {
/** 查询当前考勤状态 */
@GET("watchTask/myCurrentAttendance")
suspend fun getAttendance(): ApiResponse<AttendanceInfo>
/** 任务列表(按状态筛选) */
@GET("watchTask/pageList")
suspend fun getTaskList(@Query("status") status: Int): ApiResponse<List<TaskItem>>
/** 任务详情 */
@GET("watchTask/lookTaskDetail")
suspend fun getTaskDetail(@Query("id") taskId: Long): ApiResponse<TaskDetail>
/** 抢单 */
@POST("task/grabTask")
suspend fun grabTask(@Body params: HashMap<String, Any>): ApiResponse<Any>
/** 无场景打卡(直接确认) */
@POST("task/assignToBeginTask")
suspend fun assignToBeginTask(@Body params: HashMap<String, Any>): ApiResponse<Any>
/** 确认完成 */
@POST("task/completeTask")
suspend fun completeTask(@Body params: HashMap<String, Any>): ApiResponse<Any>
}

View File

@@ -5,6 +5,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.os.bundleOf
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.xiaoqu.watch.BuildConfig
@@ -106,18 +107,15 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
tvPunchNum = page.findViewById(R.id.tvPunchNum)
tvCompleteNum = page.findViewById(R.id.tvCompleteNum)
// 快捷区卡片点击 → 跳转任务列表(后续实现
// 快捷区卡片点击 → 跳转任务列表(传 tableStatus 参数
page.findViewById<View>(R.id.cardPool)?.setOnClickListener {
Timber.d("点击接单池")
// TODO: navigate to TaskListFragment with tableStatus=2
navigateToTaskList(2)
}
page.findViewById<View>(R.id.cardPunch)?.setOnClickListener {
Timber.d("点击待打卡")
// TODO: navigate to TaskListFragment with tableStatus=3
navigateToTaskList(3)
}
page.findViewById<View>(R.id.cardComplete)?.setOnClickListener {
Timber.d("点击待完成")
// TODO: navigate to TaskListFragment with tableStatus=4
navigateToTaskList(4)
}
}
@@ -261,4 +259,10 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>() {
}
}
}
/** 跳转到任务列表(传 tableStatus 参数) */
private fun navigateToTaskList(tableStatus: Int) {
val bundle = bundleOf("tableStatus" to tableStatus)
findNavController().navigate(R.id.action_home_to_taskList, bundle)
}
}

View File

@@ -1,15 +1,247 @@
package com.xiaoqu.watch.ui.task
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.task.TaskDetail
import com.xiaoqu.watch.databinding.FragmentTaskDetailBinding
import com.xiaoqu.watch.network.ApiResult
import com.xiaoqu.watch.network.api.TaskApi
import com.xiaoqu.watch.network.safeApiCall
import com.xiaoqu.watch.ui.common.BaseFragment
import com.xiaoqu.watch.ui.widget.QuTipDialog
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
/**
* 任务详情页
* 显示任务信息 + 底部固定操作按钮(按状态不同显示不同操作)
*/
@AndroidEntryPoint
class TaskDetailFragment : BaseFragment<FragmentTaskDetailBinding>() {
@Inject lateinit var taskApi: TaskApi
/** 当前任务数据 */
private var taskDetail: TaskDetail? = null
/** 提示弹窗 */
private lateinit var tipDialog: QuTipDialog
override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentTaskDetailBinding {
return FragmentTaskDetailBinding.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)
// 返回按钮
binding.btnBack.setOnClickListener {
findNavController().popBackStack()
}
// 从导航参数获取任务 ID
val taskId = arguments?.getLong("taskId", 0) ?: 0
if (taskId > 0) {
fetchDetail(taskId)
}
}
/** 获取任务详情 */
private fun fetchDetail(taskId: Long) {
viewLifecycleOwner.lifecycleScope.launch {
val result = safeApiCall { taskApi.getTaskDetail(taskId) }
when (result) {
is ApiResult.Success -> {
result.data?.let { detail ->
taskDetail = detail
displayDetail(detail)
setupActionButton(detail)
}
}
is ApiResult.Error -> {
Timber.w("任务详情: API 错误 ${result.code}")
}
is ApiResult.NetworkError -> {
Timber.w("任务详情: 网络异常")
}
}
}
}
/** 展示任务信息 */
private fun displayDetail(detail: TaskDetail) {
binding.tvTaskName.text = detail.taskName
binding.tvTaskNo.text = detail.no
binding.tvPosition.text = detail.positionName.ifEmpty { "" }
binding.tvPoints.text = if (detail.point > 0) "+${detail.point}" else "0"
binding.tvTime.text = detail.beginTime
// 状态标签
when (detail.status) {
2 -> binding.tvStatus.text = "待抢单"
3 -> binding.tvStatus.text = "待打卡"
4 -> binding.tvStatus.text = "进行中"
else -> binding.tvStatus.text = "已完成"
}
// 提示条(待打卡+有场景时显示)
if (detail.status == 3 && detail.hasPosition) {
binding.tvHint.visibility = View.VISIBLE
}
}
/** 根据任务状态设置底部操作按钮 */
private fun setupActionButton(detail: TaskDetail) {
val btn = binding.btnAction
when (detail.status) {
// 待抢单 → 蓝色「抢 单」
2 -> {
btn.text = "抢 单"
btn.setBackgroundResource(R.drawable.bg_foot_btn_blue)
btn.setOnClickListener { doGrabTask(detail.id) }
}
// 待打卡
3 -> {
if (detail.hasPosition) {
// 有场景 → 橙色「开启打卡」NFC后续实现
btn.text = "开启打卡"
btn.setBackgroundResource(R.drawable.bg_foot_btn_orange)
btn.setTextColor(requireContext().getColor(R.color.background))
btn.setOnClickListener {
// TODO: NFC 打卡流程
Timber.d("任务详情: NFC 打卡(后续实现)")
}
} else {
// 无场景 → 绿色「确认打卡」
btn.text = "确认打卡"
btn.setBackgroundResource(R.drawable.bg_foot_btn_green)
btn.setOnClickListener { doAssignBeginTask(detail.id) }
}
}
// 进行中 → 绿色「确认完成」
4 -> {
btn.text = "确认完成"
btn.setBackgroundResource(R.drawable.bg_foot_btn_green)
btn.setOnClickListener { doCompleteTask(detail.id) }
}
// 已完成 → 隐藏按钮
else -> {
btn.visibility = View.GONE
}
}
}
/** 抢单操作 */
private fun doGrabTask(taskId: Long) {
viewLifecycleOwner.lifecycleScope.launch {
val params = hashMapOf<String, Any>("id" to taskId)
val result = safeApiCall { taskApi.grabTask(params) }
when (result) {
is ApiResult.Success -> {
Timber.d("任务详情: 抢单成功")
tipDialog.show(
status = QuTipDialog.Status.SUCCESS,
title = "抢单成功",
back = true, step = 1, countdown = 2,
onBack = { findNavController().popBackStack() }
)
}
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 doAssignBeginTask(taskId: Long) {
viewLifecycleOwner.lifecycleScope.launch {
val params = hashMapOf<String, Any>("id" to taskId)
val result = safeApiCall { taskApi.assignToBeginTask(params) }
when (result) {
is ApiResult.Success -> {
Timber.d("任务详情: 打卡成功")
tipDialog.show(
status = QuTipDialog.Status.SUCCESS,
title = "打卡成功",
back = true, step = 1, countdown = 2,
onBack = { findNavController().popBackStack() }
)
}
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 doCompleteTask(taskId: Long) {
viewLifecycleOwner.lifecycleScope.launch {
val params = hashMapOf<String, Any>("id" to taskId)
val result = safeApiCall { taskApi.completeTask(params) }
when (result) {
is ApiResult.Success -> {
Timber.d("任务详情: 完成成功")
tipDialog.show(
status = QuTipDialog.Status.SUCCESS,
title = "任务已完成",
back = true, step = 1, countdown = 2,
onBack = { findNavController().popBackStack() }
)
}
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
)
}
}
}
}
}

View File

@@ -0,0 +1,88 @@
package com.xiaoqu.watch.ui.task
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.xiaoqu.watch.R
import com.xiaoqu.watch.data.task.TaskItem
/**
* 任务列表 RecyclerView 适配器
* 显示任务卡片:任务名 + 地点+时间 + 积分+状态标签
*/
class TaskListAdapter(
private val onItemClick: (TaskItem) -> Unit
) : RecyclerView.Adapter<TaskListAdapter.TaskViewHolder>() {
private val tasks = mutableListOf<TaskItem>()
/** 更新数据 */
fun submitList(newTasks: List<TaskItem>) {
tasks.clear()
tasks.addAll(newTasks)
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TaskViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_task_card, parent, false)
return TaskViewHolder(view)
}
override fun onBindViewHolder(holder: TaskViewHolder, position: Int) {
holder.bind(tasks[position])
}
override fun getItemCount(): Int = tasks.size
inner class TaskViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val tvTaskName: TextView = view.findViewById(R.id.tvTaskName)
private val tvTaskSub: TextView = view.findViewById(R.id.tvTaskSub)
private val tvPoints: TextView = view.findViewById(R.id.tvPoints)
private val tvStatus: TextView = view.findViewById(R.id.tvStatus)
fun bind(task: TaskItem) {
// 任务名
tvTaskName.text = task.taskName
// 地点 + 时间
val sub = StringBuilder()
if (task.positionName.isNotEmpty()) sub.appendLine(task.positionName)
if (task.beginTime.isNotEmpty()) sub.append(task.beginTime)
tvTaskSub.text = sub.toString()
// 积分
tvPoints.text = if (task.point > 0) "+${task.point}" else ""
// 状态标签
val context = itemView.context
when (task.status) {
2 -> {
tvStatus.text = "待抢单"
tvStatus.setTextColor(context.getColor(R.color.primary))
tvStatus.setBackgroundResource(R.drawable.bg_pill_blue)
}
3 -> {
tvStatus.text = "待打卡"
tvStatus.setTextColor(context.getColor(R.color.warning))
tvStatus.setBackgroundResource(R.drawable.bg_pill_orange)
}
4 -> {
tvStatus.text = "进行中"
tvStatus.setTextColor(context.getColor(R.color.success))
tvStatus.setBackgroundResource(R.drawable.bg_pill_green)
}
else -> {
tvStatus.text = "已完成"
tvStatus.setTextColor(context.getColor(R.color.text_secondary))
tvStatus.background = null
}
}
// 点击事件
itemView.setOnClickListener { onItemClick(task) }
}
}
}

View File

@@ -1,15 +1,141 @@
package com.xiaoqu.watch.ui.task
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.os.bundleOf
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.xiaoqu.watch.R
import com.xiaoqu.watch.databinding.FragmentTaskListBinding
import com.xiaoqu.watch.network.ApiResult
import com.xiaoqu.watch.network.api.TaskApi
import com.xiaoqu.watch.network.safeApiCall
import com.xiaoqu.watch.ui.common.BaseFragment
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
/**
* 任务列表页
* 分段控件切换状态接单池2/待打卡3/待完成4
* RecyclerView 显示任务卡片,点击进入详情
*/
@AndroidEntryPoint
class TaskListFragment : BaseFragment<FragmentTaskListBinding>() {
@Inject lateinit var taskApi: TaskApi
/** 当前选中的状态 */
private var currentStatus = 2
/** 列表适配器 */
private lateinit var adapter: TaskListAdapter
/** 分段控件的 3 个 Tab */
private lateinit var segTabs: List<TextView>
override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentTaskListBinding {
return FragmentTaskListBinding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 从导航参数获取初始状态(首页快捷区传入)
currentStatus = arguments?.getInt("tableStatus", 2) ?: 2
// 返回按钮
binding.btnBack.setOnClickListener {
findNavController().popBackStack()
}
// 初始化 RecyclerView
adapter = TaskListAdapter { task ->
// 点击任务卡片 → 跳转详情
val bundle = bundleOf("taskId" to task.id)
findNavController().navigate(R.id.action_taskList_to_detail, bundle)
}
binding.rvTasks.layoutManager = LinearLayoutManager(requireContext())
binding.rvTasks.adapter = adapter
// 初始化分段控件
segTabs = listOf(binding.segPool, binding.segPunch, binding.segComplete)
binding.segPool.setOnClickListener { switchStatus(2) }
binding.segPunch.setOnClickListener { switchStatus(3) }
binding.segComplete.setOnClickListener { switchStatus(4) }
// 下拉刷新
binding.swipeRefresh.setOnRefreshListener { fetchTasks() }
// 设置初始状态并加载数据
switchStatus(currentStatus)
}
/** 从详情页返回时自动刷新 */
override fun onResume() {
super.onResume()
fetchTasks()
}
/** 切换状态 Tab */
private fun switchStatus(status: Int) {
currentStatus = status
updateSegmentUI()
updateTitle()
fetchTasks()
}
/** 更新分段控件高亮 */
private fun updateSegmentUI() {
val statusList = listOf(2, 3, 4)
for (i in segTabs.indices) {
if (statusList[i] == currentStatus) {
segTabs[i].setBackgroundResource(R.drawable.bg_seg_active)
segTabs[i].setTextColor(requireContext().getColor(R.color.text_primary))
} else {
segTabs[i].background = null
segTabs[i].setTextColor(requireContext().getColor(R.color.text_secondary))
}
}
}
/** 更新页面标题 */
private fun updateTitle() {
binding.tvTitle.text = when (currentStatus) {
2 -> "接单池"
3 -> "待打卡"
4 -> "待完成"
else -> "任务列表"
}
}
/** 从 API 获取任务列表 */
private fun fetchTasks() {
viewLifecycleOwner.lifecycleScope.launch {
binding.swipeRefresh.isRefreshing = true
val result = safeApiCall { taskApi.getTaskList(currentStatus) }
binding.swipeRefresh.isRefreshing = false
when (result) {
is ApiResult.Success -> {
val tasks = result.data ?: emptyList()
Timber.d("任务列表: status=$currentStatus, count=${tasks.size}")
adapter.submitList(tasks)
}
is ApiResult.Error -> {
Timber.w("任务列表: API 错误 code=${result.code} ${result.message}")
adapter.submitList(emptyList())
}
is ApiResult.NetworkError -> {
Timber.w("任务列表: 网络异常")
adapter.submitList(emptyList())
}
}
}
}
}