diff --git a/yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/utils/AreaUtils.java b/yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/utils/AreaUtils.java index 5a7340095..1a773cda5 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/utils/AreaUtils.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/utils/AreaUtils.java @@ -4,6 +4,7 @@ import cn.hutool.core.io.resource.ResourceUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.text.csv.CsvRow; import cn.hutool.core.text.csv.CsvUtil; +import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; import cn.iocoder.yudao.framework.ip.core.Area; import cn.iocoder.yudao.framework.ip.core.enums.AreaTypeEnum; @@ -74,6 +75,32 @@ public class AreaUtils { return areas.get(id); } + /** + * 获取所有节点的全路径名称如:河南省/石家庄市/新华区 + * + * @param areas 地区树 + * @return 所有节点的全路径名称 + */ + public static List getAllAreaNodePaths(List areas) { + return convertList(areas, AreaUtils::buildTreePath); + } + + // TODO @puhui999: 展开树再构建全路径 + private static String buildTreePath(Area node) { + if (node.getParent() == null || Area.ID_CHINA.equals(node.getParent().getId())) { // 忽略中国 + // 已经是根节点,直接返回节点名称 + return node.getName(); + } else { + // 递归拼接上级节点的名称 + Area parent = getArea(node.getParent().getId()); + if (parent != null) { + String parentPath = buildTreePath(parent); + return parentPath + "/" + node.getName(); + } + } + return StrUtil.EMPTY; + } + /** * 格式化区域 * @@ -88,13 +115,13 @@ public class AreaUtils { * 格式化区域 * * 例如说: - * 1. id = “静安区”时:上海 上海市 静安区 - * 2. id = “上海市”时:上海 上海市 - * 3. id = “上海”时:上海 - * 4. id = “美国”时:美国 + * 1. id = “静安区”时:上海 上海市 静安区 + * 2. id = “上海市”时:上海 上海市 + * 3. id = “上海”时:上海 + * 4. id = “美国”时:美国 * 当区域在中国时,默认不显示中国 * - * @param id 区域编号 + * @param id 区域编号 * @param separator 分隔符 * @return 格式化后的区域 */ diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/handler/SelectSheetWriteHandler.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/handler/SelectSheetWriteHandler.java new file mode 100644 index 000000000..571f4c983 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/handler/SelectSheetWriteHandler.java @@ -0,0 +1,105 @@ +package cn.iocoder.yudao.framework.excel.core.handler; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.common.core.KeyValue; +import com.alibaba.excel.write.handler.SheetWriteHandler; +import com.alibaba.excel.write.metadata.holder.WriteSheetHolder; +import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder; +import org.apache.poi.hssf.usermodel.HSSFDataValidation; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.ss.util.CellRangeAddressList; + +import java.util.List; + +/** + * 基于固定 sheet 实现下拉框 + * + * @author HUIHUI + */ +public class SelectSheetWriteHandler implements SheetWriteHandler { + + private List>> selectMap; + + private final char[] alphabet = new char[]{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', + 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'}; + + public SelectSheetWriteHandler(List>> selectMap) { + this.selectMap = selectMap; + } + + @Override + public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) { + if (selectMap == null || CollUtil.isEmpty(selectMap)) { + return; + } + // 需要设置下拉框的sheet页 + Sheet curSheet = writeSheetHolder.getSheet(); + DataValidationHelper helper = curSheet.getDataValidationHelper(); + String dictSheetName = "字典sheet"; + Workbook workbook = writeWorkbookHolder.getWorkbook(); + // 数据字典的sheet页 + Sheet dictSheet = workbook.createSheet(dictSheetName); + for (KeyValue> keyValue : selectMap) { + // 设置下拉单元格的首行、末行、首列、末列 + CellRangeAddressList rangeAddressList = new CellRangeAddressList(1, 65533, keyValue.getKey(), keyValue.getKey()); + int rowLen = keyValue.getValue().size(); + // 设置字典sheet页的值 每一列一个字典项 + for (int i = 0; i < rowLen; i++) { + Row row = dictSheet.getRow(i); + if (row == null) { + row = dictSheet.createRow(i); + } + row.createCell(keyValue.getKey()).setCellValue(keyValue.getValue().get(i)); + } + String excelColumn = getExcelColumn(keyValue.getKey()); + // 下拉框数据来源 eg:字典sheet!$B1:$B2 + String refers = dictSheetName + "!$" + excelColumn + "$1:$" + excelColumn + "$" + rowLen; + // 创建可被其他单元格引用的名称 + Name name = workbook.createName(); + // 设置名称的名字 + name.setNameName("dict" + keyValue.getKey()); + // 设置公式 + name.setRefersToFormula(refers); + // 设置引用约束 + DataValidationConstraint constraint = helper.createFormulaListConstraint("dict" + keyValue.getKey()); + // 设置约束 + DataValidation validation = helper.createValidation(constraint, rangeAddressList); + if (validation instanceof HSSFDataValidation) { + validation.setSuppressDropDownArrow(false); + } else { + validation.setSuppressDropDownArrow(true); + validation.setShowErrorBox(true); + } + // 阻止输入非下拉框的值 + validation.setErrorStyle(DataValidation.ErrorStyle.STOP); + validation.createErrorBox("提示", "此值不存在于下拉选择中!"); + // 添加下拉框约束 + writeSheetHolder.getSheet().addValidationData(validation); + } + } + + /** + * 将数字列转化成为字母列 + * + * @param num 数字 + * @return 字母 + */ + private String getExcelColumn(int num) { + String column = ""; + int len = alphabet.length - 1; + int first = num / len; + int second = num % len; + if (num <= len) { + column = alphabet[num] + ""; + } else { + column = alphabet[first - 1] + ""; + if (second == 0) { + column = column + alphabet[len] + ""; + } else { + column = column + alphabet[second - 1] + ""; + } + } + return column; + } + +} \ No newline at end of file diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java index c2104693e..83877ece8 100644 --- a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java @@ -1,11 +1,13 @@ package cn.iocoder.yudao.framework.excel.core.util; +import cn.iocoder.yudao.framework.common.core.KeyValue; +import cn.iocoder.yudao.framework.excel.core.handler.SelectSheetWriteHandler; import com.alibaba.excel.EasyExcel; import com.alibaba.excel.converters.longconverter.LongStringConverter; import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy; +import jakarta.servlet.http.HttpServletResponse; import org.springframework.web.multipart.MultipartFile; -import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; @@ -21,12 +23,12 @@ public class ExcelUtils { /** * 将列表以 Excel 响应给前端 * - * @param response 响应 - * @param filename 文件名 + * @param response 响应 + * @param filename 文件名 * @param sheetName Excel sheet 名 - * @param head Excel head 头 - * @param data 数据列表哦 - * @param 泛型,保证 head 和 data 类型的一致性 + * @param head Excel head 头 + * @param data 数据列表哦 + * @param 泛型,保证 head 和 data 类型的一致性 * @throws IOException 写入失败的情况 */ public static void write(HttpServletResponse response, String filename, String sheetName, @@ -38,12 +40,37 @@ public class ExcelUtils { .registerConverter(new LongStringConverter()) // 避免 Long 类型丢失精度 .sheet(sheetName).doWrite(data); // 设置 header 和 contentType。写在最后的原因是,避免报错时,响应 contentType 已经被修改了 - response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, StandardCharsets.UTF_8)); + response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, StandardCharsets.UTF_8.name())); + response.setContentType("application/vnd.ms-excel;charset=UTF-8"); + } + + /** + * 将列表以 Excel 响应给前端 + * + * @param response 响应 + * @param filename 文件名 + * @param sheetName Excel sheet 名 + * @param head Excel head 头 + * @param data 数据列表哦 + * @param selectMap 下拉选择数据 Map + * @throws IOException 写入失败的情况 + */ + public static void write(HttpServletResponse response, String filename, String sheetName, + Class head, List data, List>> selectMap) throws IOException { + // 输出 Excel + EasyExcel.write(response.getOutputStream(), head) + .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理 + .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 基于 column 长度,自动适配。最大 255 宽度 + .registerWriteHandler(new SelectSheetWriteHandler(selectMap)) // 基于固定 sheet 实现下拉框 + .registerConverter(new LongStringConverter()) // 避免 Long 类型丢失精度 + .sheet(sheetName).doWrite(data); + // 设置 header 和 contentType。写在最后的原因是,避免报错时,响应 contentType 已经被修改了 + response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, StandardCharsets.UTF_8.name())); response.setContentType("application/vnd.ms-excel;charset=UTF-8"); } public static List read(MultipartFile file, Class head) throws IOException { - return EasyExcel.read(file.getInputStream(), head, null) + return EasyExcel.read(file.getInputStream(), head, null) .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理 .doReadAllSync(); } diff --git a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java index e567aeecd..beb477988 100644 --- a/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java +++ b/yudao-module-crm/yudao-module-crm-biz/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/CrmCustomerController.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.crm.controller.admin.customer; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.map.MapUtil; +import cn.iocoder.yudao.framework.common.core.KeyValue; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; @@ -10,6 +11,7 @@ import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; import cn.iocoder.yudao.framework.common.util.number.NumberUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.framework.ip.core.Area; import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils; import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; import cn.iocoder.yudao.module.crm.controller.admin.customer.vo.customer.*; @@ -19,6 +21,7 @@ import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerPoolConfigService import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService; import cn.iocoder.yudao.module.system.api.dept.DeptApi; import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import cn.iocoder.yudao.module.system.api.dict.DictDataApi; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import io.swagger.v3.oas.annotations.Operation; @@ -34,9 +37,7 @@ import org.springframework.web.bind.annotation.*; import java.io.IOException; import java.time.LocalDateTime; -import java.util.Arrays; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Stream; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; @@ -44,6 +45,7 @@ import static cn.iocoder.yudao.framework.common.pojo.PageParam.PAGE_SIZE_NONE; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; +import static cn.iocoder.yudao.module.crm.enums.DictTypeConstants.*; import static java.util.Collections.singletonList; @Tag(name = "管理后台 - CRM 客户") @@ -61,6 +63,8 @@ public class CrmCustomerController { private DeptApi deptApi; @Resource private AdminUserApi adminUserApi; + @Resource + private DictDataApi dictDataApi; @PostMapping("/create") @Operation(summary = "创建客户") @@ -258,7 +262,7 @@ public class CrmCustomerController { .areaId(null).detailAddress("").remark("").build() ); // 输出 - ExcelUtils.write(response, "客户导入模板.xls", "客户列表", CrmCustomerImportExcelVO.class, list); + ExcelUtils.write(response, "客户导入模板.xls", "客户列表", CrmCustomerImportExcelVO.class, list, builderSelectMap()); } @PostMapping("/import") @@ -314,4 +318,23 @@ public class CrmCustomerController { return success(true); } + private List>> builderSelectMap() { + List>> selectMap = new ArrayList<>(); + // 获取地区下拉数据 + Area area = AreaUtils.getArea(Area.ID_CHINA); + selectMap.add(new KeyValue<>(6, AreaUtils.getAllAreaNodePaths(area.getChildren()))); + // 获取客户所属行业 + List customerIndustries = dictDataApi.getDictDataLabelList(CRM_CUSTOMER_INDUSTRY); + selectMap.add(new KeyValue<>(8, customerIndustries)); + // 获取客户等级 + List customerLevels = dictDataApi.getDictDataLabelList(CRM_CUSTOMER_LEVEL); + selectMap.add(new KeyValue<>(9, customerLevels)); + // 获取客户来源 + List customerSources = dictDataApi.getDictDataLabelList(CRM_CUSTOMER_SOURCE); + selectMap.add(new KeyValue<>(10, customerSources)); + // 升序不然创建下拉会报错 + selectMap.sort(Comparator.comparing(item -> item.getValue().size())); + return selectMap; + } + } diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/dict/DictDataApi.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/dict/DictDataApi.java index 107184564..bfc2a1590 100644 --- a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/dict/DictDataApi.java +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/dict/DictDataApi.java @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.system.api.dict; import cn.iocoder.yudao.module.system.api.dict.dto.DictDataRespDTO; import java.util.Collection; +import java.util.List; /** * 字典数据 API 接口 @@ -39,4 +40,12 @@ public interface DictDataApi { */ DictDataRespDTO parseDictData(String type, String label); + /** + * 获得字典数据标签列表 + * + * @param dictType 字典类型 + * @return 字典数据标签列表 + */ + List getDictDataLabelList(String dictType); + } diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/dict/DictDataApiImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/dict/DictDataApiImpl.java index 55313c7cf..f3ddb6927 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/dict/DictDataApiImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/dict/DictDataApiImpl.java @@ -1,13 +1,18 @@ package cn.iocoder.yudao.module.system.api.dict; +import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.system.api.dict.dto.DictDataRespDTO; import cn.iocoder.yudao.module.system.dal.dataobject.dict.DictDataDO; import cn.iocoder.yudao.module.system.service.dict.DictDataService; +import jakarta.annotation.Resource; import org.springframework.stereotype.Service; -import jakarta.annotation.Resource; import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; /** * 字典数据 API 实现类 @@ -37,4 +42,13 @@ public class DictDataApiImpl implements DictDataApi { return BeanUtils.toBean(dictData, DictDataRespDTO.class); } + @Override + public List getDictDataLabelList(String dictType) { + List dictDataList = dictDataService.getDictDataListByDictType(dictType); + if (CollUtil.isEmpty(dictDataList)) { + return Collections.emptyList(); + } + return convertList(dictDataList, DictDataDO::getLabel); + } + } diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dict/DictDataService.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dict/DictDataService.java index a752476da..ce75c9afe 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dict/DictDataService.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dict/DictDataService.java @@ -99,4 +99,12 @@ public interface DictDataService { */ DictDataDO parseDictData(String dictType, String label); + /** + * 获得字典数据列表 + * + * @param dictType 字典类型 + * @return 字典数据列表 + */ + List getDictDataListByDictType(String dictType); + } diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dict/DictDataServiceImpl.java b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dict/DictDataServiceImpl.java index 95ef27ef5..7982e2302 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dict/DictDataServiceImpl.java +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/dict/DictDataServiceImpl.java @@ -11,10 +11,10 @@ import cn.iocoder.yudao.module.system.dal.dataobject.dict.DictDataDO; import cn.iocoder.yudao.module.system.dal.dataobject.dict.DictTypeDO; import cn.iocoder.yudao.module.system.dal.mysql.dict.DictDataMapper; import com.google.common.annotations.VisibleForTesting; +import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import jakarta.annotation.Resource; import java.util.Collection; import java.util.Comparator; import java.util.List; @@ -169,4 +169,9 @@ public class DictDataServiceImpl implements DictDataService { return dictDataMapper.selectByDictTypeAndLabel(dictType, label); } + @Override + public List getDictDataListByDictType(String dictType) { + return dictDataMapper.selectList(DictDataDO::getDictType, dictType); + } + }