导入批量修改价格明细
This commit is contained in:
@@ -17,6 +17,7 @@ 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 org.springframework.web.multipart.MultipartFile;
|
||||
import com.bms.common.annotation.Log;
|
||||
import com.bms.common.core.controller.BaseController;
|
||||
import com.bms.common.core.domain.AjaxResult;
|
||||
@@ -218,6 +219,19 @@ public class PriceSetController extends BaseController
|
||||
return toAjax(priceSetService.updateDetail(detail));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:set:edit') or @ss.hasPermi('price:set:forceEdit')")
|
||||
@Log(title = "\u5957\u9910\u4ef7\u683c\u5bfc\u5165", businessType = BusinessType.IMPORT)
|
||||
@PostMapping("/detail/importPrices")
|
||||
public AjaxResult importDetailPrices(MultipartFile file,
|
||||
@RequestParam("priceSetId") Long priceSetId,
|
||||
@RequestParam(value = "psvId", required = false) Long psvId) throws Exception
|
||||
{
|
||||
String taskId = priceSetService.submitImportDetailPricesAsync(priceSetId, psvId, file, getUsername());
|
||||
Map<String, Object> payload = new HashMap<String, Object>();
|
||||
payload.put("taskId", taskId);
|
||||
return success(payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Async batch write of detail surcharge snapshots. Returns taskId; poll /save/task/{taskId}.
|
||||
*/
|
||||
|
||||
@@ -75,6 +75,10 @@ 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.import.fixedOnly=\u4ec5\u56fa\u5b9a\u4ef7\u5957\u9910\u652f\u6301\u5bfc\u5165\u4fee\u6539\u4ef7\u683c
|
||||
price.set.import.noDetails=\u5957\u9910\u6682\u65e0\u4ef7\u683c\u660e\u7ec6\uff0c\u8bf7\u5148\u62c9\u53d6\u4ea7\u54c1\u4ef7
|
||||
price.set.import.noPriceRows=\u4ef7\u8868\u672a\u89e3\u6790\u5230\u6709\u6548\u4ef7\u683c\u884c
|
||||
price.set.import.success=\u5bfc\u5165\u5b8c\u6210\uff1a\u66f4\u65b0 {0} \u6761\uff0c\u672a\u5339\u914d {1} \u6761\uff0c\u5957\u9910\u65e0\u5bf9\u5e94\u4ea7\u54c1 {2} \u4e2a
|
||||
price.set.customer.notExists=客户绑定不存在
|
||||
price.set.customer.unbindAudit=审核中的套餐不允许解绑客户
|
||||
price.set.customer.customerId.empty=客户ID不能为空
|
||||
|
||||
@@ -37,6 +37,10 @@ public class PrcPriceSetDetail implements Serializable
|
||||
private Long productCountryId;
|
||||
private BigDecimal fvValue;
|
||||
private BigDecimal fvRegistrationFee;
|
||||
/** product base freight from bms_product_country (display only) */
|
||||
private BigDecimal baseFreightPrice;
|
||||
/** product base registration from bms_product_country (display only) */
|
||||
private BigDecimal baseRegistrationFee;
|
||||
private BigDecimal discountValue;
|
||||
/** UP / DOWN / HALF_UP */
|
||||
private String roundMode;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.bms.price.mapper;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
@@ -75,4 +76,10 @@ public interface PrcPriceSetDetailMapper
|
||||
@Param("priceSetId") Long priceSetId,
|
||||
@Param("psvId") Long psvId,
|
||||
@Param("productCode") String productCode);
|
||||
|
||||
/** Update package freight / registration only */
|
||||
public int updateDetailPrices(
|
||||
@Param("psdId") Long psdId,
|
||||
@Param("fvValue") BigDecimal fvValue,
|
||||
@Param("fvRegistrationFee") BigDecimal fvRegistrationFee);
|
||||
}
|
||||
|
||||
@@ -58,4 +58,13 @@ public interface IBmsProductService
|
||||
|
||||
/** Distinct base-price version options (by effective_date desc) for package create */
|
||||
public List<BmsProductPriceVersion> selectPriceVersionOptions();
|
||||
|
||||
/**
|
||||
* Parse Weishi B2C quote Excel into productCode -> country/zone/weight price rows (no persist).
|
||||
* Used by price-set detail price import.
|
||||
*/
|
||||
public java.util.Map<String, List<BmsProductCountry>> parseQuoteExcelByProduct(MultipartFile file) throws Exception;
|
||||
|
||||
/** Parse quote Excel from in-memory bytes (async import). */
|
||||
public java.util.Map<String, List<BmsProductCountry>> parseQuoteExcelByProduct(byte[] bytes, String filename) throws Exception;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.bms.price.service;
|
||||
|
||||
import java.util.Date;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.bms.price.domain.PrcCustomerPriceSet;
|
||||
@@ -96,4 +97,19 @@ public interface IPrcPriceSetService
|
||||
|
||||
/** poll async save progress */
|
||||
public PrcPriceSetSaveTask getSaveTask(String taskId);
|
||||
|
||||
/**
|
||||
* Submit async import of Weishi B2C quote Excel into FIXED package detail prices.
|
||||
* @return taskId for polling getSaveTask
|
||||
*/
|
||||
public String submitImportDetailPricesAsync(Long priceSetId, Long psvId, MultipartFile file, String operName) throws Exception;
|
||||
|
||||
/**
|
||||
* Sync worker: parse bytes and update FIXED detail prices matched by
|
||||
* product + country + zone(if any) + weight band.
|
||||
*/
|
||||
public String importDetailPrices(Long priceSetId, Long psvId, byte[] bytes, String filename, String operName) throws Exception;
|
||||
|
||||
/** Sync worker with optional taskId for progress reporting. */
|
||||
public String importDetailPrices(Long priceSetId, Long psvId, byte[] bytes, String filename, String operName, String taskId) throws Exception;
|
||||
}
|
||||
|
||||
+68
@@ -8,6 +8,7 @@ import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
@@ -421,6 +422,73 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<BmsProductCountry>> parseQuoteExcelByProduct(MultipartFile file) throws Exception
|
||||
{
|
||||
if (file == null || file.isEmpty())
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.product.import.fileEmpty"));
|
||||
}
|
||||
return parseQuoteExcelByProduct(file.getBytes(), file.getOriginalFilename());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<BmsProductCountry>> parseQuoteExcelByProduct(byte[] bytes, String filename) throws Exception
|
||||
{
|
||||
if (bytes == null || bytes.length == 0)
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.product.import.fileEmpty"));
|
||||
}
|
||||
if (filename == null || !(filename.toLowerCase(Locale.ROOT).endsWith(".xlsx")
|
||||
|| filename.toLowerCase(Locale.ROOT).endsWith(".xls")))
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.product.import.fileType"));
|
||||
}
|
||||
try (InputStream is = new java.io.ByteArrayInputStream(bytes); Workbook workbook = WorkbookFactory.create(is))
|
||||
{
|
||||
Sheet catalog = workbook.getSheet(CATALOG_SHEET);
|
||||
if (catalog == null)
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.product.import.sheetNotFound"));
|
||||
}
|
||||
List<BmsProduct> products = dedupeCatalogProducts(parseCatalog(catalog));
|
||||
if (products.isEmpty())
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.product.import.noData"));
|
||||
}
|
||||
Map<String, BmsRegion> regionByNameCn = loadRegionByNameCn();
|
||||
List<BmsRegion> euRegions = loadEuRegions(regionByNameCn);
|
||||
Map<String, PrcZone> zoneByCountryAndCode = loadZoneByCountryAndCode();
|
||||
Set<String> unmatchedCountries = new HashSet<>();
|
||||
Map<String, List<BmsProductCountry>> out = new LinkedHashMap<>();
|
||||
for (BmsProduct product : products)
|
||||
{
|
||||
if (product == null || StringUtils.isEmpty(product.getProductCode()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Sheet productSheet = findProductSheet(workbook, product);
|
||||
if (productSheet == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
List<BmsProductCountry> rows = parseProductSheet(productSheet, product,
|
||||
regionByNameCn, euRegions, zoneByCountryAndCode, unmatchedCountries);
|
||||
if (rows == null || rows.isEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String code = product.getProductCode().trim().toUpperCase(Locale.ROOT);
|
||||
out.put(code, rows);
|
||||
}
|
||||
if (out.isEmpty())
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.set.import.noPriceRows"));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BmsProductPriceVersion> selectPriceVersionOptions()
|
||||
{
|
||||
|
||||
+271
@@ -9,12 +9,14 @@ import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.zip.CRC32;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import com.bms.common.exception.ServiceException;
|
||||
import com.bms.common.utils.DateUtils;
|
||||
import com.bms.common.utils.MessageUtils;
|
||||
@@ -38,6 +40,7 @@ import com.bms.price.mapper.PrcPriceSetVersionMapper;
|
||||
import com.bms.price.mapper.PrcPriceSetVersionSurchargeMapper;
|
||||
import com.bms.price.mapper.PrcSurchargeMapper;
|
||||
import com.bms.price.mapper.PrcSurchargeRelMapper;
|
||||
import com.bms.price.service.IBmsProductService;
|
||||
import com.bms.price.service.IPrcPriceSetService;
|
||||
import com.bms.price.task.PrcPriceSetSaveAsyncRunner;
|
||||
|
||||
@@ -87,6 +90,10 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
@Autowired
|
||||
private BmsProductCountryMapper productCountryMapper;
|
||||
|
||||
@Lazy
|
||||
@Autowired
|
||||
private IBmsProductService productService;
|
||||
|
||||
@Lazy
|
||||
@Autowired
|
||||
private PrcPriceSetSaveAsyncRunner saveAsyncRunner;
|
||||
@@ -797,6 +804,270 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
return rows;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String submitImportDetailPricesAsync(Long priceSetId, Long psvId, MultipartFile file, String operName) throws Exception
|
||||
{
|
||||
PrcPriceSet db = requireSet(priceSetId);
|
||||
assertChildEditable(db);
|
||||
if (!TYPE_FIXED.equals(db.getSetType()))
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.set.import.fixedOnly"));
|
||||
}
|
||||
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"));
|
||||
}
|
||||
byte[] bytes = file.getBytes();
|
||||
if (psvId == null)
|
||||
{
|
||||
psvId = resolveCurrentPsvId(db);
|
||||
}
|
||||
String taskId = IdUtils.fastSimpleUUID();
|
||||
saveAsyncRunner.put(taskId, PrcPriceSetSaveTask.STATUS_PENDING, 0, "queued", priceSetId, null);
|
||||
saveAsyncRunner.runImportPrices(taskId, priceSetId, psvId, bytes, filename, operName);
|
||||
return taskId;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public String importDetailPrices(Long priceSetId, Long psvId, byte[] bytes, String filename, String operName) throws Exception
|
||||
{
|
||||
return importDetailPrices(priceSetId, psvId, bytes, filename, operName, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public String importDetailPrices(Long priceSetId, Long psvId, byte[] bytes, String filename, String operName, String taskId) throws Exception
|
||||
{
|
||||
PrcPriceSet db = requireSet(priceSetId);
|
||||
assertChildEditable(db);
|
||||
if (!TYPE_FIXED.equals(db.getSetType()))
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.set.import.fixedOnly"));
|
||||
}
|
||||
if (psvId == null)
|
||||
{
|
||||
psvId = resolveCurrentPsvId(db);
|
||||
}
|
||||
reportImportProgress(taskId, priceSetId, 20, "parsing excel");
|
||||
Map<String, List<BmsProductCountry>> byProduct = productService.parseQuoteExcelByProduct(bytes, filename);
|
||||
reportImportProgress(taskId, priceSetId, 50, "matching details");
|
||||
PrcPriceSetDetail q = new PrcPriceSetDetail();
|
||||
q.setPriceSetId(priceSetId);
|
||||
if (psvId != null)
|
||||
{
|
||||
q.setPsvId(psvId);
|
||||
}
|
||||
List<PrcPriceSetDetail> details = detailMapper.selectPrcPriceSetDetailList(q);
|
||||
if (details == null || details.isEmpty())
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.set.import.noDetails"));
|
||||
}
|
||||
Map<String, List<PrcPriceSetDetail>> detailIndex = new HashMap<String, List<PrcPriceSetDetail>>();
|
||||
for (PrcPriceSetDetail d : details)
|
||||
{
|
||||
if (d == null || StringUtils.isEmpty(d.getProductCode()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String pc = d.getProductCode().trim().toUpperCase(Locale.ROOT);
|
||||
List<PrcPriceSetDetail> list = detailIndex.get(pc);
|
||||
if (list == null)
|
||||
{
|
||||
list = new ArrayList<PrcPriceSetDetail>();
|
||||
detailIndex.put(pc, list);
|
||||
}
|
||||
list.add(d);
|
||||
}
|
||||
int updated = 0;
|
||||
int unmatched = 0;
|
||||
int skippedProduct = 0;
|
||||
Set<Long> touched = new HashSet<Long>();
|
||||
for (Map.Entry<String, List<BmsProductCountry>> e : byProduct.entrySet())
|
||||
{
|
||||
String productCode = e.getKey();
|
||||
List<PrcPriceSetDetail> candidates = detailIndex.get(productCode);
|
||||
if (candidates == null || candidates.isEmpty())
|
||||
{
|
||||
skippedProduct++;
|
||||
continue;
|
||||
}
|
||||
for (BmsProductCountry row : e.getValue())
|
||||
{
|
||||
if (row == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
List<PrcPriceSetDetail> hits = matchImportDetails(candidates, row);
|
||||
if (hits.isEmpty())
|
||||
{
|
||||
unmatched++;
|
||||
continue;
|
||||
}
|
||||
for (PrcPriceSetDetail hit : hits)
|
||||
{
|
||||
if (hit.getPsdId() == null || touched.contains(hit.getPsdId()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
detailMapper.updateDetailPrices(hit.getPsdId(), row.getFreightPrice(), row.getRegistrationFee());
|
||||
hit.setFvValue(row.getFreightPrice());
|
||||
hit.setFvRegistrationFee(row.getRegistrationFee());
|
||||
touched.add(hit.getPsdId());
|
||||
updated++;
|
||||
}
|
||||
}
|
||||
}
|
||||
reportImportProgress(taskId, priceSetId, 95, "finishing");
|
||||
return MessageUtils.message("price.set.import.success",
|
||||
Integer.valueOf(updated), Integer.valueOf(unmatched), Integer.valueOf(skippedProduct));
|
||||
}
|
||||
|
||||
private void reportImportProgress(String taskId, Long priceSetId, int percent, String message)
|
||||
{
|
||||
if (StringUtils.isEmpty(taskId) || saveAsyncRunner == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
saveAsyncRunner.put(taskId, PrcPriceSetSaveTask.STATUS_RUNNING, percent, message, priceSetId, null);
|
||||
}
|
||||
|
||||
/** Resolve editable version id: draft > pending > effect > first version. */
|
||||
private Long resolveCurrentPsvId(PrcPriceSet set)
|
||||
{
|
||||
if (set == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (set.getVersion() != null && set.getVersion().getPsvId() != null)
|
||||
{
|
||||
return set.getVersion().getPsvId();
|
||||
}
|
||||
List<PrcPriceSetVersion> versions = set.getVersions();
|
||||
if (versions == null || versions.isEmpty())
|
||||
{
|
||||
PrcPriceSetVersion q = new PrcPriceSetVersion();
|
||||
q.setPriceSetId(set.getPriceSetId());
|
||||
versions = versionMapper.selectPrcPriceSetVersionList(q);
|
||||
}
|
||||
if (versions == null || versions.isEmpty())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
PrcPriceSetVersion draft = null;
|
||||
PrcPriceSetVersion pending = null;
|
||||
PrcPriceSetVersion effect = null;
|
||||
for (PrcPriceSetVersion v : versions)
|
||||
{
|
||||
if (v == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (VER_DRAFT.equals(v.getStatus()) && draft == null)
|
||||
{
|
||||
draft = v;
|
||||
}
|
||||
else if (VER_PENDING.equals(v.getStatus()) && pending == null)
|
||||
{
|
||||
pending = v;
|
||||
}
|
||||
else if (VER_EFFECT.equals(v.getStatus()) && effect == null)
|
||||
{
|
||||
effect = v;
|
||||
}
|
||||
}
|
||||
PrcPriceSetVersion pick = draft != null ? draft : (pending != null ? pending : (effect != null ? effect : versions.get(0)));
|
||||
return pick == null ? null : pick.getPsvId();
|
||||
}
|
||||
|
||||
private List<PrcPriceSetDetail> matchImportDetails(List<PrcPriceSetDetail> candidates, BmsProductCountry row)
|
||||
{
|
||||
List<PrcPriceSetDetail> hits = new ArrayList<PrcPriceSetDetail>();
|
||||
if (candidates == null || row == null)
|
||||
{
|
||||
return hits;
|
||||
}
|
||||
String importZone = normZone(row.getZoneCode());
|
||||
for (PrcPriceSetDetail d : candidates)
|
||||
{
|
||||
if (!sameCountry(d, row))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!sameWeight(d.getWeightMin(), d.getWeightMax(), row.getWeightMin(), row.getWeightMax()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String detailZone = normZone(d.getZoneCode());
|
||||
if (StringUtils.isNotEmpty(importZone))
|
||||
{
|
||||
if (!importZone.equals(detailZone))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (StringUtils.isNotEmpty(detailZone))
|
||||
{
|
||||
// Excel has no zone: only match non-zone details
|
||||
continue;
|
||||
}
|
||||
hits.add(d);
|
||||
}
|
||||
return hits;
|
||||
}
|
||||
|
||||
private boolean sameCountry(PrcPriceSetDetail d, BmsProductCountry row)
|
||||
{
|
||||
if (d == null || row == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (StringUtils.isNotEmpty(d.getCountryCode()) && StringUtils.isNotEmpty(row.getCountryCode()))
|
||||
{
|
||||
return d.getCountryCode().trim().equalsIgnoreCase(row.getCountryCode().trim());
|
||||
}
|
||||
if (StringUtils.isNotEmpty(d.getCountryName()) && StringUtils.isNotEmpty(row.getCountryName()))
|
||||
{
|
||||
return d.getCountryName().trim().equals(row.getCountryName().trim());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean sameWeight(BigDecimal aMin, BigDecimal aMax, BigDecimal bMin, BigDecimal bMax)
|
||||
{
|
||||
return decimalEq(aMin, bMin) && decimalEq(aMax, bMax);
|
||||
}
|
||||
|
||||
private boolean decimalEq(BigDecimal a, BigDecimal b)
|
||||
{
|
||||
if (a == null && b == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (a == null || b == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return a.compareTo(b) == 0;
|
||||
}
|
||||
|
||||
private String normZone(String zone)
|
||||
{
|
||||
if (StringUtils.isEmpty(zone))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
return zone.trim().toUpperCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int updateDetail(PrcPriceSetDetail detail)
|
||||
{
|
||||
|
||||
@@ -76,6 +76,30 @@ public class PrcPriceSetSaveAsyncRunner
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Background import of quote Excel into package detail prices. */
|
||||
@Async("threadPoolTaskExecutor")
|
||||
public void runImportPrices(String taskId, Long priceSetId, Long psvId, byte[] bytes, String filename, String operName)
|
||||
{
|
||||
try
|
||||
{
|
||||
put(taskId, PrcPriceSetSaveTask.STATUS_RUNNING, 10, "parsing excel", priceSetId, null);
|
||||
String message = priceSetService.importDetailPrices(priceSetId, psvId, bytes, filename, operName, taskId);
|
||||
put(taskId, PrcPriceSetSaveTask.STATUS_SUCCESS, 100, message, priceSetId, null);
|
||||
log.info("async import prices done, taskId={}, priceSetId={}", taskId, priceSetId);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.error("async import prices failed, taskId={}, priceSetId={}", taskId, priceSetId, e);
|
||||
String msg = e.getMessage();
|
||||
if (StringUtils.isEmpty(msg))
|
||||
{
|
||||
msg = e.getClass().getSimpleName();
|
||||
}
|
||||
put(taskId, PrcPriceSetSaveTask.STATUS_FAIL, 100, "failed", priceSetId, msg);
|
||||
}
|
||||
}
|
||||
|
||||
public void put(String taskId, String status, int percent, String message, Long priceSetId, String error)
|
||||
{
|
||||
PrcPriceSetSaveTask task = new PrcPriceSetSaveTask();
|
||||
|
||||
@@ -23,6 +23,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<result property="productCountryId" column="product_country_id" />
|
||||
<result property="fvValue" column="fv_value" />
|
||||
<result property="fvRegistrationFee" column="fv_registration_fee" />
|
||||
<result property="baseFreightPrice" column="base_freight_price" />
|
||||
<result property="baseRegistrationFee" column="base_registration_fee" />
|
||||
<result property="discountValue" column="discount_value" />
|
||||
<result property="roundMode" column="round_mode" />
|
||||
<result property="prId" column="pr_id" />
|
||||
@@ -46,54 +48,62 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
</select>
|
||||
|
||||
<select id="selectPrcPriceSetDetailList" parameterType="PrcPriceSetDetail" resultMap="PrcPriceSetDetailResult">
|
||||
<include refid="selectPrcPriceSetDetailVo"/>
|
||||
select d.psd_id, d.price_set_id, d.psv_id, d.product_code, d.product_name, d.country_code, d.country_name,
|
||||
d.zone_id, d.zone_code, d.zone_name,
|
||||
d.zd_id, d.wsd_id, d.weight_label, d.weight_min, d.weight_max, d.product_country_id,
|
||||
d.fv_value, d.fv_registration_fee,
|
||||
c.freight_price as base_freight_price,
|
||||
c.registration_fee as base_registration_fee,
|
||||
d.discount_value, d.round_mode, d.pr_id, d.effective_date, d.expire_date, d.create_time, d.update_time
|
||||
from prc_price_set_detail d
|
||||
left join bms_product_country c on c.detail_id = d.product_country_id
|
||||
<where>
|
||||
<if test="priceSetId != null">
|
||||
AND price_set_id = #{priceSetId}
|
||||
AND d.price_set_id = #{priceSetId}
|
||||
</if>
|
||||
<if test="psvId != null">
|
||||
AND psv_id = #{psvId}
|
||||
AND d.psv_id = #{psvId}
|
||||
</if>
|
||||
<if test="productCode != null and productCode != ''">
|
||||
AND product_code = #{productCode}
|
||||
AND d.product_code = #{productCode}
|
||||
</if>
|
||||
<if test="countryCode != null and countryCode != ''">
|
||||
AND country_code = #{countryCode}
|
||||
AND d.country_code = #{countryCode}
|
||||
</if>
|
||||
<if test="zoneCode != null and zoneCode != ''">
|
||||
AND zone_code = #{zoneCode}
|
||||
AND d.zone_code = #{zoneCode}
|
||||
</if>
|
||||
<if test="productKeyword != null and productKeyword != ''">
|
||||
AND (
|
||||
product_code like concat('%', #{productKeyword}, '%')
|
||||
or product_name like concat('%', #{productKeyword}, '%')
|
||||
d.product_code like concat('%', #{productKeyword}, '%')
|
||||
or d.product_name like concat('%', #{productKeyword}, '%')
|
||||
)
|
||||
</if>
|
||||
<if test="countryKeyword != null and countryKeyword != ''">
|
||||
AND (
|
||||
country_code like concat('%', #{countryKeyword}, '%')
|
||||
or country_name like concat('%', #{countryKeyword}, '%')
|
||||
d.country_code like concat('%', #{countryKeyword}, '%')
|
||||
or d.country_name like concat('%', #{countryKeyword}, '%')
|
||||
)
|
||||
</if>
|
||||
<if test="zoneKeyword != null and zoneKeyword != ''">
|
||||
AND (
|
||||
zone_code like concat('%', #{zoneKeyword}, '%')
|
||||
or zone_name like concat('%', #{zoneKeyword}, '%')
|
||||
d.zone_code like concat('%', #{zoneKeyword}, '%')
|
||||
or d.zone_name like concat('%', #{zoneKeyword}, '%')
|
||||
)
|
||||
</if>
|
||||
<if test="keyword != null and keyword != ''">
|
||||
AND (
|
||||
product_code like concat('%', #{keyword}, '%')
|
||||
or product_name like concat('%', #{keyword}, '%')
|
||||
or country_code like concat('%', #{keyword}, '%')
|
||||
or country_name like concat('%', #{keyword}, '%')
|
||||
or zone_code like concat('%', #{keyword}, '%')
|
||||
or zone_name like concat('%', #{keyword}, '%')
|
||||
or weight_label like concat('%', #{keyword}, '%')
|
||||
d.product_code like concat('%', #{keyword}, '%')
|
||||
or d.product_name like concat('%', #{keyword}, '%')
|
||||
or d.country_code like concat('%', #{keyword}, '%')
|
||||
or d.country_name like concat('%', #{keyword}, '%')
|
||||
or d.zone_code like concat('%', #{keyword}, '%')
|
||||
or d.zone_name like concat('%', #{keyword}, '%')
|
||||
or d.weight_label like concat('%', #{keyword}, '%')
|
||||
)
|
||||
</if>
|
||||
</where>
|
||||
order by psd_id asc
|
||||
order by d.psd_id asc
|
||||
</select>
|
||||
|
||||
<select id="countProductByPriceSetId" parameterType="Long" resultType="int">
|
||||
@@ -271,6 +281,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
)
|
||||
</insert>
|
||||
|
||||
<update id="updateDetailPrices">
|
||||
update prc_price_set_detail
|
||||
set fv_value = #{fvValue},
|
||||
fv_registration_fee = #{fvRegistrationFee},
|
||||
update_time = sysdate()
|
||||
where psd_id = #{psdId}
|
||||
</update>
|
||||
|
||||
<update id="updatePrcPriceSetDetail" parameterType="PrcPriceSetDetail">
|
||||
update prc_price_set_detail
|
||||
<set>
|
||||
|
||||
@@ -189,6 +189,51 @@ export function addSetDetail(data) {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Async import quote Excel into FIXED package detail prices; poll until done.
|
||||
* @param {File|Blob} file
|
||||
* @param {{ priceSetId: string|number, psvId?: string|number }} query
|
||||
* @param {(task: object) => void} [onProgress]
|
||||
* @returns {Promise<{ task: object }>}
|
||||
*/
|
||||
export async function importSetDetailPricesAsync(file, query, onProgress) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
const params = new URLSearchParams()
|
||||
if (query && query.priceSetId != null) params.set('priceSetId', query.priceSetId)
|
||||
if (query && query.psvId != null && query.psvId !== '') params.set('psvId', query.psvId)
|
||||
const submitRes = await request({
|
||||
url: '/price/set/detail/importPrices?' + params.toString(),
|
||||
method: 'post',
|
||||
data: formData,
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
const taskId = pickSaveTaskId(submitRes)
|
||||
if (!taskId) {
|
||||
throw new Error('missing import taskId')
|
||||
}
|
||||
const started = Date.now()
|
||||
const maxMs = 30 * 60 * 1000
|
||||
while (Date.now() - started < maxMs) {
|
||||
const res = await getPriceSetSaveTask(taskId)
|
||||
const task = res.data || {}
|
||||
if (typeof onProgress === 'function') {
|
||||
onProgress(task)
|
||||
}
|
||||
if (task.status === 'SUCCESS') {
|
||||
return { task }
|
||||
}
|
||||
if (task.status === 'FAIL') {
|
||||
const err = new Error(task.error || task.message || 'import failed')
|
||||
err.task = task
|
||||
throw err
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 800))
|
||||
}
|
||||
throw new Error('import timeout')
|
||||
}
|
||||
|
||||
export function updateSetDetail(data) {
|
||||
return request({
|
||||
url: '/price/set/detail',
|
||||
|
||||
@@ -95,7 +95,7 @@ const props = defineProps({
|
||||
// 上传接口地址(必传)
|
||||
action: {
|
||||
type: String,
|
||||
required: true
|
||||
default: ''
|
||||
},
|
||||
// 模板下载接口地址(后端接口,走 proxy.download),不传则不显示下载模板链接
|
||||
templateAction: {
|
||||
@@ -156,6 +156,11 @@ const props = defineProps({
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
// Custom async submit: (uploadFile) => Promise<string|void>
|
||||
submitHandler: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
// 确定按钮文字
|
||||
submitText: {
|
||||
type: String,
|
||||
@@ -291,7 +296,7 @@ function handleSuccess(response) {
|
||||
}
|
||||
|
||||
// 提交上传
|
||||
function handleSubmit() {
|
||||
async function handleSubmit() {
|
||||
if (typeof props.beforeSubmit === 'function') {
|
||||
const msg = props.beforeSubmit()
|
||||
if (msg) {
|
||||
@@ -305,6 +310,29 @@ function handleSubmit() {
|
||||
proxy.$modal.msgError(err)
|
||||
return
|
||||
}
|
||||
if (typeof props.submitHandler === 'function') {
|
||||
isUploading.value = true
|
||||
progressPercent.value = 0
|
||||
try {
|
||||
const resultMsg = await props.submitHandler(file)
|
||||
visible.value = false
|
||||
validateError.value = ''
|
||||
selectedFile.value = null
|
||||
uploadRef.value?.clearFiles()
|
||||
const html = resultMsg == null || resultMsg === ''
|
||||
? '导入完成'
|
||||
: String(resultMsg)
|
||||
proxy.$alert("<div style='overflow:auto;overflow-x:hidden;max-height:70vh;padding:10px 20px 0;'>" + html + '</div>', '导入结果', { dangerouslyUseHTMLString: true })
|
||||
emit('success')
|
||||
} catch (e) {
|
||||
const tip = (e && e.message) ? e.message : '导入失败'
|
||||
proxy.$modal.msgError(tip)
|
||||
} finally {
|
||||
isUploading.value = false
|
||||
progressPercent.value = 0
|
||||
}
|
||||
return
|
||||
}
|
||||
uploadRef.value.submit()
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,16 @@
|
||||
<template #default="{ row }">{{ row.zoneName || row.zoneCode || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colWeight" prop="weightLabel" min-width="120" />
|
||||
<el-table-column :label="colBaseFv" width="130" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="price-text base-price">{{ formatNum(row._baseFreight, 4) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colBaseReg" width="130" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="price-text base-price">{{ formatNum(row._baseReg, 2) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colSetFv" width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="price-cell">
|
||||
@@ -111,11 +121,13 @@ const title = computed(() => {
|
||||
const name = props.productName || ''
|
||||
return code + ' · ' + name + ' · 国家服务价格'
|
||||
})
|
||||
const hint = '黄色行=已改价;点编辑图标后修改,失焦即刻写入数据库。单位:运费 RMB/KG,挂号费 RMB/票。'
|
||||
const hint = '黄色行=已改价(与产品基础价不同);点编辑图标后修改,失焦即刻写入数据库。单位:运费 RMB/KG,挂号费 RMB/票。'
|
||||
const phSearch = '搜索国家/分区'
|
||||
const colCountry = '国家'
|
||||
const colZone = '分区'
|
||||
const colWeight = '重量段'
|
||||
const colBaseFv = '产品基础运费'
|
||||
const colBaseReg = '产品基础挂号费'
|
||||
const colSetFv = '套餐运费'
|
||||
const colSetReg = '套餐挂号费'
|
||||
const colSurcharge = '附加费'
|
||||
@@ -289,7 +301,7 @@ watch(() => props.modelValue, v => {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: min(960px, 96vw);
|
||||
width: min(1180px, 98vw);
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
box-shadow: -4px 0 16px rgba(0, 0, 0, 0.12);
|
||||
@@ -333,6 +345,9 @@ watch(() => props.modelValue, v => {
|
||||
min-width: 64px;
|
||||
text-align: right;
|
||||
}
|
||||
.base-price {
|
||||
color: #909399;
|
||||
}
|
||||
.edit-ico {
|
||||
padding: 0 2px !important;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
v-if="loaded"
|
||||
:details="details"
|
||||
:price-set-id="priceSetId"
|
||||
:psv-id="version.psvId"
|
||||
:set-type="setType"
|
||||
:effective-time="version.effectiveTime"
|
||||
:version-no="version.versionNo"
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<el-option v-for="d in bms_tax_mode" :key="d.value" :label="d.label" :value="d.value" />
|
||||
</el-select>
|
||||
<el-button size="small" @click="noop">查询</el-button>
|
||||
<el-button size="small" type="primary" plain :disabled="!priceSetId || !isFixedType(setType)" @click="openImport">导入修改价格</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="filteredProducts" border empty-text="暂无产品价格明细">
|
||||
@@ -74,10 +75,36 @@
|
||||
@surcharge-change="onRowSurchargeChange"
|
||||
@pick-surcharge="openSurchargeFromDrawer"
|
||||
/>
|
||||
|
||||
<excel-import-dialog
|
||||
ref="importRef"
|
||||
title="导入修改套餐价格"
|
||||
width="560px"
|
||||
:show-update-support="false"
|
||||
:import-desc="importDesc"
|
||||
:submit-handler="runImportAsync"
|
||||
@success="onImportSuccess"
|
||||
/>
|
||||
|
||||
<el-dialog
|
||||
v-model="importProgressVisible"
|
||||
title="正在导入价格"
|
||||
width="420px"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
:show-close="false"
|
||||
append-to-body
|
||||
>
|
||||
<div class="import-progress">
|
||||
<el-progress :percentage="importProgressPercent" :status="importProgressStatus" />
|
||||
<div class="import-progress-msg">{{ importProgressMessage }}</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup name="StepDetails">
|
||||
import ExcelImportDialog from '@/components/ExcelImportDialog'
|
||||
import PriceEditDrawer from './PriceEditDrawer.vue'
|
||||
import SurchargePickerDialog from './SurchargePickerDialog.vue'
|
||||
import { useDetailCascade } from '../../composables/usePriceSetRefData'
|
||||
@@ -90,11 +117,12 @@ import {
|
||||
surchargeAppliesToRow,
|
||||
toDetailPayload
|
||||
} from '../../utils/priceSet'
|
||||
import { updateSetDetail, updateSetDetailSurchargeAsync, delSetDetail } from '@/api/price/set'
|
||||
import { getPriceSet, importSetDetailPricesAsync, updateSetDetail, updateSetDetailSurchargeAsync, delSetDetail } from '@/api/price/set'
|
||||
|
||||
const props = defineProps({
|
||||
details: { type: Array, required: true },
|
||||
priceSetId: { type: [Number, String], default: undefined },
|
||||
psvId: { type: [Number, String], default: undefined },
|
||||
setType: { type: String, default: 'FIXED' },
|
||||
effectiveTime: { type: String, default: undefined },
|
||||
versionNo: { type: String, default: '' },
|
||||
@@ -137,6 +165,16 @@ const discVisible = ref(false)
|
||||
const discTitle = ref('编辑价格')
|
||||
const discValue = ref(undefined)
|
||||
const discCode = ref('')
|
||||
const importRef = ref()
|
||||
const importProgressVisible = ref(false)
|
||||
const importProgressPercent = ref(0)
|
||||
const importProgressMessage = ref('')
|
||||
const importProgressStatus = ref(undefined)
|
||||
const importDesc = [
|
||||
'适用威狮B2C小包价表(含「产品目录」)。',
|
||||
'按产品+国家+分区(有则匹配)+重量段匹配已有套餐明细,仅更新运费/挂号费。',
|
||||
'仅固定价套餐可导入;未匹配行不会新增明细。'
|
||||
]
|
||||
|
||||
const typeHint = computed(() => '· 当前:' + setTypeLabel(props.setType))
|
||||
|
||||
@@ -175,6 +213,85 @@ const pulling = ref(false)
|
||||
|
||||
function noop() {}
|
||||
|
||||
function openImport() {
|
||||
if (!props.priceSetId) {
|
||||
proxy.$modal.msgWarning('缺少套餐ID')
|
||||
return
|
||||
}
|
||||
if (!isFixedType(props.setType)) {
|
||||
proxy.$modal.msgWarning('仅固定价套餐支持导入')
|
||||
return
|
||||
}
|
||||
importRef.value && importRef.value.open()
|
||||
}
|
||||
|
||||
|
||||
async function runImportAsync(uploadFile) {
|
||||
const raw = uploadFile && (uploadFile.raw || uploadFile)
|
||||
if (!raw) {
|
||||
throw new Error('请先选择文件')
|
||||
}
|
||||
importProgressVisible.value = true
|
||||
importProgressPercent.value = 0
|
||||
importProgressMessage.value = '上传并解析中…'
|
||||
importProgressStatus.value = undefined
|
||||
try {
|
||||
const { task } = await importSetDetailPricesAsync(
|
||||
raw,
|
||||
{ priceSetId: props.priceSetId, psvId: props.psvId },
|
||||
(t) => {
|
||||
importProgressPercent.value = Math.min(100, Number(t.percent) || 0)
|
||||
if (t.status === 'PENDING') importProgressMessage.value = '排队中…'
|
||||
else if (t.status === 'RUNNING') {
|
||||
const m = t.message || ''
|
||||
if (m === 'parsing excel') importProgressMessage.value = '正在解析价表…'
|
||||
else if (m === 'matching details') importProgressMessage.value = '正在匹配并更新价格…'
|
||||
else if (m === 'finishing') importProgressMessage.value = '收尾中…'
|
||||
else if (m === 'queued') importProgressMessage.value = '排队中…'
|
||||
else importProgressMessage.value = m || '正在导入…'
|
||||
} else if (t.status === 'SUCCESS') {
|
||||
importProgressMessage.value = t.message || '导入完成'
|
||||
importProgressStatus.value = 'success'
|
||||
} else if (t.status === 'FAIL') {
|
||||
importProgressMessage.value = t.error || t.message || '导入失败'
|
||||
importProgressStatus.value = 'exception'
|
||||
}
|
||||
}
|
||||
)
|
||||
importProgressPercent.value = 100
|
||||
importProgressStatus.value = 'success'
|
||||
return (task && task.message) ? task.message : '导入完成'
|
||||
} catch (e) {
|
||||
importProgressStatus.value = 'exception'
|
||||
const tip = (e && e.task && e.task.error) || (e && e.message) || '导入失败'
|
||||
importProgressMessage.value = tip
|
||||
await new Promise(resolve => setTimeout(resolve, 600))
|
||||
throw e
|
||||
} finally {
|
||||
importProgressVisible.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onImportSuccess() {
|
||||
if (!props.priceSetId) return
|
||||
try {
|
||||
const res = await getPriceSet(props.priceSetId)
|
||||
const data = res.data || {}
|
||||
const versions = data.versions || []
|
||||
const qPsv = props.psvId
|
||||
const byQuery = qPsv != null ? versions.find(v => String(v.psvId) === String(qPsv)) : null
|
||||
const current = byQuery || data.version || versions[0] || {}
|
||||
const src = (current.details && current.details.length) ? current.details : (data.details || [])
|
||||
props.details.splice(0, props.details.length, ...src.map(d => hydrateRow(d, props.setType)))
|
||||
if (priceVisible.value && editCode.value) {
|
||||
const group = productGroups.value.find(p => p.productCode === editCode.value)
|
||||
editRows.value = group ? group.rows : []
|
||||
editGroup.value = group || null
|
||||
await stampPriceFloors(editRows.value, editCode.value)
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
/** enter step 3: pull all library products + country prices (skip already added) */
|
||||
async function pullAllFromLibrary(opts = {}) {
|
||||
const silent = !!opts.silent
|
||||
@@ -268,11 +385,7 @@ function openPrice(row) {
|
||||
}
|
||||
editCode.value = row.productCode
|
||||
editName.value = row.productName || ''
|
||||
editRows.value = (row.rows || []).map(r => {
|
||||
if (r._baseFreight == null && r.fvValue != null) r._baseFreight = Number(r.fvValue)
|
||||
if (r._baseReg == null && r.fvRegistrationFee != null) r._baseReg = Number(r.fvRegistrationFee)
|
||||
return r
|
||||
})
|
||||
editRows.value = row.rows || []
|
||||
editGroup.value = row
|
||||
Promise.resolve(stampPriceFloors(editRows.value, row.productCode)).finally(() => {
|
||||
priceVisible.value = true
|
||||
@@ -326,8 +439,14 @@ async function onRowPriceChange(row) {
|
||||
|
||||
function applyImportedFloor(row, hit) {
|
||||
if (!row || !hit) return
|
||||
if (hit.freightPrice != null) row._floorFreight = Number(hit.freightPrice)
|
||||
if (hit.registrationFee != null) row._floorReg = Number(hit.registrationFee)
|
||||
if (hit.freightPrice != null) {
|
||||
row._floorFreight = Number(hit.freightPrice)
|
||||
row._baseFreight = Number(hit.freightPrice)
|
||||
}
|
||||
if (hit.registrationFee != null) {
|
||||
row._floorReg = Number(hit.registrationFee)
|
||||
row._baseReg = Number(hit.registrationFee)
|
||||
}
|
||||
}
|
||||
|
||||
function findImportedCountry(rows, row) {
|
||||
@@ -341,6 +460,11 @@ function findImportedCountry(rows, row) {
|
||||
&& String(row.countryCode).toUpperCase() === String(item.countryCode).toUpperCase())
|
||||
|| (!row.countryCode && row.countryName && item.countryName && row.countryName === item.countryName)
|
||||
if (!sameCountry) return false
|
||||
const rz = String(row.zoneCode || '').toUpperCase()
|
||||
const iz = String(item.zoneCode || '').toUpperCase()
|
||||
if (rz || iz) {
|
||||
if (rz !== iz) return false
|
||||
}
|
||||
if (row.weightMin != null && item.weightMin != null && Number(row.weightMin) !== Number(item.weightMin)) return false
|
||||
if (row.weightMax != null && item.weightMax != null && Number(row.weightMax) !== Number(item.weightMax)) return false
|
||||
return row.weightMin != null || row.weightMax != null
|
||||
@@ -514,6 +638,15 @@ defineExpose({ pullAllFromLibrary })
|
||||
font-size: 13px;
|
||||
}
|
||||
.mb12 { margin-bottom: 12px; }
|
||||
.import-progress {
|
||||
padding: 8px 4px 0;
|
||||
}
|
||||
.import-progress-msg {
|
||||
margin-top: 12px;
|
||||
color: #606266;
|
||||
font-size: 13px;
|
||||
word-break: break-all;
|
||||
}
|
||||
.filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -36,19 +36,33 @@
|
||||
<el-table-column label="重量段" prop="weightLabel" min-width="130" />
|
||||
<el-table-column label="运费单价" min-width="130" align="right">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.fvValue == null">
|
||||
<span class="unit">未覆盖</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="hl">{{ row.fvValue }}</span>
|
||||
<span class="unit"> RMB/KG</span>
|
||||
</template>
|
||||
<el-tooltip placement="top" :disabled="!hasBaseFreight(row)">
|
||||
<template #content>
|
||||
<span>产品基础运费:{{ formatBase(row.baseFreightPrice, 4) }} RMB/KG</span>
|
||||
</template>
|
||||
<span class="price-tip">
|
||||
<template v-if="row.fvValue == null">
|
||||
<span class="unit">未覆盖</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="hl">{{ row.fvValue }}</span>
|
||||
<span class="unit"> RMB/KG</span>
|
||||
</template>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="挂号费" min-width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<span class="reg">{{ row.fvRegistrationFee }}</span>
|
||||
<span class="unit"> RMB/票</span>
|
||||
<el-tooltip placement="top" :disabled="!hasBaseReg(row)">
|
||||
<template #content>
|
||||
<span>产品基础挂号费:{{ formatBase(row.baseRegistrationFee, 2) }} RMB/票</span>
|
||||
</template>
|
||||
<span class="price-tip">
|
||||
<span class="reg">{{ row.fvRegistrationFee }}</span>
|
||||
<span class="unit"> RMB/票</span>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
@@ -139,6 +153,19 @@ const hint = computed(() => isFixedType(props.set.setType)
|
||||
? '运费单价单位 RMB/KG(留空表示不覆盖,按产品基础价计费),挂号费单位 RMB/票;附加费来自产品关联配置,此处只读展示。'
|
||||
: '未配置明细时全部在售产品均适用套餐默认折扣。')
|
||||
|
||||
function hasBaseFreight(row) {
|
||||
return row && row.baseFreightPrice != null && row.baseFreightPrice !== ''
|
||||
}
|
||||
function hasBaseReg(row) {
|
||||
return row && row.baseRegistrationFee != null && row.baseRegistrationFee !== ''
|
||||
}
|
||||
function formatBase(v, precision) {
|
||||
if (v == null || v === '') return '-'
|
||||
const n = Number(v)
|
||||
if (Number.isNaN(n)) return String(v)
|
||||
return n.toFixed(precision)
|
||||
}
|
||||
|
||||
function roundModeLabel(mode) {
|
||||
const hit = (bms_round_mode.value || []).find(d => d.value === mode)
|
||||
return hit ? hit.label : (mode || '四舍五入')
|
||||
@@ -199,4 +226,11 @@ function remove(row) {
|
||||
color: #909399;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.price-tip {
|
||||
cursor: help;
|
||||
border-bottom: 1px dashed transparent;
|
||||
}
|
||||
.price-tip:hover {
|
||||
border-bottom-color: #c0c4cc;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -73,19 +73,33 @@
|
||||
<el-table-column :label="colWeight" prop="weightLabel" min-width="120" />
|
||||
<el-table-column :label="colFv" min-width="130" align="right">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.fvValue == null">
|
||||
<span class="unit">未覆盖</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="hl">{{ row.fvValue }}</span>
|
||||
<span class="unit"> RMB/KG</span>
|
||||
</template>
|
||||
<el-tooltip placement="top" :disabled="!hasBaseFreight(row)">
|
||||
<template #content>
|
||||
<span>产品基础运费:{{ formatBase(row.baseFreightPrice, 4) }} RMB/KG</span>
|
||||
</template>
|
||||
<span class="price-tip">
|
||||
<template v-if="row.fvValue == null">
|
||||
<span class="unit">未覆盖</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="hl">{{ row.fvValue }}</span>
|
||||
<span class="unit"> RMB/KG</span>
|
||||
</template>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colReg" min-width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<span class="reg">{{ row.fvRegistrationFee }}</span>
|
||||
<span class="unit"> RMB/票</span>
|
||||
<el-tooltip placement="top" :disabled="!hasBaseReg(row)">
|
||||
<template #content>
|
||||
<span>产品基础挂号费:{{ formatBase(row.baseRegistrationFee, 2) }} RMB/票</span>
|
||||
</template>
|
||||
<span class="price-tip">
|
||||
<span class="reg">{{ row.fvRegistrationFee }}</span>
|
||||
<span class="unit"> RMB/票</span>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
@@ -211,6 +225,19 @@ const title = computed(() => {
|
||||
return (v.versionNo || '-') + ' · 版本明细'
|
||||
})
|
||||
|
||||
function hasBaseFreight(row) {
|
||||
return row && row.baseFreightPrice != null && row.baseFreightPrice !== ''
|
||||
}
|
||||
function hasBaseReg(row) {
|
||||
return row && row.baseRegistrationFee != null && row.baseRegistrationFee !== ''
|
||||
}
|
||||
function formatBase(v, precision) {
|
||||
if (v == null || v === '') return '-'
|
||||
const n = Number(v)
|
||||
if (Number.isNaN(n)) return String(v)
|
||||
return n.toFixed(precision)
|
||||
}
|
||||
|
||||
function roundModeLabel(mode) {
|
||||
const hit = (bms_round_mode.value || []).find(d => d.value === mode)
|
||||
return hit ? hit.label : (mode || '四舍五入')
|
||||
@@ -313,4 +340,11 @@ watch(
|
||||
.reg { color: #d48806; font-weight: 600; }
|
||||
.pct { color: #1890ff; font-weight: 600; }
|
||||
.unit { font-size: 12px; color: #909399; }
|
||||
.price-tip {
|
||||
cursor: help;
|
||||
border-bottom: 1px dashed transparent;
|
||||
}
|
||||
.price-tip:hover {
|
||||
border-bottom-color: #c0c4cc;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -323,10 +323,7 @@ export function useDetailCascade(refData) {
|
||||
row.weightLabel = formatWeightLabel(row.weightMin, row.weightMax)
|
||||
}
|
||||
row._countryKey = countryKeyOf(row)
|
||||
// Snapshot from DB: treat current package price as baseline so drawer cells stay editable
|
||||
// and yellow "changed" highlight still works after first edit.
|
||||
if (row._baseFreight == null && row.fvValue != null) row._baseFreight = Number(row.fvValue)
|
||||
if (row._baseReg == null && row.fvRegistrationFee != null) row._baseReg = Number(row.fvRegistrationFee)
|
||||
// Keep package prices in fv*; product base is stamped later from bms_product_country.
|
||||
return row
|
||||
}
|
||||
refData.ensureCountries(row.productCode).then(rows => {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
ref="detailsRef"
|
||||
:details="form.details"
|
||||
:price-set-id="form.priceSetId"
|
||||
:psv-id="form.version.psvId"
|
||||
:set-type="form.basic.setType"
|
||||
:version-no="form.version.versionNo"
|
||||
:ref-data="refData"
|
||||
|
||||
Reference in New Issue
Block a user