提交价格功能绑定
This commit is contained in:
@@ -24,6 +24,10 @@ public class BmsProductCountry implements Serializable
|
||||
private BigDecimal weightMin;
|
||||
/** 重量段最大值 KG */
|
||||
private BigDecimal weightMax;
|
||||
/** 运费 RMB/KG */
|
||||
private BigDecimal freightPrice;
|
||||
/** 挂号费 RMB/票 */
|
||||
private BigDecimal registrationFee;
|
||||
private String sizeLimit;
|
||||
private String lastMile;
|
||||
private String leadTime;
|
||||
@@ -46,6 +50,10 @@ public class BmsProductCountry implements Serializable
|
||||
public void setWeightMin(BigDecimal weightMin) { this.weightMin = weightMin; }
|
||||
public BigDecimal getWeightMax() { return weightMax; }
|
||||
public void setWeightMax(BigDecimal weightMax) { this.weightMax = weightMax; }
|
||||
public BigDecimal getFreightPrice() { return freightPrice; }
|
||||
public void setFreightPrice(BigDecimal freightPrice) { this.freightPrice = freightPrice; }
|
||||
public BigDecimal getRegistrationFee() { return registrationFee; }
|
||||
public void setRegistrationFee(BigDecimal registrationFee) { this.registrationFee = registrationFee; }
|
||||
public String getSizeLimit() { return sizeLimit; }
|
||||
public void setSizeLimit(String sizeLimit) { this.sizeLimit = sizeLimit; }
|
||||
public String getLastMile() { return lastMile; }
|
||||
@@ -69,6 +77,8 @@ public class BmsProductCountry implements Serializable
|
||||
.append("countryCode", getCountryCode())
|
||||
.append("weightMin", getWeightMin())
|
||||
.append("weightMax", getWeightMax())
|
||||
.append("freightPrice", getFreightPrice())
|
||||
.append("registrationFee", getRegistrationFee())
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ public interface IBmsProductService
|
||||
public List<BmsProductCountry> selectCountryListByProductCode(String productCode);
|
||||
|
||||
/**
|
||||
* 导入威狮B2C小包价表:产品目录 + 各产品 sheet 国家服务属性(不含运费/挂号费)
|
||||
* 导入威狮B2C小包价表:产品目录 + 各产品 sheet 国家×重量段(含运费/挂号费/尺寸/尾程/时效)
|
||||
*
|
||||
* @param file 上传的 xlsx/xls
|
||||
* @param updateSupport 是否更新已存在产品
|
||||
|
||||
+145
-13
@@ -198,6 +198,14 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
updateNum++;
|
||||
countryNum += countries.size();
|
||||
}
|
||||
else if (!countries.isEmpty())
|
||||
{
|
||||
// Product already exists and "update" unchecked: still refresh country/fee rows from Excel.
|
||||
countryMapper.deleteByProductCode(product.getProductCode());
|
||||
saveCountries(product.getProductCode(), countries);
|
||||
updateNum++;
|
||||
countryNum += countries.size();
|
||||
}
|
||||
else
|
||||
{
|
||||
skipNum++;
|
||||
@@ -216,7 +224,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
}
|
||||
String msg = "\u5bfc\u5165\u6210\u529f\uff1a\u65b0\u589e " + successNum + " \u4e2a\u4ea7\u54c1\uff0c\u66f4\u65b0 " + updateNum + " \u4e2a\uff0c\u8df3\u8fc7 "
|
||||
+ skipNum + " \u4e2a\uff1b\u5199\u5165\u56fd\u5bb6/\u91cd\u91cf\u6bb5\u660e\u7ec6 " + countryNum + " \u884c\u3002"
|
||||
+ "<br/>\u8303\u56f4\uff1a\u4ea7\u54c1\u76ee\u5f55 + \u91cd\u91cf\u6bb5min/max + \u5c3a\u5bf8/\u5c3e\u7a0b/\u65f6\u6548\uff1b\u56fd\u5bb6\u7801\u6765\u81ea bms_region.country_name_cn / country_code_2\u3002";
|
||||
+ "<br/>\u8303\u56f4\uff1a\u4ea7\u54c1\u76ee\u5f55 + \u91cd\u91cf\u6bb5 + \u8fd0\u8d39/\u6302\u53f7\u8d39 + \u5c3a\u5bf8/\u5c3e\u7a0b/\u65f6\u6548\uff1b\u56fd\u5bb6\u7801\u6765\u81ea bms_region\u3002";
|
||||
if (!unmatchedCountries.isEmpty())
|
||||
{
|
||||
msg += "<br/>\u672a\u5339\u914d\u56fd\u5bb6\u8868\uff08country_code \u4e3a\u7a7a\uff09\uff1a" + String.join("\u3001", unmatchedCountries);
|
||||
@@ -230,8 +238,21 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
for (BmsProductCountry c : countries)
|
||||
{
|
||||
c.setProductCode(productCode);
|
||||
try
|
||||
{
|
||||
countryMapper.insertBmsProductCountry(c);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
String tip = e.getMessage() == null ? "" : e.getMessage();
|
||||
if (tip.contains("freight_price") || tip.contains("registration_fee") || tip.contains("Unknown column"))
|
||||
{
|
||||
throw new ServiceException(
|
||||
"\u8bf7\u5148\u6267\u884c SQL\uff1abms-back/sql/price/product_country_fee_alter.sql\uff08\u7f3a\u5c11\u8fd0\u8d39/\u6302\u53f7\u8d39\u5b57\u6bb5\uff09");
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<BmsProduct> parseCatalog(Sheet sheet)
|
||||
@@ -308,24 +329,60 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
|
||||
Date sheetEffective = parseSheetEffectiveDate(sheet);
|
||||
List<BmsProductCountry> list = new ArrayList<>();
|
||||
// Product sheets also often start at column B.
|
||||
int[] header = findHeaderCell(sheet, "\u56fd\u5bb6");
|
||||
int baseCol = header != null ? header[1] : 0;
|
||||
int startRow = header != null ? header[0] + 1 : 0;
|
||||
int[] countryHeader = findHeaderCell(sheet, "\u56fd\u5bb6");
|
||||
if (countryHeader == null)
|
||||
{
|
||||
return list;
|
||||
}
|
||||
int headerRowIdx = countryHeader[0];
|
||||
Row headerRow = sheet.getRow(headerRowIdx);
|
||||
int colCountry = countryHeader[1];
|
||||
int colWeight = findColByHeader(headerRow, "\u91cd\u91cf", colCountry + 1);
|
||||
int colFreight = findColByHeader(headerRow, "\u8fd0\u8d39", colCountry + 1);
|
||||
int colRegister = findColByHeader(headerRow, "\u6302\u53f7", colCountry + 1);
|
||||
int colSize = findColByHeader(headerRow, "\u5c3a\u5bf8", colCountry + 1);
|
||||
int colLastMile = findColByHeader(headerRow, "\u5c3e\u7a0b", colCountry + 1);
|
||||
int colLeadTime = findColByHeader(headerRow, "\u65f6\u6548", colCountry + 1);
|
||||
// Fallback offsets when header text missing (Weishi layout: B=country ... H=lead time)
|
||||
if (colWeight < 0)
|
||||
{
|
||||
colWeight = colCountry + 1;
|
||||
}
|
||||
if (colFreight < 0)
|
||||
{
|
||||
colFreight = colCountry + 2;
|
||||
}
|
||||
if (colRegister < 0)
|
||||
{
|
||||
colRegister = colCountry + 3;
|
||||
}
|
||||
if (colSize < 0)
|
||||
{
|
||||
colSize = colCountry + 4;
|
||||
}
|
||||
if (colLastMile < 0)
|
||||
{
|
||||
colLastMile = colCountry + 5;
|
||||
}
|
||||
if (colLeadTime < 0)
|
||||
{
|
||||
colLeadTime = colCountry + 6;
|
||||
}
|
||||
|
||||
String currentCountry = null;
|
||||
String currentSize = null;
|
||||
String currentLastMile = null;
|
||||
String currentLeadTime = null;
|
||||
int lastRow = sheet.getLastRowNum();
|
||||
for (int i = startRow; i <= lastRow; i++)
|
||||
for (int i = headerRowIdx + 1; i <= lastRow; i++)
|
||||
{
|
||||
Row row = sheet.getRow(i);
|
||||
if (row == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String countryCell = cellString(row.getCell(baseCol));
|
||||
String weightLabel = cellString(row.getCell(baseCol + 1));
|
||||
String countryCell = cellString(row.getCell(colCountry));
|
||||
String weightLabel = cellString(row.getCell(colWeight));
|
||||
if ("\u56fd\u5bb6".equals(countryCell) || weightLabel.contains("\u91cd\u91cf\u6bb5"))
|
||||
{
|
||||
continue;
|
||||
@@ -333,9 +390,9 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
if (StringUtils.isNotEmpty(countryCell))
|
||||
{
|
||||
currentCountry = countryCell.trim();
|
||||
currentSize = emptyToNull(cellString(row.getCell(baseCol + 4)));
|
||||
currentLastMile = emptyToNull(cellString(row.getCell(baseCol + 5)));
|
||||
currentLeadTime = emptyToNull(cellString(row.getCell(baseCol + 6)));
|
||||
currentSize = emptyToNull(cellString(row.getCell(colSize)));
|
||||
currentLastMile = emptyToNull(cellString(row.getCell(colLastMile)));
|
||||
currentLeadTime = emptyToNull(cellString(row.getCell(colLeadTime)));
|
||||
}
|
||||
if (StringUtils.isEmpty(currentCountry))
|
||||
{
|
||||
@@ -361,6 +418,8 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
}
|
||||
c.setWeightMin(range[0]);
|
||||
c.setWeightMax(range[1]);
|
||||
c.setFreightPrice(cellDecimal(row.getCell(colFreight)));
|
||||
c.setRegistrationFee(cellDecimal(row.getCell(colRegister)));
|
||||
c.setSizeLimit(currentSize);
|
||||
c.setLastMile(currentLastMile);
|
||||
c.setLeadTime(currentLeadTime);
|
||||
@@ -371,7 +430,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
}
|
||||
|
||||
/**
|
||||
* ???? bms_region??country_name_cn -> ????? country_code_2??
|
||||
* Load bms_region map: country_name_cn -> region (for country_code_2).
|
||||
*/
|
||||
private Map<String, BmsRegion> loadRegionByNameCn()
|
||||
{
|
||||
@@ -395,7 +454,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
}
|
||||
|
||||
/**
|
||||
* ????? country_name_cn ?????????????????????????? ??? / ?????????
|
||||
* Exact country_name_cn match first; single contains match as fallback (e.g. Czech / Czech Republic).
|
||||
*/
|
||||
private BmsRegion resolveRegion(String excelName, Map<String, BmsRegion> regionByNameCn)
|
||||
{
|
||||
@@ -507,6 +566,28 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
return "TAX_INCLUDED";
|
||||
}
|
||||
|
||||
/**
|
||||
* Find column index on header row whose text contains keyword; -1 if not found.
|
||||
*/
|
||||
private static int findColByHeader(Row headerRow, String keyword, int fromCol)
|
||||
{
|
||||
if (headerRow == null || StringUtils.isEmpty(keyword))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
int last = Math.max(headerRow.getLastCellNum(), fromCol + 12);
|
||||
last = Math.min(last, 30);
|
||||
for (int c = Math.max(fromCol, 0); c < last; c++)
|
||||
{
|
||||
String v = cellString(headerRow.getCell(c));
|
||||
if (StringUtils.isNotEmpty(v) && v.contains(keyword))
|
||||
{
|
||||
return c;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static String emptyToNull(String s)
|
||||
{
|
||||
return StringUtils.isEmpty(s) ? null : s;
|
||||
@@ -595,6 +676,57 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
}
|
||||
}
|
||||
|
||||
private static BigDecimal cellDecimal(Cell cell)
|
||||
{
|
||||
if (cell == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
CellType type = cell.getCellType();
|
||||
if (type == CellType.FORMULA)
|
||||
{
|
||||
type = cell.getCachedFormulaResultType();
|
||||
}
|
||||
try
|
||||
{
|
||||
if (type == CellType.NUMERIC)
|
||||
{
|
||||
return BigDecimal.valueOf(cell.getNumericCellValue());
|
||||
}
|
||||
String s = cellString(cell);
|
||||
if (StringUtils.isEmpty(s))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
// strip currency / unit suffixes: 56、56元、56 RMB
|
||||
s = s.replace(",", "").replace("\uffe5", "").replace("\u5143", "")
|
||||
.replace("RMB", "").replace("rmb", "").trim();
|
||||
if (s.isEmpty())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
int cut = -1;
|
||||
for (int i = 0; i < s.length(); i++)
|
||||
{
|
||||
char ch = s.charAt(i);
|
||||
if (!(ch == '.' || ch == '-' || (ch >= '0' && ch <= '9')))
|
||||
{
|
||||
cut = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (cut > 0)
|
||||
{
|
||||
s = s.substring(0, cut).trim();
|
||||
}
|
||||
return new BigDecimal(s);
|
||||
}
|
||||
catch (Exception ignored)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Date cellDate(Cell cell)
|
||||
{
|
||||
if (cell == null)
|
||||
|
||||
+14
-8
@@ -332,7 +332,8 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
int incomplete = 0;
|
||||
for (PrcPriceSetDetail d : details)
|
||||
{
|
||||
if (StringUtils.isEmpty(d.getProductCode()) || StringUtils.isEmpty(d.getCountryCode())
|
||||
boolean noCountry = StringUtils.isEmpty(d.getCountryCode()) && StringUtils.isEmpty(d.getCountryName());
|
||||
if (StringUtils.isEmpty(d.getProductCode()) || noCountry
|
||||
|| (d.getWsdId() == null && StringUtils.isEmpty(d.getWeightLabel()))
|
||||
|| d.getFvValue() == null || d.getFvRegistrationFee() == null)
|
||||
{
|
||||
@@ -341,15 +342,16 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
if (details.isEmpty())
|
||||
{
|
||||
items.add(new PrcPriceSetCheckItem("FIXED 明细完整性", "FAIL", "固定价模式至少需要 1 条明细"));
|
||||
items.add(new PrcPriceSetCheckItem("FIXED 明细完整性", "PASS",
|
||||
"\u65e0\u5957\u9910\u660e\u7ec6\uff1a\u8ba1\u8d39\u53d6 bms_product / bms_product_country \u9ed8\u8ba4\u8fd0\u8d39\u4e0e\u6302\u53f7\u8d39"));
|
||||
}
|
||||
else if (incomplete > 0)
|
||||
{
|
||||
items.add(new PrcPriceSetCheckItem("FIXED 明细完整性", "FAIL", incomplete + " 条明细缺少产品/国家/重量段/运费/挂号费"));
|
||||
items.add(new PrcPriceSetCheckItem("FIXED 明细完整性", "FAIL", incomplete + " \u6761\u660e\u7ec6\u7f3a\u5c11\u4ea7\u54c1/\u56fd\u5bb6/\u91cd\u91cf\u6bb5/\u8fd0\u8d39/\u6302\u53f7\u8d39"));
|
||||
}
|
||||
else
|
||||
{
|
||||
items.add(new PrcPriceSetCheckItem("FIXED 明细完整性", "PASS", details.size() + " 条明细,运费与挂号费均填写"));
|
||||
items.add(new PrcPriceSetCheckItem("FIXED 明细完整性", "PASS", details.size() + " \u6761\u8986\u76d6\u660e\u7ec6\uff08\u672a\u8986\u76d6\u7684\u4ea7\u54c1\u56fd\u5bb6\u91cd\u91cf\u6bb5\u4ecd\u8d70\u9ed8\u8ba4\u4ef7\uff09"));
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -418,11 +420,15 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
|
||||
if (TYPE_FIXED.equals(setType) && !details.isEmpty())
|
||||
{
|
||||
items.add(new PrcPriceSetCheckItem("运费单价 vs 默认等级价偏差", "INFO", "默认等级价对照将在计费试算上线后自动计算"));
|
||||
items.add(new PrcPriceSetCheckItem("\u8fd0\u8d39\u5355\u4ef7 vs \u9ed8\u8ba4\u7b49\u7ea7\u4ef7\u504f\u5dee", "INFO", "\u6709\u8986\u76d6\u660e\u7ec6\u65f6\u4f18\u5148\u53d6\u5957\u9910\u56fa\u5b9a\u4ef7\uff1b\u672a\u547d\u4e2d\u5219\u56de\u9000 bms_product_country"));
|
||||
}
|
||||
else if (TYPE_FIXED.equals(setType))
|
||||
{
|
||||
items.add(new PrcPriceSetCheckItem("\u8fd0\u8d39\u5355\u4ef7 vs \u9ed8\u8ba4\u7b49\u7ea7\u4ef7\u504f\u5dee", "INFO", "\u65e0\u5957\u9910\u660e\u7ec6\uff0c\u5168\u90e8\u8d70 bms_product_country \u9ed8\u8ba4\u8fd0\u8d39/\u6302\u53f7\u8d39"));
|
||||
}
|
||||
else if (TYPE_DISCOUNT.equals(setType))
|
||||
{
|
||||
items.add(new PrcPriceSetCheckItem("运费单价 vs 默认等级价偏差", "INFO", "折扣模式按下单时默认等级价 × 折扣计费"));
|
||||
items.add(new PrcPriceSetCheckItem("\u8fd0\u8d39\u5355\u4ef7 vs \u9ed8\u8ba4\u7b49\u7ea7\u4ef7\u504f\u5dee", "INFO", "\u6298\u6263\u6a21\u5f0f\u6309\u4e0b\u5355\u65f6\u9ed8\u8ba4\u7b49\u7ea7\u4ef7 \u00d7 \u6298\u6263\u8ba1\u8d39"));
|
||||
}
|
||||
|
||||
return items;
|
||||
@@ -599,9 +605,9 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
if (TYPE_FIXED.equals(set.getSetType()))
|
||||
{
|
||||
if (StringUtils.isEmpty(detail.getCountryCode()))
|
||||
if (StringUtils.isEmpty(detail.getCountryCode()) && StringUtils.isEmpty(detail.getCountryName()))
|
||||
{
|
||||
throw new ServiceException("固定价明细必须填写国家");
|
||||
throw new ServiceException("\u56fa\u5b9a\u4ef7\u660e\u7ec6\u5fc5\u987b\u586b\u5199\u56fd\u5bb6");
|
||||
}
|
||||
if (detail.getWsdId() == null && StringUtils.isEmpty(detail.getWeightLabel()))
|
||||
{
|
||||
|
||||
@@ -11,6 +11,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<result property="countryCode" column="country_code" />
|
||||
<result property="weightMin" column="weight_min" />
|
||||
<result property="weightMax" column="weight_max" />
|
||||
<result property="freightPrice" column="freight_price" />
|
||||
<result property="registrationFee" column="registration_fee" />
|
||||
<result property="sizeLimit" column="size_limit" />
|
||||
<result property="lastMile" column="last_mile" />
|
||||
<result property="leadTime" column="lead_time" />
|
||||
@@ -21,7 +23,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
|
||||
<select id="selectBmsProductCountryList" parameterType="BmsProductCountry" resultMap="BmsProductCountryResult">
|
||||
select detail_id, product_code, country_name, country_code, weight_min, weight_max,
|
||||
size_limit, last_mile, lead_time, effective_date, created_at, updated_at
|
||||
freight_price, registration_fee, size_limit, last_mile, lead_time,
|
||||
effective_date, created_at, updated_at
|
||||
from bms_product_country
|
||||
<where>
|
||||
<if test="productCode != null and productCode != ''">
|
||||
@@ -46,10 +49,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<insert id="insertBmsProductCountry" parameterType="BmsProductCountry" useGeneratedKeys="true" keyProperty="detailId">
|
||||
insert into bms_product_country (
|
||||
product_code, country_name, country_code, weight_min, weight_max,
|
||||
size_limit, last_mile, lead_time, effective_date, created_at
|
||||
freight_price, registration_fee, size_limit, last_mile, lead_time,
|
||||
effective_date, created_at
|
||||
) values (
|
||||
#{productCode}, #{countryName}, #{countryCode}, #{weightMin}, #{weightMax},
|
||||
#{sizeLimit}, #{lastMile}, #{leadTime}, #{effectiveDate}, sysdate()
|
||||
#{freightPrice}, #{registrationFee}, #{sizeLimit}, #{lastMile}, #{leadTime},
|
||||
#{effectiveDate}, sysdate()
|
||||
)
|
||||
</insert>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
-- 产品主数据(来源:威狮B2C小包价表.xlsx)
|
||||
-- 导入范围:产品目录 + 各产品 sheet 国家服务明细(尺寸/尾程/时效)
|
||||
-- 不导入:运费/挂号费矩阵、退件重派、申报说明、赔偿标准
|
||||
-- 导入范围:产品目录 + 各产品 sheet 国家×重量段(运费/挂号费/尺寸/尾程/时效)
|
||||
-- 不导入:退件重派、申报说明、赔偿标准
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS bms_product (
|
||||
@@ -33,6 +33,8 @@ CREATE TABLE IF NOT EXISTS bms_product_country (
|
||||
country_code char(2) DEFAULT NULL COMMENT 'ISO2,可空待补齐',
|
||||
weight_min decimal(10,3) DEFAULT NULL COMMENT '重量段最小值KG(含)',
|
||||
weight_max decimal(10,3) DEFAULT NULL COMMENT '重量段最大值KG(含)',
|
||||
freight_price decimal(10,4) DEFAULT NULL COMMENT '运费 RMB/KG',
|
||||
registration_fee decimal(10,2) DEFAULT NULL COMMENT '挂号费 RMB/票',
|
||||
size_limit varchar(1000) DEFAULT NULL COMMENT '尺寸限制',
|
||||
last_mile varchar(500) DEFAULT NULL COMMENT '尾程派送说明',
|
||||
lead_time varchar(100) DEFAULT NULL COMMENT '参考时效',
|
||||
@@ -44,7 +46,7 @@ CREATE TABLE IF NOT EXISTS bms_product_country (
|
||||
KEY idx_country_code (country_code),
|
||||
KEY idx_weight_match (product_code, country_code, weight_min, weight_max),
|
||||
CONSTRAINT fk_product_country_code FOREIGN KEY (product_code) REFERENCES bms_product (product_code) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='产品国家服务明细-重量段+尺寸/尾程/时效(不含运费)';
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='产品国家明细-重量段+运费挂号费+尺寸/尾程/时效';
|
||||
|
||||
-- 已建表升级(幂等:列已存在时跳过报错可忽略)
|
||||
-- ALTER TABLE bms_product_country ADD COLUMN weight_min decimal(10,3) DEFAULT NULL COMMENT '重量段最小值KG(含)' AFTER country_code;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
-- 产品国家明细增加运费/挂号费(与 Excel 重量段同行)
|
||||
-- 执行后请在「产品列表」重新导入威狮B2C小包价表.xlsx
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
ALTER TABLE bms_product_country
|
||||
ADD COLUMN freight_price decimal(10,4) DEFAULT NULL COMMENT '运费 RMB/KG' AFTER weight_max,
|
||||
ADD COLUMN registration_fee decimal(10,2) DEFAULT NULL COMMENT '挂号费 RMB/票' AFTER freight_price;
|
||||
@@ -0,0 +1,6 @@
|
||||
-- 若之前 ALTER 将 COMMENT 写成乱码,执行此脚本修复
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
ALTER TABLE bms_product_country
|
||||
MODIFY COLUMN freight_price decimal(10,4) DEFAULT NULL COMMENT '运费 RMB/KG',
|
||||
MODIFY COLUMN registration_fee decimal(10,2) DEFAULT NULL COMMENT '挂号费 RMB/票';
|
||||
@@ -57,6 +57,11 @@ const props = defineProps({
|
||||
updateSupportLabel: {
|
||||
type: String,
|
||||
default: '是否更新已经存在的数据'
|
||||
},
|
||||
// 打开时是否默认勾选“更新已存在”
|
||||
defaultUpdateSupport: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
@@ -77,7 +82,7 @@ const templateUrl = computed(() => !!props.templateAction)
|
||||
|
||||
// 打开对话框(供父组件通过 ref 调用)
|
||||
function open() {
|
||||
updateSupport.value = false
|
||||
updateSupport.value = props.defaultUpdateSupport
|
||||
isUploading.value = false
|
||||
visible.value = true
|
||||
nextTick(() => {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="mb16"
|
||||
title="Excel 导入:上传威狮B2C小包价表.xlsx。仅入库产品目录 + 各产品 sheet 的国家尺寸/尾程/时效;不导入运费、挂号费、退件重派、申报说明、赔偿标准。"
|
||||
title="Excel 导入:上传威狮B2C小包价表.xlsx。入库产品目录 + 各产品 sheet 的国家×重量段(运费/挂号费/尺寸/尾程/时效);不导入退件重派、申报说明、赔偿标准。"
|
||||
/>
|
||||
|
||||
<basic-table
|
||||
@@ -47,7 +47,7 @@
|
||||
</template>
|
||||
|
||||
<template #col-action="{ row }">
|
||||
<el-button link type="primary" icon="View" @click="openCountries(row)" v-hasPermi="['price:product:query']">国家明细</el-button>
|
||||
<el-button link type="primary" icon="View" @click="openCountries(row)" v-hasPermi="['price:product:query']">产品明细</el-button>
|
||||
<el-button link type="primary" icon="Edit" @click="handleUpdate(row)" v-hasPermi="['price:product:edit']">编辑</el-button>
|
||||
<el-button link type="primary" icon="Delete" @click="handleDelete(row.productId)" v-hasPermi="['price:product:remove']">删除</el-button>
|
||||
</template>
|
||||
@@ -122,7 +122,7 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-drawer v-model="countryOpen" size="720px" :title="countryTitle" append-to-body>
|
||||
<el-drawer v-model="countryOpen" size="920px" :title="countryTitle" append-to-body>
|
||||
<el-table v-loading="countryLoading" :data="countryList" border empty-text="暂无国家明细">
|
||||
<el-table-column label="国家" prop="countryName" width="90" show-overflow-tooltip />
|
||||
<el-table-column label="ISO2" prop="countryCode" width="60" align="center" />
|
||||
@@ -132,6 +132,8 @@
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="运费(RMB/KG)" prop="freightPrice" width="110" align="right" />
|
||||
<el-table-column label="挂号费(RMB/票)" prop="registrationFee" width="120" align="right" />
|
||||
<el-table-column label="尺寸限制" prop="sizeLimit" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<span class="pre-wrap">{{ row.sizeLimit }}</span>
|
||||
@@ -150,7 +152,8 @@
|
||||
ref="importRef"
|
||||
title="产品 Excel 导入"
|
||||
action="/price/product/importData"
|
||||
update-support-label="是否更新已经存在的产品(含国家明细覆盖)"
|
||||
:default-update-support="true"
|
||||
update-support-label="是否更新已经存在的产品主数据(国家/运费明细每次导入都会覆盖)"
|
||||
@success="() => tableRef.refresh()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
@click="form.setType = 'FIXED'"
|
||||
>
|
||||
<div class="t-title">FIXED 固定价</div>
|
||||
<div class="t-desc">每个产品×国家×重量段独立配置运费单价 + 挂号费,覆盖默认价</div>
|
||||
<div class="t-desc">可不填明细(走产品默认价);有明细则按产品×国家×重量段覆盖运费+挂号费</div>
|
||||
</div>
|
||||
<div
|
||||
class="type-card"
|
||||
@@ -77,7 +77,7 @@
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="mb12"
|
||||
title="FIXED 模式:每个产品 × 国家 × 重量段独立配置运费单价(RMB/KG)+ 挂号费(RMB/票)。命中时直接取该固定价。"
|
||||
title="FIXED 模式:可不填明细(计费取 bms_product_country 默认运费/挂号费);有明细时按产品×国家×重量段覆盖固定价。"
|
||||
/>
|
||||
<el-alert
|
||||
v-else
|
||||
@@ -88,24 +88,57 @@
|
||||
title="DISCOUNT 模式:可不填明细(全部产品适用默认折扣),或按产品限定折扣范围。"
|
||||
/>
|
||||
<el-table :data="form.details" border empty-text="暂无明细,点击添加明细行">
|
||||
<el-table-column label="产品" min-width="150">
|
||||
<el-table-column label="产品" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<el-select v-model="row.productCode" filterable allow-create placeholder="产品" @change="onProductChange(row)">
|
||||
<el-select
|
||||
v-model="row.productCode"
|
||||
filterable
|
||||
clearable
|
||||
placeholder="选择产品"
|
||||
style="width: 100%"
|
||||
@change="onProductChange(row)"
|
||||
>
|
||||
<el-option v-for="p in productOptions" :key="p.value" :label="p.label" :value="p.value" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="form.setType === 'FIXED'" label="国家" min-width="130">
|
||||
<el-table-column v-if="form.setType === 'FIXED'" label="国家" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<el-select v-model="row.countryCode" filterable allow-create placeholder="国家" @change="onCountryChange(row)">
|
||||
<el-option v-for="c in countryOptions" :key="c.value" :label="c.label" :value="c.value" />
|
||||
<el-select
|
||||
v-model="row._countryKey"
|
||||
filterable
|
||||
clearable
|
||||
placeholder="选择国家"
|
||||
style="width: 100%"
|
||||
:disabled="!row.productCode"
|
||||
@change="onCountryChange(row)"
|
||||
>
|
||||
<el-option
|
||||
v-for="c in countryOptionsOf(row)"
|
||||
:key="c.key"
|
||||
:label="c.label"
|
||||
:value="c.key"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="form.setType === 'FIXED'" label="重量段" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<el-select v-model="row.weightLabel" filterable allow-create placeholder="重量段">
|
||||
<el-option v-for="w in weightOptions" :key="w.value" :label="w.label" :value="w.value" />
|
||||
<el-select
|
||||
v-model="row.weightLabel"
|
||||
filterable
|
||||
clearable
|
||||
placeholder="选择重量段"
|
||||
style="width: 100%"
|
||||
:disabled="!row._countryKey"
|
||||
@change="onWeightChange(row)"
|
||||
>
|
||||
<el-option
|
||||
v-for="w in weightOptionsOf(row)"
|
||||
:key="w.value"
|
||||
:label="w.label"
|
||||
:value="w.value"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -212,6 +245,7 @@
|
||||
|
||||
<script setup name="PriceSetCreate">
|
||||
import { getPriceSet, savePriceSet, validatePriceSet } from '@/api/price/set'
|
||||
import { listProduct, listProductCountries } from '@/api/price/product'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const route = useRoute()
|
||||
@@ -219,25 +253,9 @@ const active = ref(0)
|
||||
const saving = ref(false)
|
||||
const formRef = ref()
|
||||
const checkItems = ref([])
|
||||
|
||||
const productOptions = [
|
||||
{ value: 'WSP037', label: 'WSP037 球衣' },
|
||||
{ value: 'WSP038', label: 'WSP038 普货' },
|
||||
{ value: 'WSP034', label: 'WSP034 带电' },
|
||||
{ value: 'WSP035', label: 'WSP035 敏感' },
|
||||
{ value: 'WSP066', label: 'WSP066 鞋盒' }
|
||||
]
|
||||
const countryOptions = [
|
||||
{ value: 'DE', label: '德国 DE' },
|
||||
{ value: 'FR', label: '法国 FR' },
|
||||
{ value: 'IT', label: '意大利 IT' },
|
||||
{ value: 'ES', label: '西班牙 ES' }
|
||||
]
|
||||
const weightOptions = [
|
||||
{ value: '0.01≤W≤0.3', label: '0.01≤W≤0.3' },
|
||||
{ value: '0.3<W≤1', label: '0.3<W≤1' },
|
||||
{ value: '1<W≤2', label: '1<W≤2' }
|
||||
]
|
||||
const productOptions = ref([])
|
||||
/** productCode -> bms_product_country rows */
|
||||
const countryCache = reactive({})
|
||||
|
||||
const form = reactive({
|
||||
priceSetId: undefined,
|
||||
@@ -257,13 +275,93 @@ const rules = {
|
||||
effectiveDate: [{ required: true, message: '生效日期不能为空', trigger: 'change' }]
|
||||
}
|
||||
|
||||
function trimNum(v) {
|
||||
if (v == null || v === '') return ''
|
||||
const n = Number(v)
|
||||
if (Number.isNaN(n)) return String(v)
|
||||
return String(parseFloat(n.toFixed(6)))
|
||||
}
|
||||
|
||||
function formatWeightLabel(min, max) {
|
||||
return `${trimNum(min)}≤W≤${trimNum(max)}`
|
||||
}
|
||||
|
||||
function countryKeyOf(row) {
|
||||
if (!row) return ''
|
||||
if (row.countryCode) return row.countryCode
|
||||
if (row.countryName) return 'N:' + row.countryName
|
||||
return ''
|
||||
}
|
||||
|
||||
function matchCountry(detail, item) {
|
||||
if (detail.countryCode && item.countryCode) {
|
||||
return detail.countryCode === item.countryCode
|
||||
}
|
||||
return detail.countryName && item.countryName === detail.countryName
|
||||
}
|
||||
|
||||
function loadProducts() {
|
||||
return listProduct({ status: '0', pageNum: 1, pageSize: 1000 }).then(res => {
|
||||
const rows = res.rows || []
|
||||
productOptions.value = rows.map(p => ({
|
||||
value: p.productCode,
|
||||
label: `${p.productCode} ${p.productName || ''}`.trim(),
|
||||
name: p.productName
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
function ensureCountries(productCode) {
|
||||
if (!productCode) return Promise.resolve([])
|
||||
if (countryCache[productCode]) return Promise.resolve(countryCache[productCode])
|
||||
return listProductCountries(productCode).then(res => {
|
||||
const rows = res.data || []
|
||||
countryCache[productCode] = rows
|
||||
return rows
|
||||
})
|
||||
}
|
||||
|
||||
function countryOptionsOf(row) {
|
||||
const rows = countryCache[row.productCode] || []
|
||||
const map = new Map()
|
||||
rows.forEach(item => {
|
||||
const key = countryKeyOf(item)
|
||||
if (!key || map.has(key)) return
|
||||
map.set(key, {
|
||||
key,
|
||||
code: item.countryCode || undefined,
|
||||
name: item.countryName,
|
||||
label: item.countryCode ? `${item.countryName} ${item.countryCode}` : item.countryName
|
||||
})
|
||||
})
|
||||
return [...map.values()]
|
||||
}
|
||||
|
||||
function weightOptionsOf(row) {
|
||||
const rows = (countryCache[row.productCode] || []).filter(item => matchCountry(row, item))
|
||||
const map = new Map()
|
||||
rows.forEach(item => {
|
||||
if (item.weightMin == null && item.weightMax == null) return
|
||||
const value = formatWeightLabel(item.weightMin, item.weightMax)
|
||||
if (map.has(value)) return
|
||||
map.set(value, {
|
||||
value,
|
||||
label: value,
|
||||
freightPrice: item.freightPrice,
|
||||
registrationFee: item.registrationFee
|
||||
})
|
||||
})
|
||||
return [...map.values()]
|
||||
}
|
||||
|
||||
function emptyDetail() {
|
||||
return {
|
||||
productCode: undefined,
|
||||
productName: undefined,
|
||||
countryCode: form.setType === 'FIXED' ? 'DE' : undefined,
|
||||
countryName: form.setType === 'FIXED' ? '德国' : undefined,
|
||||
weightLabel: form.setType === 'FIXED' ? '0.3<W≤1' : undefined,
|
||||
countryCode: undefined,
|
||||
countryName: undefined,
|
||||
_countryKey: undefined,
|
||||
weightLabel: undefined,
|
||||
fvValue: undefined,
|
||||
fvRegistrationFee: undefined,
|
||||
discountValue: undefined
|
||||
@@ -288,22 +386,74 @@ function addCustomerRow() {
|
||||
form.customers.push(emptyCustomer())
|
||||
}
|
||||
|
||||
function clearCascadeAfterProduct(row) {
|
||||
row.countryCode = undefined
|
||||
row.countryName = undefined
|
||||
row._countryKey = undefined
|
||||
row.weightLabel = undefined
|
||||
row.fvValue = undefined
|
||||
row.fvRegistrationFee = undefined
|
||||
}
|
||||
|
||||
function onProductChange(row) {
|
||||
const found = productOptions.find(p => p.value === row.productCode)
|
||||
row.productName = found ? found.label.replace(row.productCode + ' ', '') : row.productCode
|
||||
const found = productOptions.value.find(p => p.value === row.productCode)
|
||||
row.productName = found ? found.name : row.productCode
|
||||
clearCascadeAfterProduct(row)
|
||||
if (!row.productCode || form.setType !== 'FIXED') return
|
||||
ensureCountries(row.productCode)
|
||||
}
|
||||
|
||||
function onCountryChange(row) {
|
||||
const found = countryOptions.find(c => c.value === row.countryCode)
|
||||
row.countryName = found ? found.label.replace(' ' + row.countryCode, '') : row.countryCode
|
||||
const opt = countryOptionsOf(row).find(c => c.key === row._countryKey)
|
||||
row.countryCode = opt ? opt.code : undefined
|
||||
row.countryName = opt ? opt.name : undefined
|
||||
row.weightLabel = undefined
|
||||
row.fvValue = undefined
|
||||
row.fvRegistrationFee = undefined
|
||||
}
|
||||
|
||||
function onWeightChange(row) {
|
||||
const opt = weightOptionsOf(row).find(w => w.value === row.weightLabel)
|
||||
if (!opt) return
|
||||
if (opt.freightPrice != null) row.fvValue = Number(opt.freightPrice)
|
||||
if (opt.registrationFee != null) row.fvRegistrationFee = Number(opt.registrationFee)
|
||||
}
|
||||
|
||||
function hydrateDetailRow(d) {
|
||||
const row = {
|
||||
...d,
|
||||
_countryKey: countryKeyOf(d)
|
||||
}
|
||||
// normalize historical weight labels to min≤W≤max when possible
|
||||
if (row.productCode && form.setType === 'FIXED') {
|
||||
ensureCountries(row.productCode).then(rows => {
|
||||
if (!row.weightLabel) return
|
||||
const hit = rows.find(item => {
|
||||
if (!matchCountry(row, item)) return false
|
||||
return formatWeightLabel(item.weightMin, item.weightMax) === row.weightLabel
|
||||
|| row.weightLabel.includes(String(item.weightMin)) && row.weightLabel.includes(String(item.weightMax))
|
||||
})
|
||||
if (hit) {
|
||||
row.weightLabel = formatWeightLabel(hit.weightMin, hit.weightMax)
|
||||
row._countryKey = countryKeyOf(hit)
|
||||
if (!row.countryCode) row.countryCode = hit.countryCode
|
||||
if (!row.countryName) row.countryName = hit.countryName
|
||||
}
|
||||
})
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
||||
function buildPayload(submitFlag) {
|
||||
const details = (form.details || []).filter(d => d.productCode).map(d => ({
|
||||
...d,
|
||||
productCode: d.productCode,
|
||||
productName: d.productName,
|
||||
countryCode: d.countryCode || undefined,
|
||||
countryName: d.countryName || undefined,
|
||||
weightLabel: d.weightLabel || undefined,
|
||||
fvValue: d.fvValue,
|
||||
fvRegistrationFee: d.fvRegistrationFee
|
||||
fvRegistrationFee: d.fvRegistrationFee,
|
||||
discountValue: d.discountValue
|
||||
}))
|
||||
const customers = (form.customers || [])
|
||||
.filter(c => c.customerId)
|
||||
@@ -392,7 +542,7 @@ function checkTag(result) {
|
||||
}
|
||||
|
||||
function checkLabel(result) {
|
||||
const map = { PASS: '? 通过', WARN: '? 待确认', FAIL: '? 失败', INFO: '? 提示' }
|
||||
const map = { PASS: '通过', WARN: '待确认', FAIL: '失败', INFO: '提示' }
|
||||
return map[result] || result
|
||||
}
|
||||
|
||||
@@ -406,18 +556,20 @@ function loadExisting(id) {
|
||||
form.effectiveDate = data.effectiveDate
|
||||
form.expireDate = data.expireDate
|
||||
form.priceSetNote = data.priceSetNote
|
||||
form.details = (data.details || []).map(d => ({ ...d }))
|
||||
form.details = (data.details || []).map(d => hydrateDetailRow(d))
|
||||
form.customers = (data.customers || []).map(c => ({ ...c }))
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadProducts().finally(() => {
|
||||
const id = route.query.priceSetId
|
||||
if (id) {
|
||||
loadExisting(id)
|
||||
} else {
|
||||
addDetailRow()
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -126,21 +126,44 @@
|
||||
|
||||
<el-dialog :title="detailTitle" v-model="detailOpen" width="560px" append-to-body>
|
||||
<el-form ref="detailFormRef" :model="detailForm" :rules="detailRules" label-width="120px">
|
||||
<el-form-item label="产品代码" prop="productCode">
|
||||
<el-input v-model="detailForm.productCode" maxlength="10" />
|
||||
</el-form-item>
|
||||
<el-form-item label="产品名称" prop="productName">
|
||||
<el-input v-model="detailForm.productName" maxlength="50" />
|
||||
<el-form-item label="产品" prop="productCode">
|
||||
<el-select
|
||||
v-model="detailForm.productCode"
|
||||
filterable
|
||||
clearable
|
||||
placeholder="选择产品"
|
||||
style="width: 100%"
|
||||
@change="onDetailProductChange"
|
||||
>
|
||||
<el-option v-for="p in productOptions" :key="p.value" :label="p.label" :value="p.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<template v-if="set.setType === 'FIXED'">
|
||||
<el-form-item label="国家代码" prop="countryCode">
|
||||
<el-input v-model="detailForm.countryCode" maxlength="2" />
|
||||
</el-form-item>
|
||||
<el-form-item label="国家名称" prop="countryName">
|
||||
<el-input v-model="detailForm.countryName" maxlength="50" />
|
||||
<el-form-item label="国家" prop="countryCode">
|
||||
<el-select
|
||||
v-model="detailForm._countryKey"
|
||||
filterable
|
||||
clearable
|
||||
placeholder="选择国家"
|
||||
style="width: 100%"
|
||||
:disabled="!detailForm.productCode"
|
||||
@change="onDetailCountryChange"
|
||||
>
|
||||
<el-option v-for="c in detailCountryOptions" :key="c.key" :label="c.label" :value="c.key" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="重量段" prop="weightLabel">
|
||||
<el-input v-model="detailForm.weightLabel" maxlength="50" />
|
||||
<el-select
|
||||
v-model="detailForm.weightLabel"
|
||||
filterable
|
||||
clearable
|
||||
placeholder="选择重量段"
|
||||
style="width: 100%"
|
||||
:disabled="!detailForm._countryKey"
|
||||
@change="onDetailWeightChange"
|
||||
>
|
||||
<el-option v-for="w in detailWeightOptions" :key="w.value" :label="w.label" :value="w.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="运费单价" prop="fvValue">
|
||||
<el-input-number v-model="detailForm.fvValue" :min="0" :precision="4" controls-position="right" style="width: 100%" />
|
||||
@@ -194,19 +217,22 @@ import {
|
||||
addSetDetail, updateSetDetail, delSetDetail,
|
||||
addSetCustomer, updateSetCustomer, delSetCustomer
|
||||
} from '@/api/price/set'
|
||||
import { listProduct, listProductCountries } from '@/api/price/product'
|
||||
import { parseTime } from '@/utils/ruoyi'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const route = useRoute()
|
||||
const loading = ref(false)
|
||||
const set = ref({})
|
||||
const productOptions = ref([])
|
||||
const countryCache = reactive({})
|
||||
|
||||
const detailOpen = ref(false)
|
||||
const detailTitle = ref('')
|
||||
const detailFormRef = ref()
|
||||
const detailForm = ref({})
|
||||
const detailRules = {
|
||||
productCode: [{ required: true, message: '产品代码不能为空', trigger: 'blur' }]
|
||||
productCode: [{ required: true, message: '请选择产品', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const customerOpen = ref(false)
|
||||
@@ -218,6 +244,113 @@ const customerRules = {
|
||||
prBegindate: [{ required: true, message: '生效日期不能为空', trigger: 'change' }]
|
||||
}
|
||||
|
||||
function trimNum(v) {
|
||||
if (v == null || v === '') return ''
|
||||
const n = Number(v)
|
||||
if (Number.isNaN(n)) return String(v)
|
||||
return String(parseFloat(n.toFixed(6)))
|
||||
}
|
||||
|
||||
function formatWeightLabel(min, max) {
|
||||
return `${trimNum(min)}≤W≤${trimNum(max)}`
|
||||
}
|
||||
|
||||
function countryKeyOf(row) {
|
||||
if (!row) return ''
|
||||
if (row.countryCode) return row.countryCode
|
||||
if (row.countryName) return 'N:' + row.countryName
|
||||
return ''
|
||||
}
|
||||
|
||||
function matchCountry(detail, item) {
|
||||
if (detail.countryCode && item.countryCode) return detail.countryCode === item.countryCode
|
||||
return detail.countryName && item.countryName === detail.countryName
|
||||
}
|
||||
|
||||
function ensureCountries(productCode) {
|
||||
if (!productCode) return Promise.resolve([])
|
||||
if (countryCache[productCode]) return Promise.resolve(countryCache[productCode])
|
||||
return listProductCountries(productCode).then(res => {
|
||||
const rows = res.data || []
|
||||
countryCache[productCode] = rows
|
||||
return rows
|
||||
})
|
||||
}
|
||||
|
||||
const detailCountryOptions = computed(() => {
|
||||
const rows = countryCache[detailForm.value.productCode] || []
|
||||
const map = new Map()
|
||||
rows.forEach(item => {
|
||||
const key = countryKeyOf(item)
|
||||
if (!key || map.has(key)) return
|
||||
map.set(key, {
|
||||
key,
|
||||
code: item.countryCode || undefined,
|
||||
name: item.countryName,
|
||||
label: item.countryCode ? `${item.countryName} ${item.countryCode}` : item.countryName
|
||||
})
|
||||
})
|
||||
return [...map.values()]
|
||||
})
|
||||
|
||||
const detailWeightOptions = computed(() => {
|
||||
const form = detailForm.value
|
||||
const rows = (countryCache[form.productCode] || []).filter(item => matchCountry(form, item))
|
||||
const map = new Map()
|
||||
rows.forEach(item => {
|
||||
if (item.weightMin == null && item.weightMax == null) return
|
||||
const value = formatWeightLabel(item.weightMin, item.weightMax)
|
||||
if (map.has(value)) return
|
||||
map.set(value, {
|
||||
value,
|
||||
label: value,
|
||||
freightPrice: item.freightPrice,
|
||||
registrationFee: item.registrationFee
|
||||
})
|
||||
})
|
||||
return [...map.values()]
|
||||
})
|
||||
|
||||
function loadProducts() {
|
||||
listProduct({ status: '0', pageNum: 1, pageSize: 1000 }).then(res => {
|
||||
productOptions.value = (res.rows || []).map(p => ({
|
||||
value: p.productCode,
|
||||
label: `${p.productCode} ${p.productName || ''}`.trim(),
|
||||
name: p.productName
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
function onDetailProductChange() {
|
||||
const found = productOptions.value.find(p => p.value === detailForm.value.productCode)
|
||||
detailForm.value.productName = found ? found.name : detailForm.value.productCode
|
||||
detailForm.value.countryCode = undefined
|
||||
detailForm.value.countryName = undefined
|
||||
detailForm.value._countryKey = undefined
|
||||
detailForm.value.weightLabel = undefined
|
||||
detailForm.value.fvValue = undefined
|
||||
detailForm.value.fvRegistrationFee = undefined
|
||||
if (detailForm.value.productCode && set.value.setType === 'FIXED') {
|
||||
ensureCountries(detailForm.value.productCode)
|
||||
}
|
||||
}
|
||||
|
||||
function onDetailCountryChange() {
|
||||
const opt = detailCountryOptions.value.find(c => c.key === detailForm.value._countryKey)
|
||||
detailForm.value.countryCode = opt ? opt.code : undefined
|
||||
detailForm.value.countryName = opt ? opt.name : undefined
|
||||
detailForm.value.weightLabel = undefined
|
||||
detailForm.value.fvValue = undefined
|
||||
detailForm.value.fvRegistrationFee = undefined
|
||||
}
|
||||
|
||||
function onDetailWeightChange() {
|
||||
const opt = detailWeightOptions.value.find(w => w.value === detailForm.value.weightLabel)
|
||||
if (!opt) return
|
||||
if (opt.freightPrice != null) detailForm.value.fvValue = Number(opt.freightPrice)
|
||||
if (opt.registrationFee != null) detailForm.value.fvRegistrationFee = Number(opt.registrationFee)
|
||||
}
|
||||
|
||||
const childEditable = computed(() => ['D', 'R', 'E'].includes(set.value.status))
|
||||
|
||||
const statusLabel = computed(() => {
|
||||
@@ -296,12 +429,16 @@ function onCopy() {
|
||||
}
|
||||
|
||||
function openDetailDialog(row) {
|
||||
detailForm.value = row ? { ...row } : {
|
||||
detailForm.value = row ? {
|
||||
...row,
|
||||
_countryKey: countryKeyOf(row)
|
||||
} : {
|
||||
priceSetId: set.value.priceSetId,
|
||||
productCode: undefined,
|
||||
productName: undefined,
|
||||
countryCode: undefined,
|
||||
countryName: undefined,
|
||||
_countryKey: undefined,
|
||||
weightLabel: undefined,
|
||||
fvValue: undefined,
|
||||
fvRegistrationFee: undefined,
|
||||
@@ -309,12 +446,19 @@ function openDetailDialog(row) {
|
||||
}
|
||||
detailTitle.value = row ? '编辑明细' : '添加明细'
|
||||
detailOpen.value = true
|
||||
if (detailForm.value.productCode && set.value.setType === 'FIXED') {
|
||||
ensureCountries(detailForm.value.productCode)
|
||||
}
|
||||
}
|
||||
|
||||
function submitDetail() {
|
||||
proxy.$refs['detailFormRef'].validate(valid => {
|
||||
if (!valid) return
|
||||
const payload = { ...detailForm.value, priceSetId: set.value.priceSetId }
|
||||
const payload = {
|
||||
...detailForm.value,
|
||||
priceSetId: set.value.priceSetId
|
||||
}
|
||||
delete payload._countryKey
|
||||
const req = payload.psdId ? updateSetDetail(payload) : addSetDetail(payload)
|
||||
req.then(() => {
|
||||
proxy.$modal.msgSuccess('保存成功')
|
||||
@@ -369,6 +513,7 @@ function removeCustomer(row) {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadProducts()
|
||||
load()
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user