ConstructionTaskCard.vue 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. <template>
  2. <view
  3. class="task-card p-4 clickable"
  4. hover-class="scale-down"
  5. :class="{ 'is-emergency': task.type === 'emergency' }"
  6. @click="$emit('click', String((task as any).id ?? (task as any).taskId ?? ''))"
  7. >
  8. <view class="flex justify-between items-start mb-2">
  9. <view class="flex-1 min-w-0 mr-3">
  10. <text class="text-sm font-semibold text-gray-800 truncate block">{{ (task as any).taskName || task.projectName || '未命名任务' }}</text>
  11. </view>
  12. <StatusTag :status="task.status" />
  13. </view>
  14. <view class="flex flex-wrap gap-2 mb-3">
  15. <view v-if="task.urgent === 'high'" class="px-2 py-1 bg-danger-light text-danger rounded text-xs font-medium">
  16. 加急
  17. </view>
  18. <view v-if="task.coopType" class="chip-type px-2 py-1 rounded text-xs font-medium">
  19. {{ task.coopType }}
  20. </view>
  21. <view v-if="task.serviceMode" class="px-2 py-1 bg-success-light text-success rounded text-xs font-medium">
  22. {{ task.serviceMode }}
  23. </view>
  24. </view>
  25. <view class="gap-y-2 mb-3">
  26. <view class="flex items-center">
  27. <text class="uni-icons uniui-tools info-icon text-xs mr-1"></text>
  28. <text class="text-gray-500 text-xs">{{ task.serviceType || '施工服务' }}</text>
  29. </view>
  30. <view class="flex items-center">
  31. <text class="uni-icons uniui-clock-filled info-icon text-xs mr-1"></text>
  32. <text class="text-gray-500 text-xs">{{ task.serviceDate || task.scheduleDate || '待定' }} {{ task.serviceTime || task.scheduleTime || '' }}</text>
  33. </view>
  34. <view class="flex items-center">
  35. <text class="uni-icons uniui-location-filled info-icon text-xs mr-1"></text>
  36. <text class="text-gray-500 text-xs truncate">{{ task.address || '暂无地址' }}</text>
  37. </view>
  38. </view>
  39. <view class="pt-3 border-t border-gray-100">
  40. <view class="flow-row">
  41. <view
  42. v-for="(step, i) in steps"
  43. :key="step.key"
  44. class="flow-step"
  45. >
  46. <view class="flow-node-wrap">
  47. <view v-if="i !== 0" class="flow-line" :class="step.active ? 'flow-line-active' : ''" />
  48. <view class="flow-node" :class="step.active ? 'flow-node-active' : (step.current ? 'flow-node-current' : '')">
  49. <text v-if="step.active" class="uni-icons uniui-checkmarkempty flow-check"></text>
  50. <text v-else class="flow-node-text">{{ i + 1 }}</text>
  51. </view>
  52. </view>
  53. <text class="flow-label" :class="step.active ? 'flow-label-active' : ''">{{ step.label }}</text>
  54. </view>
  55. </view>
  56. </view>
  57. </view>
  58. </template>
  59. <script setup lang="ts">
  60. import { computed } from 'vue'
  61. import StatusTag from '../common/StatusTag.vue'
  62. import type { Task } from '@/types'
  63. const props = defineProps<{
  64. task: Task
  65. }>()
  66. defineEmits<{
  67. click: [taskId: string]
  68. }>()
  69. const steps = computed(() => {
  70. const status = props.task.status
  71. const list = [
  72. { key: 'confirm', label: '确认', active: true },
  73. { key: 'depart', label: '出车', active: ['departed', 'arrived', 'constructing', 'completed', 'inspecting'].includes(status) },
  74. { key: 'arrive', label: '到达', active: ['arrived', 'constructing', 'completed', 'inspecting'].includes(status) },
  75. { key: 'construct', label: '施工', active: ['constructing', 'completed', 'inspecting'].includes(status) },
  76. { key: 'complete', label: '完成', active: ['completed', 'inspecting'].includes(status) },
  77. { key: 'clean', label: '清洗', active: props.task.cleanTime != null || status === 'inspecting' || status === 'completed' },
  78. { key: 'inspect', label: '验收', active: status === 'inspecting' || status === 'completed' },
  79. ]
  80. const currentIndex = list.findIndex(s => !s.active)
  81. return list.map((s, i) => ({ ...s, current: i === currentIndex }))
  82. })
  83. </script>
  84. <style scoped>
  85. .task-card {
  86. background-color: rgba(255, 255, 255, 0.85);
  87. backdrop-filter: blur(14px);
  88. -webkit-backdrop-filter: blur(14px);
  89. border: 1px solid rgba(164, 216, 152, 0.35);
  90. border-radius: 24px;
  91. box-shadow: 0 18px 50px -12px rgba(54, 143, 111, 0.28);
  92. overflow: hidden;
  93. }
  94. .task-card.is-emergency {
  95. border-left: 4px solid #ef4444;
  96. }
  97. .info-icon {
  98. color: #368f6f;
  99. }
  100. .chip-type {
  101. background-color: rgba(164, 216, 152, 0.2);
  102. color: #368f6f;
  103. }
  104. /* 流程条:与任务详情 SOP 流程条一致 */
  105. .flow-row {
  106. display: flex;
  107. align-items: flex-start;
  108. justify-content: space-between;
  109. }
  110. .flow-step {
  111. flex: 1;
  112. display: flex;
  113. flex-direction: column;
  114. align-items: center;
  115. min-width: 0;
  116. }
  117. .flow-node-wrap {
  118. width: 100%;
  119. height: 24px;
  120. position: relative;
  121. display: flex;
  122. align-items: center;
  123. justify-content: center;
  124. }
  125. .flow-line {
  126. position: absolute;
  127. top: 50%;
  128. left: 0;
  129. right: 50%;
  130. height: 2px;
  131. background-color: #e5e7eb;
  132. transform: translateY(-50%);
  133. }
  134. .flow-line-active {
  135. background-color: #368f6f;
  136. }
  137. .flow-node {
  138. width: 22px;
  139. height: 22px;
  140. border-radius: 50%;
  141. background-color: #f3f4f6;
  142. border: 2px solid #e5e7eb;
  143. display: flex;
  144. align-items: center;
  145. justify-content: center;
  146. z-index: 1;
  147. }
  148. .flow-node-active {
  149. background-color: #368f6f;
  150. border-color: #368f6f;
  151. }
  152. .flow-node-current {
  153. background-color: #fff;
  154. border-color: #368f6f;
  155. box-shadow: 0 0 0 3px rgba(54, 143, 111, 0.2);
  156. }
  157. .flow-check {
  158. color: #fff;
  159. font-size: 11px;
  160. }
  161. .flow-node-text {
  162. font-size: 10px;
  163. color: #9ca3af;
  164. }
  165. .flow-node-current .flow-node-text {
  166. color: #368f6f;
  167. font-weight: 600;
  168. }
  169. .flow-label {
  170. margin-top: 6px;
  171. font-size: 10px;
  172. color: #9ca3af;
  173. text-align: center;
  174. }
  175. .flow-label-active {
  176. color: #368f6f;
  177. }
  178. .scale-down {
  179. transition: transform 0.15s ease;
  180. }
  181. .scale-down:active {
  182. transform: scale(0.95);
  183. }
  184. </style>