2595 lines
76 KiB
Vue
2595 lines
76 KiB
Vue
<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>
|