增加价格变更日志
This commit is contained in:
@@ -9,9 +9,13 @@
|
||||
>
|
||||
<div class="price-log-drawer" v-loading="loading">
|
||||
<div class="toolbar">
|
||||
<el-select v-model="query.status" clearable :placeholder="phStatus" style="width: 120px" @change="reload">
|
||||
<el-option v-for="o in statusOptions" :key="o.value" :label="o.label" :value="o.value" />
|
||||
</el-select>
|
||||
<el-input
|
||||
v-model="query.productKeyword"
|
||||
clearable
|
||||
:placeholder="phProduct"
|
||||
style="width: 220px"
|
||||
@keyup.enter="reload"
|
||||
/>
|
||||
<el-button type="primary" @click="reload">查询</el-button>
|
||||
<el-button @click="resetQuery">重置</el-button>
|
||||
</div>
|
||||
@@ -36,11 +40,6 @@
|
||||
<template #default="{ row }">{{ row.operator || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colCount" width="100" align="center" prop="changeCount" />
|
||||
<el-table-column :label="colStatus" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="statusTag(row.status)">{{ statusLabel(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colOps" width="90" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openProduct(row)">明细</el-button>
|
||||
@@ -50,23 +49,41 @@
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<el-drawer v-model="groupOpen" size="780px" :title="groupTitle" append-to-body destroy-on-close>
|
||||
<el-drawer v-model="groupOpen" size="860px" :title="groupTitle" append-to-body destroy-on-close>
|
||||
<div v-loading="groupLoading">
|
||||
<el-table :data="groupRows" border height="calc(100vh - 220px)" :empty-text="emptyGroup">
|
||||
<div class="toolbar">
|
||||
<el-input
|
||||
v-model="groupQuery.countryKeyword"
|
||||
clearable
|
||||
:placeholder="phCountry"
|
||||
style="width: 220px"
|
||||
@keyup.enter="searchGroup"
|
||||
/>
|
||||
<el-button type="primary" @click="searchGroup">查询</el-button>
|
||||
<el-button @click="resetGroupQuery">重置</el-button>
|
||||
</div>
|
||||
<el-table
|
||||
:data="displayGroupRows"
|
||||
border
|
||||
height="calc(100vh - 260px)"
|
||||
:empty-text="emptyGroup"
|
||||
:span-method="groupSpanMethod"
|
||||
>
|
||||
<el-table-column :label="colCountry" min-width="140" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ countryOf(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colZone" min-width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ zoneOf(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colWeight" min-width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ weightBandOf(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colAction" width="90" align="center">
|
||||
<template #default="{ row }">{{ actionLabel(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colChangeSummary" min-width="220" show-overflow-tooltip>
|
||||
<el-table-column :label="colChangeSummary" min-width="240" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ changeSummary(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colStatus" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="statusTag(row.status)">{{ statusLabel(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colTime" width="170" align="center" prop="updateTime" />
|
||||
<el-table-column :label="colOps" width="90" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
@@ -101,6 +118,8 @@
|
||||
<el-descriptions-item :label="lbTime">{{ detail.updateTime || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="lbProduct">{{ productCodeOf(detail) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="lbCountry">{{ countryOf(detail) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="lbWeight">{{ weightBandOf(detail) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="lbZone">{{ zoneOf(detail) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div class="change-panel">
|
||||
@@ -133,7 +152,7 @@ const visible = computed({
|
||||
|
||||
const loading = ref(false)
|
||||
const allGroups = ref([])
|
||||
const query = reactive({ status: undefined })
|
||||
const query = reactive({ productKeyword: '' })
|
||||
const activeTime = ref('')
|
||||
|
||||
const groupOpen = ref(false)
|
||||
@@ -142,14 +161,15 @@ const groupRows = ref([])
|
||||
const groupTotal = ref(0)
|
||||
const groupTitle = ref('')
|
||||
const activeProduct = ref(null)
|
||||
const groupQuery = reactive({ pageNum: 1, pageSize: 20 })
|
||||
const groupQuery = reactive({ pageNum: 1, pageSize: 20, countryKeyword: '' })
|
||||
|
||||
const detailOpen = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const detail = ref(null)
|
||||
const detailTitle = ref('')
|
||||
|
||||
const phStatus = '状态'
|
||||
const phProduct = '产品代码'
|
||||
const phCountry = '国家代码/名称'
|
||||
const emptyText = '该版本暂无价格变更记录'
|
||||
const emptyProducts = '该时间点暂无产品变更'
|
||||
const emptyGroup = '该产品暂无明细变更'
|
||||
@@ -159,6 +179,8 @@ const colAction = '操作'
|
||||
const colOperator = '操作人'
|
||||
const colProduct = '产品代码'
|
||||
const colCountry = '国家'
|
||||
const colZone = '分区'
|
||||
const colWeight = '公斤段'
|
||||
const colCount = '变更条数'
|
||||
const colChangeSummary = '变更摘要'
|
||||
const colStatus = '状态'
|
||||
@@ -173,6 +195,8 @@ const lbStatus = '状态'
|
||||
const lbTime = '写入时间'
|
||||
const lbProduct = '产品代码'
|
||||
const lbCountry = '国家'
|
||||
const lbWeight = '公斤段'
|
||||
const lbZone = '分区'
|
||||
|
||||
const statusOptions = [
|
||||
{ label: '成功', value: 'SUCCESS' },
|
||||
@@ -224,10 +248,12 @@ const timeTabs = computed(() => {
|
||||
const productRows = computed(() => {
|
||||
const minute = activeTime.value
|
||||
if (!minute) return []
|
||||
const kw = (query.productKeyword || '').trim().toLowerCase()
|
||||
const map = new Map()
|
||||
for (const g of allGroups.value || []) {
|
||||
if (toMinuteKey(g && g.updateTime) !== minute) continue
|
||||
const code = g.productCode || '-'
|
||||
if (kw && !String(code).toLowerCase().includes(kw)) continue
|
||||
let row = map.get(code)
|
||||
if (!row) {
|
||||
row = {
|
||||
@@ -253,7 +279,7 @@ function close() {
|
||||
}
|
||||
|
||||
function resetQuery() {
|
||||
query.status = undefined
|
||||
query.productKeyword = ''
|
||||
reload()
|
||||
}
|
||||
|
||||
@@ -295,6 +321,79 @@ function countryOf(row) {
|
||||
return keys.countryCode || keys.countryName || '-'
|
||||
}
|
||||
|
||||
function weightBandOf(row) {
|
||||
const keys = parseReq(row).bizKeys || {}
|
||||
if (keys.weightLabel) return String(keys.weightLabel)
|
||||
if (keys.weightMin != null || keys.weightMax != null) {
|
||||
const min = keys.weightMin != null ? String(keys.weightMin) : ''
|
||||
const max = keys.weightMax != null ? String(keys.weightMax) : ''
|
||||
return min + '<W≤' + max
|
||||
}
|
||||
return '-'
|
||||
}
|
||||
|
||||
function zoneOf(row) {
|
||||
const keys = parseReq(row).bizKeys || {}
|
||||
if (keys.zoneCode && keys.zoneName) return keys.zoneName + ' (' + keys.zoneCode + ')'
|
||||
return keys.zoneCode || keys.zoneName || '-'
|
||||
}
|
||||
|
||||
function countryKeyOf(row) {
|
||||
const keys = parseReq(row).bizKeys || {}
|
||||
return String(keys.countryCode || keys.countryName || '').toUpperCase()
|
||||
}
|
||||
|
||||
function zoneKeyOf(row) {
|
||||
const keys = parseReq(row).bizKeys || {}
|
||||
return String(keys.zoneCode || keys.zoneName || '')
|
||||
}
|
||||
|
||||
function weightKeyOf(row) {
|
||||
const keys = parseReq(row).bizKeys || {}
|
||||
if (keys.weightLabel) return String(keys.weightLabel)
|
||||
return String(keys.weightMin ?? '') + '~' + String(keys.weightMax ?? '')
|
||||
}
|
||||
|
||||
const displayGroupRows = computed(() => {
|
||||
const list = (groupRows.value || []).slice()
|
||||
list.sort((a, b) => {
|
||||
const ca = countryKeyOf(a)
|
||||
const cb = countryKeyOf(b)
|
||||
if (ca !== cb) return ca.localeCompare(cb)
|
||||
const za = zoneKeyOf(a)
|
||||
const zb = zoneKeyOf(b)
|
||||
if (za !== zb) return za.localeCompare(zb)
|
||||
return weightKeyOf(a).localeCompare(weightKeyOf(b))
|
||||
})
|
||||
return list
|
||||
})
|
||||
|
||||
function buildSpans(rows, keyOf) {
|
||||
const spans = new Array(rows.length).fill(1)
|
||||
let i = 0
|
||||
while (i < rows.length) {
|
||||
let j = i + 1
|
||||
const key = keyOf(rows[i])
|
||||
while (j < rows.length && keyOf(rows[j]) === key) j++
|
||||
spans[i] = j - i
|
||||
for (let k = i + 1; k < j; k++) spans[k] = 0
|
||||
i = j
|
||||
}
|
||||
return spans
|
||||
}
|
||||
|
||||
const countrySpans = computed(() => buildSpans(displayGroupRows.value, countryKeyOf))
|
||||
|
||||
function spanOf(spans, rowIndex) {
|
||||
const rowspan = spans[rowIndex] || 0
|
||||
return rowspan > 0 ? { rowspan, colspan: 1 } : { rowspan: 0, colspan: 0 }
|
||||
}
|
||||
|
||||
function groupSpanMethod({ columnIndex, rowIndex }) {
|
||||
if (columnIndex === 0) return spanOf(countrySpans.value, rowIndex)
|
||||
return { rowspan: 1, colspan: 1 }
|
||||
}
|
||||
|
||||
function changesOf(row) {
|
||||
const req = parseReq(row)
|
||||
return Array.isArray(req.changes) ? req.changes : []
|
||||
@@ -302,13 +401,19 @@ function changesOf(row) {
|
||||
|
||||
function changeSummary(row) {
|
||||
const list = changesOf(row)
|
||||
if (!list.length) return '-'
|
||||
return list.map(c => {
|
||||
const label = (c && c.fieldLabel) || (c && c.field) || ''
|
||||
const oldV = c && c.oldValue != null ? c.oldValue : ''
|
||||
const newV = c && c.newValue != null ? c.newValue : ''
|
||||
return label + ': ' + oldV + ' -> ' + newV
|
||||
}).join(';')
|
||||
const band = weightBandOf(row)
|
||||
const pricePart = !list.length
|
||||
? '-'
|
||||
: list.map(c => {
|
||||
const label = (c && c.fieldLabel) || (c && c.field) || ''
|
||||
const oldV = c && c.oldValue != null ? c.oldValue : ''
|
||||
const newV = c && c.newValue != null ? c.newValue : ''
|
||||
return label + ': ' + oldV + ' -> ' + newV
|
||||
}).join(';')
|
||||
if (band && band !== '-') {
|
||||
return '公斤段 ' + band + (pricePart === '-' ? '' : ':' + pricePart)
|
||||
}
|
||||
return pricePart
|
||||
}
|
||||
|
||||
function statusLabel(status) {
|
||||
@@ -333,7 +438,6 @@ async function reload() {
|
||||
const res = await listPriceLogGroups({
|
||||
pageNum: 1,
|
||||
pageSize: 500,
|
||||
status: query.status,
|
||||
psvId,
|
||||
priceKind: 'SALES'
|
||||
})
|
||||
@@ -354,10 +458,22 @@ function openProduct(row) {
|
||||
activeProduct.value = row
|
||||
groupTitle.value = '变更明细 · ' + (row.productCode || '-') + (activeTime.value ? ' @ ' + activeTime.value : '')
|
||||
groupQuery.pageNum = 1
|
||||
groupQuery.countryKeyword = ''
|
||||
groupOpen.value = true
|
||||
reloadGroup()
|
||||
}
|
||||
|
||||
function searchGroup() {
|
||||
groupQuery.pageNum = 1
|
||||
reloadGroup()
|
||||
}
|
||||
|
||||
function resetGroupQuery() {
|
||||
groupQuery.countryKeyword = ''
|
||||
groupQuery.pageNum = 1
|
||||
reloadGroup()
|
||||
}
|
||||
|
||||
async function reloadGroup() {
|
||||
const psvId = props.version && props.version.psvId
|
||||
const p = activeProduct.value
|
||||
@@ -368,14 +484,15 @@ async function reloadGroup() {
|
||||
}
|
||||
groupLoading.value = true
|
||||
try {
|
||||
const kw = (groupQuery.countryKeyword || '').trim()
|
||||
const res = await listPriceLog({
|
||||
pageNum: groupQuery.pageNum,
|
||||
pageSize: groupQuery.pageSize,
|
||||
status: query.status,
|
||||
psvId,
|
||||
priceKind: 'SALES',
|
||||
productCode: p.productCode === '-' ? undefined : p.productCode,
|
||||
updateTimePrefix: activeTime.value || p.updateTimePrefix || undefined
|
||||
updateTimePrefix: activeTime.value || p.updateTimePrefix || undefined,
|
||||
countryKeyword: kw || undefined
|
||||
})
|
||||
groupRows.value = res.rows || []
|
||||
groupTotal.value = Number(res.total) || 0
|
||||
|
||||
@@ -12,10 +12,10 @@
|
||||
</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" />
|
||||
<el-tab-pane :label="tabLabel('3')" name="3" />
|
||||
<el-tab-pane :label="tabLabel('pending')" name="pending" />
|
||||
<el-tab-pane :label="tabLabel('1')" name="1" />
|
||||
<el-tab-pane :label="tabLabel('2')" name="2" />
|
||||
<el-tab-pane :label="tabLabel('del')" name="del" />
|
||||
</el-tabs>
|
||||
|
||||
<el-table :data="filteredVersions" border empty-text="暂无版本">
|
||||
@@ -32,9 +32,12 @@
|
||||
</el-table-column>
|
||||
<el-table-column :label="colStatus" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="versionStatusTag(row.status)" size="small">{{ versionStatusLabel(row.status) }}</el-tag>
|
||||
<el-tag :type="isDeletedVersion(row) ? 'danger' : versionStatusTag(row.status)" size="small">{{ statusText(row) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colCreate" min-width="170" align="center">
|
||||
<template #default="{ row }">{{ row.createTime || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colRows" width="100" align="center">
|
||||
<template #default="{ row }">{{ detailCount(row) }}</template>
|
||||
</el-table-column>
|
||||
@@ -49,27 +52,27 @@
|
||||
@click="emit('view-price-log', row)"
|
||||
>变更记录</el-button>
|
||||
<el-button
|
||||
v-if="showEditButton(row)"
|
||||
v-if="!isDeletedVersion(row) && showEditButton(row)"
|
||||
link
|
||||
type="primary"
|
||||
@click="emit('edit', row)"
|
||||
>编辑</el-button>
|
||||
<el-button
|
||||
v-if="isDraftVersion(row)"
|
||||
v-if="!isDeletedVersion(row) && isDraftVersion(row)"
|
||||
link
|
||||
type="success"
|
||||
@click="emit('activate', row)"
|
||||
v-hasPermi="editPerm"
|
||||
>启动</el-button>
|
||||
<el-button
|
||||
v-if="isPendingVersion(row)"
|
||||
v-if="!isDeletedVersion(row) && isPendingVersion(row)"
|
||||
link
|
||||
type="primary"
|
||||
@click="emit('reschedule', row)"
|
||||
v-hasPermi="editPerm"
|
||||
>修改生效时间</el-button>
|
||||
<el-button
|
||||
v-if="isDraftVersion(row) || isPendingVersion(row) || isEffectVersion(row)"
|
||||
v-if="!isDeletedVersion(row) && (isDraftVersion(row) || isPendingVersion(row) || isEffectVersion(row))"
|
||||
link
|
||||
type="danger"
|
||||
:loading="isRemoving(row)"
|
||||
@@ -102,6 +105,7 @@ const emit = defineEmits(['update:modelValue', 'create-version', 'open-details',
|
||||
|
||||
const title = '套餐版本'
|
||||
const colVer = '版本号'
|
||||
const colCreate = '创建时间'
|
||||
const colEff = '生效时间'
|
||||
const colExp = '失效时间'
|
||||
const colStatus = '状态'
|
||||
@@ -109,28 +113,42 @@ const colRows = '明细行数'
|
||||
const colAction = '操作'
|
||||
const statusTab = ref('all')
|
||||
|
||||
function isDeletedVersion(row) {
|
||||
return !!row && String(row.isDel) === '1'
|
||||
}
|
||||
|
||||
function statusText(row) {
|
||||
if (isDeletedVersion(row)) return '已删除'
|
||||
return versionStatusLabel(row.status)
|
||||
}
|
||||
|
||||
const activeVersions = computed(() => (props.versions || []).filter(v => !isDeletedVersion(v)))
|
||||
const deletedVersions = computed(() => (props.versions || []).filter(v => isDeletedVersion(v)))
|
||||
|
||||
const counts = computed(() => {
|
||||
const map = { all: 0, '0': 0, '3': 0, '1': 0, '2': 0 }
|
||||
;(props.versions || []).forEach(v => {
|
||||
const map = { all: 0, pending: 0, '1': 0, '2': 0, del: 0 }
|
||||
activeVersions.value.forEach(v => {
|
||||
map.all++
|
||||
const s = String(v.status == null ? '' : v.status)
|
||||
if (map[s] != null) map[s]++
|
||||
if (s === '0' || s === '3') map.pending++
|
||||
else if (map[s] != null) map[s]++
|
||||
})
|
||||
map.del = deletedVersions.value.length
|
||||
return map
|
||||
})
|
||||
|
||||
function tabLabel(key) {
|
||||
const labels = {
|
||||
all: '全部',
|
||||
'0': '草稿',
|
||||
'3': '待生效',
|
||||
pending: '待生效',
|
||||
'1': '已生效',
|
||||
'2': '已失效'
|
||||
'2': '已失效',
|
||||
del: '已删除'
|
||||
}
|
||||
return labels[key] + ' (' + (counts.value[key] || 0) + ')'
|
||||
}
|
||||
|
||||
const hasDraft = computed(() => (props.versions || []).some(v => isDraftVersion(v)))
|
||||
const hasDraft = computed(() => activeVersions.value.some(v => isDraftVersion(v)))
|
||||
|
||||
function toEffMs(v) {
|
||||
if (!v || !v.effectiveTime) return NaN
|
||||
@@ -166,10 +184,21 @@ function sortVersions(list) {
|
||||
}
|
||||
|
||||
const filteredVersions = computed(() => {
|
||||
const list = props.versions || []
|
||||
const filtered = statusTab.value === 'all'
|
||||
? list
|
||||
: list.filter(v => String(v.status) === statusTab.value)
|
||||
if (statusTab.value === 'del') {
|
||||
return sortVersions(deletedVersions.value)
|
||||
}
|
||||
const list = activeVersions.value
|
||||
let filtered
|
||||
if (statusTab.value === 'all') {
|
||||
filtered = list
|
||||
} else if (statusTab.value === 'pending') {
|
||||
filtered = list.filter(v => {
|
||||
const s = String(v.status)
|
||||
return s === '0' || s === '3'
|
||||
})
|
||||
} else {
|
||||
filtered = list.filter(v => String(v.status) === statusTab.value)
|
||||
}
|
||||
return sortVersions(filtered)
|
||||
})
|
||||
|
||||
|
||||
@@ -299,6 +299,7 @@ function findPrevVersionEffectiveTime(row) {
|
||||
let prev = null
|
||||
for (const v of versions) {
|
||||
if (!v || v.psvId == null || String(v.psvId) === String(row.psvId)) continue
|
||||
if (String(v.isDel) === '1') continue
|
||||
const vs = Number(v.versionSeq) || 0
|
||||
if (vs >= curSeq || !v.effectiveTime) continue
|
||||
if (!prev || vs > (Number(prev.versionSeq) || 0)) prev = v
|
||||
@@ -312,6 +313,7 @@ function collectActivateBoundaryTimes(excludePsvId) {
|
||||
const seen = new Set()
|
||||
for (const v of set.value.versions || []) {
|
||||
if (!v || v.psvId == null || String(v.psvId) === String(excludePsvId)) continue
|
||||
if (String(v.isDel) === '1') continue
|
||||
;[v.effectiveTime, v.expireTime].forEach(t => {
|
||||
const n = t ? normalizeToHour(t) : ''
|
||||
if (!n || seen.has(n)) return
|
||||
@@ -449,7 +451,7 @@ 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')
|
||||
const hasDraft = (set.value.versions || []).some(v => v && String(v.isDel) !== '1' && String(v.status) === '0')
|
||||
if (hasDraft) {
|
||||
proxy.$modal.msgWarning('已有草稿版本,请先启动或删除后再新建')
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user