ModuleDetailExtension.vue 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. <template>
  2. <section class="module-extension">
  3. <div class="module-extension__header">
  4. <div>
  5. <h2>{{ moduleCode }} 模块扩展数据</h2>
  6. <p>来自统一 module-detail 接口的同步状态、L3 下钻、业务明细和告警。</p>
  7. </div>
  8. <div class="module-extension__actions">
  9. <el-tag v-if="syncStatusText" size="small" :type="syncStatusType">{{ syncStatusText }}</el-tag>
  10. <el-button size="small" :loading="loading" @click="loadDetail">刷新扩展区</el-button>
  11. </div>
  12. </div>
  13. <div v-loading="loading" class="module-extension__body">
  14. <div v-if="drillGroups.length" class="extension-card extension-card--wide">
  15. <div class="extension-card__title">L3 指标下钻</div>
  16. <div class="drill-grid">
  17. <div v-for="group in drillGroups" :key="group.title" class="drill-block">
  18. <div class="drill-block__title">{{ group.title }}</div>
  19. <div class="drill-items">
  20. <div v-for="item in group.items" :key="item.metricCode" class="drill-item" :class="`tone-${item.statusColor || 'gray'}`">
  21. <span>{{ item.metricName || item.metricCode }}</span>
  22. <strong>{{ formatValue(item.metricValue) }}</strong>
  23. <small v-if="item.targetValue != null">目标 {{ formatValue(item.targetValue) }}</small>
  24. </div>
  25. </div>
  26. </div>
  27. </div>
  28. </div>
  29. <div v-if="branchKpis.length" class="extension-card extension-card--wide">
  30. <div class="extension-card__title">供应分支 KPI</div>
  31. <div class="branch-grid">
  32. <div v-for="group in branchKpis" :key="group.branch" class="branch-block">
  33. <div class="drill-block__title">{{ group.title }}</div>
  34. <div v-for="item in group.items" :key="item.label" class="branch-row">
  35. <span>{{ item.label }}</span>
  36. <strong>{{ formatValue(item.value) }}{{ item.unit || '' }}</strong>
  37. </div>
  38. </div>
  39. </div>
  40. </div>
  41. <div v-if="schedules.length" class="extension-card extension-card--wide">
  42. <div class="extension-card__title">排程明细</div>
  43. <el-table :data="schedules" size="small" max-height="260" border>
  44. <el-table-column prop="orderNo" label="订单/工单" min-width="130" />
  45. <el-table-column prop="product" label="产品" min-width="160" />
  46. <el-table-column prop="productionLine" label="产线" min-width="120" />
  47. <el-table-column prop="quantity" label="数量" width="90" />
  48. <el-table-column prop="deliveryDate" label="交期" min-width="120" />
  49. <el-table-column prop="status" label="状态" width="110" />
  50. </el-table>
  51. </div>
  52. <div class="extension-card">
  53. <div class="extension-card__title">异常与告警</div>
  54. <div v-if="alerts.length" class="alert-list">
  55. <div v-for="(alert, idx) in alerts" :key="`${alert.time}-${idx}`" class="alert-row" :class="`level-${alert.level || 'info'}`">
  56. <span>{{ alert.time || '--:--:--' }}</span>
  57. <strong>{{ String(alert.level || 'info').toUpperCase() }}</strong>
  58. <p>{{ alert.message }}</p>
  59. </div>
  60. </div>
  61. <div v-else class="empty-text">暂无告警</div>
  62. </div>
  63. </div>
  64. </section>
  65. </template>
  66. <script setup lang="ts">
  67. import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
  68. import { fetchModuleDetail, type ModuleDetailPayload } from '../../api/kanbanData';
  69. import { AIDOP_LAYOUT_SAVED } from '../utils/s4LayoutEvents';
  70. const props = defineProps<{ moduleCode: string }>();
  71. const loading = ref(false);
  72. const detail = ref<ModuleDetailPayload | null>(null);
  73. const moduleCode = computed(() => String(props.moduleCode || '').toUpperCase());
  74. const drillGroups = computed(() => detail.value?.drillGroups || []);
  75. const branchKpis = computed(() => detail.value?.branchKpis || []);
  76. const schedules = computed(() => detail.value?.schedules || []);
  77. const alerts = computed(() => detail.value?.alerts || []);
  78. const syncStatusText = computed(() => {
  79. const s = detail.value?.syncStatus;
  80. if (!s) return '';
  81. const rows = [s.stageRows, s.standardRows, s.dwdRows].filter((x) => x != null).join('/');
  82. return `MDP ${s.status || 'UNKNOWN'}${rows ? ` (${rows})` : ''}`;
  83. });
  84. const syncStatusType = computed(() => {
  85. const status = String(detail.value?.syncStatus?.status || '').toUpperCase();
  86. if (status === 'SUCCESS') return 'success';
  87. if (status === 'RUNNING') return 'warning';
  88. if (status === 'FAILED') return 'danger';
  89. return 'info';
  90. });
  91. async function loadDetail() {
  92. loading.value = true;
  93. try {
  94. detail.value = await fetchModuleDetail(moduleCode.value);
  95. } finally {
  96. loading.value = false;
  97. }
  98. }
  99. function formatValue(value?: number | null) {
  100. if (value == null || Number.isNaN(Number(value))) return '—';
  101. const n = Number(value);
  102. return Math.abs(n - Math.round(n)) < 1e-9 ? String(Math.round(n)) : String(Math.round(n * 100) / 100);
  103. }
  104. function onLayoutSaved(event: Event) {
  105. const saved = (event as CustomEvent<{ moduleCode?: string }>).detail?.moduleCode;
  106. if (!saved || saved.toUpperCase() === moduleCode.value) loadDetail();
  107. }
  108. onMounted(() => {
  109. loadDetail();
  110. window.addEventListener(AIDOP_LAYOUT_SAVED, onLayoutSaved as EventListener);
  111. });
  112. onUnmounted(() => {
  113. window.removeEventListener(AIDOP_LAYOUT_SAVED, onLayoutSaved as EventListener);
  114. });
  115. watch(moduleCode, loadDetail);
  116. </script>
  117. <style scoped>
  118. .module-extension {
  119. margin-top: 16px;
  120. border: 1px solid #1e293b;
  121. border-radius: 14px;
  122. padding: 16px;
  123. background: rgba(15, 23, 42, 0.72);
  124. }
  125. .module-extension__header {
  126. display: flex;
  127. justify-content: space-between;
  128. gap: 12px;
  129. align-items: center;
  130. margin-bottom: 14px;
  131. }
  132. .module-extension__header h2 {
  133. margin: 0;
  134. font-size: 16px;
  135. color: #f8fafc;
  136. }
  137. .module-extension__header p {
  138. margin: 4px 0 0;
  139. color: #94a3b8;
  140. font-size: 12px;
  141. }
  142. .module-extension__actions {
  143. display: flex;
  144. gap: 8px;
  145. align-items: center;
  146. }
  147. .module-extension__body {
  148. display: grid;
  149. grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  150. gap: 12px;
  151. }
  152. .extension-card {
  153. border: 1px solid #263244;
  154. border-radius: 10px;
  155. padding: 12px;
  156. background: rgba(2, 6, 23, 0.38);
  157. }
  158. .extension-card--wide {
  159. grid-column: 1 / -1;
  160. }
  161. .extension-card__title,
  162. .drill-block__title {
  163. font-size: 13px;
  164. font-weight: 700;
  165. color: #e2e8f0;
  166. margin-bottom: 10px;
  167. }
  168. .drill-grid,
  169. .branch-grid {
  170. display: grid;
  171. grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  172. gap: 10px;
  173. }
  174. .drill-items {
  175. display: flex;
  176. flex-wrap: wrap;
  177. gap: 8px;
  178. }
  179. .drill-item {
  180. flex: 1 1 160px;
  181. border: 1px solid #334155;
  182. border-radius: 8px;
  183. padding: 10px;
  184. color: #cbd5e1;
  185. }
  186. .drill-item strong {
  187. display: block;
  188. margin-top: 6px;
  189. font-size: 18px;
  190. color: #f8fafc;
  191. }
  192. .drill-item small {
  193. color: #64748b;
  194. }
  195. .tone-green { border-color: rgba(52, 211, 153, 0.45); }
  196. .tone-yellow { border-color: rgba(251, 191, 36, 0.45); }
  197. .tone-red { border-color: rgba(239, 68, 68, 0.45); }
  198. .branch-row,
  199. .alert-row {
  200. display: flex;
  201. align-items: center;
  202. gap: 10px;
  203. padding: 8px 0;
  204. border-bottom: 1px solid rgba(51, 65, 85, 0.55);
  205. color: #cbd5e1;
  206. }
  207. .branch-row span,
  208. .alert-row p {
  209. flex: 1;
  210. margin: 0;
  211. }
  212. .alert-row span {
  213. color: #64748b;
  214. font-family: Consolas, 'Courier New', monospace;
  215. }
  216. .alert-row strong {
  217. color: #fbbf24;
  218. }
  219. .alert-row.level-critical strong,
  220. .alert-row.level-high strong,
  221. .alert-row.level-danger strong {
  222. color: #f87171;
  223. }
  224. .empty-text {
  225. color: #94a3b8;
  226. font-size: 12px;
  227. }
  228. </style>