|
|
@@ -33,8 +33,21 @@
|
|
|
<div class="section-title">
|
|
|
<div class="section-title__bar" />
|
|
|
<h2 class="section-title__text">交付异常类型</h2>
|
|
|
- <!-- S8-DELIVERY-SIDEBAR-TIME-WINDOW-LABEL-1:与下方「近7日交付异常趋势」做时间窗口区分。 -->
|
|
|
- <span class="anomaly-monitor__time-badge">近24h</span>
|
|
|
+ <!-- S8-SIDEBAR-TYPE-CARD-WINDOW-TOGGLE-1:右侧类型卡支持 近24h / 近7日 切换;同窗口同分母。 -->
|
|
|
+ <div class="anomaly-monitor__window-toggle" role="group" aria-label="时间窗口">
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ class="anomaly-monitor__window-btn"
|
|
|
+ :class="{ 'anomaly-monitor__window-btn--active': sidebarWindow === 'LAST_24H' }"
|
|
|
+ @click="setSidebarWindow('LAST_24H')"
|
|
|
+ >近24h</button>
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ class="anomaly-monitor__window-btn"
|
|
|
+ :class="{ 'anomaly-monitor__window-btn--active': sidebarWindow === 'LAST_7D' }"
|
|
|
+ @click="setSidebarWindow('LAST_7D')"
|
|
|
+ >近7日</button>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
|
|
|
<div class="anomaly-monitor__type-list">
|
|
|
@@ -49,15 +62,14 @@
|
|
|
<span class="anomaly-type-card__count">{{ formatInteger(item.total) }}</span>
|
|
|
</div>
|
|
|
<div class="anomaly-type-card__metrics">
|
|
|
- <!-- S8-MONITORING-DEMO-SEMANTICS-CLOSURE-1:cell-data 近24h 窗口下,均时/关闭率分母与待处理数量口径不一致,
|
|
|
- 演示前先统一显示 `--` 避免误读,待 cell-data 接通真实近24h 指标后恢复。 -->
|
|
|
+ <!-- S8-SIDEBAR-TYPE-CARD-WINDOW-TOGGLE-1:均时 / 关闭率 与数量同窗口同分母。 -->
|
|
|
<div class="anomaly-type-card__metric">
|
|
|
<div class="anomaly-type-card__metric-label">均时</div>
|
|
|
- <div class="anomaly-type-card__metric-value">--</div>
|
|
|
+ <div class="anomaly-type-card__metric-value">{{ formatHoursOrDash(item.avgProcessHours) }}</div>
|
|
|
</div>
|
|
|
<div class="anomaly-type-card__metric">
|
|
|
<div class="anomaly-type-card__metric-label">关闭率</div>
|
|
|
- <div class="anomaly-type-card__metric-value anomaly-type-card__metric-value--accent">--</div>
|
|
|
+ <div class="anomaly-type-card__metric-value anomaly-type-card__metric-value--accent">{{ formatPercentOrDash(item.closeRate) }}</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</article>
|
|
|
@@ -98,8 +110,8 @@ import { computed, onMounted, reactive, shallowRef, type CSSProperties, type Com
|
|
|
import { Checked, Van } from '@element-plus/icons-vue';
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
|
|
import { deepClone, createPageLayout, LAYOUT_VERSION, type S8LayoutSchema, type GridItem } from './useS8Layout';
|
|
|
-import { s8DeliveryMonitoringApi, type DeliveryAnomalyType } from '../api/s8DeliveryMonitoringApi';
|
|
|
-import { s8MonitoringApi, type S8MonitoringSummary, type S8ModuleOrderSummary, type S8DeliveryTrendData } from '../api/s8MonitoringApi';
|
|
|
+import { s8DeliveryMonitoringApi } from '../api/s8DeliveryMonitoringApi';
|
|
|
+import { s8MonitoringApi, type S8MonitoringSummary, type S8ModuleOrderSummary, type S8DeliveryTrendData, type S8DomainTypeMetrics, type S8DomainTypeWindow } from '../api/s8MonitoringApi';
|
|
|
import S8MonitoringResizableShell from './components/S8MonitoringResizableShell.vue';
|
|
|
import S8MonitoringModulesGrid from './components/S8MonitoringModulesGrid.vue';
|
|
|
import S8MonitoringEditToolbar from './components/S8MonitoringEditToolbar.vue';
|
|
|
@@ -221,25 +233,43 @@ const ANOMALY_TYPES_FALLBACK: ReadonlyArray<{ key: string; label: string }> = [
|
|
|
|
|
|
const effectiveStageMeta = computed(() => pageConfig.effectiveStageMeta(STAGE_META_FALLBACK));
|
|
|
|
|
|
-const anomalyTypes = reactive<DeliveryAnomalyType[]>(
|
|
|
- ANOMALY_TYPES_FALLBACK.map((d) => ({ ...d, total: 0, avgProcessHours: 0, closeRate: 0 })),
|
|
|
-);
|
|
|
+// S8-SIDEBAR-TYPE-CARD-WINDOW-TOGGLE-1:右侧类型卡走新统一接口 /monitoring/domain-type-metrics,
|
|
|
+// 同窗口同分母。废弃旧 loadConfiguredAnomalyTypes(混用 OPEN_COUNT/24H + AVG_DURATION/CLOSE_RATE/7D)。
|
|
|
+const sidebarWindow = shallowRef<S8DomainTypeWindow>('LAST_24H');
|
|
|
+const sidebarMetrics = shallowRef<S8DomainTypeMetrics | null>(null);
|
|
|
+const anomalyTypes = computed(() => sidebarMetrics.value?.items ?? ANOMALY_TYPES_FALLBACK.map((d) => ({
|
|
|
+ key: d.key,
|
|
|
+ label: d.label,
|
|
|
+ typeCode: '',
|
|
|
+ total: 0,
|
|
|
+ openCount: 0,
|
|
|
+ closedCount: 0,
|
|
|
+ avgProcessHours: null as number | null,
|
|
|
+ closeRate: null as number | null,
|
|
|
+})));
|
|
|
+
|
|
|
+async function loadSidebarMetrics() {
|
|
|
+ try {
|
|
|
+ sidebarMetrics.value = await s8MonitoringApi.domainTypeMetrics({
|
|
|
+ domain: 'DELIVERY',
|
|
|
+ window: sidebarWindow.value,
|
|
|
+ tenantId: 1,
|
|
|
+ factoryId: 1,
|
|
|
+ });
|
|
|
+ } catch (err) {
|
|
|
+ console.error('[S8MonitoringDeliveryPage] loadSidebarMetrics failed:', err);
|
|
|
+ sidebarMetrics.value = null;
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
-function rebuildAnomalyTypesFromConfig() {
|
|
|
- const sidebarTypes = pageConfig.effectiveSidebarTypes(ANOMALY_TYPES_FALLBACK);
|
|
|
- const next = sidebarTypes.map((t) => ({
|
|
|
- key: t.key,
|
|
|
- label: t.label,
|
|
|
- total: 0,
|
|
|
- avgProcessHours: 0,
|
|
|
- closeRate: 0,
|
|
|
- }));
|
|
|
- anomalyTypes.splice(0, anomalyTypes.length, ...next);
|
|
|
+function setSidebarWindow(w: S8DomainTypeWindow) {
|
|
|
+ if (sidebarWindow.value === w) return;
|
|
|
+ sidebarWindow.value = w;
|
|
|
+ void loadSidebarMetrics();
|
|
|
}
|
|
|
|
|
|
async function loadPageConfig() {
|
|
|
await pageConfig.load();
|
|
|
- rebuildAnomalyTypesFromConfig();
|
|
|
}
|
|
|
|
|
|
const moduleMap = computed(() => new Map(moduleData.list.map((m) => [m.moduleCode, m])));
|
|
|
@@ -294,7 +324,7 @@ const stageCards = computed(() =>
|
|
|
const stageKeys = computed(() => stageCards.value.map((c) => c.code));
|
|
|
|
|
|
// S8-DELIVERY-TREND-CHART-REPLACE-DUPLICATE-SECTION-1:原 categoryCards / categoryKeys 仅服务被替换的多维分析区,已移除。
|
|
|
-const totalAnomalies = computed(() => anomalyTypes.reduce((s, t) => s + t.total, 0));
|
|
|
+const totalAnomalies = computed(() => sidebarMetrics.value?.total ?? 0);
|
|
|
|
|
|
// S8-DELIVERY-TREND-CHART-REPLACE-DUPLICATE-SECTION-1:近 7 日交付异常趋势数据。
|
|
|
const deliveryTrendData = shallowRef<S8DeliveryTrendData | null>(null);
|
|
|
@@ -316,12 +346,22 @@ function formatHours(v: number) {
|
|
|
return v >= 24 ? `${Math.round(v)}h+` : `${v.toFixed(1)}h`;
|
|
|
}
|
|
|
function formatPercent(v: number) { return `${Math.max(0, Math.min(100, v)).toFixed(0)}%`; }
|
|
|
-function resolveTone(closeRate: number): 'good' | 'warn' | 'danger' {
|
|
|
- if (closeRate >= 95) return 'good';
|
|
|
- if (closeRate >= 80) return 'warn';
|
|
|
+// S8-SIDEBAR-TYPE-CARD-WINDOW-TOGGLE-1:null/未定义 → '--',避免 0h / 0% 误读为"刚好为零"。
|
|
|
+function formatHoursOrDash(v: number | null | undefined) {
|
|
|
+ if (v === null || v === undefined || !Number.isFinite(v) || v <= 0) return '--';
|
|
|
+ return formatHours(v);
|
|
|
+}
|
|
|
+function formatPercentOrDash(v: number | null | undefined) {
|
|
|
+ if (v === null || v === undefined || !Number.isFinite(v)) return '--';
|
|
|
+ return formatPercent(v);
|
|
|
+}
|
|
|
+function resolveTone(closeRate: number | null | undefined): 'good' | 'warn' | 'danger' {
|
|
|
+ const v = closeRate ?? 0;
|
|
|
+ if (v >= 95) return 'good';
|
|
|
+ if (v >= 80) return 'warn';
|
|
|
return 'danger';
|
|
|
}
|
|
|
-function anomalyToneClass(closeRate: number) {
|
|
|
+function anomalyToneClass(closeRate: number | null | undefined) {
|
|
|
return `anomaly-type-card--${resolveTone(closeRate)}`;
|
|
|
}
|
|
|
|
|
|
@@ -335,18 +375,12 @@ async function loadData() {
|
|
|
try {
|
|
|
// moduleCodes 由 page-config 派生;fallback 时 effectiveStageMeta 退到 STAGE_META_FALLBACK,行为与历史硬编码一致。
|
|
|
const moduleCodes = effectiveStageMeta.value.map((m) => m.code);
|
|
|
- const [summaryData, modulesData, typesData] = await Promise.all([
|
|
|
+ const [summaryData, modulesData] = await Promise.all([
|
|
|
s8DeliveryMonitoringApi.summary(moduleCodes),
|
|
|
s8DeliveryMonitoringApi.modules(moduleCodes),
|
|
|
- s8DeliveryMonitoringApi.anomalyTypes(),
|
|
|
]);
|
|
|
Object.assign(summary, summaryData);
|
|
|
moduleData.list = modulesData;
|
|
|
- // 按 key 合并,不依赖位置(兼容 page-config 重建后的 anomalyTypes 顺序)
|
|
|
- typesData.forEach((t) => {
|
|
|
- const target = anomalyTypes.find((a) => a.key === t.key);
|
|
|
- if (target) Object.assign(target, t);
|
|
|
- });
|
|
|
loadState.value = 'ok';
|
|
|
} catch (err) {
|
|
|
console.error('[S8MonitoringDeliveryPage] loadData failed:', err);
|
|
|
@@ -364,9 +398,10 @@ onMounted(async () => {
|
|
|
await loadPageConfig();
|
|
|
// 2. 用最终的 stageCards 初始化 stage 配置覆盖层(applyConfig 已 passthrough,仅维护抽屉状态)
|
|
|
initializeFromCards(stageCards.value);
|
|
|
- // 3. 拉业务数据 + 趋势
|
|
|
+ // 3. 拉业务数据 + 趋势 + 右侧类型卡(新统一接口)
|
|
|
void loadData();
|
|
|
void loadDeliveryTrend();
|
|
|
+ void loadSidebarMetrics();
|
|
|
});
|
|
|
</script>
|
|
|
|
|
|
@@ -567,19 +602,40 @@ onMounted(async () => {
|
|
|
|
|
|
.section-title { display: flex; align-items: center; gap: 12px; }
|
|
|
|
|
|
-/* S8-DELIVERY-SIDEBAR-TIME-WINDOW-LABEL-1:右侧「交付异常类型」时间窗口徽标。 */
|
|
|
-.anomaly-monitor__time-badge {
|
|
|
+/* S8-SIDEBAR-TYPE-CARD-WINDOW-TOGGLE-1:右侧标题时间窗口切换按钮。 */
|
|
|
+.anomaly-monitor__window-toggle {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
- height: 20px;
|
|
|
- padding: 0 8px;
|
|
|
+ gap: 0;
|
|
|
+ margin-left: auto;
|
|
|
+ padding: 2px;
|
|
|
+ background: rgba(11, 14, 20, 0.55);
|
|
|
+ border: 1px solid rgba(123, 208, 255, 0.18);
|
|
|
+ border-radius: 999px;
|
|
|
+}
|
|
|
+
|
|
|
+.anomaly-monitor__window-btn {
|
|
|
+ height: 22px;
|
|
|
+ padding: 0 10px;
|
|
|
font-size: 11px;
|
|
|
font-weight: 500;
|
|
|
- color: #7bd0ff;
|
|
|
- background: rgba(123, 208, 255, 0.12);
|
|
|
- border: 1px solid rgba(123, 208, 255, 0.28);
|
|
|
+ color: #c6c6cd;
|
|
|
+ background: transparent;
|
|
|
+ border: none;
|
|
|
border-radius: 999px;
|
|
|
+ cursor: pointer;
|
|
|
letter-spacing: 0.04em;
|
|
|
+ transition: background 0.18s ease, color 0.18s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.anomaly-monitor__window-btn:hover {
|
|
|
+ color: #e1e6ee;
|
|
|
+}
|
|
|
+
|
|
|
+.anomaly-monitor__window-btn--active {
|
|
|
+ color: #0b0e14;
|
|
|
+ background: linear-gradient(90deg, #7bd0ff 0%, #6de039 100%);
|
|
|
+ font-weight: 600;
|
|
|
}
|
|
|
|
|
|
.section-title__bar {
|