增加试算功能

This commit is contained in:
wuweihua
2026-09-07 14:42:27 +08:00
parent a083a0dece
commit ea2bc6f33e
17 changed files with 1060 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
import request from '@/utils/request'
/** Quote trial for billing center */
export function quoteBilling(data) {
return request({
url: '/price/billing/quote',
method: 'post',
data
})
}
+268
View File
@@ -0,0 +1,268 @@
<template>
<div class="app-container billing-quote">
<el-row :gutter="16">
<el-col :xs="24" :lg="10">
<el-card shadow="never">
<template #header><span>试算参数</span></template>
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
<el-form-item label="客户ID" prop="customerId">
<el-input-number
v-model="form.customerId"
:min="1"
:controls="false"
placeholder="请输入客户ID"
style="width: 100%"
/>
<div class="form-hint">与套餐绑定客户ID一致;无套餐时走产品默认价</div>
</el-form-item>
<el-form-item label="产品" prop="productCode">
<el-select
v-model="form.productCode"
filterable
clearable
placeholder="请选择产品"
style="width: 100%"
@change="handleProductChange"
>
<el-option
v-for="item in productOptions"
:key="item.productCode"
:label="item.productName + ' (' + item.productCode + ')'"
:value="item.productCode"
/>
</el-select>
</el-form-item>
<el-form-item label="目的国/地区" prop="countryCode">
<el-select
v-model="form.countryCode"
filterable
clearable
placeholder="请先选择产品"
style="width: 100%"
:disabled="!form.productCode"
>
<el-option
v-for="item in countryOptions"
:key="item.countryCode + '-' + (item.detailId || item.weightMin)"
:label="countryOptionLabel(item)"
:value="item.countryCode"
/>
</el-select>
</el-form-item>
<el-form-item label="计费重量(kg)" prop="weight">
<el-input-number
v-model="form.weight"
:min="0.001"
:precision="3"
:step="0.1"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="业务日期" prop="bizDate">
<el-date-picker
v-model="form.bizDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="默认今天"
style="width: 100%"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" @click="handleQuote">计算价格</el-button>
<el-button @click="resetForm">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</el-col>
<el-col :xs="24" :lg="14">
<el-card shadow="never">
<template #header><span>试算结果</span></template>
<el-empty v-if="!result && !errorMsg" description="请输入参数后点击计算价格" />
<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="客户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>
<el-descriptions-item label="计费重量(kg)">{{ result.chargeWeight ?? result.weight }}</el-descriptions-item>
<el-descriptions-item label="重量段">{{ result.weightLabel || '-' }}</el-descriptions-item>
<el-descriptions-item label="计价来源">
<el-tag size="small" :type="sourceTagType(result.matchSource)">{{ sourceLabel(result.matchSource) }}</el-tag>
</el-descriptions-item>
<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.currency || 'RMB' }}</el-descriptions-item>
<el-descriptions-item label="运费单价">{{ formatMoney(result.freightRate) }}</el-descriptions-item>
<el-descriptions-item label="折扣前单价">{{ formatMoney(result.baseFreightRate) }}</el-descriptions-item>
<el-descriptions-item label="折扣%">{{ result.discountPercent != null ? result.discountPercent : '-' }}</el-descriptions-item>
<el-descriptions-item label="运费金额">{{ formatMoney(result.freightAmount) }}</el-descriptions-item>
<el-descriptions-item label="挂号费">{{ formatMoney(result.registrationFee) }}</el-descriptions-item>
<el-descriptions-item label="应付总额">
<span class="payable">{{ formatMoney(result.payableTotal) }}</span>
</el-descriptions-item>
<el-descriptions-item label="命中说明" :span="2">{{ result.matchMessage || '-' }}</el-descriptions-item>
</el-descriptions>
<div class="formula-tip">计算公式:运费金额 = 运费单价 × 计费重量;应付总额 = 运费金额 + 挂号费</div>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup name="BillingQuote">
import { quoteBilling } from '@/api/price/billing'
import { listProduct, listProductCountries } from '@/api/price/product'
const { proxy } = getCurrentInstance()
const formRef = ref()
const loading = ref(false)
const productOptions = ref([])
const countryOptions = ref([])
const result = ref(null)
const errorMsg = ref('')
const form = reactive({
customerId: undefined,
productCode: undefined,
countryCode: undefined,
weight: undefined,
bizDate: undefined
})
const rules = {
customerId: [{ required: true, message: '请输入客户ID', trigger: 'blur' }],
productCode: [{ required: true, message: '请选择产品', trigger: 'change' }],
countryCode: [{ required: true, message: '请选择目的国/地区', trigger: 'change' }],
weight: [{ required: true, message: '请输入计费重量', trigger: 'blur' }]
}
function loadProducts() {
listProduct({ pageNum: 1, pageSize: 500, status: '0' }).then(res => {
productOptions.value = res.rows || []
})
}
function uniqueCountries(list) {
const map = new Map()
;(list || []).forEach(item => {
if (item && item.countryCode && !map.has(item.countryCode)) {
map.set(item.countryCode, item)
}
})
return Array.from(map.values())
}
function countryOptionLabel(item) {
const name = item.countryName || item.countryCode
return name + ' (' + item.countryCode + ')'
}
function handleProductChange(productCode) {
form.countryCode = undefined
countryOptions.value = []
if (!productCode) {
return
}
listProductCountries(productCode).then(res => {
countryOptions.value = uniqueCountries(res.data || [])
})
}
function handleQuote() {
formRef.value.validate(valid => {
if (!valid) {
return
}
loading.value = true
result.value = null
errorMsg.value = ''
const payload = {
customerId: form.customerId,
productCode: form.productCode,
countryCode: form.countryCode,
weight: form.weight
}
if (form.bizDate) {
payload.bizDate = form.bizDate
}
quoteBilling(payload).then(res => {
result.value = res.data || null
if (!result.value) {
errorMsg.value = res.msg || '计算失败'
}
}).catch(err => {
errorMsg.value = (err && (err.msg || err.message)) || '计算失败'
}).finally(() => {
loading.value = false
})
})
}
function resetForm() {
form.customerId = undefined
form.productCode = undefined
form.countryCode = undefined
form.weight = undefined
form.bizDate = undefined
countryOptions.value = []
result.value = null
errorMsg.value = ''
proxy.$nextTick(() => {
formRef.value && formRef.value.clearValidate()
})
}
function formatMoney(val) {
if (val === null || val === undefined || val === '') {
return '-'
}
const num = Number(val)
if (Number.isNaN(num)) {
return val
}
return num.toFixed(4)
}
function sourceLabel(source) {
const map = {
PACKAGE_FIXED: '客户固定价套餐',
PACKAGE_DISCOUNT: '客户折扣套餐',
PRODUCT_DEFAULT: '产品默认价',
FIXED: '客户固定价套餐',
DISCOUNT: '客户折扣套餐'
}
return map[source] || source || '-'
}
function sourceTagType(source) {
if (source === 'PACKAGE_FIXED' || source === 'FIXED') return 'success'
if (source === 'PACKAGE_DISCOUNT' || source === 'DISCOUNT') return 'warning'
if (source === 'PRODUCT_DEFAULT') return 'info'
return ''
}
loadProducts()
</script>
<style scoped>
.billing-quote .form-hint {
margin-top: 4px;
color: #909399;
font-size: 12px;
line-height: 1.4;
}
.billing-quote .payable {
color: #f56c6c;
font-size: 18px;
font-weight: 600;
}
.billing-quote .formula-tip {
margin-top: 12px;
color: #909399;
font-size: 12px;
}
</style>