更新快速排查功能

This commit is contained in:
2026-03-14 15:06:18 +08:00
parent 8f1e8c9381
commit 8cc5549850
13 changed files with 1540 additions and 1 deletions

View File

@@ -0,0 +1,55 @@
package com.klp.aps.controller;
import com.klp.aps.domain.dto.ApsQuickSheetQueryReq;
import com.klp.aps.domain.dto.ApsQuickSheetSaveReq;
import com.klp.aps.domain.vo.ApsQuickSheetRowVo;
import com.klp.aps.service.ApsQuickSheetService;
import com.klp.common.core.controller.BaseController;
import com.klp.common.core.domain.R;
import com.klp.common.helper.LoginHelper;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RequiredArgsConstructor
@RestController
@RequestMapping("/aps/quick-sheet")
public class ApsQuickSheetController extends BaseController {
private final ApsQuickSheetService quickSheetService;
@GetMapping("/list")
public R<List<ApsQuickSheetRowVo>> list(@Validated ApsQuickSheetQueryReq req) {
return R.ok(quickSheetService.queryList(req));
}
@GetMapping("/preset")
public R<List<ApsQuickSheetRowVo>> preset(@RequestParam(value = "lineId", required = false) Long lineId) {
String salesman = LoginHelper.getNickName();
return R.ok(quickSheetService.buildPresetRows(lineId, salesman));
}
@PostMapping("/save")
public R<Void> save(@Validated @RequestBody ApsQuickSheetSaveReq req) {
quickSheetService.saveRows(req, getUsername());
return R.ok();
}
@GetMapping("/export")
public void export(@Validated ApsQuickSheetQueryReq req, javax.servlet.http.HttpServletResponse response) {
quickSheetService.exportExcel(req, response);
}
@PostMapping("/delete")
public R<Void> delete(@RequestParam("quickSheetId") Long quickSheetId) {
quickSheetService.deleteById(quickSheetId, getUsername());
return R.ok();
}
}

View File

@@ -0,0 +1,20 @@
package com.klp.aps.domain.dto;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
@Data
public class ApsQuickSheetQueryReq {
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate startDate;
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate endDate;
private Long lineId;
private String customerName;
}

View File

@@ -0,0 +1,35 @@
package com.klp.aps.domain.dto;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
import java.util.List;
@Data
public class ApsQuickSheetSaveReq {
@NotEmpty(message = "rows 不能为空")
private List<Row> rows;
@Data
public static class Row {
private Long quickSheetId;
private Long lineId;
private String lineName;
private String planCode;
private String orderCode;
private String customerName;
private String salesman;
private String productName;
private String rawMaterialId;
private String rawCoilNos;
private String rawLocation;
private String rawPackaging;
private String rawEdgeReq;
private String rawCoatingType;
private String rawNetWeight;
private String planQty;
private String startTime;
private String endTime;
}
}

View File

@@ -0,0 +1,35 @@
package com.klp.aps.domain.entity;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
public class ApsQuickSheetEntity {
private Long quickSheetId;
private LocalDate planDate;
private Long lineId;
private String lineName;
private String planCode;
private String orderCode;
private String customerName;
private String salesman;
private String productName;
private String rawMaterialId;
private String rawCoilNos;
private String rawLocation;
private String rawPackaging;
private String rawEdgeReq;
private String rawCoatingType;
private BigDecimal rawNetWeight;
private BigDecimal planQty;
private LocalDateTime startTime;
private LocalDateTime endTime;
private String createBy;
private String updateBy;
private LocalDateTime createTime;
private LocalDateTime updateTime;
private Integer delFlag;
}

View File

@@ -0,0 +1,28 @@
package com.klp.aps.domain.vo;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
public class ApsQuickSheetRowVo {
private Long quickSheetId;
private Long lineId;
private String lineName;
private String planCode;
private String orderCode;
private String customerName;
private String salesman;
private String productName;
private String rawMaterialId;
private String rawCoilNos;
private String rawLocation;
private String rawPackaging;
private String rawEdgeReq;
private String rawCoatingType;
private BigDecimal rawNetWeight;
private BigDecimal planQty;
private LocalDateTime startTime;
private LocalDateTime endTime;
}

View File

@@ -0,0 +1,73 @@
package com.klp.aps.mapper;
import com.klp.aps.domain.vo.ApsQuickSheetRowVo;
import com.klp.aps.domain.dto.ApsQuickSheetQueryReq;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
public interface ApsQuickSheetMapper {
List<ApsQuickSheetRowVo> selectList(ApsQuickSheetQueryReq req);
@Select("SELECT COUNT(1) FROM aps_quick_sheet WHERE del_flag = 0 AND plan_date = #{planDate}")
int countToday(@Param("planDate") java.time.LocalDate planDate);
@Select("SELECT quick_sheet_id FROM aps_quick_sheet WHERE plan_code = #{planCode} AND del_flag = 0 LIMIT 1")
Long selectIdByPlanCode(@Param("planCode") String planCode);
@org.apache.ibatis.annotations.Insert("INSERT INTO aps_quick_sheet (line_id, line_name, plan_date, plan_code, order_code, customer_name, salesman, product_name, raw_material_id, raw_coil_nos, raw_location, raw_packaging, raw_edge_req, raw_coating_type, raw_net_weight, plan_qty, start_time, end_time, create_by, update_by, create_time, update_time, del_flag) "
+ "VALUES (#{lineId}, #{lineName}, #{planDate}, #{planCode}, #{orderCode}, #{customerName}, #{salesman}, #{productName}, #{rawMaterialId}, #{rawCoilNos}, #{rawLocation}, #{rawPackaging}, #{rawEdgeReq}, #{rawCoatingType}, #{rawNetWeight}, #{planQty}, #{startTime}, #{endTime}, #{createBy}, #{updateBy}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 0)")
int insertRow(@Param("lineId") Long lineId,
@Param("lineName") String lineName,
@Param("planDate") java.time.LocalDate planDate,
@Param("planCode") String planCode,
@Param("orderCode") String orderCode,
@Param("customerName") String customerName,
@Param("salesman") String salesman,
@Param("productName") String productName,
@Param("rawMaterialId") String rawMaterialId,
@Param("rawCoilNos") String rawCoilNos,
@Param("rawLocation") String rawLocation,
@Param("rawPackaging") String rawPackaging,
@Param("rawEdgeReq") String rawEdgeReq,
@Param("rawCoatingType") String rawCoatingType,
@Param("rawNetWeight") java.math.BigDecimal rawNetWeight,
@Param("planQty") java.math.BigDecimal planQty,
@Param("startTime") java.time.LocalDateTime startTime,
@Param("endTime") java.time.LocalDateTime endTime,
@Param("createBy") String createBy,
@Param("updateBy") String updateBy);
@org.apache.ibatis.annotations.Update("UPDATE aps_quick_sheet SET line_id = #{lineId}, line_name = #{lineName}, plan_code = #{planCode}, order_code = #{orderCode}, customer_name = #{customerName}, salesman = #{salesman}, product_name = #{productName}, raw_material_id = #{rawMaterialId}, raw_coil_nos = #{rawCoilNos}, raw_location = #{rawLocation}, raw_packaging = #{rawPackaging}, raw_edge_req = #{rawEdgeReq}, raw_coating_type = #{rawCoatingType}, raw_net_weight = #{rawNetWeight}, plan_qty = #{planQty}, start_time = #{startTime}, end_time = #{endTime}, update_by = #{updateBy}, update_time = CURRENT_TIMESTAMP WHERE quick_sheet_id = #{id}")
int updateRow(@Param("id") Long id,
@Param("lineId") Long lineId,
@Param("lineName") String lineName,
@Param("planCode") String planCode,
@Param("orderCode") String orderCode,
@Param("customerName") String customerName,
@Param("salesman") String salesman,
@Param("productName") String productName,
@Param("rawMaterialId") String rawMaterialId,
@Param("rawCoilNos") String rawCoilNos,
@Param("rawLocation") String rawLocation,
@Param("rawPackaging") String rawPackaging,
@Param("rawEdgeReq") String rawEdgeReq,
@Param("rawCoatingType") String rawCoatingType,
@Param("rawNetWeight") java.math.BigDecimal rawNetWeight,
@Param("planQty") java.math.BigDecimal planQty,
@Param("startTime") java.time.LocalDateTime startTime,
@Param("endTime") java.time.LocalDateTime endTime,
@Param("updateBy") String updateBy);
@org.apache.ibatis.annotations.Update("UPDATE aps_quick_sheet SET del_flag = 1, update_by = #{updateBy}, update_time = CURRENT_TIMESTAMP WHERE quick_sheet_id = #{id}")
int deleteRow(@Param("id") Long id, @Param("updateBy") String updateBy);
@org.apache.ibatis.annotations.Update("UPDATE aps_quick_sheet SET del_flag = 1, update_by = #{updateBy}, update_time = CURRENT_TIMESTAMP WHERE quick_sheet_id = #{id}")
int softDelete(@Param("id") Long id,
@Param("updateBy") String updateBy);
@org.apache.ibatis.annotations.Update("UPDATE aps_quick_sheet SET del_flag = 1, update_by = #{updateBy}, update_time = CURRENT_TIMESTAMP WHERE quick_sheet_id = #{id}")
int deleteById(@Param("id") Long id, @Param("updateBy") String updateBy);
}

View File

@@ -0,0 +1,20 @@
package com.klp.aps.service;
import com.klp.aps.domain.dto.ApsQuickSheetQueryReq;
import com.klp.aps.domain.dto.ApsQuickSheetSaveReq;
import com.klp.aps.domain.vo.ApsQuickSheetRowVo;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
public interface ApsQuickSheetService {
List<ApsQuickSheetRowVo> queryList(ApsQuickSheetQueryReq req);
void saveRows(ApsQuickSheetSaveReq req, String operator);
List<ApsQuickSheetRowVo> buildPresetRows(Long lineId, String salesman);
void exportExcel(ApsQuickSheetQueryReq req, HttpServletResponse response);
void deleteById(Long id, String operator);
}

View File

@@ -0,0 +1,214 @@
package com.klp.aps.service.impl;
import com.klp.aps.domain.dto.ApsQuickSheetSaveReq;
import com.klp.aps.domain.vo.ApsQuickSheetRowVo;
import com.klp.aps.mapper.ApsQuickSheetMapper;
import com.klp.aps.service.ApsQuickSheetService;
import com.klp.common.exception.ServiceException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
@RequiredArgsConstructor
@Service
public class ApsQuickSheetServiceImpl implements ApsQuickSheetService {
private final ApsQuickSheetMapper quickSheetMapper;
private static final DateTimeFormatter DATE_CODE = DateTimeFormatter.ofPattern("yyyyMMdd");
@Override
public List<ApsQuickSheetRowVo> queryList(com.klp.aps.domain.dto.ApsQuickSheetQueryReq req) {
return quickSheetMapper.selectList(req);
}
@Override
public void saveRows(ApsQuickSheetSaveReq req, String operator) {
if (req == null || req.getRows() == null) {
throw new ServiceException("保存数据不能为空");
}
for (ApsQuickSheetSaveReq.Row row : req.getRows()) {
if (row == null) continue;
Long id = row.getQuickSheetId();
Long lineId = row.getLineId();
String lineName = row.getLineName();
String planCode = row.getPlanCode();
String orderCode = row.getOrderCode();
String customerName = row.getCustomerName();
String salesman = row.getSalesman();
String productName = row.getProductName();
String rawMaterialId = row.getRawMaterialId();
String rawCoilNos = row.getRawCoilNos();
String rawLocation = row.getRawLocation();
String rawPackaging = row.getRawPackaging();
String rawEdgeReq = row.getRawEdgeReq();
String rawCoatingType = row.getRawCoatingType();
BigDecimal rawNetWeight = parseQty(row.getRawNetWeight());
BigDecimal planQty = parseQty(row.getPlanQty());
LocalDateTime startTime = parseTime(row.getStartTime());
LocalDateTime endTime = parseTime(row.getEndTime());
boolean hasAny = lineId != null || isNotBlank(lineName) || isNotBlank(planCode) || isNotBlank(orderCode)
|| isNotBlank(customerName) || isNotBlank(salesman) || isNotBlank(productName)
|| isNotBlank(rawMaterialId) || isNotBlank(rawCoilNos) || isNotBlank(rawLocation)
|| isNotBlank(rawPackaging) || isNotBlank(rawEdgeReq) || isNotBlank(rawCoatingType)
|| rawNetWeight != null || planQty != null || startTime != null || endTime != null;
if (!hasAny) {
continue;
}
if (id == null) {
if (!isNotBlank(planCode)) {
planCode = buildPlanCode();
}
quickSheetMapper.insertRow(lineId, lineName, LocalDate.now(), planCode, orderCode, customerName, salesman, productName,
rawMaterialId, rawCoilNos, rawLocation, rawPackaging, rawEdgeReq, rawCoatingType, rawNetWeight, planQty, startTime, endTime, operator, operator);
} else {
quickSheetMapper.updateRow(id, lineId, lineName, planCode, orderCode, customerName, salesman, productName,
rawMaterialId, rawCoilNos, rawLocation, rawPackaging, rawEdgeReq, rawCoatingType, rawNetWeight, planQty, startTime, endTime, operator);
}
}
}
@Override
public List<ApsQuickSheetRowVo> buildPresetRows(Long lineId, String salesman) {
List<ApsQuickSheetRowVo> rows = new ArrayList<>();
LocalDateTime now = LocalDateTime.now();
LocalDateTime end = now.plusHours(1);
for (int i = 0; i < 3; i++) {
ApsQuickSheetRowVo row = new ApsQuickSheetRowVo();
row.setLineId(lineId);
row.setLineName(lineId == null ? null : ("产线" + lineId));
row.setSalesman(salesman);
row.setPlanQty(BigDecimal.ZERO);
row.setStartTime(now);
row.setEndTime(end);
row.setPlanCode(buildPlanCode());
rows.add(row);
}
return rows;
}
@Override
public void exportExcel(com.klp.aps.domain.dto.ApsQuickSheetQueryReq req, javax.servlet.http.HttpServletResponse response) {
List<ApsQuickSheetRowVo> rows = queryList(req);
try (org.apache.poi.ss.usermodel.Workbook wb = new org.apache.poi.xssf.usermodel.XSSFWorkbook()) {
org.apache.poi.ss.usermodel.Sheet sheet = wb.createSheet("快速排产表");
int r = 0;
org.apache.poi.ss.usermodel.Row title = sheet.createRow(r++);
title.setHeightInPoints(36f);
org.apache.poi.ss.usermodel.Cell t0 = title.createCell(0);
t0.setCellValue("快速排产表Excel录入");
sheet.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(0, 0, 0, 14));
org.apache.poi.ss.usermodel.CellStyle titleStyle = wb.createCellStyle();
titleStyle.setAlignment(org.apache.poi.ss.usermodel.HorizontalAlignment.CENTER);
titleStyle.setVerticalAlignment(org.apache.poi.ss.usermodel.VerticalAlignment.CENTER);
org.apache.poi.xssf.usermodel.XSSFFont titleFont = ((org.apache.poi.xssf.usermodel.XSSFWorkbook) wb).createFont();
titleFont.setBold(true);
titleFont.setFontHeightInPoints((short) 15);
titleStyle.setFont(titleFont);
t0.setCellStyle(titleStyle);
String[] headers = new String[]{
"产线", "计划号", "订单号", "客户", "业务员", "产品",
"原料钢卷", "原料卷号", "钢卷位置", "包装要求", "切边要求", "镀层种类",
"原料净重", "计划数量", "开始时间", "结束时间"
};
org.apache.poi.ss.usermodel.Row head = sheet.createRow(r++);
for (int i = 0; i < headers.length; i++) {
head.createCell(i).setCellValue(headers[i]);
}
if (rows != null) {
for (ApsQuickSheetRowVo row : rows) {
org.apache.poi.ss.usermodel.Row rr = sheet.createRow(r++);
int cc = 0;
rr.createCell(cc++).setCellValue(nvl(row.getLineName(), row.getLineId()));
rr.createCell(cc++).setCellValue(nvl(row.getPlanCode(), ""));
rr.createCell(cc++).setCellValue(nvl(row.getOrderCode(), ""));
rr.createCell(cc++).setCellValue(nvl(row.getCustomerName(), ""));
rr.createCell(cc++).setCellValue(nvl(row.getSalesman(), ""));
rr.createCell(cc++).setCellValue(nvl(row.getProductName(), ""));
rr.createCell(cc++).setCellValue(nvl(row.getRawMaterialId(), ""));
rr.createCell(cc++).setCellValue(nvl(row.getRawCoilNos(), ""));
rr.createCell(cc++).setCellValue(nvl(row.getRawLocation(), ""));
rr.createCell(cc++).setCellValue(nvl(row.getRawPackaging(), ""));
rr.createCell(cc++).setCellValue(nvl(row.getRawEdgeReq(), ""));
rr.createCell(cc++).setCellValue(nvl(row.getRawCoatingType(), ""));
rr.createCell(cc++).setCellValue(row.getRawNetWeight() == null ? "" : row.getRawNetWeight().toPlainString());
rr.createCell(cc++).setCellValue(row.getPlanQty() == null ? "" : row.getPlanQty().toPlainString());
rr.createCell(cc++).setCellValue(row.getStartTime() == null ? "" : row.getStartTime().toString());
rr.createCell(cc++).setCellValue(row.getEndTime() == null ? "" : row.getEndTime().toString());
}
}
for (int i = 0; i < headers.length; i++) {
sheet.autoSizeColumn(i, true);
int w = sheet.getColumnWidth(i);
sheet.setColumnWidth(i, Math.min(Math.max(w, 3000), 12000));
}
String filename = "aps_quick_sheet_" + System.currentTimeMillis() + ".xlsx";
String encoded = java.net.URLEncoder.encode(filename, java.nio.charset.StandardCharsets.UTF_8.name());
response.setCharacterEncoding(java.nio.charset.StandardCharsets.UTF_8.name());
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + encoded);
try (javax.servlet.ServletOutputStream os = response.getOutputStream()) {
wb.write(os);
os.flush();
}
} catch (java.io.IOException e) {
throw new ServiceException("导出失败:" + e.getMessage());
}
}
private String buildPlanCode() {
LocalDate today = LocalDate.now();
int seq = quickSheetMapper.countToday(today) + 1;
return today.format(DATE_CODE) + String.format("%03d", seq);
}
private String nvl(Object v, Object fallback) {
if (v == null) return String.valueOf(fallback);
String s = String.valueOf(v);
return s == null ? String.valueOf(fallback) : s;
}
@Override
public void deleteById(Long id, String operator) {
if (id == null) return;
quickSheetMapper.deleteRow(id, operator);
}
private BigDecimal parseQty(String val) {
if (val == null || val.trim().isEmpty()) return null;
try {
return new BigDecimal(val.trim());
} catch (Exception e) {
return null;
}
}
private boolean isNotBlank(String val) {
return val != null && !val.trim().isEmpty();
}
private LocalDateTime parseTime(String val) {
if (val == null || val.trim().isEmpty()) return null;
String v = val.trim();
if (v.length() == 16) v = v + ":00";
try {
return LocalDateTime.parse(v, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
} catch (Exception e) {
return null;
}
}
}

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.klp.aps.mapper.ApsQuickSheetMapper">
<select id="selectList" parameterType="com.klp.aps.domain.dto.ApsQuickSheetQueryReq" resultType="com.klp.aps.domain.vo.ApsQuickSheetRowVo">
SELECT
quick_sheet_id AS quickSheetId,
line_id AS lineId,
line_name AS lineName,
plan_code AS planCode,
order_code AS orderCode,
customer_name AS customerName,
salesman,
product_name AS productName,
raw_material_id AS rawMaterialId,
raw_coil_nos AS rawCoilNos,
raw_location AS rawLocation,
raw_packaging AS rawPackaging,
raw_edge_req AS rawEdgeReq,
raw_coating_type AS rawCoatingType,
raw_net_weight AS rawNetWeight,
plan_qty AS planQty,
start_time AS startTime,
end_time AS endTime
FROM aps_quick_sheet
WHERE del_flag = 0
<if test="startDate != null">
AND start_time <![CDATA[>=]]> CONCAT(#{startDate}, ' 00:00:00')
</if>
<if test="endDate != null">
AND start_time <![CDATA[<=]]> CONCAT(#{endDate}, ' 23:59:59')
</if>
<if test="lineId != null">
AND line_id = #{lineId}
</if>
<if test="customerName != null and customerName != ''">
AND customer_name LIKE CONCAT('%', #{customerName}, '%')
</if>
ORDER BY quick_sheet_id DESC
</select>
</mapper>

View File

@@ -0,0 +1,42 @@
import request from '@/utils/request'
export function fetchQuickSheetList(params) {
return request({
url: '/aps/quick-sheet/list',
method: 'get',
params
})
}
export function fetchQuickSheetPreset(params) {
return request({
url: '/aps/quick-sheet/preset',
method: 'get',
params
})
}
export function saveQuickSheet(data) {
return request({
url: '/aps/quick-sheet/save',
method: 'post',
data
})
}
export function exportQuickSheet(params) {
return request({
url: '/aps/quick-sheet/export',
method: 'get',
params,
responseType: 'blob'
})
}
export function deleteQuickSheetRow(quickSheetId) {
return request({
url: '/aps/quick-sheet/delete',
method: 'post',
params: { quickSheetId }
})
}

View File

@@ -0,0 +1,748 @@
<template>
<div class="aps-quick-sheet excel-theme">
<div class="sheet-toolbar">
<div class="sheet-actions">
<el-button size="small" icon="el-icon-refresh" @click="loadRows">刷新</el-button>
<el-button size="small" type="primary" icon="el-icon-finished" :loading="saving" @click="saveAll">保存</el-button>
<el-button size="small" icon="el-icon-plus" @click="addRow">新增行</el-button>
<el-button size="small" icon="el-icon-download" @click="exportCsv">导出</el-button>
<el-date-picker
v-model="filter.range"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="yyyy-MM-dd"
size="small"
style="width: 240px"
@change="onFilterChange"
/>
</div>
<div class="preset-bar">
<span class="preset-title">预设方案</span>
<el-button
v-for="line in lineOptions"
:key="line.lineId"
size="mini"
plain
@click="applyPreset(line)"
>
{{ line.lineName || line.lineCode || ('产线' + line.lineId) }}
</el-button>
</div>
</div>
<div class="sheet-body">
<el-table
v-loading="loading"
:data="displayRows"
border
size="mini"
class="excel-table"
>
<el-table-column label="序号" width="60" align="center">
<template slot-scope="scope">
{{ ((pager.pageNum - 1) * pager.pageSize) + scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column
v-for="col in flatColumns"
:key="col.prop || col.label"
:label="col.label"
:prop="col.prop"
:width="col.width"
:min-width="col.minWidth"
:align="col.align || 'center'"
:show-overflow-tooltip="col.showOverflowTooltip !== false"
>
<template slot-scope="scope">
<template v-if="col.prop === 'rawMaterialId'">
<el-button
size="mini"
type="text"
class="coil-picker-btn"
@click="openCoilPicker(scope.row)"
>
{{ scope.row.rawCoilNos || '选择原料钢卷' }}
</el-button>
</template>
<template v-else-if="isTimeField(col.prop)">
<el-date-picker
v-model="scope.row[col.prop]"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
size="mini"
style="width: 100%"
@change="onCellChange(scope.row)"
@input="onCellChange(scope.row)"
/>
</template>
<template v-else>
<el-autocomplete
v-if="col.prop === 'lineName'"
v-model="scope.row[col.prop]"
size="mini"
clearable
:fetch-suggestions="queryLineHistory"
@select="onLineHistorySelect(scope.row)"
@input="onLineHistoryInput(scope.row)"
/>
<el-input
v-else
v-model="scope.row[col.prop]"
size="mini"
clearable
@input="onCellChange(scope.row)"
/>
</template>
</template>
</el-table-column>
<el-table-column label="操作" width="70" fixed="right" align="center">
<template slot-scope="scope">
<el-button type="text" size="mini" @click="clearRow(scope.row)">清空</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager-wrap">
<el-pagination
small
background
layout="prev, pager, next, total"
:current-page.sync="pager.pageNum"
:page-size="pager.pageSize"
:total="totalRows"
/>
</div>
</div>
<div v-if="missingRows.length" class="missing-tip">
<div v-for="msg in missingRows" :key="msg" class="missing-item">{{ msg }}</div>
</div>
<el-dialog title="选择原料钢卷" :visible.sync="coilPicker.visible" width="1280px" append-to-body>
<el-form :inline="true" size="small" class="coil-picker-form">
<el-form-item label="库区">
<el-select v-model="coilPicker.selectedWarehouseId" clearable filterable placeholder="全部库区" style="width: 220px" @change="onCoilFilterChange">
<el-option
v-for="w in coilPicker.warehouseOptions"
:key="w.actualWarehouseId"
:label="w.actualWarehouseName"
:value="w.actualWarehouseId"
/>
</el-select>
</el-form-item>
<el-form-item label="入场钢卷号">
<el-input v-model="coilPicker.enterCoilNo" clearable placeholder="支持模糊" style="width: 170px" @keyup.enter.native="searchCoils" />
</el-form-item>
<el-form-item label="当前钢卷号">
<el-input v-model="coilPicker.currentCoilNo" clearable placeholder="支持模糊" style="width: 170px" @keyup.enter.native="searchCoils" />
</el-form-item>
<el-form-item label="厂家">
<el-input v-model="coilPicker.manufacturer" clearable placeholder="支持模糊" style="width: 170px" @keyup.enter.native="searchCoils" />
</el-form-item>
<el-form-item>
<el-button size="mini" type="primary" @click="searchCoils">查询</el-button>
</el-form-item>
</el-form>
<div class="coil-stack-wrap" v-loading="coilPicker.loading">
<el-empty v-if="!coilColumnKeys.length" description="暂无可选原料钢卷" :image-size="60" />
<div v-else class="coil-bird-wrap">
<div class="coil-legend">
<span class="legend-item"><i class="dot layer1" />一层</span>
<span class="legend-item"><i class="dot layer2" />二层</span>
<span class="legend-item"><i class="dot occupied" />已占用</span>
</div>
<div class="coil-grid-scroll">
<div class="coil-col-ruler">
<div class="ruler-empty" />
<div v-for="col in coilColumnKeys" :key="`ruler-${col}`" class="ruler-col">{{ col }}</div>
</div>
<div class="coil-grid-main">
<div class="coil-row-ruler">
<div v-for="row in coilMaxRow" :key="`row-${row}`" class="ruler-row">{{ row }}</div>
</div>
<div class="coil-grid-columns">
<div v-for="col in coilColumnKeys" :key="`col-${col}`" class="coil-col-pair">
<div class="coil-layer">
<div
v-for="row in coilMaxRow"
:key="`l1-${col}-${row}`"
class="coil-cell layer1"
:class="{ occupied: !!(getCoilCell(col, row, 1) && getCoilCell(col, row, 1).rawMaterialId) }"
@click="selectCoilCell(col, row, 1)"
>
<template v-if="getCoilCell(col, row, 1)">
<div class="line1" :title="getCoilCell(col, row, 1).rawLocation">{{ getCoilCell(col, row, 1).rawLocation }}</div>
<div class="line2" :title="getCoilCell(col, row, 1).rawMaterialCode">{{ getCoilCell(col, row, 1).rawMaterialCode || '-' }}</div>
</template>
<template v-else>-</template>
</div>
</div>
<div class="coil-layer layer2-shift">
<div
v-for="row in (coilMaxRow - 1)"
:key="`l2-${col}-${row}`"
class="coil-cell layer2"
:class="{ occupied: !!(getCoilCell(col, row, 2) && getCoilCell(col, row, 2).rawMaterialId) }"
@click="selectCoilCell(col, row, 2)"
>
<template v-if="getCoilCell(col, row, 2)">
<div class="line1" :title="getCoilCell(col, row, 2).rawLocation">{{ getCoilCell(col, row, 2).rawLocation }}</div>
<div class="line2" :title="getCoilCell(col, row, 2).rawMaterialCode">{{ getCoilCell(col, row, 2).rawMaterialCode || '-' }}</div>
</template>
<template v-else>-</template>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="coil-picker-foot"> {{ coilPicker.total || 0 }} 条钢卷按库位分布展示</div>
</el-dialog>
</div>
</template>
<script>
import { fetchQuickSheetList, fetchQuickSheetPreset, saveQuickSheet, exportQuickSheet, deleteQuickSheetRow } from '@/api/aps/quickSheet'
import { listProductionLine } from '@/api/wms/productionLine'
import { getMaterialCoilLocationGrid } from '@/api/wms/coil'
import { treeActualWarehouseTwoLevel } from '@/api/wms/actualWarehouse'
import { getTemplateByKey } from './sheets/templates'
import { saveAs } from 'file-saver'
export default {
name: 'ApsQuickSheet',
data() {
return {
loading: false,
saving: false,
rows: [],
lineOptions: [],
lineHistory: [],
dirtyIds: new Set(),
autoSaveTimer: null,
templateKey: 'unified',
filter: {
range: []
},
pager: {
pageNum: 1,
pageSize: 25
},
coilPicker: {
visible: false,
loading: false,
currentRow: null,
selectedWarehouseId: undefined,
enterCoilNo: '',
currentCoilNo: '',
manufacturer: '',
warehouseOptions: [],
coilGrid: {},
total: 0
}
}
},
computed: {
currentTemplate() {
return getTemplateByKey(this.templateKey)
},
flatColumns() {
const res = []
const loop = (cols) => {
;(cols || []).forEach(c => {
if (c.children && c.children.length) loop(c.children)
else res.push(c)
})
}
loop(this.currentTemplate.columns || [])
return res
},
displayRows() {
const [startAfter, endBefore] = this.filter.range || []
const filtered = this.rows.filter(r => {
if (!r.startTime) return true
const d = String(r.startTime).slice(0, 10)
if (startAfter && d < startAfter) return false
if (endBefore && d > endBefore) return false
return true
})
const start = (this.pager.pageNum - 1) * this.pager.pageSize
const page = filtered.slice(start, start + this.pager.pageSize)
if (page.length < this.pager.pageSize) {
return page.concat(this.buildEmptyRows(this.pager.pageSize - page.length))
}
return page
},
totalRows() {
const [startAfter, endBefore] = this.filter.range || []
return this.rows.filter(r => {
if (!r.startTime) return true
const d = String(r.startTime).slice(0, 10)
if (startAfter && d < startAfter) return false
if (endBefore && d > endBefore) return false
return true
}).length
},
missingRows() {
return this.collectMissing(this.displayRows)
},
coilColumnKeys() {
const keys = Object.keys(this.coilPicker.coilGrid || {})
return keys.map(v => Number(v)).filter(v => !Number.isNaN(v)).sort((a, b) => a - b)
},
coilMaxRow() {
let max = 0
Object.values(this.coilPicker.coilGrid || {}).forEach(col => {
const l1 = col.layer1 || []
const l2 = col.layer2 || []
;[...l1, ...l2].forEach(item => {
const row = Number(item._row || 0)
if (row > max) max = row
})
})
return max || 1
}
},
created() {
const today = new Date()
const start = `${today.getFullYear()}-${`${today.getMonth() + 1}`.padStart(2, '0')}-${`${today.getDate()}`.padStart(2, '0')}`
this.filter.range = [start, '']
this.loadRows()
this.loadLines()
},
beforeDestroy() {
if (this.autoSaveTimer) clearTimeout(this.autoSaveTimer)
},
methods: {
async loadLines() {
const res = await listProductionLine({ pageNum: 1, pageSize: 1000 })
this.lineOptions = res.rows || []
},
async loadRows() {
this.loading = true
try {
const res = await fetchQuickSheetList()
this.rows = (res.data || []).map(r => this.normalizeRow(r))
this.lineHistory = this.rows
.map(r => r.lineName)
.filter(v => v && v.trim())
.filter((v, i, arr) => arr.indexOf(v) === i)
} finally {
this.loading = false
}
},
buildEmptyRows(count) {
return Array.from({ length: count }).map(() => this.buildEmptyRow())
},
async applyPreset(line) {
if (!line || !line.lineId) return
const res = await fetchQuickSheetPreset({ lineId: line.lineId })
const preset = res.data || []
if (!preset.length) return
const targetRow = this.rows.find(r => !r.lineId && !r.lineName) || this.buildEmptyRow()
const base = this.normalizeRow(preset[0])
Object.keys(base).forEach(k => {
if (k === 'quickSheetId') return
targetRow[k] = base[k]
})
targetRow.lineId = line.lineId
targetRow.lineName = line.lineName || line.lineCode || ''
if (!this.rows.includes(targetRow)) {
this.rows.push(targetRow)
}
if (this.rows.length < 25) {
this.rows = this.rows.concat(this.buildEmptyRows(25 - this.rows.length))
}
this.markDirty(targetRow)
this.scheduleAutoSave()
},
async loadWarehouseOptions() {
const res = await treeActualWarehouseTwoLevel()
const tree = res.data || []
const leaf = []
tree.forEach(p => {
;(p.children || []).forEach(c => {
leaf.push({
actualWarehouseId: c.actualWarehouseId,
actualWarehouseName: `${p.actualWarehouseName || ''}/${c.actualWarehouseName || ''}`
})
})
})
this.coilPicker.warehouseOptions = leaf
},
async loadCoilsByWarehouse() {
if (!this.coilPicker.selectedWarehouseId) {
this.coilPicker.coilGrid = {}
this.coilPicker.total = 0
return
}
this.coilPicker.loading = true
try {
const gridRes = await getMaterialCoilLocationGrid({
itemType: 'raw_material',
actualWarehouseId: this.coilPicker.selectedWarehouseId,
enterCoilNo: this.coilPicker.enterCoilNo || undefined,
currentCoilNo: this.coilPicker.currentCoilNo || undefined,
manufacturer: this.coilPicker.manufacturer || undefined
})
const data = gridRes.data || {}
const warehouses = data.warehouses || []
const coils = (data.coils || []).map(item => ({
rawMaterialId: item.coilId,
rawMaterialCode: item.currentCoilNo || item.enterCoilNo || item.coilNo || '',
rawMaterialName: item.itemName || item.materialName || '',
specification: item.specification || '',
coilWeight: item.netWeight,
packageType: item.packagingRequirement || item.packageType,
edgeType: item.trimmingRequirement || item.edgeType,
zincLayer: item.coatingType || item.zincLayer,
surfaceTreatmentDesc: item.surfaceTreatmentDesc,
actualWarehouseId: item.actualWarehouseId,
actualWarehouseName: item.actualWarehouseName || '未分配库位',
rawLocation: item.actualWarehouseName || '未分配库位'
}))
this.coilPicker.total = coils.length
const coilByWarehouseId = {}
coils.forEach(c => {
if (c.actualWarehouseId && !coilByWarehouseId[c.actualWarehouseId]) {
coilByWarehouseId[c.actualWarehouseId] = c
}
})
const grid = {}
warehouses.forEach(w => {
const code = String(w.actualWarehouseCode || '')
const m = code.match(/^([A-Za-z0-9]{3})([^-]+)-(\d{2})-(\d+)$/)
if (!m) return
const col = Number(m[2])
const row = Number(m[3])
const layer = Number(m[4])
if (!grid[col]) grid[col] = { layer1: [], layer2: [] }
const coil = coilByWarehouseId[w.actualWarehouseId] || null
const cell = {
_col: col,
_row: row,
_layer: layer,
actualWarehouseId: w.actualWarehouseId,
actualWarehouseCode: w.actualWarehouseCode,
rawLocation: w.actualWarehouseName || w.actualWarehouseCode,
...(coil || {})
}
if (layer === 1) grid[col].layer1.push(cell)
else if (layer === 2) grid[col].layer2.push(cell)
})
Object.keys(grid).forEach(k => {
grid[k].layer1.sort((a, b) => Number(a._row) - Number(b._row))
grid[k].layer2.sort((a, b) => Number(a._row) - Number(b._row))
})
this.coilPicker.coilGrid = grid
} finally {
this.coilPicker.loading = false
}
},
searchCoils() {
this.loadCoilsByWarehouse()
},
onCoilFilterChange() {
this.loadCoilsByWarehouse()
},
openCoilPicker(row) {
this.coilPicker.currentRow = row
this.coilPicker.visible = true
this.loadWarehouseOptions().then(() => {
if (!this.coilPicker.selectedWarehouseId && this.coilPicker.warehouseOptions.length) {
this.coilPicker.selectedWarehouseId = this.coilPicker.warehouseOptions[0].actualWarehouseId
}
this.loadCoilsByWarehouse()
})
},
getCoilCell(col, row, layer) {
const c = this.coilPicker.coilGrid[col]
if (!c) return null
const list = layer === 1 ? (c.layer1 || []) : (c.layer2 || [])
return list.find(i => Number(i._row) === Number(row)) || null
},
selectCoilCell(col, row, layer) {
const item = this.getCoilCell(col, row, layer)
if (!item) return
if (!item.rawMaterialId) {
this.$message.warning('该库位当前无钢卷')
return
}
this.pickRawMaterial(item)
},
pickRawMaterial(item) {
const row = this.coilPicker.currentRow
if (!row) return
this.$set(row, 'rawMaterialId', item.rawMaterialId)
this.$set(row, 'rawCoilNos', item.rawMaterialCode || '')
this.$set(row, 'rawNetWeight', item.coilWeight != null ? item.coilWeight : '')
this.$set(row, 'rawPackaging', item.packageType || '')
this.$set(row, 'rawEdgeReq', item.edgeType || '')
this.$set(row, 'rawCoatingType', item.zincLayer || item.surfaceTreatmentDesc || '')
this.$set(row, 'rawLocation', item.rawLocation || '')
this.coilPicker.visible = false
this.onCellChange(row)
},
onCellChange(row) {
if (row && !this.rows.includes(row)) {
this.rows.push(row)
}
this.markDirty(row)
this.scheduleAutoSave()
},
queryLineHistory(query, cb) {
const list = this.lineHistory || []
const q = (query || '').trim().toLowerCase()
const res = list
.filter(item => !q || String(item).toLowerCase().includes(q))
.map(item => ({ value: item }))
cb(res)
},
onLineHistoryInput(row) {
if (!row) return
if (row.lineId && row.lineName && row.lineName.trim() !== String(row.lineName).trim()) {
row.lineId = null
}
this.onCellChange(row)
},
onLineHistorySelect(row) {
return () => {
this.onCellChange(row)
}
},
onFilterChange() {
this.pager.pageNum = 1
},
normalizeRow(row) {
const base = this.buildEmptyRow()
Object.keys(base).forEach(k => {
if (row && row[k] !== undefined && row[k] !== null) base[k] = row[k]
})
const idVal = row ? (row.quickSheetId !== undefined && row.quickSheetId !== null ? row.quickSheetId : row.id) : null
base.quickSheetId = idVal != null ? idVal : null
return base
},
buildEmptyRow() {
const row = { quickSheetId: null }
this.flatColumns.forEach(col => {
if (!col.prop) return
row[col.prop] = ''
})
return row
},
isRowEmpty(row) {
if (!row) return true
return !this.flatColumns.some(col => col.prop && row[col.prop])
},
isTimeField(prop) {
return prop === 'startTime' || prop === 'endTime'
},
markDirty(row) {
if (!row) return
if (!row._tmpId) row._tmpId = Math.random().toString(36).slice(2)
this.dirtyIds.add(row._tmpId)
},
async clearRow(row) {
if (!row) return
if (!row.quickSheetId) {
this.flatColumns.forEach(col => {
if (!col.prop) return
row[col.prop] = ''
})
row.quickSheetId = null
return
}
await deleteQuickSheetRow(row.quickSheetId)
this.flatColumns.forEach(col => {
if (!col.prop) return
row[col.prop] = ''
})
row.quickSheetId = null
},
scheduleAutoSave() {
if (this.autoSaveTimer) clearTimeout(this.autoSaveTimer)
this.autoSaveTimer = setTimeout(() => {
this.saveAll(true)
}, 800)
},
async saveAll(isAuto = false, payload) {
const rows = payload && payload.rows
? payload.rows
: this.rows.filter(r => r && r._tmpId && this.dirtyIds.has(r._tmpId))
.map(r => ({
...r,
quickSheetId: r.quickSheetId || null
}))
if (!rows.length) {
if (!isAuto) this.$message.warning('暂无可保存数据')
return
}
const missing = this.collectMissing(rows)
if (missing.length && !isAuto) {
this.$message.warning(`必填项未填写:${missing.join('')}`)
return
}
this.saving = true
try {
await saveQuickSheet({ rows })
if (!isAuto) this.$message.success('保存成功')
rows.forEach(r => {
if (r._tmpId) this.dirtyIds.delete(r._tmpId)
})
} finally {
this.saving = false
}
},
addRow() {
this.rows.push(this.buildEmptyRow())
if (this.rows.length < this.pager.pageSize) {
this.rows = this.rows.concat(this.buildEmptyRows(this.pager.pageSize - this.rows.length))
}
},
onFilterChange() {
this.pager.pageNum = 1
},
exportCsv() {
this.loading = true
exportQuickSheet()
.then(blob => {
const filename = `quick_sheet_${new Date().getTime()}.xlsx`
saveAs(new Blob([blob], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }), filename)
})
.finally(() => {
this.loading = false
})
},
addRow() {
this.rows.unshift(this.buildEmptyRow())
this.pager.pageNum = 1
},
collectMissing(rows) {
const msgs = []
rows.forEach((row, idx) => {
if (this.isRowEmpty(row)) return
const line = idx + 1
const missing = []
if (!row.lineName && !row.lineId) missing.push('产线')
if (!row.planCode) missing.push('计划号')
if (!row.startTime) missing.push('开始时间')
if (!row.endTime) missing.push('结束时间')
if (missing.length) {
msgs.push(`${line}行缺少${missing.join('、')}`)
}
})
return msgs
}
}
}
</script>
<style scoped lang="scss">
.aps-quick-sheet {
padding: 8px;
background: #f7f9fc;
}
.sheet-toolbar,
.sheet-body {
border: 1px solid #eef2f7;
border-radius: 8px;
background: #fff;
}
.sheet-toolbar {
margin-bottom: 8px;
padding: 8px 12px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.sheet-actions { display: flex; gap: 8px; }
.preset-bar { display: flex; align-items: center; gap: 8px; font-size: 12px; color: #667085; }
.preset-title { font-weight: 600; color: #475467; }
::v-deep .excel-table {
border: 1px solid #edf1f7;
}
::v-deep .excel-table th.el-table__cell {
background: #fafbfe;
color: #5d6b82;
font-weight: 600;
border-right: 1px solid #edf1f7;
border-bottom: 1px solid #edf1f7;
padding: 4px 0;
}
::v-deep .excel-table td.el-table__cell {
border-right: 1px solid #f0f2f6;
border-bottom: 1px solid #f0f2f6;
padding: 1px 2px;
}
::v-deep .excel-table .el-table__row:hover > td {
background-color: #f7faff !important;
}
::v-deep .el-input__inner {
border-radius: 0;
height: 26px;
line-height: 26px;
border: none;
box-shadow: none;
background: transparent;
}
::v-deep .el-input__inner:focus {
border: none;
box-shadow: inset 0 0 0 1px #4a90e2;
background: #fff;
}
.missing-tip {
margin-top: 8px;
padding: 8px 12px;
background: #fff7e6;
border: 1px solid #ffd591;
border-radius: 6px;
font-size: 12px;
color: #d46b08;
}
.missing-item {
line-height: 1.6;
}
.coil-picker-btn {
padding: 0;
}
.coil-bird-wrap { border: 1px solid #ebeef5; border-radius: 6px; padding: 8px; }
.coil-legend { display: flex; gap: 14px; font-size: 12px; color: #606266; margin-bottom: 8px; }
.legend-item { display: inline-flex; align-items: center; gap: 4px; }
.dot { width: 8px; height: 8px; border-radius: 2px; display: inline-block; }
.dot.layer1 { background: #fff3e0; border: 1px solid #f5d7a1; }
.dot.layer2 { background: #e8f5e9; border: 1px solid #b7ddb9; }
.dot.occupied { background: #f6f8fb; border: 1px solid #d7dde5; }
.coil-grid-scroll { overflow: auto; max-height: 460px; }
.coil-col-ruler { display: flex; min-width: max-content; }
.ruler-empty { width: 28px; flex: none; }
.ruler-col { width: 138px; text-align: center; font-size: 12px; color: #606266; }
.coil-grid-main { display: flex; min-width: max-content; }
.coil-row-ruler { width: 28px; display: flex; flex-direction: column; }
.ruler-row { height: 56px; display: flex; align-items: center; justify-content: center; font-size: 12px; color: #909399; }
.coil-grid-columns { display: flex; gap: 8px; }
.coil-col-pair { display: grid; grid-template-columns: 1fr 1fr; width: 180px; border: 1px solid #eef2f7; border-radius: 4px; overflow: visible; }
.coil-layer { display: flex; flex-direction: column; }
.coil-layer.layer2-shift { margin-top: 28px; }
.coil-cell { height: 56px; border-bottom: 1px solid #eef2f7; border-right: 1px solid #eef2f7; padding: 4px; font-size: 11px; color: #909399; display: flex; align-items: center; justify-content: center; text-align: center; cursor: pointer; }
.coil-cell.layer1 { background: #fff3e0; color: #e67e22; }
.coil-cell.layer2 { background: #e8f5e9; color: #2e8b57; border-right: none; }
.coil-cell.occupied { font-weight: 600; }
.coil-cell .line1 { width: 100%; font-size: 11px; line-height: 1.1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.coil-cell .line2 { width: 100%; font-size: 11px; line-height: 1.1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.coil-picker-foot { margin-top: 8px; text-align: right; color: #909399; font-size: 12px; }
</style>

View File

@@ -0,0 +1,225 @@
<template>
<div class="aps-quick-sheet-preview excel-theme">
<div class="sheet-toolbar">
<div class="sheet-actions">
<el-date-picker
v-model="filter.range"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="yyyy-MM-dd"
size="small"
style="width: 240px"
@change="onFilterChange"
/>
<el-select v-model="filter.lineId" clearable filterable size="small" placeholder="产线" style="width: 160px" @change="onFilterChange">
<el-option v-for="line in lineOptions" :key="line.lineId" :label="line.lineName || line.lineCode || ('产线' + line.lineId)" :value="line.lineId" />
</el-select>
<el-input v-model="filter.customer" size="small" clearable placeholder="客户" style="width: 160px" @input="onFilterChange" />
<el-button size="small" icon="el-icon-refresh" @click="loadRows">刷新</el-button>
<el-button size="small" icon="el-icon-download" @click="exportExcel">导出</el-button>
</div>
</div>
<div class="sheet-body">
<el-table
v-loading="loading"
:data="displayRows"
border
size="mini"
class="excel-table"
>
<el-table-column label="序号" width="60" align="center">
<template slot-scope="scope">
{{ ((pager.pageNum - 1) * pager.pageSize) + scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column
v-for="col in flatColumns"
:key="col.prop || col.label"
:label="col.label"
:prop="col.prop"
:width="col.width"
:min-width="col.minWidth"
:align="col.align || 'center'"
:show-overflow-tooltip="col.showOverflowTooltip !== false"
/>
</el-table>
<div class="pager-wrap">
<el-pagination
small
background
layout="prev, pager, next, total"
:current-page.sync="pager.pageNum"
:page-size="pager.pageSize"
:total="totalRows"
/>
</div>
</div>
</div>
</template>
<script>
import { fetchQuickSheetList, exportQuickSheet } from '@/api/aps/quickSheet'
import { listProductionLine } from '@/api/wms/productionLine'
import { getTemplateByKey } from './sheets/templates'
import { saveAs } from 'file-saver'
export default {
name: 'ApsQuickSheetPreview',
data() {
return {
loading: false,
rows: [],
lineOptions: [],
filter: {
range: [],
lineId: null,
customer: ''
},
pager: {
pageNum: 1,
pageSize: 25
}
}
},
computed: {
currentTemplate() {
return getTemplateByKey('unified')
},
flatColumns() {
const res = []
const loop = (cols) => {
;(cols || []).forEach(c => {
if (c.children && c.children.length) loop(c.children)
else res.push(c)
})
}
loop(this.currentTemplate.columns || [])
return res
},
filteredRows() {
const [startAfter, endBefore] = this.filter.range || []
const lineName = (this.filter.lineName || '').trim()
const customer = (this.filter.customer || '').trim().toLowerCase()
return this.rows.filter(r => {
if (startAfter || endBefore) {
const d = r.startTime ? String(r.startTime).slice(0, 10) : ''
if (startAfter && d && d < startAfter) return false
if (endBefore && d && d > endBefore) return false
}
if (lineName && r.lineName !== lineName) return false
if (customer) {
const txt = String(r.customerName || '').toLowerCase()
if (!txt.includes(customer)) return false
}
return true
})
},
displayRows() {
const start = (this.pager.pageNum - 1) * this.pager.pageSize
return this.filteredRows.slice(start, start + this.pager.pageSize)
},
totalRows() {
return this.filteredRows.length
}
},
created() {
const today = new Date()
const start = `${today.getFullYear()}-${`${today.getMonth() + 1}`.padStart(2, '0')}-${`${today.getDate()}`.padStart(2, '0')}`
this.filter.range = [start, '']
this.loadRows()
this.loadLines()
},
methods: {
async loadLines() {
const res = await listProductionLine({ pageNum: 1, pageSize: 1000 })
this.lineOptions = res.rows || []
},
async loadRows() {
this.loading = true
try {
const params = this.buildQueryParams()
const res = await fetchQuickSheetList(params)
this.rows = (res.data || []).map(r => ({ ...r }))
} finally {
this.loading = false
}
},
onFilterChange() {
this.pager.pageNum = 1
this.loadRows()
},
exportExcel() {
const params = this.buildQueryParams()
exportQuickSheet(params)
.then(blob => {
const filename = `quick_sheet_preview_${new Date().getTime()}.xlsx`
saveAs(new Blob([blob], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }), filename)
})
},
buildQueryParams() {
const [startDate, endDate] = this.filter.range || []
return {
startDate: startDate || undefined,
endDate: endDate || undefined,
lineId: this.filter.lineId || undefined,
customerName: this.filter.customer || undefined
}
}
}
}
</script>
<style scoped lang="scss">
.aps-quick-sheet-preview {
padding: 8px;
background: #f7f9fc;
}
.sheet-toolbar {
margin-bottom: 8px;
padding: 8px;
border: 1px solid #eef2f7;
border-radius: 8px;
background: #fff;
}
.sheet-actions {
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
}
.sheet-body {
border: 1px solid #eef2f7;
border-radius: 8px;
background: #fff;
padding: 6px;
}
::v-deep .excel-table {
border: 1px solid #edf1f7;
}
::v-deep .excel-table th.el-table__cell {
background: #fafbfe;
color: #5d6b82;
font-weight: 600;
border-right: 1px solid #edf1f7;
border-bottom: 1px solid #edf1f7;
padding: 4px 0;
}
::v-deep .excel-table td.el-table__cell {
border-right: 1px solid #f0f2f6;
border-bottom: 1px solid #f0f2f6;
padding: 1px 2px;
}
.pager-wrap {
margin-top: 8px;
display: flex;
justify-content: flex-end;
}
</style>

View File

@@ -15,7 +15,7 @@ export const APS_SHEET_TEMPLATES = [
key: 'unified',
name: '统一排产表',
columns: [
{ label: '产线', prop: 'lineName', minWidth: 120 },
{ label: '产线', prop: 'lineName', minWidth: 140 },
{ label: '计划号', prop: 'planCode', minWidth: 140 },
{ label: '订单号', prop: 'orderCode', minWidth: 140 },
{ label: '客户', prop: 'customerName', minWidth: 140 },