Files
klp-oa/klp-ui/src/views/da/oee/index.vue

873 lines
30 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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

<template>
<div class="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 语法并由 MathJaxSVG 输出渲染显示效果接近 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.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>