feat: 任务页面布局优化(接单池防挑选+截止时间+巡检引导+语音播放)
1. 接单池改名"新任务"、隐藏积分和页码(防止用户挑肥拣瘦) 2. 待打卡/待完成页面增加截止时间显示 3. 巡检场景三层视觉:已打卡绿色+时间、下一个橙色高亮、后续灰色弱化 4. 巡检场景超屏自动滚动到当前进度 5. 用户上报任务支持语音播放(橙色药丸按钮) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,9 @@ data class InspectScene(
|
||||
/** 是否已打卡(1=已打卡,0=未打卡) */
|
||||
@SerializedName("isCheck") val isCheck: Int = 0,
|
||||
/** 场景状态 */
|
||||
@SerializedName("status") val status: Int = 0
|
||||
@SerializedName("status") val status: Int = 0,
|
||||
/** 打卡时间(服务端返回,如 "14:35") */
|
||||
@SerializedName("checkTime") val checkTime: String? = null
|
||||
) {
|
||||
/** 是否已完成打卡 */
|
||||
val checked: Boolean get() = isCheck == 1
|
||||
|
||||
@@ -56,14 +56,17 @@ data class TaskDetail(
|
||||
@SerializedName("taskRequire") val taskRequire: String? = null,
|
||||
/** 图片附件 */
|
||||
@SerializedName("uploadPic") val uploadPic: List<Any>? = null,
|
||||
/** 语音附件 */
|
||||
@SerializedName("voice") val voice: List<Any>? = null,
|
||||
/** 语音附件(用户上报任务可能包含语音描述) */
|
||||
@SerializedName("voice") val voice: List<VoiceItem>? = null,
|
||||
/** 打卡标志 */
|
||||
@SerializedName("clockFlag") val clockFlag: Int = 0
|
||||
) {
|
||||
/** 是否有打卡地点(决定打卡方式:有=NFC,无=直接确认) */
|
||||
val hasPosition: Boolean get() = !taskPositions.isNullOrEmpty()
|
||||
|
||||
/** 是否有语音附件(用户上报任务) */
|
||||
val hasVoice: Boolean get() = !voice.isNullOrEmpty()
|
||||
|
||||
/** 地点显示文字(多个用逗号分隔) */
|
||||
val positionText: String get() = taskPositions?.joinToString(",") ?: ""
|
||||
|
||||
|
||||
14
app/src/main/java/com/xiaoqu/watch/data/task/VoiceItem.kt
Normal file
14
app/src/main/java/com/xiaoqu/watch/data/task/VoiceItem.kt
Normal file
@@ -0,0 +1,14 @@
|
||||
package com.xiaoqu.watch.data.task
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
/**
|
||||
* 语音附件数据类
|
||||
* 用户上报任务可能包含语音描述
|
||||
*/
|
||||
data class VoiceItem(
|
||||
/** 语音文件 URL */
|
||||
@SerializedName("url") val url: String = "",
|
||||
/** 语音时长(秒) */
|
||||
@SerializedName("voiceLength") val voiceLength: String = ""
|
||||
)
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.xiaoqu.watch.ui.task
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.Typeface
|
||||
import android.media.MediaPlayer
|
||||
import android.os.Bundle
|
||||
import android.view.GestureDetector
|
||||
import android.view.LayoutInflater
|
||||
@@ -62,6 +64,9 @@ class TaskListFragment : BaseFragment<FragmentTaskListBinding>() {
|
||||
/** 提示弹窗 */
|
||||
private lateinit var tipDialog: QuTipDialog
|
||||
|
||||
/** 语音播放器(用户上报任务的语音附件) */
|
||||
private var mediaPlayer: MediaPlayer? = null
|
||||
|
||||
/** 手势检测(上下滑切换任务) */
|
||||
private lateinit var gestureDetector: GestureDetector
|
||||
|
||||
@@ -117,6 +122,11 @@ class TaskListFragment : BaseFragment<FragmentTaskListBinding>() {
|
||||
fetchTaskIds()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
stopVoice()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
// ===== 数据获取 =====
|
||||
|
||||
/** 第一步:获取任务 ID 列表 */
|
||||
@@ -173,6 +183,8 @@ class TaskListFragment : BaseFragment<FragmentTaskListBinding>() {
|
||||
/** 第二步:获取当前任务详情 */
|
||||
private fun fetchCurrentDetail() {
|
||||
if (taskList.isEmpty()) return
|
||||
// 切换任务时停止语音播放
|
||||
stopVoice()
|
||||
val taskId = taskList[taskIndex].id
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
@@ -226,15 +238,15 @@ class TaskListFragment : BaseFragment<FragmentTaskListBinding>() {
|
||||
binding.tvEmpty.visibility = View.GONE
|
||||
binding.loadingWrap.visibility = View.GONE
|
||||
|
||||
// 标题栏:状态名
|
||||
binding.tvTitle.text = when (currentStatus) {
|
||||
2 -> "接单池"
|
||||
3 -> "待打卡"
|
||||
4 -> "待完成"
|
||||
else -> "任务"
|
||||
// 标题栏
|
||||
binding.tvTitle.text = statusTitle()
|
||||
// 页码:接单池不显示(防止用户比较挑选),其他正常显示
|
||||
if (currentStatus == 2) {
|
||||
binding.tvPageNum.visibility = View.GONE
|
||||
} else {
|
||||
binding.tvPageNum.visibility = View.VISIBLE
|
||||
binding.tvPageNum.text = "${taskIndex + 1}/${taskList.size}"
|
||||
}
|
||||
// 页码
|
||||
binding.tvPageNum.text = "${taskIndex + 1}/${taskList.size}"
|
||||
|
||||
// 任务名(始终显示,最大最突出)
|
||||
binding.tvTaskName.text = detail.displayName
|
||||
@@ -244,6 +256,7 @@ class TaskListFragment : BaseFragment<FragmentTaskListBinding>() {
|
||||
binding.tvTimeInfo.visibility = View.GONE
|
||||
binding.tvPoints.visibility = View.GONE
|
||||
binding.tvNote.visibility = View.GONE
|
||||
binding.btnVoice.visibility = View.GONE
|
||||
binding.blockGoWhere.visibility = View.GONE
|
||||
binding.blockHowTo.visibility = View.GONE
|
||||
binding.blockNoScene.visibility = View.GONE
|
||||
@@ -274,19 +287,17 @@ class TaskListFragment : BaseFragment<FragmentTaskListBinding>() {
|
||||
binding.tvTimeInfo.text = timeLines.joinToString("\n")
|
||||
binding.tvTimeInfo.visibility = View.VISIBLE
|
||||
}
|
||||
// 积分
|
||||
if (detail.point > 0) {
|
||||
binding.tvPoints.text = "积分: ${detail.pointText}"
|
||||
binding.tvPoints.visibility = View.VISIBLE
|
||||
}
|
||||
// 接单池不显示积分(防止用户挑肥拣瘦,接单后才能看到积分)
|
||||
// 备注/描述
|
||||
showNote(detail)
|
||||
showVoice(detail)
|
||||
}
|
||||
|
||||
// ===== 待打卡:指引去哪+怎么做 =====
|
||||
3 -> {
|
||||
// 巡检时段
|
||||
// 时间信息:先巡检时段(直接写入),再截止时间(追加模式)
|
||||
showInspectTime(detail)
|
||||
showDeadline(detail)
|
||||
// 备注
|
||||
showNote(detail)
|
||||
|
||||
@@ -303,8 +314,9 @@ class TaskListFragment : BaseFragment<FragmentTaskListBinding>() {
|
||||
|
||||
// ===== 进行中/待完成:地点+打卡时间+完成指引 =====
|
||||
4 -> {
|
||||
// 巡检时段
|
||||
// 时间信息:先巡检时段(直接写入),再截止时间(追加模式)
|
||||
showInspectTime(detail)
|
||||
showDeadline(detail)
|
||||
if (detail.taskType == 5) {
|
||||
// ===== 巡检任务:显示场景打卡清单 =====
|
||||
showInspectScenes(detail)
|
||||
@@ -315,9 +327,10 @@ class TaskListFragment : BaseFragment<FragmentTaskListBinding>() {
|
||||
binding.tvPosition.visibility = View.VISIBLE
|
||||
}
|
||||
showNote(detail)
|
||||
showVoice(detail)
|
||||
if (!detail.confirmTime.isNullOrEmpty()) {
|
||||
binding.tvCheckinTime.text = "${detail.confirmTime} \u5DF2\u6253\u5361"
|
||||
binding.tvCheckinTime.visibility = View.VISIBLE
|
||||
binding.tvCheckinTime.text = "${detail.confirmTime} 已打卡"
|
||||
binding.tvCheckinTime.visibility = View.VISIBLE
|
||||
}
|
||||
binding.blockInProgress.visibility = View.VISIBLE
|
||||
}
|
||||
@@ -333,6 +346,28 @@ class TaskListFragment : BaseFragment<FragmentTaskListBinding>() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示截止/结束时间(所有状态通用)
|
||||
* 优先级:expireTime > preFinishTime > executeTimeEnd
|
||||
*/
|
||||
private fun showDeadline(detail: TaskDetail) {
|
||||
val deadlineLines = mutableListOf<String>()
|
||||
if (!detail.expireTime.isNullOrEmpty()) {
|
||||
deadlineLines.add("截止: ${detail.expireTime}")
|
||||
}
|
||||
if (!detail.preFinishTime.isNullOrEmpty()) {
|
||||
deadlineLines.add("要求: ${detail.preFinishTime}完成")
|
||||
}
|
||||
if (deadlineLines.isNotEmpty()) {
|
||||
// 复用 tvTimeInfo(如果巡检时段已占用,则追加)
|
||||
val existing = if (binding.tvTimeInfo.visibility == View.VISIBLE) {
|
||||
binding.tvTimeInfo.text.toString() + "\n"
|
||||
} else ""
|
||||
binding.tvTimeInfo.text = existing + deadlineLines.joinToString("\n")
|
||||
binding.tvTimeInfo.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
/** 显示巡检时段(仅 taskType=5 时,拆为开始/结束两行) */
|
||||
private fun showInspectTime(detail: TaskDetail) {
|
||||
if (detail.taskType == 5) {
|
||||
@@ -359,9 +394,64 @@ class TaskListFragment : BaseFragment<FragmentTaskListBinding>() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示语音播放按钮(用户上报任务 taskType=3/4 可能有语音附件)
|
||||
* 点击播放/暂停语音
|
||||
*/
|
||||
private fun showVoice(detail: TaskDetail) {
|
||||
if (!detail.hasVoice) return
|
||||
|
||||
val voiceItem = detail.voice!!.first()
|
||||
if (voiceItem.url.isEmpty()) return
|
||||
|
||||
// 显示按钮和时长
|
||||
binding.tvVoiceDuration.text = "${voiceItem.voiceLength}\""
|
||||
binding.btnVoice.visibility = View.VISIBLE
|
||||
|
||||
// 点击播放/停止
|
||||
binding.btnVoice.setOnClickListener {
|
||||
toggleVoice(voiceItem.url)
|
||||
}
|
||||
}
|
||||
|
||||
/** 播放/停止语音 */
|
||||
private fun toggleVoice(url: String) {
|
||||
if (mediaPlayer?.isPlaying == true) {
|
||||
// 正在播放 → 停止
|
||||
stopVoice()
|
||||
} else {
|
||||
// 没有播放 → 开始
|
||||
stopVoice() // 清理上一个
|
||||
mediaPlayer = MediaPlayer().apply {
|
||||
setDataSource(url)
|
||||
setOnPreparedListener { start() }
|
||||
setOnCompletionListener { stopVoice() }
|
||||
setOnErrorListener { _, _, _ ->
|
||||
Timber.w("语音播放失败: $url")
|
||||
stopVoice()
|
||||
true
|
||||
}
|
||||
prepareAsync()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 停止并释放 MediaPlayer */
|
||||
private fun stopVoice() {
|
||||
mediaPlayer?.let {
|
||||
if (it.isPlaying) it.stop()
|
||||
it.release()
|
||||
}
|
||||
mediaPlayer = null
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示巡检场景打卡清单(taskType=5,status=4)
|
||||
* 每个场景一行:✅ 已打卡 / ❌ 未打卡
|
||||
*
|
||||
* 三种视觉层级:
|
||||
* - 已打卡:绿色 ● + 场景名 + 打卡时间(确定感)
|
||||
* - 下一个待打卡:橙色高亮 ▶ + 大字体(行动引导)
|
||||
* - 后续未打卡:灰色 ○ + 小字体(减少干扰)
|
||||
*/
|
||||
private fun showInspectScenes(detail: TaskDetail) {
|
||||
val scenes = detail.taskInspectScenes
|
||||
@@ -377,22 +467,71 @@ class TaskListFragment : BaseFragment<FragmentTaskListBinding>() {
|
||||
binding.tvInspectTitle.text = "打卡进度 $checkedCount/$totalCount"
|
||||
binding.blockInspect.visibility = View.VISIBLE
|
||||
|
||||
// 动态添加场景行(绿色圆点=已打卡,灰色圆圈=未打卡)
|
||||
// 找到第一个未打卡的场景索引(即"下一个")
|
||||
val nextIndex = scenes.indexOfFirst { !it.checked }
|
||||
|
||||
// 动态添加场景行
|
||||
binding.sceneList.removeAllViews()
|
||||
for (scene in scenes) {
|
||||
var nextView: View? = null
|
||||
|
||||
for ((index, scene) in scenes.withIndex()) {
|
||||
val tv = android.widget.TextView(requireContext()).apply {
|
||||
if (scene.checked) {
|
||||
text = " \u25CF ${scene.name}" // ● 实心圆
|
||||
setTextColor(requireContext().getColor(R.color.success))
|
||||
} else {
|
||||
text = " \u25CB ${scene.name}" // ○ 空心圆
|
||||
setTextColor(requireContext().getColor(R.color.text_secondary))
|
||||
when {
|
||||
// ===== 已打卡:绿色 + 打卡时间 =====
|
||||
scene.checked -> {
|
||||
val timeStr = if (!scene.checkTime.isNullOrEmpty()) " ${scene.checkTime}" else ""
|
||||
text = " \u25CF ${scene.name}$timeStr"
|
||||
setTextColor(requireContext().getColor(R.color.success))
|
||||
textSize = 26f
|
||||
setPadding(0, 11, 0, 11)
|
||||
}
|
||||
// ===== 下一个待打卡:橙色高亮 + 大字体 =====
|
||||
index == nextIndex -> {
|
||||
text = " \u25B6 ${scene.name}" // ▶ 三角箭头
|
||||
setTextColor(requireContext().getColor(R.color.warning))
|
||||
textSize = 32f
|
||||
setTypeface(null, Typeface.BOLD)
|
||||
setPadding(0, 16, 0, 16)
|
||||
}
|
||||
// ===== 后续未打卡:灰色 + 小字体 =====
|
||||
else -> {
|
||||
text = " \u25CB ${scene.name}" // ○ 空心圆
|
||||
setTextColor(requireContext().getColor(R.color.text_secondary))
|
||||
textSize = 24f
|
||||
setPadding(0, 9, 0, 9)
|
||||
}
|
||||
}
|
||||
textSize = 28f
|
||||
setPadding(0, 13, 0, 13)
|
||||
}
|
||||
binding.sceneList.addView(tv)
|
||||
// 记录下一个待打卡的 View,用于自动滚动
|
||||
if (index == nextIndex) nextView = tv
|
||||
}
|
||||
|
||||
// 自动滚动到当前进度位置(场景多超出屏幕时)
|
||||
nextView?.let { target ->
|
||||
binding.scrollView.post {
|
||||
// 计算目标 View 在 ScrollView 中的位置
|
||||
val scrollTo = calculateScrollTarget(target)
|
||||
binding.scrollView.smoothScrollTo(0, scrollTo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算目标 View 在 ScrollView 中的滚动位置
|
||||
* 将目标 View 滚动到屏幕中间偏上的位置
|
||||
*/
|
||||
private fun calculateScrollTarget(target: View): Int {
|
||||
// 累加 target 相对于 ScrollView 内容的 Y 偏移
|
||||
var offsetY = 0
|
||||
var current: View? = target
|
||||
while (current != null && current != binding.scrollView) {
|
||||
offsetY += current.top
|
||||
current = current.parent as? View
|
||||
}
|
||||
// 滚动到目标位置偏上 1/3 屏高,让用户能看到上下文
|
||||
val screenOffset = binding.scrollView.height / 3
|
||||
return (offsetY - screenOffset).coerceAtLeast(0)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -580,18 +719,21 @@ class TaskListFragment : BaseFragment<FragmentTaskListBinding>() {
|
||||
|
||||
// ===== 状态显示 =====
|
||||
|
||||
/** 获取当前状态对应的标题 */
|
||||
private fun statusTitle(): String = when (currentStatus) {
|
||||
2 -> "新任务"
|
||||
3 -> "待打卡"
|
||||
4 -> "待完成"
|
||||
else -> "任务列表"
|
||||
}
|
||||
|
||||
/** 显示 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 -> "待打卡"
|
||||
4 -> "待完成"
|
||||
else -> "任务列表"
|
||||
}
|
||||
binding.tvTitle.text = statusTitle()
|
||||
}
|
||||
|
||||
/** 显示空状态 */
|
||||
@@ -600,11 +742,6 @@ class TaskListFragment : BaseFragment<FragmentTaskListBinding>() {
|
||||
binding.tvEmpty.visibility = View.VISIBLE
|
||||
binding.loadingWrap.visibility = View.GONE
|
||||
binding.btnAction.visibility = View.GONE
|
||||
binding.tvTitle.text = when (currentStatus) {
|
||||
2 -> "接单池"
|
||||
3 -> "待打卡"
|
||||
4 -> "待完成"
|
||||
else -> "任务列表"
|
||||
}
|
||||
binding.tvTitle.text = statusTitle()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user