导入批量修改价格明细
This commit is contained in:
@@ -27,6 +27,16 @@
|
||||
<template #default="{ row }">{{ row.zoneName || row.zoneCode || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colWeight" prop="weightLabel" min-width="120" />
|
||||
<el-table-column :label="colBaseFv" width="130" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="price-text base-price">{{ formatNum(row._baseFreight, 4) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colBaseReg" width="130" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="price-text base-price">{{ formatNum(row._baseReg, 2) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colSetFv" width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="price-cell">
|
||||
@@ -111,11 +121,13 @@ const title = computed(() => {
|
||||
const name = props.productName || ''
|
||||
return code + ' · ' + name + ' · 国家服务价格'
|
||||
})
|
||||
const hint = '黄色行=已改价;点编辑图标后修改,失焦即刻写入数据库。单位:运费 RMB/KG,挂号费 RMB/票。'
|
||||
const hint = '黄色行=已改价(与产品基础价不同);点编辑图标后修改,失焦即刻写入数据库。单位:运费 RMB/KG,挂号费 RMB/票。'
|
||||
const phSearch = '搜索国家/分区'
|
||||
const colCountry = '国家'
|
||||
const colZone = '分区'
|
||||
const colWeight = '重量段'
|
||||
const colBaseFv = '产品基础运费'
|
||||
const colBaseReg = '产品基础挂号费'
|
||||
const colSetFv = '套餐运费'
|
||||
const colSetReg = '套餐挂号费'
|
||||
const colSurcharge = '附加费'
|
||||
@@ -289,7 +301,7 @@ watch(() => props.modelValue, v => {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: min(960px, 96vw);
|
||||
width: min(1180px, 98vw);
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
box-shadow: -4px 0 16px rgba(0, 0, 0, 0.12);
|
||||
@@ -333,6 +345,9 @@ watch(() => props.modelValue, v => {
|
||||
min-width: 64px;
|
||||
text-align: right;
|
||||
}
|
||||
.base-price {
|
||||
color: #909399;
|
||||
}
|
||||
.edit-ico {
|
||||
padding: 0 2px !important;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
v-if="loaded"
|
||||
:details="details"
|
||||
:price-set-id="priceSetId"
|
||||
:psv-id="version.psvId"
|
||||
:set-type="setType"
|
||||
:effective-time="version.effectiveTime"
|
||||
:version-no="version.versionNo"
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<el-option v-for="d in bms_tax_mode" :key="d.value" :label="d.label" :value="d.value" />
|
||||
</el-select>
|
||||
<el-button size="small" @click="noop">查询</el-button>
|
||||
<el-button size="small" type="primary" plain :disabled="!priceSetId || !isFixedType(setType)" @click="openImport">导入修改价格</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="filteredProducts" border empty-text="暂无产品价格明细">
|
||||
@@ -74,10 +75,36 @@
|
||||
@surcharge-change="onRowSurchargeChange"
|
||||
@pick-surcharge="openSurchargeFromDrawer"
|
||||
/>
|
||||
|
||||
<excel-import-dialog
|
||||
ref="importRef"
|
||||
title="导入修改套餐价格"
|
||||
width="560px"
|
||||
:show-update-support="false"
|
||||
:import-desc="importDesc"
|
||||
:submit-handler="runImportAsync"
|
||||
@success="onImportSuccess"
|
||||
/>
|
||||
|
||||
<el-dialog
|
||||
v-model="importProgressVisible"
|
||||
title="正在导入价格"
|
||||
width="420px"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
:show-close="false"
|
||||
append-to-body
|
||||
>
|
||||
<div class="import-progress">
|
||||
<el-progress :percentage="importProgressPercent" :status="importProgressStatus" />
|
||||
<div class="import-progress-msg">{{ importProgressMessage }}</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup name="StepDetails">
|
||||
import ExcelImportDialog from '@/components/ExcelImportDialog'
|
||||
import PriceEditDrawer from './PriceEditDrawer.vue'
|
||||
import SurchargePickerDialog from './SurchargePickerDialog.vue'
|
||||
import { useDetailCascade } from '../../composables/usePriceSetRefData'
|
||||
@@ -90,11 +117,12 @@ import {
|
||||
surchargeAppliesToRow,
|
||||
toDetailPayload
|
||||
} from '../../utils/priceSet'
|
||||
import { updateSetDetail, updateSetDetailSurchargeAsync, delSetDetail } from '@/api/price/set'
|
||||
import { getPriceSet, importSetDetailPricesAsync, updateSetDetail, updateSetDetailSurchargeAsync, delSetDetail } from '@/api/price/set'
|
||||
|
||||
const props = defineProps({
|
||||
details: { type: Array, required: true },
|
||||
priceSetId: { type: [Number, String], default: undefined },
|
||||
psvId: { type: [Number, String], default: undefined },
|
||||
setType: { type: String, default: 'FIXED' },
|
||||
effectiveTime: { type: String, default: undefined },
|
||||
versionNo: { type: String, default: '' },
|
||||
@@ -137,6 +165,16 @@ const discVisible = ref(false)
|
||||
const discTitle = ref('编辑价格')
|
||||
const discValue = ref(undefined)
|
||||
const discCode = ref('')
|
||||
const importRef = ref()
|
||||
const importProgressVisible = ref(false)
|
||||
const importProgressPercent = ref(0)
|
||||
const importProgressMessage = ref('')
|
||||
const importProgressStatus = ref(undefined)
|
||||
const importDesc = [
|
||||
'适用威狮B2C小包价表(含「产品目录」)。',
|
||||
'按产品+国家+分区(有则匹配)+重量段匹配已有套餐明细,仅更新运费/挂号费。',
|
||||
'仅固定价套餐可导入;未匹配行不会新增明细。'
|
||||
]
|
||||
|
||||
const typeHint = computed(() => '· 当前:' + setTypeLabel(props.setType))
|
||||
|
||||
@@ -175,6 +213,85 @@ const pulling = ref(false)
|
||||
|
||||
function noop() {}
|
||||
|
||||
function openImport() {
|
||||
if (!props.priceSetId) {
|
||||
proxy.$modal.msgWarning('缺少套餐ID')
|
||||
return
|
||||
}
|
||||
if (!isFixedType(props.setType)) {
|
||||
proxy.$modal.msgWarning('仅固定价套餐支持导入')
|
||||
return
|
||||
}
|
||||
importRef.value && importRef.value.open()
|
||||
}
|
||||
|
||||
|
||||
async function runImportAsync(uploadFile) {
|
||||
const raw = uploadFile && (uploadFile.raw || uploadFile)
|
||||
if (!raw) {
|
||||
throw new Error('请先选择文件')
|
||||
}
|
||||
importProgressVisible.value = true
|
||||
importProgressPercent.value = 0
|
||||
importProgressMessage.value = '上传并解析中…'
|
||||
importProgressStatus.value = undefined
|
||||
try {
|
||||
const { task } = await importSetDetailPricesAsync(
|
||||
raw,
|
||||
{ priceSetId: props.priceSetId, psvId: props.psvId },
|
||||
(t) => {
|
||||
importProgressPercent.value = Math.min(100, Number(t.percent) || 0)
|
||||
if (t.status === 'PENDING') importProgressMessage.value = '排队中…'
|
||||
else if (t.status === 'RUNNING') {
|
||||
const m = t.message || ''
|
||||
if (m === 'parsing excel') importProgressMessage.value = '正在解析价表…'
|
||||
else if (m === 'matching details') importProgressMessage.value = '正在匹配并更新价格…'
|
||||
else if (m === 'finishing') importProgressMessage.value = '收尾中…'
|
||||
else if (m === 'queued') importProgressMessage.value = '排队中…'
|
||||
else importProgressMessage.value = m || '正在导入…'
|
||||
} else if (t.status === 'SUCCESS') {
|
||||
importProgressMessage.value = t.message || '导入完成'
|
||||
importProgressStatus.value = 'success'
|
||||
} else if (t.status === 'FAIL') {
|
||||
importProgressMessage.value = t.error || t.message || '导入失败'
|
||||
importProgressStatus.value = 'exception'
|
||||
}
|
||||
}
|
||||
)
|
||||
importProgressPercent.value = 100
|
||||
importProgressStatus.value = 'success'
|
||||
return (task && task.message) ? task.message : '导入完成'
|
||||
} catch (e) {
|
||||
importProgressStatus.value = 'exception'
|
||||
const tip = (e && e.task && e.task.error) || (e && e.message) || '导入失败'
|
||||
importProgressMessage.value = tip
|
||||
await new Promise(resolve => setTimeout(resolve, 600))
|
||||
throw e
|
||||
} finally {
|
||||
importProgressVisible.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onImportSuccess() {
|
||||
if (!props.priceSetId) return
|
||||
try {
|
||||
const res = await getPriceSet(props.priceSetId)
|
||||
const data = res.data || {}
|
||||
const versions = data.versions || []
|
||||
const qPsv = props.psvId
|
||||
const byQuery = qPsv != null ? versions.find(v => String(v.psvId) === String(qPsv)) : null
|
||||
const current = byQuery || data.version || versions[0] || {}
|
||||
const src = (current.details && current.details.length) ? current.details : (data.details || [])
|
||||
props.details.splice(0, props.details.length, ...src.map(d => hydrateRow(d, props.setType)))
|
||||
if (priceVisible.value && editCode.value) {
|
||||
const group = productGroups.value.find(p => p.productCode === editCode.value)
|
||||
editRows.value = group ? group.rows : []
|
||||
editGroup.value = group || null
|
||||
await stampPriceFloors(editRows.value, editCode.value)
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
/** enter step 3: pull all library products + country prices (skip already added) */
|
||||
async function pullAllFromLibrary(opts = {}) {
|
||||
const silent = !!opts.silent
|
||||
@@ -268,11 +385,7 @@ function openPrice(row) {
|
||||
}
|
||||
editCode.value = row.productCode
|
||||
editName.value = row.productName || ''
|
||||
editRows.value = (row.rows || []).map(r => {
|
||||
if (r._baseFreight == null && r.fvValue != null) r._baseFreight = Number(r.fvValue)
|
||||
if (r._baseReg == null && r.fvRegistrationFee != null) r._baseReg = Number(r.fvRegistrationFee)
|
||||
return r
|
||||
})
|
||||
editRows.value = row.rows || []
|
||||
editGroup.value = row
|
||||
Promise.resolve(stampPriceFloors(editRows.value, row.productCode)).finally(() => {
|
||||
priceVisible.value = true
|
||||
@@ -326,8 +439,14 @@ async function onRowPriceChange(row) {
|
||||
|
||||
function applyImportedFloor(row, hit) {
|
||||
if (!row || !hit) return
|
||||
if (hit.freightPrice != null) row._floorFreight = Number(hit.freightPrice)
|
||||
if (hit.registrationFee != null) row._floorReg = Number(hit.registrationFee)
|
||||
if (hit.freightPrice != null) {
|
||||
row._floorFreight = Number(hit.freightPrice)
|
||||
row._baseFreight = Number(hit.freightPrice)
|
||||
}
|
||||
if (hit.registrationFee != null) {
|
||||
row._floorReg = Number(hit.registrationFee)
|
||||
row._baseReg = Number(hit.registrationFee)
|
||||
}
|
||||
}
|
||||
|
||||
function findImportedCountry(rows, row) {
|
||||
@@ -341,6 +460,11 @@ function findImportedCountry(rows, row) {
|
||||
&& String(row.countryCode).toUpperCase() === String(item.countryCode).toUpperCase())
|
||||
|| (!row.countryCode && row.countryName && item.countryName && row.countryName === item.countryName)
|
||||
if (!sameCountry) return false
|
||||
const rz = String(row.zoneCode || '').toUpperCase()
|
||||
const iz = String(item.zoneCode || '').toUpperCase()
|
||||
if (rz || iz) {
|
||||
if (rz !== iz) return false
|
||||
}
|
||||
if (row.weightMin != null && item.weightMin != null && Number(row.weightMin) !== Number(item.weightMin)) return false
|
||||
if (row.weightMax != null && item.weightMax != null && Number(row.weightMax) !== Number(item.weightMax)) return false
|
||||
return row.weightMin != null || row.weightMax != null
|
||||
@@ -514,6 +638,15 @@ defineExpose({ pullAllFromLibrary })
|
||||
font-size: 13px;
|
||||
}
|
||||
.mb12 { margin-bottom: 12px; }
|
||||
.import-progress {
|
||||
padding: 8px 4px 0;
|
||||
}
|
||||
.import-progress-msg {
|
||||
margin-top: 12px;
|
||||
color: #606266;
|
||||
font-size: 13px;
|
||||
word-break: break-all;
|
||||
}
|
||||
.filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -36,19 +36,33 @@
|
||||
<el-table-column label="重量段" prop="weightLabel" min-width="130" />
|
||||
<el-table-column label="运费单价" min-width="130" align="right">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.fvValue == null">
|
||||
<span class="unit">未覆盖</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="hl">{{ row.fvValue }}</span>
|
||||
<span class="unit"> RMB/KG</span>
|
||||
</template>
|
||||
<el-tooltip placement="top" :disabled="!hasBaseFreight(row)">
|
||||
<template #content>
|
||||
<span>产品基础运费:{{ formatBase(row.baseFreightPrice, 4) }} RMB/KG</span>
|
||||
</template>
|
||||
<span class="price-tip">
|
||||
<template v-if="row.fvValue == null">
|
||||
<span class="unit">未覆盖</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="hl">{{ row.fvValue }}</span>
|
||||
<span class="unit"> RMB/KG</span>
|
||||
</template>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="挂号费" min-width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<span class="reg">{{ row.fvRegistrationFee }}</span>
|
||||
<span class="unit"> RMB/票</span>
|
||||
<el-tooltip placement="top" :disabled="!hasBaseReg(row)">
|
||||
<template #content>
|
||||
<span>产品基础挂号费:{{ formatBase(row.baseRegistrationFee, 2) }} RMB/票</span>
|
||||
</template>
|
||||
<span class="price-tip">
|
||||
<span class="reg">{{ row.fvRegistrationFee }}</span>
|
||||
<span class="unit"> RMB/票</span>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
@@ -139,6 +153,19 @@ const hint = computed(() => isFixedType(props.set.setType)
|
||||
? '运费单价单位 RMB/KG(留空表示不覆盖,按产品基础价计费),挂号费单位 RMB/票;附加费来自产品关联配置,此处只读展示。'
|
||||
: '未配置明细时全部在售产品均适用套餐默认折扣。')
|
||||
|
||||
function hasBaseFreight(row) {
|
||||
return row && row.baseFreightPrice != null && row.baseFreightPrice !== ''
|
||||
}
|
||||
function hasBaseReg(row) {
|
||||
return row && row.baseRegistrationFee != null && row.baseRegistrationFee !== ''
|
||||
}
|
||||
function formatBase(v, precision) {
|
||||
if (v == null || v === '') return '-'
|
||||
const n = Number(v)
|
||||
if (Number.isNaN(n)) return String(v)
|
||||
return n.toFixed(precision)
|
||||
}
|
||||
|
||||
function roundModeLabel(mode) {
|
||||
const hit = (bms_round_mode.value || []).find(d => d.value === mode)
|
||||
return hit ? hit.label : (mode || '四舍五入')
|
||||
@@ -199,4 +226,11 @@ function remove(row) {
|
||||
color: #909399;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.price-tip {
|
||||
cursor: help;
|
||||
border-bottom: 1px dashed transparent;
|
||||
}
|
||||
.price-tip:hover {
|
||||
border-bottom-color: #c0c4cc;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -73,19 +73,33 @@
|
||||
<el-table-column :label="colWeight" prop="weightLabel" min-width="120" />
|
||||
<el-table-column :label="colFv" min-width="130" align="right">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.fvValue == null">
|
||||
<span class="unit">未覆盖</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="hl">{{ row.fvValue }}</span>
|
||||
<span class="unit"> RMB/KG</span>
|
||||
</template>
|
||||
<el-tooltip placement="top" :disabled="!hasBaseFreight(row)">
|
||||
<template #content>
|
||||
<span>产品基础运费:{{ formatBase(row.baseFreightPrice, 4) }} RMB/KG</span>
|
||||
</template>
|
||||
<span class="price-tip">
|
||||
<template v-if="row.fvValue == null">
|
||||
<span class="unit">未覆盖</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="hl">{{ row.fvValue }}</span>
|
||||
<span class="unit"> RMB/KG</span>
|
||||
</template>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colReg" min-width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<span class="reg">{{ row.fvRegistrationFee }}</span>
|
||||
<span class="unit"> RMB/票</span>
|
||||
<el-tooltip placement="top" :disabled="!hasBaseReg(row)">
|
||||
<template #content>
|
||||
<span>产品基础挂号费:{{ formatBase(row.baseRegistrationFee, 2) }} RMB/票</span>
|
||||
</template>
|
||||
<span class="price-tip">
|
||||
<span class="reg">{{ row.fvRegistrationFee }}</span>
|
||||
<span class="unit"> RMB/票</span>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
@@ -211,6 +225,19 @@ const title = computed(() => {
|
||||
return (v.versionNo || '-') + ' · 版本明细'
|
||||
})
|
||||
|
||||
function hasBaseFreight(row) {
|
||||
return row && row.baseFreightPrice != null && row.baseFreightPrice !== ''
|
||||
}
|
||||
function hasBaseReg(row) {
|
||||
return row && row.baseRegistrationFee != null && row.baseRegistrationFee !== ''
|
||||
}
|
||||
function formatBase(v, precision) {
|
||||
if (v == null || v === '') return '-'
|
||||
const n = Number(v)
|
||||
if (Number.isNaN(n)) return String(v)
|
||||
return n.toFixed(precision)
|
||||
}
|
||||
|
||||
function roundModeLabel(mode) {
|
||||
const hit = (bms_round_mode.value || []).find(d => d.value === mode)
|
||||
return hit ? hit.label : (mode || '四舍五入')
|
||||
@@ -313,4 +340,11 @@ watch(
|
||||
.reg { color: #d48806; font-weight: 600; }
|
||||
.pct { color: #1890ff; font-weight: 600; }
|
||||
.unit { font-size: 12px; color: #909399; }
|
||||
.price-tip {
|
||||
cursor: help;
|
||||
border-bottom: 1px dashed transparent;
|
||||
}
|
||||
.price-tip:hover {
|
||||
border-bottom-color: #c0c4cc;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -323,10 +323,7 @@ export function useDetailCascade(refData) {
|
||||
row.weightLabel = formatWeightLabel(row.weightMin, row.weightMax)
|
||||
}
|
||||
row._countryKey = countryKeyOf(row)
|
||||
// Snapshot from DB: treat current package price as baseline so drawer cells stay editable
|
||||
// and yellow "changed" highlight still works after first edit.
|
||||
if (row._baseFreight == null && row.fvValue != null) row._baseFreight = Number(row.fvValue)
|
||||
if (row._baseReg == null && row.fvRegistrationFee != null) row._baseReg = Number(row.fvRegistrationFee)
|
||||
// Keep package prices in fv*; product base is stamped later from bms_product_country.
|
||||
return row
|
||||
}
|
||||
refData.ensureCountries(row.productCode).then(rows => {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
ref="detailsRef"
|
||||
:details="form.details"
|
||||
:price-set-id="form.priceSetId"
|
||||
:psv-id="form.version.psvId"
|
||||
:set-type="form.basic.setType"
|
||||
:version-no="form.version.versionNo"
|
||||
:ref-data="refData"
|
||||
|
||||
Reference in New Issue
Block a user