Files
double-rack/ruoyi-ui/src/views/mill/model-prediction.vue

2595 lines
76 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="model-page">
<div class="top-section">
<div class="model-card">
<div class="section-header">
<span>双机架冷轧深度模型</span>
<el-tag size="mini" effect="plain">{{ deepModel.version }}</el-tag>
</div>
<div class="theory-grid">
<div
v-for="item in theoryItems"
:key="item.title"
class="theory-item"
>
<div class="theory-title">{{ item.title }}</div>
<div class="theory-formula">{{ item.formula }}</div>
<div class="theory-desc">{{ item.desc }}</div>
</div>
</div>
</div>
<div class="summary-card">
<div class="section-header">
<span>预测结论</span>
<el-button
size="mini"
type="primary"
icon="el-icon-cpu"
@click="runPrediction"
>深度推理</el-button>
</div>
<div class="summary-grid">
<div class="summary-cell">
<span>最终厚度预测</span>
<b>{{ formatNumber(summary.finalThickness, 4) }}</b>
<em>mm</em>
</div>
<div class="summary-cell">
<span>平均置信度</span>
<b>{{ formatNumber(summary.avgConfidence, 2) }}</b>
<em>%</em>
</div>
<div class="summary-cell">
<span>总轧制力</span>
<b>{{ formatNumber(summary.totalForce, 1) }}</b>
<em>kN</em>
</div>
<div class="summary-cell">
<span>总功率</span>
<b>{{ formatNumber(summary.totalPower, 1) }}</b>
<em>kW</em>
</div>
</div>
<div class="risk-line">
<span>综合风险</span>
<el-tag
size="mini"
:type="riskTagType(summary.riskLevel)"
>{{ summary.riskLevel || '未计算' }}</el-tag>
<span class="risk-text">{{ summary.riskText }}</span>
</div>
</div>
</div>
<div class="line-section">
<div class="section-header">
<span>双机架产线动态图</span>
<div class="header-actions">
<el-switch
v-model="lineRunning"
size="mini"
active-text="运行"
inactive-text="暂停"
/>
<el-button
size="mini"
icon="el-icon-refresh-right"
@click="syncLineFromEquipment"
>同步设备</el-button>
</div>
</div>
<div
class="line-canvas"
:class="{ 'line-running': lineRunning }"
>
<div class="line-strip">
<span class="strip-core" />
</div>
<button
v-for="node in lineNodes"
:key="node.key"
type="button"
:class="lineNodeClass(node)"
:style="{ left: node.left }"
@click="selectLineNode(node)"
>
<span class="node-icon">{{ node.icon }}</span>
<b>{{ node.name }}</b>
<em>{{ node.metric }}</em>
</button>
</div>
<div class="line-info">
<span>当前节点<b>{{ activeLineNodeLabel }}</b></span>
<span>带钢速度<b>{{ formatNumber(lineSpeed, 1) }}</b> m/min</span>
<span>F1/F2 压下<b>{{ formatNumber(resultRows[0] && resultRows[0].reductionRate, 2) }}</b>% /
<b>{{ formatNumber(resultRows[1] && resultRows[1].reductionRate, 2) }}</b>%</span>
<span>设备健康<b>{{ selectedEquipment ? selectedEquipment.healthScore : '--' }}</b></span>
</div>
</div>
<div class="middle-section">
<div class="input-card">
<div class="section-header">
<span>计划与特征输入</span>
<div class="header-actions">
<el-button
size="mini"
icon="el-icon-refresh"
@click="loadPlanList"
>刷新计划</el-button>
<el-button
size="mini"
icon="el-icon-delete"
@click="resetAll"
>清空</el-button>
</div>
</div>
<el-form
size="mini"
label-width="92px"
class="compact-form"
>
<el-form-item label="生产计划">
<el-select
v-model="selectedPlanId"
filterable
clearable
placeholder="选择轧制队列计划"
style="width: 100%"
@change="handlePlanChange"
>
<el-option
v-for="plan in planOptions"
:key="plan.planId"
:label="planOptionLabel(plan)"
:value="plan.planId"
/>
</el-select>
</el-form-item>
<el-row :gutter="10">
<el-col :span="12">
<el-form-item label="计划号">
<el-input v-model="coil.planNo" clearable />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="钢卷号">
<el-input v-model="coil.coilNo" clearable />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="合金牌号">
<el-input v-model="coil.alloyNo" clearable />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="合金编码">
<el-input-number
v-model="coil.alloyCode"
:min="0"
:precision="2"
:controls="false"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="入口厚度">
<el-input-number
v-model="coil.entryThickness"
:min="0"
:precision="4"
:controls="false"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="目标厚度">
<el-input-number
v-model="coil.targetThickness"
:min="0"
:precision="4"
:controls="false"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="宽度">
<el-input-number
v-model="coil.width"
:min="0"
:precision="2"
:controls="false"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="来料重量">
<el-input-number
v-model="coil.weight"
:min="0"
:precision="2"
:controls="false"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div class="section-subtitle">深度模型在线适配参数</div>
<el-form
size="mini"
label-width="116px"
class="compact-form"
>
<el-row :gutter="10">
<el-col :span="12">
<el-form-item label="F1力缩放">
<el-input-number
v-model="model.forceScaleF1"
:min="0.2"
:precision="4"
:step="0.01"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="F2力缩放">
<el-input-number
v-model="model.forceScaleF2"
:min="0.2"
:precision="4"
:step="0.01"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="F1厚度偏置">
<el-input-number
v-model="model.thicknessBiasF1"
:precision="4"
:step="0.001"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="F2厚度偏置">
<el-input-number
v-model="model.thicknessBiasF2"
:precision="4"
:step="0.001"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="在线学习率">
<el-input-number
v-model="model.learningRate"
:min="0"
:max="1"
:precision="2"
:step="0.05"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="置信度下限">
<el-input-number
v-model="model.confidenceFloor"
:min="0"
:max="1"
:precision="2"
:step="0.05"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
<div class="stand-card">
<div class="section-header">
<span>双机架深度推理</span>
<div class="header-actions">
<el-button
size="mini"
icon="el-icon-magic-stick"
@click="distributeReduction"
>分配压下</el-button>
<el-button
size="mini"
type="primary"
icon="el-icon-cpu"
@click="runPrediction"
>计算</el-button>
</div>
</div>
<el-table
:data="resultRows"
border
size="mini"
class="stand-table"
height="318"
:row-class-name="standRowClass"
>
<el-table-column
label="机架"
prop="standName"
width="58"
align="center"
fixed
/>
<el-table-column
label="入口厚"
width="104"
>
<template slot-scope="{ row }">
<el-input-number
v-model="row.source.entryThickness"
:min="0"
:precision="4"
:controls="false"
size="mini"
/>
</template>
</el-table-column>
<el-table-column
label="目标厚"
width="104"
>
<template slot-scope="{ row }">
<el-input-number
v-model="row.source.targetThickness"
:min="0"
:precision="4"
:controls="false"
size="mini"
/>
</template>
</el-table-column>
<el-table-column
label="宽度"
width="98"
>
<template slot-scope="{ row }">
<el-input-number
v-model="row.source.width"
:min="0"
:precision="2"
:controls="false"
size="mini"
/>
</template>
</el-table-column>
<el-table-column
label="辊径"
width="98"
>
<template slot-scope="{ row }">
<el-input-number
v-model="row.source.rollDiameter"
:min="1"
:precision="1"
:controls="false"
size="mini"
/>
</template>
</el-table-column>
<el-table-column
label="摩擦"
width="86"
>
<template slot-scope="{ row }">
<el-input-number
v-model="row.source.friction"
:min="0.001"
:precision="4"
:step="0.005"
size="mini"
/>
</template>
</el-table-column>
<el-table-column
label="入口张力"
width="106"
>
<template slot-scope="{ row }">
<el-input-number
v-model="row.source.entryTension"
:min="0"
:precision="2"
:controls="false"
size="mini"
/>
</template>
</el-table-column>
<el-table-column
label="出口张力"
width="106"
>
<template slot-scope="{ row }">
<el-input-number
v-model="row.source.exitTension"
:min="0"
:precision="2"
:controls="false"
size="mini"
/>
</template>
</el-table-column>
<el-table-column
label="速度"
width="96"
>
<template slot-scope="{ row }">
<el-input-number
v-model="row.source.speed"
:min="0"
:precision="1"
:controls="false"
size="mini"
/>
</template>
</el-table-column>
<el-table-column
label="设定力"
width="98"
>
<template slot-scope="{ row }">
<el-input-number
v-model="row.source.setForce"
:min="0"
:precision="1"
:controls="false"
size="mini"
/>
</template>
</el-table-column>
<el-table-column
label="弯辊力"
width="98"
>
<template slot-scope="{ row }">
<el-input-number
v-model="row.source.bendForce"
:precision="1"
:controls="false"
size="mini"
/>
</template>
</el-table-column>
<el-table-column
label="预测轧制力"
width="108"
align="right"
>
<template slot-scope="{ row }">
<span class="calc-val">{{ formatNumber(row.force, 1) }}</span>
</template>
</el-table-column>
<el-table-column
label="预测出口厚"
width="108"
align="right"
>
<template slot-scope="{ row }">
<span class="calc-val">{{ formatNumber(row.predictedExit, 4) }}</span>
</template>
</el-table-column>
<el-table-column
label="置信度"
width="86"
align="right"
>
<template slot-scope="{ row }">
<span :class="confidenceClass(row.confidence)">{{ formatPercent(row.confidence) }}</span>
</template>
</el-table-column>
<el-table-column
label="风险"
min-width="160"
>
<template slot-scope="{ row }">
<el-tag
size="mini"
:type="riskTagType(row.riskLevel)"
>{{ row.riskLevel }}</el-tag>
<span class="risk-cell">{{ row.riskText }}</span>
</template>
</el-table-column>
</el-table>
</div>
</div>
<div class="bottom-section">
<div class="actual-card">
<div class="section-header">
<span>实绩对比与在线校正</span>
<div class="header-actions">
<el-button
size="mini"
icon="el-icon-search"
@click="loadActualList"
>加载实绩</el-button>
<el-button
size="mini"
type="warning"
icon="el-icon-set-up"
:disabled="!correction.canApply"
@click="applyActualCorrection"
>更新适配层</el-button>
<el-button
size="mini"
type="primary"
icon="el-icon-edit-outline"
@click="openTuningDialog(1)"
>录入微调</el-button>
</div>
</div>
<div class="actual-body">
<div class="actual-list">
<el-table
:data="actualList"
border
size="mini"
height="224"
highlight-current-row
@current-change="handleActualSelect"
>
<el-table-column
label="成品卷号"
prop="exitMatId"
width="116"
/>
<el-table-column
label="来料卷号"
prop="entryMatId"
width="116"
/>
<el-table-column
label="计划号"
prop="planNo"
width="105"
/>
<el-table-column
label="成品厚度"
prop="exitThickness"
width="92"
align="right"
/>
<el-table-column
label="成品宽度"
prop="exitWidth"
width="92"
align="right"
/>
<el-table-column
label="实际重量"
prop="actualWeight"
min-width="88"
align="right"
/>
</el-table>
</div>
<div class="actual-edit">
<el-table
:data="resultRows"
border
size="mini"
height="224"
class="actual-table"
>
<el-table-column
label="机架"
prop="standName"
width="58"
align="center"
/>
<el-table-column
label="实绩力"
width="112"
>
<template slot-scope="{ row }">
<el-input-number
v-model="row.source.actualForce"
:min="0"
:precision="1"
:controls="false"
size="mini"
/>
</template>
</el-table-column>
<el-table-column
label="实绩出口厚"
width="124"
>
<template slot-scope="{ row }">
<el-input-number
v-model="row.source.actualExitThickness"
:min="0"
:precision="4"
:controls="false"
size="mini"
/>
</template>
</el-table-column>
<el-table-column
label="力误差"
width="86"
align="right"
>
<template slot-scope="{ row }">
<span :class="errorClass(row.forceError)">{{ formatSigned(row.forceError, 1) }}</span>
</template>
</el-table-column>
<el-table-column
label="厚差"
width="82"
align="right"
>
<template slot-scope="{ row }">
<span :class="errorClass(row.thicknessError)">{{ formatSigned(row.thicknessError, 4) }}</span>
</template>
</el-table-column>
<el-table-column
label="建议力缩放"
min-width="96"
align="right"
>
<template slot-scope="{ row }">
<span class="calc-val">{{ formatNumber(row.forceScaleSuggested, 4) }}</span>
</template>
</el-table-column>
<el-table-column
label="操作"
width="72"
align="center"
>
<template slot-scope="{ row }">
<el-button
size="mini"
type="text"
@click="openTuningDialog(row.source.standNo)"
>微调</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
<div class="correction-line">
<span>适配建议</span>
<span>F1力缩放<b>{{ formatNumber(correction.forceScaleF1, 4) }}</b></span>
<span>F2力缩放<b>{{ formatNumber(correction.forceScaleF2, 4) }}</b></span>
<span>厚度偏置<b>{{ formatSigned(correction.avgThicknessBias, 4) }}</b> mm</span>
<span>样本数<b>{{ correction.sampleCount }}</b></span>
</div>
</div>
<div class="detail-card">
<div class="section-header">
<span>深度模型诊断</span>
</div>
<el-table
:data="resultRows"
border
size="mini"
height="262"
class="detail-table"
>
<el-table-column
label="机架"
prop="standName"
width="58"
align="center"
/>
<el-table-column
label="压下率"
width="74"
align="right"
>
<template slot-scope="{ row }">{{ formatNumber(row.reductionRate, 2) }}%</template>
</el-table-column>
<el-table-column
label="功率"
width="82"
align="right"
>
<template slot-scope="{ row }">{{ formatNumber(row.power, 1) }}</template>
</el-table-column>
<el-table-column
label="板形分"
width="76"
align="right"
>
<template slot-scope="{ row }">{{ formatNumber(row.flatnessScore, 1) }}</template>
</el-table-column>
<el-table-column
label="越界惩罚"
width="82"
align="right"
>
<template slot-scope="{ row }">{{ formatNumber(row.extrapolationPenalty, 2) }}</template>
</el-table-column>
<el-table-column
label="主要特征"
min-width="170"
>
<template slot-scope="{ row }">
<span class="feature-text">{{ row.topFeatures }}</span>
</template>
</el-table-column>
</el-table>
</div>
</div>
<div class="equipment-section">
<div class="section-header">
<span>设备管理与模型反哺</span>
<div class="header-actions">
<el-button
size="mini"
icon="el-icon-plus"
@click="openEquipmentDialog()"
>新增设备</el-button>
<el-button
size="mini"
icon="el-icon-edit"
:disabled="!selectedEquipment"
@click="openEquipmentDialog(selectedEquipment)"
>维护</el-button>
<el-button
size="mini"
type="primary"
icon="el-icon-connection"
:disabled="!selectedEquipment"
@click="applyEquipmentToModel"
>反哺模型</el-button>
</div>
</div>
<el-table
:data="equipmentList"
border
size="mini"
height="286"
highlight-current-row
class="equipment-table"
@current-change="handleEquipmentSelect"
>
<el-table-column
label="设备位号"
prop="equipNo"
width="110"
fixed
/>
<el-table-column
label="设备名称"
prop="equipName"
width="130"
/>
<el-table-column
label="区域"
prop="area"
width="86"
align="center"
/>
<el-table-column
label="类型"
prop="type"
width="96"
align="center"
/>
<el-table-column
label="状态"
prop="status"
width="82"
align="center"
>
<template slot-scope="{ row }">
<el-tag
size="mini"
:type="equipmentStatusType(row.status)"
>{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column
label="健康分"
prop="healthScore"
width="78"
align="right"
/>
<el-table-column
label="采集值"
prop="processValue"
width="86"
align="right"
/>
<el-table-column
label="单位"
prop="unit"
width="62"
align="center"
/>
<el-table-column
label="下限"
prop="lowerLimit"
width="76"
align="right"
/>
<el-table-column
label="上限"
prop="upperLimit"
width="76"
align="right"
/>
<el-table-column
label="模型特征"
prop="modelFeature"
width="120"
/>
<el-table-column
label="影响系数"
prop="impactWeight"
width="86"
align="right"
/>
<el-table-column
label="最近维护"
prop="lastMaintainTime"
width="116"
align="center"
/>
<el-table-column
label="负责人"
prop="owner"
width="82"
align="center"
/>
<el-table-column
label="备注"
prop="remark"
min-width="180"
/>
</el-table>
</div>
<el-dialog
:title="equipmentDialogTitle"
:visible.sync="equipmentDialogVisible"
width="760px"
append-to-body
>
<el-form
ref="equipmentFormRef"
:model="equipmentForm"
:rules="equipmentRules"
size="mini"
label-width="92px"
>
<el-row :gutter="12">
<el-col :span="8">
<el-form-item
label="设备位号"
prop="equipNo"
>
<el-input v-model="equipmentForm.equipNo" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item
label="设备名称"
prop="equipName"
>
<el-input v-model="equipmentForm.equipName" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="区域">
<el-select
v-model="equipmentForm.area"
style="width: 100%"
>
<el-option
v-for="item in areaOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="设备类型">
<el-select
v-model="equipmentForm.type"
style="width: 100%"
>
<el-option label="轧机主机" value="轧机主机" />
<el-option label="张力系统" value="张力系统" />
<el-option label="传动系统" value="传动系统" />
<el-option label="液压系统" value="液压系统" />
<el-option label="测厚仪" value="测厚仪" />
<el-option label="板形仪" value="板形仪" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="状态">
<el-select
v-model="equipmentForm.status"
style="width: 100%"
>
<el-option label="运行" value="运行" />
<el-option label="关注" value="关注" />
<el-option label="检修" value="检修" />
<el-option label="停用" value="停用" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="健康分">
<el-input-number
v-model="equipmentForm.healthScore"
:min="0"
:max="100"
:precision="0"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="采集值">
<el-input-number
v-model="equipmentForm.processValue"
:precision="3"
:controls="false"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="单位">
<el-input v-model="equipmentForm.unit" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="模型特征">
<el-select
v-model="equipmentForm.modelFeature"
style="width: 100%"
>
<el-option label="F1辊径" value="f1RollDiameter" />
<el-option label="F2辊径" value="f2RollDiameter" />
<el-option label="F1张力" value="f1Tension" />
<el-option label="F2张力" value="f2Tension" />
<el-option label="速度" value="speed" />
<el-option label="板形偏置" value="flatnessBias" />
<el-option label="功率缩放" value="powerScale" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="下限">
<el-input-number
v-model="equipmentForm.lowerLimit"
:precision="3"
:controls="false"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="上限">
<el-input-number
v-model="equipmentForm.upperLimit"
:precision="3"
:controls="false"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="影响系数">
<el-input-number
v-model="equipmentForm.impactWeight"
:precision="3"
:step="0.01"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="维护日期">
<el-date-picker
v-model="equipmentForm.lastMaintainTime"
type="date"
value-format="yyyy-MM-dd"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="负责人">
<el-input v-model="equipmentForm.owner" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注">
<el-input
v-model="equipmentForm.remark"
type="textarea"
:rows="2"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<span slot="footer">
<el-button
size="mini"
@click="equipmentDialogVisible = false"
>取消</el-button>
<el-button
size="mini"
type="primary"
@click="submitEquipmentForm"
>保存</el-button>
</span>
</el-dialog>
<el-dialog
title="模型微调样本录入"
:visible.sync="tuningDialogVisible"
width="720px"
append-to-body
>
<el-form
ref="tuningFormRef"
:model="tuningForm"
:rules="tuningRules"
size="mini"
label-width="112px"
>
<el-row :gutter="12">
<el-col :span="8">
<el-form-item label="样本来源">
<el-select
v-model="tuningForm.source"
style="width: 100%"
>
<el-option label="人工复核" value="人工复核" />
<el-option label="质检反馈" value="质检反馈" />
<el-option label="班组经验" value="班组经验" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item
label="机架"
prop="standNo"
>
<el-select
v-model="tuningForm.standNo"
style="width: 100%"
>
<el-option label="F1" :value="1" />
<el-option label="F2" :value="2" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="可信权重">
<el-input-number
v-model="tuningForm.weight"
:min="0.1"
:max="1"
:precision="2"
:step="0.05"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item
label="实绩轧制力"
prop="actualForce"
>
<el-input-number
v-model="tuningForm.actualForce"
:min="0"
:precision="1"
:controls="false"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item
label="实绩出口厚"
prop="actualExitThickness"
>
<el-input-number
v-model="tuningForm.actualExitThickness"
:min="0"
:precision="4"
:controls="false"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="实绩板形分">
<el-input-number
v-model="tuningForm.actualFlatnessScore"
:min="0"
:max="100"
:precision="1"
:controls="false"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注">
<el-input
v-model="tuningForm.remark"
type="textarea"
:rows="2"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div class="tuning-preview">
<span>当前预测力<b>{{ formatNumber(activeTuningRow && activeTuningRow.force, 1) }}</b> kN</span>
<span>当前预测厚<b>{{ formatNumber(activeTuningRow && activeTuningRow.predictedExit, 4) }}</b> mm</span>
<span>应用学习率<b>{{ formatNumber(model.learningRate * tuningForm.weight, 2) }}</b></span>
</div>
<span slot="footer">
<el-button
size="mini"
@click="tuningDialogVisible = false"
>取消</el-button>
<el-button
size="mini"
type="primary"
@click="submitTuningForm"
>写入微调</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import { listActual } from '@/api/mill/actual'
import { listPlan } from '@/api/mill/plan'
import { getRecipeDetail } from '@/api/mill/recipe'
const FEATURE_NAMES = [
'机架号',
'入口厚度',
'目标厚度',
'带钢宽度',
'压下率',
'辊径',
'摩擦系数',
'入口张力',
'出口张力',
'张力差',
'速度',
'设定力',
'轧机刚度',
'弯辊力',
'合金编码',
'来料重量',
'前机架出口',
'宽厚比'
]
const FEATURE_MEAN = [
1.5, 1.2, 0.75, 1250, 32, 350, 0.065, 28, 35,
0, 650, 780, 4400, 0, 2.5, 6, 0.9, 1300
]
const FEATURE_SCALE = [
0.5, 0.8, 0.55, 420, 18, 55, 0.025, 26, 28,
42, 420, 520, 900, 160, 2.5, 5, 0.7, 900
]
const OUTPUT_MEAN = {
1: [820, 0, 520, 42, 0.76],
2: [640, 0, 460, 38, 0.78]
}
const OUTPUT_SCALE = {
1: [360, 0.065, 340, 25, 0.18],
2: [300, 0.045, 290, 23, 0.16]
}
const emptyCoil = () => ({
planId: null,
planNo: '',
coilNo: '',
alloyNo: '',
alloyCode: null,
entryThickness: null,
targetThickness: null,
width: null,
weight: null,
length: null
})
const emptyModel = () => ({
forceScaleF1: 1,
forceScaleF2: 1,
thicknessBiasF1: 0,
thicknessBiasF2: 0,
powerScale: 1,
flatnessBias: 0,
learningRate: 0.35,
confidenceFloor: 0.55,
forceLimit: 1800,
powerLimit: 1600,
reductionLimit: 62,
flatnessLimit: 72
})
const emptyStand = (standNo) => ({
standNo,
entryThickness: null,
targetThickness: null,
width: null,
rollDiameter: standNo === 1 ? 360 : 340,
friction: standNo === 1 ? 0.07 : 0.06,
entryTension: null,
exitTension: null,
speed: null,
setForce: null,
millModulus: standNo === 1 ? 4200 : 4600,
bendForce: null,
actualForce: null,
actualExitThickness: null
})
const emptyEquipment = () => ({
equipNo: '',
equipName: '',
area: 'F1',
type: '轧机主机',
status: '运行',
healthScore: 95,
processValue: null,
unit: '',
lowerLimit: null,
upperLimit: null,
modelFeature: '',
impactWeight: 1,
lastMaintainTime: '',
owner: '',
remark: ''
})
const defaultEquipmentList = () => ([
{
equipNo: 'F1-MILL-001',
equipName: 'F1轧机主传动',
area: 'F1',
type: '轧机主机',
status: '运行',
healthScore: 94,
processValue: 360,
unit: 'mm',
lowerLimit: 320,
upperLimit: 390,
modelFeature: 'f1RollDiameter',
impactWeight: 1,
lastMaintainTime: '2026-05-18',
owner: '设备班',
remark: '工作辊径参与F1模型特征'
},
{
equipNo: 'F2-MILL-001',
equipName: 'F2轧机主传动',
area: 'F2',
type: '轧机主机',
status: '运行',
healthScore: 92,
processValue: 340,
unit: 'mm',
lowerLimit: 320,
upperLimit: 390,
modelFeature: 'f2RollDiameter',
impactWeight: 1,
lastMaintainTime: '2026-05-18',
owner: '设备班',
remark: '工作辊径参与F2模型特征'
},
{
equipNo: 'TENS-ENTRY',
equipName: '入口张力计',
area: '入口段',
type: '张力系统',
status: '运行',
healthScore: 90,
processValue: 28,
unit: 'kN',
lowerLimit: 5,
upperLimit: 80,
modelFeature: 'f1Tension',
impactWeight: 0.85,
lastMaintainTime: '2026-05-20',
owner: '仪表班',
remark: '影响F1入口张力'
},
{
equipNo: 'TENS-INTER',
equipName: '机架间张力计',
area: '机架间',
type: '张力系统',
status: '关注',
healthScore: 78,
processValue: 35,
unit: 'kN',
lowerLimit: 5,
upperLimit: 90,
modelFeature: 'f2Tension',
impactWeight: 0.75,
lastMaintainTime: '2026-05-12',
owner: '仪表班',
remark: '健康分低时会降低模型置信度'
},
{
equipNo: 'SPD-MAIN',
equipName: '主线速度反馈',
area: '主线',
type: '传动系统',
status: '运行',
healthScore: 96,
processValue: 650,
unit: 'm/min',
lowerLimit: 0,
upperLimit: 1200,
modelFeature: 'speed',
impactWeight: 1,
lastMaintainTime: '2026-05-21',
owner: '电气班',
remark: '同步F1/F2速度输入'
},
{
equipNo: 'SHAPE-EXIT',
equipName: '出口板形仪',
area: '出口段',
type: '板形仪',
status: '运行',
healthScore: 88,
processValue: 0,
unit: 'I-Unit',
lowerLimit: -80,
upperLimit: 80,
modelFeature: 'flatnessBias',
impactWeight: 0.2,
lastMaintainTime: '2026-05-15',
owner: '仪表班',
remark: '用于板形风险偏置'
},
{
equipNo: 'POWER-MAIN',
equipName: '主电机功率采集',
area: '主线',
type: '传动系统',
status: '运行',
healthScore: 91,
processValue: 1,
unit: 'ratio',
lowerLimit: 0.7,
upperLimit: 1.3,
modelFeature: 'powerScale',
impactWeight: 0.15,
lastMaintainTime: '2026-05-17',
owner: '电气班',
remark: '用于功率输出缩放'
}
])
const emptyTuningForm = () => ({
source: '人工复核',
standNo: 1,
weight: 0.8,
actualForce: null,
actualExitThickness: null,
actualFlatnessScore: null,
remark: ''
})
const clamp = (value, min, max) => Math.min(Math.max(value, min), max)
const makeDense = (inputSize, outputSize, seed, gain) => {
const weights = []
for (let row = 0; row < outputSize; row++) {
const line = []
for (let col = 0; col < inputSize; col++) {
const raw = Math.sin((row + 1) * 12.9898 + (col + 1) * 78.233 + seed) *
Math.cos((row + 1) * 4.1414 + seed)
line.push(Number((raw * gain).toFixed(6)))
}
weights.push(line)
}
const bias = []
for (let index = 0; index < outputSize; index++) {
bias.push(Number((Math.sin(seed + index * 0.71) * gain * 0.15).toFixed(6)))
}
return { weights, bias }
}
const DEEP_MODEL = {
version: 'DCR-MLP-2Stand-v1',
inputSize: FEATURE_NAMES.length,
featureNames: FEATURE_NAMES,
layers: [
{ ...makeDense(FEATURE_NAMES.length, 28, 1.7, 0.34), activation: 'gelu' },
{ ...makeDense(28, 18, 2.9, 0.28), activation: 'gelu' },
{ ...makeDense(18, 10, 4.2, 0.22), activation: 'tanh' },
{ ...makeDense(10, 5, 5.6, 0.18), activation: 'linear' }
]
}
export default {
name: 'MillModelPrediction',
data() {
return {
deepModel: DEEP_MODEL,
selectedPlanId: null,
planOptions: [],
actualList: [],
coil: emptyCoil(),
model: emptyModel(),
stands: [emptyStand(1), emptyStand(2)],
lineRunning: true,
activeLineNodeKey: 'F1',
selectedEquipment: null,
equipmentList: defaultEquipmentList(),
equipmentDialogVisible: false,
equipmentEditIndex: -1,
equipmentForm: emptyEquipment(),
areaOptions: ['入口段', 'F1', '机架间', 'F2', '出口段', '主线'],
tuningDialogVisible: false,
tuningForm: emptyTuningForm(),
tuningSamples: [],
equipmentRules: {
equipNo: [{ required: true, message: '请输入设备位号', trigger: 'blur' }],
equipName: [{ required: true, message: '请输入设备名称', trigger: 'blur' }]
},
tuningRules: {
standNo: [{ required: true, message: '请选择机架', trigger: 'change' }],
actualForce: [{ required: true, message: '请输入实绩轧制力', trigger: 'blur' }],
actualExitThickness: [{ required: true, message: '请输入实绩出口厚', trigger: 'blur' }]
},
theoryItems: [
{
title: '输入特征',
formula: '18维工艺特征向量',
desc: '入口/目标厚度、宽度、压下率、张力、速度、辊径、摩擦、弯辊力、合金编码等全部参与推理。'
},
{
title: '网络结构',
formula: 'MLP: 18 -> 28 -> 18 -> 10 -> 5',
desc: '每个机架独立执行多层前向推理,隐藏层使用 GELU/Tanh 激活,输出多任务结果。'
},
{
title: '输出头',
formula: 'Force / Thickness / Power / Flatness / Confidence',
desc: '一次推理同时给出轧制力、出口厚度偏差、功率、板形风险分和模型置信度。'
},
{
title: '在线适配',
formula: 'Adapter = Adapter + lr * Error',
desc: '生产实绩只更新适配层,不改基础网络权重,适合按钢种或班次逐步校正。'
}
]
}
},
computed: {
resultRows() {
const rows = []
let previousExit = null
this.stands.forEach((stand, index) => {
const prediction = this.predictStand(stand, previousExit)
rows.push({
...prediction,
standName: `F${index + 1}`,
source: stand
})
previousExit = prediction.predictedExit || prediction.targetThickness
})
return rows
},
summary() {
const validRows = this.resultRows.filter(row => row.valid)
const last = validRows[validRows.length - 1]
const riskOrder = ['正常', '关注', '预警', '高风险']
const maxRisk = validRows.reduce((risk, row) => {
return riskOrder.indexOf(row.riskLevel) > riskOrder.indexOf(risk) ? row.riskLevel : risk
}, validRows.length ? '正常' : '')
const avgConfidence = validRows.length
? validRows.reduce((sum, row) => sum + row.confidence, 0) / validRows.length * 100
: null
return {
finalThickness: last ? last.predictedExit : null,
avgConfidence,
totalForce: validRows.reduce((sum, row) => sum + (row.force || 0), 0),
totalPower: validRows.reduce((sum, row) => sum + (row.power || 0), 0),
riskLevel: maxRisk,
riskText: this.buildSummaryRiskText(validRows, maxRisk)
}
},
correction() {
const forceScale = { 1: [], 2: [] }
const thicknessBias = []
this.resultRows.forEach(row => {
const actualForce = this.toNumber(row.source.actualForce)
const actualThickness = this.toNumber(row.source.actualExitThickness)
if (row.force > 0 && actualForce > 0) {
forceScale[row.source.standNo].push(actualForce / row.force)
}
if (row.predictedExit > 0 && actualThickness > 0) {
thicknessBias.push(actualThickness - row.predictedExit)
}
})
const forceScaleF1 = this.average(forceScale[1])
const forceScaleF2 = this.average(forceScale[2])
return {
forceScaleF1,
forceScaleF2,
avgThicknessBias: this.average(thicknessBias),
sampleCount: forceScale[1].length + forceScale[2].length + thicknessBias.length,
canApply: forceScale[1].length > 0 || forceScale[2].length > 0 || thicknessBias.length > 0
}
},
lineNodes() {
const f1 = this.resultRows[0] || {}
const f2 = this.resultRows[1] || {}
return [
{
key: 'ENTRY',
name: '入口开卷',
icon: 'IN',
left: '6%',
status: this.getAreaStatus('入口段'),
metric: `${this.formatNumber(this.coil.entryThickness, 3)} mm`
},
{
key: 'F1',
name: 'F1机架',
icon: 'F1',
left: '28%',
status: this.getAreaStatus('F1'),
metric: `${this.formatNumber(f1.force, 0)} kN`
},
{
key: 'INTER',
name: '机架间',
icon: 'T',
left: '50%',
status: this.getAreaStatus('机架间'),
metric: `${this.formatNumber(this.stands[1].entryThickness, 3)} mm`
},
{
key: 'F2',
name: 'F2机架',
icon: 'F2',
left: '70%',
status: this.getAreaStatus('F2'),
metric: `${this.formatNumber(f2.force, 0)} kN`
},
{
key: 'EXIT',
name: '出口卷取',
icon: 'OUT',
left: '90%',
status: this.getAreaStatus('出口段'),
metric: `${this.formatNumber(this.summary.finalThickness, 4)} mm`
}
]
},
activeLineNodeLabel() {
const node = this.lineNodes.find(item => item.key === this.activeLineNodeKey)
return node ? node.name : '--'
},
lineSpeed() {
const f1Speed = this.toNumber(this.stands[0].speed)
const f2Speed = this.toNumber(this.stands[1].speed)
return f2Speed || f1Speed || 0
},
equipmentDialogTitle() {
return this.equipmentEditIndex >= 0 ? '设备维护' : '新增设备'
},
activeTuningRow() {
return this.resultRows.find(row => row.source.standNo === this.tuningForm.standNo) || null
}
},
mounted() {
this.loadPlanList()
},
methods: {
loadPlanList() {
listPlan({ pageNum: 1, pageSize: 100 }).then(res => {
this.planOptions = res.rows || res.data || []
}).catch(() => {
this.$message.warning('生产计划加载失败')
})
},
handlePlanChange(planId) {
if (!planId) return
const plan = this.planOptions.find(item => item.planId === planId)
if (!plan) return
this.applyPlan(plan)
if (plan.recipeId) {
getRecipeDetail(plan.recipeId).then(res => {
this.applyRecipe(res.data || {})
}).catch(() => {
this.distributeReduction()
this.$message.warning('工艺方案加载失败,已按厚度自动分配压下')
})
} else {
this.distributeReduction()
}
},
applyPlan(plan) {
this.coil = {
planId: plan.planId,
planNo: plan.planNo || '',
coilNo: plan.inMatNo || '',
alloyNo: plan.alloyNo || '',
alloyCode: this.inferAlloyCode(plan.alloyNo),
entryThickness: this.toNumber(plan.inMatThick),
targetThickness: this.toNumber(plan.outThick),
width: this.toNumber(plan.inMatWidth),
weight: this.toNumber(plan.inMatWeight),
length: this.toNumber(plan.inMatLength)
}
this.stands.forEach(stand => {
stand.width = this.coil.width
})
},
applyRecipe(recipe) {
const passList = Array.isArray(recipe.passList) ? recipe.passList : []
if (passList.length >= 2) {
this.fillStandFromPass(this.stands[0], passList[0])
this.fillStandFromPass(this.stands[1], passList[1])
return
}
if (passList.length === 1) {
this.fillStandFromPass(this.stands[0], passList[0])
this.stands[1].entryThickness = this.toNumber(passList[0].outThick)
this.stands[1].targetThickness = this.toNumber(this.coil.targetThickness)
this.stands[1].width = this.toNumber(passList[0].width) || this.coil.width
return
}
this.distributeReduction(false)
},
fillStandFromPass(stand, pass) {
stand.entryThickness = this.toNumber(pass.inThick)
stand.targetThickness = this.toNumber(pass.outThick)
stand.width = this.toNumber(pass.width) || this.coil.width
stand.setForce = this.toNumber(pass.rollForce)
stand.entryTension = this.toNumber(pass.inTension)
stand.exitTension = this.toNumber(pass.outTension)
stand.speed = this.toNumber(pass.maxSpeed)
},
distributeReduction(showWarning = true) {
const entry = this.toNumber(this.coil.entryThickness)
const target = this.toNumber(this.coil.targetThickness)
const width = this.toNumber(this.coil.width)
if (!entry || !target || entry <= target) {
if (showWarning) this.$message.warning('请先输入有效的入口厚度和目标厚度')
return
}
const middle = this.round(entry - (entry - target) * 0.56, 4)
this.stands[0].entryThickness = entry
this.stands[0].targetThickness = middle
this.stands[0].width = width
this.stands[1].entryThickness = middle
this.stands[1].targetThickness = target
this.stands[1].width = width
},
loadActualList() {
const query = { pageNum: 1, pageSize: 30 }
if (this.coil.planNo) {
query.planNo = this.coil.planNo
} else if (this.coil.coilNo) {
query.entryMatId = this.coil.coilNo
}
listActual(query).then(res => {
this.actualList = res.rows || []
if (!this.actualList.length) {
this.$message.info('未查询到匹配的生产实绩')
}
}).catch(() => {
this.$message.warning('生产实绩加载失败')
})
},
handleActualSelect(row) {
if (!row) return
const exitThickness = this.normalizeActualThickness(row.exitThickness)
if (exitThickness) {
this.stands[1].actualExitThickness = exitThickness
}
},
selectLineNode(node) {
this.activeLineNodeKey = node.key
const areaMap = {
ENTRY: '入口段',
F1: 'F1',
INTER: '机架间',
F2: 'F2',
EXIT: '出口段'
}
const area = areaMap[node.key]
const equipment = this.equipmentList.find(item => item.area === area)
if (equipment) this.selectedEquipment = equipment
},
lineNodeClass(node) {
return [
'line-node',
`line-node--${node.status}`,
{ active: node.key === this.activeLineNodeKey }
]
},
getAreaStatus(area) {
const list = this.equipmentList.filter(item => item.area === area)
if (!list.length) return 'normal'
if (list.some(item => item.status === '检修' || item.status === '停用' || item.healthScore < 60)) return 'danger'
if (list.some(item => item.status === '关注' || item.healthScore < 82)) return 'warning'
return 'normal'
},
handleEquipmentSelect(row) {
this.selectedEquipment = row
if (!row) return
const keyMap = {
'入口段': 'ENTRY',
F1: 'F1',
'机架间': 'INTER',
F2: 'F2',
'出口段': 'EXIT'
}
this.activeLineNodeKey = keyMap[row.area] || this.activeLineNodeKey
},
openEquipmentDialog(row) {
if (row) {
this.equipmentEditIndex = this.equipmentList.indexOf(row)
this.equipmentForm = { ...row }
} else {
this.equipmentEditIndex = -1
this.equipmentForm = emptyEquipment()
}
this.equipmentDialogVisible = true
},
submitEquipmentForm() {
this.$refs.equipmentFormRef.validate(valid => {
if (!valid) return
const payload = { ...this.equipmentForm }
if (this.equipmentEditIndex >= 0) {
this.$set(this.equipmentList, this.equipmentEditIndex, payload)
this.selectedEquipment = payload
} else {
this.equipmentList.push(payload)
this.selectedEquipment = payload
}
this.equipmentDialogVisible = false
this.$message.success('设备信息已保存')
})
},
syncLineFromEquipment() {
this.equipmentList.forEach(item => {
this.applyEquipmentValue(item, false)
})
this.$message.success('设备采集值已同步到模型输入')
},
applyEquipmentToModel() {
if (!this.selectedEquipment) return
this.applyEquipmentValue(this.selectedEquipment, true)
},
applyEquipmentValue(equipment, notify) {
const value = this.toNumber(equipment.processValue)
const impact = this.toNumber(equipment.impactWeight) || 1
if (value === null) return
if (equipment.modelFeature === 'f1RollDiameter') this.stands[0].rollDiameter = value
if (equipment.modelFeature === 'f2RollDiameter') this.stands[1].rollDiameter = value
if (equipment.modelFeature === 'f1Tension') {
this.stands[0].entryTension = value
this.stands[0].exitTension = this.stands[0].exitTension || value
}
if (equipment.modelFeature === 'f2Tension') {
this.stands[1].entryTension = value
this.stands[1].exitTension = this.stands[1].exitTension || value
}
if (equipment.modelFeature === 'speed') {
this.stands[0].speed = value
this.stands[1].speed = value
}
if (equipment.modelFeature === 'flatnessBias') {
this.model.flatnessBias = this.round(value * impact, 3)
}
if (equipment.modelFeature === 'powerScale') {
this.model.powerScale = this.round(value * impact + (1 - impact), 4)
}
if (equipment.healthScore < 82) {
this.model.confidenceFloor = clamp(this.model.confidenceFloor + 0.03, 0, 0.9)
}
if (notify) this.$message.success('设备参数已反哺模型')
},
openTuningDialog(standNo) {
this.tuningForm = {
...emptyTuningForm(),
standNo: standNo || 1
}
this.tuningDialogVisible = true
},
submitTuningForm() {
this.$refs.tuningFormRef.validate(valid => {
if (!valid) return
const row = this.activeTuningRow
if (!row || !row.valid) {
this.$message.warning('当前机架没有可用预测结果')
return
}
const sample = {
...this.tuningForm,
predictedForce: row.force,
predictedExit: row.predictedExit,
createTime: new Date().toISOString()
}
this.tuningSamples.push(sample)
this.applyTuningSample(sample)
this.tuningDialogVisible = false
this.$message.success('微调样本已写入适配层')
})
},
applyTuningSample(sample) {
const rate = clamp(this.model.learningRate * sample.weight, 0, 1)
const forceRatio = sample.predictedForce > 0 ? sample.actualForce / sample.predictedForce : null
const thicknessError = sample.actualExitThickness - sample.predictedExit
if (sample.standNo === 1) {
if (forceRatio) this.model.forceScaleF1 = this.round(this.model.forceScaleF1 * (1 + rate * (forceRatio - 1)), 4)
this.model.thicknessBiasF1 = this.round(this.model.thicknessBiasF1 + rate * thicknessError, 4)
} else {
if (forceRatio) this.model.forceScaleF2 = this.round(this.model.forceScaleF2 * (1 + rate * (forceRatio - 1)), 4)
this.model.thicknessBiasF2 = this.round(this.model.thicknessBiasF2 + rate * thicknessError, 4)
}
if (sample.actualFlatnessScore !== null && sample.actualFlatnessScore !== undefined) {
const flatnessError = sample.actualFlatnessScore - (this.activeTuningRow ? this.activeTuningRow.flatnessScore : 0)
this.model.flatnessBias = this.round(this.model.flatnessBias + rate * flatnessError * 0.1, 3)
}
},
applyActualCorrection() {
const rate = clamp(this.model.learningRate, 0, 1)
if (this.correction.forceScaleF1) {
this.model.forceScaleF1 = this.round(
this.model.forceScaleF1 * (1 + rate * (this.correction.forceScaleF1 - 1)),
4
)
}
if (this.correction.forceScaleF2) {
this.model.forceScaleF2 = this.round(
this.model.forceScaleF2 * (1 + rate * (this.correction.forceScaleF2 - 1)),
4
)
}
if (this.correction.avgThicknessBias != null) {
this.model.thicknessBiasF1 = this.round(this.model.thicknessBiasF1 + rate * this.correction.avgThicknessBias, 4)
this.model.thicknessBiasF2 = this.round(this.model.thicknessBiasF2 + rate * this.correction.avgThicknessBias, 4)
}
this.$message.success('深度模型适配层已更新')
},
runPrediction() {
if (!this.resultRows.some(row => row.valid)) {
this.$message.warning('请先输入双机架厚度、宽度和目标参数')
return
}
this.$message.success('深度模型推理已刷新')
},
resetAll() {
this.selectedPlanId = null
this.actualList = []
this.coil = emptyCoil()
this.model = emptyModel()
this.stands = [emptyStand(1), emptyStand(2)]
},
predictStand(stand, previousExit) {
const featureState = this.buildFeatureVector(stand, previousExit)
const base = {
valid: false,
entryThickness: featureState.entryThickness,
targetThickness: featureState.targetThickness,
force: null,
predictedExit: null,
power: null,
flatnessScore: null,
confidence: null,
riskLevel: '未计算',
riskText: '参数不足',
topFeatures: '--'
}
if (!featureState.valid) return base
const normalized = this.normalizeVector(featureState.features)
const rawOutput = this.forwardNetwork(normalized)
const decoded = this.decodeOutput(rawOutput, stand, featureState)
const risk = this.evaluateRisk(decoded, featureState)
const topFeatures = this.getTopFeatures(normalized, decoded, stand, featureState)
const actualForce = this.toNumber(stand.actualForce)
const actualExitThickness = this.toNumber(stand.actualExitThickness)
return {
valid: true,
entryThickness: featureState.entryThickness,
targetThickness: featureState.targetThickness,
reductionRate: featureState.reductionRate,
force: decoded.force,
predictedExit: decoded.predictedExit,
power: decoded.power,
flatnessScore: decoded.flatnessScore,
confidence: decoded.confidence,
extrapolationPenalty: featureState.extrapolationPenalty,
riskLevel: risk.level,
riskText: risk.text,
topFeatures,
forceError: actualForce ? actualForce - decoded.force : null,
thicknessError: actualExitThickness ? actualExitThickness - decoded.predictedExit : null,
forceScaleSuggested: actualForce && decoded.force > 0 ? actualForce / decoded.force : null
}
},
buildFeatureVector(stand, previousExit) {
const entryThickness = this.toNumber(stand.entryThickness) || previousExit
const targetThickness = this.toNumber(stand.targetThickness)
const width = this.toNumber(stand.width) || this.toNumber(this.coil.width)
const rollDiameter = this.toNumber(stand.rollDiameter)
if (!entryThickness || !targetThickness || !width || !rollDiameter || entryThickness <= targetThickness) {
return { valid: false, entryThickness, targetThickness, features: [] }
}
const reduction = entryThickness - targetThickness
const reductionRate = reduction / entryThickness * 100
const entryTension = this.toNumber(stand.entryTension) || 0
const exitTension = this.toNumber(stand.exitTension) || 0
const speed = this.toNumber(stand.speed) || 0
const setForce = this.toNumber(stand.setForce) || 0
const millModulus = this.toNumber(stand.millModulus) || (stand.standNo === 1 ? 4200 : 4600)
const bendForce = this.toNumber(stand.bendForce) || 0
const friction = this.toNumber(stand.friction) || 0.065
const alloyCode = this.toNumber(this.coil.alloyCode) || this.inferAlloyCode(this.coil.alloyNo)
const weight = this.toNumber(this.coil.weight) || 0
const prevExit = previousExit || entryThickness
const widthThicknessRatio = width / Math.max(targetThickness, 0.001)
const features = [
stand.standNo,
entryThickness,
targetThickness,
width,
reductionRate,
rollDiameter,
friction,
entryTension,
exitTension,
exitTension - entryTension,
speed,
setForce,
millModulus,
bendForce,
alloyCode,
weight,
prevExit,
widthThicknessRatio
]
return {
valid: true,
features,
entryThickness,
targetThickness,
width,
reductionRate,
setForce,
speed,
bendForce,
extrapolationPenalty: this.calcExtrapolationPenalty(features)
}
},
normalizeVector(features) {
return features.map((value, index) => {
return clamp((value - FEATURE_MEAN[index]) / FEATURE_SCALE[index], -4, 4)
})
},
forwardNetwork(input) {
let vector = input.slice()
this.deepModel.layers.forEach(layer => {
const next = layer.weights.map((weights, rowIndex) => {
const sum = weights.reduce((total, weight, colIndex) => total + weight * vector[colIndex], layer.bias[rowIndex])
return this.activate(sum, layer.activation)
})
vector = next
})
return vector
},
activate(value, activation) {
if (activation === 'gelu') {
return 0.5 * value * (1 + Math.tanh(Math.sqrt(2 / Math.PI) * (value + 0.044715 * Math.pow(value, 3))))
}
if (activation === 'tanh') return Math.tanh(value)
return value
},
decodeOutput(rawOutput, stand, state) {
const standNo = stand.standNo
const mean = OUTPUT_MEAN[standNo]
const scale = OUTPUT_SCALE[standNo]
const forceScale = standNo === 1 ? this.model.forceScaleF1 : this.model.forceScaleF2
const thicknessBias = standNo === 1 ? this.model.thicknessBiasF1 : this.model.thicknessBiasF2
const networkForce = mean[0] + rawOutput[0] * scale[0]
const setForce = state.setForce || networkForce
const force = clamp((networkForce * 0.78 + setForce * 0.22) * forceScale, 80, 3200)
const thicknessDelta = clamp(rawOutput[1] * scale[1] + thicknessBias, -0.28, 0.28)
const predictedExit = clamp(
state.targetThickness + thicknessDelta,
Math.max(0.01, state.targetThickness * 0.7),
state.entryThickness * 1.02
)
const power = clamp((mean[2] + rawOutput[2] * scale[2]) * this.model.powerScale, 0, 3600)
const flatnessScore = clamp(mean[3] + rawOutput[3] * scale[3] + this.model.flatnessBias, 0, 100)
const confidenceBase = clamp(mean[4] + rawOutput[4] * scale[4], 0.05, 0.98)
const confidence = clamp(confidenceBase - state.extrapolationPenalty, 0.05, 0.98)
return { force, predictedExit, power, flatnessScore, confidence }
},
getTopFeatures(input, decoded, stand, state) {
const baseline = this.featureObjective(decoded)
const impacts = input.map((value, index) => {
const changed = input.slice()
changed[index] = clamp(value + 0.15, -4, 4)
const raw = this.forwardNetwork(changed)
const next = this.decodeOutput(raw, stand, state)
return {
name: FEATURE_NAMES[index],
impact: Math.abs(this.featureObjective(next) - baseline)
}
})
return impacts
.sort((a, b) => b.impact - a.impact)
.slice(0, 3)
.map(item => item.name)
.join('、')
},
featureObjective(output) {
return output.force / 1200 + output.predictedExit * 3 + output.flatnessScore / 80
},
calcExtrapolationPenalty(features) {
const penalty = features.reduce((sum, value, index) => {
const normalized = Math.abs((value - FEATURE_MEAN[index]) / FEATURE_SCALE[index])
return sum + Math.max(0, normalized - 2.5) * 0.035
}, 0)
return clamp(penalty, 0, 0.45)
},
evaluateRisk(decoded, state) {
const risks = []
if (decoded.confidence < this.model.confidenceFloor) risks.push('模型置信度偏低')
if (decoded.force > this.model.forceLimit) risks.push('轧制力接近上限')
if (decoded.power > this.model.powerLimit) risks.push('功率负载偏高')
if (state.reductionRate > this.model.reductionLimit) risks.push('压下率超限')
if (decoded.flatnessScore > this.model.flatnessLimit) risks.push('板形风险偏高')
if (risks.length >= 3) return { level: '高风险', text: risks.join('、') }
if (risks.length === 2) return { level: '预警', text: risks.join('、') }
if (risks.length === 1) return { level: '关注', text: risks[0] }
return { level: '正常', text: '深度模型未发现明显风险' }
},
buildSummaryRiskText(rows, level) {
if (!rows.length) return '等待输入计划、工艺或手工参数'
if (level === '正常') return '双机架深度推理结果处于当前阈值范围'
return rows
.filter(row => row.riskLevel !== '正常')
.map(row => `${row.standName}${row.riskText}`)
.join('')
},
inferAlloyCode(alloyNo) {
const text = String(alloyNo || '').toUpperCase()
if (!text) return null
if (text.indexOf('3') === 0) return 3
if (text.indexOf('5') === 0) return 5
if (text.indexOf('6') === 0) return 6
const digit = text.match(/\d/)
return digit ? Number(digit[0]) : 2
},
normalizeActualThickness(value) {
const thickness = this.toNumber(value)
const entry = this.toNumber(this.coil.entryThickness)
if (!thickness) return null
if (entry && entry < 20 && thickness > 20) {
return this.round(thickness / 1000, 4)
}
return thickness
},
average(values) {
if (!values.length) return null
return values.reduce((sum, item) => sum + item, 0) / values.length
},
toNumber(value) {
if (value === null || value === undefined || value === '') return null
const numberValue = Number(value)
return Number.isFinite(numberValue) ? numberValue : null
},
round(value, precision) {
if (value === null || value === undefined || !Number.isFinite(Number(value))) return null
return Number(Number(value).toFixed(precision))
},
formatNumber(value, precision) {
const numberValue = this.toNumber(value)
if (numberValue === null) return '--'
return numberValue.toFixed(precision)
},
formatPercent(value) {
const numberValue = this.toNumber(value)
if (numberValue === null) return '--'
return `${(numberValue * 100).toFixed(1)}%`
},
formatSigned(value, precision) {
const numberValue = this.toNumber(value)
if (numberValue === null) return '--'
const prefix = numberValue > 0 ? '+' : ''
return `${prefix}${numberValue.toFixed(precision)}`
},
riskTagType(level) {
if (level === '高风险') return 'danger'
if (level === '预警') return 'warning'
if (level === '关注') return 'info'
if (level === '正常') return 'success'
return ''
},
confidenceClass(value) {
const numberValue = this.toNumber(value)
if (numberValue === null) return 'muted-val'
if (numberValue < this.model.confidenceFloor) return 'bad-val'
if (numberValue < 0.7) return 'warn-val'
return 'good-val'
},
errorClass(value) {
const numberValue = this.toNumber(value)
if (numberValue === null) return 'muted-val'
return Math.abs(numberValue) <= 0.01 ? 'good-val' : 'bad-val'
},
planOptionLabel(plan) {
const coilNo = plan.inMatNo || '未填卷号'
const size = `${plan.inMatThick || '-'} -> ${plan.outThick || '-'} mm`
return `${coilNo} / ${plan.planNo || '-'} / ${size}`
},
standRowClass({ rowIndex }) {
return rowIndex % 2 === 0 ? '' : 'alt-row'
}
}
}
</script>
<style scoped lang="scss">
.model-page {
min-height: calc(100vh - 84px);
padding: 10px 12px;
box-sizing: border-box;
background: #f0f2f5;
color: #1f2d3d;
}
.top-section,
.middle-section,
.bottom-section {
display: grid;
grid-template-columns: minmax(0, 1fr) 360px;
gap: 10px;
margin-bottom: 10px;
}
.middle-section {
grid-template-columns: 360px minmax(0, 1fr);
}
.bottom-section {
grid-template-columns: minmax(0, 1fr) 420px;
margin-bottom: 0;
}
.line-section,
.equipment-section {
margin-bottom: 10px;
background: #fff;
border: 1px solid #dde1e6;
border-radius: 3px;
overflow: hidden;
}
.model-card,
.summary-card,
.input-card,
.stand-card,
.actual-card,
.detail-card {
background: #fff;
border: 1px solid #dde1e6;
border-radius: 3px;
overflow: hidden;
}
.section-header {
min-height: 34px;
padding: 7px 10px;
box-sizing: border-box;
background: #1c2b3a;
color: #ecf0f1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 12px;
font-weight: 700;
}
.header-actions {
display: flex;
align-items: center;
gap: 6px;
}
.line-canvas {
position: relative;
height: 154px;
margin: 10px;
border: 1px solid #e4e7ed;
border-radius: 3px;
background:
linear-gradient(180deg, #fbfcfe 0%, #f6f8fb 100%);
overflow: hidden;
}
.line-strip {
position: absolute;
left: 5%;
right: 5%;
top: 72px;
height: 10px;
border-radius: 10px;
background: #c8d2df;
overflow: hidden;
}
.strip-core {
display: block;
width: 35%;
height: 100%;
border-radius: 10px;
background: linear-gradient(90deg, #5f7f9f, #1d4e89);
}
.line-running .strip-core {
animation: stripMove 1.8s linear infinite;
}
@keyframes stripMove {
0% { transform: translateX(-120%); }
100% { transform: translateX(360%); }
}
.line-node {
position: absolute;
top: 34px;
width: 106px;
height: 88px;
transform: translateX(-50%);
border: 1px solid #cfd8e3;
border-radius: 4px;
background: #fff;
color: #1f2d3d;
cursor: pointer;
box-shadow: 0 1px 3px rgba(18, 35, 54, 0.08);
transition: border-color .16s, box-shadow .16s, transform .16s;
&:hover,
&.active {
border-color: #1d4e89;
box-shadow: 0 4px 12px rgba(29, 78, 137, 0.18);
transform: translateX(-50%) translateY(-2px);
}
b,
em {
display: block;
font-style: normal;
text-align: center;
}
b {
margin-top: 5px;
font-size: 12px;
}
em {
margin-top: 4px;
font-family: Consolas, Monaco, monospace;
font-size: 11px;
color: #606266;
}
}
.node-icon {
display: block;
width: 42px;
height: 26px;
line-height: 26px;
margin: 9px auto 0;
border-radius: 3px;
background: #1c2b3a;
color: #fff;
text-align: center;
font-size: 11px;
font-weight: 700;
}
.line-node--warning .node-icon {
background: #d99000;
}
.line-node--danger .node-icon {
background: #d9534f;
}
.line-info {
display: flex;
align-items: center;
gap: 18px;
min-height: 34px;
padding: 0 12px 8px;
font-size: 12px;
color: #606266;
b {
color: #1c2b3a;
font-family: Consolas, Monaco, monospace;
}
}
.theory-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 8px;
padding: 10px;
}
.theory-item {
min-height: 100px;
padding: 9px 10px;
border: 1px solid #e4e7ed;
border-radius: 3px;
background: #fbfcfe;
box-sizing: border-box;
}
.theory-title {
font-size: 12px;
font-weight: 700;
color: #1c2b3a;
}
.theory-formula {
margin-top: 6px;
padding: 5px 6px;
border-left: 3px solid #1d4e89;
background: #eef4fb;
font-family: Consolas, Monaco, monospace;
font-size: 12px;
color: #1d4e89;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.theory-desc {
margin-top: 6px;
line-height: 1.45;
font-size: 12px;
color: #606266;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
padding: 10px 10px 0;
}
.summary-cell {
min-height: 58px;
padding: 8px;
border: 1px solid #e4e7ed;
border-radius: 3px;
background: #fbfcfe;
box-sizing: border-box;
span {
display: block;
font-size: 12px;
color: #7f8c8d;
}
b {
display: inline-block;
margin-top: 4px;
font-size: 20px;
color: #1c2b3a;
font-family: Consolas, Monaco, monospace;
}
em {
margin-left: 4px;
font-style: normal;
font-size: 12px;
color: #909399;
}
}
.risk-line,
.correction-line {
display: flex;
align-items: center;
gap: 10px;
min-height: 36px;
padding: 0 10px;
font-size: 12px;
color: #606266;
}
.risk-line {
border-top: 1px solid #edf0f5;
margin-top: 10px;
}
.risk-text,
.feature-text {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.compact-form {
padding: 10px 10px 0;
::v-deep .el-form-item {
margin-bottom: 8px;
}
}
.section-subtitle {
padding: 7px 10px;
border-top: 1px solid #edf0f5;
border-bottom: 1px solid #edf0f5;
background: #f7f9fc;
font-size: 12px;
font-weight: 700;
color: #1c2b3a;
}
.stand-table,
.actual-table,
.detail-table,
.equipment-table {
::v-deep .el-input-number {
width: 100%;
}
::v-deep .el-input__inner {
text-align: right;
padding: 0 4px;
}
::v-deep .el-table__row.alt-row td {
background: #f7f9fc !important;
}
}
.equipment-section {
margin-top: 10px;
}
.actual-body {
display: grid;
grid-template-columns: 43% minmax(0, 1fr);
gap: 8px;
padding: 8px;
}
.actual-list,
.actual-edit {
min-width: 0;
}
.correction-line {
border-top: 1px solid #edf0f5;
background: #fbfcfe;
b {
color: #1c2b3a;
font-family: Consolas, Monaco, monospace;
}
}
.calc-val {
font-family: Consolas, Monaco, monospace;
font-weight: 700;
color: #1d4e89;
}
.risk-cell {
margin-left: 6px;
color: #606266;
}
.good-val {
color: #13a463;
font-family: Consolas, Monaco, monospace;
font-weight: 700;
}
.warn-val {
color: #d99000;
font-family: Consolas, Monaco, monospace;
font-weight: 700;
}
.bad-val {
color: #d9534f;
font-family: Consolas, Monaco, monospace;
font-weight: 700;
}
.muted-val {
color: #909399;
}
.tuning-preview {
display: flex;
gap: 16px;
padding: 8px 10px;
border: 1px solid #e4e7ed;
border-radius: 3px;
background: #fbfcfe;
font-size: 12px;
color: #606266;
b {
color: #1c2b3a;
font-family: Consolas, Monaco, monospace;
}
}
@media (max-width: 1280px) {
.top-section,
.middle-section,
.bottom-section {
grid-template-columns: 1fr;
}
.theory-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.line-info {
flex-wrap: wrap;
gap: 8px;
}
}
</style>