Compare commits
2 Commits
da9efc422e
...
f26e03efb1
| Author | SHA1 | Date | |
|---|---|---|---|
| f26e03efb1 | |||
| 1a0483c7fe |
+61
-2
@@ -2,6 +2,7 @@ package com.bms.web.controller.price;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
@@ -173,6 +174,15 @@ public class LabelPackageController extends BaseController
|
||||
return success(payload);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:label:remove')")
|
||||
@Log(title = "打单套餐删除版本", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{priceSetId}/versions/{versionNo}")
|
||||
public AjaxResult deleteVersion(@PathVariable Long priceSetId, @PathVariable Integer versionNo)
|
||||
{
|
||||
priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_LABEL);
|
||||
return toAjax(payProductService.deleteLabelVersion(priceSetId, versionNo));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:label:list')")
|
||||
@GetMapping("/{priceSetId}/products")
|
||||
public AjaxResult products(@PathVariable Long priceSetId,
|
||||
@@ -207,6 +217,55 @@ public class LabelPackageController extends BaseController
|
||||
return toAjax(payProductService.removeLabelProducts(priceSetId, productIds));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:label:query')")
|
||||
@GetMapping("/{priceSetId}/channels")
|
||||
public AjaxResult listChannels(@PathVariable Long priceSetId)
|
||||
{
|
||||
priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_LABEL);
|
||||
return success(payProductService.listLabelChannels(priceSetId));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:label:edit')")
|
||||
@Log(title = "打单套餐绑定渠道", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/{priceSetId}/channels")
|
||||
public AjaxResult bindChannels(@PathVariable Long priceSetId, @RequestBody Map<String, Object> body)
|
||||
{
|
||||
priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_LABEL);
|
||||
List<String> channelIds = new ArrayList<String>();
|
||||
Object rawIds = body == null ? null : body.get("channelIds");
|
||||
if (rawIds instanceof List<?>)
|
||||
{
|
||||
for (Object item : (List<?>) rawIds)
|
||||
{
|
||||
if (item != null && !String.valueOf(item).trim().isEmpty())
|
||||
{
|
||||
channelIds.add(String.valueOf(item).trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
Long productId = null;
|
||||
Object rawProductId = body == null ? null : body.get("productId");
|
||||
if (rawProductId instanceof Number)
|
||||
{
|
||||
productId = ((Number) rawProductId).longValue();
|
||||
}
|
||||
else if (rawProductId != null && !String.valueOf(rawProductId).trim().isEmpty()
|
||||
&& !"null".equalsIgnoreCase(String.valueOf(rawProductId).trim()))
|
||||
{
|
||||
productId = Long.valueOf(String.valueOf(rawProductId).trim());
|
||||
}
|
||||
return success(payProductService.bindLabelChannels(priceSetId, channelIds, productId));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:label:edit')")
|
||||
@Log(title = "打单套餐解绑渠道", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{priceSetId}/channels/{channelId}")
|
||||
public AjaxResult unbindChannel(@PathVariable Long priceSetId, @PathVariable String channelId)
|
||||
{
|
||||
priceSetService.assertSetCategory(priceSetId, PrcPriceSet.CATEGORY_LABEL);
|
||||
return toAjax(payProductService.unbindLabelChannel(priceSetId, channelId));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('price:label:list')")
|
||||
@GetMapping("/customer/list")
|
||||
public TableDataInfo customerList(PrcCustomerPriceSet customer)
|
||||
@@ -289,8 +348,8 @@ public class LabelPackageController extends BaseController
|
||||
{
|
||||
return null;
|
||||
}
|
||||
// 兼容 yyyy-MM-dd / yyyy-MM-dd HH:mm:ss
|
||||
if (text.length() >= 19)
|
||||
// 兼容 yyyy-MM-dd / yyyy-MM-dd HH:mm / yyyy-MM-dd HH:mm:ss
|
||||
if (text.length() >= 16)
|
||||
{
|
||||
return com.bms.common.utils.DateUtils.parseDate(text);
|
||||
}
|
||||
|
||||
+43
@@ -1,6 +1,8 @@
|
||||
package com.bms.web.controller.price;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Locale;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
@@ -139,6 +141,29 @@ public class PayProductController extends BaseController
|
||||
return success(payProductService.selectChannelPrice(channelId, payType));
|
||||
}
|
||||
|
||||
/** 批量绑定渠道应收/应付产品 */
|
||||
@PreAuthorize("@ss.hasAnyPermi('price:pay:edit,data:serviceChannel:edit')")
|
||||
@Log(title = "渠道批量绑定产品", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/channel/batchBind")
|
||||
public AjaxResult batchBindChannel(@RequestBody Map<String, Object> body)
|
||||
{
|
||||
List<String> channelIds = new ArrayList<String>();
|
||||
Object rawIds = body == null ? null : body.get("channelIds");
|
||||
if (rawIds instanceof List<?>)
|
||||
{
|
||||
for (Object item : (List<?>) rawIds)
|
||||
{
|
||||
if (item != null && !String.valueOf(item).trim().isEmpty())
|
||||
{
|
||||
channelIds.add(String.valueOf(item).trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
Long apProductId = toLong(body == null ? null : body.get("apProductId"));
|
||||
Long arProductId = toLong(body == null ? null : body.get("arProductId"));
|
||||
return success(payProductService.batchBindChannel(channelIds, apProductId, arProductId));
|
||||
}
|
||||
|
||||
/** 对照两个价格版本 */
|
||||
@PreAuthorize("@ss.hasPermi('price:pay:query')")
|
||||
@GetMapping("/compare")
|
||||
@@ -247,4 +272,22 @@ public class PayProductController extends BaseController
|
||||
throw new ServiceException("价卡文件不能超过30MB");
|
||||
}
|
||||
}
|
||||
|
||||
private Long toLong(Object raw)
|
||||
{
|
||||
if (raw == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (raw instanceof Number)
|
||||
{
|
||||
return ((Number) raw).longValue();
|
||||
}
|
||||
String text = String.valueOf(raw).trim();
|
||||
if (text.isEmpty() || "null".equalsIgnoreCase(text))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return Long.valueOf(text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,15 @@ public class ChannelProductMap
|
||||
|
||||
private Long productId;
|
||||
|
||||
/** 打单套餐 ID,对应 prc_price_set.price_set_id(应收可填) */
|
||||
private Long priceSetId;
|
||||
|
||||
/** 渠道名称(列表回显) */
|
||||
private String channelName;
|
||||
|
||||
/** 产品名称(列表回显) */
|
||||
private String productName;
|
||||
|
||||
/** AR 应收,AP 应付 */
|
||||
private String payType;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.bms.price.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.bms.price.domain.ChannelProductMap;
|
||||
|
||||
@@ -10,9 +11,14 @@ public interface ChannelProductMapMapper
|
||||
{
|
||||
ChannelProductMap selectByChannelAndType(@Param("channelId") String channelId, @Param("payType") String payType);
|
||||
|
||||
List<ChannelProductMap> selectByPriceSetId(@Param("priceSetId") Long priceSetId);
|
||||
|
||||
int upsert(ChannelProductMap row);
|
||||
|
||||
int deleteByChannelAndType(@Param("channelId") String channelId, @Param("payType") String payType);
|
||||
|
||||
int deleteByChannelTypeAndPriceSet(@Param("channelId") String channelId, @Param("payType") String payType,
|
||||
@Param("priceSetId") Long priceSetId);
|
||||
|
||||
int deleteByProductId(@Param("productId") Long productId);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,9 @@ public interface PayProductTermMapper
|
||||
|
||||
PayProductTerm selectByProductId(Long productId);
|
||||
|
||||
/** 打单产品所属套餐 ID */
|
||||
Long selectLabelPriceSetIdByProduct(@Param("productId") Long productId);
|
||||
|
||||
List<PayProductTerm> selectByPriceSetId(@Param("priceSetId") Long priceSetId);
|
||||
|
||||
List<PayProductTerm> selectByPriceSetIdAndVersion(@Param("priceSetId") Long priceSetId,
|
||||
@@ -68,6 +71,9 @@ public interface PayProductTermMapper
|
||||
int restorePrematureClosed(@Param("priceSetId") Long priceSetId, @Param("today") Date today,
|
||||
@Param("updateBy") String updateBy);
|
||||
|
||||
/** 打单:删除待生效版后,清掉旧版预约失效日 */
|
||||
int clearScheduledEnd(@Param("priceSetId") Long priceSetId, @Param("updateBy") String updateBy);
|
||||
|
||||
int deleteByProductIds(@Param("productIds") List<Long> productIds);
|
||||
|
||||
int deleteByTermIds(@Param("termIds") List<Long> termIds);
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.bms.price.domain.LabelPackageVersion;
|
||||
import com.bms.price.domain.PayParseProgress;
|
||||
import com.bms.price.domain.PayPreview;
|
||||
import com.bms.price.domain.PayPriceCompare;
|
||||
import com.bms.price.domain.ChannelProductMap;
|
||||
import com.bms.price.domain.PayProduct;
|
||||
import com.bms.price.domain.PayProductTerm;
|
||||
import com.bms.price.domain.PaySaveCommand;
|
||||
@@ -57,6 +58,9 @@ public interface IPayProductService
|
||||
/** 只绑定产品编号,不产生新版本;同时落 channel_code。payType 为空时按应付 AP */
|
||||
void bindChannel(String channelId, Long productId, String payType);
|
||||
|
||||
/** 批量绑定渠道应收/应付产品;未传的产品不改原绑定 */
|
||||
String batchBindChannel(java.util.List<String> channelIds, Long apProductId, Long arProductId);
|
||||
|
||||
/**
|
||||
* 打单套餐预览:解析复用应付解析器,但「已存在」只按本套餐 price_set_id 判断,
|
||||
* 不与应付公共模板互相挡导入。
|
||||
@@ -72,6 +76,9 @@ public interface IPayProductService
|
||||
/** 启动/修改打单套餐某一版生效时间,并视情况启用套餐 */
|
||||
String activateLabelVersion(Long priceSetId, Integer versionNo, Date effectiveTime);
|
||||
|
||||
/** 删除打单套餐待生效版本(仅 status=0) */
|
||||
int deleteLabelVersion(Long priceSetId, Integer versionNo);
|
||||
|
||||
/** 打单套餐内当前生效/待生效产品列表 */
|
||||
List<PayProduct> selectLabelProducts(Long priceSetId);
|
||||
|
||||
@@ -84,6 +91,15 @@ public interface IPayProductService
|
||||
/** 从打单套餐移除产品(失效/清理对应 term 明细) */
|
||||
int removeLabelProducts(Long priceSetId, Long[] productIds);
|
||||
|
||||
/** 打单套餐已绑定渠道列表 */
|
||||
java.util.List<ChannelProductMap> listLabelChannels(Long priceSetId);
|
||||
|
||||
/** 打单套餐批量绑定渠道(写 AR + price_set_id) */
|
||||
String bindLabelChannels(Long priceSetId, java.util.List<String> channelIds, Long productId);
|
||||
|
||||
/** 打单套餐解绑渠道 */
|
||||
int unbindLabelChannel(Long priceSetId, String channelId);
|
||||
|
||||
/** 取消渠道上指定收付类型的产品。payType 为空时按应付 AP */
|
||||
void unbindChannel(String channelId, String payType);
|
||||
|
||||
|
||||
+230
-23
@@ -893,8 +893,8 @@ public class PayProductServiceImpl implements IPayProductService
|
||||
boolean editPendingVersion = command.getVersionNo() != null;
|
||||
if (editPendingVersion)
|
||||
{
|
||||
// 编辑待生效版:清掉该 version 后按原号重写,保留原生效日
|
||||
LabelVersionKeep keep = clearPendingLabelVersion(priceSetId, command.getVersionNo(), operator);
|
||||
// 编辑待生效/已生效版:清掉该 version 后按原号重写,保留原生效日与状态
|
||||
LabelVersionKeep keep = clearEditableLabelVersion(priceSetId, command.getVersionNo(), operator);
|
||||
keepStart = keep.effectiveStart;
|
||||
keepStatus = keep.status == null ? TERM_PENDING : keep.status;
|
||||
nextVersionNo = command.getVersionNo();
|
||||
@@ -971,8 +971,8 @@ public class PayProductServiceImpl implements IPayProductService
|
||||
private String status;
|
||||
}
|
||||
|
||||
/** 仅允许清待生效版;返回原生效日便于编辑后回写 */
|
||||
private LabelVersionKeep clearPendingLabelVersion(Long priceSetId, Integer versionNo, String operator)
|
||||
/** 编辑版本:允许待生效或已生效;返回原生效日与状态便于回写 */
|
||||
private LabelVersionKeep clearEditableLabelVersion(Long priceSetId, Integer versionNo, String operator)
|
||||
{
|
||||
if (priceSetId == null || versionNo == null)
|
||||
{
|
||||
@@ -987,15 +987,13 @@ public class PayProductServiceImpl implements IPayProductService
|
||||
List<Long> termIds = new ArrayList<>();
|
||||
for (PayProductTerm term : terms)
|
||||
{
|
||||
if (!TERM_PENDING.equals(term.getStatus()))
|
||||
{
|
||||
// 兼容:生效日未到但 status 误为 1,仍允许编辑
|
||||
boolean pending = TERM_PENDING.equals(term.getStatus());
|
||||
boolean effect = TERM_EFFECT.equals(term.getStatus());
|
||||
boolean futureStart = term.getEffectiveStart() != null
|
||||
&& toLocalDate(term.getEffectiveStart()).isAfter(toLocalDate(today()));
|
||||
if (!futureStart)
|
||||
if (!pending && !effect && !futureStart)
|
||||
{
|
||||
throw new ServiceException("只能编辑待生效版本");
|
||||
}
|
||||
throw new ServiceException("只能编辑待生效或已生效版本");
|
||||
}
|
||||
if (keep.effectiveStart == null && term.getEffectiveStart() != null)
|
||||
{
|
||||
@@ -1003,7 +1001,7 @@ public class PayProductServiceImpl implements IPayProductService
|
||||
}
|
||||
if (keep.status == null)
|
||||
{
|
||||
keep.status = TERM_PENDING;
|
||||
keep.status = term.getStatus() == null ? TERM_PENDING : term.getStatus();
|
||||
}
|
||||
termIds.add(term.getTermId());
|
||||
}
|
||||
@@ -1014,6 +1012,148 @@ public class PayProductServiceImpl implements IPayProductService
|
||||
return keep;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int deleteLabelVersion(Long priceSetId, Integer versionNo)
|
||||
{
|
||||
if (priceSetId == null || versionNo == null)
|
||||
{
|
||||
throw new ServiceException("请选择要删除的版本");
|
||||
}
|
||||
PrcPriceSet set = priceSetMapper.selectPrcPriceSetById(priceSetId);
|
||||
if (set == null)
|
||||
{
|
||||
throw new ServiceException("打单套餐不存在");
|
||||
}
|
||||
if (!PrcPriceSet.CATEGORY_LABEL.equalsIgnoreCase(String.valueOf(set.getSetCategory())))
|
||||
{
|
||||
throw new ServiceException("只能删除打单套餐版本");
|
||||
}
|
||||
List<PayProductTerm> terms = payProductTermMapper.selectByPriceSetIdAndVersion(priceSetId, versionNo);
|
||||
if (terms == null || terms.isEmpty())
|
||||
{
|
||||
throw new ServiceException("该版本不存在或没有产品");
|
||||
}
|
||||
List<Long> termIds = new ArrayList<>();
|
||||
for (PayProductTerm term : terms)
|
||||
{
|
||||
boolean pending = TERM_PENDING.equals(term.getStatus());
|
||||
boolean futureStart = term.getEffectiveStart() != null
|
||||
&& toLocalDate(term.getEffectiveStart()).isAfter(toLocalDate(today()));
|
||||
if (!pending && !futureStart)
|
||||
{
|
||||
throw new ServiceException("只能删除待生效版本");
|
||||
}
|
||||
termIds.add(term.getTermId());
|
||||
}
|
||||
String operator = SecurityUtils.getUsername();
|
||||
payProductMapper.deleteWeightBandsByTerm(termIds);
|
||||
payProductMapper.deleteSurchargesByTerm(termIds);
|
||||
payProductMapper.deleteExtraLinesByTerm(termIds);
|
||||
int n = payProductTermMapper.deleteByTermIds(termIds);
|
||||
payProductTermMapper.restorePrematureClosed(priceSetId, today(), operator);
|
||||
payProductTermMapper.clearScheduledEnd(priceSetId, operator);
|
||||
return n;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ChannelProductMap> listLabelChannels(Long priceSetId)
|
||||
{
|
||||
if (priceSetId == null)
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<ChannelProductMap> list = channelProductMapMapper.selectByPriceSetId(priceSetId);
|
||||
return list == null ? Collections.emptyList() : list;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public String bindLabelChannels(Long priceSetId, List<String> channelIds, Long productId)
|
||||
{
|
||||
if (priceSetId == null)
|
||||
{
|
||||
throw new ServiceException("请选择打单套餐");
|
||||
}
|
||||
if (channelIds == null || channelIds.isEmpty())
|
||||
{
|
||||
throw new ServiceException("请选择要绑定的渠道");
|
||||
}
|
||||
if (productId == null)
|
||||
{
|
||||
throw new ServiceException("请选择套餐内应收产品");
|
||||
}
|
||||
PrcPriceSet set = priceSetMapper.selectPrcPriceSetById(priceSetId);
|
||||
if (set == null || !PrcPriceSet.CATEGORY_LABEL.equalsIgnoreCase(String.valueOf(set.getSetCategory())))
|
||||
{
|
||||
throw new ServiceException("打单套餐不存在");
|
||||
}
|
||||
Long root = resolveRoot(productId);
|
||||
boolean inSet = false;
|
||||
List<PayProduct> products = selectLabelProducts(priceSetId);
|
||||
for (PayProduct p : products)
|
||||
{
|
||||
if (p != null && root.equals(p.getProductId()))
|
||||
{
|
||||
inSet = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!inSet)
|
||||
{
|
||||
throw new ServiceException("所选产品不属于本打单套餐");
|
||||
}
|
||||
String operator = SecurityUtils.getUsername();
|
||||
int n = 0;
|
||||
for (String channelId : channelIds)
|
||||
{
|
||||
if (isBlank(channelId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
ServiceChannel channel = serviceChannelMapper.selectServiceChannelById(channelId.trim());
|
||||
if (channel == null || isBlank(channel.getChannelCode()))
|
||||
{
|
||||
throw new ServiceException("渠道不存在或渠道编码为空:" + channelId);
|
||||
}
|
||||
ChannelProductMap row = new ChannelProductMap();
|
||||
row.setChannelId(channelId.trim());
|
||||
row.setChannelCode(channel.getChannelCode());
|
||||
row.setProductId(root);
|
||||
row.setPriceSetId(priceSetId);
|
||||
row.setPayType(PAY_TYPE_AR);
|
||||
row.setCreateBy(operator);
|
||||
row.setUpdateBy(operator);
|
||||
channelProductMapMapper.upsert(row);
|
||||
n++;
|
||||
}
|
||||
if (n <= 0)
|
||||
{
|
||||
throw new ServiceException("没有可绑定的渠道");
|
||||
}
|
||||
return "已绑定 " + n + " 个渠道";
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int unbindLabelChannel(Long priceSetId, String channelId)
|
||||
{
|
||||
if (priceSetId == null || isBlank(channelId))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return channelProductMapMapper.deleteByChannelTypeAndPriceSet(channelId.trim(), PAY_TYPE_AR, priceSetId);
|
||||
}
|
||||
|
||||
private Long resolveLabelPriceSetId(Long productId)
|
||||
{
|
||||
if (productId == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return payProductTermMapper.selectLabelPriceSetIdByProduct(productId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PayProduct> selectLabelProducts(Long priceSetId)
|
||||
{
|
||||
@@ -1199,9 +1339,9 @@ public class PayProductServiceImpl implements IPayProductService
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
Date now = today();
|
||||
Date now = nowDateTime();
|
||||
// 纠正历史:未来生效版启动时把旧版提前标成已失效
|
||||
payProductTermMapper.restorePrematureClosed(priceSetId, now, "system");
|
||||
payProductTermMapper.restorePrematureClosed(priceSetId, today(), "system");
|
||||
List<LabelPackageVersion> groups = payProductTermMapper.selectLabelVersionGroups(priceSetId);
|
||||
if (groups == null || groups.isEmpty())
|
||||
{
|
||||
@@ -1215,16 +1355,16 @@ public class PayProductServiceImpl implements IPayProductService
|
||||
int pending = ver.getPendingCount() == null ? 0 : ver.getPendingCount();
|
||||
Date start = ver.getEffectiveTime();
|
||||
Date end = ver.getExpireTime();
|
||||
// 按日期判定:未到生效日=待生效;已过失效日=已失效;中间段=生效
|
||||
// 按时间(到小时)判定:未到生效=待生效;已过失效=已失效;中间段=生效
|
||||
if (start == null)
|
||||
{
|
||||
ver.setStatus(pending > 0 ? TERM_PENDING : (active > 0 ? TERM_EFFECT : TERM_EXPIRED));
|
||||
}
|
||||
else if (toLocalDate(start).isAfter(toLocalDate(now)))
|
||||
else if (toLocalDateTime(start).isAfter(toLocalDateTime(now)))
|
||||
{
|
||||
ver.setStatus(TERM_PENDING);
|
||||
}
|
||||
else if (end != null && toLocalDate(end).isBefore(toLocalDate(now)))
|
||||
else if (end != null && !toLocalDateTime(end).isAfter(toLocalDateTime(now)))
|
||||
{
|
||||
ver.setStatus(TERM_EXPIRED);
|
||||
}
|
||||
@@ -1258,20 +1398,22 @@ public class PayProductServiceImpl implements IPayProductService
|
||||
throw new ServiceException("只能启动打单套餐版本");
|
||||
}
|
||||
String operator = SecurityUtils.getUsername();
|
||||
boolean future = toLocalDate(effectiveTime).isAfter(toLocalDate(today()));
|
||||
Date endDate = toDate(toLocalDate(effectiveTime).minusDays(1));
|
||||
Date effectiveHour = truncateToHour(effectiveTime);
|
||||
boolean future = toLocalDateTime(effectiveHour).isAfter(toLocalDateTime(nowDateTime()));
|
||||
// 旧版失效时间 = 新版生效时间(取价按 start <= biz < end)
|
||||
Date endDate = effectiveHour;
|
||||
if (future)
|
||||
{
|
||||
// 未到生效日:本版保持待生效;旧版只预约失效日,当前仍生效
|
||||
// 未到生效时间:本版保持待生效;旧版只预约失效时间,当前仍生效
|
||||
payProductTermMapper.scheduleEndExceptVersion(priceSetId, versionNo, endDate, today(), operator);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 当天或已过:本版立刻生效,旧版关掉
|
||||
// 已到/已过:本版立刻生效,旧版关掉
|
||||
payProductTermMapper.closeEffectExceptVersion(priceSetId, versionNo, endDate, operator);
|
||||
}
|
||||
String termStatus = future ? TERM_PENDING : TERM_EFFECT;
|
||||
int n = payProductTermMapper.updateLabelVersionPeriod(priceSetId, versionNo, effectiveTime, null, termStatus, operator);
|
||||
int n = payProductTermMapper.updateLabelVersionPeriod(priceSetId, versionNo, effectiveHour, null, termStatus, operator);
|
||||
if (n <= 0)
|
||||
{
|
||||
throw new ServiceException("该版本没有可启动的产品价卡");
|
||||
@@ -1614,12 +1756,52 @@ public class PayProductServiceImpl implements IPayProductService
|
||||
row.setChannelCode(channel.getChannelCode());
|
||||
row.setProductId(storedId);
|
||||
row.setPayType(type);
|
||||
if (PAY_TYPE_AR.equals(type))
|
||||
{
|
||||
row.setPriceSetId(resolveLabelPriceSetId(storedId));
|
||||
}
|
||||
String operator = SecurityUtils.getUsername();
|
||||
row.setCreateBy(operator);
|
||||
row.setUpdateBy(operator);
|
||||
channelProductMapMapper.upsert(row);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public String batchBindChannel(List<String> channelIds, Long apProductId, Long arProductId)
|
||||
{
|
||||
if (channelIds == null || channelIds.isEmpty())
|
||||
{
|
||||
throw new ServiceException("请先勾选渠道");
|
||||
}
|
||||
if (apProductId == null && arProductId == null)
|
||||
{
|
||||
throw new ServiceException("请至少选择应付或应收产品");
|
||||
}
|
||||
int n = 0;
|
||||
for (String channelId : channelIds)
|
||||
{
|
||||
if (isBlank(channelId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (apProductId != null)
|
||||
{
|
||||
bindChannel(channelId.trim(), apProductId, PAY_TYPE_AP);
|
||||
}
|
||||
if (arProductId != null)
|
||||
{
|
||||
bindChannel(channelId.trim(), arProductId, PAY_TYPE_AR);
|
||||
}
|
||||
n++;
|
||||
}
|
||||
if (n <= 0)
|
||||
{
|
||||
throw new ServiceException("没有可绑定的渠道");
|
||||
}
|
||||
return "已为 " + n + " 个渠道绑定产品";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unbindChannel(String channelId, String payType)
|
||||
{
|
||||
@@ -2161,6 +2343,12 @@ public class PayProductServiceImpl implements IPayProductService
|
||||
return toDate(java.time.LocalDate.now(java.time.ZoneId.of("GMT+8")));
|
||||
}
|
||||
|
||||
private static Date nowDateTime()
|
||||
{
|
||||
return java.util.Date.from(java.time.LocalDateTime.now(java.time.ZoneId.of("GMT+8"))
|
||||
.atZone(java.time.ZoneId.of("GMT+8")).toInstant());
|
||||
}
|
||||
|
||||
private static Date toDate(java.time.LocalDate day)
|
||||
{
|
||||
return java.sql.Date.valueOf(day);
|
||||
@@ -2168,11 +2356,30 @@ public class PayProductServiceImpl implements IPayProductService
|
||||
|
||||
private static java.time.LocalDate toLocalDate(Date date)
|
||||
{
|
||||
return toLocalDateTime(date).toLocalDate();
|
||||
}
|
||||
|
||||
private static java.time.LocalDateTime toLocalDateTime(Date date)
|
||||
{
|
||||
if (date == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (date instanceof java.sql.Date sql)
|
||||
{
|
||||
return sql.toLocalDate();
|
||||
return sql.toLocalDate().atStartOfDay();
|
||||
}
|
||||
return date.toInstant().atZone(java.time.ZoneId.of("GMT+8")).toLocalDate();
|
||||
if (date instanceof java.sql.Timestamp ts)
|
||||
{
|
||||
return ts.toLocalDateTime();
|
||||
}
|
||||
return date.toInstant().atZone(java.time.ZoneId.of("GMT+8")).toLocalDateTime();
|
||||
}
|
||||
|
||||
private static Date truncateToHour(Date date)
|
||||
{
|
||||
java.time.LocalDateTime dt = toLocalDateTime(date).withMinute(0).withSecond(0).withNano(0);
|
||||
return java.util.Date.from(dt.atZone(java.time.ZoneId.of("GMT+8")).toInstant());
|
||||
}
|
||||
|
||||
private static String formatDay(Date date)
|
||||
|
||||
@@ -6,22 +6,37 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
|
||||
<select id="selectByChannelAndType" resultType="com.bms.price.domain.ChannelProductMap">
|
||||
select id, channel_id as channelId, channel_code as channelCode, product_id as productId,
|
||||
pay_type as payType, create_by as createBy, create_time as createTime,
|
||||
price_set_id as priceSetId, pay_type as payType, create_by as createBy, create_time as createTime,
|
||||
update_by as updateBy, update_time as updateTime
|
||||
from srv_channel_product_map
|
||||
where channel_id = #{channelId} and pay_type = #{payType}
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<select id="selectByPriceSetId" resultType="com.bms.price.domain.ChannelProductMap">
|
||||
select m.id, m.channel_id as channelId, m.channel_code as channelCode, m.product_id as productId,
|
||||
m.price_set_id as priceSetId, m.pay_type as payType,
|
||||
c.channel_name as channelName, p.product_name as productName,
|
||||
m.create_by as createBy, m.create_time as createTime,
|
||||
m.update_by as updateBy, m.update_time as updateTime
|
||||
from srv_channel_product_map m
|
||||
left join srv_service_channel c on c.id = m.channel_id
|
||||
left join pay_product p on p.product_id = m.product_id
|
||||
where m.price_set_id = #{priceSetId}
|
||||
and m.pay_type = 'AR'
|
||||
order by m.update_time desc, m.id desc
|
||||
</select>
|
||||
|
||||
<insert id="upsert">
|
||||
insert into srv_channel_product_map (
|
||||
channel_id, channel_code, product_id, pay_type, create_by, create_time, update_by, update_time
|
||||
channel_id, channel_code, product_id, price_set_id, pay_type, create_by, create_time, update_by, update_time
|
||||
) values (
|
||||
#{channelId}, #{channelCode}, #{productId}, #{payType}, #{createBy}, sysdate(), #{updateBy}, sysdate()
|
||||
#{channelId}, #{channelCode}, #{productId}, #{priceSetId}, #{payType}, #{createBy}, sysdate(), #{updateBy}, sysdate()
|
||||
)
|
||||
on duplicate key update
|
||||
channel_code = values(channel_code),
|
||||
product_id = values(product_id),
|
||||
price_set_id = values(price_set_id),
|
||||
update_by = values(update_by),
|
||||
update_time = sysdate()
|
||||
</insert>
|
||||
@@ -31,6 +46,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
where channel_id = #{channelId} and pay_type = #{payType}
|
||||
</delete>
|
||||
|
||||
<delete id="deleteByChannelTypeAndPriceSet">
|
||||
delete from srv_channel_product_map
|
||||
where channel_id = #{channelId}
|
||||
and pay_type = #{payType}
|
||||
and price_set_id = #{priceSetId}
|
||||
</delete>
|
||||
|
||||
<delete id="deleteByProductId">
|
||||
delete from srv_channel_product_map where product_id = #{productId}
|
||||
</delete>
|
||||
|
||||
@@ -274,9 +274,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<!-- 渠道下拉:AP=应付公共模板;AR=pay_product 应收(含打单套餐产品) -->
|
||||
<select id="selectTemplateOptions" resultMap="PayProductResult">
|
||||
select p.product_id, p.product_name, p.product_type, p.dict_value, p.sheet_name, p.supplier_name, p.effective_date,
|
||||
p.currency, ifnull(cur.version_no, p.version_no) as version_no
|
||||
p.currency, ifnull(ifnull(t.version_no, cur.version_no), p.version_no) as version_no
|
||||
from pay_product p
|
||||
left join pay_product_term t on t.term_id = (
|
||||
select t2.term_id
|
||||
@@ -284,14 +285,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
inner join pay_product v on v.product_id = t2.product_id
|
||||
where (v.channel_id is null or v.channel_id = '')
|
||||
and (v.product_id = p.product_id or v.origin_id = p.product_id)
|
||||
<if test='payType == null or "AP".equals(payType)'>
|
||||
and t2.price_set_id is null
|
||||
order by case when t2.status = '1' then 0 when t2.status = '0' then 1 else 2 end, ifnull(v.version_no, 1) desc, t2.term_id desc
|
||||
</if>
|
||||
order by case when t2.status = '1' then 0 when t2.status = '0' then 1 else 2 end,
|
||||
ifnull(t2.version_no, ifnull(v.version_no, 1)) desc, t2.term_id desc
|
||||
limit 1
|
||||
)
|
||||
left join pay_product cur on cur.product_id = t.product_id
|
||||
where (p.channel_id is null or p.channel_id = '')
|
||||
and p.origin_id is null
|
||||
and ifnull(p.product_type, 'AP') = #{payType}
|
||||
<if test='payType == null or "AP".equals(payType)'>
|
||||
and not (
|
||||
exists (
|
||||
select 1 from pay_product_term t_label
|
||||
@@ -304,6 +309,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
and t_pay.price_set_id is null
|
||||
)
|
||||
)
|
||||
</if>
|
||||
order by p.product_name
|
||||
</select>
|
||||
|
||||
|
||||
@@ -41,6 +41,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<select id="selectLabelPriceSetIdByProduct" resultType="long">
|
||||
select price_set_id
|
||||
from pay_product_term
|
||||
where product_id = #{productId}
|
||||
and price_set_id is not null
|
||||
order by case when status = '1' then 0 when status = '0' then 1 else 2 end, version_no desc, term_id desc
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<!-- 打单套餐产品列表:待生效+生效 -->
|
||||
<select id="selectByPriceSetId" resultType="com.bms.price.domain.PayProductTerm">
|
||||
select t.term_id as termId, t.dict_value as dictValue, t.product_id as productId, t.price_set_id as priceSetId,
|
||||
@@ -76,8 +85,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
where t.price_set_id = #{priceSetId}
|
||||
and t.product_id = #{productId}
|
||||
and t.effective_start is not null
|
||||
and t.effective_start <= date(#{bizDate})
|
||||
and (t.effective_end is null or t.effective_end >= date(#{bizDate}))
|
||||
and t.effective_start <= #{bizDate}
|
||||
and (t.effective_end is null or t.effective_end > #{bizDate})
|
||||
order by ifnull(t.version_no, 1) desc, t.term_id desc
|
||||
limit 1
|
||||
</select>
|
||||
@@ -236,6 +245,16 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
and effective_end >= #{today}
|
||||
</update>
|
||||
|
||||
<update id="clearScheduledEnd">
|
||||
update pay_product_term
|
||||
set effective_end = null,
|
||||
updated_by = #{updateBy},
|
||||
update_time = sysdate()
|
||||
where price_set_id = #{priceSetId}
|
||||
and status = '1'
|
||||
and effective_end is not null
|
||||
</update>
|
||||
|
||||
<delete id="deleteByProductIds">
|
||||
delete from pay_product_term where product_id in
|
||||
<foreach collection="productIds" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
|
||||
@@ -93,6 +93,13 @@ export function activateLabelVersion(priceSetId, versionNo, data) {
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteLabelVersion(priceSetId, versionNo) {
|
||||
return request({
|
||||
url: '/price/label/' + priceSetId + '/versions/' + versionNo,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
export function listLabelProducts(priceSetId, versionNo) {
|
||||
return request({
|
||||
url: '/price/label/' + priceSetId + '/products',
|
||||
@@ -154,3 +161,25 @@ export function removeLabelProducts(priceSetId, productIds) {
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
export function listLabelChannels(priceSetId) {
|
||||
return request({
|
||||
url: '/price/label/' + priceSetId + '/channels',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function bindLabelChannels(priceSetId, data) {
|
||||
return request({
|
||||
url: '/price/label/' + priceSetId + '/channels',
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function unbindLabelChannel(priceSetId, channelId) {
|
||||
return request({
|
||||
url: '/price/label/' + priceSetId + '/channels/' + channelId,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -51,7 +51,9 @@ export function updatePay(data) {
|
||||
return request({
|
||||
url: '/price/pay',
|
||||
method: 'put',
|
||||
data
|
||||
data,
|
||||
headers: { repeatSubmit: false },
|
||||
timeout: 120000
|
||||
})
|
||||
}
|
||||
|
||||
@@ -72,6 +74,15 @@ export function bindChannelPay(channelId, productId, payType = 'AP') {
|
||||
})
|
||||
}
|
||||
|
||||
/** 批量绑定渠道应收/应付产品;至少传一个产品 */
|
||||
export function batchBindChannelPay(data) {
|
||||
return request({
|
||||
url: '/price/pay/channel/batchBind',
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function comparePay(leftId, rightId) {
|
||||
return request({
|
||||
url: '/price/pay/compare',
|
||||
|
||||
@@ -21,6 +21,16 @@
|
||||
<el-col :span="1.5">
|
||||
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['data:serviceChannel:export']">导出</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
icon="Link"
|
||||
:disabled="!ids.length"
|
||||
@click="openBatchBind(selection, ids)"
|
||||
v-hasPermi="['data:serviceChannel:edit']"
|
||||
>批量绑定产品</el-button>
|
||||
</el-col>
|
||||
</template>
|
||||
|
||||
<template #col-blOpen="{ row }">
|
||||
@@ -165,6 +175,29 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="batchBindOpen" title="批量绑定应收/应付产品" width="520px" append-to-body @closed="onBatchBindClosed">
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="已选渠道">
|
||||
<span>{{ batchBindIds.length }} 个</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="应付产品">
|
||||
<el-select v-model="batchApId" filterable clearable placeholder="可选,不选则不改应付绑定" style="width: 100%">
|
||||
<el-option v-for="item in payOptions" :key="'ap-' + item.productId" :label="payOptionLabel(item)" :value="item.productId" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="应收产品">
|
||||
<el-select v-model="batchArId" filterable clearable placeholder="可选,不选则不改应收绑定" style="width: 100%">
|
||||
<el-option v-for="item in arOptions" :key="'ar-' + item.productId" :label="payOptionLabel(item)" :value="item.productId" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<div class="pay-tip">至少选择一个产品;将覆盖已选渠道对应收付类型的绑定。</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="batchBindOpen = false">取消</el-button>
|
||||
<el-button type="primary" :loading="batchBindSaving" @click="submitBatchBind">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -174,7 +207,7 @@ import {
|
||||
listServiceChannel, getServiceChannel, addServiceChannel,
|
||||
updateServiceChannel, delServiceChannel, changeServiceChannelStatus
|
||||
} from '@/api/masterdata/serviceChannel'
|
||||
import { payOptions as listPayOptions, getChannelPay, bindChannelPay } from '@/api/price/pay'
|
||||
import { payOptions as listPayOptions, getChannelPay, bindChannelPay, batchBindChannelPay } from '@/api/price/pay'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const tableRef = ref()
|
||||
@@ -190,6 +223,11 @@ const payProduct = ref(null)
|
||||
const arProduct = ref(null)
|
||||
const syncingPay = ref(false)
|
||||
const syncingAr = ref(false)
|
||||
const batchBindOpen = ref(false)
|
||||
const batchBindSaving = ref(false)
|
||||
const batchBindIds = ref([])
|
||||
const batchApId = ref()
|
||||
const batchArId = ref()
|
||||
const payTip = computed(() => {
|
||||
if (viewing.value) {
|
||||
if (payProduct.value?.versionNo) return `已绑定第 ${payProduct.value.versionNo} 版。`
|
||||
@@ -293,8 +331,7 @@ function resetPay() {
|
||||
}
|
||||
|
||||
function payOptionLabel(item) {
|
||||
const version = item.versionNo > 1 ? `(当前第 ${item.versionNo} 版)` : ''
|
||||
return `${item.productName}${version}`
|
||||
return item?.productName || ''
|
||||
}
|
||||
|
||||
function loadPayOptions() {
|
||||
@@ -447,6 +484,46 @@ function handleExport() {
|
||||
...tableRef.value.getQueryParams()
|
||||
}, `service_channel_${new Date().getTime()}.xlsx`)
|
||||
}
|
||||
|
||||
function openBatchBind(selection, ids) {
|
||||
const list = Array.isArray(ids) ? ids.filter(Boolean) : []
|
||||
if (!list.length) {
|
||||
proxy.$modal.msgWarning('请先勾选渠道')
|
||||
return
|
||||
}
|
||||
batchBindIds.value = list.map(String)
|
||||
batchApId.value = undefined
|
||||
batchArId.value = undefined
|
||||
loadPayOptions()
|
||||
batchBindOpen.value = true
|
||||
}
|
||||
|
||||
function onBatchBindClosed() {
|
||||
batchBindIds.value = []
|
||||
batchApId.value = undefined
|
||||
batchArId.value = undefined
|
||||
batchBindSaving.value = false
|
||||
}
|
||||
|
||||
function submitBatchBind() {
|
||||
if (!batchBindIds.value.length) return
|
||||
if (!batchApId.value && !batchArId.value) {
|
||||
proxy.$modal.msgWarning('请至少选择应付或应收产品')
|
||||
return
|
||||
}
|
||||
batchBindSaving.value = true
|
||||
batchBindChannelPay({
|
||||
channelIds: batchBindIds.value,
|
||||
apProductId: batchApId.value || null,
|
||||
arProductId: batchArId.value || null
|
||||
}).then(res => {
|
||||
proxy.$modal.msgSuccess(res.msg || res.data || '批量绑定成功')
|
||||
batchBindOpen.value = false
|
||||
tableRef.value?.refresh()
|
||||
}).finally(() => {
|
||||
batchBindSaving.value = false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -23,24 +23,21 @@
|
||||
<el-tabs v-model="pane.tab">
|
||||
<el-tab-pane :label="`重量段(${(pane.item.weightBands || []).length})`" name="band">
|
||||
<el-input v-model="pane.bandKeyword" clearable placeholder="筛选国家、分区、重量段、费用项" class="pay-filter" @input="pane.bandPageNo = 1" />
|
||||
<div v-if="editable" class="pay-edit-bar">
|
||||
<el-button size="small" type="primary" plain @click="addBand(pane)">添加重量段</el-button>
|
||||
</div>
|
||||
<el-table :data="pageBands(pane)" border size="small" max-height="360" empty-text="没有重量段或起重配置">
|
||||
<el-table :data="pageBands(pane)" border size="small" empty-text="没有重量段或起重配置">
|
||||
<el-table-column prop="sectionName" label="区块" min-width="110" show-overflow-tooltip>
|
||||
<template #default="{ row }"><cell-text v-model="row.sectionName" :edit="editable" /></template>
|
||||
<template #default="{ row }"><cell-text v-model="row.sectionName" :edit="isRowEditing(row)" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="countryName" label="国家" min-width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }"><cell-text v-model="row.countryName" :edit="editable" /></template>
|
||||
<template #default="{ row }"><cell-text v-model="row.countryName" :edit="isRowEditing(row)" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="countryCode" label="代码" width="80" show-overflow-tooltip>
|
||||
<template #default="{ row }"><cell-text v-model="row.countryCode" :edit="editable" /></template>
|
||||
<template #default="{ row }"><cell-text v-model="row.countryCode" :edit="isRowEditing(row)" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="zoneName" label="分区" min-width="140" show-overflow-tooltip>
|
||||
<template #default="{ row }"><cell-text v-model="row.zoneName" :edit="editable" /></template>
|
||||
<template #default="{ row }"><cell-text v-model="row.zoneName" :edit="isRowEditing(row)" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="weightLabel" label="重量段" min-width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }"><cell-text v-model="row.weightLabel" :edit="editable" /></template>
|
||||
<template #default="{ row }"><cell-text v-model="row.weightLabel" :edit="isRowEditing(row)" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="下限KG" width="90" align="right">
|
||||
<template #default="{ row }">{{ money(row.weightMin) }}</template>
|
||||
@@ -49,21 +46,22 @@
|
||||
<template #default="{ row }">{{ money(row.weightMax) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="chargeName" label="费用项" min-width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }"><cell-text v-model="row.chargeName" :edit="editable" /></template>
|
||||
<template #default="{ row }"><cell-text v-model="row.chargeName" :edit="isRowEditing(row)" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="单价" width="110" align="right">
|
||||
<template #default="{ row }">
|
||||
<el-input v-if="editable" v-model="row.price" size="small" />
|
||||
<el-input v-if="isRowEditing(row)" v-model="row.price" size="small" />
|
||||
<span v-else>{{ money(row.price) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="priceUnit" label="单位" width="110" show-overflow-tooltip>
|
||||
<template #default="{ row }"><cell-text v-model="row.priceUnit" :edit="editable" /></template>
|
||||
<template #default="{ row }"><cell-text v-model="row.priceUnit" :edit="isRowEditing(row)" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="editable" label="操作" width="120" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" :loading="isRowSaving(row)" @click="saveBandRow(pane, row)">保存</el-button>
|
||||
<el-button link type="danger" :disabled="!!savingRowKey" @click="removeRow(pane.item.weightBands, row)">删除</el-button>
|
||||
<el-button v-if="!isRowEditing(row)" link type="primary" :disabled="rowBusy" @click="startEdit(row)">修改</el-button>
|
||||
<el-button v-else link type="primary" :disabled="rowBusy" @click="saveBandRow(pane, row)">保存</el-button>
|
||||
<el-button link type="danger" :disabled="rowBusy" @click="removeRow(pane.item.weightBands, row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -82,11 +80,11 @@
|
||||
<el-input v-model="pane.feeKeyword" clearable placeholder="筛选附加费用、地区、价格、货币或费用解释" class="pay-filter" @input="pane.feePageNo = 1" />
|
||||
<el-button v-if="editable" size="small" type="primary" plain @click="addFee(pane)">添加附加费</el-button>
|
||||
</div>
|
||||
<el-table :data="pageFees(pane)" border size="small" max-height="360" empty-text="没有附加费">
|
||||
<el-table :data="pageFees(pane)" border size="small" empty-text="没有附加费">
|
||||
<el-table-column prop="feeName" label="附加费用" min-width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
v-if="editable"
|
||||
v-if="isRowEditing(row)"
|
||||
v-model="row.feeName"
|
||||
filterable
|
||||
clearable
|
||||
@@ -106,18 +104,18 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="zoneName" label="地区" min-width="160" show-overflow-tooltip>
|
||||
<template #default="{ row }"><cell-text v-model="row.zoneName" :edit="editable" /></template>
|
||||
<template #default="{ row }"><cell-text v-model="row.zoneName" :edit="isRowEditing(row)" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="价格" width="110" align="right">
|
||||
<template #default="{ row }">
|
||||
<el-input v-if="editable" v-model="row.amount" size="small" />
|
||||
<el-input v-if="isRowEditing(row)" v-model="row.amount" size="small" />
|
||||
<span v-else>{{ money(row.amount) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="币种" width="110" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
v-if="editable"
|
||||
v-if="isRowEditing(row)"
|
||||
:model-value="feeCurrencyOf(row)"
|
||||
filterable
|
||||
allow-create
|
||||
@@ -141,7 +139,7 @@
|
||||
<el-table-column label="单位" width="110" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
v-if="editable"
|
||||
v-if="isRowEditing(row)"
|
||||
:model-value="feeUnitOf(row)"
|
||||
filterable
|
||||
allow-create
|
||||
@@ -164,16 +162,16 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="收费标准" min-width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<cell-text v-if="editable && (!row.chargeText || feeNote(row))" v-model="row.chargeText" :edit="true" />
|
||||
<cell-text v-if="isRowEditing(row) && (!row.chargeText || feeNote(row))" v-model="row.chargeText" :edit="true" />
|
||||
<span v-else>{{ feeNote(row) || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="feeDesc" label="费用解释" min-width="240" show-overflow-tooltip>
|
||||
<template #default="{ row }"><cell-text v-model="row.feeDesc" :edit="editable" /></template>
|
||||
<template #default="{ row }"><cell-text v-model="row.feeDesc" :edit="isRowEditing(row)" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="calcRule" label="计费条件" width="140">
|
||||
<template #default="{ row }">
|
||||
<el-select v-if="editable" v-model="row.calcRule" size="small" placeholder="人工确认">
|
||||
<el-select v-if="isRowEditing(row)" v-model="row.calcRule" size="small" placeholder="人工确认">
|
||||
<el-option v-for="item in calcRules" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
<span v-else>{{ calcRuleLabel(row.calcRule) }}</span>
|
||||
@@ -181,7 +179,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column prop="calcParam" label="条件参数" min-width="220" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span v-if="!editable">{{ feeCondition(row) }}</span>
|
||||
<span v-if="!isRowEditing(row)">{{ feeCondition(row) }}</span>
|
||||
<template v-else>
|
||||
<div v-if="conditionHint(row)" class="pay-cond-hint">{{ conditionHint(row) }}</div>
|
||||
<cell-text
|
||||
@@ -194,8 +192,9 @@
|
||||
</el-table-column>
|
||||
<el-table-column v-if="editable" label="操作" width="120" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" :loading="isRowSaving(row)" @click="saveFeeRow(pane, row)">保存</el-button>
|
||||
<el-button link type="danger" :disabled="!!savingRowKey" @click="removeRow(pane.item.surcharges, row)">删除</el-button>
|
||||
<el-button v-if="!isRowEditing(row)" link type="primary" :disabled="rowBusy" @click="startEdit(row)">修改</el-button>
|
||||
<el-button v-else link type="primary" :disabled="rowBusy" @click="saveFeeRow(pane, row)">保存</el-button>
|
||||
<el-button link type="danger" :disabled="rowBusy" @click="removeRow(pane.item.surcharges, row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -212,7 +211,7 @@
|
||||
<el-tab-pane v-if="postcodeLines(pane).length" :label="`邮编分区(${postcodeLines(pane).length})`" name="postcode">
|
||||
<p class="pay-hint">{{ postcodeHint(pane) }}</p>
|
||||
<el-input v-model="pane.postKeyword" clearable placeholder="筛选邮编、州、城市或分区" class="pay-filter" @input="pane.postPageNo = 1" />
|
||||
<el-table :data="pagePostcodes(pane)" border size="small" max-height="360" empty-text="没有邮编">
|
||||
<el-table :data="pagePostcodes(pane)" border size="small" empty-text="没有邮编">
|
||||
<el-table-column v-for="(title, index) in postcodeTitles(pane)" :key="title" :label="title" min-width="110" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.parts[index] || '-' }}</template>
|
||||
</el-table-column>
|
||||
@@ -233,7 +232,7 @@
|
||||
|
||||
<script setup>
|
||||
// 重量段和附加费。传入 products 时一次展示全部产品。
|
||||
import { h, onMounted } from 'vue'
|
||||
import { h, onMounted, watch } from 'vue'
|
||||
import { ElInput } from 'element-plus'
|
||||
import { listSurchargeOptions } from '@/api/price/surcharge'
|
||||
|
||||
@@ -258,6 +257,12 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['save-row'])
|
||||
|
||||
const editingKeys = ref(new Set())
|
||||
|
||||
watch(() => props.product, () => {
|
||||
editingKeys.value = new Set()
|
||||
})
|
||||
|
||||
function rowKeyOf(row) {
|
||||
if (!row) return ''
|
||||
if (row.surchargeId != null) return 'fee-' + row.surchargeId
|
||||
@@ -267,16 +272,58 @@ function rowKeyOf(row) {
|
||||
return row.__rowKey
|
||||
}
|
||||
|
||||
function isRowSaving(row) {
|
||||
return props.savingRowKey && String(props.savingRowKey) === String(rowKeyOf(row))
|
||||
function isRowEditing(row) {
|
||||
return editingKeys.value.has(rowKeyOf(row))
|
||||
}
|
||||
|
||||
function startEdit(row) {
|
||||
const next = new Set(editingKeys.value)
|
||||
next.add(rowKeyOf(row))
|
||||
editingKeys.value = next
|
||||
}
|
||||
|
||||
function stopEdit(row) {
|
||||
const next = new Set(editingKeys.value)
|
||||
next.delete(rowKeyOf(row))
|
||||
editingKeys.value = next
|
||||
}
|
||||
|
||||
const localSavingKey = ref('')
|
||||
const rowBusy = computed(() => !!(props.savingRowKey || localSavingKey.value))
|
||||
|
||||
function finishSave(row, ok) {
|
||||
localSavingKey.value = ''
|
||||
if (ok !== false) stopEdit(row)
|
||||
}
|
||||
|
||||
function runSave(kind, pane, row) {
|
||||
if (localSavingKey.value || props.savingRowKey) return
|
||||
const rowKey = rowKeyOf(row)
|
||||
localSavingKey.value = rowKey
|
||||
let settled = false
|
||||
const done = (ok) => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
finishSave(row, ok)
|
||||
}
|
||||
try {
|
||||
emit('save-row', { kind, product: pane.item, row, rowKey, done })
|
||||
} catch (e) {
|
||||
done(false)
|
||||
return
|
||||
}
|
||||
// Parent must call done(); hard stop UI lock if it does not.
|
||||
window.setTimeout(() => {
|
||||
if (!settled) done(false)
|
||||
}, 8000)
|
||||
}
|
||||
|
||||
function saveBandRow(pane, row) {
|
||||
emit('save-row', { kind: 'band', product: pane.item, row, rowKey: rowKeyOf(row) })
|
||||
runSave('band', pane, row)
|
||||
}
|
||||
|
||||
function saveFeeRow(pane, row) {
|
||||
emit('save-row', { kind: 'fee', product: pane.item, row, rowKey: rowKeyOf(row) })
|
||||
runSave('fee', pane, row)
|
||||
}
|
||||
|
||||
const surchargeOptions = ref([])
|
||||
@@ -470,16 +517,10 @@ function filteredFees(pane) {
|
||||
return filterRows(pane.item.surcharges || [], pane.feeKeyword, ['feeName', 'zoneName', 'chargeText', 'amount', 'currency', 'priceUnit', 'feeDesc', 'calcRule', 'calcParam', 'postcodeParam'])
|
||||
}
|
||||
|
||||
function addBand(pane) {
|
||||
if (!pane.item.weightBands) pane.item.weightBands = []
|
||||
pane.item.weightBands.push({ sectionName: '', countryName: '', countryCode: '', zoneName: '', weightLabel: '', chargeName: '', price: '', priceUnit: '' })
|
||||
pane.bandPageNo = Math.ceil(pane.item.weightBands.length / pageSize) || 1
|
||||
}
|
||||
|
||||
function addFee(pane) {
|
||||
if (!pane.item.surcharges) pane.item.surcharges = []
|
||||
const currency = String(pane.item.currency || 'EUR').trim().toUpperCase() || 'EUR'
|
||||
pane.item.surcharges.push({
|
||||
const row = {
|
||||
feeName: '',
|
||||
scId: undefined,
|
||||
zoneName: '',
|
||||
@@ -490,7 +531,9 @@ function addFee(pane) {
|
||||
feeDesc: '',
|
||||
calcRule: 'MANUAL',
|
||||
calcParam: ''
|
||||
})
|
||||
}
|
||||
pane.item.surcharges.push(row)
|
||||
startEdit(row)
|
||||
pane.feePageNo = Math.ceil(pane.item.surcharges.length / pageSize) || 1
|
||||
}
|
||||
|
||||
|
||||
@@ -167,25 +167,17 @@
|
||||
size="78%"
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
@closed="editing = false"
|
||||
>
|
||||
<pay-config-detail
|
||||
v-loading="detailLoading || !!savingRowKey"
|
||||
v-loading="detailLoading"
|
||||
:product="detail"
|
||||
:editable="editing"
|
||||
:editable="true"
|
||||
:saving-row-key="savingRowKey"
|
||||
@save-row="saveProductRow"
|
||||
/>
|
||||
<template #footer>
|
||||
<el-button @click="detailVisible = false">关闭</el-button>
|
||||
<el-button v-if="detail" @click="downloadSource(detail)" v-hasPermi="['price:pay:query']">下载源文件</el-button>
|
||||
<el-button
|
||||
v-if="!editing"
|
||||
type="primary"
|
||||
@click="editing = true"
|
||||
v-hasPermi="['price:pay:import']"
|
||||
>编辑</el-button>
|
||||
<el-button v-else @click="editing = false">完成</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</div>
|
||||
@@ -201,7 +193,6 @@ const tableRef = ref()
|
||||
const importRef = ref()
|
||||
const detailVisible = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const editing = ref(false)
|
||||
const savingRowKey = ref('')
|
||||
const detail = ref(null)
|
||||
const historyVisible = ref(false)
|
||||
@@ -325,7 +316,6 @@ function handleDelete(ids) {
|
||||
|
||||
function openDetail(row) {
|
||||
detailVisible.value = true
|
||||
editing.value = false
|
||||
savingRowKey.value = ''
|
||||
detailLoading.value = true
|
||||
detail.value = null
|
||||
@@ -336,25 +326,34 @@ function openDetail(row) {
|
||||
})
|
||||
}
|
||||
|
||||
function saveProductRow(payload) {
|
||||
if (!detail.value || savingRowKey.value) return
|
||||
async function saveProductRow(payload) {
|
||||
if (!detail.value || savingRowKey.value) {
|
||||
payload?.done?.(false)
|
||||
return
|
||||
}
|
||||
cleanMoney(detail.value)
|
||||
;(detail.value.surcharges || []).forEach(row => {
|
||||
if (row.amount === '') row.amount = null
|
||||
})
|
||||
const productId = detail.value.productId
|
||||
savingRowKey.value = payload?.rowKey || 'row'
|
||||
updatePay(detail.value).then(() => {
|
||||
let ok = false
|
||||
try {
|
||||
await updatePay(detail.value)
|
||||
ok = true
|
||||
proxy.$modal.msgSuccess('该行已保存')
|
||||
if (!productId) return
|
||||
return getPay(productId).then(res => {
|
||||
if (res.data) detail.value = res.data
|
||||
})
|
||||
}).then(() => {
|
||||
tableRef.value?.refresh()
|
||||
}).finally(() => {
|
||||
} catch (e) {
|
||||
ok = false
|
||||
} finally {
|
||||
savingRowKey.value = ''
|
||||
})
|
||||
payload?.done?.(ok)
|
||||
}
|
||||
if (!ok || !productId) return
|
||||
try {
|
||||
const res = await getPay(productId)
|
||||
if (res.data) detail.value = res.data
|
||||
tableRef.value?.refresh()
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function cleanMoney(product) {
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
<template>
|
||||
<set-section-card
|
||||
:title="title"
|
||||
tone="blue"
|
||||
:count="rows.length"
|
||||
:sub="sub"
|
||||
>
|
||||
<template #action>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon="Plus"
|
||||
:disabled="!priceSetId"
|
||||
@click="openBind"
|
||||
v-hasPermi="['price:label:edit']"
|
||||
>添加渠道</el-button>
|
||||
</template>
|
||||
|
||||
<el-table v-loading="loading" :data="rows" border empty-text="暂未绑定渠道">
|
||||
<el-table-column label="渠道编码" prop="channelCode" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="渠道名称" prop="channelName" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="应收产品" prop="productName" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="100" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="danger" @click="unbind(row)" v-hasPermi="['price:label:edit']">解绑</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="bindOpen" title="绑定渠道" width="560px" append-to-body @closed="onClosed">
|
||||
<el-form label-width="110px">
|
||||
<el-form-item label="套餐产品" required>
|
||||
<el-select v-model="form.productId" filterable clearable placeholder="选择本套餐内应收产品" style="width: 100%">
|
||||
<el-option
|
||||
v-for="item in productOptions"
|
||||
:key="item.productId"
|
||||
:label="item.productName"
|
||||
:value="item.productId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="渠道" required>
|
||||
<el-select
|
||||
v-model="form.channelIds"
|
||||
multiple
|
||||
filterable
|
||||
clearable
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
placeholder="可多选渠道"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in channelOptions"
|
||||
:key="item.id"
|
||||
:label="channelLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<div class="hint">绑定后会写入渠道应收产品,并关联本打单套餐。</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="bindOpen = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="submit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</set-section-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import SetSectionCard from '../../set/components/detail/SetSectionCard.vue'
|
||||
import { listLabelChannels, bindLabelChannels, unbindLabelChannel, listLabelProducts } from '@/api/price/label'
|
||||
import { listChannelOptions } from '@/api/masterdata/serviceChannel'
|
||||
|
||||
const props = defineProps({
|
||||
priceSetId: { type: [Number, String], default: null }
|
||||
})
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const title = '绑定渠道'
|
||||
const sub = '渠道计费时按应收产品 + 本套餐取价'
|
||||
const loading = ref(false)
|
||||
const rows = ref([])
|
||||
const bindOpen = ref(false)
|
||||
const saving = ref(false)
|
||||
const productOptions = ref([])
|
||||
const channelOptions = ref([])
|
||||
const form = reactive({
|
||||
productId: undefined,
|
||||
channelIds: []
|
||||
})
|
||||
|
||||
function channelLabel(item) {
|
||||
const code = item.channelCode || item.id || ''
|
||||
const name = item.channelName || ''
|
||||
return name ? (code + ' / ' + name) : String(code)
|
||||
}
|
||||
|
||||
async function loadRows() {
|
||||
if (!props.priceSetId) {
|
||||
rows.value = []
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await listLabelChannels(props.priceSetId)
|
||||
rows.value = res.data || []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function openBind() {
|
||||
if (!props.priceSetId) return
|
||||
form.productId = undefined
|
||||
form.channelIds = []
|
||||
const [prodRes, chRes] = await Promise.all([
|
||||
listLabelProducts(props.priceSetId),
|
||||
listChannelOptions({ blOpen: '1' })
|
||||
])
|
||||
productOptions.value = prodRes.data || []
|
||||
channelOptions.value = chRes.data || []
|
||||
if (!productOptions.value.length) {
|
||||
proxy.$modal.msgWarning('请先导入套餐产品价卡')
|
||||
return
|
||||
}
|
||||
bindOpen.value = true
|
||||
}
|
||||
|
||||
function onClosed() {
|
||||
form.productId = undefined
|
||||
form.channelIds = []
|
||||
saving.value = false
|
||||
}
|
||||
|
||||
function submit() {
|
||||
if (!form.productId) {
|
||||
proxy.$modal.msgWarning('请选择套餐内应收产品')
|
||||
return
|
||||
}
|
||||
if (!form.channelIds.length) {
|
||||
proxy.$modal.msgWarning('请选择渠道')
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
bindLabelChannels(props.priceSetId, {
|
||||
productId: form.productId,
|
||||
channelIds: form.channelIds
|
||||
}).then(res => {
|
||||
proxy.$modal.msgSuccess(res.msg || res.data || '绑定成功')
|
||||
bindOpen.value = false
|
||||
loadRows()
|
||||
}).finally(() => {
|
||||
saving.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function unbind(row) {
|
||||
if (!row?.channelId) return
|
||||
const name = row.channelName || row.channelCode || row.channelId
|
||||
proxy.$modal.confirm('确认解绑渠道「' + name + '」?').then(() => {
|
||||
return unbindLabelChannel(props.priceSetId, row.channelId)
|
||||
}).then(() => {
|
||||
proxy.$modal.msgSuccess('已解绑')
|
||||
loadRows()
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
watch(() => props.priceSetId, () => loadRows(), { immediate: true })
|
||||
|
||||
defineExpose({ reload: loadRows })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.hint {
|
||||
margin: -4px 0 0 110px;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
@@ -95,11 +95,19 @@ const filteredRows = computed(() => {
|
||||
})
|
||||
})
|
||||
|
||||
function formatHour(val) {
|
||||
if (!val) return ''
|
||||
const text = String(val).replace('T', ' ')
|
||||
if (text.length >= 16) return text.slice(0, 13) + ':00'
|
||||
if (text.length >= 10) return text.slice(0, 10) + ' 00:00'
|
||||
return text
|
||||
}
|
||||
|
||||
function termText(row) {
|
||||
const start = row.termStart || row.effectiveDate || ''
|
||||
const end = row.termEnd || '不限'
|
||||
const start = formatHour(row.termStart || row.effectiveDate || '')
|
||||
const end = row.termEnd ? formatHour(row.termEnd) : '不限'
|
||||
if (!start) return '未设置'
|
||||
return `${String(start).slice(0, 10)} ~ ${String(end).slice(0, 10)}`
|
||||
return start + ' ~ ' + end
|
||||
}
|
||||
|
||||
function termStatusText(status) {
|
||||
|
||||
@@ -28,15 +28,15 @@
|
||||
<el-table-column label="版本号" width="100" align="center">
|
||||
<template #default="{ row }">{{ row.versionLabel || ('V' + (row.versionNo || 1)) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="生效时间" min-width="160" align="center">
|
||||
<el-table-column label="生效时间" min-width="170" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="$emit('open-details', row)">
|
||||
{{ formatDay(row.effectiveTime) || '未设置' }}
|
||||
{{ formatHour(row.effectiveTime) || '未设置' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="失效时间" min-width="160" align="center">
|
||||
<template #default="{ row }">{{ formatDay(row.expireTime) || '-' }}</template>
|
||||
<el-table-column label="失效时间" min-width="170" align="center">
|
||||
<template #default="{ row }">{{ formatHour(row.expireTime) || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
@@ -49,7 +49,7 @@
|
||||
<el-table-column label="创建时间" min-width="170" align="center">
|
||||
<template #default="{ row }">{{ row.createTime || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="280" align="center" fixed="right">
|
||||
<el-table-column label="操作" width="340" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="$emit('open-details', row)">查看详情</el-button>
|
||||
<el-button
|
||||
@@ -66,6 +66,13 @@
|
||||
@click="$emit('reschedule', row)"
|
||||
v-hasPermi="['price:label:edit']"
|
||||
>修改生效时间</el-button>
|
||||
<el-button
|
||||
v-if="canDelete(row)"
|
||||
link
|
||||
type="danger"
|
||||
@click="$emit('remove', row)"
|
||||
v-hasPermi="['price:label:remove']"
|
||||
>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -76,7 +83,7 @@
|
||||
const props = defineProps({
|
||||
versions: { type: Array, default: () => [] }
|
||||
})
|
||||
defineEmits(['edit', 'reschedule', 'open-details', 'create-version'])
|
||||
defineEmits(['edit', 'reschedule', 'remove', 'open-details', 'create-version'])
|
||||
|
||||
const statusTab = ref('all')
|
||||
|
||||
@@ -103,9 +110,12 @@ const filteredVersions = computed(() => {
|
||||
return list.filter(v => String(v.status) === statusTab.value)
|
||||
})
|
||||
|
||||
function formatDay(val) {
|
||||
function formatHour(val) {
|
||||
if (!val) return ''
|
||||
return String(val).slice(0, 10)
|
||||
const text = String(val).replace('T', ' ')
|
||||
if (text.length >= 16) return text.slice(0, 13) + ':00'
|
||||
if (text.length >= 10) return text.slice(0, 10) + ' 00:00'
|
||||
return text
|
||||
}
|
||||
|
||||
function statusText(status) {
|
||||
@@ -118,13 +128,19 @@ function statusTag(status) {
|
||||
return map[String(status)] || 'info'
|
||||
}
|
||||
|
||||
/** 待生效:可编辑价卡 */
|
||||
/** 待生效、已生效:可编辑价卡 */
|
||||
function canEdit(row) {
|
||||
const s = row ? String(row.status) : ''
|
||||
return s === '0' || s === '1'
|
||||
}
|
||||
|
||||
/** 仅待生效可改生效时间 */
|
||||
function canReschedule(row) {
|
||||
return row && String(row.status) === '0'
|
||||
}
|
||||
|
||||
/** 仅待生效可改生效时间;已生效不可改 */
|
||||
function canReschedule(row) {
|
||||
/** 仅待生效可删除整版 */
|
||||
function canDelete(row) {
|
||||
return row && String(row.status) === '0'
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -76,12 +76,12 @@
|
||||
/>
|
||||
|
||||
<el-drawer v-model="detailVisible" :title="detailTitle" size="78%" destroy-on-close append-to-body @closed="onDetailClosed">
|
||||
<pay-config-detail :product="detailProduct" :editable="detailEditing" />
|
||||
<template #footer>
|
||||
<el-button v-if="!detailEditing" type="primary" icon="Edit" @click="startEdit">修改</el-button>
|
||||
<el-button v-else type="primary" @click="applyDetail">保存</el-button>
|
||||
<el-button @click="detailVisible = false">关闭</el-button>
|
||||
</template>
|
||||
<pay-config-detail
|
||||
:product="detailProduct"
|
||||
:editable="true"
|
||||
:saving-row-key="savingRowKey"
|
||||
@save-row="saveDetailRow"
|
||||
/>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
@@ -111,9 +111,9 @@ const importMeta = reactive({
|
||||
overwrite: false
|
||||
})
|
||||
const detailVisible = ref(false)
|
||||
const detailEditing = ref(false)
|
||||
const detailProduct = ref(null)
|
||||
const detailRowKey = ref('')
|
||||
const savingRowKey = ref('')
|
||||
const detailTitle = computed(() => detailProduct.value?.productName || detailProduct.value?.sheetName || '产品明细')
|
||||
|
||||
const priceSetId = computed(() => {
|
||||
@@ -135,13 +135,13 @@ const cardTitle = computed(() => {
|
||||
const pageBanner = computed(() => {
|
||||
const name = form.basic.priceSetName || ''
|
||||
const code = form.basic.priceSetCode || ''
|
||||
if (editVersionMode.value) return `编辑待生效版本 V${versionNo.value}:${name}(${code})`
|
||||
if (editVersionMode.value) return `编辑版本 V${versionNo.value}:${name}(${code})`
|
||||
return `新建版本:${name}(${code})`
|
||||
})
|
||||
const headerTip = computed(() => {
|
||||
if (editVersionMode.value) return '可重新导入或修改明细;完成后覆盖本待生效版本,不新增版本号'
|
||||
if (editVersionMode.value) return '可重新导入或修改明细;完成后覆盖本版本,不新增版本号'
|
||||
if (newVersionMode.value) return '导入后生成待生效新版本;生效时间请回详情页设置'
|
||||
return '打单价卡与应付独立;解析后回填到此表,查看后点修改可改明细'
|
||||
return '打单价卡与应付独立;解析后回填到此表,查看明细后可在行内保存修改'
|
||||
})
|
||||
|
||||
function goBack() {
|
||||
@@ -219,28 +219,21 @@ function onImportConfirm(payload) {
|
||||
|
||||
function openDetail(row) {
|
||||
if (!row) return
|
||||
detailEditing.value = false
|
||||
savingRowKey.value = ''
|
||||
detailRowKey.value = row.rowKey || `${row.sheetName || ''}-${row.productName || ''}`
|
||||
detailProduct.value = JSON.parse(JSON.stringify(row))
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
function startEdit() {
|
||||
detailEditing.value = true
|
||||
}
|
||||
|
||||
function onDetailClosed() {
|
||||
detailEditing.value = false
|
||||
detailProduct.value = null
|
||||
detailRowKey.value = ''
|
||||
savingRowKey.value = ''
|
||||
}
|
||||
|
||||
function applyDetail() {
|
||||
function applyEditedProduct() {
|
||||
const edited = detailProduct.value
|
||||
if (!edited) {
|
||||
detailVisible.value = false
|
||||
return
|
||||
}
|
||||
if (!edited) return false
|
||||
const key = detailRowKey.value
|
||||
const idx = productRows.value.findIndex(item => {
|
||||
const itemKey = item.rowKey || `${item.sheetName || ''}-${item.productName || ''}`
|
||||
@@ -248,7 +241,7 @@ function applyDetail() {
|
||||
})
|
||||
if (idx < 0) {
|
||||
proxy.$modal.msgError('未找到对应产品行')
|
||||
return
|
||||
return false
|
||||
}
|
||||
const bands = edited.weightBands || []
|
||||
const fees = edited.surcharges || []
|
||||
@@ -261,9 +254,25 @@ function applyDetail() {
|
||||
extraCount: extras.length,
|
||||
parseStatus: bands.length + fees.length + extras.length === 0 ? 'EMPTY' : (edited.parseStatus || 'OK')
|
||||
})
|
||||
detailEditing.value = false
|
||||
detailVisible.value = false
|
||||
proxy.$modal.msgSuccess('已更新该产品明细')
|
||||
return true
|
||||
}
|
||||
|
||||
function saveDetailRow(payload) {
|
||||
if (savingRowKey.value) {
|
||||
payload?.done?.(false)
|
||||
return
|
||||
}
|
||||
savingRowKey.value = payload?.rowKey || 'row'
|
||||
let ok = false
|
||||
try {
|
||||
ok = !!applyEditedProduct()
|
||||
if (ok) proxy.$modal.msgSuccess('该行已保存')
|
||||
} catch (e) {
|
||||
ok = false
|
||||
} finally {
|
||||
savingRowKey.value = ''
|
||||
payload?.done?.(ok)
|
||||
}
|
||||
}
|
||||
|
||||
function statusType(status) {
|
||||
@@ -295,7 +304,7 @@ async function finish() {
|
||||
return
|
||||
}
|
||||
const tip = editVersionMode.value
|
||||
? `将覆盖待生效版本 V${versionNo.value} 的 ${productRows.value.length} 个产品,确认继续?`
|
||||
? `将覆盖版本 V${versionNo.value} 的 ${productRows.value.length} 个产品,确认继续?`
|
||||
: newVersionMode.value
|
||||
? `将为套餐「${form.basic.priceSetName || ''}」新建待生效版本并保存 ${productRows.value.length} 个产品,确认继续?`
|
||||
: `将创建打单套餐并保存 ${productRows.value.length} 个应收产品,确认继续?`
|
||||
|
||||
@@ -14,10 +14,13 @@
|
||||
|
||||
<set-customer-table :set="setInfo" @refresh="loadHeader" />
|
||||
|
||||
<label-channel-table ref="channelTableRef" :price-set-id="priceSetId" />
|
||||
|
||||
<label-version-table
|
||||
:versions="versions"
|
||||
@edit="openEditVersion"
|
||||
@reschedule="openReschedule"
|
||||
@remove="removeVersion"
|
||||
@open-details="openVersionDetails"
|
||||
@create-version="openCreateVersion"
|
||||
/>
|
||||
@@ -45,13 +48,14 @@
|
||||
<el-form-item label="生效时间" prop="effectiveTime">
|
||||
<el-date-picker
|
||||
v-model="activateForm.effectiveTime"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DD HH:00:00"
|
||||
format="YYYY-MM-DD HH:00"
|
||||
placeholder="请选择生效时间"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<div class="activate-hint">仅待生效版本可修改;未到生效日仍为待生效,到日后自动按生效处理。</div>
|
||||
<div class="activate-hint">仅待生效版本可修改;精确到小时。未到生效时间仍为待生效,到时自动按生效处理。</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="activateVisible = false">取消</el-button>
|
||||
@@ -66,6 +70,7 @@ import {
|
||||
getLabelPackage,
|
||||
listLabelVersions,
|
||||
activateLabelVersion,
|
||||
deleteLabelVersion,
|
||||
listLabelCustomer,
|
||||
addLabelCustomer,
|
||||
batchAddLabelCustomer,
|
||||
@@ -78,6 +83,7 @@ import SetCustomerTable from '../set/components/detail/SetCustomerTable.vue'
|
||||
import LabelImportDialog from './components/LabelImportDialog.vue'
|
||||
import LabelVersionTable from './components/LabelVersionTable.vue'
|
||||
import LabelVersionProductDrawer from './components/LabelVersionProductDrawer.vue'
|
||||
import LabelChannelTable from './components/LabelChannelTable.vue'
|
||||
|
||||
const labelModuleApi = {
|
||||
...priceSetApi,
|
||||
@@ -127,6 +133,7 @@ const setInfo = reactive({
|
||||
})
|
||||
const versions = ref([])
|
||||
const importRef = ref()
|
||||
const channelTableRef = ref()
|
||||
const productDrawerVisible = ref(false)
|
||||
const activeVersion = ref(null)
|
||||
|
||||
@@ -176,6 +183,7 @@ async function loadVersions() {
|
||||
async function reloadAll() {
|
||||
await loadHeader()
|
||||
await loadVersions()
|
||||
channelTableRef.value?.reload?.()
|
||||
}
|
||||
|
||||
function openVersionDetails(row) {
|
||||
@@ -196,7 +204,7 @@ function openCreateVersion() {
|
||||
})
|
||||
}
|
||||
|
||||
/** 编辑待生效版本价卡 */
|
||||
/** 编辑待生效/已生效版本价卡 */
|
||||
function openEditVersion(row) {
|
||||
if (!priceSetId.value || !row?.versionNo) return
|
||||
proxy.$router.push({
|
||||
@@ -209,10 +217,28 @@ function openEditVersion(row) {
|
||||
})
|
||||
}
|
||||
|
||||
function removeVersion(row) {
|
||||
if (!priceSetId.value || !row?.versionNo) return
|
||||
const label = row.versionLabel || ('V' + row.versionNo)
|
||||
proxy.$modal.confirm('确认删除待生效版本 ' + label + '?该版产品价卡将一起清除。').then(() => {
|
||||
return deleteLabelVersion(priceSetId.value, row.versionNo)
|
||||
}).then(() => {
|
||||
proxy.$modal.msgSuccess('版本已删除')
|
||||
reloadAll()
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
function toHourValue(val) {
|
||||
const text = String(val || '').replace('T', ' ')
|
||||
if (text.length >= 13) return text.slice(0, 13) + ':00:00'
|
||||
if (text.length >= 10) return text.slice(0, 10) + ' 00:00:00'
|
||||
return undefined
|
||||
}
|
||||
|
||||
function openReschedule(row) {
|
||||
activateForm.versionNo = row.versionNo
|
||||
activateForm.versionLabel = row.versionLabel || ('V' + row.versionNo)
|
||||
activateForm.effectiveTime = row.effectiveTime ? String(row.effectiveTime).slice(0, 10) : undefined
|
||||
activateForm.effectiveTime = row.effectiveTime ? toHourValue(row.effectiveTime) : undefined
|
||||
activateVisible.value = true
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user