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