| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226 |
- <!--
- 单张工单的时间线(票 12):**状态路径** + **动作轨迹**两段。
- 为什么两段都要有(而不是只画一条动作流水):
- · 动作流水回答「什么时候、谁、做了什么」——它是**发生过的事实**;
- · 状态路径回答「这张工单在码表里的什么位置」——同一时刻只有一张工单的整体位置。
- 只画流水,值班人员得自己数着状态读;只画路径,回退(待验收→处理中)就看不出来了。
- 两条本票验收原句的落地方式:
- · 「验收驳回显示为**回退动作**,不是独立终态」→ 回退在动作流水里是一行带 `回退` 标记的记录
- (`domain/workOrderTimeline.js` 按状态码比较判定),状态路径**只铺码表里的节点**,
- 驳回不会多出节点;
- · 「不展示延期状态」→ **票 20 已按后端枚举更正**:码表是 `WorkOrderStatusEnum` 的 0–7,
- 其中 **6 就是「延期中」**(处理旁路),是正式状态,不再是"不展示"的档位。
- 「已取消」(7) 与「延期中」(6) 都刻意画成**路径外**节点:主线是 0→5,
- 把它们接在主线上看起来就像多出来的一步。
- 只读:本组件没有任何写入口(spec 验收第 6 条),也不改工单状态。
- -->
- <template>
- <div class="wo-timeline" data-work-order-timeline :data-wo-code="track.code">
- <!-- ---------- 状态路径:码表节点一个不少,当前位置与走到过的最远处分开标 ---------- -->
- <ol class="wo-timeline__path" data-wo-path>
- <li
- v-for="node in nodes"
- :key="node.code"
- class="wo-timeline__node"
- :data-wo-path-node="node.code"
- :data-wo-path-name="node.name"
- :data-wo-path-state="node.current ? 'current' : node.reached ? 'reached' : 'pending'"
- :data-wo-path-off="node.offPath ? 'true' : 'false'"
- :data-wo-path-bypass="node.bypass ? 'true' : 'false'"
- :title="`${node.code} ${node.name}${node.bypass ? '(处理旁路)' : node.offPath ? '(路径外终态)' : ''}`"
- >
- <i class="wo-timeline__dot" aria-hidden="true"></i>
- <span class="wo-timeline__node-name">{{ node.name }}</span>
- </li>
- </ol>
- <!-- ---------- 动作轨迹:每条记录一行,回退单独带标记 ---------- -->
- <ol v-if="track.steps.length" class="wo-timeline__steps" data-wo-steps>
- <li
- v-for="step in track.steps"
- :key="step.key"
- class="wo-timeline__step"
- :class="{ 'is-rollback': step.isRollback }"
- :data-wo-step="step.index"
- :data-wo-step-action="step.action"
- :data-wo-step-state="step.stateCode === null ? '' : step.stateCode"
- >
- <span class="wo-timeline__step-head">
- <span class="wo-timeline__time">{{ step.operatedAt || placeholder }}</span>
- <span class="wo-timeline__state">{{ step.stateName || placeholder }}</span>
- <!-- 回退的可见标记:颜色之外的第二种提示(灰度下也读得出) -->
- <em v-if="step.isRollback" class="wo-timeline__rollback" data-wo-rollback>
- {{ step.actionLabel }} · 自{{ step.fromStateName }}
- </em>
- </span>
- <span class="wo-timeline__step-body">
- <span class="wo-timeline__op">{{ step.operationName || placeholder }}</span>
- <span v-if="step.operationDesc" class="wo-timeline__desc">{{ step.operationDesc }}</span>
- <span v-if="step.operatorName" class="wo-timeline__who">{{ step.operatorName }}</span>
- </span>
- </li>
- </ol>
- <!--
- 没有动作流水时:说清楚是哪一种"没有"。
- 轨迹分区没拿到(`pathOnly`)时**不说**「这张工单没有动作记录」——
- 那是把"没取到"说成"没有"(两者在工单面板里是分开的两句话,见 EquipmentDetailSections.vue)。
- -->
- <p v-else-if="!pathOnly" class="wo-timeline__empty" data-wo-steps-empty>这张工单没有动作记录</p>
- </div>
- </template>
- <script setup>
- import { computed } from 'vue'
- import { DETAIL_PLACEHOLDER } from '../domain/detailFlow.js'
- import { pathView } from '../domain/workOrderTimeline.js'
- const props = defineProps({
- /** `domain/equipmentDetailView.js` 的 `timelineSource().tracks[workOrderCode]`。 */
- track: { type: Object, required: true },
- /**
- * 只画状态路径、不画动作流水(轨迹分区没取到时由调用方传 true)。
- * 它决定"没有流水"这一行**说不出**「这张工单没有动作记录」—— 见上面的注释。
- */
- pathOnly: { type: Boolean, default: false },
- })
- const placeholder = DETAIL_PLACEHOLDER
- const nodes = computed(() => pathView(props.track))
- </script>
- <style scoped>
- .wo-timeline,
- .wo-timeline * {
- box-sizing: border-box;
- }
- .wo-timeline {
- display: grid;
- gap: calc(3 * var(--u, 1px));
- margin-top: calc(3 * var(--u, 1px));
- padding-left: calc(6 * var(--u, 1px));
- border-left: 1px solid rgba(1, 171, 234, 0.28);
- }
- /* 状态路径:横向铺开,节点等宽;窄栏里允许换行。 */
- .wo-timeline__path {
- display: flex;
- flex-wrap: wrap;
- gap: calc(4 * var(--u, 1px));
- margin: 0;
- padding: 0;
- list-style: none;
- }
- .wo-timeline__node {
- display: inline-flex;
- align-items: center;
- gap: 3px;
- padding: 1px 5px;
- border: 1px solid rgba(1, 171, 234, 0.2);
- border-radius: calc(8 * var(--u, 1px));
- background: rgba(2, 9, 27, 0.45);
- color: var(--muted, #5680b0);
- font-size: var(--fs-caption, 10.5px);
- white-space: nowrap;
- }
- .wo-timeline__dot {
- flex: none;
- width: 5px;
- height: 5px;
- border-radius: 50%;
- background: currentcolor;
- }
- /* 走到过的路径态:说明"到过这里",但与当前态区分(回退后两者不同)。 */
- .wo-timeline__node[data-wo-path-state='reached'] {
- border-color: rgba(1, 171, 234, 0.5);
- color: var(--text-2, #88b7e8);
- }
- /* 当前所在态:最亮、带辉光。 */
- .wo-timeline__node[data-wo-path-state='current'] {
- border-color: var(--lv3, #01abea);
- background: rgba(0, 82, 168, 0.55);
- color: var(--text);
- box-shadow: inset 0 0 10px rgba(1, 171, 234, 0.28);
- }
- /* 已取消:铜色偏红,一眼与推进路径分开。 */
- .wo-timeline__node[data-wo-path-off='true'][data-wo-path-state='current'] {
- border-color: var(--lv1, #ff3b52);
- background: rgba(94, 24, 34, 0.5);
- color: #ffc2c9;
- box-shadow: none;
- }
- .wo-timeline__steps {
- display: grid;
- gap: calc(2 * var(--u, 1px));
- margin: 0;
- padding: 0;
- list-style: none;
- }
- .wo-timeline__step {
- display: grid;
- gap: 1px;
- padding: 2px 0 2px calc(5 * var(--u, 1px));
- border-left: 2px solid rgba(1, 171, 234, 0.35);
- }
- /* 回退:左侧换成警示色 + 记录里带「回退」文字标记。 */
- .wo-timeline__step.is-rollback {
- border-left-color: var(--lv2, #ffb020);
- background: linear-gradient(90deg, rgba(255, 176, 32, 0.12), transparent 70%);
- }
- .wo-timeline__step-head,
- .wo-timeline__step-body {
- display: flex;
- flex-wrap: wrap;
- gap: var(--space-2, 8px);
- align-items: baseline;
- }
- .wo-timeline__time,
- .wo-timeline__who,
- .wo-timeline__desc {
- color: var(--muted, #5680b0);
- font-size: var(--fs-caption, 10.5px);
- }
- .wo-timeline__time {
- font-family: var(--num, monospace);
- }
- .wo-timeline__state {
- color: var(--text, #d7ecff);
- font-size: var(--fs-caption, 10.5px);
- font-weight: 700;
- }
- .wo-timeline__rollback {
- color: var(--lv2, #ffb020);
- font-size: var(--fs-caption, 10.5px);
- font-style: normal;
- letter-spacing: 0.04em;
- }
- .wo-timeline__op {
- color: var(--text-2, #88b7e8);
- font-size: var(--fs-caption, 10.5px);
- }
- .wo-timeline__empty {
- margin: 0;
- color: var(--muted, #5680b0);
- font-size: var(--fs-caption, 10.5px);
- letter-spacing: 0.04em;
- }
- </style>
|