| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264 |
- <template>
- <section class="module-extension">
- <div class="module-extension__header">
- <div>
- <h2>{{ moduleCode }} 模块扩展数据</h2>
- <p>来自统一 module-detail 接口的同步状态、L3 下钻、业务明细和告警。</p>
- </div>
- <div class="module-extension__actions">
- <el-tag v-if="syncStatusText" size="small" :type="syncStatusType">{{ syncStatusText }}</el-tag>
- <el-button size="small" :loading="loading" @click="loadDetail">刷新扩展区</el-button>
- </div>
- </div>
- <div v-loading="loading" class="module-extension__body">
- <div v-if="drillGroups.length" class="extension-card extension-card--wide">
- <div class="extension-card__title">L3 指标下钻</div>
- <div class="drill-grid">
- <div v-for="group in drillGroups" :key="group.title" class="drill-block">
- <div class="drill-block__title">{{ group.title }}</div>
- <div class="drill-items">
- <div v-for="item in group.items" :key="item.metricCode" class="drill-item" :class="`tone-${item.statusColor || 'gray'}`">
- <span>{{ item.metricName || item.metricCode }}</span>
- <strong>{{ formatValue(item.metricValue) }}</strong>
- <small v-if="item.targetValue != null">目标 {{ formatValue(item.targetValue) }}</small>
- </div>
- </div>
- </div>
- </div>
- </div>
- <div v-if="branchKpis.length" class="extension-card extension-card--wide">
- <div class="extension-card__title">供应分支 KPI</div>
- <div class="branch-grid">
- <div v-for="group in branchKpis" :key="group.branch" class="branch-block">
- <div class="drill-block__title">{{ group.title }}</div>
- <div v-for="item in group.items" :key="item.label" class="branch-row">
- <span>{{ item.label }}</span>
- <strong>{{ formatValue(item.value) }}{{ item.unit || '' }}</strong>
- </div>
- </div>
- </div>
- </div>
- <div v-if="schedules.length" class="extension-card extension-card--wide">
- <div class="extension-card__title">排程明细</div>
- <el-table :data="schedules" size="small" max-height="260" border>
- <el-table-column prop="orderNo" label="订单/工单" min-width="130" />
- <el-table-column prop="product" label="产品" min-width="160" />
- <el-table-column prop="productionLine" label="产线" min-width="120" />
- <el-table-column prop="quantity" label="数量" width="90" />
- <el-table-column prop="deliveryDate" label="交期" min-width="120" />
- <el-table-column prop="status" label="状态" width="110" />
- </el-table>
- </div>
- <div class="extension-card">
- <div class="extension-card__title">异常与告警</div>
- <div v-if="alerts.length" class="alert-list">
- <div v-for="(alert, idx) in alerts" :key="`${alert.time}-${idx}`" class="alert-row" :class="`level-${alert.level || 'info'}`">
- <span>{{ alert.time || '--:--:--' }}</span>
- <strong>{{ String(alert.level || 'info').toUpperCase() }}</strong>
- <p>{{ alert.message }}</p>
- </div>
- </div>
- <div v-else class="empty-text">暂无告警</div>
- </div>
- </div>
- </section>
- </template>
- <script setup lang="ts">
- import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
- import { fetchModuleDetail, type ModuleDetailPayload } from '../../api/kanbanData';
- import { AIDOP_LAYOUT_SAVED } from '../utils/s4LayoutEvents';
- const props = defineProps<{ moduleCode: string }>();
- const loading = ref(false);
- const detail = ref<ModuleDetailPayload | null>(null);
- const moduleCode = computed(() => String(props.moduleCode || '').toUpperCase());
- const drillGroups = computed(() => detail.value?.drillGroups || []);
- const branchKpis = computed(() => detail.value?.branchKpis || []);
- const schedules = computed(() => detail.value?.schedules || []);
- const alerts = computed(() => detail.value?.alerts || []);
- const syncStatusText = computed(() => {
- const s = detail.value?.syncStatus;
- if (!s) return '';
- const rows = [s.stageRows, s.standardRows, s.dwdRows].filter((x) => x != null).join('/');
- return `MDP ${s.status || 'UNKNOWN'}${rows ? ` (${rows})` : ''}`;
- });
- const syncStatusType = computed(() => {
- const status = String(detail.value?.syncStatus?.status || '').toUpperCase();
- if (status === 'SUCCESS') return 'success';
- if (status === 'RUNNING') return 'warning';
- if (status === 'FAILED') return 'danger';
- return 'info';
- });
- async function loadDetail() {
- loading.value = true;
- try {
- detail.value = await fetchModuleDetail(moduleCode.value);
- } finally {
- loading.value = false;
- }
- }
- function formatValue(value?: number | null) {
- if (value == null || Number.isNaN(Number(value))) return '—';
- const n = Number(value);
- return Math.abs(n - Math.round(n)) < 1e-9 ? String(Math.round(n)) : String(Math.round(n * 100) / 100);
- }
- function onLayoutSaved(event: Event) {
- const saved = (event as CustomEvent<{ moduleCode?: string }>).detail?.moduleCode;
- if (!saved || saved.toUpperCase() === moduleCode.value) loadDetail();
- }
- onMounted(() => {
- loadDetail();
- window.addEventListener(AIDOP_LAYOUT_SAVED, onLayoutSaved as EventListener);
- });
- onUnmounted(() => {
- window.removeEventListener(AIDOP_LAYOUT_SAVED, onLayoutSaved as EventListener);
- });
- watch(moduleCode, loadDetail);
- </script>
- <style scoped>
- .module-extension {
- margin-top: 16px;
- border: 1px solid #1e293b;
- border-radius: 14px;
- padding: 16px;
- background: rgba(15, 23, 42, 0.72);
- }
- .module-extension__header {
- display: flex;
- justify-content: space-between;
- gap: 12px;
- align-items: center;
- margin-bottom: 14px;
- }
- .module-extension__header h2 {
- margin: 0;
- font-size: 16px;
- color: #f8fafc;
- }
- .module-extension__header p {
- margin: 4px 0 0;
- color: #94a3b8;
- font-size: 12px;
- }
- .module-extension__actions {
- display: flex;
- gap: 8px;
- align-items: center;
- }
- .module-extension__body {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
- gap: 12px;
- }
- .extension-card {
- border: 1px solid #263244;
- border-radius: 10px;
- padding: 12px;
- background: rgba(2, 6, 23, 0.38);
- }
- .extension-card--wide {
- grid-column: 1 / -1;
- }
- .extension-card__title,
- .drill-block__title {
- font-size: 13px;
- font-weight: 700;
- color: #e2e8f0;
- margin-bottom: 10px;
- }
- .drill-grid,
- .branch-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
- gap: 10px;
- }
- .drill-items {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- }
- .drill-item {
- flex: 1 1 160px;
- border: 1px solid #334155;
- border-radius: 8px;
- padding: 10px;
- color: #cbd5e1;
- }
- .drill-item strong {
- display: block;
- margin-top: 6px;
- font-size: 18px;
- color: #f8fafc;
- }
- .drill-item small {
- color: #64748b;
- }
- .tone-green { border-color: rgba(52, 211, 153, 0.45); }
- .tone-yellow { border-color: rgba(251, 191, 36, 0.45); }
- .tone-red { border-color: rgba(239, 68, 68, 0.45); }
- .branch-row,
- .alert-row {
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 8px 0;
- border-bottom: 1px solid rgba(51, 65, 85, 0.55);
- color: #cbd5e1;
- }
- .branch-row span,
- .alert-row p {
- flex: 1;
- margin: 0;
- }
- .alert-row span {
- color: #64748b;
- font-family: Consolas, 'Courier New', monospace;
- }
- .alert-row strong {
- color: #fbbf24;
- }
- .alert-row.level-critical strong,
- .alert-row.level-high strong,
- .alert-row.level-danger strong {
- color: #f87171;
- }
- .empty-text {
- color: #94a3b8;
- font-size: 12px;
- }
- </style>
|