feat: 用户上报任务显示图片附件

- 新建 PicItem 数据类,uploadPic 改为强类型
- 布局加 picContainer 横排展示区
- showPictures 方法:最多3张缩略图横排,HTTP 加载
- 三个状态页面都显示图片(和语音同级)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
dongliang
2026-05-07 12:04:26 +09:30
parent 1b198e0941
commit 49732ac79a
4 changed files with 87 additions and 2 deletions

View File

@@ -0,0 +1,12 @@
package com.xiaoqu.watch.data.task
import com.google.gson.annotations.SerializedName
/**
* 图片附件数据类
* 用户上报任务可能包含图片描述
*/
data class PicItem(
/** 图片 URL */
@SerializedName("url") val url: String = ""
)

View File

@@ -54,8 +54,8 @@ data class TaskDetail(
@SerializedName("standard") val standard: String = "",
/** 任务要求(黄色提示) */
@SerializedName("taskRequire") val taskRequire: String? = null,
/** 图片附件 */
@SerializedName("uploadPic") val uploadPic: List<Any>? = null,
/** 图片附件(用户上报任务可能包含图片) */
@SerializedName("uploadPic") val uploadPic: List<PicItem>? = null,
/** 语音附件(用户上报任务可能包含语音描述) */
@SerializedName("voice") val voice: List<VoiceItem>? = null,
/** 打卡标志 */
@@ -67,6 +67,9 @@ data class TaskDetail(
/** 是否有语音附件(用户上报任务) */
val hasVoice: Boolean get() = !voice.isNullOrEmpty()
/** 是否有图片附件(用户上报任务) */
val hasPictures: Boolean get() = !uploadPic.isNullOrEmpty()
/** 地点显示文字(多个用逗号分隔) */
val positionText: String get() = taskPositions?.joinToString(",") ?: ""

View File

@@ -260,6 +260,8 @@ class TaskListFragment : BaseFragment<FragmentTaskListBinding>() {
binding.tvPosition.visibility = View.GONE
binding.tvTimeInfo.visibility = View.GONE
binding.tvPoints.visibility = View.GONE
binding.picContainer.visibility = View.GONE
binding.picContainer.removeAllViews()
binding.tvNote.visibility = View.GONE
binding.btnVoice.visibility = View.GONE
binding.blockGoWhere.visibility = View.GONE
@@ -284,6 +286,7 @@ class TaskListFragment : BaseFragment<FragmentTaskListBinding>() {
// 备注/描述
showNote(detail)
showVoice(detail)
showPictures(detail)
}
// ===== 待打卡:地点最优先,其他次要 =====
@@ -314,6 +317,7 @@ class TaskListFragment : BaseFragment<FragmentTaskListBinding>() {
// 备注和语音放在指引块后面(次要信息)
showNote(detail)
showVoice(detail)
showPictures(detail)
}
// ===== 进行中/待完成:地点+打卡时间+完成指引 =====
@@ -331,6 +335,7 @@ class TaskListFragment : BaseFragment<FragmentTaskListBinding>() {
}
showNote(detail)
showVoice(detail)
showPictures(detail)
if (!detail.confirmTime.isNullOrEmpty()) {
binding.tvCheckinTime.text = "${detail.confirmTime} 已打卡"
binding.tvCheckinTime.visibility = View.VISIBLE
@@ -404,6 +409,62 @@ class TaskListFragment : BaseFragment<FragmentTaskListBinding>() {
}
}
/**
* 显示图片缩略图(用户上报任务 taskType=3/4 可能有图片附件)
* 横排展示最多3张点击查看大图
*/
private fun showPictures(detail: TaskDetail) {
if (!detail.hasPictures) return
val pics = detail.uploadPic!!.filter { it.url.isNotEmpty() }
if (pics.isEmpty()) return
binding.picContainer.visibility = View.VISIBLE
binding.picContainer.removeAllViews()
// 计算每张图片尺寸:容器宽度 / 最多3列留 gap
val maxCount = minOf(pics.size, 3)
val gap = 8 // dp
val containerWidth = resources.displayMetrics.widthPixels -
(21 * 2 * resources.displayMetrics.density).toInt() // 减去 safe area padding
val imgSize = (containerWidth - gap * (maxCount - 1) *
resources.displayMetrics.density.toInt()) / maxCount
for ((index, pic) in pics.take(3).withIndex()) {
val imageView = android.widget.ImageView(requireContext()).apply {
layoutParams = android.widget.LinearLayout.LayoutParams(imgSize, imgSize).apply {
if (index > 0) marginStart = (gap * resources.displayMetrics.density).toInt()
}
scaleType = android.widget.ImageView.ScaleType.CENTER_CROP
setBackgroundColor(requireContext().getColor(R.color.card_background))
}
// 用 HTTP 加载图片(和语音一样的 TLS 问题)
val httpUrl = pic.url.replace("https://", "http://")
loadImage(imageView, httpUrl)
binding.picContainer.addView(imageView)
}
}
/** 异步加载图片到 ImageView */
private fun loadImage(imageView: android.widget.ImageView, url: String) {
viewLifecycleOwner.lifecycleScope.launch {
try {
val bitmap = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) {
val request = Request.Builder().url(url).build()
val response = downloadClient.newCall(request).execute()
if (response.isSuccessful) {
response.body?.byteStream()?.let {
android.graphics.BitmapFactory.decodeStream(it)
}
} else null
}
bitmap?.let { imageView.setImageBitmap(it) }
} catch (e: Exception) {
Timber.w(e, "图片加载失败: $url")
}
}
}
/** 更新语音按钮的图标和背景 */
private fun updateVoiceUI(playing: Boolean) {
if (playing) {

View File

@@ -179,6 +179,15 @@
</LinearLayout>
<!-- 图片展示区(用户上报任务附带图片) -->
<LinearLayout
android:id="@+id/picContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="11dp"
android:visibility="gone" />
<!-- 指引块1去哪里橙色 -->
<LinearLayout
android:id="@+id/blockGoWhere"