Refactor billing options retrieval to include data permissions and add effective date handling for product imports
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user