修改套餐价格
This commit is contained in:
+70
-1
@@ -349,7 +349,8 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
}
|
||||
List<BmsRegion> euRegions = loadEuRegions(regionByNameCn);
|
||||
Map<String, PrcZone> zoneByCountryAndCode = loadZoneByCountryAndCode();
|
||||
// Pre-check: same product cannot have duplicate country + weight band in Excel
|
||||
// Pre-check: any price sheet with duplicate country+weight fails the whole import.
|
||||
assertAllPriceSheetsNoDupCountryWeight(workbook, regionByNameCn, euRegions, zoneByCountryAndCode);
|
||||
assertImportProductsNoDupCountryWeight(workbook, products, regionByNameCn, euRegions,
|
||||
zoneByCountryAndCode);
|
||||
|
||||
@@ -632,6 +633,8 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
Map<String, BmsRegion> regionByNameCn = loadRegionByNameCn();
|
||||
List<BmsRegion> euRegions = loadEuRegions(regionByNameCn);
|
||||
Map<String, PrcZone> zoneByCountryAndCode = loadZoneByCountryAndCode();
|
||||
// Fail whole import when any price sheet has duplicate country+weight (incl. unmatched sheets).
|
||||
assertAllPriceSheetsNoDupCountryWeight(workbook, regionByNameCn, euRegions, zoneByCountryAndCode);
|
||||
Set<String> unmatchedCountries = new HashSet<>();
|
||||
Map<String, List<BmsProductCountry>> out = new LinkedHashMap<>();
|
||||
for (BmsProduct product : products)
|
||||
@@ -1166,6 +1169,12 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
{
|
||||
score = 80;
|
||||
}
|
||||
else if (StringUtils.isNotEmpty(nameKey) && sheetNameBase(nameKey).equals(sheetNameBase(key))
|
||||
&& sheetNameBase(nameKey).length() >= 2)
|
||||
{
|
||||
// e.g. catalog name suffix differs from sheet (tax/cargo label)
|
||||
score = 70;
|
||||
}
|
||||
else if (StringUtils.isNotEmpty(codeKey) && key.contains(codeKey))
|
||||
{
|
||||
score = 60;
|
||||
@@ -1192,6 +1201,21 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Base name before last '-' (catalog/sheet suffix may differ). */
|
||||
private static String sheetNameBase(String normalizedKey)
|
||||
{
|
||||
if (StringUtils.isEmpty(normalizedKey))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
int idx = normalizedKey.lastIndexOf('-');
|
||||
if (idx <= 0)
|
||||
{
|
||||
return normalizedKey;
|
||||
}
|
||||
return normalizedKey.substring(0, idx);
|
||||
}
|
||||
|
||||
private List<BmsRegion> loadEuRegions(Map<String, BmsRegion> regionByNameCn)
|
||||
{
|
||||
List<BmsRegion> list = new ArrayList<>();
|
||||
@@ -2119,6 +2143,51 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan every price sheet (not only catalog-matched ones).
|
||||
* Duplicate country+weight on any sheet fails the whole import
|
||||
* (e.g. same country twice for different last-mile carriers).
|
||||
*/
|
||||
private void assertAllPriceSheetsNoDupCountryWeight(Workbook workbook,
|
||||
Map<String, BmsRegion> regionByNameCn, List<BmsRegion> euRegions,
|
||||
Map<String, PrcZone> zoneByCountryAndCode)
|
||||
{
|
||||
if (workbook == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
List<String> tips = new ArrayList<String>();
|
||||
Set<String> unmatchedCountries = new HashSet<String>();
|
||||
for (int i = 0; i < workbook.getNumberOfSheets(); i++)
|
||||
{
|
||||
String sheetName = workbook.getSheetName(i);
|
||||
if (shouldSkipCountryPriceSheet(sheetName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Sheet sheet = workbook.getSheetAt(i);
|
||||
if (sheet == null || findHeaderCell(sheet, "\u56fd\u5bb6") == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
BmsProduct stub = new BmsProduct();
|
||||
stub.setProductCode(sheetName);
|
||||
stub.setProductName(sheetName);
|
||||
List<BmsProductCountry> rows = parseProductSheet(sheet, stub,
|
||||
regionByNameCn, euRegions, zoneByCountryAndCode, unmatchedCountries);
|
||||
String tip = buildDuplicateCountryWeightTip(stub, rows);
|
||||
if (StringUtils.isNotEmpty(tip))
|
||||
{
|
||||
tips.add(tip);
|
||||
}
|
||||
}
|
||||
if (!tips.isEmpty())
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.product.import.dupCountry",
|
||||
String.join("<br/>", tips)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Workbook pre-check for full import: reject before any write when a product sheet
|
||||
* has duplicate country + weight band (same zone) rows.
|
||||
|
||||
+74
-2
@@ -3018,6 +3018,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
/**
|
||||
* Draft only: when priceBuildMode changes (PULL/IMPORT), delete this version's
|
||||
* product / detail / SALES surcharge rel rows. Same mode or non-draft: no-op.
|
||||
* IMPORT: after wipe, copy products+details from latest prior version (if any).
|
||||
*/
|
||||
private void clearDraftVersionChildrenIfBuildModeChanged(PrcPriceSetVersion db, PrcPriceSetVersion incoming)
|
||||
{
|
||||
@@ -3043,9 +3044,80 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
deleteSalesRelsByPsvId(psvId);
|
||||
detailMapper.deletePrcPriceSetDetailByPsvId(psvId);
|
||||
setProductMapper.deleteByPsvId(psvId);
|
||||
if ("IMPORT".equalsIgnoreCase(newMode))
|
||||
{
|
||||
copyLatestPriorVersionChildren(db.getPriceSetId(), psvId);
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer same effective_time, else draft, else first version of the set. */
|
||||
/**
|
||||
* Copy products + details from latest prior version into the draft (Excel build-price base).
|
||||
* Prefer EFFECT, then PENDING, then highest versionSeq.
|
||||
*/
|
||||
private void copyLatestPriorVersionChildren(Long priceSetId, Long targetPsvId)
|
||||
{
|
||||
Long srcPsvId = findLatestPriorPsvId(priceSetId, targetPsvId);
|
||||
if (srcPsvId == null || targetPsvId == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
setProductMapper.copyFromVersion(srcPsvId, targetPsvId);
|
||||
int detailRows = detailMapper.copyDetailsFromVersion(srcPsvId, targetPsvId);
|
||||
if (detailRows > 0)
|
||||
{
|
||||
setProductMapper.bindDetailPspByPsvId(targetPsvId);
|
||||
}
|
||||
}
|
||||
|
||||
private Long findLatestPriorPsvId(Long priceSetId, Long excludePsvId)
|
||||
{
|
||||
if (priceSetId == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
PrcPriceSetVersion q = new PrcPriceSetVersion();
|
||||
q.setPriceSetId(priceSetId);
|
||||
List<PrcPriceSetVersion> list = versionMapper.selectPrcPriceSetVersionList(q);
|
||||
if (list == null || list.isEmpty())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
PrcPriceSetVersion effect = null;
|
||||
PrcPriceSetVersion pending = null;
|
||||
PrcPriceSetVersion best = null;
|
||||
for (PrcPriceSetVersion v : list)
|
||||
{
|
||||
if (v == null || v.getPsvId() == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (excludePsvId != null && excludePsvId.equals(v.getPsvId()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (VER_EFFECT.equals(v.getStatus()) && effect == null)
|
||||
{
|
||||
effect = v;
|
||||
}
|
||||
else if (VER_PENDING.equals(v.getStatus()) && pending == null)
|
||||
{
|
||||
pending = v;
|
||||
}
|
||||
if (best == null || (v.getVersionSeq() != null && best.getVersionSeq() != null
|
||||
&& v.getVersionSeq().intValue() > best.getVersionSeq().intValue())
|
||||
|| (best.getVersionSeq() == null && v.getVersionSeq() != null))
|
||||
{
|
||||
best = v;
|
||||
}
|
||||
}
|
||||
PrcPriceSetVersion pick = effect != null ? effect : (pending != null ? pending : best);
|
||||
return pick == null ? null : pick.getPsvId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer same effective_time, else an existing draft.
|
||||
* Never fall back to EFFECT/EXPIRED — that would overwrite a live version when creating a new one.
|
||||
*/
|
||||
private PrcPriceSetVersion findReusableVersion(Long priceSetId, Date effectiveTime)
|
||||
{
|
||||
PrcPriceSetVersion q = new PrcPriceSetVersion();
|
||||
@@ -3072,7 +3144,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
return v;
|
||||
}
|
||||
}
|
||||
return list.get(0);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -151,6 +151,20 @@ function reloadSavedProducts() {
|
||||
})
|
||||
}
|
||||
|
||||
async function reloadProductsAfterImportModeSave() {
|
||||
const id = props.priceSetId
|
||||
if (!id) return
|
||||
const res = await getPriceSet(id)
|
||||
const data = res.data || {}
|
||||
const versions = data.versions || []
|
||||
const qPsv = props.psvId || version.psvId
|
||||
const byQuery = qPsv != null ? versions.find(v => String(v.psvId) === String(qPsv)) : null
|
||||
const current = byQuery || data.version || versions[0] || {}
|
||||
products.value = (current.products || []).map(p => ({ ...p }))
|
||||
savedProductsSnapshot.value = products.value.map(p => ({ ...p }))
|
||||
details.value = []
|
||||
}
|
||||
|
||||
async function finish() {
|
||||
if (!props.priceSetId) {
|
||||
close()
|
||||
@@ -178,12 +192,17 @@ async function finish() {
|
||||
}
|
||||
await savePriceSetAsync(payload)
|
||||
savedPriceBuildMode.value = version.priceBuildMode
|
||||
savedProductsSnapshot.value = (products.value || []).map(p => ({ ...p }))
|
||||
await nextTick()
|
||||
if (version.priceBuildMode === 'PULL' && detailsRef.value && detailsRef.value.pullAllFromLibrary) {
|
||||
await detailsRef.value.pullAllFromLibrary({ silent: true })
|
||||
savedProductsSnapshot.value = (products.value || []).map(p => ({ ...p }))
|
||||
proxy.$modal.msgSuccess("建价方式已保存并清空旧明细,已重新拉取产品列表")
|
||||
} else if (version.priceBuildMode === 'IMPORT') {
|
||||
// Backend copies latest prior version products/details into this draft
|
||||
await reloadProductsAfterImportModeSave()
|
||||
proxy.$modal.msgSuccess("建价方式已保存,已带出上一版产品,可导入 Excel 按产品+国家覆盖或新增")
|
||||
} else {
|
||||
savedProductsSnapshot.value = (products.value || []).map(p => ({ ...p }))
|
||||
proxy.$modal.msgSuccess("建价方式已保存并清空旧明细,可导入 Excel 建价")
|
||||
}
|
||||
close()
|
||||
|
||||
@@ -203,6 +203,7 @@ import {
|
||||
isFixedType,
|
||||
isImportBuildMode,
|
||||
isPullBuildMode,
|
||||
pickLatestPriorVersion,
|
||||
setTypeLabel,
|
||||
surchargeAppliesToRow,
|
||||
toDetailPayload
|
||||
@@ -306,8 +307,8 @@ const importDesc = computed(() => {
|
||||
}
|
||||
return [
|
||||
'适用威狮B2C小包价表(含「产品目录」)。',
|
||||
'按产品+国家覆盖:Excel 中出现的产品+国家会替换已有同范围明细,其它产品/国家保留。',
|
||||
'仅固定价套餐「导入 Excel 建价」可用;不影响导入修改价格/拉取产品库。'
|
||||
'先带出上一版全部产品;导入按「产品+国家」匹配:命中则覆盖该国家明细,未命中则新增;其它产品/国家保留。',
|
||||
'同一产品下重复国家+重量段将整单失败。仅固定价套餐「导入 Excel 建价」可用。'
|
||||
]
|
||||
})
|
||||
|
||||
@@ -324,7 +325,7 @@ const showImportAction = computed(() => hasBuildMode.value && isFixedType(props.
|
||||
const emptyTip = computed(() => {
|
||||
if (props.livePersist) return '暂无产品'
|
||||
if (isPullBuildMode(props.priceBuildMode)) return '暂无产品,请先返回上一步重新拉取或等待拉取完成'
|
||||
if (isImportBuildMode(props.priceBuildMode)) return '暂无产品,请点击右上角导入 Excel 建价'
|
||||
if (isImportBuildMode(props.priceBuildMode)) return '暂无上一版产品,请点击右上角导入 Excel 建价(按产品+国家覆盖或新增)'
|
||||
return '暂无产品,请先选择创建价格方式'
|
||||
})
|
||||
|
||||
@@ -373,6 +374,41 @@ function clearLocalDraft() {
|
||||
editGroup.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Excel build-price: preload products + details from latest prior version
|
||||
* so import can upsert by product+country.
|
||||
*/
|
||||
async function loadLatestPriorVersionDraft() {
|
||||
if (!props.priceSetId) return false
|
||||
pulling.value = true
|
||||
try {
|
||||
const res = await getPriceSet(props.priceSetId)
|
||||
const data = res.data || {}
|
||||
const versions = data.versions || []
|
||||
const prior = pickLatestPriorVersion(versions, props.psvId || resolvedPsvId.value)
|
||||
if (!prior || prior.psvId == null) return false
|
||||
const products = (prior.products || [])
|
||||
.filter(p => p && p.productCode)
|
||||
.map(p => {
|
||||
const row = { ...p }
|
||||
delete row.pspId
|
||||
delete row.psvId
|
||||
delete row.priceSetId
|
||||
return row
|
||||
})
|
||||
if (!products.length) return false
|
||||
const chunks = await Promise.all(products.map(p =>
|
||||
listSetDetailByProduct(prior.psvId, p.productCode)
|
||||
.then(r => r.data || [])
|
||||
.catch(() => [])
|
||||
))
|
||||
applyLocalDraft(products, chunks.flat())
|
||||
return true
|
||||
} finally {
|
||||
pulling.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadProductDetails(productCode, force) {
|
||||
if (!productCode) return []
|
||||
if (!force && detailByProduct[productCode]) return detailByProduct[productCode]
|
||||
@@ -987,7 +1023,7 @@ async function removeProduct(code) {
|
||||
proxy.$modal.msgSuccess('已移除产品明细')
|
||||
}
|
||||
|
||||
defineExpose({ pullAllFromLibrary, clearLocalDraft })
|
||||
defineExpose({ pullAllFromLibrary, clearLocalDraft, loadLatestPriorVersionDraft })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -7,142 +7,129 @@
|
||||
@close="close"
|
||||
>
|
||||
<div class="drawer-body" v-loading="loading">
|
||||
<div class="toolbar">
|
||||
<el-select
|
||||
v-model="query.productKeyword"
|
||||
filterable
|
||||
clearable
|
||||
:placeholder="phProduct"
|
||||
:loading="optionsLoading"
|
||||
style="width: 200px"
|
||||
>
|
||||
<el-option v-for="p in optionProducts" :key="p.code" :label="p.label" :value="p.code">
|
||||
<span class="opt-name">{{ p.name }}</span>
|
||||
<span class="opt-code">{{ p.code }}</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
<el-select
|
||||
v-if="isFixed"
|
||||
v-model="query.countryKeyword"
|
||||
filterable
|
||||
clearable
|
||||
:placeholder="phCountry"
|
||||
:loading="optionsLoading"
|
||||
style="width: 160px"
|
||||
>
|
||||
<el-option v-for="c in optionCountries" :key="c.key" :label="c.label" :value="c.value">
|
||||
<span class="opt-name">{{ c.name }}</span>
|
||||
<span class="opt-code">{{ c.code }}</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
<el-select
|
||||
v-if="isFixed"
|
||||
v-model="query.zoneKeyword"
|
||||
filterable
|
||||
clearable
|
||||
:placeholder="phZone"
|
||||
:loading="optionsLoading"
|
||||
style="width: 160px"
|
||||
>
|
||||
<el-option v-for="z in optionZones" :key="z.key" :label="z.label" :value="z.value">
|
||||
<span class="opt-name">{{ z.name }}</span>
|
||||
<span class="opt-code">{{ z.code }}</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
<el-button type="primary" @click="search">查询</el-button>
|
||||
<el-button @click="reset">重置</el-button>
|
||||
<div v-show="!detailOpen" class="product-pane">
|
||||
<div class="toolbar">
|
||||
<el-input v-model="productKeyword" clearable :placeholder="phSearch" style="width: 220px" />
|
||||
<el-button type="primary" @click="noop">查询</el-button>
|
||||
<el-button @click="productKeyword = ''">重置</el-button>
|
||||
</div>
|
||||
<el-alert type="info" :closable="false" show-icon class="mb12" :title="hint" />
|
||||
<el-empty v-if="!filteredProducts.length" :description="emptyProduct" :image-size="72" />
|
||||
<el-table v-else :data="filteredProducts" border height="calc(100vh - 280px)" @row-click="openProductDetail">
|
||||
<el-table-column :label="colCode" min-width="100">
|
||||
<template #default="{ row }"><b>{{ row.productCode }}</b></template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colName" prop="productName" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column :label="colLevel" prop="serviceLevel" width="90" align="center" />
|
||||
<el-table-column :label="colCargo" min-width="140" align="center" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.cargoAttr" type="primary" size="small" effect="plain">{{ row.cargoAttr }}</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colRegion" prop="destRegion" width="100" align="center" />
|
||||
<el-table-column :label="colAction" width="110" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click.stop="openProductDetail(row)">查看明细</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="product-count">{{ productCountText }}</div>
|
||||
</div>
|
||||
|
||||
<el-table :data="rows" border height="calc(100vh - 220px)" empty-text="暂无明细">
|
||||
<el-table-column :label="colProduct" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div class="cell-product">
|
||||
<span class="p-code">{{ row.productCode }}</span>
|
||||
<span class="p-name">{{ row.productName || '' }}</span>
|
||||
</div>
|
||||
<div v-show="detailOpen" class="detail-pane">
|
||||
<div class="toolbar">
|
||||
<el-button link type="primary" @click="backToProducts">
|
||||
<el-icon><arrow-left /></el-icon>
|
||||
返回产品
|
||||
</el-button>
|
||||
<span class="detail-title">{{ detailTitleText }}</span>
|
||||
<el-input
|
||||
v-if="isFixed"
|
||||
v-model="detailKeyword"
|
||||
clearable
|
||||
:placeholder="phDetailSearch"
|
||||
style="width: 200px; margin-left: auto"
|
||||
/>
|
||||
</div>
|
||||
<el-table
|
||||
v-loading="detailLoading"
|
||||
:data="displayDetailRows"
|
||||
border
|
||||
height="calc(100vh - 260px)"
|
||||
:empty-text="emptyDetail"
|
||||
:span-method="spanMethod"
|
||||
>
|
||||
<template v-if="isFixed">
|
||||
<el-table-column :label="colCountry" min-width="100">
|
||||
<template #default="{ row }">{{ row.countryName || row.countryCode || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colZone" min-width="90">
|
||||
<template #default="{ row }">{{ row.zoneName || row.zoneCode || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colWeight" prop="weightLabel" min-width="120" />
|
||||
<el-table-column :label="colFv" min-width="130" align="right">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip placement="top" :disabled="!hasBaseFreight(row)">
|
||||
<template #content>
|
||||
<span>产品基础运费:{{ formatBase(row.baseFreightPrice, 4) }} RMB/KG</span>
|
||||
</template>
|
||||
<span class="price-tip">
|
||||
<template v-if="row.fvValue == null">
|
||||
<span class="unit">未覆盖</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="hl">{{ row.fvValue }}</span>
|
||||
<span class="unit"> RMB/KG</span>
|
||||
</template>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colReg" min-width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip placement="top" :disabled="!hasBaseReg(row)">
|
||||
<template #content>
|
||||
<span>产品基础挂号费:{{ formatBase(row.baseRegistrationFee, 2) }} RMB/票</span>
|
||||
</template>
|
||||
<span class="price-tip">
|
||||
<span class="reg">{{ row.fvRegistrationFee }}</span>
|
||||
<span class="unit"> RMB/票</span>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<template v-if="isFixed">
|
||||
<el-table-column :label="colCountry" min-width="120">
|
||||
<template #default="{ row }">{{ row.countryName || row.countryCode || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colZone" min-width="110">
|
||||
<template #default="{ row }">{{ row.zoneName || row.zoneCode || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colWeight" prop="weightLabel" min-width="120" />
|
||||
<el-table-column :label="colFv" min-width="130" align="right">
|
||||
<el-table-column v-else :label="colDisc" min-width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip placement="top" :disabled="!hasBaseFreight(row)">
|
||||
<template #content>
|
||||
<span>产品基础运费:{{ formatBase(row.baseFreightPrice, 4) }} RMB/KG</span>
|
||||
</template>
|
||||
<span class="price-tip">
|
||||
<template v-if="row.fvValue == null">
|
||||
<span class="unit">未覆盖</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="hl">{{ row.fvValue }}</span>
|
||||
<span class="unit"> RMB/KG</span>
|
||||
</template>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
<el-tag v-if="row.discountValue == null" size="small" type="info" effect="plain">默认折扣</el-tag>
|
||||
<span v-else class="pct">{{ row.discountValue }}%</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colReg" min-width="120" align="right">
|
||||
<el-table-column :label="colSc" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip placement="top" :disabled="!hasBaseReg(row)">
|
||||
<template #content>
|
||||
<span>产品基础挂号费:{{ formatBase(row.baseRegistrationFee, 2) }} RMB/票</span>
|
||||
</template>
|
||||
<span class="price-tip">
|
||||
<span class="reg">{{ row.fvRegistrationFee }}</span>
|
||||
<span class="unit"> RMB/票</span>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
<surcharge-tags :list="row.surcharges || []" :empty-text="noneSc" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
|
||||
<el-table-column v-else :label="colDisc" min-width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.discountValue == null" size="small" type="info" effect="plain">默认折扣</el-tag>
|
||||
<span v-else class="pct">{{ row.discountValue }}%</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="colSc" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<surcharge-tags :list="row.surcharges || []" :empty-text="noneSc" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="colRound" min-width="100" align="center">
|
||||
<template #default="{ row }">{{ roundModeLabel(row.roundMode) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pager">
|
||||
<pagination
|
||||
v-show="total > 0"
|
||||
:total="total"
|
||||
v-model:page="query.pageNum"
|
||||
v-model:limit="query.pageSize"
|
||||
@pagination="load"
|
||||
/>
|
||||
<el-table-column :label="colRound" min-width="100" align="center">
|
||||
<template #default="{ row }">{{ roundModeLabel(row.roundMode) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button v-if="detailOpen" @click="backToProducts">返回产品</el-button>
|
||||
<el-button @click="close">关闭</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup name="SetVersionDetailDrawer">
|
||||
import { ArrowLeft } from '@element-plus/icons-vue'
|
||||
import SurchargeTags from '../SurchargeTags.vue'
|
||||
import { isFixedType } from '../../utils/priceSet'
|
||||
import { listSetDetail, listSetDetailOptions } from '@/api/price/set'
|
||||
import { listSetDetailByProduct, listSetDetailOptions } from '@/api/price/set'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
@@ -154,61 +141,25 @@ const emit = defineEmits(['update:modelValue'])
|
||||
const { bms_round_mode } = useDict('bms_round_mode')
|
||||
|
||||
const loading = ref(false)
|
||||
const rows = ref([])
|
||||
const total = ref(0)
|
||||
const query = reactive({
|
||||
pageNum: 1,
|
||||
pageSize: 20,
|
||||
productKeyword: '',
|
||||
countryKeyword: '',
|
||||
zoneKeyword: ''
|
||||
})
|
||||
const productKeyword = ref('')
|
||||
const products = ref([])
|
||||
const detailOpen = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const detailKeyword = ref('')
|
||||
const detailRows = ref([])
|
||||
const activeProduct = ref(null)
|
||||
|
||||
// 下拉选项:当前版本内去重后的 产品/国家/分区
|
||||
const optionsLoading = ref(false)
|
||||
const optionProducts = ref([])
|
||||
const optionCountries = ref([])
|
||||
const optionZones = ref([])
|
||||
|
||||
function loadOptions() {
|
||||
const psvId = props.version && props.version.psvId
|
||||
if (!psvId) {
|
||||
optionProducts.value = []
|
||||
optionCountries.value = []
|
||||
optionZones.value = []
|
||||
return
|
||||
}
|
||||
optionsLoading.value = true
|
||||
listSetDetailOptions(psvId).then(res => {
|
||||
const data = res.data || {}
|
||||
optionProducts.value = (data.products || []).map(p => ({
|
||||
code: p.code,
|
||||
name: p.name || p.code,
|
||||
label: (p.name && p.name !== p.code) ? `${p.code} ${p.name}` : p.code
|
||||
}))
|
||||
optionCountries.value = (data.countries || []).map(c => ({
|
||||
key: c.code || c.name,
|
||||
code: c.code || '-',
|
||||
name: c.name || c.code,
|
||||
value: c.code || c.name, // 有代码按代码搜,否则按名称搜(后端 code/name 双 LIKE)
|
||||
label: (c.name && c.code) ? `${c.name} ${c.code}` : (c.name || c.code)
|
||||
}))
|
||||
optionZones.value = (data.zones || []).map(z => ({
|
||||
key: z.code || z.name,
|
||||
code: z.code || '-',
|
||||
name: z.name || z.code,
|
||||
value: z.code || z.name,
|
||||
label: (z.name && z.code) ? `${z.name} ${z.code}` : (z.name || z.code)
|
||||
}))
|
||||
}).finally(() => {
|
||||
optionsLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const phProduct = '产品'
|
||||
const phCountry = '国家'
|
||||
const phZone = '分区'
|
||||
const colProduct = '产品'
|
||||
const phSearch = '产品代码/名称'
|
||||
const phDetailSearch = '搜索国家/分区'
|
||||
const hint = '点击产品行查看国家/重量段明细(仅查看)'
|
||||
const emptyProduct = '暂无产品'
|
||||
const emptyDetail = '暂无明细'
|
||||
const colCode = '产品代码'
|
||||
const colName = '产品名称'
|
||||
const colLevel = '时效等级'
|
||||
const colCargo = '走货属性'
|
||||
const colRegion = '目的区域'
|
||||
const colAction = '操作'
|
||||
const colCountry = '国家'
|
||||
const colZone = '分区'
|
||||
const colWeight = '重量段'
|
||||
@@ -224,6 +175,92 @@ const title = computed(() => {
|
||||
const v = props.version || {}
|
||||
return (v.versionNo || '-') + ' · 版本明细'
|
||||
})
|
||||
const detailTitleText = computed(() => {
|
||||
const p = activeProduct.value || {}
|
||||
return (p.productCode || '') + ' · ' + (p.productName || '') + ' · 国家服务价格'
|
||||
})
|
||||
|
||||
const filteredProducts = computed(() => {
|
||||
const kw = (productKeyword.value || '').trim().toLowerCase()
|
||||
return (products.value || []).filter(p => {
|
||||
if (!p || !p.productCode) return false
|
||||
if (!kw) return true
|
||||
const blob = [p.productCode, p.productName].filter(Boolean).join(' ').toLowerCase()
|
||||
return blob.includes(kw)
|
||||
})
|
||||
})
|
||||
|
||||
/** Footer: "共 N 个产品" (show filtered vs total when searching). */
|
||||
const productCountText = computed(() => {
|
||||
const n = filteredProducts.value.length
|
||||
const total = (products.value || []).length
|
||||
if (n === total) {
|
||||
return '\u5171 ' + n + ' \u4e2a\u4ea7\u54c1'
|
||||
}
|
||||
return '\u5171 ' + n + ' \u4e2a\u4ea7\u54c1\uff08\u5168\u90e8 ' + total + ' \u4e2a\uff09'
|
||||
})
|
||||
|
||||
function countryKey(row) {
|
||||
return String((row && (row.countryCode || row.countryName)) || '').toUpperCase()
|
||||
}
|
||||
|
||||
function zoneKey(row) {
|
||||
return String((row && (row.zoneCode || row.zoneName)) || '')
|
||||
}
|
||||
|
||||
const displayDetailRows = computed(() => {
|
||||
const kw = (detailKeyword.value || '').trim().toLowerCase()
|
||||
let list = [...(detailRows.value || [])]
|
||||
if (kw) {
|
||||
list = list.filter(r => {
|
||||
if (!r) return false
|
||||
const blob = [r.countryName, r.countryCode, r.zoneName, r.zoneCode, r.weightLabel]
|
||||
.filter(Boolean).join(' ').toLowerCase()
|
||||
return blob.includes(kw)
|
||||
})
|
||||
}
|
||||
list.sort((a, b) => {
|
||||
const ca = countryKey(a)
|
||||
const cb = countryKey(b)
|
||||
if (ca !== cb) return ca.localeCompare(cb)
|
||||
const za = zoneKey(a)
|
||||
const zb = zoneKey(b)
|
||||
if (za !== zb) return za.localeCompare(zb)
|
||||
return String((a && a.weightLabel) || '').localeCompare(String((b && b.weightLabel) || ''))
|
||||
})
|
||||
return list
|
||||
})
|
||||
|
||||
function buildSpans(rows, keyOf) {
|
||||
const spans = new Array(rows.length).fill(1)
|
||||
let i = 0
|
||||
while (i < rows.length) {
|
||||
let j = i + 1
|
||||
const key = keyOf(rows[i])
|
||||
while (j < rows.length && keyOf(rows[j]) === key) j++
|
||||
spans[i] = j - i
|
||||
for (let k = i + 1; k < j; k++) spans[k] = 0
|
||||
i = j
|
||||
}
|
||||
return spans
|
||||
}
|
||||
|
||||
const countrySpans = computed(() => buildSpans(displayDetailRows.value, countryKey))
|
||||
const zoneSpans = computed(() => buildSpans(displayDetailRows.value, r => countryKey(r) + '|' + zoneKey(r)))
|
||||
|
||||
function spanOf(spans, rowIndex) {
|
||||
const rowspan = spans[rowIndex] || 0
|
||||
return rowspan > 0 ? { rowspan, colspan: 1 } : { rowspan: 0, colspan: 0 }
|
||||
}
|
||||
|
||||
function spanMethod({ columnIndex, rowIndex }) {
|
||||
if (!isFixed.value) return { rowspan: 1, colspan: 1 }
|
||||
if (columnIndex === 0) return spanOf(countrySpans.value, rowIndex)
|
||||
if (columnIndex === 1) return spanOf(zoneSpans.value, rowIndex)
|
||||
return { rowspan: 1, colspan: 1 }
|
||||
}
|
||||
|
||||
function noop() {}
|
||||
|
||||
function hasBaseFreight(row) {
|
||||
return row && row.baseFreightPrice != null && row.baseFreightPrice !== ''
|
||||
@@ -237,7 +274,6 @@ function formatBase(v, precision) {
|
||||
if (Number.isNaN(n)) return String(v)
|
||||
return n.toFixed(precision)
|
||||
}
|
||||
|
||||
function roundModeLabel(mode) {
|
||||
const hit = (bms_round_mode.value || []).find(d => d.value === mode)
|
||||
return hit ? hit.label : (mode || '四舍五入')
|
||||
@@ -247,52 +283,73 @@ function close() {
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
|
||||
function search() {
|
||||
query.pageNum = 1
|
||||
load()
|
||||
function backToProducts() {
|
||||
detailOpen.value = false
|
||||
activeProduct.value = null
|
||||
detailRows.value = []
|
||||
detailKeyword.value = ''
|
||||
}
|
||||
|
||||
function reset() {
|
||||
query.productKeyword = ''
|
||||
query.countryKeyword = ''
|
||||
query.zoneKeyword = ''
|
||||
query.pageNum = 1
|
||||
load()
|
||||
function normalizeProducts(list) {
|
||||
return (list || [])
|
||||
.filter(p => p && (p.productCode || p.code))
|
||||
.map(p => ({
|
||||
productCode: p.productCode || p.code,
|
||||
productName: p.productName || p.name || p.productCode || p.code,
|
||||
serviceLevel: p.serviceLevel,
|
||||
cargoAttr: p.cargoAttr,
|
||||
destRegion: p.destRegion,
|
||||
taxMode: p.taxMode
|
||||
}))
|
||||
}
|
||||
|
||||
function load() {
|
||||
const psvId = props.version && props.version.psvId
|
||||
async function loadProducts() {
|
||||
const v = props.version || {}
|
||||
const psvId = v.psvId
|
||||
if (!psvId) {
|
||||
rows.value = []
|
||||
total.value = 0
|
||||
products.value = []
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
listSetDetail({
|
||||
psvId,
|
||||
productKeyword: query.productKeyword || undefined,
|
||||
countryKeyword: isFixed.value ? (query.countryKeyword || undefined) : undefined,
|
||||
zoneKeyword: isFixed.value ? (query.zoneKeyword || undefined) : undefined,
|
||||
pageNum: query.pageNum,
|
||||
pageSize: query.pageSize
|
||||
}).then(res => {
|
||||
rows.value = res.rows || []
|
||||
total.value = res.total || 0
|
||||
}).finally(() => {
|
||||
try {
|
||||
const fromVersion = normalizeProducts(v.products)
|
||||
if (fromVersion.length) {
|
||||
products.value = fromVersion
|
||||
return
|
||||
}
|
||||
const res = await listSetDetailOptions(psvId)
|
||||
const data = res.data || {}
|
||||
products.value = normalizeProducts(data.products || [])
|
||||
} finally {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function openProductDetail(row) {
|
||||
if (!row || !row.productCode) return
|
||||
const psvId = props.version && props.version.psvId
|
||||
if (!psvId) return
|
||||
activeProduct.value = row
|
||||
detailOpen.value = true
|
||||
detailKeyword.value = ''
|
||||
detailLoading.value = true
|
||||
try {
|
||||
const res = await listSetDetailByProduct(psvId, row.productCode)
|
||||
detailRows.value = res.data || []
|
||||
} catch (e) {
|
||||
detailRows.value = []
|
||||
} finally {
|
||||
detailLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.modelValue, props.version && props.version.psvId],
|
||||
([open]) => {
|
||||
if (open) {
|
||||
query.pageNum = 1
|
||||
query.productKeyword = ''
|
||||
query.countryKeyword = ''
|
||||
query.zoneKeyword = ''
|
||||
loadOptions()
|
||||
load()
|
||||
productKeyword.value = ''
|
||||
backToProducts()
|
||||
loadProducts()
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -312,29 +369,16 @@ watch(
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
.pager {
|
||||
margin-top: 12px;
|
||||
}
|
||||
.cell-product {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
}
|
||||
.p-code {
|
||||
font-family: Consolas, Monaco, monospace;
|
||||
font-size: 12px;
|
||||
.mb12 { margin-bottom: 12px; }
|
||||
.product-count {
|
||||
margin-top: 10px;
|
||||
text-align: right;
|
||||
color: #606266;
|
||||
background: #f4f4f5;
|
||||
border-radius: 3px;
|
||||
padding: 0 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.p-name { color: #303133; }
|
||||
.opt-name { margin-right: 8px; }
|
||||
.opt-code {
|
||||
float: right;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
font-family: Consolas, Monaco, monospace;
|
||||
.detail-title {
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
.hl { color: #1890ff; font-weight: 600; }
|
||||
.reg { color: #d48806; font-weight: 600; }
|
||||
@@ -347,4 +391,7 @@ watch(
|
||||
.price-tip:hover {
|
||||
border-bottom-color: #c0c4cc;
|
||||
}
|
||||
:deep(.el-table__row) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -69,13 +69,6 @@
|
||||
@click="emit('remove', row)"
|
||||
v-hasPermi="['price:set:edit']"
|
||||
>删除</el-button>
|
||||
<el-button
|
||||
v-if="String(row.status) === '1' && !hasDraft"
|
||||
link
|
||||
type="primary"
|
||||
@click="emit('copy-version', row)"
|
||||
v-hasPermi="['price:set:add']"
|
||||
>复制为新版本</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -91,7 +84,7 @@ const props = defineProps({
|
||||
versions: { type: Array, default: () => [] },
|
||||
modelValue: { type: [Number, String], default: undefined }
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue', 'copy-version', 'create-version', 'open-details', 'view-import-attach', 'activate', 'reschedule', 'edit', 'remove'])
|
||||
const emit = defineEmits(['update:modelValue', 'create-version', 'open-details', 'view-import-attach', 'activate', 'reschedule', 'edit', 'remove'])
|
||||
|
||||
const title = '套餐版本'
|
||||
const colNo = '版本号'
|
||||
|
||||
@@ -72,7 +72,7 @@ import {
|
||||
toCustomerPayload,
|
||||
toDetailPayload
|
||||
} from './utils/priceSet'
|
||||
import { getPriceSet, savePriceSetAsync } from '@/api/price/set'
|
||||
import { getPriceSet, savePriceSetAsync, createPriceSetVersion, deletePriceSetVersion } from '@/api/price/set'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const route = useRoute()
|
||||
@@ -202,12 +202,19 @@ async function finishEdit() {
|
||||
delete payload.details
|
||||
await runSaveWithProgress(payload)
|
||||
savedPriceBuildMode.value = form.version.priceBuildMode
|
||||
savedProductsSnapshot.value = (form.products || []).map(p => ({ ...p }))
|
||||
await nextTick()
|
||||
if (form.version.priceBuildMode === 'PULL' && detailsRef.value && detailsRef.value.pullAllFromLibrary) {
|
||||
await detailsRef.value.pullAllFromLibrary({ silent: true })
|
||||
savedProductsSnapshot.value = (form.products || []).map(p => ({ ...p }))
|
||||
proxy.$modal.msgSuccess("建价方式已保存")
|
||||
} else if (form.version.priceBuildMode === 'IMPORT') {
|
||||
// Backend copied prior version into this draft; refresh product list from current psv
|
||||
await refreshProductsFromCurrentVersion()
|
||||
proxy.$modal.msgSuccess("建价方式已保存,已带出上一版产品")
|
||||
} else {
|
||||
savedProductsSnapshot.value = (form.products || []).map(p => ({ ...p }))
|
||||
proxy.$modal.msgSuccess("建价方式已保存")
|
||||
}
|
||||
proxy.$modal.msgSuccess("建价方式已保存")
|
||||
} catch (e) {
|
||||
return
|
||||
} finally {
|
||||
@@ -243,6 +250,12 @@ async function finishEdit() {
|
||||
}
|
||||
creating.value = true
|
||||
try {
|
||||
if (newVersionMode.value && form.version.psvId == null && form.priceSetId) {
|
||||
const verRes = await createPriceSetVersion(form.priceSetId)
|
||||
if (verRes.data != null) {
|
||||
form.version.psvId = verRes.data
|
||||
}
|
||||
}
|
||||
const payload = buildPayload({ forceDetails: true })
|
||||
const priceSetId = await runSaveWithProgress(payload)
|
||||
form.priceSetId = priceSetId
|
||||
@@ -296,7 +309,7 @@ function buildPayload(opts = {}) {
|
||||
defaultDiscount: undefined,
|
||||
priceSetNote: basic.priceSetNote,
|
||||
version: {
|
||||
psvId: copyMode.value ? undefined : version.psvId,
|
||||
psvId: (copyMode.value || (newVersionMode.value && version.psvId == null)) ? undefined : version.psvId,
|
||||
versionNo: version.versionNo,
|
||||
priceBuildMode: version.priceBuildMode || undefined
|
||||
}
|
||||
@@ -323,7 +336,30 @@ function buildPayload(opts = {}) {
|
||||
return payload
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
/**
|
||||
* New-version flow creates an empty draft as soon as user clicks "新建价格版本".
|
||||
* If user cancels without finishing (no build mode / no details on server), remove that orphan draft.
|
||||
*/
|
||||
async function tryDeleteEmptyNewVersionDraft() {
|
||||
if (!newVersionMode.value || !form.priceSetId || form.version.psvId == null) return
|
||||
const psvId = form.version.psvId
|
||||
try {
|
||||
const res = await getPriceSet(form.priceSetId)
|
||||
const versions = (res.data || {}).versions || []
|
||||
const v = versions.find(x => x && String(x.psvId) === String(psvId))
|
||||
if (!v || String(v.status) !== '0') return
|
||||
const noMode = !v.priceBuildMode
|
||||
const noDetails = !(Number(v.detailCount) > 0) && !(v.details || []).length
|
||||
if (noMode && noDetails) {
|
||||
await deletePriceSetVersion(psvId)
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore cleanup errors; still navigate back
|
||||
}
|
||||
}
|
||||
|
||||
async function goBack() {
|
||||
await tryDeleteEmptyNewVersionDraft()
|
||||
const obj = (newVersionMode.value || copyMode.value) && form.priceSetId
|
||||
? { path: '/customer/setDetail', query: { priceSetId: form.priceSetId } }
|
||||
: { path: '/customer/set' }
|
||||
@@ -383,7 +419,20 @@ function onPriceBuildModeUpdate(mode) {
|
||||
}).catch(() => {})
|
||||
})
|
||||
} else if (mode === 'IMPORT') {
|
||||
proxy.$modal.msgSuccess("请导入 Excel 建价")
|
||||
nextTick(() => {
|
||||
const loader = detailsRef.value && detailsRef.value.loadLatestPriorVersionDraft
|
||||
if (!loader) {
|
||||
proxy.$modal.msgSuccess("请导入 Excel 建价")
|
||||
return
|
||||
}
|
||||
loader().then(ok => {
|
||||
proxy.$modal.msgSuccess(ok
|
||||
? "已带出上一版全部产品,导入 Excel 按产品+国家覆盖或新增"
|
||||
: "暂无上一版产品,请导入 Excel 建价")
|
||||
}).catch(() => {
|
||||
proxy.$modal.msgSuccess("请导入 Excel 建价")
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,6 +450,20 @@ function reloadSavedProductsForCreate() {
|
||||
})
|
||||
}
|
||||
|
||||
async function refreshProductsFromCurrentVersion() {
|
||||
if (!form.priceSetId) return
|
||||
const res = await getPriceSet(form.priceSetId)
|
||||
const data = res.data || {}
|
||||
const versions = data.versions || []
|
||||
const qPsv = form.version.psvId
|
||||
const byQuery = qPsv != null ? versions.find(v => String(v.psvId) === String(qPsv)) : null
|
||||
const current = byQuery || data.version || versions[0] || {}
|
||||
const list = (current.products || []).map(p => ({ ...p }))
|
||||
form.products.splice(0, form.products.length, ...list)
|
||||
form.details.splice(0, form.details.length)
|
||||
savedProductsSnapshot.value = list.map(p => ({ ...p }))
|
||||
}
|
||||
|
||||
function applySetData(data, asCopy, asNewVersion) {
|
||||
form.priceSetId = data.priceSetId
|
||||
Object.assign(form.basic, {
|
||||
@@ -426,12 +489,13 @@ function applySetData(data, asCopy, asNewVersion) {
|
||||
form.details = []
|
||||
form.products = []
|
||||
} else if (asNewVersion) {
|
||||
// Pre-created empty draft from detail page; same step2 UX as create package
|
||||
form.version.psvId = current.psvId
|
||||
form.version.versionNo = current.versionNo || nextVersionNo(versions)
|
||||
// Draft is created on finish only. Resume only when query has psvId (legacy empty draft).
|
||||
const resume = qPsv != null ? byQuery : null
|
||||
form.version.psvId = resume ? resume.psvId : undefined
|
||||
form.version.versionNo = (resume && resume.versionNo) || nextVersionNo(versions)
|
||||
form.version.effectiveTime = undefined
|
||||
form.version.expireTime = undefined
|
||||
form.version.status = current.status || '0'
|
||||
form.version.status = (resume && resume.status) || '0'
|
||||
form.version.priceBuildMode = undefined
|
||||
draftPefId.value = undefined
|
||||
savedPriceBuildMode.value = undefined
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
<set-version-table
|
||||
v-model="currentPsvId"
|
||||
:versions="set.versions || []"
|
||||
@copy-version="onCopyVersion"
|
||||
@create-version="onCreateVersion"
|
||||
@open-details="onOpenVersionDetails"
|
||||
@view-import-attach="onViewImportAttach"
|
||||
@@ -87,7 +86,7 @@ import SetVersionExcelFileDrawer from './components/detail/SetVersionExcelFileDr
|
||||
import PriceEditPanel from './components/create/PriceEditPanel.vue'
|
||||
import { usePriceSetRefData } from './composables/usePriceSetRefData'
|
||||
import {
|
||||
getPriceSet, disablePriceSet, enablePriceSet, activatePriceSetVersion, copyPriceSetVersion, createPriceSetVersion, deletePriceSetVersion
|
||||
getPriceSet, disablePriceSet, enablePriceSet, activatePriceSetVersion, deletePriceSetVersion
|
||||
} from '@/api/price/set'
|
||||
import { isDraftVersion, isPendingVersion } from './utils/priceSet'
|
||||
|
||||
@@ -379,33 +378,11 @@ function onCreateVersion() {
|
||||
proxy.$modal.msgWarning('已有草稿版本,请先启动或删除后再新建')
|
||||
return
|
||||
}
|
||||
createPriceSetVersion(priceSetId).then(res => {
|
||||
const newPsvId = res.data
|
||||
proxy.$modal.msgSuccess('已创建草稿版本,请继续编辑产品价格')
|
||||
router.push({
|
||||
path: '/customer/setCreate',
|
||||
query: { priceSetId, psvId: newPsvId, step: '2', newVersion: '1' }
|
||||
})
|
||||
}).catch(() => {})
|
||||
}
|
||||
function onCopyVersion(row) {
|
||||
if (!row || row.psvId == null) return
|
||||
const hasDraft = (set.value.versions || []).some(v => v && String(v.status) === '0')
|
||||
if (hasDraft) {
|
||||
proxy.$modal.msgWarning('已有草稿版本,请先启动或删除后再复制')
|
||||
return
|
||||
}
|
||||
proxy.$modal.confirm(
|
||||
'确认复制版本 ' + (row.versionNo || '') + '为新草稿?将新增版本并拷贝全部明细。'
|
||||
).then(() => copyPriceSetVersion(row.psvId)).then(res => {
|
||||
proxy.$modal.msgSuccess('已复制为新版本')
|
||||
const newPsvId = res.data
|
||||
load()
|
||||
if (newPsvId != null) {
|
||||
editPsvId.value = newPsvId
|
||||
editVisible.value = true
|
||||
}
|
||||
}).catch(() => {})
|
||||
// Do not insert draft until user clicks finish on the build-price page.
|
||||
router.push({
|
||||
path: '/customer/setCreate',
|
||||
query: { priceSetId, step: '2', newVersion: '1' }
|
||||
})
|
||||
}
|
||||
function onRemoveVersion(row) {
|
||||
if (!row || row.psvId == null) return
|
||||
|
||||
@@ -108,6 +108,24 @@ export function nextVersionNo(versions) {
|
||||
return 'V' + (max + 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Latest prior version for Excel build-price preload (exclude current draft).
|
||||
* Prefer EFFECT, then PENDING, then highest versionSeq.
|
||||
*/
|
||||
export function pickLatestPriorVersion(versions, excludePsvId) {
|
||||
const list = (versions || []).filter(v => {
|
||||
if (!v || v.psvId == null) return false
|
||||
if (excludePsvId != null && String(v.psvId) === String(excludePsvId)) return false
|
||||
return true
|
||||
})
|
||||
if (!list.length) return null
|
||||
const effect = list.find(v => String(v.status) === VER_STATUS.EFFECT)
|
||||
if (effect) return effect
|
||||
const pending = list.find(v => String(v.status) === VER_STATUS.PENDING)
|
||||
if (pending) return pending
|
||||
return list.slice().sort((a, b) => (Number(b.versionSeq) || 0) - (Number(a.versionSeq) || 0))[0]
|
||||
}
|
||||
|
||||
/** Package status label */
|
||||
export function setStatusLabel(set) {
|
||||
if (set && set.expired) return '已过期'
|
||||
|
||||
Reference in New Issue
Block a user