feat: 任务列表改为单任务全屏展示+上下滑切换(方案B,和旧版一致)

核心改动:
- queryTaskIds 获取ID列表 → lookTaskDetail 获取当前任务详情
- 上滑下一个、下滑上一个任务
- 顶部显示"第X/Y个任务"
- 派单时间+任务名(带类型前缀)+地点+派单号+积分+协作人+描述
- 底部固定操作按钮(抢单/打卡/完成)
- 操作成功后刷新列表+详情
- 空状态+Loading状态

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
dongliang
2026-04-28 10:34:44 +09:30
parent aea7b349b8
commit 5e2d71c25d
2 changed files with 571 additions and 150 deletions

View File

@@ -1,93 +1,131 @@
package com.xiaoqu.watch.ui.task
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.GestureDetector
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
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.data.task.TaskDetail
import com.xiaoqu.watch.data.task.TaskItem
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 com.xiaoqu.watch.ui.widget.QuTipDialog
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import kotlin.math.abs
/**
* 任务列表页
* 分段控件切换状态接单池2/待打卡3/待完成4
* RecyclerView 显示任务卡片,点击进入详情
* 任务列表页(单任务全屏展示,上下滑切换)
*
* 流程(和旧版一致):
* 1. queryTaskIds 获取 ID+name 列表
* 2. lookTaskDetail 获取当前任务完整详情
* 3. 上滑/下滑切换 taskIndex → 重新获取详情
* 4. 操作成功 → 刷新详情 → 刷新列表
*/
@AndroidEntryPoint
class TaskListFragment : BaseFragment<FragmentTaskListBinding>() {
@Inject lateinit var taskApi: TaskApi
/** 当前选中的状态 */
/** 任务 ID 列表queryTaskIds 返回) */
private var taskList: List<TaskItem> = emptyList()
/** 当前任务索引 */
private var taskIndex = 0
/** 当前任务详情 */
private var currentDetail: TaskDetail? = null
/** 当前状态筛选 */
private var currentStatus = 2
/** 列表适配器 */
private lateinit var adapter: TaskListAdapter
/** 分段控件的 3 个 Tab */
/** 分段控件 Tab */
private lateinit var segTabs: List<TextView>
/** 提示弹窗 */
private lateinit var tipDialog: QuTipDialog
/** 手势检测(上下滑切换任务) */
private lateinit var gestureDetector: GestureDetector
override fun createBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentTaskListBinding {
return FragmentTaskListBinding.inflate(inflater, container, false)
}
@SuppressLint("ClickableViewAccessibility")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 从导航参数获取初始状态(首页快捷区传入)
// 从导航参数获取初始状态
currentStatus = arguments?.getInt("tableStatus", 2) ?: 2
// 初始化弹窗
val dialogContainer = requireActivity().findViewById<FrameLayout>(R.id.dialog_container)
tipDialog = QuTipDialog(dialogContainer)
// 返回按钮
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() }
// 上下滑手势(切换任务)
gestureDetector = GestureDetector(requireContext(), object : GestureDetector.SimpleOnGestureListener() {
override fun onFling(e1: MotionEvent?, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
if (e1 == null) return false
val dy = e2.y - e1.y
val dx = e2.x - e1.x
// 垂直滑动且幅度 > 水平
if (abs(dy) > abs(dx) && abs(dy) > 50) {
if (dy < 0) {
// 上滑 → 下一个任务
nextTask()
} else {
// 下滑 → 上一个任务
prevTask()
}
return true
}
return false
}
})
// 设置初始状态并加载数据
// 给 ScrollView 设置触摸监听
binding.scrollView.setOnTouchListener { _, event ->
gestureDetector.onTouchEvent(event)
false // 不拦截,让 ScrollView 继续处理滚动
}
// 初始化
switchStatus(currentStatus)
}
/** 从详情页返回时自动刷新 */
override fun onResume() {
super.onResume()
fetchTasks()
}
// ===== 状态切换 =====
/** 切换状态 Tab */
private fun switchStatus(status: Int) {
currentStatus = status
taskIndex = 0
updateSegmentUI()
updateTitle()
fetchTasks()
fetchTaskIds()
}
/** 更新分段控件高亮 */
@@ -104,8 +142,266 @@ class TaskListFragment : BaseFragment<FragmentTaskListBinding>() {
}
}
/** 更新页面标题 */
private fun updateTitle() {
// ===== 数据获取 =====
/** 第一步:获取任务 ID 列表 */
private fun fetchTaskIds() {
showLoading()
viewLifecycleOwner.lifecycleScope.launch {
val params = hashMapOf<String, Any>("status" to currentStatus)
val result = safeApiCall { taskApi.getTaskIds(params) }
when (result) {
is ApiResult.Success -> {
taskList = result.data ?: emptyList()
Timber.d("任务列表: status=$currentStatus, count=${taskList.size}")
if (taskList.isNotEmpty()) {
// 确保 taskIndex 在范围内
if (taskIndex >= taskList.size) taskIndex = taskList.size - 1
fetchCurrentDetail()
} else {
showEmpty()
}
}
is ApiResult.Error -> {
Timber.w("任务列表: API 错误 ${result.code} ${result.message}")
taskList = emptyList()
showEmpty()
}
is ApiResult.NetworkError -> {
Timber.w("任务列表: 网络异常")
taskList = emptyList()
showEmpty()
}
}
}
}
/** 第二步:获取当前任务详情 */
private fun fetchCurrentDetail() {
if (taskList.isEmpty()) return
val taskId = taskList[taskIndex].id
viewLifecycleOwner.lifecycleScope.launch {
val params = hashMapOf<String, Any>("id" to taskId)
val result = safeApiCall { taskApi.getTaskDetail(params) }
when (result) {
is ApiResult.Success -> {
result.data?.let { detail ->
currentDetail = detail
displayDetail(detail)
setupActionButton(detail)
}
}
is ApiResult.Error -> {
Timber.w("任务详情: API 错误 ${result.code}")
}
is ApiResult.NetworkError -> {
Timber.w("任务详情: 网络异常")
}
}
}
}
// ===== 上下滑切换 =====
/** 下一个任务(上滑) */
private fun nextTask() {
if (taskIndex < taskList.size - 1) {
taskIndex++
fetchCurrentDetail()
}
}
/** 上一个任务(下滑) */
private fun prevTask() {
if (taskIndex > 0) {
taskIndex--
fetchCurrentDetail()
}
}
// ===== UI 显示 =====
/** 显示任务详情 */
private fun displayDetail(detail: TaskDetail) {
binding.taskContent.visibility = View.VISIBLE
binding.tvEmpty.visibility = View.GONE
binding.loadingWrap.visibility = View.GONE
// 标题第X/Y个任务
binding.tvTitle.text = "${taskIndex + 1}/${taskList.size}个任务"
// 派单时间
if (detail.beginTime.isNotEmpty()) {
val timePart = detail.beginTime.split(" ").lastOrNull() ?: detail.beginTime
binding.tvSendTime.text = "${timePart}派单"
binding.tvSendTime.visibility = View.VISIBLE
} else {
binding.tvSendTime.visibility = View.GONE
}
// 任务名(带类型前缀)
val prefix = when (detail.taskType) {
2 -> "指派:"
3, 4 -> "上报:"
5 -> "巡检:"
else -> ""
}
binding.tvTaskName.text = "$prefix${detail.taskName.ifEmpty { detail.no }}"
// 地点
if (detail.positionName.isNotEmpty()) {
binding.tvPosition.text = detail.positionName
binding.tvPosition.visibility = View.VISIBLE
} else {
binding.tvPosition.visibility = View.GONE
}
// 派单号
binding.tvNo.text = detail.no
// 积分
binding.tvPoints.text = if (detail.point > 0) "+${detail.point}" else "0"
// 协作人
if (detail.executorName.isNotEmpty()) {
binding.tvWorkers.text = detail.executorName
binding.rowWorkers.visibility = View.VISIBLE
} else {
binding.rowWorkers.visibility = View.GONE
}
// 描述
if (detail.description.isNotEmpty()) {
binding.tvDescription.text = detail.description
binding.tvDescription.visibility = View.VISIBLE
} else {
binding.tvDescription.visibility = View.GONE
}
// 提示条(待打卡+有场景)
if (detail.status == 3 && detail.hasPosition) {
binding.tvHint.visibility = View.VISIBLE
} else {
binding.tvHint.visibility = View.GONE
}
}
/** 根据状态设置底部操作按钮 */
private fun setupActionButton(detail: TaskDetail) {
val btn = binding.btnAction
btn.visibility = View.VISIBLE
when (detail.status) {
// 待抢单 → 蓝色「抢 单」
2 -> {
btn.text = "抢 单"
btn.setBackgroundResource(R.drawable.bg_foot_btn_blue)
btn.setTextColor(requireContext().getColor(R.color.text_primary))
btn.setOnClickListener { doAction("grab", detail.id) }
}
// 待打卡
3 -> {
if (detail.hasPosition) {
// 有场景 → 橙色「开启打卡」
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.setTextColor(requireContext().getColor(R.color.text_primary))
btn.setOnClickListener { doAction("assign", detail.id) }
}
}
// 进行中 → 绿色「确认完成」
4 -> {
btn.text = "确认完成"
btn.setBackgroundResource(R.drawable.bg_foot_btn_green)
btn.setTextColor(requireContext().getColor(R.color.text_primary))
btn.setOnClickListener { doAction("complete", detail.id) }
}
// 其他 → 灰色「返回」
else -> {
btn.text = "返 回"
btn.setBackgroundResource(R.drawable.bg_foot_btn_blue)
btn.setTextColor(requireContext().getColor(R.color.text_primary))
btn.setOnClickListener { findNavController().popBackStack() }
}
}
}
// ===== 操作 =====
/** 执行任务操作(抢单/打卡/完成) */
private fun doAction(action: String, taskId: Long) {
viewLifecycleOwner.lifecycleScope.launch {
val params = hashMapOf<String, Any>("id" to taskId)
val result = when (action) {
"grab" -> safeApiCall { taskApi.grabTask(params) }
"assign" -> safeApiCall { taskApi.assignToBeginTask(params) }
"complete" -> safeApiCall { taskApi.completeTask(params) }
else -> return@launch
}
val successMsg = when (action) {
"grab" -> "抢单成功"
"assign" -> "打卡成功"
"complete" -> "任务已完成"
else -> "操作成功"
}
val failMsg = when (action) {
"grab" -> "抢单失败"
"assign" -> "打卡失败"
"complete" -> "完成失败"
else -> "操作失败"
}
when (result) {
is ApiResult.Success -> {
Timber.d("任务操作: $successMsg")
tipDialog.show(
status = QuTipDialog.Status.SUCCESS,
title = successMsg,
back = true, step = 0, countdown = 2
)
// 操作成功后刷新:重新获取列表和详情
fetchTaskIds()
}
is ApiResult.Error -> {
tipDialog.show(
status = QuTipDialog.Status.ERROR,
title = failMsg,
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
)
}
}
}
}
// ===== 状态显示 =====
/** 显示 Loading */
private fun showLoading() {
binding.taskContent.visibility = View.GONE
binding.tvEmpty.visibility = View.GONE
binding.loadingWrap.visibility = View.VISIBLE
binding.btnAction.visibility = View.GONE
binding.tvTitle.text = when (currentStatus) {
2 -> "接单池"
3 -> "待打卡"
@@ -114,29 +410,17 @@ class TaskListFragment : BaseFragment<FragmentTaskListBinding>() {
}
}
/** 从 API 获取任务列表 */
private fun fetchTasks() {
viewLifecycleOwner.lifecycleScope.launch {
binding.swipeRefresh.isRefreshing = true
val params = hashMapOf<String, Any>("status" to currentStatus)
val result = safeApiCall { taskApi.getTaskIds(params) }
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())
}
}
/** 显示空状态 */
private fun showEmpty() {
binding.taskContent.visibility = View.GONE
binding.tvEmpty.visibility = View.VISIBLE
binding.loadingWrap.visibility = View.GONE
binding.btnAction.visibility = View.GONE
binding.tvTitle.text = when (currentStatus) {
2 -> "接单池"
3 -> "待打卡"
4 -> "待完成"
else -> "任务列表"
}
}
}

View File

@@ -1,22 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 任务列表页:返回按钮 + 分段控件 + RecyclerView -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<!-- 任务列表页(单任务展示,上下滑切换)
顶部:返回 + "第X/Y个任务" + 分段控件
中间当前任务详情lookTaskDetail
底部:固定操作按钮 -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background"
android:orientation="vertical"
android:background="@color/background">
<!-- 可滑动内容区(底部留出按钮空间) -->
<ScrollView
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="none"
android:paddingStart="21dp"
android:paddingTop="27dp"
android:paddingEnd="21dp">
android:paddingEnd="21dp"
android:paddingBottom="72dp"
android:clipToPadding="false">
<!-- 页面头部:返回 + 标题 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- 页面头部:返回 + 标题第X/Y个任务 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:layout_marginBottom="8dp">
<!-- 返回按钮 -->
<TextView
android:id="@+id/btnBack"
android:layout_width="32dp"
@@ -26,14 +41,12 @@
android:textColor="@color/primary"
android:textSize="27sp" />
<!-- 标题 -->
<TextView
android:id="@+id/tvTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="接单池"
android:textColor="@color/text_primary"
android:textSize="20sp"
android:textStyle="bold"
@@ -84,20 +97,144 @@
</LinearLayout>
<!-- 任务列表(下拉刷新 + RecyclerView -->
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefresh"
<!-- 任务详情区域lookTaskDetail 数据 -->
<LinearLayout
android:id="@+id/taskContent"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvTasks"
<!-- 派单时间 -->
<TextView
android:id="@+id/tvSendTime"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="27dp" />
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="@color/text_primary"
android:textSize="22sp"
android:layout_marginBottom="8dp" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<!-- 任务名称(大字居中) -->
<TextView
android:id="@+id/tvTaskName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="@color/text_primary"
android:textSize="26sp"
android:textStyle="bold"
android:layout_marginBottom="8dp" />
<!-- 地点 -->
<TextView
android:id="@+id/tvPosition"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="@color/primary"
android:textSize="20sp"
android:layout_marginBottom="16dp" />
<!-- 信息行列表 -->
<LinearLayout style="@style/ConfigRow">
<TextView style="@style/ConfigLabel" android:text="派单号" />
<TextView android:id="@+id/tvNo" style="@style/ConfigValue"
android:textColor="@color/text_secondary" />
</LinearLayout>
<LinearLayout style="@style/ConfigRow">
<TextView style="@style/ConfigLabel" android:text="积分" />
<TextView android:id="@+id/tvPoints" style="@style/ConfigValue"
android:textColor="@color/warning" />
</LinearLayout>
<LinearLayout android:id="@+id/rowWorkers" style="@style/ConfigRow"
android:visibility="gone">
<TextView style="@style/ConfigLabel" android:text="协作人" />
<TextView android:id="@+id/tvWorkers" style="@style/ConfigValue" />
</LinearLayout>
<!-- 任务描述 -->
<TextView
android:id="@+id/tvDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/text_secondary"
android:textSize="17sp"
android:lineSpacingMultiplier="1.5"
android:layout_marginTop="8dp"
android:visibility="gone" />
<!-- 提示条(待打卡+有场景) -->
<TextView
android:id="@+id/tvHint"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="11dp"
android:text="请将手表贴近打卡信标"
android:textColor="@color/warning"
android:textSize="16sp"
android:textStyle="bold"
android:layout_marginTop="8dp"
android:visibility="gone" />
</LinearLayout>
<!-- 空状态 -->
<TextView
android:id="@+id/tvEmpty"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="暂无任务"
android:textColor="@color/text_secondary"
android:textSize="20sp"
android:paddingTop="80dp"
android:visibility="gone" />
<!-- Loading -->
<LinearLayout
android:id="@+id/loadingWrap"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:paddingTop="80dp"
android:visibility="gone">
<ProgressBar
android:layout_width="32dp"
android:layout_height="32dp"
android:indeterminateTint="@color/text_secondary" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="加载中"
android:textColor="@color/text_secondary"
android:textSize="16sp"
android:layout_marginTop="8dp" />
</LinearLayout>
</LinearLayout>
</ScrollView>
<!-- 底部固定操作按钮 -->
<TextView
android:id="@+id/btnAction"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:gravity="center"
android:padding="19dp"
android:textColor="@color/text_primary"
android:textSize="21sp"
android:textStyle="bold"
android:letterSpacing="0.05"
android:visibility="gone" />
</FrameLayout>