Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -8,3 +8,20 @@ export function quoteBilling(data) {
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/** Historical quote list */
|
||||
export function listQuote(query) {
|
||||
return request({
|
||||
url: '/price/billing/quote/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
/** Historical quote detail */
|
||||
export function getQuote(quoteId) {
|
||||
return request({
|
||||
url: '/price/billing/quote/' + quoteId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -17,11 +17,15 @@ export function getProduct(productId) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 查询产品国家明细 */
|
||||
export function listProductCountries(productCode) {
|
||||
/** 查询产品国家明细(优先 productId,绑定到具体产品版本行) */
|
||||
export function listProductCountries(productCode, effectiveDate, productId) {
|
||||
const params = {}
|
||||
if (effectiveDate) params.effectiveDate = effectiveDate
|
||||
if (productId) params.productId = productId
|
||||
return request({
|
||||
url: '/price/product/' + productCode + '/countries',
|
||||
method: 'get'
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,10 @@
|
||||
|
||||
<!-- 选项与提示 -->
|
||||
<div class="import-options">
|
||||
<el-checkbox v-if="showUpdateSupport" v-model="updateSupport" class="update-checkbox">{{ updateSupportLabel }}</el-checkbox>
|
||||
<el-checkbox v-if="showUpdateSupport" v-model="updateSupport" class="update-checkbox">
|
||||
<span class="update-label">{{ updateSupportLabel }}</span>
|
||||
</el-checkbox>
|
||||
<div v-if="showUpdateSupport && updateSupportHint" class="update-hint">{{ updateSupportHint }}</div>
|
||||
<div class="import-tips">
|
||||
<el-icon class="tip-icon"><Info-Filled /></el-icon>
|
||||
<div class="tip-text">
|
||||
@@ -105,6 +108,11 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: '是否更新已经存在的数据'
|
||||
},
|
||||
// 勾选框下方补充说明(长文案放这里,避免与复选框挤叠)
|
||||
updateSupportHint: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 打开时是否默认勾选“更新已存在”
|
||||
defaultUpdateSupport: {
|
||||
type: Boolean,
|
||||
@@ -350,8 +358,38 @@ defineExpose({ open })
|
||||
}
|
||||
|
||||
.update-checkbox {
|
||||
display: flex !important;
|
||||
align-items: flex-start;
|
||||
height: auto !important;
|
||||
white-space: normal;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.update-checkbox :deep(.el-checkbox__input) {
|
||||
margin-top: 3px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.update-checkbox :deep(.el-checkbox__label) {
|
||||
white-space: normal;
|
||||
line-height: 1.5;
|
||||
word-break: break-word;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.update-label {
|
||||
display: inline-block;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.update-hint {
|
||||
margin: 6px 0 0 22px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
line-height: 1.6;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.import-tips {
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
class="simple-table"
|
||||
v-loading="loading"
|
||||
:data="data"
|
||||
:row-key="rowKey"
|
||||
:border="border"
|
||||
:stripe="stripe"
|
||||
:size="size"
|
||||
:height="height"
|
||||
:max-height="maxHeight"
|
||||
:span-method="spanMethod"
|
||||
:empty-text="emptyText"
|
||||
:highlight-current-row="highlightCurrentRow"
|
||||
@selection-change="(val) => $emit('selection-change', val)"
|
||||
@row-click="(row, col, e) => $emit('row-click', row, col, e)"
|
||||
>
|
||||
<el-table-column v-if="selection" type="selection" width="55" align="center" />
|
||||
<el-table-column v-if="showIndex" type="index" :label="indexLabel" width="55" align="center" />
|
||||
|
||||
<el-table-column
|
||||
v-for="col in columns"
|
||||
:key="col.key || col.prop || col.label"
|
||||
:prop="col.prop"
|
||||
:label="col.label"
|
||||
:width="col.width"
|
||||
:min-width="col.minWidth"
|
||||
:align="col.align"
|
||||
:fixed="col.fixed"
|
||||
:sortable="col.sortable"
|
||||
:show-overflow-tooltip="col.showOverflowTooltip"
|
||||
>
|
||||
<template #header="scope">
|
||||
<slot :name="`header-${col.prop}`" v-bind="scope">{{ col.label }}</slot>
|
||||
</template>
|
||||
<template #default="scope">
|
||||
<slot :name="`col-${col.slot || col.prop}`" v-bind="scope" :row="scope.row">
|
||||
<dict-tag
|
||||
v-if="col.dict"
|
||||
:options="getDictOptions(col.dict)"
|
||||
:value="scope.row[col.prop]"
|
||||
/>
|
||||
<span v-else-if="col.formatter" :class="col.className">{{ col.formatter(scope.row, col, scope.row[col.prop], scope.$index) }}</span>
|
||||
<span v-else :class="col.className">{{ scope.row[col.prop] }}</span>
|
||||
</slot>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* SimpleTable ¡ª local-data table (no search / toolbar / pagination).
|
||||
* Column config aligns with BasicTable: prop, label, width, minWidth, align, fixed,
|
||||
* sortable, showOverflowTooltip, dict, formatter, slot, className.
|
||||
*
|
||||
* <simple-table :data="list" :columns="columns" :loading="loading" border>
|
||||
* <template #col-action="{ row }">...</template>
|
||||
* </simple-table>
|
||||
*/
|
||||
defineOptions({ name: 'SimpleTable' })
|
||||
|
||||
const props = defineProps({
|
||||
data: { type: Array, default: () => [] },
|
||||
columns: { type: Array, default: () => [] },
|
||||
loading: { type: Boolean, default: false },
|
||||
rowKey: { type: [String, Function], default: undefined },
|
||||
border: { type: Boolean, default: false },
|
||||
stripe: { type: Boolean, default: false },
|
||||
size: { type: String, default: undefined },
|
||||
height: { type: [String, Number], default: undefined },
|
||||
maxHeight: { type: [String, Number], default: undefined },
|
||||
spanMethod: { type: Function, default: null },
|
||||
emptyText: { type: String, default: undefined },
|
||||
selection: { type: Boolean, default: false },
|
||||
showIndex: { type: Boolean, default: false },
|
||||
indexLabel: { type: String, default: '#' },
|
||||
highlightCurrentRow: { type: Boolean, default: false }
|
||||
})
|
||||
|
||||
defineEmits(['selection-change', 'row-click'])
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const tableRef = ref()
|
||||
const dictMap = reactive({})
|
||||
|
||||
const dictTypes = computed(() => {
|
||||
const set = new Set()
|
||||
props.columns.forEach((c) => c.dict && set.add(c.dict))
|
||||
return [...set]
|
||||
})
|
||||
|
||||
watch(
|
||||
dictTypes,
|
||||
(types) => {
|
||||
if (!types.length) return
|
||||
const dictRefs = proxy.useDict(...types)
|
||||
types.forEach((t) => {
|
||||
const d = dictRefs[t]
|
||||
dictMap[t] = isRef(d) ? d : ref(d)
|
||||
})
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
function getDictOptions(type) {
|
||||
const d = dictMap[type]
|
||||
return isRef(d) ? d.value : d || []
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
getTableRef: () => tableRef.value
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.simple-table {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -47,6 +47,7 @@ import DictTag from '@/components/DictTag'
|
||||
import BasicForm from '@/components/BasicForm'
|
||||
// 公共表格组件
|
||||
import BasicTable from '@/components/BasicTable'
|
||||
import SimpleTable from '@/components/SimpleTable'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
@@ -71,6 +72,7 @@ app.component('RightToolbar', RightToolbar)
|
||||
app.component('Editor', Editor)
|
||||
app.component('BasicForm', BasicForm)
|
||||
app.component('BasicTable', BasicTable)
|
||||
app.component('SimpleTable', SimpleTable)
|
||||
|
||||
app.use(router)
|
||||
app.use(store)
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<el-drawer v-model="visible" size="920px" :title="title" append-to-body>
|
||||
<div class="mb8" style="display: flex; align-items: center; gap: 12px">
|
||||
<el-input v-model="keyword" placeholder="搜索国家(中文名 / ISO2)" clearable style="width: 260px" />
|
||||
</div>
|
||||
<simple-table
|
||||
:data="filteredList"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
border
|
||||
max-height="calc(100vh - 120px)"
|
||||
empty-text="暂无国家明细"
|
||||
:span-method="spanMethod"
|
||||
>
|
||||
<template #col-sizeLimit="{ row }">
|
||||
<span class="pre-wrap">{{ row.sizeLimit }}</span>
|
||||
</template>
|
||||
<template #col-lastMile="{ row }">
|
||||
<span class="pre-wrap">{{ row.lastMile }}</span>
|
||||
</template>
|
||||
</simple-table>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import SimpleTable from '@/components/SimpleTable'
|
||||
import { listProductCountries } from '@/api/price/product'
|
||||
|
||||
const columns = [
|
||||
{ prop: 'countryName', label: '国家', width: 90, showOverflowTooltip: true },
|
||||
{ prop: 'countryCode', label: 'ISO2', width: 60, align: 'center' },
|
||||
{
|
||||
prop: 'weightMin',
|
||||
label: '重量段(KG)',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
formatter: (row) => (row.weightMin != null || row.weightMax != null ? (row.weightMin + ' ~ ' + row.weightMax) : '-')
|
||||
},
|
||||
{ prop: 'freightPrice', label: '运费(RMB/KG)', width: 110, align: 'right' },
|
||||
{ prop: 'registrationFee', label: '挂号费(RMB/票)', width: 120, align: 'right' },
|
||||
{ prop: 'sizeLimit', label: '尺寸限制', minWidth: 140 },
|
||||
{ prop: 'lastMile', label: '尾程', minWidth: 110 },
|
||||
{ prop: 'leadTime', label: '时效', width: 100, showOverflowTooltip: true },
|
||||
{ prop: 'effectiveDate', label: '生效日期', width: 110, align: 'center' },
|
||||
{ prop: 'expireDate', label: '失效日期', width: 110, align: 'center', formatter: (row) => row.expireDate || '长期' },
|
||||
{ prop: 'status', label: '状态', width: 90, align: 'center', dict: 'bms_product_effect_status' }
|
||||
]
|
||||
|
||||
const visible = ref(false)
|
||||
const title = ref('')
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const keyword = ref('')
|
||||
const status = ref('')
|
||||
|
||||
const filteredList = computed(() => {
|
||||
const kw = keyword.value.trim().toLowerCase()
|
||||
const st = status.value
|
||||
return list.value.filter(r => {
|
||||
if (kw && !(String(r.countryName || '').toLowerCase().includes(kw) ||
|
||||
String(r.countryCode || '').toLowerCase().includes(kw))) return false
|
||||
if (st && String(r.status) !== String(st)) return false
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
||||
function formatEffDate(v) {
|
||||
if (!v) return undefined
|
||||
return String(v).slice(0, 10)
|
||||
}
|
||||
|
||||
function open(row) {
|
||||
title.value = row.productCode + ' · ' + row.productName + ' · 国家服务明细'
|
||||
visible.value = true
|
||||
loading.value = true
|
||||
list.value = []
|
||||
keyword.value = ''
|
||||
status.value = ''
|
||||
listProductCountries(row.productCode, formatEffDate(row.effectiveDate), row.productId).then(res => {
|
||||
list.value = (res.data || []).slice().sort((a, b) => {
|
||||
return String(a.countryCode || '').localeCompare(String(b.countryCode || ''))
|
||||
})
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function spanMethod({ column, rowIndex }) {
|
||||
if (column.property !== 'countryName' && column.property !== 'countryCode') return
|
||||
const rows = filteredList.value
|
||||
if (!rows.length) return
|
||||
const cur = rows[rowIndex]?.countryCode
|
||||
if (rowIndex > 0 && rows[rowIndex - 1]?.countryCode === cur) {
|
||||
return { rowspan: 0, colspan: 0 }
|
||||
}
|
||||
let count = 1
|
||||
while (rowIndex + count < rows.length && rows[rowIndex + count]?.countryCode === cur) count++
|
||||
return { rowspan: count, colspan: 1 }
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mb8 { margin-bottom: 8px; }
|
||||
.pre-wrap { white-space: pre-wrap; font-size: 12px; line-height: 1.4; }
|
||||
</style>
|
||||
@@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<el-dialog :title="title" v-model="visible" width="640px" append-to-body>
|
||||
<basic-form ref="formRef" v-model="form" :schemas="schemas" label-width="100px" />
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="visible = false">取 消</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import BasicForm from '@/components/BasicForm'
|
||||
import { getProduct, addProduct, updateProduct } from '@/api/price/product'
|
||||
|
||||
const emit = defineEmits(['success'])
|
||||
const { proxy } = getCurrentInstance()
|
||||
|
||||
const formRef = ref()
|
||||
const visible = ref(false)
|
||||
const title = ref('')
|
||||
const form = ref(emptyForm())
|
||||
|
||||
const schemas = computed(() => [
|
||||
{
|
||||
label: '产品代码',
|
||||
prop: 'productCode',
|
||||
component: 'input',
|
||||
placeholder: '如 WSP037',
|
||||
span: 12,
|
||||
width: '100%',
|
||||
disabled: !!form.value.productId,
|
||||
props: { maxlength: 20 },
|
||||
rules: [{ required: true, message: '产品代码不能为空', trigger: 'blur' }]
|
||||
},
|
||||
{
|
||||
label: '产品名称',
|
||||
prop: 'productName',
|
||||
component: 'input',
|
||||
placeholder: '产品名称',
|
||||
span: 12,
|
||||
width: '100%',
|
||||
props: { maxlength: 100 },
|
||||
rules: [{ required: true, message: '产品名称不能为空', trigger: 'blur' }]
|
||||
},
|
||||
{
|
||||
label: '时效等级',
|
||||
prop: 'serviceLevel',
|
||||
component: 'input',
|
||||
placeholder: '如 标准',
|
||||
span: 12,
|
||||
width: '100%',
|
||||
props: { maxlength: 20 }
|
||||
},
|
||||
{
|
||||
label: '走货属性',
|
||||
prop: 'cargoAttr',
|
||||
component: 'input',
|
||||
placeholder: '普货/带电等',
|
||||
span: 12,
|
||||
width: '100%',
|
||||
props: { maxlength: 100 }
|
||||
},
|
||||
{
|
||||
label: '目的区域',
|
||||
prop: 'destRegion',
|
||||
component: 'input',
|
||||
placeholder: '如 欧盟',
|
||||
span: 12,
|
||||
width: '100%',
|
||||
props: { maxlength: 50 }
|
||||
},
|
||||
{
|
||||
label: '税模式',
|
||||
prop: 'taxMode',
|
||||
component: 'select',
|
||||
placeholder: '请选择',
|
||||
dict: 'bms_tax_mode',
|
||||
span: 12,
|
||||
width: '100%',
|
||||
rules: [{ required: true, message: '请选择税模式', trigger: 'change' }]
|
||||
},
|
||||
{
|
||||
label: '生效日期',
|
||||
prop: 'effectiveDate',
|
||||
component: 'date',
|
||||
placeholder: '选择日期',
|
||||
span: 12,
|
||||
width: '100%',
|
||||
rules: [{ required: true, message: '生效日期不能为空', trigger: 'change' }]
|
||||
},
|
||||
{
|
||||
label: '失效日期',
|
||||
prop: 'expireDate',
|
||||
component: 'date',
|
||||
placeholder: '选择日期',
|
||||
span: 12,
|
||||
width: '100%'
|
||||
},
|
||||
{
|
||||
label: '更新内容',
|
||||
prop: 'updateNote',
|
||||
component: 'textarea',
|
||||
span: 24,
|
||||
width: '100%',
|
||||
props: { rows: 2, maxlength: 500 }
|
||||
},
|
||||
{
|
||||
label: '备注',
|
||||
prop: 'remark',
|
||||
component: 'textarea',
|
||||
span: 24,
|
||||
width: '100%',
|
||||
props: { rows: 2, maxlength: 500 }
|
||||
}
|
||||
])
|
||||
|
||||
function emptyForm() {
|
||||
return {
|
||||
productId: undefined,
|
||||
productCode: undefined,
|
||||
productName: undefined,
|
||||
serviceLevel: undefined,
|
||||
cargoAttr: undefined,
|
||||
destRegion: undefined,
|
||||
updateNote: undefined,
|
||||
taxMode: 'TAX_INCLUDED',
|
||||
effectiveDate: undefined,
|
||||
expireDate: undefined,
|
||||
status: '0',
|
||||
remark: undefined
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
form.value = emptyForm()
|
||||
nextTick(() => formRef.value?.clearValidate())
|
||||
}
|
||||
|
||||
function open(row) {
|
||||
reset()
|
||||
if (!row) {
|
||||
title.value = '新建产品'
|
||||
visible.value = true
|
||||
return
|
||||
}
|
||||
getProduct(row.productId).then(res => {
|
||||
form.value = { ...emptyForm(), ...(res.data || {}) }
|
||||
title.value = '修改产品'
|
||||
visible.value = true
|
||||
nextTick(() => formRef.value?.clearValidate())
|
||||
})
|
||||
}
|
||||
|
||||
function submitForm() {
|
||||
formRef.value.validate(valid => {
|
||||
if (!valid) return
|
||||
const req = form.value.productId ? updateProduct(form.value) : addProduct(form.value)
|
||||
req.then(() => {
|
||||
proxy.$modal.msgSuccess('操作成功')
|
||||
visible.value = false
|
||||
emit('success')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<el-drawer v-model="visible" size="920px" :title="title" append-to-body>
|
||||
<div class="mb8" style="display: flex; justify-content: space-between">
|
||||
<el-button type="primary" plain icon="Plus" @click="handleRelAdd" v-hasPermi="['price:surcharge:add']">新增关联</el-button>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="list" border max-height="calc(100vh - 160px)" empty-text="该产品暂无关联附加费">
|
||||
<el-table-column label="附加费名称" prop="scName" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="计费方式" width="110" align="center">
|
||||
<template #default="{ row }">{{ chargeTypeLabel(row.chargeType) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="计费数值" width="140" align="right">
|
||||
<template #default="{ row }">
|
||||
<span class="charge-value">{{ formatChargeValue(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="取值方式" width="100" align="center">
|
||||
<template #default="{ row }">{{ roundModeLabel(row.roundMode) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="适用国家" width="110" align="center">
|
||||
<template #default="{ row }">{{ countryLabel(row.countryCode) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="作用范围" width="170" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.detailId == null" type="primary" size="small" effect="plain">产品级 · 全部明细</el-tag>
|
||||
<el-tag v-else type="warning" size="small" effect="plain">{{ row.countryName || '-' }} · {{ row.weightMin }}-{{ row.weightMax }}kg</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="String(row.ssCode).toLowerCase() === 'on' ? 'success' : 'info'" size="small">
|
||||
{{ String(row.ssCode).toLowerCase() === 'on' ? '有效' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" prop="scNote" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="90" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="danger" icon="Delete" @click="handleRelDelete(row.relId)" v-hasPermi="['price:surcharge:remove']">解绑</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<surcharge-rel-dialog ref="relRef" @success="handleRelSuccess" />
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted } from 'vue'
|
||||
import { listSurchargeByProduct, delSurchargeRel } from '@/api/price/surcharge'
|
||||
import { listRegionOptions } from '@/api/data/region'
|
||||
import SurchargeRelDialog from './SurchargeRelDialog.vue'
|
||||
|
||||
const emit = defineEmits(['success'])
|
||||
const { proxy } = getCurrentInstance()
|
||||
const { bms_round_mode, bms_charge_type } = useDict('bms_round_mode', 'bms_charge_type')
|
||||
|
||||
const relRef = ref()
|
||||
const visible = ref(false)
|
||||
const title = ref('')
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const product = ref(null)
|
||||
const regionOptions = ref([])
|
||||
|
||||
function open(row) {
|
||||
product.value = row
|
||||
title.value = row.productCode + ' · ' + row.productName + ' · 附加费关联'
|
||||
visible.value = true
|
||||
loadList()
|
||||
}
|
||||
|
||||
function loadList() {
|
||||
if (!product.value) return
|
||||
loading.value = true
|
||||
listSurchargeByProduct(product.value.productCode).then(res => {
|
||||
list.value = res.data || []
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function handleRelAdd() {
|
||||
relRef.value.open(product.value)
|
||||
}
|
||||
|
||||
function handleRelSuccess() {
|
||||
loadList()
|
||||
emit('success')
|
||||
}
|
||||
|
||||
function handleRelDelete(relId) {
|
||||
proxy.$modal.confirm('是否确认解绑该附加费?').then(() => {
|
||||
return delSurchargeRel(relId)
|
||||
}).then(() => {
|
||||
proxy.$modal.msgSuccess('解绑成功')
|
||||
loadList()
|
||||
emit('success')
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
function chargeTypeLabel(val) {
|
||||
const found = bms_charge_type.value.find(c => c.value === val)
|
||||
return found ? found.label : (val || '-')
|
||||
}
|
||||
|
||||
function roundModeLabel(val) {
|
||||
const found = bms_round_mode.value.find(d => d.value === val)
|
||||
return found ? found.label : (val || '-')
|
||||
}
|
||||
|
||||
function formatChargeValue(row) {
|
||||
if (row.chargeValue == null) return '-'
|
||||
const v = Number(row.chargeValue)
|
||||
if (row.chargeType === 'RATE') return (v * 100).toFixed(2) + '%'
|
||||
return v + ' ' + (row.currency || 'CNY')
|
||||
}
|
||||
|
||||
function countryLabel(code) {
|
||||
if (!code) return '全部'
|
||||
const found = regionOptions.value.find(r => r.countryCode2 === code)
|
||||
return found ? (found.countryNameCn || found.countryNameEn || code) : code
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
listRegionOptions().then(res => {
|
||||
regionOptions.value = res.data || []
|
||||
})
|
||||
})
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mb8 { margin-bottom: 8px; }
|
||||
.charge-value { font-weight: 600; color: #d63031; }
|
||||
</style>
|
||||
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<el-dialog :title="title" v-model="visible" width="560px" append-to-body>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||
<el-form-item label="附加费" prop="scId">
|
||||
<el-select v-model="form.scId" placeholder="请选择附加费" filterable style="width: 100%">
|
||||
<el-option v-for="s in surchargePool" :key="s.scId" :label="s.scName" :value="s.scId" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="作用范围" prop="scope">
|
||||
<el-radio-group v-model="form.scope">
|
||||
<el-radio value="PRODUCT">产品级(全部明细)</el-radio>
|
||||
<el-radio value="DETAIL">明细级(指定国家×重量段)</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.scope === 'DETAIL'" label="明细" prop="detailId">
|
||||
<el-select v-model="form.detailId" placeholder="请选择明细" filterable style="width: 100%">
|
||||
<el-option v-for="d in countryList" :key="d.detailId" :label="detailLabel(d)" :value="d.detailId" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="submit">确 定</el-button>
|
||||
<el-button @click="visible = false">取 消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { listProductCountries } from '@/api/price/product'
|
||||
import { listSurcharge, addSurchargeRel } from '@/api/price/surcharge'
|
||||
|
||||
const emit = defineEmits(['success'])
|
||||
const { proxy } = getCurrentInstance()
|
||||
|
||||
const formRef = ref()
|
||||
const visible = ref(false)
|
||||
const title = ref('新增附加费关联')
|
||||
const product = ref(null)
|
||||
const surchargePool = ref([])
|
||||
const countryList = ref([])
|
||||
const form = ref({})
|
||||
const rules = {
|
||||
scId: [{ required: true, message: '请选择附加费', trigger: 'change' }]
|
||||
}
|
||||
|
||||
function detailLabel(d) {
|
||||
const w = d.weightMin != null && d.weightMax != null ? (d.weightMin + '-' + d.weightMax + 'kg') : '-'
|
||||
return (d.countryName || d.countryCode || '-') + ' · ' + w
|
||||
}
|
||||
|
||||
function reset() {
|
||||
form.value = {
|
||||
scId: undefined,
|
||||
scope: 'PRODUCT',
|
||||
detailId: undefined
|
||||
}
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
function open(row) {
|
||||
product.value = row
|
||||
reset()
|
||||
visible.value = true
|
||||
listSurcharge({ ssCode: 'on', pageNum: 1, pageSize: 1000 }).then(res => {
|
||||
surchargePool.value = res.rows || []
|
||||
})
|
||||
if (row && row.productCode) {
|
||||
listProductCountries(row.productCode, undefined, row.productId).then(res => {
|
||||
countryList.value = res.data || []
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function submit() {
|
||||
formRef.value.validate(valid => {
|
||||
if (!valid) return
|
||||
if (form.value.scope === 'DETAIL' && !form.value.detailId) {
|
||||
proxy.$modal.msgError('请选择明细')
|
||||
return
|
||||
}
|
||||
const payload = {
|
||||
scId: form.value.scId,
|
||||
productId: product.value ? product.value.productId : undefined,
|
||||
productCode: product.value ? product.value.productCode : undefined,
|
||||
detailId: form.value.scope === 'DETAIL' ? form.value.detailId : null
|
||||
}
|
||||
addSurchargeRel(payload).then(() => {
|
||||
proxy.$modal.msgSuccess('关联成功')
|
||||
visible.value = false
|
||||
emit('success')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
@@ -5,14 +5,24 @@
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="mb16"
|
||||
title="Excel 导入:上传威狮B2C小包价表.xlsx。入库产品目录 + 各产品 sheet 的国家×重量段(运费/挂号费/尺寸/尾程/时效);不导入退件重派、申报说明、赔偿标准。"
|
||||
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 }">
|
||||
@@ -35,196 +45,26 @@
|
||||
</template>
|
||||
|
||||
<template #col-status="{ row }">
|
||||
<dict-tag :options="bms_product_effect_status" :value="resolveEffectStatus(row)" />
|
||||
<dict-tag :options="bms_product_effect_status" :value="row.status" />
|
||||
</template>
|
||||
|
||||
<template #col-action="{ row }">
|
||||
<el-button link type="primary" icon="View" @click="openCountries(row)" v-hasPermi="['price:product:query']">产品明细</el-button>
|
||||
<el-button link type="primary" icon="Money" @click="openSurcharges(row)" v-hasPermi="['price:surcharge:query']">附加费</el-button>
|
||||
<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>
|
||||
|
||||
<el-dialog :title="title" v-model="open" width="640px" append-to-body>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="产品代码" prop="productCode">
|
||||
<el-input v-model="form.productCode" placeholder="如 WSP037" maxlength="20" :disabled="!!form.productId" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="产品名称" prop="productName">
|
||||
<el-input v-model="form.productName" placeholder="产品名称" maxlength="100" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="时效等级" prop="serviceLevel">
|
||||
<el-input v-model="form.serviceLevel" placeholder="如 标准" maxlength="20" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="走货属性" prop="cargoAttr">
|
||||
<el-input v-model="form.cargoAttr" placeholder="普货/带电等" maxlength="100" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="目的区域" prop="destRegion">
|
||||
<el-input v-model="form.destRegion" placeholder="如 欧盟" maxlength="50" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="税模式" prop="taxMode">
|
||||
<el-select v-model="form.taxMode" placeholder="请选择" style="width: 100%">
|
||||
<el-option
|
||||
v-for="dict in bms_tax_mode"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="生效日期" prop="effectiveDate">
|
||||
<el-date-picker v-model="form.effectiveDate" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio value="0">启用(按生效日期自动:待生效/已生效)</el-radio>
|
||||
<el-radio value="1">已失效</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="更新内容" prop="updateNote">
|
||||
<el-input v-model="form.updateNote" type="textarea" :rows="2" maxlength="500" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" maxlength="500" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="open = false">取 消</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-drawer v-model="countryOpen" size="920px" :title="countryTitle" append-to-body>
|
||||
<el-input v-model="countryKeyword" placeholder="搜索国家(中文名 / ISO2)" clearable class="mb8" style="width: 260px" />
|
||||
<el-table v-loading="countryLoading" :data="filteredCountryList" border :span-method="countrySpanMethod" max-height="calc(100vh - 120px)" empty-text="暂无国家明细">
|
||||
<el-table-column label="国家" prop="countryName" width="90" show-overflow-tooltip />
|
||||
<el-table-column label="ISO2" prop="countryCode" width="60" align="center" />
|
||||
<el-table-column label="重量段(KG)" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.weightMin != null || row.weightMax != null">{{ row.weightMin }} ~ {{ row.weightMax }}</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="运费(RMB/KG)" prop="freightPrice" width="110" align="right" />
|
||||
<el-table-column label="挂号费(RMB/票)" prop="registrationFee" width="120" align="right" />
|
||||
<el-table-column label="尺寸限制" prop="sizeLimit" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<span class="pre-wrap">{{ row.sizeLimit }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="尾程" prop="lastMile" min-width="110">
|
||||
<template #default="{ row }">
|
||||
<span class="pre-wrap">{{ row.lastMile }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="时效" prop="leadTime" width="100" show-overflow-tooltip />
|
||||
<el-table-column label="生效日期" prop="effectiveDate" width="110" align="center" />
|
||||
<el-table-column label="失效日期" width="110" align="center">
|
||||
<template #default="{ row }">{{ row.expireDate || '长期' }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-drawer>
|
||||
|
||||
<el-drawer v-model="surchargeOpen" size="920px" :title="surchargeTitle" append-to-body>
|
||||
<div class="mb8" style="display: flex; justify-content: space-between">
|
||||
<el-button type="primary" plain icon="Plus" @click="handleRelAdd" v-hasPermi="['price:surcharge:add']">新增关联</el-button>
|
||||
</div>
|
||||
<el-table v-loading="surchargeLoading" :data="surchargeList" border max-height="calc(100vh - 160px)" empty-text="该产品暂无关联附加费">
|
||||
<el-table-column label="附加费名称" prop="scName" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="计费方式" width="110" align="center">
|
||||
<template #default="{ row }">{{ chargeTypeLabel(row.chargeType) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="计费数值" width="140" align="right">
|
||||
<template #default="{ row }">
|
||||
<span class="charge-value">{{ formatChargeValue(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="取值方式" width="100" align="center">
|
||||
<template #default="{ row }">{{ roundModeLabel(row.roundMode) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="适用国家" width="110" align="center">
|
||||
<template #default="{ row }">{{ countryLabel(row.countryCode) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="作用范围" width="170" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.detailId == null" type="primary" size="small" effect="plain">产品级 · 全部明细</el-tag>
|
||||
<el-tag v-else type="warning" size="small" effect="plain">{{ row.countryName || '-' }} · {{ row.weightMin }}-{{ row.weightMax }}kg</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="String(row.ssCode).toLowerCase() === 'on' ? 'success' : 'info'" size="small">
|
||||
{{ String(row.ssCode).toLowerCase() === 'on' ? '有效' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" prop="scNote" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="90" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="danger" icon="Delete" @click="handleRelDelete(row.relId)" v-hasPermi="['price:surcharge:remove']">解绑</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-drawer>
|
||||
|
||||
<el-dialog :title="relTitle" v-model="relOpen" width="560px" append-to-body>
|
||||
<el-form ref="relFormRef" :model="relForm" :rules="relRules" label-width="100px">
|
||||
<el-form-item label="附加费" prop="scId">
|
||||
<el-select v-model="relForm.scId" placeholder="请选择附加费" filterable style="width: 100%">
|
||||
<el-option v-for="s in surchargePool" :key="s.scId" :label="s.scName" :value="s.scId" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="作用范围" prop="scope">
|
||||
<el-radio-group v-model="relForm.scope">
|
||||
<el-radio value="PRODUCT">产品级(全部明细)</el-radio>
|
||||
<el-radio value="DETAIL">明细级(指定国家×重量段)</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="relForm.scope === 'DETAIL'" label="明细" prop="detailId">
|
||||
<el-select v-model="relForm.detailId" placeholder="请选择明细" filterable style="width: 100%">
|
||||
<el-option v-for="d in countryList" :key="d.detailId" :label="detailLabel(d)" :value="d.detailId" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="submitRel">确 定</el-button>
|
||||
<el-button @click="relOpen = false">取 消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<product-form-dialog ref="formRef" @success="() => tableRef.refresh()" />
|
||||
<product-country-drawer ref="countryRef" />
|
||||
<product-surcharge-drawer ref="surchargeRef" @success="() => tableRef.refresh()" />
|
||||
<excel-import-dialog
|
||||
ref="importRef"
|
||||
title="产品 Excel 导入"
|
||||
action="/price/product/importData"
|
||||
:default-update-support="true"
|
||||
update-support-label="是否更新已存在的产品主数据(国家明细:新生效日晚于上次则保留历史,上次失效日=本次生效日)"
|
||||
width="560px"
|
||||
:show-update-support="false"
|
||||
@success="() => tableRef.refresh()"
|
||||
/>
|
||||
</div>
|
||||
@@ -232,43 +72,54 @@
|
||||
|
||||
<script setup name="PriceProduct">
|
||||
import ExcelImportDialog from '@/components/ExcelImportDialog'
|
||||
import { listProduct, getProduct, addProduct, updateProduct, delProduct, listProductCountries } from '@/api/price/product'
|
||||
import { listSurchargeByProduct, listSurcharge, addSurchargeRel, delSurchargeRel } from '@/api/price/surcharge'
|
||||
import { listRegionOptions } from '@/api/data/region'
|
||||
import { onMounted } from 'vue'
|
||||
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'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const { bms_product_effect_status, bms_tax_mode, bms_round_mode, bms_charge_type } = useDict('bms_product_effect_status', 'bms_tax_mode', 'bms_round_mode', 'bms_charge_type')
|
||||
const { bms_product_effect_status } = useDict('bms_product_effect_status')
|
||||
|
||||
const tableRef = ref()
|
||||
const importRef = ref()
|
||||
const formRef = ref()
|
||||
const open = ref(false)
|
||||
const title = ref('')
|
||||
const countryOpen = ref(false)
|
||||
const countryTitle = ref('')
|
||||
const countryLoading = ref(false)
|
||||
const countryList = ref([])
|
||||
const countryKeyword = ref('')
|
||||
const countryRef = ref()
|
||||
const surchargeRef = ref()
|
||||
const importRef = ref()
|
||||
|
||||
const surchargeOpen = ref(false)
|
||||
const surchargeTitle = ref('')
|
||||
const surchargeLoading = ref(false)
|
||||
const surchargeList = ref([])
|
||||
const currentProduct = ref(null)
|
||||
const regionOptions = ref([])
|
||||
const TAB_ACTIVE = 'active'
|
||||
const TAB_PENDING = 'pending'
|
||||
const TAB_EXPIRED = 'expired'
|
||||
|
||||
const surchargePool = ref([])
|
||||
const relOpen = ref(false)
|
||||
const relTitle = ref('')
|
||||
const relFormRef = ref()
|
||||
const filteredCountryList = computed(() => {
|
||||
const kw = countryKeyword.value.trim().toLowerCase()
|
||||
if (!kw) return countryList.value
|
||||
return countryList.value.filter(r =>
|
||||
String(r.countryName || '').toLowerCase().includes(kw) ||
|
||||
String(r.countryCode || '').toLowerCase().includes(kw)
|
||||
)
|
||||
})
|
||||
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 },
|
||||
@@ -278,6 +129,7 @@ const columns = [
|
||||
{ prop: 'destRegion', label: '目的区域', width: 90, align: 'center' },
|
||||
{ prop: 'taxMode', label: '税模式', width: 90, align: 'center', dict: 'bms_tax_mode' },
|
||||
{ prop: 'effectiveDate', label: '生效日期', width: 120, align: 'center' },
|
||||
{ prop: 'expireDate', label: '失效日期', width: 120, align: 'center' },
|
||||
{ prop: 'countryCount', label: '国家数', width: 80, align: 'center' },
|
||||
{ prop: 'surchargeCount', label: '附加费数', width: 90, align: 'center' },
|
||||
{ prop: 'status', label: '状态', width: 90, align: 'center', slot: 'status' },
|
||||
@@ -295,69 +147,19 @@ const searchColumns = [
|
||||
dict: 'bms_tax_mode',
|
||||
placeholder: '税模式',
|
||||
clearable: true
|
||||
},
|
||||
{
|
||||
prop: 'effectStatus',
|
||||
label: '状态',
|
||||
component: 'select',
|
||||
dict: 'bms_product_effect_status',
|
||||
placeholder: '请选择状态',
|
||||
clearable: true
|
||||
}
|
||||
]
|
||||
|
||||
const data = reactive({
|
||||
form: {},
|
||||
rules: {
|
||||
productCode: [{ required: true, message: '产品代码不能为空', trigger: 'blur' }],
|
||||
productName: [{ required: true, message: '产品名称不能为空', trigger: 'blur' }],
|
||||
taxMode: [{ required: true, message: '请选择税模式', trigger: 'change' }]
|
||||
}
|
||||
})
|
||||
const { form, rules } = toRefs(data)
|
||||
|
||||
function queryList(params) {
|
||||
return listProduct(params)
|
||||
return listProduct({ ...params, status: tabToStatus(effectTab.value) })
|
||||
}
|
||||
|
||||
function todayStr() {
|
||||
return new Date().toISOString().slice(0, 10)
|
||||
}
|
||||
|
||||
/** ACTIVE已生效 / PENDING待生效 / EXPIRED已失效 */
|
||||
function resolveEffectStatus(row) {
|
||||
if (!row) return 'PENDING'
|
||||
if (row.status === '1') return 'EXPIRED'
|
||||
if (!row.effectiveDate) return 'PENDING'
|
||||
return String(row.effectiveDate).slice(0, 10) <= todayStr() ? 'ACTIVE' : 'PENDING'
|
||||
}
|
||||
|
||||
// 已生效不可编辑/删除
|
||||
function isProductActive(row) {
|
||||
return resolveEffectStatus(row) === 'ACTIVE'
|
||||
}
|
||||
|
||||
function reset() {
|
||||
form.value = {
|
||||
productId: undefined,
|
||||
productCode: undefined,
|
||||
productName: undefined,
|
||||
serviceLevel: undefined,
|
||||
cargoAttr: undefined,
|
||||
destRegion: undefined,
|
||||
updateNote: undefined,
|
||||
taxMode: 'TAX_INCLUDED',
|
||||
effectiveDate: undefined,
|
||||
status: '0',
|
||||
remark: undefined
|
||||
}
|
||||
proxy.resetForm('formRef')
|
||||
return row && String(row.status) === '1'
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
reset()
|
||||
open.value = true
|
||||
title.value = '新建产品'
|
||||
formRef.value.open()
|
||||
}
|
||||
|
||||
function handleUpdate(row) {
|
||||
@@ -365,24 +167,7 @@ function handleUpdate(row) {
|
||||
proxy.$modal.msgError('已生效的产品不可编辑')
|
||||
return
|
||||
}
|
||||
reset()
|
||||
getProduct(row.productId).then(res => {
|
||||
form.value = res.data
|
||||
open.value = true
|
||||
title.value = '修改产品'
|
||||
})
|
||||
}
|
||||
|
||||
function submitForm() {
|
||||
proxy.$refs['formRef'].validate(valid => {
|
||||
if (!valid) return
|
||||
const req = form.value.productId ? updateProduct(form.value) : addProduct(form.value)
|
||||
req.then(() => {
|
||||
proxy.$modal.msgSuccess('操作成功')
|
||||
open.value = false
|
||||
tableRef.value.refresh()
|
||||
})
|
||||
})
|
||||
formRef.value.open(row)
|
||||
}
|
||||
|
||||
function handleDelete(ids) {
|
||||
@@ -398,155 +183,8 @@ function handleDelete(ids) {
|
||||
function handleImport() {
|
||||
importRef.value.open()
|
||||
}
|
||||
|
||||
function openCountries(row) {
|
||||
countryTitle.value = `${row.productCode} · ${row.productName} · 国家服务明细`
|
||||
countryOpen.value = true
|
||||
countryLoading.value = true
|
||||
countryList.value = []
|
||||
countryKeyword.value = ''
|
||||
listProductCountries(row.productCode).then(res => {
|
||||
// 按 ISO2 排序,保证相同国家相邻,便于合并单元格
|
||||
const list = (res.data || []).slice().sort((a, b) => {
|
||||
return String(a.countryCode || '').localeCompare(String(b.countryCode || ''))
|
||||
})
|
||||
countryList.value = list
|
||||
}).finally(() => {
|
||||
countryLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
// 弹窗表格合并:相同 countryCode 的连续行合并「国家」「ISO2」列
|
||||
function countrySpanMethod({ column, rowIndex }) {
|
||||
if (column.property !== 'countryName' && column.property !== 'countryCode') return
|
||||
const list = filteredCountryList.value
|
||||
if (!list.length) return
|
||||
const cur = list[rowIndex]?.countryCode
|
||||
if (rowIndex > 0 && list[rowIndex - 1]?.countryCode === cur) {
|
||||
return { rowspan: 0, colspan: 0 }
|
||||
}
|
||||
let count = 1
|
||||
while (rowIndex + count < list.length && list[rowIndex + count]?.countryCode === cur) count++
|
||||
return { rowspan: count, colspan: 1 }
|
||||
}
|
||||
|
||||
// ===== 产品附加费关联 =====
|
||||
const relData = reactive({
|
||||
relForm: {},
|
||||
relRules: {
|
||||
scId: [{ required: true, message: '请选择附加费', trigger: 'change' }]
|
||||
}
|
||||
})
|
||||
const { relForm, relRules } = toRefs(relData)
|
||||
|
||||
function openSurcharges(row) {
|
||||
currentProduct.value = row
|
||||
surchargeTitle.value = `${row.productCode} · ${row.productName} · 附加费关联`
|
||||
surchargeOpen.value = true
|
||||
loadSurcharges()
|
||||
}
|
||||
|
||||
function loadSurcharges() {
|
||||
if (!currentProduct.value) return
|
||||
surchargeLoading.value = true
|
||||
listSurchargeByProduct(currentProduct.value.productCode).then(res => {
|
||||
surchargeList.value = res.data || []
|
||||
}).finally(() => {
|
||||
surchargeLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function loadSurchargePool() {
|
||||
return listSurcharge({ ssCode: 'on', pageNum: 1, pageSize: 1000 }).then(res => {
|
||||
surchargePool.value = res.rows || []
|
||||
})
|
||||
}
|
||||
|
||||
function resetRelForm() {
|
||||
relForm.value = {
|
||||
scId: undefined,
|
||||
scope: 'PRODUCT',
|
||||
detailId: undefined
|
||||
}
|
||||
relFormRef.value && relFormRef.value.resetFields()
|
||||
}
|
||||
|
||||
function handleRelAdd() {
|
||||
resetRelForm()
|
||||
relTitle.value = '新增附加费关联'
|
||||
relOpen.value = true
|
||||
loadSurchargePool()
|
||||
}
|
||||
|
||||
function submitRel() {
|
||||
relFormRef.value.validate(valid => {
|
||||
if (!valid) return
|
||||
if (relForm.value.scope === 'DETAIL' && !relForm.value.detailId) {
|
||||
proxy.$modal.msgError('请选择明细')
|
||||
return
|
||||
}
|
||||
const payload = {
|
||||
scId: relForm.value.scId,
|
||||
productId: currentProduct.value ? currentProduct.value.productId : undefined,
|
||||
productCode: currentProduct.value ? currentProduct.value.productCode : undefined,
|
||||
detailId: relForm.value.scope === 'DETAIL' ? relForm.value.detailId : null
|
||||
}
|
||||
addSurchargeRel(payload).then(() => {
|
||||
proxy.$modal.msgSuccess('关联成功')
|
||||
relOpen.value = false
|
||||
loadSurcharges()
|
||||
tableRef.value.refresh()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function handleRelDelete(relId) {
|
||||
proxy.$modal.confirm('是否确认解绑该附加费?').then(() => {
|
||||
return delSurchargeRel(relId)
|
||||
}).then(() => {
|
||||
proxy.$modal.msgSuccess('解绑成功')
|
||||
loadSurcharges()
|
||||
tableRef.value.refresh()
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
function detailLabel(d) {
|
||||
const w = d.weightMin != null && d.weightMax != null ? `${d.weightMin}-${d.weightMax}kg` : '-'
|
||||
return `${d.countryName || d.countryCode || '-'} · ${w}`
|
||||
}
|
||||
|
||||
function chargeTypeLabel(val) {
|
||||
const found = bms_charge_type.value.find(c => c.value === val)
|
||||
return found ? found.label : (val || '-')
|
||||
}
|
||||
|
||||
function roundModeLabel(val) {
|
||||
const found = bms_round_mode.value.find(d => d.value === val)
|
||||
return found ? found.label : (val || '-')
|
||||
}
|
||||
|
||||
function formatChargeValue(row) {
|
||||
if (row.chargeValue == null) return '-'
|
||||
const v = Number(row.chargeValue)
|
||||
if (row.chargeType === 'RATE') return (v * 100).toFixed(2) + '%'
|
||||
return v + ' ' + (row.currency || 'CNY')
|
||||
}
|
||||
|
||||
function countryLabel(code) {
|
||||
if (!code) return '全部'
|
||||
const found = regionOptions.value.find(r => r.countryCode2 === code)
|
||||
return found ? (found.countryNameCn || found.countryNameEn || code) : code
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
listRegionOptions().then(res => {
|
||||
regionOptions.value = res.data || []
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mb16 { margin-bottom: 16px; }
|
||||
.pre-wrap { white-space: pre-wrap; font-size: 12px; line-height: 1.4; }
|
||||
.charge-value { font-weight: 600; color: #d63031; }
|
||||
</style>
|
||||
|
||||
@@ -95,37 +95,194 @@
|
||||
<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="币种">
|
||||
<dict-tag :options="bms_currency" :value="displayCurrency(result.currency)" />
|
||||
</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="取值方式">{{ roundModeLabel(result.roundMode) }}</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="附加费">{{ formatMoney(result.surchargeAmount) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="应付总额">
|
||||
<span class="payable">{{ formatMoney(result.payableTotal) }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="试算来源">
|
||||
<el-tag v-if="result.fromCache" size="small" type="success">命中历史试算</el-tag>
|
||||
<el-tag v-else size="small" type="primary">实时计算并落库</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="命中说明" :span="2">{{ result.matchMessage || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div class="formula-tip">计算公式:运费金额 = 运费单价 × 计费重量;应付总额 = 运费金额 + 挂号费</div>
|
||||
<el-table
|
||||
v-if="result.feeDetails && result.feeDetails.length"
|
||||
:data="result.feeDetails"
|
||||
border
|
||||
size="small"
|
||||
class="fee-table"
|
||||
>
|
||||
<el-table-column label="费用名称" min-width="140" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ displayFeeName(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="费用类型" width="110" align="center">
|
||||
<template #default="{ row }">{{ feeTypeLabel(row.feeType) || row.feeType }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额" min-width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<span>{{ formatMoney(row.amount) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="币种" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :options="bms_currency" :value="displayCurrency(row.currency)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="取值方式" width="110" align="center">
|
||||
<template #default="{ row }">{{ roundModeLabel(row.roundMode) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="说明" min-width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.remark || '-' }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="formula-tip">计算公式:运费金额 = 运费单价 × 计费重量;应付总额 = 运费金额 + 挂号费 + 附加费。相同试算参数直接返回已落库费用明细。</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-card shadow="never" class="history-card">
|
||||
<template #header><span>历史试算</span></template>
|
||||
<basic-table
|
||||
ref="historyTableRef"
|
||||
:columns="historyColumns"
|
||||
:search-columns="historySearchColumns"
|
||||
:query="queryHistory"
|
||||
row-key="quoteId"
|
||||
>
|
||||
<template #col-product="{ row }">
|
||||
{{ row.productName || '-' }} ({{ row.productCode }})
|
||||
</template>
|
||||
<template #col-country="{ row }">
|
||||
{{ row.countryName || '-' }} ({{ row.countryCode || '-' }})
|
||||
</template>
|
||||
<template #col-matchSource="{ row }">
|
||||
<el-tag size="small" :type="sourceTagType(row.matchSource)">{{ sourceLabel(row.matchSource) }}</el-tag>
|
||||
</template>
|
||||
<template #col-payableTotal="{ row }">
|
||||
<span class="payable">{{ formatMoney(row.payableTotal) }}</span>
|
||||
</template>
|
||||
<template #col-action="{ row }">
|
||||
<el-button link type="primary" icon="View" @click="handleViewHistory(row)">查看</el-button>
|
||||
</template>
|
||||
</basic-table>
|
||||
</el-card>
|
||||
|
||||
<el-drawer v-model="historyOpen" size="720px" :title="historyTitle" append-to-body>
|
||||
<div v-loading="historyLoading">
|
||||
<el-empty v-if="!viewResult" description="暂无详情" />
|
||||
<div v-else class="quote-result">
|
||||
<el-descriptions :column="2" border size="small">
|
||||
<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>
|
||||
<el-descriptions-item label="计费重量(kg)">{{ viewResult.chargeWeight ?? viewResult.weight }}</el-descriptions-item>
|
||||
<el-descriptions-item label="重量段">{{ viewResult.weightLabel || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="计价来源">
|
||||
<el-tag size="small" :type="sourceTagType(viewResult.matchSource)">{{ sourceLabel(viewResult.matchSource) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="套餐">{{ viewResult.priceSetName || '-' }} {{ viewResult.priceSetCode ? '(' + viewResult.priceSetCode + ')' : '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="运费金额">{{ formatMoney(viewResult.freightAmount) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="挂号费">{{ formatMoney(viewResult.registrationFee) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="附加费">{{ formatMoney(viewResult.surchargeAmount) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="应付总额">
|
||||
<span class="payable">{{ formatMoney(viewResult.payableTotal) }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="命中说明" :span="2">{{ viewResult.matchMessage || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-table
|
||||
v-if="viewResult.feeDetails && viewResult.feeDetails.length"
|
||||
:data="viewResult.feeDetails"
|
||||
border
|
||||
size="small"
|
||||
class="fee-table"
|
||||
>
|
||||
<el-table-column label="费用名称" min-width="140" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ displayFeeName(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="费用类型" width="110" align="center">
|
||||
<template #default="{ row }">{{ feeTypeLabel(row.feeType) || row.feeType }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额" min-width="120" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.amount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="币种" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :options="bms_currency" :value="displayCurrency(row.currency)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="说明" min-width="160" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.remark || '-' }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="BillingQuote">
|
||||
import { quoteBilling } from '@/api/price/billing'
|
||||
import { quoteBilling, listQuote, getQuote } from '@/api/price/billing'
|
||||
import { listProduct, listProductCountries } from '@/api/price/product'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const { bms_fee_type, bms_currency } = useDict('bms_fee_type', 'bms_currency')
|
||||
const formRef = ref()
|
||||
const historyTableRef = ref()
|
||||
const loading = ref(false)
|
||||
const productOptions = ref([])
|
||||
const countryOptions = ref([])
|
||||
const result = ref(null)
|
||||
const errorMsg = ref('')
|
||||
const historyOpen = ref(false)
|
||||
const historyTitle = ref('试算详情')
|
||||
const historyLoading = ref(false)
|
||||
const viewResult = ref(null)
|
||||
|
||||
const historyColumns = [
|
||||
{ label: '客户ID', prop: 'customerId', align: 'center', width: 90 },
|
||||
{ label: '产品', prop: 'product', minWidth: 160, slot: 'product' },
|
||||
{ label: '国家', prop: 'country', minWidth: 130, slot: 'country' },
|
||||
{ label: '计费重量', prop: 'weight', align: 'center', width: 100 },
|
||||
{ label: '计费日期', prop: 'bizDate', align: 'center', width: 120 },
|
||||
{ label: '计价来源', prop: 'matchSource', align: 'center', width: 140, slot: 'matchSource' },
|
||||
{ label: '套餐', prop: 'priceSetName', minWidth: 120, showOverflowTooltip: true },
|
||||
{ label: '应付总额', prop: 'payableTotal', align: 'right', width: 120, slot: 'payableTotal' },
|
||||
{ label: '创建时间', prop: 'createTime', align: 'center', width: 170, parseTime: true },
|
||||
{ label: '操作', prop: 'action', align: 'center', width: 90, fixed: 'right', slot: 'action', showInToolbar: false }
|
||||
]
|
||||
|
||||
const historySearchColumns = [
|
||||
{ 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' },
|
||||
{ label: '套餐名称', prop: 'priceSetName', component: 'input', placeholder: '套餐名称', width: '150px' }
|
||||
]
|
||||
|
||||
function queryHistory(params) {
|
||||
return listQuote(params)
|
||||
}
|
||||
|
||||
function handleViewHistory(row) {
|
||||
historyTitle.value = '试算详情 #' + row.quoteId
|
||||
historyOpen.value = true
|
||||
historyLoading.value = true
|
||||
viewResult.value = null
|
||||
getQuote(row.quoteId).then(res => {
|
||||
viewResult.value = res.data || null
|
||||
}).finally(() => {
|
||||
historyLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const form = reactive({
|
||||
customerId: undefined,
|
||||
@@ -195,6 +352,8 @@ function handleQuote() {
|
||||
result.value = res.data || null
|
||||
if (!result.value) {
|
||||
errorMsg.value = res.msg || '计算失败'
|
||||
} else if (historyTableRef.value && historyTableRef.value.refresh) {
|
||||
historyTableRef.value.refresh()
|
||||
}
|
||||
}).catch(err => {
|
||||
errorMsg.value = (err && (err.msg || err.message)) || '计算失败'
|
||||
@@ -257,6 +416,38 @@ function roundModeLabel(mode) {
|
||||
return map[mode] || mode || '四舍五入'
|
||||
}
|
||||
|
||||
function feeTypeLabel(type) {
|
||||
const found = (bms_fee_type.value || []).find(d => d.value === type)
|
||||
return found ? found.label : ''
|
||||
}
|
||||
|
||||
function displayCurrency(v) {
|
||||
if (!v || v === 'RMB' || v === 'rmb') return 'CNY'
|
||||
return v
|
||||
}
|
||||
|
||||
function displayFeeName(row) {
|
||||
const typeLabel = feeTypeLabel(row.feeType)
|
||||
const name = String(row.feeName || row.fee_name || '').trim()
|
||||
const isSurcharge = row.feeType === 'SURCHARGE' || row.feeType === '附加费'
|
||||
if (isSurcharge) {
|
||||
if (name && name !== typeLabel && name !== 'SURCHARGE' && name !== '附加费') {
|
||||
return name
|
||||
}
|
||||
const fromRemark = specificNameFromRemark(row.remark)
|
||||
if (fromRemark) {
|
||||
return fromRemark
|
||||
}
|
||||
}
|
||||
return name || typeLabel || row.feeType || '-'
|
||||
}
|
||||
|
||||
function specificNameFromRemark(remark) {
|
||||
if (!remark) return ''
|
||||
const first = String(remark).split(';')[0].trim()
|
||||
return first.replace(/\s+-?\d+(\.\d+)?$/, '').trim()
|
||||
}
|
||||
|
||||
function sourceTagType(source) {
|
||||
if (source === 'PACKAGE_FIXED' || source === 'FIXED') return 'success'
|
||||
if (source === 'PACKAGE_DISCOUNT' || source === 'DISCOUNT') return 'warning'
|
||||
@@ -284,4 +475,10 @@ loadProducts()
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
.billing-quote .fee-table {
|
||||
margin-top: 12px;
|
||||
}
|
||||
.billing-quote .history-card {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
<el-step title="基本信息" />
|
||||
<el-step title="套餐明细" />
|
||||
<el-step title="绑定客户" />
|
||||
<el-step title="校验确认" />
|
||||
</el-steps>
|
||||
|
||||
<!-- 第 1 步 -->
|
||||
@@ -220,7 +219,7 @@
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="mb12"
|
||||
title="套餐创建后处于草稿状态,可暂不绑定客户。优先级数字越小越优先(1 = 最高)。同一客户可绑定多个套餐。"
|
||||
title="套餐创建后处于草稿状态,可暂不绑定客户。一个客户只能绑定一个套餐。"
|
||||
/>
|
||||
<el-table :data="form.customers" border empty-text="暂未绑定客户">
|
||||
<el-table-column label="客户ID" min-width="120">
|
||||
@@ -256,29 +255,10 @@
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 第 4 步 -->
|
||||
<el-card v-show="active === 3" shadow="never" class="step-card">
|
||||
<template #header>
|
||||
<div class="card-head">
|
||||
<span>第 4 步 · 校验确认</span>
|
||||
<el-button type="primary" size="small" @click="runValidate">重新校验</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="checkItems" border empty-text="点击「下一步」或「重新校验」执行校验">
|
||||
<el-table-column label="校验项" prop="item" min-width="200" />
|
||||
<el-table-column label="结果" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="checkTag(row.result)" size="small">{{ checkLabel(row.result) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="说明" prop="message" min-width="280" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<div class="wizard-actions">
|
||||
<el-button @click="goBack">取消</el-button>
|
||||
<el-button v-if="active > 0" @click="active--">上一步</el-button>
|
||||
<el-button v-if="active < 3" type="primary" @click="nextStep">下一步</el-button>
|
||||
<el-button v-if="active < 2" type="primary" @click="nextStep">下一步</el-button>
|
||||
<el-button type="info" :loading="saving" @click="saveDraft">保存草稿</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="saveAndSubmit">保存并提交审核</el-button>
|
||||
</div>
|
||||
@@ -286,7 +266,7 @@
|
||||
</template>
|
||||
|
||||
<script setup name="PriceSetCreate">
|
||||
import { getPriceSet, savePriceSet, validatePriceSet } from '@/api/price/set'
|
||||
import { getPriceSet, savePriceSet } from '@/api/price/set'
|
||||
import { listProduct, listProductCountries } from '@/api/price/product'
|
||||
import { listSurchargeByProduct } from '@/api/price/surcharge'
|
||||
|
||||
@@ -296,7 +276,6 @@ const route = useRoute()
|
||||
const active = ref(0)
|
||||
const saving = ref(false)
|
||||
const formRef = ref()
|
||||
const checkItems = ref([])
|
||||
const productOptions = ref([])
|
||||
/** productCode -> bms_product_country rows */
|
||||
const countryCache = reactive({})
|
||||
@@ -571,6 +550,8 @@ function buildPayload(submitFlag) {
|
||||
...c,
|
||||
customerId: Number(c.customerId)
|
||||
}))
|
||||
console.log('details', details)
|
||||
|
||||
return {
|
||||
priceSetId: form.priceSetId,
|
||||
priceSetName: form.priceSetName,
|
||||
@@ -592,20 +573,9 @@ function nextStep() {
|
||||
})
|
||||
return
|
||||
}
|
||||
if (active.value === 2) {
|
||||
active.value++
|
||||
runValidate()
|
||||
return
|
||||
}
|
||||
active.value++
|
||||
}
|
||||
|
||||
function runValidate() {
|
||||
validatePriceSet(buildPayload(false)).then(res => {
|
||||
checkItems.value = res.data || []
|
||||
})
|
||||
}
|
||||
|
||||
function saveDraft() {
|
||||
formRef.value.validate(valid => {
|
||||
if (!valid) {
|
||||
@@ -627,6 +597,11 @@ function saveAndSubmit() {
|
||||
}
|
||||
|
||||
function doSave(submit) {
|
||||
const ids = (form.customers || []).filter(c => c.customerId).map(c => String(c.customerId))
|
||||
if (new Set(ids).size !== ids.length) {
|
||||
proxy.$modal.msgError('该客户已绑定套餐,一个客户只能绑定一个套餐')
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
savePriceSet(buildPayload(submit)).then(res => {
|
||||
form.priceSetId = res.data
|
||||
@@ -646,16 +621,6 @@ function goBack() {
|
||||
}
|
||||
}
|
||||
|
||||
function checkTag(result) {
|
||||
const map = { PASS: 'success', WARN: 'warning', FAIL: 'danger', INFO: 'primary' }
|
||||
return map[result] || 'info'
|
||||
}
|
||||
|
||||
function checkLabel(result) {
|
||||
const map = { PASS: '通过', WARN: '待确认', FAIL: '失败', INFO: '提示' }
|
||||
return map[result] || result
|
||||
}
|
||||
|
||||
function loadExisting(id) {
|
||||
getPriceSet(id).then(res => {
|
||||
const data = res.data || {}
|
||||
|
||||
@@ -158,7 +158,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="rel-hint">优先级数字越小越优先;同一客户绑定多个套餐时,按优先级 → 生效日期依次命中。</div>
|
||||
<div class="rel-hint">一个客户只能绑定一个套餐;解绑后可绑定其他套餐。</div>
|
||||
</el-card>
|
||||
|
||||
<el-dialog :title="detailTitle" v-model="detailOpen" width="560px" append-to-body>
|
||||
|
||||
Reference in New Issue
Block a user