执行重构加入镀锌线1的后端调用接口
This commit is contained in:
@@ -1,873 +0,0 @@
|
||||
<template>
|
||||
<div class="oee-report-page">
|
||||
<!-- 左侧:报告主体 -->
|
||||
<div class="oee-report-main">
|
||||
<!-- 条件概览(报告式,不要大标题) -->
|
||||
<header class="oee-report-header">
|
||||
<div class="oee-report-meta">
|
||||
<div>报告日期区间:{{ query.startDate }} 至 {{ query.endDate }}</div>
|
||||
<div>产线:{{ lineNamesText }}</div>
|
||||
</div>
|
||||
<div v-if="bgLoading.active" class="oee-bg-progress">
|
||||
<el-progress
|
||||
:percentage="bgLoading.percent"
|
||||
:stroke-width="10"
|
||||
status="success"
|
||||
:text-inside="false"
|
||||
/>
|
||||
<div class="oee-bg-progress-text">{{ bgLoading.text }}</div>
|
||||
</div>
|
||||
<!-- 简洁查询区:更像“报告参数选择”,而不是大屏搜索栏 -->
|
||||
<el-form
|
||||
ref="queryForm"
|
||||
:inline="true"
|
||||
size="mini"
|
||||
class="oee-report-query-form"
|
||||
>
|
||||
<el-form-item label="日期范围">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="yyyy-MM-dd"
|
||||
:picker-options="pickerOptions"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="产线">
|
||||
<el-select
|
||||
v-model="selectedLineIds"
|
||||
multiple
|
||||
collapse-tags
|
||||
style="min-width: 220px"
|
||||
>
|
||||
<el-option
|
||||
v-for="line in allLines"
|
||||
:key="line.lineId"
|
||||
:label="line.lineName"
|
||||
:value="line.lineId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleQuery">生成报告</el-button>
|
||||
<el-button :disabled="!query.startDate || !query.endDate" @click="handleExportWord">导出 Word</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</header>
|
||||
|
||||
<!-- 生成中占位:先展示框架 + 进度条,数据到达后再填充表格 -->
|
||||
<section class="oee-section" v-if="loading && summaryLines.length === 0">
|
||||
<h2 class="oee-section-title">正在生成报告数据…</h2>
|
||||
<p class="oee-paragraph">
|
||||
当前进度:{{ bgLoading.percent }}%({{ bgLoading.text }})
|
||||
</p>
|
||||
<p class="oee-paragraph">
|
||||
汇总数据就绪后会立即显示表格;事件明细与理论节拍回归会在后台继续补全。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- 报告内容:按“章节”布局,模拟 Word 报告结构 -->
|
||||
<section class="oee-section" v-if="summaryLines.length">
|
||||
<h2 class="oee-section-title">一、关键指标总览(按产线)</h2>
|
||||
<el-table
|
||||
:data="summaryLines"
|
||||
border
|
||||
size="mini"
|
||||
class="oee-kpi-table"
|
||||
>
|
||||
<el-table-column prop="lineName" label="产线" fixed="left" />
|
||||
<el-table-column prop="oeeText" label="OEE" />
|
||||
<el-table-column prop="availabilityText" label="时间稼动率" />
|
||||
<el-table-column prop="performanceText" label="性能稼动率" />
|
||||
<el-table-column prop="qualityText" label="良品率" />
|
||||
<el-table-column prop="loadingTimeH" label="负荷时间(h)" />
|
||||
<el-table-column prop="downtimeH" label="停机时间(h)" />
|
||||
<el-table-column prop="runTimeH" label="实际运转时间(h)" />
|
||||
<el-table-column prop="totalOutput" label="总产量" />
|
||||
<el-table-column prop="goodOutput" label="良品量" />
|
||||
<el-table-column prop="defectOutput" label="不良量" />
|
||||
</el-table>
|
||||
</section>
|
||||
|
||||
<section class="oee-section" v-if="trendDates.length">
|
||||
<h2 class="oee-section-title">二、趋势(按日)</h2>
|
||||
<div class="oee-charts">
|
||||
<div class="oee-chart-card">
|
||||
<div class="oee-chart-title">OEE 日趋势</div>
|
||||
<div ref="oeeTrendChart" class="oee-chart"></div>
|
||||
</div>
|
||||
<div class="oee-chart-card">
|
||||
<div class="oee-chart-title">时间稼动率 / 性能稼动率 / 良品率(日趋势)</div>
|
||||
<div ref="apqTrendChart" class="oee-chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="oee-section" v-if="summaryLines.length">
|
||||
<h2 class="oee-section-title">三、指标日明细(用于报告点评)</h2>
|
||||
<p class="oee-paragraph">
|
||||
本节展示所选日期范围内,各产线每日的 OEE 及三大构成指标(时间稼动率、性能稼动率、良品率)。
|
||||
页面以文字+表格形式呈现,便于在 Word 报告中直接使用和点评。
|
||||
</p>
|
||||
<div
|
||||
v-for="line in summaryLines"
|
||||
:key="line.lineId"
|
||||
class="oee-subsection"
|
||||
>
|
||||
<h3 class="oee-subsection-title">
|
||||
产线:{{ line.lineName }}
|
||||
</h3>
|
||||
<el-table
|
||||
:data="line.daily || []"
|
||||
border
|
||||
size="mini"
|
||||
class="oee-daily-table"
|
||||
>
|
||||
<el-table-column prop="statDate" label="日期" width="110" />
|
||||
<el-table-column prop="oeeText" label="OEE" width="90" />
|
||||
<el-table-column prop="availabilityText" label="时间稼动率" width="110" />
|
||||
<el-table-column prop="performanceText" label="性能稼动率" width="110" />
|
||||
<el-table-column prop="qualityText" label="良品率" width="90" />
|
||||
<el-table-column prop="loadingTimeH" label="负荷时间(h)" width="120" />
|
||||
<el-table-column prop="downtimeH" label="停机时间(h)" width="120" />
|
||||
<el-table-column prop="runTimeH" label="实际运转时间(h)" width="140" />
|
||||
<el-table-column prop="totalOutput" label="总产量" width="100" />
|
||||
<el-table-column prop="goodOutput" label="良品量" width="100" />
|
||||
<el-table-column prop="defectOutput" label="不良量" width="100" />
|
||||
</el-table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="oee-section" v-if="lossByLine.length">
|
||||
<h2 class="oee-section-title">四、7 大损失分类汇总</h2>
|
||||
<p class="oee-paragraph">
|
||||
本节按照 7 大损失分类,统计各类别的时间损失与占比,帮助识别 OEE 损失的主要来源。
|
||||
</p>
|
||||
<div
|
||||
v-for="item in lossByLine"
|
||||
:key="item.lineId"
|
||||
class="oee-subsection"
|
||||
>
|
||||
<h3 class="oee-subsection-title">
|
||||
产线:{{ item.lineName }}
|
||||
</h3>
|
||||
<el-table
|
||||
:data="item.losses || []"
|
||||
border
|
||||
size="mini"
|
||||
class="oee-loss-table"
|
||||
>
|
||||
<el-table-column prop="lossCategoryName" label="损失类别" />
|
||||
<el-table-column prop="lossTimeH" label="损失时间(h)" />
|
||||
<el-table-column prop="lossTimeRateText" label="占比" />
|
||||
<el-table-column prop="count" label="次数" />
|
||||
<el-table-column prop="avgDurationH" label="平均时长(h)" />
|
||||
</el-table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="oee-section">
|
||||
<h2 class="oee-section-title">五、停机/损失事件明细(节选)</h2>
|
||||
<p class="oee-paragraph">
|
||||
下表为当前条件下的部分停机/损失事件明细(默认展示前 50 条),便于在报告中引用典型案例。
|
||||
</p>
|
||||
<el-table
|
||||
:data="eventRows"
|
||||
border
|
||||
size="mini"
|
||||
class="oee-events-table"
|
||||
>
|
||||
<el-table-column prop="eventStartTime" label="开始时间" width="160" />
|
||||
<el-table-column prop="eventEndTime" label="结束时间" width="160" />
|
||||
<el-table-column prop="lineName" label="产线" width="90" />
|
||||
<el-table-column prop="lossCategoryName" label="损失类别" width="120" />
|
||||
<el-table-column prop="rawReasonName" label="原因" min-width="180" />
|
||||
<el-table-column prop="durationMin" label="时长(min)" width="90" />
|
||||
<el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:理论依据 / 公式(MathType 风格,使用 LaTeX + MathJax 渲染) -->
|
||||
<aside class="oee-report-aside">
|
||||
<div class="oee-formula-card">
|
||||
<h3 class="oee-formula-title">OEE 计算公式与理论依据</h3>
|
||||
<div class="oee-formula-body">
|
||||
<!-- 总公式 -->
|
||||
<div class="math-block" v-html="renderMath('OEE = A \\\\times P \\\\times Q')" />
|
||||
|
||||
<!-- 时间稼动率 -->
|
||||
<div class="oee-formula-caption">时间稼动率 A:</div>
|
||||
<div
|
||||
class="math-block"
|
||||
v-html="renderMath('A = \\frac{T_{\\text{load}} - T_{\\text{down}}}{T_{\\text{load}}}')"
|
||||
/>
|
||||
|
||||
<!-- 性能稼动率 -->
|
||||
<div class="oee-formula-caption">性能稼动率 P(按标准节拍):</div>
|
||||
<div
|
||||
class="math-block"
|
||||
v-html="renderMath('P = \\frac{t_{\\text{ideal}} \\times Q_{\\text{out}}}{T_{\\text{run}}}')"
|
||||
/>
|
||||
|
||||
<!-- 良品率 -->
|
||||
<div class="oee-formula-caption">良品率 Q:</div>
|
||||
<div
|
||||
class="math-block"
|
||||
v-html="renderMath('Q = \\frac{Q_{\\text{good}}}{Q_{\\text{total}}}')"
|
||||
/>
|
||||
|
||||
<div class="oee-formula-footnote">
|
||||
说明:公式采用 TeX 语法并由 MathJax(SVG 输出)渲染,显示效果接近 Word/MathType。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="oee-formula-card" style="margin-top: 12px">
|
||||
<h3 class="oee-formula-title">理论节拍回归(用于性能口径)</h3>
|
||||
<div class="oee-formula-body">
|
||||
<div v-if="regressionStatus.refreshing" class="oee-formula-footnote">
|
||||
理论节拍回归正在后台计算中,稍后可点击“生成报告”再次刷新查看结果。
|
||||
</div>
|
||||
<div v-else-if="regressionLines.length === 0" class="oee-formula-footnote">
|
||||
暂无回归数据(可稍后重试或检查近 6 个月是否有足够样本)。
|
||||
</div>
|
||||
<div v-for="r in regressionLines" :key="r.lineId" class="oee-regression-item">
|
||||
<div class="oee-regression-title">{{ r.lineName }}({{ r.lineId }})</div>
|
||||
<div class="oee-regression-row">
|
||||
<span>斜率(分钟/吨):{{ r.slopeMinPerTon }}</span>
|
||||
<span>R²:{{ r.r2 }}</span>
|
||||
<span>样本数:{{ r.sampleCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { fetchOeeSummary, fetchOeeLoss7, fetchOeeEvents, createOeeTheoryCycleRegressionJob, exportOeeWord } from '@/api/da/oee'
|
||||
import WebSocketManager from '@/utils/websocketManager'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
// 组件内加载 MathJax(不依赖全局 index.html),并使用 SVG 输出接近 MathType 的显示效果
|
||||
function ensureMathJaxLoaded() {
|
||||
if (window.MathJax && window.MathJax.typesetPromise) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
if (window.__KLP_MATHJAX_LOADING__) {
|
||||
return window.__KLP_MATHJAX_LOADING__
|
||||
}
|
||||
window.MathJax = {
|
||||
tex: { inlineMath: [['\\\\(', '\\\\)']], displayMath: [['\\\\[', '\\\\]']] },
|
||||
svg: { fontCache: 'global' }
|
||||
}
|
||||
window.__KLP_MATHJAX_LOADING__ = new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script')
|
||||
script.async = true
|
||||
script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js'
|
||||
script.onload = () => resolve()
|
||||
script.onerror = (e) => reject(e)
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
return window.__KLP_MATHJAX_LOADING__
|
||||
}
|
||||
|
||||
function typesetMath() {
|
||||
if (window.MathJax && window.MathJax.typesetPromise) {
|
||||
return window.MathJax.typesetPromise()
|
||||
}
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'DaOeeReport',
|
||||
data() {
|
||||
const today = this.parseTime(new Date(), '{y}-{m}-{d}')
|
||||
const d = new Date()
|
||||
d.setDate(d.getDate() - 29)
|
||||
const monthAgo = this.parseTime(d, '{y}-{m}-{d}')
|
||||
return {
|
||||
query: {
|
||||
startDate: monthAgo,
|
||||
endDate: today
|
||||
},
|
||||
dateRange: [monthAgo, today],
|
||||
pickerOptions: {
|
||||
disabledDate(time) {
|
||||
return time.getTime() > Date.now()
|
||||
}
|
||||
},
|
||||
allLines: [
|
||||
// 与后端约定好的 lineId/lineName,后续也可以从接口动态获取
|
||||
{ lineId: 'SY', lineName: '酸轧线' },
|
||||
{ lineId: 'DX1', lineName: '镀锌一线' }
|
||||
],
|
||||
selectedLineIds: ['SY', 'DX1'],
|
||||
summaryLines: [],
|
||||
lossByLine: [],
|
||||
eventRows: [],
|
||||
regressionLines: [],
|
||||
regressionStatus: {},
|
||||
trendDates: [],
|
||||
charts: {
|
||||
oee: null,
|
||||
apq: null
|
||||
},
|
||||
loading: false,
|
||||
loadStage: 'idle', // idle | base | events | regression | done
|
||||
loadMessage: '',
|
||||
bgLoading: { active: true, percent: 10, text: '正在准备加载 OEE 报告…' },
|
||||
wsManager: null,
|
||||
regressionWsType: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
lineNamesText() {
|
||||
const names = this.allLines
|
||||
.filter(l => this.selectedLineIds.includes(l.lineId))
|
||||
.map(l => l.lineName)
|
||||
return names.length ? names.join('、') : '(未选择)'
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
ensureMathJaxLoaded()
|
||||
.then(() => this.handleQuery())
|
||||
.then(() => this.$nextTick(() => typesetMath()))
|
||||
.catch(() => {
|
||||
// MathJax 加载失败也不影响数据展示
|
||||
this.handleQuery()
|
||||
})
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.disposeCharts()
|
||||
// 断开 websocket,避免页面离开后仍在推送
|
||||
if (this.wsManager) {
|
||||
try {
|
||||
if (this.regressionWsType) {
|
||||
this.wsManager.disconnect(this.regressionWsType)
|
||||
} else {
|
||||
this.wsManager.disconnectAll()
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getWsBaseUrl() {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
|
||||
return `${protocol}://${window.location.host}/websocket`
|
||||
},
|
||||
// 将 TeX 字符串包装成 MathJax 识别的行内/块级格式
|
||||
renderMath(tex) {
|
||||
const escaped = tex
|
||||
this.$nextTick(() => {
|
||||
typesetMath()
|
||||
})
|
||||
// 注意:这里返回的是“可被 MathJax 识别的文本”,不是直接插 SVG
|
||||
return `\\\\[${escaped}\\\\]`
|
||||
},
|
||||
buildQueryParams() {
|
||||
const [start, end] = this.dateRange || []
|
||||
return {
|
||||
startDate: start,
|
||||
endDate: end,
|
||||
lineIds: (this.selectedLineIds || []).join(',')
|
||||
}
|
||||
},
|
||||
async handleQuery() {
|
||||
this.loading = true
|
||||
this.loadStage = 'base'
|
||||
this.loadMessage = '正在加载汇总指标与趋势…'
|
||||
this.bgLoading = { active: true, percent: 10, text: '正在获取基础数据…' }
|
||||
const params = this.buildQueryParams()
|
||||
this.query.startDate = params.startDate
|
||||
this.query.endDate = params.endDate
|
||||
|
||||
try {
|
||||
// 0) 事件明细不依赖 summary,提前拉取,避免 summary 卡住导致第五节永远空
|
||||
;(async () => {
|
||||
try {
|
||||
const eventsRes = await fetchOeeEvents({
|
||||
startTime: params.startDate + ' 00:00:00',
|
||||
endTime: params.endDate + ' 23:59:59',
|
||||
pageNum: 1,
|
||||
pageSize: 50
|
||||
})
|
||||
this.eventRows = (eventsRes.data && eventsRes.data.rows) || []
|
||||
} catch (e) {
|
||||
console.warn('加载事件明细失败(不影响主报表)', e)
|
||||
this.eventRows = []
|
||||
}
|
||||
})()
|
||||
|
||||
// 1) 先加载“报表主体”所必需的数据(summary(http) + loss7(http))
|
||||
const [summaryRes, lossRes] = await Promise.all([
|
||||
fetchOeeSummary(params),
|
||||
fetchOeeLoss7({ ...params, topN: 10 })
|
||||
])
|
||||
|
||||
const rawSummary = summaryRes && summaryRes.data ? summaryRes.data : summaryRes
|
||||
const lines = (rawSummary && rawSummary.lines) || rawSummary || []
|
||||
this.summaryLines = lines.map(l => {
|
||||
const total = l.total || {}
|
||||
return {
|
||||
lineId: l.lineId,
|
||||
lineName: l.lineName,
|
||||
// 区间汇总
|
||||
loadingTimeH: this.formatHours(total.loadingTimeMin),
|
||||
downtimeH: this.formatHours(total.downtimeMin),
|
||||
runTimeH: this.formatHours(total.runTimeMin),
|
||||
totalOutput: total.totalOutput,
|
||||
goodOutput: total.goodOutput,
|
||||
defectOutput: total.defectOutput,
|
||||
availabilityText: this.formatPercent(total.availability),
|
||||
performanceText: this.formatPercent(total.performance),
|
||||
qualityText: this.formatPercent(total.quality),
|
||||
oeeText: this.formatPercent(total.oee),
|
||||
// 日明细
|
||||
daily: (l.daily || []).map(d => ({
|
||||
...d,
|
||||
availabilityText: this.formatPercent(d.availability),
|
||||
performanceText: this.formatPercent(d.performance),
|
||||
qualityText: this.formatPercent(d.quality),
|
||||
oeeText: this.formatPercent(d.oee),
|
||||
loadingTimeH: this.formatHours(d.loadingTimeMin),
|
||||
downtimeH: this.formatHours(d.downtimeMin),
|
||||
runTimeH: this.formatHours(d.runTimeMin)
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
const byLine = (lossRes.data && lossRes.data.byLine) || []
|
||||
this.lossByLine = byLine.map(b => ({
|
||||
...b,
|
||||
losses: (b.losses || []).map(x => ({
|
||||
...x,
|
||||
lossTimeRateText: this.formatPercent(x.lossTimeRate),
|
||||
lossTimeH: this.formatHours(x.lossTimeMin),
|
||||
avgDurationH: this.formatHours(x.avgDurationMin)
|
||||
}))
|
||||
}))
|
||||
this.buildTrendData()
|
||||
// 主体已就绪:先展示报表,再后台补全
|
||||
this.loading = false
|
||||
this.bgLoading = { active: true, percent: 70, text: '主体已生成:正在加载事件明细…' }
|
||||
this.loadStage = 'events'
|
||||
this.loadMessage = ''
|
||||
|
||||
this.loadStage = 'regression'
|
||||
this.bgLoading = { active: true, percent: 85, text: '正在获取理论节拍回归…' }
|
||||
// 3) 回归(异步任务 + WebSocket 推送进度/结果;失败不影响主体)
|
||||
try {
|
||||
// 创建任务
|
||||
const jobRes = await createOeeTheoryCycleRegressionJob({
|
||||
startTime: params.startDate + ' 00:00:00',
|
||||
endTime: params.endDate + ' 23:59:59',
|
||||
lineIds: params.lineIds
|
||||
})
|
||||
const jobInfo = jobRes.data || {}
|
||||
const wsType = jobInfo.wsType
|
||||
this.regressionWsType = wsType
|
||||
this.regressionStatus = { refreshing: true, ...jobInfo }
|
||||
this.regressionLines = []
|
||||
|
||||
// 订阅 WS
|
||||
const wsBase = this.getWsBaseUrl()
|
||||
this.wsManager = new WebSocketManager(wsBase)
|
||||
if (wsType) {
|
||||
this.wsManager.connect(wsType, {
|
||||
onMessage: (msg) => {
|
||||
if (!msg) return
|
||||
// 统一显示进度
|
||||
if (typeof msg.progress === 'number') {
|
||||
this.bgLoading = {
|
||||
active: true,
|
||||
percent: Math.max(0, Math.min(100, msg.progress)),
|
||||
text: msg.text || this.bgLoading.text
|
||||
}
|
||||
}
|
||||
|
||||
if (msg.status === 'success') {
|
||||
const data = msg.data || {}
|
||||
this.regressionStatus = { ...(data || {}), refreshing: false }
|
||||
this.regressionLines = (data && data.lines) || []
|
||||
this.bgLoading = { active: false, percent: 100, text: '报告生成完成' }
|
||||
try { this.wsManager.disconnect(wsType) } catch (e) {}
|
||||
} else if (msg.status === 'failed') {
|
||||
this.regressionStatus = { refreshing: false, errorMsg: msg.errorMsg || '回归计算失败' }
|
||||
this.regressionLines = []
|
||||
this.bgLoading = { active: false, percent: 100, text: '报告生成完成' }
|
||||
try { this.wsManager.disconnect(wsType) } catch (e) {}
|
||||
} else {
|
||||
// running
|
||||
this.regressionStatus = { refreshing: true, ...this.regressionStatus }
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// wsType 缺失:退化为不展示回归
|
||||
this.regressionStatus = {}
|
||||
this.regressionLines = []
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('加载理论节拍回归失败(不影响主报表)', e)
|
||||
this.regressionStatus = {}
|
||||
this.regressionLines = []
|
||||
}
|
||||
|
||||
this.loadStage = 'done'
|
||||
this.loadMessage = ''
|
||||
this.bgLoading = { active: false, percent: 100, text: '报告生成完成' }
|
||||
} catch (e) {
|
||||
console.error('加载 OEE 报表数据失败', e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
this.$nextTick(() => {
|
||||
typesetMath()
|
||||
this.renderCharts()
|
||||
})
|
||||
}
|
||||
},
|
||||
buildTrendData() {
|
||||
// 统一 X 轴日期:取所有产线 daily 的 statDate 并去重排序
|
||||
const set = new Set()
|
||||
this.summaryLines.forEach(l => {
|
||||
;(l.daily || []).forEach(d => {
|
||||
if (d && d.statDate) set.add(d.statDate)
|
||||
})
|
||||
})
|
||||
this.trendDates = Array.from(set).sort()
|
||||
},
|
||||
disposeCharts() {
|
||||
if (this.charts.oee) {
|
||||
this.charts.oee.dispose()
|
||||
this.charts.oee = null
|
||||
}
|
||||
if (this.charts.apq) {
|
||||
this.charts.apq.dispose()
|
||||
this.charts.apq = null
|
||||
}
|
||||
},
|
||||
renderCharts() {
|
||||
if (!this.trendDates || this.trendDates.length === 0) return
|
||||
if (!this.$refs.oeeTrendChart || !this.$refs.apqTrendChart) return
|
||||
|
||||
// init
|
||||
if (!this.charts.oee) {
|
||||
this.charts.oee = echarts.init(this.$refs.oeeTrendChart)
|
||||
}
|
||||
if (!this.charts.apq) {
|
||||
this.charts.apq = echarts.init(this.$refs.apqTrendChart)
|
||||
}
|
||||
|
||||
// 样式池:纯黑线+不同线型/点形状,用于区分所有图例
|
||||
const stylePool = [
|
||||
{ lineType: 'solid', symbol: 'circle' },
|
||||
{ lineType: 'dashed', symbol: 'rect' },
|
||||
{ lineType: 'dotted', symbol: 'triangle' },
|
||||
{ lineType: 'solid', symbol: 'diamond' },
|
||||
{ lineType: 'dashed', symbol: 'pin' },
|
||||
{ lineType: 'dotted', symbol: 'arrow' }
|
||||
]
|
||||
let styleIndex = 0
|
||||
const nextStyle = () => {
|
||||
const s = stylePool[styleIndex % stylePool.length]
|
||||
styleIndex += 1
|
||||
return s
|
||||
}
|
||||
|
||||
const buildSeries = (key, nameSuffix) => {
|
||||
return this.summaryLines.map(line => {
|
||||
const dailyMap = new Map((line.daily || []).map(d => [d.statDate, d]))
|
||||
const style = nextStyle()
|
||||
return {
|
||||
name: `${line.lineName}${nameSuffix}`,
|
||||
type: 'line',
|
||||
smooth: false,
|
||||
symbol: style.symbol,
|
||||
lineStyle: { type: style.lineType },
|
||||
itemStyle: { color: '#000000' },
|
||||
data: this.trendDates.map(dt => {
|
||||
const d = dailyMap.get(dt) || {}
|
||||
const v = d[key]
|
||||
if (v === null || v === undefined || isNaN(v)) return null
|
||||
const n = Number(v)
|
||||
return n <= 1 ? +(n * 100).toFixed(3) : +n.toFixed(3)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
this.charts.oee.setOption({
|
||||
grid: { left: 12, right: 16, top: 36, bottom: 18, containLabel: true },
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { top: 6 },
|
||||
xAxis: { type: 'category', data: this.trendDates },
|
||||
yAxis: { type: 'value', axisLabel: { formatter: '{value}%' } },
|
||||
series: buildSeries('oee', '')
|
||||
})
|
||||
|
||||
const apqSeries = [
|
||||
...buildSeries('availability', ' A'),
|
||||
...buildSeries('performance', ' P'),
|
||||
...buildSeries('quality', ' Q')
|
||||
]
|
||||
this.charts.apq.setOption({
|
||||
grid: { left: 12, right: 16, top: 36, bottom: 18, containLabel: true },
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { top: 6, type: 'scroll' },
|
||||
xAxis: { type: 'category', data: this.trendDates },
|
||||
yAxis: { type: 'value', axisLabel: { formatter: '{value}%' } },
|
||||
series: apqSeries
|
||||
})
|
||||
|
||||
// resize once
|
||||
this.charts.oee.resize()
|
||||
this.charts.apq.resize()
|
||||
},
|
||||
async handleExportWord() {
|
||||
const params = this.buildQueryParams()
|
||||
// 统一文件名:OEE_yyyyMMdd_yyyyMMdd.docx
|
||||
const filename = `OEE_${(params.startDate || '').replace(/-/g, '')}_${(params.endDate || '').replace(/-/g, '')}.docx`
|
||||
try {
|
||||
const blobData = await exportOeeWord(params)
|
||||
// request.js 对 blob 会直接返回 res.data,这里 blobData 就是 Blob/ArrayBuffer
|
||||
const blob = blobData instanceof Blob ? blobData : new Blob([blobData])
|
||||
const link = document.createElement('a')
|
||||
link.href = URL.createObjectURL(blob)
|
||||
link.download = filename
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(link.href)
|
||||
} catch (e) {
|
||||
console.error('导出 Word 失败', e)
|
||||
}
|
||||
},
|
||||
formatPercent(value) {
|
||||
if (value === null || value === undefined || isNaN(value)) {
|
||||
return ''
|
||||
}
|
||||
let v = Number(value)
|
||||
if (v <= 1) {
|
||||
v = v * 100
|
||||
}
|
||||
return v.toFixed(3) + '%'
|
||||
},
|
||||
formatHours(min) {
|
||||
if (min === null || min === undefined || min === '') return ''
|
||||
const v = Number(min)
|
||||
if (isNaN(v)) return ''
|
||||
return (v / 60).toFixed(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.oee-report-page {
|
||||
display: flex;
|
||||
padding: 16px 24px;
|
||||
box-sizing: border-box;
|
||||
background: #f5f7fa;
|
||||
font-family: 'Microsoft YaHei', -apple-system, BlinkMacSystemFont, 'Segoe UI',
|
||||
'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
.oee-report-main {
|
||||
flex: 1;
|
||||
background: #ffffff;
|
||||
padding: 24px 28px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||
border-radius: 4px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.oee-loading-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.oee-loading-card {
|
||||
max-width: 520px;
|
||||
padding: 24px 28px;
|
||||
background: #ffffff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.12);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.oee-loading-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.oee-loading-text {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.oee-report-header {
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
padding-bottom: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.oee-report-meta {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
column-gap: 24px;
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.oee-report-query-form {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.oee-section {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.oee-section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.oee-charts {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.oee-chart-card {
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.oee-chart-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.oee-chart {
|
||||
width: 100%;
|
||||
height: 260px;
|
||||
}
|
||||
|
||||
.oee-paragraph {
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #606266;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.oee-subsection {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.oee-subsection-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.oee-kpi-table,
|
||||
.oee-daily-table,
|
||||
.oee-loss-table,
|
||||
.oee-events-table {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.oee-report-aside {
|
||||
width: 320px;
|
||||
position: sticky;
|
||||
top: 72px;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.oee-formula-card {
|
||||
background: #ffffff;
|
||||
padding: 16px 18px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.oee-formula-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.oee-formula-body {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.oee-formula-caption {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 2px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.math-block {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.oee-formula-footnote {
|
||||
margin-top: 10px;
|
||||
font-size: 11px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.oee-bg-progress {
|
||||
margin-top: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.oee-bg-progress-text {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
/* 让进度数字在条外侧显示,避免被遮挡 */
|
||||
.oee-bg-progress :deep(.el-progress__text) {
|
||||
margin-left: 8px;
|
||||
color: #606266;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.oee-regression-item {
|
||||
padding: 8px 0;
|
||||
border-top: 1px dashed #ebeef5;
|
||||
}
|
||||
|
||||
.oee-regression-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.oee-regression-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user