Enhance product import validation by adding checks for duplicate country and weight band entries. Update related error messages in properties file. Introduce new method to delete auto fees while retaining manual entries during rebill operations. Update front-end components to manage build mode changes and ensure proper handling of product lists during price set creation.

This commit is contained in:
wuweihua
2026-10-09 15:51:21 +08:00
parent 94e7a18e0b
commit 853187ad58
11 changed files with 519 additions and 54 deletions
@@ -11,13 +11,14 @@
</div>
<div class="panel-actions">
<el-button @click="close">关闭</el-button>
<el-button type="primary" :disabled="!priceSetId" @click="close">完成</el-button>
<el-button type="primary" :loading="saving" :disabled="!priceSetId" @click="finish">完成</el-button>
</div>
</div>
<div class="panel-body">
<step-details
v-if="loaded"
ref="detailsRef"
:details="details"
:products="products"
:price-set-id="priceSetId"
@@ -25,9 +26,13 @@
@update:psv-id="v => { if (v != null && v !== '') version.psvId = v }"
:set-type="setType"
:price-build-mode="version.priceBuildMode"
@update:price-build-mode="onPriceBuildModeUpdate"
:effective-time="version.effectiveTime"
:version-no="version.versionNo"
:version-status="version.status"
:ref-data="refData"
:live-persist="true"
:build-mode-dirty="buildModeDirty"
header-title="编辑产品价格"
/>
</div>
@@ -38,7 +43,8 @@
import { ArrowLeft } from '@element-plus/icons-vue'
import StepDetails from './StepDetails.vue'
import { usePriceSetRefData } from '../../composables/usePriceSetRefData'
import { getPriceSet } from '@/api/price/set'
import { isBuildModeLockedVersion } from '../../utils/priceSet'
import { getPriceSet, savePriceSetAsync } from '@/api/price/set'
const props = defineProps({
priceSetId: { type: [Number, String], default: undefined },
@@ -53,9 +59,18 @@ const refData = usePriceSetRefData()
const loading = ref(false)
const loaded = ref(false)
const setType = ref('FIXED')
const version = reactive({ psvId: undefined, versionNo: 'V1', effectiveTime: undefined, priceBuildMode: undefined })
const priceSetName = ref('')
const version = reactive({ psvId: undefined, versionNo: 'V1', effectiveTime: undefined, status: undefined, priceBuildMode: undefined })
const savedPriceBuildMode = ref(undefined)
const saving = ref(false)
const details = ref([])
const products = ref([])
const detailsRef = ref()
/** Snapshot of products under the last saved build mode (restore if switch back before Complete) */
const savedProductsSnapshot = ref([])
const buildModeDirty = computed(() =>
String(version.priceBuildMode || '') !== String(savedPriceBuildMode.value || '')
)
const title = computed(() => '编辑产品价格 · ' + (version.versionNo || ''))
@@ -70,6 +85,7 @@ function load() {
getPriceSet(id).then(res => {
const data = res.data || {}
setType.value = data.setType || 'FIXED'
priceSetName.value = data.priceSetName || ''
const versions = data.versions || []
const qPsv = props.psvId
@@ -78,16 +94,107 @@ function load() {
version.psvId = current.psvId
version.versionNo = current.versionNo || 'V1'
version.effectiveTime = current.effectiveTime
version.status = current.status
version.priceBuildMode = current.priceBuildMode
savedPriceBuildMode.value = current.priceBuildMode
details.value = []
products.value = (current.products || []).map(p => ({ ...p }))
savedProductsSnapshot.value = products.value.map(p => ({ ...p }))
loaded.value = true
}).finally(() => {
loading.value = false
})
}
function onPriceBuildModeUpdate(mode) {
if (!mode || isBuildModeLockedVersion(version.status)) return
if (mode === version.priceBuildMode) return
const leavingSaved = String(version.priceBuildMode || '') === String(savedPriceBuildMode.value || '')
if (leavingSaved) {
savedProductsSnapshot.value = (products.value || []).map(p => ({ ...p }))
}
version.priceBuildMode = mode
if (detailsRef.value && detailsRef.value.clearLocalDraft) {
detailsRef.value.clearLocalDraft()
}
details.value = []
const backToSaved = String(mode || '') === String(savedPriceBuildMode.value || '')
if (backToSaved) {
products.value = (savedProductsSnapshot.value || []).map(p => ({ ...p }))
if (!products.value.length) {
reloadSavedProducts()
} else {
proxy.$modal.msgSuccess("已恢复原建价方式及产品列表(未保存,库数据未变)")
}
return
}
products.value = []
proxy.$modal.msgSuccess("已切换建价方式(本地),请点「完成」保存后再拉取或导入;切回原方式可恢复列表")
}
function reloadSavedProducts() {
const id = props.priceSetId
if (!id) return
loading.value = true
getPriceSet(id).then(res => {
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 }))
proxy.$modal.msgSuccess("已恢复原建价方式及产品列表(未保存,库数据未变)")
}).finally(() => {
loading.value = false
})
}
async function finish() {
if (!props.priceSetId) {
close()
return
}
if (!buildModeDirty.value) {
close()
return
}
if (!version.priceBuildMode) {
proxy.$modal.msgWarning("请先选择创建价格方式")
return
}
saving.value = true
try {
const payload = {
priceSetId: props.priceSetId,
priceSetName: priceSetName.value,
setType: setType.value || 'FIXED',
version: {
psvId: version.psvId,
versionNo: version.versionNo,
priceBuildMode: version.priceBuildMode
}
}
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 })
proxy.$modal.msgSuccess("建价方式已保存并清空旧明细,已重新拉取产品列表")
} else {
proxy.$modal.msgSuccess("建价方式已保存并清空旧明细,可导入 Excel 建价")
}
close()
} catch (e) {
const msg = (e && e.task && e.task.error) || (e && e.message) || "保存失败"
proxy.$modal.msgError(msg)
} finally {
saving.value = false
}
}
function close() {
emit('close')
}
@@ -17,21 +17,21 @@
v-if="showImportAction"
type="primary"
size="small"
:disabled="(livePersist && !priceSetId) || pulling"
:disabled="(livePersist && !priceSetId) || pulling || buildModeDirty"
@click="openImport"
>{{ importActionLabel }}</el-button>
</div>
</div>
</template>
<div v-if="showBuildChoice" class="build-choice">
<div class="build-choice">
<p class="build-choice__tip">创建价格方式</p>
<div class="build-choice__cards">
<button
type="button"
class="build-card build-card--pull"
:class="{ selected: isPullBuildMode(priceBuildMode) }"
:disabled="choosingMode"
:disabled="buildModeLocked || choosingMode"
@click="chooseBuildMode('PULL')"
>
<div class="build-card__title">按产品价建价</div>
@@ -41,7 +41,7 @@
type="button"
class="build-card build-card--import"
:class="{ selected: isImportBuildMode(priceBuildMode) }"
:disabled="!isFixedType(setType) || choosingMode"
:disabled="buildModeLocked || !isFixedType(setType) || choosingMode"
@click="chooseBuildMode('IMPORT')"
>
<div class="build-card__title">Excel 导入建价</div>
@@ -152,6 +152,7 @@ import { useDetailCascade } from '../../composables/usePriceSetRefData'
import {
createEmptyDetailRow,
formatWeightLabel,
isBuildModeLockedVersion,
isDiscountType,
isFixedType,
isImportBuildMode,
@@ -174,7 +175,11 @@ const props = defineProps({
refData: { type: Object, required: true },
headerTitle: { type: String, default: '第 2 步 · 编辑产品价格' },
/** true=edit live DB; false=create local draft until finish */
livePersist: { type: Boolean, default: true }
livePersist: { type: Boolean, default: true },
/** Version status: draft can change build mode; started versions lock it */
versionStatus: { type: [String, Number], default: undefined },
/** True when build mode changed locally and not saved yet */
buildModeDirty: { type: Boolean, default: false }
})
const emit = defineEmits(['update:psvId', 'update:priceBuildMode'])
@@ -221,14 +226,11 @@ const importProgressStatus = ref(undefined)
const progressDialogTitle = ref('正在导入价格')
const hasBuildMode = computed(() => {
if (isPullBuildMode(props.priceBuildMode) || isImportBuildMode(props.priceBuildMode)) return true
// Legacy / already has products: skip choice cards
// Legacy / already has products: treat as build mode ready
return (props.products || []).some(p => p && p.productCode)
})
/** Create/newVersion only: show choice until a mode is picked */
const showBuildChoice = computed(() => {
if (props.livePersist) return false
return !isPullBuildMode(props.priceBuildMode) && !isImportBuildMode(props.priceBuildMode)
})
/** Started (non-draft) versions: show cards but disable switching */
const buildModeLocked = computed(() => isBuildModeLockedVersion(props.versionStatus))
const choosingMode = ref(false)
const isModifyImport = computed(() => {
if (isPullBuildMode(props.priceBuildMode)) return true
@@ -255,7 +257,6 @@ const importDesc = computed(() => {
})
const typeHint = computed(() => {
if (props.livePersist) return ''
if (isPullBuildMode(props.priceBuildMode)) return '· 按产品价建价'
if (isImportBuildMode(props.priceBuildMode)) return '· Excel 导入'
if (hasBuildMode.value) return '· 当前:' + setTypeLabel(props.setType)
@@ -367,7 +368,7 @@ const pulling = ref(false)
function noop() {}
async function chooseBuildMode(mode) {
if (!mode || choosingMode.value || props.livePersist) return
if (!mode || choosingMode.value || buildModeLocked.value) return
if (mode === props.priceBuildMode) return
choosingMode.value = true
try {
@@ -405,6 +406,10 @@ function applyLocalDraft(products, details) {
}
async function openImport() {
if (props.buildModeDirty) {
proxy.$modal.msgWarning("请先点「完成」保存建价方式")
return
}
if (props.livePersist && !props.priceSetId) {
proxy.$modal.msgWarning('缺少套餐ID')
return
+94 -36
View File
@@ -27,8 +27,10 @@
:price-build-mode="form.version.priceBuildMode"
@update:price-build-mode="onPriceBuildModeUpdate"
:version-no="form.version.versionNo"
:version-status="form.version.status"
:ref-data="refData"
:live-persist="livePersist"
:build-mode-dirty="buildModeDirty"
:header-title="detailsHeaderTitle"
/>
@@ -64,6 +66,7 @@ import StepDetails from './components/create/StepDetails.vue'
import { usePriceSetRefData } from './composables/usePriceSetRefData'
import {
createEmptyBasicInfo,
isBuildModeLockedVersion,
nextVersionNo,
toCustomerPayload,
toDetailPayload
@@ -102,11 +105,16 @@ const detailsHeaderTitle = computed(() =>
const form = reactive({
priceSetId: undefined,
basic: createEmptyBasicInfo(),
version: { psvId: undefined, versionNo: 'V1', effectiveTime: undefined, expireTime: undefined, priceBuildMode: undefined },
version: { psvId: undefined, versionNo: 'V1', effectiveTime: undefined, expireTime: undefined, status: undefined, priceBuildMode: undefined },
details: [],
products: [],
customers: []
})
const savedPriceBuildMode = ref(undefined)
const savedProductsSnapshot = ref([])
const buildModeDirty = computed(() =>
String(form.version.priceBuildMode || '') !== String(savedPriceBuildMode.value || '')
)
function mapSaveProgressMessage(message) {
const m = message || ''
@@ -178,7 +186,33 @@ async function finishEdit() {
goBack()
return
}
proxy.$modal.msgSuccess('修改已实时落库')
if (buildModeDirty.value) {
if (!form.version.priceBuildMode) {
proxy.$modal.msgWarning("请先选择创建价格方式")
return
}
creating.value = true
try {
const payload = buildPayload()
delete payload.pullFromLibrary
delete payload.forcePullLibrary
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 })
}
proxy.$modal.msgSuccess("建价方式已保存")
} catch (e) {
return
} finally {
creating.value = false
}
} else {
proxy.$modal.msgSuccess("修改已实时落库")
}
const obj = {
path: '/customer/setDetail',
query: { priceSetId: form.priceSetId }
@@ -299,42 +333,59 @@ function onPsvIdUpdate(psvId) {
}
/** Step2 chose PULL / IMPORT: edit live-saves mode; create only local + optional preview pull. */
async function onPriceBuildModeUpdate(mode) {
function onPriceBuildModeUpdate(mode) {
if (!mode) return
const prev = form.version.priceBuildMode
if (prev === mode) return
form.version.priceBuildMode = mode
try {
// Create/copy: switching build mode clears the product table before reload/import.
if (!livePersist.value && prev) {
form.details.splice(0, form.details.length)
form.products.splice(0, form.products.length)
if (detailsRef.value && detailsRef.value.clearLocalDraft) {
detailsRef.value.clearLocalDraft()
}
}
if (livePersist.value) {
const payload = buildPayload()
delete payload.pullFromLibrary
delete payload.forcePullLibrary
delete payload.details
await runSaveWithProgress(payload)
}
await nextTick()
if (mode === 'PULL' && detailsRef.value && detailsRef.value.pullAllFromLibrary) {
const hasProducts = (form.products || []).some(p => p && p.productCode)
if (!hasProducts) {
await detailsRef.value.pullAllFromLibrary({ silent: true })
proxy.$modal.msgSuccess('已拉取产品列表,可继续编辑价格')
} else if (prev !== mode) {
proxy.$modal.msgSuccess('已切换为按产品价建价,可导入 Excel 修改价格')
}
} else if (mode === 'IMPORT' && prev !== mode) {
proxy.$modal.msgSuccess('请导入 Excel 建价')
}
} catch (e) {
form.version.priceBuildMode = prev
if (isBuildModeLockedVersion(form.version.status)) return
if (mode === form.version.priceBuildMode) return
const leavingSaved = livePersist.value
&& String(form.version.priceBuildMode || '') === String(savedPriceBuildMode.value || '')
if (leavingSaved) {
savedProductsSnapshot.value = (form.products || []).map(p => ({ ...p }))
}
form.version.priceBuildMode = mode
form.details.splice(0, form.details.length)
if (detailsRef.value && detailsRef.value.clearLocalDraft) {
detailsRef.value.clearLocalDraft()
}
if (livePersist.value) {
const backToSaved = String(mode || '') === String(savedPriceBuildMode.value || '')
if (backToSaved) {
form.products.splice(0, form.products.length, ...(savedProductsSnapshot.value || []).map(p => ({ ...p })))
if (!form.products.length && form.priceSetId) {
reloadSavedProductsForCreate()
} else {
proxy.$modal.msgSuccess("已恢复原建价方式及产品列表(未保存,库数据未变)")
}
return
}
form.products.splice(0, form.products.length)
proxy.$modal.msgSuccess("已切换建价方式(本地),请点「完成」保存后再拉取或导入;切回原方式可恢复列表")
return
}
form.products.splice(0, form.products.length)
if (mode === 'PULL' && detailsRef.value && detailsRef.value.pullAllFromLibrary) {
nextTick(() => {
detailsRef.value.pullAllFromLibrary({ silent: true }).then(() => {
proxy.$modal.msgSuccess("已拉取产品列表,可继续编辑价格")
}).catch(() => {})
})
} else if (mode === 'IMPORT') {
proxy.$modal.msgSuccess("请导入 Excel 建价")
}
}
function reloadSavedProductsForCreate() {
getPriceSet(form.priceSetId).then(res => {
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)
savedProductsSnapshot.value = list.map(p => ({ ...p }))
proxy.$modal.msgSuccess("已恢复原建价方式及产品列表(未保存,库数据未变)")
})
}
function applySetData(data, asCopy, asNewVersion) {
@@ -355,7 +406,9 @@ function applySetData(data, asCopy, asNewVersion) {
form.version.versionNo = nextVersionNo(versions)
form.version.effectiveTime = undefined
form.version.expireTime = undefined
form.version.status = undefined
form.version.priceBuildMode = undefined
savedPriceBuildMode.value = undefined
form.details = []
form.products = []
} else if (asNewVersion) {
@@ -364,7 +417,9 @@ function applySetData(data, asCopy, asNewVersion) {
form.version.versionNo = current.versionNo || nextVersionNo(versions)
form.version.effectiveTime = undefined
form.version.expireTime = undefined
form.version.status = current.status || '0'
form.version.priceBuildMode = undefined
savedPriceBuildMode.value = undefined
form.details = []
form.products = []
} else {
@@ -372,9 +427,12 @@ function applySetData(data, asCopy, asNewVersion) {
form.version.versionNo = current.versionNo || nextVersionNo(versions)
form.version.effectiveTime = current.effectiveTime
form.version.expireTime = current.expireTime
form.version.status = current.status
form.version.priceBuildMode = current.priceBuildMode || undefined
savedPriceBuildMode.value = current.priceBuildMode || undefined
form.details = []
form.products = (current.products || []).map(p => ({ ...p }))
savedProductsSnapshot.value = form.products.map(p => ({ ...p }))
}
form.customers = (data.customers || []).map(c => ({ ...c }))
}
@@ -91,6 +91,17 @@ export function isPriceEditableVersion(row) {
return isDraftVersion(row) || isPendingVersion(row)
}
/**
* Build-mode cards stay visible; locked for enabled/expired versions (已生效/已失效).
* Draft / pending / create wizard remain selectable.
*/
export function isBuildModeLockedVersion(rowOrStatus) {
const s = rowOrStatus && typeof rowOrStatus === 'object' ? rowOrStatus.status : rowOrStatus
if (s == null || s === '') return false
const status = String(s)
return status === VER_STATUS.EFFECT || status === VER_STATUS.EXPIRED
}
export function nextVersionNo(versions) {
const seqs = (versions || []).map(v => Number(v.versionSeq) || 0)
const max = seqs.length ? Math.max.apply(null, seqs) : 0