|
|
@ -62,7 +62,7 @@ |
|
|
<input v-model.number="query.kVal" type="number" min="5" max="200"> |
|
|
<input v-model.number="query.kVal" type="number" min="5" max="200"> |
|
|
</div> |
|
|
</div> |
|
|
<button class="btn btn-primary" :disabled="loading" @click="runCalc">{{ loading ? '计算中...' : '计算' }}</button> |
|
|
<button class="btn btn-primary" :disabled="loading" @click="runCalc">{{ loading ? '计算中...' : '计算' }}</button> |
|
|
<!-- <button class="btn btn-ghost" @click="exportCSV">导出</button> --> |
|
|
|
|
|
|
|
|
<button class="btn btn-ghost" @click="exportExcel">导出Excel</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</section> |
|
|
</section> |
|
|
@ -451,6 +451,26 @@ import { |
|
|
getSpcMeasurements |
|
|
getSpcMeasurements |
|
|
} from '@/api/qc/qc_report.js' |
|
|
} from '@/api/qc/qc_report.js' |
|
|
|
|
|
|
|
|
|
|
|
const EXCEL_COLORS = { |
|
|
|
|
|
accent: 'FF1B4D89', |
|
|
|
|
|
accentSoft: 'FFE8F0F8', |
|
|
|
|
|
steel: 'FF3B82C4', |
|
|
|
|
|
amber: 'FFD97706', |
|
|
|
|
|
amberBg: 'FFFFF7ED', |
|
|
|
|
|
ok: 'FF047857', |
|
|
|
|
|
okBg: 'FFECFDF5', |
|
|
|
|
|
ng: 'FFB91C1C', |
|
|
|
|
|
ngBg: 'FFFEF2F2', |
|
|
|
|
|
warn: 'FFB45309', |
|
|
|
|
|
warnBg: 'FFFFFBEB', |
|
|
|
|
|
ink: 'FF1E293B', |
|
|
|
|
|
muted: 'FF64748B', |
|
|
|
|
|
line: 'FFE2E8F0', |
|
|
|
|
|
paper: 'FFF4F6F8', |
|
|
|
|
|
white: 'FFFFFFFF', |
|
|
|
|
|
header: 'FF1B4D89' |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
const CT_TABLE = { |
|
|
const CT_TABLE = { |
|
|
2: { d2: 1.128, A2: 1.880, D3: 0, D4: 3.267, c4: 0.7979 }, |
|
|
2: { d2: 1.128, A2: 1.880, D3: 0, D4: 3.267, c4: 0.7979 }, |
|
|
3: { d2: 1.693, A2: 1.023, D3: 0, D4: 2.574, c4: 0.8862 }, |
|
|
3: { d2: 1.693, A2: 1.023, D3: 0, D4: 2.574, c4: 0.8862 }, |
|
|
@ -1254,42 +1274,598 @@ export default { |
|
|
}) |
|
|
}) |
|
|
this.chartInst.hist = chart |
|
|
this.chartInst.hist = chart |
|
|
}, |
|
|
}, |
|
|
exportCSV () { |
|
|
|
|
|
|
|
|
getExcelJS () { |
|
|
|
|
|
return window.ExcelJS || null |
|
|
|
|
|
}, |
|
|
|
|
|
exportExcel () { |
|
|
if (!this.hasResult) { |
|
|
if (!this.hasResult) { |
|
|
this.$message.warning('请先计算后再导出') |
|
|
this.$message.warning('请先计算后再导出') |
|
|
return |
|
|
return |
|
|
} |
|
|
} |
|
|
let csv = '\uFEFF' |
|
|
|
|
|
|
|
|
if (!this.getExcelJS()) { |
|
|
|
|
|
this.$message.error('Excel组件未加载,请刷新页面后重试') |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
this.doExportExcel().catch(err => { |
|
|
|
|
|
console.error(err) |
|
|
|
|
|
this.$message.error('导出失败,请重试') |
|
|
|
|
|
}) |
|
|
|
|
|
}, |
|
|
|
|
|
async doExportExcel () { |
|
|
|
|
|
const ExcelJS = this.getExcelJS() |
|
|
|
|
|
const wb = new ExcelJS.Workbook() |
|
|
|
|
|
wb.creator = 'SPC Workstation' |
|
|
|
|
|
wb.created = new Date() |
|
|
|
|
|
const ws = wb.addWorksheet('SPC过程稳定性', { |
|
|
|
|
|
views: [{ state: 'frozen', ySplit: 1, showGridLines: false }] |
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
const maxCols = this.stage === 'ppk' |
|
|
|
|
|
? Math.max(12, 1 + (this.result.groups || []).length) |
|
|
|
|
|
: Math.max(12, 5 + this.nVal) |
|
|
|
|
|
for (let i = 1; i <= maxCols; i++) { |
|
|
|
|
|
ws.getColumn(i).width = i === 1 ? 16 : 12 |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
let row = 1 |
|
|
|
|
|
row = this.excelWriteTitle(ws, row, maxCols) |
|
|
|
|
|
row = this.excelWriteQuery(ws, row, maxCols) |
|
|
|
|
|
row += 1 |
|
|
|
|
|
row = this.excelWriteKpi(ws, row) |
|
|
|
|
|
row += 1 |
|
|
|
|
|
row = this.excelWriteStats(ws, row) |
|
|
|
|
|
row += 1 |
|
|
|
|
|
row = this.excelWriteCriteria(ws, row) |
|
|
|
|
|
row += 1 |
|
|
|
|
|
row = await this.excelWriteCharts(wb, ws, row) |
|
|
|
|
|
row += 1 |
|
|
|
|
|
this.excelWriteDetail(ws, row) |
|
|
|
|
|
|
|
|
|
|
|
const buffer = await wb.xlsx.writeBuffer() |
|
|
|
|
|
const stageName = this.stage === 'ppk' ? 'PPK样品' : 'CPK量产' |
|
|
|
|
|
const part = (this.query.partNo || 'SPC').replace(/[\\/:*?"<>|]/g, '_') |
|
|
|
|
|
const item = (this.query.itemNo || '').replace(/[\\/:*?"<>|]/g, '_') |
|
|
|
|
|
const stamp = this.excelStamp() |
|
|
|
|
|
const fileName = 'SPC过程稳定性_' + stageName + '_' + part + (item ? '_' + item : '') + '_' + stamp + '.xlsx' |
|
|
|
|
|
const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }) |
|
|
|
|
|
const url = URL.createObjectURL(blob) |
|
|
|
|
|
const a = document.createElement('a') |
|
|
|
|
|
a.href = url |
|
|
|
|
|
a.download = fileName |
|
|
|
|
|
a.click() |
|
|
|
|
|
URL.revokeObjectURL(url) |
|
|
|
|
|
this.$message.success('导出成功') |
|
|
|
|
|
}, |
|
|
|
|
|
excelStamp () { |
|
|
|
|
|
const d = new Date() |
|
|
|
|
|
const p = n => String(n).padStart(2, '0') |
|
|
|
|
|
return d.getFullYear() + p(d.getMonth() + 1) + p(d.getDate()) + '_' + p(d.getHours()) + p(d.getMinutes()) |
|
|
|
|
|
}, |
|
|
|
|
|
excelBorder () { |
|
|
|
|
|
const c = { argb: EXCEL_COLORS.line } |
|
|
|
|
|
return { |
|
|
|
|
|
top: { style: 'thin', color: c }, |
|
|
|
|
|
left: { style: 'thin', color: c }, |
|
|
|
|
|
bottom: { style: 'thin', color: c }, |
|
|
|
|
|
right: { style: 'thin', color: c } |
|
|
|
|
|
} |
|
|
|
|
|
}, |
|
|
|
|
|
excelFill (argb) { |
|
|
|
|
|
return { type: 'pattern', pattern: 'solid', fgColor: { argb: argb } } |
|
|
|
|
|
}, |
|
|
|
|
|
excelFont (opts) { |
|
|
|
|
|
return Object.assign({ name: '微软雅黑', size: 10, color: { argb: EXCEL_COLORS.ink } }, opts || {}) |
|
|
|
|
|
}, |
|
|
|
|
|
excelStyleCell (cell, opts) { |
|
|
|
|
|
opts = opts || {} |
|
|
|
|
|
if (opts.fill) cell.fill = this.excelFill(opts.fill) |
|
|
|
|
|
if (opts.font) cell.font = this.excelFont(opts.font) |
|
|
|
|
|
if (opts.align) cell.alignment = opts.align |
|
|
|
|
|
if (opts.border !== false) cell.border = this.excelBorder() |
|
|
|
|
|
}, |
|
|
|
|
|
excelMergeSet (ws, r1, c1, r2, c2, value, style) { |
|
|
|
|
|
if (r1 !== r2 || c1 !== c2) ws.mergeCells(r1, c1, r2, c2) |
|
|
|
|
|
const cell = ws.getCell(r1, c1) |
|
|
|
|
|
cell.value = value |
|
|
|
|
|
this.excelStyleCell(cell, style || {}) |
|
|
|
|
|
return cell |
|
|
|
|
|
}, |
|
|
|
|
|
excelWriteTitle (ws, row, maxCols) { |
|
|
|
|
|
const stageLabel = this.stage === 'ppk' ? '样品阶段 · 短期 PPK' : '量产阶段 · 长期 CPK' |
|
|
|
|
|
const span = Math.min(maxCols, 12) |
|
|
|
|
|
this.excelMergeSet(ws, row, 1, row, span, |
|
|
|
|
|
'SPC 过程稳定性分析 | ' + stageLabel, |
|
|
|
|
|
{ |
|
|
|
|
|
fill: EXCEL_COLORS.header, |
|
|
|
|
|
font: { size: 16, bold: true, color: { argb: EXCEL_COLORS.white } }, |
|
|
|
|
|
align: { vertical: 'middle', horizontal: 'left', indent: 1 } |
|
|
|
|
|
}) |
|
|
|
|
|
ws.getRow(row).height = 32 |
|
|
|
|
|
row += 1 |
|
|
|
|
|
this.excelMergeSet(ws, row, 1, row, span, |
|
|
|
|
|
'DOC QP-8.2-03-R05 · Internal Use Only · 导出时间 ' + new Date().toLocaleString(), |
|
|
|
|
|
{ |
|
|
|
|
|
fill: EXCEL_COLORS.paper, |
|
|
|
|
|
font: { size: 9, color: { argb: EXCEL_COLORS.muted } }, |
|
|
|
|
|
align: { vertical: 'middle', horizontal: 'left', indent: 1 } |
|
|
|
|
|
}) |
|
|
|
|
|
ws.getRow(row).height = 20 |
|
|
|
|
|
return row + 1 |
|
|
|
|
|
}, |
|
|
|
|
|
excelWriteQuery (ws, row, maxCols) { |
|
|
|
|
|
const span = Math.min(maxCols, 12) |
|
|
|
|
|
this.excelMergeSet(ws, row, 1, row, span, '查询条件', { |
|
|
|
|
|
fill: EXCEL_COLORS.accentSoft, |
|
|
|
|
|
font: { bold: true, size: 11, color: { argb: EXCEL_COLORS.accent } }, |
|
|
|
|
|
align: { vertical: 'middle', horizontal: 'left', indent: 1 } |
|
|
|
|
|
}) |
|
|
|
|
|
ws.getRow(row).height = 22 |
|
|
|
|
|
row += 1 |
|
|
|
|
|
|
|
|
|
|
|
const unit = this.itemSpec.unit || '' |
|
|
|
|
|
const fields = [ |
|
|
|
|
|
['工单号', this.query.woNumber || '--'], |
|
|
|
|
|
['设备编号', this.query.deviceNo || '--'], |
|
|
|
|
|
['操作员', this.query.operator || '--'], |
|
|
|
|
|
['物料编码', this.query.partNo || '--'], |
|
|
|
|
|
['物料名称', this.query.partDesc || '--'], |
|
|
|
|
|
['检验项目编码', this.query.itemNo || '--'], |
|
|
|
|
|
['检验项目名称', this.query.itemDesc || '--'], |
|
|
|
|
|
['开始日期', this.query.startDate || '--'], |
|
|
|
|
|
['截止日期', this.query.endDate || '--'], |
|
|
|
|
|
['子组容量 n', String(this.nVal)], |
|
|
|
|
|
['子组数 k', String(this.kVal)], |
|
|
|
|
|
['规格限', 'LSL ' + (this.itemSpec.LSL != null ? this.itemSpec.LSL : '--') + ' / USL ' + (this.itemSpec.USL != null ? this.itemSpec.USL : '--') + (unit ? ' ' + unit : '')] |
|
|
|
|
|
] |
|
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < fields.length; i += 4) { |
|
|
|
|
|
const chunk = fields.slice(i, i + 4) |
|
|
|
|
|
chunk.forEach((pair, idx) => { |
|
|
|
|
|
const c = idx * 3 + 1 |
|
|
|
|
|
this.excelMergeSet(ws, row, c, row, c, pair[0], { |
|
|
|
|
|
fill: EXCEL_COLORS.paper, |
|
|
|
|
|
font: { size: 9, color: { argb: EXCEL_COLORS.muted } }, |
|
|
|
|
|
align: { vertical: 'middle', horizontal: 'right' } |
|
|
|
|
|
}) |
|
|
|
|
|
this.excelMergeSet(ws, row, c + 1, row, c + 2, pair[1], { |
|
|
|
|
|
fill: EXCEL_COLORS.white, |
|
|
|
|
|
font: { size: 10, bold: true }, |
|
|
|
|
|
align: { vertical: 'middle', horizontal: 'left', indent: 1 } |
|
|
|
|
|
}) |
|
|
|
|
|
}) |
|
|
|
|
|
ws.getRow(row).height = 20 |
|
|
|
|
|
row += 1 |
|
|
|
|
|
} |
|
|
|
|
|
return row |
|
|
|
|
|
}, |
|
|
|
|
|
excelWriteKpi (ws, row) { |
|
|
|
|
|
this.excelMergeSet(ws, row, 1, row, 12, '关键指标 KPI', { |
|
|
|
|
|
fill: EXCEL_COLORS.accentSoft, |
|
|
|
|
|
font: { bold: true, size: 11, color: { argb: EXCEL_COLORS.accent } }, |
|
|
|
|
|
align: { vertical: 'middle', horizontal: 'left', indent: 1 } |
|
|
|
|
|
}) |
|
|
|
|
|
ws.getRow(row).height = 22 |
|
|
|
|
|
row += 1 |
|
|
|
|
|
|
|
|
|
|
|
const r = this.result |
|
|
|
|
|
const unit = this.itemSpec.unit || '' |
|
|
|
|
|
let cards |
|
|
|
|
|
if (this.stage === 'ppk') { |
|
|
|
|
|
const ok = this.ppkOk |
|
|
|
|
|
const grade = this.gradeCpk(r.Ppk) |
|
|
|
|
|
cards = [ |
|
|
|
|
|
{ label: 'PPK (Sample)', value: this.fm(r.Ppk, 2), sub: '标准≥1.67 · ' + (ok ? 'PASS' : 'FAIL'), tone: ok ? 'ok' : 'ng' }, |
|
|
|
|
|
{ label: 'Pp', value: this.fm(r.Pp, 2), sub: '过程潜力', tone: 'accent' }, |
|
|
|
|
|
{ label: 'PPU / PPL', value: this.fm(r.PPU, 2) + ' / ' + this.fm(r.PPL, 2), sub: '上侧 / 下侧', tone: 'normal' }, |
|
|
|
|
|
{ label: 'σ WITHIN', value: this.fm(r.sigma, 4) + (unit ? ' ' + unit : ''), sub: '组内·R̄/d₂', tone: 'amber' }, |
|
|
|
|
|
{ label: '样本量', value: String(r.N), sub: r.gCnt + '×' + r.groups[0].length + ' 子组', tone: 'normal' }, |
|
|
|
|
|
{ label: '判定', value: ok ? 'OK' : 'NG', sub: grade.label + ' · ' + (ok ? '满足样品准入' : '需改善制程'), tone: ok ? 'ok' : 'ng' } |
|
|
|
|
|
] |
|
|
|
|
|
} else { |
|
|
|
|
|
const ok = this.cpkOk |
|
|
|
|
|
const grade = this.gradeCpk(r.Cpk) |
|
|
|
|
|
cards = [ |
|
|
|
|
|
{ label: 'CPK (Mass Prod)', value: this.fm(r.Cpk, 2), sub: '标准≥1.33 · ' + (ok ? 'PASS' : 'FAIL'), tone: ok ? 'ok' : 'ng' }, |
|
|
|
|
|
{ label: 'Cp', value: this.fm(r.Cp, 2), sub: '过程能力', tone: 'accent' }, |
|
|
|
|
|
{ label: 'CPU / CPL', value: this.fm(r.CPU, 2) + ' / ' + this.fm(r.CPL, 2), sub: '上侧 / 下侧', tone: 'normal' }, |
|
|
|
|
|
{ label: 'σ OVERALL', value: this.fm(r.sO, 4) + (unit ? ' ' + unit : ''), sub: '整体·样本标准差', tone: 'amber' }, |
|
|
|
|
|
{ label: 'PPM', value: this.formatNum(r.ppmT), sub: 'USL:' + this.formatNum(r.ppmU) + ' LSL:' + this.formatNum(r.ppmL), tone: 'normal' }, |
|
|
|
|
|
{ label: '判定', value: ok ? 'OK' : 'NG', sub: grade.label + ' · ' + (ok ? '量产稳定受控' : '需改善制程'), tone: ok ? 'ok' : 'ng' } |
|
|
|
|
|
] |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const toneMap = { |
|
|
|
|
|
ok: { bg: EXCEL_COLORS.okBg, fg: EXCEL_COLORS.ok, border: EXCEL_COLORS.ok }, |
|
|
|
|
|
ng: { bg: EXCEL_COLORS.ngBg, fg: EXCEL_COLORS.ng, border: EXCEL_COLORS.ng }, |
|
|
|
|
|
accent: { bg: EXCEL_COLORS.accentSoft, fg: EXCEL_COLORS.accent, border: EXCEL_COLORS.steel }, |
|
|
|
|
|
amber: { bg: EXCEL_COLORS.amberBg, fg: EXCEL_COLORS.amber, border: EXCEL_COLORS.amber }, |
|
|
|
|
|
normal: { bg: EXCEL_COLORS.white, fg: EXCEL_COLORS.ink, border: EXCEL_COLORS.line } |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
cards.forEach((card, i) => { |
|
|
|
|
|
const c1 = i * 2 + 1 |
|
|
|
|
|
const c2 = c1 + 1 |
|
|
|
|
|
const tone = toneMap[card.tone] || toneMap.normal |
|
|
|
|
|
this.excelMergeSet(ws, row, c1, row, c2, card.label, { |
|
|
|
|
|
fill: tone.bg, |
|
|
|
|
|
font: { size: 8, bold: true, color: { argb: EXCEL_COLORS.muted } }, |
|
|
|
|
|
align: { vertical: 'middle', horizontal: 'center' } |
|
|
|
|
|
}) |
|
|
|
|
|
this.excelMergeSet(ws, row + 1, c1, row + 1, c2, card.value, { |
|
|
|
|
|
fill: tone.bg, |
|
|
|
|
|
font: { size: 16, bold: true, color: { argb: tone.fg } }, |
|
|
|
|
|
align: { vertical: 'middle', horizontal: 'center' } |
|
|
|
|
|
}) |
|
|
|
|
|
this.excelMergeSet(ws, row + 2, c1, row + 2, c2, card.sub, { |
|
|
|
|
|
fill: tone.bg, |
|
|
|
|
|
font: { size: 8, color: { argb: tone.fg } }, |
|
|
|
|
|
align: { vertical: 'middle', horizontal: 'center', wrapText: true } |
|
|
|
|
|
}) |
|
|
|
|
|
;[row, row + 1, row + 2].forEach(rr => { |
|
|
|
|
|
for (let cc = c1; cc <= c2; cc++) { |
|
|
|
|
|
ws.getCell(rr, cc).border = { |
|
|
|
|
|
top: { style: 'medium', color: { argb: tone.border } }, |
|
|
|
|
|
left: { style: 'medium', color: { argb: tone.border } }, |
|
|
|
|
|
bottom: { style: 'medium', color: { argb: tone.border } }, |
|
|
|
|
|
right: { style: 'medium', color: { argb: tone.border } } |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
}) |
|
|
|
|
|
}) |
|
|
|
|
|
ws.getRow(row).height = 18 |
|
|
|
|
|
ws.getRow(row + 1).height = 28 |
|
|
|
|
|
ws.getRow(row + 2).height = 22 |
|
|
|
|
|
return row + 3 |
|
|
|
|
|
}, |
|
|
|
|
|
excelWriteStats (ws, row) { |
|
|
|
|
|
this.excelMergeSet(ws, row, 1, row, 12, '过程能力指数 / 控制界限 / 分布参数', { |
|
|
|
|
|
fill: EXCEL_COLORS.accentSoft, |
|
|
|
|
|
font: { bold: true, size: 11, color: { argb: EXCEL_COLORS.accent } }, |
|
|
|
|
|
align: { vertical: 'middle', horizontal: 'left', indent: 1 } |
|
|
|
|
|
}) |
|
|
|
|
|
ws.getRow(row).height = 22 |
|
|
|
|
|
row += 1 |
|
|
|
|
|
|
|
|
|
|
|
const r = this.result |
|
|
|
|
|
const unit = this.itemSpec.unit || '' |
|
|
|
|
|
let leftBlock |
|
|
|
|
|
let rightBlock |
|
|
|
|
|
if (this.stage === 'ppk') { |
|
|
|
|
|
leftBlock = { |
|
|
|
|
|
title: '样品阶段 · PPK', |
|
|
|
|
|
rows: [ |
|
|
|
|
|
['总均值 X̄̄', (this.fm(r.Xb) + ' ' + unit).trim()], |
|
|
|
|
|
['平均极差 R̄', this.fm(r.Rb)], |
|
|
|
|
|
['组内σ', this.fm(r.sigma, 4)], |
|
|
|
|
|
['样本 N', r.N + ' (' + r.gCnt + '×' + r.groups[0].length + ')'], |
|
|
|
|
|
['Pp', this.fm(r.Pp)], |
|
|
|
|
|
['PPU (USL)', this.fm(r.PPU)], |
|
|
|
|
|
['PPL (LSL)', this.fm(r.PPL)], |
|
|
|
|
|
['Ppk', this.fm(r.Ppk)] |
|
|
|
|
|
], |
|
|
|
|
|
highlightKey: 'Ppk', |
|
|
|
|
|
ok: this.ppkOk |
|
|
|
|
|
} |
|
|
|
|
|
const ct = getCt(r.groups[0].length) |
|
|
|
|
|
rightBlock = { |
|
|
|
|
|
title: 'X-bar / R 图界限', |
|
|
|
|
|
rows: [ |
|
|
|
|
|
['X-bar UCL', this.fm(r.XU, 4)], |
|
|
|
|
|
['X-bar CL (X̄̄)', this.fm(r.Xb, 4)], |
|
|
|
|
|
['X-bar LCL', this.fm(r.XL, 4)], |
|
|
|
|
|
['系数 A₂', ct.A2.toFixed(3)], |
|
|
|
|
|
['R UCL', this.fm(r.RU, 4)], |
|
|
|
|
|
['R CL (R̄)', this.fm(r.Rb, 4)], |
|
|
|
|
|
['R LCL', this.fm(r.RL, 4)], |
|
|
|
|
|
['X超限 / R超限', r.xOut + ' / ' + r.rOut] |
|
|
|
|
|
] |
|
|
|
|
|
} |
|
|
|
|
|
} else { |
|
|
|
|
|
leftBlock = { |
|
|
|
|
|
title: '量产阶段 · CPK', |
|
|
|
|
|
rows: [ |
|
|
|
|
|
['总均值 X̄', (this.fm(r.Xa) + ' ' + unit).trim()], |
|
|
|
|
|
['整体σ (s)', this.fm(r.sO, 4)], |
|
|
|
|
|
['样本总数 N', String(r.N)], |
|
|
|
|
|
['Cp', this.fm(r.Cp)], |
|
|
|
|
|
['CPU (USL)', this.fm(r.CPU)], |
|
|
|
|
|
['CPL (LSL)', this.fm(r.CPL)], |
|
|
|
|
|
['Cpk', this.fm(r.Cpk)], |
|
|
|
|
|
['总 PPM', this.formatNum(r.ppmT)] |
|
|
|
|
|
], |
|
|
|
|
|
highlightKey: 'Cpk', |
|
|
|
|
|
ok: this.cpkOk |
|
|
|
|
|
} |
|
|
|
|
|
rightBlock = { |
|
|
|
|
|
title: '正态分布 / PPM', |
|
|
|
|
|
rows: [ |
|
|
|
|
|
['μ (均值)', this.fm(r.Xa, 4)], |
|
|
|
|
|
['σ (标准差)', this.fm(r.sO, 4)], |
|
|
|
|
|
['μ + 3σ', this.fm(r.Xa + 3 * r.sO, 4)], |
|
|
|
|
|
['μ - 3σ', this.fm(r.Xa - 3 * r.sO, 4)], |
|
|
|
|
|
['USL 侧 PPM', this.formatNum(r.ppmU)], |
|
|
|
|
|
['LSL 侧 PPM', this.formatNum(r.ppmL)], |
|
|
|
|
|
['总 PPM', this.formatNum(r.ppmT)], |
|
|
|
|
|
['I-CL / MR̄', this.fm(r.Xa, 4) + ' / ' + this.fm(r.MRbar, 4)] |
|
|
|
|
|
] |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const writeBlock = (block, startCol, titleFill) => { |
|
|
|
|
|
this.excelMergeSet(ws, row, startCol, row, startCol + 5, block.title, { |
|
|
|
|
|
fill: titleFill, |
|
|
|
|
|
font: { bold: true, size: 10, color: { argb: EXCEL_COLORS.white } }, |
|
|
|
|
|
align: { vertical: 'middle', horizontal: 'center' } |
|
|
|
|
|
}) |
|
|
|
|
|
block.rows.forEach((pair, idx) => { |
|
|
|
|
|
const rr = row + 1 + idx |
|
|
|
|
|
const isHi = block.highlightKey && pair[0] === block.highlightKey |
|
|
|
|
|
const valFill = isHi ? (block.ok ? EXCEL_COLORS.okBg : EXCEL_COLORS.ngBg) : EXCEL_COLORS.white |
|
|
|
|
|
const valColor = isHi ? (block.ok ? EXCEL_COLORS.ok : EXCEL_COLORS.ng) : EXCEL_COLORS.ink |
|
|
|
|
|
this.excelMergeSet(ws, rr, startCol, rr, startCol + 2, pair[0], { |
|
|
|
|
|
fill: EXCEL_COLORS.paper, |
|
|
|
|
|
font: { size: 9, color: { argb: EXCEL_COLORS.muted } }, |
|
|
|
|
|
align: { vertical: 'middle', horizontal: 'left', indent: 1 } |
|
|
|
|
|
}) |
|
|
|
|
|
this.excelMergeSet(ws, rr, startCol + 3, rr, startCol + 5, pair[1], { |
|
|
|
|
|
fill: valFill, |
|
|
|
|
|
font: { size: 10, bold: true, color: { argb: valColor } }, |
|
|
|
|
|
align: { vertical: 'middle', horizontal: 'right', indent: 1 } |
|
|
|
|
|
}) |
|
|
|
|
|
ws.getRow(rr).height = 18 |
|
|
|
|
|
}) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
writeBlock(leftBlock, 1, EXCEL_COLORS.accent) |
|
|
|
|
|
writeBlock(rightBlock, 7, EXCEL_COLORS.steel) |
|
|
|
|
|
ws.getRow(row).height = 20 |
|
|
|
|
|
return row + 1 + leftBlock.rows.length |
|
|
|
|
|
}, |
|
|
|
|
|
excelWriteCriteria (ws, row) { |
|
|
|
|
|
this.excelMergeSet(ws, row, 1, row, 12, '判定准则', { |
|
|
|
|
|
fill: EXCEL_COLORS.accentSoft, |
|
|
|
|
|
font: { bold: true, size: 11, color: { argb: EXCEL_COLORS.accent } }, |
|
|
|
|
|
align: { vertical: 'middle', horizontal: 'left', indent: 1 } |
|
|
|
|
|
}) |
|
|
|
|
|
ws.getRow(row).height = 22 |
|
|
|
|
|
row += 1 |
|
|
|
|
|
|
|
|
|
|
|
this.excelMergeSet(ws, row, 1, row, 6, '准则', { |
|
|
|
|
|
fill: EXCEL_COLORS.paper, |
|
|
|
|
|
font: { bold: true, size: 9 }, |
|
|
|
|
|
align: { vertical: 'middle', horizontal: 'center' } |
|
|
|
|
|
}) |
|
|
|
|
|
this.excelMergeSet(ws, row, 7, row, 9, '实测值', { |
|
|
|
|
|
fill: EXCEL_COLORS.paper, |
|
|
|
|
|
font: { bold: true, size: 9 }, |
|
|
|
|
|
align: { vertical: 'middle', horizontal: 'center' } |
|
|
|
|
|
}) |
|
|
|
|
|
this.excelMergeSet(ws, row, 10, row, 12, '结果', { |
|
|
|
|
|
fill: EXCEL_COLORS.paper, |
|
|
|
|
|
font: { bold: true, size: 9 }, |
|
|
|
|
|
align: { vertical: 'middle', horizontal: 'center' } |
|
|
|
|
|
}) |
|
|
|
|
|
row += 1 |
|
|
|
|
|
|
|
|
|
|
|
this.criteriaList.forEach(c => { |
|
|
|
|
|
const bg = c.pass ? EXCEL_COLORS.okBg : EXCEL_COLORS.ngBg |
|
|
|
|
|
const fg = c.pass ? EXCEL_COLORS.ok : EXCEL_COLORS.ng |
|
|
|
|
|
this.excelMergeSet(ws, row, 1, row, 6, c.label, { |
|
|
|
|
|
fill: EXCEL_COLORS.white, |
|
|
|
|
|
font: { size: 10 }, |
|
|
|
|
|
align: { vertical: 'middle', horizontal: 'left', indent: 1 } |
|
|
|
|
|
}) |
|
|
|
|
|
this.excelMergeSet(ws, row, 7, row, 9, c.value, { |
|
|
|
|
|
fill: EXCEL_COLORS.white, |
|
|
|
|
|
font: { size: 10, bold: true }, |
|
|
|
|
|
align: { vertical: 'middle', horizontal: 'center' } |
|
|
|
|
|
}) |
|
|
|
|
|
this.excelMergeSet(ws, row, 10, row, 12, c.pass ? 'PASS' : 'FAIL', { |
|
|
|
|
|
fill: bg, |
|
|
|
|
|
font: { size: 10, bold: true, color: { argb: fg } }, |
|
|
|
|
|
align: { vertical: 'middle', horizontal: 'center' } |
|
|
|
|
|
}) |
|
|
|
|
|
ws.getRow(row).height = 18 |
|
|
|
|
|
row += 1 |
|
|
|
|
|
}) |
|
|
|
|
|
return row |
|
|
|
|
|
}, |
|
|
|
|
|
async excelWriteCharts (wb, ws, row) { |
|
|
|
|
|
this.excelMergeSet(ws, row, 1, row, 12, '控制图 / 分布图', { |
|
|
|
|
|
fill: EXCEL_COLORS.accentSoft, |
|
|
|
|
|
font: { bold: true, size: 11, color: { argb: EXCEL_COLORS.accent } }, |
|
|
|
|
|
align: { vertical: 'middle', horizontal: 'left', indent: 1 } |
|
|
|
|
|
}) |
|
|
|
|
|
ws.getRow(row).height = 22 |
|
|
|
|
|
row += 1 |
|
|
|
|
|
|
|
|
|
|
|
const chartPairs = this.stage === 'ppk' |
|
|
|
|
|
? [ |
|
|
|
|
|
{ key: 'xbar', title: '01 X-bar 控制图', meta: 'CL:' + this.fm(this.result.Xb, 4) + ' UCL:' + this.fm(this.result.XU, 4) + ' LCL:' + this.fm(this.result.XL, 4) }, |
|
|
|
|
|
{ key: 'r', title: '02 R 控制图', meta: 'CL:' + this.fm(this.result.Rb, 4) + ' UCL:' + this.fm(this.result.RU, 4) + ' LCL:' + this.fm(this.result.RL, 4) } |
|
|
|
|
|
] |
|
|
|
|
|
: [ |
|
|
|
|
|
{ key: 'imr', title: '01 I-MR 控制图', meta: 'I-CL:' + this.fm(this.result.Xa, 4) + ' MR̄:' + this.fm(this.result.MRbar, 4) + ' MR-UCL:' + this.fm(this.result.MR_UCL, 4) }, |
|
|
|
|
|
{ key: 'hist', title: '02 直方图 + 正态分布', meta: 'μ:' + this.fm(this.result.Xa, 4) + ' σ:' + this.fm(this.result.sO, 4) } |
|
|
|
|
|
] |
|
|
|
|
|
|
|
|
|
|
|
for (let pi = 0; pi < chartPairs.length; pi++) { |
|
|
|
|
|
const item = chartPairs[pi] |
|
|
|
|
|
this.excelMergeSet(ws, row, 1, row, 8, item.title, { |
|
|
|
|
|
fill: EXCEL_COLORS.paper, |
|
|
|
|
|
font: { bold: true, size: 10, color: { argb: EXCEL_COLORS.ink } }, |
|
|
|
|
|
align: { vertical: 'middle', horizontal: 'left', indent: 1 } |
|
|
|
|
|
}) |
|
|
|
|
|
this.excelMergeSet(ws, row, 9, row, 12, item.meta, { |
|
|
|
|
|
fill: EXCEL_COLORS.paper, |
|
|
|
|
|
font: { size: 8, color: { argb: EXCEL_COLORS.muted } }, |
|
|
|
|
|
align: { vertical: 'middle', horizontal: 'right', indent: 1 } |
|
|
|
|
|
}) |
|
|
|
|
|
row += 1 |
|
|
|
|
|
|
|
|
|
|
|
const chart = this.chartInst[item.key] |
|
|
|
|
|
if (chart && typeof chart.getDataURL === 'function') { |
|
|
|
|
|
try { |
|
|
|
|
|
const dataUrl = chart.getDataURL({ |
|
|
|
|
|
type: 'png', |
|
|
|
|
|
pixelRatio: 2, |
|
|
|
|
|
backgroundColor: '#ffffff' |
|
|
|
|
|
}) |
|
|
|
|
|
const base64 = dataUrl.replace(/^data:image\/\w+;base64,/, '') |
|
|
|
|
|
const imgId = wb.addImage({ base64: base64, extension: 'png' }) |
|
|
|
|
|
const imgRow = row - 1 |
|
|
|
|
|
ws.addImage(imgId, { |
|
|
|
|
|
tl: { col: 0, row: imgRow + 0.2 }, |
|
|
|
|
|
ext: { width: 860, height: 260 } |
|
|
|
|
|
}) |
|
|
|
|
|
for (let i = 0; i < 14; i++) { |
|
|
|
|
|
ws.getRow(row + i).height = 15 |
|
|
|
|
|
} |
|
|
|
|
|
row += 14 |
|
|
|
|
|
} catch (e) { |
|
|
|
|
|
this.excelMergeSet(ws, row, 1, row, 12, '(图表导出失败)', { |
|
|
|
|
|
fill: EXCEL_COLORS.warnBg, |
|
|
|
|
|
font: { size: 9, color: { argb: EXCEL_COLORS.warn } }, |
|
|
|
|
|
align: { vertical: 'middle', horizontal: 'center' } |
|
|
|
|
|
}) |
|
|
|
|
|
row += 1 |
|
|
|
|
|
} |
|
|
|
|
|
} else { |
|
|
|
|
|
this.excelMergeSet(ws, row, 1, row, 12, '(暂无图表,请先完成计算)', { |
|
|
|
|
|
fill: EXCEL_COLORS.paper, |
|
|
|
|
|
font: { size: 9, color: { argb: EXCEL_COLORS.muted } }, |
|
|
|
|
|
align: { vertical: 'middle', horizontal: 'center' } |
|
|
|
|
|
}) |
|
|
|
|
|
row += 1 |
|
|
|
|
|
} |
|
|
|
|
|
row += 1 |
|
|
|
|
|
} |
|
|
|
|
|
return row |
|
|
|
|
|
}, |
|
|
|
|
|
excelWriteDetail (ws, row) { |
|
|
|
|
|
this.excelMergeSet(ws, row, 1, row, 12, '子组 / 样本明细数据', { |
|
|
|
|
|
fill: EXCEL_COLORS.accentSoft, |
|
|
|
|
|
font: { bold: true, size: 11, color: { argb: EXCEL_COLORS.accent } }, |
|
|
|
|
|
align: { vertical: 'middle', horizontal: 'left', indent: 1 } |
|
|
|
|
|
}) |
|
|
|
|
|
ws.getRow(row).height = 22 |
|
|
|
|
|
row += 1 |
|
|
|
|
|
|
|
|
|
|
|
const headerFill = EXCEL_COLORS.header |
|
|
|
|
|
const headerFont = { size: 9, bold: true, color: { argb: EXCEL_COLORS.white } } |
|
|
|
|
|
const center = { vertical: 'middle', horizontal: 'center' } |
|
|
|
|
|
|
|
|
if (this.stage === 'ppk') { |
|
|
if (this.stage === 'ppk') { |
|
|
const headers = [''].concat(this.result.groups.map((_, i) => 'G' + String(i + 1).padStart(2, '0'))) |
|
|
|
|
|
csv += headers.join(',') + '\n' |
|
|
|
|
|
|
|
|
const groups = this.result.groups || [] |
|
|
|
|
|
const headers = [''].concat(groups.map((_, i) => 'G' + String(i + 1).padStart(2, '0'))) |
|
|
|
|
|
headers.forEach((h, i) => { |
|
|
|
|
|
const cell = ws.getCell(row, i + 1) |
|
|
|
|
|
cell.value = h |
|
|
|
|
|
this.excelStyleCell(cell, { fill: headerFill, font: headerFont, align: center }) |
|
|
|
|
|
}) |
|
|
|
|
|
ws.getRow(row).height = 20 |
|
|
|
|
|
row += 1 |
|
|
|
|
|
|
|
|
for (let j = 0; j < this.nVal; j++) { |
|
|
for (let j = 0; j < this.nVal; j++) { |
|
|
const row = ['X' + (j + 1)] |
|
|
|
|
|
this.result.groups.forEach(g => row.push(g[j] != null ? Number(g[j]).toFixed(3) : '')) |
|
|
|
|
|
csv += row.join(',') + '\n' |
|
|
|
|
|
|
|
|
const labelCell = ws.getCell(row, 1) |
|
|
|
|
|
labelCell.value = 'X' + (j + 1) |
|
|
|
|
|
this.excelStyleCell(labelCell, { fill: EXCEL_COLORS.paper, font: { bold: true, size: 9 }, align: center }) |
|
|
|
|
|
groups.forEach((g, i) => { |
|
|
|
|
|
const cell = ws.getCell(row, i + 2) |
|
|
|
|
|
cell.value = g[j] != null ? Number(Number(g[j]).toFixed(3)) : '' |
|
|
|
|
|
this.excelStyleCell(cell, { align: center, font: { size: 9 } }) |
|
|
|
|
|
}) |
|
|
|
|
|
ws.getRow(row).height = 18 |
|
|
|
|
|
row += 1 |
|
|
} |
|
|
} |
|
|
csv += ['X̄'].concat(this.result.means.map(v => Number(v).toFixed(4))).join(',') + '\n' |
|
|
|
|
|
csv += ['R'].concat(this.result.ranges.map(v => Number(v).toFixed(4))).join(',') + '\n' |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const meanLabel = ws.getCell(row, 1) |
|
|
|
|
|
meanLabel.value = 'X̄' |
|
|
|
|
|
this.excelStyleCell(meanLabel, { fill: EXCEL_COLORS.accentSoft, font: { bold: true, size: 9 }, align: center }) |
|
|
|
|
|
this.result.means.forEach((v, i) => { |
|
|
|
|
|
const cell = ws.getCell(row, i + 2) |
|
|
|
|
|
cell.value = Number(Number(v).toFixed(4)) |
|
|
|
|
|
this.excelStyleCell(cell, { fill: EXCEL_COLORS.accentSoft, font: { bold: true, size: 9 }, align: center }) |
|
|
|
|
|
}) |
|
|
|
|
|
row += 1 |
|
|
|
|
|
|
|
|
|
|
|
const rLabel = ws.getCell(row, 1) |
|
|
|
|
|
rLabel.value = 'R' |
|
|
|
|
|
this.excelStyleCell(rLabel, { fill: EXCEL_COLORS.amberBg, font: { bold: true, size: 9 }, align: center }) |
|
|
|
|
|
this.result.ranges.forEach((v, i) => { |
|
|
|
|
|
const cell = ws.getCell(row, i + 2) |
|
|
|
|
|
cell.value = Number(Number(v).toFixed(4)) |
|
|
|
|
|
this.excelStyleCell(cell, { fill: EXCEL_COLORS.amberBg, font: { size: 9 }, align: center }) |
|
|
|
|
|
}) |
|
|
|
|
|
row += 1 |
|
|
|
|
|
|
|
|
|
|
|
const xJudgeLabel = ws.getCell(row, 1) |
|
|
|
|
|
xJudgeLabel.value = 'X̄判定' |
|
|
|
|
|
this.excelStyleCell(xJudgeLabel, { fill: EXCEL_COLORS.paper, font: { bold: true, size: 9 }, align: center }) |
|
|
|
|
|
this.result.means.forEach((v, i) => { |
|
|
|
|
|
const out = v > this.result.XU || v < this.result.XL |
|
|
|
|
|
const cell = ws.getCell(row, i + 2) |
|
|
|
|
|
cell.value = out ? '超限' : 'OK' |
|
|
|
|
|
this.excelStyleCell(cell, { |
|
|
|
|
|
fill: out ? EXCEL_COLORS.ngBg : EXCEL_COLORS.okBg, |
|
|
|
|
|
font: { bold: true, size: 9, color: { argb: out ? EXCEL_COLORS.ng : EXCEL_COLORS.ok } }, |
|
|
|
|
|
align: center |
|
|
|
|
|
}) |
|
|
|
|
|
}) |
|
|
|
|
|
row += 1 |
|
|
|
|
|
|
|
|
|
|
|
const rJudgeLabel = ws.getCell(row, 1) |
|
|
|
|
|
rJudgeLabel.value = 'R判定' |
|
|
|
|
|
this.excelStyleCell(rJudgeLabel, { fill: EXCEL_COLORS.paper, font: { bold: true, size: 9 }, align: center }) |
|
|
|
|
|
this.result.ranges.forEach((v, i) => { |
|
|
|
|
|
const out = v > this.result.RU |
|
|
|
|
|
const cell = ws.getCell(row, i + 2) |
|
|
|
|
|
cell.value = out ? '超限' : 'OK' |
|
|
|
|
|
this.excelStyleCell(cell, { |
|
|
|
|
|
fill: out ? EXCEL_COLORS.ngBg : EXCEL_COLORS.okBg, |
|
|
|
|
|
font: { bold: true, size: 9, color: { argb: out ? EXCEL_COLORS.ng : EXCEL_COLORS.ok } }, |
|
|
|
|
|
align: center |
|
|
|
|
|
}) |
|
|
|
|
|
}) |
|
|
} else { |
|
|
} else { |
|
|
const headers = ['#', '子组'].concat(Array.from({ length: this.nVal }, (_, i) => 'X' + (i + 1))).concat(['X̄', 'R', '判定']) |
|
|
const headers = ['#', '子组'].concat(Array.from({ length: this.nVal }, (_, i) => 'X' + (i + 1))).concat(['X̄', 'R', '判定']) |
|
|
csv += headers.join(',') + '\n' |
|
|
|
|
|
this.cpkTableRows.forEach((row, i) => { |
|
|
|
|
|
const vals = row.vals.map(v => (v != null ? Number(v).toFixed(3) : '')) |
|
|
|
|
|
while (vals.length < this.nVal) vals.push('') |
|
|
|
|
|
csv += [i + 1, 'G' + String(i + 1).padStart(2, '0')].concat(vals).concat([ |
|
|
|
|
|
Number(row.mean).toFixed(4), |
|
|
|
|
|
Number(row.range).toFixed(4), |
|
|
|
|
|
row.out ? '超规' : 'OK' |
|
|
|
|
|
]).join(',') + '\n' |
|
|
|
|
|
|
|
|
headers.forEach((h, i) => { |
|
|
|
|
|
const cell = ws.getCell(row, i + 1) |
|
|
|
|
|
cell.value = h |
|
|
|
|
|
this.excelStyleCell(cell, { fill: headerFill, font: headerFont, align: center }) |
|
|
|
|
|
}) |
|
|
|
|
|
ws.getRow(row).height = 20 |
|
|
|
|
|
row += 1 |
|
|
|
|
|
|
|
|
|
|
|
this.cpkTableRows.forEach((dataRow, i) => { |
|
|
|
|
|
const vals = [] |
|
|
|
|
|
for (let j = 0; j < this.nVal; j++) { |
|
|
|
|
|
vals.push(dataRow.vals[j] != null ? Number(Number(dataRow.vals[j]).toFixed(3)) : '') |
|
|
|
|
|
} |
|
|
|
|
|
const values = [i + 1, 'G' + String(i + 1).padStart(2, '0')].concat(vals).concat([ |
|
|
|
|
|
Number(Number(dataRow.mean).toFixed(4)), |
|
|
|
|
|
Number(Number(dataRow.range).toFixed(4)), |
|
|
|
|
|
dataRow.out ? '超规' : 'OK' |
|
|
|
|
|
]) |
|
|
|
|
|
values.forEach((v, ci) => { |
|
|
|
|
|
const cell = ws.getCell(row, ci + 1) |
|
|
|
|
|
cell.value = v |
|
|
|
|
|
const isJudge = ci === values.length - 1 |
|
|
|
|
|
this.excelStyleCell(cell, { |
|
|
|
|
|
fill: isJudge ? (dataRow.out ? EXCEL_COLORS.ngBg : EXCEL_COLORS.okBg) : (i % 2 ? EXCEL_COLORS.paper : EXCEL_COLORS.white), |
|
|
|
|
|
font: { |
|
|
|
|
|
size: 9, |
|
|
|
|
|
bold: isJudge || ci === headers.length - 3, |
|
|
|
|
|
color: { argb: isJudge ? (dataRow.out ? EXCEL_COLORS.ng : EXCEL_COLORS.ok) : EXCEL_COLORS.ink } |
|
|
|
|
|
}, |
|
|
|
|
|
align: center |
|
|
|
|
|
}) |
|
|
|
|
|
}) |
|
|
|
|
|
ws.getRow(row).height = 18 |
|
|
|
|
|
row += 1 |
|
|
}) |
|
|
}) |
|
|
} |
|
|
} |
|
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' }) |
|
|
|
|
|
const url = URL.createObjectURL(blob) |
|
|
|
|
|
const a = document.createElement('a') |
|
|
|
|
|
a.href = url |
|
|
|
|
|
a.download = 'spc_data.csv' |
|
|
|
|
|
a.click() |
|
|
|
|
|
URL.revokeObjectURL(url) |
|
|
|
|
|
|
|
|
return row |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|