Explorar o código

fix(s8): improve department anomaly card readability

YY968XX hai 2 meses
pai
achega
f861c06f8e

+ 1 - 1
Web/package.json

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

+ 132 - 79
Web/src/views/aidop/s8/monitoring/components/S8DeptSeverityTabsCard.vue

@@ -1,5 +1,5 @@
 <template>
-	<div class="s8-dept-severity" :style="minHeightStyle">
+	<div class="s8-dept-severity" :class="`s8-dept-severity--${activeTab}`" :style="minHeightStyle">
 		<div class="s8-dept-severity__header">
 			<div class="s8-dept-severity__title-wrap">
 				<span class="s8-dept-severity__code">S8</span>
@@ -11,8 +11,8 @@
 				<DeptList
 					:loading="loading"
 					:rows="seriousRows"
-					count-label="严重异常"
 					empty-text="暂无严重异常"
+					tone="serious"
 					:count-key="'seriousCount'"
 				/>
 			</el-tab-pane>
@@ -20,8 +20,8 @@
 				<DeptList
 					:loading="loading"
 					:rows="followRows"
-					count-label="关注异常"
 					empty-text="暂无关注异常"
+					tone="follow"
 					:count-key="'followCount'"
 				/>
 			</el-tab-pane>
@@ -42,7 +42,6 @@ const props = defineProps<{
 type Tab = 'serious' | 'follow';
 const activeTab = shallowRef<Tab>('serious');
 
-// S8-DEPT-CARD-LIST-LAYOUT-FIX-1:严重 / 关注两个视角,分别按 seriousCount / followCount > 0 过滤并降序。
 const seriousRows = computed(() =>
 	[...(props.items ?? [])]
 		.filter((x) => x && x.seriousCount > 0)
@@ -59,8 +58,8 @@ const minHeightStyle = computed<CSSProperties>(() => {
 	return { minHeight: `${props.minHeight}px` };
 });
 
-// S8-DEPT-CARD-LIST-LAYOUT-FIX-1:avgProcessHours 无样本(null/undefined/<=0)→ '--';
-// closeRate 缺字段 → '--',0 显示 '0%'(数据真实闭环为 0,不当作空态)。
+// S8-DEPT-CARD-READABLE-LIST-REDESIGN-1:
+// avgProcessHours 无样本(null/undefined/<=0)→ '--';closeRate null → '--',0 显示 '0%'(真实闭环 0 不当空态)。
 function formatHoursOrDash(v: number | null | undefined): string {
 	if (v === null || v === undefined || !Number.isFinite(v) || v <= 0) return '--';
 	return v >= 24 ? `${Math.round(v)}h+` : `${v.toFixed(1)}h`;
@@ -71,54 +70,49 @@ function formatPercentOrDash(v: number | null | undefined): string {
 	return `${clamped.toFixed(clamped % 1 === 0 ? 0 : 1)}%`;
 }
 
-// S8-DEPT-CARD-LIST-LAYOUT-FIX-1:窄屏列表方案 —— 每部门一行 card,2 行结构:
-//   第一行:部门名 + 异常数 (右对齐徽标)
-//   第二行:均时 X · 关闭率 Y (副信息行)
-// 与之前 4 列 grid 相比,列宽不再受卡片总宽限制,文字不会挤压。
+// S8-DEPT-CARD-READABLE-LIST-REDESIGN-1:部门指标列表卡。
+// 每条 dept item 两层:
+//   主行:部门名(左、可省略)+ severity badge(右、真正的 pill 徽标)
+//   副行:均时 pill · 关闭率 pill
+// badge 严重红色系 / 关注黄色系;item 背景加深 + 颜色描边,避免与背板融为一体。
 const DeptList = (rawProps: {
 	loading?: boolean;
 	rows: S8DeptBacklogItem[];
-	countLabel: string;
 	emptyText: string;
+	tone: 'serious' | 'follow';
 	countKey: 'seriousCount' | 'followCount';
 }) => {
 	if (rawProps.loading) return h('div', { class: 's8-dept-severity__empty' }, '加载中…');
 	if (!rawProps.rows.length) return h('div', { class: 's8-dept-severity__empty' }, rawProps.emptyText);
+	const badgeText = rawProps.tone === 'serious' ? '严重' : '关注';
 	return h(
 		'div',
 		{ class: 's8-dept-severity__list' },
 		rawProps.rows.map((row) => {
 			const count = row[rawProps.countKey];
-			const hot = count > 0;
 			return h(
 				'div',
-				{ key: row.deptId, class: 's8-dept-severity__item' },
+				{ key: row.deptId, class: ['s8-dept-item', `s8-dept-item--${rawProps.tone}`] },
 				[
-					h('div', { class: 's8-dept-severity__row-primary' }, [
-						h('span', { class: 's8-dept-severity__dept-name', title: row.deptName }, row.deptName),
-						h('span', { class: 's8-dept-severity__count-wrap' }, [
-							h('span', { class: 's8-dept-severity__count-label' }, rawProps.countLabel),
-							h(
-								'span',
-								{
-									class: [
-										's8-dept-severity__count-value',
-										hot ? 's8-dept-severity__count-value--hot' : 's8-dept-severity__count-value--zero',
-									],
-								},
-								String(count),
-							),
-						]),
+					h('div', { class: 's8-dept-item__primary' }, [
+						h('span', { class: 's8-dept-item__name', title: row.deptName }, row.deptName),
+						h(
+							'span',
+							{ class: ['s8-dept-item__badge', `s8-dept-item__badge--${rawProps.tone}`] },
+							[
+								h('span', { class: 's8-dept-item__badge-label' }, badgeText),
+								h('span', { class: 's8-dept-item__badge-value' }, String(count)),
+							],
+						),
 					]),
-					h('div', { class: 's8-dept-severity__row-secondary' }, [
-						h('span', { class: 's8-dept-severity__metric' }, [
-							h('span', { class: 's8-dept-severity__metric-label' }, '均时 '),
-							h('span', { class: 's8-dept-severity__metric-value' }, formatHoursOrDash(row.avgProcessHours)),
+					h('div', { class: 's8-dept-item__secondary' }, [
+						h('span', { class: 's8-dept-item__pill' }, [
+							h('span', { class: 's8-dept-item__pill-label' }, '均时'),
+							h('span', { class: 's8-dept-item__pill-value' }, formatHoursOrDash(row.avgProcessHours)),
 						]),
-						h('span', { class: 's8-dept-severity__metric-sep' }, '·'),
-						h('span', { class: 's8-dept-severity__metric' }, [
-							h('span', { class: 's8-dept-severity__metric-label' }, '关闭率 '),
-							h('span', { class: 's8-dept-severity__metric-value' }, formatPercentOrDash(row.closeRate)),
+						h('span', { class: 's8-dept-item__pill' }, [
+							h('span', { class: 's8-dept-item__pill-label' }, '闭环率'),
+							h('span', { class: 's8-dept-item__pill-value' }, formatPercentOrDash(row.closeRate)),
 						]),
 					]),
 				],
@@ -129,13 +123,15 @@ const DeptList = (rawProps: {
 DeptList.props = {
 	loading: { type: Boolean as PropType<boolean>, default: false },
 	rows: { type: Array as PropType<S8DeptBacklogItem[]>, required: true },
-	countLabel: { type: String, required: true },
 	emptyText: { type: String, required: true },
+	tone: { type: String as PropType<'serious' | 'follow'>, required: true },
 	countKey: { type: String as PropType<'seriousCount' | 'followCount'>, required: true },
 };
 </script>
 
-<style scoped>
+<!-- S8-DEPT-CARD-READABLE-LIST-REDESIGN-1:去掉 scoped,因为 DeptList 用 h() 函数式渲染创建的元素不会获得 Vue 注入的 scoped data-attribute;
+     所有类名已使用 BEM 命名(s8-dept-severity__*, s8-dept-item__*),无冲突风险。 -->
+<style>
 .s8-dept-severity {
 	background: rgba(255, 255, 255, 0.03);
 	border: 1px solid rgba(255, 255, 255, 0.07);
@@ -149,6 +145,12 @@ DeptList.props = {
 	height: 100%;
 	box-sizing: border-box;
 	overflow: hidden;
+	transition: border-left-color 0.2s ease;
+}
+
+/* S8-DEPT-CARD-READABLE-LIST-REDESIGN-1:左侧 accent 颜色跟随当前 Tab,严重红/关注黄。 */
+.s8-dept-severity--follow {
+	border-left-color: #f59e0b;
 }
 
 .s8-dept-severity__header {
@@ -169,6 +171,11 @@ DeptList.props = {
 	font-weight: 700;
 	color: #ef4444;
 	letter-spacing: 0.6px;
+	transition: color 0.2s ease;
+}
+
+.s8-dept-severity--follow .s8-dept-severity__code {
+	color: #f59e0b;
 }
 
 .s8-dept-severity__title {
@@ -184,15 +191,15 @@ DeptList.props = {
 	flex-direction: column;
 }
 
-.s8-dept-severity__tabs :deep(.el-tabs__header) {
+.s8-dept-severity__tabs .el-tabs__header {
 	margin: 0 0 6px;
 }
 
-.s8-dept-severity__tabs :deep(.el-tabs__nav-wrap::after) {
+.s8-dept-severity__tabs .el-tabs__nav-wrap::after {
 	background-color: rgba(255, 255, 255, 0.08);
 }
 
-.s8-dept-severity__tabs :deep(.el-tabs__item) {
+.s8-dept-severity__tabs .el-tabs__item {
 	color: #94a3b8;
 	font-size: 12px;
 	height: 28px;
@@ -200,45 +207,71 @@ DeptList.props = {
 	padding: 0 10px;
 }
 
-.s8-dept-severity__tabs :deep(.el-tabs__item.is-active) {
+.s8-dept-severity__tabs .el-tabs__item.is-active {
 	color: #ef4444;
 }
 
-.s8-dept-severity__tabs :deep(.el-tabs__active-bar) {
+.s8-dept-severity--follow .s8-dept-severity__tabs .el-tabs__item.is-active {
+	color: #f59e0b;
+}
+
+.s8-dept-severity__tabs .el-tabs__active-bar {
 	background-color: #ef4444;
 }
 
-.s8-dept-severity__tabs :deep(.el-tabs__content) {
+.s8-dept-severity--follow .s8-dept-severity__tabs .el-tabs__active-bar {
+	background-color: #f59e0b;
+}
+
+.s8-dept-severity__tabs .el-tabs__content {
 	flex: 1;
 	min-height: 0;
 	overflow: auto;
 }
 
-/* S8-DEPT-CARD-LIST-LAYOUT-FIX-1:列表方案 */
+/* ── 列表 ── */
 .s8-dept-severity__list {
 	display: flex;
 	flex-direction: column;
-	gap: 4px;
+	gap: 6px;
+	padding-right: 2px;
 }
 
-.s8-dept-severity__item {
+/* ── 单条 item ── */
+.s8-dept-item {
 	display: flex;
 	flex-direction: column;
-	gap: 3px;
-	padding: 6px 8px;
-	border-radius: 6px;
-	background: rgba(255, 255, 255, 0.02);
+	gap: 6px;
+	padding: 8px 10px;
+	border-radius: 8px;
+	background: rgba(15, 23, 42, 0.5);
+	border: 1px solid rgba(148, 163, 184, 0.1);
+	border-left-width: 2px;
+	transition: background 0.15s ease, border-color 0.15s ease;
+}
+
+.s8-dept-item--serious {
+	border-left-color: rgba(239, 68, 68, 0.55);
 }
 
-.s8-dept-severity__row-primary {
+.s8-dept-item--follow {
+	border-left-color: rgba(245, 158, 11, 0.55);
+}
+
+.s8-dept-item:hover {
+	background: rgba(30, 41, 59, 0.6);
+}
+
+/* 主行:部门名 + badge */
+.s8-dept-item__primary {
 	display: flex;
-	align-items: baseline;
+	align-items: center;
 	justify-content: space-between;
-	gap: 8px;
+	gap: 10px;
 	min-width: 0;
 }
 
-.s8-dept-severity__dept-name {
+.s8-dept-item__name {
 	color: #e2e8f0;
 	font-size: 13px;
 	font-weight: 600;
@@ -249,66 +282,86 @@ DeptList.props = {
 	flex: 1;
 }
 
-.s8-dept-severity__count-wrap {
+/* badge:严重红 / 关注黄,真正的 pill 徽标,不与部门名连读 */
+.s8-dept-item__badge {
 	display: inline-flex;
 	align-items: baseline;
 	gap: 4px;
+	padding: 2px 8px;
+	border-radius: 999px;
+	font-size: 11px;
+	font-weight: 500;
+	white-space: nowrap;
 	flex-shrink: 0;
+	border: 1px solid transparent;
 }
 
-.s8-dept-severity__count-label {
-	font-size: 11px;
-	color: #94a3b8;
+.s8-dept-item__badge--serious {
+	background: rgba(239, 68, 68, 0.14);
+	border-color: rgba(239, 68, 68, 0.35);
+	color: #fca5a5;
 }
 
-.s8-dept-severity__count-value {
+.s8-dept-item__badge--follow {
+	background: rgba(245, 158, 11, 0.14);
+	border-color: rgba(245, 158, 11, 0.35);
+	color: #fcd34d;
+}
+
+.s8-dept-item__badge-label {
+	letter-spacing: 0.5px;
+}
+
+.s8-dept-item__badge-value {
 	font-family: 'Roboto Mono', monospace;
-	font-size: 15px;
+	font-size: 14px;
 	font-weight: 700;
 }
 
-.s8-dept-severity__count-value--hot {
+.s8-dept-item__badge--serious .s8-dept-item__badge-value {
 	color: #ef4444;
 }
 
-.s8-dept-severity__count-value--zero {
-	color: #94a3b8;
+.s8-dept-item__badge--follow .s8-dept-item__badge-value {
+	color: #f59e0b;
 }
 
-.s8-dept-severity__row-secondary {
+/* 副行:均时 / 闭环率 两个独立 pill */
+.s8-dept-item__secondary {
 	display: flex;
-	align-items: baseline;
+	align-items: center;
 	gap: 6px;
-	font-size: 11px;
-	color: #94a3b8;
-	min-width: 0;
+	flex-wrap: wrap;
 }
 
-.s8-dept-severity__metric {
+.s8-dept-item__pill {
 	display: inline-flex;
 	align-items: baseline;
-	gap: 2px;
+	gap: 4px;
+	padding: 2px 8px;
+	border-radius: 6px;
+	background: rgba(148, 163, 184, 0.08);
+	border: 1px solid rgba(148, 163, 184, 0.12);
 	white-space: nowrap;
 }
 
-.s8-dept-severity__metric-label {
+.s8-dept-item__pill-label {
+	font-size: 10.5px;
 	color: #94a3b8;
+	letter-spacing: 0.3px;
 }
 
-.s8-dept-severity__metric-value {
+.s8-dept-item__pill-value {
 	font-family: 'Roboto Mono', monospace;
-	color: #cbd5e1;
 	font-size: 12px;
-}
-
-.s8-dept-severity__metric-sep {
-	color: rgba(148, 163, 184, 0.5);
+	color: #cbd5e1;
+	font-weight: 500;
 }
 
 .s8-dept-severity__empty {
 	color: #64748b;
 	font-size: 12px;
-	padding: 12px 6px;
+	padding: 16px 6px;
 	text-align: center;
 }
 </style>