diff --git a/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/price/ProductController.java b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/price/ProductController.java index 8a1cf507..06b16bb3 100644 --- a/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/price/ProductController.java +++ b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/price/ProductController.java @@ -110,4 +110,16 @@ public class ProductController extends BaseController String message = productService.importProductExcel(file, updateSupport, getUsername()); return success(message); } + + /** + * Import a single-country linehaul cost sheet. countryCode is the destination. + */ + @Log(title = "\u4ea7\u54c1\u5217\u8868", businessType = BusinessType.IMPORT) + @PreAuthorize("@ss.hasPermi('price:product:import')") + @PostMapping("/importCountryData") + public AjaxResult importCountryData(MultipartFile file, String countryCode) throws Exception + { + String message = productService.importCountryProductExcel(file, countryCode, getUsername()); + return success(message); + } } diff --git a/bms-back/ruoyi-admin/src/main/resources/i18n/messages.properties b/bms-back/ruoyi-admin/src/main/resources/i18n/messages.properties index 3c9e85f3..f233348a 100644 --- a/bms-back/ruoyi-admin/src/main/resources/i18n/messages.properties +++ b/bms-back/ruoyi-admin/src/main/resources/i18n/messages.properties @@ -73,6 +73,8 @@ price.set.detail.freight.empty=固定价明细必须填写运费单价 price.set.detail.registration.empty=固定价明细必须填写挂号费 price.set.detail.zone.empty=固定价明细必须选择分区 price.set.detail.roundMode.invalid=取值方式仅支持 UP / DOWN / HALF_UP +price.set.detail.freight.belowBase=套餐运费不能低于导入基准价 {0} +price.set.detail.registration.belowBase=套餐挂号费不能低于导入基准价 {0} price.set.customer.notExists=客户绑定不存在 price.set.customer.unbindAudit=审核中的套餐不允许解绑客户 price.set.customer.customerId.empty=客户ID不能为空 @@ -117,6 +119,11 @@ price.product.import.noSheet=未找到产品明细sheet(需与产品名称一 price.product.import.noCountry=产品明细sheet无有效国家/重量段数据:{0} price.product.import.euEmpty=国家表无欧盟成员(is_eu=1),无法展开「欧盟」 price.product.import.success.unmatched=未匹配国家表(country_code 为空):{0} +price.product.import.countryRequired=请选择目的国家 +price.product.import.countryNotFound=国家不存在或已停用:{0} +price.product.import.countrySheetNotFound=未找到「产品目录」sheet,请上传单国专线成本价表 +price.product.import.countryNoPrice=未找到该产品的重量段价格:{0} +price.product.import.countrySuccess=按国家导入成功({0}):新增 {1} 个产品,跳过 {2} 个(同代码同生效日不覆盖主数据);写入重量段 {3} 行。 # 报价试算 TmsPriceQuote(对外 TMS API 错误码,保持英文契约) price.quote.postcodeRequired=该国家/产品有分区,请填写目的邮编 price.quote.postcodeZoneNotFound=邮编未匹配到分区,请核对目的邮编 diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/IBmsProductService.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/IBmsProductService.java index d7f9689e..4eef65f6 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/IBmsProductService.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/IBmsProductService.java @@ -43,4 +43,15 @@ public interface IBmsProductService * @return 导入结果说明(可含 HTML 换行) */ public String importProductExcel(MultipartFile file, boolean updateSupport, String operName) throws Exception; + + /** + * Import a single-country cost sheet (catalog + product weight rows). + * The destination country is chosen by the user, not read from the sheet. + * + * @param file uploaded xlsx/xls + * @param countryCode ISO country code (bms_region.country_code_2) + * @param operName operator + * @return HTML result message + */ + public String importCountryProductExcel(MultipartFile file, String countryCode, String operName) throws Exception; } diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/BmsProductServiceImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/BmsProductServiceImpl.java index 9344fabc..80bc1dd5 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/BmsProductServiceImpl.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/BmsProductServiceImpl.java @@ -321,6 +321,100 @@ public class BmsProductServiceImpl implements IBmsProductService } } + @Override + public String importCountryProductExcel(MultipartFile file, String countryCode, String operName) throws Exception + { + if (file == null || file.isEmpty()) + { + throw new ServiceException(MessageUtils.message("price.product.import.fileEmpty")); + } + String filename = file.getOriginalFilename(); + if (filename == null || !(filename.toLowerCase(Locale.ROOT).endsWith(".xlsx") + || filename.toLowerCase(Locale.ROOT).endsWith(".xls"))) + { + throw new ServiceException(MessageUtils.message("price.product.import.fileType")); + } + if (StringUtils.isEmpty(countryCode)) + { + throw new ServiceException(MessageUtils.message("price.product.import.countryRequired")); + } + BmsRegion region = regionMapper.selectRegionByCode2(countryCode.trim().toUpperCase(Locale.ROOT)); + if (region == null || "2".equals(region.getDelFlag()) + || (StringUtils.isNotEmpty(region.getStatus()) && !"0".equals(region.getStatus()))) + { + throw new ServiceException(MessageUtils.message("price.product.import.countryNotFound", countryCode)); + } + + try (InputStream is = file.getInputStream(); Workbook workbook = WorkbookFactory.create(is)) + { + Sheet catalog = workbook.getSheet(CATALOG_SHEET); + if (catalog == null) + { + throw new ServiceException(MessageUtils.message("price.product.import.countrySheetNotFound")); + } + List products = parseCountryCatalog(catalog); + if (products.isEmpty()) + { + throw new ServiceException(MessageUtils.message("price.product.import.noData")); + } + products = dedupeCatalogProducts(products); + Map priceBlocks = parseCountryPriceBlocks(workbook); + + int successNum = 0; + int skipNum = 0; + int countryNum = 0; + StringBuilder failureMsg = new StringBuilder(); + String countryLabel = StringUtils.isNotEmpty(region.getCountryNameCn()) + ? region.getCountryNameCn() : region.getCountryCode2(); + + for (BmsProduct product : products) + { + DefaultTransactionDefinition def = new DefaultTransactionDefinition(); + def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + TransactionStatus tx = transactionManager.getTransaction(def); + try + { + PriceBlock block = findPriceBlock(product.getProductName(), priceBlocks); + if (block == null || block.rows.isEmpty()) + { + throw new ServiceException(MessageUtils.message("price.product.import.countryNoPrice", + product.getProductName())); + } + product.setDestRegion(countryLabel); + product.setSourceSheet(block.sheetName); + if (StringUtils.isEmpty(product.getTaxMode())) + { + product.setTaxMode(resolveTaxMode(product.getProductName())); + } + List countries = stampSelectedCountry(block.rows, region, product.getEffectiveDate()); + int[] nums = persistImportedProduct(product, countries, operName); + transactionManager.commit(tx); + successNum += nums[0]; + skipNum += nums[1]; + countryNum += nums[2]; + } + catch (Exception e) + { + transactionManager.rollback(tx); + String tip = e.getMessage(); + if (StringUtils.isEmpty(tip)) + { + tip = e.getClass().getSimpleName(); + } + failureMsg.append("
").append(product.getProductCode()).append("\uff1a").append(tip); + } + } + + if (failureMsg.length() > 0) + { + return MessageUtils.message("price.product.import.partialFail", + Integer.valueOf(successNum), Integer.valueOf(skipNum), failureMsg.toString()); + } + return MessageUtils.message("price.product.import.countrySuccess", + countryLabel, Integer.valueOf(successNum), Integer.valueOf(skipNum), Integer.valueOf(countryNum)); + } + } + /** @return int[]{added, skipped, countryRows} */ private int[] importOneProduct(Workbook workbook, BmsProduct product, String operName, Map regionByNameCn, List euRegions, @@ -1289,6 +1383,364 @@ public class BmsProductServiceImpl implements IBmsProductService } } + /** + * Country cost sheet catalog: product id / name / service / cargo / note / update date. + * Destination country is not in the sheet. + */ + private List parseCountryCatalog(Sheet sheet) + { + List list = new ArrayList<>(); + int[] header = findHeaderCell(sheet, "\u4ea7\u54c1\u540d\u79f0"); + if (header == null) + { + header = findHeaderCell(sheet, "\u4ea7\u54c1ID"); + } + if (header == null) + { + return list; + } + Row head = sheet.getRow(header[0]); + int colCode = firstCol(head, "\u4ea7\u54c1ID", "\u4ea7\u54c1\u5bf9\u63a5\u4ee3\u7801", "\u4ea7\u54c1\u4ee3\u7801"); + int colName = findColByHeader(head, "\u4ea7\u54c1\u540d\u79f0", 0); + int colService = firstCol(head, "\u670d\u52a1", "\u65f6\u6548\u7b49\u7ea7"); + int colCargo = findColByHeader(head, "\u8d70\u8d27\u5c5e\u6027", 0); + int colNote = firstCol(head, "\u5907\u6ce8", "\u66f4\u65b0\u5185\u5bb9"); + int colDate = firstCol(head, "\u66f4\u65b0\u65f6\u95f4", "\u751f\u6548\u65e5\u671f"); + if (colCode < 0 || colName < 0) + { + return list; + } + int lastRow = sheet.getLastRowNum(); + for (int i = header[0] + 1; i <= lastRow; i++) + { + Row row = sheet.getRow(i); + if (row == null) + { + continue; + } + String code = cellString(row.getCell(colCode)); + if (StringUtils.isEmpty(code) || !code.matches("(?i)^[A-Z0-9_-]+$")) + { + continue; + } + String name = cellString(row.getCell(colName)); + if (StringUtils.isEmpty(name)) + { + continue; + } + BmsProduct p = new BmsProduct(); + p.setProductCode(code.trim()); + p.setProductName(name.trim()); + if (colService >= 0) + { + p.setServiceLevel(emptyToNull(cellString(row.getCell(colService)))); + } + if (colCargo >= 0) + { + p.setCargoAttr(emptyToNull(cellString(row.getCell(colCargo)))); + } + if (colNote >= 0) + { + p.setUpdateNote(emptyToNull(cellString(row.getCell(colNote)))); + } + if (colDate >= 0) + { + Date eff = cellDate(row.getCell(colDate)); + if (eff == null) + { + eff = parseLooseDate(cellString(row.getCell(colDate))); + } + p.setEffectiveDate(toDateOnly(eff)); + } + p.setTaxMode(resolveTaxMode(p.getProductName())); + list.add(p); + } + return list; + } + + private Map parseCountryPriceBlocks(Workbook workbook) + { + Map map = new HashMap<>(); + if (workbook == null) + { + return map; + } + for (int i = 0; i < workbook.getNumberOfSheets(); i++) + { + String sheetName = workbook.getSheetName(i); + if (shouldSkipCountryPriceSheet(sheetName)) + { + continue; + } + Sheet sheet = workbook.getSheetAt(i); + int[] header = findHeaderCell(sheet, "\u4ea7\u54c1\u540d\u79f0"); + if (header == null) + { + continue; + } + Row headerRow = sheet.getRow(header[0]); + int colName = header[1]; + int colWeight = findColByHeader(headerRow, "\u91cd\u91cf", 0); + if (colWeight < 0) + { + continue; + } + int colFreight = findColByHeader(headerRow, "\u8fd0\u8d39", 0); + int colRegister = findColByHeader(headerRow, "\u6302\u53f7", 0); + int colSize = findColByHeader(headerRow, "\u5c3a\u5bf8", 0); + int colLastMile = findColByHeader(headerRow, "\u5c3e\u7a0b", 0); + int colLeadTime = findColByHeader(headerRow, "\u65f6\u6548", 0); + + String currentName = null; + String currentSize = null; + String currentLastMile = null; + String currentLeadTime = null; + int lastRow = sheet.getLastRowNum(); + for (int r = header[0] + 1; r <= lastRow; r++) + { + Row row = sheet.getRow(r); + if (row == null) + { + continue; + } + String nameCell = cellString(row.getCell(colName)); + if (StringUtils.isNotEmpty(nameCell)) + { + if (isSectionTitle(nameCell)) + { + currentName = null; + continue; + } + currentName = nameCell.replace("\n", " ").replace("\r", " ").trim(); + currentSize = colSize >= 0 ? emptyToNull(cellString(row.getCell(colSize))) : null; + currentLastMile = colLastMile >= 0 ? emptyToNull(cellString(row.getCell(colLastMile))) : null; + currentLeadTime = colLeadTime >= 0 ? emptyToNull(cellString(row.getCell(colLeadTime))) : null; + } + if (StringUtils.isEmpty(currentName)) + { + continue; + } + String weightLabel = cellString(row.getCell(colWeight)); + BigDecimal[] range = parseWeightRange(weightLabel); + if (range == null) + { + continue; + } + if (colSize >= 0 && StringUtils.isNotEmpty(cellString(row.getCell(colSize)))) + { + currentSize = emptyToNull(cellString(row.getCell(colSize))); + } + if (colLastMile >= 0 && StringUtils.isNotEmpty(cellString(row.getCell(colLastMile)))) + { + currentLastMile = emptyToNull(cellString(row.getCell(colLastMile))); + } + if (colLeadTime >= 0 && StringUtils.isNotEmpty(cellString(row.getCell(colLeadTime)))) + { + currentLeadTime = emptyToNull(cellString(row.getCell(colLeadTime))); + } + BmsProductCountry c = new BmsProductCountry(); + c.setWeightMin(range[0]); + c.setWeightMax(range[1]); + c.setFreightPrice(colFreight >= 0 ? cellDecimal(row.getCell(colFreight)) : null); + c.setRegistrationFee(colRegister >= 0 ? cellDecimal(row.getCell(colRegister)) : null); + c.setSizeLimit(currentSize); + c.setLastMile(currentLastMile); + c.setLeadTime(currentLeadTime); + PriceBlock block = map.get(currentName); + if (block == null) + { + block = new PriceBlock(); + block.sheetName = sheetName; + map.put(currentName, block); + } + block.rows.add(c); + } + } + return map; + } + + private static boolean shouldSkipCountryPriceSheet(String name) + { + if (StringUtils.isEmpty(name)) + { + return true; + } + if (CATALOG_SHEET.equals(name) || SKIP_SHEETS.contains(name)) + { + return true; + } + return name.contains("\u90ae\u7f16") || name.contains("\u8d54\u507f"); + } + + private static boolean isSectionTitle(String name) + { + if (StringUtils.isEmpty(name)) + { + return false; + } + String n = name.trim(); + return n.startsWith("\u4e00\u3001") || n.startsWith("\u4e8c\u3001") || n.startsWith("\u4e09\u3001") + || n.startsWith("\u56db\u3001") || n.startsWith("\u4e94\u3001") || n.startsWith("\u516d\u3001") + || n.startsWith("\u7279\u522b"); + } + + private PriceBlock findPriceBlock(String productName, Map blocks) + { + if (StringUtils.isEmpty(productName) || blocks == null || blocks.isEmpty()) + { + return null; + } + String name = productName.trim(); + PriceBlock exact = blocks.get(name); + if (exact != null) + { + return exact; + } + String stripped = name.replace("\u6807\u51c6", "").trim(); + exact = blocks.get(stripped); + if (exact != null) + { + return exact; + } + String want = compactName(name); + PriceBlock hit = null; + int hits = 0; + for (Map.Entry entry : blocks.entrySet()) + { + String key = compactName(entry.getKey()); + if (key.equals(want) || (want.contains(key) && key.length() >= 4) || (key.contains(want) && want.length() >= 4)) + { + hit = entry.getValue(); + hits++; + } + } + return hits == 1 ? hit : null; + } + + private static String compactName(String s) + { + if (s == null) + { + return ""; + } + return s.trim().replace(" ", "").replace("\u3000", "") + .replace("-", "").replace("\u2013", "").replace("\u2014", "").replace("\uff0d", ""); + } + + private List stampSelectedCountry(List rows, BmsRegion region, Date eff) + { + List out = new ArrayList<>(); + if (rows == null) + { + return out; + } + String countryName = region == null ? null : region.getCountryNameCn(); + String countryCode = region == null ? null : region.getCountryCode2(); + for (BmsProductCountry src : rows) + { + BmsProductCountry c = new BmsProductCountry(); + c.setCountryName(countryName); + c.setCountryCode(countryCode); + c.setWeightMin(src.getWeightMin()); + c.setWeightMax(src.getWeightMax()); + c.setFreightPrice(src.getFreightPrice()); + c.setRegistrationFee(src.getRegistrationFee()); + c.setSizeLimit(src.getSizeLimit()); + c.setLastMile(src.getLastMile()); + c.setLeadTime(src.getLeadTime()); + c.setEffectiveDate(eff); + out.add(c); + } + return out; + } + + /** @return int[]{added, skipped, countryRows} */ + private int[] persistImportedProduct(BmsProduct product, List countries, String operName) + { + product.setCreateBy(operName); + product.setUpdateBy(operName); + if (StringUtils.isEmpty(product.getSourceSheet())) + { + product.setSourceSheet(product.getProductName()); + } + Date newEff = toDateOnly(product.getEffectiveDate()); + if (newEff == null) + { + newEff = toDateOnly(resolveImportEffectiveDate(countries)); + } + product.setEffectiveDate(newEff); + if (newEff == null) + { + throw new ServiceException(MessageUtils.message("price.product.import.effEmpty")); + } + BmsProduct existing = productMapper.selectBmsProductByCodeAndDate(product.getProductCode(), newEff); + if (existing != null) + { + product.setProductId(existing.getProductId()); + int countryRows = saveCountriesVersioned(product, countries); + return new int[] { 0, 1, countryRows }; + } + applyProductStatus(product); + try + { + productMapper.insertBmsProduct(product); + } + catch (Exception e) + { + if (isProductCodeUniqueViolation(e)) + { + throw new ServiceException(MessageUtils.message("price.product.import.needCodeEffUnique")); + } + throw e; + } + productMapper.expireOpenByProductCode(product.getProductCode(), newEff, product.getProductId()); + productMapper.refreshStatusByDates(); + int countryRows = saveCountriesVersioned(product, countries); + return new int[] { 1, 0, countryRows }; + } + + private Date parseLooseDate(String text) + { + if (StringUtils.isEmpty(text)) + { + return null; + } + String s = text.trim(); + if (s.length() > 20) + { + return null; + } + Matcher ymd = EFFECTIVE_DATE_YMD.matcher(s); + if (ymd.matches()) + { + return parseYmd(ymd.group(1), ymd.group(2), ymd.group(3)); + } + return null; + } + + private static int firstCol(Row row, String... keywords) + { + if (keywords == null) + { + return -1; + } + for (String keyword : keywords) + { + int col = findColByHeader(row, keyword, 0); + if (col >= 0) + { + return col; + } + } + return -1; + } + + private static final class PriceBlock + { + private String sheetName; + private final List rows = new ArrayList<>(); + } + private static Date cellDate(Cell cell) { if (cell == null) diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcPriceSetServiceImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcPriceSetServiceImpl.java index a1a1a18a..8e5bed6f 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcPriceSetServiceImpl.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcPriceSetServiceImpl.java @@ -1039,6 +1039,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService // (见 TmsPriceQuoteServiceImpl.tryFixed)。 fillProductCountryId(detail); snapshotWeightAndDates(detail); + assertNotBelowImportedPrice(detail); } if (StringUtils.isEmpty(detail.getRoundMode())) { @@ -1059,6 +1060,89 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService } } + /** Package freight / registration must not go below the imported product country price. */ + private void assertNotBelowImportedPrice(PrcPriceSetDetail detail) + { + BmsProductCountry base = findImportedPrice(detail); + if (base == null) + { + return; + } + if (detail.getFvValue() != null && base.getFreightPrice() != null + && detail.getFvValue().compareTo(base.getFreightPrice()) < 0) + { + throw new ServiceException(MessageUtils.message("price.set.detail.freight.belowBase", + stripTrailingZeros(base.getFreightPrice()))); + } + if (detail.getFvRegistrationFee() != null && base.getRegistrationFee() != null + && detail.getFvRegistrationFee().compareTo(base.getRegistrationFee()) < 0) + { + throw new ServiceException(MessageUtils.message("price.set.detail.registration.belowBase", + stripTrailingZeros(base.getRegistrationFee()))); + } + } + + private BmsProductCountry findImportedPrice(PrcPriceSetDetail detail) + { + if (detail == null || StringUtils.isEmpty(detail.getProductCode())) + { + return null; + } + BmsProductCountry query = new BmsProductCountry(); + query.setProductCode(detail.getProductCode()); + List rows = productCountryMapper.selectBmsProductCountryList(query); + if (rows == null || rows.isEmpty()) + { + return null; + } + if (detail.getProductCountryId() != null) + { + for (BmsProductCountry row : rows) + { + if (row != null && detail.getProductCountryId().equals(row.getDetailId())) + { + return row; + } + } + } + for (BmsProductCountry row : rows) + { + if (row == null || row.getExpireDate() != null) + { + continue; + } + if (!matchDetailCountry(detail, row)) + { + continue; + } + String label = formatWeightLabel(row.getWeightMin(), row.getWeightMax()); + if (StringUtils.isNotEmpty(detail.getWeightLabel()) && detail.getWeightLabel().equals(label)) + { + return row; + } + if (detail.getWeightMin() != null && row.getWeightMin() != null + && detail.getWeightMin().compareTo(row.getWeightMin()) == 0 + && sameWeightMax(detail.getWeightMax(), row.getWeightMax())) + { + return row; + } + } + return null; + } + + private static boolean sameWeightMax(java.math.BigDecimal a, java.math.BigDecimal b) + { + if (a == null && b == null) + { + return true; + } + if (a == null || b == null) + { + return false; + } + return a.compareTo(b) == 0; + } + /** * When product country rows for this country have zones, zone is required. */ diff --git a/bms-front/src/components/ExcelImportDialog/index.vue b/bms-front/src/components/ExcelImportDialog/index.vue index 3b6adf2f..6fd92f47 100644 --- a/bms-front/src/components/ExcelImportDialog/index.vue +++ b/bms-front/src/components/ExcelImportDialog/index.vue @@ -9,6 +9,10 @@ +
+ +
+ [] }, + extraQuery: { + type: Object, + default: () => ({}) + }, + beforeSubmit: { + type: Function, + default: null + }, // 确定按钮文字 submitText: { type: String, @@ -157,7 +169,16 @@ const validateError = ref('') const headers = { Authorization: 'Bearer ' + getToken() } const uploadUrl = computed(() => { - return import.meta.env.VITE_APP_BASE_API + props.action + '?updateSupport=' + (updateSupport.value ? 1 : 0) + const query = new URLSearchParams() + query.set('updateSupport', updateSupport.value ? '1' : '0') + const extra = props.extraQuery || {} + Object.keys(extra).forEach(key => { + const value = extra[key] + if (value !== undefined && value !== null && value !== '') { + query.set(key, value) + } + }) + return import.meta.env.VITE_APP_BASE_API + props.action + '?' + query.toString() }) const templateUrl = computed(() => !!props.templateAction) @@ -256,6 +277,13 @@ function handleSuccess(response) { // 提交上传 function handleSubmit() { + if (typeof props.beforeSubmit === 'function') { + const msg = props.beforeSubmit() + if (msg) { + proxy.$modal.msgError(msg) + return + } + } const file = selectedFile.value const err = checkFile(file) if (err) { @@ -440,6 +468,10 @@ defineExpose({ open }) flex: 1; } +.import-extra { + margin-bottom: 14px; +} + .file-progress { margin-top: 12px; } diff --git a/bms-front/src/views/price/basic/product/components/ProductCountryDrawer.vue b/bms-front/src/views/price/basic/product/components/ProductCountryDrawer.vue index a5f63c46..db24b387 100644 --- a/bms-front/src/views/price/basic/product/components/ProductCountryDrawer.vue +++ b/bms-front/src/views/price/basic/product/components/ProductCountryDrawer.vue @@ -115,7 +115,7 @@ const columns = [ { prop: 'effectiveDate', label: '生效日期', width: 110, align: 'center' }, { prop: 'expireDate', label: '失效日期', width: 110, align: 'center', formatter: (row) => row.expireDate || '长期' }, { prop: 'status', label: '状态', width: 90, align: 'center', dict: 'bms_product_effect_status' }, - { prop: 'action', label: '操作', width: 70, fixed: 'right', slot: 'action' } + // { prop: 'action', label: '操作', width: 70, fixed: 'right', slot: 'action' } ] const visible = ref(false) diff --git a/bms-front/src/views/price/basic/product/index.vue b/bms-front/src/views/price/basic/product/index.vue index 5758969e..b908c9e6 100644 --- a/bms-front/src/views/price/basic/product/index.vue +++ b/bms-front/src/views/price/basic/product/index.vue @@ -5,7 +5,7 @@ :closable="false" show-icon class="mb16" - title="Excel 导入:相同产品代码+生效日期不覆盖;不同生效日期新增一条产品。同时入库国家×重量段(运费/挂号费/尺寸/尾程/时效)。" + title="多国家价表用「导入 Excel」。单国专线成本价用「按国家导入」:先选目的国家,目录里的产品都记到该国。相同产品代码+生效日期不覆盖主数据。" /> 导入 Excel + + 按国家导入 + @@ -62,6 +91,7 @@ import ProductFormDialog from './components/ProductFormDialog.vue' import ProductCountryDrawer from './components/ProductCountryDrawer.vue' import ProductSurchargeDrawer from './components/ProductSurchargeDrawer.vue' import { listProduct } from '@/api/price/product' +import { listRegionOptions } from '@/api/data/region' const { proxy } = getCurrentInstance() const { bms_product_effect_status } = useDict('bms_product_effect_status') @@ -71,6 +101,14 @@ const formRef = ref() const countryRef = ref() const surchargeRef = ref() const importRef = ref() +const countryImportRef = ref() +const importCountryCode = ref('') +const regionOptions = ref([]) +const countryImportQuery = computed(() => ({ countryCode: importCountryCode.value })) +const countryImportDesc = [ + '选择目的国家后上传专线成本价表,目录中的产品都会记到该国家。', + '邮编、赔偿标准等 sheet 不导入。相同产品代码+生效日期不覆盖主数据,重量段按生效日写入。' +] const columns = [ { prop: 'productCode', label: '产品代码', minWidth: 110 }, @@ -120,6 +158,32 @@ function handleUpdate(row) { function handleImport() { importRef.value.open() } + +function regionLabel(item) { + const name = item.countryNameCn || item.countryNameEn || '' + const code = item.countryCode2 || '' + return name && code ? name + ' (' + code + ')' : (name || code) +} + +function loadRegionOptions() { + if (regionOptions.value.length) return + listRegionOptions().then(res => { + regionOptions.value = res.data || [] + }) +} + +function handleCountryImport() { + importCountryCode.value = '' + loadRegionOptions() + countryImportRef.value.open() +} + +function validateCountryImport() { + if (!importCountryCode.value) { + return '请先选择目的国家' + } + return '' +}