|
|
@@ -0,0 +1,324 @@
|
|
|
+<script setup lang="ts" name="FinalAssemblyCollabPanel">
|
|
|
+// ORDER-CHAIN-FINAL-ASSEMBLY-DEPT-COLLAB-MVP-1:总装发货 · 末端协同达成面板。
|
|
|
+// 顶部摘要走 stageSnapshot 真实字段;主门禁/并行准备矩阵中无真实来源字段统一显示 "--",
|
|
|
+// 状态灯仅在真实状态可派生时着色,不复制总装发货整体绿灯到每个门禁。
|
|
|
+import { computed } from 'vue';
|
|
|
+import type { S8OrderFlowStageSnapshotItem } from '/@/views/aidop/s8/monitoring/data/order-execution/domainMapper';
|
|
|
+import {
|
|
|
+ MAIN_GATES,
|
|
|
+ PARALLEL_PREPS,
|
|
|
+ type FinalAssemblyCollabSummary,
|
|
|
+ type FinalAssemblyObjectStat,
|
|
|
+} from '/@/views/aidop/s8/monitoring/data/order-execution/final-assembly-collab';
|
|
|
+
|
|
|
+interface Props {
|
|
|
+ aggregateFinal: S8OrderFlowStageSnapshotItem | null;
|
|
|
+ collabSummary: FinalAssemblyCollabSummary | null;
|
|
|
+ hitCount: number;
|
|
|
+}
|
|
|
+
|
|
|
+const props = defineProps<Props>();
|
|
|
+
|
|
|
+const DASH = '--';
|
|
|
+
|
|
|
+type Tone = 'green' | 'yellow' | 'red';
|
|
|
+
|
|
|
+function classifyTone(actual: number, kpi: number): Tone {
|
|
|
+ if (kpi <= 0) return 'green';
|
|
|
+ if (actual <= kpi) return 'green';
|
|
|
+ return (actual - kpi) / kpi <= 0.2 ? 'yellow' : 'red';
|
|
|
+}
|
|
|
+
|
|
|
+const TONE_LABEL: Record<Tone, string> = { green: '绿', yellow: '黄', red: '红' };
|
|
|
+const TONE_COLOR: Record<Tone, string> = {
|
|
|
+ green: '#88fd54',
|
|
|
+ yellow: '#ffc107',
|
|
|
+ red: '#ff4d4f',
|
|
|
+};
|
|
|
+
|
|
|
+const summary = computed(() => {
|
|
|
+ const final = props.aggregateFinal;
|
|
|
+ if (!final) {
|
|
|
+ return {
|
|
|
+ kpiText: DASH,
|
|
|
+ actualText: DASH,
|
|
|
+ rateText: DASH,
|
|
|
+ toneLabel: DASH,
|
|
|
+ toneColor: null as string | null,
|
|
|
+ };
|
|
|
+ }
|
|
|
+ const kpi = final.kpiAvgDays;
|
|
|
+ const actual = final.actualAvgDays;
|
|
|
+ const tone = classifyTone(actual, kpi);
|
|
|
+ return {
|
|
|
+ kpiText: `${kpi.toFixed(1)} 天`,
|
|
|
+ actualText: `${actual.toFixed(1)} 天`,
|
|
|
+ rateText: `${final.onTimeRate}%`,
|
|
|
+ toneLabel: TONE_LABEL[tone],
|
|
|
+ toneColor: TONE_COLOR[tone],
|
|
|
+ };
|
|
|
+});
|
|
|
+
|
|
|
+const hitText = computed(() =>
|
|
|
+ props.hitCount > 0 ? `${props.hitCount} 单` : DASH,
|
|
|
+);
|
|
|
+
|
|
|
+interface GateCell {
|
|
|
+ code: string;
|
|
|
+ name: string;
|
|
|
+ ownerSide: string;
|
|
|
+ kpiText: string;
|
|
|
+ actualText: string;
|
|
|
+ varianceText: string;
|
|
|
+ impactedOrderText: string;
|
|
|
+ complianceText: string;
|
|
|
+ complianceTone: Tone | null;
|
|
|
+}
|
|
|
+
|
|
|
+interface ParallelRow {
|
|
|
+ code: string;
|
|
|
+ name: string;
|
|
|
+ ownerSide: string;
|
|
|
+ kittingRateText: string;
|
|
|
+ riskOrderText: string;
|
|
|
+ topRiskText: string;
|
|
|
+ topRiskColor: string | null;
|
|
|
+ statusText: string;
|
|
|
+ statusTone: Tone | null;
|
|
|
+}
|
|
|
+
|
|
|
+function pickStat(map: Map<string, FinalAssemblyObjectStat> | undefined, code: string): FinalAssemblyObjectStat | null {
|
|
|
+ if (!map) return null;
|
|
|
+ return map.get(code) ?? null;
|
|
|
+}
|
|
|
+
|
|
|
+const gateCells = computed<GateCell[]>(() =>
|
|
|
+ MAIN_GATES.map((g) => {
|
|
|
+ const stat = pickStat(props.collabSummary?.gates, g.code);
|
|
|
+ const impacted = stat?.impactedOrderCount;
|
|
|
+ return {
|
|
|
+ code: g.code,
|
|
|
+ name: g.name,
|
|
|
+ ownerSide: g.ownerSideText,
|
|
|
+ // 门禁级 KPI / 实际达成 / 偏差 / 达标情况:当前系统无门禁级字段,统一 "--",不复制节点级数据。
|
|
|
+ kpiText: DASH,
|
|
|
+ actualText: DASH,
|
|
|
+ varianceText: DASH,
|
|
|
+ impactedOrderText: impacted == null ? DASH : `${impacted} 单`,
|
|
|
+ complianceText: DASH,
|
|
|
+ complianceTone: null,
|
|
|
+ };
|
|
|
+ }),
|
|
|
+);
|
|
|
+
|
|
|
+function pickSeverityColor(severity: string | null): string | null {
|
|
|
+ if (!severity) return null;
|
|
|
+ const s = severity.toUpperCase();
|
|
|
+ if (s === 'SERIOUS') return TONE_COLOR.red;
|
|
|
+ if (s === 'FOLLOW') return TONE_COLOR.yellow;
|
|
|
+ return null;
|
|
|
+}
|
|
|
+
|
|
|
+const parallelRows = computed<ParallelRow[]>(() =>
|
|
|
+ PARALLEL_PREPS.map((p) => {
|
|
|
+ const stat = pickStat(props.collabSummary?.parallels, p.code);
|
|
|
+ const risk = stat?.riskOrderCount;
|
|
|
+ const title = stat?.topRiskTitle;
|
|
|
+ return {
|
|
|
+ code: p.code,
|
|
|
+ name: p.name,
|
|
|
+ ownerSide: p.ownerSideText,
|
|
|
+ // 齐套率:无公式 / 无数据源,统一 "--"。
|
|
|
+ kittingRateText: DASH,
|
|
|
+ riskOrderText: risk == null ? DASH : `${risk} 单`,
|
|
|
+ topRiskText: title && title.length > 0 ? title : DASH,
|
|
|
+ topRiskColor: pickSeverityColor(stat?.topRiskSeverity ?? null),
|
|
|
+ statusText: DASH,
|
|
|
+ statusTone: null,
|
|
|
+ };
|
|
|
+ }),
|
|
|
+);
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <section class="fac-panel">
|
|
|
+ <header class="fac-panel__head">
|
|
|
+ <span class="fac-panel__bar" />
|
|
|
+ <h2 class="fac-panel__title">总装发货 · 末端协同达成</h2>
|
|
|
+ </header>
|
|
|
+
|
|
|
+ <div class="fac-panel__summary">
|
|
|
+ <div class="fac-panel__summary-cell">
|
|
|
+ <span class="fac-panel__summary-label">KPI</span>
|
|
|
+ <span class="fac-panel__summary-value">{{ summary.kpiText }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="fac-panel__summary-cell">
|
|
|
+ <span class="fac-panel__summary-label">实际</span>
|
|
|
+ <span class="fac-panel__summary-value">{{ summary.actualText }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="fac-panel__summary-cell">
|
|
|
+ <span class="fac-panel__summary-label">达标率</span>
|
|
|
+ <span class="fac-panel__summary-value">{{ summary.rateText }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="fac-panel__summary-cell">
|
|
|
+ <span class="fac-panel__summary-label">状态</span>
|
|
|
+ <span class="fac-panel__summary-value fac-panel__summary-value--with-dot">
|
|
|
+ <span
|
|
|
+ v-if="summary.toneColor"
|
|
|
+ class="fac-panel__dot"
|
|
|
+ :style="{ background: summary.toneColor }"
|
|
|
+ />
|
|
|
+ {{ summary.toneLabel }}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ <div class="fac-panel__summary-cell">
|
|
|
+ <span class="fac-panel__summary-label">命中订单</span>
|
|
|
+ <span class="fac-panel__summary-value">{{ hitText }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="fac-panel__block">
|
|
|
+ <div class="fac-panel__caption">主门禁状态</div>
|
|
|
+ <table class="fac-panel__table">
|
|
|
+ <thead>
|
|
|
+ <tr>
|
|
|
+ <th class="fac-panel__row-head">门禁环节</th>
|
|
|
+ <th v-for="cell in gateCells" :key="cell.code">{{ cell.name }}</th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody>
|
|
|
+ <tr>
|
|
|
+ <td class="fac-panel__row-label">责任侧</td>
|
|
|
+ <td v-for="cell in gateCells" :key="`os-${cell.code}`">{{ cell.ownerSide }}</td>
|
|
|
+ </tr>
|
|
|
+ <tr>
|
|
|
+ <td class="fac-panel__row-label">KPI</td>
|
|
|
+ <td v-for="cell in gateCells" :key="`k-${cell.code}`">{{ cell.kpiText }}</td>
|
|
|
+ </tr>
|
|
|
+ <tr>
|
|
|
+ <td class="fac-panel__row-label">实际达成</td>
|
|
|
+ <td v-for="cell in gateCells" :key="`a-${cell.code}`">{{ cell.actualText }}</td>
|
|
|
+ </tr>
|
|
|
+ <tr>
|
|
|
+ <td class="fac-panel__row-label">偏差</td>
|
|
|
+ <td v-for="cell in gateCells" :key="`v-${cell.code}`">{{ cell.varianceText }}</td>
|
|
|
+ </tr>
|
|
|
+ <tr>
|
|
|
+ <td class="fac-panel__row-label">影响订单</td>
|
|
|
+ <td v-for="cell in gateCells" :key="`io-${cell.code}`">{{ cell.impactedOrderText }}</td>
|
|
|
+ </tr>
|
|
|
+ <tr>
|
|
|
+ <td class="fac-panel__row-label">达标情况</td>
|
|
|
+ <td v-for="cell in gateCells" :key="`c-${cell.code}`">
|
|
|
+ <template v-if="cell.complianceTone">
|
|
|
+ <span class="fac-panel__dot" :style="{ background: TONE_COLOR[cell.complianceTone] }" />
|
|
|
+ {{ cell.complianceText }}
|
|
|
+ </template>
|
|
|
+ <template v-else>{{ cell.complianceText }}</template>
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="fac-panel__block">
|
|
|
+ <div class="fac-panel__caption">并行准备状态</div>
|
|
|
+ <table class="fac-panel__table">
|
|
|
+ <thead>
|
|
|
+ <tr>
|
|
|
+ <th>准备项</th>
|
|
|
+ <th>责任侧</th>
|
|
|
+ <th>齐套率</th>
|
|
|
+ <th>风险订单</th>
|
|
|
+ <th>主要风险</th>
|
|
|
+ <th>状态</th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody>
|
|
|
+ <tr v-for="row in parallelRows" :key="row.code">
|
|
|
+ <td class="fac-panel__row-label">{{ row.name }}</td>
|
|
|
+ <td>{{ row.ownerSide }}</td>
|
|
|
+ <td>{{ row.kittingRateText }}</td>
|
|
|
+ <td>{{ row.riskOrderText }}</td>
|
|
|
+ <td :style="row.topRiskColor ? { color: row.topRiskColor, fontWeight: 600 } : undefined">
|
|
|
+ {{ row.topRiskText }}
|
|
|
+ </td>
|
|
|
+ <td>
|
|
|
+ <template v-if="row.statusTone">
|
|
|
+ <span class="fac-panel__dot" :style="{ background: TONE_COLOR[row.statusTone] }" />
|
|
|
+ {{ row.statusText }}
|
|
|
+ </template>
|
|
|
+ <template v-else>{{ row.statusText }}</template>
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.fac-panel { display: flex; flex-direction: column; gap: 12px; }
|
|
|
+.fac-panel__head { display: flex; align-items: center; gap: 10px; }
|
|
|
+.fac-panel__bar { width: 5px; height: 18px; border-radius: 999px; background: var(--order-accent, #7bd0ff); }
|
|
|
+.fac-panel__title { margin: 0; font-size: 16px; font-weight: 700; }
|
|
|
+
|
|
|
+.fac-panel__summary {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(5, minmax(0, 1fr));
|
|
|
+ gap: 12px;
|
|
|
+ padding: 12px 14px;
|
|
|
+ border-radius: 10px;
|
|
|
+ background: rgba(25, 28, 34, 0.55);
|
|
|
+ border: 1px solid rgba(69, 70, 77, 0.28);
|
|
|
+}
|
|
|
+.fac-panel__summary-cell { display: flex; flex-direction: column; gap: 4px; }
|
|
|
+.fac-panel__summary-label { font-size: 11px; color: var(--order-text-muted, #909097); }
|
|
|
+.fac-panel__summary-value {
|
|
|
+ font-size: 15px; font-weight: 700;
|
|
|
+ color: var(--order-text-primary, #e1e2eb);
|
|
|
+ font-family: 'JetBrains Mono', 'Cascadia Code', Menlo, monospace;
|
|
|
+}
|
|
|
+.fac-panel__summary-value--with-dot { display: inline-flex; align-items: center; gap: 6px; }
|
|
|
+
|
|
|
+.fac-panel__block {
|
|
|
+ background: rgba(25, 28, 34, 0.55);
|
|
|
+ border: 1px solid rgba(69, 70, 77, 0.28);
|
|
|
+ border-radius: 10px;
|
|
|
+ padding: 12px;
|
|
|
+ overflow-x: auto;
|
|
|
+}
|
|
|
+.fac-panel__caption {
|
|
|
+ font-size: 12px; color: var(--order-text-muted, #909097); margin-bottom: 8px;
|
|
|
+}
|
|
|
+.fac-panel__table {
|
|
|
+ width: 100%; border-collapse: collapse; font-size: 12px;
|
|
|
+ color: var(--order-text-primary, #e1e2eb);
|
|
|
+}
|
|
|
+.fac-panel__table th, .fac-panel__table td {
|
|
|
+ padding: 8px 10px; text-align: center;
|
|
|
+ border: 1px solid rgba(69, 70, 77, 0.18);
|
|
|
+ font-family: 'JetBrains Mono', 'Cascadia Code', Menlo, monospace;
|
|
|
+}
|
|
|
+.fac-panel__table th {
|
|
|
+ background: rgba(15, 19, 26, 0.7);
|
|
|
+ font-weight: 600; font-family: 'PingFang SC', sans-serif;
|
|
|
+}
|
|
|
+.fac-panel__row-head, .fac-panel__row-label {
|
|
|
+ width: 110px; text-align: left;
|
|
|
+ color: var(--order-text-muted, #909097);
|
|
|
+ background: rgba(15, 19, 26, 0.55);
|
|
|
+ font-family: 'PingFang SC', sans-serif; font-weight: 600;
|
|
|
+}
|
|
|
+.fac-panel__dot {
|
|
|
+ display: inline-block; width: 10px; height: 10px;
|
|
|
+ border-radius: 50%; margin-right: 4px; vertical-align: middle;
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 1080px) {
|
|
|
+ .fac-panel__summary { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
|
+}
|
|
|
+@media (max-width: 640px) {
|
|
|
+ .fac-panel__summary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
|
+}
|
|
|
+</style>
|