修改产品价和红线价功能

This commit is contained in:
wuweihua
2026-10-28 22:44:13 +08:00
parent 1d47d5781b
commit 3bb7267b0b
9 changed files with 269 additions and 69 deletions
@@ -1,10 +1,7 @@
<!-- start catalog version: pick effective hour -->
<!-- start catalog version: pick effective hour;IMPORT_FRESH 对齐销售价规则 -->
<template>
<el-dialog v-model="visible" :title="dialogTitle" width="460px" append-to-body @closed="onClosed">
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<el-form-item :label="labNo">
<span>{{ form.versionNo || '-' }}</span>
</el-form-item>
<el-form-item :label="labEff" prop="effectiveTime">
<el-date-picker
v-model="form.effectiveTime"
@@ -17,6 +14,7 @@
:disabled-hours="disabledHours"
style="width: 100%"
@change="onTimeChange"
@visible-change="onPickerVisible"
/>
</el-form-item>
<div class="activate-hint">{{ hint }}</div>
@@ -28,6 +26,8 @@
</el-dialog>
</template>
<script setup>
import { isImportFreshBuildMode } from '@/views/price/set/utils/priceSet'
const props = defineProps({
loadVersions: { type: Function, required: true },
activateFn: { type: Function, required: true }
@@ -39,31 +39,43 @@ const submitting = ref(false)
const formRef = ref()
const versions = ref([])
const prevEff = ref()
const form = reactive({ psvId: undefined, versionNo: '', versionSeq: 0, effectiveTime: undefined })
const isImportFresh = ref(false)
const boundaryTimes = ref([])
const form = reactive({ psvId: undefined, versionNo: '', versionSeq: 0, effectiveTime: undefined, priceBuildMode: undefined })
const dialogTitle = '启动版本'
const labNo = '版本号'
const labEff = '生效时间'
const ph = '请选择生效时间'
const btnCancel = '取消'
const btnOk = '确认启动'
const beforePrev = '生效时间不能早于上一版本的生效时间'
const hitBoundary = '生效时间不能与已有版本的生效时间或失效时间相同'
const hint = computed(() => {
if (isImportFresh.value) {
return '重新建价:生效时间可落在历史版本区间内;落在版本 B 区间时,B 失效时间改为本次启动时间,本版本失效时间取 B 原失效时间,并同步产品/明细。不可与本类型已有版本的生效时间或失效时间相同。'
}
const base = '若生效时间晚于当前,版本将进入待生效;到时自动生效并使上一版本失效。'
return prevEff.value ? (base + '不得早于上一版:' + prevEff.value + '。') : base
})
const rules = {
const rules = computed(() => ({
effectiveTime: [
{ required: true, message: ph, trigger: 'change' },
{
validator: (_r, v, cb) => {
if (!v || !prevEff.value) return cb()
if (toMs(v) < toMs(prevEff.value)) return cb(new Error(beforePrev))
if (!v) return cb()
if (isImportFresh.value) {
const ms = toMs(v)
if (boundaryTimes.value.some(b => toMs(b) === ms)) {
return cb(new Error(hitBoundary))
}
return cb()
}
if (prevEff.value && toMs(v) < toMs(prevEff.value)) return cb(new Error(beforePrev))
cb()
},
trigger: 'change'
}
]
}
}))
function pad2(n) { return String(n).padStart(2, '0') }
function formatHour(d) {
if (!d || Number.isNaN(d.getTime())) return undefined
@@ -93,14 +105,29 @@ function findPrev(row) {
}
return prev ? normalizeToHour(prev.effectiveTime) : undefined
}
function pickDefault(prev) {
function collectBoundaryTimes(excludePsvId) {
const out = []
const seen = new Set()
for (const v of versions.value || []) {
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 pickDefault(prev, importFresh) {
const d = new Date()
d.setHours(9, 0, 0, 0)
const now = formatHour(d)
if (!prev) return now
if (importFresh || !prev) return now
return toMs(now) >= toMs(prev) ? now : prev
}
function disabledDate(date) {
if (isImportFresh.value) return false
if (!prevEff.value || !date) return false
const prevDay = new Date(String(prevEff.value).replace(/-/g, '/'))
prevDay.setHours(0, 0, 0, 0)
@@ -109,11 +136,25 @@ function disabledDate(date) {
return d.getTime() < prevDay.getTime()
}
function disabledHours() {
if (!prevEff.value) return []
const prevD = new Date(String(prevEff.value).replace(/-/g, '/'))
const curD = form.effectiveTime
? new Date(String(normalizeToHour(form.effectiveTime)).replace(/-/g, '/'))
: new Date()
if (isImportFresh.value) {
const hours = new Set()
for (const b of boundaryTimes.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]
}
if (!prevEff.value) return []
const prevD = new Date(String(prevEff.value).replace(/-/g, '/'))
if (prevD.getFullYear() !== curD.getFullYear() || prevD.getMonth() !== curD.getMonth() || prevD.getDate() !== curD.getDate()) return []
const hours = []
for (let h = 0; h < prevD.getHours(); h++) hours.push(h)
@@ -123,6 +164,9 @@ function onTimeChange(val) {
form.effectiveTime = normalizeToHour(val)
formRef.value && formRef.value.validateField('effectiveTime')
}
function onPickerVisible(visible) {
if (visible && formRef.value) formRef.value.validateField('effectiveTime')
}
async function open(row) {
if (!row || row.psvId == null) return
const list = await props.loadVersions()
@@ -131,15 +175,21 @@ async function open(row) {
form.psvId = found.psvId
form.versionNo = found.versionNo || ''
form.versionSeq = found.versionSeq
form.priceBuildMode = found.priceBuildMode
isImportFresh.value = isImportFreshBuildMode(found.priceBuildMode)
prevEff.value = findPrev(found)
form.effectiveTime = pickDefault(prevEff.value)
boundaryTimes.value = isImportFresh.value ? collectBoundaryTimes(found.psvId) : []
form.effectiveTime = pickDefault(prevEff.value, isImportFresh.value)
visible.value = true
}
function onClosed() {
form.psvId = undefined
form.versionNo = ''
form.effectiveTime = undefined
form.priceBuildMode = undefined
prevEff.value = undefined
isImportFresh.value = false
boundaryTimes.value = []
formRef.value && formRef.value.clearValidate()
}
function submit() {
@@ -29,8 +29,8 @@
<div class="build-choice__cards">
<button
type="button"
class="build-card build-card--pull"
:class="{ selected: buildMode === 'COPY' }"
class="build-card build-card--import"
:class="{ selected: buildMode === 'IMPORT' }"
:disabled="!!psvId || choosing || finishing"
@click="chooseMode(true)"
>
@@ -39,8 +39,8 @@
</button>
<button
type="button"
class="build-card build-card--import"
:class="{ selected: buildMode === 'IMPORT' }"
class="build-card build-card--import-fresh"
:class="{ selected: buildMode === 'IMPORT_FRESH' }"
:disabled="!!psvId || choosing || finishing"
@click="chooseMode(false)"
>
@@ -183,7 +183,7 @@ const route = useRoute()
const labTip = '创建价格方式'
const labModeCopy = '按现有生效版本建价'
const labModeCopyDesc = '先本地预览已生效版本产品,点击「完成」时再复制到待生效草稿'
const labModeFresh = '重新建产品价'
const labModeFresh = '新建产品价'
const labModeFreshDesc = "不沿用已生效版本;导入解析后先展示,点「完成」时再写入草稿"
const labImport = '导入 Excel 建价'
const labImportAll = '全部导入(多国家价表)'
@@ -300,9 +300,10 @@ function applyVersion(ver) {
}
psvId.value = ver.psvId
versionNo.value = ver.versionNo || ''
// Catalog: IMPORT = 沿用生效版; IMPORT_FRESH = 重新建价. Legacy COPY → IMPORT.
const mode = String(ver.priceBuildMode || '').toUpperCase()
if (mode === 'COPY') buildMode.value = 'COPY'
else if (mode === 'IMPORT' || mode === 'PULL') buildMode.value = 'IMPORT'
if (mode === 'IMPORT' || mode === 'COPY') buildMode.value = 'IMPORT'
else if (mode === 'IMPORT_FRESH' || mode === 'PULL') buildMode.value = 'IMPORT_FRESH'
}
async function loadProducts() {
@@ -329,7 +330,7 @@ async function bootstrap() {
psvId.value = Number(qPsv)
versionNo.value = (route.query && route.query.versionNo) ? String(route.query.versionNo) : ''
editingExisting.value = true
if (!buildMode.value) buildMode.value = 'IMPORT'
if (!buildMode.value) buildMode.value = 'IMPORT_FRESH'
await loadProducts()
}
} finally {
@@ -337,10 +338,10 @@ async function bootstrap() {
}
}
/** Mode select: local only, no DB write. */
/** Mode select: local only, no DB write. inherit=IMPORT, fresh=IMPORT_FRESH. */
async function chooseMode(copyFromLatest) {
if (psvId.value || choosing.value || finishing.value) return
const next = copyFromLatest ? 'COPY' : 'IMPORT'
const next = copyFromLatest ? 'IMPORT' : 'IMPORT_FRESH'
if (buildMode.value === next && (copyFromLatest ? rows.value.length : true)) {
if (copyFromLatest && rows.value.length) return
if (!copyFromLatest && !rows.value.length && !pendingImports.value.length) return
@@ -426,7 +427,7 @@ async function persistDraft() {
proxy.$modal.msgWarning('请先选择建价方式')
return false
}
const res = await props.prepareFn(buildMode.value === 'COPY')
const res = await props.prepareFn(buildMode.value === 'IMPORT')
applyVersion(res.data)
return !!psvId.value
}
@@ -682,8 +683,10 @@ onMounted(() => { bootstrap() })
.build-card:hover:not(:disabled) { box-shadow: 0 4px 14px rgba(0, 0, 0, 0.06); }
.build-card:disabled { opacity: 0.55; cursor: not-allowed; }
.build-card__title { font-size: 16px; font-weight: 600; color: #303133; margin-bottom: 10px; }
.build-card--pull .build-card__title { color: #67c23a; }
.build-card--import .build-card__title { color: #409eff; }
.build-card--import-fresh .build-card__title { color: #e6a23c; }
.build-card--import:hover:not(:disabled) { border-color: #409eff; }
.build-card--import-fresh:hover:not(:disabled) { border-color: #e6a23c; }
.build-card__desc { font-size: 13px; line-height: 1.6; color: #909399; }
.filters { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; }
.filters__left { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
@@ -75,7 +75,7 @@
:disabled="!canChooseBuildMode || !isFixedType(setType)"
@click="chooseBuildMode('IMPORT_FRESH')"
>
<div class="build-card__title">Excel 重新建价</div>
<div class="build-card__title">Excel 新建价</div>
<div class="build-card__desc">不延用以前的产品价格,不带出上一版,上传 Excel 直接生成套餐销售价</div>
</button>
</div>
@@ -338,7 +338,7 @@ const importDesc = computed(() => {
return [
'适用威狮B2C小包价表(含「产品目录」)。',
'不延用以前的产品价格;导入按「产品+国家」写入套餐销售价,直接生成产品明细。',
'同一产品下重复国家+重量段将整单失败。仅固定价套餐「Excel 重新建价」可用。'
'同一产品下重复国家+重量段将整单失败。仅固定价套餐「Excel 新建价」可用。'
]
}
return [
@@ -351,7 +351,7 @@ const importDesc = computed(() => {
const typeHint = computed(() => {
if (isPullBuildMode(props.priceBuildMode)) return '· 按产品价建价'
if (isImportBuildMode(props.priceBuildMode)) return '· 按上一版 Excel 建价'
if (isImportFreshBuildMode(props.priceBuildMode)) return '· Excel 重新建价'
if (isImportFreshBuildMode(props.priceBuildMode)) return '· Excel 新建价'
if (hasBuildMode.value) return '· 当前:' + setTypeLabel(props.setType)
return '· 请选择创建价格方式'
})
+2 -2
View File
@@ -235,8 +235,8 @@ const activateEffHitBoundaryMsg = '生效时间不能与已有版本的生效时
const activateDialogHint = computed(() => {
if (activateIsImportFresh.value) {
const base = activateMode.value === 'reschedule'
? 'Excel 重新建价:生效时间可落在历史版本区间内,将自动截断该版本失效时间并同步产品/明细。'
: 'Excel 重新建价:生效时间可落在历史版本区间内;落在版本 B 区间时,B 失效时间改为本次启动时间,本版本失效时间取 B 原失效时间,并同步双方产品/明细。'
? 'Excel 新建价:生效时间可落在历史版本区间内,将自动截断该版本失效时间并同步产品/明细。'
: 'Excel 新建价:生效时间可落在历史版本区间内;落在版本 B 区间时,B 失效时间改为本次启动时间,本版本失效时间取 B 原失效时间,并同步双方产品/明细。'
return base + '不可与已有版本的生效时间或失效时间相同。'
}
const base = activateMode.value === 'reschedule'