Merge branch '0.8.X' of https://gitee.com/hdka/klp-oa into 0.8.X

This commit is contained in:
砂糖
2026-01-28 09:49:27 +08:00
34 changed files with 1841 additions and 206 deletions

39
klp-ui/src/api/da/oee.js Normal file
View File

@@ -0,0 +1,39 @@
import request from '@/utils/request'
// OEE 汇总(两条产线 KPI + 日趋势)
export function fetchOeeSummary(query) {
return request({
url: '/oee/line/summary',
method: 'get',
params: query
})
}
// 7 大损失汇总
export function fetchOeeLoss7(query) {
return request({
url: '/oee/line/loss7',
method: 'get',
params: query
})
}
// 停机/损失事件明细
export function fetchOeeEvents(query) {
return request({
url: '/oee/line/events',
method: 'get',
params: query
})
}
// 导出 Word 报表
export function exportOeeWord(query) {
return request({
url: '/oee/line/exportWord',
method: 'get',
params: query,
responseType: 'blob'
})
}

View File

@@ -89,13 +89,13 @@ export function batchCalculateCost(calcDate) {
})
}
// 按入场钢卷号维度计算成本
export function calculateCostByEnterCoilNo(enterCoilNo, calcDate) {
// 按当前钢卷号维度计算成本(单卷详情)
export function calculateCostByCurrentCoilNo(currentCoilNo, calcDate) {
return request({
url: '/wms/cost/coil/calculateByEnterCoilNo',
url: '/wms/cost/coil/calculateByCurrentCoilNo',
method: 'post',
params: {
enterCoilNo,
currentCoilNo,
calcDate
}
})

View File

@@ -0,0 +1,384 @@
<template>
<div class="oee-page">
<!-- 查询条件 -->
<el-card class="oee-card query-card" shadow="never">
<el-form :inline="true" :model="queryParams" size="small">
<el-form-item label="日期范围">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="yyyy-MM-dd"
/>
</el-form-item>
<el-form-item label="产线">
<el-select v-model="queryParams.lineIds" multiple collapse-tags style="min-width: 200px">
<el-option label="酸轧线" value="SY" />
<el-option label="镀锌一线" value="DX1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="handleQuery">查询</el-button>
<el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
<el-button type="success" icon="el-icon-printer" @click="handlePrint">打印报表</el-button>
<el-button type="warning" icon="el-icon-document" @click="handleExportWord">导出 Word</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 1. 指标总览报告形式表格 -->
<el-card class="oee-card kpi-card" shadow="never">
<div class="card-header">产线 OEE 指标总览</div>
<el-table :data="lineSummary" border size="small">
<el-table-column prop="lineName" label="产线" width="120" />
<el-table-column label="OEE">
<template slot-scope="scope">
{{ toPercent(scope.row.total && scope.row.total.oee) }}
</template>
</el-table-column>
<el-table-column label="时间稼动率 A">
<template slot-scope="scope">
{{ toPercent(scope.row.total && scope.row.total.availability) }}
</template>
</el-table-column>
<el-table-column label="性能稼动率 P">
<template slot-scope="scope">
{{ toPercent(scope.row.total && scope.row.total.performance) }}
</template>
</el-table-column>
<el-table-column label="良品率 Q">
<template slot-scope="scope">
{{ toPercent(scope.row.total && scope.row.total.quality) }}
</template>
</el-table-column>
<el-table-column label="负荷时间(min)" width="130">
<template slot-scope="scope">
{{ (scope.row.total && scope.row.total.loadingTimeMin) || 0 }}
</template>
</el-table-column>
<el-table-column label="停机时间(min)" width="130">
<template slot-scope="scope">
{{ (scope.row.total && scope.row.total.downtimeMin) || 0 }}
</template>
</el-table-column>
<el-table-column label="运转时间(min)" width="130">
<template slot-scope="scope">
{{ (scope.row.total && scope.row.total.runTimeMin) || 0 }}
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 2. 趋势报告中的图 1 占位 -->
<el-card class="oee-card chart-card" shadow="never">
<div class="card-header">OEE 与三大指标日趋势 1占位</div>
<div class="chart-placeholder">
<span> 1OEE / A / P / Q 日趋势曲线后续接入图表组件</span>
</div>
</el-card>
<!-- 3. 7 大损失 -->
<el-card class="oee-card loss-card" shadow="never">
<div class="card-header">7 大损失汇总</div>
<el-tabs v-model="activeLossLine">
<el-tab-pane
v-for="line in lossByLine"
:key="line.lineId"
:label="line.lineName"
:name="line.lineId"
>
<el-table :data="line.losses" border size="small">
<el-table-column prop="lossCategoryName" label="损失类别" />
<el-table-column prop="lossTimeMin" label="损失时间(min)" width="140" />
</el-table>
</el-tab-pane>
</el-tabs>
</el-card>
<!-- 4. 事件明细 -->
<el-card class="oee-card event-card" shadow="never">
<div class="card-header">停机 / 损失事件明细</div>
<el-table :data="eventList" border size="small">
<el-table-column prop="lineName" label="产线" width="100" />
<el-table-column prop="eventStartTime" label="开始时间" width="160" />
<el-table-column prop="eventEndTime" label="结束时间" width="160" />
<el-table-column prop="durationMin" label="时长(min)" width="100" />
<el-table-column prop="lossCategoryName" label="损失类别" width="120" />
<el-table-column prop="rawReasonName" label="原因" />
</el-table>
<pagination
v-show="eventTotal > 0"
:total="eventTotal"
:page.sync="eventQuery.pageNum"
:limit.sync="eventQuery.pageSize"
@pagination="loadEvents"
/>
</el-card>
<!-- 5. 公式与口径说明MathType 风格 -->
<el-card class="oee-card formula-card" shadow="never">
<div class="card-header">OEE 公式与口径说明</div>
<div class="formula-content">
<div class="formula-row">
<span class="formula-label">总公式</span>
<div class="equation">
<span class="var">OEE</span>
<span>=</span>
<span class="var">A</span>
<span>×</span>
<span class="var">P</span>
<span>×</span>
<span class="var">Q</span>
</div>
</div>
<div class="formula-row">
<span class="formula-label">时间稼动率 A</span>
<div class="equation">
<span class="var">A</span>
<span>=</span>
<span class="frac">
<span class="num">负荷时间 停机时间</span>
<span class="line"></span>
<span class="den">负荷时间</span>
</span>
</div>
</div>
<div class="formula-row">
<span class="formula-label">性能稼动率 P</span>
<div class="equation">
<span class="var">P</span>
<span>=</span>
<span class="frac">
<span class="num">理论加工时间 × 加工数量</span>
<span class="line"></span>
<span class="den">实际运转时间</span>
</span>
</div>
<div class="formula-note">
当前阶段按 P = 1 处理待接入标准节拍 / 标准速度后启用上述公式
</div>
</div>
<div class="formula-row">
<span class="formula-label">良品率 Q</span>
<div class="equation">
<span class="var">Q</span>
<span>=</span>
<span class="frac">
<span class="num">良品数</span>
<span class="line"></span>
<span class="den">总产量</span>
</span>
</div>
<div class="formula-note">
本系统中良品数通过钢卷质量状态 <code>quality_status = 0</code> 统计总产量为对应库区内所有钢卷数量
</div>
</div>
</div>
</el-card>
</div>
</template>
<script>
import { fetchOeeSummary, fetchOeeLoss7, fetchOeeEvents, exportOeeWord } from '@/api/da/oee'
export default {
name: 'OeeReport',
data() {
const today = this.$moment ? this.$moment().format('YYYY-MM-DD') : ''
return {
dateRange: [today, today],
queryParams: {
startDate: today,
endDate: today,
lineIds: ['SY', 'DX1']
},
lineSummary: [],
lossByLine: [],
activeLossLine: 'SY',
eventList: [],
eventTotal: 0,
eventQuery: {
pageNum: 1,
pageSize: 10
},
loading: false
}
},
created() {
this.handleQuery()
},
methods: {
buildBaseQuery() {
const [start, end] = this.dateRange || []
return {
startDate: start,
endDate: end,
lineIds: (this.queryParams.lineIds || []).join(',')
}
},
handleQuery() {
const baseQuery = this.buildBaseQuery()
this.loading = true
Promise.all([
fetchOeeSummary(baseQuery),
fetchOeeLoss7(baseQuery)
])
.then(([summaryRes, lossRes]) => {
this.lineSummary = summaryRes.data || []
this.lossByLine = (lossRes.data && lossRes.data.byLine) || []
if (this.lossByLine.length && !this.lossByLine.find(l => l.lineId === this.activeLossLine)) {
this.activeLossLine = this.lossByLine[0].lineId
}
this.eventQuery.pageNum = 1
this.loadEvents()
})
.finally(() => {
this.loading = false
})
},
resetQuery() {
const today = this.$moment ? this.$moment().format('YYYY-MM-DD') : ''
this.dateRange = [today, today]
this.queryParams.lineIds = ['SY', 'DX1']
this.handleQuery()
},
loadEvents() {
const baseQuery = this.buildBaseQuery()
const params = Object.assign({}, baseQuery, this.eventQuery)
fetchOeeEvents(params).then(res => {
this.eventList = res.rows || []
this.eventTotal = res.total || 0
})
},
toPercent(v) {
if (v == null) return '-'
const num = Number(v)
if (isNaN(num)) return '-'
return (num * 100).toFixed(1) + '%'
},
handlePrint() {
window.print()
},
handleExportWord() {
const baseQuery = this.buildBaseQuery()
exportOeeWord(baseQuery).then(res => {
const blob = new Blob([res], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' })
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `OEE报表_${baseQuery.startDate}_${baseQuery.endDate}.docx`
a.click()
window.URL.revokeObjectURL(url)
})
}
}
}
</script>
<style scoped>
.oee-page {
padding: 10px;
}
.oee-card {
margin-bottom: 12px;
}
.card-header {
font-weight: 600;
margin-bottom: 8px;
}
.line-kpi {
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 8px 12px;
}
.line-title {
font-weight: 600;
margin-bottom: 6px;
}
.kpi-items {
display: flex;
flex-wrap: wrap;
}
.kpi-item {
min-width: 120px;
margin-right: 12px;
margin-bottom: 4px;
}
.kpi-label {
font-size: 12px;
color: #909399;
}
.kpi-value {
font-size: 14px;
font-weight: 600;
}
.kpi-value.primary {
color: #409eff;
font-size: 16px;
}
.kpi-footer {
margin-top: 4px;
font-size: 12px;
color: #909399;
display: flex;
flex-wrap: wrap;
}
.kpi-footer span {
margin-right: 12px;
}
.chart-placeholder {
height: 260px;
display: flex;
align-items: center;
justify-content: center;
color: #909399;
border: 1px dashed #e4e7ed;
border-radius: 4px;
}
.formula-content {
font-size: 13px;
line-height: 1.6;
}
.formula-row {
display: flex;
align-items: flex-start;
margin-bottom: 8px;
}
.formula-label {
min-width: 110px;
color: #606266;
}
.equation {
display: inline-flex;
align-items: center;
flex-wrap: wrap;
}
.equation .var {
font-weight: 600;
margin: 0 2px;
}
.frac {
display: inline-flex;
flex-direction: column;
align-items: center;
margin: 0 4px;
font-family: 'Times New Roman', serif;
}
.frac .num,
.frac .den {
padding: 0 4px;
}
.frac .line {
border-top: 1px solid #303133;
width: 100%;
margin: 1px 0;
}
.formula-note {
margin-left: 110px;
font-size: 12px;
color: #909399;
}
</style>

View File

@@ -54,7 +54,7 @@
<el-card class="block-card">
<div slot="header" class="clearfix">
<span class="card-title">成本汇总按入场卷号</span>
<span class="card-title">成本汇总</span>
</div>
<el-table :data="mergedRows" border stripe>
<el-table-column prop="enterCoilNo" label="入场卷号"></el-table-column>
@@ -257,6 +257,7 @@ export default {
}
fetchCoilTotalMerged(params).then(res => {
this.mergedRows = res.rows || []
console.log(res)
this.mergedTotal = res.total || 0
const rows = this.mergedRows || []

View File

@@ -81,7 +81,7 @@
style="width: 100%"
@sort-change="handleSortChange"
>
<el-table-column prop="enterCoilNo" label="入场钢卷号" />
<el-table-column prop="currentCoilNo" label="当前钢卷号" />
<el-table-column prop="coilCount" label="子钢卷数" align="right" />
<el-table-column prop="totalGrossWeight" label="总毛重(吨)" align="right">
<template slot-scope="scope">
@@ -112,7 +112,7 @@
</el-table-column>
<el-table-column label="操作" fixed="right">
<template slot-scope="scope">
<el-button type="text" size="small" @click="viewCoilDetail(scope.row)">查看子钢卷成本</el-button>
<el-button type="text" size="small" @click="viewCoilDetail(scope.row)">查看详情</el-button>
</template>
</el-table-column>
</el-table>
@@ -126,56 +126,45 @@
/>
</div>
<!-- 入场钢卷号下子钢卷详情对话框 -->
<!-- 当前钢卷成本详情对话框 -->
<el-dialog
title="入场钢卷号下子钢卷成本详情"
title="当前钢卷成本详情"
:visible.sync="showDetailDialog"
width="900px"
>
<div v-if="detailEnterCoilNo" style="margin-bottom: 10px;">
入场钢卷号<strong>{{ detailEnterCoilNo }}</strong>
</div>
<el-table :data="detailList" stripe style="width: 100%">
<el-table-column prop="currentCoilNo" label="当前钢卷号" />
<el-table-column prop="grossWeightTon" label="毛重(吨)" align="right">
<template slot-scope="scope">
{{ formatWeight(scope.row.grossWeightTon) }}
</template>
</el-table-column>
<el-table-column prop="netWeightTon" label="净重(吨)" align="right">
<template slot-scope="scope">
{{ formatWeight(scope.row.netWeightTon) }}
</template>
</el-table-column>
<el-table-column prop="storageDays" label="在库天数" align="right">
<template slot-scope="scope">
<span :class="getStorageDaysClass(scope.row.storageDays)">
{{ scope.row.storageDays || '-' }}
<div v-if="detailInfo">
<el-form label-width="140px" size="small">
<el-form-item label="当前钢卷号">
<span>{{ detailCoilNo }}</span>
</el-form-item>
<el-form-item label="毛重(吨)">
<span>{{ formatWeight(detailInfo.grossWeightTon) }}</span>
</el-form-item>
<el-form-item label="净重(吨)">
<span>{{ formatWeight(detailInfo.netWeightTon) }}</span>
</el-form-item>
<el-form-item label="在库天数">
<span :class="getStorageDaysClass(detailInfo.storageDays)">
{{ detailInfo.storageDays || '-' }}
</span>
</template>
</el-table-column>
<el-table-column prop="unitCost" label="单位成本(元/吨/天)" align="right">
<template slot-scope="scope">
{{ formatMoney(scope.row.unitCost) }}
</template>
</el-table-column>
<el-table-column prop="dailyCost" label="成本(元)" align="right">
<template slot-scope="scope">
{{ formatMoney(scope.row.dailyCost) }}
</template>
</el-table-column>
<el-table-column prop="totalCost" label="累计成本(元)" align="right">
<template slot-scope="scope">
<span class="cost-total">{{ formatMoney(scope.row.totalCost) }}</span>
</template>
</el-table-column>
</el-table>
</el-form-item>
<el-form-item label="单位成本(元/吨/天)">
<span>{{ formatMoney(detailInfo.unitCost) }}</span>
</el-form-item>
<el-form-item label="日成本(元)">
<span>{{ formatMoney(detailInfo.dailyCost) }}</span>
</el-form-item>
<el-form-item label="累计成本(元)">
<span class="cost-total">{{ formatMoney(detailInfo.totalCost) }}</span>
</el-form-item>
</el-form>
</div>
</el-dialog>
</div>
</template>
<script>
import { calculateCostByEnterCoilNo, getStockpileCostList } from '@/api/wms/cost'
import { calculateCostByCurrentCoilNo, getStockpileCostList } from '@/api/wms/cost'
export default {
name: 'CostStockpile',
@@ -197,8 +186,8 @@ export default {
currentCoilNo: null
},
showDetailDialog: false,
detailEnterCoilNo: null,
detailList: []
detailCoilNo: null,
detailInfo: null
}
},
created() {
@@ -272,10 +261,10 @@ export default {
},
async viewCoilDetail(row) {
try {
const res = await calculateCostByEnterCoilNo(row.enterCoilNo, null)
const res = await calculateCostByCurrentCoilNo(row.currentCoilNo, null)
if (res.code === 200 && res.data && !res.data.error) {
this.detailEnterCoilNo = row.enterCoilNo
this.detailList = res.data.coilDetails || []
this.detailCoilNo = row.currentCoilNo
this.detailInfo = res.data
this.showDetailDialog = true
} else {
this.$message.error(res.data?.error || '加载详情失败')