2026-05-11 15:44:37 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="actual-container">
|
|
|
|
|
|
<!-- 顶部实绩列表 (PLTCM_PDO_EXCOIL) -->
|
|
|
|
|
|
<div class="top-section">
|
|
|
|
|
|
<el-table
|
|
|
|
|
|
ref="excoilTable"
|
|
|
|
|
|
:data="excoilRows"
|
|
|
|
|
|
size="mini"
|
|
|
|
|
|
highlight-current-row
|
|
|
|
|
|
border
|
|
|
|
|
|
:height="topTableHeight"
|
|
|
|
|
|
style="width:100%"
|
|
|
|
|
|
@row-click="handleRowClick"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-table-column label="子卷号" min-width="110" show-overflow-tooltip>
|
|
|
|
|
|
<template slot-scope="{ row }">{{ row.EXCOILID || row.excoilid }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="热卷号" min-width="100" show-overflow-tooltip>
|
|
|
|
|
|
<template slot-scope="{ row }">{{ row.HOT_COILID || row.hot_coilid || '—' }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="班" width="40" align="center">
|
|
|
|
|
|
<template slot-scope="{ row }">{{ row.SHIFT || row.shift || '—' }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="组" width="40" align="center">
|
|
|
|
|
|
<template slot-scope="{ row }">{{ row.CREW || row.crew || '—' }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="钢种" min-width="80" show-overflow-tooltip>
|
|
|
|
|
|
<template slot-scope="{ row }">{{ row.ORDER_QUALITY || row.order_quality || row.GRADE || row.grade || '—' }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="来料厚度" width="68" align="right">
|
|
|
|
|
|
<template slot-scope="{ row }">{{ row.ENTRY_THICK || row.entry_thick || '—' }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="出口厚度" width="68" align="right">
|
|
|
|
|
|
<template slot-scope="{ row }">{{ row.EXIT_THICK || row.exit_thick || '—' }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="偏差上限" width="60" align="right">
|
|
|
|
|
|
<template slot-scope="{ row }">{{ row.EXIT_POS_DEV || row.exit_pos_dev || '0' }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="偏差下限" width="60" align="right">
|
|
|
|
|
|
<template slot-scope="{ row }">{{ row.EXIT_NEG_DEV || row.exit_neg_dev || '0' }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="来料宽度" width="60" align="right">
|
|
|
|
|
|
<template slot-scope="{ row }">{{ row.ENTRY_WIDTH || row.entry_width || '—' }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="出口宽度" width="60" align="right">
|
|
|
|
|
|
<template slot-scope="{ row }">{{ row.EXIT_WIDTH || row.exit_width || '—' }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="来料重量" width="60" align="right">
|
|
|
|
|
|
<template slot-scope="{ row }">{{ row.USED_ENTRY_WEIGHT || row.used_entry_weight || row.ENTRY_WEIGHT || row.entry_weight || '—' }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="称重重量" width="60" align="right">
|
|
|
|
|
|
<template slot-scope="{ row }">{{ row.MEAS_EXIT_WEIGHT || row.meas_exit_weight || '—' }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="包装要求" width="60">
|
|
|
|
|
|
<template slot-scope="{ row }">{{ row.PARK_TYPE || row.park_type || '—' }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="切边要求" width="60">
|
|
|
|
|
|
<template slot-scope="{ row }">{{ row.SIDE_TRIM || row.side_trim || '—' }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="成品质量" width="58" align="right">
|
|
|
|
|
|
<template slot-scope="{ row }">{{ row.QUALITY || row.quality || '—' }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="成品长度" width="60" align="right">
|
|
|
|
|
|
<template slot-scope="{ row }">{{ row.EXIT_LENGTH || row.exit_length || '—' }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="吨钢长度" width="60" align="right">
|
|
|
|
|
|
<template slot-scope="{ row }">{{ calcLengthPerTon(row) }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="下线时间" width="140">
|
|
|
|
|
|
<template slot-scope="{ row }">{{ formatDate(row.END_DATE || row.end_date) }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="状态" width="68" align="center">
|
|
|
|
|
|
<template slot-scope="{ row }">
|
|
|
|
|
|
<el-tag type="primary" size="mini" effect="plain">{{ row.STATUS || row.status || '产出' }}</el-tag>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
<div class="table-pagination">
|
|
|
|
|
|
<el-pagination
|
|
|
|
|
|
small layout="total, prev, pager, next"
|
|
|
|
|
|
:total="pagination.total"
|
|
|
|
|
|
:page-size="pagination.pageSize"
|
|
|
|
|
|
:current-page="pagination.page"
|
|
|
|
|
|
@current-change="handlePageChange"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 底部图表区域 -->
|
|
|
|
|
|
<div class="bottom-section">
|
|
|
|
|
|
<div class="chart-section">
|
|
|
|
|
|
<el-tabs v-model="activeTab" size="small" class="chart-tabs" @tab-click="handleTabSwitch">
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ① 趋势参数:左树形目录 + 右单图 -->
|
|
|
|
|
|
<el-tab-pane label="趋势参数" name="trend">
|
|
|
|
|
|
<div v-if="!selectedRow" class="no-data-hint">请在上方选择钢卷</div>
|
|
|
|
|
|
<div v-else-if="segLoading" class="no-data-hint">加载中…</div>
|
|
|
|
|
|
<div v-else-if="!segData" class="no-data-hint">暂无 SEG 数据</div>
|
|
|
|
|
|
<div v-else class="trend-layout">
|
|
|
|
|
|
<!-- 左侧目录树 -->
|
|
|
|
|
|
<div class="trend-tree">
|
|
|
|
|
|
<div v-for="group in trendGroups" :key="group.label" class="tree-group">
|
|
|
|
|
|
<div class="tree-group-label" @click="toggleGroup(group.label)">
|
|
|
|
|
|
<i :class="expandedGroups[group.label] ? 'el-icon-caret-bottom' : 'el-icon-caret-right'" />
|
|
|
|
|
|
{{ group.label }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-show="expandedGroups[group.label]" class="tree-children">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="item in group.children"
|
|
|
|
|
|
:key="item.col"
|
|
|
|
|
|
class="tree-item"
|
|
|
|
|
|
:class="{ active: selectedTrendParam && selectedTrendParam.col === item.col }"
|
|
|
|
|
|
@click="selectTrendParam(item)"
|
|
|
|
|
|
>{{ item.label }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<!-- 右侧图表 -->
|
|
|
|
|
|
<div class="trend-chart-area">
|
|
|
|
|
|
<div v-if="!selectedTrendParam" class="no-data-hint">← 点击左侧参数查看曲线</div>
|
|
|
|
|
|
<!-- 保持 DOM 存在,仅用 v-show 控制显示,避免 ref 失效 -->
|
|
|
|
|
|
<div ref="trendSingleChart" :style="{ display: selectedTrendParam ? 'block' : 'none', height: '100%', width: '100%' }" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ② 厚度曲线 -->
|
|
|
|
|
|
<el-tab-pane label="厚度曲线" name="thickness">
|
|
|
|
|
|
<div v-if="!selectedRow" class="no-data-hint">请在上方选择钢卷</div>
|
|
|
|
|
|
<div v-else-if="realtimeLoading" class="no-data-hint">加载中…</div>
|
|
|
|
|
|
<div v-else-if="!gaugeRows || !gaugeRows.length" class="no-data-hint">暂无厚度数据</div>
|
|
|
|
|
|
<div v-else class="charts-scroll charts-grid">
|
|
|
|
|
|
<div ref="chartGauge1" class="chart-box" />
|
|
|
|
|
|
<div ref="chartGauge2" class="chart-box" />
|
|
|
|
|
|
<div ref="chartGauge3" class="chart-box" />
|
|
|
|
|
|
<div ref="chartGauge4" class="chart-box" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
|
2026-05-12 17:15:29 +08:00
|
|
|
|
<!-- ③ 带钢板形 -->
|
2026-05-11 15:44:37 +08:00
|
|
|
|
<el-tab-pane label="带钢板形" name="flatness3d">
|
|
|
|
|
|
<div v-if="!selectedRow" class="no-data-hint">请在上方选择钢卷</div>
|
|
|
|
|
|
<div v-else-if="realtimeLoading" class="no-data-hint">加载中…</div>
|
|
|
|
|
|
<div v-else-if="!shapeRows || !shapeRows.length" class="no-data-hint">暂无板形数据</div>
|
|
|
|
|
|
<div v-else class="charts-scroll">
|
|
|
|
|
|
<div ref="chartFlatness3d" class="chart-box chart-box-tall" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ④ 板形曲线:一行两图 -->
|
|
|
|
|
|
<el-tab-pane label="板形曲线" name="flatness">
|
|
|
|
|
|
<div v-if="!selectedRow" class="no-data-hint">请在上方选择钢卷</div>
|
|
|
|
|
|
<div v-else-if="realtimeLoading" class="no-data-hint">加载中…</div>
|
|
|
|
|
|
<div v-else-if="!shapeRows || !shapeRows.length" class="no-data-hint">暂无板形数据</div>
|
|
|
|
|
|
<div v-else class="charts-scroll charts-grid">
|
|
|
|
|
|
<div ref="chartFlatDev" class="chart-box" />
|
|
|
|
|
|
<div ref="chartTilt" class="chart-box" />
|
|
|
|
|
|
<div ref="chartWrBend" class="chart-box" />
|
|
|
|
|
|
<div ref="chartIrBend" class="chart-box" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
</el-tabs>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 右侧查找面板 -->
|
|
|
|
|
|
<div class="search-panel">
|
|
|
|
|
|
<div class="panel-title">查找</div>
|
|
|
|
|
|
<div class="search-type-group">
|
|
|
|
|
|
<el-radio v-model="searchType" label="coil">按钢卷号</el-radio>
|
|
|
|
|
|
<div v-if="searchType === 'coil'" class="search-field">
|
|
|
|
|
|
<span class="search-label">钢卷号:</span>
|
|
|
|
|
|
<el-input v-model="searchCoilId" size="mini" style="width:140px" placeholder="EXCOILID" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="search-type-group">
|
|
|
|
|
|
<el-radio v-model="searchType" label="time">按时间</el-radio>
|
|
|
|
|
|
<template v-if="searchType === 'time'">
|
|
|
|
|
|
<div class="search-field">
|
|
|
|
|
|
<span class="search-label">开始时间:</span>
|
|
|
|
|
|
<el-date-picker v-model="searchStartDate" type="datetime" size="mini" style="width:160px"
|
|
|
|
|
|
value-format="yyyy-MM-dd HH:mm:ss" placeholder="开始时间" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="search-field">
|
|
|
|
|
|
<span class="search-label">结束时间:</span>
|
|
|
|
|
|
<el-date-picker v-model="searchEndDate" type="datetime" size="mini" style="width:160px"
|
|
|
|
|
|
value-format="yyyy-MM-dd HH:mm:ss" placeholder="结束时间" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="search-actions">
|
|
|
|
|
|
<el-button type="primary" size="mini" :loading="excoilLoading" @click="handleFindSearch">查找</el-button>
|
|
|
|
|
|
<el-button size="mini" @click="handleFindReset">重置</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
import * as echarts from 'echarts'
|
|
|
|
|
|
import 'echarts-gl'
|
|
|
|
|
|
import {
|
|
|
|
|
|
getExcoilList,
|
|
|
|
|
|
getExcoilCount,
|
|
|
|
|
|
getTimingSegByEncoilId,
|
2026-05-12 17:15:29 +08:00
|
|
|
|
getTimingRealtimeData,
|
|
|
|
|
|
getPresetSetupByCoilId
|
2026-05-11 15:44:37 +08:00
|
|
|
|
} from '@/api/l2/timing'
|
2026-05-12 17:15:29 +08:00
|
|
|
|
import { listProcessSpecVersion } from '@/api/wms/processSpecVersion'
|
|
|
|
|
|
import { listProcessPlan, addProcessPlan } from '@/api/wms/processPlan'
|
|
|
|
|
|
import { listProcessPlanParam, addProcessPlanParam, updateProcessPlanParam } from '@/api/wms/processPlanParam'
|
|
|
|
|
|
import { upsertProcessCoilRecord } from '@/api/wms/processCoilRecord'
|
|
|
|
|
|
import { batchAddProcessAnomaly } from '@/api/wms/processAnomaly'
|
2026-05-11 15:44:37 +08:00
|
|
|
|
|
|
|
|
|
|
// 趋势参数树结构,对应 PLTCM_PRO_SEG 列名
|
|
|
|
|
|
const TREND_GROUPS = [
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '张力',
|
|
|
|
|
|
children: [
|
|
|
|
|
|
{ label: '开卷张力', col: 'PORTENS' },
|
|
|
|
|
|
{ label: '入口活套张力', col: 'ENLTENS' },
|
|
|
|
|
|
{ label: '拉矫张力', col: 'TLTENS' },
|
|
|
|
|
|
{ label: '酸洗张力', col: 'PLTENS' },
|
|
|
|
|
|
{ label: '出口活套张力', col: 'CXLTENS' },
|
|
|
|
|
|
{ label: '圆盘剪张力', col: 'TRIMTENS' }
|
|
|
|
|
|
]
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '速度',
|
|
|
|
|
|
children: [
|
|
|
|
|
|
{ label: '开卷速度', col: 'PORSPEED' },
|
|
|
|
|
|
{ label: '酸洗速度', col: 'PLSPEED' },
|
|
|
|
|
|
{ label: '圆盘剪速度', col: 'TRIMSPEED' },
|
|
|
|
|
|
{ label: '轧机入口速度', col: 'MILLENTRYSPEED' },
|
|
|
|
|
|
{ label: '轧机出口速度', col: 'MILLEXITSPEED' }
|
|
|
|
|
|
]
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '拉矫机',
|
|
|
|
|
|
children: [
|
|
|
|
|
|
{ label: '1#插入量', col: 'TLMESH1' },
|
|
|
|
|
|
{ label: '2#插入量', col: 'TLMESH2' },
|
|
|
|
|
|
{ label: '3#插入量', col: 'TLMESH3' },
|
|
|
|
|
|
{ label: '延伸率', col: 'TLELONG' }
|
|
|
|
|
|
]
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '酸洗段',
|
|
|
|
|
|
children: [
|
|
|
|
|
|
{ label: '1#温度', col: 'TK1TEMP' },
|
|
|
|
|
|
{ label: '2#温度', col: 'TK2TEMP' },
|
|
|
|
|
|
{ label: '3#温度', col: 'TK3TEMP' },
|
|
|
|
|
|
{ label: '漂洗温度', col: 'RINSETEMP' }
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
// V_VBDA_GAUGE 厚度曲线:4 个图,列名来自 DDL
|
|
|
|
|
|
const GAUGE_COLS = [
|
|
|
|
|
|
{ col: 'THICK0', title: '入口测厚仪 [mm]' },
|
|
|
|
|
|
{ col: 'THICK1', title: '1架出口厚度 [mm]' },
|
|
|
|
|
|
{ col: 'THICK4', title: '末架出口厚度 [mm]' },
|
|
|
|
|
|
{ col: 'EXIT_SPEED', title: '轧制速度 [m/min]' }
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
// V_VBDA_SHAPE 板形曲线:4 个图,列名来自 DDL
|
|
|
|
|
|
const SHAPE_SCALAR_COLS = [
|
|
|
|
|
|
{ col: 'ABSDEVIATION', title: '总板形偏差 [IU]' },
|
|
|
|
|
|
{ col: 'TILT', title: '末架倾斜量 [mm]' },
|
|
|
|
|
|
{ col: 'WRBEND', title: '工作辊弯辊力 [kN]' },
|
|
|
|
|
|
{ col: 'IRBEND', title: '中间辊弯辊力 [kN]' }
|
|
|
|
|
|
]
|
|
|
|
|
|
|
2026-05-12 17:15:29 +08:00
|
|
|
|
// 参数单位映射
|
|
|
|
|
|
const TREND_UNIT_MAP = {
|
|
|
|
|
|
PORTENS: 'N/mm²', ENLTENS: 'N/mm²', TLTENS: 'N/mm²', PLTENS: 'N/mm²',
|
|
|
|
|
|
CXLTENS: 'N/mm²', TRIMTENS: 'N/mm²', TRTENS: 'N/mm²', TELTENS: 'N/mm²',
|
|
|
|
|
|
PORSPEED: 'm/min', PLSPEED: 'm/min', TRIMSPEED: 'm/min',
|
|
|
|
|
|
MILLENTRYSPEED: 'm/min', MILLEXITSPEED: 'm/min',
|
|
|
|
|
|
TLMESH1: 'mm', TLMESH2: 'mm', TLMESH3: 'mm',
|
|
|
|
|
|
TLELONG: '%',
|
|
|
|
|
|
TK1TEMP: '℃', TK2TEMP: '℃', TK3TEMP: '℃', RINSETEMP: '℃'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// PLTCM_PRO_SEG 列 → PLTCM_PRESET_SETUP 设定值列
|
|
|
|
|
|
const TREND_PRESET_MAP = {
|
|
|
|
|
|
PORTENS: 'POR_TEN', ENLTENS: 'CEL_TEN', TLTENS: 'TLV_TEN',
|
|
|
|
|
|
PLTENS: 'CPL_TEN', CXLTENS: 'CXL_TEN', TRIMTENS: 'TRIM_TEN',
|
|
|
|
|
|
TRTENS: 'TR_TEN', TELTENS: 'TEL_TEN',
|
|
|
|
|
|
TLMESH1: 'TLV_MESH_1', TLMESH2: 'TLV_MESH_2', TLMESH3: 'TLV_MESH_3',
|
|
|
|
|
|
TLELONG: 'TLV_ELONG', PLSPEED: 'CPL_MAX_SPEED'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-11 15:44:37 +08:00
|
|
|
|
function calcYRange(vals) {
|
|
|
|
|
|
const nums = vals.filter(v => v != null && isFinite(Number(v))).map(Number)
|
|
|
|
|
|
if (!nums.length) return {}
|
|
|
|
|
|
const min = Math.min(...nums)
|
|
|
|
|
|
const max = Math.max(...nums)
|
|
|
|
|
|
if (min === max) {
|
|
|
|
|
|
const base = Math.abs(min) || 1
|
|
|
|
|
|
return { min: parseFloat((min - base * 0.2).toFixed(4)), max: parseFloat((max + base * 0.2).toFixed(4)) }
|
|
|
|
|
|
}
|
|
|
|
|
|
const pad = (max - min) * 0.15
|
2026-05-12 17:15:29 +08:00
|
|
|
|
return { min: parseFloat((min - pad).toFixed(4)), max: parseFloat((max + pad).toFixed(4)) }
|
2026-05-11 15:44:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 17:15:29 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 生成折线图 option。
|
|
|
|
|
|
* extras: [{ name, data, color, dash }] — 上下限或参考线
|
|
|
|
|
|
*/
|
|
|
|
|
|
function makeLine(title, xData, yData, extras = []) {
|
|
|
|
|
|
const allVals = [yData, ...extras.map(e => e.data)].flat()
|
|
|
|
|
|
const range = calcYRange(allVals)
|
|
|
|
|
|
const hasExtras = extras.length > 0
|
|
|
|
|
|
const mainSeries = {
|
|
|
|
|
|
name: title, type: 'line', smooth: false, symbol: 'none',
|
|
|
|
|
|
lineStyle: { width: 1.5, color: '#409EFF' }, data: yData, z: 3
|
|
|
|
|
|
}
|
|
|
|
|
|
const extraSeries = extras.map(e => ({
|
|
|
|
|
|
name: e.name,
|
|
|
|
|
|
type: 'line', smooth: false, symbol: 'none',
|
|
|
|
|
|
lineStyle: { width: 1, color: e.color || '#E6A23C', type: e.dash !== false ? 'dashed' : 'solid' },
|
|
|
|
|
|
data: e.data, z: 2
|
|
|
|
|
|
}))
|
2026-05-11 15:44:37 +08:00
|
|
|
|
return {
|
|
|
|
|
|
title: { text: title, textStyle: { fontSize: 12, fontWeight: 'normal' }, top: 4, left: 8 },
|
2026-05-12 17:15:29 +08:00
|
|
|
|
legend: hasExtras
|
|
|
|
|
|
? { data: [title, ...extras.map(e => e.name)], top: 4, right: 4,
|
|
|
|
|
|
textStyle: { fontSize: 9 }, itemWidth: 14, itemHeight: 8 }
|
|
|
|
|
|
: { show: false },
|
2026-05-11 15:44:37 +08:00
|
|
|
|
tooltip: { trigger: 'axis' },
|
2026-05-12 17:15:29 +08:00
|
|
|
|
grid: { top: hasExtras ? 44 : 36, bottom: 28, left: 8, right: 16, containLabel: true },
|
2026-05-11 15:44:37 +08:00
|
|
|
|
xAxis: {
|
|
|
|
|
|
type: 'category', data: xData,
|
|
|
|
|
|
name: 'pos(m)', nameTextStyle: { fontSize: 10 }, axisLabel: { fontSize: 10 }
|
|
|
|
|
|
},
|
|
|
|
|
|
yAxis: {
|
2026-05-12 17:15:29 +08:00
|
|
|
|
type: 'value', min: range.min, max: range.max,
|
|
|
|
|
|
nameTextStyle: { fontSize: 10 }, axisLabel: { fontSize: 10 }
|
2026-05-11 15:44:37 +08:00
|
|
|
|
},
|
|
|
|
|
|
dataZoom: [
|
|
|
|
|
|
{ type: 'inside', xAxisIndex: 0, zoomOnMouseWheel: true, moveOnMouseMove: true },
|
|
|
|
|
|
{ type: 'inside', yAxisIndex: 0, zoomOnMouseWheel: false, moveOnMouseWheel: true }
|
|
|
|
|
|
],
|
2026-05-12 17:15:29 +08:00
|
|
|
|
series: [mainSeries, ...extraSeries]
|
2026-05-11 15:44:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getRowVal(row, col) {
|
|
|
|
|
|
const v = row[col] !== undefined ? row[col] : row[col.toLowerCase()]
|
|
|
|
|
|
return v == null ? null : Number(v)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function xLocData(rows) {
|
|
|
|
|
|
return rows.map(r => {
|
|
|
|
|
|
const v = r.XLOCATION !== undefined ? r.XLOCATION : r.xlocation
|
|
|
|
|
|
return v == null ? '' : Number(v).toFixed(1)
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
|
name: 'ActualPerformance',
|
|
|
|
|
|
data() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
excoilLoading: false,
|
|
|
|
|
|
segLoading: false,
|
|
|
|
|
|
realtimeLoading: false,
|
|
|
|
|
|
excoilRows: [],
|
|
|
|
|
|
selectedRow: null,
|
|
|
|
|
|
segData: null,
|
|
|
|
|
|
gaugeRows: null,
|
|
|
|
|
|
shapeRows: null,
|
|
|
|
|
|
activeTab: 'trend',
|
|
|
|
|
|
// 趋势参数树状态
|
|
|
|
|
|
trendGroups: TREND_GROUPS,
|
|
|
|
|
|
expandedGroups: { '张力': true, '速度': true, '拉矫机': true, '酸洗段': true },
|
|
|
|
|
|
selectedTrendParam: null,
|
|
|
|
|
|
trendChartInst: null,
|
2026-05-12 17:15:29 +08:00
|
|
|
|
presetData: null,
|
2026-05-11 15:44:37 +08:00
|
|
|
|
// 查找
|
|
|
|
|
|
searchType: 'coil',
|
|
|
|
|
|
searchCoilId: '',
|
|
|
|
|
|
searchStartDate: '',
|
|
|
|
|
|
searchEndDate: '',
|
|
|
|
|
|
pagination: { page: 1, pageSize: 50, total: 0 },
|
|
|
|
|
|
topTableHeight: 'calc(40vh - 80px)',
|
|
|
|
|
|
chartInstances: [],
|
|
|
|
|
|
resizeHandler: null
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
created() {
|
2026-05-12 17:15:29 +08:00
|
|
|
|
this._clickRev = 0
|
2026-05-11 15:44:37 +08:00
|
|
|
|
this.loadExcoilCount()
|
|
|
|
|
|
this.loadExcoilList()
|
|
|
|
|
|
},
|
|
|
|
|
|
beforeDestroy() {
|
|
|
|
|
|
this.disposeAllCharts()
|
|
|
|
|
|
},
|
|
|
|
|
|
methods: {
|
|
|
|
|
|
async loadExcoilCount() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await getExcoilCount()
|
|
|
|
|
|
this.pagination.total = res?.data?.total ?? 0
|
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
|
},
|
|
|
|
|
|
async loadExcoilList() {
|
|
|
|
|
|
this.excoilLoading = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await getExcoilList(this.pagination.page, this.pagination.pageSize)
|
|
|
|
|
|
this.excoilRows = res?.data?.rows || []
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this.excoilLoading = false
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
handlePageChange(page) {
|
|
|
|
|
|
this.pagination.page = page
|
|
|
|
|
|
this.loadExcoilList()
|
|
|
|
|
|
},
|
|
|
|
|
|
async handleRowClick(row) {
|
2026-05-12 17:15:29 +08:00
|
|
|
|
// 快速点击防重:每次点击递增版本号,旧的 sync 任务检测到版本号变更后自动放弃
|
|
|
|
|
|
const clickRev = ++this._clickRev
|
2026-05-11 15:44:37 +08:00
|
|
|
|
this.selectedRow = row
|
|
|
|
|
|
this.segData = null
|
|
|
|
|
|
this.gaugeRows = null
|
|
|
|
|
|
this.shapeRows = null
|
2026-05-12 17:15:29 +08:00
|
|
|
|
this.presetData = null
|
2026-05-11 15:44:37 +08:00
|
|
|
|
this.selectedTrendParam = null
|
|
|
|
|
|
this.disposeAllCharts()
|
|
|
|
|
|
|
|
|
|
|
|
const encoilId = row.ENCOILID || row.encoilid
|
|
|
|
|
|
const excoilId = row.EXCOILID || row.excoilid
|
|
|
|
|
|
|
|
|
|
|
|
await Promise.all([
|
|
|
|
|
|
encoilId ? this.loadSeg(encoilId) : Promise.resolve(),
|
2026-05-12 17:15:29 +08:00
|
|
|
|
encoilId ? this.loadPreset(encoilId) : Promise.resolve(),
|
2026-05-11 15:44:37 +08:00
|
|
|
|
excoilId ? this.loadRealtime(excoilId) : Promise.resolve()
|
|
|
|
|
|
])
|
|
|
|
|
|
|
2026-05-12 17:15:29 +08:00
|
|
|
|
// 如果期间又点击了其他行则放弃后续操作
|
|
|
|
|
|
if (this._clickRev !== clickRev) return
|
|
|
|
|
|
|
|
|
|
|
|
// 后台静默同步到规程(不阻塞 UI)
|
|
|
|
|
|
this.autoSyncToActiveSpec(excoilId || encoilId, clickRev)
|
|
|
|
|
|
|
2026-05-11 15:44:37 +08:00
|
|
|
|
await this.$nextTick()
|
|
|
|
|
|
// 加载完成后自动选中第一个趋势参数
|
|
|
|
|
|
if (this.activeTab === 'trend' && this.segData) {
|
|
|
|
|
|
this.selectTrendParam(TREND_GROUPS[0].children[0])
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.renderCurrentTab()
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
async loadSeg(encoilId) {
|
|
|
|
|
|
this.segLoading = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await getTimingSegByEncoilId(encoilId)
|
|
|
|
|
|
const rows = res?.data?.rows || []
|
|
|
|
|
|
this.segData = rows.length ? (res?.data?.series || null) : null
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this.segLoading = false
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
async loadRealtime(excoilId) {
|
|
|
|
|
|
this.realtimeLoading = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await getTimingRealtimeData(excoilId)
|
|
|
|
|
|
const g = res?.data?.gauge?.result
|
|
|
|
|
|
const s = res?.data?.shape?.result
|
|
|
|
|
|
this.gaugeRows = Array.isArray(g) ? g : null
|
|
|
|
|
|
this.shapeRows = Array.isArray(s) ? s : null
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this.realtimeLoading = false
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-05-12 17:15:29 +08:00
|
|
|
|
async loadPreset(coilId) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await getPresetSetupByCoilId(coilId)
|
|
|
|
|
|
this.presetData = res?.data?.data || null
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
this.presetData = null
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-05-11 15:44:37 +08:00
|
|
|
|
// ── 趋势参数树 ──────────────────────────────
|
|
|
|
|
|
toggleGroup(label) {
|
|
|
|
|
|
this.$set(this.expandedGroups, label, !this.expandedGroups[label])
|
|
|
|
|
|
},
|
|
|
|
|
|
isGroupExpanded(label) {
|
|
|
|
|
|
return !!this.expandedGroups[label]
|
|
|
|
|
|
},
|
|
|
|
|
|
selectTrendParam(item) {
|
|
|
|
|
|
this.selectedTrendParam = item
|
|
|
|
|
|
this.$nextTick(() => this.renderTrendSingleChart())
|
|
|
|
|
|
},
|
|
|
|
|
|
renderTrendSingleChart() {
|
|
|
|
|
|
if (!this.selectedTrendParam || !this.segData) return
|
|
|
|
|
|
const el = this.$refs.trendSingleChart
|
|
|
|
|
|
if (!el) return
|
|
|
|
|
|
// 复用已有实例,避免重复 init
|
|
|
|
|
|
if (!this.trendChartInst || this.trendChartInst.isDisposed()) {
|
|
|
|
|
|
this.trendChartInst = echarts.init(el)
|
|
|
|
|
|
// 滚轮缩放支持
|
|
|
|
|
|
const resizeFn = () => this.trendChartInst && !this.trendChartInst.isDisposed() && this.trendChartInst.resize()
|
|
|
|
|
|
window.addEventListener('resize', resizeFn)
|
|
|
|
|
|
this._trendResizeFn = resizeFn
|
|
|
|
|
|
}
|
2026-05-12 17:15:29 +08:00
|
|
|
|
const col = this.selectedTrendParam.col
|
2026-05-11 15:44:37 +08:00
|
|
|
|
const x = this.segX()
|
2026-05-12 17:15:29 +08:00
|
|
|
|
const yData = this.seg(col)
|
|
|
|
|
|
// 实测最大值 / 最小值:来自 PLTCM_PRO_SEG 的段内统计 MAX / MIN
|
|
|
|
|
|
const maxData = this.seg(col + 'MAX')
|
|
|
|
|
|
const minData = this.seg(col + 'MIN')
|
|
|
|
|
|
const extras = []
|
|
|
|
|
|
if (maxData.some(v => v != null)) {
|
|
|
|
|
|
extras.push({ name: '最大值', data: maxData, color: '#F56C6C', dash: true })
|
|
|
|
|
|
}
|
|
|
|
|
|
if (minData.some(v => v != null)) {
|
|
|
|
|
|
extras.push({ name: '最小值', data: minData, color: '#67C23A', dash: true })
|
|
|
|
|
|
}
|
|
|
|
|
|
// 设定值:来自 PLTCM_PRESET_SETUP(若有映射)
|
|
|
|
|
|
const presetCol = TREND_PRESET_MAP[col]
|
|
|
|
|
|
if (presetCol && this.presetData) {
|
|
|
|
|
|
const setVal = this.presetData[presetCol] !== undefined
|
|
|
|
|
|
? this.presetData[presetCol]
|
|
|
|
|
|
: this.presetData[presetCol.toLowerCase()]
|
|
|
|
|
|
if (setVal != null && Number(setVal) !== 0) {
|
|
|
|
|
|
const sv = Number(Number(setVal).toFixed(3))
|
|
|
|
|
|
extras.push({ name: '设定值', data: new Array(x.length).fill(sv), color: '#E6A23C', dash: true })
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
this.trendChartInst.setOption(makeLine(this.selectedTrendParam.label, x, yData, extras), true)
|
2026-05-11 15:44:37 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// ── Tab 切换 ────────────────────────────────
|
|
|
|
|
|
handleTabSwitch() {
|
|
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
|
if (this.activeTab === 'trend' && this.selectedTrendParam && this.segData) {
|
|
|
|
|
|
this.renderTrendSingleChart()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.renderCurrentTab()
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
renderCurrentTab() {
|
|
|
|
|
|
this.disposeSideCharts()
|
|
|
|
|
|
if (this.activeTab === 'thickness' && this.gaugeRows?.length) this.renderGaugeCharts()
|
|
|
|
|
|
if (this.activeTab === 'flatness3d' && this.shapeRows?.length) this.renderFlatness3d()
|
|
|
|
|
|
if (this.activeTab === 'flatness' && this.shapeRows?.length) this.renderFlatnessCharts()
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// ── 销毁 ────────────────────────────────────
|
|
|
|
|
|
disposeSideCharts() {
|
|
|
|
|
|
if (this.resizeHandler) {
|
|
|
|
|
|
window.removeEventListener('resize', this.resizeHandler)
|
|
|
|
|
|
this.resizeHandler = null
|
|
|
|
|
|
}
|
|
|
|
|
|
this.chartInstances.forEach(c => { if (c && !c.isDisposed()) c.dispose() })
|
|
|
|
|
|
this.chartInstances = []
|
|
|
|
|
|
},
|
|
|
|
|
|
disposeAllCharts() {
|
|
|
|
|
|
this.disposeSideCharts()
|
|
|
|
|
|
if (this._trendResizeFn) {
|
|
|
|
|
|
window.removeEventListener('resize', this._trendResizeFn)
|
|
|
|
|
|
this._trendResizeFn = null
|
|
|
|
|
|
}
|
|
|
|
|
|
if (this.trendChartInst && !this.trendChartInst.isDisposed()) {
|
|
|
|
|
|
this.trendChartInst.dispose()
|
|
|
|
|
|
this.trendChartInst = null
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// ── SEG 数据辅助 ─────────────────────────────
|
|
|
|
|
|
seg(col) {
|
|
|
|
|
|
const s = this.segData
|
|
|
|
|
|
// Oracle 返回大写列名,兼容小写
|
|
|
|
|
|
const arr = s[col] !== undefined ? s[col] : (s[col.toLowerCase()] || [])
|
|
|
|
|
|
return arr.map(v => v == null ? null : Number(Number(v).toFixed(3)))
|
|
|
|
|
|
},
|
|
|
|
|
|
segX() {
|
|
|
|
|
|
const s = this.segData
|
|
|
|
|
|
const arr = s['STARTPOS'] !== undefined ? s['STARTPOS'] : (s['startpos'] || [])
|
|
|
|
|
|
return arr.map(v => v == null ? '' : Number(v).toFixed(1))
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// ── 图表初始化 ───────────────────────────────
|
|
|
|
|
|
makeChart(ref, option) {
|
|
|
|
|
|
const el = this.$refs[ref]
|
|
|
|
|
|
if (!el) return null
|
|
|
|
|
|
const chart = echarts.init(el)
|
|
|
|
|
|
chart.setOption(option)
|
|
|
|
|
|
return chart
|
|
|
|
|
|
},
|
|
|
|
|
|
setupResize() {
|
|
|
|
|
|
this.resizeHandler = () => this.chartInstances.forEach(c => {
|
|
|
|
|
|
if (c && !c.isDisposed()) c.resize()
|
|
|
|
|
|
})
|
|
|
|
|
|
window.addEventListener('resize', this.resizeHandler)
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// ── 厚度曲线 (V_VBDA_GAUGE) ──────────────────
|
|
|
|
|
|
renderGaugeCharts() {
|
|
|
|
|
|
const rows = this.gaugeRows
|
|
|
|
|
|
if (!rows || !rows.length) return
|
|
|
|
|
|
const xData = xLocData(rows)
|
2026-05-12 17:15:29 +08:00
|
|
|
|
// THICK0~THICK4 对应的参考列
|
|
|
|
|
|
const refColMap = { THICK0: 'THICK0REF', THICK1: 'THICK1REF', THICK4: 'THICK4REF', THICK5: 'THICK5REF' }
|
|
|
|
|
|
const chartRefs = ['chartGauge1', 'chartGauge2', 'chartGauge3', 'chartGauge4']
|
|
|
|
|
|
const charts = chartRefs.map((ref, i) => {
|
2026-05-11 15:44:37 +08:00
|
|
|
|
const { col, title } = GAUGE_COLS[i]
|
|
|
|
|
|
const yData = rows.map(r => {
|
|
|
|
|
|
const v = getRowVal(r, col)
|
|
|
|
|
|
return v == null ? null : parseFloat(v.toFixed(4))
|
|
|
|
|
|
})
|
2026-05-12 17:15:29 +08:00
|
|
|
|
const extras = []
|
|
|
|
|
|
const refCol = refColMap[col]
|
|
|
|
|
|
if (refCol) {
|
|
|
|
|
|
const refData = rows.map(r => {
|
|
|
|
|
|
const rv = getRowVal(r, refCol)
|
|
|
|
|
|
return rv == null ? null : parseFloat(rv.toFixed(4))
|
|
|
|
|
|
})
|
|
|
|
|
|
// 上限 = REF + TOPLIMIT;下限 = REF + BOTLIMIT(TOPLIMIT/BOTLIMIT 单位与测厚仪一致)
|
|
|
|
|
|
const upData = rows.map((r, j) => {
|
|
|
|
|
|
const rv = getRowVal(r, refCol)
|
|
|
|
|
|
const tl = getRowVal(r, 'TOPLIMIT')
|
|
|
|
|
|
return rv == null ? null : parseFloat((rv + (tl ?? 3)).toFixed(4))
|
|
|
|
|
|
})
|
|
|
|
|
|
const loData = rows.map((r, j) => {
|
|
|
|
|
|
const rv = getRowVal(r, refCol)
|
|
|
|
|
|
const bl = getRowVal(r, 'BOTLIMIT')
|
|
|
|
|
|
return rv == null ? null : parseFloat((rv + (bl ?? -3)).toFixed(4))
|
|
|
|
|
|
})
|
|
|
|
|
|
if (refData.some(v => v != null)) extras.push({ name: '目标值', data: refData, color: '#909399', dash: false })
|
|
|
|
|
|
if (upData.some(v => v != null)) extras.push({ name: '上限', data: upData, color: '#F56C6C', dash: true })
|
|
|
|
|
|
if (loData.some(v => v != null)) extras.push({ name: '下限', data: loData, color: '#67C23A', dash: true })
|
|
|
|
|
|
}
|
|
|
|
|
|
return this.makeChart(ref, makeLine(title, xData, yData, extras))
|
2026-05-11 15:44:37 +08:00
|
|
|
|
})
|
|
|
|
|
|
this.chartInstances = charts.filter(Boolean)
|
|
|
|
|
|
this.setupResize()
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// ── 带钢板形 3D 线图 (V_VBDA_SHAPE) ────────────
|
|
|
|
|
|
// 每个通道画一条独立 line3D,通道之间不连面,形成镂空效果
|
|
|
|
|
|
renderFlatness3d() {
|
|
|
|
|
|
const rows = this.shapeRows
|
|
|
|
|
|
if (!rows || !rows.length) return
|
|
|
|
|
|
const firstRow = rows[0]
|
|
|
|
|
|
const high = parseInt(getRowVal(firstRow, 'HIGHZONEID')) || 26
|
|
|
|
|
|
const low = parseInt(getRowVal(firstRow, 'LOWZONEID')) || 1
|
|
|
|
|
|
const numZones = Math.min(Math.max(high - low + 1, 1), 26)
|
|
|
|
|
|
const zoneCols = Array.from({ length: numZones }, (_, i) =>
|
|
|
|
|
|
`VALUES${String(low + i).padStart(2, '0')}`
|
|
|
|
|
|
)
|
|
|
|
|
|
// X 方向降采样,最多 200 个点
|
|
|
|
|
|
const step = Math.max(1, Math.floor(rows.length / 200))
|
|
|
|
|
|
const sampled = rows.filter((_, i) => i % step === 0)
|
|
|
|
|
|
const numX = sampled.length
|
|
|
|
|
|
|
|
|
|
|
|
// X 轴标签(位置,单位 m)
|
|
|
|
|
|
const xLabels = sampled.map(r => {
|
|
|
|
|
|
const v = r.XLOCATION !== undefined ? r.XLOCATION : r.xlocation
|
|
|
|
|
|
return v == null ? '' : Number(v).toFixed(0)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 收集值域用于 visualMap
|
|
|
|
|
|
let minV = Infinity, maxV = -Infinity
|
|
|
|
|
|
sampled.forEach(row => {
|
|
|
|
|
|
zoneCols.forEach(col => {
|
|
|
|
|
|
const v = getRowVal(row, col)
|
|
|
|
|
|
if (v != null) {
|
|
|
|
|
|
if (v < minV) minV = v
|
|
|
|
|
|
if (v > maxV) maxV = v
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
if (!isFinite(minV)) { minV = -30; maxV = 30 }
|
|
|
|
|
|
const absMax = Math.max(Math.abs(minV), Math.abs(maxV))
|
|
|
|
|
|
|
|
|
|
|
|
// ① 沿 X 方向网格线(每通道一条,按 Z 值着色)
|
|
|
|
|
|
const channelLines = zoneCols.map((col, yi) => ({
|
|
|
|
|
|
type: 'line3D',
|
|
|
|
|
|
coordinateSystem: 'cartesian3D',
|
|
|
|
|
|
data: sampled.map((row, xi) => {
|
|
|
|
|
|
const v = getRowVal(row, col)
|
|
|
|
|
|
return v == null ? null : [xi, yi, parseFloat(v.toFixed(2))]
|
|
|
|
|
|
}).filter(Boolean),
|
|
|
|
|
|
lineStyle: { width: 2, opacity: 1 }
|
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
|
|
// ② 沿 Y 方向网格线(每隔若干位置连通各通道,按 Z 值着色)
|
|
|
|
|
|
const xStride = Math.max(1, Math.floor(numX / 60))
|
|
|
|
|
|
const crossLines = []
|
|
|
|
|
|
for (let xi = 0; xi < numX; xi += xStride) {
|
|
|
|
|
|
const pts = zoneCols.map((col, yi) => {
|
|
|
|
|
|
const v = getRowVal(sampled[xi], col)
|
|
|
|
|
|
return v == null ? null : [xi, yi, parseFloat(v.toFixed(2))]
|
|
|
|
|
|
}).filter(Boolean)
|
|
|
|
|
|
if (pts.length > 1) {
|
|
|
|
|
|
crossLines.push({
|
|
|
|
|
|
type: 'line3D',
|
|
|
|
|
|
coordinateSystem: 'cartesian3D',
|
|
|
|
|
|
data: pts,
|
|
|
|
|
|
lineStyle: { width: 1.5, opacity: 1 }
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const series = [...channelLines, ...crossLines]
|
|
|
|
|
|
|
|
|
|
|
|
const option = {
|
|
|
|
|
|
title: { text: '实测平直度 [IU]', textStyle: { fontSize: 13, fontWeight: 'normal' }, top: 6, left: 10 },
|
|
|
|
|
|
tooltip: {},
|
|
|
|
|
|
visualMap: {
|
|
|
|
|
|
show: true,
|
|
|
|
|
|
dimension: 2,
|
|
|
|
|
|
min: -absMax,
|
|
|
|
|
|
max: absMax,
|
|
|
|
|
|
calculable: true,
|
|
|
|
|
|
orient: 'vertical',
|
|
|
|
|
|
right: 10,
|
|
|
|
|
|
top: 'center',
|
|
|
|
|
|
textStyle: { fontSize: 10 },
|
|
|
|
|
|
inRange: {
|
|
|
|
|
|
// 负值红色 → 零值绿色 → 正值蓝紫
|
|
|
|
|
|
color: ['#8B0000','#CC2200','#E84C00','#F46D43',
|
|
|
|
|
|
'#FDAE61','#FEE08B',
|
|
|
|
|
|
'#66BD63','#1A9850','#006837',
|
|
|
|
|
|
'#3288BD','#5E4FA2','#762A83']
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
grid3D: {
|
|
|
|
|
|
boxWidth: 200,
|
|
|
|
|
|
boxHeight: 60,
|
|
|
|
|
|
boxDepth: 80,
|
|
|
|
|
|
viewControl: {
|
|
|
|
|
|
projection: 'orthographic',
|
|
|
|
|
|
autoRotate: false,
|
|
|
|
|
|
rotateSensitivity: 1,
|
|
|
|
|
|
zoomSensitivity: 1
|
|
|
|
|
|
},
|
|
|
|
|
|
light: {
|
|
|
|
|
|
main: { intensity: 1.2, shadow: false },
|
|
|
|
|
|
ambient: { intensity: 0.3 }
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
xAxis3D: {
|
|
|
|
|
|
type: 'value',
|
|
|
|
|
|
name: '位置',
|
|
|
|
|
|
min: 0,
|
|
|
|
|
|
max: numX - 1,
|
|
|
|
|
|
nameTextStyle: { fontSize: 10 },
|
|
|
|
|
|
axisLabel: {
|
|
|
|
|
|
fontSize: 9,
|
|
|
|
|
|
formatter: v => xLabels[Math.round(v)] || ''
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
yAxis3D: {
|
|
|
|
|
|
type: 'value',
|
|
|
|
|
|
name: '通道',
|
|
|
|
|
|
min: 0,
|
|
|
|
|
|
max: numZones - 1,
|
|
|
|
|
|
nameTextStyle: { fontSize: 10 },
|
|
|
|
|
|
axisLabel: {
|
|
|
|
|
|
fontSize: 9,
|
|
|
|
|
|
formatter: v => String(low + Math.round(v))
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
zAxis3D: {
|
|
|
|
|
|
type: 'value',
|
|
|
|
|
|
name: 'IU',
|
|
|
|
|
|
nameTextStyle: { fontSize: 10 },
|
|
|
|
|
|
axisLabel: { fontSize: 9 }
|
|
|
|
|
|
},
|
|
|
|
|
|
series
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const el = this.$refs.chartFlatness3d
|
|
|
|
|
|
if (!el) return
|
|
|
|
|
|
const chart = echarts.init(el)
|
|
|
|
|
|
chart.setOption(option)
|
|
|
|
|
|
this.chartInstances = [chart]
|
|
|
|
|
|
this.setupResize()
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// ── 板形曲线 (V_VBDA_SHAPE) ──────────────────
|
|
|
|
|
|
renderFlatnessCharts() {
|
|
|
|
|
|
const rows = this.shapeRows
|
|
|
|
|
|
if (!rows || !rows.length) return
|
|
|
|
|
|
const xData = xLocData(rows)
|
|
|
|
|
|
const refs = ['chartFlatDev', 'chartTilt', 'chartWrBend', 'chartIrBend']
|
|
|
|
|
|
const charts = refs.map((ref, i) => {
|
|
|
|
|
|
const { col, title } = SHAPE_SCALAR_COLS[i]
|
|
|
|
|
|
const yData = rows.map(r => {
|
|
|
|
|
|
const v = getRowVal(r, col)
|
|
|
|
|
|
return v == null ? null : parseFloat(v.toFixed(3))
|
|
|
|
|
|
})
|
|
|
|
|
|
return this.makeChart(ref, makeLine(title, xData, yData))
|
|
|
|
|
|
})
|
|
|
|
|
|
this.chartInstances = charts.filter(Boolean)
|
|
|
|
|
|
this.setupResize()
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// ── 查找 ─────────────────────────────────────
|
|
|
|
|
|
handleFindSearch() {
|
|
|
|
|
|
if (this.searchType === 'coil' && this.searchCoilId) {
|
|
|
|
|
|
const found = this.excoilRows.find(r =>
|
|
|
|
|
|
(r.EXCOILID || r.excoilid || '').includes(this.searchCoilId)
|
|
|
|
|
|
)
|
|
|
|
|
|
if (found) {
|
|
|
|
|
|
this.$refs.excoilTable && this.$refs.excoilTable.setCurrentRow(found)
|
|
|
|
|
|
this.handleRowClick(found)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.$message.info('当前页未找到该卷号,请翻页查找')
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.pagination.page = 1
|
|
|
|
|
|
this.loadExcoilList()
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
handleFindReset() {
|
|
|
|
|
|
this.searchCoilId = ''
|
|
|
|
|
|
this.searchStartDate = ''
|
|
|
|
|
|
this.searchEndDate = ''
|
|
|
|
|
|
this.selectedRow = null
|
|
|
|
|
|
this.segData = null
|
|
|
|
|
|
this.gaugeRows = null
|
|
|
|
|
|
this.shapeRows = null
|
|
|
|
|
|
this.selectedTrendParam = null
|
|
|
|
|
|
this.disposeAllCharts()
|
|
|
|
|
|
this.pagination.page = 1
|
|
|
|
|
|
this.loadExcoilCount()
|
|
|
|
|
|
this.loadExcoilList()
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-05-12 17:15:29 +08:00
|
|
|
|
// ── 自动同步到生效规程 ────────────────────────
|
|
|
|
|
|
async autoSyncToActiveSpec(coilId, rev) {
|
|
|
|
|
|
if (!this.presetData && !this.segData) return
|
|
|
|
|
|
const guard = () => rev !== undefined && this._clickRev !== rev
|
|
|
|
|
|
try {
|
|
|
|
|
|
// ① 查找生效版本
|
|
|
|
|
|
const verRes = await listProcessSpecVersion({ isActive: 1, pageNum: 1, pageSize: 10 })
|
|
|
|
|
|
if (guard()) return
|
|
|
|
|
|
const activeVer = (verRes.rows || []).find(v => v.isActive === 1)
|
|
|
|
|
|
if (!activeVer) return
|
|
|
|
|
|
const versionId = activeVer.versionId
|
|
|
|
|
|
|
|
|
|
|
|
// ② 构建本次写入条目
|
|
|
|
|
|
const items = this.buildSpecSyncItems()
|
|
|
|
|
|
if (!items.length) return
|
|
|
|
|
|
|
|
|
|
|
|
// ③ 加载已有 plan 点位
|
|
|
|
|
|
const plansRes = await listProcessPlan({ versionId, pageNum: 1, pageSize: 500 })
|
|
|
|
|
|
if (guard()) return
|
|
|
|
|
|
const planMap = {}
|
|
|
|
|
|
;(plansRes.rows || []).forEach(p => { planMap[p.pointCode] = p })
|
|
|
|
|
|
|
|
|
|
|
|
// ④ 逐条 upsert 点位 & 参数,收集异常
|
|
|
|
|
|
const anomalies = []
|
|
|
|
|
|
const detectedAt = new Date().toISOString()
|
|
|
|
|
|
// 提取本次钢卷的 enCoilId(入口卷号)
|
|
|
|
|
|
const enCoilId = this.selectedRow ? (this.selectedRow.ENCOILID || this.selectedRow.encoilid || null) : null
|
|
|
|
|
|
|
|
|
|
|
|
for (const item of items) {
|
|
|
|
|
|
if (guard()) return
|
|
|
|
|
|
|
|
|
|
|
|
// 确保 plan 点位存在
|
|
|
|
|
|
let planId
|
|
|
|
|
|
const ep = planMap[item.pointCode]
|
|
|
|
|
|
if (ep) {
|
|
|
|
|
|
planId = ep.planId
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const r = await addProcessPlan({
|
|
|
|
|
|
versionId,
|
|
|
|
|
|
segmentType: 'PROCESS',
|
|
|
|
|
|
segmentName: item.groupLabel,
|
|
|
|
|
|
pointName: item.pointName,
|
|
|
|
|
|
pointCode: item.pointCode,
|
|
|
|
|
|
sortOrder: 0
|
|
|
|
|
|
})
|
|
|
|
|
|
if (guard()) return
|
|
|
|
|
|
planId = r.data
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 查已存储参数
|
|
|
|
|
|
const prRes = await listProcessPlanParam({ planId, pageNum: 1, pageSize: 100 })
|
|
|
|
|
|
if (guard()) return
|
|
|
|
|
|
const stored = (prRes.rows || []).find(p => p.paramCode === item.paramCode)
|
|
|
|
|
|
|
|
|
|
|
|
// 异常检测:与已有上下限比对
|
|
|
|
|
|
if (stored) {
|
|
|
|
|
|
const sUp = stored.upperLimit != null ? Number(stored.upperLimit) : null
|
|
|
|
|
|
const sLo = stored.lowerLimit != null ? Number(stored.lowerLimit) : null
|
|
|
|
|
|
const aUp = item.upperLimit
|
|
|
|
|
|
const aLo = item.lowerLimit
|
|
|
|
|
|
const overTypes = []
|
|
|
|
|
|
if (sUp != null && aUp != null && aUp > sUp) overTypes.push('OVER_MAX')
|
|
|
|
|
|
if (sLo != null && aLo != null && aLo < sLo) overTypes.push('UNDER_MIN')
|
|
|
|
|
|
if (overTypes.length) {
|
|
|
|
|
|
anomalies.push({
|
|
|
|
|
|
versionId,
|
|
|
|
|
|
planId,
|
|
|
|
|
|
paramId: stored.paramId || null,
|
|
|
|
|
|
coilId,
|
|
|
|
|
|
enCoilId,
|
|
|
|
|
|
paramCode: item.paramCode,
|
|
|
|
|
|
paramName: item.paramName,
|
|
|
|
|
|
unit: item.unit,
|
|
|
|
|
|
anomalyType: overTypes.length === 2 ? 'BOTH' : overTypes[0],
|
|
|
|
|
|
storedTarget: stored.targetValue != null ? Number(stored.targetValue) : null,
|
|
|
|
|
|
storedUpper: sUp,
|
|
|
|
|
|
storedLower: sLo,
|
|
|
|
|
|
actualTarget: item.targetValue,
|
|
|
|
|
|
actualMax: aUp,
|
|
|
|
|
|
actualMin: aLo,
|
|
|
|
|
|
deviationMax: sUp != null && aUp != null ? parseFloat((aUp - sUp).toFixed(4)) : null,
|
|
|
|
|
|
deviationMin: sLo != null && aLo != null ? parseFloat((aLo - sLo).toFixed(4)) : null,
|
|
|
|
|
|
detectedAt
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 写入/更新参数
|
|
|
|
|
|
// target_value 始终覆盖(反映最新L1设定)
|
|
|
|
|
|
// upper/lower 仅首次写入(null 时写入作为基线)
|
|
|
|
|
|
// actualSrcId / presetSrcId 仅首次写入
|
|
|
|
|
|
if (stored) {
|
|
|
|
|
|
await updateProcessPlanParam({
|
|
|
|
|
|
...stored,
|
|
|
|
|
|
targetValue: item.targetValue ?? stored.targetValue,
|
|
|
|
|
|
upperLimit: stored.upperLimit ?? item.upperLimit,
|
|
|
|
|
|
lowerLimit: stored.lowerLimit ?? item.lowerLimit,
|
|
|
|
|
|
unit: item.unit || stored.unit,
|
|
|
|
|
|
actualSrcId: stored.actualSrcId || enCoilId,
|
|
|
|
|
|
presetSrcId: stored.presetSrcId || coilId
|
|
|
|
|
|
})
|
|
|
|
|
|
} else {
|
|
|
|
|
|
await addProcessPlanParam({
|
|
|
|
|
|
planId,
|
|
|
|
|
|
paramCode: item.paramCode,
|
|
|
|
|
|
paramName: item.paramName,
|
|
|
|
|
|
targetValue: item.targetValue,
|
|
|
|
|
|
upperLimit: item.upperLimit,
|
|
|
|
|
|
lowerLimit: item.lowerLimit,
|
|
|
|
|
|
unit: item.unit,
|
|
|
|
|
|
actualSrcId: enCoilId,
|
|
|
|
|
|
presetSrcId: coilId
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (guard()) return
|
|
|
|
|
|
|
|
|
|
|
|
// ⑤ 写入钢卷服役记录(幂等 upsert)
|
|
|
|
|
|
await upsertProcessCoilRecord({
|
|
|
|
|
|
versionId,
|
|
|
|
|
|
coilId,
|
|
|
|
|
|
enCoilId,
|
|
|
|
|
|
hasAnomaly: anomalies.length > 0 ? 1 : 0,
|
|
|
|
|
|
anomalyCnt: anomalies.length,
|
|
|
|
|
|
processTime: detectedAt
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// ⑥ 持久化异常到数据库
|
|
|
|
|
|
if (anomalies.length) {
|
|
|
|
|
|
await batchAddProcessAnomaly(anomalies)
|
|
|
|
|
|
console.log(`[规程同步] 检测到 ${anomalies.length} 个参数异常,已写入数据库`)
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.warn('[规程同步] 后台同步失败:', e)
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/** 从当前 segData + presetData 构建写入条目 */
|
|
|
|
|
|
buildSpecSyncItems() {
|
|
|
|
|
|
const items = []
|
|
|
|
|
|
for (const group of TREND_GROUPS) {
|
|
|
|
|
|
for (const item of group.children) {
|
|
|
|
|
|
const col = item.col
|
|
|
|
|
|
const maxArr = this.segData ? this.seg(col + 'MAX').filter(v => v != null) : []
|
|
|
|
|
|
const minArr = this.segData ? this.seg(col + 'MIN').filter(v => v != null) : []
|
|
|
|
|
|
const presetCol = TREND_PRESET_MAP[col]
|
|
|
|
|
|
|
|
|
|
|
|
let targetValue = null
|
|
|
|
|
|
if (presetCol && this.presetData) {
|
|
|
|
|
|
const sv = this.presetData[presetCol] !== undefined
|
|
|
|
|
|
? this.presetData[presetCol]
|
|
|
|
|
|
: this.presetData[presetCol.toLowerCase()]
|
|
|
|
|
|
if (sv != null && Number(sv) !== 0) targetValue = parseFloat(Number(sv).toFixed(4))
|
|
|
|
|
|
}
|
|
|
|
|
|
const upperLimit = maxArr.length ? parseFloat(Math.max(...maxArr).toFixed(4)) : null
|
|
|
|
|
|
const lowerLimit = minArr.length ? parseFloat(Math.min(...minArr).toFixed(4)) : null
|
|
|
|
|
|
|
|
|
|
|
|
if (targetValue == null && upperLimit == null && lowerLimit == null) continue
|
|
|
|
|
|
items.push({
|
|
|
|
|
|
groupLabel: group.label,
|
|
|
|
|
|
pointCode: col,
|
|
|
|
|
|
pointName: item.label,
|
|
|
|
|
|
paramCode: col,
|
|
|
|
|
|
paramName: item.label,
|
|
|
|
|
|
targetValue,
|
|
|
|
|
|
upperLimit,
|
|
|
|
|
|
lowerLimit,
|
|
|
|
|
|
unit: TREND_UNIT_MAP[col] || ''
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return items
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-05-11 15:44:37 +08:00
|
|
|
|
calcLengthPerTon(row) {
|
|
|
|
|
|
const len = parseFloat(row.EXIT_LENGTH || row.exit_length)
|
|
|
|
|
|
const wt = parseFloat(row.MEAS_EXIT_WEIGHT || row.meas_exit_weight)
|
|
|
|
|
|
if (!len || !wt || wt === 0) return '—'
|
|
|
|
|
|
return (len / wt).toFixed(2)
|
|
|
|
|
|
},
|
|
|
|
|
|
formatDate(v) {
|
|
|
|
|
|
if (!v) return '—'
|
|
|
|
|
|
return String(v).replace('T', ' ').substring(0, 19)
|
2026-05-12 17:15:29 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
2026-05-11 15:44:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
|
.actual-container {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
padding: 8px;
|
|
|
|
|
|
background: #f0f2f5;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.top-section {
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.table-pagination {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
padding: 4px 8px;
|
|
|
|
|
|
border-top: 1px solid #ebeef5;
|
|
|
|
|
|
background: #fafafa;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.bottom-section {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chart-section {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
|
|
|
|
|
padding: 8px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
|
|
|
|
|
|
.chart-tabs {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
|
|
|
|
|
|
::v-deep .el-tabs__content {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
::v-deep .el-tab-pane {
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ── 趋势参数:左树 + 右图 ── */
|
|
|
|
|
|
.trend-layout {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
gap: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.trend-tree {
|
|
|
|
|
|
width: 140px;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
border-right: 1px solid #ebeef5;
|
|
|
|
|
|
padding: 4px 0;
|
|
|
|
|
|
|
|
|
|
|
|
&::-webkit-scrollbar { width: 3px; }
|
|
|
|
|
|
&::-webkit-scrollbar-thumb { background: #dcdfe6; border-radius: 2px; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tree-group {
|
|
|
|
|
|
user-select: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tree-group-label {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
padding: 5px 8px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #303133;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover { background: #f5f7fa; }
|
|
|
|
|
|
|
|
|
|
|
|
i { font-size: 10px; color: #909399; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tree-children { padding-left: 4px; }
|
|
|
|
|
|
|
|
|
|
|
|
.tree-item {
|
|
|
|
|
|
padding: 4px 8px 4px 18px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #606266;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
margin: 1px 4px;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover { background: #ecf5ff; color: #409eff; }
|
|
|
|
|
|
|
|
|
|
|
|
&.active {
|
|
|
|
|
|
background: #ecf5ff;
|
|
|
|
|
|
color: #409eff;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.trend-chart-area {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
padding: 4px 4px 4px 8px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: stretch;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ── 其他图表 ── */
|
|
|
|
|
|
.charts-scroll {
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
padding: 4px;
|
|
|
|
|
|
|
|
|
|
|
|
&::-webkit-scrollbar { width: 4px; }
|
|
|
|
|
|
&::-webkit-scrollbar-thumb { background: #dcdfe6; border-radius: 2px; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 一行两图 */
|
|
|
|
|
|
.charts-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 1fr 1fr;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
align-content: start;
|
|
|
|
|
|
|
|
|
|
|
|
.chart-box { margin-bottom: 0; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chart-box {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 200px;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chart-box-tall { height: 480px; }
|
|
|
|
|
|
|
|
|
|
|
|
/* ── 查找面板 ── */
|
|
|
|
|
|
.search-panel {
|
|
|
|
|
|
width: 210px;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
|
|
|
|
|
padding: 10px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 14px;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.panel-title {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #303133;
|
|
|
|
|
|
padding-bottom: 6px;
|
|
|
|
|
|
border-bottom: 1px solid #ebeef5;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.search-type-group { display: flex; flex-direction: column; gap: 8px; }
|
|
|
|
|
|
|
|
|
|
|
|
.search-field {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
padding-left: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.search-label { font-size: 11px; color: #909399; }
|
|
|
|
|
|
|
|
|
|
|
|
.search-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
margin-top: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.no-data-hint {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 120px;
|
|
|
|
|
|
color: #c0c4cc;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
}
|
2026-05-12 17:15:29 +08:00
|
|
|
|
|
2026-05-11 15:44:37 +08:00
|
|
|
|
</style>
|