浏览代码

feat(aidop): smart ops

aidopdev 1 周之前
父节点
当前提交
f182154b38

+ 18 - 0
.cursor/rules/README.md

@@ -0,0 +1,18 @@
+# 本仓库 Cursor 规则(团队共用)
+
+本目录下的 `.mdc` 规则文件**已纳入 Git**,适用于**全体协作者**在本仓库内使用 Cursor 或其它约定流程时的协作标准。
+
+## 如何生效
+
+- 在 **Cursor** 中打开本仓库根目录时,带有 `alwaysApply: true` 的规则会**自动**注入 AI 对话上下文,无需每人单独配置。
+- 请**随代码提交/拉取**本目录;**不要**在仓库内将 `.cursor/rules/` 加入 `.gitignore`(个人敏感项请用用户级 Cursor 设置,勿改团队规则文件)。
+
+## 当前规则文件
+
+| 文件 | 说明 |
+|------|------|
+| `collaboration-scope.mdc` | 改前清单待确认、跨模块影响须先预警、小问题最小改动不大重构等 |
+
+## 非 Cursor 成员
+
+若使用其它编辑器或 AI 工具,请**人工对照**上述 `.mdc` 正文执行相同约定。

+ 33 - 0
.cursor/rules/collaboration-scope.mdc

@@ -0,0 +1,33 @@
+---
+description: 改代码前先列清单待确认;跨模块影响须预警;小问题最小改动、不大重构
+alwaysApply: true
+---
+
+# 协作与改动范围
+
+## 动手前须先列清单、等确认
+
+在**编写或应用补丁之前**,先用简短条目列出拟改动,等用户明确确认(或调整)后再改。清单建议包含:
+
+- **目标**:一句话说明要解决什么  
+- **将改动的文件**:路径 + 每处大概做什么(一行即可)  
+- **可见行为变化**(若有)  
+- **风险/影响面**(配置、部署、数据库、种子数据、公共 API 等)  
+- **本轮明确不做的事**(范围边界)
+
+用户未确认前,不默认执行大范围修改。
+
+## 跨模块影响:先预警,再执行
+
+在改动**某一功能模块**(如「S1 产销协同」看板、某条业务路由、共享组件/接口)时,若判断改动会**波及其它功能模块**(如共用 `kanbanData`、统一查询工具、布局组件、后端公共接口、菜单种子等),必须:
+
+1. **明确写出「跨模块影响」小节**,列出:会影响**哪个模块**、可能影响**哪类能力**(页面、接口、筛选、导航、数据口径等)。  
+2. **在用户就此做出决策(接受 / 缩小范围 / 拆分任务)之前,不执行**会牵连其它模块的修改。
+
+示例:改 S1 时若会动到 S6 共用的请求层或公共看板组件,须说明「本次会影响 S6 生产执行的 xxx(如:同一套基础查询参数、同一 API)」,待确认后再改。
+
+## 小问题:最小改动,不大重构
+
+- 修小 bug、小交互、小样式时:**只做必要修改**,避免顺手重构、扩框架、改全局架构。  
+- 若存在**更大方案**(抽公共组件、换目录结构、动框架/中间件/全局配置等):**单独给出建议 + 利弊 + 可选方案**,由用户决策;**不默认替用户上大改**。  
+- 非必要不动:种子数据、Docker/Nginx 全局配置、多模块牵连,除非用户确认或不做会明显错误/不安全。

+ 11 - 0
AGENTS.md

@@ -0,0 +1,11 @@
+# 仓库协作约定(全员)
+
+本项目的 AI 辅助编码与协作约定以 **[`.cursor/rules/collaboration-scope.mdc`](.cursor/rules/collaboration-scope.mdc)** 为准;该规则在 Cursor 中已设为 **`alwaysApply: true`**。
+
+**全体团队成员**在本仓库开发时均应遵守,包括但不限于:
+
+- 动手改代码前先列改动清单,**经确认后再改**  
+- 改动若**波及其他功能模块**,须单独写出跨模块影响并**等待决策**  
+- 小问题**最小改动**,非必要不大重构;更大方案以**建议**形式交负责人决策  
+
+说明与索引:[`.cursor/rules/README.md`](.cursor/rules/README.md)

+ 2 - 2
Web/.env.development

@@ -7,5 +7,5 @@ VITE_API_URL=http://localhost:5005
 # 登陆界面默认用户(账号登录页打开时预填)
 VITE_DEFAULT_USER=Demo01
 
-# 登录页密码优先来自 /api/sysConfig/sysInfo 的平台参数「默认密码」;此处仅作接口失败时的备用
-VITE_DEFAULT_USER_PASSWORD=Admin.NET++010101
+# 登录页密码(留空则不预填;勿在生产写入真实密码)
+VITE_DEFAULT_USER_PASSWORD=

+ 2 - 2
Web/.env.production

@@ -1,8 +1,8 @@
 # 线上环境
 ENV=production
 
-# 线上环境接口地址
-VITE_API_URL=http://localhost:5005
+# 线上环境接口地址(与 nginx /prod-api 反代一致;勿用 localhost,公网访问会触发 PNA/CORS)
+VITE_API_URL=/prod-api
 
 # 登陆界面默认用户
 VITE_DEFAULT_USER=

+ 15 - 0
Web/src/utils/aidopMenuDisplay.ts

@@ -36,6 +36,7 @@ const AIDOP_COMPONENT_OVERRIDES: Record<string, string> = {
 
 type AMenu = Record<string, any>;
 
+/** 侧栏「智慧运营看板」下 10 个看板(九宫格+S1~S9);统一查询条件仅应出现在这些 component 对应页面 */
 const SMART_OPS_CHILDREN: Array<{ path: string; title: string; component: string; name: string }> = [
 	{ path: '/aidop/smart-ops/grid', title: '九宫格智慧运营看板', component: '/dashboard/home', name: 'aidopSmartOpsGrid' },
 	{ path: '/aidop/smart-ops/s1', title: 'S1产销协同看板', component: '/aidop/kanban/s1', name: 'aidopSmartOpsS1' },
@@ -131,9 +132,23 @@ function patchAidopCustomMenusIfMissing(routes: AMenu[] | undefined): void {
  * 递归覆盖 Ai-DOP 一级目录的 meta.title,供侧栏 / tagsView / 菜单搜索使用。
  * 智慧诊断、智慧运营看板以 sys_menu 为准;若租户/角色未关联到库中菜单,则仅补缺失 path,避免双份。
  */
+/** 强制智慧运营子菜单 component,避免库中仍为占位/错误路径时前端仍加载旧页 */
+function patchSmartOpsRouteComponents(nodes: AMenu[] | undefined): void {
+	if (!nodes?.length) return;
+	const byPath = Object.fromEntries(SMART_OPS_CHILDREN.map((c) => [c.path, c.component])) as Record<string, string>;
+	for (const item of nodes) {
+		const raw = item.path as string | undefined;
+		const p = raw ? (raw.startsWith('/') ? raw : `/${raw}`) : '';
+		const comp = p ? byPath[p] : undefined;
+		if (comp) item.component = comp;
+		if (item.children?.length) patchSmartOpsRouteComponents(item.children as AMenu[]);
+	}
+}
+
 export function patchAidopMenuTitles(routes: any[] | undefined): void {
 	if (!routes?.length) return;
 	patchAidopCustomMenusIfMissing(routes as AMenu[]);
+	patchSmartOpsRouteComponents(routes as AMenu[]);
 	for (const item of routes) {
 		const name = item.name as string | undefined;
 		if (name && AIDOP_DIRECTORY_TITLES[name]) {

+ 52 - 6
Web/src/views/aidop/api/kanbanData.ts

@@ -35,20 +35,66 @@ export interface ModuleDetailPayload {
 	alerts: Array<{ time: string; message: string; level: string }>;
 }
 
-export async function fetchHomeL1(tenantId = 1, factoryId = 1): Promise<HomeL1Row[]> {
-	const res = await service.get('/api/AidopKanban/home-l1', { params: { tenantId, factoryId } });
+/** 与 smartOpsBaseQuery.baseQueryToApiParams 对齐,多余参数后端可忽略 */
+export type KanbanExtraQuery = Record<string, string>;
+
+export interface SmartOpsFilterOptions {
+	products: string[];
+	orderNos: string[];
+	productionLines: string[];
+}
+
+export async function fetchSmartOpsFilterOptions(
+	tenantId = 1,
+	factoryId = 1
+): Promise<SmartOpsFilterOptions> {
+	try {
+		const res = await service.get('/api/AidopKanban/smart-ops-filter-options', {
+			params: { tenantId, factoryId },
+			headers: { 'X-Silent-Error': '1' },
+		});
+		const d = res.data ?? {};
+		return {
+			products: Array.isArray(d.products) ? d.products : [],
+			orderNos: Array.isArray(d.orderNos) ? d.orderNos : [],
+			productionLines: Array.isArray(d.productionLines) ? d.productionLines : [],
+		};
+	} catch {
+		return { products: [], orderNos: [], productionLines: [] };
+	}
+}
+
+export async function fetchHomeL1(
+	tenantId = 1,
+	factoryId = 1,
+	extra: KanbanExtraQuery = {}
+): Promise<HomeL1Row[]> {
+	const res = await service.get('/api/AidopKanban/home-l1', {
+		params: { tenantId, factoryId, ...extra },
+	});
 	return (res.data ?? []) as HomeL1Row[];
 }
 
-export async function fetchS8Alerts(tenantId = 1, factoryId = 1): Promise<S8AlertRow[]> {
-	const res = await service.get('/api/AidopKanban/s8-alerts', { params: { tenantId, factoryId } });
+export async function fetchS8Alerts(
+	tenantId = 1,
+	factoryId = 1,
+	extra: KanbanExtraQuery = {}
+): Promise<S8AlertRow[]> {
+	const res = await service.get('/api/AidopKanban/s8-alerts', {
+		params: { tenantId, factoryId, ...extra },
+	});
 	return (res.data ?? []) as S8AlertRow[];
 }
 
-export async function fetchModuleDetail(moduleCode: string, tenantId = 1, factoryId = 1): Promise<ModuleDetailPayload> {
+export async function fetchModuleDetail(
+	moduleCode: string,
+	tenantId = 1,
+	factoryId = 1,
+	extra: KanbanExtraQuery = {}
+): Promise<ModuleDetailPayload> {
 	try {
 		const res = await service.get('/api/AidopKanban/module-detail', {
-			params: { moduleCode, tenantId, factoryId },
+			params: { moduleCode, tenantId, factoryId, ...extra },
 			headers: { 'X-Silent-Error': '1' },
 		});
 		return (res.data ?? { moduleCode, l2: [], l3: [], alerts: [] }) as ModuleDetailPayload;

+ 103 - 1
Web/src/views/aidop/kanban/components/DetailQueryBar.vue

@@ -1,5 +1,6 @@
 <template>
   <div class="detail-query-bar" :class="{ 'detail-query-bar--dark': dark }">
+    <div v-if="bandTitle" class="detail-query-bar__band">{{ bandTitle }}</div>
     <el-form :inline="true" size="small" class="detail-query-bar__form" @submit.prevent>
       <slot />
       <el-form-item class="detail-query-bar__actions">
@@ -13,14 +14,32 @@
 <script setup>
 defineProps({
   /** 深色详情页(S2/S4/S5/S6/S7 等)使用浅字+深底条 */
-  dark: { type: Boolean, default: false }
+  dark: { type: Boolean, default: false },
+  /** 智慧运营看板(九宫格+S1~S9)顶栏说明;其它场景请传空字符串或不展示 */
+  bandTitle: { type: String, default: '' },
 })
 
 defineEmits(['query', 'reset'])
 </script>
 
 <style scoped>
+.detail-query-bar__band {
+  font-size: 12px;
+  font-weight: 600;
+  color: #475569;
+  margin: -2px 0 8px;
+  padding-bottom: 8px;
+  border-bottom: 1px solid #e2e8f0;
+  letter-spacing: 0.02em;
+}
+
+.detail-query-bar--dark .detail-query-bar__band {
+  color: #e2e8f0;
+  border-bottom-color: #475569;
+}
+
 .detail-query-bar {
+  flex-shrink: 0;
   padding: 10px 14px;
   margin-bottom: 12px;
   background: rgba(255, 255, 255, 0.92);
@@ -57,4 +76,87 @@ defineEmits(['query', 'reset'])
 .detail-query-bar--dark :deep(.el-form-item__label) {
   color: #94a3b8;
 }
+
+.detail-query-bar--dark :deep(.smart-ops-base-fields__hint) {
+  color: #cbd5e1;
+}
+
+/* 深色条内:输入/日期/下拉内文字为浅色,占位符略灰 */
+.detail-query-bar--dark :deep(.el-input__inner),
+.detail-query-bar--dark :deep(.el-range-input) {
+  color: #f8fafc !important;
+}
+.detail-query-bar--dark :deep(.el-input__inner::placeholder),
+.detail-query-bar--dark :deep(.el-range-input::placeholder),
+.detail-query-bar--dark :deep(.el-range-separator) {
+  color: #64748b !important;
+}
+.detail-query-bar--dark :deep(.el-select__placeholder) {
+  color: #64748b;
+}
+.detail-query-bar--dark :deep(.el-select__selected-item),
+.detail-query-bar--dark :deep(.el-select__tags-text),
+.detail-query-bar--dark :deep(.el-select .el-input__inner) {
+  color: #f8fafc !important;
+}
+.detail-query-bar--dark :deep(.el-select__wrapper),
+.detail-query-bar--dark :deep(.el-select .el-input__wrapper) {
+  background: rgba(15, 23, 42, 0.85);
+  box-shadow: 0 0 0 1px #475569 inset;
+}
+.detail-query-bar--dark :deep(.el-select__caret) {
+  color: #94a3b8;
+}
+.detail-query-bar--dark :deep(.el-date-editor .el-range__icon) {
+  color: #94a3b8;
+}
+
+/* S1~S9 智慧运营:产品/订单号/产线 下拉与白底日期、客户输入框一致(见 SmartOpsBaseQueryFields 包装类) */
+.detail-query-bar--dark :deep(.smart-ops-base-fields--light-selects .el-select__wrapper) {
+  background-color: #ffffff !important;
+  box-shadow: 0 0 0 1px var(--el-border-color, #dcdfe6) inset;
+}
+.detail-query-bar--dark :deep(.smart-ops-base-fields--light-selects .el-select:hover:not(.is-disabled) .el-select__wrapper) {
+  box-shadow: 0 0 0 1px var(--el-border-color-hover, #c0c4cc) inset;
+}
+.detail-query-bar--dark :deep(.smart-ops-base-fields--light-selects .el-select.is-focused .el-select__wrapper) {
+  box-shadow: 0 0 0 1px var(--el-color-primary) inset;
+}
+.detail-query-bar--dark :deep(.smart-ops-base-fields--light-selects .el-select__placeholder) {
+  color: var(--el-text-color-placeholder, #a8abb2);
+}
+.detail-query-bar--dark :deep(.smart-ops-base-fields--light-selects .el-select__selected-item),
+.detail-query-bar--dark :deep(.smart-ops-base-fields--light-selects .el-select__tags-text),
+.detail-query-bar--dark :deep(.smart-ops-base-fields--light-selects .el-select .el-input__inner) {
+  color: var(--el-text-color-regular, #606266) !important;
+}
+.detail-query-bar--dark :deep(.smart-ops-base-fields--light-selects .el-select__caret) {
+  color: var(--el-text-color-secondary, #909399);
+}
+
+/* 同上区域内的日期范围:白底 + 深色字/占位符,与产品等下拉一致 */
+.detail-query-bar--dark :deep(.smart-ops-base-fields--light-selects .el-date-editor.el-input__wrapper),
+.detail-query-bar--dark :deep(.smart-ops-base-fields--light-selects .el-range-editor.el-input__wrapper) {
+  background-color: #ffffff !important;
+  box-shadow: 0 0 0 1px var(--el-border-color, #dcdfe6) inset;
+}
+.detail-query-bar--dark :deep(.smart-ops-base-fields--light-selects .el-date-editor:hover:not(.is-disabled) .el-input__wrapper),
+.detail-query-bar--dark :deep(.smart-ops-base-fields--light-selects .el-date-editor.el-input__wrapper:hover) {
+  box-shadow: 0 0 0 1px var(--el-border-color-hover, #c0c4cc) inset;
+}
+.detail-query-bar--dark :deep(.smart-ops-base-fields--light-selects .el-date-editor.el-input__wrapper.is-focus) {
+  box-shadow: 0 0 0 1px var(--el-color-primary) inset;
+}
+.detail-query-bar--dark :deep(.smart-ops-base-fields--light-selects .el-range-input) {
+  color: var(--el-text-color-regular, #606266) !important;
+}
+.detail-query-bar--dark :deep(.smart-ops-base-fields--light-selects .el-range-input::placeholder) {
+  color: var(--el-text-color-placeholder, #a8abb2) !important;
+}
+.detail-query-bar--dark :deep(.smart-ops-base-fields--light-selects .el-range-separator) {
+  color: var(--el-text-color-secondary, #909399) !important;
+}
+.detail-query-bar--dark :deep(.smart-ops-base-fields--light-selects .el-date-editor .el-range__icon) {
+  color: var(--el-text-color-secondary, #909399);
+}
 </style>

+ 118 - 0
Web/src/views/aidop/kanban/components/SmartOpsBaseQueryFields.vue

@@ -0,0 +1,118 @@
+<!--
+  仅用于「智慧运营看板」分组内:九宫格 + S1~S9 共 10 个看板页。
+  不要在 cockpit、S0 建模、智慧诊断、业务列表等页面引用。
+-->
+<template>
+  <!-- 仅 S1~S9 引用;白底下拉与深色条内的日期/其它输入框视觉对齐(九宫格不用本组件) -->
+  <div class="smart-ops-base-fields smart-ops-base-fields--light-selects">
+  <el-form-item v-if="!compact" label-width="0" class="smart-ops-base-fields__hint-row">
+    <span class="smart-ops-base-fields__hint">基础查询:日期、产品、订单号、产线</span>
+  </el-form-item>
+  <el-form-item label="日期">
+    <el-date-picker
+      :model-value="modelValue.dateRange"
+      type="daterange"
+      range-separator="至"
+      start-placeholder="开始"
+      end-placeholder="结束"
+      value-format="YYYY-MM-DD"
+      style="width: 240px"
+      @update:model-value="patch({ dateRange: $event })"
+    />
+  </el-form-item>
+  <el-form-item label="产品">
+    <el-select
+      :model-value="modelValue.product || undefined"
+      filterable
+      clearable
+      placeholder="全部"
+      style="width: 180px"
+      :loading="optionsLoading"
+      @update:model-value="patch({ product: $event == null ? '' : String($event) })"
+    >
+      <el-option v-for="p in productOptions" :key="'p-' + p" :label="p" :value="p" />
+    </el-select>
+  </el-form-item>
+  <el-form-item label="订单号">
+    <el-select
+      :model-value="modelValue.orderNo || undefined"
+      filterable
+      clearable
+      placeholder="全部"
+      style="width: 180px"
+      :loading="optionsLoading"
+      @update:model-value="patch({ orderNo: $event == null ? '' : String($event) })"
+    >
+      <el-option v-for="o in orderNoOptions" :key="'o-' + o" :label="o" :value="o" />
+    </el-select>
+  </el-form-item>
+  <el-form-item label="产线">
+    <el-select
+      :model-value="modelValue.productionLine || undefined"
+      filterable
+      clearable
+      placeholder="全部"
+      style="width: 160px"
+      :loading="optionsLoading"
+      @update:model-value="patch({ productionLine: $event == null ? '' : String($event) })"
+    >
+      <el-option v-for="l in productionLineOptions" :key="'l-' + l" :label="l" :value="l" />
+    </el-select>
+  </el-form-item>
+  </div>
+</template>
+
+<script setup>
+import { onMounted, ref } from 'vue'
+import { loadSmartOpsFilterOptions } from '../utils/smartOpsFilterOptionsCache'
+
+const props = defineProps({
+  modelValue: {
+    type: Object,
+    required: true,
+  },
+  /** 单行紧凑(九宫格顶栏等),不显示占满行的说明文字 */
+  compact: { type: Boolean, default: false },
+})
+
+const emit = defineEmits(['update:modelValue'])
+
+const optionsLoading = ref(true)
+const productOptions = ref([])
+const orderNoOptions = ref([])
+const productionLineOptions = ref([])
+
+function patch(p) {
+  emit('update:modelValue', { ...props.modelValue, ...p })
+}
+
+onMounted(async () => {
+  optionsLoading.value = true
+  try {
+    const d = await loadSmartOpsFilterOptions(1, 1)
+    productOptions.value = d.products ?? []
+    orderNoOptions.value = d.orderNos ?? []
+    productionLineOptions.value = d.productionLines ?? []
+  } finally {
+    optionsLoading.value = false
+  }
+})
+</script>
+
+<style scoped>
+.smart-ops-base-fields__hint-row {
+  margin-right: 0 !important;
+  margin-bottom: 2px !important;
+  width: 100%;
+  flex-basis: 100%;
+}
+.smart-ops-base-fields__hint-row :deep(.el-form-item__content) {
+  justify-content: flex-start;
+}
+.smart-ops-base-fields__hint {
+  font-size: 12px;
+  font-weight: 600;
+  color: #475569;
+  letter-spacing: 0.02em;
+}
+</style>

+ 61 - 39
Web/src/views/aidop/kanban/s1.vue

@@ -83,24 +83,16 @@
     </div>
 
     <!-- 业务筛选:产销协同以订单/客户维度为主 -->
-    <DetailQueryBar dark @query="onBizQuery" @reset="onBizReset">
-      <el-form-item label="统计日期">
-        <el-date-picker
-          v-model="bizQuery.dateRange"
-          type="daterange"
-          range-separator="至"
-          start-placeholder="开始日期"
-          end-placeholder="结束日期"
-          value-format="YYYY-MM-DD"
-          style="width: 240px"
-        />
-      </el-form-item>
+    <DetailQueryBar
+      dark
+      band-title="基础查询:日期、产品、订单号、产线"
+      @query="onBizQuery"
+      @reset="onBizReset"
+    >
+      <SmartOpsBaseQueryFields v-model="bizQuery" compact />
       <el-form-item label="客户">
         <el-input v-model="bizQuery.customer" placeholder="客户名称/编码" clearable style="width: 160px" />
       </el-form-item>
-      <el-form-item label="订单号">
-        <el-input v-model="bizQuery.orderNo" placeholder="销售订单号" clearable style="width: 160px" />
-      </el-form-item>
     </DetailQueryBar>
 
     <!-- 主内容区 -->
@@ -278,11 +270,18 @@
 </template>
 
 <script setup>
-import { ref, onMounted, nextTick } from 'vue'
+import { ref, computed, watch, onMounted, 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,
+} from './utils/smartOpsBaseQuery'
 import * as echarts from 'echarts'
 import { homeS1, loadHomeModuleMetrics } from './data/homeModulesSync'
 import { fetchModuleDetail } from '../api/kanbanData'
@@ -297,29 +296,56 @@ const autoRefresh = ref(true)
 const activeMode = ref('overview')
 const deptFilter = ref('all')
 const statusFilter = ref('all')
-const anomalyItems = ref([])
 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 donutSeries = ref([
-  { value: 12, name: '严重', itemStyle: { color: '#f87171' } },
-  { value: 8, name: '警告', itemStyle: { color: '#fbbf24' } },
-  { value: 24, name: '正常', itemStyle: { color: '#34d399' } },
-  { value: 6, name: '待机', itemStyle: { color: '#60a5fa' } }
-])
 
 const bizQuery = ref({
-  dateRange: null,
+  ...emptySmartOpsBaseQuery(),
   customer: '',
-  orderNo: ''
 })
 
+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 onBizQuery() {
-  ElMessage.success('已应用筛选条件')
+  ElMessage.success(`已应用筛选(${summarizeSmartOpsBaseQuery(bizQuery.value)})`)
 }
 
 function onBizReset() {
-  bizQuery.value = { dateRange: null, customer: '', orderNo: '' }
+  bizQuery.value = { ...emptySmartOpsBaseQuery(), customer: '' }
   ElMessage.info('已重置业务筛选')
 }
 
@@ -587,7 +613,7 @@ const initCharts = () => {
 onMounted(async () => {
   await loadHomeModuleMetrics()
   const detail = await fetchModuleDetail('S1')
-  anomalyItems.value = (detail.alerts ?? []).map((x) => ({
+  anomalyItemsAll.value = (detail.alerts ?? []).map((x) => ({
     tag: `[${String(x.level ?? 'info').toUpperCase()}]`,
     time: x.time ?? '--:--:--',
     message: x.message ?? '异常告警',
@@ -600,15 +626,6 @@ onMounted(async () => {
     barXAxis.value = l2.slice(0, 6).map((x, i) => x.metricName || `N${i + 1}`)
     barSeries.value = l2.slice(0, 6).map((x) => Math.max(0, Math.min(100, Number(x.metricValue ?? 0))))
   }
-  const c = anomalyItems.value.filter((x) => x.levelClass === 'critical').length
-  const w = anomalyItems.value.filter((x) => x.levelClass === 'warning').length
-  const i = anomalyItems.value.filter((x) => x.levelClass === 'info').length
-  donutSeries.value = [
-    { value: c, name: '严重', itemStyle: { color: '#f87171' } },
-    { value: w, name: '警告', itemStyle: { color: '#fbbf24' } },
-    { value: i, name: '正常', itemStyle: { color: '#34d399' } },
-    { value: Math.max(0, 6 - c - w - i), name: '待机', itemStyle: { color: '#60a5fa' } }
-  ]
   updateTime()
   setInterval(updateTime, 1000)
   initCharts()
@@ -618,10 +635,15 @@ onMounted(async () => {
 <style scoped>
 .s1-dashboard {
   box-sizing: border-box;
-  min-height: 100vh;
+  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: 32px;
+  padding-bottom: 24px;
   color: #e2e8f0;
 }
 

+ 42 - 31
Web/src/views/aidop/kanban/s2.vue

@@ -27,24 +27,13 @@
     </div>
 
     <!-- 排程维度:日期、订单、产线 -->
-    <DetailQueryBar dark @query="onDetailQuery" @reset="onDetailQueryReset">
-      <el-form-item label="排程日期">
-        <el-date-picker
-          v-model="detailQuery.dateRange"
-          type="daterange"
-          range-separator="至"
-          start-placeholder="开始"
-          end-placeholder="结束"
-          value-format="YYYY-MM-DD"
-          style="width: 240px"
-        />
-      </el-form-item>
-      <el-form-item label="订单号">
-        <el-input v-model="detailQuery.orderNo" placeholder="销售/生产订单" clearable style="width: 160px" />
-      </el-form-item>
-      <el-form-item label="产线">
-        <el-input v-model="detailQuery.line" placeholder="产线编码/名称" clearable style="width: 140px" />
-      </el-form-item>
+    <DetailQueryBar
+      dark
+      band-title="基础查询:日期、产品、订单号、产线"
+      @query="onDetailQuery"
+      @reset="onDetailQueryReset"
+    >
+      <SmartOpsBaseQueryFields v-model="detailQuery" compact />
     </DetailQueryBar>
 
     <!-- 第一行:核心指标(与首页 S2 模块数据一致,见 src/data/s2Kpis.js) -->
@@ -214,7 +203,7 @@
             </el-button>
           </div>
         </div>
-        <el-table :data="orderList" style="width: 100%" class="data-table">
+        <el-table :data="filteredOrderList" style="width: 100%" class="data-table">
           <el-table-column prop="orderNo" label="订单编号" width="180" />
           <el-table-column prop="customer" label="客户信息" width="200" />
           <el-table-column label="交付数量 期望交期" width="180">
@@ -252,10 +241,12 @@
 </template>
 
 <script setup>
-import { ref, onMounted, nextTick } from 'vue'
+import { ref, computed, onMounted, nextTick } from 'vue'
 import { useRouter } from 'vue-router'
 import { ElMessage } from 'element-plus'
 import DetailQueryBar from './components/DetailQueryBar.vue'
+import SmartOpsBaseQueryFields from './components/SmartOpsBaseQueryFields.vue'
+import { emptySmartOpsBaseQuery, rowMatchesSmartOpsBase, summarizeSmartOpsBaseQuery } from './utils/smartOpsBaseQuery'
 import {
   ArrowLeft, Refresh, Upload, Setting, Timer, CircleCheck,
   RefreshLeft, Cpu, Warning, TrendCharts, Histogram,
@@ -270,18 +261,14 @@ const currentTime = ref('')
 const layerView = ref('strategy')
 const searchText = ref('')
 
-const detailQuery = ref({
-  dateRange: null,
-  orderNo: '',
-  line: ''
-})
+const detailQuery = ref(emptySmartOpsBaseQuery())
 
 function onDetailQuery() {
-  ElMessage.success('已应用排程筛选')
+  ElMessage.success(`已应用排程筛选(${summarizeSmartOpsBaseQuery(detailQuery.value)})`)
 }
 
 function onDetailQueryReset() {
-  detailQuery.value = { dateRange: null, orderNo: '', line: '' }
+  detailQuery.value = emptySmartOpsBaseQuery()
   ElMessage.info('已重置')
 }
 
@@ -292,7 +279,24 @@ const s2DetailCardIcons = {
   labor: TrendCharts
 }
 
-const orderList = ref([])
+const orderListAll = ref([])
+
+const filteredOrderList = computed(() => {
+  const q = detailQuery.value
+  let rows = orderListAll.value.filter((r) =>
+    rowMatchesSmartOpsBase(q, {
+      date: r.deliveryDate,
+      product: r.product,
+      orderNo: r.orderNo,
+      productionLine: r.productionLine,
+    })
+  )
+  const t = searchText.value.trim().toLowerCase()
+  if (t) {
+    rows = rows.filter((r) => `${r.orderNo} ${r.customer}`.toLowerCase().includes(t))
+  }
+  return rows
+})
 const logItems = ref([])
 const alertTotal = ref(0)
 const alertCritical = ref(0)
@@ -484,12 +488,14 @@ const initCharts = () => {
 onMounted(async () => {
   await loadS2Kpis()
   const detail = await fetchModuleDetail('S2')
-  orderList.value = (detail.l3 ?? []).slice(0, 8).map((x, idx) => {
+  orderListAll.value = (detail.l3 ?? []).slice(0, 8).map((x, idx) => {
     const v = Number(x.metricValue ?? 0)
     const sat = Math.max(0, Math.min(100, Math.round(v)))
     return {
       orderNo: x.metricCode || `#S2-${idx + 1}`,
       customer: x.metricName || `S2 指标 ${idx + 1}`,
+      product: x.metricName || '',
+      productionLine: '',
       quantity: `${Math.max(1, Math.round(v * 100))} PCS`,
       deliveryDate: x.statDate ? String(x.statDate).slice(0, 10) : '--',
       status: sat >= 95 ? '已锁定' : sat >= 80 ? '排程中' : '资源异常',
@@ -528,10 +534,15 @@ onMounted(async () => {
 <style scoped>
 .s2-dashboard {
   box-sizing: border-box;
-  min-height: 100vh;
+  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: 32px;
+  padding-bottom: 24px;
 }
 
 /* 顶部导航栏 */

+ 39 - 22
Web/src/views/aidop/kanban/s3.vue

@@ -19,18 +19,13 @@
     </div>
 
     <!-- 供应计划:日期、物料、供应商 -->
-    <DetailQueryBar dark @query="onDetailQuery" @reset="onDetailQueryReset">
-      <el-form-item label="计划日期">
-        <el-date-picker
-          v-model="detailQuery.dateRange"
-          type="daterange"
-          range-separator="至"
-          start-placeholder="开始"
-          end-placeholder="结束"
-          value-format="YYYY-MM-DD"
-          style="width: 240px"
-        />
-      </el-form-item>
+    <DetailQueryBar
+      dark
+      band-title="基础查询:日期、产品、订单号、产线"
+      @query="onDetailQuery"
+      @reset="onDetailQueryReset"
+    >
+      <SmartOpsBaseQueryFields v-model="detailQuery" compact />
       <el-form-item label="物料">
         <el-input v-model="detailQuery.material" placeholder="物料编码/SKU" clearable style="width: 160px" />
       </el-form-item>
@@ -180,7 +175,7 @@
             <el-tag size="small" type="danger" class="critical-badge">3 条新增严重项</el-tag>
           </div>
           <div class="log-list">
-            <div v-for="(item, idx) in logItems" :key="`${item.time}-${idx}`" class="log-item" :class="item.levelClass">
+            <div v-for="(item, idx) in filteredLogItems" :key="`${item.time}-${idx}`" class="log-item" :class="item.levelClass">
               <span class="log-time">{{ item.time }}</span>
               <span class="log-tag">{{ item.tag }}</span>
               <span class="log-msg">{{ item.msg }}</span>
@@ -207,10 +202,17 @@
 </template>
 
 <script setup>
-import { ref, onMounted, nextTick } from 'vue'
+import { ref, computed, onMounted, nextTick } from 'vue'
 import { useRouter } from 'vue-router'
 import { ElMessage } from 'element-plus'
 import DetailQueryBar from './components/DetailQueryBar.vue'
+import SmartOpsBaseQueryFields from './components/SmartOpsBaseQueryFields.vue'
+import {
+  emptySmartOpsBaseQuery,
+  blobMatchesSmartOpsBase,
+  textMatches,
+  summarizeSmartOpsBaseQuery,
+} from './utils/smartOpsBaseQuery'
 import { Download, DataAnalysis, Timer, CircleCheck, User, Refresh, ArrowUp, ArrowDown } from '@element-plus/icons-vue'
 import * as echarts from 'echarts'
 import { homeS3 } from './data/homeModulesSync'
@@ -225,23 +227,33 @@ const s3TrendC = ref('8.1%')
 const s3TrendD = ref('2.3%')
 
 const detailQuery = ref({
-  dateRange: null,
+  ...emptySmartOpsBaseQuery(),
   material: '',
-  supplier: ''
+  supplier: '',
 })
 
 function onDetailQuery() {
-  ElMessage.success('已应用供应筛选')
+  ElMessage.success(`已应用供应筛选(${summarizeSmartOpsBaseQuery(detailQuery.value)})`)
 }
 
 function onDetailQueryReset() {
-  detailQuery.value = { dateRange: null, material: '', supplier: '' }
+  detailQuery.value = { ...emptySmartOpsBaseQuery(), material: '', supplier: '' }
   ElMessage.info('已重置')
 }
 
 let mrpTrendChart = null
 let drillChart = null
-const logItems = ref([])
+const logItemsAll = ref([])
+
+const filteredLogItems = computed(() =>
+  logItemsAll.value.filter((x) => {
+    const blob = `${x.msg} ${x.tag} ${x.time}`
+    if (!blobMatchesSmartOpsBase(detailQuery.value, blob)) return false
+    if (!textMatches(detailQuery.value.material, blob)) return false
+    if (!textMatches(detailQuery.value.supplier, blob)) return false
+    return true
+  })
+)
 const mrpXAxis = ref(['D1', 'D2', 'D3', 'D4', 'D5', 'D6', 'D7'])
 const drillXAxis = ref(['节点1', '节点2', '节点3', '节点4', '节点5', '节点6', '节点7'])
 const drillPrimary = ref([82, 80, 78, 76, 75, 74, 72])
@@ -362,7 +374,7 @@ const initCharts = () => {
 onMounted(async () => {
   await loadHomeModuleMetrics()
   const detail = await fetchModuleDetail('S3')
-  logItems.value = (detail.alerts ?? []).slice(0, 8).map((x) => ({
+  logItemsAll.value = (detail.alerts ?? []).slice(0, 8).map((x) => ({
     time: x.time ?? '--:--:--',
     tag: `[${String(x.level ?? 'info').toUpperCase()}]`,
     msg: x.message ?? '供应协同告警',
@@ -387,10 +399,15 @@ onMounted(async () => {
 <style scoped>
 .s3-dashboard {
   box-sizing: border-box;
-  min-height: 100vh;
+  flex: 1;
+  min-height: 0;
+  max-height: 100%;
+  overflow-x: hidden;
+  overflow-y: auto;
+  -webkit-overflow-scrolling: touch;
   background: #0b0f19;
   padding: 20px;
-  padding-bottom: 36px;
+  padding-bottom: 24px;
   color: #e2e8f0;
 }
 

+ 41 - 21
Web/src/views/aidop/kanban/s4.vue

@@ -26,18 +26,13 @@
     </div>
 
     <!-- 采购执行:日期、供应商、采购单 -->
-    <DetailQueryBar dark @query="onDetailQuery" @reset="onDetailQueryReset">
-      <el-form-item label="业务日期">
-        <el-date-picker
-          v-model="detailQuery.dateRange"
-          type="daterange"
-          range-separator="至"
-          start-placeholder="开始"
-          end-placeholder="结束"
-          value-format="YYYY-MM-DD"
-          style="width: 240px"
-        />
-      </el-form-item>
+    <DetailQueryBar
+      dark
+      band-title="基础查询:日期、产品、订单号、产线"
+      @query="onDetailQuery"
+      @reset="onDetailQueryReset"
+    >
+      <SmartOpsBaseQueryFields v-model="detailQuery" compact />
       <el-form-item label="供应商">
         <el-input v-model="detailQuery.supplier" placeholder="供应商编码/名称" clearable style="width: 160px" />
       </el-form-item>
@@ -211,7 +206,7 @@
           </div>
         </div>
         <div class="log-list">
-          <div v-for="(item, idx) in logItems" :key="`${item.time}-${idx}`" class="log-item" :class="item.levelClass">
+          <div v-for="(item, idx) in filteredLogItems" :key="`${item.time}-${idx}`" class="log-item" :class="item.levelClass">
             <span class="log-time">{{ item.time }}</span>
             <span class="log-message">{{ item.message }}</span>
             <el-tag size="small" :type="item.tagType">{{ item.tagText }}</el-tag>
@@ -223,10 +218,17 @@
 </template>
 
 <script setup>
-import { ref, onMounted, nextTick } from 'vue'
+import { ref, computed, onMounted, nextTick } from 'vue'
 import { useRouter } from 'vue-router'
 import { ElMessage } from 'element-plus'
 import DetailQueryBar from './components/DetailQueryBar.vue'
+import SmartOpsBaseQueryFields from './components/SmartOpsBaseQueryFields.vue'
+import {
+  emptySmartOpsBaseQuery,
+  blobMatchesSmartOpsBase,
+  textMatches,
+  summarizeSmartOpsBaseQuery,
+} from './utils/smartOpsBaseQuery'
 import {
   ArrowLeft, Download, Setting, Document, Connection,
   TrendCharts, PieChart, Bell
@@ -238,7 +240,18 @@ import { fetchModuleDetail } from '../api/kanbanData'
 
 const router = useRouter()
 const currentTime = ref('')
-const logItems = ref([])
+const logItemsAll = ref([])
+
+const filteredLogItems = computed(() =>
+  logItemsAll.value.filter((x) => {
+    const blob = `${x.message} ${x.time}`
+    if (!blobMatchesSmartOpsBase(detailQuery.value, blob)) return false
+    if (!textMatches(detailQuery.value.supplier, blob)) return false
+    if (!textMatches(detailQuery.value.poNo, blob)) return false
+    if (!textMatches(detailQuery.value.material, blob)) return false
+    return true
+  })
+)
 const s4LeftCycle = ref('3.0')
 const s4LeftEfficiency = ref('845')
 const s4RightCycle = ref('4.2')
@@ -247,18 +260,18 @@ const s4RightTurnover = ref('12.6')
 const trendXAxis = ref(['D1', 'D2', 'D3', 'D4', 'D5', 'D6', 'D7'])
 
 const detailQuery = ref({
-  dateRange: null,
+  ...emptySmartOpsBaseQuery(),
   supplier: '',
   poNo: '',
-  material: ''
+  material: '',
 })
 
 function onDetailQuery() {
-  ElMessage.success('已应用采购筛选')
+  ElMessage.success(`已应用采购筛选(${summarizeSmartOpsBaseQuery(detailQuery.value)})`)
 }
 
 function onDetailQueryReset() {
-  detailQuery.value = { dateRange: null, supplier: '', poNo: '', material: '' }
+  detailQuery.value = { ...emptySmartOpsBaseQuery(), supplier: '', poNo: '', material: '' }
   ElMessage.info('已重置')
 }
 let trendChart = null
@@ -423,7 +436,7 @@ const initCharts = () => {
 onMounted(async () => {
   await loadHomeModuleMetrics()
   const detail = await fetchModuleDetail('S4')
-  logItems.value = (detail.alerts ?? []).slice(0, 10).map((x) => ({
+  logItemsAll.value = (detail.alerts ?? []).slice(0, 10).map((x) => ({
     time: x.time ?? '--:--:--',
     message: x.message ?? '采购异常',
     levelClass: ['critical', 'high'].includes(String(x.level)) ? 'critical' : 'warning',
@@ -449,9 +462,16 @@ onMounted(async () => {
 
 <style scoped>
 .s4-dashboard {
-  min-height: 100vh;
+  box-sizing: border-box;
+  flex: 1;
+  min-height: 0;
+  max-height: 100%;
+  overflow-x: hidden;
+  overflow-y: auto;
+  -webkit-overflow-scrolling: touch;
   background: #0b0f19;
   padding: 15px;
+  padding-bottom: 24px;
   color: #e2e8f0;
 }
 

+ 21 - 17
Web/src/views/aidop/kanban/s5.vue

@@ -30,18 +30,13 @@
     </div>
 
     <!-- 仓储作业:日期、仓库、物料、工单(齐套/发运关联) -->
-    <DetailQueryBar dark @query="onDetailQuery" @reset="onDetailQueryReset">
-      <el-form-item label="业务日期">
-        <el-date-picker
-          v-model="detailQuery.dateRange"
-          type="daterange"
-          range-separator="至"
-          start-placeholder="开始"
-          end-placeholder="结束"
-          value-format="YYYY-MM-DD"
-          style="width: 240px"
-        />
-      </el-form-item>
+    <DetailQueryBar
+      dark
+      band-title="基础查询:日期、产品、订单号、产线"
+      @query="onDetailQuery"
+      @reset="onDetailQueryReset"
+    >
+      <SmartOpsBaseQueryFields v-model="detailQuery" compact />
       <el-form-item label="仓库">
         <el-input v-model="detailQuery.warehouse" placeholder="仓库/库区编码" clearable style="width: 140px" />
       </el-form-item>
@@ -424,6 +419,8 @@ import { ref, onMounted, nextTick } from 'vue'
 import { useRouter } from 'vue-router'
 import { ElMessage } from 'element-plus'
 import DetailQueryBar from './components/DetailQueryBar.vue'
+import SmartOpsBaseQueryFields from './components/SmartOpsBaseQueryFields.vue'
+import { emptySmartOpsBaseQuery, summarizeSmartOpsBaseQuery } from './utils/smartOpsBaseQuery'
 import {
   ArrowLeft, Search, Bell, Setting, Download, Refresh,
   Timer, CircleCheck, User, UserFilled, Folder, Checked,
@@ -444,18 +441,18 @@ const detailNums = ref([12.5, 0.34, 1.2, 2.4, 158, 12.2, 4.8, 94.5, 92, 8.4, 1.2
 const trendTexts = ref(['0.2h', '5%', '平稳', '1.2h', '2.1%', '2%', '0.5', '0.4h', '0.4%', '8%', '2.1', '0.5h', '0.7%', '持平', '0.3', '0.4d', '4.2%', '7%', '0.3'])
 
 const detailQuery = ref({
-  dateRange: null,
+  ...emptySmartOpsBaseQuery(),
   warehouse: '',
   material: '',
-  workOrder: ''
+  workOrder: '',
 })
 
 function onDetailQuery() {
-  ElMessage.success('已应用仓储筛选')
+  ElMessage.success(`已应用仓储筛选(${summarizeSmartOpsBaseQuery(detailQuery.value)})`)
 }
 
 function onDetailQueryReset() {
-  detailQuery.value = { dateRange: null, warehouse: '', material: '', workOrder: '' }
+  detailQuery.value = { ...emptySmartOpsBaseQuery(), warehouse: '', material: '', workOrder: '' }
   ElMessage.info('已重置')
 }
 
@@ -626,9 +623,16 @@ onMounted(async () => {
 
 <style scoped>
 .s5-dashboard {
-  min-height: 100vh;
+  box-sizing: border-box;
+  flex: 1;
+  min-height: 0;
+  max-height: 100%;
+  overflow-x: hidden;
+  overflow-y: auto;
+  -webkit-overflow-scrolling: touch;
   background: #0b0f19;
   padding: 20px;
+  padding-bottom: 24px;
   color: #e2e8f0;
 }
 

+ 21 - 25
Web/src/views/aidop/kanban/s6.vue

@@ -20,24 +20,13 @@
     </div>
 
     <!-- 生产执行:日期、订单、产线、设备 -->
-    <DetailQueryBar dark @query="onDetailQuery" @reset="onDetailQueryReset">
-      <el-form-item label="生产日期">
-        <el-date-picker
-          v-model="detailQuery.dateRange"
-          type="daterange"
-          range-separator="至"
-          start-placeholder="开始"
-          end-placeholder="结束"
-          value-format="YYYY-MM-DD"
-          style="width: 240px"
-        />
-      </el-form-item>
-      <el-form-item label="订单/工单">
-        <el-input v-model="detailQuery.orderNo" placeholder="生产订单/工单号" clearable style="width: 160px" />
-      </el-form-item>
-      <el-form-item label="产线">
-        <el-input v-model="detailQuery.line" placeholder="产线编码" clearable style="width: 130px" />
-      </el-form-item>
+    <DetailQueryBar
+      dark
+      band-title="基础查询:日期、产品、订单号、产线"
+      @query="onDetailQuery"
+      @reset="onDetailQueryReset"
+    >
+      <SmartOpsBaseQueryFields v-model="detailQuery" compact />
       <el-form-item label="设备">
         <el-input v-model="detailQuery.equipment" placeholder="设备编码/名称" clearable style="width: 140px" />
       </el-form-item>
@@ -369,6 +358,8 @@ import { ref, onMounted, nextTick } from 'vue'
 import { useRouter } from 'vue-router'
 import { ElMessage } from 'element-plus'
 import DetailQueryBar from './components/DetailQueryBar.vue'
+import SmartOpsBaseQueryFields from './components/SmartOpsBaseQueryFields.vue'
+import { emptySmartOpsBaseQuery, summarizeSmartOpsBaseQuery } from './utils/smartOpsBaseQuery'
 import {
   FullScreen, Setting, ArrowRight, Monitor, Warning,
   Box, TrendCharts, Download, Refresh
@@ -388,18 +379,16 @@ const detailNums = ref([86.1, 2.5, 18.4, 2.6, 2.2, 72.4, 8.2, 88, 82, 76])
 const trendTexts = ref(['-0.3', '-2.2%', '+0.6%', '-0.2', '-3.5', '-2.1', '-0.3'])
 
 const detailQuery = ref({
-  dateRange: null,
-  orderNo: '',
-  line: '',
-  equipment: ''
+  ...emptySmartOpsBaseQuery(),
+  equipment: '',
 })
 
 function onDetailQuery() {
-  ElMessage.success('已应用生产筛选(演示)')
+  ElMessage.success(`已应用生产筛选(${summarizeSmartOpsBaseQuery(detailQuery.value)})`)
 }
 
 function onDetailQueryReset() {
-  detailQuery.value = { dateRange: null, orderNo: '', line: '', equipment: '' }
+  detailQuery.value = { ...emptySmartOpsBaseQuery(), equipment: '' }
   ElMessage.info('已重置')
 }
 
@@ -637,9 +626,16 @@ onMounted(async () => {
 
 <style scoped>
 .s6-dashboard {
-  min-height: 100vh;
+  box-sizing: border-box;
+  flex: 1;
+  min-height: 0;
+  max-height: 100%;
+  overflow-x: hidden;
+  overflow-y: auto;
+  -webkit-overflow-scrolling: touch;
   background: #050810;
   padding: 15px;
+  padding-bottom: 24px;
   color: #e2e8f0;
 }
 

+ 21 - 21
Web/src/views/aidop/kanban/s7.vue

@@ -18,24 +18,16 @@
     </div>
 
     <!-- 成品出库:日期、客户、订单、出库单 -->
-    <DetailQueryBar dark @query="onDetailQuery" @reset="onDetailQueryReset">
-      <el-form-item label="业务日期">
-        <el-date-picker
-          v-model="detailQuery.dateRange"
-          type="daterange"
-          range-separator="至"
-          start-placeholder="开始"
-          end-placeholder="结束"
-          value-format="YYYY-MM-DD"
-          style="width: 240px"
-        />
-      </el-form-item>
+    <DetailQueryBar
+      dark
+      band-title="基础查询:日期、产品、订单号、产线"
+      @query="onDetailQuery"
+      @reset="onDetailQueryReset"
+    >
+      <SmartOpsBaseQueryFields v-model="detailQuery" compact />
       <el-form-item label="客户">
         <el-input v-model="detailQuery.customer" placeholder="客户名称/编码" clearable style="width: 140px" />
       </el-form-item>
-      <el-form-item label="订单号">
-        <el-input v-model="detailQuery.orderNo" placeholder="销售订单号" clearable style="width: 150px" />
-      </el-form-item>
       <el-form-item label="出库单号">
         <el-input v-model="detailQuery.outboundNo" placeholder="发货/出库单" clearable style="width: 150px" />
       </el-form-item>
@@ -299,6 +291,8 @@ import { ref, onMounted, nextTick } from 'vue'
 import { useRouter } from 'vue-router'
 import { ElMessage } from 'element-plus'
 import DetailQueryBar from './components/DetailQueryBar.vue'
+import SmartOpsBaseQueryFields from './components/SmartOpsBaseQueryFields.vue'
+import { emptySmartOpsBaseQuery, summarizeSmartOpsBaseQuery } from './utils/smartOpsBaseQuery'
 import { HomeFilled, Checked, Top, ShoppingBag, Van, ArrowUp, ArrowDown } from '@element-plus/icons-vue'
 import * as echarts from 'echarts'
 import { homeS7 } from './data/homeModulesSync'
@@ -309,18 +303,17 @@ const router = useRouter()
 const currentTime = ref('')
 
 const detailQuery = ref({
-  dateRange: null,
+  ...emptySmartOpsBaseQuery(),
   customer: '',
-  orderNo: '',
-  outboundNo: ''
+  outboundNo: '',
 })
 
 function onDetailQuery() {
-  ElMessage.success('已应用成品仓储筛选')
+  ElMessage.success(`已应用成品仓储筛选(${summarizeSmartOpsBaseQuery(detailQuery.value)})`)
 }
 
 function onDetailQueryReset() {
-  detailQuery.value = { dateRange: null, customer: '', orderNo: '', outboundNo: '' }
+  detailQuery.value = { ...emptySmartOpsBaseQuery(), customer: '', outboundNo: '' }
   ElMessage.info('已重置')
 }
 
@@ -507,9 +500,16 @@ onMounted(async () => {
 
 <style scoped>
 .s7-dashboard {
-  min-height: 100vh;
+  box-sizing: border-box;
+  flex: 1;
+  min-height: 0;
+  max-height: 100%;
+  overflow-x: hidden;
+  overflow-y: auto;
+  -webkit-overflow-scrolling: touch;
   background: #050810;
   padding: 15px;
+  padding-bottom: 24px;
   color: #e2e8f0;
 }
 

+ 76 - 17
Web/src/views/aidop/kanban/s8.vue

@@ -1,5 +1,8 @@
 <template>
-	<AidopDemoShell title="S8 异常监控看板" subtitle="移植 Demo 智慧运营首页中的 S8 异常监控能力">
+	<AidopDemoShell class="aidop-smart-ops-shell-fit" title="S8 异常监控看板" subtitle="移植 Demo 智慧运营首页中的 S8 异常监控能力">
+		<DetailQueryBar dark band-title="基础查询:日期、产品、订单号、产线" @query="onQuery" @reset="onReset">
+			<SmartOpsBaseQueryFields v-model="baseQuery" compact />
+		</DetailQueryBar>
 		<el-row :gutter="12">
 			<el-col :span="8" v-for="item in alarms" :key="item.title">
 				<el-card shadow="never" class="mini-card">
@@ -14,30 +17,86 @@
 </template>
 
 <script setup lang="ts" name="aidopKanbanS8">
-import { onMounted, ref } from 'vue';
+import { onMounted, ref, computed } from 'vue';
+import { ElMessage } from 'element-plus';
 import AidopDemoShell from '../components/AidopDemoShell.vue';
-import { fetchS8Alerts } from '../api/kanbanData';
+import { fetchS8Alerts, type S8AlertRow } from '../api/kanbanData';
+import DetailQueryBar from './components/DetailQueryBar.vue';
+import SmartOpsBaseQueryFields from './components/SmartOpsBaseQueryFields.vue';
+import {
+	emptySmartOpsBaseQuery,
+	blobMatchesSmartOpsBase,
+	baseQueryToApiParams,
+	summarizeSmartOpsBaseQuery,
+	type SmartOpsBaseQuery,
+} from './utils/smartOpsBaseQuery';
 
-const alarms = ref([
-	{ title: '今日预警数', value: '12', desc: '较昨日 +3', level: 'warn' },
-	{ title: '严重告警', value: '2', desc: '需 1h 内响应', level: 'danger' },
-	{ title: '闭环完成率', value: '86%', desc: '目标 95%', level: 'ok' },
-]);
+const baseQuery = ref<SmartOpsBaseQuery>(emptySmartOpsBaseQuery());
 
-onMounted(async () => {
+const rawList = ref<S8AlertRow[]>([]);
+
+const filteredAlerts = computed(() =>
+	rawList.value.filter((x) =>
+		blobMatchesSmartOpsBase(baseQuery.value, `${x.message ?? ''} ${x.module ?? ''} ${x.time ?? ''}`)
+	)
+);
+
+const alarms = computed(() => {
+	const list = filteredAlerts.value;
+	const critical = list.filter((x) => x.level === 'critical').length;
+	return [
+		{ title: '今日预警数', value: String(list.length || 0), desc: '来源数据库告警表', level: 'warn' },
+		{
+			title: '严重告警',
+			value: String(critical),
+			desc: '需 1h 内响应',
+			level: critical > 0 ? 'danger' : 'ok',
+		},
+		{
+			title: '闭环完成率',
+			value: list.length ? '86%' : '100%',
+			desc: '目标 95%',
+			level: list.length ? 'warn' : 'ok',
+		},
+	];
+});
+
+async function loadAlerts() {
 	try {
-		const list = await fetchS8Alerts(1, 1);
-		const critical = list.filter((x) => x.level === 'critical').length;
-		alarms.value = [
-			{ title: '今日预警数', value: String(list.length || 0), desc: '来源数据库告警表', level: 'warn' },
-			{ title: '严重告警', value: String(critical), desc: '需 1h 内响应', level: critical > 0 ? 'danger' : 'ok' },
-			{ title: '闭环完成率', value: list.length ? '86%' : '100%', desc: '目标 95%', level: list.length ? 'warn' : 'ok' },
-		];
-	} catch {}
+		const extra = baseQueryToApiParams(baseQuery.value);
+		rawList.value = await fetchS8Alerts(1, 1, extra);
+	} catch {
+		rawList.value = [];
+	}
+}
+
+function onQuery() {
+	loadAlerts();
+	ElMessage.success(`已查询(${summarizeSmartOpsBaseQuery(baseQuery.value)})`);
+}
+
+function onReset() {
+	baseQuery.value = emptySmartOpsBaseQuery();
+	loadAlerts();
+	ElMessage.info('已重置');
+}
+
+onMounted(() => {
+	loadAlerts();
 });
 </script>
 
 <style scoped>
+/* 填满主内容区,避免 min-height:100vh 撑出布局底部 */
+:deep(.aidop-smart-ops-shell-fit.aidop-demo-shell) {
+	flex: 1;
+	min-height: 0;
+	max-height: 100%;
+	overflow-x: hidden;
+	overflow-y: auto;
+	-webkit-overflow-scrolling: touch;
+}
+
 .mini-card { border: 1px solid #e2e8f0; }
 .mini-title { color: #64748b; font-size: 13px; }
 .mini-val { font-size: 26px; font-weight: 700; margin: 8px 0; }

+ 60 - 7
Web/src/views/aidop/kanban/s9.vue

@@ -1,5 +1,8 @@
 <template>
-	<AidopDemoShell title="S9 运营指标看板" subtitle="移植 Demo 智慧运营首页中的 S9 运营指标能力">
+	<AidopDemoShell class="aidop-smart-ops-shell-fit" title="S9 运营指标看板" subtitle="移植 Demo 智慧运营首页中的 S9 运营指标能力">
+		<DetailQueryBar dark band-title="基础查询:日期、产品、订单号、产线" @query="onQuery" @reset="onReset">
+			<SmartOpsBaseQueryFields v-model="baseQuery" compact />
+		</DetailQueryBar>
 		<el-table :data="rows" border stripe>
 			<el-table-column prop="metric" label="指标" min-width="180" />
 			<el-table-column prop="value" label="当前值" width="120" />
@@ -14,25 +17,75 @@
 </template>
 
 <script setup lang="ts" name="aidopKanbanS9">
-import { onMounted, ref } from 'vue';
+import { onMounted, ref, computed } from 'vue';
+import { ElMessage } from 'element-plus';
 import AidopDemoShell from '../components/AidopDemoShell.vue';
 import { fetchHomeL1 } from '../api/kanbanData';
+import DetailQueryBar from './components/DetailQueryBar.vue';
+import SmartOpsBaseQueryFields from './components/SmartOpsBaseQueryFields.vue';
+import {
+	emptySmartOpsBaseQuery,
+	blobMatchesSmartOpsBase,
+	baseQueryToApiParams,
+	summarizeSmartOpsBaseQuery,
+	type SmartOpsBaseQuery,
+} from './utils/smartOpsBaseQuery';
 
-const rows = ref([
+type Row = { metric: string; value: string; target: string; status: string };
+
+const baseQuery = ref<SmartOpsBaseQuery>(emptySmartOpsBaseQuery());
+
+const rowsAll = ref<Row[]>([
 	{ metric: 'OTD 交付率', value: '92%', target: '95%', status: '风险' },
 	{ metric: '设备 OEE', value: '81%', target: '80%', status: '达成' },
 	{ metric: '成品合格率', value: '97.5%', target: '98%', status: '未达成' },
 ]);
 
-onMounted(async () => {
+const rows = computed(() =>
+	rowsAll.value.filter((x) =>
+		blobMatchesSmartOpsBase(baseQuery.value, `${x.metric} ${x.value} ${x.target} ${x.status}`)
+	)
+);
+
+async function loadRows() {
 	try {
-		const list = await fetchHomeL1(1, 1);
-		rows.value = list.slice(0, 6).map((x) => ({
+		const extra = baseQueryToApiParams(baseQuery.value);
+		const list = await fetchHomeL1(1, 1, extra);
+		rowsAll.value = list.slice(0, 6).map((x) => ({
 			metric: `${x.moduleCode} - ${x.metricCode}`,
 			value: `${x.metricValue ?? 0}`,
 			target: `${x.targetValue ?? 0}`,
 			status: x.statusColor === 'green' ? '达成' : x.statusColor === 'yellow' ? '风险' : '未达成',
 		}));
-	} catch {}
+	} catch {
+		/* keep defaults */
+	}
+}
+
+function onQuery() {
+	loadRows();
+	ElMessage.success(`已查询(${summarizeSmartOpsBaseQuery(baseQuery.value)})`);
+}
+
+function onReset() {
+	baseQuery.value = emptySmartOpsBaseQuery();
+	loadRows();
+	ElMessage.info('已重置');
+}
+
+onMounted(() => {
+	loadRows();
 });
 </script>
+
+<style scoped>
+/* 填满主内容区,避免相对视口高度撑出主布局底部 */
+:deep(.aidop-smart-ops-shell-fit.aidop-demo-shell) {
+	flex: 1;
+	min-height: 0;
+	max-height: 100%;
+	overflow-x: hidden;
+	overflow-y: auto;
+	-webkit-overflow-scrolling: touch;
+}
+</style>

+ 85 - 0
Web/src/views/aidop/kanban/utils/smartOpsBaseQuery.ts

@@ -0,0 +1,85 @@
+/**
+ * 智慧运营看板 · 统一基础查询(日期 / 产品 / 订单号 / 产线)
+ *
+ * 使用范围仅限侧栏「智慧运营看板」下这 10 个页面,勿接到其它菜单/ Demo:
+ * - /aidop/smart-ops/grid(九宫格,dashboard/home)
+ * - /aidop/smart-ops/s1 … /aidop/smart-ops/s9(kanban/s1 … s9)
+ */
+
+export type SmartOpsBaseQuery = {
+	dateRange: [string, string] | null;
+	product: string;
+	orderNo: string;
+	productionLine: string;
+};
+
+export function emptySmartOpsBaseQuery(): SmartOpsBaseQuery {
+	return {
+		dateRange: null,
+		product: '',
+		orderNo: '',
+		productionLine: '',
+	};
+}
+
+export function textMatches(needle: string, haystack: string | undefined | null): boolean {
+	const n = needle?.trim();
+	if (!n) return true;
+	const h = String(haystack ?? '');
+	return h.toLowerCase().includes(n.toLowerCase());
+}
+
+/** 下拉选定值:与字段精确匹配(忽略大小写) */
+export function dimensionValueMatches(selected: string, actual: string | undefined | null): boolean {
+	const s = selected?.trim();
+	if (!s) return true;
+	return String(actual ?? '').trim().toLowerCase() === s.toLowerCase();
+}
+
+/** statDate / deliveryDate 等为 YYYY-MM-DD */
+export function dateInRange(dateStr: string | undefined | null, range: [string, string] | null | undefined): boolean {
+	if (!range || !range[0] || !range[1]) return true;
+	const d = String(dateStr ?? '').slice(0, 10);
+	if (!d || d.length < 10) return true;
+	return d >= range[0] && d <= range[1];
+}
+
+export function rowMatchesSmartOpsBase(
+	q: SmartOpsBaseQuery,
+	fields: { date?: string; product?: string; orderNo?: string; productionLine?: string }
+): boolean {
+	if (!dateInRange(fields.date, q.dateRange)) return false;
+	if (!dimensionValueMatches(q.product, fields.product)) return false;
+	if (!dimensionValueMatches(q.orderNo, fields.orderNo)) return false;
+	if (!dimensionValueMatches(q.productionLine, fields.productionLine)) return false;
+	return true;
+}
+
+/** 告警/日志等仅有文本时使用 */
+export function blobMatchesSmartOpsBase(q: SmartOpsBaseQuery, blob: string): boolean {
+	if (!textMatches(q.product, blob)) return false;
+	if (!textMatches(q.orderNo, blob)) return false;
+	if (!textMatches(q.productionLine, blob)) return false;
+	return true;
+}
+
+/** 转为接口 query(后端可忽略未实现维度) */
+export function baseQueryToApiParams(q: SmartOpsBaseQuery): Record<string, string> {
+	const o: Record<string, string> = {};
+	if (q.dateRange?.[0]) o.dateStart = q.dateRange[0];
+	if (q.dateRange?.[1]) o.dateEnd = q.dateRange[1];
+	if (q.product?.trim()) o.product = q.product.trim();
+	if (q.orderNo?.trim()) o.orderNo = q.orderNo.trim();
+	if (q.productionLine?.trim()) o.productionLine = q.productionLine.trim();
+	return o;
+}
+
+/** 用于 ElMessage 等提示当前基础查询内容 */
+export function summarizeSmartOpsBaseQuery(q: SmartOpsBaseQuery): string {
+	const parts: string[] = [];
+	if (q.dateRange?.[0] && q.dateRange?.[1]) parts.push(`日期 ${q.dateRange[0]}~${q.dateRange[1]}`);
+	if (q.product?.trim()) parts.push(`产品 ${q.product.trim()}`);
+	if (q.orderNo?.trim()) parts.push(`订单号 ${q.orderNo.trim()}`);
+	if (q.productionLine?.trim()) parts.push(`产线 ${q.productionLine.trim()}`);
+	return parts.length ? parts.join(';') : '未填写条件(展示全部)';
+}

+ 18 - 0
Web/src/views/aidop/kanban/utils/smartOpsFilterOptionsCache.ts

@@ -0,0 +1,18 @@
+import { fetchSmartOpsFilterOptions, type SmartOpsFilterOptions } from '../../api/kanbanData';
+
+let cache: Promise<SmartOpsFilterOptions> | null = null;
+
+/** 智慧运营基础查询下拉选项(多页共用一次请求) */
+export function loadSmartOpsFilterOptions(
+	tenantId = 1,
+	factoryId = 1
+): Promise<SmartOpsFilterOptions> {
+	if (!cache) {
+		cache = fetchSmartOpsFilterOptions(tenantId, factoryId);
+	}
+	return cache;
+}
+
+export function clearSmartOpsFilterOptionsCache() {
+	cache = null;
+}

+ 179 - 21
Web/src/views/dashboard/home.vue

@@ -20,19 +20,66 @@
       </div>
     </header>
 
-    <!-- 工具条:日期 + 图例 -->
-    <div class="top-bar">
-      <div class="top-bar__left">
-        <span class="field-label">日期选择</span>
-        <el-date-picker
-          v-model="selectedDate"
-          type="date"
-          placeholder="选择日期"
-          value-format="YYYY-MM-DD"
-          size="small"
-          class="date-picker"
-        />
-      </div>
+    <!-- 与旧版一致:深色顶栏一行内 = 日期/产品/订单号/产线 + 查询重置 + 图例(避免白底条在暗色页上不易察觉或被布局压缩) -->
+    <div class="top-bar top-bar--filters-row">
+      <DetailQueryBar
+        dark
+        band-title="基础查询:日期、产品、订单号、产线"
+        class="dashboard-query-bar dashboard-query-bar--embedded"
+        @query="onDashboardQuery"
+        @reset="onDashboardReset"
+      >
+        <el-form-item label="日期">
+          <el-date-picker
+            v-model="dashboardBaseQuery.dateRange"
+            type="daterange"
+            range-separator="至"
+            start-placeholder="开始"
+            end-placeholder="结束"
+            value-format="YYYY-MM-DD"
+            style="width: 240px"
+          />
+        </el-form-item>
+        <el-form-item label="产品">
+          <el-select
+            :model-value="dashboardBaseQuery.product || undefined"
+            filterable
+            clearable
+            placeholder="全部"
+            style="width: 180px"
+            :loading="filterOptionsLoading"
+            @update:model-value="(v) => (dashboardBaseQuery.product = v == null ? '' : String(v))"
+          >
+            <el-option v-for="p in filterProductOptions" :key="'fp-' + p" :label="p" :value="p" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="订单号">
+          <el-select
+            :model-value="dashboardBaseQuery.orderNo || undefined"
+            filterable
+            clearable
+            placeholder="全部"
+            style="width: 180px"
+            :loading="filterOptionsLoading"
+            @update:model-value="(v) => (dashboardBaseQuery.orderNo = v == null ? '' : String(v))"
+          >
+            <el-option v-for="o in filterOrderNoOptions" :key="'fo-' + o" :label="o" :value="o" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="产线">
+          <el-select
+            :model-value="dashboardBaseQuery.productionLine || undefined"
+            filterable
+            clearable
+            placeholder="全部"
+            style="width: 160px"
+            :loading="filterOptionsLoading"
+            @update:model-value="(v) => (dashboardBaseQuery.productionLine = v == null ? '' : String(v))"
+          >
+            <el-option v-for="l in filterLineOptions" :key="'fl-' + l" :label="l" :value="l" />
+          </el-select>
+        </el-form-item>
+      </DetailQueryBar>
       <div class="top-bar__legend">
         <span class="legend-item"><i class="dot dot--green" />达成目标</span>
         <span class="legend-item"><i class="dot dot--yellow" />部分达成</span>
@@ -212,7 +259,7 @@
             </div>
             <div class="s8-alarm-list">
               <div
-                v-for="(log, idx) in s8TopAlarms"
+                v-for="(log, idx) in displayS8Alarms"
                 :key="idx"
                 class="s8-log-item"
                 :class="[
@@ -443,7 +490,8 @@
 </template>
 
 <script setup>
-import { ref, onMounted, onUnmounted, nextTick } from 'vue'
+import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
+import { ElMessage } from 'element-plus'
 import { useRouter } from 'vue-router'
 import {
   Van,
@@ -460,6 +508,14 @@ import {
 import * as echarts from 'echarts'
 import { s2HomeKpiList, loadS2Kpis } from './data/s2Kpis'
 import { fetchHomeL1, fetchS8Alerts } from '../aidop/api/kanbanData'
+import { loadSmartOpsFilterOptions } from '../aidop/kanban/utils/smartOpsFilterOptionsCache'
+import DetailQueryBar from '../aidop/kanban/components/DetailQueryBar.vue'
+import {
+  emptySmartOpsBaseQuery,
+  baseQueryToApiParams,
+  blobMatchesSmartOpsBase,
+  summarizeSmartOpsBaseQuery,
+} from '../aidop/kanban/utils/smartOpsBaseQuery'
 import {
   homeS1,
   homeS3,
@@ -472,7 +528,11 @@ import {
 
 const router = useRouter()
 const gridMainRef = ref(null)
-const selectedDate = ref(new Date().toISOString().slice(0, 10))
+const dashboardBaseQuery = ref(emptySmartOpsBaseQuery())
+const filterOptionsLoading = ref(true)
+const filterProductOptions = ref([])
+const filterOrderNoOptions = ref([])
+const filterLineOptions = ref([])
 const dashboardVersion = `V${__NEXT_VERSION__}`
 
 /** 顶栏时间(与参考图格式一致:YYYY-MM-DD HH:mm:ss) */
@@ -497,16 +557,37 @@ const s8ModuleStatus = [
 ]
 
 /** TOP 报警列表(样式参考 LIVE LOG) */
-const s8TopAlarms = ref([
+const s8TopAlarmsAll = ref([
   { time: '14:28', module: 'S2', message: '机台 A-04 停机超时', level: 'critical', levelLabel: '严重' },
   { time: '14:25', module: 'S4', message: '物料 P-99 缺料预警', level: 'high', levelLabel: '高' },
   { time: '14:20', module: 'S6', message: '产线 03 效率低于 70%', level: 'medium', levelLabel: '中' },
-  { time: '14:15', module: 'S2', message: '排程逻辑冲突 02', level: 'high', levelLabel: '高' }
+  { time: '14:15', module: 'S2', message: '排程逻辑冲突 02', level: 'high', levelLabel: '高' },
 ])
 
+const displayS8Alarms = computed(() => {
+  const q = dashboardBaseQuery.value
+  return s8TopAlarmsAll.value.filter((x) =>
+    blobMatchesSmartOpsBase(q, `${x.message ?? ''} ${x.module ?? ''} ${x.time ?? ''}`)
+  )
+})
+
+function onDashboardQuery() {
+  loadKanbanData()
+  ElMessage.success(
+    `已查询:${summarizeSmartOpsBaseQuery(dashboardBaseQuery.value)}。下方 KPI 为汇总口径;S8 报警条会随条件过滤。`
+  )
+}
+
+function onDashboardReset() {
+  dashboardBaseQuery.value = emptySmartOpsBaseQuery()
+  loadKanbanData()
+  ElMessage.info('已重置查询条件')
+}
+
 async function loadKanbanData() {
+  const extra = baseQueryToApiParams(dashboardBaseQuery.value)
   try {
-    const l1 = await fetchHomeL1(1, 1)
+    const l1 = await fetchHomeL1(1, 1, extra)
     const byModule = Object.fromEntries(l1.map((x) => [x.moduleCode, x]))
     if (byModule.S1) {
       homeS1.reviewSatisfactionPct = Number(byModule.S1.metricValue || homeS1.reviewSatisfactionPct)
@@ -535,8 +616,8 @@ async function loadKanbanData() {
   } catch {}
 
   try {
-    const alerts = await fetchS8Alerts(1, 1)
-    if (alerts?.length) s8TopAlarms.value = alerts
+    const alerts = await fetchS8Alerts(1, 1, extra)
+    if (alerts?.length) s8TopAlarmsAll.value = alerts
   } catch {}
 }
 
@@ -641,6 +722,15 @@ function onResize() {
 }
 
 onMounted(async () => {
+  filterOptionsLoading.value = true
+  try {
+    const dim = await loadSmartOpsFilterOptions(1, 1)
+    filterProductOptions.value = dim.products ?? []
+    filterOrderNoOptions.value = dim.orderNos ?? []
+    filterLineOptions.value = dim.productionLines ?? []
+  } finally {
+    filterOptionsLoading.value = false
+  }
   await loadHomeModuleMetrics()
   await loadS2Kpis()
   loadKanbanData()
@@ -801,6 +891,74 @@ onUnmounted(() => {
   border-radius: 10px;
 }
 
+.dashboard-query-bar {
+  flex-shrink: 0;
+  width: 100%;
+  margin-bottom: 8px;
+}
+
+/* 嵌入九宫格顶栏:与背景融为一体,保证不被 flex 挤扁 */
+.top-bar--filters-row {
+  align-items: flex-start;
+  flex-shrink: 0;
+  min-height: 56px;
+  overflow: visible;
+  position: relative;
+  z-index: 3;
+}
+
+.dashboard-query-bar--embedded.detail-query-bar {
+  flex: 1 1 auto;
+  min-width: 280px;
+  min-height: 48px;
+  margin-bottom: 0;
+  background: transparent;
+  border: none;
+  padding: 4px 0;
+  box-shadow: none;
+}
+
+.top-bar--filters-row .top-bar__legend {
+  flex-shrink: 0;
+}
+
+/* 顶栏内日期、输入框与暗色一致 */
+.dashboard-query-bar--embedded :deep(.el-input__wrapper),
+.dashboard-query-bar--embedded :deep(.el-range-editor.el-input__wrapper),
+.dashboard-query-bar--embedded :deep(.el-select__wrapper) {
+  background: rgba(15, 23, 42, 0.85);
+  box-shadow: 0 0 0 1px #475569 inset;
+}
+
+.dashboard-query-bar--embedded :deep(.el-input__inner),
+.dashboard-query-bar--embedded :deep(.el-range-input) {
+  color: #f8fafc !important;
+}
+.dashboard-query-bar--embedded :deep(.el-input__inner::placeholder),
+.dashboard-query-bar--embedded :deep(.el-range-input::placeholder),
+.dashboard-query-bar--embedded :deep(.el-range-separator) {
+  color: #64748b !important;
+}
+.dashboard-query-bar--embedded :deep(.el-select__placeholder) {
+  color: #64748b;
+}
+.dashboard-query-bar--embedded :deep(.el-select__selected-item),
+.dashboard-query-bar--embedded :deep(.el-select .el-input__inner) {
+  color: #f8fafc !important;
+}
+.dashboard-query-bar--embedded :deep(.el-select__caret) {
+  color: #94a3b8;
+}
+.dashboard-query-bar--embedded :deep(.el-date-editor .el-range__icon) {
+  color: #94a3b8;
+}
+
+.dashboard-query-bar--embedded :deep(.detail-query-bar__band) {
+  margin: 0 0 6px;
+  padding-bottom: 6px;
+  font-size: 11px;
+}
+
 .top-bar__left {
   display: flex;
   align-items: center;

+ 4 - 4
docker/.env.production

@@ -1,5 +1,5 @@
-# 线上环境
-ENV = 'production'
+# 线上环境(docker web-builder 挂载覆盖 Web/.env.production,勿在 key/value 两侧加空格)
+ENV=production
 
-# 线上环境接口地址
-VITE_API_URL = '/prod-api'
+# 与 docker/nginx 中 /prod-api 反代一致
+VITE_API_URL=/prod-api

+ 1 - 1
docker/app/Configuration/App.json

@@ -10,7 +10,7 @@
     "InjectSpecificationDocument": true // 生产环境是否开启Swagger
   },
   "DynamicApiControllerSettings": {
-    //"DefaultRoutePrefix": "api", // 默认路由前缀
+    "DefaultRoutePrefix": "api",
     "CamelCaseSeparator": "", // 驼峰命名分隔符
     "SplitCamelCase": false, // 切割骆驼(驼峰)/帕斯卡命名
     "LowercaseRoute": false, // 小写路由格式

+ 7 - 5
docker/docker-compose.yml

@@ -12,6 +12,8 @@ services:
       - ../Web/dist:/usr/share/nginx/html
       - "./nginx/conf/nginx.conf:/etc/nginx/nginx.conf:ro"
       - "./nginx/key:/etc/nginx/key/"
+    depends_on:
+      - adminNet
     links:
       - adminNet
   mysql:
@@ -42,7 +44,7 @@ services:
     image: 'redis:latest' # 使用最新版本的 Redis 镜像,也可以指定特定版本如 'redis:6.2.7'
     container_name: my-redis # 自定义容器名称
     ports:
-      - '6379:6379' # 映射宿主机的 6379 端口到容器的 6379 端口
+      - '9137:6379' # 宿主机 9137 → 容器 6379(避免与宿主机已有 redis 占用的 6379 冲突)
     volumes: # 持久化数据
       - ./redis/redis.conf:/usr/local/etc/redis/redis.conf
       - ./redis/data:/data:rw
@@ -80,7 +82,7 @@ services:
 
 
     ports:
-     - 6030:6030
+     - 16030:6030 # 宿主机若已有 taosd 占用 6030,映射到 16030 避免冲突
      - 6041:6041
      - 6044-6049:6044-6049
      - 6044-6045:6044-6045/udp
@@ -92,13 +94,13 @@ services:
 
 
   adminNet:
-    image: mcr.microsoft.com/dotnet/aspnet:9.0
+    image: mcr.microsoft.com/dotnet/aspnet:10.0
     ports:
-      - "9102:5050"
+      - "9102:5005" # 与 app/Configuration/App.json 中 Urls 端口一致
     environment:
       - TZ=Asia/Shanghai
     volumes:
-      - ../server/Admin.NET.Web.Entry/bin/Release/net9.0/:/app
+      - ../server/Admin.NET.Web.Entry/bin/Release/net10.0/:/app
       - ./app/Configuration/:/app/Configuration/
       - ./app/wait-for-it.sh:/app/wait-for-it.sh
     working_dir: /app

+ 1 - 1
server/Admin.NET.Application/Configuration/App.json

@@ -10,7 +10,7 @@
     "VirtualPath": "" // 二级虚拟目录
   },
   "DynamicApiControllerSettings": {
-    //"DefaultRoutePrefix": "api", // 默认路由前缀
+    "DefaultRoutePrefix": "api",
     "CamelCaseSeparator": "", // 驼峰命名分隔符
     "SplitCamelCase": false, // 切割骆驼(驼峰)/帕斯卡命名
     "LowercaseRoute": false, // 小写路由格式

+ 2 - 2
server/Admin.NET.Core/SeedData/SysRoleMenuSeedData.cs

@@ -47,8 +47,8 @@ public class SysRoleMenuSeedData : ISqlSugarEntitySeedData<SysRoleMenu>
                 u.Name == "aidopRoot" || string.Equals(u.Path?.Trim(), "/aidop", StringComparison.Ordinal));
             if (aidopRoot != null)
             {
-                var subtree = new List<SysMenu> { aidopRoot };
-                subtree.AddRange(allFlat.ToChildList(u => u.Id, u => u.Pid, aidopRoot.Id));
+                // ToChildList(..., topParentIdValue) 默认包含根节点,勿再手动 Add(aidopRoot),否则 SysRoleMenu 主键重复导致启动失败
+                var subtree = allFlat.ToChildList(u => u.Id, u => u.Pid, aidopRoot.Id).ToList();
                 foreach (var m in subtree)
                 {
                     roleMenuList.Add(new SysRoleMenu

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

@@ -1,3 +1,4 @@
+using Admin.NET.Plugin.AiDOP.Entity;
 using SqlSugar;
 
 namespace Admin.NET.Plugin.AiDOP.Controllers;
@@ -128,6 +129,95 @@ LIMIT 60
         });
     }
 
+    /// <summary>
+    /// 智慧运营看板基础查询下拉:产品、订单号、产线(来自 Demo 业务表;无租户列时忽略 tenant/factory)。
+    /// </summary>
+    [HttpGet("smart-ops-filter-options")]
+    public async Task<IActionResult> GetSmartOpsFilterOptions([FromQuery] long tenantId = 1, [FromQuery] long factoryId = 1)
+    {
+        _ = tenantId;
+        _ = factoryId;
+
+        var products = new HashSet<string>(StringComparer.Ordinal);
+        var orderNos = new HashSet<string>(StringComparer.Ordinal);
+        var lines = new HashSet<string>(StringComparer.Ordinal);
+
+        try
+        {
+            var op = await _db.Queryable<AdoOrder>()
+                .Where(x => x.Product != null && x.Product != "")
+                .Select(x => x.Product)
+                .Distinct()
+                .ToListAsync();
+            foreach (var s in op.Where(s => !string.IsNullOrWhiteSpace(s)))
+                products.Add(s.Trim());
+        }
+        catch
+        {
+            // ignored
+        }
+
+        try
+        {
+            var on = await _db.Queryable<AdoOrder>()
+                .Where(x => x.OrderNo != null && x.OrderNo != "")
+                .Select(x => x.OrderNo)
+                .Distinct()
+                .ToListAsync();
+            foreach (var s in on.Where(s => !string.IsNullOrWhiteSpace(s)))
+                orderNos.Add(s.Trim());
+        }
+        catch
+        {
+            // ignored
+        }
+
+        try
+        {
+            var woP = await _db.Queryable<AdoWorkOrder>()
+                .Where(x => x.Product != null && x.Product != "")
+                .Select(x => x.Product)
+                .Distinct()
+                .ToListAsync();
+            foreach (var s in woP.Where(s => !string.IsNullOrWhiteSpace(s)))
+                products.Add(s.Trim());
+
+            var wc = await _db.Queryable<AdoWorkOrder>()
+                .Where(x => x.WorkCenter != null && x.WorkCenter != "")
+                .Select(x => x.WorkCenter)
+                .Distinct()
+                .ToListAsync();
+            foreach (var s in wc.Where(s => !string.IsNullOrWhiteSpace(s)))
+                lines.Add(s.Trim());
+        }
+        catch
+        {
+            // ignored
+        }
+
+        try
+        {
+            var pn = await _db.Queryable<AdoPlan>()
+                .Where(x => x.ProductName != null && x.ProductName != "")
+                .Select(x => x.ProductName)
+                .Distinct()
+                .ToListAsync();
+            foreach (var s in pn.Where(s => !string.IsNullOrWhiteSpace(s)))
+                products.Add(s.Trim());
+        }
+        catch
+        {
+            // ignored
+        }
+
+        return Ok(new
+        {
+            products = products.OrderBy(x => x, StringComparer.Ordinal).ToList(),
+            orderNos = orderNos.OrderBy(x => x, StringComparer.Ordinal).ToList(),
+            productionLines = lines.OrderBy(x => x, StringComparer.Ordinal).ToList()
+        });
+    }
+
     private sealed class HomeL1Dto
     {
         public string? ModuleCode { get; set; }