WorkOrderTimeline.vue 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. <!--
  2. 单张工单的时间线(票 12):**状态路径** + **动作轨迹**两段。
  3. 为什么两段都要有(而不是只画一条动作流水):
  4. · 动作流水回答「什么时候、谁、做了什么」——它是**发生过的事实**;
  5. · 状态路径回答「这张工单在码表里的什么位置」——同一时刻只有一张工单的整体位置。
  6. 只画流水,值班人员得自己数着状态读;只画路径,回退(待验收→处理中)就看不出来了。
  7. 两条本票验收原句的落地方式:
  8. · 「验收驳回显示为**回退动作**,不是独立终态」→ 回退在动作流水里是一行带 `回退` 标记的记录
  9. (`domain/workOrderTimeline.js` 按状态码比较判定),状态路径**只铺码表里的节点**,
  10. 驳回不会多出节点;
  11. · 「不展示延期状态」→ **票 20 已按后端枚举更正**:码表是 `WorkOrderStatusEnum` 的 0–7,
  12. 其中 **6 就是「延期中」**(处理旁路),是正式状态,不再是"不展示"的档位。
  13. 「已取消」(7) 与「延期中」(6) 都刻意画成**路径外**节点:主线是 0→5,
  14. 把它们接在主线上看起来就像多出来的一步。
  15. 只读:本组件没有任何写入口(spec 验收第 6 条),也不改工单状态。
  16. -->
  17. <template>
  18. <div class="wo-timeline" data-work-order-timeline :data-wo-code="track.code">
  19. <!-- ---------- 状态路径:码表节点一个不少,当前位置与走到过的最远处分开标 ---------- -->
  20. <ol class="wo-timeline__path" data-wo-path>
  21. <li
  22. v-for="node in nodes"
  23. :key="node.code"
  24. class="wo-timeline__node"
  25. :data-wo-path-node="node.code"
  26. :data-wo-path-name="node.name"
  27. :data-wo-path-state="node.current ? 'current' : node.reached ? 'reached' : 'pending'"
  28. :data-wo-path-off="node.offPath ? 'true' : 'false'"
  29. :data-wo-path-bypass="node.bypass ? 'true' : 'false'"
  30. :title="`${node.code} ${node.name}${node.bypass ? '(处理旁路)' : node.offPath ? '(路径外终态)' : ''}`"
  31. >
  32. <i class="wo-timeline__dot" aria-hidden="true"></i>
  33. <span class="wo-timeline__node-name">{{ node.name }}</span>
  34. </li>
  35. </ol>
  36. <!-- ---------- 动作轨迹:每条记录一行,回退单独带标记 ---------- -->
  37. <ol v-if="track.steps.length" class="wo-timeline__steps" data-wo-steps>
  38. <li
  39. v-for="step in track.steps"
  40. :key="step.key"
  41. class="wo-timeline__step"
  42. :class="{ 'is-rollback': step.isRollback }"
  43. :data-wo-step="step.index"
  44. :data-wo-step-action="step.action"
  45. :data-wo-step-state="step.stateCode === null ? '' : step.stateCode"
  46. >
  47. <span class="wo-timeline__step-head">
  48. <span class="wo-timeline__time">{{ step.operatedAt || placeholder }}</span>
  49. <span class="wo-timeline__state">{{ step.stateName || placeholder }}</span>
  50. <!-- 回退的可见标记:颜色之外的第二种提示(灰度下也读得出) -->
  51. <em v-if="step.isRollback" class="wo-timeline__rollback" data-wo-rollback>
  52. {{ step.actionLabel }} · 自{{ step.fromStateName }}
  53. </em>
  54. </span>
  55. <span class="wo-timeline__step-body">
  56. <span class="wo-timeline__op">{{ step.operationName || placeholder }}</span>
  57. <span v-if="step.operationDesc" class="wo-timeline__desc">{{ step.operationDesc }}</span>
  58. <span v-if="step.operatorName" class="wo-timeline__who">{{ step.operatorName }}</span>
  59. </span>
  60. </li>
  61. </ol>
  62. <!--
  63. 没有动作流水时:说清楚是哪一种"没有"。
  64. 轨迹分区没拿到(`pathOnly`)时**不说**「这张工单没有动作记录」——
  65. 那是把"没取到"说成"没有"(两者在工单面板里是分开的两句话,见 EquipmentDetailSections.vue)。
  66. -->
  67. <p v-else-if="!pathOnly" class="wo-timeline__empty" data-wo-steps-empty>这张工单没有动作记录</p>
  68. </div>
  69. </template>
  70. <script setup>
  71. import { computed } from 'vue'
  72. import { DETAIL_PLACEHOLDER } from '../domain/detailFlow.js'
  73. import { pathView } from '../domain/workOrderTimeline.js'
  74. const props = defineProps({
  75. /** `domain/equipmentDetailView.js` 的 `timelineSource().tracks[workOrderCode]`。 */
  76. track: { type: Object, required: true },
  77. /**
  78. * 只画状态路径、不画动作流水(轨迹分区没取到时由调用方传 true)。
  79. * 它决定"没有流水"这一行**说不出**「这张工单没有动作记录」—— 见上面的注释。
  80. */
  81. pathOnly: { type: Boolean, default: false },
  82. })
  83. const placeholder = DETAIL_PLACEHOLDER
  84. const nodes = computed(() => pathView(props.track))
  85. </script>
  86. <style scoped>
  87. .wo-timeline,
  88. .wo-timeline * {
  89. box-sizing: border-box;
  90. }
  91. .wo-timeline {
  92. display: grid;
  93. gap: calc(3 * var(--u, 1px));
  94. margin-top: calc(3 * var(--u, 1px));
  95. padding-left: calc(6 * var(--u, 1px));
  96. border-left: 1px solid rgba(1, 171, 234, 0.28);
  97. }
  98. /* 状态路径:横向铺开,节点等宽;窄栏里允许换行。 */
  99. .wo-timeline__path {
  100. display: flex;
  101. flex-wrap: wrap;
  102. gap: calc(4 * var(--u, 1px));
  103. margin: 0;
  104. padding: 0;
  105. list-style: none;
  106. }
  107. .wo-timeline__node {
  108. display: inline-flex;
  109. align-items: center;
  110. gap: 3px;
  111. padding: 1px 5px;
  112. border: 1px solid rgba(1, 171, 234, 0.2);
  113. border-radius: calc(8 * var(--u, 1px));
  114. background: rgba(2, 9, 27, 0.45);
  115. color: var(--muted, #5680b0);
  116. font-size: var(--fs-caption, 10.5px);
  117. white-space: nowrap;
  118. }
  119. .wo-timeline__dot {
  120. flex: none;
  121. width: 5px;
  122. height: 5px;
  123. border-radius: 50%;
  124. background: currentcolor;
  125. }
  126. /* 走到过的路径态:说明"到过这里",但与当前态区分(回退后两者不同)。 */
  127. .wo-timeline__node[data-wo-path-state='reached'] {
  128. border-color: rgba(1, 171, 234, 0.5);
  129. color: var(--text-2, #88b7e8);
  130. }
  131. /* 当前所在态:最亮、带辉光。 */
  132. .wo-timeline__node[data-wo-path-state='current'] {
  133. border-color: var(--lv3, #01abea);
  134. background: rgba(0, 82, 168, 0.55);
  135. color: var(--text);
  136. box-shadow: inset 0 0 10px rgba(1, 171, 234, 0.28);
  137. }
  138. /* 已取消:铜色偏红,一眼与推进路径分开。 */
  139. .wo-timeline__node[data-wo-path-off='true'][data-wo-path-state='current'] {
  140. border-color: var(--lv1, #ff3b52);
  141. background: rgba(94, 24, 34, 0.5);
  142. color: #ffc2c9;
  143. box-shadow: none;
  144. }
  145. .wo-timeline__steps {
  146. display: grid;
  147. gap: calc(2 * var(--u, 1px));
  148. margin: 0;
  149. padding: 0;
  150. list-style: none;
  151. }
  152. .wo-timeline__step {
  153. display: grid;
  154. gap: 1px;
  155. padding: 2px 0 2px calc(5 * var(--u, 1px));
  156. border-left: 2px solid rgba(1, 171, 234, 0.35);
  157. }
  158. /* 回退:左侧换成警示色 + 记录里带「回退」文字标记。 */
  159. .wo-timeline__step.is-rollback {
  160. border-left-color: var(--lv2, #ffb020);
  161. background: linear-gradient(90deg, rgba(255, 176, 32, 0.12), transparent 70%);
  162. }
  163. .wo-timeline__step-head,
  164. .wo-timeline__step-body {
  165. display: flex;
  166. flex-wrap: wrap;
  167. gap: var(--space-2, 8px);
  168. align-items: baseline;
  169. }
  170. .wo-timeline__time,
  171. .wo-timeline__who,
  172. .wo-timeline__desc {
  173. color: var(--muted, #5680b0);
  174. font-size: var(--fs-caption, 10.5px);
  175. }
  176. .wo-timeline__time {
  177. font-family: var(--num, monospace);
  178. }
  179. .wo-timeline__state {
  180. color: var(--text, #d7ecff);
  181. font-size: var(--fs-caption, 10.5px);
  182. font-weight: 700;
  183. }
  184. .wo-timeline__rollback {
  185. color: var(--lv2, #ffb020);
  186. font-size: var(--fs-caption, 10.5px);
  187. font-style: normal;
  188. letter-spacing: 0.04em;
  189. }
  190. .wo-timeline__op {
  191. color: var(--text-2, #88b7e8);
  192. font-size: var(--fs-caption, 10.5px);
  193. }
  194. .wo-timeline__empty {
  195. margin: 0;
  196. color: var(--muted, #5680b0);
  197. font-size: var(--fs-caption, 10.5px);
  198. letter-spacing: 0.04em;
  199. }
  200. </style>