Refactor billing options retrieval to include data permissions and add effective date handling for product imports

This commit is contained in:
wuweihua
2026-10-09 09:27:28 +08:00
parent fea34d1d10
commit c95a9861cc
42 changed files with 984 additions and 219 deletions
@@ -63,13 +63,13 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="生效日期" prop="effectiveDate">
<el-date-picker v-model="editForm.effectiveDate" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" style="width: 100%" />
<el-form-item label="生效时间" prop="effectiveDate">
<el-date-picker v-model="editForm.effectiveDate" type="datetime" format="YYYY-MM-DD HH:00" value-format="YYYY-MM-DD HH:00:00" placeholder="选择时间" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="失效日期" prop="expireDate">
<el-date-picker v-model="editForm.expireDate" type="date" value-format="YYYY-MM-DD" placeholder="留空为长期" style="width: 100%" />
<el-form-item label="失效时间" prop="expireDate">
<el-date-picker v-model="editForm.expireDate" type="datetime" format="YYYY-MM-DD HH:00" value-format="YYYY-MM-DD HH:00:00" placeholder="留空为长期" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="24">
@@ -112,8 +112,8 @@ const columns = [
{ prop: 'sizeLimit', label: '尺寸限制', minWidth: 140 },
{ prop: 'lastMile', label: '尾程', minWidth: 110 },
{ prop: 'leadTime', label: '时效', width: 100, showOverflowTooltip: true },
{ prop: 'effectiveDate', label: '生效日期', width: 110, align: 'center' },
{ prop: 'expireDate', label: '失效日期', width: 110, align: 'center', formatter: (row) => row.expireDate || '长期' },
{ prop: 'effectiveDate', label: '生效时间', width: 160, align: 'center' },
{ prop: 'expireDate', label: '失效时间', width: 110, align: 'center', formatter: (row) => row.expireDate || '长期' },
{ prop: 'status', label: '状态', width: 90, align: 'center', dict: 'bms_product_effect_status' },
// { prop: 'action', label: '操作', width: 70, fixed: 'right', slot: 'action' }
]
@@ -176,6 +176,15 @@ const editTitle = ref('编辑国家明细')
const editFormRef = ref()
const saving = ref(false)
const editRow = ref({})
function normalizeDrawerHour(val) {
if (val == null || val === '') return ''
const s = String(val).trim().replace('T', ' ')
const m = s.match(/^(\d{4}-\d{2}-\d{2})[ ](\d{2})/)
if (m) return m[1] + ' ' + m[2] + ':00:00'
if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s + ' 00:00:00'
return s
}
const editForm = reactive({
detailId: null,
weightMin: null,
@@ -202,7 +211,7 @@ const editRules = {
trigger: 'blur'
}],
freightPrice: [{ required: true, message: '运费不能为空', trigger: 'blur' }],
effectiveDate: [{ required: true, message: '生效日期不能为空', trigger: 'change' }]
effectiveDate: [{ required: true, message: '生效时间不能为空', trigger: 'change' }]
}
function openEdit(row) {
@@ -217,8 +226,8 @@ function openEdit(row) {
sizeLimit: row.sizeLimit || '',
lastMile: row.lastMile || '',
leadTime: row.leadTime || '',
effectiveDate: row.effectiveDate ? String(row.effectiveDate).slice(0, 10) : '',
expireDate: row.expireDate ? String(row.expireDate).slice(0, 10) : ''
effectiveDate: row.effectiveDate ? normalizeDrawerHour(row.effectiveDate) : '',
expireDate: row.expireDate ? normalizeDrawerHour(row.expireDate) : '',
})
editVisible.value = true
nextTick(() => editFormRef.value?.clearValidate())
@@ -80,19 +80,19 @@ const schemas = computed(() => [
// rules: [{ required: true, message: '请选择税模式', trigger: 'change' }]
// },
{
label: '生效日期',
label: '生效时间',
prop: 'effectiveDate',
component: 'date',
placeholder: '选择日期',
component: 'datetime',
placeholder: '选择时间',
span: 12,
width: '100%',
rules: [{ required: true, message: '生效日期不能为空', trigger: 'change' }]
rules: [{ required: true, message: '生效时间不能为空', trigger: 'change' }]
},
{
label: '失效日期',
label: '失效时间',
prop: 'expireDate',
component: 'date',
placeholder: '选择日期',
component: 'datetime',
placeholder: '选择时间',
span: 12,
width: '100%'
},
@@ -5,7 +5,7 @@
:closable="false"
show-icon
class="mb16"
title="多国家价表用「全部导入」。单国专线成本价用「按国家导入」:先选目的国家,目录里的产品都记到该国。相同产品代码+生效日期不覆盖主数据。两种导入均可在弹窗内下载对应模板。"
title="导入前须先选择生效时间;产品与国家明细均按此时间落库,上一版产品/明细失效时间设为该时间。多国家价表用「全部导入」,单国专线成本价用「按国家导入」。相同产品代码+生效日期不覆盖主数据。"
/>
<basic-table
@@ -65,9 +65,27 @@
template-url="/templates/product/product-import-all-template.xlsx"
template-file-name="产品多国家价表导入模板"
:import-desc="allImportDesc"
:before-submit="validateImportEffectiveDate"
:submit-handler="runImportAllAsync"
@success="() => tableRef.refresh()"
/>
>
<template #extra>
<el-form label-width="90px">
<el-form-item label="生效时间" required>
<el-date-picker
v-model="importEffectiveDate"
type="datetime"
format="YYYY-MM-DD HH:00"
value-format="YYYY-MM-DD HH:00:00"
popper-class="product-import-hour-picker"
placeholder="请选择生效时间"
style="width: 100%"
@change="onImportEffectiveTimeChange"
/>
</el-form-item>
</el-form>
</template>
</excel-import-dialog>
<excel-import-dialog
ref="countryImportRef"
title="按国家导入"
@@ -82,6 +100,18 @@
>
<template #extra>
<el-form label-width="90px">
<el-form-item label="生效时间" required>
<el-date-picker
v-model="importEffectiveDate"
type="datetime"
format="YYYY-MM-DD HH:00"
value-format="YYYY-MM-DD HH:00:00"
popper-class="product-import-hour-picker"
placeholder="请选择生效时间"
style="width: 100%"
@change="onImportEffectiveTimeChange"
/>
</el-form-item>
<el-form-item label="目的国家" required>
<el-select v-model="importCountryCode" filterable clearable placeholder="请选择国家" style="width: 100%">
<el-option
@@ -133,6 +163,31 @@ const expiredRef = ref()
const importRef = ref()
const countryImportRef = ref()
const importCountryCode = ref('')
const importEffectiveDate = ref('')
function pad2(n) {
return String(n).padStart(2, '0')
}
function formatImportHour(d) {
if (!d || Number.isNaN(d.getTime())) return undefined
return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate())
+ ' ' + pad2(d.getHours()) + ':00:00'
}
function normalizeImportToHour(val) {
if (val == null || val === '') return val
if (val instanceof Date) return formatImportHour(val)
const s = String(val).trim().replace('T', ' ')
const m = s.match(/^(\d{4}-\d{2}-\d{2})[ ](\d{2})/)
if (m) return m[1] + ' ' + m[2] + ':00:00'
if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s + ' 00:00:00'
return formatImportHour(new Date(s.replace(/-/g, '/'))) || val
}
function onImportEffectiveTimeChange(val) {
importEffectiveDate.value = normalizeImportToHour(val) || ''
}
const regionOptions = ref([])
const importProgressVisible = ref(false)
const importProgressPercent = ref(0)
@@ -140,12 +195,12 @@ const importProgressMessage = ref('')
const importProgressStatus = ref(undefined)
const allImportDesc = [
'适用多国家价表(B2C 小包价表等),按目录 sheet 匹配国家导入。',
'生效日期必填。产品+国家有新生效日时,先失效该国家下全部旧重量段,再插入 Excel 重量段。',
'必选生效时间:产品与 product_country 明细均按此时间计费;上一版产品/明细失效时间设为该时间。',
'请在弹窗底部下载模板,按模板格式整理后上传。相同产品代码+生效日期不覆盖主数据。'
]
const countryImportDesc = [
'选择目的国家后上传专线成本价表,目录中的产品都会记到该国家。',
'生效日期必填。产品+国家有新生效日时,先失效该国家下全部旧重量段,再插入 Excel 重量段。',
'必选生效时间:产品与 product_country 明细均按此时间计费;上一版产品/明细失效时间设为该时间。',
'邮编、赔偿标准等 sheet 不导入。可在弹窗底部下载模板。'
]
@@ -155,8 +210,8 @@ const columns = [
{ prop: 'serviceLevel', label: '时效等级', width: 90, align: 'center' },
{ prop: 'cargoAttr', label: '走货属性', minWidth: 120, slot: 'cargoAttr' },
{ prop: 'destRegion', label: '目的区域', width: 90, align: 'center' },
{ prop: 'effectiveDate', label: '生效日期', width: 120, align: 'center' },
{ prop: 'expireDate', label: '失效日期', width: 120, align: 'center' },
{ prop: 'effectiveDate', label: '生效时间', width: 160, align: 'center' },
{ prop: 'expireDate', label: '失效时间', width: 160, align: 'center' },
{ prop: 'countryCount', label: '国家数', width: 80, align: 'center' },
{ prop: 'surchargeCount', label: '附加费数', width: 90, align: 'center' },
{ prop: 'status', label: '状态', width: 90, align: 'center', slot: 'status' },
@@ -190,6 +245,7 @@ function handleImportCommand(command) {
}
function handleImport() {
importEffectiveDate.value = ''
importRef.value.open()
}
@@ -208,11 +264,21 @@ function loadRegionOptions() {
function handleCountryImport() {
importCountryCode.value = ''
importEffectiveDate.value = ''
loadRegionOptions()
countryImportRef.value.open()
}
function validateImportEffectiveDate() {
if (!importEffectiveDate.value) {
return '请先选择生效时间'
}
return ''
}
function validateCountryImport() {
const effErr = validateImportEffectiveDate()
if (effErr) return effErr
if (!importCountryCode.value) {
return '请先选择目的国家'
}
@@ -266,7 +332,11 @@ async function runImportAllAsync(uploadFile) {
if (!raw) {
throw new Error('请先选择文件')
}
return runImportWithProgress((onProgress) => importProductExcelAsync(raw, onProgress))
if (!importEffectiveDate.value) {
throw new Error('请先选择生效时间')
}
const eff = importEffectiveDate.value
return runImportWithProgress((onProgress) => importProductExcelAsync(raw, eff, onProgress))
}
async function runImportCountryAsync(uploadFile) {
@@ -274,11 +344,15 @@ async function runImportCountryAsync(uploadFile) {
if (!raw) {
throw new Error('请先选择文件')
}
if (!importEffectiveDate.value) {
throw new Error('请先选择生效时间')
}
if (!importCountryCode.value) {
throw new Error('请先选择目的国家')
}
const code = importCountryCode.value
return runImportWithProgress((onProgress) => importProductCountryExcelAsync(raw, code, onProgress))
const eff = importEffectiveDate.value
return runImportWithProgress((onProgress) => importProductCountryExcelAsync(raw, code, eff, onProgress))
}
</script>
@@ -24,7 +24,7 @@
</div>
</template>
<div v-if="!livePersist" class="build-choice">
<div v-if="showBuildChoice" class="build-choice">
<p class="build-choice__tip">创建价格方式</p>
<div class="build-choice__cards">
<button
@@ -224,6 +224,11 @@ const hasBuildMode = computed(() => {
// Legacy / already has products: skip choice cards
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)
})
const choosingMode = ref(false)
const isModifyImport = computed(() => {
if (isPullBuildMode(props.priceBuildMode)) return true
@@ -1,5 +1,15 @@
<template>
<set-section-card :title="title" tone="green" :count="filteredVersions.length">
<template #action>
<el-button
type="primary"
plain
size="small"
:disabled="hasDraft"
v-hasPermi="['price:set:add']"
@click="emit('create-version')"
>新建价格版本</el-button>
</template>
<el-tabs v-model="statusTab" class="ver-tabs">
<el-tab-pane :label="tabLabel('all')" name="all" />
<el-tab-pane :label="tabLabel('0')" name="0" />
@@ -80,7 +90,7 @@ const props = defineProps({
versions: { type: Array, default: () => [] },
modelValue: { type: [Number, String], default: undefined }
})
const emit = defineEmits(['update:modelValue', 'copy-version', 'open-details', 'activate', 'reschedule', 'edit', 'remove'])
const emit = defineEmits(['update:modelValue', 'copy-version', 'create-version', 'open-details', 'activate', 'reschedule', 'edit', 'remove'])
const title = '套餐版本'
const colNo = '版本号'
@@ -4,7 +4,7 @@
* 页面内共享一份(在父页面调用一次,作为 prop 传给各步骤组件),
* 避免产品 / 产品国家 / 附加费 被重复请求。
*/
import { listProduct, listProductCountries } from '@/api/price/product'
import { listProductOptions, listProductCountries } from '@/api/price/product'
import { listSurchargeByProduct, listSurchargeOptions } from '@/api/price/surcharge'
import {
buildCountryOptions,
@@ -40,8 +40,8 @@ export function usePriceSetRefData() {
/** 加载可选产品(已生效),保留列表展示字段 */
function loadProducts() {
return listProduct({ status: '1', pageNum: 1, pageSize: 1000 }).then(res => {
productOptions.value = (res.rows || []).map(p => ({
return listProductOptions({ status: '1' }).then(res => {
productOptions.value = (res.data || []).map(p => ({
value: p.productCode,
label: `${p.productCode} ${p.productName || ''}`.trim(),
name: p.productName,
+39 -13
View File
@@ -1,6 +1,12 @@
<template>
<div class="app-container">
<el-steps :active="active" finish-status="success" align-center class="wizard-steps">
<el-steps
v-if="!newVersionMode"
:active="active"
finish-status="success"
align-center
class="wizard-steps"
>
<el-step :title="step1Title" />
<el-step :title="step2Title" />
</el-steps>
@@ -23,6 +29,7 @@
:version-no="form.version.versionNo"
:ref-data="refData"
:live-persist="livePersist"
:header-title="detailsHeaderTitle"
/>
<div class="wizard-actions">
@@ -78,13 +85,19 @@ const progressStatus = ref(undefined)
const basicRef = ref()
const detailsRef = ref()
const copyMode = computed(() => String(route.query.copyVersion || '') === '1')
/** Entered from detail: continue-config / copy-version jump to step2 — hide 上一步 */
const directStep2 = computed(() => copyMode.value || String(route.query.step || '') === '2')
/** Edit existing version: live PUT/pull/import. New create / copy-version: defer until finish. */
const livePersist = computed(() => !!form.priceSetId && !copyMode.value)
const newVersionMode = computed(() => String(route.query.newVersion || '') === '1')
/** copy / newVersion: defer DB write until finish (same as create step2) */
const deferPersist = computed(() => copyMode.value || newVersionMode.value)
/** Entered from detail: continue-config / copy-version / new-version jump to step2 */
const directStep2 = 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)
const step1Title = '基本信息'
const step2Title = '编辑产品价格'
const detailsHeaderTitle = computed(() =>
newVersionMode.value ? '编辑产品价格' : '第 2 步 · 编辑产品价格'
)
const form = reactive({
priceSetId: undefined,
@@ -271,7 +284,9 @@ function buildPayload(opts = {}) {
}
function goBack() {
const obj = { path: '/customer/set' }
const obj = (newVersionMode.value || copyMode.value) && form.priceSetId
? { path: '/customer/setDetail', query: { priceSetId: form.priceSetId } }
: { path: '/customer/set' }
if (proxy.$tab) {
proxy.$tab.closeOpenPage(obj)
} else {
@@ -322,7 +337,7 @@ async function onPriceBuildModeUpdate(mode) {
}
}
function applySetData(data, asCopy) {
function applySetData(data, asCopy, asNewVersion) {
form.priceSetId = data.priceSetId
Object.assign(form.basic, {
priceSetName: data.priceSetName,
@@ -341,22 +356,33 @@ function applySetData(data, asCopy) {
form.version.effectiveTime = undefined
form.version.expireTime = undefined
form.version.priceBuildMode = undefined
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)
form.version.effectiveTime = undefined
form.version.expireTime = undefined
form.version.priceBuildMode = undefined
form.details = []
form.products = []
} else {
form.version.psvId = current.psvId
form.version.versionNo = current.versionNo || nextVersionNo(versions)
form.version.effectiveTime = current.effectiveTime
form.version.expireTime = current.expireTime
form.version.priceBuildMode = current.priceBuildMode || undefined
form.details = []
form.products = (current.products || []).map(p => ({ ...p }))
}
form.details = []
form.products = asCopy ? [] : ((current.products || []).map(p => ({ ...p })))
form.customers = (data.customers || []).map(c => ({ ...c }))
}
function loadExisting(id, asCopy) {
function loadExisting(id, asCopy, asNewVersion) {
getPriceSet(id).then(res => {
applySetData(res.data || {}, asCopy)
if (asCopy || String(route.query.step || '') === '2') {
applySetData(res.data || {}, asCopy, asNewVersion)
if (asCopy || asNewVersion || String(route.query.step || '') === '2') {
active.value = 1
}
})
@@ -366,7 +392,7 @@ onMounted(() => {
refData.loadProducts().finally(() => {
const id = route.query.priceSetId
if (id) {
loadExisting(id, copyMode.value)
loadExisting(id, copyMode.value, newVersionMode.value)
}
})
})
+176 -13
View File
@@ -24,6 +24,7 @@
v-model="currentPsvId"
:versions="set.versions || []"
@copy-version="onCopyVersion"
@create-version="onCreateVersion"
@open-details="onOpenVersionDetails"
@activate="onActivateVersion"
@reschedule="onRescheduleVersion"
@@ -52,9 +53,15 @@
<el-date-picker
v-model="activateForm.effectiveTime"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
format="YYYY-MM-DD HH:00"
value-format="YYYY-MM-DD HH:00:00"
popper-class="activate-hour-picker"
:placeholder="activatePh"
:disabled-date="disabledActivateDate"
:disabled-hours="disabledActivateHours"
style="width: 100%"
@change="onActivateTimeChange"
@visible-change="onActivatePickerVisible"
/>
</el-form-item>
<div class="activate-hint">{{ activateDialogHint }}</div>
@@ -77,11 +84,13 @@ import SetVersionDetailDrawer from './components/detail/SetVersionDetailDrawer.v
import PriceEditPanel from './components/create/PriceEditPanel.vue'
import { usePriceSetRefData } from './composables/usePriceSetRefData'
import {
getPriceSet, disablePriceSet, enablePriceSet, activatePriceSetVersion, copyPriceSetVersion, deletePriceSetVersion
getPriceSet, disablePriceSet, enablePriceSet, activatePriceSetVersion, copyPriceSetVersion, createPriceSetVersion, deletePriceSetVersion
} from '@/api/price/set'
import { isDraftVersion } from './utils/priceSet'
const { proxy } = getCurrentInstance()
const route = useRoute()
const router = useRouter()
const refData = usePriceSetRefData()
const loading = ref(false)
@@ -124,11 +133,35 @@ function onDisable() {
function onEnable() {
confirmAction('启用后套餐将重新参与计费命中,确认启用?', () => enablePriceSet(set.value.priceSetId), '已启用')
}
/** Effective time precision: hour only (mm:ss always 00) */
function pad2(n) {
return String(n).padStart(2, '0')
}
function formatHour(d) {
if (!d || Number.isNaN(d.getTime())) return undefined
return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate())
+ ' ' + pad2(d.getHours()) + ':00:00'
}
function normalizeToHour(val) {
if (val == null || val === '') return val
if (val instanceof Date) return formatHour(val)
const s = String(val).trim().replace('T', ' ')
const m = s.match(/^(\d{4}-\d{2}-\d{2})[ ](\d{2})/)
if (m) return m[1] + ' ' + m[2] + ':00:00'
return formatHour(new Date(s.replace(/-/g, '/'))) || val
}
function formatNow() {
const d = new Date()
const p = n => String(n).padStart(2, '0')
return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate())
+ ' ' + p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds())
return formatHour(new Date())
}
function onActivateTimeChange(val) {
activateForm.effectiveTime = normalizeToHour(val)
if (activateFormRef.value) {
activateFormRef.value.validateField('effectiveTime')
}
}
const activateVisible = ref(false)
@@ -147,13 +180,93 @@ const activateDialogTitle = computed(() =>
const activateConfirmText = computed(() =>
activateMode.value === 'reschedule' ? '确认修改' : '确认启动'
)
const activateDialogHint = computed(() =>
activateMode.value === 'reschedule'
const activatePrevEffectiveTime = ref(undefined)
const activateEffBeforePrevMsg = '生效时间不能早于上一版本的生效时间'
const activateDialogHint = computed(() => {
const base = activateMode.value === 'reschedule'
? '仅待生效版本可修改;若时间晚于当前仍为待生效,到时自动生效并使上一版本失效。'
: '若生效时间晚于当前,版本将进入「待生效」;到时自动生效并使上一版本失效。'
)
const prev = activatePrevEffectiveTime.value
if (!prev) return base
return base + '不得早于上一版:' + prev + '。'
})
const activateRules = {
effectiveTime: [{ required: true, message: '请选择生效时间', trigger: 'change' }]
effectiveTime: [
{ required: true, message: '请选择生效时间', trigger: 'change' },
{
validator: (_r, v, cb) => {
const prev = activatePrevEffectiveTime.value
if (!v || !prev) return cb()
if (toTimeMs(v) < toTimeMs(prev)) return cb(new Error(activateEffBeforePrevMsg))
cb()
},
trigger: 'change'
}
]
}
function toTimeMs(val) {
if (val == null || val === '') return NaN
const n = normalizeToHour(val)
if (!n) return NaN
return new Date(String(n).replace(/-/g, '/')).getTime()
}
function findPrevVersionEffectiveTime(row) {
if (!row) return undefined
const versions = set.value.versions || []
const curSeq = Number(row.versionSeq) || 0
let prev = null
for (const v of versions) {
if (!v || v.psvId == null || String(v.psvId) === String(row.psvId)) continue
const vs = Number(v.versionSeq) || 0
if (vs >= curSeq || !v.effectiveTime) continue
if (!prev || vs > (Number(prev.versionSeq) || 0)) prev = v
}
return prev ? normalizeToHour(prev.effectiveTime) : undefined
}
function pickActivateDefaultTime(prev) {
const now = formatNow()
if (!prev) return now
return toTimeMs(now) >= toTimeMs(prev) ? now : prev
}
function disabledActivateDate(date) {
const prev = activatePrevEffectiveTime.value
if (!prev || !date) return false
const prevDay = new Date(String(prev).replace(/-/g, '/'))
prevDay.setHours(0, 0, 0, 0)
const d = new Date(date)
d.setHours(0, 0, 0, 0)
return d.getTime() < prevDay.getTime()
}
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 (
prevD.getFullYear() !== curD.getFullYear() ||
prevD.getMonth() !== curD.getMonth() ||
prevD.getDate() !== curD.getDate()
) {
return []
}
const minH = prevD.getHours()
const hours = []
for (let h = 0; h < minH; h++) hours.push(h)
return hours
}
function onActivatePickerVisible(visible) {
if (visible && activateFormRef.value) {
activateFormRef.value.validateField('effectiveTime')
}
}
function onActivateVersion(row) {
@@ -161,7 +274,8 @@ function onActivateVersion(row) {
activateMode.value = 'activate'
activateForm.psvId = row.psvId
activateForm.versionNo = row.versionNo || ''
activateForm.effectiveTime = formatNow()
activatePrevEffectiveTime.value = findPrevVersionEffectiveTime(row)
activateForm.effectiveTime = pickActivateDefaultTime(activatePrevEffectiveTime.value)
activateVisible.value = true
}
@@ -170,7 +284,14 @@ function onRescheduleVersion(row) {
activateMode.value = 'reschedule'
activateForm.psvId = row.psvId
activateForm.versionNo = row.versionNo || ''
activateForm.effectiveTime = row.effectiveTime || formatNow()
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)
}
activateVisible.value = true
}
@@ -179,12 +300,14 @@ function onActivateClosed() {
activateForm.psvId = undefined
activateForm.versionNo = ''
activateForm.effectiveTime = undefined
activatePrevEffectiveTime.value = undefined
activateFormRef.value && activateFormRef.value.clearValidate()
}
function submitActivate() {
activateFormRef.value.validate(valid => {
if (!valid) return
activateForm.effectiveTime = normalizeToHour(activateForm.effectiveTime)
activateSubmitting.value = true
activatePriceSetVersion(activateForm.psvId, { effectiveTime: activateForm.effectiveTime })
.then(() => {
@@ -202,6 +325,23 @@ function submitActivate() {
})
})
}
function onCreateVersion() {
const priceSetId = set.value.priceSetId
if (priceSetId == null) return
const hasDraft = (set.value.versions || []).some(v => v && String(v.status) === '0')
if (hasDraft) {
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')
@@ -231,7 +371,19 @@ function onRemoveVersion(row) {
}).catch(() => {})
}
function onEditVersion(row) {
editPsvId.value = row && row.psvId != null ? row.psvId : undefined
if (!row || row.psvId == null) return
const priceSetId = set.value.priceSetId
const noMode = !row.priceBuildMode
const noProducts = !(row.products || []).some(p => p && p.productCode)
// Empty unfinished draft (new version, not finished): resume create step2 with build-mode choice
if (isDraftVersion(row) && noMode && noProducts && priceSetId != null) {
router.push({
path: '/customer/setCreate',
query: { priceSetId, psvId: row.psvId, step: '2', newVersion: '1' }
})
return
}
editPsvId.value = row.psvId
editVisible.value = true
}
@@ -263,3 +415,14 @@ onMounted(() => {
line-height: 1.5;
}
</style>
<style>
/* Hour-only: hide minute/second columns in activate datetime popper */
.activate-hour-picker .el-time-spinner__wrapper:nth-child(2),
.activate-hour-picker .el-time-spinner__wrapper:nth-child(3) {
display: none;
}
.activate-hour-picker .el-time-spinner__wrapper:nth-child(1) {
width: 100%;
}
</style>