Просмотр исходного кода

feat(s8): add department serious exception card

YY968XX 1 неделя назад
Родитель
Сommit
aa5ed6a212

+ 1 - 1
Web/package.json

@@ -1,7 +1,7 @@
 {
 	"name": "admin.net",
 	"type": "module",
-	"version": "2.4.133",
+	"version": "2.4.134",
 	"packageManager": "pnpm@10.32.1",
 	"lastBuildTime": "2026.03.15",
 	"description": "Admin.NET 站在巨人肩膀上的 .NET 通用权限开发框架",

+ 3 - 0
Web/src/views/aidop/s8/api/s8DashboardApi.ts

@@ -50,6 +50,9 @@ export interface S8DeptBacklogItem {
 	inProgress: number;
 	timeout: number;
 	total: number;
+	// TASK-013-DEPT-SERIOUS-CARD-1:按 severity 归一化切两桶,与 status 维度正交;首期 UI 仅消费 seriousCount。
+	seriousCount: number;
+	followCount: number;
 }
 
 export interface S8QuickException {

+ 30 - 3
Web/src/views/aidop/s8/monitoring/S8MonitoringOverviewPage.vue

@@ -28,6 +28,8 @@
 						:result-kpi-source="resultKpiSource"
 						:result-kpi-loading="resultKpiState === 'loading'"
 						:result-kpi-error="resultKpiState === 'error' ? resultKpiError : null"
+						:dept-backlog-items="deptBacklogItems"
+						:dept-backlog-loading="deptBacklogLoading"
 						:layout="draftLayout.modules"
 						:editable="editMode"
 						:row-height="60"
@@ -80,6 +82,7 @@ import {
 	type S8QdcSummaryItem,
 	type S8ResultKpiItem,
 } from '../api/s8MonitoringApi';
+import { s8DashboardApi, type S8DeptBacklogItem } from '../api/s8DashboardApi';
 import { deepClone, useS8Layout } from './useS8Layout';
 import type { S8LayoutSchema } from './useS8Layout';
 import { DEMO_LAYOUT } from './useS8Layout';
@@ -373,7 +376,10 @@ const s9SummaryCard = computed<SummaryCardData>(() => {
 });
 
 // S9 由 S9QdcSummaryCard 特判渲染,不再注入 summaryCards
-const allSummaryCards = computed<SummaryCardData[]>(() => [s8SummaryCard.value]);
+// TASK-013-DEPT-SERIOUS-CARD-1:S8 cell 由 S8DeptSeverityTabsCard 特判渲染(在 ModulesGrid 内),
+// 不再把 s8SummaryCard(功能异常多维分析)注入 summaryCards。s8SummaryCard / s9SummaryCard
+// computed 暂保留以减少范围蔓延,作为 P3 死代码清理(TASK-013-CLEANUP-S9-DEPT-DEAD-CODE-1)。
+const allSummaryCards = computed<SummaryCardData[]>(() => []);
 
 const deptCards = computed<DeptDisplay[]>(() => {
 	const source = gridData.byDept.length
@@ -600,6 +606,23 @@ const resultKpiSource = shallowRef<string>('DICTIONARY_MOCK');
 const resultKpiState = shallowRef<'idle' | 'loading' | 'ok' | 'error'>('idle');
 const resultKpiError = shallowRef<string>('');
 
+// TASK-013-DEPT-SERIOUS-CARD-1:S8 cell 部门严重异常 Tab 数据;按 period 联动刷新。
+const deptBacklogItems = shallowRef<S8DeptBacklogItem[]>([]);
+const deptBacklogLoading = shallowRef(false);
+
+async function loadDeptBacklog() {
+	deptBacklogLoading.value = true;
+	try {
+		const data = await s8DashboardApi.deptBacklog({ tenantId: 1, factoryId: 1, period: period.value });
+		deptBacklogItems.value = data ?? [];
+	} catch (err) {
+		console.error('[S8MonitoringOverviewPage] loadDeptBacklog failed:', err);
+		deptBacklogItems.value = [];
+	} finally {
+		deptBacklogLoading.value = false;
+	}
+}
+
 async function loadResultKpi() {
 	resultKpiState.value = 'loading';
 	resultKpiError.value = '';
@@ -675,7 +698,7 @@ onMounted(async () => {
 	// initializeFromCards 因 initialized=true 跳过,导致真实 API 数据无法覆盖。
 	// 改用 reset(无条件用最新 cards 重建 state.items),每次进入本页都对齐 API。
 	await loadPageConfig();
-	await Promise.all([loadData(), loadResultKpi()]);
+	await Promise.all([loadData(), loadResultKpi(), loadDeptBacklog()]);
 	resetStageConfigState(stageCards.value);
 	resetCategoryConfigState(categoryCards.value);
 	resetDeptConfigState(deptCards.value);
@@ -689,7 +712,11 @@ watch(
 	},
 );
 
-watch(period, () => void loadData());
+watch(period, () => {
+	void loadData();
+	// TASK-013-DEPT-SERIOUS-CARD-1:period 切换时联动刷新部门严重异常。
+	void loadDeptBacklog();
+});
 watch(categoryCards, (cats) => { initializeFromCategories(cats); });
 watch(deptCards, (depts) => {
 	initializeFromDepts(depts);

+ 191 - 0
Web/src/views/aidop/s8/monitoring/components/S8DeptSeverityTabsCard.vue

@@ -0,0 +1,191 @@
+<template>
+	<div class="s8-dept-severity" :style="minHeightStyle">
+		<div class="s8-dept-severity__header">
+			<div class="s8-dept-severity__title-wrap">
+				<span class="s8-dept-severity__code">S8</span>
+				<span class="s8-dept-severity__title">部门严重异常</span>
+			</div>
+			<span class="s8-dept-severity__subtitle">按责任部门聚合</span>
+		</div>
+		<el-tabs v-model="activeTab" class="s8-dept-severity__tabs">
+			<el-tab-pane label="严重" name="serious">
+				<div v-if="loading" class="s8-dept-severity__empty">加载中…</div>
+				<div v-else-if="!sortedItems.length" class="s8-dept-severity__empty">暂无严重异常</div>
+				<ul v-else class="s8-dept-severity__list">
+					<li v-for="row in sortedItems" :key="row.deptId" class="s8-dept-severity__row">
+						<span class="s8-dept-severity__name" :title="row.deptName">{{ row.deptName }}</span>
+						<span
+							class="s8-dept-severity__count"
+							:class="row.seriousCount > 0 ? 's8-dept-severity__count--hot' : 's8-dept-severity__count--zero'"
+						>
+							{{ row.seriousCount }}
+						</span>
+					</li>
+				</ul>
+			</el-tab-pane>
+		</el-tabs>
+	</div>
+</template>
+
+<script setup lang="ts">
+import { computed, shallowRef, type CSSProperties } from 'vue';
+import type { S8DeptBacklogItem } from '../../api/s8DashboardApi';
+
+const props = defineProps<{
+	items: S8DeptBacklogItem[];
+	loading?: boolean;
+	minHeight?: number;
+}>();
+
+const activeTab = shallowRef<'serious'>('serious');
+
+// TASK-013-DEPT-SERIOUS-CARD-1:seriousCount desc, total desc 兜底;
+// 全员 seriousCount=0 视为空态(模板里 empty 判定)。
+const sortedItems = computed(() => {
+	const filtered = (props.items ?? []).filter((item) => item.seriousCount > 0);
+	return [...filtered].sort((a, b) => {
+		if (b.seriousCount !== a.seriousCount) return b.seriousCount - a.seriousCount;
+		return b.total - a.total;
+	});
+});
+
+const minHeightStyle = computed<CSSProperties>(() => {
+	if (!props.minHeight || props.minHeight <= 0) return {};
+	return { minHeight: `${props.minHeight}px` };
+});
+</script>
+
+<style scoped>
+.s8-dept-severity {
+	background: rgba(255, 255, 255, 0.03);
+	border: 1px solid rgba(255, 255, 255, 0.07);
+	border-left: 3px solid #ef4444;
+	border-radius: 10px;
+	padding: 12px 14px 10px;
+	display: flex;
+	flex-direction: column;
+	gap: 8px;
+	width: 100%;
+	height: 100%;
+	box-sizing: border-box;
+	overflow: hidden;
+}
+
+.s8-dept-severity__header {
+	display: flex;
+	align-items: baseline;
+	justify-content: space-between;
+	gap: 8px;
+}
+
+.s8-dept-severity__title-wrap {
+	display: flex;
+	align-items: baseline;
+	gap: 6px;
+}
+
+.s8-dept-severity__code {
+	font-size: 11px;
+	font-weight: 700;
+	color: #ef4444;
+	letter-spacing: 0.6px;
+}
+
+.s8-dept-severity__title {
+	font-size: 13px;
+	color: #e2e8f0;
+	font-weight: 600;
+}
+
+.s8-dept-severity__subtitle {
+	font-size: 11px;
+	color: #64748b;
+}
+
+.s8-dept-severity__tabs {
+	flex: 1;
+	min-height: 0;
+	display: flex;
+	flex-direction: column;
+}
+
+.s8-dept-severity__tabs :deep(.el-tabs__header) {
+	margin: 0 0 6px;
+}
+
+.s8-dept-severity__tabs :deep(.el-tabs__nav-wrap::after) {
+	background-color: rgba(255, 255, 255, 0.08);
+}
+
+.s8-dept-severity__tabs :deep(.el-tabs__item) {
+	color: #94a3b8;
+	font-size: 12px;
+	height: 28px;
+	line-height: 28px;
+	padding: 0 10px;
+}
+
+.s8-dept-severity__tabs :deep(.el-tabs__item.is-active) {
+	color: #ef4444;
+}
+
+.s8-dept-severity__tabs :deep(.el-tabs__active-bar) {
+	background-color: #ef4444;
+}
+
+.s8-dept-severity__tabs :deep(.el-tabs__content) {
+	flex: 1;
+	min-height: 0;
+	overflow: auto;
+}
+
+.s8-dept-severity__list {
+	margin: 0;
+	padding: 0;
+	list-style: none;
+	display: flex;
+	flex-direction: column;
+	gap: 4px;
+}
+
+.s8-dept-severity__row {
+	display: flex;
+	align-items: center;
+	gap: 8px;
+	font-size: 12px;
+	padding: 4px 6px;
+	border-radius: 6px;
+	background: rgba(255, 255, 255, 0.02);
+}
+
+.s8-dept-severity__name {
+	flex: 1;
+	color: #cbd5e1;
+	white-space: nowrap;
+	overflow: hidden;
+	text-overflow: ellipsis;
+}
+
+.s8-dept-severity__count {
+	min-width: 32px;
+	text-align: right;
+	font-family: 'Roboto Mono', monospace;
+	font-size: 14px;
+	font-weight: 700;
+}
+
+.s8-dept-severity__count--hot {
+	color: #ef4444;
+}
+
+.s8-dept-severity__count--zero {
+	color: #94a3b8;
+}
+
+.s8-dept-severity__empty {
+	color: #64748b;
+	font-size: 12px;
+	padding: 12px 6px;
+	text-align: center;
+}
+</style>

+ 12 - 0
Web/src/views/aidop/s8/monitoring/components/S8MonitoringModulesGrid.vue

@@ -40,6 +40,13 @@
           :min-height="item.h * rowHeight - gap * 2"
           style="width:100%;height:100%"
         />
+        <!-- TASK-013-DEPT-SERIOUS-CARD-1:S8 cell 改为部门严重异常卡,替换原「功能异常多维分析」SummaryCard。 -->
+        <S8DeptSeverityTabsCard
+          v-else-if="item.i === 'S8'"
+          :items="deptBacklogItems ?? []"
+          :loading="deptBacklogLoading"
+          :min-height="item.h * rowHeight - gap * 2"
+        />
         <S8MonitoringStageCard
           v-else-if="stageCardMap.get(item.i)"
           v-bind="stageCardMap.get(item.i)!"
@@ -66,9 +73,11 @@ import { useResizeObserver } from '@vueuse/core'
 import S8MonitoringStageCard from './S8MonitoringStageCard.vue'
 import S8MonitoringSummaryCard, { type SummaryMetric } from './S8MonitoringSummaryCard.vue'
 import S9QdcSummaryCard from './S9QdcSummaryCard.vue'
+import S8DeptSeverityTabsCard from './S8DeptSeverityTabsCard.vue'
 import type { ModuleGridItem } from '../useS8Layout'
 import type { Component } from 'vue'
 import type { S8QdcSummaryItem, S8ResultKpiItem } from '../../api/s8MonitoringApi'
+import type { S8DeptBacklogItem } from '../../api/s8DashboardApi'
 
 interface StageCardData {
   code: string
@@ -119,6 +128,9 @@ const props = defineProps<{
   resultKpiSource?: string
   resultKpiLoading?: boolean
   resultKpiError?: string | null
+  // TASK-013-DEPT-SERIOUS-CARD-1:S8 cell 部门严重异常数据。
+  deptBacklogItems?: S8DeptBacklogItem[]
+  deptBacklogLoading?: boolean
   layout: ModuleGridItem[]
   editable: boolean
   rowHeight?: number

+ 3 - 3
server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj

@@ -11,9 +11,9 @@
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <Copyright>Admin.NET</Copyright>
     <Description>Admin.NET 通用权限开发平台</Description>
-    <AssemblyVersion>1.0.103</AssemblyVersion>
-    <FileVersion>1.0.103</FileVersion>
-    <Version>1.0.103</Version>
+    <AssemblyVersion>1.0.104</AssemblyVersion>
+    <FileVersion>1.0.104</FileVersion>
+    <Version>1.0.104</Version>
   </PropertyGroup>
 
   <ItemGroup>

+ 8 - 2
server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/S8DashboardService.cs

@@ -204,12 +204,14 @@ public class S8DashboardService : ITransient
         var (periodFrom, periodTo) = S8PeriodHelper.Resolve(period);
 
         // S8-SLA-TIMEOUT-RUNTIME-1(P3):投影 SlaDeadline 替代 TimeoutFlag;timeout 由内存计算(与 GetSummaryAsync 一致 now)。
+        // TASK-013-DEPT-SERIOUS-CARD-1:补投 Severity 用于 seriousCount/followCount 内存聚合(归一化走 S8SeverityCode.IsSerious/IsFollow,
+        // 兼容 legacy HIGH/CRITICAL/MEDIUM/LOW 值)。
         var list = await _rep.AsQueryable()
             .Where(x => x.TenantId == tenantId && x.FactoryId == factoryId && !x.IsDeleted)
             .Where(x => S8ModuleCode.All.Contains(x.StageCode))
             .WhereIF(periodFrom.HasValue, x => x.CreatedAt >= periodFrom!.Value)
             .WhereIF(periodTo.HasValue,   x => x.CreatedAt < periodTo!.Value)
-            .Select(x => new { x.ResponsibleDeptId, x.Status, x.SlaDeadline })
+            .Select(x => new { x.ResponsibleDeptId, x.Status, x.SlaDeadline, x.Severity })
             .ToListAsync();
         var nowForTimeout = DateTime.Now;
 
@@ -232,8 +234,12 @@ public class S8DashboardService : ITransient
                 // S8-SLA-TIMEOUT-RUNTIME-1:当前超时 = sla_deadline 在 now 之前 AND 未 CLOSED/RECOVERED。
                 timeout    = g.Count(x => x.SlaDeadline != null && x.SlaDeadline < nowForTimeout && x.Status != "CLOSED" && x.Status != "RECOVERED"),
                 total      = g.Count(),
+                // TASK-013-DEPT-SERIOUS-CARD-1:按 severity 归一化切两桶,与 ado_s8_exception 列 status 维度正交。
+                seriousCount = g.Count(x => S8SeverityCode.IsSerious(x.Severity)),
+                followCount  = g.Count(x => S8SeverityCode.IsFollow(x.Severity)),
             })
-            .OrderByDescending(x => x.pending)
+            .OrderByDescending(x => x.seriousCount)
+            .ThenByDescending(x => x.total)
             .ToList();
     }