Explorar o código

fix(kanban): unify dashboard layout and add fuzzy filter selects

Remove module extension blocks from S1-S9 detail pages; migrate S5-S9 to DynamicModuleDashboard. Add remote fuzzy-search filter API and SmartOpsFuzzySelect for all query fields.

chore: bump version Web 2.4.185 / server 1.0.160
skygu hai 2 meses
pai
achega
e91382a335

+ 1 - 1
Web/package.json

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

+ 31 - 0
Web/src/views/aidop/api/kanbanData.ts

@@ -325,6 +325,37 @@ export interface SmartOpsFilterOptions {
 	productionLines: string[];
 }
 
+export type SmartOpsFilterField =
+	| 'product'
+	| 'orderNo'
+	| 'productionLine'
+	| 'customer'
+	| 'supplier'
+	| 'material'
+	| 'poNo'
+	| 'warehouse'
+	| 'workOrder'
+	| 'equipment'
+	| 'outboundNo';
+
+export async function searchSmartOpsFilterOptions(
+	field: SmartOpsFilterField,
+	keyword = '',
+	factoryId = 1,
+	limit = 30
+): Promise<string[]> {
+	try {
+		const res = await service.get('/api/AidopKanban/smart-ops-filter-search', {
+			params: withAidopTenantParams({ factoryId, field, keyword: keyword.trim(), limit }),
+			headers: { 'X-Silent-Error': '1' },
+		});
+		const items = res.data?.items;
+		return Array.isArray(items) ? items.map((x) => String(x)) : [];
+	} catch {
+		return [];
+	}
+}
+
 export async function fetchSmartOpsFilterOptions(
 	factoryId = 1
 ): Promise<SmartOpsFilterOptions> {

+ 0 - 8
Web/src/views/aidop/kanban/components/DynamicModuleDashboard.vue

@@ -252,14 +252,6 @@
 				</div>
 			</section>
 		</div>
-		<slot
-			name="extension"
-			:module-code="moduleCode"
-			:page="page"
-			:base-query="baseQuery"
-			:extra-query="extraQuery"
-			:reload="reload"
-		/>
 		<ChatBIFloat
 			v-if="moduleConfig.enableChatBI !== false"
 			:title="moduleConfig.chatTitle || `${moduleCode} 智慧运营智能问答`"

+ 0 - 264
Web/src/views/aidop/kanban/components/ModuleDetailExtension.vue

@@ -1,264 +0,0 @@
-<template>
-	<section class="module-extension">
-		<div class="module-extension__header">
-			<div>
-				<h2>{{ moduleCode }} 模块扩展数据</h2>
-				<p>来自统一 module-detail 接口的同步状态、L3 下钻、业务明细和告警。</p>
-			</div>
-			<div class="module-extension__actions">
-				<el-tag v-if="syncStatusText" size="small" :type="syncStatusType">{{ syncStatusText }}</el-tag>
-				<el-button size="small" :loading="loading" @click="loadDetail">刷新扩展区</el-button>
-			</div>
-		</div>
-
-		<div v-loading="loading" class="module-extension__body">
-			<div v-if="drillGroups.length" class="extension-card extension-card--wide">
-				<div class="extension-card__title">L3 指标下钻</div>
-				<div class="drill-grid">
-					<div v-for="group in drillGroups" :key="group.title" class="drill-block">
-						<div class="drill-block__title">{{ group.title }}</div>
-						<div class="drill-items">
-							<div v-for="item in group.items" :key="item.metricCode" class="drill-item" :class="`tone-${item.statusColor || 'gray'}`">
-								<span>{{ item.metricName || item.metricCode }}</span>
-								<strong>{{ formatValue(item.metricValue) }}</strong>
-								<small v-if="item.targetValue != null">目标 {{ formatValue(item.targetValue) }}</small>
-							</div>
-						</div>
-					</div>
-				</div>
-			</div>
-
-			<div v-if="branchKpis.length" class="extension-card extension-card--wide">
-				<div class="extension-card__title">供应分支 KPI</div>
-				<div class="branch-grid">
-					<div v-for="group in branchKpis" :key="group.branch" class="branch-block">
-						<div class="drill-block__title">{{ group.title }}</div>
-						<div v-for="item in group.items" :key="item.label" class="branch-row">
-							<span>{{ item.label }}</span>
-							<strong>{{ formatValue(item.value) }}{{ item.unit || '' }}</strong>
-						</div>
-					</div>
-				</div>
-			</div>
-
-			<div v-if="schedules.length" class="extension-card extension-card--wide">
-				<div class="extension-card__title">排程明细</div>
-				<el-table :data="schedules" size="small" max-height="260" border>
-					<el-table-column prop="orderNo" label="订单/工单" min-width="130" />
-					<el-table-column prop="product" label="产品" min-width="160" />
-					<el-table-column prop="productionLine" label="产线" min-width="120" />
-					<el-table-column prop="quantity" label="数量" width="90" />
-					<el-table-column prop="deliveryDate" label="交期" min-width="120" />
-					<el-table-column prop="status" label="状态" width="110" />
-				</el-table>
-			</div>
-
-			<div class="extension-card">
-				<div class="extension-card__title">异常与告警</div>
-				<div v-if="alerts.length" class="alert-list">
-					<div v-for="(alert, idx) in alerts" :key="`${alert.time}-${idx}`" class="alert-row" :class="`level-${alert.level || 'info'}`">
-						<span>{{ alert.time || '--:--:--' }}</span>
-						<strong>{{ String(alert.level || 'info').toUpperCase() }}</strong>
-						<p>{{ alert.message }}</p>
-					</div>
-				</div>
-				<div v-else class="empty-text">暂无告警</div>
-			</div>
-		</div>
-	</section>
-</template>
-
-<script setup lang="ts">
-import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
-import { fetchModuleDetail, type ModuleDetailPayload } from '../../api/kanbanData';
-import { AIDOP_LAYOUT_SAVED } from '../utils/s4LayoutEvents';
-
-const props = defineProps<{ moduleCode: string }>();
-
-const loading = ref(false);
-const detail = ref<ModuleDetailPayload | null>(null);
-const moduleCode = computed(() => String(props.moduleCode || '').toUpperCase());
-const drillGroups = computed(() => detail.value?.drillGroups || []);
-const branchKpis = computed(() => detail.value?.branchKpis || []);
-const schedules = computed(() => detail.value?.schedules || []);
-const alerts = computed(() => detail.value?.alerts || []);
-
-const syncStatusText = computed(() => {
-	const s = detail.value?.syncStatus;
-	if (!s) return '';
-	const rows = [s.stageRows, s.standardRows, s.dwdRows].filter((x) => x != null).join('/');
-	return `MDP ${s.status || 'UNKNOWN'}${rows ? ` (${rows})` : ''}`;
-});
-
-const syncStatusType = computed(() => {
-	const status = String(detail.value?.syncStatus?.status || '').toUpperCase();
-	if (status === 'SUCCESS') return 'success';
-	if (status === 'RUNNING') return 'warning';
-	if (status === 'FAILED') return 'danger';
-	return 'info';
-});
-
-async function loadDetail() {
-	loading.value = true;
-	try {
-		detail.value = await fetchModuleDetail(moduleCode.value);
-	} finally {
-		loading.value = false;
-	}
-}
-
-function formatValue(value?: number | null) {
-	if (value == null || Number.isNaN(Number(value))) return '—';
-	const n = Number(value);
-	return Math.abs(n - Math.round(n)) < 1e-9 ? String(Math.round(n)) : String(Math.round(n * 100) / 100);
-}
-
-function onLayoutSaved(event: Event) {
-	const saved = (event as CustomEvent<{ moduleCode?: string }>).detail?.moduleCode;
-	if (!saved || saved.toUpperCase() === moduleCode.value) loadDetail();
-}
-
-onMounted(() => {
-	loadDetail();
-	window.addEventListener(AIDOP_LAYOUT_SAVED, onLayoutSaved as EventListener);
-});
-
-onUnmounted(() => {
-	window.removeEventListener(AIDOP_LAYOUT_SAVED, onLayoutSaved as EventListener);
-});
-
-watch(moduleCode, loadDetail);
-</script>
-
-<style scoped>
-.module-extension {
-	margin-top: 16px;
-	border: 1px solid #1e293b;
-	border-radius: 14px;
-	padding: 16px;
-	background: rgba(15, 23, 42, 0.72);
-}
-
-.module-extension__header {
-	display: flex;
-	justify-content: space-between;
-	gap: 12px;
-	align-items: center;
-	margin-bottom: 14px;
-}
-
-.module-extension__header h2 {
-	margin: 0;
-	font-size: 16px;
-	color: #f8fafc;
-}
-
-.module-extension__header p {
-	margin: 4px 0 0;
-	color: #94a3b8;
-	font-size: 12px;
-}
-
-.module-extension__actions {
-	display: flex;
-	gap: 8px;
-	align-items: center;
-}
-
-.module-extension__body {
-	display: grid;
-	grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
-	gap: 12px;
-}
-
-.extension-card {
-	border: 1px solid #263244;
-	border-radius: 10px;
-	padding: 12px;
-	background: rgba(2, 6, 23, 0.38);
-}
-
-.extension-card--wide {
-	grid-column: 1 / -1;
-}
-
-.extension-card__title,
-.drill-block__title {
-	font-size: 13px;
-	font-weight: 700;
-	color: #e2e8f0;
-	margin-bottom: 10px;
-}
-
-.drill-grid,
-.branch-grid {
-	display: grid;
-	grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
-	gap: 10px;
-}
-
-.drill-items {
-	display: flex;
-	flex-wrap: wrap;
-	gap: 8px;
-}
-
-.drill-item {
-	flex: 1 1 160px;
-	border: 1px solid #334155;
-	border-radius: 8px;
-	padding: 10px;
-	color: #cbd5e1;
-}
-
-.drill-item strong {
-	display: block;
-	margin-top: 6px;
-	font-size: 18px;
-	color: #f8fafc;
-}
-
-.drill-item small {
-	color: #64748b;
-}
-
-.tone-green { border-color: rgba(52, 211, 153, 0.45); }
-.tone-yellow { border-color: rgba(251, 191, 36, 0.45); }
-.tone-red { border-color: rgba(239, 68, 68, 0.45); }
-
-.branch-row,
-.alert-row {
-	display: flex;
-	align-items: center;
-	gap: 10px;
-	padding: 8px 0;
-	border-bottom: 1px solid rgba(51, 65, 85, 0.55);
-	color: #cbd5e1;
-}
-
-.branch-row span,
-.alert-row p {
-	flex: 1;
-	margin: 0;
-}
-
-.alert-row span {
-	color: #64748b;
-	font-family: Consolas, 'Courier New', monospace;
-}
-
-.alert-row strong {
-	color: #fbbf24;
-}
-
-.alert-row.level-critical strong,
-.alert-row.level-high strong,
-.alert-row.level-danger strong {
-	color: #f87171;
-}
-
-.empty-text {
-	color: #94a3b8;
-	font-size: 12px;
-}
-</style>

+ 19 - 49
Web/src/views/aidop/kanban/components/SmartOpsBaseQueryFields.vue

@@ -21,50 +21,37 @@
     />
   </el-form-item>
   <el-form-item label="产品">
-    <el-select
-      :model-value="modelValue.product || undefined"
-      filterable
-      clearable
+    <SmartOpsFuzzySelect
+      :model-value="modelValue.product"
+      field="product"
       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>
+      width="180px"
+      @update:model-value="patch({ product: $event })"
+    />
   </el-form-item>
   <el-form-item label="订单号">
-    <el-select
-      :model-value="modelValue.orderNo || undefined"
-      filterable
-      clearable
+    <SmartOpsFuzzySelect
+      :model-value="modelValue.orderNo"
+      field="orderNo"
       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>
+      width="180px"
+      @update:model-value="patch({ orderNo: $event })"
+    />
   </el-form-item>
   <el-form-item label="产线">
-    <el-select
-      :model-value="modelValue.productionLine || undefined"
-      filterable
-      clearable
+    <SmartOpsFuzzySelect
+      :model-value="modelValue.productionLine"
+      field="productionLine"
       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>
+      width="160px"
+      @update:model-value="patch({ productionLine: $event })"
+    />
   </el-form-item>
   </div>
 </template>
 
 <script setup>
-import { onMounted, ref } from 'vue'
-import { loadSmartOpsFilterOptions } from '../utils/smartOpsFilterOptionsCache'
+import SmartOpsFuzzySelect from './SmartOpsFuzzySelect.vue'
 
 const props = defineProps({
   modelValue: {
@@ -77,26 +64,9 @@ const props = defineProps({
 
 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>

+ 72 - 0
Web/src/views/aidop/kanban/components/SmartOpsFuzzySelect.vue

@@ -0,0 +1,72 @@
+<template>
+	<el-select
+		:model-value="modelValue || undefined"
+		filterable
+		remote
+		reserve-keyword
+		clearable
+		:placeholder="placeholder"
+		:style="{ width }"
+		:loading="loading"
+		:remote-method="onRemoteSearch"
+		@visible-change="onVisibleChange"
+		@update:model-value="onSelect"
+	>
+		<el-option v-for="item in options" :key="field + '-' + item" :label="item" :value="item" />
+	</el-select>
+</template>
+
+<script setup lang="ts">
+import { onMounted, ref } from 'vue';
+import { searchSmartOpsFilterOptions, type SmartOpsFilterField } from '../../api/kanbanData';
+
+const props = withDefaults(
+	defineProps<{
+		modelValue?: string;
+		field: SmartOpsFilterField;
+		placeholder?: string;
+		width?: string;
+		factoryId?: number;
+		limit?: number;
+	}>(),
+	{
+		modelValue: '',
+		placeholder: '全部',
+		width: '180px',
+		factoryId: 1,
+		limit: 30,
+	}
+);
+
+const emit = defineEmits<{ 'update:modelValue': [value: string] }>();
+
+const options = ref<string[]>([]);
+const loading = ref(false);
+let searchTimer: ReturnType<typeof setTimeout> | undefined;
+let lastKeyword = '';
+
+async function loadOptions(keyword = '') {
+	loading.value = true;
+	lastKeyword = keyword;
+	try {
+		options.value = await searchSmartOpsFilterOptions(props.field, keyword, props.factoryId, props.limit);
+	} finally {
+		loading.value = false;
+	}
+}
+
+function onRemoteSearch(keyword: string) {
+	clearTimeout(searchTimer);
+	searchTimer = setTimeout(() => loadOptions(keyword), 280);
+}
+
+function onVisibleChange(visible: boolean) {
+	if (visible && options.value.length === 0) loadOptions(lastKeyword);
+}
+
+function onSelect(value: string | null | undefined) {
+	emit('update:modelValue', value == null ? '' : String(value));
+}
+
+onMounted(() => loadOptions(''));
+</script>

+ 2 - 1
Web/src/views/aidop/kanban/s1.vue

@@ -7,7 +7,7 @@
 	>
 		<template #extra-fields="{ query }">
 			<el-form-item label="客户">
-				<el-input v-model="query.customer" placeholder="客户名称/编码" clearable style="width: 160px" />
+				<SmartOpsFuzzySelect v-model="query.customer" field="customer" placeholder="客户名称/编码" width="160px" />
 			</el-form-item>
 		</template>
 	</DynamicModuleDashboard>
@@ -15,6 +15,7 @@
 
 <script setup lang="ts" name="aidopKanbanS1">
 import DynamicModuleDashboard from './components/DynamicModuleDashboard.vue';
+import SmartOpsFuzzySelect from './components/SmartOpsFuzzySelect.vue';
 
 const extraQueryInit = {
 	customer: '',

+ 1 - 6
Web/src/views/aidop/kanban/s2.vue

@@ -1,12 +1,7 @@
 <template>
-	<DynamicModuleDashboard module-code="S2">
-		<template #extension>
-			<ModuleDetailExtension module-code="S2" />
-		</template>
-	</DynamicModuleDashboard>
+	<DynamicModuleDashboard module-code="S2" />
 </template>
 
 <script setup lang="ts" name="aidopKanbanS2">
 import DynamicModuleDashboard from './components/DynamicModuleDashboard.vue';
-import ModuleDetailExtension from './components/ModuleDetailExtension.vue';
 </script>

+ 3 - 6
Web/src/views/aidop/kanban/s3.vue

@@ -2,21 +2,18 @@
 	<DynamicModuleDashboard module-code="S3" :extra-query-init="extraQueryInit" :extra-query-mapper="mapExtraQuery">
 		<template #extra-fields="{ query }">
 			<el-form-item label="供应商">
-				<el-input v-model="query.supplier" placeholder="供应商编码/名称" clearable style="width: 160px" />
+				<SmartOpsFuzzySelect v-model="query.supplier" field="supplier" placeholder="供应商编码/名称" width="160px" />
 			</el-form-item>
 			<el-form-item label="物料">
-				<el-input v-model="query.material" placeholder="物料编码" clearable style="width: 150px" />
+				<SmartOpsFuzzySelect v-model="query.material" field="material" placeholder="物料编码" width="150px" />
 			</el-form-item>
 		</template>
-		<template #extension>
-			<ModuleDetailExtension module-code="S3" />
-		</template>
 	</DynamicModuleDashboard>
 </template>
 
 <script setup lang="ts" name="aidopKanbanS3">
 import DynamicModuleDashboard from './components/DynamicModuleDashboard.vue';
-import ModuleDetailExtension from './components/ModuleDetailExtension.vue';
+import SmartOpsFuzzySelect from './components/SmartOpsFuzzySelect.vue';
 
 const extraQueryInit = {
 	supplier: '',

+ 4 - 7
Web/src/views/aidop/kanban/s4.vue

@@ -2,24 +2,21 @@
 	<DynamicModuleDashboard module-code="S4" :extra-query-init="extraQueryInit" :extra-query-mapper="mapExtraQuery">
 		<template #extra-fields="{ query }">
 			<el-form-item label="供应商">
-				<el-input v-model="query.supplier" placeholder="供应商编码/名称" clearable style="width: 160px" />
+				<SmartOpsFuzzySelect v-model="query.supplier" field="supplier" placeholder="供应商编码/名称" width="160px" />
 			</el-form-item>
 			<el-form-item label="采购单号">
-				<el-input v-model="query.poNo" placeholder="PO/合同号" clearable style="width: 160px" />
+				<SmartOpsFuzzySelect v-model="query.poNo" field="poNo" placeholder="PO/合同号" width="160px" />
 			</el-form-item>
 			<el-form-item label="物料">
-				<el-input v-model="query.material" placeholder="物料编码" clearable style="width: 150px" />
+				<SmartOpsFuzzySelect v-model="query.material" field="material" placeholder="物料编码" width="150px" />
 			</el-form-item>
 		</template>
-		<template #extension>
-			<ModuleDetailExtension module-code="S4" />
-		</template>
 	</DynamicModuleDashboard>
 </template>
 
 <script setup lang="ts" name="aidopKanbanS4">
 import DynamicModuleDashboard from './components/DynamicModuleDashboard.vue';
-import ModuleDetailExtension from './components/ModuleDetailExtension.vue';
+import SmartOpsFuzzySelect from './components/SmartOpsFuzzySelect.vue';
 
 const extraQueryInit = {
 	supplier: '',

+ 36 - 1093
Web/src/views/aidop/kanban/s5.vue

@@ -1,1098 +1,41 @@
 <template>
-  <div class="s5-dashboard">
-    <!-- 顶部标题栏 -->
-    <div class="top-header">
-      <div class="header-left">
-        <div class="breadcrumb">
-          <el-icon class="back-icon" @click="$router.push('/')"><ArrowLeft /></el-icon>
-          <span>企业数字化运营看板 V5.0</span>
-          <el-tag size="small" class="gold-tag">正式版</el-tag>
-        </div>
-        <h1 class="page-title">
-          S5 物料仓储(仓储)全流程详情
-          <span class="subtitle">实时监控仓储五大环节核心指标与异常状态</span>
-        </h1>
-      </div>
-      <div class="header-right">
-        <div class="system-time">{{ currentTime }}</div>
-        <el-button size="small" class="btn-icon"><el-icon><Search /></el-icon></el-button>
-        <el-button size="small" class="btn-icon"><el-icon><Bell /></el-icon></el-button>
-        <el-button size="small" class="btn-icon"><el-icon><Setting /></el-icon></el-button>
-        <el-button size="small" class="btn-export">
-          <el-icon><Download /></el-icon>
-          导出数据集
-        </el-button>
-        <el-button size="small" type="primary" class="btn-sync">
-          <el-icon><Refresh /></el-icon>
-          立即同步
-        </el-button>
-      </div>
-    </div>
-
-    <!-- 仓储作业:日期、仓库、物料、工单(齐套/发运关联) -->
-    <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>
-      <el-form-item label="物料">
-        <el-input v-model="detailQuery.material" placeholder="物料编码" clearable style="width: 140px" />
-      </el-form-item>
-      <el-form-item label="工单号">
-        <el-input v-model="detailQuery.workOrder" placeholder="生产/配送工单" clearable style="width: 160px" />
-      </el-form-item>
-    </DetailQueryBar>
-
-    <!-- 第一行:4 个 KPI 卡片 -->
-    <div class="row-1">
-      <div class="kpi-card">
-        <div class="kpi-header">
-          <span class="kpi-label">{{ l1Slots[0]?.displayName || '物料上线周期' }}</span>
-          <el-icon :size="24"><Timer /></el-icon>
-        </div>
-        <div class="kpi-value">
-          <span class="value">{{ homeS5.matchLeadTimeActualHrs }}</span>
-          <span class="unit">Hrs</span>
-        </div>
-        <div class="kpi-trend down">
-          <el-icon><ArrowDown /></el-icon>
-          <span>{{ d(0, 12.5, 1) }}%</span>
-        </div>
-        <div class="kpi-bar warning">
-          <div
-            class="bar-fill"
-            :style="{
-              width: `${Math.min(100, (homeS5.matchLeadTimeTargetHrs / homeS5.matchLeadTimeActualHrs) * 100)}%`
-            }"
-          />
-        </div>
-      </div>
-
-      <div class="kpi-card success">
-        <div class="kpi-header">
-          <span class="kpi-label">{{ l1Slots[1]?.displayName || '物料上线满足率' }}</span>
-          <el-icon :size="24"><CircleCheck /></el-icon>
-        </div>
-        <div class="kpi-value">
-          <span class="value success">{{ homeS5.matchOnTimeActualPct }}</span>
-          <span class="unit">%</span>
-        </div>
-        <div class="kpi-trend up">
-          <el-icon><ArrowUp /></el-icon>
-          <span>{{ d(1, 0.34, 2) }}%</span>
-        </div>
-        <div class="kpi-bar success">
-          <div class="bar-fill" :style="{ width: `${homeS5.matchOnTimeActualPct}%` }"></div>
-        </div>
-      </div>
-
-      <div class="kpi-card">
-        <div class="kpi-header">
-          <span class="kpi-label">{{ l1Slots[2]?.displayName || '物料仓储人效' }}</span>
-          <el-icon :size="24"><User /></el-icon>
-        </div>
-        <div class="kpi-value">
-          <span class="value">{{ homeS5.rawWarehouseHeadcount }}</span>
-          <span class="unit subtle">人</span>
-        </div>
-        <div class="kpi-status">
-          <el-icon><UserFilled /></el-icon>
-          <span>Active</span>
-        </div>
-        <div class="kpi-bar danger">
-          <div
-            class="bar-fill"
-            :style="{
-              width: `${Math.min(100, (homeS5.rawWarehouseHeadcount / homeS5.rawWarehouseHeadcountTarget) * 100)}%`
-            }"
-          />
-        </div>
-      </div>
-
-      <div class="kpi-card warning">
-        <div class="kpi-header">
-          <span class="kpi-label">{{ l1Slots[3]?.displayName || '物料库存周转' }}</span>
-          <el-icon :size="24"><Refresh /></el-icon>
-        </div>
-        <div class="kpi-value">
-          <span class="value">{{ homeS5.materialInventoryTurnoverDays }}</span>
-          <span class="unit">Days</span>
-        </div>
-        <div class="kpi-trend up">
-          <el-icon><ArrowUp /></el-icon>
-          <span>{{ d(2, 1.2, 1) }}d</span>
-        </div>
-        <div class="kpi-bar"><div class="bar-fill" :style="{ width: `${Math.min(100, (homeS5.materialInventoryTurnoverDays / 30) * 100)}%` }"></div></div>
-      </div>
-    </div>
-
-    <!-- 第二行:两个图表 -->
-    <div class="row-2">
-      <div class="chart-card">
-        <div class="card-header">
-          <span class="card-title">
-            <span class="title-line"></span>
-            7 日环节周期变化趋势
-          </span>
-          <div class="chart-legend">
-            <span class="legend-item"><span class="legend-dot blue"></span>收货</span>
-            <span class="legend-item"><span class="legend-dot red"></span>发货</span>
-          </div>
-        </div>
-        <div id="trend-chart" class="chart-container"></div>
-      </div>
-
-      <div class="chart-card">
-        <div class="card-header">
-          <span class="card-title">
-            <span class="title-line"></span>
-            子流程满足率对比分析
-          </span>
-        </div>
-        <div id="radar-chart" class="radar-container"></div>
-      </div>
-    </div>
-
-    <!-- 第三行:5 个流程卡片 -->
-    <div class="row-3">
-      <!-- 收货 -->
-      <div class="process-card">
-        <div class="process-header">
-          <span class="process-title">
-            <el-icon><Folder /></el-icon>
-            物料仓储(收货)
-          </span>
-          <el-tag size="small" :type="statusTypeByRate(Number(d(7, 94.5, 1)))">{{ statusTextByRate(Number(d(7, 94.5, 1))) }}</el-tag>
-        </div>
-        <div class="process-metrics">
-          <div class="metric-item">
-            <span class="metric-label">1. 物料收货周期</span>
-            <div class="metric-value">
-              <span class="value">{{ d(3, 2.4, 1) }}</span>
-              <span class="unit">h</span>
-              <span class="trend down"><el-icon><ArrowDown /></el-icon>{{ t(0, '0.2h') }}</span>
-            </div>
-          </div>
-          <div class="metric-item">
-            <span class="metric-label">2. 物料收货满足率</span>
-            <div class="metric-value">
-              <span class="value success">{{ homeS5.matchOnTimeActualPct }}</span>
-              <span class="unit">%</span>
-              <span class="trend">目标 {{ homeS5.matchOnTimeTargetPct }}%</span>
-            </div>
-          </div>
-          <div class="metric-item">
-            <span class="metric-label">3. 收货人效</span>
-            <div class="metric-value">
-              <span class="value">{{ d(4, 158, 0) }}</span>
-              <span class="unit">P/H</span>
-              <span class="trend up"><el-icon><ArrowUp /></el-icon>{{ t(1, '5%') }}</span>
-            </div>
-          </div>
-          <div class="metric-item">
-            <span class="metric-label">4. 收货物料周转</span>
-            <div class="metric-value">
-              <span class="value">{{ d(5, 12.2, 1) }}</span>
-              <span class="unit">x</span>
-              <span class="trend stable">{{ t(2, '平稳') }}</span>
-            </div>
-          </div>
-        </div>
-      </div>
-
-      <!-- IQC -->
-      <div class="process-card" :class="statusTypeByRate(Number(d(7, 94.5, 1))) === 'danger' ? 'danger-border' : statusTypeByRate(Number(d(7, 94.5, 1))) === 'warning' ? 'warning-border' : ''">
-        <div class="process-header">
-          <span class="process-title">
-            <el-icon><Checked /></el-icon>
-            来料检验 (IQC)
-          </span>
-          <el-tag size="small" :type="statusTypeByRate(Number(d(7, 94.5, 1)))">{{ statusTextByRate(Number(d(7, 94.5, 1))) }}</el-tag>
-        </div>
-        <div class="process-metrics">
-          <div class="metric-item">
-            <span class="metric-label">1. 物料检验周期</span>
-            <div class="metric-value">
-              <span class="value warning">{{ d(6, 4.8, 1) }}</span>
-              <span class="unit">h</span>
-              <span class="trend up"><el-icon><ArrowUp /></el-icon>{{ t(3, '1.2h') }}</span>
-            </div>
-          </div>
-          <div class="metric-item">
-            <span class="metric-label">2. 物料检验合格率</span>
-            <div class="metric-value">
-              <span class="value">{{ d(7, 94.5, 1) }}</span>
-              <span class="unit">%</span>
-              <span class="trend down"><el-icon><ArrowDown /></el-icon>{{ t(4, '2.1%') }}</span>
-            </div>
-          </div>
-          <div class="metric-item">
-            <span class="metric-label">3. 物料检验人效</span>
-            <div class="metric-value">
-              <span class="value">{{ d(8, 92, 0) }}</span>
-              <span class="unit">P/H</span>
-              <span class="trend up"><el-icon><ArrowUp /></el-icon>{{ t(5, '2%') }}</span>
-            </div>
-          </div>
-          <div class="metric-item">
-            <span class="metric-label">4. 检验物料周转</span>
-            <div class="metric-value">
-              <span class="value">{{ d(9, 8.4, 1) }}</span>
-              <span class="unit">x</span>
-              <span class="trend down"><el-icon><ArrowDown /></el-icon>{{ t(6, '0.5') }}</span>
-            </div>
-          </div>
-        </div>
-      </div>
-
-      <!-- 上架 -->
-      <div class="process-card">
-        <div class="process-header">
-          <span class="process-title">
-            <el-icon><Top /></el-icon>
-            物料仓储(上架)
-          </span>
-          <el-tag size="small" :type="statusTypeByRate(Number(d(11, 99.1, 1)))">{{ statusTextByRate(Number(d(11, 99.1, 1))) }}</el-tag>
-        </div>
-        <div class="process-metrics">
-          <div class="metric-item">
-            <span class="metric-label">1. 物料上架周期</span>
-            <div class="metric-value">
-              <span class="value">{{ d(10, 1.2, 1) }}</span>
-              <span class="unit">h</span>
-              <span class="trend down"><el-icon><ArrowDown /></el-icon>{{ t(7, '0.4h') }}</span>
-            </div>
-          </div>
-          <div class="metric-item">
-            <span class="metric-label">2. 物料上架满足率</span>
-            <div class="metric-value">
-              <span class="value success">{{ d(11, 99.1, 1) }}</span>
-              <span class="unit">%</span>
-              <span class="trend up"><el-icon><ArrowUp /></el-icon>{{ t(8, '0.4%') }}</span>
-            </div>
-          </div>
-          <div class="metric-item">
-            <span class="metric-label">3. 物料上架人效</span>
-            <div class="metric-value">
-              <span class="value">{{ d(12, 210, 0) }}</span>
-              <span class="unit">P/H</span>
-              <span class="trend up"><el-icon><ArrowUp /></el-icon>{{ t(9, '8%') }}</span>
-            </div>
-          </div>
-          <div class="metric-item">
-            <span class="metric-label">4. 上架物料周转</span>
-            <div class="metric-value">
-              <span class="value">{{ d(13, 15.4, 1) }}</span>
-              <span class="unit">x</span>
-              <span class="trend up"><el-icon><ArrowUp /></el-icon>{{ t(10, '2.1') }}</span>
-            </div>
-          </div>
-        </div>
-      </div>
-
-      <!-- 备货 -->
-      <div class="process-card">
-        <div class="process-header">
-          <span class="process-title">
-            <el-icon><ShoppingBag /></el-icon>
-            物料仓储(备货)
-          </span>
-          <el-tag size="small" :type="statusTypeByRate(Number(d(15, 97.8, 1)))">{{ statusTextByRate(Number(d(15, 97.8, 1))) }}</el-tag>
-        </div>
-        <div class="process-metrics">
-          <div class="metric-item">
-            <span class="metric-label">1. 物料备货周期</span>
-            <div class="metric-value">
-              <span class="value">{{ d(14, 5.2, 1) }}</span>
-              <span class="unit">h</span>
-              <span class="trend down"><el-icon><ArrowDown /></el-icon>{{ t(11, '0.5h') }}</span>
-            </div>
-          </div>
-          <div class="metric-item">
-            <span class="metric-label">2. 物料备货满足率</span>
-            <div class="metric-value">
-              <span class="value success">{{ d(15, 97.8, 1) }}</span>
-              <span class="unit">%</span>
-              <span class="trend up"><el-icon><ArrowUp /></el-icon>{{ t(12, '0.7%') }}</span>
-            </div>
-          </div>
-          <div class="metric-item">
-            <span class="metric-label">3. 物料备货人效</span>
-            <div class="metric-value">
-              <span class="value">{{ d(16, 114, 0) }}</span>
-              <span class="unit">P/H</span>
-              <span class="trend stable">{{ t(13, '持平') }}</span>
-            </div>
-          </div>
-          <div class="metric-item">
-            <span class="metric-label">4. 备货物料周转</span>
-            <div class="metric-value">
-              <span class="value">{{ d(17, 9.6, 1) }}</span>
-              <span class="unit">x</span>
-              <span class="trend up"><el-icon><ArrowUp /></el-icon>{{ t(14, '0.3') }}</span>
-            </div>
-          </div>
-        </div>
-      </div>
-
-      <!-- 发货 -->
-      <div class="process-card" :class="statusTypeByRate(Number(d(19, 88.5, 1))) === 'danger' ? 'danger-border' : statusTypeByRate(Number(d(19, 88.5, 1))) === 'warning' ? 'warning-border' : ''">
-        <div class="process-header">
-          <span class="process-title">
-            <el-icon><Van /></el-icon>
-            物料仓储(发货)
-          </span>
-          <el-tag size="small" :type="statusTypeByRate(Number(d(19, 88.5, 1)))">{{ statusTextByRate(Number(d(19, 88.5, 1))) }}</el-tag>
-        </div>
-        <div class="process-metrics">
-          <div class="metric-item">
-            <span class="metric-label">1. 物料发货周期</span>
-            <div class="metric-value">
-              <span class="value danger">{{ d(18, 2.8, 1) }}</span>
-              <span class="unit">d</span>
-              <span class="trend up"><el-icon><ArrowUp /></el-icon>{{ t(15, '0.4d') }}</span>
-            </div>
-          </div>
-          <div class="metric-item">
-            <span class="metric-label">2. 物料发货满足率</span>
-            <div class="metric-value">
-              <span class="value danger">{{ d(19, 88.5, 1) }}</span>
-              <span class="unit">%</span>
-              <span class="trend down"><el-icon><ArrowDown /></el-icon>{{ t(16, '4.2%') }}</span>
-            </div>
-          </div>
-          <div class="metric-item">
-            <span class="metric-label">3. 物料发货人效</span>
-            <div class="metric-value">
-              <span class="value">{{ d(20, 2197, 0) }}</span>
-              <span class="unit">人</span>
-              <span class="trend up"><el-icon><ArrowUp /></el-icon>{{ t(17, '7%') }}</span>
-            </div>
-          </div>
-          <div class="metric-item">
-            <span class="metric-label">4. 发货物料周转</span>
-            <div class="metric-value">
-              <span class="value">{{ d(21, 8.2, 1) }}</span>
-              <span class="unit">x</span>
-              <span class="trend down"><el-icon><ArrowDown /></el-icon>{{ t(18, '0.3') }}</span>
-            </div>
-          </div>
-        </div>
-      </div>
-    </div>
-
-    <!-- 右下角:辅助工具 -->
-    <div class="tools-section">
-      <div class="tools-header">
-        <el-icon><Tools /></el-icon>
-        <span>辅助工具</span>
-      </div>
-      <div class="tools-grid">
-        <div class="tool-item">
-          <el-icon><Document /></el-icon>
-          <span>历史报告</span>
-        </div>
-        <div class="tool-item">
-          <el-icon><Location /></el-icon>
-          <span>库位导航</span>
-        </div>
-        <div class="tool-item">
-          <el-icon><User /></el-icon>
-          <span>人员调度</span>
-        </div>
-        <div class="tool-item warning">
-          <el-icon><Warning /></el-icon>
-          <span>风险预警</span>
-        </div>
-      </div>
-    </div>
-  </div>
+	<DynamicModuleDashboard
+		module-code="S5"
+		:extra-query-init="extraQueryInit"
+		:extra-query-mapper="mapExtraQuery"
+	>
+		<template #extra-fields="{ query }">
+			<el-form-item label="仓库">
+				<SmartOpsFuzzySelect v-model="query.warehouse" field="warehouse" placeholder="仓库/库区编码" width="140px" />
+			</el-form-item>
+			<el-form-item label="物料">
+				<SmartOpsFuzzySelect v-model="query.material" field="material" placeholder="物料编码" width="140px" />
+			</el-form-item>
+			<el-form-item label="工单号">
+				<SmartOpsFuzzySelect v-model="query.workOrder" field="workOrder" placeholder="生产/配送工单" width="160px" />
+			</el-form-item>
+		</template>
+	</DynamicModuleDashboard>
 </template>
 
-<script setup>
-import { ref, computed, onMounted, onUnmounted, 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,
-  Top, ShoppingBag, Van, Tools, Document, Location, Warning
-} from '@element-plus/icons-vue'
-import * as echarts from 'echarts'
-import { homeS5 } from './data/homeModulesSync'
-import { loadHomeModuleMetrics } from './data/homeModulesSync'
-import { fetchModuleDetail, fetchHomeGrid, fetchDetailKpis } from '../api/kanbanData'
-import { AIDOP_LAYOUT_SAVED } from './utils/s4LayoutEvents'
-
-const MODULE_CODE = 'S5'
-const l1Items = ref([])
-const l2Items = ref([])
-const l1Slots = computed(() => (Array.isArray(l1Items.value) ? l1Items.value : []))
-
-async function loadLayoutKpis() {
-  try {
-    const [grid, l2] = await Promise.all([
-      fetchHomeGrid(MODULE_CODE).catch(() => null),
-      fetchDetailKpis(MODULE_CODE).catch(() => []),
-    ])
-    l1Items.value = (grid && grid.items) ? grid.items : []
-    l2Items.value = Array.isArray(l2) ? l2 : []
-  } catch { /* ignore */ }
-}
-
-function onLayoutSaved(e) {
-  const mc = String((e && e.detail && e.detail.moduleCode) || '').toUpperCase()
-  if (!mc || mc === MODULE_CODE) loadLayoutKpis()
+<script setup lang="ts" name="aidopKanbanS5">
+import DynamicModuleDashboard from './components/DynamicModuleDashboard.vue';
+import SmartOpsFuzzySelect from './components/SmartOpsFuzzySelect.vue';
+
+const extraQueryInit = {
+	warehouse: '',
+	material: '',
+	workOrder: '',
+};
+
+function mapExtraQuery(query: Record<string, string>) {
+	const warehouse = String(query.warehouse || '').trim();
+	const material = String(query.material || '').trim();
+	const workOrder = String(query.workOrder || '').trim();
+	return {
+		...(warehouse ? { warehouse } : {}),
+		...(material ? { material } : {}),
+		...(workOrder ? { workOrder } : {}),
+	};
 }
-
-const router = useRouter()
-const currentTime = ref('')
-const trendXAxis = ref(['D1', 'D2', 'D3', 'D4', 'D5', 'D6', 'D7'])
-const inboundTrend = ref([2.8, 2.6, 2.7, 2.5, 2.4, 2.4, 2.4])
-const outboundTrend = ref([1.8, 2.0, 2.2, 2.5, 2.6, 2.7, 2.8])
-const radarValues = ref([98.2, 88.5, 97.8, 99.1, 94.5])
-const detailNums = ref([12.5, 0.34, 1.2, 2.4, 158, 12.2, 4.8, 94.5, 92, 8.4, 1.2, 99.1, 210, 15.4, 5.2, 97.8, 114, 9.6, 2.8, 88.5, 2197, 8.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({
-  ...emptySmartOpsBaseQuery(),
-  warehouse: '',
-  material: '',
-  workOrder: '',
-})
-
-function onDetailQuery() {
-  ElMessage.success(`已应用仓储筛选(${summarizeSmartOpsBaseQuery(detailQuery.value)})`)
-}
-
-function onDetailQueryReset() {
-  detailQuery.value = { ...emptySmartOpsBaseQuery(), warehouse: '', material: '', workOrder: '' }
-  ElMessage.info('已重置')
-}
-
-let trendChart = null
-let radarChart = null
-
-const updateTime = () => {
-  currentTime.value = new Date().toLocaleString('zh-CN', {
-    year: 'numeric',
-    month: '2-digit',
-    day: '2-digit',
-    hour: '2-digit',
-    minute: '2-digit',
-    second: '2-digit',
-    hour12: false
-  })
-}
-
-const d = (idx, fallback = 0, digits = 1) => {
-  const n = Number(detailNums.value[idx] ?? fallback)
-  return Number.isFinite(n) ? n.toFixed(digits) : Number(fallback).toFixed(digits)
-}
-const t = (idx, fallback = '-') => trendTexts.value[idx] ?? fallback
-const statusTypeByRate = (val, target = 95) => {
-  if (val >= target) return 'success'
-  if (val >= target - 5) return 'warning'
-  return 'danger'
-}
-const statusTextByRate = (val, target = 95) => {
-  if (val >= target) return '优秀'
-  if (val >= target - 5) return '警告'
-  return '严重'
-}
-
-const initTrendChart = () => {
-  const chartDom = document.getElementById('trend-chart')
-  if (!chartDom) return
-  
-  trendChart = echarts.init(chartDom)
-  const option = {
-    tooltip: {
-      trigger: 'axis',
-      backgroundColor: 'rgba(15, 23, 42, 0.95)',
-      borderColor: '#334155',
-      textStyle: { color: '#e2e8f0' }
-    },
-    legend: { show: false },
-    grid: { left: '3%', right: '4%', bottom: '10%', top: '15%' },
-    xAxis: {
-      type: 'category',
-      data: trendXAxis.value,
-      axisLine: { lineStyle: { color: '#334155' } },
-      axisLabel: { color: '#64748b' }
-    },
-    yAxis: {
-      type: 'value',
-      min: 0,
-      max: 3,
-      splitLine: { lineStyle: { color: '#1e293b' } },
-      axisLabel: { color: '#64748b' }
-    },
-    series: [
-      {
-        name: '收货',
-        type: 'line',
-        smooth: true,
-        symbol: 'circle',
-        symbolSize: 6,
-        itemStyle: { color: '#60a5fa' },
-        lineStyle: { color: '#60a5fa', width: 2 },
-        data: inboundTrend.value
-      },
-      {
-        name: '发货',
-        type: 'line',
-        smooth: true,
-        symbol: 'circle',
-        symbolSize: 6,
-        itemStyle: { color: '#f87171' },
-        lineStyle: { color: '#f87171', width: 2 },
-        data: outboundTrend.value
-      }
-    ]
-  }
-  trendChart.setOption(option)
-}
-
-const initRadarChart = () => {
-  const chartDom = document.getElementById('radar-chart')
-  if (!chartDom) return
-  
-  radarChart = echarts.init(chartDom)
-  const option = {
-    tooltip: {},
-    radar: {
-      indicator: [
-        { name: '收货满足率', max: 100 },
-        { name: '发货满足率', max: 100 },
-        { name: '备货满足率', max: 100 },
-        { name: '上架满足率', max: 100 },
-        { name: '检验合格率', max: 100 }
-      ],
-      axisName: {
-        color: '#94a3b8',
-        fontSize: 11
-      },
-      splitLine: {
-        lineStyle: { color: '#334155' }
-      },
-      splitArea: {
-        areaStyle: { color: ['rgba(30, 41, 59, 0.5)', 'rgba(15, 23, 42, 0.5)'] }
-      }
-    },
-    series: [{
-      type: 'radar',
-      data: [
-        {
-          value: radarValues.value,
-          name: '子流程满足率',
-          areaStyle: {
-            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-              { offset: 0, color: 'rgba(96, 165, 250, 0.5)' },
-              { offset: 1, color: 'rgba(96, 165, 250, 0.1)' }
-            ])
-          },
-          lineStyle: { color: '#60a5fa', width: 2 },
-          itemStyle: { color: '#60a5fa' }
-        }
-      ]
-    }]
-  }
-  radarChart.setOption(option)
-}
-
-const initCharts = () => {
-  nextTick(() => {
-    initTrendChart()
-    initRadarChart()
-    
-    window.addEventListener('resize', () => {
-      trendChart?.resize()
-      radarChart?.resize()
-    })
-  })
-}
-
-onMounted(async () => {
-  window.addEventListener(AIDOP_LAYOUT_SAVED, onLayoutSaved)
-  loadLayoutKpis()
-  await loadHomeModuleMetrics()
-  const detail = await fetchModuleDetail('S5')
-  const l2 = detail.l2 ?? []
-  const l3 = detail.l3 ?? []
-  const merged = [...l2, ...l3]
-  if (l2.length > 0) {
-    const vals = l2.slice(0, 7).map((x) => Number(x.metricValue ?? 0))
-    trendXAxis.value = l2.slice(0, 7).map((x, i) => (x.statDate ? String(x.statDate).slice(5, 10) : `D${i + 1}`))
-    inboundTrend.value = vals.map((v) => Number((v / 10).toFixed(1)))
-    outboundTrend.value = [...inboundTrend.value].reverse()
-    radarValues.value = l2.slice(0, 5).map((x) => Math.max(0, Math.min(100, Number(x.metricValue ?? 0))))
-  }
-  if (merged.length > 0) {
-    detailNums.value = merged.slice(0, 22).map((x, i) => Number(x.metricValue ?? detailNums.value[i] ?? 0))
-  }
-  updateTime()
-  setInterval(updateTime, 1000)
-  initCharts()
-})
-
-onUnmounted(() => {
-  window.removeEventListener(AIDOP_LAYOUT_SAVED, onLayoutSaved)
-})
 </script>
-
-<style scoped>
-.s5-dashboard {
-  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;
-}
-
-/* 顶部标题栏 */
-.top-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: flex-start;
-  margin-bottom: 20px;
-}
-
-.header-left {
-  display: flex;
-  flex-direction: column;
-  gap: 8px;
-}
-
-.breadcrumb {
-  display: flex;
-  align-items: center;
-  gap: 10px;
-  font-size: 12px;
-  color: #64748b;
-}
-
-.back-icon {
-  cursor: pointer;
-  color: #60a5fa;
-}
-
-.back-icon:hover {
-  color: #93c5fd;
-}
-
-.gold-tag {
-  background: linear-gradient(135deg, #fbbf24, #f59e0b);
-  border: none;
-  color: white;
-  font-size: 10px;
-  padding: 2px 8px;
-}
-
-.page-title {
-  margin: 0;
-  font-size: 20px;
-  font-weight: 700;
-  color: #e2e8f0;
-  display: flex;
-  flex-direction: column;
-  gap: 4px;
-}
-
-.page-title .subtitle {
-  font-size: 12px;
-  font-weight: 400;
-  color: #64748b;
-}
-
-.header-right {
-  display: flex;
-  align-items: center;
-  gap: 10px;
-}
-
-.system-time {
-  font-size: 12px;
-  color: #64748b;
-  margin-right: 10px;
-}
-
-.btn-icon {
-  padding: 8px;
-  background: rgba(51, 65, 85, 0.6);
-  border: 1px solid #334155;
-  color: #94a3b8;
-}
-
-.btn-export {
-  background: rgba(51, 65, 85, 0.6);
-  border: 1px solid #334155;
-  color: #94a3b8;
-}
-
-.btn-sync {
-  background: linear-gradient(135deg, #3b82f6, #2563eb);
-  border: none;
-}
-
-/* 第一行 KPI */
-.row-1 {
-  display: grid;
-  grid-template-columns: repeat(4, 1fr);
-  gap: 15px;
-  margin-bottom: 15px;
-}
-
-.kpi-card {
-  background: rgba(30, 41, 59, 0.4);
-  border-radius: 8px;
-  padding: 15px;
-  border: 1px solid #1e293b;
-}
-
-.kpi-card.success {
-  border-color: rgba(52, 211, 153, 0.5);
-}
-
-.kpi-card.warning {
-  border-color: rgba(251, 191, 36, 0.5);
-}
-
-.kpi-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 12px;
-  color: #94a3b8;
-  font-size: 12px;
-}
-
-.kpi-value {
-  display: flex;
-  align-items: baseline;
-  gap: 6px;
-  margin-bottom: 8px;
-}
-
-.kpi-value .value {
-  font-size: 32px;
-  font-weight: 700;
-  color: #e2e8f0;
-}
-
-.kpi-value .value.success { color: #34d399; }
-.kpi-value .value.warning { color: #fbbf24; }
-
-.kpi-value .unit {
-  font-size: 12px;
-  color: #64748b;
-}
-
-.kpi-trend {
-  display: flex;
-  align-items: center;
-  gap: 4px;
-  font-size: 11px;
-  font-weight: 600;
-  margin-bottom: 8px;
-}
-
-.kpi-trend.up { color: #34d399; }
-.kpi-trend.down { color: #34d399; }
-
-.kpi-status {
-  display: flex;
-  align-items: center;
-  gap: 6px;
-  font-size: 11px;
-  color: #60a5fa;
-  margin-bottom: 8px;
-}
-
-.kpi-bar {
-  height: 3px;
-  background: #1e293b;
-  border-radius: 2px;
-  overflow: hidden;
-}
-
-.kpi-bar .bar-fill {
-  height: 100%;
-  background: linear-gradient(90deg, #60a5fa, #3b82f6);
-  border-radius: 2px;
-}
-
-.kpi-bar.success .bar-fill { background: linear-gradient(90deg, #34d399, #10b981); }
-.kpi-bar.warning .bar-fill { background: linear-gradient(90deg, #fbbf24, #f59e0b); }
-.kpi-bar.danger .bar-fill { background: linear-gradient(90deg, #ef4444, #dc2626); }
-
-/* 第二行图表 */
-.row-2 {
-  display: grid;
-  grid-template-columns: 1fr 1fr;
-  gap: 15px;
-  margin-bottom: 15px;
-}
-
-.chart-card {
-  background: rgba(30, 41, 59, 0.4);
-  border-radius: 8px;
-  padding: 15px;
-  border: 1px solid #1e293b;
-}
-
-.card-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 15px;
-}
-
-.card-title {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-  font-size: 13px;
-  font-weight: 700;
-  color: #e2e8f0;
-}
-
-.title-line {
-  width: 4px;
-  height: 16px;
-  background: #60a5fa;
-  border-radius: 2px;
-}
-
-.chart-legend {
-  display: flex;
-  gap: 15px;
-  font-size: 12px;
-  color: #94a3b8;
-}
-
-.legend-item {
-  display: flex;
-  align-items: center;
-  gap: 6px;
-}
-
-.legend-dot {
-  width: 8px;
-  height: 8px;
-  border-radius: 50%;
-}
-
-.legend-dot.blue { background: #60a5fa; }
-.legend-dot.red { background: #f87171; }
-
-.chart-container {
-  width: 100%;
-  height: 280px;
-}
-
-.radar-container {
-  width: 100%;
-  height: 280px;
-}
-
-/* 第三行流程卡片 */
-.row-3 {
-  display: grid;
-  grid-template-columns: repeat(5, 1fr);
-  gap: 12px;
-  margin-bottom: 15px;
-}
-
-.process-card {
-  background: rgba(30, 41, 59, 0.4);
-  border-radius: 8px;
-  padding: 12px;
-  border: 1px solid #1e293b;
-}
-
-.process-card.warning-border {
-  border-color: rgba(251, 191, 36, 0.5);
-}
-
-.process-card.danger-border {
-  border-color: rgba(239, 68, 68, 0.5);
-}
-
-.process-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 12px;
-  padding-bottom: 10px;
-  border-bottom: 1px solid #1e293b;
-}
-
-.process-title {
-  display: flex;
-  align-items: center;
-  gap: 6px;
-  font-size: 12px;
-  font-weight: 700;
-  color: #e2e8f0;
-}
-
-.process-metrics {
-  display: flex;
-  flex-direction: column;
-  gap: 10px;
-}
-
-.metric-item {
-  padding: 8px;
-  background: rgba(15, 23, 42, 0.5);
-  border-radius: 4px;
-}
-
-.metric-label {
-  display: block;
-  font-size: 11px;
-  color: #64748b;
-  margin-bottom: 6px;
-}
-
-.metric-value {
-  display: flex;
-  align-items: center;
-  gap: 6px;
-}
-
-.metric-value .value {
-  font-size: 16px;
-  font-weight: 700;
-  color: #e2e8f0;
-}
-
-.metric-value .value.success { color: #34d399; }
-.metric-value .value.warning { color: #fbbf24; }
-.metric-value .value.danger { color: #ef4444; }
-
-.metric-value .unit {
-  font-size: 10px;
-  color: #64748b;
-}
-
-.metric-value .trend {
-  font-size: 10px;
-  font-weight: 600;
-  margin-left: auto;
-}
-
-.metric-value .trend.up { color: #34d399; }
-.metric-value .trend.down { color: #ef4444; }
-.metric-value .trend.stable { color: #64748b; }
-
-/* 辅助工具 */
-.tools-section {
-  position: fixed;
-  bottom: 20px;
-  right: 20px;
-  background: rgba(30, 41, 59, 0.8);
-  border-radius: 8px;
-  padding: 15px;
-  border: 1px solid #1e293b;
-  min-width: 280px;
-}
-
-.tools-header {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-  font-size: 13px;
-  font-weight: 700;
-  color: #e2e8f0;
-  margin-bottom: 12px;
-  padding-bottom: 10px;
-  border-bottom: 1px solid #1e293b;
-}
-
-.tools-grid {
-  display: grid;
-  grid-template-columns: repeat(2, 1fr);
-  gap: 10px;
-}
-
-.tool-item {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  gap: 6px;
-  padding: 12px;
-  background: rgba(15, 23, 42, 0.5);
-  border-radius: 6px;
-  cursor: pointer;
-  transition: all 0.3s;
-  border: 1px solid transparent;
-}
-
-.tool-item:hover {
-  background: rgba(96, 165, 250, 0.1);
-  border-color: #60a5fa;
-}
-
-.tool-item.warning {
-  color: #fbbf24;
-}
-
-.tool-item.warning:hover {
-  background: rgba(251, 191, 36, 0.1);
-  border-color: #fbbf24;
-}
-
-.tool-item .el-icon {
-  font-size: 20px;
-}
-
-.tool-item span {
-  font-size: 11px;
-}
-
-/* Element Plus 覆盖 */
-:deep(.el-button--small) {
-  padding: 8px 16px;
-  font-size: 12px;
-}
-
-:deep(.el-tag) {
-  font-size: 10px;
-  padding: 2px 8px;
-  border-radius: 4px;
-}
-
-:deep(.el-tag--success) {
-  background: rgba(52, 211, 153, 0.2);
-  border-color: rgba(52, 211, 153, 0.3);
-  color: #34d399;
-}
-
-:deep(.el-tag--warning) {
-  background: rgba(251, 191, 36, 0.2);
-  border-color: rgba(251, 191, 36, 0.3);
-  color: #fbbf24;
-}
-
-:deep(.el-tag--danger) {
-  background: rgba(239, 68, 68, 0.2);
-  border-color: rgba(239, 68, 68, 0.3);
-  color: #ef4444;
-}
-</style>

+ 20 - 1153
Web/src/views/aidop/kanban/s6.vue

@@ -1,1160 +1,27 @@
 <template>
-  <div class="s6-dashboard">
-    <!-- 顶部标题栏 -->
-    <div class="top-header">
-      <div class="header-left">
-        <h1 class="page-title">
-          企业数字化运营看板 V5.0
-          <el-tag size="small" class="gold-tag">正式版</el-tag>
-        </h1>
-        <p class="page-subtitle">生产执行详情分析</p>
-      </div>
-      <div class="header-right">
-        <div class="system-time">
-          <div class="time-label">当前系统时间</div>
-          <div class="time-value">{{ currentTime }}</div>
-        </div>
-        <el-button size="small" circle class="btn-icon"><el-icon><FullScreen /></el-icon></el-button>
-        <el-button size="small" circle class="btn-icon"><el-icon><Setting /></el-icon></el-button>
-      </div>
-    </div>
-
-    <!-- 生产执行:日期、订单、产线、设备 -->
-    <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>
-    </DetailQueryBar>
-
-    <!-- 第一行:4 个层级卡片 -->
-    <div class="row-1">
-      <div class="level-card level-1">
-        <div class="level-header">
-          <span class="level-tag">层级:S6-1</span>
-          <span class="level-layer">核心决策层</span>
-        </div>
-        <div class="level-content">
-          <h2 class="level-title">{{ l1Slots[0]?.displayName || '订单级执行' }}</h2>
-          <div class="level-value">
-            <span class="value">{{ homeS6.woOnTimeDeliveryPct }}</span>
-            <span class="unit">%</span>
-          </div>
-          <div class="level-status warning">
-            <span>综合健康指数</span>
-            <span class="trend">目标 {{ homeS6.woOnTimeDeliveryTargetPct }}%</span>
-          </div>
-        </div>
-      </div>
-
-      <div class="level-card level-2">
-        <div class="level-header">
-          <span class="level-tag">层级:S6-2</span>
-          <span class="level-layer">调度控制层</span>
-        </div>
-        <div class="level-content">
-          <h2 class="level-title">{{ l1Slots[1]?.displayName || '工单级执行' }}</h2>
-          <div class="level-value">
-            <span class="value">{{ homeS6.woOnTimeDeliveryPct }}</span>
-            <span class="unit">%</span>
-          </div>
-          <div class="level-status warning">
-            <span>排程达成率</span>
-            <span class="trend">目标 {{ homeS6.woOnTimeDeliveryTargetPct }}%</span>
-          </div>
-        </div>
-      </div>
-
-      <div class="level-card level-3">
-        <div class="level-header">
-          <span class="level-tag">层级:S6-3</span>
-          <span class="level-layer purple">工序协同层</span>
-        </div>
-        <div class="level-content">
-          <h2 class="level-title">{{ l1Slots[2]?.displayName || '工序级执行' }}</h2>
-          <div class="level-value">
-            <span class="value">{{ homeS6.firstPassYieldPct }}</span>
-            <span class="unit">%</span>
-          </div>
-          <div class="level-status warning">
-            <span>流转平衡率</span>
-            <span class="trend">目标 {{ homeS6.firstPassYieldTargetPct }}%</span>
-          </div>
-        </div>
-      </div>
-
-      <div class="level-card level-4 critical">
-        <div class="level-header">
-          <span class="level-tag">层级:S6-4</span>
-          <span class="level-layer red">设备基础层</span>
-        </div>
-        <div class="level-content">
-          <h2 class="level-title">{{ l1Slots[3]?.displayName || '设备级执行' }}</h2>
-          <div class="level-value">
-            <span class="value danger">{{ d(0, 86.1, 1) }}</span>
-            <span class="unit">%</span>
-          </div>
-          <div class="level-status danger">
-            <span>OEE 综合效率</span>
-            <span class="trend down">↓ {{ d(5, 3.5, 1) }}% {{ statusTextByRate(Number(d(0, 86.1, 1)), 95) }}</span>
-          </div>
-        </div>
-      </div>
-    </div>
-
-    <!-- 第二行:订单 + 工序指标分析 -->
-    <div class="row-2">
-      <!-- 左侧:订单制造指标分析 -->
-      <div class="analysis-card order-analysis">
-        <div class="card-header">
-          <div class="card-title">
-            <span class="title-line blue"></span>
-            订单制造指标分析
-          </div>
-          <span class="data-source">数据源:ERP/MES 实时</span>
-        </div>
-        <div class="metrics-grid">
-          <div class="metric-box">
-            <div class="metric-label">订单制造周期</div>
-            <div class="metric-value">
-              <span class="value">{{ d(1, 2.5, 1) }}</span>
-              <span class="unit">Days</span>
-            </div>
-            <div class="metric-trend success">
-              <span>↓ {{ t(0, '-0.3') }}</span>
-              <span class="target">(目标:{{ d(1, 2.8, 1) }})</span>
-            </div>
-            <div class="progress-bar">
-              <div class="progress-fill blue" :style="{ width: `${Math.max(10, Math.min(100, Number(d(0, 86.1, 1))))}%` }"></div>
-            </div>
-          </div>
-
-          <div class="metric-box">
-            <div class="metric-label">订单制造满足率</div>
-            <div class="metric-value">
-              <span class="value danger">{{ homeS6.woOnTimeDeliveryPct }}</span>
-              <span class="unit">%</span>
-            </div>
-            <div class="metric-trend danger">
-              <span>{{ statusTextByRate(Number(homeS6.woOnTimeDeliveryPct), Number(homeS6.woOnTimeDeliveryTargetPct)) }}</span>
-              <span class="target">(目标:{{ homeS6.woOnTimeDeliveryTargetPct }})</span>
-            </div>
-            <div class="progress-bar">
-              <div class="progress-fill green" :style="{ width: `${homeS6.woOnTimeDeliveryPct}%` }"></div>
-            </div>
-          </div>
-
-          <div class="metric-box">
-            <div class="metric-label">订单制造人效</div>
-            <div class="metric-value">
-              <span class="value">{{ homeS6.manufacturingEfficiency }}</span>
-              <span class="unit">指数</span>
-            </div>
-            <div class="metric-trend warning">
-              <span>目标 {{ homeS6.manufacturingEfficiencyTarget }}</span>
-            </div>
-            <div class="progress-bar">
-              <div
-                class="progress-fill purple"
-                :style="{
-                  width: `${Math.min(100, (homeS6.manufacturingEfficiency / homeS6.manufacturingEfficiencyTarget) * 100)}%`
-                }"
-              ></div>
-            </div>
-          </div>
-        </div>
-      </div>
-
-      <!-- 右侧:工序制造指标分析 -->
-      <div class="analysis-card operation-analysis">
-        <div class="card-header">
-          <div class="card-title">
-            <span class="title-line purple"></span>
-            工序制造指标分析
-          </div>
-          <el-button link size="small" class="btn-check">
-            查看堵塞节点
-            <el-icon><ArrowRight /></el-icon>
-          </el-button>
-        </div>
-        <div class="gauges-grid">
-          <div class="gauge-box">
-            <div class="gauge-label">工序周期</div>
-            <div id="gauge-cycle" class="gauge-chart"></div>
-            <div class="gauge-value">{{ gaugeCycleValue }}h</div>
-          </div>
-          <div class="gauge-box">
-            <div class="gauge-label">工序满足率</div>
-            <div id="gauge-satisfaction" class="gauge-chart"></div>
-            <div class="gauge-value warning">{{ homeS6.firstPassYieldPct }}%</div>
-          </div>
-          <div class="gauge-box">
-            <div class="gauge-label">工序人效</div>
-            <div id="gauge-efficiency" class="gauge-chart"></div>
-            <div class="gauge-value">{{ homeS6.manufacturingEfficiency }}</div>
-          </div>
-          <div class="gauge-box">
-            <div class="gauge-label">在制周转</div>
-            <div id="gauge-turnover" class="gauge-chart"></div>
-            <div class="gauge-value warning">{{ gaugeTurnoverValue }}d</div>
-          </div>
-        </div>
-      </div>
-    </div>
-
-    <!-- 第三行:工单 + 设备指标分析 -->
-    <div class="row-3">
-      <!-- 左侧:工单制造指标分析 -->
-      <div class="analysis-card workorder-analysis">
-        <div class="card-header">
-          <div class="card-title">
-            <span class="title-line cyan"></span>
-            工单制造指标分析
-          </div>
-          <el-tag size="small" type="danger" class="alert-tag">{{ Math.max(1, Math.round(Number(d(2, 18.4, 1)) / 6)) }} 个延期工单</el-tag>
-        </div>
-        <div class="wo-metrics">
-          <div class="wo-metric">
-            <div class="metric-label">工单制造周期</div>
-            <div class="metric-value">
-              <span class="value">{{ d(2, 18.4, 1) }}</span>
-              <span class="unit">Hrs</span>
-            </div>
-            <div class="metric-trend success">↓ {{ t(1, '-2.2%') }}</div>
-          </div>
-          <div class="wo-metric">
-            <div class="metric-label">工单满足率</div>
-            <div class="metric-value">
-              <span class="value danger">{{ homeS6.woOnTimeDeliveryPct }}</span>
-              <span class="unit">%</span>
-            </div>
-            <div class="metric-trend danger">目标 {{ homeS6.woOnTimeDeliveryTargetPct }}%</div>
-          </div>
-          <div class="wo-metric">
-            <div class="metric-label">工单制造人效</div>
-            <div class="metric-value">
-              <span class="value">{{ homeS6.manufacturingEfficiency }}</span>
-              <span class="unit">指数</span>
-            </div>
-            <div class="metric-trend warning">目标 {{ homeS6.manufacturingEfficiencyTarget }}</div>
-          </div>
-          <div class="wo-metric">
-            <div class="metric-label">工单在制周转</div>
-            <div class="metric-value">
-              <span class="value">{{ d(3, 2.6, 1) }}</span>
-              <span class="unit">Days</span>
-            </div>
-            <div class="metric-trend success">↑ {{ t(2, '+0.6%') }}</div>
-          </div>
-        </div>
-        <div class="wo-chart">
-          <div id="wo-trend-chart" class="chart-container"></div>
-        </div>
-      </div>
-
-      <!-- 右侧:设备制造指标分析 -->
-      <div class="analysis-card machine-analysis">
-        <div class="card-header">
-          <div class="card-title">
-            <span class="title-line red"></span>
-            设备制造指标分析
-          </div>
-          <span class="alert-text">报警中:S2/S6 产线设备</span>
-        </div>
-        <div class="machine-metrics">
-          <div class="machine-metric">
-            <div class="metric-label">设备周期</div>
-            <div class="metric-value">
-              <span class="value">{{ d(4, 2.2, 1) }}</span>
-              <span class="unit">Days</span>
-            </div>
-            <div class="metric-trend success">↓ {{ t(3, '-0.2') }}</div>
-          </div>
-          <div class="machine-metric">
-            <div class="metric-label">设备满足率</div>
-            <div class="metric-value">
-              <span class="value danger">{{ d(0, 86.1, 1) }}</span>
-              <span class="unit">%</span>
-            </div>
-            <div class="metric-trend danger">↓ {{ t(4, '-3.5') }}</div>
-          </div>
-          <div class="machine-metric">
-            <div class="metric-label">OEE 效率</div>
-            <div class="metric-value">
-              <span class="value warning">{{ d(5, 72.4, 1) }}</span>
-              <span class="unit">%</span>
-            </div>
-            <div class="metric-trend danger">↓ {{ t(5, '-2.1') }}</div>
-          </div>
-          <div class="machine-metric">
-            <div class="metric-label">设备周转</div>
-            <div class="metric-value">
-              <span class="value">{{ d(6, 8.2, 1) }}</span>
-              <span class="unit">Days</span>
-            </div>
-            <div class="metric-trend success">↓ {{ t(6, '-0.3') }}</div>
-          </div>
-        </div>
-        <div class="machine-bars">
-          <div class="bar-item">
-            <span class="bar-label">稼动</span>
-            <div class="bar-container">
-              <div class="bar-fill green" :style="{ width: `${Math.max(10, Math.min(100, Number(d(7, 88, 0))))}%` }"></div>
-            </div>
-          </div>
-          <div class="bar-item">
-            <span class="bar-label">良率</span>
-            <div class="bar-container">
-              <div class="bar-fill green" :style="{ width: `${Math.max(10, Math.min(100, Number(d(8, 82, 0))))}%` }"></div>
-            </div>
-          </div>
-          <div class="bar-item">
-            <span class="bar-label">效率</span>
-            <div class="bar-container">
-              <div class="bar-fill red" :style="{ width: `${Math.max(10, Math.min(100, Number(d(9, 72, 0))))}%` }"></div>
-            </div>
-          </div>
-        </div>
-      </div>
-    </div>
-
-    <!-- 底部功能按钮 -->
-    <div class="bottom-actions">
-      <el-button class="action-btn">
-        <el-icon><Monitor /></el-icon>
-        实时看板
-      </el-button>
-      <el-button class="action-btn warning">
-        <el-icon><Warning /></el-icon>
-        异常闭环
-      </el-button>
-      <el-button class="action-btn success">
-        <el-icon><Box /></el-icon>
-        库存周转
-      </el-button>
-      <el-button class="action-btn purple">
-        <el-icon><TrendCharts /></el-icon>
-        效能追溯
-      </el-button>
-      <el-button class="action-btn cyan">
-        <el-icon><Download /></el-icon>
-        导出报告
-      </el-button>
-      <el-button class="action-btn primary">
-        <el-icon><Refresh /></el-icon>
-        刷新数据
-      </el-button>
-    </div>
-  </div>
+	<DynamicModuleDashboard
+		module-code="S6"
+		:extra-query-init="extraQueryInit"
+		:extra-query-mapper="mapExtraQuery"
+	>
+		<template #extra-fields="{ query }">
+			<el-form-item label="设备">
+				<SmartOpsFuzzySelect v-model="query.equipment" field="equipment" placeholder="设备编码/名称" width="140px" />
+			</el-form-item>
+		</template>
+	</DynamicModuleDashboard>
 </template>
 
-<script setup>
-import { ref, computed, onMounted, onUnmounted, 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
-} from '@element-plus/icons-vue'
-import * as echarts from 'echarts'
-import { homeS6 } from './data/homeModulesSync'
-import { loadHomeModuleMetrics } from './data/homeModulesSync'
-import { fetchModuleDetail, fetchHomeGrid, fetchDetailKpis } from '../api/kanbanData'
-import { AIDOP_LAYOUT_SAVED } from './utils/s4LayoutEvents'
-
-const MODULE_CODE = 'S6'
-const l1Items = ref([])
-const l2Items = ref([])
-const l1Slots = computed(() => (Array.isArray(l1Items.value) ? l1Items.value : []))
+<script setup lang="ts" name="aidopKanbanS6">
+import DynamicModuleDashboard from './components/DynamicModuleDashboard.vue';
+import SmartOpsFuzzySelect from './components/SmartOpsFuzzySelect.vue';
 
-async function loadLayoutKpis() {
-  try {
-    const [grid, l2] = await Promise.all([
-      fetchHomeGrid(MODULE_CODE).catch(() => null),
-      fetchDetailKpis(MODULE_CODE).catch(() => []),
-    ])
-    l1Items.value = (grid && grid.items) ? grid.items : []
-    l2Items.value = Array.isArray(l2) ? l2 : []
-  } catch { /* ignore */ }
-}
-
-function onLayoutSaved(e) {
-  const mc = String((e && e.detail && e.detail.moduleCode) || '').toUpperCase()
-  if (!mc || mc === MODULE_CODE) loadLayoutKpis()
-}
-
-const router = useRouter()
-const currentTime = ref('')
-const gaugeCycleValue = ref(0.8)
-const gaugeTurnoverValue = ref(14.6)
-const woTrendXAxis = ref(['D1', 'D2', 'D3', 'D4', 'D5', 'D6'])
-const woTrendValues = ref([92, 93, 91, 94, 95, 97])
-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({
-  ...emptySmartOpsBaseQuery(),
-  equipment: '',
-})
-
-function onDetailQuery() {
-  ElMessage.success(`已应用生产筛选(${summarizeSmartOpsBaseQuery(detailQuery.value)})`)
-}
-
-function onDetailQueryReset() {
-  detailQuery.value = { ...emptySmartOpsBaseQuery(), equipment: '' }
-  ElMessage.info('已重置')
-}
-
-let gaugeCycle = null
-let gaugeSatisfaction = null
-let gaugeEfficiency = null
-let gaugeTurnover = null
-let woTrendChart = null
-
-const updateTime = () => {
-  currentTime.value = new Date().toLocaleString('zh-CN', {
-    year: 'numeric',
-    month: '2-digit',
-    day: '2-digit',
-    hour: '2-digit',
-    minute: '2-digit',
-    second: '2-digit',
-    hour12: false
-  })
-}
+const extraQueryInit = {
+	equipment: '',
+};
 
-const d = (idx, fallback = 0, digits = 1) => {
-  const n = Number(detailNums.value[idx] ?? fallback)
-  return Number.isFinite(n) ? n.toFixed(digits) : Number(fallback).toFixed(digits)
+function mapExtraQuery(query: Record<string, string>) {
+	const equipment = String(query.equipment || '').trim();
+	return equipment ? { equipment } : {};
 }
-const t = (idx, fallback = '-') => trendTexts.value[idx] ?? fallback
-const statusTextByRate = (val, target = 95) => {
-  if (val >= target) return '达标'
-  if (val >= target - 5) return '预警'
-  return '严重'
-}
-
-const initGauges = () => {
-  // 工序周期
-  const cycleDom = document.getElementById('gauge-cycle')
-  if (cycleDom) {
-    gaugeCycle = echarts.init(cycleDom)
-    gaugeCycle.setOption({
-      series: [{
-        type: 'gauge',
-        startAngle: 180,
-        endAngle: 0,
-        min: 0,
-        max: 2,
-        radius: '100%',
-        center: ['50%', '60%'],
-        itemStyle: { color: '#2dd4bf' },
-        progress: { show: true, width: 8, roundCap: true },
-        pointer: { show: false },
-        axisLine: { lineStyle: { width: 8, color: [[1, '#1e293b']] } },
-        axisTick: { show: false },
-        splitLine: { show: false },
-        axisLabel: { show: false },
-        title: { show: false },
-        detail: { show: false },
-        data: [{ value: gaugeCycleValue.value }]
-      }]
-    })
-  }
-
-  // 工序满足率
-  const satisfactionDom = document.getElementById('gauge-satisfaction')
-  if (satisfactionDom) {
-    gaugeSatisfaction = echarts.init(satisfactionDom)
-    gaugeSatisfaction.setOption({
-      series: [{
-        type: 'gauge',
-        startAngle: 180,
-        endAngle: 0,
-        min: 0,
-        max: 100,
-        radius: '100%',
-        center: ['50%', '60%'],
-        itemStyle: { color: '#fbbf24' },
-        progress: { show: true, width: 8, roundCap: true },
-        pointer: { show: false },
-        axisLine: { lineStyle: { width: 8, color: [[1, '#1e293b']] } },
-        axisTick: { show: false },
-        splitLine: { show: false },
-        axisLabel: { show: false },
-        title: { show: false },
-        detail: { show: false },
-        data: [{ value: homeS6.firstPassYieldPct }]
-      }]
-    })
-  }
-
-  // 工序人效
-  const efficiencyDom = document.getElementById('gauge-efficiency')
-  if (efficiencyDom) {
-    gaugeEfficiency = echarts.init(efficiencyDom)
-    gaugeEfficiency.setOption({
-      series: [{
-        type: 'gauge',
-        startAngle: 180,
-        endAngle: 0,
-        min: 0,
-        max: 3,
-        radius: '100%',
-        center: ['50%', '60%'],
-        itemStyle: { color: '#60a5fa' },
-        progress: { show: true, width: 8, roundCap: true },
-        pointer: { show: false },
-        axisLine: { lineStyle: { width: 8, color: [[1, '#1e293b']] } },
-        axisTick: { show: false },
-        splitLine: { show: false },
-        axisLabel: { show: false },
-        title: { show: false },
-        detail: { show: false },
-        data: [{ value: homeS6.manufacturingEfficiency }]
-      }]
-    })
-  }
-
-  // 在制周转
-  const turnoverDom = document.getElementById('gauge-turnover')
-  if (turnoverDom) {
-    gaugeTurnover = echarts.init(turnoverDom)
-    gaugeTurnover.setOption({
-      series: [{
-        type: 'gauge',
-        startAngle: 180,
-        endAngle: 0,
-        min: 0,
-        max: 20,
-        radius: '100%',
-        center: ['50%', '60%'],
-        itemStyle: { color: '#fbbf24' },
-        progress: { show: true, width: 8, roundCap: true },
-        pointer: { show: false },
-        axisLine: { lineStyle: { width: 8, color: [[1, '#1e293b']] } },
-        axisTick: { show: false },
-        splitLine: { show: false },
-        axisLabel: { show: false },
-        title: { show: false },
-        detail: { show: false },
-        data: [{ value: gaugeTurnoverValue.value }]
-      }]
-    })
-  }
-}
-
-const initWoTrendChart = () => {
-  const chartDom = document.getElementById('wo-trend-chart')
-  if (!chartDom) return
-  
-  woTrendChart = 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: '10%', top: '15%' },
-    xAxis: {
-      type: 'category',
-      data: woTrendXAxis.value,
-      axisLine: { lineStyle: { color: '#334155' } },
-      axisLabel: { color: '#64748b' }
-    },
-    yAxis: {
-      type: 'value',
-      min: 0,
-      max: 100,
-      splitLine: { lineStyle: { color: '#1e293b' } },
-      axisLabel: { color: '#64748b' }
-    },
-    series: [{
-      data: woTrendValues.value,
-      type: 'line',
-      smooth: true,
-      symbol: 'circle',
-      symbolSize: 6,
-      itemStyle: { color: '#2dd4bf' },
-      lineStyle: { color: '#2dd4bf', width: 2 },
-      areaStyle: {
-        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-          { offset: 0, color: 'rgba(45, 212, 191, 0.3)' },
-          { offset: 1, color: 'rgba(45, 212, 191, 0)' }
-        ])
-      }
-    }]
-  }
-  woTrendChart.setOption(option)
-}
-
-const initCharts = () => {
-  nextTick(() => {
-    initGauges()
-    initWoTrendChart()
-    
-    window.addEventListener('resize', () => {
-      gaugeCycle?.resize()
-      gaugeSatisfaction?.resize()
-      gaugeEfficiency?.resize()
-      gaugeTurnover?.resize()
-      woTrendChart?.resize()
-    })
-  })
-}
-
-onMounted(async () => {
-  window.addEventListener(AIDOP_LAYOUT_SAVED, onLayoutSaved)
-  loadLayoutKpis()
-  await loadHomeModuleMetrics()
-  const detail = await fetchModuleDetail('S6')
-  const l2 = detail.l2 ?? []
-  const l3 = detail.l3 ?? []
-  const merged = [...l2, ...l3]
-  if (l2.length > 0) {
-    const nums = l2.slice(0, 6).map((x) => Number(x.metricValue ?? 0))
-    woTrendValues.value = nums.map((x) => Math.max(0, Math.min(100, x)))
-    woTrendXAxis.value = l2.slice(0, 6).map((x, i) => (x.statDate ? String(x.statDate).slice(5, 10) : `D${i + 1}`))
-    gaugeCycleValue.value = Number((Math.max(nums[0] ?? 8, 1) / 10).toFixed(1))
-    gaugeTurnoverValue.value = Number((Math.max(nums[1] ?? 75, 1) / 5).toFixed(1))
-  }
-  if (merged.length > 0) {
-    const nums = merged.slice(0, 10).map((x, i) => Number(x.metricValue ?? detailNums.value[i] ?? 0))
-    detailNums.value = nums
-    const deltas = []
-    for (let i = 1; i < Math.min(8, nums.length); i++) {
-      const prev = nums[i - 1] || 1
-      const cur = nums[i]
-      const pct = ((cur - prev) / Math.abs(prev)) * 100
-      const sign = pct >= 0 ? '+' : ''
-      deltas.push(`${sign}${pct.toFixed(1)}%`)
-    }
-    while (deltas.length < 7) deltas.push('-')
-    trendTexts.value = deltas.slice(0, 7)
-  }
-  updateTime()
-  setInterval(updateTime, 1000)
-  initCharts()
-})
-
-onUnmounted(() => {
-  window.removeEventListener(AIDOP_LAYOUT_SAVED, onLayoutSaved)
-})
 </script>
-
-<style scoped>
-.s6-dashboard {
-  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;
-}
-
-/* 顶部标题栏 */
-.top-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: flex-start;
-  margin-bottom: 15px;
-}
-
-.header-left {
-  display: flex;
-  flex-direction: column;
-  gap: 4px;
-}
-
-.page-title {
-  margin: 0;
-  font-size: 20px;
-  font-weight: 700;
-  color: #e2e8f0;
-  display: flex;
-  align-items: center;
-  gap: 12px;
-}
-
-.gold-tag {
-  background: linear-gradient(135deg, #fbbf24, #f59e0b);
-  border: none;
-  color: white;
-  font-size: 10px;
-  padding: 2px 8px;
-}
-
-.page-subtitle {
-  margin: 0;
-  font-size: 11px;
-  color: #64748b;
-  letter-spacing: 1px;
-}
-
-.header-right {
-  display: flex;
-  align-items: center;
-  gap: 10px;
-}
-
-.system-time {
-  text-align: right;
-  margin-right: 10px;
-}
-
-.time-label {
-  font-size: 10px;
-  color: #64748b;
-}
-
-.time-value {
-  font-size: 14px;
-  font-weight: 700;
-  color: #22d3ee;
-  font-family: 'Courier New', monospace;
-}
-
-.btn-icon {
-  padding: 8px;
-  background: rgba(51, 65, 85, 0.6);
-  border: 1px solid #334155;
-  color: #94a3b8;
-}
-
-/* 第一行:4 个层级卡片 */
-.row-1 {
-  display: grid;
-  grid-template-columns: repeat(4, 1fr);
-  gap: 12px;
-  margin-bottom: 12px;
-}
-
-.level-card {
-  background: rgba(15, 23, 42, 0.6);
-  border-radius: 8px;
-  padding: 15px;
-  border: 1px solid #1e293b;
-}
-
-.level-card.level-1 { border-left: 3px solid #60a5fa; }
-.level-card.level-2 { border-left: 3px solid #a855f7; }
-.level-card.level-3 { border-left: 3px solid #c084fc; }
-.level-card.level-4.critical { border-left: 3px solid #ef4444; }
-
-.level-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 12px;
-}
-
-.level-tag {
-  font-size: 11px;
-  color: #64748b;
-}
-
-.level-layer {
-  font-size: 11px;
-  color: #60a5fa;
-}
-
-.level-layer.purple { color: #c084fc; }
-.level-layer.red { color: #ef4444; }
-
-.level-content {
-  display: flex;
-  flex-direction: column;
-  gap: 8px;
-}
-
-.level-title {
-  margin: 0;
-  font-size: 14px;
-  font-weight: 700;
-  color: #e2e8f0;
-}
-
-.level-value {
-  display: flex;
-  align-items: baseline;
-  gap: 6px;
-}
-
-.level-value .value {
-  font-size: 36px;
-  font-weight: 700;
-  color: #e2e8f0;
-}
-
-.level-value .value.danger { color: #ef4444; }
-
-.level-value .unit {
-  font-size: 14px;
-  color: #64748b;
-}
-
-.level-status {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  font-size: 11px;
-  color: #64748b;
-}
-
-.level-status.success { color: #4ade80; }
-.level-status.warning { color: #fbbf24; }
-.level-status.danger { color: #ef4444; }
-
-.level-status .trend { font-weight: 600; }
-.level-status .trend.down { color: #ef4444; }
-
-/* 第二行 */
-.row-2 {
-  display: grid;
-  grid-template-columns: 1fr 1fr;
-  gap: 12px;
-  margin-bottom: 12px;
-}
-
-.analysis-card {
-  background: rgba(15, 23, 42, 0.6);
-  border-radius: 8px;
-  padding: 15px;
-  border: 1px solid #1e293b;
-}
-
-.card-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 15px;
-}
-
-.card-title {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-  font-size: 13px;
-  font-weight: 700;
-  color: #e2e8f0;
-}
-
-.title-line {
-  width: 4px;
-  height: 16px;
-  border-radius: 2px;
-}
-
-.title-line.blue { background: #60a5fa; }
-.title-line.purple { background: #c084fc; }
-.title-line.cyan { background: #22d3ee; }
-.title-line.red { background: #ef4444; }
-
-.data-source {
-  font-size: 11px;
-  color: #64748b;
-}
-
-.btn-check {
-  color: #60a5fa;
-  font-size: 11px;
-}
-
-.metrics-grid {
-  display: grid;
-  grid-template-columns: repeat(3, 1fr);
-  gap: 15px;
-}
-
-.metric-box {
-  background: rgba(30, 41, 59, 0.4);
-  border-radius: 6px;
-  padding: 12px;
-  border: 1px solid #1e293b;
-}
-
-.metric-label {
-  font-size: 11px;
-  color: #64748b;
-  margin-bottom: 8px;
-}
-
-.metric-value {
-  display: flex;
-  align-items: baseline;
-  gap: 6px;
-  margin-bottom: 6px;
-}
-
-.metric-value .value {
-  font-size: 24px;
-  font-weight: 700;
-  color: #e2e8f0;
-}
-
-.metric-value .value.success { color: #4ade80; }
-
-.metric-value .unit {
-  font-size: 12px;
-  color: #64748b;
-}
-
-.metric-trend {
-  font-size: 11px;
-  font-weight: 600;
-  color: #4ade80;
-  margin-bottom: 8px;
-}
-
-.metric-trend .target {
-  font-weight: 400;
-  color: #64748b;
-}
-
-.metric-trend.danger {
-  color: #ef4444;
-}
-
-.metric-trend.warning {
-  color: #fbbf24;
-}
-
-.progress-bar {
-  height: 4px;
-  background: #1e293b;
-  border-radius: 2px;
-  overflow: hidden;
-}
-
-.progress-bar .progress-fill {
-  height: 100%;
-  border-radius: 2px;
-}
-
-.progress-fill.blue { background: #60a5fa; }
-.progress-fill.green { background: #4ade80; }
-.progress-fill.purple { background: #c084fc; }
-
-.gauges-grid {
-  display: grid;
-  grid-template-columns: repeat(4, 1fr);
-  gap: 15px;
-}
-
-.gauge-box {
-  text-align: center;
-  background: rgba(30, 41, 59, 0.4);
-  border-radius: 6px;
-  padding: 12px;
-  border: 1px solid #1e293b;
-}
-
-.gauge-label {
-  font-size: 11px;
-  color: #64748b;
-  margin-bottom: 8px;
-}
-
-.gauge-chart {
-  width: 100%;
-  height: 80px;
-}
-
-.gauge-value {
-  font-size: 14px;
-  font-weight: 700;
-  color: #e2e8f0;
-  margin-top: 6px;
-}
-
-.gauge-value.success { color: #4ade80; }
-.gauge-value.warning { color: #fbbf24; }
-
-/* 第三行 */
-.row-3 {
-  display: grid;
-  grid-template-columns: 1fr 1fr;
-  gap: 12px;
-  margin-bottom: 15px;
-}
-
-.wo-metrics {
-  display: grid;
-  grid-template-columns: repeat(4, 1fr);
-  gap: 12px;
-  margin-bottom: 15px;
-}
-
-.wo-metric {
-  background: rgba(30, 41, 59, 0.4);
-  border-radius: 6px;
-  padding: 12px;
-  border: 1px solid #1e293b;
-}
-
-.wo-metric .metric-label {
-  font-size: 11px;
-  color: #64748b;
-  margin-bottom: 8px;
-}
-
-.wo-metric .metric-value {
-  display: flex;
-  align-items: baseline;
-  gap: 6px;
-  margin-bottom: 6px;
-}
-
-.wo-metric .metric-value .value {
-  font-size: 20px;
-  font-weight: 700;
-  color: #e2e8f0;
-}
-
-.wo-metric .metric-value .value.success { color: #4ade80; }
-
-.wo-metric .metric-value .unit {
-  font-size: 11px;
-  color: #64748b;
-}
-
-.wo-metric .metric-trend {
-  font-size: 11px;
-  font-weight: 600;
-  color: #4ade80;
-}
-
-.wo-chart {
-  background: rgba(30, 41, 59, 0.4);
-  border-radius: 6px;
-  padding: 12px;
-  border: 1px solid #1e293b;
-}
-
-.chart-container {
-  width: 100%;
-  height: 200px;
-}
-
-.machine-metrics {
-  display: grid;
-  grid-template-columns: repeat(4, 1fr);
-  gap: 12px;
-  margin-bottom: 15px;
-}
-
-.machine-metric {
-  background: rgba(30, 41, 59, 0.4);
-  border-radius: 6px;
-  padding: 12px;
-  border: 1px solid #1e293b;
-}
-
-.machine-metric .metric-value .value.danger { color: #ef4444; }
-.machine-metric .metric-value .value.warning { color: #fbbf24; }
-
-.machine-metric .metric-trend.danger { color: #ef4444; }
-.machine-metric .metric-trend.success { color: #4ade80; }
-
-.machine-bars {
-  display: flex;
-  flex-direction: column;
-  gap: 12px;
-}
-
-.bar-item {
-  display: flex;
-  align-items: center;
-  gap: 12px;
-}
-
-.bar-label {
-  width: 40px;
-  font-size: 11px;
-  color: #64748b;
-}
-
-.bar-container {
-  flex: 1;
-  height: 12px;
-  background: #1e293b;
-  border-radius: 6px;
-  overflow: hidden;
-}
-
-.bar-container .bar-fill {
-  height: 100%;
-  border-radius: 6px;
-}
-
-.bar-fill.green { background: #4ade80; }
-.bar-fill.red { background: #ef4444; }
-
-.alert-tag {
-  font-size: 10px;
-}
-
-.alert-text {
-  font-size: 11px;
-  color: #ef4444;
-}
-
-/* 底部功能按钮 */
-.bottom-actions {
-  display: grid;
-  grid-template-columns: repeat(6, 1fr);
-  gap: 12px;
-}
-
-.action-btn {
-  height: 48px;
-  background: rgba(30, 41, 59, 0.6);
-  border: 1px solid #1e293b;
-  color: #94a3b8;
-  font-size: 12px;
-}
-
-.action-btn:hover {
-  background: rgba(51, 65, 85, 0.8);
-  border-color: #334155;
-}
-
-.action-btn.warning { color: #fbbf24; }
-.action-btn.success { color: #4ade80; }
-.action-btn.purple { color: #c084fc; }
-.action-btn.cyan { color: #22d3ee; }
-.action-btn.primary { color: #60a5fa; }
-
-/* Element Plus 覆盖 */
-:deep(.el-button--small) {
-  padding: 8px 16px;
-  font-size: 12px;
-}
-
-:deep(.el-tag) {
-  font-size: 10px;
-  padding: 2px 8px;
-  border-radius: 4px;
-}
-
-:deep(.el-tag--danger) {
-  background: rgba(239, 68, 68, 0.2);
-  border-color: rgba(239, 68, 68, 0.3);
-  color: #ef4444;
-}
-</style>

+ 28 - 943
Web/src/views/aidop/kanban/s7.vue

@@ -1,950 +1,35 @@
 <template>
-  <div class="s7-dashboard">
-    <!-- 顶部标题栏 -->
-    <div class="top-header">
-      <div class="header-left">
-        <el-icon :size="24"><HomeFilled /></el-icon>
-        <h1 class="page-title">
-          S7 成品仓储运营监控详情
-          <el-tag size="small" class="gold-tag">V5.0 正式版</el-tag>
-        </h1>
-      </div>
-      <div class="header-right">
-        <div class="current-time">
-          <div class="time-label">当前时间</div>
-          <div class="time-value">{{ currentTime }}</div>
-        </div>
-      </div>
-    </div>
-
-    <!-- 成品出库:日期、客户、订单、出库单 -->
-    <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.outboundNo" placeholder="发货/出库单" clearable style="width: 150px" />
-      </el-form-item>
-    </DetailQueryBar>
-
-    <!-- 三列布局 -->
-    <div class="three-columns">
-      <!-- 左侧:仓储运营概览 -->
-      <div class="column-left">
-        <div class="overview-card">
-          <div class="card-header">
-            <span class="card-title">仓储运营概览</span>
-          </div>
-          <div class="overview-content">
-            <div class="main-kpi">
-              <div class="kpi-label">{{ l1Slots[0]?.displayName || '当前库存总量' }}</div>
-              <div class="kpi-value large">{{ Number(d(0, 42850, 0)).toLocaleString('zh-CN') }}</div>
-              <div class="kpi-trend success">
-                <el-icon><ArrowUp /></el-icon>
-                <span>{{ d(1, 1.2, 1) }}%</span>
-              </div>
-            </div>
-
-            <div class="sub-kpis">
-              <div class="sub-kpi">
-                <div class="kpi-label">{{ l1Slots[1]?.displayName || '库位利用率' }}</div>
-                <div class="kpi-value">{{ d(2, 84.5, 1) }}%</div>
-              </div>
-              <div class="sub-kpi">
-                <div class="kpi-label">{{ l1Slots[2]?.displayName || '在库周转天数' }}</div>
-                <div class="kpi-value">{{ homeS7.orderShipMatchCycle }}</div>
-              </div>
-            </div>
-
-            <div class="trend-chart">
-              <div class="chart-title">入出库趋势图</div>
-              <div id="inout-chart" class="chart-container"></div>
-            </div>
-
-            <div class="distribution-chart">
-              <div class="chart-title">库区状态分布</div>
-              <div id="distribution-chart" class="donut-container"></div>
-            </div>
-          </div>
-        </div>
-      </div>
-
-      <!-- 中间:4 个流程卡片 -->
-      <div class="column-middle">
-        <!-- 成品检验 FQC -->
-        <div class="process-card success-border">
-          <div class="card-header">
-            <div class="card-title">
-              <el-icon><Checked /></el-icon>
-              成品终检
-            </div>
-            <el-tag size="small" :type="statusTypeByRate(Number((100 - homeS7.shippingDefectPct).toFixed(1)), Number((100 - homeS7.shippingDefectTargetPct).toFixed(1)))">{{ statusTextByRate(Number((100 - homeS7.shippingDefectPct).toFixed(1)), Number((100 - homeS7.shippingDefectTargetPct).toFixed(1))) }}</el-tag>
-          </div>
-          <div class="card-content">
-            <div class="process-metrics">
-              <div class="metric-item">
-                <div class="metric-label">成品检验周期</div>
-                <div class="metric-value">
-                  <span class="value">{{ d(3, 0.8, 1) }}</span>
-                  <span class="unit">H</span>
-                </div>
-                <div class="metric-trend success">
-                  <el-icon><ArrowUp /></el-icon>
-                  <span>{{ d(4, 0.2, 1) }}</span>
-                </div>
-              </div>
-              <div class="metric-item right">
-                <div class="metric-label">成品检验合格率</div>
-                <div class="metric-value">
-                  <span class="value success">{{ (100 - homeS7.shippingDefectPct).toFixed(1) }}</span>
-                  <span class="unit">%</span>
-                </div>
-                <div class="metric-trend warning">
-                  <span>目标 {{ (100 - homeS7.shippingDefectTargetPct).toFixed(1) }}%</span>
-                </div>
-              </div>
-            </div>
-          </div>
-        </div>
-
-        <!-- 成品仓储 (上架) -->
-        <div class="process-card success-border">
-          <div class="card-header">
-            <div class="card-title">
-              <el-icon><Top /></el-icon>
-              成品仓储 (上架)
-            </div>
-            <el-tag size="small" :type="statusTypeByRate(Number(homeS7.laborEfficiency), Number(homeS7.laborEfficiencyTarget))">{{ statusTextByRate(Number(homeS7.laborEfficiency), Number(homeS7.laborEfficiencyTarget)) }}</el-tag>
-          </div>
-          <div class="card-content">
-            <div class="process-metrics">
-              <div class="metric-item">
-                <div class="metric-label">成品上架周期</div>
-                <div class="metric-value">
-                  <span class="value">{{ d(5, 1.5, 1) }}</span>
-                  <span class="unit">H</span>
-                </div>
-                <div class="metric-trend success">
-                  <el-icon><ArrowUp /></el-icon>
-                  <span>{{ d(6, 0.5, 1) }}</span>
-                </div>
-              </div>
-              <div class="metric-item right">
-                <div class="metric-label">上架人效</div>
-                <div class="metric-value">
-                  <span class="value">{{ homeS7.laborEfficiency }}</span>
-                  <span class="unit">指数</span>
-                </div>
-                <div class="metric-trend danger">
-                  <span>目标 {{ homeS7.laborEfficiencyTarget }}</span>
-                </div>
-              </div>
-            </div>
-          </div>
-        </div>
-
-        <!-- 成品仓储 (备货) -->
-        <div class="process-card" :class="statusTypeByRate(Number(homeS7.shipmentPlanRatePct), Number(homeS7.shipmentPlanRateTargetPct)) === 'danger' ? 'danger-border' : statusTypeByRate(Number(homeS7.shipmentPlanRatePct), Number(homeS7.shipmentPlanRateTargetPct)) === 'warning' ? 'warning-border' : ''">
-          <div class="card-header">
-            <div class="card-title">
-              <el-icon><ShoppingBag /></el-icon>
-              成品仓储 (备货)
-            </div>
-            <el-tag size="small" :type="statusTypeByRate(Number(homeS7.shipmentPlanRatePct), Number(homeS7.shipmentPlanRateTargetPct))">{{ statusTextByRate(Number(homeS7.shipmentPlanRatePct), Number(homeS7.shipmentPlanRateTargetPct)) }}</el-tag>
-          </div>
-          <div class="card-content">
-            <div class="process-metrics">
-              <div class="metric-item">
-                <div class="metric-label">成品备货周期</div>
-                <div class="metric-value warning">
-                  <span class="value">{{ d(7, 4.2, 1) }}</span>
-                  <span class="unit">H</span>
-                </div>
-                <div class="metric-trend danger">
-                  <el-icon><ArrowUp /></el-icon>
-                  <span>{{ d(8, 1.3, 1) }}</span>
-                </div>
-              </div>
-              <div class="metric-item right">
-                <div class="metric-label">备货满足率</div>
-                <div class="metric-value success">
-                  <span class="value">{{ homeS7.shipmentPlanRatePct }}</span>
-                  <span class="unit">%</span>
-                </div>
-                <div class="metric-trend success">
-                  <span>目标 {{ homeS7.shipmentPlanRateTargetPct }}%</span>
-                </div>
-              </div>
-            </div>
-          </div>
-        </div>
-
-        <!-- 成品仓储 (发货) -->
-        <div class="process-card success-border">
-          <div class="card-header">
-            <div class="card-title">
-              <el-icon><Van /></el-icon>
-              成品仓储 (发货)
-            </div>
-            <el-tag size="small" :type="statusTypeByRate(Number(homeS7.orderShipMatchCycleTarget - homeS7.orderShipMatchCycle + 95), 95)">{{ statusTextByRate(Number(homeS7.orderShipMatchCycleTarget - homeS7.orderShipMatchCycle + 95), 95) }}</el-tag>
-          </div>
-          <div class="card-content">
-            <div class="process-metrics">
-              <div class="metric-item">
-                <div class="metric-label">发货周期</div>
-                <div class="metric-value warning">
-                  <span class="value">{{ homeS7.orderShipMatchCycle }}</span>
-                  <span class="unit">D</span>
-                </div>
-                <div class="metric-trend warning">
-                  <span>目标 {{ homeS7.orderShipMatchCycleTarget }}D</span>
-                </div>
-              </div>
-              <div class="metric-item right">
-                <div class="metric-label">当日发货单量</div>
-                <div class="metric-value">
-                  <span class="value">{{ Number(d(9, 2197, 0)).toLocaleString('zh-CN') }}</span>
-                </div>
-                <div class="metric-trend success">
-                  <el-icon><ArrowUp /></el-icon>
-                  <span>{{ d(10, 7, 0) }}%</span>
-                </div>
-              </div>
-            </div>
-          </div>
-        </div>
-      </div>
-
-      <!-- 右侧:库位实景可视化 -->
-      <div class="column-right">
-        <div class="visualization-card">
-          <div class="card-header">
-            <span class="card-title">库位实景可视化</span>
-            <div class="legend">
-              <span class="legend-item"><span class="legend-dot idle"></span>空闲</span>
-              <span class="legend-item"><span class="legend-dot in-stock"></span>在库</span>
-              <span class="legend-item"><span class="legend-dot warning"></span>预警</span>
-            </div>
-          </div>
-          <div class="viz-content">
-            <div class="zone-header">
-              <span>A 区【成品一库】</span>
-              <span class="level">1–{{ maxLevel }} 层</span>
-            </div>
-            <div class="grid-container">
-              <div class="grid-row" v-for="row in 12" :key="row">
-                <div class="grid-cell" v-for="col in 8" :key="col" :class="getCellClass(row, col)"></div>
-              </div>
-            </div>
-            <div class="slider-container">
-              <el-slider v-model="currentLevel" :min="1" :max="maxLevel" :step="1" class="level-slider" />
-            </div>
-          </div>
-          <div class="stats-row">
-            <div class="stat-box">
-              <div class="stat-label">可用库位</div>
-              <div class="stat-value blue">{{ d(11, 142, 0) }}</div>
-            </div>
-            <div class="stat-box">
-              <div class="stat-label">待上架</div>
-              <div class="stat-value orange">{{ d(12, 28, 0) }}</div>
-            </div>
-            <div class="stat-box">
-              <div class="stat-label">异常锁库</div>
-              <div class="stat-value red">{{ d(13, 4, 0) }}</div>
-            </div>
-          </div>
-          <div class="detail-section">
-            <div class="detail-header">
-              <span class="detail-title">库位详情:A-04-12</span>
-              <el-button link size="small" class="btn-detail">详情</el-button>
-            </div>
-            <div class="detail-content">
-              <div class="detail-item">
-                <span class="detail-label">物料编号:</span>
-                <span class="detail-value">P-2026-X99</span>
-              </div>
-              <div class="detail-item">
-                <span class="detail-label">入库日期:</span>
-                <span class="detail-value">{{ lastSyncDate }}</span>
-              </div>
-              <div class="detail-item">
-                <span class="detail-label">在库时长:</span>
-                <span class="detail-value">{{ d(14, 44.5, 1) }} HRS</span>
-              </div>
-            </div>
-          </div>
-        </div>
-      </div>
-    </div>
-  </div>
+	<DynamicModuleDashboard
+		module-code="S7"
+		:extra-query-init="extraQueryInit"
+		:extra-query-mapper="mapExtraQuery"
+	>
+		<template #extra-fields="{ query }">
+			<el-form-item label="客户">
+				<SmartOpsFuzzySelect v-model="query.customer" field="customer" placeholder="客户名称/编码" width="140px" />
+			</el-form-item>
+			<el-form-item label="出库单号">
+				<SmartOpsFuzzySelect v-model="query.outboundNo" field="outboundNo" placeholder="发货/出库单" width="150px" />
+			</el-form-item>
+		</template>
+	</DynamicModuleDashboard>
 </template>
 
-<script setup>
-import { ref, computed, onMounted, onUnmounted, 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'
-import { loadHomeModuleMetrics } from './data/homeModulesSync'
-import { fetchModuleDetail, fetchHomeGrid, fetchDetailKpis } from '../api/kanbanData'
-import { AIDOP_LAYOUT_SAVED } from './utils/s4LayoutEvents'
-
-const MODULE_CODE = 'S7'
-const l1Items = ref([])
-const l2Items = ref([])
-const l1Slots = computed(() => (Array.isArray(l1Items.value) ? l1Items.value : []))
-
-async function loadLayoutKpis() {
-  try {
-    const [grid, l2] = await Promise.all([
-      fetchHomeGrid(MODULE_CODE).catch(() => null),
-      fetchDetailKpis(MODULE_CODE).catch(() => []),
-    ])
-    l1Items.value = (grid && grid.items) ? grid.items : []
-    l2Items.value = Array.isArray(l2) ? l2 : []
-  } catch { /* ignore */ }
-}
-
-function onLayoutSaved(e) {
-  const mc = String((e && e.detail && e.detail.moduleCode) || '').toUpperCase()
-  if (!mc || mc === MODULE_CODE) loadLayoutKpis()
-}
-
-const router = useRouter()
-const currentTime = ref('')
-
-const detailQuery = ref({
-  ...emptySmartOpsBaseQuery(),
-  customer: '',
-  outboundNo: '',
-})
-
-function onDetailQuery() {
-  ElMessage.success(`已应用成品仓储筛选(${summarizeSmartOpsBaseQuery(detailQuery.value)})`)
-}
-
-function onDetailQueryReset() {
-  detailQuery.value = { ...emptySmartOpsBaseQuery(), customer: '', outboundNo: '' }
-  ElMessage.info('已重置')
-}
-
-const currentLevel = ref(1)
-const maxLevel = ref(6)
-const inoutXAxis = ref(['08:00', '10:00', '12:00', '14:00', '16:00'])
-const inboundSeries = ref([120, 280, 240, 420, 380])
-const outboundSeries = ref([80, 220, 180, 350, 400])
-const distributionValues = ref([45, 25, 15, 15])
-const cellSeed = ref(0)
-const detailNums = ref([42850, 1.2, 84.5, 0.8, 0.2, 1.5, 0.5, 4.2, 1.3, 2197, 7, 142, 28, 4, 44.5])
-const lastSyncDate = ref('--')
-let inoutChart = null
-let distributionChart = null
-
-const updateTime = () => {
-  currentTime.value = new Date().toLocaleString('zh-CN', {
-    year: 'numeric',
-    month: '2-digit',
-    day: '2-digit',
-    hour: '2-digit',
-    minute: '2-digit',
-    second: '2-digit',
-    hour12: false
-  })
-}
+<script setup lang="ts" name="aidopKanbanS7">
+import DynamicModuleDashboard from './components/DynamicModuleDashboard.vue';
+import SmartOpsFuzzySelect from './components/SmartOpsFuzzySelect.vue';
 
-const d = (idx, fallback = 0, digits = 1) => {
-  const n = Number(detailNums.value[idx] ?? fallback)
-  return Number.isFinite(n) ? n.toFixed(digits) : Number(fallback).toFixed(digits)
-}
-const statusTypeByRate = (val, target = 95) => {
-  if (val >= target) return 'success'
-  if (val >= target - 5) return 'warning'
-  return 'danger'
-}
-const statusTextByRate = (val, target = 95) => {
-  if (val >= target) return '优秀'
-  if (val >= target - 5) return '警示'
-  return '严重'
-}
+const extraQueryInit = {
+	customer: '',
+	outboundNo: '',
+};
 
-// 基于数据库种子生成稳定库位状态(避免静态随机)
-const getCellClass = (row, col) => {
-  const v = (row * 17 + col * 31 + cellSeed.value) % 100
-  if (v > 88) return 'idle'
-  if (v > 75) return 'warning'
-  return 'in-stock'
+function mapExtraQuery(query: Record<string, string>) {
+	const customer = String(query.customer || '').trim();
+	const outboundNo = String(query.outboundNo || '').trim();
+	return {
+		...(customer ? { customer } : {}),
+		...(outboundNo ? { outboundNo } : {}),
+	};
 }
-
-const initInOutChart = () => {
-  const chartDom = document.getElementById('inout-chart')
-  if (!chartDom) return
-  
-  inoutChart = echarts.init(chartDom)
-  const option = {
-    tooltip: {
-      trigger: 'axis',
-      backgroundColor: 'rgba(15, 23, 42, 0.95)',
-      borderColor: '#334155',
-      textStyle: { color: '#e2e8f0' }
-    },
-    grid: { left: '8%', right: '4%', bottom: '15%', top: '15%' },
-    xAxis: {
-      type: 'category',
-      data: inoutXAxis.value,
-      axisLine: { lineStyle: { color: '#334155' } },
-      axisLabel: { color: '#64748b', fontSize: 10 }
-    },
-    yAxis: {
-      type: 'value',
-      min: 0,
-      max: 500,
-      splitLine: { lineStyle: { color: '#1e293b' } },
-      axisLabel: { color: '#64748b', fontSize: 10 }
-    },
-    series: [
-      {
-        name: '入库',
-        type: 'line',
-        smooth: true,
-        symbol: 'circle',
-        symbolSize: 6,
-        itemStyle: { color: '#60a5fa' },
-        lineStyle: { color: '#60a5fa', width: 2 },
-        data: inboundSeries.value
-      },
-      {
-        name: '出库',
-        type: 'line',
-        smooth: true,
-        symbol: 'circle',
-        symbolSize: 6,
-        itemStyle: { color: '#4ade80' },
-        lineStyle: { color: '#4ade80', width: 2 },
-        data: outboundSeries.value
-      }
-    ]
-  }
-  inoutChart.setOption(option)
-}
-
-const initDistributionChart = () => {
-  const chartDom = document.getElementById('distribution-chart')
-  if (!chartDom) return
-  
-  distributionChart = echarts.init(chartDom)
-  const option = {
-    tooltip: {
-      trigger: 'item',
-      backgroundColor: 'rgba(15, 23, 42, 0.95)',
-      borderColor: '#334155',
-      textStyle: { color: '#e2e8f0' }
-    },
-    series: [{
-      type: 'pie',
-      radius: ['50%', '70%'],
-      center: ['50%', '50%'],
-      avoidLabelOverlap: false,
-      label: {
-        show: true,
-        position: 'outside',
-        formatter: '{b}\n{d}%',
-        color: '#94a3b8',
-        fontSize: 10
-      },
-      labelLine: {
-        show: true,
-        length: 10,
-        length2: 10,
-        lineStyle: { color: '#334155' }
-      },
-      data: [
-        { value: distributionValues.value[0], name: '冷链区', itemStyle: { color: '#60a5fa' } },
-        { value: distributionValues.value[1], name: '一般区', itemStyle: { color: '#3b82f6' } },
-        { value: distributionValues.value[2], name: '待验区', itemStyle: { color: '#fbbf24' } },
-        { value: distributionValues.value[3], name: '异常区', itemStyle: { color: '#ef4444' } }
-      ]
-    }]
-  }
-  distributionChart.setOption(option)
-}
-
-const initCharts = () => {
-  nextTick(() => {
-    initInOutChart()
-    initDistributionChart()
-    
-    window.addEventListener('resize', () => {
-      inoutChart?.resize()
-      distributionChart?.resize()
-    })
-  })
-}
-
-onMounted(async () => {
-  window.addEventListener(AIDOP_LAYOUT_SAVED, onLayoutSaved)
-  loadLayoutKpis()
-  await loadHomeModuleMetrics()
-  const detail = await fetchModuleDetail('S7')
-  const l2 = detail.l2 ?? []
-  const l3 = detail.l3 ?? []
-  const merged = [...l2, ...l3]
-  if (l2.length > 0) {
-    const vals = l2.slice(0, 5).map((x) => Number(x.metricValue ?? 0))
-    lastSyncDate.value = l2[0]?.statDate ? String(l2[0].statDate).slice(0, 10) : '--'
-    inoutXAxis.value = l2.slice(0, 5).map((x, i) => (x.statDate ? String(x.statDate).slice(11, 16) : `${8 + i * 2}:00`))
-    inboundSeries.value = vals.map((v) => Math.round(v * 4))
-    outboundSeries.value = vals.map((v) => Math.round(v * 3.6))
-    distributionValues.value = [
-      Math.max(1, Math.round(vals[0] ?? 45)),
-      Math.max(1, Math.round(vals[1] ?? 25)),
-      Math.max(1, Math.round(vals[2] ?? 15)),
-      Math.max(1, Math.round(vals[3] ?? 15))
-    ]
-    cellSeed.value = Math.round(vals.reduce((s, n) => s + n, 0))
-  }
-  if (merged.length > 0) {
-    detailNums.value = merged.slice(0, 15).map((x, i) => Number(x.metricValue ?? detailNums.value[i] ?? 0))
-  }
-  updateTime()
-  setInterval(updateTime, 1000)
-  initCharts()
-})
-
-onUnmounted(() => {
-  window.removeEventListener(AIDOP_LAYOUT_SAVED, onLayoutSaved)
-})
 </script>
-
-<style scoped>
-.s7-dashboard {
-  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;
-}
-
-/* 顶部标题栏 */
-.top-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 15px;
-}
-
-.header-left {
-  display: flex;
-  align-items: center;
-  gap: 12px;
-  color: #60a5fa;
-}
-
-.page-title {
-  margin: 0;
-  font-size: 18px;
-  font-weight: 700;
-  color: #e2e8f0;
-  display: flex;
-  align-items: center;
-  gap: 12px;
-}
-
-.gold-tag {
-  background: linear-gradient(135deg, #fbbf24, #f59e0b);
-  border: none;
-  color: white;
-  font-size: 10px;
-  padding: 2px 8px;
-}
-
-.header-right {
-  display: flex;
-  align-items: center;
-  gap: 15px;
-}
-
-.current-time {
-  text-align: right;
-}
-
-.time-label {
-  font-size: 10px;
-  color: #64748b;
-}
-
-.time-value {
-  font-size: 14px;
-  font-weight: 700;
-  color: #22d3ee;
-  font-family: 'Courier New', monospace;
-}
-
-/* 三列布局 */
-.three-columns {
-  display: grid;
-  grid-template-columns: 320px 1fr 420px;
-  gap: 15px;
-}
-
-/* 通用卡片样式 */
-.overview-card, .process-card, .visualization-card {
-  background: rgba(15, 23, 42, 0.6);
-  border-radius: 8px;
-  padding: 15px;
-  border: 1px solid #1e293b;
-}
-
-.card-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 15px;
-}
-
-.card-title {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-  font-size: 13px;
-  font-weight: 700;
-  color: #e2e8f0;
-}
-
-/* 左侧列 */
-.column-left {
-  display: flex;
-  flex-direction: column;
-  gap: 15px;
-}
-
-.overview-content {
-  display: flex;
-  flex-direction: column;
-  gap: 15px;
-}
-
-.main-kpi {
-  background: rgba(30, 41, 59, 0.4);
-  border-radius: 6px;
-  padding: 15px;
-  border: 1px solid #1e293b;
-}
-
-.main-kpi .kpi-label {
-  font-size: 11px;
-  color: #64748b;
-  margin-bottom: 8px;
-}
-
-.main-kpi .kpi-value.large {
-  font-size: 36px;
-  font-weight: 700;
-  color: #e2e8f0;
-}
-
-.kpi-trend {
-  display: flex;
-  align-items: center;
-  gap: 4px;
-  font-size: 11px;
-  font-weight: 600;
-  color: #4ade80;
-  margin-top: 6px;
-}
-
-.sub-kpis {
-  display: grid;
-  grid-template-columns: 1fr 1fr;
-  gap: 12px;
-}
-
-.sub-kpi {
-  background: rgba(30, 41, 59, 0.4);
-  border-radius: 6px;
-  padding: 12px;
-  border: 1px solid #1e293b;
-}
-
-.sub-kpi .kpi-label {
-  font-size: 11px;
-  color: #64748b;
-  margin-bottom: 6px;
-}
-
-.sub-kpi .kpi-value {
-  font-size: 20px;
-  font-weight: 700;
-  color: #e2e8f0;
-}
-
-.trend-chart, .distribution-chart {
-  background: rgba(30, 41, 59, 0.4);
-  border-radius: 6px;
-  padding: 12px;
-  border: 1px solid #1e293b;
-}
-
-.chart-title {
-  font-size: 11px;
-  color: #64748b;
-  margin-bottom: 10px;
-}
-
-.chart-container {
-  width: 100%;
-  height: 180px;
-}
-
-.donut-container {
-  width: 100%;
-  height: 200px;
-}
-
-/* 中间列 */
-.column-middle {
-  display: flex;
-  flex-direction: column;
-  gap: 12px;
-}
-
-.process-card {
-  background: rgba(15, 23, 42, 0.6);
-  border-radius: 8px;
-  padding: 15px;
-  border-left: 3px solid;
-}
-
-.process-card.success-border { border-left-color: #4ade80; }
-.process-card.warning-border { border-left-color: #fbbf24; }
-
-.process-metrics {
-  display: flex;
-  justify-content: space-between;
-}
-
-.metric-item {
-  display: flex;
-  flex-direction: column;
-  gap: 6px;
-}
-
-.metric-item.right {
-  text-align: right;
-}
-
-.metric-label {
-  font-size: 11px;
-  color: #64748b;
-}
-
-.metric-value {
-  display: flex;
-  align-items: baseline;
-  gap: 6px;
-  justify-content: inherit;
-}
-
-.metric-value .value {
-  font-size: 24px;
-  font-weight: 700;
-  color: #e2e8f0;
-}
-
-.metric-value .value.success { color: #4ade80; }
-.metric-value .value.warning { color: #fbbf24; }
-
-.metric-value .unit {
-  font-size: 12px;
-  color: #64748b;
-}
-
-.metric-trend {
-  display: flex;
-  align-items: center;
-  gap: 4px;
-  font-size: 11px;
-  font-weight: 600;
-  justify-content: inherit;
-}
-
-.metric-trend.success { color: #4ade80; }
-.metric-trend.danger { color: #ef4444; }
-
-/* 右侧列 */
-.column-right {
-  display: flex;
-  flex-direction: column;
-  gap: 15px;
-}
-
-.viz-content {
-  display: flex;
-  flex-direction: column;
-  gap: 12px;
-}
-
-.zone-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  font-size: 11px;
-  color: #64748b;
-}
-
-.zone-header .level {
-  color: #94a3b8;
-}
-
-.grid-container {
-  display: flex;
-  flex-direction: column;
-  gap: 4px;
-  background: rgba(30, 41, 59, 0.4);
-  border-radius: 6px;
-  padding: 8px;
-  border: 1px solid #1e293b;
-}
-
-.grid-row {
-  display: grid;
-  grid-template-columns: repeat(8, 1fr);
-  gap: 4px;
-}
-
-.grid-cell {
-  aspect-ratio: 1;
-  border-radius: 4px;
-  background: #1e293b;
-}
-
-.grid-cell.idle { background: #1e293b; }
-.grid-cell.in-stock { background: #3b82f6; }
-.grid-cell.warning { background: #d97706; }
-
-.slider-container {
-  padding: 0 10px;
-}
-
-.level-slider {
-  --el-slider-main-bg-color: #60a5fa;
-  --el-slider-runway-bg-color: #1e293b;
-}
-
-.stats-row {
-  display: grid;
-  grid-template-columns: repeat(3, 1fr);
-  gap: 10px;
-}
-
-.stat-box {
-  background: rgba(30, 41, 59, 0.4);
-  border-radius: 6px;
-  padding: 12px;
-  text-align: center;
-  border: 1px solid #1e293b;
-}
-
-.stat-label {
-  font-size: 11px;
-  color: #64748b;
-  margin-bottom: 6px;
-}
-
-.stat-value {
-  font-size: 20px;
-  font-weight: 700;
-}
-
-.stat-value.blue { color: #60a5fa; }
-.stat-value.orange { color: #fbbf24; }
-.stat-value.red { color: #ef4444; }
-
-.detail-section {
-  background: rgba(30, 41, 59, 0.4);
-  border-radius: 6px;
-  padding: 12px;
-  border: 1px solid #1e293b;
-}
-
-.detail-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 10px;
-}
-
-.detail-title {
-  font-size: 11px;
-  font-weight: 700;
-  color: #e2e8f0;
-}
-
-.btn-detail {
-  color: #60a5fa;
-  font-size: 10px;
-}
-
-.detail-content {
-  display: flex;
-  flex-direction: column;
-  gap: 6px;
-}
-
-.detail-item {
-  display: flex;
-  justify-content: space-between;
-  font-size: 10px;
-}
-
-.detail-label {
-  color: #64748b;
-}
-
-.detail-value {
-  color: #e2e8f0;
-}
-
-/* Element Plus 覆盖 */
-:deep(.el-tag) {
-  font-size: 10px;
-  padding: 2px 8px;
-  border-radius: 4px;
-}
-
-:deep(.el-tag--success) {
-  background: rgba(74, 222, 128, 0.2);
-  border-color: rgba(74, 222, 128, 0.3);
-  color: #4ade80;
-}
-
-:deep(.el-tag--warning) {
-  background: rgba(251, 191, 36, 0.2);
-  border-color: rgba(251, 191, 36, 0.3);
-  color: #fbbf24;
-}
-
-:deep(.el-slider__bar) {
-  background: #60a5fa;
-}
-
-:deep(.el-slider__button) {
-  border-color: #60a5fa;
-}
-</style>

+ 2 - 114
Web/src/views/aidop/kanban/s9.vue

@@ -1,119 +1,7 @@
 <template>
-	<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" />
-			<el-table-column prop="target" label="目标值" width="120" />
-			<el-table-column prop="status" label="状态" width="120">
-				<template #default="{ row }">
-					<el-tag :type="row.status === '达成' ? 'success' : row.status === '风险' ? 'warning' : 'danger'">{{ row.status }}</el-tag>
-				</template>
-			</el-table-column>
-		</el-table>
-	</AidopDemoShell>
+	<DynamicModuleDashboard module-code="S9" />
 </template>
 
 <script setup lang="ts" name="aidopKanbanS9">
-import { onMounted, onUnmounted, ref, computed } from 'vue';
-import { ElMessage } from 'element-plus';
-import AidopDemoShell from '../components/AidopDemoShell.vue';
-import { fetchHomeGrid, fetchDetailKpis } from '../api/kanbanData';
-import { AIDOP_LAYOUT_SAVED } from './utils/s4LayoutEvents';
-import DetailQueryBar from './components/DetailQueryBar.vue';
-import SmartOpsBaseQueryFields from './components/SmartOpsBaseQueryFields.vue';
-import {
-	emptySmartOpsBaseQuery,
-	blobMatchesSmartOpsBase,
-	baseQueryToApiParams,
-	summarizeSmartOpsBaseQuery,
-	type SmartOpsBaseQuery,
-} from './utils/smartOpsBaseQuery';
-
-const MODULE_CODE = 'S9';
-
-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: '未达成' },
-]);
-
-const rows = computed(() =>
-	rowsAll.value.filter((x) =>
-		blobMatchesSmartOpsBase(baseQuery.value, `${x.metric} ${x.value} ${x.target} ${x.status}`)
-	)
-);
-
-function mapStatus(color?: string | null): string {
-	const c = String(color || '').toLowerCase();
-	if (c === 'green') return '达成';
-	if (c === 'yellow') return '风险';
-	if (!c) return '—';
-	return '未达成';
-}
-
-async function loadRows() {
-	try {
-		const extra = baseQueryToApiParams(baseQuery.value);
-		const [grid, l2] = await Promise.all([
-			fetchHomeGrid(MODULE_CODE, 1, extra as Record<string, unknown>).catch(() => null),
-			fetchDetailKpis(MODULE_CODE, 1, undefined, extra as Record<string, unknown>).catch(() => [] as any[]),
-		]);
-		const l1 = (grid && Array.isArray((grid as any).items)) ? (grid as any).items as any[] : [];
-		const l2Arr = Array.isArray(l2) ? l2 as any[] : [];
-		const merged = [...l1, ...l2Arr];
-		if (merged.length === 0) return;
-		rowsAll.value = merged.map((x) => ({
-			metric: String(x.displayName || x.metricName || x.metricCode || ''),
-			value: `${x.metricValue ?? 0}`,
-			target: `${x.targetValue ?? 0}`,
-			status: mapStatus(x.statusColor),
-		}));
-	} catch {
-		/* keep defaults */
-	}
-}
-
-function onQuery() {
-	loadRows();
-	ElMessage.success(`已查询(${summarizeSmartOpsBaseQuery(baseQuery.value)})`);
-}
-
-function onReset() {
-	baseQuery.value = emptySmartOpsBaseQuery();
-	loadRows();
-	ElMessage.info('已重置');
-}
-
-function onLayoutSaved(e: Event) {
-	const mc = String(((e as CustomEvent).detail && (e as CustomEvent).detail.moduleCode) || '').toUpperCase();
-	if (!mc || mc === MODULE_CODE) loadRows();
-}
-
-onMounted(() => {
-	window.addEventListener(AIDOP_LAYOUT_SAVED, onLayoutSaved);
-	loadRows();
-});
-
-onUnmounted(() => {
-	window.removeEventListener(AIDOP_LAYOUT_SAVED, onLayoutSaved);
-});
+import DynamicModuleDashboard from './components/DynamicModuleDashboard.vue';
 </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>

+ 2 - 64
Web/src/views/dashboard/home.vue

@@ -29,56 +29,7 @@
         @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>
+        <SmartOpsBaseQueryFields v-model="dashboardBaseQuery" compact />
       </DetailQueryBar>
       <div class="top-bar__legend">
         <span class="legend-item"><i class="dot dot--green" />达成目标</span>
@@ -262,8 +213,8 @@ import {
 } from '@element-plus/icons-vue'
 import * as echarts from 'echarts'
 import { fetchS8Alerts } from '../aidop/api/kanbanData'
-import { loadSmartOpsFilterOptions } from '../aidop/kanban/utils/smartOpsFilterOptionsCache'
 import DetailQueryBar from '../aidop/kanban/components/DetailQueryBar.vue'
+import SmartOpsBaseQueryFields from '../aidop/kanban/components/SmartOpsBaseQueryFields.vue'
 import {
   emptySmartOpsBaseQuery,
   baseQueryToApiParams,
@@ -276,10 +227,6 @@ import ChatBIFloat from '../aidop/chatbi/ChatBIFloat.vue'
 const router = useRouter()
 const gridMainRef = ref(null)
 const dashboardBaseQuery = ref(emptySmartOpsBaseQuery())
-const filterOptionsLoading = ref(true)
-const filterProductOptions = ref([])
-const filterOrderNoOptions = ref([])
-const filterLineOptions = ref([])
 const dashboardVersion = `V${__NEXT_VERSION__}`
 
 /** 供 HomeModuleKpiCard 的 extra prop 透传基础查询参数 */
@@ -454,15 +401,6 @@ 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
-  }
   loadS8Data()
   tickHeaderClock()
   headerClockTimer = window.setInterval(tickHeaderClock, 1000)

+ 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.159</AssemblyVersion>
-    <FileVersion>1.0.159</FileVersion>
-    <Version>1.0.159</Version>
+    <AssemblyVersion>1.0.160</AssemblyVersion>
+    <FileVersion>1.0.160</FileVersion>
+    <Version>1.0.160</Version>
   </PropertyGroup>
 
   <ItemGroup>

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

@@ -0,0 +1,312 @@
+using Admin.NET.Plugin.AiDOP.Infrastructure;
+using SqlSugar;
+
+namespace Admin.NET.Plugin.AiDOP.Controllers;
+
+public partial class AidopKanbanController
+{
+    /// <summary>
+    /// 智慧运营看板查询条件下拉:支持按字段远程模糊搜索。
+    /// </summary>
+    [HttpGet("smart-ops-filter-search")]
+    public async Task<IActionResult> GetSmartOpsFilterSearch(
+        [FromQuery] string field,
+        [FromQuery] string? keyword = null,
+        [FromQuery] long factoryId = 1,
+        [FromQuery] int limit = 30)
+    {
+        _ = factoryId;
+        var tenantId = AidopTenantHelper.GetTenantId(HttpContext);
+        var cap = Math.Clamp(limit, 1, 100);
+        var kw = string.IsNullOrWhiteSpace(keyword) ? null : keyword.Trim();
+        var items = await SearchSmartOpsFilterFieldAsync(field, tenantId, kw, cap);
+        return Ok(new { items });
+    }
+
+    private async Task<List<string>> SearchSmartOpsFilterFieldAsync(string? field, long tenantId, string? keyword, int limit)
+    {
+        var key = field?.Trim().ToLowerInvariant() ?? string.Empty;
+        return key switch
+        {
+            "product" => await SearchProductsAsync(tenantId, keyword, limit),
+            "orderno" or "order_no" => await SearchOrderNosAsync(tenantId, keyword, limit),
+            "productionline" or "production_line" => await SearchProductionLinesAsync(tenantId, keyword, limit),
+            "customer" => await SearchCustomersAsync(tenantId, keyword, limit),
+            "supplier" => await SearchSuppliersAsync(tenantId, keyword, limit),
+            "material" => await SearchMaterialsAsync(tenantId, keyword, limit),
+            "pono" or "po_no" => await SearchPoNosAsync(tenantId, keyword, limit),
+            "warehouse" => await SearchWarehousesAsync(tenantId, keyword, limit),
+            "workorder" or "work_order" => await SearchWorkOrdersAsync(tenantId, keyword, limit),
+            "equipment" => await SearchEquipmentsAsync(tenantId, keyword, limit),
+            "outboundno" or "outbound_no" => await SearchOutboundNosAsync(tenantId, keyword, limit),
+            _ => new List<string>()
+        };
+    }
+
+    private static List<SugarParameter> BuildKwParams(long tenantId, string? keyword, int limit)
+    {
+        var pars = new List<SugarParameter>
+        {
+            new("@TenantId", tenantId),
+            new("@Limit", limit),
+        };
+        if (string.IsNullOrWhiteSpace(keyword))
+            pars.Add(new("@Kw", DBNull.Value));
+        else
+            pars.Add(new("@Kw", $"%{keyword.Trim()}%"));
+        return pars;
+    }
+
+    private async Task<List<string>> QueryOptionValuesAsync(string sql, long tenantId, string? keyword, int limit)
+    {
+        try
+        {
+            var rows = await _db.Ado.SqlQueryAsync<SmartOpsOptionRow>(sql, BuildKwParams(tenantId, keyword, limit));
+            return rows
+                .Select(x => x.Value?.Trim())
+                .Where(s => !string.IsNullOrWhiteSpace(s))
+                .Select(s => s!)
+                .Distinct(StringComparer.Ordinal)
+                .OrderBy(x => x, StringComparer.Ordinal)
+                .Take(limit)
+                .ToList();
+        }
+        catch
+        {
+            return new List<string>();
+        }
+    }
+
+    private async Task<List<string>> SearchProductsAsync(long tenantId, string? keyword, int limit)
+    {
+        var merged = new HashSet<string>(StringComparer.Ordinal);
+        var sqlList = new[]
+        {
+            """
+            SELECT TRIM(IFNULL(NULLIF(e.item_name, ''), e.item_number)) AS Value
+            FROM crm_seorder o
+            INNER JOIN crm_seorderentry e ON e.seorder_id = o.Id
+            WHERE o.tenant_id = @TenantId
+              AND IFNULL(o.IsDeleted, 0) = 0
+              AND IFNULL(e.IsDeleted, 0) = 0
+              AND TRIM(IFNULL(NULLIF(e.item_name, ''), e.item_number)) <> ''
+              AND (@Kw IS NULL OR e.item_name LIKE @Kw OR e.item_number LIKE @Kw)
+            GROUP BY TRIM(IFNULL(NULLIF(e.item_name, ''), e.item_number))
+            ORDER BY Value
+            LIMIT @Limit
+            """,
+            """
+            SELECT TRIM(IFNULL(NULLIF(ItemName, ''), ItemNum)) AS Value
+            FROM WorkOrdMaster
+            WHERE tenant_id = @TenantId
+              AND TRIM(IFNULL(NULLIF(ItemName, ''), ItemNum)) <> ''
+              AND (@Kw IS NULL OR ItemName LIKE @Kw OR ItemNum LIKE @Kw)
+            GROUP BY TRIM(IFNULL(NULLIF(ItemName, ''), ItemNum))
+            ORDER BY Value
+            LIMIT @Limit
+            """,
+            """
+            SELECT TRIM(item_name) AS Value
+            FROM bd_material
+            WHERE IFNULL(IsDeleted, 0) = 0
+              AND TRIM(IFNULL(item_name, '')) <> ''
+              AND (@Kw IS NULL OR item_name LIKE @Kw OR item_number LIKE @Kw)
+            GROUP BY TRIM(item_name)
+            ORDER BY Value
+            LIMIT @Limit
+            """
+        };
+        foreach (var sql in sqlList)
+        {
+            foreach (var v in await QueryOptionValuesAsync(sql, tenantId, keyword, limit))
+                merged.Add(v);
+            if (merged.Count >= limit) break;
+        }
+        return merged.OrderBy(x => x, StringComparer.Ordinal).Take(limit).ToList();
+    }
+
+    private Task<List<string>> SearchOrderNosAsync(long tenantId, string? keyword, int limit) =>
+        QueryOptionValuesAsync(
+            """
+            SELECT TRIM(IFNULL(o.bill_no, '')) AS Value
+            FROM crm_seorder o
+            WHERE o.tenant_id = @TenantId
+              AND IFNULL(o.IsDeleted, 0) = 0
+              AND TRIM(IFNULL(o.bill_no, '')) <> ''
+              AND (@Kw IS NULL OR o.bill_no LIKE @Kw)
+            GROUP BY TRIM(IFNULL(o.bill_no, ''))
+            ORDER BY Value
+            LIMIT @Limit
+            """,
+            tenantId, keyword, limit);
+
+    private Task<List<string>> SearchProductionLinesAsync(long tenantId, string? keyword, int limit) =>
+        QueryOptionValuesAsync(
+            """
+            SELECT TRIM(IFNULL(`Line`, '')) AS Value
+            FROM LineMaster
+            WHERE tenant_id = @TenantId
+              AND IFNULL(IsActive, 1) <> 0
+              AND TRIM(IFNULL(`Line`, '')) <> ''
+              AND (@Kw IS NULL OR `Line` LIKE @Kw)
+            GROUP BY TRIM(IFNULL(`Line`, ''))
+            ORDER BY Value
+            LIMIT @Limit
+            """,
+            tenantId, keyword, limit);
+
+    private async Task<List<string>> SearchCustomersAsync(long tenantId, string? keyword, int limit)
+    {
+        var merged = new HashSet<string>(StringComparer.Ordinal);
+        var sqlList = new[]
+        {
+            """
+            SELECT TRIM(COALESCE(NULLIF(o.custom_name, ''), o.custom_no)) AS Value
+            FROM crm_seorder o
+            WHERE o.tenant_id = @TenantId
+              AND IFNULL(o.IsDeleted, 0) = 0
+              AND TRIM(COALESCE(NULLIF(o.custom_name, ''), o.custom_no)) <> ''
+              AND (@Kw IS NULL OR o.custom_name LIKE @Kw OR o.custom_no LIKE @Kw)
+            GROUP BY TRIM(COALESCE(NULLIF(o.custom_name, ''), o.custom_no))
+            ORDER BY Value
+            LIMIT @Limit
+            """,
+            """
+            SELECT TRIM(COALESCE(NULLIF(SortName, ''), Cust)) AS Value
+            FROM CustMaster
+            WHERE TRIM(COALESCE(NULLIF(SortName, ''), Cust)) <> ''
+              AND (@Kw IS NULL OR SortName LIKE @Kw OR Cust LIKE @Kw)
+            GROUP BY TRIM(COALESCE(NULLIF(SortName, ''), Cust))
+            ORDER BY Value
+            LIMIT @Limit
+            """
+        };
+        foreach (var sql in sqlList)
+        {
+            foreach (var v in await QueryOptionValuesAsync(sql, tenantId, keyword, limit))
+                merged.Add(v);
+            if (merged.Count >= limit) break;
+        }
+        return merged.OrderBy(x => x, StringComparer.Ordinal).Take(limit).ToList();
+    }
+
+    private Task<List<string>> SearchSuppliersAsync(long tenantId, string? keyword, int limit) =>
+        QueryOptionValuesAsync(
+            """
+            SELECT TRIM(COALESCE(NULLIF(supplier_name, ''), supplier_code)) AS Value
+            FROM dwd_supplier_delivery
+            WHERE tenant_id = @TenantId
+              AND TRIM(COALESCE(NULLIF(supplier_name, ''), supplier_code)) <> ''
+              AND (@Kw IS NULL OR supplier_name LIKE @Kw OR supplier_code LIKE @Kw)
+            GROUP BY TRIM(COALESCE(NULLIF(supplier_name, ''), supplier_code))
+            ORDER BY Value
+            LIMIT @Limit
+            """,
+            tenantId, keyword, limit);
+
+    private Task<List<string>> SearchMaterialsAsync(long tenantId, string? keyword, int limit) =>
+        QueryOptionValuesAsync(
+            """
+            SELECT TRIM(COALESCE(NULLIF(item_name, ''), item_number)) AS Value
+            FROM bd_material
+            WHERE IFNULL(IsDeleted, 0) = 0
+              AND TRIM(COALESCE(NULLIF(item_name, ''), item_number)) <> ''
+              AND (@Kw IS NULL OR item_name LIKE @Kw OR item_number LIKE @Kw)
+            GROUP BY TRIM(COALESCE(NULLIF(item_name, ''), item_number))
+            ORDER BY Value
+            LIMIT @Limit
+            """,
+            tenantId, keyword, limit);
+
+    private Task<List<string>> SearchPoNosAsync(long tenantId, string? keyword, int limit) =>
+        QueryOptionValuesAsync(
+            """
+            SELECT TRIM(IFNULL(po_no, '')) AS Value
+            FROM dwd_supplier_delivery
+            WHERE tenant_id = @TenantId
+              AND TRIM(IFNULL(po_no, '')) <> ''
+              AND (@Kw IS NULL OR po_no LIKE @Kw)
+            GROUP BY TRIM(IFNULL(po_no, ''))
+            ORDER BY Value
+            LIMIT @Limit
+            """,
+            tenantId, keyword, limit);
+
+    private async Task<List<string>> SearchWarehousesAsync(long tenantId, string? keyword, int limit)
+    {
+        var merged = new HashSet<string>(StringComparer.Ordinal);
+        var sqlList = new[]
+        {
+            """
+            SELECT TRIM(IFNULL(Location, '')) AS Value
+            FROM WorkOrdMaster
+            WHERE tenant_id = @TenantId
+              AND TRIM(IFNULL(Location, '')) <> ''
+              AND (@Kw IS NULL OR Location LIKE @Kw)
+            GROUP BY TRIM(IFNULL(Location, ''))
+            ORDER BY Value
+            LIMIT @Limit
+            """,
+            """
+            SELECT TRIM(IFNULL(location, '')) AS Value
+            FROM LocationMaster
+            WHERE tenant_id = @TenantId
+              AND IFNULL(is_active, 1) <> 0
+              AND TRIM(IFNULL(location, '')) <> ''
+              AND (@Kw IS NULL OR location LIKE @Kw OR descr LIKE @Kw)
+            GROUP BY TRIM(IFNULL(location, ''))
+            ORDER BY Value
+            LIMIT @Limit
+            """
+        };
+        foreach (var sql in sqlList)
+        {
+            foreach (var v in await QueryOptionValuesAsync(sql, tenantId, keyword, limit))
+                merged.Add(v);
+            if (merged.Count >= limit) break;
+        }
+        return merged.OrderBy(x => x, StringComparer.Ordinal).Take(limit).ToList();
+    }
+
+    private Task<List<string>> SearchWorkOrdersAsync(long tenantId, string? keyword, int limit) =>
+        QueryOptionValuesAsync(
+            """
+            SELECT TRIM(IFNULL(WorkOrd, '')) AS Value
+            FROM WorkOrdMaster
+            WHERE tenant_id = @TenantId
+              AND TRIM(IFNULL(WorkOrd, '')) <> ''
+              AND (@Kw IS NULL OR WorkOrd LIKE @Kw)
+            GROUP BY TRIM(IFNULL(WorkOrd, ''))
+            ORDER BY Value
+            LIMIT @Limit
+            """,
+            tenantId, keyword, limit);
+
+    private Task<List<string>> SearchEquipmentsAsync(long tenantId, string? keyword, int limit) =>
+        QueryOptionValuesAsync(
+            """
+            SELECT TRIM(IFNULL(Machine, '')) AS Value
+            FROM WorkOrdRouting
+            WHERE tenant_id = @TenantId
+              AND TRIM(IFNULL(Machine, '')) <> ''
+              AND (@Kw IS NULL OR Machine LIKE @Kw)
+            GROUP BY TRIM(IFNULL(Machine, ''))
+            ORDER BY Value
+            LIMIT @Limit
+            """,
+            tenantId, keyword, limit);
+
+    private Task<List<string>> SearchOutboundNosAsync(long tenantId, string? keyword, int limit) =>
+        QueryOptionValuesAsync(
+            """
+            SELECT TRIM(IFNULL(shipment_no, '')) AS Value
+            FROM dwd_supplier_delivery
+            WHERE tenant_id = @TenantId
+              AND TRIM(IFNULL(shipment_no, '')) <> ''
+              AND (@Kw IS NULL OR shipment_no LIKE @Kw)
+            GROUP BY TRIM(IFNULL(shipment_no, ''))
+            ORDER BY Value
+            LIMIT @Limit
+            """,
+            tenantId, keyword, limit);
+}