套餐启用增加生效时间

This commit is contained in:
wuweihua
2026-09-17 14:42:48 +08:00
parent 51c8013cd9
commit f3ef2df949
25 changed files with 684 additions and 287 deletions
+3 -2
View File
@@ -113,10 +113,11 @@ export function rejectPriceSet(priceSetId) {
})
}
export function activatePriceSetVersion(psvId) {
export function activatePriceSetVersion(psvId, data) {
return request({
url: '/price/set/version/activate/' + psvId,
method: 'put'
method: 'put',
data: data || {}
})
}
-15
View File
@@ -1,15 +0,0 @@
import request from '@/utils/request'
/**
* 立即刷新 bms_product / bms_product_country 的 status 字段
*
* 日常由后端定时任务 BmsStatusRefreshTask 每天 02:00 自动跑,
* 此接口用于运维/测试手动触发。返回 success msg 形如:
* 「状态刷新完成:bms_product 更新 X 行,bms_product_country 更新 Y 行」
*/
export function refreshStatus() {
return request({
url: '/price/status/refresh',
method: 'post'
})
}
@@ -8,39 +8,23 @@
title="Excel 导入:相同产品代码+生效日期不覆盖;不同生效日期新增一条产品。同时入库国家×重量段(运费/挂号费/尺寸/尾程/时效)。"
/>
<el-tabs v-model="effectTab" class="mb8" @tab-change="onTabChange">
<el-tab-pane
v-for="t in statusTabs"
:key="t.name"
:label="t.label"
:name="t.name"
/>
</el-tabs>
<basic-table
ref="tableRef"
:columns="columns"
:search-columns="searchColumns"
:query="queryList"
:extra-params="extraParams"
row-key="productId"
>
<template #toolbar="{ selection, ids }">
<template #toolbar="{ selection }">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['price:product:add']">新建产品</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="Edit" :disabled="selection.length !== 1 || (selection[0] && isProductActive(selection[0]))" @click="handleUpdate(selection[0])" v-hasPermi="['price:product:edit']">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="!selection.length || selection.some(isProductActive)" @click="handleDelete(ids)" v-hasPermi="['price:product:remove']">删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="info" plain icon="Upload" @click="handleImport" v-hasPermi="['price:product:import']">导入 Excel</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="Refresh" @click="handleRefreshStatus" v-hasPermi="['price:product:edit']">刷新状态</el-button>
</el-col>
</template>
<template #col-cargoAttr="{ row }">
@@ -55,7 +39,6 @@
<el-button link type="primary" icon="View" @click="countryRef.open(row)" v-hasPermi="['price:product:query']">产品明细</el-button>
<el-button link type="primary" icon="Money" @click="surchargeRef.open(row)" v-hasPermi="['price:surcharge:query']">附加费</el-button>
<el-button :disabled="isProductActive(row)" :title="isProductActive(row) ? '已生效不可编辑' : ''" link type="primary" icon="Edit" @click="handleUpdate(row)" v-hasPermi="['price:product:edit']">编辑</el-button>
<el-button :disabled="isProductActive(row)" :title="isProductActive(row) ? '已生效不可删除' : ''" link type="primary" icon="Delete" @click="handleDelete(row.productId)" v-hasPermi="['price:product:remove']">删除</el-button>
</template>
</basic-table>
@@ -78,8 +61,7 @@ import ExcelImportDialog from '@/components/ExcelImportDialog'
import ProductFormDialog from './components/ProductFormDialog.vue'
import ProductCountryDrawer from './components/ProductCountryDrawer.vue'
import ProductSurchargeDrawer from './components/ProductSurchargeDrawer.vue'
import { listProduct, delProduct } from '@/api/price/product'
import { refreshStatus } from '@/api/price/status'
import { listProduct } from '@/api/price/product'
const { proxy } = getCurrentInstance()
const { bms_product_effect_status } = useDict('bms_product_effect_status')
@@ -90,41 +72,6 @@ const countryRef = ref()
const surchargeRef = ref()
const importRef = ref()
const TAB_ACTIVE = 'active'
const TAB_PENDING = 'pending'
const TAB_EXPIRED = 'expired'
const effectTab = ref(TAB_ACTIVE)
const statusTabs = computed(() => [
{ name: TAB_ACTIVE, code: '1', label: dictLabel('1', 'ACTIVE') },
{ name: TAB_PENDING, code: '0', label: dictLabel('0', 'PENDING') },
{ name: TAB_EXPIRED, code: '2', label: dictLabel('2', 'EXPIRED') }
])
const extraParams = computed(() => ({ status: tabToStatus(effectTab.value) }))
function dictLabel(code, legacy) {
const list = bms_product_effect_status.value || []
const hit = list.find(d => String(d.value) === code || String(d.value) === legacy)
return hit?.label || code
}
function tabToStatus(name) {
if (name === TAB_ACTIVE || name === 'ACTIVE' || name === '1') return '1'
if (name === TAB_PENDING || name === 'PENDING' || name === '0') return '0'
if (name === TAB_EXPIRED || name === 'EXPIRED' || name === '2') return '2'
return '1'
}
function onTabChange(name) {
effectTab.value = String(name || TAB_ACTIVE)
nextTick(() => {
tableRef.value?.clearSelection()
tableRef.value?.reload()
})
}
const columns = [
{ prop: 'productCode', label: '产品代码', minWidth: 110 },
{ prop: 'productName', label: '产品名称', minWidth: 140, showOverflowTooltip: true },
@@ -138,7 +85,7 @@ const columns = [
{ prop: 'surchargeCount', label: '附加费数', width: 90, align: 'center' },
{ prop: 'status', label: '状态', width: 90, align: 'center', slot: 'status' },
{ prop: 'updateNote', label: '更新内容', minWidth: 160, showOverflowTooltip: true },
{ prop: 'action', label: '操作', width: 300, fixed: 'right', slot: 'action' }
{ prop: 'action', label: '操作', width: 240, fixed: 'right', slot: 'action' }
]
const searchColumns = [
@@ -155,7 +102,7 @@ const searchColumns = [
]
function queryList(params) {
return listProduct({ ...params, status: tabToStatus(effectTab.value) })
return listProduct(params)
}
function isProductActive(row) {
@@ -174,32 +121,9 @@ function handleUpdate(row) {
formRef.value.open(row)
}
function handleDelete(ids) {
const idText = Array.isArray(ids) ? ids.join(',') : ids
proxy.$modal.confirm('是否确认删除所选产品?国家明细将一并删除。').then(() => {
return delProduct(idText)
}).then(() => {
proxy.$modal.msgSuccess('删除成功')
tableRef.value.refresh()
}).catch(() => {})
}
function handleImport() {
importRef.value.open()
}
/**
* 手动触发 bms_product / bms_product_country 状态刷新
* 日常由后端定时任务每天 02:00 自动跑;此处用于运维即时刷新
*/
function handleRefreshStatus() {
proxy.$modal.confirm('将立即按 effective_date / expire_date 重新计算所有产品及国家明细的状态,是否继续?').then(() => {
return refreshStatus()
}).then(res => {
proxy.$modal.msgSuccess(res.msg || '刷新成功')
tableRef.value && tableRef.value.refresh()
}).catch(() => {})
}
</script>
<style scoped>
@@ -87,6 +87,7 @@
<el-alert v-else-if="errorMsg" :title="errorMsg" type="error" show-icon :closable="false" />
<div v-else class="quote-result">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="客户代码">{{ result.customerCode || '-' }}</el-descriptions-item>
<el-descriptions-item label="客户ID">{{ result.customerId }}</el-descriptions-item>
<el-descriptions-item label="产品">{{ result.productName || '-' }} ({{ result.productCode }})</el-descriptions-item>
<el-descriptions-item label="国家">{{ result.countryName || '-' }} ({{ result.countryCode }})</el-descriptions-item>
@@ -189,6 +190,7 @@
<el-empty v-if="!viewResult" description="暂无详情" />
<div v-else class="quote-result">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="客户代码">{{ viewResult.customerCode || '-' }}</el-descriptions-item>
<el-descriptions-item label="客户ID">{{ viewResult.customerId }}</el-descriptions-item>
<el-descriptions-item label="产品">{{ viewResult.productName || '-' }} ({{ viewResult.productCode }})</el-descriptions-item>
<el-descriptions-item label="国家">{{ viewResult.countryName || '-' }} ({{ viewResult.countryCode }})</el-descriptions-item>
@@ -259,6 +261,7 @@ const historyLoading = ref(false)
const viewResult = ref(null)
const historyColumns = [
{ label: '客户代码', prop: 'customerCode', minWidth: 120, showOverflowTooltip: true },
{ label: '客户ID', prop: 'customerId', align: 'center', width: 90 },
{ label: '产品', prop: 'product', minWidth: 160, slot: 'product' },
{ label: '国家', prop: 'country', minWidth: 130, slot: 'country' },
@@ -272,6 +275,7 @@ const historyColumns = [
]
const historySearchColumns = [
{ label: '客户代码', prop: 'customerCode', component: 'input', placeholder: '客户代码', width: '150px' },
{ label: '客户ID', prop: 'customerId', component: 'input', placeholder: '客户ID', width: '140px' },
{ label: '产品代码', prop: 'productCode', component: 'input', placeholder: '产品代码', width: '150px' },
{ label: '国家代码', prop: 'countryCode', component: 'input', placeholder: '如 DE', width: '120px' },
@@ -3,6 +3,7 @@
<el-tabs v-model="statusTab" class="ver-tabs">
<el-tab-pane :label="tabLabel('all')" name="all" />
<el-tab-pane :label="tabLabel('0')" name="0" />
<el-tab-pane :label="tabLabel('3')" name="3" />
<el-tab-pane :label="tabLabel('1')" name="1" />
<el-tab-pane :label="tabLabel('2')" name="2" />
</el-tabs>
@@ -31,28 +32,28 @@
<template #default="{ row }">
<el-button link type="primary" @click="openDetails(row)">查看详情</el-button>
<el-button
v-if="String(row.status) === '0'"
v-if="isPriceEditableVersion(row)"
link
type="primary"
@click="emit('edit', row)"
v-hasPermi="['price:set:edit']"
>继续配置</el-button>
<el-button
v-if="String(row.status) === '0'"
v-if="isDraftVersion(row)"
link
type="success"
@click="emit('activate', row)"
v-hasPermi="['price:set:edit']"
>启动</el-button>
<el-button
v-if="String(row.status) === '0'"
v-if="isDraftVersion(row)"
link
type="danger"
@click="emit('remove', row)"
v-hasPermi="['price:set:edit']"
>删除</el-button>
<el-button
v-if="String(row.status) === '1'"
v-if="String(row.status) === '1' && !hasDraft"
link
type="primary"
@click="emit('copy-version', row)"
@@ -66,7 +67,7 @@
<script setup name="SetVersionTable">
import SetSectionCard from './SetSectionCard.vue'
import { versionStatusLabel, versionStatusTag } from '../../utils/priceSet'
import { versionStatusLabel, versionStatusTag, isDraftVersion, isPriceEditableVersion } from '../../utils/priceSet'
const props = defineProps({
versions: { type: Array, default: () => [] },
@@ -84,7 +85,7 @@ const colAction = '操作'
const statusTab = ref('all')
const counts = computed(() => {
const map = { all: 0, '0': 0, '1': 0, '2': 0 }
const map = { all: 0, '0': 0, '3': 0, '1': 0, '2': 0 }
;(props.versions || []).forEach(v => {
map.all++
const s = String(v.status == null ? '' : v.status)
@@ -97,12 +98,15 @@ function tabLabel(key) {
const labels = {
all: '全部',
'0': '草稿',
'3': '待生效',
'1': '生效',
'2': '失效'
}
return labels[key] + ' (' + (counts.value[key] || 0) + ')'
}
const hasDraft = computed(() => (props.versions || []).some(v => isDraftVersion(v)))
const filteredVersions = computed(() => {
const list = props.versions || []
if (statusTab.value === 'all') return list
+93 -5
View File
@@ -35,6 +35,34 @@
:version="detailVersion"
:set-type="set.setType"
/>
<el-dialog
v-model="activateVisible"
:title="activateTitle"
width="460px"
append-to-body
@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"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
:placeholder="activatePh"
style="width: 100%"
/>
</el-form-item>
<div class="activate-hint">{{ activateHint }}</div>
</el-form>
<template #footer>
<el-button @click="activateVisible = false">取消</el-button>
<el-button type="primary" :loading="activateSubmitting" @click="submitActivate">确认启动</el-button>
</template>
</el-dialog>
</template>
</div>
</template>
@@ -92,13 +120,59 @@ function confirmAction(text, request, successText, done) {
function onDisable() {
confirmAction('停用后不再参与计费命中,确认停用?', () => disablePriceSet(set.value.priceSetId), '已停用')
}
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())
}
const activateVisible = ref(false)
const activateSubmitting = ref(false)
const activateFormRef = ref()
const activateTitle = '启动版本'
const activatePh = '请选择生效时间'
const activateHint = '若生效时间晚于当前,版本将进入「待生效」;到时自动生效并使上一版本失效。'
const activateForm = reactive({
psvId: undefined,
versionNo: '',
effectiveTime: undefined
})
const activateRules = {
effectiveTime: [{ required: true, message: '请选择生效时间', trigger: 'change' }]
}
function onActivateVersion(row) {
if (!row || row.psvId == null) return
confirmAction(
'确认启动版本 ' + (row.versionNo || '') + '?启动后将以当前时间生效,上一版本自动失效。',
() => activatePriceSetVersion(row.psvId),
'版本已启动'
)
activateForm.psvId = row.psvId
activateForm.versionNo = row.versionNo || ''
activateForm.effectiveTime = formatNow()
activateVisible.value = true
}
function onActivateClosed() {
activateForm.psvId = undefined
activateForm.versionNo = ''
activateForm.effectiveTime = undefined
activateFormRef.value && activateFormRef.value.clearValidate()
}
function submitActivate() {
activateFormRef.value.validate(valid => {
if (!valid) return
activateSubmitting.value = true
activatePriceSetVersion(activateForm.psvId, { effectiveTime: activateForm.effectiveTime })
.then(() => {
const eff = activateForm.effectiveTime
const pending = eff && new Date(eff.replace(/-/g, '/')).getTime() > Date.now()
proxy.$modal.msgSuccess(pending ? '已设为待生效' : '版本已启动')
activateVisible.value = false
load()
})
.finally(() => {
activateSubmitting.value = false
})
})
}
function onCopy() {
copyPriceSet(set.value.priceSetId).then(res => {
@@ -108,6 +182,11 @@ function onCopy() {
}
function onCopyVersion(row) {
if (!row || row.psvId == null) return
const hasDraft = (set.value.versions || []).some(v => v && String(v.status) === '0')
if (hasDraft) {
proxy.$modal.msgWarning('已有草稿版本,请先启动或删除后再复制')
return
}
proxy.$modal.confirm(
'确认复制版本 ' + (row.versionNo || '') + '为新草稿?将新增版本并拷贝全部明细。'
).then(() => copyPriceSetVersion(row.psvId)).then(res => {
@@ -153,3 +232,12 @@ onMounted(() => {
load()
})
</script>
<style scoped>
.activate-hint {
margin: -4px 0 0 100px;
color: #909399;
font-size: 12px;
line-height: 1.5;
}
</style>
@@ -40,23 +40,43 @@ const STATUS_TAG = {
/** 默认取值方式 */
export const DEFAULT_ROUND_MODE = 'HALF_UP'
/** version status: 0 draft / 1 active / 2 expired */
/** version status: 0 draft / 3 pending / 1 active / 2 expired */
export const VER_STATUS = {
DRAFT: '0',
PENDING: '3',
EFFECT: '1',
EXPIRED: '2'
}
export function versionStatusLabel(status) {
const map = { '0': '草稿', '1': '生效', '2': '失效' }
const map = {
'0': '草稿',
'3': '待生效',
'1': '生效',
'2': '失效'
}
return map[status] || status || '-'
}
export function versionStatusTag(status) {
const map = { '0': 'warning', '1': 'success', '2': 'info' }
const map = { '0': 'warning', '3': 'primary', '1': 'success', '2': 'info' }
return map[status] || 'info'
}
/** True draft only (not pending) */
export function isDraftVersion(row) {
return !!row && String(row.status) === VER_STATUS.DRAFT
}
export function isPendingVersion(row) {
return !!row && String(row.status) === VER_STATUS.PENDING
}
/** Draft or pending: may continue editing price details */
export function isPriceEditableVersion(row) {
return isDraftVersion(row) || isPendingVersion(row)
}
export function nextVersionNo(versions) {
const seqs = (versions || []).map(v => Number(v.versionSeq) || 0)
const max = seqs.length ? Math.max.apply(null, seqs) : 0