task(62a18e54-9143-4263-9927-83f1631aa54c): Add M3 task management module (2.6/2.7 task lists and detail)
S-M3 deliverables: TaskApi (2.6 GetUserPlanGroup / 2.7 GetPlanGroup), TaskModels, feature/task package (Repository/ViewModel/Workspace/Detail/ListAdapter), layouts and resources; FeatureMount Tab.TASK mounted; placeholder TaskFragment removed. CommitOnArchive was stuck after staging; committed by master chat as compensation.
This commit is contained in:
@@ -2,6 +2,7 @@ package com.stec.cmd
|
||||
|
||||
import com.stec.cmd.feature.home.ProjectWorkspaceFragment
|
||||
import com.stec.cmd.feature.mine.MineFragment
|
||||
import com.stec.cmd.feature.task.TaskWorkspaceFragment
|
||||
|
||||
/**
|
||||
* 底部导航 5 Tab 与业务模块(功能明细表编号)的挂载点注册中心。
|
||||
@@ -32,7 +33,7 @@ object FeatureMount {
|
||||
/** 挂载注册表:S1+ 替换 fragmentClass 即完成业务页挂载。 */
|
||||
val mounts: List<MountItem> = listOf(
|
||||
MountItem(Tab.HOME, ProjectWorkspaceFragment::class.java, "M2 项目工作台"),
|
||||
MountItem(Tab.TASK, TaskFragment::class.java, "M3 任务管理"),
|
||||
MountItem(Tab.TASK, TaskWorkspaceFragment::class.java, "M3 任务管理"),
|
||||
MountItem(Tab.COLLECT, CollectFragment::class.java, "M4+M5 数据采集"),
|
||||
MountItem(Tab.STATS, StatsFragment::class.java, "M6 统计与上传"),
|
||||
MountItem(Tab.MINE, MineFragment::class.java, "M1 我的"),
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.stec.cmd
|
||||
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
/** 任务占位(M3 任务管理,S2 挂载任务列表)。 */
|
||||
@AndroidEntryPoint
|
||||
class TaskFragment : PlaceholderFragment()
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.stec.cmd.feature.task
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.stec.cmd.R
|
||||
import com.stec.cmd.core.network.api.TaskItem
|
||||
import com.stec.cmd.databinding.FragmentTaskDetailBinding
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
/**
|
||||
* 任务详情(M3-03 测组/测点完成度/计划周期展示)。
|
||||
*
|
||||
* 数据来自列表条目本身(明细表 M3-03 对应 API「2.6/2.7 data」,2.9 测点列表归属 M5),
|
||||
* 经 arguments JSON 传参(ProjectSwitcherSheet 同款先例),纯展示无网络交互——
|
||||
* 故不设 ViewModel(MVVM 最小状态机原则,见 design 留档)。
|
||||
*/
|
||||
class TaskDetailFragment : Fragment() {
|
||||
|
||||
private var _binding: FragmentTaskDetailBinding? = null
|
||||
private val binding get() = checkNotNull(_binding)
|
||||
|
||||
/** 传参解析失败时退化为全「-」展示(数据源为本端自编码 JSON,正常不触发)。 */
|
||||
private val task: TaskItem? by lazy {
|
||||
arguments?.getString(ARG_TASK_JSON)?.let { raw ->
|
||||
runCatching { JSON.decodeFromString(TaskItem.serializer(), raw) }.getOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View {
|
||||
_binding = FragmentTaskDetailBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
binding.toolbar.setNavigationOnClickListener { parentFragmentManager.popBackStack() }
|
||||
render()
|
||||
}
|
||||
|
||||
private fun render() {
|
||||
val data = task
|
||||
binding.tvDetailCode.text = data?.TaskCode.orDash()
|
||||
binding.tvDetailMold.text = data?.Mold.orDash()
|
||||
binding.tvDetailMonitoring.text = data?.MonitoringType.orDash()
|
||||
binding.tvDetailDuty.text = data?.DutyName.orDash()
|
||||
binding.tvDetailProject.text = data?.SimpleName.orDash()
|
||||
binding.tvDetailWorkPoint.text = data?.WorkPointName.orDash()
|
||||
binding.tvDetailPlanId.text = data?.PlanID.orDash()
|
||||
binding.tvDetailPeriod.text = data?.period
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?.let { getString(R.string.task_detail_label_period_fmt, it) }
|
||||
?: getString(R.string.task_detail_unknown)
|
||||
binding.tvDetailBegin.text = data?.BeginTime.orDash()
|
||||
binding.tvDetailEnd.text = data?.EndTime.orDash()
|
||||
binding.tvDetailGroup.text = data?.GroupID.orDash()
|
||||
binding.tvDetailDegree.text = data?.CompleteDegree.orDash()
|
||||
binding.tvDetailWorking.text = data?.Working.orDash()
|
||||
|
||||
degreePercent(data?.CompleteDegree)?.let { percent ->
|
||||
binding.progressDegree.isVisible = true
|
||||
binding.progressDegree.setProgressCompat(percent, true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun String?.orDash(): String =
|
||||
this?.takeIf { it.isNotBlank() } ?: getString(R.string.task_detail_unknown)
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ARG_TASK_JSON = "task_json"
|
||||
|
||||
private val JSON = Json {
|
||||
ignoreUnknownKeys = true
|
||||
encodeDefaults = true
|
||||
}
|
||||
|
||||
fun newInstance(taskJson: String): TaskDetailFragment =
|
||||
TaskDetailFragment().apply {
|
||||
arguments = Bundle().apply { putString(ARG_TASK_JSON, taskJson) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.stec.cmd.feature.task
|
||||
|
||||
import android.content.res.ColorStateList
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.stec.cmd.R
|
||||
import com.stec.cmd.core.network.api.TaskItem
|
||||
import com.stec.cmd.databinding.ItemTaskBinding
|
||||
|
||||
/**
|
||||
* 任务列表适配器(M3-01/02 条目 + M3-04 状态 Chip 三态配色)。
|
||||
*
|
||||
* 状态 Chip 配色:未开始=surfaceVariant 灰 / 进行中=primaryContainer / 已完成=brand_success 绿。
|
||||
*/
|
||||
class TaskListAdapter(
|
||||
private val onClick: (TaskItem) -> Unit,
|
||||
) : ListAdapter<TaskItem, TaskListAdapter.TaskViewHolder>(DIFF) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TaskViewHolder {
|
||||
val binding = ItemTaskBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return TaskViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: TaskViewHolder, position: Int) {
|
||||
holder.bind(getItem(position))
|
||||
}
|
||||
|
||||
inner class TaskViewHolder(
|
||||
private val binding: ItemTaskBinding,
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(task: TaskItem) {
|
||||
val context = binding.root.context
|
||||
binding.tvTaskCode.text = task.TaskCode?.takeIf { it.isNotBlank() }
|
||||
?: context.getString(R.string.task_detail_unknown)
|
||||
binding.chipStatus.applyStatus(statusOf(task))
|
||||
binding.tvTaskType.text = task.MonitoringType?.takeIf { it.isNotBlank() }
|
||||
?: context.getString(R.string.task_detail_unknown)
|
||||
binding.tvTaskMold.text = task.Mold.orEmpty()
|
||||
binding.tvTaskMold.isVisible = !task.Mold.isNullOrBlank()
|
||||
binding.tvTaskMeta.text =
|
||||
listOf(task.WorkPointName, task.DutyName)
|
||||
.filter { !it.isNullOrBlank() }
|
||||
.joinToString(" · ")
|
||||
binding.tvTaskMeta.isVisible = binding.tvTaskMeta.text.isNotBlank()
|
||||
binding.tvTaskPeriod.text = context.getString(
|
||||
R.string.task_item_period_fmt,
|
||||
task.BeginTime?.takeIf { it.isNotBlank() } ?: context.getString(R.string.task_detail_unknown),
|
||||
task.EndTime?.takeIf { it.isNotBlank() } ?: context.getString(R.string.task_detail_unknown),
|
||||
)
|
||||
binding.tvTaskDegree.text = context.getString(
|
||||
R.string.task_item_degree_fmt,
|
||||
task.CompleteDegree?.takeIf { it.isNotBlank() }
|
||||
?: context.getString(R.string.task_detail_unknown),
|
||||
)
|
||||
binding.root.setOnClickListener { onClick(task) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun Chip.applyStatus(status: TaskStatus) {
|
||||
val context = context
|
||||
text = context.getString(status.labelRes)
|
||||
val (background, foreground) = when (status) {
|
||||
TaskStatus.NOT_STARTED ->
|
||||
MaterialColors.getColor(this, com.google.android.material.R.attr.colorSurfaceVariant) to
|
||||
MaterialColors.getColor(this, com.google.android.material.R.attr.colorOnSurfaceVariant)
|
||||
TaskStatus.IN_PROGRESS ->
|
||||
MaterialColors.getColor(this, com.google.android.material.R.attr.colorPrimaryContainer) to
|
||||
MaterialColors.getColor(this, com.google.android.material.R.attr.colorOnPrimaryContainer)
|
||||
TaskStatus.COMPLETED ->
|
||||
ContextCompat.getColor(context, R.color.brand_success) to ContextCompat.getColor(context, R.color.brand_on_primary)
|
||||
}
|
||||
chipBackgroundColor = ColorStateList.valueOf(background)
|
||||
setTextColor(foreground)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val DIFF = object : DiffUtil.ItemCallback<TaskItem>() {
|
||||
override fun areItemsTheSame(oldItem: TaskItem, newItem: TaskItem): Boolean =
|
||||
oldItem.TaskCode == newItem.TaskCode &&
|
||||
oldItem.GroupID == newItem.GroupID &&
|
||||
oldItem.PlanID == newItem.PlanID
|
||||
|
||||
override fun areContentsTheSame(oldItem: TaskItem, newItem: TaskItem): Boolean =
|
||||
oldItem == newItem
|
||||
}
|
||||
}
|
||||
}
|
||||
61
app/src/main/kotlin/com/stec/cmd/feature/task/TaskModels.kt
Normal file
61
app/src/main/kotlin/com/stec/cmd/feature/task/TaskModels.kt
Normal file
@@ -0,0 +1,61 @@
|
||||
package com.stec.cmd.feature.task
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import com.stec.cmd.R
|
||||
import com.stec.cmd.core.network.api.TaskItem
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
/**
|
||||
* 任务状态流转(M3-04)。文档 2.6/2.7 的 data 无显式状态字段,
|
||||
* 按 design 兜底序从 CompleteDegree 与 BeginTime 推导:
|
||||
* 完成度可解析且已完成 → 已完成;BeginTime 可解析且晚于当前 → 未开始;其余 → 进行中。
|
||||
*/
|
||||
enum class TaskStatus(@StringRes val labelRes: Int) {
|
||||
NOT_STARTED(R.string.task_status_not_started),
|
||||
IN_PROGRESS(R.string.task_status_in_progress),
|
||||
COMPLETED(R.string.task_status_completed),
|
||||
}
|
||||
|
||||
/** 文档口径的计划周期格式:yyyy-MM-dd HH:mm(2.6/2.7 BeginTime/EndTime)。 */
|
||||
private val TASK_TIME_FORMATS = listOf(
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"),
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"),
|
||||
)
|
||||
|
||||
/** 完成度「已上传点数/总点数」,容忍空白与百分比后缀等夹带字符。 */
|
||||
private val DEGREE_REGEX = Regex("(\\d+)\\s*/\\s*(\\d+)")
|
||||
|
||||
/** 解析计划时间;主格式失败时退回 ISO(LocalDateTime 默认格式)兜底。 */
|
||||
fun parseTaskTime(raw: String?): LocalDateTime? {
|
||||
val value = raw?.trim().orEmpty()
|
||||
if (value.isEmpty()) return null
|
||||
TASK_TIME_FORMATS.forEach { format ->
|
||||
runCatching { return LocalDateTime.parse(value, format) }
|
||||
}
|
||||
return runCatching { LocalDateTime.parse(value) }.getOrNull()
|
||||
}
|
||||
|
||||
/** 解析完成度为 (已上传, 总数);不可解析返回 null(UI 透传原文)。 */
|
||||
fun parseDegree(raw: String?): Pair<Int, Int>? {
|
||||
val match = raw?.let { DEGREE_REGEX.find(it) } ?: return null
|
||||
val uploaded = match.groupValues[1].toIntOrNull() ?: return null
|
||||
val total = match.groupValues[2].toIntOrNull() ?: return null
|
||||
return uploaded to total
|
||||
}
|
||||
|
||||
/** 完成度百分比(0–100);总数为 0 或不可解析时返回 null。 */
|
||||
fun degreePercent(raw: String?): Int? {
|
||||
val (uploaded, total) = parseDegree(raw) ?: return null
|
||||
if (total <= 0) return null
|
||||
return (uploaded * 100 / total).coerceIn(0, 100)
|
||||
}
|
||||
|
||||
/** 按兜底序推导任务状态([now] 由调用方传入以便测试与刷新一致性)。 */
|
||||
fun statusOf(task: TaskItem, now: LocalDateTime = LocalDateTime.now()): TaskStatus {
|
||||
val (uploaded, total) = parseDegree(task.CompleteDegree) ?: Pair(-1, -1)
|
||||
if (total > 0 && uploaded >= total) return TaskStatus.COMPLETED
|
||||
val begin = parseTaskTime(task.BeginTime)
|
||||
if (begin != null && now.isBefore(begin)) return TaskStatus.NOT_STARTED
|
||||
return TaskStatus.IN_PROGRESS
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.stec.cmd.feature.task
|
||||
|
||||
import com.stec.cmd.core.network.ApiCaller
|
||||
import com.stec.cmd.core.network.ApiError
|
||||
import com.stec.cmd.core.network.api.TaskApi
|
||||
import com.stec.cmd.core.network.api.TaskItem
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.decodeFromJsonElement
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* 任务仓储(M3-01/02,2.6 GetUserPlanGroup / 2.7 GetPlanGroup)。
|
||||
*
|
||||
* 契约宽容:data 形态以 [taskListFromData] 归一提取——
|
||||
* 兼容文档口径的 `{"TaskList":[…]}` 对象(含 TaskList 键值为二次 JSON 编码字符串)、
|
||||
* 平铺数组、以及 data 直接为编码字符串(与 ProjectRepository.projectListFromData
|
||||
* 对项目列表的宽容模式一致)。
|
||||
*/
|
||||
@Singleton
|
||||
class TaskRepository @Inject constructor(
|
||||
private val taskApi: TaskApi,
|
||||
private val apiCaller: ApiCaller,
|
||||
private val json: Json,
|
||||
) {
|
||||
|
||||
/**
|
||||
* 2.6 APP 端任务列表(本人测组任务);[projectId] 传 null 时返回本人
|
||||
* 全部项目当日任务,[planDate] 必填(yyyy-MM-dd)。
|
||||
*/
|
||||
suspend fun myTasks(projectId: String?, planDate: String): List<TaskItem> =
|
||||
taskListFromData(apiCaller.call { taskApi.getUserPlanGroup(projectId, planDate) })
|
||||
|
||||
/** 2.7 项目当前监测计划任务列表;[projectId] 传 null 时返回所有进行中项目。 */
|
||||
suspend fun planTasks(projectId: String?): List<TaskItem> =
|
||||
taskListFromData(apiCaller.call { taskApi.getPlanGroup(projectId) })
|
||||
|
||||
/**
|
||||
* data → 任务列表归一提取:
|
||||
* - JsonObject:优先取 `TaskList` 键(文档口径);缺键时取首个 JsonArray 值兜底
|
||||
* (防御键名大小写漂移);
|
||||
* - 提取物为 JsonArray 直接解码;为 JsonPrimitive 时按二次 JSON 编码字符串解码;
|
||||
* - data 本身为 JsonArray / JsonPrimitive 同样支持。
|
||||
* 结构无法识别或字段不符时以 [ApiError.EmptyBodyError] 抛出。
|
||||
*/
|
||||
private fun taskListFromData(data: JsonElement): List<TaskItem> {
|
||||
val payload = when (data) {
|
||||
is JsonObject ->
|
||||
data[KEY_TASK_LIST]
|
||||
?: data.values.filterIsInstance<JsonArray>().firstOrNull()
|
||||
?: throw ApiError.EmptyBodyError("任务列表数据结构异常")
|
||||
else -> data
|
||||
}
|
||||
val serializer = ListSerializer(TaskItem.serializer())
|
||||
return try {
|
||||
when (payload) {
|
||||
is JsonArray -> json.decodeFromJsonElement(serializer, payload)
|
||||
is JsonPrimitive -> json.decodeFromString(serializer, payload.content)
|
||||
else -> throw ApiError.EmptyBodyError("任务列表数据结构异常")
|
||||
}
|
||||
} catch (e: SerializationException) {
|
||||
throw ApiError.EmptyBodyError("任务列表解析失败:${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
/** 文档 2.6/2.7:data 内列表键名(确切大小写)。 */
|
||||
const val KEY_TASK_LIST = "TaskList"
|
||||
}
|
||||
}
|
||||
189
app/src/main/kotlin/com/stec/cmd/feature/task/TaskViewModel.kt
Normal file
189
app/src/main/kotlin/com/stec/cmd/feature/task/TaskViewModel.kt
Normal file
@@ -0,0 +1,189 @@
|
||||
package com.stec.cmd.feature.task
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.stec.cmd.config.ConfigRepository
|
||||
import com.stec.cmd.core.common.UiState
|
||||
import com.stec.cmd.core.network.ApiError
|
||||
import com.stec.cmd.core.network.api.TaskItem
|
||||
import com.stec.cmd.feature.mine.userMessage
|
||||
import com.stec.cmd.session.ProjectSession
|
||||
import com.stec.cmd.session.SessionRepository
|
||||
import com.stec.cmd.session.SessionState
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
/** 任务页分段:我的任务(2.6 GetUserPlanGroup)/ 计划任务(2.7 GetPlanGroup)。 */
|
||||
enum class TaskTab {
|
||||
MY_TASKS,
|
||||
PLAN_TASKS,
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务工作台(M3-01/02/04/05):
|
||||
* 会话 / 配置 / 选中项目门控 + 双列表独立 UiState + 本地筛选链。
|
||||
*
|
||||
* 门控沿用 HomeViewModel:SignedIn 且三要素就绪才允许加载;会话离开 SignedIn
|
||||
* 复位为 Idle;**selectedProject 任一变化即复位双列表并重载当前 Tab**
|
||||
* (M2-03「切换项目后任务跟随刷新」承诺的落地)。
|
||||
*/
|
||||
@HiltViewModel
|
||||
class TaskViewModel @Inject constructor(
|
||||
private val taskRepository: TaskRepository,
|
||||
projectSession: ProjectSession,
|
||||
sessionRepository: SessionRepository,
|
||||
private val configRepository: ConfigRepository,
|
||||
) : ViewModel() {
|
||||
|
||||
/** 会话状态流(Restoring/SignedOut/SignedIn 门控)。 */
|
||||
val sessionState = sessionRepository.state
|
||||
|
||||
/** 连接配置快照流(三要素缺失时引导配置)。 */
|
||||
val configSnapshot = configRepository.snapshotStateFlow
|
||||
|
||||
/** 当前选中项目(ProjectSession 共享,切换即联动刷新)。 */
|
||||
val selectedProject = projectSession.selectedProject
|
||||
|
||||
private val _tab = MutableStateFlow(TaskTab.MY_TASKS)
|
||||
val tab: StateFlow<TaskTab> = _tab.asStateFlow()
|
||||
|
||||
/** 状态筛选(null=全部,M3-05 本地过滤,双列表通用)。 */
|
||||
private val _statusFilter = MutableStateFlow<TaskStatus?>(null)
|
||||
val statusFilter: StateFlow<TaskStatus?> = _statusFilter.asStateFlow()
|
||||
|
||||
/** 2.6 PlanDate(文档必填,默认当天;日期选择后服务端重查)。 */
|
||||
private val _planDate = MutableStateFlow(LocalDate.now())
|
||||
val planDate: StateFlow<LocalDate> = _planDate.asStateFlow()
|
||||
|
||||
/** 2.7 无日期参数:本地按 EndTime 同日过滤(null=全部)。 */
|
||||
private val _dateFilter = MutableStateFlow<LocalDate?>(null)
|
||||
val dateFilter: StateFlow<LocalDate?> = _dateFilter.asStateFlow()
|
||||
|
||||
private val _myTasks = MutableStateFlow<UiState<List<TaskItem>>>(UiState.Idle)
|
||||
val myTasks: StateFlow<UiState<List<TaskItem>>> = _myTasks.asStateFlow()
|
||||
|
||||
private val _planTasks = MutableStateFlow<UiState<List<TaskItem>>>(UiState.Idle)
|
||||
val planTasks: StateFlow<UiState<List<TaskItem>>> = _planTasks.asStateFlow()
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
sessionState.collect { state ->
|
||||
if (state is SessionState.SignedIn) {
|
||||
if (configRepository.snapshotStateFlow.value.isReady) loadCurrentIfIdle()
|
||||
} else {
|
||||
resetLists()
|
||||
}
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
configSnapshot.collect { snapshot ->
|
||||
if (snapshot.isReady && sessionState.value is SessionState.SignedIn) {
|
||||
loadCurrentIfIdle()
|
||||
}
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
selectedProject.collect {
|
||||
resetLists()
|
||||
loadCurrentIfIdle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 分段切换:惰性加载目标列表,清掉 2.7 的本地日期过滤(两 Tab 语义不同)。 */
|
||||
fun selectTab(tab: TaskTab) {
|
||||
if (_tab.value == tab) return
|
||||
_tab.value = tab
|
||||
_dateFilter.value = null
|
||||
loadCurrentIfIdle()
|
||||
}
|
||||
|
||||
/** 下拉刷新 / 错误重试 / 空态刷新。 */
|
||||
fun refresh() {
|
||||
when (_tab.value) {
|
||||
TaskTab.MY_TASKS -> if (_myTasks.value !is UiState.Loading) loadMyTasks()
|
||||
TaskTab.PLAN_TASKS -> if (_planTasks.value !is UiState.Loading) loadPlanTasks()
|
||||
}
|
||||
}
|
||||
|
||||
/** M3-05 状态筛选(本地)。 */
|
||||
fun updateStatusFilter(status: TaskStatus?) {
|
||||
_statusFilter.value = status
|
||||
}
|
||||
|
||||
/**
|
||||
* M3-05 时间筛选:2.6 改 PlanDate 服务端重查;2.7 本地按 EndTime 过滤。
|
||||
*/
|
||||
fun pickDate(date: LocalDate) {
|
||||
when (_tab.value) {
|
||||
TaskTab.MY_TASKS -> {
|
||||
_planDate.value = date
|
||||
if (selectedProject.value != null && _myTasks.value !is UiState.Loading) {
|
||||
loadMyTasks()
|
||||
}
|
||||
}
|
||||
TaskTab.PLAN_TASKS -> _dateFilter.value = date
|
||||
}
|
||||
}
|
||||
|
||||
/** 清除 2.7 本地日期过滤(恢复全部)。 */
|
||||
fun clearDateFilter() {
|
||||
_dateFilter.value = null
|
||||
}
|
||||
|
||||
/** 当前 Tab 经状态/日期筛选链后的可见列表(UI 只渲染不加工)。 */
|
||||
fun displayedTasks(): List<TaskItem> {
|
||||
val state = if (_tab.value == TaskTab.MY_TASKS) _myTasks.value else _planTasks.value
|
||||
val list = (state as? UiState.Success)?.data.orEmpty()
|
||||
val now = LocalDateTime.now()
|
||||
return list.filter { task ->
|
||||
val statusOk = _statusFilter.value == null || statusOf(task, now) == _statusFilter.value
|
||||
val dateOk = _tab.value != TaskTab.PLAN_TASKS || _dateFilter.value == null ||
|
||||
parseTaskTime(task.EndTime)?.toLocalDate() == _dateFilter.value
|
||||
statusOk && dateOk
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadCurrentIfIdle() {
|
||||
if (selectedProject.value == null) return
|
||||
when (_tab.value) {
|
||||
TaskTab.MY_TASKS -> if (_myTasks.value is UiState.Idle) loadMyTasks()
|
||||
TaskTab.PLAN_TASKS -> if (_planTasks.value is UiState.Idle) loadPlanTasks()
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetLists() {
|
||||
_myTasks.value = UiState.Idle
|
||||
_planTasks.value = UiState.Idle
|
||||
}
|
||||
|
||||
private fun loadMyTasks() {
|
||||
viewModelScope.launch {
|
||||
_myTasks.value = UiState.Loading
|
||||
_myTasks.value = try {
|
||||
UiState.Success(
|
||||
taskRepository.myTasks(selectedProject.value?.ID, _planDate.value.toString()),
|
||||
)
|
||||
} catch (e: ApiError) {
|
||||
UiState.Error(e.userMessage())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadPlanTasks() {
|
||||
viewModelScope.launch {
|
||||
_planTasks.value = UiState.Loading
|
||||
_planTasks.value = try {
|
||||
UiState.Success(taskRepository.planTasks(selectedProject.value?.ID))
|
||||
} catch (e: ApiError) {
|
||||
UiState.Error(e.userMessage())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
package com.stec.cmd.feature.task
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
import com.google.android.material.datepicker.MaterialDatePicker
|
||||
import com.stec.cmd.R
|
||||
import com.stec.cmd.core.common.UiState
|
||||
import com.stec.cmd.core.network.api.TaskItem
|
||||
import com.stec.cmd.databinding.FragmentTaskBinding
|
||||
import com.stec.cmd.feature.mine.ConfigFragment
|
||||
import com.stec.cmd.feature.mine.LoginFragment
|
||||
import com.stec.cmd.session.SessionState
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
/**
|
||||
* 任务工作台(M3-01/02 双列表入口 / M3-04 状态流转 / M3-05 筛选与下拉刷新)。
|
||||
*
|
||||
* 渲染状态机对齐首页:Restoring → 中性加载;SignedOut → 登录 CTA;
|
||||
* SignedIn → 配置门控 → 未选项目引导空态(CTA→首页切换项目)→
|
||||
* 当前 Tab 列表加载/空/错误/内容四态。
|
||||
* 子页(详情/登录/配置)沿用「add + hide + addToBackStack」壳层模式。
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class TaskWorkspaceFragment : Fragment() {
|
||||
|
||||
private var _binding: FragmentTaskBinding? = null
|
||||
private val binding get() = checkNotNull(_binding)
|
||||
|
||||
private val viewModel: TaskViewModel by viewModels()
|
||||
|
||||
private val listAdapter by lazy { TaskListAdapter(::openDetail) }
|
||||
|
||||
private val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
encodeDefaults = true
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View {
|
||||
_binding = FragmentTaskBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
binding.recyclerTasks.layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.recyclerTasks.adapter = listAdapter
|
||||
binding.swipeRefresh.setColorSchemeResources(R.color.brand_primary)
|
||||
binding.swipeRefresh.setOnRefreshListener { viewModel.refresh() }
|
||||
binding.btnGoLogin.setOnClickListener { openOnTop(LoginFragment(), TAG_LOGIN) }
|
||||
binding.btnGoConfig.setOnClickListener { openOnTop(ConfigFragment(), TAG_CONFIG) }
|
||||
binding.btnGoHome.setOnClickListener { goHomeTab() }
|
||||
binding.btnRetry.setOnClickListener { viewModel.refresh() }
|
||||
binding.btnEmptyRefresh.setOnClickListener { viewModel.refresh() }
|
||||
binding.chipDate.setOnClickListener { showDatePicker() }
|
||||
binding.chipDate.setOnCloseIconClickListener { viewModel.clearDateFilter() }
|
||||
|
||||
binding.toggleTab.addOnButtonCheckedListener { _, checkedId, isChecked ->
|
||||
if (isChecked) {
|
||||
viewModel.selectTab(
|
||||
if (checkedId == R.id.btn_tab_my) TaskTab.MY_TASKS else TaskTab.PLAN_TASKS,
|
||||
)
|
||||
}
|
||||
}
|
||||
binding.chipGroupFilter.setOnCheckedStateChangeListener { _, checkedIds ->
|
||||
viewModel.updateStatusFilter(
|
||||
when (checkedIds.firstOrNull()) {
|
||||
R.id.chip_filter_not_started -> TaskStatus.NOT_STARTED
|
||||
R.id.chip_filter_in_progress -> TaskStatus.IN_PROGRESS
|
||||
R.id.chip_filter_completed -> TaskStatus.COMPLETED
|
||||
else -> null
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
launch { viewModel.sessionState.collect { render() } }
|
||||
launch { viewModel.configSnapshot.collect { render() } }
|
||||
launch { viewModel.selectedProject.collect { render() } }
|
||||
launch { viewModel.tab.collect { render() } }
|
||||
launch { viewModel.statusFilter.collect { render() } }
|
||||
launch { viewModel.dateFilter.collect { render() } }
|
||||
launch { viewModel.planDate.collect { render() } }
|
||||
launch { viewModel.myTasks.collect { render() } }
|
||||
launch { viewModel.planTasks.collect { render() } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 任一流变化即整体重渲(读 ViewModel 当前值,避免组合流样板)。 */
|
||||
private fun render() {
|
||||
val session = viewModel.sessionState.value
|
||||
val config = viewModel.configSnapshot.value
|
||||
val selected = viewModel.selectedProject.value
|
||||
|
||||
binding.progress.isVisible = false
|
||||
binding.stateSignedOut.isVisible = false
|
||||
binding.stateConfig.isVisible = false
|
||||
binding.stateNoProject.isVisible = false
|
||||
binding.stateError.isVisible = false
|
||||
binding.stateEmpty.isVisible = false
|
||||
|
||||
when (session) {
|
||||
// 冷启动恢复中:中性加载,不引导登录
|
||||
SessionState.Restoring -> binding.progress.isVisible = true
|
||||
SessionState.SignedOut -> binding.stateSignedOut.isVisible = true
|
||||
is SessionState.SignedIn -> {
|
||||
if (!config.isReady) {
|
||||
binding.stateConfig.isVisible = true
|
||||
return
|
||||
}
|
||||
if (selected == null) {
|
||||
binding.stateNoProject.isVisible = true
|
||||
return
|
||||
}
|
||||
binding.groupHeader.isVisible = true
|
||||
binding.tvTaskProject.text = selected.SimpleName?.takeIf { it.isNotBlank() }
|
||||
?: getString(R.string.home_unnamed_project)
|
||||
renderList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderList() {
|
||||
val tab = viewModel.tab.value
|
||||
val state =
|
||||
if (tab == TaskTab.MY_TASKS) viewModel.myTasks.value else viewModel.planTasks.value
|
||||
|
||||
binding.swipeRefresh.isVisible = true
|
||||
binding.swipeRefresh.isRefreshing = state is UiState.Loading
|
||||
|
||||
// 日期 Chip 双语义:2.6=PlanDate(服务端重查);2.7=本地 EndTime 过滤(可清除)
|
||||
if (tab == TaskTab.MY_TASKS) {
|
||||
binding.chipDate.text =
|
||||
getString(R.string.task_filter_date_fmt, viewModel.planDate.value.toString())
|
||||
binding.chipDate.isCloseIconVisible = false
|
||||
} else {
|
||||
val filter = viewModel.dateFilter.value
|
||||
binding.chipDate.text = filter?.let { getString(R.string.task_filter_date_fmt, it.toString()) }
|
||||
?: getString(R.string.task_filter_date_any)
|
||||
binding.chipDate.isCloseIconVisible = filter != null
|
||||
}
|
||||
|
||||
when (state) {
|
||||
is UiState.Loading -> Unit // 首载由下拉圈呈现
|
||||
is UiState.Error -> {
|
||||
binding.tvErrorMessage.text = state.message
|
||||
binding.stateError.isVisible = true
|
||||
}
|
||||
is UiState.Success -> {
|
||||
val visible = viewModel.displayedTasks()
|
||||
listAdapter.submitList(visible)
|
||||
if (visible.isEmpty()) {
|
||||
binding.tvEmptyMessage.text =
|
||||
if (tab == TaskTab.MY_TASKS) getString(R.string.task_empty_my)
|
||||
else getString(R.string.task_empty_plan)
|
||||
binding.stateEmpty.isVisible = true
|
||||
}
|
||||
}
|
||||
UiState.Idle -> Unit // 会话/配置/项目流将触发自动加载
|
||||
}
|
||||
}
|
||||
|
||||
/** M3-05 时间筛选入口(MaterialDatePicker,UTC 毫秒 → 本地语义日期)。 */
|
||||
private fun showDatePicker() {
|
||||
val picker = MaterialDatePicker.Builder.datePicker()
|
||||
.setTitleText(R.string.task_date_picker_title)
|
||||
.build()
|
||||
picker.addOnPositiveButtonClickListener { utcMillis ->
|
||||
viewModel.pickDate(
|
||||
Instant.ofEpochMilli(utcMillis).atZone(ZoneOffset.UTC).toLocalDate(),
|
||||
)
|
||||
}
|
||||
picker.show(childFragmentManager, TAG_DATE)
|
||||
}
|
||||
|
||||
/** 空态 CTA:切回首页 Tab 选项目(导航壳零改动,经底栏 itemId 触发既有切页)。 */
|
||||
private fun goHomeTab() {
|
||||
requireActivity().findViewById<BottomNavigationView>(R.id.bottom_nav)
|
||||
.selectedItemId = R.id.nav_home
|
||||
}
|
||||
|
||||
/** 详情页跳转:TaskItem 经 JSON 传参(与 ProjectSwitcherSheet 同款先例)。 */
|
||||
private fun openDetail(task: TaskItem) {
|
||||
openOnTop(
|
||||
TaskDetailFragment.newInstance(json.encodeToString(TaskItem.serializer(), task)),
|
||||
TAG_DETAIL,
|
||||
)
|
||||
}
|
||||
|
||||
private fun openOnTop(fragment: Fragment, tag: String) {
|
||||
parentFragmentManager.commit {
|
||||
setReorderingAllowed(true)
|
||||
add(R.id.fragment_container, fragment, tag)
|
||||
hide(this@TaskWorkspaceFragment)
|
||||
addToBackStack(null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val TAG_LOGIN = "task_login"
|
||||
const val TAG_CONFIG = "task_config"
|
||||
const val TAG_DETAIL = "task_detail"
|
||||
const val TAG_DATE = "task_date_picker"
|
||||
}
|
||||
}
|
||||
312
app/src/main/res/layout/fragment_task.xml
Normal file
312
app/src/main/res/layout/fragment_task.xml
Normal file
@@ -0,0 +1,312 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 任务工作台(M3-01/02 双列表入口 / M3-04 状态 Chip / M3-05 筛选与下拉刷新) -->
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/brand_background">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- 头部:项目上下文 + 分段切换 + 筛选(仅已登录且选中项目时可见) -->
|
||||
<LinearLayout
|
||||
android:id="@+id/group_header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_task_project"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="20dp"
|
||||
android:paddingTop="16dp"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="20dp"
|
||||
android:paddingTop="2dp"
|
||||
android:alpha="0.6"
|
||||
android:text="@string/task_project_hint"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButtonToggleGroup
|
||||
android:id="@+id/toggle_tab"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="20dp"
|
||||
android:layout_marginTop="12dp"
|
||||
app:checkedButton="@+id/btn_tab_my"
|
||||
app:selectionRequired="true"
|
||||
app:singleSelection="true">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_tab_my"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/task_tab_my" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_tab_plan"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/task_tab_plan" />
|
||||
</com.google.android.material.button.MaterialButtonToggleGroup>
|
||||
|
||||
<!-- M3-05 筛选行:状态单选(本地过滤)+ 日期(2.6 服务端重查 / 2.7 本地过滤) -->
|
||||
<HorizontalScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="4dp"
|
||||
android:scrollbars="none">
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:id="@+id/chip_group_filter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:chipSpacingHorizontal="8dp"
|
||||
app:selectionRequired="true"
|
||||
app:singleLine="true"
|
||||
app:singleSelection="true">
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chip_filter_all"
|
||||
style="@style/Widget.Material3.Chip.Filter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:checked="true"
|
||||
android:text="@string/task_filter_all" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chip_filter_not_started"
|
||||
style="@style/Widget.Material3.Chip.Filter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/task_filter_not_started" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chip_filter_in_progress"
|
||||
style="@style/Widget.Material3.Chip.Filter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/task_filter_in_progress" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chip_filter_completed"
|
||||
style="@style/Widget.Material3.Chip.Filter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/task_filter_completed" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chip_date"
|
||||
style="@style/Widget.Material3.Chip.Assist"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/task_filter_date_any"
|
||||
app:chipIcon="@drawable/ic_nav_task"
|
||||
app:chipIconTint="?attr/colorPrimary" />
|
||||
</com.google.android.material.chip.ChipGroup>
|
||||
</HorizontalScrollView>
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipe_refresh"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_tasks"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:overScrollMode="never"
|
||||
android:paddingBottom="24dp" />
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone" />
|
||||
|
||||
<!-- 未登录空态 -->
|
||||
<LinearLayout
|
||||
android:id="@+id/state_signed_out"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/brand_background"
|
||||
android:clickable="true"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="32dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/task_signed_out_title"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:alpha="0.7"
|
||||
android:gravity="center"
|
||||
android:text="@string/task_signed_out_hint"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_go_login"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:text="@string/task_go_login" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 配置缺失空态 -->
|
||||
<LinearLayout
|
||||
android:id="@+id/state_config"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/brand_background"
|
||||
android:clickable="true"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="32dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/task_config_missing"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_go_config"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:text="@string/task_go_config" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 未选项目空态(CTA→首页切换项目,M2-03 联动刷新承诺闭环) -->
|
||||
<LinearLayout
|
||||
android:id="@+id/state_no_project"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/brand_background"
|
||||
android:clickable="true"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="32dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/task_no_project_title"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:alpha="0.7"
|
||||
android:gravity="center"
|
||||
android:text="@string/task_no_project_hint"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_go_home"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:text="@string/task_go_home" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 错误态 -->
|
||||
<LinearLayout
|
||||
android:id="@+id/state_error"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/brand_background"
|
||||
android:clickable="true"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="32dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_error_message"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textColor="?attr/colorError"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_retry"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/task_retry" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 空列表态 -->
|
||||
<LinearLayout
|
||||
android:id="@+id/state_empty"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/brand_background"
|
||||
android:clickable="true"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="32dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_empty_message"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:alpha="0.7"
|
||||
android:gravity="center"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_empty_refresh"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/task_empty_refresh" />
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
338
app/src/main/res/layout/fragment_task_detail.xml
Normal file
338
app/src/main/res/layout/fragment_task_detail.xml
Normal file
@@ -0,0 +1,338 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 任务详情(M3-03 测组/测点完成度/计划周期 + M3-04 状态来源字段分区展示) -->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/brand_background"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:navigationIcon="@drawable/ic_back"
|
||||
app:navigationIconTint="?attr/colorOnSurface"
|
||||
app:title="@string/task_detail_title"
|
||||
app:titleTextColor="?attr/colorOnSurface" />
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:overScrollMode="never">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="20dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="32dp">
|
||||
|
||||
<!-- 任务标识 -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
style="@style/TaskDetailCard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardBackgroundColor="@color/brand_surface"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:cardElevation="0dp">
|
||||
|
||||
<LinearLayout style="@style/TaskDetailCardContent">
|
||||
|
||||
<TextView
|
||||
style="@style/TaskDetailSectionTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/task_detail_section_identity" />
|
||||
|
||||
<LinearLayout style="@style/TaskDetailRow">
|
||||
|
||||
<TextView
|
||||
style="@style/TaskDetailLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/task_detail_label_code" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_detail_code"
|
||||
style="@style/TaskDetailValue"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout style="@style/TaskDetailRow">
|
||||
|
||||
<TextView
|
||||
style="@style/TaskDetailLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/task_detail_label_mold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_detail_mold"
|
||||
style="@style/TaskDetailValue"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout style="@style/TaskDetailRow">
|
||||
|
||||
<TextView
|
||||
style="@style/TaskDetailLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/task_detail_label_monitoring" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_detail_monitoring"
|
||||
style="@style/TaskDetailValue"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout style="@style/TaskDetailRow">
|
||||
|
||||
<TextView
|
||||
style="@style/TaskDetailLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/task_detail_label_duty" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_detail_duty"
|
||||
style="@style/TaskDetailValue"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- 项目与工点 -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
style="@style/TaskDetailCard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardBackgroundColor="@color/brand_surface"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:cardElevation="0dp">
|
||||
|
||||
<LinearLayout style="@style/TaskDetailCardContent">
|
||||
|
||||
<TextView
|
||||
style="@style/TaskDetailSectionTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/task_detail_section_project" />
|
||||
|
||||
<LinearLayout style="@style/TaskDetailRow">
|
||||
|
||||
<TextView
|
||||
style="@style/TaskDetailLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/task_detail_label_project" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_detail_project"
|
||||
style="@style/TaskDetailValue"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout style="@style/TaskDetailRow">
|
||||
|
||||
<TextView
|
||||
style="@style/TaskDetailLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/task_detail_label_work_point" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_detail_work_point"
|
||||
style="@style/TaskDetailValue"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- 监测计划 -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
style="@style/TaskDetailCard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardBackgroundColor="@color/brand_surface"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:cardElevation="0dp">
|
||||
|
||||
<LinearLayout style="@style/TaskDetailCardContent">
|
||||
|
||||
<TextView
|
||||
style="@style/TaskDetailSectionTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/task_detail_section_plan" />
|
||||
|
||||
<LinearLayout style="@style/TaskDetailRow">
|
||||
|
||||
<TextView
|
||||
style="@style/TaskDetailLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/task_detail_label_plan_id" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_detail_plan_id"
|
||||
style="@style/TaskDetailValue"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout style="@style/TaskDetailRow">
|
||||
|
||||
<TextView
|
||||
style="@style/TaskDetailLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/task_detail_label_period" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_detail_period"
|
||||
style="@style/TaskDetailValue"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout style="@style/TaskDetailRow">
|
||||
|
||||
<TextView
|
||||
style="@style/TaskDetailLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/task_detail_label_begin" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_detail_begin"
|
||||
style="@style/TaskDetailValue"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout style="@style/TaskDetailRow">
|
||||
|
||||
<TextView
|
||||
style="@style/TaskDetailLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/task_detail_label_end" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_detail_end"
|
||||
style="@style/TaskDetailValue"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- 测组与完成度(M3-03 测点维度以完成度呈现,2.9 归属 M5) -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
style="@style/TaskDetailCard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardBackgroundColor="@color/brand_surface"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:cardElevation="0dp">
|
||||
|
||||
<LinearLayout style="@style/TaskDetailCardContent">
|
||||
|
||||
<TextView
|
||||
style="@style/TaskDetailSectionTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/task_detail_section_group" />
|
||||
|
||||
<LinearLayout style="@style/TaskDetailRow">
|
||||
|
||||
<TextView
|
||||
style="@style/TaskDetailLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/task_detail_label_group" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_detail_group"
|
||||
style="@style/TaskDetailValue"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout style="@style/TaskDetailRow">
|
||||
|
||||
<TextView
|
||||
style="@style/TaskDetailLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/task_detail_label_degree" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_detail_degree"
|
||||
style="@style/TaskDetailValue"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/progress_degree"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:max="100"
|
||||
android:visibility="gone"
|
||||
app:indicatorColor="?attr/colorPrimary"
|
||||
app:trackCornerRadius="4dp"
|
||||
app:trackThickness="8dp" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- 施工工况 -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
style="@style/TaskDetailCard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardBackgroundColor="@color/brand_surface"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:cardElevation="0dp">
|
||||
|
||||
<LinearLayout style="@style/TaskDetailCardContent">
|
||||
|
||||
<TextView
|
||||
style="@style/TaskDetailSectionTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/task_detail_section_working" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_detail_working"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:alpha="0.8"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</LinearLayout>
|
||||
98
app/src/main/res/layout/item_task.xml
Normal file
98
app/src/main/res/layout/item_task.xml
Normal file
@@ -0,0 +1,98 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 任务卡片(M3-01/02 列表条目,M3-04 状态 Chip 三态配色) -->
|
||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/card_task"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="20dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
app:cardBackgroundColor="@color/brand_surface"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:cardElevation="0dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_task_code"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chip_status"
|
||||
style="@style/Widget.Material3.Chip.Assist"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:clickable="false"
|
||||
android:focusable="false" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_task_type"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="?attr/colorPrimary"
|
||||
android:textSize="13sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_task_mold"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="10dp"
|
||||
android:alpha="0.65"
|
||||
android:textSize="13sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_task_meta"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:alpha="0.75"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_task_period"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:alpha="0.75"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_task_degree"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:alpha="0.75"
|
||||
android:textSize="13sp" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
@@ -7,4 +7,6 @@
|
||||
<color name="brand_surface">#FFFFFF</color>
|
||||
<color name="launcher_background">#1E6FD9</color>
|
||||
<color name="login_error">#B3261E</color>
|
||||
<!-- M3-04 已完成任务状态 Chip -->
|
||||
<color name="brand_success">#2E7D32</color>
|
||||
</resources>
|
||||
|
||||
@@ -26,6 +26,55 @@
|
||||
<string name="home_empty_refresh">刷新</string>
|
||||
<string name="sheet_switch_title">切换项目</string>
|
||||
|
||||
<!-- M3 任务管理(M3-01/02/03/04/05) -->
|
||||
<string name="task_tab_my">我的任务</string>
|
||||
<string name="task_tab_plan">计划任务</string>
|
||||
<string name="task_project_hint">在首页切换项目后,任务列表自动刷新</string>
|
||||
<string name="task_filter_all">全部</string>
|
||||
<string name="task_filter_not_started">未开始</string>
|
||||
<string name="task_filter_in_progress">进行中</string>
|
||||
<string name="task_filter_completed">已完成</string>
|
||||
<string name="task_filter_date_fmt">日期:%1$s</string>
|
||||
<string name="task_filter_date_any">时间</string>
|
||||
<string name="task_date_picker_title">选择任务日期</string>
|
||||
<string name="task_signed_out_title">登录后查看您的任务</string>
|
||||
<string name="task_signed_out_hint">任务与监测计划需要登录后同步</string>
|
||||
<string name="task_go_login">立即登录</string>
|
||||
<string name="task_config_missing">尚未配置服务器与密钥,无法加载任务</string>
|
||||
<string name="task_go_config">去配置</string>
|
||||
<string name="task_no_project_title">尚未选择项目</string>
|
||||
<string name="task_no_project_hint">选择项目后即可查看对应任务</string>
|
||||
<string name="task_go_home">去首页切换项目</string>
|
||||
<string name="task_retry">重试</string>
|
||||
<string name="task_empty_my">该日期下暂无任务,可点击日期筛选换一天</string>
|
||||
<string name="task_empty_plan">当前项目暂无监测计划任务</string>
|
||||
<string name="task_empty_refresh">刷新</string>
|
||||
<string name="task_status_not_started">未开始</string>
|
||||
<string name="task_status_in_progress">进行中</string>
|
||||
<string name="task_status_completed">已完成</string>
|
||||
<string name="task_item_period_fmt">周期:%1$s ~ %2$s</string>
|
||||
<string name="task_item_degree_fmt">完成度:%1$s</string>
|
||||
<string name="task_detail_title">任务详情</string>
|
||||
<string name="task_detail_section_identity">任务标识</string>
|
||||
<string name="task_detail_section_project">项目与工点</string>
|
||||
<string name="task_detail_section_plan">监测计划</string>
|
||||
<string name="task_detail_section_group">测组与完成度</string>
|
||||
<string name="task_detail_section_working">施工工况</string>
|
||||
<string name="task_detail_label_code">任务编号</string>
|
||||
<string name="task_detail_label_mold">类型</string>
|
||||
<string name="task_detail_label_monitoring">监测类型</string>
|
||||
<string name="task_detail_label_duty">责任人</string>
|
||||
<string name="task_detail_label_project">项目</string>
|
||||
<string name="task_detail_label_work_point">工点</string>
|
||||
<string name="task_detail_label_plan_id">计划 ID</string>
|
||||
<string name="task_detail_label_period">期数</string>
|
||||
<string name="task_detail_label_period_fmt">第 %1$s 期</string>
|
||||
<string name="task_detail_label_begin">开始时间</string>
|
||||
<string name="task_detail_label_end">截止时间</string>
|
||||
<string name="task_detail_label_group">测组 ID</string>
|
||||
<string name="task_detail_label_degree">完成度</string>
|
||||
<string name="task_detail_unknown">-</string>
|
||||
|
||||
<!-- 通用 -->
|
||||
<string name="action_save">保存</string>
|
||||
|
||||
|
||||
@@ -17,4 +17,44 @@
|
||||
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_launcher_foreground</item>
|
||||
<item name="postSplashScreenTheme">@style/Theme.SteCmd</item>
|
||||
</style>
|
||||
|
||||
<!-- 任务详情分区卡片(M3-03) -->
|
||||
<style name="TaskDetailCard">
|
||||
<item name="android:layout_marginTop">12dp</item>
|
||||
</style>
|
||||
|
||||
<style name="TaskDetailCardContent">
|
||||
<item name="android:layout_width">match_parent</item>
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:orientation">vertical</item>
|
||||
<item name="android:padding">16dp</item>
|
||||
</style>
|
||||
|
||||
<style name="TaskDetailSectionTitle">
|
||||
<item name="android:textColor">?attr/colorOnSurface</item>
|
||||
<item name="android:textSize">15sp</item>
|
||||
<item name="android:textStyle">bold</item>
|
||||
<item name="android:layout_marginBottom">8dp</item>
|
||||
</style>
|
||||
|
||||
<style name="TaskDetailRow">
|
||||
<item name="android:layout_width">match_parent</item>
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:orientation">horizontal</item>
|
||||
<item name="android:gravity">center_vertical</item>
|
||||
<item name="android:layout_marginTop">6dp</item>
|
||||
</style>
|
||||
|
||||
<style name="TaskDetailLabel">
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:alpha">0.6</item>
|
||||
<item name="android:textSize">13sp</item>
|
||||
<item name="android:layout_marginEnd">12dp</item>
|
||||
</style>
|
||||
|
||||
<style name="TaskDetailValue">
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:textColor">?attr/colorOnSurface</item>
|
||||
<item name="android:textSize">14sp</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.stec.cmd.core.network
|
||||
|
||||
import com.stec.cmd.core.network.api.AuthApi
|
||||
import com.stec.cmd.core.network.api.ProjectApi
|
||||
import com.stec.cmd.core.network.api.TaskApi
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
@@ -78,6 +79,11 @@ object NetworkModule {
|
||||
@Singleton
|
||||
fun provideProjectApi(retrofit: Retrofit): ProjectApi = retrofit.create(ProjectApi::class.java)
|
||||
|
||||
/** 任务接口(M3,2.6/2.7)。 */
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTaskApi(retrofit: Retrofit): TaskApi = retrofit.create(TaskApi::class.java)
|
||||
|
||||
/** 仅满足 Retrofit 构造约束;真实地址请求期经 DynamicBaseUrlInterceptor 改写。 */
|
||||
private const val PLACEHOLDER_BASE_URL = "https://placeholder.stec.invalid/"
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.stec.cmd.core.network.api
|
||||
|
||||
import com.stec.cmd.core.network.ApiEnvelope
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Query
|
||||
|
||||
/**
|
||||
* 任务接口(接口管理规范 V4.5 · 测试环境 https://jcd.stec.p-q.co):
|
||||
* - 2.6 获取 APP 端任务列表(M3-01/03/04/05)
|
||||
* - 2.7 获取项目当前监测计划的任务列表(M3-02/03/04/05)
|
||||
*
|
||||
* 三要素请求头(SecretKey/SystemCode/Token)由 AuthInterceptor 统一注入。
|
||||
*
|
||||
* 契约宽容:文档约定 data 为 {"TaskList":[…]};与项目列表(任务 M2 先例)同理
|
||||
* 保留形态偏差可能,故以 [JsonElement] 宽容承载,
|
||||
* 由 app 层 TaskRepository.taskListFromData 归一提取。
|
||||
*/
|
||||
interface TaskApi {
|
||||
|
||||
/**
|
||||
* 2.6 APP 端任务列表:一个项目的一个监测计划的一个测组为一条任务。
|
||||
* [planDate] 必填(yyyy-MM-dd);[projectId]/[workPointId] 传 null 时
|
||||
* Retrofit 省略参数(空=本人全部项目当日任务)。
|
||||
*/
|
||||
@GET(PATH_GET_USER_PLAN_GROUP)
|
||||
suspend fun getUserPlanGroup(
|
||||
@Query(QUERY_PROJECT_ID) projectId: String? = null,
|
||||
@Query(QUERY_PLAN_DATE) planDate: String,
|
||||
@Query(QUERY_WORK_POINT_ID) workPointId: String? = null,
|
||||
): ApiEnvelope<JsonElement>
|
||||
|
||||
/**
|
||||
* 2.7 项目当前监测计划任务列表(离当前时间最近的一个监测计划)。
|
||||
* [projectId] 传 null 时返回所有「进行中」项目的计划任务。
|
||||
*/
|
||||
@GET(PATH_GET_PLAN_GROUP)
|
||||
suspend fun getPlanGroup(
|
||||
@Query(QUERY_PROJECT_ID) projectId: String? = null,
|
||||
): ApiEnvelope<JsonElement>
|
||||
|
||||
companion object {
|
||||
// ---- 接口管理规范 V4.5 · 章节 2.1 所有 API 目录 ----
|
||||
const val PATH_GET_USER_PLAN_GROUP = "OutWebApi/api/GetUserPlanGroup"
|
||||
const val PATH_GET_PLAN_GROUP = "OutWebApi/api/GetPlanGroup"
|
||||
|
||||
/** 查询参数名(2.6/2.7 共用 ProjectID;PlanDate 为 2.6 必填)。 */
|
||||
const val QUERY_PROJECT_ID = "ProjectID"
|
||||
const val QUERY_PLAN_DATE = "PlanDate"
|
||||
const val QUERY_WORK_POINT_ID = "WorkPointID"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.stec.cmd.core.network.api
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* 任务 DTO(接口管理规范 V4.5 · 2.6/2.7 返回值示例,一条任务 = 一个测组)。
|
||||
*
|
||||
* 字段名与平台契约一致(PascalCase);文档未给类型,全 `String?` 容错,
|
||||
* 响应侧配合 Json.ignoreUnknownKeys 容忍字段演进。
|
||||
* 字段来源差异:[PlanNum] 仅 2.6、[PeriodsNum] 仅 2.7(同为期数)、
|
||||
* [WorkPointID]/[WorkPointName] 仅 2.6。
|
||||
*/
|
||||
@Serializable
|
||||
data class TaskItem(
|
||||
/** 项目简称。 */
|
||||
val SimpleName: String? = null,
|
||||
/** 任务编号:项目编号-计划期数-测组编号。 */
|
||||
val TaskCode: String? = null,
|
||||
/** 项目 ID。 */
|
||||
val ProjectID: String? = null,
|
||||
/** 监测计划 ID。 */
|
||||
val PlanID: String? = null,
|
||||
/** 期数(2.6:计划对应期数值)。 */
|
||||
val PlanNum: String? = null,
|
||||
/** 期数(2.7 字段名)。 */
|
||||
val PeriodsNum: String? = null,
|
||||
/** 施工工况。 */
|
||||
val Working: String? = null,
|
||||
/** 类型:常规 | 加测。 */
|
||||
val Mold: String? = null,
|
||||
/** 责任人。 */
|
||||
val DutyName: String? = null,
|
||||
/** 测组 ID。 */
|
||||
val GroupID: String? = null,
|
||||
/** 工点 ID(仅 2.6)。 */
|
||||
val WorkPointID: String? = null,
|
||||
/** 工点名称(仅 2.6)。 */
|
||||
val WorkPointName: String? = null,
|
||||
/** 监测类型:水平位移/竖向位移/测斜/收敛/水位/轴力。 */
|
||||
val MonitoringType: String? = null,
|
||||
/** 计划周期开始(上一期计划截止时间,yyyy-MM-dd HH:mm)。 */
|
||||
val BeginTime: String? = null,
|
||||
/** 计划周期截止(yyyy-MM-dd HH:mm)。 */
|
||||
val EndTime: String? = null,
|
||||
/** 完成度:本测组已上传点数/测组总点数。 */
|
||||
val CompleteDegree: String? = null,
|
||||
) {
|
||||
/** 归一期数:2.6 取 PlanNum,2.7 取 PeriodsNum。 */
|
||||
val period: String?
|
||||
get() = PlanNum?.takeIf { it.isNotBlank() } ?: PeriodsNum?.takeIf { it.isNotBlank() }
|
||||
}
|
||||
Reference in New Issue
Block a user