优化套餐新建版本的逻辑
This commit is contained in:
@@ -205,6 +205,7 @@ price.bill.fee.idRequired=费用ID不能为空
|
||||
|
||||
price.set.version.effectiveTime.empty=版本生效时间不能为空
|
||||
price.set.version.effectiveTime.beforePrev=生效时间不能早于上一版本的生效时间
|
||||
price.set.version.effectiveTime.hitBoundary=生效时间不能与已有版本的生效时间或失效时间相同
|
||||
price.set.version.notExists=套餐版本不存在
|
||||
price.set.version.activate.statusNotAllowed=仅草稿或待生效版本可设置生效时间
|
||||
price.set.version.delete.statusNotAllowed=仅草稿、待生效或已生效版本可删除
|
||||
|
||||
+45
@@ -401,11 +401,21 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.set.edit.stopped"));
|
||||
}
|
||||
boolean importFresh = "IMPORT_FRESH".equalsIgnoreCase(StringUtils.trim(version.getPriceBuildMode()));
|
||||
if (importFresh)
|
||||
{
|
||||
// May start inside a prior version window; forbid equal to any other version's eff/exp.
|
||||
// refreshVersionStatus then sets B.expire=A.eff, A.expire=B.oldExpire and syncs children.
|
||||
validateImportFreshActivateTime(version, effectiveTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
Date prevEffective = findPrevVersionEffectiveTime(version);
|
||||
if (prevEffective != null && effectiveTime.before(prevEffective))
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.set.version.effectiveTime.beforePrev"));
|
||||
}
|
||||
}
|
||||
// Set schedule; refreshVersionStatus decides PENDING(3) vs EFFECT(1) and chains expire_time
|
||||
version.setEffectiveTime(effectiveTime);
|
||||
version.setExpireTime(null);
|
||||
@@ -415,6 +425,41 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* IMPORT_FRESH activate: allow effective time earlier than prev-by-seq (incl. inside prior window B).
|
||||
* Must not equal any other timed version's effectiveTime or expireTime.
|
||||
*/
|
||||
private void validateImportFreshActivateTime(PrcPriceSetVersion version, Date effectiveTime)
|
||||
{
|
||||
if (version == null || version.getPriceSetId() == null || effectiveTime == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
PrcPriceSetVersion q = new PrcPriceSetVersion();
|
||||
q.setPriceSetId(version.getPriceSetId());
|
||||
q.setPriceType(com.bms.price.domain.PriceType.SALES);
|
||||
List<PrcPriceSetVersion> list = versionMapper.selectPrcPriceSetVersionList(q);
|
||||
if (list == null || list.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
for (PrcPriceSetVersion v : list)
|
||||
{
|
||||
if (v == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (version.getPsvId() != null && version.getPsvId().equals(v.getPsvId()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (sameTime(v.getEffectiveTime(), effectiveTime) || sameTime(v.getExpireTime(), effectiveTime))
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.set.version.effectiveTime.hitBoundary"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Previous version (lower version_seq) effective_time; null if none. */
|
||||
private Date findPrevVersionEffectiveTime(PrcPriceSetVersion version)
|
||||
{
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
<div class="build-card__desc">按产品库基础价生成套餐产品列表,可再导入 Excel 修改价格</div>
|
||||
</button>
|
||||
<button
|
||||
v-if="showImportInherit"
|
||||
type="button"
|
||||
class="build-card build-card--import"
|
||||
:class="{ selected: isImportBuildMode(priceBuildMode) }"
|
||||
@@ -247,7 +248,12 @@ const props = defineProps({
|
||||
/** True when build mode changed locally and not saved yet */
|
||||
buildModeDirty: { type: Boolean, default: false },
|
||||
/** False while parent still loading package data — avoid first click being wiped by applySetData */
|
||||
buildModeReady: { type: Boolean, default: true }
|
||||
buildModeReady: { type: Boolean, default: true },
|
||||
/**
|
||||
* Show「按上一版 Excel 建价」(IMPORT). Hidden for brand-new package create
|
||||
* (no prior version); keep for newVersion / copy / edit.
|
||||
*/
|
||||
showImportInherit: { type: Boolean, default: true }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:psvId', 'update:priceBuildMode', 'update:pefId'])
|
||||
|
||||
@@ -173,7 +173,8 @@ const noneSc = '无'
|
||||
const isFixed = computed(() => isFixedType(props.setType))
|
||||
const title = computed(() => {
|
||||
const v = props.version || {}
|
||||
return (v.versionNo || '-') + ' · 版本明细'
|
||||
const eff = v.effectiveTime || '未设置生效时间'
|
||||
return eff + ' · 版本明细'
|
||||
})
|
||||
const detailTitleText = computed(() => {
|
||||
const p = activeProduct.value || {}
|
||||
|
||||
@@ -19,13 +19,10 @@
|
||||
</el-tabs>
|
||||
|
||||
<el-table :data="filteredVersions" border empty-text="暂无版本">
|
||||
<el-table-column :label="colNo" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openDetails(row)">{{ row.versionNo || '-' }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colEff" min-width="170">
|
||||
<template #default="{ row }">{{ row.effectiveTime || '-' }}</template>
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openDetails(row)">{{ row.effectiveTime || '未设置' }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colExp" min-width="170">
|
||||
<template #default="{ row }">{{ row.expireTime || '-' }}</template>
|
||||
@@ -90,7 +87,6 @@ const props = defineProps({
|
||||
const emit = defineEmits(['update:modelValue', 'create-version', 'open-details', 'view-import-attach', 'activate', 'reschedule', 'edit', 'remove'])
|
||||
|
||||
const title = '套餐版本'
|
||||
const colNo = '版本号'
|
||||
const colEff = '生效时间'
|
||||
const colExp = '失效时间'
|
||||
const colStatus = '状态'
|
||||
@@ -121,10 +117,33 @@ function tabLabel(key) {
|
||||
|
||||
const hasDraft = computed(() => (props.versions || []).some(v => isDraftVersion(v)))
|
||||
|
||||
function toEffMs(v) {
|
||||
if (!v || !v.effectiveTime) return NaN
|
||||
const t = new Date(String(v.effectiveTime).replace(/-/g, '/')).getTime()
|
||||
return Number.isNaN(t) ? NaN : t
|
||||
}
|
||||
|
||||
/** Sort by effective time descending; drafts without time last. */
|
||||
function sortByEffectiveTime(list) {
|
||||
return (list || []).slice().sort((a, b) => {
|
||||
const ta = toEffMs(a)
|
||||
const tb = toEffMs(b)
|
||||
const aNull = Number.isNaN(ta)
|
||||
const bNull = Number.isNaN(tb)
|
||||
if (aNull && bNull) return (Number(b.versionSeq) || 0) - (Number(a.versionSeq) || 0)
|
||||
if (aNull) return 1
|
||||
if (bNull) return -1
|
||||
if (tb !== ta) return tb - ta
|
||||
return (Number(b.versionSeq) || 0) - (Number(a.versionSeq) || 0)
|
||||
})
|
||||
}
|
||||
|
||||
const filteredVersions = computed(() => {
|
||||
const list = props.versions || []
|
||||
if (statusTab.value === 'all') return list
|
||||
return list.filter(v => String(v.status) === statusTab.value)
|
||||
const filtered = statusTab.value === 'all'
|
||||
? list
|
||||
: list.filter(v => String(v.status) === statusTab.value)
|
||||
return sortByEffectiveTime(filtered)
|
||||
})
|
||||
|
||||
function detailCount(row) {
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
:live-persist="livePersist"
|
||||
:build-mode-dirty="buildModeDirty"
|
||||
:build-mode-ready="pageReady"
|
||||
:show-import-inherit="showImportInherit"
|
||||
:header-title="detailsHeaderTitle"
|
||||
/>
|
||||
</div>
|
||||
@@ -82,6 +83,8 @@ const deferPersist = computed(() => copyMode.value || newVersionMode.value)
|
||||
const showBasic = computed(() => !deferPersist.value && String(route.query.step || '') !== '2')
|
||||
/** Edit existing version: live PUT/pull/import. New create / copy / newVersion: defer until finish. */
|
||||
const livePersist = computed(() => !!form.priceSetId && !deferPersist.value)
|
||||
/** Brand-new package has no prior version — hide IMPORT inherit card. */
|
||||
const showImportInherit = computed(() => newVersionMode.value || copyMode.value)
|
||||
|
||||
const detailsHeaderTitle = '编辑产品价格'
|
||||
|
||||
@@ -382,7 +385,7 @@ function onPriceBuildModeUpdate(mode) {
|
||||
proxy.$modal.msgSuccess("已拉取产品列表,可继续编辑价格")
|
||||
}).catch(() => {})
|
||||
})
|
||||
} else if (mode === 'IMPORT') {
|
||||
} else if (mode === 'IMPORT' && showImportInherit.value) {
|
||||
nextTick(async () => {
|
||||
// newVersion/copy: wait until loadExisting fills priceSetId
|
||||
const qid = route.query.priceSetId
|
||||
@@ -397,7 +400,7 @@ function onPriceBuildModeUpdate(mode) {
|
||||
return
|
||||
}
|
||||
if (!form.priceSetId) {
|
||||
proxy.$modal.msgSuccess("新建套餐无历史版本可带出,请直接导入 Excel 建价")
|
||||
proxy.$modal.msgSuccess("请直接导入 Excel 建价")
|
||||
return
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -50,9 +50,6 @@
|
||||
@closed="onActivateClosed"
|
||||
>
|
||||
<el-form ref="activateFormRef" :model="activateForm" :rules="activateRules" label-width="100px">
|
||||
<el-form-item label="版本号">
|
||||
<span>{{ activateForm.versionNo || '-' }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="生效时间" prop="effectiveTime">
|
||||
<el-date-picker
|
||||
v-model="activateForm.effectiveTime"
|
||||
@@ -92,7 +89,7 @@ import { usePriceSetRefData } from './composables/usePriceSetRefData'
|
||||
import {
|
||||
getPriceSet, disablePriceSet, enablePriceSet, activatePriceSetVersion, deletePriceSetVersion
|
||||
} from '@/api/price/set'
|
||||
import { isDraftVersion, isPendingVersion, isEffectVersion } from './utils/priceSet'
|
||||
import { isDraftVersion, isPendingVersion, isEffectVersion, isImportFreshBuildMode } from './utils/priceSet'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const route = useRoute()
|
||||
@@ -233,8 +230,18 @@ const activateConfirmText = computed(() =>
|
||||
activateMode.value === 'reschedule' ? '确认修改' : '确认启动'
|
||||
)
|
||||
const activatePrevEffectiveTime = ref(undefined)
|
||||
const activateIsImportFresh = ref(false)
|
||||
/** Other versions' effective/expire times (hour) — forbidden for IMPORT_FRESH */
|
||||
const activateBoundaryTimes = ref([])
|
||||
const activateEffBeforePrevMsg = '生效时间不能早于上一版本的生效时间'
|
||||
const activateEffHitBoundaryMsg = '生效时间不能与已有版本的生效时间或失效时间相同'
|
||||
const activateDialogHint = computed(() => {
|
||||
if (activateIsImportFresh.value) {
|
||||
const base = activateMode.value === 'reschedule'
|
||||
? 'Excel 重新建价:生效时间可落在历史版本区间内,将自动截断该版本失效时间并同步产品/明细。'
|
||||
: 'Excel 重新建价:生效时间可落在历史版本区间内;落在版本 B 区间时,B 失效时间改为本次启动时间,本版本失效时间取 B 原失效时间,并同步双方产品/明细。'
|
||||
return base + '不可与已有版本的生效时间或失效时间相同。'
|
||||
}
|
||||
const base = activateMode.value === 'reschedule'
|
||||
? '仅待生效版本可修改;若时间晚于当前仍为待生效,到时自动生效并使上一版本失效。'
|
||||
: '若生效时间晚于当前,版本将进入「待生效」;到时自动生效并使上一版本失效。'
|
||||
@@ -242,20 +249,27 @@ const activateDialogHint = computed(() => {
|
||||
if (!prev) return base
|
||||
return base + '不得早于上一版:' + prev + '。'
|
||||
})
|
||||
const activateRules = {
|
||||
const activateRules = computed(() => ({
|
||||
effectiveTime: [
|
||||
{ required: true, message: '请选择生效时间', trigger: 'change' },
|
||||
{
|
||||
validator: (_r, v, cb) => {
|
||||
if (!v) return cb()
|
||||
if (activateIsImportFresh.value) {
|
||||
const ms = toTimeMs(v)
|
||||
if (activateBoundaryTimes.value.some(b => toTimeMs(b) === ms)) {
|
||||
return cb(new Error(activateEffHitBoundaryMsg))
|
||||
}
|
||||
return cb()
|
||||
}
|
||||
const prev = activatePrevEffectiveTime.value
|
||||
if (!v || !prev) return cb()
|
||||
if (toTimeMs(v) < toTimeMs(prev)) return cb(new Error(activateEffBeforePrevMsg))
|
||||
if (prev && toTimeMs(v) < toTimeMs(prev)) return cb(new Error(activateEffBeforePrevMsg))
|
||||
cb()
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
}
|
||||
}))
|
||||
|
||||
function toTimeMs(val) {
|
||||
if (val == null || val === '') return NaN
|
||||
@@ -278,13 +292,30 @@ function findPrevVersionEffectiveTime(row) {
|
||||
return prev ? normalizeToHour(prev.effectiveTime) : undefined
|
||||
}
|
||||
|
||||
function pickActivateDefaultTime(prev) {
|
||||
/** Collect other versions' effective/expire hour times (forbidden for IMPORT_FRESH). */
|
||||
function collectActivateBoundaryTimes(excludePsvId) {
|
||||
const out = []
|
||||
const seen = new Set()
|
||||
for (const v of set.value.versions || []) {
|
||||
if (!v || v.psvId == null || String(v.psvId) === String(excludePsvId)) continue
|
||||
;[v.effectiveTime, v.expireTime].forEach(t => {
|
||||
const n = t ? normalizeToHour(t) : ''
|
||||
if (!n || seen.has(n)) return
|
||||
seen.add(n)
|
||||
out.push(n)
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function pickActivateDefaultTime(prev, importFresh) {
|
||||
const now = formatNow()
|
||||
if (!prev) return now
|
||||
if (importFresh || !prev) return now
|
||||
return toTimeMs(now) >= toTimeMs(prev) ? now : prev
|
||||
}
|
||||
|
||||
function disabledActivateDate(date) {
|
||||
if (activateIsImportFresh.value) return false
|
||||
const prev = activatePrevEffectiveTime.value
|
||||
if (!prev || !date) return false
|
||||
const prevDay = new Date(String(prev).replace(/-/g, '/'))
|
||||
@@ -295,13 +326,27 @@ function disabledActivateDate(date) {
|
||||
}
|
||||
|
||||
function disabledActivateHours() {
|
||||
const prev = activatePrevEffectiveTime.value
|
||||
const cur = activateForm.effectiveTime
|
||||
if (!prev) return []
|
||||
const prevD = new Date(String(prev).replace(/-/g, '/'))
|
||||
const curD = cur
|
||||
? new Date(String(normalizeToHour(cur)).replace(/-/g, '/'))
|
||||
: new Date()
|
||||
if (activateIsImportFresh.value) {
|
||||
const hours = new Set()
|
||||
for (const b of activateBoundaryTimes.value) {
|
||||
const bd = new Date(String(b).replace(/-/g, '/'))
|
||||
if (
|
||||
bd.getFullYear() === curD.getFullYear() &&
|
||||
bd.getMonth() === curD.getMonth() &&
|
||||
bd.getDate() === curD.getDate()
|
||||
) {
|
||||
hours.add(bd.getHours())
|
||||
}
|
||||
}
|
||||
return [...hours]
|
||||
}
|
||||
const prev = activatePrevEffectiveTime.value
|
||||
if (!prev) return []
|
||||
const prevD = new Date(String(prev).replace(/-/g, '/'))
|
||||
if (
|
||||
prevD.getFullYear() !== curD.getFullYear() ||
|
||||
prevD.getMonth() !== curD.getMonth() ||
|
||||
@@ -321,30 +366,38 @@ function onActivatePickerVisible(visible) {
|
||||
}
|
||||
}
|
||||
|
||||
function onActivateVersion(row) {
|
||||
function openActivateDialog(row, mode) {
|
||||
if (!row || row.psvId == null) return
|
||||
activateMode.value = 'activate'
|
||||
activateMode.value = mode
|
||||
activateForm.psvId = row.psvId
|
||||
activateForm.versionNo = row.versionNo || ''
|
||||
activateIsImportFresh.value = isImportFreshBuildMode(row.priceBuildMode)
|
||||
activatePrevEffectiveTime.value = findPrevVersionEffectiveTime(row)
|
||||
activateForm.effectiveTime = pickActivateDefaultTime(activatePrevEffectiveTime.value)
|
||||
activateBoundaryTimes.value = activateIsImportFresh.value
|
||||
? collectActivateBoundaryTimes(row.psvId)
|
||||
: []
|
||||
const prev = activatePrevEffectiveTime.value
|
||||
if (mode === 'reschedule') {
|
||||
const cur = normalizeToHour(row.effectiveTime)
|
||||
if (activateIsImportFresh.value) {
|
||||
activateForm.effectiveTime = cur || pickActivateDefaultTime(prev, true)
|
||||
} else if (cur && (!prev || toTimeMs(cur) >= toTimeMs(prev))) {
|
||||
activateForm.effectiveTime = cur
|
||||
} else {
|
||||
activateForm.effectiveTime = pickActivateDefaultTime(prev, false)
|
||||
}
|
||||
} else {
|
||||
activateForm.effectiveTime = pickActivateDefaultTime(prev, activateIsImportFresh.value)
|
||||
}
|
||||
activateVisible.value = true
|
||||
}
|
||||
|
||||
function onRescheduleVersion(row) {
|
||||
if (!row || row.psvId == null) return
|
||||
activateMode.value = 'reschedule'
|
||||
activateForm.psvId = row.psvId
|
||||
activateForm.versionNo = row.versionNo || ''
|
||||
activatePrevEffectiveTime.value = findPrevVersionEffectiveTime(row)
|
||||
const cur = normalizeToHour(row.effectiveTime)
|
||||
const prev = activatePrevEffectiveTime.value
|
||||
if (cur && (!prev || toTimeMs(cur) >= toTimeMs(prev))) {
|
||||
activateForm.effectiveTime = cur
|
||||
} else {
|
||||
activateForm.effectiveTime = pickActivateDefaultTime(prev)
|
||||
function onActivateVersion(row) {
|
||||
openActivateDialog(row, 'activate')
|
||||
}
|
||||
activateVisible.value = true
|
||||
|
||||
function onRescheduleVersion(row) {
|
||||
openActivateDialog(row, 'reschedule')
|
||||
}
|
||||
|
||||
function onActivateClosed() {
|
||||
@@ -353,6 +406,8 @@ function onActivateClosed() {
|
||||
activateForm.versionNo = ''
|
||||
activateForm.effectiveTime = undefined
|
||||
activatePrevEffectiveTime.value = undefined
|
||||
activateIsImportFresh.value = false
|
||||
activateBoundaryTimes.value = []
|
||||
activateFormRef.value && activateFormRef.value.clearValidate()
|
||||
}
|
||||
|
||||
@@ -394,8 +449,8 @@ function onCreateVersion() {
|
||||
function onRemoveVersion(row) {
|
||||
if (!row || row.psvId == null) return
|
||||
const msg = isEffectVersion(row)
|
||||
? "确认软删除已生效版本 " + (row.versionNo || '') + "?版本及产品、明细将标记为已删除,上一版将自动恢复生效并清空失效时间。"
|
||||
: "确认删除" + (isPendingVersion(row) ? "待生效" : "草稿") + "版本 " + (row.versionNo || '') + "?其明细与附加费将一并删除。"
|
||||
? "确认软删除已生效版本(生效时间:" + (row.effectiveTime || '未设置') + ")?版本及产品、明细将标记为已删除,上一版将自动恢复生效并清空失效时间。"
|
||||
: "确认删除" + (isPendingVersion(row) ? "待生效" : "草稿") + "版本(生效时间:" + (row.effectiveTime || '未设置') + ")?其明细与附加费将一并删除。"
|
||||
proxy.$modal.confirm(msg).then(() => {
|
||||
removingPsvId.value = row.psvId
|
||||
proxy.$modal.loading("正在删除,请稍候...")
|
||||
|
||||
Reference in New Issue
Block a user