Kaynağa Gözat

feat(home-card): 九宫格新增「状态卡片」样式 + 修复 S1/S9 重叠与建模页遗留 + bump 2.4.93/1.0.60

家模块卡呈现样式扩展(沿用 C2 注册表):
- 新增 tile_cards 样式组件 HomeCardTilesView.vue:2×2(>4 自动 2×4 dense)独立圆角卡
  + 左侧色条,颜色直接由已有 cell.trendTone(后端 achievementLevel 经 KpiMaster 的
  YellowThreshold/RedThreshold + Direction 计算)驱动,0 后端公式变更。
- homeCardStyles.ts 注册新样式 + 内嵌 SVG 缩略图(viewBox 0 0 80 48 保持规格一致)。
- 后端 AidopKanbanController.Generic.cs AllowedLayoutPatterns 白名单放行 tile_cards。
- 溢出兜底(>8 显示 +N 徽标)与 card_grid/table_list 一致,单行 ellipsis 防撑破栅格。

顺带修复(和 C2 建模联动链条相关的历史遗留,一并带上避免下次还得想):
- s0.vue:drawerTitle 不再硬编码 "S4",按 currentModuleCode 动态展示;
- operationModelSchema.ts:补齐缺失的 S9 模块块(5 项 L1,无 L2),建模图不再漏 S9;
- dashboard/home.vue:修正 .area-s1 的 grid-row(1/3 → 1/2),解决 S1 与 S9 在第 3 列
  的物理行跨越冲突,同时保留 S9 与 S8 等高。

版本:Web 2.4.92 → 2.4.93;后端 1.0.59 → 1.0.60。

Made-with: Cursor
skygu 2 ay önce
ebeveyn
işleme
c4cba4114b

+ 12 - 0
Web/src/views/aidop/kanban/data/operationModelSchema.ts

@@ -287,6 +287,18 @@ export const defaultOperationModel = [
         aggregation: '与首页人效指数对齐'
       }
     ]
+  },
+  {
+    moduleId: 'S9',
+    moduleName: '运营指标',
+    l1: [
+      { id: 'S9-L1-A', homeLabel: '质量退货率(PPM)', dataRef: 'homeS9.qualityReturnPpm' },
+      { id: 'S9-L1-B', homeLabel: '订单交付周期(天)', dataRef: 'homeS9.orderDeliveryCycle' },
+      { id: 'S9-L1-C', homeLabel: '订单交付满足率(%)', dataRef: 'homeS9.orderDeliveryRatePct' },
+      { id: 'S9-L1-D', homeLabel: '生产效率(%)', dataRef: 'homeS9.productionEfficiencyPct' },
+      { id: 'S9-L1-E', homeLabel: '全库存周转(天)', dataRef: 'homeS9.totalInventoryTurnover' }
+    ],
+    l2: []
   }
 ]
 

+ 4 - 1
Web/src/views/aidop/kanban/s0.vue

@@ -649,7 +649,10 @@ const drawerWidth = computed(() =>
 )
 
 const drawerTitle = computed(() => {
-  if (drawerS4.value) return 'S4 · 服务端 L1 布局(九宫格)'
+  if (drawerS4.value) {
+    const mc = currentModuleCode.value || selectedNode.value?.rawId || 'S?'
+    return `${mc} · 服务端 L1 布局(九宫格)`
+  }
   if (drawerS4L1.value) {
     const nm = selectedNode.value?.name?.replace(/\n/g, ' ') ?? 'L1'
     return `S4 · 服务端 L2(${nm})`

+ 241 - 0
Web/src/views/dashboard/components/HomeCardTilesView.vue

@@ -0,0 +1,241 @@
+<template>
+  <div
+    v-if="visibleCells.length > 0"
+    class="tiles-view"
+    :class="{ 'tiles-view--dense': props.cells.length > 4 }"
+  >
+    <div
+      v-for="cell in visibleCells"
+      :key="cell.key"
+      class="tile"
+      :class="{
+        'tile--good': cell.trendTone === 'good',
+        'tile--warn': cell.trendTone === 'warn',
+        'tile--bad': cell.trendTone === 'bad',
+      }"
+    >
+      <span class="tile__accent" aria-hidden="true"></span>
+      <div class="tile__body">
+        <div class="tile__info">
+          <el-tooltip
+            v-if="cell.formulaText"
+            placement="top"
+            :show-after="200"
+            :content="cell.formulaText"
+          >
+            <div class="tile__label tile__label--formula">{{ cell.label }}</div>
+          </el-tooltip>
+          <div v-else class="tile__label">{{ cell.label }}</div>
+          <div class="tile__target">
+            目标值: <span class="tile__target-val">{{ cell.targetDisplay || '—' }}</span>
+          </div>
+        </div>
+        <div class="tile__main">
+          <span class="tile__value">{{ cell.value }}</span>
+          <span v-if="cell.unit" class="tile__unit">{{ cell.unit }}</span>
+        </div>
+      </div>
+    </div>
+    <div v-if="overflowCount > 0" class="tiles-view__overflow" :title="`还有 ${overflowCount} 个指标未显示`">
+      +{{ overflowCount }}
+    </div>
+  </div>
+  <div v-else class="home-kpi-card__empty">
+    <span v-if="loading">加载中…</span>
+    <span v-else>暂无指标配置,点击卡片进入模块后到"运营指标建模"配置 L1 指标</span>
+  </div>
+</template>
+
+<script setup>
+import { computed } from 'vue'
+
+const props = defineProps({
+  moduleCode: { type: String, required: true },
+  cells: { type: Array, required: true },
+  loading: { type: Boolean, default: false },
+})
+
+/**
+ * 上限与 card_grid 对齐:<=4 走 2x2 大卡;>4 自动进 dense(2x4 = 8 格),
+ * 超出部分显示 +N 徽标,避免挤压与溢出。
+ */
+const MAX_DENSE = 8
+const MAX_NORMAL = 4
+
+const visibleCells = computed(() => {
+  const arr = Array.isArray(props.cells) ? props.cells : []
+  const cap = arr.length > MAX_NORMAL ? MAX_DENSE : MAX_NORMAL
+  return arr.slice(0, cap)
+})
+
+const overflowCount = computed(() => {
+  const arr = Array.isArray(props.cells) ? props.cells : []
+  const cap = arr.length > MAX_NORMAL ? MAX_DENSE : MAX_NORMAL
+  return Math.max(0, arr.length - cap)
+})
+</script>
+
+<style scoped>
+.tiles-view {
+  position: relative;
+  flex: 1;
+  min-height: 0;
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  grid-template-rows: repeat(2, minmax(0, 1fr));
+  grid-auto-rows: 0;
+  gap: 10px;
+  align-content: stretch;
+  overflow: hidden;
+}
+
+.tiles-view--dense {
+  grid-template-columns: repeat(4, minmax(0, 1fr));
+  grid-template-rows: repeat(2, minmax(0, 1fr));
+  gap: 8px;
+}
+
+.tile {
+  position: relative;
+  display: flex;
+  align-items: stretch;
+  min-height: 0;
+  min-width: 0;
+  background: rgba(15, 23, 42, 0.55);
+  border: 1px solid rgba(51, 65, 85, 0.45);
+  border-radius: 8px;
+  overflow: hidden;
+  --tile-accent: #64748b;
+}
+
+.tile--good {
+  --tile-accent: #22c55e;
+}
+
+.tile--warn {
+  --tile-accent: #eab308;
+}
+
+.tile--bad {
+  --tile-accent: #ef4444;
+}
+
+.tile__accent {
+  flex: 0 0 4px;
+  background: var(--tile-accent);
+  box-shadow: 0 0 6px color-mix(in srgb, var(--tile-accent) 55%, transparent);
+}
+
+.tile__body {
+  flex: 1;
+  min-width: 0;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 8px;
+  padding: 8px 10px 8px 10px;
+}
+
+.tile__info {
+  flex: 1;
+  min-width: 0;
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+  justify-content: center;
+}
+
+.tile__label {
+  font-size: 12px;
+  font-weight: 500;
+  color: #cbd5e1;
+  line-height: 1.25;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  overflow: hidden;
+}
+
+.tile__label--formula {
+  cursor: help;
+  border-bottom: 1px dashed rgba(148, 163, 184, 0.55);
+  align-self: flex-start;
+  max-width: 100%;
+}
+
+.tile__target {
+  font-size: 11px;
+  color: #94a3b8;
+  line-height: 1.25;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  overflow: hidden;
+}
+
+.tile__target-val {
+  color: #cbd5e1;
+  font-weight: 500;
+}
+
+.tile__main {
+  display: flex;
+  align-items: baseline;
+  gap: 3px;
+  flex-shrink: 0;
+  max-width: 55%;
+  justify-content: flex-end;
+}
+
+.tile__value {
+  font-size: clamp(18px, 2.6vw, 26px);
+  font-weight: 800;
+  color: #ffffff;
+  line-height: 1.05;
+  letter-spacing: 0.02em;
+  white-space: nowrap;
+}
+
+.tile__unit {
+  font-size: 12px;
+  color: #e2e8f0;
+  font-weight: 500;
+}
+
+.tiles-view--dense .tile__value {
+  font-size: clamp(14px, 1.9vw, 18px);
+}
+
+.tiles-view--dense .tile__label {
+  font-size: 11px;
+}
+
+.tiles-view--dense .tile__target {
+  font-size: 10px;
+}
+
+.tiles-view__overflow {
+  position: absolute;
+  right: 4px;
+  bottom: 2px;
+  font-size: 10px;
+  font-weight: 700;
+  color: #93c5fd;
+  background: rgba(15, 23, 42, 0.75);
+  border: 1px solid rgba(96, 165, 250, 0.55);
+  border-radius: 10px;
+  padding: 1px 6px;
+  pointer-events: none;
+  line-height: 1.2;
+}
+
+.home-kpi-card__empty {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #94a3b8;
+  font-size: 11px;
+  padding: 8px;
+  text-align: center;
+  line-height: 1.4;
+}
+</style>

+ 32 - 0
Web/src/views/dashboard/components/homeCardStyles.ts

@@ -13,6 +13,7 @@
 import type { Component } from 'vue';
 import HomeCardGridView from './HomeCardGridView.vue';
 import HomeCardTableView from './HomeCardTableView.vue';
+import HomeCardTilesView from './HomeCardTilesView.vue';
 
 export interface HomeCardStyleDef {
 	/** 后端 LayoutPattern 值(snake_case) */
@@ -72,6 +73,30 @@ const SVG_TABLE_LIST = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80
   <line x1="63" y1="37.5" x2="73" y2="37.5" stroke="#facc15" stroke-width="0.8"/>
 </svg>`;
 
+const SVG_TILE_CARDS = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 48" width="100%" height="100%">
+  <rect x="0.5" y="0.5" width="79" height="47" rx="4" fill="rgba(30,41,59,0.55)" stroke="rgba(100,116,139,0.7)"/>
+  <rect x="4" y="5" width="34" height="17" rx="3" fill="rgba(15,23,42,0.7)" stroke="rgba(51,65,85,0.85)"/>
+  <rect x="4" y="5" width="3" height="17" rx="1" fill="#22c55e"/>
+  <line x1="10" y1="10" x2="22" y2="10" stroke="#cbd5e1" stroke-width="1"/>
+  <line x1="10" y1="14" x2="20" y2="14" stroke="#64748b" stroke-width="0.8"/>
+  <line x1="26" y1="11.5" x2="35" y2="11.5" stroke="#f8fafc" stroke-width="2.2"/>
+  <rect x="42" y="5" width="34" height="17" rx="3" fill="rgba(15,23,42,0.7)" stroke="rgba(51,65,85,0.85)"/>
+  <rect x="42" y="5" width="3" height="17" rx="1" fill="#ef4444"/>
+  <line x1="48" y1="10" x2="60" y2="10" stroke="#cbd5e1" stroke-width="1"/>
+  <line x1="48" y1="14" x2="58" y2="14" stroke="#64748b" stroke-width="0.8"/>
+  <line x1="64" y1="11.5" x2="73" y2="11.5" stroke="#f8fafc" stroke-width="2.2"/>
+  <rect x="4" y="26" width="34" height="17" rx="3" fill="rgba(15,23,42,0.7)" stroke="rgba(51,65,85,0.85)"/>
+  <rect x="4" y="26" width="3" height="17" rx="1" fill="#eab308"/>
+  <line x1="10" y1="31" x2="22" y2="31" stroke="#cbd5e1" stroke-width="1"/>
+  <line x1="10" y1="35" x2="20" y2="35" stroke="#64748b" stroke-width="0.8"/>
+  <line x1="26" y1="32.5" x2="35" y2="32.5" stroke="#f8fafc" stroke-width="2.2"/>
+  <rect x="42" y="26" width="34" height="17" rx="3" fill="rgba(15,23,42,0.7)" stroke="rgba(51,65,85,0.85)"/>
+  <rect x="42" y="26" width="3" height="17" rx="1" fill="#ef4444"/>
+  <line x1="48" y1="31" x2="60" y2="31" stroke="#cbd5e1" stroke-width="1"/>
+  <line x1="48" y1="35" x2="58" y2="35" stroke="#64748b" stroke-width="0.8"/>
+  <line x1="64" y1="32.5" x2="73" y2="32.5" stroke="#f8fafc" stroke-width="2.2"/>
+</svg>`;
+
 /** 全局样式注册表。新增样式在此追加一条即可。 */
 export const HOME_CARD_STYLES: HomeCardStyleDef[] = [
 	{
@@ -88,6 +113,13 @@ export const HOME_CARD_STYLES: HomeCardStyleDef[] = [
 		thumbnail: SVG_TABLE_LIST,
 		component: HomeCardTableView,
 	},
+	{
+		code: 'tile_cards',
+		label: '状态卡片',
+		description: '独立圆角卡片 + 左侧状态色条(绿/黄/红由指标主数据的 YellowThreshold / RedThreshold / Direction 驱动)。适合重点 KPI 一眼看达标情况。',
+		thumbnail: SVG_TILE_CARDS,
+		component: HomeCardTilesView,
+	},
 ];
 
 /** 按 code 取样式定义;找不到时回落到默认样式,保证永不返回 undefined。 */

+ 1 - 1
Web/src/views/dashboard/home.vue

@@ -759,7 +759,7 @@ onUnmounted(() => {
 
 .area-s1 {
   grid-column: 3;
-  grid-row: 1 / 3;
+  grid-row: 1 / 2;
   min-height: 0;
 }
 

+ 1 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/AidopKanbanController.Generic.cs

@@ -285,6 +285,7 @@ WHERE tenant_id=@t AND factory_id=@f AND module_code=@m AND is_deleted=0
     {
         "card_grid",
         "table_list",
+        "tile_cards",
     };
 
     public class GenericLayoutL1Dto