修改价格套餐版本

This commit is contained in:
wuweihua
2026-09-17 10:41:19 +08:00
parent f76c90643e
commit 5fe2351990
21 changed files with 668 additions and 116 deletions
@@ -65,6 +65,7 @@
<el-tag size="small" :type="sourceTagType(detail.matchSource)">{{ sourceLabel(detail.matchSource) }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="套餐">{{ priceSetLabel(detail) }}</el-descriptions-item>
<el-descriptions-item label="套餐版本">{{ detail.versionNo || (detail.psvId ? ('psv=' + detail.psvId) : '-') }}</el-descriptions-item>
<el-descriptions-item label="币种">
<dict-tag :options="bms_currency" :value="displayCurrency(detail.currency)" />
</el-descriptions-item>
@@ -101,6 +101,7 @@
<el-descriptions-item label="套餐编号">{{ result.priceSetCode || '-' }}</el-descriptions-item>
<el-descriptions-item label="套餐名称">{{ result.priceSetName || '-' }}</el-descriptions-item>
<el-descriptions-item label="套餐类型">{{ result.setType || '-' }}</el-descriptions-item>
<el-descriptions-item label="套餐版本">{{ result.versionNo || (result.psvId ? ('psv=' + result.psvId) : '-') }}</el-descriptions-item>
<el-descriptions-item label="币种">
<dict-tag :options="bms_currency" :value="displayCurrency(result.currency)" />
</el-descriptions-item>
@@ -107,7 +107,7 @@ const editInputRefs = {}
const title = computed(() => {
const code = props.productCode || ''
const name = props.productName || ''
return code + ' �� ' + name + ' �� 国家服务价格'
return code + ' · ' + name + ' · 国家服务价格'
})
const hint = '黄色行=已改价;点编辑图标后修改,失焦即刻写入数据库。单位:运费 RMB/KG,挂号费 RMB/票。'
const phSearch = '搜索国家/分区'
@@ -0,0 +1,136 @@
<template>
<div class="price-edit-panel" v-loading="loading">
<div class="panel-head">
<div class="panel-title">
<el-button link type="primary" @click="close">
<el-icon><arrow-left /></el-icon>
返回详情
</el-button>
<i class="panel-bar" />
<span>{{ title }}</span>
</div>
<div class="panel-actions">
<el-button @click="close">关闭</el-button>
<el-button type="primary" :disabled="!priceSetId" @click="close">完成</el-button>
</div>
</div>
<div class="panel-body">
<step-details
v-if="loaded"
:details="details"
:price-set-id="priceSetId"
:set-type="setType"
:effective-time="version.effectiveTime"
:version-no="version.versionNo"
:ref-data="refData"
header-title="编辑产品价格"
/>
</div>
</div>
</template>
<script setup name="PriceEditPanel">
import { ArrowLeft } from '@element-plus/icons-vue'
import StepDetails from './StepDetails.vue'
import { usePriceSetRefData, useDetailCascade } from '../../composables/usePriceSetRefData'
import { getPriceSet } from '@/api/price/set'
const props = defineProps({
priceSetId: { type: [Number, String], default: undefined },
psvId: { type: [Number, String], default: undefined }
})
const emit = defineEmits(['close'])
const { proxy } = getCurrentInstance()
const refData = usePriceSetRefData()
const cascade = useDetailCascade(refData)
const loading = ref(false)
const loaded = ref(false)
const setType = ref('FIXED')
const version = reactive({ psvId: undefined, versionNo: 'V1', effectiveTime: undefined })
const details = ref([])
const title = computed(() => '编辑产品价格 · ' + (version.versionNo || ''))
function load() {
const id = props.priceSetId
if (!id) {
proxy.$modal.msgError('缺少套餐ID')
return
}
loading.value = true
loaded.value = false
getPriceSet(id).then(res => {
const data = res.data || {}
setType.value = data.setType || 'FIXED'
const versions = data.versions || []
const qPsv = props.psvId
const byQuery = qPsv != null ? versions.find(v => String(v.psvId) === String(qPsv)) : null
const current = byQuery || data.version || versions[0] || {}
version.psvId = current.psvId
version.versionNo = current.versionNo || 'V1'
version.effectiveTime = current.effectiveTime
const src = (current.details && current.details.length) ? current.details : (data.details || [])
details.value = src.map(d => cascade.hydrateRow(d, setType.value))
loaded.value = true
}).finally(() => {
loading.value = false
})
}
function close() {
emit('close')
}
onMounted(() => {
refData.loadProducts().finally(load)
})
</script>
<style scoped>
.price-edit-panel {
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 8px;
margin-bottom: 16px;
}
.panel-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px 16px;
border-bottom: 1px solid #ebeef5;
}
.panel-title {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.panel-bar {
width: 3px;
height: 14px;
border-radius: 2px;
background: #1890ff;
flex: none;
}
.panel-title span {
font-size: 15px;
font-weight: 600;
color: #303133;
}
.panel-actions {
flex: none;
display: flex;
align-items: center;
gap: 8px;
}
.panel-body {
padding: 16px;
}</style>
@@ -105,7 +105,8 @@ const props = defineProps({
setType: { type: String, default: 'FIXED' },
effectiveTime: { type: String, default: undefined },
versionNo: { type: String, default: '' },
refData: { type: Object, required: true }
refData: { type: Object, required: true },
headerTitle: { type: String, default: '第 2 步 · 编辑产品价格' }
})
const { proxy } = getCurrentInstance()
@@ -113,7 +114,6 @@ const { bms_tax_mode } = useDict('bms_tax_mode')
const { hydrateRow } = useDetailCascade(props.refData)
const loadingText = '正在全量拉取产品价格…'
const headerTitle = '第 2 步 · 编辑产品价格'
const phKeyword = '产品代码/名称'
const phTax = '全部税模式'
const colCode = '产品代码'
@@ -9,10 +9,26 @@
<div class="drawer-body" v-loading="loading">
<div class="toolbar">
<el-input
v-model="query.keyword"
v-model="query.productKeyword"
clearable
:placeholder="phSearch"
style="width: 280px"
:placeholder="phProduct"
style="width: 160px"
@keyup.enter="search"
/>
<el-input
v-if="isFixed"
v-model="query.countryKeyword"
clearable
:placeholder="phCountry"
style="width: 140px"
@keyup.enter="search"
/>
<el-input
v-if="isFixed"
v-model="query.zoneKeyword"
clearable
:placeholder="phZone"
style="width: 140px"
@keyup.enter="search"
/>
<el-button type="primary" @click="search">查询</el-button>
@@ -111,10 +127,14 @@ const total = ref(0)
const query = reactive({
pageNum: 1,
pageSize: 20,
keyword: ''
productKeyword: '',
countryKeyword: '',
zoneKeyword: ''
})
const phSearch = '产品/国家/分区/重量段'
const phProduct = '产品代码/名称'
const phCountry = '国家'
const phZone = '分区'
const colProduct = '产品'
const colCountry = '国家'
const colZone = '分区'
@@ -147,7 +167,9 @@ function search() {
}
function reset() {
query.keyword = ''
query.productKeyword = ''
query.countryKeyword = ''
query.zoneKeyword = ''
query.pageNum = 1
load()
}
@@ -162,7 +184,9 @@ function load() {
loading.value = true
listSetDetail({
psvId,
keyword: query.keyword || undefined,
productKeyword: query.productKeyword || undefined,
countryKeyword: isFixed.value ? (query.countryKeyword || undefined) : undefined,
zoneKeyword: isFixed.value ? (query.zoneKeyword || undefined) : undefined,
pageNum: query.pageNum,
pageSize: query.pageSize
}).then(res => {
@@ -178,7 +202,9 @@ watch(
([open]) => {
if (open) {
query.pageNum = 1
query.keyword = ''
query.productKeyword = ''
query.countryKeyword = ''
query.zoneKeyword = ''
load()
}
}
@@ -197,6 +223,7 @@ watch(
gap: 8px;
margin-bottom: 12px;
flex-wrap: wrap;
align-items: center;
}
.pager {
margin-top: 12px;
+36 -6
View File
@@ -24,7 +24,7 @@
<el-button v-if="active > 0 && !directStep2" @click="active--">上一步</el-button>
<el-button v-if="active === 0" type="primary" :loading="creating" @click="goStep2">创建套餐价格</el-button>
<el-button v-if="active === 0" type="info" :loading="saving" @click="saveDraft">保存草稿</el-button>
<el-button v-if="active === 1" type="primary" :loading="saving" @click="saveDraft">保存</el-button>
<el-button v-if="active === 1" type="primary" @click="finishEdit">完成</el-button>
</div>
<el-dialog
@@ -97,6 +97,7 @@ function mapSaveProgressMessage(message) {
if (m === 'saving version') return '保存版本'
if (m.startsWith('saving details ')) return '保存明细 ' + m.slice('saving details '.length)
if (m === 'saving surcharges') return '保存附加费'
if (m === 'details unchanged') return '明细未变更'
if (m === 'saving customers') return '保存客户'
if (m === 'finishing') return '收尾中'
if (m === 'done') return '完成'
@@ -169,6 +170,25 @@ async function goStep2() {
}
}
/** Step2: prices already live-saved via PUT /detail; go to detail page. */
function finishEdit() {
if (!form.priceSetId) {
goBack()
return
}
proxy.$modal.msgSuccess('修改已实时落库')
const obj = {
path: '/customer/setDetail',
query: { priceSetId: form.priceSetId }
}
if (proxy.$tab) {
proxy.$tab.closeOpenPage(obj)
} else {
proxy.$router.push(obj)
}
}
/** Step0 only: persist basic draft (no detail wipe). */
async function saveDraft() {
const validBasic = await basicRef.value.validate()
if (!validBasic) {
@@ -190,7 +210,11 @@ async function saveDraft() {
function buildPayload() {
const { basic, version } = form
return {
const detailRows = form.details.filter(d => d && d.productCode)
// Price edits already persist via PUT /detail; product remove uses DELETE.
// Omit details when all rows already have psdId so save only updates header/version.
const hasUnsavedDetail = detailRows.some(d => d.psdId == null)
const payload = {
priceSetId: form.priceSetId,
priceSetName: basic.priceSetName,
setType: basic.setType,
@@ -199,11 +223,17 @@ function buildPayload() {
version: {
psvId: copyMode.value ? undefined : version.psvId,
versionNo: version.versionNo
// no effectiveTime: set on activate
},
details: form.details.filter(d => d.productCode).map(toDetailPayload),
customers: form.customers.filter(c => c.customerCode).map(toCustomerPayload)
}
}
if (hasUnsavedDetail) {
payload.details = detailRows.map(toDetailPayload)
}
// Customer bind is managed on detail page APIs; only send when present to avoid wipe.
const customers = form.customers.filter(c => c && c.customerCode).map(toCustomerPayload)
if (customers.length) {
payload.customers = customers
}
return payload
}
function goBack() {
+44 -28
View File
@@ -1,31 +1,41 @@
<template>
<div class="app-container" v-loading="loading">
<set-detail-header
:set="set"
@back="goList"
@disable="onDisable"
@copy="onCopy"
<!-- 编辑模式:整屏切换,等同独立页面 -->
<price-edit-panel
v-if="editVisible"
:price-set-id="set.priceSetId"
:psv-id="editPsvId"
@close="closeEdit"
/>
<set-basic-info-card :set="set" />
<template v-else>
<set-detail-header
:set="set"
@back="goList"
@disable="onDisable"
@copy="onCopy"
/>
<set-customer-table :set="set" />
<set-basic-info-card :set="set" />
<set-version-table
v-model="currentPsvId"
:versions="set.versions || []"
@copy-version="onCopyVersion"
@open-details="onOpenVersionDetails"
@activate="onActivateVersion"
@edit="onEditVersion"
@remove="onRemoveVersion"
/>
<set-customer-table :set="set" />
<set-version-detail-drawer
v-model="detailDrawerVisible"
:version="detailVersion"
:set-type="set.setType"
/>
<set-version-table
v-model="currentPsvId"
:versions="set.versions || []"
@copy-version="onCopyVersion"
@open-details="onOpenVersionDetails"
@activate="onActivateVersion"
@edit="onEditVersion"
@remove="onRemoveVersion"
/>
<set-version-detail-drawer
v-model="detailDrawerVisible"
:version="detailVersion"
:set-type="set.setType"
/>
</template>
</div>
</template>
@@ -35,6 +45,7 @@ import SetBasicInfoCard from './components/detail/SetBasicInfoCard.vue'
import SetCustomerTable from './components/detail/SetCustomerTable.vue'
import SetVersionTable from './components/detail/SetVersionTable.vue'
import SetVersionDetailDrawer from './components/detail/SetVersionDetailDrawer.vue'
import PriceEditPanel from './components/create/PriceEditPanel.vue'
import { usePriceSetRefData } from './composables/usePriceSetRefData'
import {
getPriceSet, disablePriceSet, copyPriceSet, activatePriceSetVersion, copyPriceSetVersion, deletePriceSetVersion
@@ -49,6 +60,8 @@ const set = ref({})
const currentPsvId = ref()
const detailDrawerVisible = ref(false)
const detailVersion = ref(null)
const editVisible = ref(false)
const editPsvId = ref(undefined)
function onOpenVersionDetails(row) {
detailVersion.value = row || null
@@ -102,10 +115,8 @@ function onCopyVersion(row) {
const newPsvId = res.data
load()
if (newPsvId != null) {
proxy.$router.push({
path: '/customer/setCreate',
query: { priceSetId: set.value.priceSetId, psvId: newPsvId, step: '2' }
})
editPsvId.value = newPsvId
editVisible.value = true
}
}).catch(() => {})
}
@@ -119,9 +130,14 @@ function onRemoveVersion(row) {
}).catch(() => {})
}
function onEditVersion(row) {
const q = { priceSetId: set.value.priceSetId, step: '2' }
if (row && row.psvId != null) q.psvId = row.psvId
proxy.$router.push({ path: '/customer/setCreate', query: q })
editPsvId.value = row && row.psvId != null ? row.psvId : undefined
editVisible.value = true
}
/** 关闭内嵌编辑面板,刷新版本列表(明细数、状态可能变化) */
function closeEdit() {
editVisible.value = false
load()
}
function goList() {
const obj = { path: '/customer/set' }
+43 -1
View File
@@ -33,6 +33,16 @@
<el-tag :type="statusTag(row)" size="small">{{ statusLabel(row) }}</el-tag>
</template>
<template #col-activeDetail="{ row }">
<el-button
link
type="primary"
:loading="activeLoadingId === row.priceSetId"
@click="handleActiveDetail(row)"
v-hasPermi="['price:set:query']"
>查看明细</el-button>
</template>
<template #col-action="{ row }">
<el-button link type="primary" @click="handleDetail(row)" v-hasPermi="['price:set:query']">详情</el-button>
<el-button
@@ -44,14 +54,21 @@
</template>
</basic-table>
<set-version-detail-drawer
v-model="drawerVisible"
:version="activeVersion"
:set-type="drawerSetType"
/>
</div>
</template>
<script setup name="PriceSet">
import {
listPriceSet, disablePriceSet, copyPriceSet
listPriceSet, disablePriceSet, copyPriceSet, getPriceSet
} from '@/api/price/set'
import { parseTime } from '@/utils/ruoyi'
import SetVersionDetailDrawer from './components/detail/SetVersionDetailDrawer.vue'
const { proxy } = getCurrentInstance()
const tableRef = ref()
@@ -88,6 +105,7 @@ const columns = [
{ label: '套餐名称', prop: 'priceSetName', minWidth: 160 },
{ label: '类型', prop: 'setType', width: 140 },
{ label: '说明', prop: 'priceSetNote', minWidth: 160 },
{ label: '生效明细', prop: 'activeDetail', width: 100 },
{ label: '操作', prop: 'action', width: 160, showInToolbar: false }
]
@@ -119,6 +137,30 @@ function handleDetail(row) {
proxy.$router.push({ path: '/customer/setDetail', query: { priceSetId: row.priceSetId } })
}
/** 生效明细:拉详情找生效版本(status=1)弹抽屉 */
const drawerVisible = ref(false)
const activeVersion = ref(null)
const drawerSetType = ref('FIXED')
const activeLoadingId = ref(null)
function handleActiveDetail(row) {
activeLoadingId.value = row.priceSetId
getPriceSet(row.priceSetId).then(res => {
const data = res.data || {}
const versions = data.versions || []
const active = versions.find(v => String(v.status) === '1')
if (!active || active.psvId == null) {
proxy.$modal.msgWarning('该套餐暂无生效版本')
return
}
drawerSetType.value = row.setType
activeVersion.value = { psvId: active.psvId, versionNo: active.versionNo }
drawerVisible.value = true
}).finally(() => {
activeLoadingId.value = null
})
}
function refresh() {
tableRef.value?.refresh()