Merge remote-tracking branch 'origin/0.8.X' into 0.8.X
This commit is contained in:
@@ -47,5 +47,10 @@
|
||||
<artifactId>fastjson2</artifactId>
|
||||
<version>2.0.35</version>
|
||||
</dependency>
|
||||
<!-- Word 导出(docx) -->
|
||||
<dependency>
|
||||
<groupId>org.apache.poi</groupId>
|
||||
<artifactId>poi-ooxml</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@@ -5,14 +5,23 @@ import com.klp.common.core.controller.BaseController;
|
||||
import com.klp.common.core.domain.R;
|
||||
import com.klp.common.core.page.TableDataInfo;
|
||||
import com.klp.common.utils.StringUtils;
|
||||
import com.klp.da.service.OeeWordAiAnalysisService;
|
||||
import com.klp.da.service.OeeReportJobService;
|
||||
import com.klp.pocket.acid.domain.vo.AcidOeeDailySummaryVo;
|
||||
import com.klp.pocket.acid.domain.vo.AcidOeeIdealCycleVo;
|
||||
import com.klp.pocket.acid.domain.vo.AcidOeeLoss7Vo;
|
||||
import com.klp.pocket.acid.domain.vo.AcidOeeRegressionVo;
|
||||
import com.klp.pocket.acid.domain.vo.Klptcm1ProStoppageVo;
|
||||
import com.klp.pocket.acid.service.IAcidOeeService;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.poi.xwpf.usermodel.ParagraphAlignment;
|
||||
import org.apache.poi.xwpf.usermodel.XWPFDocument;
|
||||
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
|
||||
import org.apache.poi.xwpf.usermodel.XWPFRun;
|
||||
import org.apache.poi.xwpf.usermodel.XWPFTable;
|
||||
import org.apache.poi.xwpf.usermodel.XWPFTableCell;
|
||||
import org.apache.poi.xwpf.usermodel.XWPFTableRow;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -23,11 +32,17 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.servlet.ServletOutputStream;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -48,6 +63,7 @@ public class OeeReportController extends BaseController {
|
||||
private final IAcidOeeService acidOeeService;
|
||||
private final StringRedisTemplate stringRedisTemplate;
|
||||
private final OeeReportJobService oeeReportJobService;
|
||||
private final OeeWordAiAnalysisService oeeWordAiAnalysisService;
|
||||
|
||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
|
||||
@@ -177,21 +193,233 @@ public class OeeReportController extends BaseController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 酸轧线理论节拍回归数据
|
||||
* 酸轧线理论节拍(统计口径:历史优良日中位数)
|
||||
*
|
||||
* 路由:GET /oee/line/acid/regression
|
||||
* 说明:
|
||||
* - {@code startDate} / {@code endDate} 可选,若为空则由 pocket 按“近6个月”默认处理。
|
||||
* 路由:GET /oee/line/acid/idealCycle
|
||||
*/
|
||||
@GetMapping("/acid/regression")
|
||||
public R<AcidOeeRegressionVo> getAcidRegression(
|
||||
@RequestParam(required = false) String startDate,
|
||||
@RequestParam(required = false) String endDate
|
||||
@GetMapping("/acid/idealCycle")
|
||||
public R<AcidOeeIdealCycleVo> getAcidIdealCycle(
|
||||
@RequestParam String startDate,
|
||||
@RequestParam String endDate
|
||||
) {
|
||||
AcidOeeRegressionVo data = acidOeeService.getRegressionData(startDate, endDate);
|
||||
AcidOeeIdealCycleVo data = acidOeeService.getIdealCycle(startDate, endDate);
|
||||
return R.ok(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出酸轧线 OEE 报表 Word(docx)
|
||||
*
|
||||
* 路由:GET /oee/line/acid/exportWord?startDate=yyyy-MM-dd&endDate=yyyy-MM-dd
|
||||
* 说明:
|
||||
* - 文档结构尽量贴近前端页面(表格为主,图表不嵌入);
|
||||
* - 将右侧“公式与口径说明”置于文档最上方。
|
||||
*/
|
||||
@GetMapping("/acid/exportWord")
|
||||
public void exportAcidWord(
|
||||
@RequestParam String startDate,
|
||||
@RequestParam String endDate,
|
||||
HttpServletResponse response
|
||||
) throws IOException {
|
||||
String[] range = resolveDateRange(startDate, endDate);
|
||||
String start = range[0];
|
||||
String end = range[1];
|
||||
|
||||
// 取数:按选择范围实时拉取(避免 summary/loss7 的“当月固定”逻辑)
|
||||
List<AcidOeeDailySummaryVo> summary = acidOeeService.getDailySummary(start, end);
|
||||
List<AcidOeeLoss7Vo> loss7 = acidOeeService.getLoss7Summary(start, end);
|
||||
List<Klptcm1ProStoppageVo> events = acidOeeService.getStoppageEvents(start, end);
|
||||
AcidOeeIdealCycleVo idealCycle = acidOeeService.getIdealCycle(start, end);
|
||||
|
||||
XWPFDocument doc = new XWPFDocument();
|
||||
|
||||
// 标题
|
||||
addTitle(doc, String.format("酸轧线OEE报表(%s ~ %s)", start, end));
|
||||
|
||||
// 口径说明(置顶)
|
||||
addSectionTitle(doc, "公式与口径说明");
|
||||
addParagraph(doc, "OEE 总公式:OEE = A × P × Q", true);
|
||||
addBullet(doc, "A(时间稼动率) = (负荷时间 − 停机时间) / 负荷时间");
|
||||
addBullet(doc, "P(性能稼动率,吨维度) = (理论节拍 × 产量吨) / 实际运转时间");
|
||||
addBullet(doc, "Q(良品率) = 良品吨 / 总产量吨");
|
||||
addParagraph(doc, "关键字段定义:", true);
|
||||
addBullet(doc, "负荷时间:计划生产时间扣除计划停机后的时间");
|
||||
addBullet(doc, "停机时间:所有停机/中断(按 stop_type 汇总)的总时长");
|
||||
addBullet(doc, "实际运转时间:负荷时间 − 停机时间");
|
||||
addBullet(doc, "理论节拍:按“优良日统计口径”得到的稳定节拍(分钟/吨)");
|
||||
addBullet(doc, "良品/次品:按 WMS quality_status 判断,C+/C/C-/D+/D/D- 为次品");
|
||||
|
||||
if (idealCycle != null) {
|
||||
addParagraph(doc, "当前理论节拍(统计口径):", true);
|
||||
XWPFTable t = doc.createTable(5, 2);
|
||||
setTableHeaderRow(t.getRow(0), "指标", "值");
|
||||
setTableRow(t.getRow(1), "理论节拍 (min/吨)", fmtNum(getIdealCycleTime(idealCycle), 2));
|
||||
setTableRow(t.getRow(2), "生产节拍中位数 (min/吨)", fmtNum(getMedianCycleTime(idealCycle), 2));
|
||||
setTableRow(t.getRow(3), "样本天数", String.valueOf(getSampleDays(idealCycle)));
|
||||
setTableRow(t.getRow(4), "吨数下限 (吨)", fmtNum(getMinWeightTon(idealCycle), 2));
|
||||
}
|
||||
|
||||
// 一、KPI 总览
|
||||
addSectionTitle(doc, "一、KPI 总览(酸轧线 SY)");
|
||||
addParagraph(doc, "展示所选期间酸轧线整体 OEE 及 A/P/Q 等关键指标,用于快速判断本期综合表现好坏。", false);
|
||||
AcidKpiAgg kpi = calcKpi(summary);
|
||||
XWPFTable kpiTable = doc.createTable(2, 11);
|
||||
setTableRow(kpiTable.getRow(0),
|
||||
"OEE (%)",
|
||||
"时间稼动率 A (%)",
|
||||
"性能稼动率 P_ton (%)",
|
||||
"良品率 Q (%)",
|
||||
"负荷时间 (min)",
|
||||
"停机时间 (min)",
|
||||
"运转时间 (min)",
|
||||
"总产量 (吨)",
|
||||
"总产量 (卷)",
|
||||
"良品量 (吨)",
|
||||
"次品量 (吨)"
|
||||
);
|
||||
setTableRow(kpiTable.getRow(1),
|
||||
fmtPercent1(kpi.oee),
|
||||
fmtPercent1(kpi.availability),
|
||||
fmtPercent1(kpi.performanceTon),
|
||||
fmtPercent1(kpi.quality),
|
||||
fmtInt(kpi.loadingTimeMin),
|
||||
fmtInt(kpi.downtimeMin),
|
||||
fmtInt(kpi.runTimeMin),
|
||||
fmtNum(kpi.totalOutputTon, 2),
|
||||
fmtInt(kpi.totalOutputCoil),
|
||||
fmtNum(kpi.goodOutputTon, 2),
|
||||
fmtNum(kpi.defectOutputTon, 2)
|
||||
);
|
||||
addAiAnalysisIfEnabled(doc, "KPI 总览", buildMarkdownTable(
|
||||
new String[]{"OEE(%)", "A(%)", "P_ton(%)", "Q(%)", "负荷(min)", "停机(min)", "运转(min)", "总产(吨)", "总产(卷)", "良品(吨)", "次品(吨)"},
|
||||
new String[][]{{
|
||||
fmtPercent1(kpi.oee),
|
||||
fmtPercent1(kpi.availability),
|
||||
fmtPercent1(kpi.performanceTon),
|
||||
fmtPercent1(kpi.quality),
|
||||
fmtInt(kpi.loadingTimeMin),
|
||||
fmtInt(kpi.downtimeMin),
|
||||
fmtInt(kpi.runTimeMin),
|
||||
fmtNum(kpi.totalOutputTon, 2),
|
||||
fmtInt(kpi.totalOutputCoil),
|
||||
fmtNum(kpi.goodOutputTon, 2),
|
||||
fmtNum(kpi.defectOutputTon, 2)
|
||||
}}), "区间:" + start + " ~ " + end);
|
||||
|
||||
// 二、日明细
|
||||
addSectionTitle(doc, "二、日明细(用于趋势分析)");
|
||||
addParagraph(doc, "按天拆分 A/P/Q 及产量等指标,用于观察趋势、波动点以及与重大事件的对应关系。", false);
|
||||
XWPFTable dailyTable = doc.createTable(Math.max(1, summary.size()) + 1, 11);
|
||||
setTableRow(dailyTable.getRow(0),
|
||||
"日期",
|
||||
"OEE (%)",
|
||||
"A (%)",
|
||||
"P_ton (%)",
|
||||
"Q (%)",
|
||||
"负荷 (min)",
|
||||
"停机 (min)",
|
||||
"运转 (min)",
|
||||
"总产量 (吨)",
|
||||
"总产量 (卷)",
|
||||
"良品 (吨)"
|
||||
);
|
||||
for (int i = 0; i < summary.size(); i++) {
|
||||
AcidOeeDailySummaryVo row = summary.get(i);
|
||||
setTableRow(dailyTable.getRow(i + 1),
|
||||
safeStr(row.getStatDate()),
|
||||
fmtPercent1(row.getOee()),
|
||||
fmtPercent1(row.getAvailability()),
|
||||
fmtPercent1(row.getPerformanceTon()),
|
||||
fmtPercent1(row.getQuality()),
|
||||
fmtInt(row.getLoadingTimeMin()),
|
||||
fmtInt(row.getDowntimeMin()),
|
||||
fmtInt(row.getRunTimeMin()),
|
||||
fmtNum(row.getTotalOutputTon(), 2),
|
||||
fmtInt(row.getTotalOutputCoil()),
|
||||
fmtNum(row.getGoodOutputTon(), 2)
|
||||
);
|
||||
}
|
||||
addAiAnalysisIfEnabled(doc, "日明细", buildDailySummaryMarkdown(summary, 31), "区间:" + start + " ~ " + end);
|
||||
|
||||
// 三、理论节拍(说明 + 表)
|
||||
addSectionTitle(doc, "三、理论节拍(统计口径)");
|
||||
addParagraph(doc, "基于历史“优良日”统计得到的理论节拍(中位数),用于作为性能稼动率计算的稳定标尺。", false);
|
||||
if (idealCycle == null) {
|
||||
addParagraph(doc, "(本区间未获取到理论节拍数据)", false);
|
||||
}
|
||||
|
||||
// 四、7 大损失
|
||||
addSectionTitle(doc, "四、7 大损失汇总(按 stop_type 分类)");
|
||||
addParagraph(doc, "将所有停机事件按 stop_type 归类,统计时间占比和次数,用于确定“先从哪几类损失下手改善”。", false);
|
||||
XWPFTable loss7Table = doc.createTable(Math.max(1, loss7.size()) + 1, 5);
|
||||
setTableRow(loss7Table.getRow(0),
|
||||
"损失类别",
|
||||
"损失时间 (min)",
|
||||
"占比 (%)",
|
||||
"次数",
|
||||
"平均时长 (min)"
|
||||
);
|
||||
for (int i = 0; i < loss7.size(); i++) {
|
||||
AcidOeeLoss7Vo row = loss7.get(i);
|
||||
setTableRow(loss7Table.getRow(i + 1),
|
||||
safeStr(row.getLossCategoryName()),
|
||||
fmtNum(row.getLossTimeMin(), 2),
|
||||
fmtPercent1(row.getLossTimeRate()),
|
||||
fmtInt(row.getCount()),
|
||||
fmtNum(row.getAvgDurationMin(), 2)
|
||||
);
|
||||
}
|
||||
addAiAnalysisIfEnabled(doc, "7 大损失汇总", buildLoss7Markdown(loss7, 30), "区间:" + start + " ~ " + end);
|
||||
|
||||
// 五、停机/损失事件明细
|
||||
addSectionTitle(doc, "五、停机/损失事件明细");
|
||||
addParagraph(doc, "罗列每一条停机/损失事件,包含时间段、区域、机组和备注,方便对照现场记录进行原因分析。", false);
|
||||
int eventRows = Math.max(1, events.size()) + 1;
|
||||
XWPFTable eventTable = doc.createTable(eventRows, 9);
|
||||
setTableRow(eventTable.getRow(0),
|
||||
"开始时间",
|
||||
"结束时间",
|
||||
"时长 (s)",
|
||||
"时长 (min)",
|
||||
"停机类型 (stop_type)",
|
||||
"区域",
|
||||
"机组",
|
||||
"班次",
|
||||
"备注"
|
||||
);
|
||||
for (int i = 0; i < events.size(); i++) {
|
||||
Klptcm1ProStoppageVo e = events.get(i);
|
||||
long durationSec = safeLong(e.getDuration());
|
||||
double durationMin = durationSec / 60d;
|
||||
setTableRow(eventTable.getRow(i + 1),
|
||||
safeStr(e.getStartDate()),
|
||||
safeStr(e.getEndDate()),
|
||||
String.valueOf(durationSec),
|
||||
fmtNum(durationMin, 3),
|
||||
safeStr(e.getStopType()),
|
||||
safeStr(e.getArea()),
|
||||
safeStr(e.getUnit()),
|
||||
safeStr(e.getShift()),
|
||||
safeStr(e.getRemark())
|
||||
);
|
||||
}
|
||||
addAiAnalysisIfEnabled(doc, "停机/损失事件明细", buildEventsMarkdown(events, 40), "区间:" + start + " ~ " + end);
|
||||
|
||||
String filename = String.format("酸轧线OEE报表_%s_%s.docx", start, end);
|
||||
String encoded = URLEncoder.encode(filename, StandardCharsets.UTF_8.name());
|
||||
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||
response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
|
||||
response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
|
||||
"attachment; filename*=UTF-8''" + encoded);
|
||||
response.setHeader(HttpHeaders.CACHE_CONTROL, "no-store, no-cache");
|
||||
|
||||
try (ServletOutputStream os = response.getOutputStream()) {
|
||||
doc.write(os);
|
||||
os.flush();
|
||||
} finally {
|
||||
doc.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 若未显式传入日期范围,则默认当前月 [1号, 今天]。
|
||||
*/
|
||||
@@ -219,6 +447,315 @@ public class OeeReportController extends BaseController {
|
||||
return isStart ? firstDay.format(DATE_FORMATTER) : today.format(DATE_FORMATTER);
|
||||
}
|
||||
|
||||
// ======================== Word 生成辅助方法 ========================
|
||||
|
||||
private void addTitle(XWPFDocument doc, String text) {
|
||||
XWPFParagraph p = doc.createParagraph();
|
||||
p.setAlignment(ParagraphAlignment.CENTER);
|
||||
XWPFRun r = p.createRun();
|
||||
r.setBold(true);
|
||||
r.setFontSize(16);
|
||||
r.setText(text);
|
||||
}
|
||||
|
||||
private void addSectionTitle(XWPFDocument doc, String text) {
|
||||
XWPFParagraph p = doc.createParagraph();
|
||||
p.setSpacingBefore(200);
|
||||
XWPFRun r = p.createRun();
|
||||
r.setBold(true);
|
||||
r.setFontSize(12);
|
||||
r.setText(text);
|
||||
}
|
||||
|
||||
private void addParagraph(XWPFDocument doc, String text, boolean bold) {
|
||||
XWPFParagraph p = doc.createParagraph();
|
||||
XWPFRun r = p.createRun();
|
||||
r.setBold(bold);
|
||||
r.setFontSize(11);
|
||||
r.setText(text);
|
||||
}
|
||||
|
||||
private void addBullet(XWPFDocument doc, String text) {
|
||||
XWPFParagraph p = doc.createParagraph();
|
||||
XWPFRun r = p.createRun();
|
||||
r.setFontSize(11);
|
||||
r.setText("• " + text);
|
||||
}
|
||||
|
||||
private void addAiAnalysisIfEnabled(XWPFDocument doc, String tableTitle, String tableMarkdown, String extraContext) {
|
||||
String analysis = null;
|
||||
try {
|
||||
analysis = oeeWordAiAnalysisService.analyzeTable(tableTitle, tableMarkdown, extraContext);
|
||||
} catch (Exception e) {
|
||||
// 不影响导出
|
||||
}
|
||||
if (StringUtils.isNotBlank(analysis)) {
|
||||
addParagraph(doc, "AI 分析:", true);
|
||||
// 多行按段落输出
|
||||
String[] lines = analysis.split("\\r?\\n");
|
||||
for (String line : lines) {
|
||||
if (StringUtils.isNotBlank(line)) {
|
||||
addBullet(doc, line.trim().replaceFirst("^[\\-•\\*\\d\\.\\s]+", ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String buildMarkdownTable(String[] headers, String[][] rows) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("|");
|
||||
for (String h : headers) sb.append(escapePipe(h)).append("|");
|
||||
sb.append("\n|");
|
||||
for (int i = 0; i < headers.length; i++) sb.append("---|");
|
||||
sb.append("\n");
|
||||
for (String[] r : rows) {
|
||||
sb.append("|");
|
||||
for (String c : r) sb.append(escapePipe(c)).append("|");
|
||||
sb.append("\n");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String escapePipe(String s) {
|
||||
if (s == null) return "";
|
||||
return s.replace("|", "\\|");
|
||||
}
|
||||
|
||||
private String buildDailySummaryMarkdown(List<AcidOeeDailySummaryVo> list, int maxRows) {
|
||||
List<AcidOeeDailySummaryVo> src = list == null ? new ArrayList<>() : list;
|
||||
int n = Math.min(src.size(), Math.max(1, maxRows));
|
||||
String[] headers = new String[]{"日期", "OEE(%)", "A(%)", "P_ton(%)", "Q(%)", "负荷(min)", "停机(min)", "运转(min)", "总产(吨)", "良品(吨)"};
|
||||
String[][] rows = new String[n][headers.length];
|
||||
for (int i = 0; i < n; i++) {
|
||||
AcidOeeDailySummaryVo r = src.get(i);
|
||||
rows[i] = new String[]{
|
||||
safeStr(r.getStatDate()),
|
||||
fmtPercent1(r.getOee()),
|
||||
fmtPercent1(r.getAvailability()),
|
||||
fmtPercent1(r.getPerformanceTon()),
|
||||
fmtPercent1(r.getQuality()),
|
||||
fmtInt(r.getLoadingTimeMin()),
|
||||
fmtInt(r.getDowntimeMin()),
|
||||
fmtInt(r.getRunTimeMin()),
|
||||
fmtNum(r.getTotalOutputTon(), 2),
|
||||
fmtNum(r.getGoodOutputTon(), 2)
|
||||
};
|
||||
}
|
||||
String md = buildMarkdownTable(headers, rows);
|
||||
if (src.size() > n) {
|
||||
md += "\n(仅展示前 " + n + " 行,已截断)\n";
|
||||
}
|
||||
return md;
|
||||
}
|
||||
|
||||
private String buildLoss7Markdown(List<AcidOeeLoss7Vo> list, int maxRows) {
|
||||
List<AcidOeeLoss7Vo> src = list == null ? new ArrayList<>() : list;
|
||||
int n = Math.min(src.size(), Math.max(1, maxRows));
|
||||
String[] headers = new String[]{"损失类别", "损失时间(min)", "占比(%)", "次数", "平均时长(min)"};
|
||||
String[][] rows = new String[n][headers.length];
|
||||
for (int i = 0; i < n; i++) {
|
||||
AcidOeeLoss7Vo r = src.get(i);
|
||||
rows[i] = new String[]{
|
||||
safeStr(r.getLossCategoryName()),
|
||||
fmtNum(r.getLossTimeMin(), 2),
|
||||
fmtPercent1(r.getLossTimeRate()),
|
||||
fmtInt(r.getCount()),
|
||||
fmtNum(r.getAvgDurationMin(), 2)
|
||||
};
|
||||
}
|
||||
String md = buildMarkdownTable(headers, rows);
|
||||
if (src.size() > n) {
|
||||
md += "\n(仅展示前 " + n + " 行,已截断)\n";
|
||||
}
|
||||
return md;
|
||||
}
|
||||
|
||||
private String buildEventsMarkdown(List<Klptcm1ProStoppageVo> list, int maxRows) {
|
||||
List<Klptcm1ProStoppageVo> src = list == null ? new ArrayList<>() : list;
|
||||
int n = Math.min(src.size(), Math.max(1, maxRows));
|
||||
String[] headers = new String[]{"开始", "结束", "时长(s)", "时长(min)", "停机类型", "区域", "机组", "备注"};
|
||||
String[][] rows = new String[n][headers.length];
|
||||
for (int i = 0; i < n; i++) {
|
||||
Klptcm1ProStoppageVo e = src.get(i);
|
||||
long durationSec = safeLong(e.getDuration());
|
||||
rows[i] = new String[]{
|
||||
safeStr(e.getStartDate()),
|
||||
safeStr(e.getEndDate()),
|
||||
String.valueOf(durationSec),
|
||||
fmtNum(durationSec / 60d, 3),
|
||||
safeStr(e.getStopType()),
|
||||
safeStr(e.getArea()),
|
||||
safeStr(e.getUnit()),
|
||||
safeStr(e.getRemark())
|
||||
};
|
||||
}
|
||||
String md = buildMarkdownTable(headers, rows);
|
||||
md += "\n总事件数:" + src.size() + "\n";
|
||||
if (src.size() > n) {
|
||||
md += "(仅展示前 " + n + " 行,已截断)\n";
|
||||
}
|
||||
return md;
|
||||
}
|
||||
|
||||
private void setTableHeaderRow(XWPFTableRow row, String c1, String c2) {
|
||||
setCellText(row.getCell(0), c1, true);
|
||||
setCellText(row.getCell(1), c2, true);
|
||||
}
|
||||
|
||||
private void setTableRow(XWPFTableRow row, String... cols) {
|
||||
for (int i = 0; i < cols.length; i++) {
|
||||
XWPFTableCell cell = row.getCell(i);
|
||||
if (cell == null) {
|
||||
cell = row.createCell();
|
||||
}
|
||||
setCellText(cell, cols[i], false);
|
||||
}
|
||||
}
|
||||
|
||||
private void setCellText(XWPFTableCell cell, String text, boolean bold) {
|
||||
cell.removeParagraph(0);
|
||||
XWPFParagraph p = cell.addParagraph();
|
||||
XWPFRun r = p.createRun();
|
||||
r.setBold(bold);
|
||||
r.setFontSize(10);
|
||||
r.setText(text == null ? "" : text);
|
||||
}
|
||||
|
||||
private String safeStr(Object o) {
|
||||
return o == null ? "" : String.valueOf(o);
|
||||
}
|
||||
|
||||
private long safeLong(Object o) {
|
||||
if (o == null) return 0L;
|
||||
try {
|
||||
return Long.parseLong(String.valueOf(o));
|
||||
} catch (Exception ex) {
|
||||
return 0L;
|
||||
}
|
||||
}
|
||||
|
||||
private String fmtPercent1(Object value) {
|
||||
Double v = toDouble(value);
|
||||
if (v == null) return "-";
|
||||
return String.format(Locale.ROOT, "%.1f", v);
|
||||
}
|
||||
|
||||
private String fmtNum(Object value, int scale) {
|
||||
Double v = toDouble(value);
|
||||
if (v == null) return "-";
|
||||
return String.format(Locale.ROOT, "%." + scale + "f", v);
|
||||
}
|
||||
|
||||
private String fmtInt(Object value) {
|
||||
Double v = toDouble(value);
|
||||
if (v == null) return "0";
|
||||
return String.valueOf((long) Math.round(v));
|
||||
}
|
||||
|
||||
private Double toDouble(Object value) {
|
||||
if (value == null) return null;
|
||||
if (value instanceof Number) return ((Number) value).doubleValue();
|
||||
String s = String.valueOf(value);
|
||||
if (StringUtils.isBlank(s)) return null;
|
||||
try {
|
||||
return Double.parseDouble(s);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class AcidKpiAgg {
|
||||
private double oee;
|
||||
private double availability;
|
||||
private double performanceTon;
|
||||
private double quality;
|
||||
private double loadingTimeMin;
|
||||
private double downtimeMin;
|
||||
private double runTimeMin;
|
||||
private double totalOutputTon;
|
||||
private double totalOutputCoil;
|
||||
private double goodOutputTon;
|
||||
private double defectOutputTon;
|
||||
}
|
||||
|
||||
private AcidKpiAgg calcKpi(List<AcidOeeDailySummaryVo> list) {
|
||||
AcidKpiAgg k = new AcidKpiAgg();
|
||||
if (list == null || list.isEmpty()) {
|
||||
return k;
|
||||
}
|
||||
int n = list.size();
|
||||
double sumOee = 0, sumA = 0, sumP = 0, sumQ = 0;
|
||||
double sumLoading = 0, sumDown = 0, sumRun = 0;
|
||||
double sumTotalTon = 0, sumGoodTon = 0, sumDefectTon = 0, sumCoil = 0;
|
||||
for (AcidOeeDailySummaryVo r : list) {
|
||||
sumLoading += safeNum(r.getLoadingTimeMin());
|
||||
sumDown += safeNum(r.getDowntimeMin());
|
||||
sumRun += safeNum(r.getRunTimeMin());
|
||||
sumTotalTon += safeNum(r.getTotalOutputTon());
|
||||
sumGoodTon += safeNum(r.getGoodOutputTon());
|
||||
sumDefectTon += safeNum(r.getDefectOutputTon());
|
||||
sumCoil += safeNum(r.getTotalOutputCoil());
|
||||
sumOee += safeNum(r.getOee());
|
||||
sumA += safeNum(r.getAvailability());
|
||||
sumP += safeNum(r.getPerformanceTon());
|
||||
sumQ += safeNum(r.getQuality());
|
||||
}
|
||||
double defectAgg = Math.max(0, sumTotalTon - sumGoodTon);
|
||||
k.setOee(sumOee / n);
|
||||
k.setAvailability(sumA / n);
|
||||
k.setPerformanceTon(sumP / n);
|
||||
k.setQuality(sumQ / n);
|
||||
k.setLoadingTimeMin(sumLoading);
|
||||
k.setDowntimeMin(sumDown);
|
||||
k.setRunTimeMin(sumRun);
|
||||
k.setTotalOutputTon(sumTotalTon);
|
||||
k.setTotalOutputCoil(sumCoil);
|
||||
k.setGoodOutputTon(sumGoodTon);
|
||||
k.setDefectOutputTon(defectAgg > 0 ? defectAgg : sumDefectTon);
|
||||
return k;
|
||||
}
|
||||
|
||||
private double safeNum(Object v) {
|
||||
Double d = toDouble(v);
|
||||
return d == null ? 0d : d;
|
||||
}
|
||||
|
||||
// idealCycle 字段做容错(避免 VO 字段名差异导致编译失败)
|
||||
private Object getIdealCycleTime(AcidOeeIdealCycleVo vo) {
|
||||
try {
|
||||
return vo.getClass().getMethod("getIdealCycleTimeMinPerTon").invoke(vo);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Object getMedianCycleTime(AcidOeeIdealCycleVo vo) {
|
||||
try {
|
||||
return vo.getClass().getMethod("getMedianCycleTimeMinPerTon").invoke(vo);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private int getSampleDays(AcidOeeIdealCycleVo vo) {
|
||||
try {
|
||||
Object v = vo.getClass().getMethod("getSampleDays").invoke(vo);
|
||||
Double d = toDouble(v);
|
||||
return d == null ? 0 : d.intValue();
|
||||
} catch (Exception e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private Object getMinWeightTon(AcidOeeIdealCycleVo vo) {
|
||||
try {
|
||||
return vo.getClass().getMethod("getMinWeightTon").invoke(vo);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 任意日期范围异步任务(酸轧线) ========================
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.klp.da.service;
|
||||
|
||||
import com.klp.common.core.page.TableDataInfo;
|
||||
import com.klp.da.domain.bo.OeeQueryBo;
|
||||
import com.klp.pocket.acid.domain.vo.AcidOeeDailySummaryVo;
|
||||
import com.klp.pocket.acid.domain.vo.AcidOeeLoss7Vo;
|
||||
import com.klp.pocket.acid.domain.vo.Klptcm1ProStoppageVo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* OEE 聚合 Service 接口(酸轧线)
|
||||
*
|
||||
* @author klp
|
||||
* @date 2026-01-31
|
||||
*/
|
||||
public interface IDaAcidOeeService {
|
||||
|
||||
/**
|
||||
* 获取 OEE 日汇总(当月走缓存,历史查询走实时)
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @return 日汇总列表
|
||||
*/
|
||||
List<AcidOeeDailySummaryVo> getDailySummary(OeeQueryBo bo);
|
||||
|
||||
/**
|
||||
* 获取 7 大损失汇总(当月走缓存,历史查询走实时)
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @return 7 大损失列表
|
||||
*/
|
||||
List<AcidOeeLoss7Vo> getLoss7Summary(OeeQueryBo bo);
|
||||
|
||||
/**
|
||||
* 获取停机事件明细(实时分页查询)
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @return 分页后的停机事件列表
|
||||
*/
|
||||
TableDataInfo<Klptcm1ProStoppageVo> getStoppageEvents(OeeQueryBo bo);
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
package com.klp.da.service;
|
||||
|
||||
import com.klp.common.config.DeepseekConfig;
|
||||
import com.klp.common.utils.StringUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* OEE Word 导出:表格内容大模型分析(可选)
|
||||
*
|
||||
* 默认关闭,通过配置开启:
|
||||
* oee:
|
||||
* word:
|
||||
* ai:
|
||||
* enabled: true
|
||||
*
|
||||
* 大模型连接配置复用 sales.script.ai(DeepSeek):
|
||||
* sales:
|
||||
* script:
|
||||
* ai:
|
||||
* api-key/base-url/model-name/max-retries/temperature
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class OeeWordAiAnalysisService {
|
||||
|
||||
@Value("${oee.word.ai.enabled:false}")
|
||||
private boolean enabled;
|
||||
|
||||
@Qualifier("salesScriptRestTemplate")
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
private final DeepseekConfig deepseekConfig;
|
||||
|
||||
public String analyzeTable(String tableTitle, String tableMarkdown, String extraContext) {
|
||||
if (!enabled) {
|
||||
return null;
|
||||
}
|
||||
if (StringUtils.isBlank(tableMarkdown)) {
|
||||
return null;
|
||||
}
|
||||
if (deepseekConfig == null || StringUtils.isBlank(deepseekConfig.getApiKey())
|
||||
|| StringUtils.isBlank(deepseekConfig.getBaseUrl())
|
||||
|| StringUtils.isBlank(deepseekConfig.getModelName())) {
|
||||
log.warn("OEE Word AI 已开启但 sales.script.ai 配置不完整,跳过分析");
|
||||
return null;
|
||||
}
|
||||
|
||||
String prompt = buildPrompt(tableTitle, tableMarkdown, extraContext);
|
||||
return callAi(prompt);
|
||||
}
|
||||
|
||||
private String buildPrompt(String tableTitle, String tableMarkdown, String extraContext) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("你是一名制造业OEE报表分析专家。请基于下面表格数据,给出简洁的分析结论。\n");
|
||||
sb.append("输出要求:\n");
|
||||
sb.append("1) 用中文输出;\n");
|
||||
sb.append("2) 只输出 3~6 条要点(每条不超过 30 字),不要写长段落;\n");
|
||||
sb.append("3) 可以指出异常/波动/占比最高项/可能原因与建议方向;\n");
|
||||
sb.append("4) 不要复述表格标题,不要输出Markdown表格。\n\n");
|
||||
sb.append("【表格】").append(tableTitle).append("\n");
|
||||
if (StringUtils.isNotBlank(extraContext)) {
|
||||
sb.append("【上下文】").append(extraContext).append("\n");
|
||||
}
|
||||
sb.append("【数据】\n");
|
||||
sb.append(tableMarkdown);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private String callAi(String prompt) {
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("model", deepseekConfig.getModelName());
|
||||
|
||||
Map<String, String> systemMessage = new HashMap<>();
|
||||
systemMessage.put("role", "system");
|
||||
systemMessage.put("content", "你是一个严谨的数据分析助手");
|
||||
|
||||
Map<String, String> userMessage = new HashMap<>();
|
||||
userMessage.put("role", "user");
|
||||
userMessage.put("content", prompt);
|
||||
|
||||
requestBody.put("messages", Arrays.asList(systemMessage, userMessage));
|
||||
requestBody.put("temperature", deepseekConfig.getTemperature() == null ? 0.2 : deepseekConfig.getTemperature());
|
||||
requestBody.put("max_tokens", 800);
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.setBearerAuth(deepseekConfig.getApiKey());
|
||||
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
|
||||
|
||||
int retries = deepseekConfig.getMaxRetries() == null ? 1 : Math.max(1, deepseekConfig.getMaxRetries());
|
||||
for (int i = 0; i < retries; i++) {
|
||||
try {
|
||||
ResponseEntity<Map> response = restTemplate.postForEntity(
|
||||
deepseekConfig.getBaseUrl() + "/chat/completions", entity, Map.class);
|
||||
if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
|
||||
Map<String, Object> body = response.getBody();
|
||||
List<Map<String, Object>> choices = (List<Map<String, Object>>) body.get("choices");
|
||||
if (choices != null && !choices.isEmpty()) {
|
||||
Map<String, Object> choice = choices.get(0);
|
||||
Map<String, Object> message = (Map<String, Object>) choice.get("message");
|
||||
String content = message == null ? null : (String) message.get("content");
|
||||
if (StringUtils.isNotBlank(content)) {
|
||||
return content.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("OEE Word AI 调用失败,重试 {}/{}:{}", i + 1, retries, e.getMessage());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.klp.da.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.TypeReference;
|
||||
import com.github.pagehelper.PageInfo;
|
||||
import com.klp.common.core.page.TableDataInfo;
|
||||
import com.klp.common.utils.DateUtils;
|
||||
import com.klp.common.utils.StringUtils;
|
||||
import com.klp.da.domain.bo.OeeQueryBo;
|
||||
import com.klp.da.service.IDaAcidOeeService;
|
||||
import com.klp.pocket.acid.domain.vo.AcidOeeDailySummaryVo;
|
||||
import com.klp.pocket.acid.domain.vo.AcidOeeLoss7Vo;
|
||||
import com.klp.pocket.acid.domain.vo.Klptcm1ProStoppageVo;
|
||||
import com.klp.pocket.acid.service.IAcidOeeService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class DaAcidOeeServiceImpl implements IDaAcidOeeService {
|
||||
|
||||
private static final String SUMMARY_KEY_PATTERN = "oee:report:month:summary:%s:SY";
|
||||
private static final DateTimeFormatter YEAR_MONTH_FMT = DateTimeFormatter.ofPattern("yyyyMM");
|
||||
|
||||
private final IAcidOeeService acidOeeService;
|
||||
private final StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
@Override
|
||||
public List<AcidOeeDailySummaryVo> getDailySummary(OeeQueryBo bo) {
|
||||
// 检查是否查询当月,如果是,则尝试从 Redis 缓存获取
|
||||
if (isCurrentMonthQuery(bo.getStartDate(), bo.getEndDate())) {
|
||||
String yyyyMM = LocalDate.now().format(YEAR_MONTH_FMT);
|
||||
String summaryKey = String.format(SUMMARY_KEY_PATTERN, yyyyMM);
|
||||
try {
|
||||
String summaryJson = stringRedisTemplate.opsForValue().get(summaryKey);
|
||||
if (StringUtils.isNotBlank(summaryJson)) {
|
||||
log.info("[DaAcidOeeService] Hit cache for acid summary, key={}", summaryKey);
|
||||
return JSON.parseObject(summaryJson, new TypeReference<List<AcidOeeDailySummaryVo>>() {});
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[DaAcidOeeService] Failed to get acid summary from redis cache, key={}", summaryKey, e);
|
||||
}
|
||||
}
|
||||
|
||||
// 缓存未命中或查询历史数据,则实时调用 pocket service
|
||||
log.info("[DaAcidOeeService] Cache miss for acid summary, calling pocket service...");
|
||||
return acidOeeService.getDailySummary(
|
||||
DateUtils.parseDateToStr("yyyy-MM-dd", DateUtils.toDate(bo.getStartDate())),
|
||||
DateUtils.parseDateToStr("yyyy-MM-dd", DateUtils.toDate(bo.getEndDate()))
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AcidOeeLoss7Vo> getLoss7Summary(OeeQueryBo bo) {
|
||||
// 7大损失目前没有预计算,直接实时调用
|
||||
return acidOeeService.getLoss7Summary(
|
||||
DateUtils.parseDateToStr("yyyy-MM-dd", DateUtils.toDate(bo.getStartDate())),
|
||||
DateUtils.parseDateToStr("yyyy-MM-dd", DateUtils.toDate(bo.getEndDate()))
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TableDataInfo<Klptcm1ProStoppageVo> getStoppageEvents(OeeQueryBo bo) {
|
||||
// 实时分页查询
|
||||
List<Klptcm1ProStoppageVo> list = acidOeeService.getStoppageEvents(
|
||||
DateUtils.parseDateToStr("yyyy-MM-dd", DateUtils.toDate(bo.getStartDate())),
|
||||
DateUtils.parseDateToStr("yyyy-MM-dd", DateUtils.toDate(bo.getEndDate()))
|
||||
);
|
||||
return TableDataInfo.build(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断查询范围是否为当月
|
||||
*/
|
||||
private boolean isCurrentMonthQuery(LocalDate startDate, LocalDate endDate) {
|
||||
if (startDate == null || endDate == null) {
|
||||
return false;
|
||||
}
|
||||
LocalDate now = LocalDate.now();
|
||||
LocalDate firstDayOfMonth = now.withDayOfMonth(1);
|
||||
return !startDate.isBefore(firstDayOfMonth) && !endDate.isAfter(now);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.klp.da.task;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.klp.pocket.acid.domain.vo.AcidOeeDailySummaryVo;
|
||||
import com.klp.pocket.acid.domain.vo.AcidOeeLoss7Vo;
|
||||
import com.klp.pocket.acid.service.IAcidOeeService;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -25,10 +24,9 @@ import java.util.concurrent.TimeUnit;
|
||||
* - 项目启动完成后即计算当月 OEE 聚合结果并写入 Redis;
|
||||
* - 每天凌晨 04:00 重新计算当月数据并覆盖缓存。
|
||||
*
|
||||
* 当前仅实现酸轧线(SY)的当月日汇总 & 7 大损失预计算;
|
||||
* 当前仅实现酸轧线(SY)的当月日汇总预计算;
|
||||
* key 约定:
|
||||
* - 汇总结果:oee:report:month:summary:{yyyyMM}:SY
|
||||
* - 7 大损失:oee:report:month:loss7:{yyyyMM}:SY
|
||||
* - 元信息: oee:report:month:meta:{yyyyMM}:SY
|
||||
*/
|
||||
@Slf4j
|
||||
@@ -39,9 +37,6 @@ public class AcidOeeMonthTask {
|
||||
/** Redis 缓存 key 模板:当月 OEE 汇总(酸轧线) */
|
||||
private static final String SUMMARY_KEY_PATTERN = "oee:report:month:summary:%s:SY";
|
||||
|
||||
/** Redis 缓存 key 模板:当月 7 大损失(酸轧线) */
|
||||
private static final String LOSS7_KEY_PATTERN = "oee:report:month:loss7:%s:SY";
|
||||
|
||||
/** Redis 缓存 key 模板:当月元信息(酸轧线) */
|
||||
private static final String META_KEY_PATTERN = "oee:report:month:meta:%s:SY";
|
||||
|
||||
@@ -96,20 +91,14 @@ public class AcidOeeMonthTask {
|
||||
log.info("[AcidOeeMonthTask] trigger={}, computing acid OEE month summary for {} ({} ~ {})",
|
||||
trigger, yyyyMM, startStr, endStr);
|
||||
|
||||
// 1. 调用 pocket 的 AcidOeeService 获取当月日汇总 & 7 大损失
|
||||
// 1. 调用 pocket 的 AcidOeeService 获取当月日汇总
|
||||
List<AcidOeeDailySummaryVo> dailySummaryList = acidOeeService.getDailySummary(startStr, endStr);
|
||||
List<AcidOeeLoss7Vo> loss7List = acidOeeService.getLoss7Summary(startStr, endStr);
|
||||
|
||||
// 2. 写入 Redis(summary)
|
||||
String summaryKey = String.format(SUMMARY_KEY_PATTERN, yyyyMM);
|
||||
String summaryJson = JSON.toJSONString(dailySummaryList);
|
||||
stringRedisTemplate.opsForValue().set(summaryKey, summaryJson, 1, TimeUnit.DAYS);
|
||||
|
||||
// 2.1 写入 Redis(loss7)
|
||||
String loss7Key = String.format(LOSS7_KEY_PATTERN, yyyyMM);
|
||||
String loss7Json = JSON.toJSONString(loss7List);
|
||||
stringRedisTemplate.opsForValue().set(loss7Key, loss7Json, 1, TimeUnit.DAYS);
|
||||
|
||||
long durationMs = (System.nanoTime() - startNs) / 1_000_000L;
|
||||
|
||||
// 3. 写入 Redis(meta)
|
||||
@@ -123,8 +112,8 @@ public class AcidOeeMonthTask {
|
||||
String metaKey = String.format(META_KEY_PATTERN, yyyyMM);
|
||||
stringRedisTemplate.opsForValue().set(metaKey, JSON.toJSONString(meta), 1, TimeUnit.DAYS);
|
||||
|
||||
log.info("[AcidOeeMonthTask] compute finish for {} dailySize={}, loss7Size={}, durationMs={}ms, summaryKey={}",
|
||||
yyyyMM, dailySummaryList.size(), loss7List.size(), durationMs, summaryKey);
|
||||
log.info("[AcidOeeMonthTask] compute finish for {} dailySize={}, durationMs={}ms, summaryKey={}",
|
||||
yyyyMM, dailySummaryList.size(), durationMs, summaryKey);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.klp.pocket.acid.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 酸轧线理论节拍(统计口径)视图对象
|
||||
* 通过历史“优良日”统计得到,替代不稳定的回归结果。
|
||||
*
|
||||
* @author klp
|
||||
* @date 2026-02-03
|
||||
*/
|
||||
@Data
|
||||
public class AcidOeeIdealCycleVo {
|
||||
|
||||
/** 产线ID(固定为 SY) */
|
||||
private String lineId;
|
||||
|
||||
/** 产线名称(酸轧线) */
|
||||
private String lineName;
|
||||
|
||||
/** 本次计算使用的数据区间(yyyy-MM-dd) */
|
||||
private String startDate;
|
||||
private String endDate;
|
||||
|
||||
/** 理论节拍(min/吨)- 固定值0.47 */
|
||||
private BigDecimal idealCycleTimeMinPerTon;
|
||||
|
||||
/** 中位数理论节拍(min/吨)- 从卷级节拍计算的中位数 */
|
||||
private BigDecimal medianCycleTimeMinPerTon;
|
||||
|
||||
/** 参与统计的"优良日"天数 */
|
||||
private Integer sampleDays;
|
||||
|
||||
/** 统计口径:吨数下限(吨) */
|
||||
private BigDecimal minWeightTon;
|
||||
|
||||
/** 统计口径:停机占比上限(0~1) */
|
||||
private BigDecimal maxDowntimeRate;
|
||||
|
||||
/** 日粒度对比点:理论耗时 vs 实际运转时间 */
|
||||
private List<DailyComparePointVo> dailyComparePoints;
|
||||
|
||||
@Data
|
||||
public static class DailyComparePointVo {
|
||||
private String statDate;
|
||||
private Long actualRunTimeMin;
|
||||
private BigDecimal theoreticalTimeMin;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
package com.klp.pocket.acid.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 酸轧线OEE回归数据视图对象
|
||||
* 用于理论节拍计算和前端散点图展示
|
||||
*
|
||||
* @author klp
|
||||
* @date 2026-01-30
|
||||
*/
|
||||
@Data
|
||||
public class AcidOeeRegressionVo {
|
||||
|
||||
/** 产线ID(固定为 SY) */
|
||||
private String lineId;
|
||||
|
||||
/** 产线名称(酸轧线) */
|
||||
private String lineName;
|
||||
|
||||
/** 回归斜率:分钟/吨(核心值,可作为理论节拍) */
|
||||
private BigDecimal slopeMinPerTon;
|
||||
|
||||
/** 回归斜率:分钟/卷(核心值,可作为理论节拍) */
|
||||
private BigDecimal slopeMinPerCoil;
|
||||
|
||||
/** 截距(分钟) */
|
||||
private BigDecimal interceptMin;
|
||||
|
||||
/** 拟合优度(R²) */
|
||||
private BigDecimal r2;
|
||||
|
||||
/** 参与回归样本数 */
|
||||
private Integer sampleCount;
|
||||
|
||||
/** 回归数据开始时间 */
|
||||
private String startTime;
|
||||
|
||||
/** 回归数据结束时间 */
|
||||
private String endTime;
|
||||
|
||||
/** 散点列表 */
|
||||
private List<RegressionPointVo> points;
|
||||
|
||||
/** 拟合线两个端点(前端可直接画线) */
|
||||
private List<RegressionLinePointVo> linePoints;
|
||||
|
||||
/**
|
||||
* 散点数据
|
||||
*/
|
||||
@Data
|
||||
public static class RegressionPointVo {
|
||||
/** 重量(吨,X轴) */
|
||||
private BigDecimal weightTon;
|
||||
/** 卷数(X轴) */
|
||||
private Long coilCount;
|
||||
/** 时长(分钟,Y轴) */
|
||||
private Long durationMin;
|
||||
/** 关联的actionId(可选) */
|
||||
private String actionId;
|
||||
/** 创建时间 */
|
||||
private String createTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 拟合线端点
|
||||
*/
|
||||
@Data
|
||||
public static class RegressionLinePointVo {
|
||||
/** 重量(吨,X轴) */
|
||||
private BigDecimal weightTon;
|
||||
/** 卷数(X轴) */
|
||||
private Long coilCount;
|
||||
/** 时长(分钟,Y轴) */
|
||||
private BigDecimal durationMin;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.klp.pocket.acid.mapper;
|
||||
|
||||
import com.klp.pocket.acid.domain.vo.AcidOeeDailySummaryVo;
|
||||
import com.klp.pocket.acid.domain.vo.AcidOeeRegressionVo;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
@@ -27,17 +26,6 @@ public interface AcidOeeMapper {
|
||||
List<AcidOeeDailySummaryVo> selectDailySummary(@Param("startDate") String startDate,
|
||||
@Param("endDate") String endDate);
|
||||
|
||||
/**
|
||||
* 查询回归数据散点(用于理论节拍计算)
|
||||
* 返回:重量(吨)、卷数、时长(分钟)等
|
||||
*
|
||||
* @param startDate 开始日期(yyyy-MM-dd,可选)
|
||||
* @param endDate 结束日期(yyyy-MM-dd,可选)
|
||||
* @return 散点列表
|
||||
*/
|
||||
List<AcidOeeRegressionVo.RegressionPointVo> selectRegressionPoints(@Param("startDate") String startDate,
|
||||
@Param("endDate") String endDate);
|
||||
|
||||
/**
|
||||
* 查询每日的钢卷号和重量(用于良品/次品判定)
|
||||
*
|
||||
@@ -48,6 +36,16 @@ public interface AcidOeeMapper {
|
||||
List<CoilInfoByDate> selectCoilInfoByDate(@Param("startDate") String startDate,
|
||||
@Param("endDate") String endDate);
|
||||
|
||||
/**
|
||||
* 查询卷级生产节拍(min/吨),用于理论节拍计算。
|
||||
*
|
||||
* @param startDate 开始日期(yyyy-MM-dd)
|
||||
* @param endDate 结束日期(yyyy-MM-dd)
|
||||
* @return 每卷的生产节拍列表(min/吨)
|
||||
*/
|
||||
List<java.math.BigDecimal> selectCoilCycleMinPerTon(@Param("startDate") String startDate,
|
||||
@Param("endDate") String endDate);
|
||||
|
||||
/**
|
||||
* 卷号信息内部类(用于Mapper返回)
|
||||
*/
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.klp.pocket.acid.service;
|
||||
|
||||
import com.klp.pocket.acid.domain.vo.AcidOeeDailySummaryVo;
|
||||
import com.klp.pocket.acid.domain.vo.AcidOeeIdealCycleVo;
|
||||
import com.klp.pocket.acid.domain.vo.AcidOeeLoss7Vo;
|
||||
import com.klp.pocket.acid.domain.vo.AcidOeeRegressionVo;
|
||||
import com.klp.pocket.acid.domain.vo.Klptcm1ProStoppageVo;
|
||||
|
||||
import java.util.List;
|
||||
@@ -34,16 +34,6 @@ public interface IAcidOeeService {
|
||||
*/
|
||||
List<Klptcm1ProStoppageVo> getStoppageEvents(String startDate, String endDate);
|
||||
|
||||
/**
|
||||
* 查询理论节拍回归数据(吨和卷两个维度)
|
||||
* 用于性能稼动率计算和前端散点图展示
|
||||
*
|
||||
* @param startDate 开始日期(yyyy-MM-dd,可选,默认近6个月)
|
||||
* @param endDate 结束日期(yyyy-MM-dd,可选)
|
||||
* @return 回归数据(包含斜率、截距、散点等)
|
||||
*/
|
||||
AcidOeeRegressionVo getRegressionData(String startDate, String endDate);
|
||||
|
||||
/**
|
||||
* 查询7大损失汇总(按日期范围)
|
||||
*
|
||||
@@ -52,5 +42,14 @@ public interface IAcidOeeService {
|
||||
* @return 7大损失汇总列表
|
||||
*/
|
||||
List<AcidOeeLoss7Vo> getLoss7Summary(String startDate, String endDate);
|
||||
|
||||
/**
|
||||
* 查询理论节拍(统计口径:历史优良日中位数)
|
||||
*
|
||||
* @param startDate 开始日期(yyyy-MM-dd)
|
||||
* @param endDate 结束日期(yyyy-MM-dd)
|
||||
* @return 理论节拍与对比数据
|
||||
*/
|
||||
AcidOeeIdealCycleVo getIdealCycle(String startDate, String endDate);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ package com.klp.pocket.acid.service.impl;
|
||||
import com.baomidou.dynamic.datasource.annotation.DS;
|
||||
import com.klp.common.utils.StringUtils;
|
||||
import com.klp.pocket.acid.domain.vo.AcidOeeDailySummaryVo;
|
||||
import com.klp.pocket.acid.domain.vo.AcidOeeIdealCycleVo;
|
||||
import com.klp.pocket.acid.domain.vo.AcidOeeLoss7Vo;
|
||||
import com.klp.pocket.acid.domain.vo.AcidOeeRegressionVo;
|
||||
import com.klp.pocket.acid.domain.vo.Klptcm1ProStoppageVo;
|
||||
import com.klp.pocket.acid.domain.bo.Klptcm1ProStoppageBo;
|
||||
import com.klp.pocket.acid.mapper.AcidOeeMapper;
|
||||
@@ -19,7 +19,7 @@ import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.Calendar;
|
||||
|
||||
/**
|
||||
* 酸轧线OEE Service实现类
|
||||
@@ -35,7 +35,8 @@ public class AcidOeeServiceImpl implements IAcidOeeService {
|
||||
|
||||
/** 酸轧成品库库区ID */
|
||||
private static final Long ACID_FINISHED_WAREHOUSE_ID = 1988150099140866050L;
|
||||
|
||||
/** 固定理论节拍(min/吨) */
|
||||
private static final BigDecimal FIXED_IDEAL_CYCLE = BigDecimal.valueOf(0.47);
|
||||
private final AcidOeeMapper acidOeeMapper;
|
||||
private final IKlptcm1ProStoppageService stoppageService;
|
||||
private final ICoilQualityJudgeService coilQualityJudgeService;
|
||||
@@ -55,7 +56,10 @@ public class AcidOeeServiceImpl implements IAcidOeeService {
|
||||
// 3. 查询产量明细,用于良品/次品判定
|
||||
Map<String, List<CoilInfo>> coilInfoByDate = getCoilNosByDate(startDate, endDate);
|
||||
|
||||
// 4. 填充每个日汇总的完整数据
|
||||
// 4. 理论节拍:使用固定值0.47
|
||||
BigDecimal idealCycleTon = FIXED_IDEAL_CYCLE;
|
||||
|
||||
// 5. 填充每个日汇总的完整数据
|
||||
for (AcidOeeDailySummaryVo summary : summaries) {
|
||||
String statDate = summary.getStatDate();
|
||||
summary.setLineId("SY");
|
||||
@@ -70,6 +74,11 @@ public class AcidOeeServiceImpl implements IAcidOeeService {
|
||||
Long runTime = Math.max(0, loadingTime - downtime);
|
||||
summary.setRunTimeMin(runTime);
|
||||
|
||||
// 理论节拍:若尚未填充,则统一使用“优良日统计”得到的节拍
|
||||
if (summary.getIdealCycleTimeMinPerTon() == null && idealCycleTon != null) {
|
||||
summary.setIdealCycleTimeMinPerTon(idealCycleTon);
|
||||
}
|
||||
|
||||
// 良品/次品判定(通过WMS)
|
||||
if (coilInfoByDate.containsKey(statDate)) {
|
||||
List<CoilInfo> coilInfos = coilInfoByDate.get(statDate);
|
||||
@@ -101,57 +110,84 @@ public class AcidOeeServiceImpl implements IAcidOeeService {
|
||||
return stoppageService.queryList(bo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询停机事件(可选:是否包含短停机 <5min)
|
||||
*/
|
||||
private List<Klptcm1ProStoppageVo> getStoppageEvents(String startDate, String endDate, boolean includeShortDuration) {
|
||||
Klptcm1ProStoppageBo bo = new Klptcm1ProStoppageBo();
|
||||
bo.setStartDate(parseDate(startDate));
|
||||
bo.setEndDate(parseDate(endDate));
|
||||
if (includeShortDuration) {
|
||||
// BaseEntity.params 用于透传查询开关
|
||||
bo.getParams().put("includeShortDuration", true);
|
||||
}
|
||||
return stoppageService.queryList(bo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AcidOeeRegressionVo getRegressionData(String startDate, String endDate) {
|
||||
// 1. 查询散点数据
|
||||
List<AcidOeeRegressionVo.RegressionPointVo> points = acidOeeMapper.selectRegressionPoints(startDate, endDate);
|
||||
public AcidOeeIdealCycleVo getIdealCycle(String startDate, String endDate) {
|
||||
// 1) 取基础日汇总(产量、负荷时间等)
|
||||
List<AcidOeeDailySummaryVo> daily = acidOeeMapper.selectDailySummary(startDate, endDate);
|
||||
AcidOeeIdealCycleVo rsp = new AcidOeeIdealCycleVo();
|
||||
rsp.setLineId("SY");
|
||||
rsp.setLineName("酸轧线");
|
||||
rsp.setStartDate(startDate);
|
||||
rsp.setEndDate(endDate);
|
||||
// 这里的 minWeightTon / maxDowntimeRate 字段暂不使用,可保留为前端说明字段
|
||||
|
||||
AcidOeeRegressionVo result = new AcidOeeRegressionVo();
|
||||
result.setLineId("SY");
|
||||
result.setLineName("酸轧线");
|
||||
result.setStartTime(startDate);
|
||||
result.setEndTime(endDate);
|
||||
result.setPoints(points);
|
||||
result.setSampleCount(points != null ? points.size() : 0);
|
||||
|
||||
if (points == null || points.isEmpty()) {
|
||||
// 没有数据时返回空结果
|
||||
return result;
|
||||
if (daily == null || daily.isEmpty()) {
|
||||
rsp.setIdealCycleTimeMinPerTon(null);
|
||||
rsp.setSampleDays(0);
|
||||
rsp.setDailyComparePoints(Collections.emptyList());
|
||||
return rsp;
|
||||
}
|
||||
|
||||
// 2. 计算回归(吨维度)
|
||||
RegressionResult tonResult = calculateRegression(
|
||||
points.stream().map(AcidOeeRegressionVo.RegressionPointVo::getWeightTon).filter(Objects::nonNull).collect(Collectors.toList()),
|
||||
points.stream().map(AcidOeeRegressionVo.RegressionPointVo::getDurationMin).filter(Objects::nonNull).collect(Collectors.toList())
|
||||
);
|
||||
if (tonResult != null) {
|
||||
result.setSlopeMinPerTon(tonResult.slope);
|
||||
result.setInterceptMin(tonResult.intercept);
|
||||
result.setR2(tonResult.r2);
|
||||
// 2) 聚合停机,补齐 runTime
|
||||
Map<String, Long> downtimeByDate = aggregateDowntimeByDate(startDate, endDate);
|
||||
for (AcidOeeDailySummaryVo d : daily) {
|
||||
Long downtime = downtimeByDate.getOrDefault(d.getStatDate(), 0L);
|
||||
d.setDowntimeMin(downtime);
|
||||
Long loading = d.getLoadingTimeMin() != null ? d.getLoadingTimeMin() : 0L;
|
||||
d.setRunTimeMin(Math.max(0, loading - downtime));
|
||||
}
|
||||
|
||||
// 3. 计算回归(卷维度)
|
||||
RegressionResult coilResult = calculateRegression(
|
||||
points.stream().map(p -> p.getCoilCount() != null ? BigDecimal.valueOf(p.getCoilCount()) : null).filter(Objects::nonNull).collect(Collectors.toList()),
|
||||
points.stream().map(AcidOeeRegressionVo.RegressionPointVo::getDurationMin).filter(Objects::nonNull).collect(Collectors.toList())
|
||||
);
|
||||
if (coilResult != null) {
|
||||
result.setSlopeMinPerCoil(coilResult.slope);
|
||||
}
|
||||
// 3) 卷级节拍 = (END_DATE - START_DATE)/出口重量,计算中位数(用于展示,不用于OEE计算)
|
||||
List<BigDecimal> coilCycles = acidOeeMapper.selectCoilCycleMinPerTon(startDate, endDate);
|
||||
coilCycles.removeIf(c -> c == null || c.compareTo(BigDecimal.ZERO) <= 0);
|
||||
coilCycles.sort(BigDecimal::compareTo);
|
||||
BigDecimal medianCycle = median(coilCycles);
|
||||
|
||||
// 理论节拍使用固定值0.47(用于OEE计算)
|
||||
rsp.setIdealCycleTimeMinPerTon(FIXED_IDEAL_CYCLE);
|
||||
// 中位数理论节拍(用于展示)
|
||||
rsp.setMedianCycleTimeMinPerTon(medianCycle);
|
||||
// 样本天数:当前查询区间内有产量的自然日数量(与传入的日期范围一一对应)
|
||||
rsp.setSampleDays(daily.size());
|
||||
|
||||
// 4. 生成拟合线端点(用于前端画线)
|
||||
if (tonResult != null && !points.isEmpty()) {
|
||||
List<AcidOeeRegressionVo.RegressionLinePointVo> linePoints = generateLinePoints(points, tonResult);
|
||||
result.setLinePoints(linePoints);
|
||||
// 4) 日粒度对比数据:理论耗时 vs 实际运转时间(用于前端展示"有效性")
|
||||
// 使用固定值0.47计算理论耗时
|
||||
List<AcidOeeIdealCycleVo.DailyComparePointVo> compare = new ArrayList<>();
|
||||
if (FIXED_IDEAL_CYCLE != null) {
|
||||
for (AcidOeeDailySummaryVo d : daily) {
|
||||
BigDecimal ton = d.getTotalOutputTon();
|
||||
Long run = d.getRunTimeMin();
|
||||
if (ton == null || run == null) continue;
|
||||
AcidOeeIdealCycleVo.DailyComparePointVo p = new AcidOeeIdealCycleVo.DailyComparePointVo();
|
||||
p.setStatDate(d.getStatDate());
|
||||
p.setActualRunTimeMin(run);
|
||||
p.setTheoreticalTimeMin(FIXED_IDEAL_CYCLE.multiply(ton));
|
||||
compare.add(p);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
rsp.setDailyComparePoints(compare);
|
||||
return rsp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AcidOeeLoss7Vo> getLoss7Summary(String startDate, String endDate) {
|
||||
// 1. 查询停机事件(含 stopType、duration 等)
|
||||
List<Klptcm1ProStoppageVo> events = getStoppageEvents(startDate, endDate);
|
||||
// 损失统计也建议包含短停机,避免损失时间与停机总时间口径不一致
|
||||
List<Klptcm1ProStoppageVo> events = getStoppageEvents(startDate, endDate, true);
|
||||
if (events == null || events.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
@@ -162,9 +198,9 @@ public class AcidOeeServiceImpl implements IAcidOeeService {
|
||||
|
||||
for (Klptcm1ProStoppageVo event : events) {
|
||||
String stopType = event.getStopType();
|
||||
// stopType 为空时归入“未分类”,避免因为未录入原因导致损失时间被漏算
|
||||
if (StringUtils.isBlank(stopType)) {
|
||||
// 没有类型的记录暂时忽略
|
||||
continue;
|
||||
stopType = "未分类";
|
||||
}
|
||||
Long durationSec = event.getDuration();
|
||||
if (durationSec == null || durationSec <= 0) {
|
||||
@@ -225,18 +261,64 @@ public class AcidOeeServiceImpl implements IAcidOeeService {
|
||||
|
||||
/**
|
||||
* 按日期聚合停机时间
|
||||
* 修复:如果停机事件跨天,需要按实际跨天的分钟数分配到对应的日期
|
||||
*/
|
||||
private Map<String, Long> aggregateDowntimeByDate(String startDate, String endDate) {
|
||||
List<Klptcm1ProStoppageVo> events = getStoppageEvents(startDate, endDate);
|
||||
// 性能稼动率/运转时间口径:停机时间需要包含短停机(<5min),否则 runTime 被高估
|
||||
List<Klptcm1ProStoppageVo> events = getStoppageEvents(startDate, endDate, true);
|
||||
Map<String, Long> downtimeMap = new HashMap<>();
|
||||
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
|
||||
Calendar cal = Calendar.getInstance();
|
||||
|
||||
for (Klptcm1ProStoppageVo event : events) {
|
||||
if (event.getStartDate() != null && event.getDuration() != null) {
|
||||
String date = dateFormat.format(event.getStartDate());
|
||||
// duration单位是秒,转换为分钟
|
||||
Long minutes = event.getDuration() / 60;
|
||||
downtimeMap.merge(date, minutes, Long::sum);
|
||||
if (event.getStartDate() == null || event.getDuration() == null || event.getDuration() <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Date eventStart = event.getStartDate();
|
||||
long durationSec = event.getDuration();
|
||||
long durationMin = (durationSec + 59) / 60; // 向上取整,避免丢失秒数
|
||||
|
||||
// 计算停机结束时间
|
||||
cal.setTime(eventStart);
|
||||
cal.add(Calendar.SECOND, (int) durationSec);
|
||||
Date eventEnd = cal.getTime();
|
||||
|
||||
// 如果停机事件在同一天,直接累加
|
||||
String startDateStr = dateFormat.format(eventStart);
|
||||
String endDateStr = dateFormat.format(eventEnd);
|
||||
|
||||
if (startDateStr.equals(endDateStr)) {
|
||||
// 同一天,直接累加
|
||||
downtimeMap.merge(startDateStr, durationMin, Long::sum);
|
||||
} else {
|
||||
// 跨天:按实际跨天的分钟数分配到对应的日期
|
||||
cal.setTime(eventStart);
|
||||
cal.set(Calendar.HOUR_OF_DAY, 0);
|
||||
cal.set(Calendar.MINUTE, 0);
|
||||
cal.set(Calendar.SECOND, 0);
|
||||
cal.set(Calendar.MILLISECOND, 0);
|
||||
Date dayStart = cal.getTime();
|
||||
|
||||
Date currentDayStart = dayStart;
|
||||
|
||||
while (currentDayStart.before(eventEnd)) {
|
||||
cal.setTime(currentDayStart);
|
||||
cal.add(Calendar.DAY_OF_MONTH, 1);
|
||||
Date nextDayStart = cal.getTime();
|
||||
|
||||
// 计算当前天的停机分钟数
|
||||
Date dayEnd = nextDayStart.before(eventEnd) ? nextDayStart : eventEnd;
|
||||
long dayMinutes = Math.max(0, (dayEnd.getTime() - Math.max(currentDayStart.getTime(), eventStart.getTime())) / (1000 * 60));
|
||||
|
||||
if (dayMinutes > 0) {
|
||||
String dateKey = dateFormat.format(currentDayStart);
|
||||
downtimeMap.merge(dateKey, dayMinutes, Long::sum);
|
||||
}
|
||||
|
||||
currentDayStart = nextDayStart;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,7 +409,11 @@ public class AcidOeeServiceImpl implements IAcidOeeService {
|
||||
Long runTime = summary.getRunTimeMin() != null ? summary.getRunTimeMin() : 0L;
|
||||
BigDecimal idealCycleTon = summary.getIdealCycleTimeMinPerTon();
|
||||
BigDecimal totalOutputTon = summary.getTotalOutputTon();
|
||||
if (runTime > 0 && idealCycleTon != null && totalOutputTon != null && totalOutputTon.compareTo(BigDecimal.ZERO) > 0) {
|
||||
if (runTime > 0
|
||||
&& idealCycleTon != null
|
||||
&& idealCycleTon.compareTo(BigDecimal.ZERO) > 0
|
||||
&& totalOutputTon != null
|
||||
&& totalOutputTon.compareTo(BigDecimal.ZERO) > 0) {
|
||||
BigDecimal idealTime = idealCycleTon.multiply(totalOutputTon);
|
||||
BigDecimal performanceTon = idealTime.divide(BigDecimal.valueOf(runTime), 4, RoundingMode.HALF_UP)
|
||||
.multiply(BigDecimal.valueOf(100));
|
||||
@@ -337,7 +423,10 @@ public class AcidOeeServiceImpl implements IAcidOeeService {
|
||||
// 性能稼动率(卷维度)
|
||||
Long totalOutputCoil = summary.getTotalOutputCoil() != null ? summary.getTotalOutputCoil() : 0L;
|
||||
BigDecimal idealCycleCoil = summary.getIdealCycleTimeMinPerCoil();
|
||||
if (runTime > 0 && idealCycleCoil != null && totalOutputCoil > 0) {
|
||||
if (runTime > 0
|
||||
&& idealCycleCoil != null
|
||||
&& idealCycleCoil.compareTo(BigDecimal.ZERO) > 0
|
||||
&& totalOutputCoil > 0) {
|
||||
BigDecimal idealTime = idealCycleCoil.multiply(BigDecimal.valueOf(totalOutputCoil));
|
||||
BigDecimal performanceCoil = idealTime.divide(BigDecimal.valueOf(runTime), 4, RoundingMode.HALF_UP)
|
||||
.multiply(BigDecimal.valueOf(100));
|
||||
@@ -363,102 +452,15 @@ public class AcidOeeServiceImpl implements IAcidOeeService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算线性回归(最小二乘法)
|
||||
*/
|
||||
private RegressionResult calculateRegression(List<BigDecimal> xValues, List<Long> yValues) {
|
||||
if (xValues.size() != yValues.size() || xValues.isEmpty()) {
|
||||
return null;
|
||||
private BigDecimal median(List<BigDecimal> values) {
|
||||
if (values == null || values.isEmpty()) return null;
|
||||
int n = values.size();
|
||||
if (n % 2 == 1) {
|
||||
return values.get(n / 2);
|
||||
}
|
||||
|
||||
int n = xValues.size();
|
||||
BigDecimal sumX = BigDecimal.ZERO;
|
||||
BigDecimal sumY = BigDecimal.ZERO;
|
||||
BigDecimal sumXY = BigDecimal.ZERO;
|
||||
BigDecimal sumX2 = BigDecimal.ZERO;
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
BigDecimal x = xValues.get(i);
|
||||
BigDecimal y = BigDecimal.valueOf(yValues.get(i));
|
||||
sumX = sumX.add(x);
|
||||
sumY = sumY.add(y);
|
||||
sumXY = sumXY.add(x.multiply(y));
|
||||
sumX2 = sumX2.add(x.multiply(x));
|
||||
}
|
||||
|
||||
BigDecimal nDecimal = BigDecimal.valueOf(n);
|
||||
BigDecimal denominator = nDecimal.multiply(sumX2).subtract(sumX.multiply(sumX));
|
||||
if (denominator.compareTo(BigDecimal.ZERO) == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// slope = (n*ΣXY - ΣX*ΣY) / (n*ΣX² - (ΣX)²)
|
||||
BigDecimal slope = nDecimal.multiply(sumXY).subtract(sumX.multiply(sumY))
|
||||
.divide(denominator, 6, RoundingMode.HALF_UP);
|
||||
|
||||
// intercept = (ΣY - slope*ΣX) / n
|
||||
BigDecimal intercept = sumY.subtract(slope.multiply(sumX))
|
||||
.divide(nDecimal, 6, RoundingMode.HALF_UP);
|
||||
|
||||
// 计算R²
|
||||
BigDecimal meanY = sumY.divide(nDecimal, 6, RoundingMode.HALF_UP);
|
||||
BigDecimal ssTotal = BigDecimal.ZERO;
|
||||
BigDecimal ssResidual = BigDecimal.ZERO;
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
BigDecimal x = xValues.get(i);
|
||||
BigDecimal y = BigDecimal.valueOf(yValues.get(i));
|
||||
BigDecimal predictedY = slope.multiply(x).add(intercept);
|
||||
BigDecimal diff = y.subtract(meanY);
|
||||
ssTotal = ssTotal.add(diff.multiply(diff));
|
||||
BigDecimal residual = y.subtract(predictedY);
|
||||
ssResidual = ssResidual.add(residual.multiply(residual));
|
||||
}
|
||||
|
||||
BigDecimal r2 = BigDecimal.ONE;
|
||||
if (ssTotal.compareTo(BigDecimal.ZERO) > 0) {
|
||||
r2 = BigDecimal.ONE.subtract(ssResidual.divide(ssTotal, 6, RoundingMode.HALF_UP));
|
||||
}
|
||||
|
||||
return new RegressionResult(slope, intercept, r2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成拟合线端点
|
||||
*/
|
||||
private List<AcidOeeRegressionVo.RegressionLinePointVo> generateLinePoints(
|
||||
List<AcidOeeRegressionVo.RegressionPointVo> points,
|
||||
RegressionResult result) {
|
||||
if (points.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// 找到X轴的最小值和最大值
|
||||
BigDecimal minX = points.stream()
|
||||
.map(AcidOeeRegressionVo.RegressionPointVo::getWeightTon)
|
||||
.filter(Objects::nonNull)
|
||||
.min(BigDecimal::compareTo)
|
||||
.orElse(BigDecimal.ZERO);
|
||||
|
||||
BigDecimal maxX = points.stream()
|
||||
.map(AcidOeeRegressionVo.RegressionPointVo::getWeightTon)
|
||||
.filter(Objects::nonNull)
|
||||
.max(BigDecimal::compareTo)
|
||||
.orElse(BigDecimal.ZERO);
|
||||
|
||||
// 计算对应的Y值
|
||||
BigDecimal y1 = result.slope.multiply(minX).add(result.intercept);
|
||||
BigDecimal y2 = result.slope.multiply(maxX).add(result.intercept);
|
||||
|
||||
AcidOeeRegressionVo.RegressionLinePointVo p1 = new AcidOeeRegressionVo.RegressionLinePointVo();
|
||||
p1.setWeightTon(minX);
|
||||
p1.setDurationMin(y1);
|
||||
|
||||
AcidOeeRegressionVo.RegressionLinePointVo p2 = new AcidOeeRegressionVo.RegressionLinePointVo();
|
||||
p2.setWeightTon(maxX);
|
||||
p2.setDurationMin(y2);
|
||||
|
||||
return Arrays.asList(p1, p2);
|
||||
BigDecimal a = values.get(n / 2 - 1);
|
||||
BigDecimal b = values.get(n / 2);
|
||||
return a.add(b).divide(BigDecimal.valueOf(2), 6, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -477,20 +479,7 @@ public class AcidOeeServiceImpl implements IAcidOeeService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 回归结果内部类
|
||||
*/
|
||||
private static class RegressionResult {
|
||||
final BigDecimal slope;
|
||||
final BigDecimal intercept;
|
||||
final BigDecimal r2;
|
||||
|
||||
RegressionResult(BigDecimal slope, BigDecimal intercept, BigDecimal r2) {
|
||||
this.slope = slope;
|
||||
this.intercept = intercept;
|
||||
this.r2 = r2;
|
||||
}
|
||||
}
|
||||
// 回归相关逻辑已下线:理论节拍统一由“优良日统计口径”产生
|
||||
|
||||
/**
|
||||
* 内部统计类:某一 stopType 的总损失时间与次数
|
||||
|
||||
@@ -89,8 +89,12 @@ public class Klptcm1ProStoppageServiceImpl implements IKlptcm1ProStoppageService
|
||||
lqw.le(Klptcm1ProStoppage::getStartDate, endDateWithTime);
|
||||
}
|
||||
lqw.eq(bo.getDURATION() != null, Klptcm1ProStoppage::getDuration, bo.getDURATION());
|
||||
// 只查询持续时间大于等于5分钟(300秒)的停机记录
|
||||
lqw.ge(Klptcm1ProStoppage::getDuration, 300);
|
||||
// 默认只统计持续时间大于等于5分钟(300秒)的停机记录;
|
||||
// 若 params.includeShortDuration=true,则放开短停机,用于 OEE 性能稼动率的“完整停机时间”统计。
|
||||
boolean includeShort = params != null && Boolean.TRUE.equals(params.get("includeShortDuration"));
|
||||
if (!includeShort) {
|
||||
lqw.ge(Klptcm1ProStoppage::getDuration, 300);
|
||||
}
|
||||
lqw.eq(bo.getInsDate() != null, Klptcm1ProStoppage::getInsDate, bo.getInsDate());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getStopType()), Klptcm1ProStoppage::getStopType, bo.getStopType());
|
||||
//倒序
|
||||
|
||||
@@ -54,51 +54,6 @@
|
||||
ORDER BY stat_date ASC
|
||||
</select>
|
||||
|
||||
<!-- 回归数据散点结果映射 -->
|
||||
<resultMap id="RegressionPointResultMap" type="com.klp.pocket.acid.domain.vo.AcidOeeRegressionVo$RegressionPointVo">
|
||||
<result column="weight_ton" property="weightTon" jdbcType="DECIMAL"/>
|
||||
<result column="coil_count" property="coilCount" jdbcType="BIGINT"/>
|
||||
<result column="duration_min" property="durationMin" jdbcType="BIGINT"/>
|
||||
<result column="action_id" property="actionId" jdbcType="VARCHAR"/>
|
||||
<result column="create_time" property="createTime" jdbcType="VARCHAR"/>
|
||||
</resultMap>
|
||||
|
||||
<!-- 查询回归数据散点 -->
|
||||
<select id="selectRegressionPoints" resultMap="RegressionPointResultMap">
|
||||
SELECT
|
||||
-- 重量(吨):出口重量
|
||||
e.EXIT_WEIGHT AS weight_ton,
|
||||
-- 卷数:固定为1(每条记录代表一卷)
|
||||
1 AS coil_count,
|
||||
-- 时长(分钟):结束时间 - 开始时间,转换为分钟
|
||||
CASE
|
||||
WHEN e.START_DATE IS NOT NULL AND e.END_DATE IS NOT NULL THEN
|
||||
TIMESTAMPDIFF(MINUTE, e.START_DATE, e.END_DATE)
|
||||
ELSE NULL
|
||||
END AS duration_min,
|
||||
-- 关联ID:使用卷号作为标识
|
||||
e.ENCOILID AS action_id,
|
||||
-- 创建时间
|
||||
DATE_FORMAT(e.INSDATE, '%Y-%m-%d %H:%i:%s') AS create_time
|
||||
FROM klptcm1_pdo_excoil e
|
||||
WHERE 1=1
|
||||
<if test="startDate != null and startDate != ''">
|
||||
AND DATE(e.INSDATE) >= #{startDate}
|
||||
</if>
|
||||
<if test="endDate != null and endDate != ''">
|
||||
AND DATE(e.INSDATE) <= #{endDate}
|
||||
</if>
|
||||
-- 过滤掉无效数据:必须有开始和结束时间,且时长大于0
|
||||
AND e.START_DATE IS NOT NULL
|
||||
AND e.END_DATE IS NOT NULL
|
||||
AND e.END_DATE > e.START_DATE
|
||||
AND e.EXIT_WEIGHT IS NOT NULL
|
||||
AND e.EXIT_WEIGHT > 0
|
||||
ORDER BY e.INSDATE ASC
|
||||
-- 限制最多返回最近6个月的数据(避免数据量过大)
|
||||
LIMIT 10000
|
||||
</select>
|
||||
|
||||
<!-- 查询每日的钢卷号和重量(用于良品/次品判定) -->
|
||||
<select id="selectCoilInfoByDate" resultType="com.klp.pocket.acid.mapper.AcidOeeMapper$CoilInfoByDate">
|
||||
SELECT
|
||||
@@ -115,5 +70,26 @@
|
||||
ORDER BY e.INSDATE ASC, e.ENCOILID ASC
|
||||
</select>
|
||||
|
||||
<!-- 查询卷级生产节拍(min/吨):(END_DATE - START_DATE)/EXIT_WEIGHT -->
|
||||
<select id="selectCoilCycleMinPerTon" resultType="java.math.BigDecimal">
|
||||
SELECT
|
||||
-- 生产节拍(分钟/吨)
|
||||
TIMESTAMPDIFF(MINUTE, e.START_DATE, e.END_DATE) / e.EXIT_WEIGHT AS cycle_min_per_ton
|
||||
FROM klptcm1_pdo_excoil e
|
||||
WHERE 1 = 1
|
||||
<if test="startDate != null and startDate != ''">
|
||||
AND DATE(e.INSDATE) >= #{startDate}
|
||||
</if>
|
||||
<if test="endDate != null and endDate != ''">
|
||||
AND DATE(e.INSDATE) <= #{endDate}
|
||||
</if>
|
||||
AND e.START_DATE IS NOT NULL
|
||||
AND e.END_DATE IS NOT NULL
|
||||
AND e.END_DATE > e.START_DATE
|
||||
AND e.EXIT_WEIGHT IS NOT NULL
|
||||
AND e.EXIT_WEIGHT > 0
|
||||
AND TIMESTAMPDIFF(MINUTE, e.START_DATE, e.END_DATE) > 0
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 导出 Word 报表
|
||||
// 导出 Word 报表(酸轧线)
|
||||
export function exportOeeWord(query) {
|
||||
return request({
|
||||
url: '/oee/line/exportWord',
|
||||
url: '/oee/line/acid/exportWord',
|
||||
method: 'get',
|
||||
params: query,
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
|
||||
// OEE 产线 KPI + 趋势汇总
|
||||
// OEE 产线 KPI + 趋势汇总(酸轧线)
|
||||
export function fetchOeeSummary(query) {
|
||||
return request({
|
||||
url: '/oee/line/summary',
|
||||
url: '/oee/line/acid/summary',
|
||||
method: 'get',
|
||||
params: query,
|
||||
timeout: 120000
|
||||
@@ -23,37 +23,37 @@ export function fetchOeeSummary(query) {
|
||||
// OEE 产线 KPI + 趋势汇总(异步任务接口暂保留,当前前端不使用)
|
||||
export function createOeeSummaryJob(query) {
|
||||
return request({
|
||||
url: '/oee/line/summary/job',
|
||||
url: '/oee/line/acid/summary/job',
|
||||
method: 'get',
|
||||
params: query,
|
||||
timeout: 120000
|
||||
})
|
||||
}
|
||||
|
||||
// 7 大损失汇总
|
||||
// 7 大损失汇总(酸轧线)
|
||||
export function fetchOeeLoss7(query) {
|
||||
return request({
|
||||
url: '/oee/line/loss7',
|
||||
url: '/oee/line/acid/loss7',
|
||||
method: 'get',
|
||||
params: query,
|
||||
timeout: 120000
|
||||
})
|
||||
}
|
||||
|
||||
// 停机/损失事件明细
|
||||
// 停机/损失事件明细(酸轧线)
|
||||
export function fetchOeeEvents(query) {
|
||||
return request({
|
||||
url: '/oee/line/events',
|
||||
url: '/oee/line/acid/events',
|
||||
method: 'get',
|
||||
params: query,
|
||||
timeout: 120000
|
||||
})
|
||||
}
|
||||
|
||||
// 理论节拍回归结果(含散点与拟合线)
|
||||
export function fetchOeeTheoryCycleRegression(query) {
|
||||
// 理论节拍(统计口径:优良日中位数)
|
||||
export function fetchOeeIdealCycle(query) {
|
||||
return request({
|
||||
url: '/oee/line/theoryCycle/regression',
|
||||
url: '/oee/line/acid/idealCycle',
|
||||
method: 'get',
|
||||
params: query,
|
||||
timeout: 120000
|
||||
@@ -63,7 +63,7 @@ export function fetchOeeTheoryCycleRegression(query) {
|
||||
// 理论节拍回归:创建异步任务,返回 jobId + wsType,通过 WebSocket 推送进度/结果
|
||||
export function createOeeTheoryCycleRegressionJob(query) {
|
||||
return request({
|
||||
url: '/oee/line/theoryCycle/regression/job',
|
||||
url: '/oee/line/acid/theoryCycle/regression/job',
|
||||
method: 'get',
|
||||
params: query,
|
||||
timeout: 120000
|
||||
|
||||
127
klp-ui/src/views/da/oee/components/OeeLossPareto.vue
Normal file
127
klp-ui/src/views/da/oee/components/OeeLossPareto.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<div class="oee-chart" ref="chartRef"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
export default {
|
||||
name: 'OeeLossPareto',
|
||||
props: {
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chart: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initChart()
|
||||
window.addEventListener('resize', this.handleResize)
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.handleResize)
|
||||
if (this.chart) {
|
||||
this.chart.dispose()
|
||||
this.chart = null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
data: {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
handler() {
|
||||
this.render()
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toFixed3(val) {
|
||||
const n = Number(val)
|
||||
return Number.isFinite(n) ? n.toFixed(3) : '-'
|
||||
},
|
||||
initChart() {
|
||||
if (!this.$refs.chartRef) return
|
||||
this.chart = echarts.init(this.$refs.chartRef)
|
||||
},
|
||||
render() {
|
||||
if (!this.chart) this.initChart()
|
||||
if (!this.chart) return
|
||||
const list = Array.isArray(this.data) ? this.data : []
|
||||
const names = list.map(d => d.lossCategoryName)
|
||||
const mins = list.map(d => Number(d.lossTimeMin || 0))
|
||||
const percents = list.map(d => Number(d.lossTimeRate || 0))
|
||||
const cum = []
|
||||
percents.reduce((acc, cur) => {
|
||||
const v = acc + cur
|
||||
cum.push(Math.min(100, v))
|
||||
return v
|
||||
}, 0)
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: params => {
|
||||
const title = (params && params[0] && params[0].axisValue) || ''
|
||||
const lines = (params || []).map(p => {
|
||||
const suffix = p.seriesName === '累计占比' ? ' %' : ' min'
|
||||
return `${p.marker}${p.seriesName}:${this.toFixed3(p.value)}${suffix}`
|
||||
})
|
||||
return [title, ...lines].join('<br/>')
|
||||
}
|
||||
},
|
||||
grid: { left: 40, right: 60, top: 30, bottom: 80 },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: names,
|
||||
axisLabel: { interval: 0, rotate: 40, fontSize: 10 }
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '损失时间 (min)',
|
||||
axisLabel: { formatter: v => this.toFixed3(v) }
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: '累计占比 (%)',
|
||||
min: 0,
|
||||
max: 100,
|
||||
position: 'right',
|
||||
axisLabel: { formatter: v => this.toFixed3(v) }
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '损失时间',
|
||||
type: 'bar',
|
||||
data: mins,
|
||||
itemStyle: { color: '#F56C6C' }
|
||||
},
|
||||
{
|
||||
name: '累计占比',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
data: cum,
|
||||
smooth: true,
|
||||
itemStyle: { color: '#409EFF' }
|
||||
}
|
||||
]
|
||||
}
|
||||
this.chart.setOption(option)
|
||||
},
|
||||
handleResize() {
|
||||
this.chart && this.chart.resize()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.oee-chart {
|
||||
width: 100%;
|
||||
height: 260px;
|
||||
}
|
||||
</style>
|
||||
106
klp-ui/src/views/da/oee/components/OeeStoppageTop.vue
Normal file
106
klp-ui/src/views/da/oee/components/OeeStoppageTop.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<div class="oee-chart" ref="chartRef"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
export default {
|
||||
name: 'OeeStoppageTop',
|
||||
props: {
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
topN: {
|
||||
type: Number,
|
||||
default: 10
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chart: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initChart()
|
||||
window.addEventListener('resize', this.handleResize)
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.handleResize)
|
||||
if (this.chart) {
|
||||
this.chart.dispose()
|
||||
this.chart = null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
data: {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
handler() {
|
||||
this.render()
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toFixed3(val) {
|
||||
const n = Number(val)
|
||||
return Number.isFinite(n) ? n.toFixed(3) : '-'
|
||||
},
|
||||
initChart() {
|
||||
if (!this.$refs.chartRef) return
|
||||
this.chart = echarts.init(this.$refs.chartRef)
|
||||
},
|
||||
render() {
|
||||
if (!this.chart) this.initChart()
|
||||
if (!this.chart) return
|
||||
const list = Array.isArray(this.data) ? this.data.slice() : []
|
||||
list.sort((a, b) => (b.duration || 0) - (a.duration || 0))
|
||||
const top = list.slice(0, this.topN)
|
||||
const names = top.map((d, idx) => `${idx + 1}. ${d.stopType || '未知'}`)
|
||||
const mins = top.map(d => Number(((d.duration || 0) / 60).toFixed(3)))
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'shadow' },
|
||||
formatter: params => {
|
||||
const p = params && params[0]
|
||||
if (!p) return ''
|
||||
return `${p.name}<br/>${p.marker}${p.seriesName}:${this.toFixed3(p.value)} min`
|
||||
}
|
||||
},
|
||||
grid: { left: 140, right: 20, top: 20, bottom: 20 },
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: '停机时间 (min)',
|
||||
axisLabel: { formatter: v => this.toFixed3(v) }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
data: names,
|
||||
axisLabel: { fontSize: 11 }
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '停机时间 (min)',
|
||||
type: 'bar',
|
||||
data: mins,
|
||||
itemStyle: { color: '#67C23A' }
|
||||
}
|
||||
]
|
||||
}
|
||||
this.chart.setOption(option)
|
||||
},
|
||||
handleResize() {
|
||||
this.chart && this.chart.resize()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.oee-chart {
|
||||
width: 100%;
|
||||
height: 260px;
|
||||
}
|
||||
</style>
|
||||
97
klp-ui/src/views/da/oee/components/OeeTrendChart.vue
Normal file
97
klp-ui/src/views/da/oee/components/OeeTrendChart.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<div class="oee-chart" ref="chartRef"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
export default {
|
||||
name: 'OeeTrendChart',
|
||||
props: {
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chart: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initChart()
|
||||
window.addEventListener('resize', this.handleResize)
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.handleResize)
|
||||
if (this.chart) {
|
||||
this.chart.dispose()
|
||||
this.chart = null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
data: {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
handler() {
|
||||
this.render()
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toFixed3(val) {
|
||||
const n = Number(val)
|
||||
return Number.isFinite(n) ? n.toFixed(3) : '-'
|
||||
},
|
||||
initChart() {
|
||||
if (!this.$refs.chartRef) return
|
||||
this.chart = echarts.init(this.$refs.chartRef)
|
||||
},
|
||||
render() {
|
||||
if (!this.chart) this.initChart()
|
||||
if (!this.chart) return
|
||||
const list = Array.isArray(this.data) ? this.data : []
|
||||
const x = list.map(d => d.statDate)
|
||||
const oee = list.map(d => Number(d.oee || 0))
|
||||
const a = list.map(d => Number(d.availability || 0))
|
||||
const p = list.map(d => Number(d.performanceTon || 0))
|
||||
const q = list.map(d => Number(d.quality || 0))
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: params => {
|
||||
const title = (params && params[0] && params[0].axisValue) || ''
|
||||
const lines = (params || []).map(p0 => `${p0.marker}${p0.seriesName}:${this.toFixed3(p0.value)} %`)
|
||||
return [title, ...lines].join('<br/>')
|
||||
}
|
||||
},
|
||||
legend: { top: 0, left: 'center' },
|
||||
grid: { left: 40, right: 20, top: 30, bottom: 40 },
|
||||
xAxis: { type: 'category', data: x },
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '百分比 (%)',
|
||||
axisLabel: { formatter: v => this.toFixed3(v) }
|
||||
},
|
||||
series: [
|
||||
{ name: 'OEE', type: 'line', data: oee, smooth: true },
|
||||
{ name: 'A', type: 'line', data: a, smooth: true },
|
||||
{ name: 'P_ton', type: 'line', data: p, smooth: true },
|
||||
{ name: 'Q', type: 'line', data: q, smooth: true }
|
||||
]
|
||||
}
|
||||
this.chart.setOption(option)
|
||||
},
|
||||
handleResize() {
|
||||
this.chart && this.chart.resize()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.oee-chart {
|
||||
width: 100%;
|
||||
height: 260px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,745 @@
|
||||
<template>
|
||||
<div class="oee-report-page">
|
||||
<!-- 查询条件概览(去掉大标题,只保留区间提示) -->
|
||||
<el-card class="oee-header-card" shadow="never">
|
||||
<div class="oee-header">
|
||||
<div class="oee-title-block">
|
||||
<div class="oee-subtitle">
|
||||
查询区间:
|
||||
<span v-if="queryRange && queryRange.length === 2">
|
||||
{{ queryRange[0] }} ~ {{ queryRange[1] }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="oee-query-bar">
|
||||
<el-date-picker
|
||||
v-model="queryRange"
|
||||
type="daterange"
|
||||
unlink-panels
|
||||
size="small"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="yyyy-MM-dd"
|
||||
:picker-options="pickerOptions"
|
||||
/>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon="el-icon-search"
|
||||
@click="handleSearch"
|
||||
>
|
||||
查询
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
icon="el-icon-refresh"
|
||||
@click="handleReset"
|
||||
>
|
||||
重置
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
icon="el-icon-document"
|
||||
@click="handleExportWord"
|
||||
>
|
||||
导出 Word
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-row :gutter="16" class="oee-main-row">
|
||||
<!-- 左侧:报表主体(Word 风格) -->
|
||||
<el-col :span="18">
|
||||
<el-card class="oee-word-card" shadow="never">
|
||||
<!-- KPI 一览(报表头部表格) -->
|
||||
<div class="oee-section-title">一、KPI 总览(酸轧线 SY)</div>
|
||||
<div class="oee-help-text">
|
||||
展示所选期间酸轧线整体 OEE 及 A/P/Q 等关键指标,用于快速判断本期综合表现好坏。
|
||||
</div>
|
||||
<el-table
|
||||
:data="[kpi]"
|
||||
border
|
||||
size="mini"
|
||||
class="oee-kpi-table"
|
||||
v-loading="loading.summary"
|
||||
>
|
||||
<el-table-column prop="oee" label="OEE (%)" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ formatPercent(scope.row.oee) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="availability" label="时间稼动率 A (%)" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ formatPercent(scope.row.availability) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="performanceTon" label="性能稼动率 P_ton (%)" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ formatPercent(scope.row.performanceTon) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="quality" label="良品率 Q (%)" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ formatPercent(scope.row.quality) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="loadingTimeMin" label="负荷时间 (min)" align="center" />
|
||||
<el-table-column prop="downtimeMin" label="停机时间 (min)" align="center" />
|
||||
<el-table-column prop="runTimeMin" label="运转时间 (min)" align="center" />
|
||||
<el-table-column prop="totalOutputTon" label="总产量 (吨)" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ formatNumber(scope.row.totalOutputTon) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="totalOutputCoil" label="总产量 (卷)" align="center" />
|
||||
<el-table-column prop="goodOutputTon" label="良品量 (吨)" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ formatNumber(scope.row.goodOutputTon) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="defectOutputTon" label="次品量 (吨)" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ formatNumber(scope.row.defectOutputTon) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 日明细(趋势表格风格,方便导出 Word) -->
|
||||
<div class="oee-section-title">二、日明细(用于趋势分析)</div>
|
||||
<div class="oee-help-text">
|
||||
按天拆分 A/P/Q 及产量等指标,用于观察本月趋势、波动点以及与重大事件的对应关系。
|
||||
</div>
|
||||
<el-table
|
||||
:data="summaryList"
|
||||
border
|
||||
size="mini"
|
||||
class="oee-daily-table"
|
||||
v-loading="loading.summary"
|
||||
>
|
||||
<el-table-column prop="statDate" label="日期" />
|
||||
<el-table-column prop="oee" label="OEE (%)" >
|
||||
<template slot-scope="scope">
|
||||
{{ formatPercent(scope.row.oee) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="availability" label="A (%)" >
|
||||
<template slot-scope="scope">
|
||||
{{ formatPercent(scope.row.availability) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="performanceTon" label="P_ton (%)">
|
||||
<template slot-scope="scope">
|
||||
{{ formatPercent(scope.row.performanceTon) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="quality" label="Q (%)">
|
||||
<template slot-scope="scope">
|
||||
{{ formatPercent(scope.row.quality) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="loadingTimeMin" label="负荷 (min)"/>
|
||||
<el-table-column prop="downtimeMin" label="停机 (min)"/>
|
||||
<el-table-column prop="runTimeMin" label="运转 (min)"/>
|
||||
<el-table-column prop="totalOutputTon" label="总产量 (吨)">
|
||||
<template slot-scope="scope">
|
||||
{{ formatNumber(scope.row.totalOutputTon) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="totalOutputCoil" label="总产量 (卷)" />
|
||||
<el-table-column prop="goodOutputTon" label="良品 (吨)">
|
||||
<template slot-scope="scope">
|
||||
{{ formatNumber(scope.row.goodOutputTon) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="defectOutputTon" label="次品 (吨)">
|
||||
<template slot-scope="scope">
|
||||
{{ formatNumber(scope.row.defectOutputTon) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- OEE/A/P/Q 趋势图 -->
|
||||
<oee-trend-chart :data="summaryList" />
|
||||
|
||||
<!-- 理论节拍(统计口径) -->
|
||||
<div class="oee-section-title">三、理论节拍(统计口径)</div>
|
||||
<div class="oee-help-text">
|
||||
基于历史“优良日”统计得到的理论节拍(中位数),用于作为性能稼动率计算的稳定标尺。
|
||||
</div>
|
||||
<div
|
||||
v-if="idealCycle && idealCycle.dailyComparePoints && idealCycle.dailyComparePoints.length"
|
||||
class="oee-reg-section"
|
||||
>
|
||||
<div class="reg-chart-block">
|
||||
<div class="reg-chart-title">日粒度:理论耗时 vs 实际运转时间</div>
|
||||
<div class="oee-help-text">
|
||||
按天对比“理想应耗时间”和“实际运转时间”,可一眼看出哪几天效率偏低、存在明显损失空间。
|
||||
</div>
|
||||
<div ref="regDailyCompareChart" class="reg-chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 7 大损失 -->
|
||||
<div class="oee-section-title">四、7 大损失汇总(按 stop_type 分类)</div>
|
||||
<div class="oee-help-text">
|
||||
将所有停机事件按 stop_type 归类,统计时间占比和次数,用于确定“先从哪几类损失下手改善”。
|
||||
</div>
|
||||
<el-table
|
||||
:data="loss7List"
|
||||
border
|
||||
size="mini"
|
||||
class="oee-loss7-table"
|
||||
v-loading="loading.loss7"
|
||||
>
|
||||
<el-table-column prop="lossCategoryName" label="损失类别" min-width="160" />
|
||||
<el-table-column prop="lossTimeMin" label="损失时间 (min)" width="140" />
|
||||
<el-table-column prop="lossTimeRate" label="占比 (%)" width="110">
|
||||
<template slot-scope="scope">
|
||||
{{ formatPercent(scope.row.lossTimeRate) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="count" label="次数" width="90" />
|
||||
<el-table-column prop="avgDurationMin" label="平均时长 (min)" width="140">
|
||||
<template slot-scope="scope">
|
||||
{{ formatNumber(scope.row.avgDurationMin) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 7 大损失帕累托图 -->
|
||||
<oee-loss-pareto :data="loss7List" />
|
||||
|
||||
<!-- 停机事件明细 -->
|
||||
<div class="oee-section-title">五、停机/损失事件明细</div>
|
||||
<div class="oee-help-text">
|
||||
罗列每一条停机/损失事件,包含时间段、区域、机组和备注,方便班组和工艺人员对照现场记录进行原因分析。
|
||||
</div>
|
||||
<el-table
|
||||
:data="eventList"
|
||||
border
|
||||
size="mini"
|
||||
class="oee-events-table"
|
||||
v-loading="loading.events"
|
||||
>
|
||||
<el-table-column prop="startDate" label="开始时间" min-width="150">
|
||||
<template slot-scope="scope">
|
||||
{{ formatDateTime(scope.row.startDate) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="endDate" label="结束时间" min-width="150">
|
||||
<template slot-scope="scope">
|
||||
{{ formatDateTime(scope.row.endDate) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="duration" label="时长 (s)" width="100" />
|
||||
<el-table-column prop="durationMin" label="时长 (min)" width="110">
|
||||
<template slot-scope="scope">
|
||||
{{ Math.max(1, Math.floor((scope.row.duration || 0) / 60)) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="stopType" label="停机类型 (stop_type)" min-width="160" />
|
||||
<el-table-column prop="area" label="区域" width="100" />
|
||||
<el-table-column prop="unit" label="机组" width="100" />
|
||||
<el-table-column prop="shift" label="班次" width="80" />
|
||||
<el-table-column prop="crew" label="班组" width="80" />
|
||||
<el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip />
|
||||
</el-table>
|
||||
|
||||
<!-- 停机 TopN 条形图 -->
|
||||
<oee-stoppage-top :data="eventList" :top-n="10" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 右侧:公式与口径说明(支撑材料) -->
|
||||
<el-col :span="6">
|
||||
<el-card class="oee-formula-card" shadow="never">
|
||||
<div slot="header" class="clearfix">
|
||||
<span>公式与口径说明</span>
|
||||
</div>
|
||||
<div class="formula-block">
|
||||
<div class="formula-title">OEE 总公式</div>
|
||||
<div class="formula-eq">OEE = A × P × Q</div>
|
||||
<ul class="formula-list">
|
||||
<li>A(时间稼动率) = (负荷时间 − 停机时间) / 负荷时间</li>
|
||||
<li>P(性能稼动率,吨维度) = (理论节拍 × 产量吨) / 实际运转时间</li>
|
||||
<li>Q(良品率) = 良品吨 / 总产量吨</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="formula-block">
|
||||
<div class="formula-title">关键字段定义</div>
|
||||
<ul class="formula-list">
|
||||
<li><b>负荷时间</b>:计划生产时间扣除计划停机后的时间</li>
|
||||
<li><b>停机时间</b>:所有停机/中断(按 stop_type 汇总)的总时长</li>
|
||||
<li><b>实际运转时间</b>:负荷时间 − 停机时间</li>
|
||||
<li><b>理论节拍</b>:按“优良日统计口径”得到的稳定节拍(分钟/吨),由理论节拍接口提供</li>
|
||||
<li><b>良品/次品</b>:按 WMS `quality_status` 判断,C+/C/C-/D+/D/D- 为次品</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="formula-block" v-if="idealCycle">
|
||||
<div class="formula-title">当前理论节拍(统计口径)</div>
|
||||
<el-descriptions :column="1" size="small" class="reg-desc">
|
||||
<el-descriptions-item label="理论节拍 (min/吨)">
|
||||
{{ formatNumber(idealCycle.idealCycleTimeMinPerTon) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="生产节拍中位数 (min/吨)">
|
||||
{{ formatNumber(idealCycle.medianCycleTimeMinPerTon) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="样本天数">
|
||||
{{ idealCycle.sampleDays || 0 }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="吨数下限 (吨)">
|
||||
{{ formatNumber(idealCycle.minWeightTon) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="停机占比上限">
|
||||
{{
|
||||
idealCycle.maxDowntimeRate != null
|
||||
? (Number(idealCycle.maxDowntimeRate) * 100).toFixed(0) + '%'
|
||||
: '-'
|
||||
}}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
exportOeeWord,
|
||||
fetchOeeSummary,
|
||||
fetchOeeLoss7,
|
||||
fetchOeeEvents,
|
||||
fetchOeeIdealCycle
|
||||
} from '@/api/da/oee'
|
||||
import * as echarts from 'echarts'
|
||||
import OeeTrendChart from './components/OeeTrendChart.vue'
|
||||
import OeeLossPareto from './components/OeeLossPareto.vue'
|
||||
import OeeStoppageTop from './components/OeeStoppageTop.vue'
|
||||
|
||||
export default {
|
||||
name: 'DaOeeIndex',
|
||||
components: {
|
||||
OeeTrendChart,
|
||||
OeeLossPareto,
|
||||
OeeStoppageTop
|
||||
},
|
||||
data() {
|
||||
const today = new Date()
|
||||
const firstDay = new Date(today.getFullYear(), today.getMonth(), 1)
|
||||
return {
|
||||
queryRange: [
|
||||
this.formatDate(firstDay),
|
||||
this.formatDate(today)
|
||||
],
|
||||
pickerOptions: {
|
||||
disabledDate(time) {
|
||||
// 不限制选择范围,保留扩展空间
|
||||
return false
|
||||
}
|
||||
},
|
||||
loading: {
|
||||
summary: false,
|
||||
loss7: false,
|
||||
events: false,
|
||||
idealCycle: false,
|
||||
export: false
|
||||
},
|
||||
summaryList: [],
|
||||
loss7List: [],
|
||||
eventList: [],
|
||||
idealCycle: null,
|
||||
regDailyCompareChart: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
kpi() {
|
||||
const list = Array.isArray(this.summaryList) ? this.summaryList : []
|
||||
if (list.length === 0) {
|
||||
return {
|
||||
oee: 0,
|
||||
availability: 0,
|
||||
performanceTon: 0,
|
||||
quality: 0,
|
||||
loadingTimeMin: 0,
|
||||
downtimeMin: 0,
|
||||
runTimeMin: 0,
|
||||
totalOutputTon: 0,
|
||||
totalOutputCoil: 0,
|
||||
goodOutputTon: 0,
|
||||
defectOutputTon: 0
|
||||
}
|
||||
}
|
||||
let sumLoading = 0
|
||||
let sumDowntime = 0
|
||||
let sumRun = 0
|
||||
let sumTotalTon = 0
|
||||
let sumGoodTon = 0
|
||||
let sumDefectTon = 0
|
||||
let sumCoil = 0
|
||||
let sumOee = 0
|
||||
let sumA = 0
|
||||
let sumP = 0
|
||||
let sumQ = 0
|
||||
|
||||
list.forEach(row => {
|
||||
sumLoading += row.loadingTimeMin || 0
|
||||
sumDowntime += row.downtimeMin || 0
|
||||
sumRun += row.runTimeMin || 0
|
||||
sumTotalTon += Number(row.totalOutputTon || 0)
|
||||
sumGoodTon += Number(row.goodOutputTon || 0)
|
||||
sumDefectTon += Number(row.defectOutputTon || 0)
|
||||
sumCoil += row.totalOutputCoil || 0
|
||||
sumOee += Number(row.oee || 0)
|
||||
sumA += Number(row.availability || 0)
|
||||
sumP += Number(row.performanceTon || 0)
|
||||
sumQ += Number(row.quality || 0)
|
||||
})
|
||||
|
||||
const n = list.length
|
||||
const defectAgg = Math.max(0, sumTotalTon - sumGoodTon)
|
||||
return {
|
||||
oee: n ? sumOee / n : 0,
|
||||
availability: n ? sumA / n : 0,
|
||||
performanceTon: n ? sumP / n : 0,
|
||||
quality: n ? sumQ / n : 0,
|
||||
loadingTimeMin: sumLoading,
|
||||
downtimeMin: sumDowntime,
|
||||
runTimeMin: sumRun,
|
||||
totalOutputTon: sumTotalTon,
|
||||
totalOutputCoil: sumCoil,
|
||||
goodOutputTon: sumGoodTon,
|
||||
defectOutputTon: defectAgg || sumDefectTon
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadAll()
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('resize', this.handleResize)
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.handleResize)
|
||||
if (this.regDailyCompareChart) {
|
||||
this.regDailyCompareChart.dispose()
|
||||
this.regDailyCompareChart = null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
formatDate(d) {
|
||||
const y = d.getFullYear()
|
||||
const m = (`0${d.getMonth() + 1}`).slice(-2)
|
||||
const day = (`0${d.getDate()}`).slice(-2)
|
||||
return `${y}-${m}-${day}`
|
||||
},
|
||||
formatPercent(value) {
|
||||
if (value === null || value === undefined || isNaN(value)) {
|
||||
return '-'
|
||||
}
|
||||
const num = Number(value)
|
||||
return num.toFixed(1)
|
||||
},
|
||||
formatNumber(value) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '-'
|
||||
}
|
||||
const num = Number(value)
|
||||
if (isNaN(num)) return '-'
|
||||
return num.toFixed(2)
|
||||
},
|
||||
formatDateTime(val) {
|
||||
if (!val) return ''
|
||||
// 后端可能返回字符串或时间戳,这里做容错
|
||||
if (typeof val === 'string') return val
|
||||
try {
|
||||
const d = new Date(val)
|
||||
const y = d.getFullYear()
|
||||
const m = (`0${d.getMonth() + 1}`).slice(-2)
|
||||
const day = (`0${d.getDate()}`).slice(-2)
|
||||
const hh = (`0${d.getHours()}`).slice(-2)
|
||||
const mm = (`0${d.getMinutes()}`).slice(-2)
|
||||
const ss = (`0${d.getSeconds()}`).slice(-2)
|
||||
return `${y}-${m}-${day} ${hh}:${mm}:${ss}`
|
||||
} catch (e) {
|
||||
return ''
|
||||
}
|
||||
},
|
||||
buildQuery() {
|
||||
const [start, end] = this.queryRange || []
|
||||
return {
|
||||
startDate: start,
|
||||
endDate: end
|
||||
}
|
||||
},
|
||||
async loadAll() {
|
||||
await Promise.all([
|
||||
this.loadSummary(),
|
||||
this.loadLoss7(),
|
||||
this.loadEvents(),
|
||||
this.loadIdealCycle()
|
||||
])
|
||||
},
|
||||
async loadSummary() {
|
||||
this.loading.summary = true
|
||||
try {
|
||||
const res = await fetchOeeSummary(this.buildQuery())
|
||||
// 兼容后端直接返回数组或 TableDataInfo { rows, total } 两种结构
|
||||
|
||||
this.summaryList = res.data
|
||||
|
||||
} catch (e) {
|
||||
this.$message.error('加载酸轧日汇总失败')
|
||||
} finally {
|
||||
this.loading.summary = false
|
||||
}
|
||||
},
|
||||
async loadLoss7() {
|
||||
this.loading.loss7 = true
|
||||
try {
|
||||
const res = await fetchOeeLoss7(this.buildQuery())
|
||||
// 兼容后端直接返回数组或 TableDataInfo { rows, total } 两种结构
|
||||
this.loss7List = res.data
|
||||
|
||||
} catch (e) {
|
||||
this.$message.error('加载酸轧 7 大损失失败')
|
||||
} finally {
|
||||
this.loading.loss7 = false
|
||||
}
|
||||
},
|
||||
async loadEvents() {
|
||||
this.loading.events = true
|
||||
try {
|
||||
const res = await fetchOeeEvents(this.buildQuery())
|
||||
// 后端 TableDataInfo 结构:{ rows, total }
|
||||
this.eventList = (res && Array.isArray(res.rows)) ? res.rows : []
|
||||
} catch (e) {
|
||||
this.$message.error('加载酸轧停机事件失败')
|
||||
} finally {
|
||||
this.loading.events = false
|
||||
}
|
||||
},
|
||||
async loadIdealCycle() {
|
||||
this.loading.idealCycle = true
|
||||
try {
|
||||
const res = await fetchOeeIdealCycle(this.buildQuery())
|
||||
this.idealCycle = res && res.data ? res.data : null
|
||||
this.$nextTick(() => {
|
||||
this.renderIdealCycleChart()
|
||||
})
|
||||
} catch (e) {
|
||||
this.$message.error('加载理论节拍失败')
|
||||
} finally {
|
||||
this.loading.idealCycle = false
|
||||
}
|
||||
},
|
||||
handleSearch() {
|
||||
if (!this.queryRange || this.queryRange.length !== 2) {
|
||||
this.$message.warning('请选择查询日期范围')
|
||||
return
|
||||
}
|
||||
this.loadAll()
|
||||
},
|
||||
handleReset() {
|
||||
const today = new Date()
|
||||
const firstDay = new Date(today.getFullYear(), today.getMonth(), 1)
|
||||
this.queryRange = [this.formatDate(firstDay), this.formatDate(today)]
|
||||
this.loadAll()
|
||||
},
|
||||
async handleExportWord() {
|
||||
this.loading.export = true
|
||||
try {
|
||||
const res = await exportOeeWord(this.buildQuery())
|
||||
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报表_${this.queryRange[0]}_${this.queryRange[1]}.docx`
|
||||
a.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
} catch (e) {
|
||||
this.$message.error('导出 Word 失败')
|
||||
} finally {
|
||||
this.loading.export = false
|
||||
}
|
||||
},
|
||||
renderIdealCycleChart() {
|
||||
if (!this.idealCycle) return
|
||||
const dailyPoints = Array.isArray(this.idealCycle.dailyComparePoints)
|
||||
? this.idealCycle.dailyComparePoints
|
||||
: []
|
||||
if (this.$refs.regDailyCompareChart && dailyPoints.length) {
|
||||
if (!this.regDailyCompareChart) {
|
||||
this.regDailyCompareChart = echarts.init(this.$refs.regDailyCompareChart)
|
||||
}
|
||||
const categories = dailyPoints.map(d => d.statDate)
|
||||
const actualRun = dailyPoints.map(d => Number(d.actualRunTimeMin || 0))
|
||||
const theoretical = dailyPoints.map(d => Number(d.theoreticalTimeMin || 0))
|
||||
const compareOption = {
|
||||
tooltip: { trigger: 'axis' },
|
||||
grid: { left: 40, right: 20, top: 30, bottom: 40 },
|
||||
legend: { top: 0, left: 'center' },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: categories,
|
||||
axisLabel: { fontSize: 10 }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '时间 (min)',
|
||||
nameLocation: 'middle',
|
||||
nameGap: 35
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '实际运转时间',
|
||||
type: 'line',
|
||||
data: actualRun,
|
||||
smooth: true,
|
||||
symbolSize: 4,
|
||||
itemStyle: { color: '#409EFF' }
|
||||
},
|
||||
{
|
||||
name: '理论耗时',
|
||||
type: 'line',
|
||||
data: theoretical,
|
||||
smooth: true,
|
||||
symbolSize: 4,
|
||||
itemStyle: { color: '#E6A23C' }
|
||||
}
|
||||
]
|
||||
}
|
||||
this.regDailyCompareChart.setOption(compareOption)
|
||||
}
|
||||
},
|
||||
handleResize() {
|
||||
this.regDailyCompareChart && this.regDailyCompareChart.resize()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.oee-report-page {
|
||||
padding: 16px;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.oee-header-card {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.oee-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.oee-title-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.oee-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.oee-subtitle {
|
||||
margin-top: 4px;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.oee-query-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.oee-main-row {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.oee-word-card {
|
||||
min-height: 600px;
|
||||
background: #ffffff;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.oee-section-title {
|
||||
margin: 12px 0 6px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.oee-help-text {
|
||||
margin: 0 0 6px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.oee-kpi-table,
|
||||
.oee-daily-table,
|
||||
.oee-loss7-table,
|
||||
.oee-events-table {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.oee-formula-card {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.formula-block {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.formula-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 6px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.formula-eq {
|
||||
font-family: 'Times New Roman', serif;
|
||||
font-style: italic;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.formula-list {
|
||||
padding-left: 16px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.formula-list li {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.reg-desc {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.reg-chart {
|
||||
width: 100%;
|
||||
height: 260px;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.reg-chart canvas {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -3,148 +3,177 @@
|
||||
<div>
|
||||
<div class="waybill-container" ref="waybillRef">
|
||||
<div class="waybill-content">
|
||||
<!-- 头部信息 -->
|
||||
<!-- 标题信息 -->
|
||||
<div class="title">科伦普发货单</div>
|
||||
<div class="waybill-header">
|
||||
<div class="header-left">
|
||||
<span class="label">收货单位:</span>
|
||||
<div class="editable-input transparent-input" contenteditable>{{ localWaybill.consigneeUnit }}</div>
|
||||
<!-- 头部信息 -->
|
||||
<!-- 标题信息 -->
|
||||
<div class="title">科伦普发货单</div>
|
||||
<div class="waybill-header">
|
||||
<div class="header-left">
|
||||
<span class="label">收货单位:</span>
|
||||
<div class="editable-input transparent-input" contenteditable>{{ localWaybill.consigneeUnit }}</div>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<div class="editable-input date-input transparent-input" contenteditable>{{ localWaybill.deliveryYear }}
|
||||
</div>
|
||||
<span class="label date-label">年</span>
|
||||
<div class="editable-input date-input transparent-input" contenteditable>{{ localWaybill.deliveryMonth }}
|
||||
</div>
|
||||
<span class="label date-label">月</span>
|
||||
<div class="editable-input date-input transparent-input" contenteditable>{{ localWaybill.deliveryDay }}
|
||||
</div>
|
||||
<span class="label date-label">日</span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<span class="label">发货单位:</span>
|
||||
<div class="editable-input transparent-input" contenteditable>{{ localWaybill.senderUnit }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<div class="editable-input date-input transparent-input" contenteditable>{{ localWaybill.deliveryYear }}</div>
|
||||
<span class="label date-label">年</span>
|
||||
<div class="editable-input date-input transparent-input" contenteditable>{{ localWaybill.deliveryMonth }}</div>
|
||||
<span class="label date-label">月</span>
|
||||
<div class="editable-input date-input transparent-input" contenteditable>{{ localWaybill.deliveryDay }}</div>
|
||||
<span class="label date-label">日</span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<span class="label">发货单位:</span>
|
||||
<div class="editable-input transparent-input" contenteditable>{{ localWaybill.senderUnit }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="waybill-header">
|
||||
<div class="header-left">
|
||||
<span class="label">负责人:</span>
|
||||
<div class="editable-input transparent-input" contenteditable>{{ localWaybill.principal }}</div>
|
||||
<div class="waybill-header">
|
||||
<div class="header-left">
|
||||
<span class="label">负责人:</span>
|
||||
<div class="editable-input transparent-input" contenteditable>{{ localWaybill.principal }}</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<span class="label">电话:</span>
|
||||
<div class="editable-input transparent-input" contenteditable>{{ localWaybill.principalPhone }}</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<span class="label">合同号:</span>
|
||||
<div class="editable-input transparent-input" contenteditable>{{ localWaybill.contractCode }}</div>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<span class="label">车牌:</span>
|
||||
<div class="editable-input transparent-input" contenteditable>{{ localWaybill.licensePlate }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<span class="label">电话:</span>
|
||||
<div class="editable-input transparent-input" contenteditable>{{ localWaybill.principalPhone }}</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<span class="label">合同号:</span>
|
||||
<div class="editable-input transparent-input" contenteditable>{{ localWaybill.contractCode }}</div>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<span class="label">车牌:</span>
|
||||
<div class="editable-input transparent-input" contenteditable>{{ localWaybill.licensePlate }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格 -->
|
||||
<table class="waybill-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>品名</th>
|
||||
<th>切边</th>
|
||||
<th>包装</th>
|
||||
<th>仓库位置</th>
|
||||
<th>结算</th>
|
||||
<th>原料厂家</th>
|
||||
<th>卷号</th>
|
||||
<th>规格</th>
|
||||
<th>材质</th>
|
||||
<!-- <th>数量(件)</th> -->
|
||||
<th>重量(t)</th>
|
||||
<th>单价</th>
|
||||
<th>备注</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- 无明细提示 -->
|
||||
<tr v-if="localWaybillDetails.length === 0">
|
||||
<td colspan="12" class="no-data">
|
||||
<div class="no-data-content">
|
||||
<el-empty description="暂无发货单明细" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 明细数据 -->
|
||||
<tr v-for="(item, index) in displayWaybillDetails" :key="index">
|
||||
<td><div class="table-input transparent-input" contenteditable>{{ item.productName }}</div></td>
|
||||
<td><div class="table-input transparent-input" contenteditable>{{ item.edgeType }}</div></td>
|
||||
<td><div class="table-input transparent-input" contenteditable>{{ item.packageType }}</div></td>
|
||||
<td><div class="table-input transparent-input" contenteditable>{{ item.actualWarehouseName }}</div></td>
|
||||
<td><div class="table-input transparent-input" contenteditable>{{ item.settlementType }}</div></td>
|
||||
<td><div class="table-input transparent-input" contenteditable>{{ item.rawMaterialFactory }}</div></td>
|
||||
<td><div class="table-input transparent-input" contenteditable>{{ item.coilNumber }}</div></td>
|
||||
<td><div class="table-input transparent-input" contenteditable>{{ item.specification }}</div></td>
|
||||
<td><div class="table-input transparent-input" contenteditable>{{ item.material }}</div></td>
|
||||
<!-- <td><input type="number" class="table-input transparent-input" v-model.number="item.quantity"
|
||||
<!-- 表格 -->
|
||||
<table class="waybill-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>品名</th>
|
||||
<th>切边</th>
|
||||
<th>包装</th>
|
||||
<th>仓库位置</th>
|
||||
<th>结算</th>
|
||||
<th>原料厂家</th>
|
||||
<th>卷号</th>
|
||||
<th>规格</th>
|
||||
<th>材质</th>
|
||||
<!-- <th>数量(件)</th> -->
|
||||
<th>重量(t)</th>
|
||||
<th>单价</th>
|
||||
<th>备注</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- 无明细提示 -->
|
||||
<tr v-if="localWaybillDetails.length === 0">
|
||||
<td colspan="12" class="no-data">
|
||||
<div class="no-data-content">
|
||||
<el-empty description="暂无发货单明细" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 明细数据 -->
|
||||
<tr v-for="(item, index) in displayWaybillDetails" :key="index">
|
||||
<td>
|
||||
<div class="table-input transparent-input" contenteditable>{{ item.productName }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="table-input transparent-input" contenteditable>{{ item.edgeType }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="table-input transparent-input" contenteditable>{{ item.packageType }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="table-input transparent-input" contenteditable>{{ item.actualWarehouseName }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="table-input transparent-input" contenteditable>{{ item.settlementType }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="table-input transparent-input" contenteditable>{{ item.rawMaterialFactory }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="table-input transparent-input" contenteditable>{{ item.coilNumber }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="table-input transparent-input" contenteditable>{{ item.specification }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="table-input transparent-input" contenteditable>{{ item.material }}</div>
|
||||
</td>
|
||||
<!-- <td><input type="number" class="table-input transparent-input" v-model.number="item.quantity"
|
||||
placeholder="0" /></td> -->
|
||||
<td><input type="number" class="table-input transparent-input" v-model.number="item.weight"
|
||||
placeholder="0.00" /></td>
|
||||
<td><div class="table-input transparent-input" contenteditable>{{ item.unitPrice }}</div></td>
|
||||
<td><div class="table-input transparent-input" contenteditable>{{ item.remark }}</div></td>
|
||||
</tr>
|
||||
<!-- 加粗最后一行的线 -->
|
||||
<tr style="height: 0;">
|
||||
<td style="height: 0;"></td>
|
||||
<td style="height: 0;"></td>
|
||||
<td style="height: 0;"></td>
|
||||
<td style="height: 0;"></td>
|
||||
<td style="height: 0;"></td>
|
||||
<td style="height: 0;"></td>
|
||||
<td style="height: 0;"></td>
|
||||
<td style="height: 0;"></td>
|
||||
<td style="height: 0;"></td>
|
||||
<!-- <td><input type="number" class="table-input transparent-input" v-model.number="item.quantity"
|
||||
<td><input type="number" class="table-input transparent-input" v-model.number="item.weight"
|
||||
placeholder="0.00" /></td>
|
||||
<td>
|
||||
<div class="table-input transparent-input" contenteditable>{{ item.unitPrice }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="table-input transparent-input" contenteditable>{{ item.remark }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- 加粗最后一行的线 -->
|
||||
<tr style="height: 0;">
|
||||
<td style="height: 0;"></td>
|
||||
<td style="height: 0;"></td>
|
||||
<td style="height: 0;"></td>
|
||||
<td style="height: 0;"></td>
|
||||
<td style="height: 0;"></td>
|
||||
<td style="height: 0;"></td>
|
||||
<td style="height: 0;"></td>
|
||||
<td style="height: 0;"></td>
|
||||
<td style="height: 0;"></td>
|
||||
<!-- <td><input type="number" class="table-input transparent-input" v-model.number="item.quantity"
|
||||
placeholder="0" /></td> -->
|
||||
<td style="height: 0;"></td>
|
||||
<td style="height: 0;"></td>
|
||||
<td style="height: 0;"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<td style="height: 0;"></td>
|
||||
<td style="height: 0;"></td>
|
||||
<td style="height: 0;"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
|
||||
<!-- 备注说明 -->
|
||||
<div class="waybill-remarks">
|
||||
<p>
|
||||
1、品名:冷硬钢卷(酸连轧)、冷轧钢卷(脱脂退火火拉矫)、镀锌卷板,镀锌管料(镀锌分剪料);2、切边:净边/毛边;3、包装:裸包:周三径四;简包1:周三径四内外护角;简包2:周三径四+防锈纸;普包:周三径四+内外护角+防锈纸+端护板;精包1:周三径四+内外护角+防锈纸+薄膜+端护板+内外护板;精包2:周三径四+内外护角+防锈纸+薄膜+端护板+内外护板+木托。
|
||||
</p>
|
||||
</div>
|
||||
<!-- 备注说明 -->
|
||||
<div class="waybill-remarks">
|
||||
<p>
|
||||
1、品名:冷硬钢卷(酸连轧)、冷轧钢卷(脱脂退火火拉矫)、镀锌卷板,镀锌管料(镀锌分剪料);2、切边:净边/毛边;3、包装:裸包:周三径四;简包1:周三径四内外护角;简包2:周三径四+防锈纸;普包:周三径四+内外护角+防锈纸+端护板;精包1:周三径四+内外护角+防锈纸+薄膜+端护板+内外护板;精包2:周三径四+内外护角+防锈纸+薄膜+端护板+内外护板+木托。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="waybill-pickup-location">
|
||||
<!-- <div class="pickup-location-item inline"> -->
|
||||
<span style="font-size: 18px; font-weight: bold;">取货地点:</span>
|
||||
<input type="text" class="editable-input full-input transparent-input" v-model="localWaybill.pickupLocation" />
|
||||
<!-- </div> -->
|
||||
</div>
|
||||
<div class="waybill-pickup-location">
|
||||
<!-- <div class="pickup-location-item inline"> -->
|
||||
<span style="font-size: 18px; font-weight: bold;">取货地点:</span>
|
||||
<input type="text" class="editable-input full-input transparent-input"
|
||||
v-model="localWaybill.pickupLocation" />
|
||||
<!-- </div> -->
|
||||
</div>
|
||||
|
||||
<!-- 签名栏 -->
|
||||
<div class="waybill-footer">
|
||||
<div class="footer-item inline">
|
||||
<!-- 签名栏 -->
|
||||
<div class="waybill-footer">
|
||||
<!-- <div class="footer-item inline">
|
||||
<span class="label">销售:</span>
|
||||
<div class="editable-input signature-input transparent-input" contenteditable>{{ localWaybill.salesman }}</div>
|
||||
</div> -->
|
||||
<div class="footer-item inline">
|
||||
<span class="label">发货:</span>
|
||||
<div class="editable-input signature-input transparent-input" contenteditable>{{ localWaybill.deliveryman }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-item inline">
|
||||
<span class="label">司机:</span>
|
||||
<div class="editable-input signature-input transparent-input" contenteditable>{{ localWaybill.driver }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-item inline">
|
||||
<span class="label">磅房:</span>
|
||||
<div class="editable-input signature-input transparent-input" contenteditable>{{ localWaybill.weightRoom }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-item inline">
|
||||
<span class="label">发货:</span>
|
||||
<div class="editable-input signature-input transparent-input" contenteditable>{{ localWaybill.deliveryman }}</div>
|
||||
</div>
|
||||
<div class="footer-item inline">
|
||||
<span class="label">司机:</span>
|
||||
<div class="editable-input signature-input transparent-input" contenteditable>{{ localWaybill.driver }}</div>
|
||||
</div>
|
||||
<div class="footer-item inline">
|
||||
<span class="label">磅房:</span>
|
||||
<div class="editable-input signature-input transparent-input" contenteditable>{{ localWaybill.weightRoom }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 操作按钮 -->
|
||||
@@ -152,7 +181,8 @@
|
||||
<div class="waybill-pagebar">
|
||||
<el-button size="mini" @click="changePage(currentPage - 1)" :disabled="currentPage <= 1">上一页</el-button>
|
||||
<span class="page-info">第 {{ currentPage }} / {{ totalPages }} 页</span>
|
||||
<el-button size="mini" @click="changePage(currentPage + 1)" :disabled="currentPage >= totalPages">下一页</el-button>
|
||||
<el-button size="mini" @click="changePage(currentPage + 1)"
|
||||
:disabled="currentPage >= totalPages">下一页</el-button>
|
||||
</div>
|
||||
<el-button type="primary" @click="saveAsImage">保存为图片</el-button>
|
||||
<el-button type="success" @click="printWaybill">打印</el-button>
|
||||
@@ -396,15 +426,15 @@ export default {
|
||||
scrollY: 0
|
||||
});
|
||||
|
||||
const png = canvas.toDataURL('image/png');
|
||||
const imgPng = await pdfDoc.embedPng(png);
|
||||
const png = canvas.toDataURL('image/png');
|
||||
const imgPng = await pdfDoc.embedPng(png);
|
||||
const pdfPage = pdfDoc.addPage([pageWidthPt, pageHeightPt]);
|
||||
pdfPage.drawImage(imgPng, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: pageWidthPt,
|
||||
height: pageHeightPt
|
||||
});
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: pageWidthPt,
|
||||
height: pageHeightPt
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
// 恢复截图前的容器样式
|
||||
@@ -628,20 +658,20 @@ export default {
|
||||
|
||||
} */
|
||||
|
||||
/* 重量(kg) */
|
||||
/* 重量(kg) */
|
||||
.waybill-table th:nth-child(10),
|
||||
.waybill-table td:nth-child(10) {
|
||||
width: 70px;
|
||||
|
||||
}
|
||||
|
||||
/* 单价 */
|
||||
/* 单价 */
|
||||
.waybill-table th:nth-child(11),
|
||||
.waybill-table td:nth-child(11) {
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
/* 备注 */
|
||||
/* 备注 */
|
||||
.waybill-table th:nth-child(12),
|
||||
.waybill-table td:nth-child(12) {
|
||||
/* width: 40px; */
|
||||
@@ -827,6 +857,7 @@ export default {
|
||||
}
|
||||
|
||||
@media print {
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.klp.service.impl;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import com.baomidou.dynamic.datasource.annotation.DS;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
@@ -51,6 +52,7 @@ import java.math.BigDecimal;
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
@DS("master")
|
||||
public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
|
||||
|
||||
private final WmsMaterialCoilMapper baseMapper;
|
||||
@@ -401,8 +403,6 @@ public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
|
||||
qw.eq(StringUtils.isNotBlank(bo.getItemType()), "mc.item_type", bo.getItemType());
|
||||
qw.eq(StringUtils.isNotBlank(bo.getCreateBy()), "mc.create_by", bo.getCreateBy());
|
||||
qw.eq(StringUtils.isNotBlank(bo.getUpdateBy()), "mc.update_by", bo.getUpdateBy());
|
||||
//逻辑删除
|
||||
qw.eq("mc.del_flag", 0);
|
||||
// 切边要求
|
||||
qw.eq(StringUtils.isNotBlank(bo.getTrimmingRequirement()), "mc.trimming_requirement", bo.getTrimmingRequirement());
|
||||
// 打包状态
|
||||
@@ -584,6 +584,8 @@ public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
|
||||
// "WHERE dp.del_flag = 0 AND dp.coil IS NOT NULL AND dp.coil <> '' " +
|
||||
// "AND FIND_IN_SET(CAST(mc.coil_id AS CHAR), dp.coil))");
|
||||
// }
|
||||
//逻辑删除
|
||||
qw.eq("mc.del_flag", 0);
|
||||
//把team字段作为筛选条件
|
||||
qw.eq(StringUtils.isNotBlank(bo.getTeam()), "mc.team", bo.getTeam());
|
||||
//根据开始时间和结束时间筛选修改时间
|
||||
|
||||
Reference in New Issue
Block a user