ConstructionFlowBar.vue 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. <template>
  2. <view class="flow-bar card p-4">
  3. <view class="flex items-start justify-between">
  4. <view
  5. v-for="(step, index) in steps"
  6. :key="step.key"
  7. class="flow-step"
  8. >
  9. <view class="flow-node-wrap">
  10. <view
  11. v-if="index !== 0"
  12. class="flow-line flow-line-left"
  13. :class="step.active || step.current ? 'flow-line-active' : ''"
  14. />
  15. <view
  16. class="flow-node"
  17. :class="nodeClass(step)"
  18. >
  19. <text v-if="step.active && !step.current" class="uni-icons uniui-checkmarkempty flow-check"></text>
  20. <text v-else class="flow-node-text">{{ index + 1 }}</text>
  21. </view>
  22. <view
  23. v-if="index !== steps.length - 1"
  24. class="flow-line flow-line-right"
  25. :class="nextActive(index) ? 'flow-line-active' : ''"
  26. />
  27. </view>
  28. <text class="flow-label" :class="step.current ? 'flow-label-current' : step.active ? 'flow-label-active' : ''">
  29. {{ step.label }}
  30. </text>
  31. </view>
  32. </view>
  33. </view>
  34. </template>
  35. <script setup lang="ts">
  36. import { computed } from 'vue'
  37. import type { TimelineRecord } from './Timeline.vue'
  38. interface FlowStep {
  39. key: string
  40. label: string
  41. threshold: number
  42. active: boolean
  43. current: boolean
  44. }
  45. const props = withDefaults(
  46. defineProps<{
  47. status: string
  48. timeline?: TimelineRecord[]
  49. }>(),
  50. {
  51. timeline: () => []
  52. }
  53. )
  54. const STATUS_ORDER: string[] = [
  55. 'pending',
  56. 'confirmed',
  57. 'scheduled',
  58. 'departed',
  59. 'arrived',
  60. 'constructing',
  61. 'inspecting',
  62. 'completed'
  63. ]
  64. const currentIndex = computed(() => {
  65. const idx = STATUS_ORDER.indexOf(props.status)
  66. return idx < 0 ? 0 : idx
  67. })
  68. const steps = computed<FlowStep[]>(() => {
  69. const idx = currentIndex.value
  70. const defs: Omit<FlowStep, 'active' | 'current'>[] = [
  71. { key: 'confirm', label: '确认', threshold: 1 },
  72. { key: 'depart', label: '出车', threshold: 3 },
  73. { key: 'arrive', label: '到达', threshold: 4 },
  74. { key: 'construct', label: '施工', threshold: 5 },
  75. { key: 'inspect', label: '验收', threshold: 6 },
  76. { key: 'complete', label: '完成', threshold: 7 }
  77. ]
  78. return defs.map(d => ({
  79. ...d,
  80. active: idx >= d.threshold,
  81. current: idx === d.threshold
  82. }))
  83. })
  84. function nodeClass(step: FlowStep): string {
  85. if (step.current) return 'flow-node-current'
  86. if (step.active) return 'flow-node-active'
  87. return ''
  88. }
  89. function nextActive(index: number): boolean {
  90. const next = steps.value[index + 1]
  91. return !!next && next.active
  92. }
  93. </script>
  94. <style scoped>
  95. .flow-bar {
  96. margin-bottom: 16px;
  97. }
  98. .flow-step {
  99. flex: 1;
  100. display: flex;
  101. flex-direction: column;
  102. align-items: center;
  103. min-width: 0;
  104. }
  105. .flow-node-wrap {
  106. width: 100%;
  107. height: 28px;
  108. position: relative;
  109. display: flex;
  110. align-items: center;
  111. justify-content: center;
  112. }
  113. .flow-line {
  114. position: absolute;
  115. top: 50%;
  116. height: 2px;
  117. background-color: #e5e7eb;
  118. transform: translateY(-50%);
  119. }
  120. .flow-line-left {
  121. left: 0;
  122. right: 50%;
  123. }
  124. .flow-line-right {
  125. left: 50%;
  126. right: 0;
  127. }
  128. .flow-line-active {
  129. background-color: #2563eb;
  130. }
  131. .flow-node {
  132. width: 24px;
  133. height: 24px;
  134. border-radius: 50%;
  135. background-color: #f3f4f6;
  136. border: 2px solid #e5e7eb;
  137. display: flex;
  138. align-items: center;
  139. justify-content: center;
  140. z-index: 1;
  141. }
  142. .flow-node-active {
  143. background-color: #2563eb;
  144. border-color: #2563eb;
  145. }
  146. .flow-node-current {
  147. background-color: #fff;
  148. border-color: #2563eb;
  149. box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.2);
  150. }
  151. .flow-check {
  152. color: #fff;
  153. font-size: 12px;
  154. }
  155. .flow-node-text {
  156. font-size: 11px;
  157. color: #9ca3af;
  158. }
  159. .flow-node-current .flow-node-text {
  160. color: #2563eb;
  161. font-weight: 600;
  162. }
  163. .flow-label {
  164. margin-top: 6px;
  165. font-size: 11px;
  166. color: #9ca3af;
  167. text-align: center;
  168. }
  169. .flow-label-active {
  170. color: #2563eb;
  171. }
  172. .flow-label-current {
  173. color: #2563eb;
  174. font-weight: 600;
  175. }
  176. </style>