feat(quotation): 添加报价单导出功能

- 在报价单列表页面添加导出按钮,支持单个报价单导出为Excel文件
- 实现exportSingle方法,支持按ID导出指定报价单数据
- 集成EasyExcel库,实现报价单数据的Excel格式化导出
- 添加报价单导出的API接口和权限控制
- 实现包含Logo、标题、产品明细等完整报价单格式的Excel文件生成
- 添加导出功能的前端调用和下载处理逻辑
dev_1.0.2
chenhao 2026-01-29 18:07:31 +08:00
parent aa2efbd42e
commit 61b10eba26
5 changed files with 318 additions and 3 deletions

View File

@ -42,3 +42,9 @@ export function delQuotation(id) {
method: 'delete'
})
}
export function exportSingleQuotation(id) {
return request({
url: '/quotation/export/single/' + id,
method: 'get'
})
}

View File

@ -108,6 +108,13 @@
@click="handleDelete(scope.row)"
v-hasPermi="['base:quotation:remove']"
>删除</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-download"
@click="handleExport(scope.row)"
v-hasPermi="['base:quotation:export']"
>导出</el-button>
</template>
</el-table-column>
</el-table>
@ -215,10 +222,11 @@
</template>
<script>
import { listQuotation, getQuotation, delQuotation, addQuotation, updateQuotation } from "@/api/base/quotation";
import { listQuotation, getQuotation, delQuotation, addQuotation, updateQuotation,exportSingleQuotation } from "@/api/base/quotation";
import { listAgent } from "@/api/system/agent";
import ProductConfig from "@/views/project/info/ProductConfig";
import {isEmpty} from "@/utils/validate";
import {exportPurchaseorder} from "@/api/sip/purchaseorder";
export default {
name: "Quotation",
@ -338,6 +346,13 @@ export default {
this.loading = false;
});
},
handleExport(row){
this.$modal.confirm('是否确认导出已审批的采购数据项?').then(() => {
return exportSingleQuotation(row.id);
}).then(response => {
this.$download.download( response.msg)
})
},
/** 查询代表处列表 */
getAgentList() {
listAgent().then(response => {

View File

@ -69,4 +69,8 @@ public class QuotationController extends BaseController {
public AjaxResult batchRemove(@PathVariable("ids") Integer[] ids) {
return AjaxResult.success(quotationService.batchRemove(ids));
}
@GetMapping("/export/single/{id}")
public AjaxResult exportSingle(@PathVariable("id") Integer id) {
return AjaxResult.success(quotationService.exportSingle(id));
}
}

View File

@ -43,6 +43,7 @@ public interface IQuotationService {
*/
int batchRemove(Integer[] ids);
String exportSingle(Integer id);
}

View File

@ -1,17 +1,36 @@
package com.ruoyi.sip.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.metadata.CellData;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteTableHolder;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
import com.alibaba.excel.write.style.column.AbstractColumnWidthStyleStrategy;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.sip.domain.*;
import com.ruoyi.sip.mapper.QuotationMapper;
import com.ruoyi.sip.service.ICodeGenTableService;
import com.ruoyi.sip.service.IQuotationProductInfoService;
import com.ruoyi.sip.service.IQuotationService;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Cell;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.io.InputStream;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
/**
@ -23,6 +42,7 @@ import java.util.stream.Collectors;
@Service
@Transactional(rollbackFor = Exception.class)
@Slf4j
public class QuotationServiceImpl implements IQuotationService {
@Resource
@ -121,7 +141,276 @@ public class QuotationServiceImpl implements IQuotationService {
return quotationMapper.batchRemove(ids);
}
}
@Override
public String exportSingle(Integer id) {
try {
Quotation quotation = this.queryById(id);
List<List<Object>> rows = new ArrayList<>();
// 加载Logo
byte[] logoBytes = null;
try (InputStream is = SpringUtils.getResource("classpath:static/img/companyLogo.png").getInputStream()) {
logoBytes = IoUtil.readBytes(is);
} catch (Exception e) {
log.warn("读取公司Logo失败", e);
}
// 1. 第一部分:标题信息
// Row 1: Title
List<Object> row1 = new ArrayList<>();
row1.add("云桌面产品价格明细清单");
for (int i = 1; i < 11; i++) row1.add("");
if (logoBytes != null) {
row1.add(logoBytes);
} else {
row1.add("");
}
rows.add(row1);
// Calculate Date
String validDate = LocalDate.now().plusDays(7).format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
// Helper to create padded row
List<Object> row2 = new ArrayList<>();
row2.add("项目名称:");
row2.add(quotation.getQuotationName());
row2.add(""); row2.add(""); row2.add(""); row2.add(""); // Padding
row2.add("*本报价单有效期至:");
row2.add(validDate);
rows.add(row2);
List<Object> row3 = new ArrayList<>();
row3.add("*项目ID:");
row3.add(quotation.getProjectCode());
row3.add(""); row3.add(""); row3.add(""); row3.add("");
row3.add("*云桌面完整报价单必须包含部署服务、现场维保、省代集成服务,此三项由省代进行补充报价,不能缺项");
rows.add(row3);
List<Object> row4 = new ArrayList<>();
row4.add("国家/地区 :");
row4.add("中国大陆");
row4.add(""); row4.add(""); row4.add(""); row4.add("");
row4.add("*因上游CPU、内存、存储波动较大封标前3天与汇智区域接口人邮件确定商务折扣和供货周期否则报价单无效");
rows.add(row4);
List<Object> row5 = new ArrayList<>();
row5.add("备注:");
row5.add(quotation.getRemark());
rows.add(row5);
rows.add(Collections.emptyList());
// 2. 第二部分:列标题
List<String> headers = Arrays.asList(
"序号", "产品编码", "产品型号", "产品代码描述", "数量",
"目录单价(RMB)", "推荐折扣", "折扣单价(RMB)", "总价(RMB)",
"目录总价(RMB)", "CID信息", "备注"
);
// 记录列标题行索引,使其变灰
Set<Integer> coloredRowIndices = new HashSet<>();
coloredRowIndices.add(rows.size());
rows.add(new ArrayList<>(headers));
// 3. 第三部分:数据分组
Set<Integer> aquaRowIndices = new HashSet<>();
AtomicInteger sectionCounter = new AtomicInteger(1);
addSection(rows, "软件", quotation.getSoftwareProjectProductInfoList(), coloredRowIndices, aquaRowIndices, sectionCounter);
addSection(rows, "硬件", quotation.getHardwareProjectProductInfoList(), coloredRowIndices, aquaRowIndices, sectionCounter);
addSection(rows, "服务", quotation.getMaintenanceProjectProductInfoList(), coloredRowIndices, aquaRowIndices, sectionCounter);
ExcelUtil<Object> util = new ExcelUtil<>(Object.class);
String fileName = util.encodingFilename("报价单_" + quotation.getQuotationCode());
String filePath = util.getAbsoluteFile(fileName);
// 自动换行样式
WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
contentWriteCellStyle.setWrapped(true);
contentWriteCellStyle.setVerticalAlignment(org.apache.poi.ss.usermodel.VerticalAlignment.CENTER);
HorizontalCellStyleStrategy horizontalCellStyleStrategy = new HorizontalCellStyleStrategy(null, contentWriteCellStyle);
EasyExcel.write(filePath)
.registerWriteHandler(horizontalCellStyleStrategy)
.registerWriteHandler(new CustomColumnWidthStrategy())
.registerWriteHandler(new com.alibaba.excel.write.handler.CellWriteHandler() {
private org.apache.poi.ss.usermodel.CellStyle coloredStyle;
private org.apache.poi.ss.usermodel.CellStyle aquaStyle;
private org.apache.poi.ss.usermodel.CellStyle centerStyle;
@Override
public void beforeCellCreate(com.alibaba.excel.write.metadata.holder.WriteSheetHolder writeSheetHolder, com.alibaba.excel.write.metadata.holder.WriteTableHolder writeTableHolder, org.apache.poi.ss.usermodel.Row row, com.alibaba.excel.metadata.Head head, Integer columnIndex, Integer relativeRowIndex, Boolean isHead) {
}
@Override
public void afterCellCreate(com.alibaba.excel.write.metadata.holder.WriteSheetHolder writeSheetHolder, com.alibaba.excel.write.metadata.holder.WriteTableHolder writeTableHolder, org.apache.poi.ss.usermodel.Cell cell, com.alibaba.excel.metadata.Head head, Integer relativeRowIndex, Boolean isHead) {
}
@Override
public void afterCellDataConverted(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, CellData cellData, Cell cell, Head head, Integer integer, Boolean aBoolean) {
}
@Override
public void afterCellDispose(com.alibaba.excel.write.metadata.holder.WriteSheetHolder writeSheetHolder, com.alibaba.excel.write.metadata.holder.WriteTableHolder writeTableHolder, List<com.alibaba.excel.metadata.CellData> cellDataList, org.apache.poi.ss.usermodel.Cell cell, com.alibaba.excel.metadata.Head head, Integer relativeRowIndex, Boolean isHead) {
// Title Centering (Row 0)
if (cell.getRowIndex() == 0 && cell.getColumnIndex() == 0) {
if (centerStyle == null) {
org.apache.poi.ss.usermodel.Workbook workbook = writeSheetHolder.getSheet().getWorkbook();
centerStyle = workbook.createCellStyle();
centerStyle.setAlignment(org.apache.poi.ss.usermodel.HorizontalAlignment.CENTER);
centerStyle.setVerticalAlignment(org.apache.poi.ss.usermodel.VerticalAlignment.CENTER);
org.apache.poi.ss.usermodel.Font font = workbook.createFont();
font.setBold(true);
font.setFontHeightInPoints((short) 16);
centerStyle.setFont(font);
}
cell.setCellStyle(centerStyle);
// Merge cells for title (0-10, Logo is at 11 usually, but let's merge fewer if needed, or 0-10)
writeSheetHolder.getSheet().addMergedRegionUnsafe(new org.apache.poi.ss.util.CellRangeAddress(0, 0, 0, 10));
}
if (coloredRowIndices.contains(cell.getRowIndex())) {
if (coloredStyle == null) {
org.apache.poi.ss.usermodel.Workbook workbook = writeSheetHolder.getSheet().getWorkbook();
coloredStyle = workbook.createCellStyle();
coloredStyle.setFillForegroundColor(org.apache.poi.ss.usermodel.IndexedColors.GREY_25_PERCENT.getIndex());
coloredStyle.setFillPattern(org.apache.poi.ss.usermodel.FillPatternType.SOLID_FOREGROUND);
coloredStyle.setWrapText(true);
coloredStyle.setVerticalAlignment(org.apache.poi.ss.usermodel.VerticalAlignment.CENTER);
org.apache.poi.ss.usermodel.Font font = workbook.createFont();
font.setBold(true);
coloredStyle.setFont(font);
}
cell.setCellStyle(coloredStyle);
} else if (aquaRowIndices.contains(cell.getRowIndex())) {
if (aquaStyle == null) {
org.apache.poi.ss.usermodel.Workbook workbook = writeSheetHolder.getSheet().getWorkbook();
aquaStyle = workbook.createCellStyle();
aquaStyle.setFillForegroundColor(org.apache.poi.ss.usermodel.IndexedColors.AQUA.getIndex());
aquaStyle.setFillPattern(org.apache.poi.ss.usermodel.FillPatternType.SOLID_FOREGROUND);
aquaStyle.setWrapText(true);
}
cell.setCellStyle(aquaStyle);
}
}
})
.sheet("报价单")
.doWrite(rows);
return fileName;
} catch (Exception e) {
log.error("导出报价单失败", e);
throw new ServiceException("导出报价单失败,请稍后重试");
}
}
private void addSection(List<List<Object>> rows, String title, List<QuotationProductInfo> list, Set<Integer> coloredRowIndices, Set<Integer> aquaRowIndices, AtomicInteger sectionCounter) {
if (CollUtil.isEmpty(list)) {
return;
}
// 记录标题行索引 (Aqua)
aquaRowIndices.add(rows.size());
int currentSection = sectionCounter.getAndIncrement();
// 添加标题行 (补齐12列)
List<Object> titleRow = new ArrayList<>();
titleRow.add(currentSection);
titleRow.add(title);
for (int i = 0; i < 10; i++) {
titleRow.add("");
}
rows.add(titleRow);
double sumAllPrice = 0.0;
double sumCatalogueAllPrice = 0.0;
// 添加数据行
int index = 1;
for (QuotationProductInfo item : list) {
List<Object> row = new ArrayList<>();
row.add(currentSection + "_" + index++);
row.add(item.getProductBomCode());
row.add(item.getModel());
row.add(item.getProductDesc());
row.add(item.getQuantity());
row.add(item.getCataloguePrice());
row.add(item.getGuidanceDiscount());
row.add(item.getPrice());
row.add(item.getAllPrice());
row.add(item.getCatalogueAllPrice());
row.add(""); // CID信息
row.add(item.getRemark());
rows.add(row);
if (item.getAllPrice() != null) {
sumAllPrice += item.getAllPrice();
}
if (item.getCatalogueAllPrice() != null) {
sumCatalogueAllPrice += item.getCatalogueAllPrice();
}
}
// 添加合计行1
coloredRowIndices.add(rows.size());
List<Object> subTotalRow1 = new ArrayList<>();
subTotalRow1.add("");
subTotalRow1.add(title);
subTotalRow1.add("");
subTotalRow1.add("");
subTotalRow1.add("");
subTotalRow1.add("");
subTotalRow1.add("");
subTotalRow1.add("");
subTotalRow1.add(sumAllPrice);
subTotalRow1.add(sumCatalogueAllPrice);
subTotalRow1.add("");
subTotalRow1.add("");
rows.add(subTotalRow1);
// 添加合计行2
coloredRowIndices.add(rows.size());
List<Object> subTotalRow2 = new ArrayList<>();
subTotalRow2.add("");
subTotalRow2.add("配置组小计");
subTotalRow2.add("");
subTotalRow2.add("");
subTotalRow2.add("");
subTotalRow2.add("");
subTotalRow2.add("");
subTotalRow2.add("");
subTotalRow2.add(sumAllPrice);
subTotalRow2.add(sumCatalogueAllPrice);
subTotalRow2.add("");
subTotalRow2.add("");
rows.add(subTotalRow2);
}
// 自定义列宽策略:自适应但有最大宽度
public static class CustomColumnWidthStrategy extends AbstractColumnWidthStyleStrategy {
private static final int MAX_COLUMN_WIDTH = 50;
@Override
protected void setColumnWidth(WriteSheetHolder writeSheetHolder, List<CellData> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
if (isHead != null && isHead) {
// 如果是表头不需特别处理EasyExcel会自动处理或者这里也可以计算
return;
}
if (cellDataList != null && !cellDataList.isEmpty()) {
CellData cellData = cellDataList.get(0);
if (cellData.getType() == com.alibaba.excel.enums.CellDataTypeEnum.STRING) {
String stringValue = cellData.getStringValue();
if (StrUtil.isNotEmpty(stringValue)) {
int length = stringValue.getBytes().length;
int width = Math.min(length + 2, MAX_COLUMN_WIDTH);
// 当前列宽
int currentColumnWidth = writeSheetHolder.getSheet().getColumnWidth(cell.getColumnIndex()) / 256;
if (width > currentColumnWidth) {
writeSheetHolder.getSheet().setColumnWidth(cell.getColumnIndex(), width * 256);
}
}
}
}
}
}
}