Compare commits

2 Commits

Author SHA1 Message Date
wuweihua 6ced4f237f 增加查看计费历史记录 2026-11-05 18:33:14 +08:00
wuweihua 87c75922aa 增加查看价格套餐历史版本的变更 2026-11-05 18:28:57 +08:00
15 changed files with 1354 additions and 7 deletions
@@ -16,6 +16,7 @@ import com.bms.common.core.page.TableSupport;
import com.bms.price.search.BillSearchPage;
import com.bms.price.search.EsLogType;
import com.bms.price.search.IBillSearchStore;
import com.bms.price.search.PriceChangeGroupPage;
/**
* 来自 ES 的价格变更前后日志(销售价 / 产品价 / 红线价)。
@@ -31,12 +32,39 @@ public class PriceLogController extends BaseController
@GetMapping("/list")
public TableDataInfo list(
@RequestParam(value = "bizId", required = false) String bizId,
@RequestParam(value = "status", required = false) String status)
@RequestParam(value = "status", required = false) String status,
@RequestParam(value = "psvId", required = false) Long psvId,
@RequestParam(value = "priceKind", required = false) String priceKind,
@RequestParam(value = "productCode", required = false) String productCode,
@RequestParam(value = "updateTime", required = false) String updateTime,
@RequestParam(value = "updateTimePrefix", required = false) String updateTimePrefix)
{
PageDomain pageDomain = TableSupport.buildPageRequest();
int pageNum = pageDomain.getPageNum() == null ? 1 : pageDomain.getPageNum().intValue();
int pageSize = pageDomain.getPageSize() == null ? 10 : pageDomain.getPageSize().intValue();
BillSearchPage page = billSearchStore.search(EsLogType.PRICE_CHANGE, bizId, status, pageNum, pageSize);
BillSearchPage page = billSearchStore.search(EsLogType.PRICE_CHANGE, bizId, status, psvId, priceKind,
productCode, updateTime, updateTimePrefix, pageNum, pageSize);
TableDataInfo rsp = new TableDataInfo();
rsp.setCode(HttpStatus.SUCCESS);
rsp.setMsg("查询成功");
rsp.setRows(page.getRows());
rsp.setTotal(page.getTotal());
return rsp;
}
/** Group by productCode + write time for package-version drill-down. */
@PreAuthorize("@ss.hasPermi('monitor:priceLog:list')")
@GetMapping("/groups")
public TableDataInfo groups(
@RequestParam(value = "psvId", required = true) Long psvId,
@RequestParam(value = "status", required = false) String status,
@RequestParam(value = "priceKind", required = false) String priceKind)
{
PageDomain pageDomain = TableSupport.buildPageRequest();
int pageNum = pageDomain.getPageNum() == null ? 1 : pageDomain.getPageNum().intValue();
int pageSize = pageDomain.getPageSize() == null ? 10 : pageDomain.getPageSize().intValue();
PriceChangeGroupPage page = billSearchStore.searchPriceChangeGroups(psvId, priceKind, status, pageNum,
pageSize);
TableDataInfo rsp = new TableDataInfo();
rsp.setCode(HttpStatus.SUCCESS);
rsp.setMsg("查询成功");
@@ -22,9 +22,12 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.bms.common.annotation.Log;
import com.bms.common.constant.HttpStatus;
import com.bms.common.core.controller.BaseController;
import com.bms.common.core.domain.AjaxResult;
import com.bms.common.core.page.PageDomain;
import com.bms.common.core.page.TableDataInfo;
import com.bms.common.core.page.TableSupport;
import com.bms.common.enums.BusinessType;
import com.bms.common.utils.DateUtils;
import com.bms.common.utils.MessageUtils;
@@ -54,6 +57,11 @@ import com.bms.price.domain.vo.TmsPriceQuoteRequest;
import com.bms.price.domain.vo.TmsPriceQuoteResult;
import com.bms.price.enums.BillTaskSuccessStatus;
import com.bms.price.enums.BillTaskType;
import com.bms.price.search.BillSearchPage;
import com.bms.price.search.BillSyncSnapshot;
import com.bms.price.search.EsBizLogSupport;
import com.bms.price.search.EsLogType;
import com.bms.price.search.IBillSearchStore;
import com.bms.price.service.IPrcBillingTaskService;
import com.bms.price.service.IPrcOrderBillService;
import com.bms.price.service.ITmsPriceQuoteService;
@@ -84,6 +92,9 @@ public class BillingController extends BaseController
@Autowired
private OrderBillProductCheckService orderBillProductCheckService;
@Autowired
private IBillSearchStore billSearchStore;
/**
* Historical quote list.
*/
@@ -355,6 +366,97 @@ public class BillingController extends BaseController
return success(prcOrderBillService.getBill(billId));
}
/**
* ES BILL logs for this order (billing / upstream sync / fail snapshots).
* bizId prefers transferNo then orderNo (same as write path).
*/
@PreAuthorize("@ss.hasPermi('price:billing:bill') or @ss.hasPermi('price:orderBill:list')")
@GetMapping("/bill/{billId}/history")
public TableDataInfo billHistory(@PathVariable("billId") String billId)
{
if (StringUtils.isEmpty(billId))
{
return getDataTable(new ArrayList<BillSyncSnapshot>());
}
TmsPriceQuoteResult bill = prcOrderBillService.getBill(billId.trim());
if (bill == null)
{
TableDataInfo empty = new TableDataInfo();
empty.setCode(HttpStatus.SUCCESS);
empty.setMsg("查询成功");
empty.setRows(new ArrayList<BillSyncSnapshot>());
empty.setTotal(0L);
return empty;
}
PageDomain pageDomain = TableSupport.buildPageRequest();
int pageNum = pageDomain.getPageNum() == null ? 1 : pageDomain.getPageNum().intValue();
int pageSize = pageDomain.getPageSize() == null ? 10 : pageDomain.getPageSize().intValue();
String primary = EsBizLogSupport.billBizId(bill.getTransferNo(), bill.getOrderNo());
BillSearchPage page = billSearchStore.search(EsLogType.BILL, primary, null, pageNum, pageSize);
// Also pull by orderNo when transferNo was used as bizId, so older orderNo-keyed rows appear.
if (StringUtils.isNotEmpty(bill.getTransferNo()) && StringUtils.isNotEmpty(bill.getOrderNo())
&& !bill.getTransferNo().trim().equalsIgnoreCase(bill.getOrderNo().trim())
&& pageNum == 1)
{
BillSearchPage byOrder = billSearchStore.search(EsLogType.BILL, bill.getOrderNo().trim(), null, 1,
Math.max(pageSize, 50));
page = mergeBillHistory(page, byOrder, pageSize);
}
TableDataInfo rsp = new TableDataInfo();
rsp.setCode(HttpStatus.SUCCESS);
rsp.setMsg("查询成功");
rsp.setRows(page.getRows());
rsp.setTotal(page.getTotal());
return rsp;
}
private BillSearchPage mergeBillHistory(BillSearchPage primary, BillSearchPage secondary, int pageSize)
{
BillSearchPage out = new BillSearchPage();
Map<String, BillSyncSnapshot> byId = new HashMap<String, BillSyncSnapshot>();
if (primary != null && primary.getRows() != null)
{
for (BillSyncSnapshot row : primary.getRows())
{
if (row != null && StringUtils.isNotEmpty(row.getDocId()))
{
byId.put(row.getDocId(), row);
}
}
}
if (secondary != null && secondary.getRows() != null)
{
for (BillSyncSnapshot row : secondary.getRows())
{
if (row != null && StringUtils.isNotEmpty(row.getDocId()) && !byId.containsKey(row.getDocId()))
{
byId.put(row.getDocId(), row);
}
}
}
List<BillSyncSnapshot> merged = new ArrayList<BillSyncSnapshot>(byId.values());
merged.sort((a, b) -> {
String ta = a == null || a.getUpdateTime() == null ? "" : a.getUpdateTime();
String tb = b == null || b.getUpdateTime() == null ? "" : b.getUpdateTime();
return tb.compareTo(ta);
});
long total = Math.max(primary == null ? 0L : primary.getTotal(), merged.size());
if (secondary != null)
{
total = Math.max(total, secondary.getTotal());
}
out.setTotal(total);
if (merged.size() > pageSize)
{
out.setRows(new ArrayList<BillSyncSnapshot>(merged.subList(0, pageSize)));
}
else
{
out.setRows(merged);
}
return out;
}
/**
* Refund one receivable bill via billing task billType=REFUND.
*/
@@ -3,7 +3,11 @@ package com.bms.price.search;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpEntity;
@@ -148,6 +152,27 @@ public class EsBillSearchStore implements IBillSearchStore
@Override
public BillSearchPage search(String logType, String bizId, String status, int pageNum, int pageSize)
{
return search(logType, bizId, status, null, null, pageNum, pageSize);
}
@Override
public BillSearchPage search(String logType, String bizId, String status, Long psvId, String priceKind,
int pageNum, int pageSize)
{
return search(logType, bizId, status, psvId, priceKind, null, null, pageNum, pageSize);
}
@Override
public BillSearchPage search(String logType, String bizId, String status, Long psvId, String priceKind,
String productCode, String updateTime, int pageNum, int pageSize)
{
return search(logType, bizId, status, psvId, priceKind, productCode, updateTime, null, pageNum, pageSize);
}
@Override
public BillSearchPage search(String logType, String bizId, String status, Long psvId, String priceKind,
String productCode, String updateTime, String updateTimePrefix, int pageNum, int pageSize)
{
BillSearchPage page = new BillSearchPage();
if (pageNum < 1)
@@ -171,7 +196,8 @@ public class EsBillSearchStore implements IBillSearchStore
sortItem.put("updatedAt.keyword", sortOpt);
sort.add(sortItem);
body.put("sort", sort);
body.put("query", buildQuery(logType, bizId, status));
body.put("query", buildQuery(logType, bizId, status, psvId, priceKind, productCode, updateTime,
updateTimePrefix));
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> entity = new HttpEntity<String>(body.toJSONString(), headers);
@@ -217,6 +243,108 @@ public class EsBillSearchStore implements IBillSearchStore
return page;
}
@Override
public PriceChangeGroupPage searchPriceChangeGroups(Long psvId, String priceKind, String status,
int pageNum, int pageSize)
{
PriceChangeGroupPage page = new PriceChangeGroupPage();
if (psvId == null)
{
return page;
}
if (pageNum < 1)
{
pageNum = 1;
}
if (pageSize < 1)
{
pageSize = 10;
}
List<BillSyncSnapshot> all = loadAllPriceChanges(psvId, priceKind, status, 10000);
Map<String, PriceChangeGroup> grouped = new LinkedHashMap<String, PriceChangeGroup>();
for (BillSyncSnapshot snap : all)
{
if (snap == null)
{
continue;
}
JSONObject req = parseRequestObject(snap.getRequestJson());
JSONObject keys = req == null ? null : req.getJSONObject("bizKeys");
String productCode = keys == null ? "" : StringUtils.nvl(keys.getString("productCode"), "");
String time = StringUtils.nvl(snap.getUpdateTime(), "");
String key = productCode + "|" + time;
PriceChangeGroup group = grouped.get(key);
if (group == null)
{
group = new PriceChangeGroup();
group.setProductCode(StringUtils.isEmpty(productCode) ? "-" : productCode);
group.setUpdateTime(time);
group.setAction(req == null ? null : req.getString("action"));
group.setOperator(req == null ? null : req.getString("operator"));
group.setStatus(snap.getStatus());
group.setChangeCount(0);
grouped.put(key, group);
}
group.setChangeCount(group.getChangeCount() + 1);
if (EsBizLogSupport.STATUS_FAIL.equals(snap.getStatus()))
{
group.setStatus(EsBizLogSupport.STATUS_FAIL);
}
}
List<PriceChangeGroup> list = new ArrayList<PriceChangeGroup>(grouped.values());
page.setTotal(list.size());
int from = (pageNum - 1) * pageSize;
if (from >= list.size())
{
return page;
}
int to = Math.min(from + pageSize, list.size());
page.setRows(new ArrayList<PriceChangeGroup>(list.subList(from, to)));
return page;
}
private List<BillSyncSnapshot> loadAllPriceChanges(Long psvId, String priceKind, String status, int max)
{
List<BillSyncSnapshot> all = new ArrayList<BillSyncSnapshot>();
int batch = 1000;
int pageNum = 1;
while (all.size() < max)
{
int size = Math.min(batch, max - all.size());
BillSearchPage chunk = search(EsLogType.PRICE_CHANGE, null, status, psvId, priceKind, null, null,
pageNum, size);
List<BillSyncSnapshot> rows = chunk.getRows();
if (rows == null || rows.isEmpty())
{
break;
}
all.addAll(rows);
if (rows.size() < size || all.size() >= chunk.getTotal())
{
break;
}
pageNum++;
}
return all;
}
private JSONObject parseRequestObject(String requestJson)
{
if (StringUtils.isEmpty(requestJson))
{
return null;
}
try
{
Object parsed = JSON.parse(requestJson);
return parsed instanceof JSONObject ? (JSONObject) parsed : null;
}
catch (Exception e)
{
return null;
}
}
private BillSyncSnapshot toSnapshot(JSONObject source, String id)
{
if (source == null)
@@ -249,12 +377,18 @@ public class EsBillSearchStore implements IBillSearchStore
return snap;
}
private JSONObject buildQuery(String logType, String bizId, String status)
private JSONObject buildQuery(String logType, String bizId, String status, Long psvId, String priceKind,
String productCode, String updateTime, String updateTimePrefix)
{
JSONArray filters = new JSONArray();
addTerm(filters, "logType", logType);
addWildcard(filters, "bizId", bizId);
addTerm(filters, "status", status);
addPsvIdFilter(filters, psvId);
addTerm(filters, "request.priceKind", priceKind);
addProductCodeFilter(filters, productCode);
addUpdateTimeFilter(filters, updateTime);
addUpdateTimePrefixFilter(filters, updateTimePrefix);
if (filters.isEmpty())
{
JSONObject matchAll = new JSONObject();
@@ -268,6 +402,136 @@ public class EsBillSearchStore implements IBillSearchStore
return query;
}
private void addProductCodeFilter(JSONArray filters, String productCode)
{
if (StringUtils.isEmpty(productCode))
{
return;
}
String code = productCode.trim();
JSONArray should = new JSONArray();
JSONObject termKw = new JSONObject();
termKw.put("request.bizKeys.productCode.keyword", code);
JSONObject wrapKw = new JSONObject();
wrapKw.put("term", termKw);
should.add(wrapKw);
JSONObject term = new JSONObject();
term.put("request.bizKeys.productCode", code);
JSONObject wrap = new JSONObject();
wrap.put("term", term);
should.add(wrap);
JSONObject inner = new JSONObject();
inner.put("should", should);
inner.put("minimum_should_match", Integer.valueOf(1));
JSONObject boolWrap = new JSONObject();
boolWrap.put("bool", inner);
filters.add(boolWrap);
}
private void addUpdateTimeFilter(JSONArray filters, String updateTime)
{
if (StringUtils.isEmpty(updateTime))
{
return;
}
String time = updateTime.trim();
JSONArray should = new JSONArray();
JSONObject t1 = new JSONObject();
t1.put("updateTime.keyword", time);
JSONObject w1 = new JSONObject();
w1.put("term", t1);
should.add(w1);
JSONObject t2 = new JSONObject();
t2.put("updatedAt.keyword", time);
JSONObject w2 = new JSONObject();
w2.put("term", t2);
should.add(w2);
JSONObject t3 = new JSONObject();
t3.put("updateTime", time);
JSONObject w3 = new JSONObject();
w3.put("term", t3);
should.add(w3);
JSONObject t4 = new JSONObject();
t4.put("updatedAt", time);
JSONObject w4 = new JSONObject();
w4.put("term", t4);
should.add(w4);
JSONObject inner = new JSONObject();
inner.put("should", should);
inner.put("minimum_should_match", Integer.valueOf(1));
JSONObject wrap = new JSONObject();
wrap.put("bool", inner);
filters.add(wrap);
}
/** Match write-time by minute prefix, e.g. 2026-11-05 13:27. */
private void addUpdateTimePrefixFilter(JSONArray filters, String updateTimePrefix)
{
if (StringUtils.isEmpty(updateTimePrefix))
{
return;
}
String prefix = escapeWildcard(updateTimePrefix.trim()) + "*";
JSONArray should = new JSONArray();
for (String field : new String[] { "updateTime.keyword", "updatedAt.keyword", "updateTime", "updatedAt" })
{
JSONObject opts = new JSONObject();
opts.put("value", prefix);
opts.put("case_insensitive", Boolean.TRUE);
JSONObject wildcard = new JSONObject();
wildcard.put(field, opts);
JSONObject wrap = new JSONObject();
wrap.put("wildcard", wildcard);
should.add(wrap);
}
JSONObject inner = new JSONObject();
inner.put("should", should);
inner.put("minimum_should_match", Integer.valueOf(1));
JSONObject wrap = new JSONObject();
wrap.put("bool", inner);
filters.add(wrap);
}
/** Prefer numeric term; also accept keyword/string mapping of bizKeys.psvId. */
private void addPsvIdFilter(JSONArray filters, Long psvId)
{
if (psvId == null)
{
return;
}
JSONArray should = new JSONArray();
JSONObject termNum = new JSONObject();
termNum.put("request.bizKeys.psvId", psvId);
JSONObject wrapNum = new JSONObject();
wrapNum.put("term", termNum);
should.add(wrapNum);
JSONObject termKw = new JSONObject();
termKw.put("request.bizKeys.psvId.keyword", String.valueOf(psvId));
JSONObject wrapKw = new JSONObject();
wrapKw.put("term", termKw);
should.add(wrapKw);
JSONObject termStr = new JSONObject();
termStr.put("request.bizKeys.psvId", String.valueOf(psvId));
JSONObject wrapStr = new JSONObject();
wrapStr.put("term", termStr);
should.add(wrapStr);
JSONObject inner = new JSONObject();
inner.put("should", should);
inner.put("minimum_should_match", Integer.valueOf(1));
JSONObject wrap = new JSONObject();
wrap.put("bool", inner);
filters.add(wrap);
}
private void addTerm(JSONArray filters, String field, String value)
{
if (StringUtils.isEmpty(value))
@@ -17,4 +17,37 @@ public interface IBillSearchStore
BillSyncSnapshot getByDocId(String docId);
BillSearchPage search(String logType, String bizId, String status, int pageNum, int pageSize);
/**
* Search with optional sales-version / priceKind filters (request.bizKeys.psvId, request.priceKind).
*/
default BillSearchPage search(String logType, String bizId, String status, Long psvId, String priceKind,
int pageNum, int pageSize)
{
return search(logType, bizId, status, psvId, priceKind, null, null, pageNum, pageSize);
}
/**
* Search with optional productCode + updateTime (exact) / updateTimePrefix (e.g. yyyy-MM-dd HH:mm) filters.
*/
default BillSearchPage search(String logType, String bizId, String status, Long psvId, String priceKind,
String productCode, String updateTime, int pageNum, int pageSize)
{
return search(logType, bizId, status, psvId, priceKind, productCode, updateTime, null, pageNum, pageSize);
}
default BillSearchPage search(String logType, String bizId, String status, Long psvId, String priceKind,
String productCode, String updateTime, String updateTimePrefix, int pageNum, int pageSize)
{
return search(logType, bizId, status, pageNum, pageSize);
}
/**
* Group PRICE_CHANGE logs by productCode + updateTime for one package version.
*/
default PriceChangeGroupPage searchPriceChangeGroups(Long psvId, String priceKind, String status,
int pageNum, int pageSize)
{
return new PriceChangeGroupPage();
}
}
@@ -25,12 +25,33 @@ public final class PriceChangeEsLog
public static final String ACTION_DELETE = "DELETE";
public static final String ACTION_IMPORT = "IMPORT";
/** Async import workers have no SecurityContext; pass operName via this override. */
private static final ThreadLocal<String> OPERATOR_OVERRIDE = new ThreadLocal<String>();
private PriceChangeEsLog()
{
}
public static void beginOperator(String operName)
{
if (StringUtils.isNotEmpty(operName))
{
OPERATOR_OVERRIDE.set(operName.trim());
}
}
public static void endOperator()
{
OPERATOR_OVERRIDE.remove();
}
public static String operator()
{
String override = OPERATOR_OVERRIDE.get();
if (StringUtils.isNotEmpty(override))
{
return override;
}
try
{
String name = SecurityUtils.getUsername();
@@ -0,0 +1,74 @@
package com.bms.price.search;
/**
* Price-change log group key: productCode + write time.
*/
public class PriceChangeGroup
{
private String productCode;
private String updateTime;
private String action;
private String operator;
private String status;
private int changeCount;
public String getProductCode()
{
return productCode;
}
public void setProductCode(String productCode)
{
this.productCode = productCode;
}
public String getUpdateTime()
{
return updateTime;
}
public void setUpdateTime(String updateTime)
{
this.updateTime = updateTime;
}
public String getAction()
{
return action;
}
public void setAction(String action)
{
this.action = action;
}
public String getOperator()
{
return operator;
}
public void setOperator(String operator)
{
this.operator = operator;
}
public String getStatus()
{
return status;
}
public void setStatus(String status)
{
this.status = status;
}
public int getChangeCount()
{
return changeCount;
}
public void setChangeCount(int changeCount)
{
this.changeCount = changeCount;
}
}
@@ -0,0 +1,31 @@
package com.bms.price.search;
import java.util.ArrayList;
import java.util.List;
/** Paginated product+time groups of price-change logs. */
public class PriceChangeGroupPage
{
private long total;
private List<PriceChangeGroup> rows = new ArrayList<PriceChangeGroup>();
public long getTotal()
{
return total;
}
public void setTotal(long total)
{
this.total = total;
}
public List<PriceChangeGroup> getRows()
{
return rows;
}
public void setRows(List<PriceChangeGroup> rows)
{
this.rows = rows == null ? new ArrayList<PriceChangeGroup>() : rows;
}
}
@@ -1582,6 +1582,23 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
@Transactional(rollbackFor = Exception.class)
public String importDetailPrices(Long priceSetId, Long psvId, byte[] bytes, String filename, String operName,
String taskId, boolean updateOnly, boolean replaceByProductCountry, String countryCode) throws Exception
{
// Async import has no SecurityContext; bind operName for PriceChangeEsLog.operator().
PriceChangeEsLog.beginOperator(operName);
try
{
return importDetailPricesInOperator(priceSetId, psvId, bytes, filename, operName, taskId, updateOnly,
replaceByProductCountry, countryCode);
}
finally
{
PriceChangeEsLog.endOperator();
}
}
private String importDetailPricesInOperator(Long priceSetId, Long psvId, byte[] bytes, String filename,
String operName, String taskId, boolean updateOnly, boolean replaceByProductCountry, String countryCode)
throws Exception
{
PrcPriceSet db = requireSet(priceSetId);
assertChildEditable(db);
+9
View File
@@ -8,6 +8,15 @@ export function listPriceLog(query) {
})
}
/** Group by productCode + updateTime for a package version. */
export function listPriceLogGroups(query) {
return request({
url: '/monitor/priceLog/groups',
method: 'get',
params: query
})
}
export function getPriceLog(docId) {
return request({
url: '/monitor/priceLog/' + encodeURIComponent(docId),
+9
View File
@@ -187,6 +187,15 @@ export function getBill(billId) {
})
}
/** ES billing history for one bill/order */
export function listBillHistory(billId, query) {
return request({
url: '/price/billing/bill/' + encodeURIComponent(billId) + '/history',
method: 'get',
params: query
})
}
/** Refund receivable bill (billType=REFUND) */
export function refundBill(billId) {
return request({
@@ -0,0 +1,265 @@
<template>
<el-drawer
:model-value="modelValue"
:title="title"
size="860px"
append-to-body
destroy-on-close
@update:model-value="$emit('update:modelValue', $event)"
>
<div class="hist-wrap" v-loading="loading">
<el-table :data="rows" border height="calc(100vh - 220px)" :empty-text="emptyText">
<el-table-column :label="colTime" width="170" align="center" prop="updateTime" />
<el-table-column :label="colAction" width="100" align="center">
<template #default="{ row }">
<el-tag size="small" :type="actionTag(row)">{{ actionLabel(row) }}</el-tag>
</template>
</el-table-column>
<el-table-column :label="colStatus" width="90" align="center">
<template #default="{ row }">
<el-tag size="small" :type="statusTag(row.status)">{{ statusLabel(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column :label="colSet" min-width="150" show-overflow-tooltip>
<template #default="{ row }">{{ priceSetOf(row) }}</template>
</el-table-column>
<el-table-column :label="colVer" width="90" align="center">
<template #default="{ row }">{{ versionOf(row) }}</template>
</el-table-column>
<el-table-column :label="colPsd" width="100" align="center">
<template #default="{ row }">{{ psdOf(row) }}</template>
</el-table-column>
<el-table-column :label="colAmount" width="110" align="right">
<template #default="{ row }">{{ amountOf(row) }}</template>
</el-table-column>
<el-table-column :label="colOps" width="80" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openDetail(row)">查看</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination
v-model:current-page="query.pageNum"
v-model:page-size="query.pageSize"
:total="total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
@size-change="reload"
@current-change="reload"
/>
</div>
</div>
<el-drawer v-model="detailOpen" size="720px" :title="detailTitle" append-to-body destroy-on-close>
<div v-if="!active">
<el-empty :description="emptyDetail" />
</div>
<div v-else>
<el-descriptions :column="2" border size="small" class="mb12">
<el-descriptions-item :label="colTime">{{ active.updateTime || '-' }}</el-descriptions-item>
<el-descriptions-item :label="colAction">{{ actionLabel(active) }}</el-descriptions-item>
<el-descriptions-item :label="colStatus">{{ statusLabel(active.status) }}</el-descriptions-item>
<el-descriptions-item :label="colSet">{{ priceSetOf(active) }}</el-descriptions-item>
<el-descriptions-item :label="colVer">{{ versionOf(active) }}</el-descriptions-item>
<el-descriptions-item :label="colPsd">{{ psdOf(active) }}</el-descriptions-item>
<el-descriptions-item :label="colAmount">{{ amountOf(active) }}</el-descriptions-item>
<el-descriptions-item :label="lbMatch">{{ matchMsgOf(active) }}</el-descriptions-item>
<el-descriptions-item v-if="active.errorMessage" :label="lbError" :span="2">
<span class="err">{{ active.errorMessage }}</span>
</el-descriptions-item>
</el-descriptions>
<div class="json-panel">
<div class="json-panel__head">请求报文</div>
<pre class="code-pre">{{ formatJson(active.requestJson) }}</pre>
</div>
<div class="json-panel">
<div class="json-panel__head">返回报文</div>
<pre class="code-pre">{{ formatJson(active.responseJson) }}</pre>
</div>
</div>
</el-drawer>
</el-drawer>
</template>
<script setup name="BillBillingHistoryDrawer">
import { listBillHistory } from '@/api/price/billing'
const props = defineProps({
modelValue: { type: Boolean, default: false },
billId: { type: String, default: '' },
orderNo: { type: String, default: '' }
})
defineEmits(['update:modelValue'])
const loading = ref(false)
const rows = ref([])
const total = ref(0)
const query = reactive({ pageNum: 1, pageSize: 10 })
const detailOpen = ref(false)
const active = ref(null)
const emptyText = '暂无历史计费记录'
const emptyDetail = '暂无详情'
const colTime = '写入时间'
const colAction = '类型'
const colStatus = '状态'
const colSet = '套餐'
const colVer = '版本'
const colPsd = '明细ID'
const colAmount = '应收总额'
const colOps = '操作'
const lbMatch = '命中说明'
const lbError = '错误信息'
const title = computed(() => {
const no = props.orderNo || ''
return no ? '历史计费记录 · ' + no : '历史计费记录'
})
const detailTitle = computed(() => '计费记录详情')
function parseJson(val) {
if (val == null || val === '') return {}
if (typeof val === 'object') return val
try { return JSON.parse(val) } catch (e) { return {} }
}
function respOf(row) {
return parseJson(row && row.responseJson)
}
function reqOf(row) {
return parseJson(row && row.requestJson)
}
function actionLabel(row) {
const req = reqOf(row)
if (req.action === 'UPSTREAM_SYNC') return '上游同步'
if (row && row.status === 'FAIL') return '计费失败'
const resp = respOf(row)
if (resp.matchSource === 'BILLING_FAIL' || resp.billingFailStatus) return '计费失败'
return '计费'
}
function actionTag(row) {
const label = actionLabel(row)
if (label === '上游同步') return 'info'
if (label === '计费失败') return 'danger'
return 'success'
}
function statusLabel(status) {
return status === 'FAIL' ? '失败' : '成功'
}
function statusTag(status) {
return status === 'FAIL' ? 'danger' : 'success'
}
function priceSetOf(row) {
const r = respOf(row)
if (r.priceSetName && r.priceSetCode) return r.priceSetName + ' (' + r.priceSetCode + ')'
return r.priceSetName || r.priceSetCode || (r.priceSetId != null ? String(r.priceSetId) : '-')
}
function versionOf(row) {
const r = respOf(row)
if (r.versionNo) return r.versionNo
return r.psvId != null ? ('psv=' + r.psvId) : '-'
}
function psdOf(row) {
const r = respOf(row)
return r.psdId != null ? String(r.psdId) : '-'
}
function amountOf(row) {
const r = respOf(row)
const v = r.incomeTotal != null ? r.incomeTotal : r.payableTotal
if (v == null || v === '') return '-'
const n = Number(v)
return Number.isNaN(n) ? String(v) : n.toFixed(2)
}
function matchMsgOf(row) {
return respOf(row).matchMessage || '-'
}
function formatJson(val) {
if (val == null || val === '') return '无数据'
if (typeof val === 'object') {
try { return JSON.stringify(val, null, 2) } catch (e) { return String(val) }
}
try { return JSON.stringify(JSON.parse(val), null, 2) } catch (e) { return String(val) }
}
function openDetail(row) {
active.value = row
detailOpen.value = true
}
async function reload() {
if (!props.billId) {
rows.value = []
total.value = 0
return
}
loading.value = true
try {
const res = await listBillHistory(props.billId, {
pageNum: query.pageNum,
pageSize: query.pageSize
})
rows.value = res.rows || []
total.value = Number(res.total) || 0
} finally {
loading.value = false
}
}
watch(
() => [props.modelValue, props.billId],
([open]) => {
if (open) {
query.pageNum = 1
detailOpen.value = false
active.value = null
reload()
}
}
)
</script>
<style scoped>
.hist-wrap { min-height: 200px; }
.pager {
display: flex;
justify-content: flex-end;
margin-top: 12px;
}
.mb12 { margin-bottom: 12px; }
.err { color: #f56c6c; word-break: break-all; }
.json-panel {
margin-bottom: 12px;
background: #f7f9fb;
border: 1px solid #e8ecf0;
border-radius: 6px;
overflow: hidden;
}
.json-panel__head {
padding: 8px 12px;
font-size: 13px;
font-weight: 600;
color: #606266;
background: #fafbfc;
border-bottom: 1px solid #ebeef5;
}
.code-pre {
margin: 0;
padding: 12px 14px;
max-height: 260px;
overflow: auto;
white-space: pre-wrap;
word-break: break-all;
}
</style>
@@ -41,7 +41,17 @@
</section>
<section class="detail-block">
<div class="sec-title">计费信息</div>
<div class="sec-title">
计费信息
<el-button
v-if="detail && detail.billId"
type="primary"
link
size="small"
class="hist-btn"
@click="historyOpen = true"
>历史计费</el-button>
</div>
<div class="amount-summary">
<div class="amount-card">
<div class="amount-label">运费金额</div>
@@ -184,12 +194,18 @@
:current-charge-weight="detail?.chargeWeight"
@success="onChargeWeightUpdated"
/>
<bill-billing-history-drawer
v-model="historyOpen"
:bill-id="detail?.billId || ''"
:order-no="detail?.orderNo || ''"
/>
</el-drawer>
</template>
<script setup>
import AddIncomeFeeDialog from './AddIncomeFeeDialog.vue'
import EditChargeWeightDialog from './EditChargeWeightDialog.vue'
import BillBillingHistoryDrawer from './BillBillingHistoryDrawer.vue'
import { deleteIncomeBillFee } from '@/api/price/billing'
const { proxy } = getCurrentInstance()
@@ -205,6 +221,7 @@ const props = defineProps({
const emit = defineEmits(['update:modelValue', 'updated'])
const feeTab = ref('income')
const historyOpen = ref(false)
const addFeeOpen = ref(false)
const editFeeRow = ref(null)
const editCwOpen = ref(false)
@@ -224,6 +241,7 @@ const payFees = computed(() => {
watch(() => props.detail, () => {
feeTab.value = 'income'
historyOpen.value = false
})
function canEditManualFee(row) {
@@ -465,6 +483,9 @@ function refundLabel(status) {
border-radius: 2px;
background: var(--el-color-primary);
}
.hist-btn {
margin-left: auto;
}
.amount-summary {
display: grid;
grid-template-columns: repeat(5, 1fr);
@@ -0,0 +1,450 @@
<template>
<el-drawer
:model-value="visible"
:title="title"
size="900px"
append-to-body
destroy-on-close
@close="close"
>
<div class="price-log-drawer" v-loading="loading">
<div class="toolbar">
<el-select v-model="query.status" clearable :placeholder="phStatus" style="width: 120px" @change="reload">
<el-option v-for="o in statusOptions" :key="o.value" :label="o.label" :value="o.value" />
</el-select>
<el-button type="primary" @click="reload">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</div>
<el-empty v-if="!timeTabs.length" :description="emptyText" />
<template v-else>
<el-tabs v-model="activeTime" class="time-tabs" @tab-change="onTimeTabChange">
<el-tab-pane
v-for="t in timeTabs"
:key="t.key"
:name="t.key"
:label="t.label"
/>
</el-tabs>
<el-table :data="productRows" border height="calc(100vh - 320px)" :empty-text="emptyProducts">
<el-table-column :label="colProduct" min-width="120" prop="productCode" show-overflow-tooltip />
<el-table-column :label="colAction" width="90" align="center">
<template #default="{ row }">{{ actionLabelOf(row.action) }}</template>
</el-table-column>
<el-table-column :label="colOperator" width="110" show-overflow-tooltip>
<template #default="{ row }">{{ row.operator || '-' }}</template>
</el-table-column>
<el-table-column :label="colCount" width="100" align="center" prop="changeCount" />
<el-table-column :label="colStatus" width="90" align="center">
<template #default="{ row }">
<el-tag size="small" :type="statusTag(row.status)">{{ statusLabel(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column :label="colOps" width="90" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openProduct(row)">明细</el-button>
</template>
</el-table-column>
</el-table>
</template>
</div>
<el-drawer v-model="groupOpen" size="780px" :title="groupTitle" append-to-body destroy-on-close>
<div v-loading="groupLoading">
<el-table :data="groupRows" border height="calc(100vh - 220px)" :empty-text="emptyGroup">
<el-table-column :label="colCountry" min-width="140" show-overflow-tooltip>
<template #default="{ row }">{{ countryOf(row) }}</template>
</el-table-column>
<el-table-column :label="colAction" width="90" align="center">
<template #default="{ row }">{{ actionLabel(row) }}</template>
</el-table-column>
<el-table-column :label="colChangeSummary" min-width="220" show-overflow-tooltip>
<template #default="{ row }">{{ changeSummary(row) }}</template>
</el-table-column>
<el-table-column :label="colStatus" width="90" align="center">
<template #default="{ row }">
<el-tag size="small" :type="statusTag(row.status)">{{ statusLabel(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column :label="colTime" width="170" align="center" prop="updateTime" />
<el-table-column :label="colOps" width="90" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="handleView(row)">查看</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination
v-model:current-page="groupQuery.pageNum"
v-model:page-size="groupQuery.pageSize"
:total="groupTotal"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next"
@size-change="reloadGroup"
@current-change="reloadGroup"
/>
</div>
</div>
</el-drawer>
<el-drawer v-model="detailOpen" size="720px" :title="detailTitle" append-to-body destroy-on-close>
<div v-loading="detailLoading">
<el-empty v-if="!detail" :description="emptyDetail" />
<div v-else>
<el-descriptions :column="2" border size="small" class="mb12">
<el-descriptions-item :label="lbAction">{{ actionLabel(detail) }}</el-descriptions-item>
<el-descriptions-item :label="lbOperator">{{ operatorOf(detail) }}</el-descriptions-item>
<el-descriptions-item :label="lbStatus">
<el-tag size="small" :type="statusTag(detail.status)">{{ statusLabel(detail.status) }}</el-tag>
</el-descriptions-item>
<el-descriptions-item :label="lbTime">{{ detail.updateTime || '-' }}</el-descriptions-item>
<el-descriptions-item :label="lbProduct">{{ productCodeOf(detail) }}</el-descriptions-item>
<el-descriptions-item :label="lbCountry">{{ countryOf(detail) }}</el-descriptions-item>
</el-descriptions>
<div class="change-panel">
<div class="change-panel__head">改前改后</div>
<el-table :data="changesOf(detail)" border size="small" :empty-text="emptyChanges">
<el-table-column prop="fieldLabel" :label="colField" min-width="120" />
<el-table-column prop="oldValue" :label="colOld" min-width="140" show-overflow-tooltip />
<el-table-column prop="newValue" :label="colNew" min-width="140" show-overflow-tooltip />
</el-table>
</div>
</div>
</div>
</el-drawer>
</el-drawer>
</template>
<script setup name="SetVersionPriceLogDrawer">
import { listPriceLog, listPriceLogGroups, getPriceLog } from '@/api/monitor/priceLog'
const props = defineProps({
modelValue: { type: Boolean, default: false },
version: { type: Object, default: null }
})
const emit = defineEmits(['update:modelValue'])
const visible = computed({
get: () => props.modelValue,
set: v => emit('update:modelValue', v)
})
const loading = ref(false)
const allGroups = ref([])
const query = reactive({ status: undefined })
const activeTime = ref('')
const groupOpen = ref(false)
const groupLoading = ref(false)
const groupRows = ref([])
const groupTotal = ref(0)
const groupTitle = ref('')
const activeProduct = ref(null)
const groupQuery = reactive({ pageNum: 1, pageSize: 20 })
const detailOpen = ref(false)
const detailLoading = ref(false)
const detail = ref(null)
const detailTitle = ref('')
const phStatus = '状态'
const emptyText = '该版本暂无价格变更记录'
const emptyProducts = '该时间点暂无产品变更'
const emptyGroup = '该产品暂无明细变更'
const emptyDetail = '暂无详情'
const emptyChanges = '无字段变更'
const colAction = '操作'
const colOperator = '操作人'
const colProduct = '产品代码'
const colCountry = '国家'
const colCount = '变更条数'
const colChangeSummary = '变更摘要'
const colStatus = '状态'
const colTime = '写入时间'
const colOps = '操作'
const colField = '字段'
const colOld = '改前'
const colNew = '改后'
const lbAction = '操作'
const lbOperator = '操作人'
const lbStatus = '状态'
const lbTime = '写入时间'
const lbProduct = '产品代码'
const lbCountry = '国家'
const statusOptions = [
{ label: '成功', value: 'SUCCESS' },
{ label: '失败', value: 'FAIL' }
]
const actionOptions = [
{ label: '新增', value: 'INSERT' },
{ label: '修改', value: 'UPDATE' },
{ label: '删除', value: 'DELETE' },
{ label: '导入', value: 'IMPORT' }
]
const title = computed(() => {
const v = props.version || {}
const ver = v.versionNo || ''
const eff = v.effectiveTime || ''
const base = '版本价格变更记录'
if (ver || eff) return base + ' · ' + (ver || '') + (eff ? ' (' + eff + ')' : '')
return base
})
/** Minute key: yyyy-MM-dd HH:mm */
function toMinuteKey(time) {
if (!time) return '-'
const s = String(time).trim()
return s.length >= 16 ? s.slice(0, 16) : s
}
const timeTabs = computed(() => {
const map = new Map()
for (const g of allGroups.value || []) {
const key = toMinuteKey(g && g.updateTime)
if (!map.has(key)) {
map.set(key, { key, label: key, changeCount: 0, productSet: new Set() })
}
const tab = map.get(key)
tab.changeCount += Number(g.changeCount) || 0
if (g.productCode) tab.productSet.add(g.productCode)
}
return Array.from(map.values())
.sort((a, b) => String(b.key).localeCompare(String(a.key)))
.map(t => ({
key: t.key,
label: t.key + ' (' + t.productSet.size + ')',
changeCount: t.changeCount
}))
})
const productRows = computed(() => {
const minute = activeTime.value
if (!minute) return []
const map = new Map()
for (const g of allGroups.value || []) {
if (toMinuteKey(g && g.updateTime) !== minute) continue
const code = g.productCode || '-'
let row = map.get(code)
if (!row) {
row = {
productCode: code,
action: g.action,
operator: g.operator,
status: g.status,
changeCount: 0,
updateTimePrefix: minute
}
map.set(code, row)
}
row.changeCount += Number(g.changeCount) || 0
if (g.status === 'FAIL') row.status = 'FAIL'
if (!row.operator && g.operator) row.operator = g.operator
if (!row.action && g.action) row.action = g.action
}
return Array.from(map.values()).sort((a, b) => String(a.productCode).localeCompare(String(b.productCode)))
})
function close() {
visible.value = false
}
function resetQuery() {
query.status = undefined
reload()
}
function onTimeTabChange() {
groupOpen.value = false
detailOpen.value = false
}
function parseReq(row) {
if (!row || !row.requestJson) return {}
try {
return typeof row.requestJson === 'object' ? row.requestJson : JSON.parse(row.requestJson)
} catch (e) {
return {}
}
}
function actionLabelOf(action) {
const found = actionOptions.find(i => i.value === action)
return found ? found.label : (action || '-')
}
function actionLabel(row) {
return actionLabelOf(parseReq(row).action)
}
function operatorOf(row) {
return parseReq(row).operator || '-'
}
function productCodeOf(row) {
const keys = parseReq(row).bizKeys || {}
return keys.productCode || '-'
}
function countryOf(row) {
const keys = parseReq(row).bizKeys || {}
if (keys.countryCode && keys.countryName) return keys.countryName + ' (' + keys.countryCode + ')'
return keys.countryCode || keys.countryName || '-'
}
function changesOf(row) {
const req = parseReq(row)
return Array.isArray(req.changes) ? req.changes : []
}
function changeSummary(row) {
const list = changesOf(row)
if (!list.length) return '-'
return list.map(c => {
const label = (c && c.fieldLabel) || (c && c.field) || ''
const oldV = c && c.oldValue != null ? c.oldValue : ''
const newV = c && c.newValue != null ? c.newValue : ''
return label + ': ' + oldV + ' -> ' + newV
}).join(';')
}
function statusLabel(status) {
const found = statusOptions.find(item => item.value === status)
if (found) return found.label
return status === 'FAIL' ? '失败' : '成功'
}
function statusTag(status) {
return status === 'FAIL' ? 'danger' : 'success'
}
async function reload() {
const psvId = props.version && props.version.psvId
if (psvId == null) {
allGroups.value = []
activeTime.value = ''
return
}
loading.value = true
try {
const res = await listPriceLogGroups({
pageNum: 1,
pageSize: 500,
status: query.status,
psvId,
priceKind: 'SALES'
})
allGroups.value = res.rows || []
const tabs = timeTabs.value
if (!tabs.length) {
activeTime.value = ''
} else if (!tabs.some(t => t.key === activeTime.value)) {
activeTime.value = tabs[0].key
}
} finally {
loading.value = false
}
}
function openProduct(row) {
if (!row) return
activeProduct.value = row
groupTitle.value = '变更明细 · ' + (row.productCode || '-') + (activeTime.value ? ' @ ' + activeTime.value : '')
groupQuery.pageNum = 1
groupOpen.value = true
reloadGroup()
}
async function reloadGroup() {
const psvId = props.version && props.version.psvId
const p = activeProduct.value
if (psvId == null || !p) {
groupRows.value = []
groupTotal.value = 0
return
}
groupLoading.value = true
try {
const res = await listPriceLog({
pageNum: groupQuery.pageNum,
pageSize: groupQuery.pageSize,
status: query.status,
psvId,
priceKind: 'SALES',
productCode: p.productCode === '-' ? undefined : p.productCode,
updateTimePrefix: activeTime.value || p.updateTimePrefix || undefined
})
groupRows.value = res.rows || []
groupTotal.value = Number(res.total) || 0
} finally {
groupLoading.value = false
}
}
function handleView(row) {
detailTitle.value = '价格变更 #' + (row.bizId || row.docId)
detailOpen.value = true
detailLoading.value = true
detail.value = null
getPriceLog(row.docId).then(res => {
detail.value = res.data || row
}).finally(() => {
detailLoading.value = false
})
}
watch(
() => [props.modelValue, props.version && props.version.psvId],
([open]) => {
if (open) {
activeTime.value = ''
reload()
} else {
groupOpen.value = false
detailOpen.value = false
detail.value = null
activeProduct.value = null
allGroups.value = []
}
}
)
</script>
<style scoped>
.price-log-drawer { min-height: 240px; }
.toolbar {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.time-tabs {
margin-bottom: 8px;
}
.time-tabs :deep(.el-tabs__header) {
margin-bottom: 8px;
}
.pager {
display: flex;
justify-content: flex-end;
margin-top: 12px;
}
.mb12 { margin-bottom: 12px; }
.change-panel {
margin-bottom: 12px;
border: 1px solid #e8ecf0;
border-radius: 6px;
overflow: hidden;
}
.change-panel__head {
padding: 8px 12px;
font-size: 13px;
font-weight: 600;
color: #606266;
background: #fafbfc;
border-bottom: 1px solid #ebeef5;
}
</style>
@@ -35,10 +35,16 @@
<el-table-column :label="colRows" width="100" align="center">
<template #default="{ row }">{{ detailCount(row) }}</template>
</el-table-column>
<el-table-column :label="colAction" width="460" align="center" fixed="right">
<el-table-column :label="colAction" width="540" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openDetails(row)">查看详情</el-button>
<el-button link type="primary" @click="emit('view-import-attach', row)">查看附件</el-button>
<el-button
link
type="primary"
v-hasPermi="['monitor:priceLog:list']"
@click="emit('view-price-log', row)"
>变更记录</el-button>
<el-button
v-if="showEditButton(row)"
link
@@ -89,7 +95,7 @@ const props = defineProps({
modelValue: { type: [Number, String], default: undefined },
removingPsvId: { type: [Number, String], default: null }
})
const emit = defineEmits(['update:modelValue', 'create-version', 'open-details', 'view-import-attach', 'activate', 'reschedule', 'edit', 'remove'])
const emit = defineEmits(['update:modelValue', 'create-version', 'open-details', 'view-import-attach', 'view-price-log', 'activate', 'reschedule', 'edit', 'remove'])
const title = '套餐版本'
const colEff = '生效时间'
+17
View File
@@ -27,6 +27,7 @@
@create-version="onCreateVersion"
@open-details="onOpenVersionDetails"
@view-import-attach="onViewImportAttach"
@view-price-log="onViewPriceLog"
@activate="onActivateVersion"
@reschedule="onRescheduleVersion"
@edit="onEditVersion"
@@ -39,6 +40,10 @@
:set-type="set.setType"
/>
<set-version-excel-file-drawer ref="versionExcelDrawerRef" />
<set-version-price-log-drawer
v-model="priceLogVisible"
:version="priceLogVersion"
/>
<el-dialog
v-model="activateVisible"
@@ -81,6 +86,7 @@ import SetCustomerTable from './components/detail/SetCustomerTable.vue'
import SetVersionTable from './components/detail/SetVersionTable.vue'
import SetVersionDetailDrawer from './components/detail/SetVersionDetailDrawer.vue'
import SetVersionExcelFileDrawer from './components/detail/SetVersionExcelFileDrawer.vue'
import SetVersionPriceLogDrawer from './components/detail/SetVersionPriceLogDrawer.vue'
import PriceEditPanel from './components/create/PriceEditPanel.vue'
import { usePriceSetRefData } from './composables/usePriceSetRefData'
import { usePriceSetModule } from './composables/usePriceSetModule'
@@ -102,6 +108,8 @@ const detailVersion = ref(null)
const versionExcelDrawerRef = ref()
const editVisible = ref(false)
const editPsvId = ref(undefined)
const priceLogVisible = ref(false)
const priceLogVersion = ref(null)
/** 已加载的套餐ID;页面被 keep-alive 缓存复用时据此判断是否需要重新加载 */
const loadedId = ref(null)
/** 从详情页跳到新建/编辑套餐版本页后,返回时套餐版本数据可能已变化,需强制刷新 */
@@ -115,6 +123,8 @@ function resetTransientState() {
detailVersion.value = null
editVisible.value = false
editPsvId.value = undefined
priceLogVisible.value = false
priceLogVersion.value = null
activateVisible.value = false
activateSubmitting.value = false
activateMode.value = 'activate'
@@ -134,6 +144,13 @@ function onViewImportAttach(row) {
versionExcelDrawerRef.value?.open?.(row)
}
function onViewPriceLog(row) {
if (!row || row.psvId == null) return
priceLogVersion.value = row
priceLogVisible.value = true
}
function load() {
const id = route.query.priceSetId
if (!id) return