增加查看价格套餐历史版本的变更

This commit is contained in:
wuweihua
2026-11-05 18:28:57 +08:00
committed by caojia
parent 220b01127b
commit f57af555ea
11 changed files with 956 additions and 6 deletions
+9
View File
@@ -8,6 +8,15 @@ export function listPriceLog(query) {
})
}
/** Group by productCode + updateTime for a package version. */
export function listPriceLogGroups(query) {
return request({
url: '/monitor/priceLog/groups',
method: 'get',
params: query
})
}
export function getPriceLog(docId) {
return request({
url: '/monitor/priceLog/' + encodeURIComponent(docId),
@@ -0,0 +1,450 @@
<template>
<el-drawer
:model-value="visible"
:title="title"
size="900px"
append-to-body
destroy-on-close
@close="close"
>
<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-button type="primary" @click="reload">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</div>
<el-empty v-if="!timeTabs.length" :description="emptyText" />
<template v-else>
<el-tabs v-model="activeTime" class="time-tabs" @tab-change="onTimeTabChange">
<el-tab-pane
v-for="t in timeTabs"
:key="t.key"
:name="t.key"
:label="t.label"
/>
</el-tabs>
<el-table :data="productRows" border height="calc(100vh - 320px)" :empty-text="emptyProducts">
<el-table-column :label="colProduct" min-width="120" prop="productCode" show-overflow-tooltip />
<el-table-column :label="colAction" width="90" align="center">
<template #default="{ row }">{{ actionLabelOf(row.action) }}</template>
</el-table-column>
<el-table-column :label="colOperator" width="110" show-overflow-tooltip>
<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>
</template>
</el-table-column>
</el-table>
</template>
</div>
<el-drawer v-model="groupOpen" size="780px" :title="groupTitle" append-to-body destroy-on-close>
<div v-loading="groupLoading">
<el-table :data="groupRows" border height="calc(100vh - 220px)" :empty-text="emptyGroup">
<el-table-column :label="colCountry" min-width="140" show-overflow-tooltip>
<template #default="{ row }">{{ countryOf(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>
<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 }">
<el-button link type="primary" @click="handleView(row)">查看</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination
v-model:current-page="groupQuery.pageNum"
v-model:page-size="groupQuery.pageSize"
:total="groupTotal"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next"
@size-change="reloadGroup"
@current-change="reloadGroup"
/>
</div>
</div>
</el-drawer>
<el-drawer v-model="detailOpen" size="720px" :title="detailTitle" append-to-body destroy-on-close>
<div v-loading="detailLoading">
<el-empty v-if="!detail" :description="emptyDetail" />
<div v-else>
<el-descriptions :column="2" border size="small" class="mb12">
<el-descriptions-item :label="lbAction">{{ actionLabel(detail) }}</el-descriptions-item>
<el-descriptions-item :label="lbOperator">{{ operatorOf(detail) }}</el-descriptions-item>
<el-descriptions-item :label="lbStatus">
<el-tag size="small" :type="statusTag(detail.status)">{{ statusLabel(detail.status) }}</el-tag>
</el-descriptions-item>
<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>
<div class="change-panel">
<div class="change-panel__head">改前改后</div>
<el-table :data="changesOf(detail)" border size="small" :empty-text="emptyChanges">
<el-table-column prop="fieldLabel" :label="colField" min-width="120" />
<el-table-column prop="oldValue" :label="colOld" min-width="140" show-overflow-tooltip />
<el-table-column prop="newValue" :label="colNew" min-width="140" show-overflow-tooltip />
</el-table>
</div>
</div>
</div>
</el-drawer>
</el-drawer>
</template>
<script setup name="SetVersionPriceLogDrawer">
import { listPriceLog, listPriceLogGroups, getPriceLog } from '@/api/monitor/priceLog'
const props = defineProps({
modelValue: { type: Boolean, default: false },
version: { type: Object, default: null }
})
const emit = defineEmits(['update:modelValue'])
const visible = computed({
get: () => props.modelValue,
set: v => emit('update:modelValue', v)
})
const loading = ref(false)
const allGroups = ref([])
const query = reactive({ status: undefined })
const activeTime = ref('')
const groupOpen = ref(false)
const groupLoading = ref(false)
const groupRows = ref([])
const groupTotal = ref(0)
const groupTitle = ref('')
const activeProduct = ref(null)
const groupQuery = reactive({ pageNum: 1, pageSize: 20 })
const detailOpen = ref(false)
const detailLoading = ref(false)
const detail = ref(null)
const detailTitle = ref('')
const phStatus = '状态'
const emptyText = '该版本暂无价格变更记录'
const emptyProducts = '该时间点暂无产品变更'
const emptyGroup = '该产品暂无明细变更'
const emptyDetail = '暂无详情'
const emptyChanges = '无字段变更'
const colAction = '操作'
const colOperator = '操作人'
const colProduct = '产品代码'
const colCountry = '国家'
const colCount = '变更条数'
const colChangeSummary = '变更摘要'
const colStatus = '状态'
const colTime = '写入时间'
const colOps = '操作'
const colField = '字段'
const colOld = '改前'
const colNew = '改后'
const lbAction = '操作'
const lbOperator = '操作人'
const lbStatus = '状态'
const lbTime = '写入时间'
const lbProduct = '产品代码'
const lbCountry = '国家'
const statusOptions = [
{ label: '成功', value: 'SUCCESS' },
{ label: '失败', value: 'FAIL' }
]
const actionOptions = [
{ label: '新增', value: 'INSERT' },
{ label: '修改', value: 'UPDATE' },
{ label: '删除', value: 'DELETE' },
{ label: '导入', value: 'IMPORT' }
]
const title = computed(() => {
const v = props.version || {}
const ver = v.versionNo || ''
const eff = v.effectiveTime || ''
const base = '版本价格变更记录'
if (ver || eff) return base + ' · ' + (ver || '') + (eff ? ' (' + eff + ')' : '')
return base
})
/** Minute key: yyyy-MM-dd HH:mm */
function toMinuteKey(time) {
if (!time) return '-'
const s = String(time).trim()
return s.length >= 16 ? s.slice(0, 16) : s
}
const timeTabs = computed(() => {
const map = new Map()
for (const g of allGroups.value || []) {
const key = toMinuteKey(g && g.updateTime)
if (!map.has(key)) {
map.set(key, { key, label: key, changeCount: 0, productSet: new Set() })
}
const tab = map.get(key)
tab.changeCount += Number(g.changeCount) || 0
if (g.productCode) tab.productSet.add(g.productCode)
}
return Array.from(map.values())
.sort((a, b) => String(b.key).localeCompare(String(a.key)))
.map(t => ({
key: t.key,
label: t.key + ' (' + t.productSet.size + ')',
changeCount: t.changeCount
}))
})
const productRows = computed(() => {
const minute = activeTime.value
if (!minute) return []
const map = new Map()
for (const g of allGroups.value || []) {
if (toMinuteKey(g && g.updateTime) !== minute) continue
const code = g.productCode || '-'
let row = map.get(code)
if (!row) {
row = {
productCode: code,
action: g.action,
operator: g.operator,
status: g.status,
changeCount: 0,
updateTimePrefix: minute
}
map.set(code, row)
}
row.changeCount += Number(g.changeCount) || 0
if (g.status === 'FAIL') row.status = 'FAIL'
if (!row.operator && g.operator) row.operator = g.operator
if (!row.action && g.action) row.action = g.action
}
return Array.from(map.values()).sort((a, b) => String(a.productCode).localeCompare(String(b.productCode)))
})
function close() {
visible.value = false
}
function resetQuery() {
query.status = undefined
reload()
}
function onTimeTabChange() {
groupOpen.value = false
detailOpen.value = false
}
function parseReq(row) {
if (!row || !row.requestJson) return {}
try {
return typeof row.requestJson === 'object' ? row.requestJson : JSON.parse(row.requestJson)
} catch (e) {
return {}
}
}
function actionLabelOf(action) {
const found = actionOptions.find(i => i.value === action)
return found ? found.label : (action || '-')
}
function actionLabel(row) {
return actionLabelOf(parseReq(row).action)
}
function operatorOf(row) {
return parseReq(row).operator || '-'
}
function productCodeOf(row) {
const keys = parseReq(row).bizKeys || {}
return keys.productCode || '-'
}
function countryOf(row) {
const keys = parseReq(row).bizKeys || {}
if (keys.countryCode && keys.countryName) return keys.countryName + ' (' + keys.countryCode + ')'
return keys.countryCode || keys.countryName || '-'
}
function changesOf(row) {
const req = parseReq(row)
return Array.isArray(req.changes) ? req.changes : []
}
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(';')
}
function statusLabel(status) {
const found = statusOptions.find(item => item.value === status)
if (found) return found.label
return status === 'FAIL' ? '失败' : '成功'
}
function statusTag(status) {
return status === 'FAIL' ? 'danger' : 'success'
}
async function reload() {
const psvId = props.version && props.version.psvId
if (psvId == null) {
allGroups.value = []
activeTime.value = ''
return
}
loading.value = true
try {
const res = await listPriceLogGroups({
pageNum: 1,
pageSize: 500,
status: query.status,
psvId,
priceKind: 'SALES'
})
allGroups.value = res.rows || []
const tabs = timeTabs.value
if (!tabs.length) {
activeTime.value = ''
} else if (!tabs.some(t => t.key === activeTime.value)) {
activeTime.value = tabs[0].key
}
} finally {
loading.value = false
}
}
function openProduct(row) {
if (!row) return
activeProduct.value = row
groupTitle.value = '变更明细 · ' + (row.productCode || '-') + (activeTime.value ? ' @ ' + activeTime.value : '')
groupQuery.pageNum = 1
groupOpen.value = true
reloadGroup()
}
async function reloadGroup() {
const psvId = props.version && props.version.psvId
const p = activeProduct.value
if (psvId == null || !p) {
groupRows.value = []
groupTotal.value = 0
return
}
groupLoading.value = true
try {
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
})
groupRows.value = res.rows || []
groupTotal.value = Number(res.total) || 0
} finally {
groupLoading.value = false
}
}
function handleView(row) {
detailTitle.value = '价格变更 #' + (row.bizId || row.docId)
detailOpen.value = true
detailLoading.value = true
detail.value = null
getPriceLog(row.docId).then(res => {
detail.value = res.data || row
}).finally(() => {
detailLoading.value = false
})
}
watch(
() => [props.modelValue, props.version && props.version.psvId],
([open]) => {
if (open) {
activeTime.value = ''
reload()
} else {
groupOpen.value = false
detailOpen.value = false
detail.value = null
activeProduct.value = null
allGroups.value = []
}
}
)
</script>
<style scoped>
.price-log-drawer { min-height: 240px; }
.toolbar {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.time-tabs {
margin-bottom: 8px;
}
.time-tabs :deep(.el-tabs__header) {
margin-bottom: 8px;
}
.pager {
display: flex;
justify-content: flex-end;
margin-top: 12px;
}
.mb12 { margin-bottom: 12px; }
.change-panel {
margin-bottom: 12px;
border: 1px solid #e8ecf0;
border-radius: 6px;
overflow: hidden;
}
.change-panel__head {
padding: 8px 12px;
font-size: 13px;
font-weight: 600;
color: #606266;
background: #fafbfc;
border-bottom: 1px solid #ebeef5;
}
</style>
@@ -35,10 +35,16 @@
<el-table-column :label="colRows" width="100" align="center">
<template #default="{ row }">{{ detailCount(row) }}</template>
</el-table-column>
<el-table-column :label="colAction" width="460" align="center" fixed="right">
<el-table-column :label="colAction" width="540" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openDetails(row)">查看详情</el-button>
<el-button link type="primary" @click="emit('view-import-attach', row)">查看附件</el-button>
<el-button
link
type="primary"
v-hasPermi="['monitor:priceLog:list']"
@click="emit('view-price-log', row)"
>变更记录</el-button>
<el-button
v-if="showEditButton(row)"
link
@@ -89,7 +95,7 @@ const props = defineProps({
modelValue: { type: [Number, String], default: undefined },
removingPsvId: { type: [Number, String], default: null }
})
const emit = defineEmits(['update:modelValue', 'create-version', 'open-details', 'view-import-attach', 'activate', 'reschedule', 'edit', 'remove'])
const emit = defineEmits(['update:modelValue', 'create-version', 'open-details', 'view-import-attach', 'view-price-log', 'activate', 'reschedule', 'edit', 'remove'])
const title = '套餐版本'
const colEff = '生效时间'
+17
View File
@@ -27,6 +27,7 @@
@create-version="onCreateVersion"
@open-details="onOpenVersionDetails"
@view-import-attach="onViewImportAttach"
@view-price-log="onViewPriceLog"
@activate="onActivateVersion"
@reschedule="onRescheduleVersion"
@edit="onEditVersion"
@@ -39,6 +40,10 @@
:set-type="set.setType"
/>
<set-version-excel-file-drawer ref="versionExcelDrawerRef" />
<set-version-price-log-drawer
v-model="priceLogVisible"
:version="priceLogVersion"
/>
<el-dialog
v-model="activateVisible"
@@ -81,6 +86,7 @@ import SetCustomerTable from './components/detail/SetCustomerTable.vue'
import SetVersionTable from './components/detail/SetVersionTable.vue'
import SetVersionDetailDrawer from './components/detail/SetVersionDetailDrawer.vue'
import SetVersionExcelFileDrawer from './components/detail/SetVersionExcelFileDrawer.vue'
import SetVersionPriceLogDrawer from './components/detail/SetVersionPriceLogDrawer.vue'
import PriceEditPanel from './components/create/PriceEditPanel.vue'
import { usePriceSetRefData } from './composables/usePriceSetRefData'
import { usePriceSetModule } from './composables/usePriceSetModule'
@@ -102,6 +108,8 @@ const detailVersion = ref(null)
const versionExcelDrawerRef = ref()
const editVisible = ref(false)
const editPsvId = ref(undefined)
const priceLogVisible = ref(false)
const priceLogVersion = ref(null)
/** 已加载的套餐ID;页面被 keep-alive 缓存复用时据此判断是否需要重新加载 */
const loadedId = ref(null)
/** 从详情页跳到新建/编辑套餐版本页后,返回时套餐版本数据可能已变化,需强制刷新 */
@@ -115,6 +123,8 @@ function resetTransientState() {
detailVersion.value = null
editVisible.value = false
editPsvId.value = undefined
priceLogVisible.value = false
priceLogVersion.value = null
activateVisible.value = false
activateSubmitting.value = false
activateMode.value = 'activate'
@@ -134,6 +144,13 @@ function onViewImportAttach(row) {
versionExcelDrawerRef.value?.open?.(row)
}
function onViewPriceLog(row) {
if (!row || row.psvId == null) return
priceLogVersion.value = row
priceLogVisible.value = true
}
function load() {
const id = route.query.priceSetId
if (!id) return