|
|
@@ -1,9 +1,11 @@
|
|
|
<script setup lang="ts" name="OrderExecutionCustomerGroups">
|
|
|
-import { computed } from 'vue';
|
|
|
+import { computed, ref } from 'vue';
|
|
|
import type {
|
|
|
CustomerGroup,
|
|
|
SalesOrderExecution,
|
|
|
} from '/@/views/aidop/s8/monitoring/data/order-execution/types';
|
|
|
+import { computeCustomerGroupStats } from '/@/views/aidop/s8/monitoring/data/order-execution/aggregation';
|
|
|
+import OrderExecutionOrderCard from './OrderExecutionOrderCard.vue';
|
|
|
|
|
|
interface Props {
|
|
|
customers: CustomerGroup[];
|
|
|
@@ -22,26 +24,44 @@ function onOpenChain(order: SalesOrderExecution) {
|
|
|
emit('open-chain', order);
|
|
|
}
|
|
|
|
|
|
-const customerCardData = computed(() =>
|
|
|
- props.customers.map((customer) => ({
|
|
|
- id: customer.id,
|
|
|
- name: customer.name,
|
|
|
- type: customer.type,
|
|
|
- orderCount: customer.orders.length,
|
|
|
- previewOrders: customer.orders.slice(0, props.previewLimit),
|
|
|
- })),
|
|
|
-);
|
|
|
+const expanded = ref<Set<string>>(new Set());
|
|
|
|
|
|
-const workflowText = (status: SalesOrderExecution['workflowStatus']) =>
|
|
|
- status === 'in_progress' ? '执行中' : '已完成';
|
|
|
+function toggleExpanded(customerId: string) {
|
|
|
+ const next = new Set(expanded.value);
|
|
|
+ if (next.has(customerId)) next.delete(customerId);
|
|
|
+ else next.add(customerId);
|
|
|
+ expanded.value = next;
|
|
|
+}
|
|
|
|
|
|
-const nodeStatusClass = (status: SalesOrderExecution['nodeStatus']) =>
|
|
|
- `oe-order-node--${status}`;
|
|
|
+const fmtMinutes = (v: number | null) => (v == null ? '--' : `${v.toFixed(1)} 分`);
|
|
|
+
|
|
|
+const customerCardData = computed(() =>
|
|
|
+ props.customers.map((customer) => {
|
|
|
+ const stats = computeCustomerGroupStats(customer.orders);
|
|
|
+ const isExpanded = expanded.value.has(customer.id);
|
|
|
+ const visibleOrders = isExpanded
|
|
|
+ ? customer.orders
|
|
|
+ : customer.orders.slice(0, props.previewLimit);
|
|
|
+ return {
|
|
|
+ id: customer.id,
|
|
|
+ name: customer.name,
|
|
|
+ type: customer.type,
|
|
|
+ isExpanded,
|
|
|
+ canToggle: customer.orders.length > props.previewLimit,
|
|
|
+ visibleOrders,
|
|
|
+ totalOrders: stats.totalOrders,
|
|
|
+ totalExceptions: stats.totalExceptions,
|
|
|
+ avgResponseText: fmtMinutes(stats.avgResponse),
|
|
|
+ avgProcessingText: fmtMinutes(stats.avgProcessing),
|
|
|
+ avgLossText: fmtMinutes(stats.avgLoss),
|
|
|
+ };
|
|
|
+ }),
|
|
|
+);
|
|
|
</script>
|
|
|
|
|
|
<template>
|
|
|
<div class="oe-customer-groups">
|
|
|
- <div
|
|
|
+ <section
|
|
|
v-for="customer in customerCardData"
|
|
|
:key="customer.id"
|
|
|
class="oe-customer-card"
|
|
|
@@ -54,55 +74,65 @@ const nodeStatusClass = (status: SalesOrderExecution['nodeStatus']) =>
|
|
|
</span>
|
|
|
</div>
|
|
|
<div class="oe-customer-card__count">
|
|
|
- <span class="oe-customer-card__count-num">{{ customer.orderCount }}</span>
|
|
|
+ <span class="oe-customer-card__count-num">{{ customer.totalOrders }}</span>
|
|
|
<span class="oe-customer-card__count-label">订单</span>
|
|
|
</div>
|
|
|
</header>
|
|
|
|
|
|
- <ul class="oe-customer-card__orders">
|
|
|
- <li
|
|
|
- v-for="order in customer.previewOrders"
|
|
|
+ <dl class="oe-customer-card__stats">
|
|
|
+ <div class="oe-customer-card__stat">
|
|
|
+ <dt>异常总数</dt>
|
|
|
+ <dd :class="customer.totalExceptions > 0 && 'oe-customer-card__stat-value--accent'">
|
|
|
+ {{ customer.totalExceptions }}
|
|
|
+ </dd>
|
|
|
+ </div>
|
|
|
+ <div class="oe-customer-card__stat">
|
|
|
+ <dt>平均响应</dt>
|
|
|
+ <dd>{{ customer.avgResponseText }}</dd>
|
|
|
+ </div>
|
|
|
+ <div class="oe-customer-card__stat">
|
|
|
+ <dt>平均处理</dt>
|
|
|
+ <dd>{{ customer.avgProcessingText }}</dd>
|
|
|
+ </div>
|
|
|
+ <div class="oe-customer-card__stat">
|
|
|
+ <dt>平均损失</dt>
|
|
|
+ <dd>{{ customer.avgLossText }}</dd>
|
|
|
+ </div>
|
|
|
+ </dl>
|
|
|
+
|
|
|
+ <div class="oe-customer-card__orders">
|
|
|
+ <OrderExecutionOrderCard
|
|
|
+ v-for="order in customer.visibleOrders"
|
|
|
:key="order.soNo"
|
|
|
- class="oe-order-row"
|
|
|
+ :order="order"
|
|
|
+ @open-chain="onOpenChain"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <footer v-if="customer.canToggle" class="oe-customer-card__footer">
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ class="oe-customer-card__toggle"
|
|
|
+ @click="toggleExpanded(customer.id)"
|
|
|
>
|
|
|
- <div class="oe-order-row__top">
|
|
|
- <span class="oe-order-row__so">{{ order.soNo }}</span>
|
|
|
- <span class="oe-order-row__product">{{ order.productName }}</span>
|
|
|
- <button
|
|
|
- type="button"
|
|
|
- class="oe-order-row__chain-btn"
|
|
|
- @click="onOpenChain(order)"
|
|
|
- >
|
|
|
- 链路全景
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- <div class="oe-order-row__bottom">
|
|
|
- <span class="oe-order-row__node" :class="nodeStatusClass(order.nodeStatus)">
|
|
|
- {{ order.currentNode }}
|
|
|
- </span>
|
|
|
- <span class="oe-order-row__exception">
|
|
|
- 异常 {{ order.exceptionCount }}
|
|
|
- </span>
|
|
|
- <span class="oe-order-row__workflow">
|
|
|
- {{ workflowText(order.workflowStatus) }}
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- </li>
|
|
|
- </ul>
|
|
|
- </div>
|
|
|
+ {{ customer.isExpanded ? '收起' : `展开全部 ${customer.totalOrders} 条` }}
|
|
|
+ </button>
|
|
|
+ </footer>
|
|
|
+ </section>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<style scoped>
|
|
|
.oe-customer-groups {
|
|
|
display: grid;
|
|
|
- grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
|
+ grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
|
|
|
gap: 14px;
|
|
|
}
|
|
|
|
|
|
.oe-customer-card {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
+ gap: 10px;
|
|
|
padding: 14px 16px;
|
|
|
border-radius: 14px;
|
|
|
background: var(--order-panel, rgba(25, 28, 34, 0.72));
|
|
|
@@ -113,7 +143,6 @@ const nodeStatusClass = (status: SalesOrderExecution['nodeStatus']) =>
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
|
- margin-bottom: 10px;
|
|
|
}
|
|
|
|
|
|
.oe-customer-card__title {
|
|
|
@@ -135,20 +164,9 @@ const nodeStatusClass = (status: SalesOrderExecution['nodeStatus']) =>
|
|
|
line-height: 1.4;
|
|
|
}
|
|
|
|
|
|
-.oe-customer-card__type--ka {
|
|
|
- background: rgba(123, 208, 255, 0.16);
|
|
|
- color: #7bd0ff;
|
|
|
-}
|
|
|
-
|
|
|
-.oe-customer-card__type--smb {
|
|
|
- background: rgba(109, 224, 57, 0.14);
|
|
|
- color: #88fd54;
|
|
|
-}
|
|
|
-
|
|
|
-.oe-customer-card__type--micro {
|
|
|
- background: rgba(255, 193, 7, 0.14);
|
|
|
- color: #ffc107;
|
|
|
-}
|
|
|
+.oe-customer-card__type--ka { background: rgba(123, 208, 255, 0.16); color: #7bd0ff; }
|
|
|
+.oe-customer-card__type--smb { background: rgba(109, 224, 57, 0.14); color: #88fd54; }
|
|
|
+.oe-customer-card__type--micro { background: rgba(255, 193, 7, 0.14); color: #ffc107; }
|
|
|
|
|
|
.oe-customer-card__count {
|
|
|
display: flex;
|
|
|
@@ -167,94 +185,61 @@ const nodeStatusClass = (status: SalesOrderExecution['nodeStatus']) =>
|
|
|
font-size: 12px;
|
|
|
}
|
|
|
|
|
|
-.oe-customer-card__orders {
|
|
|
+.oe-customer-card__stats {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
|
+ gap: 6px;
|
|
|
margin: 0;
|
|
|
- padding: 0;
|
|
|
- list-style: none;
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- gap: 8px;
|
|
|
-}
|
|
|
-
|
|
|
-.oe-order-row {
|
|
|
padding: 8px 10px;
|
|
|
border-radius: 10px;
|
|
|
background: rgba(15, 19, 26, 0.55);
|
|
|
- border: 1px solid rgba(69, 70, 77, 0.28);
|
|
|
+ border: 1px solid rgba(69, 70, 77, 0.18);
|
|
|
}
|
|
|
|
|
|
-.oe-order-row__top {
|
|
|
+.oe-customer-card__stat {
|
|
|
display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 10px;
|
|
|
- margin-bottom: 4px;
|
|
|
-}
|
|
|
-
|
|
|
-.oe-order-row__chain-btn {
|
|
|
- margin-left: auto;
|
|
|
- padding: 2px 10px;
|
|
|
- border-radius: 999px;
|
|
|
- border: 1px solid rgba(123, 208, 255, 0.32);
|
|
|
- background: rgba(123, 208, 255, 0.08);
|
|
|
- color: #7bd0ff;
|
|
|
- font-size: 11px;
|
|
|
- cursor: pointer;
|
|
|
- flex: 0 0 auto;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 2px;
|
|
|
+ margin: 0;
|
|
|
}
|
|
|
|
|
|
-.oe-order-row__chain-btn:hover {
|
|
|
- background: rgba(123, 208, 255, 0.18);
|
|
|
+.oe-customer-card__stat dt {
|
|
|
+ font-size: 10px;
|
|
|
+ color: var(--order-text-muted, #909097);
|
|
|
}
|
|
|
|
|
|
-.oe-order-row__so {
|
|
|
+.oe-customer-card__stat dd {
|
|
|
+ margin: 0;
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: var(--order-text-primary, #e1e2eb);
|
|
|
font-family: 'JetBrains Mono', 'Cascadia Code', Menlo, monospace;
|
|
|
- font-size: 12px;
|
|
|
- color: #7bd0ff;
|
|
|
}
|
|
|
|
|
|
-.oe-order-row__product {
|
|
|
- font-size: 13px;
|
|
|
- color: var(--order-text-primary, #e1e2eb);
|
|
|
- overflow: hidden;
|
|
|
- text-overflow: ellipsis;
|
|
|
- white-space: nowrap;
|
|
|
+.oe-customer-card__stat-value--accent { color: #ffc107; }
|
|
|
+
|
|
|
+.oe-customer-card__orders {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8px;
|
|
|
}
|
|
|
|
|
|
-.oe-order-row__bottom {
|
|
|
+.oe-customer-card__footer {
|
|
|
display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 12px;
|
|
|
- font-size: 11px;
|
|
|
- color: var(--order-text-secondary, #c6c6cd);
|
|
|
+ justify-content: center;
|
|
|
}
|
|
|
|
|
|
-.oe-order-row__node {
|
|
|
- padding: 1px 8px;
|
|
|
+.oe-customer-card__toggle {
|
|
|
+ padding: 4px 14px;
|
|
|
border-radius: 999px;
|
|
|
+ border: 1px solid rgba(123, 208, 255, 0.28);
|
|
|
+ background: rgba(123, 208, 255, 0.06);
|
|
|
+ color: #7bd0ff;
|
|
|
font-size: 11px;
|
|
|
+ cursor: pointer;
|
|
|
}
|
|
|
|
|
|
-.oe-order-node--green {
|
|
|
- background: rgba(109, 224, 57, 0.12);
|
|
|
- color: #88fd54;
|
|
|
-}
|
|
|
-
|
|
|
-.oe-order-node--yellow {
|
|
|
- background: rgba(255, 193, 7, 0.12);
|
|
|
- color: #ffc107;
|
|
|
-}
|
|
|
-
|
|
|
-.oe-order-node--red {
|
|
|
- background: rgba(255, 180, 171, 0.14);
|
|
|
- color: #ffb4ab;
|
|
|
-}
|
|
|
-
|
|
|
-.oe-order-row__exception {
|
|
|
- color: #ffc107;
|
|
|
-}
|
|
|
-
|
|
|
-.oe-order-row__workflow {
|
|
|
- margin-left: auto;
|
|
|
- color: var(--order-text-muted, #909097);
|
|
|
+.oe-customer-card__toggle:hover {
|
|
|
+ background: rgba(123, 208, 255, 0.16);
|
|
|
}
|
|
|
</style>
|