Przeglądaj źródła

feat(s1-detail): C3 回滚至 pre-C2 原样式 + L1/L2 动态数据接入

- s1.vue 按 9122483a 之前的视觉结构恢复,移除 S4 统一 KPI 网格
- 保留通过 fetchHomeGrid / fetchDetailKpis 从「运营指标建模」动态加载 L1 / L2
- 监听 AIDOP_LAYOUT_SAVED 事件,模块布局保存后自动刷新
- chore: bump version Web 2.4.97 / Backend 1.0.64

Made-with: Cursor
skygu 1 miesiąc temu
rodzic
commit
0a75c1318e

+ 1 - 1
Web/package.json

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

+ 1276 - 14
Web/src/views/aidop/kanban/s1.vue

@@ -1,21 +1,1283 @@
 <template>
-  <ModuleDashboardPage
-    module-code="S1"
-    title="S1 产销协同详情"
-    left-title="产销协同执行"
-    left-subtitle="合同/BOM/工艺/交期评审 — 周期与人效"
-    right-title="产销协同结果"
-    right-subtitle="评审满足率与流转周转"
-    :extra-query-init="{ customer: '' }"
-  >
-    <template #extra-fields="{ query }">
+  <div class="s1-dashboard">
+    <!-- 顶部标题栏 -->
+    <div class="top-bar">
+      <div class="top-left">
+        <div class="page-title">
+          <el-icon :size="28"><Document /></el-icon>
+          <div>
+            <h1>产销协同指标看板</h1>
+            <div class="version-info">
+              <span>系统版本 V3.0.4</span>
+              <span class="status-dot"></span>
+              <span>状态:运行正常</span>
+            </div>
+          </div>
+        </div>
+      </div>
+      <div class="top-center">
+        <div class="search-box">
+          <el-input 
+            v-model="searchText" 
+            placeholder="搜索指标名称、部门 (如:工艺)..."
+            prefix-icon="Search"
+            clearable
+          />
+        </div>
+      </div>
+      <div class="top-right">
+        <div class="datetime">
+          <div class="date">{{ currentDate }}</div>
+          <div class="time">{{ currentTime }}</div>
+        </div>
+        <el-button type="primary" size="small">
+          <el-icon><Download /></el-icon>
+          导出实时快报
+        </el-button>
+        <el-button size="small">系统设置</el-button>
+      </div>
+    </div>
+
+    <!-- 模式切换栏 -->
+    <div class="mode-bar">
+      <div class="mode-left">
+        <el-button size="small" :type="activeMode === 'overview' ? 'primary' : ''">概览模式</el-button>
+        <el-button size="small" :type="activeMode === 'manufacturing' ? 'primary' : ''">制造侧监控</el-button>
+        <el-button size="small" :type="activeMode === 'sales' ? 'primary' : ''">销售侧分析</el-button>
+        <el-button size="small" :type="activeMode === 'supply' ? 'primary' : ''">供应链链动</el-button>
+      </div>
+      <div class="mode-right">
+        <span class="auto-refresh">自动轮播:</span>
+        <el-switch v-model="autoRefresh" />
+      </div>
+    </div>
+
+    <!-- 筛选栏 -->
+    <div class="filter-bar">
+      <div class="filter-left">
+        <span class="filter-label">部门视图</span>
+        <el-radio-group v-model="deptFilter" size="small">
+          <el-radio-button label="all">全部</el-radio-button>
+          <el-radio-button label="plan">主计划</el-radio-button>
+          <el-radio-button label="sales">销售</el-radio-button>
+          <el-radio-button label="design">设计</el-radio-button>
+          <el-radio-button label="tech">工艺</el-radio-button>
+        </el-radio-group>
+      </div>
+      <div class="filter-right">
+        <span class="filter-label">状态监控</span>
+        <el-radio-group v-model="statusFilter" size="small">
+          <el-radio-button label="all">全部</el-radio-button>
+          <el-radio-button label="optimal">最优</el-radio-button>
+          <el-radio-button label="standby">待机</el-radio-button>
+          <el-radio-button label="critical">严重</el-radio-button>
+        </el-radio-group>
+        <div class="showing-info">
+          展示:8 / 8 张卡片
+          <el-button link type="primary" size="small">
+            <el-icon><Refresh /></el-icon>
+            重置
+          </el-button>
+        </div>
+      </div>
+    </div>
+
+    <!-- 业务筛选:产销协同以订单/客户维度为主 -->
+    <DetailQueryBar
+      dark
+      band-title="基础查询:日期、产品、订单号、产线"
+      @query="onBizQuery"
+      @reset="onBizReset"
+    >
+      <SmartOpsBaseQueryFields v-model="bizQuery" compact />
       <el-form-item label="客户">
-        <el-input v-model="query.customer" placeholder="客户编码/名称" clearable style="width: 160px" />
+        <el-input v-model="bizQuery.customer" placeholder="客户名称/编码" clearable style="width: 160px" />
       </el-form-item>
-    </template>
-  </ModuleDashboardPage>
+    </DetailQueryBar>
+
+    <!-- 主内容区 -->
+    <div class="main-content">
+      <!-- 第一行:L1 指标卡(由"运营指标建模"动态驱动) -->
+      <div class="row-1">
+        <!-- Slot 0 / Slot 1 / Slot 2:普通指标卡(动态 L1 前三位) -->
+        <div
+          v-for="(slot, idx) in l1Slots.slice(0, 3)"
+          :key="`l1-slot-${idx}`"
+          class="card metric-card"
+        >
+          <template v-if="slot">
+            <div class="card-header">
+              <el-tooltip v-if="slot.formulaText" placement="top" :show-after="200" :content="slot.formulaText">
+                <span class="card-title is-formula">{{ slot.displayName || slot.metricCode }}</span>
+              </el-tooltip>
+              <span v-else class="card-title">{{ slot.displayName || slot.metricCode }}</span>
+              <el-tag size="small" :type="kpiTagType(slot.achievementLevel)">{{ kpiTagText(slot.achievementLevel) }}</el-tag>
+            </div>
+            <div class="metric-value">
+              <span class="value large">{{ fmtKpiValue(slot.currentValue) }}</span>
+              <span v-if="slot.unit" class="unit">{{ slot.unit }}</span>
+              <span class="trend" :class="kpiTrendClass(slot.achievementLevel)">
+                {{ slot.gapLabel || (slot.targetValue != null ? `目标 ${fmtKpiValue(slot.targetValue)}${slot.unit || ''}` : '') }}
+              </span>
+            </div>
+            <div v-if="slot.targetValue != null" class="progress-section">
+              <div class="progress-label">
+                <span>目标达成</span>
+                <span>{{ fmtKpiValue(slot.targetValue) }}{{ slot.unit || '' }} 目标</span>
+              </div>
+              <div class="progress-bar">
+                <div
+                  class="progress-fill"
+                  :style="{
+                    width: `${Math.max(0, Math.min(100, Number(slot.currentValue != null && slot.targetValue ? (Number(slot.currentValue) / Number(slot.targetValue)) * 100 : 0) || 0))}%`,
+                  }"
+                ></div>
+              </div>
+            </div>
+          </template>
+          <template v-else>
+            <div class="card-header">
+              <span class="card-title is-placeholder">(未配置 L1)</span>
+            </div>
+            <div class="metric-value">
+              <span class="value large">—</span>
+            </div>
+            <div class="placeholder-hint">请到「运营指标建模」为 {{ MODULE_CODE }} 添加 L1</div>
+          </template>
+        </div>
+
+        <!-- Slot 3:评审队列负荷 仪表盘(保留原形态;值取自动态 L1 第 4 位达成率) -->
+        <div class="card gauge-card">
+          <div class="card-header">
+            <el-tooltip v-if="l1Slots[3]?.formulaText" placement="top" :show-after="200" :content="l1Slots[3].formulaText">
+              <span class="card-title is-formula">{{ l1Slots[3]?.displayName || '评审队列负荷' }}</span>
+            </el-tooltip>
+            <span v-else class="card-title">{{ l1Slots[3]?.displayName || '评审队列负荷' }}</span>
+          </div>
+          <div id="gauge-pressure" class="gauge-chart"></div>
+          <div class="gauge-footer">
+            <div class="label">{{ l1Slots[3] ? '当前值' : '待评审订单数' }}</div>
+            <div class="value">
+              {{ l1Slots[3] ? fmtKpiValue(l1Slots[3].currentValue) : 128 }}
+              <span class="unit">{{ l1Slots[3]?.unit || '单' }}</span>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- 第二行 -->
+      <div class="row-2">
+        <!-- 评审满足率与周期趋势 -->
+        <div class="card large-chart-card">
+          <div class="card-header">
+            <div>
+              <span class="card-title">评审满足率与周期趋势(采购执行联动)</span>
+              <div class="subtitle">实时数据流</div>
+            </div>
+            <div class="legend">
+              <span class="legend-item">
+                <span class="dot" style="background: #60a5fa"></span>
+                满足率
+              </span>
+              <span class="legend-item">
+                <span class="dot" style="background: #34d399"></span>
+                均值线
+              </span>
+            </div>
+          </div>
+          <div id="chart-trend" class="main-chart"></div>
+        </div>
+
+        <!-- 异常流 -->
+        <div class="card anomaly-card">
+          <div class="card-header">
+            <span class="card-title">
+              <span class="live-dot"></span>
+              异常实时流
+            </span>
+            <span class="buffer-info">缓冲区:1024KB</span>
+          </div>
+          <div class="anomaly-list">
+            <div v-for="(item, idx) in anomalyItems" :key="`${item.time}-${idx}`" class="anomaly-item" :class="item.levelClass">
+              <div class="anomaly-header">
+                <span class="tag">{{ item.tag }}</span>
+                <span class="time">{{ item.time }}</span>
+              </div>
+              <div class="anomaly-message">{{ item.message }}</div>
+              <div v-if="item.source" class="anomaly-source">{{ item.source }}</div>
+            </div>
+          </div>
+          <div class="command-input">
+            <el-input 
+              v-model="commandText" 
+              placeholder="输入指令以响应..."
+              size="small"
+            >
+              <template #append>
+                <el-button type="primary">执行</el-button>
+              </template>
+            </el-input>
+          </div>
+        </div>
+      </div>
+
+      <!-- 第三行 -->
+      <div class="row-3">
+        <!-- 异常状态分布 -->
+        <div class="card chart-card">
+          <div class="card-header">
+            <span class="card-title">异常状态分布(物料仓储)</span>
+          </div>
+          <div id="chart-donut" class="donut-chart"></div>
+        </div>
+
+        <!-- 人效热度监控 -->
+        <div class="card chart-card">
+          <div class="card-header">
+            <span class="card-title">人效热度监控(生产执行)</span>
+          </div>
+          <div id="chart-bar" class="bar-chart"></div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 底部版权信息 -->
+    <div class="footer">
+      <div class="footer-left">数据流已加密(AES-256-GCM)</div>
+      <div class="footer-right">版权所有 © 2026 核心智能运营 · 系统运行正常</div>
+    </div>
+  </div>
 </template>
 
 <script setup>
-import ModuleDashboardPage from './components/ModuleDashboardPage.vue'
+import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
+import { useRouter } from 'vue-router'
+import { ElMessage } from 'element-plus'
+import { Document, Search, Download, Refresh } from '@element-plus/icons-vue'
+import DetailQueryBar from './components/DetailQueryBar.vue'
+import SmartOpsBaseQueryFields from './components/SmartOpsBaseQueryFields.vue'
+import {
+  emptySmartOpsBaseQuery,
+  blobMatchesSmartOpsBase,
+  textMatches,
+  summarizeSmartOpsBaseQuery,
+  baseQueryToApiParams,
+} from './utils/smartOpsBaseQuery'
+import * as echarts from 'echarts'
+import { homeS1, loadHomeModuleMetrics } from './data/homeModulesSync'
+import { fetchModuleDetail, fetchHomeGrid, fetchDetailKpis } from '../api/kanbanData'
+import { AIDOP_LAYOUT_SAVED } from './utils/s4LayoutEvents'
+
+const MODULE_CODE = 'S1'
+
+const router = useRouter()
+
+const searchText = ref('')
+const commandText = ref('')
+const currentTime = ref('')
+const currentDate = ref('')
+const autoRefresh = ref(true)
+const activeMode = ref('overview')
+const deptFilter = ref('all')
+const statusFilter = ref('all')
+const trendXAxis = ref(['D1', 'D2', 'D3', 'D4', 'D5', 'D6', 'D7', 'D8'])
+const barXAxis = ref(['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'])
+const barSeries = ref([65, 78, 52, 92, 75, 68])
+
+const bizQuery = ref({
+  ...emptySmartOpsBaseQuery(),
+  customer: '',
+})
+
+const l1Items = ref([])
+const l2Items = ref([])
+
+const l1Slots = computed(() => {
+  const arr = Array.isArray(l1Items.value) ? l1Items.value : []
+  return [0, 1, 2, 3].map((i) => arr[i] || null)
+})
+
+function fmtKpiValue(v) {
+  if (v === null || v === undefined || v === '') return '—'
+  const n = Number(v)
+  if (!Number.isFinite(n)) return String(v)
+  if (Math.abs(n) >= 100) return n.toFixed(0)
+  if (Math.abs(n) >= 10) return n.toFixed(1)
+  return n.toFixed(2)
+}
+
+function kpiTagType(level) {
+  const lv = String(level || '').toLowerCase()
+  if (lv === 'red') return 'danger'
+  if (lv === 'yellow') return 'warning'
+  if (lv === 'green') return 'success'
+  return 'info'
+}
+
+function kpiTagText(level) {
+  const lv = String(level || '').toLowerCase()
+  if (lv === 'red') return '未达标'
+  if (lv === 'yellow') return '待改善'
+  if (lv === 'green') return '达标'
+  return '—'
+}
+
+function kpiTrendClass(level) {
+  const lv = String(level || '').toLowerCase()
+  if (lv === 'red') return 'down'
+  if (lv === 'green') return 'up'
+  return ''
+}
+
+function buildDonutFromList(list) {
+  const c = list.filter((x) => x.levelClass === 'critical').length
+  const w = list.filter((x) => x.levelClass === 'warning').length
+  const i = list.filter((x) => x.levelClass === 'info').length
+  const standby = Math.max(0, list.length - c - w - i)
+  return [
+    { value: c, name: '严重', itemStyle: { color: '#f87171' } },
+    { value: w, name: '警告', itemStyle: { color: '#fbbf24' } },
+    { value: i, name: '正常', itemStyle: { color: '#34d399' } },
+    { value: standby, name: '待机', itemStyle: { color: '#60a5fa' } },
+  ]
+}
+
+const anomalyItemsAll = ref([])
+
+const anomalyItems = computed(() =>
+  anomalyItemsAll.value.filter((x) => {
+    const blob = `${x.message} ${x.tag} ${x.source}`
+    if (!blobMatchesSmartOpsBase(bizQuery.value, blob)) return false
+    if (!textMatches(bizQuery.value.customer, blob)) return false
+    return true
+  })
+)
+
+const donutSeries = ref(buildDonutFromList([]))
+
+watch(
+  anomalyItems,
+  (list) => {
+    donutSeries.value = buildDonutFromList(list)
+    nextTick(() => donutChart?.setOption({ series: [{ data: donutSeries.value }] }))
+  },
+  { deep: true }
+)
+
+function bizQueryExtra() {
+  return { ...baseQueryToApiParams(bizQuery.value) }
+}
+
+async function loadDynamicKpis() {
+  const extra = bizQueryExtra()
+  const [grid, detail] = await Promise.all([
+    fetchHomeGrid(MODULE_CODE, 1, extra),
+    fetchDetailKpis(MODULE_CODE, 1, undefined, extra),
+  ])
+  l1Items.value = Array.isArray(grid?.items) ? grid.items : []
+  l2Items.value = Array.isArray(detail) ? detail : []
+  applyDynamicChartData()
+}
+
+function applyDynamicChartData() {
+  const l2 = l2Items.value
+  if (l2.length > 0) {
+    const slice = l2.slice(0, 10)
+    barXAxis.value = slice.map((x, i) => x.displayName || x.metricCode || `N${i + 1}`)
+    barSeries.value = slice.map((x) => {
+      const n = Number(x.currentValue)
+      return Number.isFinite(n) ? n : 0
+    })
+    nextTick(() => barChart?.setOption({ xAxis: { data: barXAxis.value }, series: [{ data: barSeries.value }] }))
+  }
+}
+
+function onBizQuery() {
+  loadDynamicKpis()
+  ElMessage.success(`已应用筛选(${summarizeSmartOpsBaseQuery(bizQuery.value)})`)
+}
+
+function onBizReset() {
+  bizQuery.value = { ...emptySmartOpsBaseQuery(), customer: '' }
+  loadDynamicKpis()
+  ElMessage.info('已重置业务筛选')
+}
+
+function onLayoutSaved(e) {
+  const mc = String(e?.detail?.moduleCode || '').toUpperCase()
+  if (!mc || mc === MODULE_CODE) loadDynamicKpis()
+}
+
+let gaugeChart = null
+let trendChart = null
+let donutChart = null
+let barChart = null
+
+const updateTime = () => {
+  const now = new Date()
+  currentTime.value = now.toLocaleString('zh-CN', {
+    year: 'numeric',
+    month: '2-digit',
+    day: '2-digit',
+    hour: '2-digit',
+    minute: '2-digit',
+    second: '2-digit',
+    hour12: false
+  })
+  currentDate.value = now.toLocaleDateString('zh-CN')
+}
+
+// 初始化评审队列负荷仪表
+const initGaugeChart = () => {
+  const chartDom = document.getElementById('gauge-pressure')
+  if (!chartDom) return
+  
+  gaugeChart = echarts.init(chartDom)
+  const option = {
+    series: [{
+      type: 'gauge',
+      startAngle: 180,
+      endAngle: 0,
+      min: 0,
+      max: 100,
+      radius: '90%',
+      center: ['50%', '60%'],
+      itemStyle: {
+        color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
+          { offset: 0, color: '#34d399' },
+          { offset: 0.5, color: '#fbbf24' },
+          { offset: 1, color: '#f87171' }
+        ])
+      },
+      progress: {
+        show: true,
+        width: 12,
+        roundCap: true
+      },
+      pointer: { show: false },
+      axisLine: {
+        lineStyle: {
+          width: 12,
+          color: [[1, '#1e293b']]
+        }
+      },
+      axisTick: { show: false },
+      splitLine: { show: false },
+      axisLabel: {
+        show: true,
+        color: '#64748b',
+        fontSize: 10,
+        distance: -15
+      },
+      title: { show: false },
+      detail: {
+        valueAnimation: true,
+        offsetCenter: [0, '10%'],
+        fontSize: 24,
+        fontWeight: 'bold',
+        color: '#e2e8f0',
+        formatter: '{value}%'
+      },
+      data: [{ value: computeGaugeValue() }]
+    }]
+  }
+  gaugeChart.setOption(option)
+}
+
+function computeGaugeValue() {
+  const s = l1Slots.value[3]
+  if (s && s.currentValue != null && s.targetValue != null && Number(s.targetValue) !== 0) {
+    const pct = (Number(s.currentValue) / Number(s.targetValue)) * 100
+    if (Number.isFinite(pct)) return Math.max(0, Math.min(100, Math.round(pct)))
+  }
+  return homeS1.systemPressureGaugePct
+}
+
+watch(l1Slots, () => {
+  nextTick(() => gaugeChart?.setOption({ series: [{ data: [{ value: computeGaugeValue() }] }] }))
+}, { deep: true })
+
+// 初始化趋势图
+const initTrendChart = () => {
+  const chartDom = document.getElementById('chart-trend')
+  if (!chartDom) return
+  
+  trendChart = echarts.init(chartDom)
+  const option = {
+    tooltip: {
+      trigger: 'axis',
+      backgroundColor: 'rgba(15, 23, 42, 0.95)',
+      borderColor: '#334155',
+      textStyle: { color: '#e2e8f0' },
+      axisPointer: { type: 'cross' }
+    },
+    grid: {
+      left: '3%',
+      right: '4%',
+      bottom: '10%',
+      top: '15%',
+      containLabel: true
+    },
+    xAxis: {
+      type: 'category',
+      boundaryGap: false,
+      data: trendXAxis.value,
+      axisLine: { lineStyle: { color: '#334155' } },
+      axisLabel: { color: '#64748b' }
+    },
+    yAxis: {
+      type: 'value',
+      min: 70,
+      max: 100,
+      splitLine: {
+        lineStyle: {
+          color: '#1e293b',
+          type: 'solid'
+        }
+      },
+      axisLabel: { color: '#64748b' }
+    },
+    series: [
+      {
+        name: '满足率',
+        type: 'line',
+        smooth: true,
+        symbol: 'circle',
+        symbolSize: 6,
+        itemStyle: {
+          color: '#60a5fa',
+          borderColor: '#1e293b',
+          borderWidth: 2
+        },
+        lineStyle: {
+          color: '#60a5fa',
+          width: 3
+        },
+        areaStyle: {
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: 'rgba(96, 165, 250, 0.3)' },
+            { offset: 1, color: 'rgba(96, 165, 250, 0)' }
+          ])
+        },
+        data: homeS1.trendSatisfaction
+      },
+      {
+        name: '均值线',
+        type: 'line',
+        smooth: true,
+        symbol: 'none',
+        lineStyle: {
+          color: '#34d399',
+          width: 2,
+          type: 'dashed'
+        },
+        data: Array(8).fill(homeS1.trendMeanLine)
+      }
+    ]
+  }
+  trendChart.setOption(option)
+}
+
+// 初始化环形图
+const initDonutChart = () => {
+  const chartDom = document.getElementById('chart-donut')
+  if (!chartDom) return
+  
+  donutChart = echarts.init(chartDom)
+  const option = {
+    tooltip: {
+      trigger: 'item',
+      backgroundColor: 'rgba(15, 23, 42, 0.95)',
+      borderColor: '#334155',
+      textStyle: { color: '#e2e8f0' }
+    },
+    series: [{
+      name: '异常状态',
+      type: 'pie',
+      radius: ['50%', '70%'],
+      center: ['50%', '50%'],
+      avoidLabelOverlap: false,
+      label: {
+        show: false
+      },
+      emphasis: {
+        label: {
+          show: true,
+          fontSize: 14,
+          fontWeight: 'bold',
+          color: '#e2e8f0'
+        }
+      },
+      labelLine: { show: false },
+      data: donutSeries.value
+    }]
+  }
+  donutChart.setOption(option)
+}
+
+// 初始化柱状图
+const initBarChart = () => {
+  const chartDom = document.getElementById('chart-bar')
+  if (!chartDom) return
+  
+  barChart = echarts.init(chartDom)
+  const option = {
+    tooltip: {
+      trigger: 'axis',
+      backgroundColor: 'rgba(15, 23, 42, 0.95)',
+      borderColor: '#334155',
+      textStyle: { color: '#e2e8f0' }
+    },
+    grid: {
+      left: '3%',
+      right: '4%',
+      bottom: '15%',
+      top: '10%',
+      containLabel: true
+    },
+    xAxis: {
+      type: 'category',
+      data: barXAxis.value,
+      axisLine: { lineStyle: { color: '#334155' } },
+      axisLabel: { color: '#64748b' }
+    },
+    yAxis: {
+      type: 'value',
+      show: false
+    },
+    series: [{
+      data: barSeries.value,
+      type: 'bar',
+      barWidth: '60%',
+      itemStyle: {
+        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+          { offset: 0, color: '#60a5fa' },
+          { offset: 1, color: '#3b82f6' }
+        ]),
+        borderRadius: [4, 4, 0, 0]
+      },
+      showBackground: true,
+      backgroundStyle: {
+        color: 'rgba(51, 65, 85, 0.3)',
+        borderRadius: [4, 4, 0, 0]
+      }
+    }]
+  }
+  barChart.setOption(option)
+}
+
+const initCharts = () => {
+  nextTick(() => {
+    initGaugeChart()
+    initTrendChart()
+    initDonutChart()
+    initBarChart()
+    
+    window.addEventListener('resize', () => {
+      gaugeChart?.resize()
+      trendChart?.resize()
+      donutChart?.resize()
+      barChart?.resize()
+    })
+  })
+}
+
+onMounted(async () => {
+  await loadHomeModuleMetrics()
+  const detail = await fetchModuleDetail(MODULE_CODE)
+  anomalyItemsAll.value = (detail.alerts ?? []).map((x) => ({
+    tag: `[${String(x.level ?? 'info').toUpperCase()}]`,
+    time: x.time ?? '--:--:--',
+    message: x.message ?? '异常告警',
+    source: `Source: Ai-DOP / Module: ${MODULE_CODE}`,
+    levelClass: ['critical', 'high'].includes(String(x.level)) ? 'critical' : String(x.level) === 'warning' ? 'warning' : 'info'
+  }))
+  await loadDynamicKpis()
+  updateTime()
+  setInterval(updateTime, 1000)
+  initCharts()
+  if (typeof window !== 'undefined') {
+    window.addEventListener(AIDOP_LAYOUT_SAVED, onLayoutSaved)
+  }
+})
+
+onUnmounted(() => {
+  if (typeof window !== 'undefined') {
+    window.removeEventListener(AIDOP_LAYOUT_SAVED, onLayoutSaved)
+  }
+})
 </script>
+
+<style scoped>
+.s1-dashboard {
+  box-sizing: border-box;
+  flex: 1;
+  min-height: 0;
+  max-height: 100%;
+  overflow-x: hidden;
+  overflow-y: auto;
+  -webkit-overflow-scrolling: touch;
+  background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
+  padding: 15px;
+  padding-bottom: 24px;
+  color: #e2e8f0;
+}
+
+/* 顶部标题栏 */
+.top-bar {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  background: rgba(15, 23, 42, 0.8);
+  padding: 15px 20px;
+  border-radius: 8px;
+  margin-bottom: 15px;
+  border: 1px solid #1e293b;
+}
+
+.top-left {
+  display: flex;
+  align-items: center;
+  gap: 15px;
+}
+
+.page-title {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  color: #60a5fa;
+}
+
+.page-title h1 {
+  margin: 0;
+  font-size: 22px;
+  font-weight: 700;
+  color: #e2e8f0;
+}
+
+.version-info {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  font-size: 11px;
+  color: #64748b;
+  margin-top: 4px;
+}
+
+.status-dot {
+  width: 6px;
+  height: 6px;
+  border-radius: 50%;
+  background: #34d399;
+}
+
+.top-center {
+  flex: 1;
+  max-width: 500px;
+  margin: 0 30px;
+}
+
+.search-box :deep(.el-input__wrapper) {
+  background: rgba(30, 41, 59, 0.6);
+  border: 1px solid #334155;
+}
+
+.search-box :deep(.el-input__inner) {
+  color: #e2e8f0;
+}
+
+.top-right {
+  display: flex;
+  align-items: center;
+  gap: 15px;
+}
+
+.datetime {
+  text-align: right;
+  margin-right: 10px;
+}
+
+.datetime .date {
+  font-size: 13px;
+  color: #93c5fd;
+  font-weight: 600;
+}
+
+.datetime .time {
+  font-size: 18px;
+  color: #60a5fa;
+  font-weight: 700;
+  font-family: 'Courier New', monospace;
+}
+
+/* 模式切换栏 */
+.mode-bar {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  background: rgba(30, 41, 59, 0.6);
+  padding: 10px 15px;
+  border-radius: 6px;
+  margin-bottom: 15px;
+  border: 1px solid #334155;
+}
+
+.mode-left {
+  display: flex;
+  gap: 10px;
+}
+
+.mode-right {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  color: #94a3b8;
+  font-size: 13px;
+}
+
+/* 筛选栏 */
+.filter-bar {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  background: rgba(30, 41, 59, 0.4);
+  padding: 12px 15px;
+  border-radius: 6px;
+  margin-bottom: 15px;
+  border: 1px solid #1e293b;
+}
+
+.filter-left, .filter-right {
+  display: flex;
+  align-items: center;
+  gap: 15px;
+}
+
+.filter-label {
+  font-size: 12px;
+  color: #64748b;
+  font-weight: 600;
+}
+
+.showing-info {
+  font-size: 12px;
+  color: #64748b;
+  margin-left: 20px;
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+
+/* 主内容区 */
+.main-content {
+  display: flex;
+  flex-direction: column;
+  gap: 15px;
+}
+
+.row-1 {
+  display: grid;
+  grid-template-columns: repeat(4, 1fr);
+  gap: 15px;
+}
+
+.row-2 {
+  display: grid;
+  grid-template-columns: 2fr 1fr;
+  gap: 15px;
+}
+
+.row-3 {
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  gap: 15px;
+}
+
+/* 卡片通用样式 */
+.card {
+  background: rgba(30, 41, 59, 0.6);
+  border-radius: 8px;
+  padding: 15px;
+  border: 1px solid #334155;
+}
+
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 15px;
+}
+
+.card-title {
+  font-size: 13px;
+  font-weight: 700;
+  color: #e2e8f0;
+}
+
+.card-title.is-formula {
+  cursor: help;
+  border-bottom: 1px dashed rgba(148, 163, 184, 0.5);
+}
+
+.card-title.is-placeholder {
+  color: #64748b;
+  font-weight: 500;
+}
+
+.placeholder-hint {
+  margin-top: 10px;
+  font-size: 11px;
+  color: #64748b;
+  line-height: 1.5;
+}
+
+.subtitle {
+  font-size: 10px;
+  color: #64748b;
+  margin-top: 4px;
+}
+
+/* 指标卡片 */
+.metric-card .metric-value {
+  display: flex;
+  align-items: baseline;
+  gap: 8px;
+  margin-bottom: 15px;
+}
+
+.metric-card .value {
+  font-size: 36px;
+  font-weight: 700;
+  color: #e2e8f0;
+}
+
+.metric-card .value.large {
+  font-size: 48px;
+}
+
+.metric-card .unit {
+  font-size: 16px;
+  color: #64748b;
+}
+
+.trend {
+  font-size: 13px;
+  font-weight: 700;
+}
+
+.trend.up {
+  color: #34d399;
+}
+
+.trend.down {
+  color: #f87171;
+}
+
+.progress-section {
+  margin-top: 10px;
+}
+
+.progress-label {
+  display: flex;
+  justify-content: space-between;
+  font-size: 11px;
+  color: #94a3b8;
+  margin-bottom: 6px;
+}
+
+.progress-bar {
+  height: 6px;
+  background: #1e293b;
+  border-radius: 3px;
+  overflow: hidden;
+}
+
+.progress-fill {
+  height: 100%;
+  background: linear-gradient(90deg, #60a5fa, #3b82f6);
+  border-radius: 3px;
+}
+
+.sub-metrics {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 10px;
+  margin-top: 15px;
+}
+
+.sub-metric {
+  background: rgba(0, 0, 0, 0.2);
+  padding: 10px;
+  border-radius: 4px;
+}
+
+.sub-metric .label {
+  font-size: 11px;
+  color: #64748b;
+  margin-bottom: 4px;
+}
+
+.sub-metric .value {
+  font-size: 16px;
+  font-weight: 700;
+  color: #e2e8f0;
+}
+
+/* 约束列表 */
+.constraint-list {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+}
+
+.constraint-item {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  padding: 8px;
+  background: rgba(0, 0, 0, 0.2);
+  border-radius: 4px;
+}
+
+.constraint-item .dot {
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  background: #34d399;
+}
+
+.constraint-item .dot.warning {
+  background: #fbbf24;
+}
+
+.constraint-item .label {
+  flex: 1;
+  font-size: 12px;
+  color: #94a3b8;
+}
+
+.constraint-item .value {
+  font-size: 14px;
+  font-weight: 700;
+  color: #e2e8f0;
+}
+
+.constraint-item .value.warning {
+  color: #fbbf24;
+}
+
+/* 仪表盘卡片 */
+.gauge-card {
+  text-align: center;
+}
+
+.gauge-chart {
+  width: 100%;
+  height: 150px;
+}
+
+.gauge-footer {
+  margin-top: 10px;
+}
+
+.gauge-footer .label {
+  font-size: 11px;
+  color: #64748b;
+  margin-bottom: 4px;
+}
+
+.gauge-footer .value {
+  font-size: 20px;
+  font-weight: 700;
+  color: #e2e8f0;
+}
+
+.gauge-footer .unit {
+  font-size: 12px;
+  color: #64748b;
+}
+
+/* 大图表卡片 */
+.large-chart-card .main-chart {
+  width: 100%;
+  height: 350px;
+}
+
+.legend {
+  display: flex;
+  gap: 15px;
+  font-size: 12px;
+  color: #64748b;
+}
+
+.legend-item {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+}
+
+.legend-item .dot {
+  width: 10px;
+  height: 10px;
+  border-radius: 50%;
+}
+
+/* 异常流卡片 */
+.anomaly-card {
+  display: flex;
+  flex-direction: column;
+}
+
+.anomaly-card .card-header {
+  margin-bottom: 12px;
+}
+
+.anomaly-card .card-title {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.live-dot {
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  background: #ef4444;
+  animation: pulse 2s infinite;
+}
+
+@keyframes pulse {
+  0%, 100% { opacity: 1; }
+  50% { opacity: 0.5; }
+}
+
+.buffer-info {
+  font-size: 11px;
+  color: #64748b;
+}
+
+.anomaly-list {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  overflow-y: auto;
+  max-height: 400px;
+  margin-bottom: 12px;
+}
+
+.anomaly-item {
+  padding: 10px;
+  background: rgba(0, 0, 0, 0.2);
+  border-radius: 4px;
+  border-left: 3px solid;
+}
+
+.anomaly-item.critical {
+  border-color: #ef4444;
+  background: rgba(239, 68, 68, 0.1);
+}
+
+.anomaly-item.warning {
+  border-color: #f59e0b;
+  background: rgba(245, 158, 11, 0.1);
+}
+
+.anomaly-item.info {
+  border-color: #3b82f6;
+  background: rgba(59, 130, 246, 0.1);
+}
+
+.anomaly-item.success {
+  border-color: #10b981;
+  background: rgba(16, 185, 129, 0.1);
+}
+
+.anomaly-item.stall {
+  border-color: #8b5cf6;
+  background: rgba(139, 92, 246, 0.1);
+}
+
+.anomaly-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 6px;
+}
+
+.anomaly-header .tag {
+  font-size: 10px;
+  font-weight: 700;
+  padding: 2px 6px;
+  border-radius: 3px;
+}
+
+.anomaly-item.critical .tag { color: #ef4444; background: rgba(239, 68, 68, 0.2); }
+.anomaly-item.warning .tag { color: #f59e0b; background: rgba(245, 158, 11, 0.2); }
+.anomaly-item.info .tag { color: #3b82f6; background: rgba(59, 130, 246, 0.2); }
+.anomaly-item.success .tag { color: #10b981; background: rgba(16, 185, 129, 0.2); }
+.anomaly-item.stall .tag { color: #8b5cf6; background: rgba(139, 92, 246, 0.2); }
+
+.anomaly-header .time {
+  font-size: 11px;
+  color: #64748b;
+}
+
+.anomaly-message {
+  font-size: 12px;
+  color: #e2e8f0;
+  margin-bottom: 4px;
+}
+
+.anomaly-source {
+  font-size: 10px;
+  color: #64748b;
+}
+
+.command-input {
+  margin-top: 10px;
+}
+
+/* 图表卡片 */
+.chart-card .donut-chart,
+.chart-card .bar-chart {
+  width: 100%;
+  height: 200px;
+}
+
+/* 底部版权信息 */
+.footer {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-top: 20px;
+  padding-top: 15px;
+  border-top: 1px solid #1e293b;
+  font-size: 10px;
+  color: #475569;
+}
+
+/* Element Plus 覆盖 */
+:deep(.el-radio-button__inner) {
+  background: rgba(30, 41, 59, 0.6);
+  border: 1px solid #334155;
+  color: #94a3b8;
+}
+
+:deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
+  background: linear-gradient(135deg, #3b82f6, #2563eb);
+  border-color: #3b82f6;
+  color: white;
+}
+
+:deep(.el-button--primary) {
+  background: linear-gradient(135deg, #3b82f6, #2563eb);
+  border: none;
+}
+
+:deep(.el-tag--success) {
+  background: rgba(16, 185, 129, 0.2);
+  border-color: rgba(16, 185, 129, 0.3);
+  color: #10b981;
+}
+</style>

+ 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.63</AssemblyVersion>
-    <FileVersion>1.0.63</FileVersion>
-    <Version>1.0.63</Version>
+    <AssemblyVersion>1.0.64</AssemblyVersion>
+    <FileVersion>1.0.64</FileVersion>
+    <Version>1.0.64</Version>
   </PropertyGroup>
 
   <ItemGroup>