新增打单产品明细保存接口,优化附加费管理逻辑
This commit is contained in:
@@ -155,6 +155,17 @@ export function getLabelProduct(productId, termId) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 保存打单产品明细(重量段/附加费) */
|
||||
export function updateLabelProduct(data) {
|
||||
return request({
|
||||
url: '/price/label/product',
|
||||
method: 'put',
|
||||
data,
|
||||
headers: { repeatSubmit: false },
|
||||
timeout: 120000
|
||||
})
|
||||
}
|
||||
|
||||
export function removeLabelProducts(priceSetId, productIds) {
|
||||
return request({
|
||||
url: '/price/label/' + priceSetId + '/products/' + productIds,
|
||||
|
||||
@@ -79,16 +79,26 @@
|
||||
<div class="pay-edit-bar">
|
||||
<el-input v-model="pane.feeKeyword" clearable placeholder="筛选附加费用、地区、价格、货币或费用解释" class="pay-filter" @input="pane.feePageNo = 1" />
|
||||
<el-button v-if="editable" size="small" type="primary" plain @click="openFeePicker(pane)">添加附加费</el-button>
|
||||
<template v-if="billSelect">
|
||||
<el-button size="small" @click="setFeesBill(pane, true)">全选计费</el-button>
|
||||
<el-button size="small" @click="setFeesBill(pane, false)">取消全选</el-button>
|
||||
<span class="pay-bill-tip">勾选后才会写入 pay_surcharge;默认不勾选</span>
|
||||
</template>
|
||||
<el-button
|
||||
v-if="editable"
|
||||
size="small"
|
||||
type="danger"
|
||||
plain
|
||||
:disabled="!feeCheckedCount(pane)"
|
||||
@click="removeCheckedFees(pane)"
|
||||
>批量删除</el-button>
|
||||
</div>
|
||||
<el-table :data="pageFees(pane)" border size="small" empty-text="没有附加费">
|
||||
<el-table-column v-if="billSelect" label="是否计费" width="88" align="center" fixed="left">
|
||||
<el-table-column v-if="editable" width="48" align="center" fixed="left">
|
||||
<template #header>
|
||||
<el-checkbox
|
||||
:model-value="isAllFilteredFeesChecked(pane)"
|
||||
:indeterminate="isFeeCheckIndeterminate(pane)"
|
||||
@update:model-value="v => setFilteredFeesChecked(pane, v)"
|
||||
/>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<el-checkbox :model-value="row.needBill === true" @update:model-value="v => { row.needBill = !!v }" />
|
||||
<el-checkbox :model-value="row._checked === true" @update:model-value="v => { row._checked = !!v }" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="feeName" label="附加费用" min-width="180" show-overflow-tooltip>
|
||||
@@ -266,11 +276,6 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/** 打单套餐:附加费表显示「是否计费」勾选,仅勾选行入库 */
|
||||
billSelect: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
savingRowKey: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
@@ -541,18 +546,51 @@ function filteredFees(pane) {
|
||||
|
||||
function feeTabLabel(pane) {
|
||||
const fees = pane.item.surcharges || []
|
||||
if (!props.billSelect) return '附加费(' + fees.length + ')'
|
||||
const n = fees.filter(f => f && f.needBill === true).length
|
||||
return '附加费(已选 ' + n + '/' + fees.length + ')'
|
||||
return '附加费(' + fees.length + ')'
|
||||
}
|
||||
|
||||
function setFeesBill(pane, value) {
|
||||
const fees = pane.item.surcharges || []
|
||||
for (const fee of fees) {
|
||||
if (fee) fee.needBill = !!value
|
||||
function feeCheckedCount(pane) {
|
||||
return (pane?.item?.surcharges || []).filter(fee => fee && fee._checked === true).length
|
||||
}
|
||||
|
||||
function isAllFilteredFeesChecked(pane) {
|
||||
const rows = filteredFees(pane)
|
||||
return rows.length > 0 && rows.every(fee => fee && fee._checked === true)
|
||||
}
|
||||
|
||||
function isFeeCheckIndeterminate(pane) {
|
||||
const rows = filteredFees(pane)
|
||||
if (!rows.length) return false
|
||||
const n = rows.filter(fee => fee && fee._checked === true).length
|
||||
return n > 0 && n < rows.length
|
||||
}
|
||||
|
||||
function setFilteredFeesChecked(pane, value) {
|
||||
for (const fee of filteredFees(pane)) {
|
||||
if (fee) fee._checked = !!value
|
||||
}
|
||||
}
|
||||
|
||||
async function removeCheckedFees(pane) {
|
||||
const list = pane?.item?.surcharges
|
||||
if (!list?.length) return
|
||||
const checked = list.filter(fee => fee && fee._checked === true)
|
||||
if (!checked.length) {
|
||||
proxy.$modal.msgWarning('请先勾选要删除的附加费')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await proxy.$modal.confirm(`确认删除所选 ${checked.length} 项附加费?`)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
const remain = list.filter(fee => !(fee && fee._checked === true))
|
||||
pane.item.surcharges = remain
|
||||
const maxPage = Math.max(1, Math.ceil(remain.length / pageSize) || 1)
|
||||
if (pane.feePageNo > maxPage) pane.feePageNo = maxPage
|
||||
proxy.$modal.msgSuccess(`已删除 ${checked.length} 项`)
|
||||
}
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const feePickerVisible = ref(false)
|
||||
const feePickerPane = ref(null)
|
||||
@@ -589,9 +627,7 @@ function configToFeeRow(cfg, product) {
|
||||
priceUnit: isPercent ? '%' : (currency + '/ITEM'),
|
||||
feeDesc: String(cfg?.scNote || '').trim(),
|
||||
calcRule: toCalcRule(cfg?.chargeType),
|
||||
calcParam: '',
|
||||
// 从配置添加默认要计费
|
||||
needBill: true
|
||||
calcParam: ''
|
||||
}
|
||||
}
|
||||
|
||||
@@ -738,10 +774,6 @@ function statusText(status) {
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.pay-bill-tip {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
.pay-hint {
|
||||
margin: 0 0 8px;
|
||||
color: var(--el-text-color-secondary);
|
||||
|
||||
@@ -14,6 +14,13 @@
|
||||
placeholder="搜索附加费名称、计费方式、币种、国家或备注"
|
||||
class="picker-search"
|
||||
/>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
icon="Plus"
|
||||
v-hasPermi="['price:surcharge:add']"
|
||||
@click="openCreate"
|
||||
>新建附加费</el-button>
|
||||
<span class="picker-meta">共 {{ filteredRows.length }} 项,已选 {{ picked.length }} 项</span>
|
||||
</div>
|
||||
|
||||
@@ -43,17 +50,20 @@
|
||||
<el-table-column prop="scNote" label="备注" min-width="160" show-overflow-tooltip />
|
||||
</el-table>
|
||||
|
||||
<div class="picker-hint">勾选后点确定写入当前产品附加费表;已存在相同配置的不会重复添加。</div>
|
||||
<div class="picker-hint">可先「新建附加费」写入配置库;勾选后点确定写入当前产品附加费表;已存在相同配置的不会重复添加。</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取 消</el-button>
|
||||
<el-button type="primary" :disabled="!picked.length" @click="submit">确 定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<surcharge-form-dialog ref="formDialogRef" @success="onCreated" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { listSurchargeOptions } from '@/api/price/surcharge'
|
||||
import SurchargeFormDialog from '@/views/price/basic/surcharge/components/SurchargeFormDialog.vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
@@ -64,6 +74,7 @@ const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const tableRef = ref()
|
||||
const formDialogRef = ref()
|
||||
const loading = ref(false)
|
||||
const keyword = ref('')
|
||||
const rows = ref([])
|
||||
@@ -187,9 +198,25 @@ function onOpened() {
|
||||
nextTick(() => {
|
||||
tableRef.value?.clearSelection?.()
|
||||
})
|
||||
if (!rows.value.length) {
|
||||
loadRows()
|
||||
loadRows()
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
formDialogRef.value?.open()
|
||||
}
|
||||
|
||||
async function onCreated() {
|
||||
const before = new Set((rows.value || []).map(row => String(row.scId)))
|
||||
await loadRows()
|
||||
// 自动勾选刚新建的配置
|
||||
const next = new Set(pickedIds.value)
|
||||
for (const row of rows.value || []) {
|
||||
if (row?.scId == null) continue
|
||||
const id = String(row.scId)
|
||||
if (!before.has(id) && canSelect(row)) next.add(id)
|
||||
}
|
||||
pickedIds.value = next
|
||||
nextTick(() => restoreSelection())
|
||||
}
|
||||
|
||||
function submit() {
|
||||
@@ -215,11 +242,12 @@ watch(() => props.modelValue, v => {
|
||||
.picker-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.picker-search {
|
||||
width: 360px;
|
||||
width: 320px;
|
||||
}
|
||||
.picker-meta {
|
||||
color: var(--el-text-color-secondary);
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
</div>
|
||||
|
||||
<el-drawer v-model="detailVisible" :title="detailTitle" size="78%" destroy-on-close append-to-body>
|
||||
<pay-config-detail :product="detailProduct" :editable="false" :bill-select="true" />
|
||||
<pay-config-detail :product="detailProduct" :editable="false" />
|
||||
<template #footer>
|
||||
<el-button @click="detailVisible = false">关闭</el-button>
|
||||
</template>
|
||||
@@ -187,9 +187,7 @@ async function onFile(uploadFile) {
|
||||
const data = res.data || {}
|
||||
data.products = (data.products || []).map((item, index) => ({
|
||||
...item,
|
||||
rowKey: `${item.sheetName || ''}-${item.productName || ''}-${index}`,
|
||||
// 解析后默认不计费,勾选后才入 pay_surcharge
|
||||
surcharges: (item.surcharges || []).map(fee => ({ ...fee, needBill: fee?.needBill === true }))
|
||||
rowKey: `${item.sheetName || ''}-${item.productName || ''}-${index}`
|
||||
}))
|
||||
preview.value = data
|
||||
const picked = data.products.filter(row => canSelect(row))
|
||||
@@ -312,17 +310,16 @@ async function submit(payload = {}) {
|
||||
const productsPayload = (payload.products && payload.products.length)
|
||||
? payload.products
|
||||
: selected.value.map(row => ({ ...row, productType: 'AR' }))
|
||||
const filteredProducts = productsPayload.map(product => ({
|
||||
const productsForSave = productsPayload.map(product => ({
|
||||
...product,
|
||||
productType: product.productType || 'AR',
|
||||
surcharges: (product.surcharges || []).filter(fee => fee && fee.needBill === true)
|
||||
productType: product.productType || 'AR'
|
||||
}))
|
||||
const res = await saveLabelImport({
|
||||
priceSetId: payload.priceSetId || null,
|
||||
priceSetName: String(payload.priceSetName || '').trim(),
|
||||
priceSetNote: payload.priceSetNote || '',
|
||||
overwrite: payload.overwrite != null ? payload.overwrite : overwrite.value,
|
||||
products: filteredProducts
|
||||
products: productsForSave
|
||||
})
|
||||
const data = res.data || {}
|
||||
return {
|
||||
|
||||
@@ -22,11 +22,11 @@
|
||||
<el-table-column :label="colTerm" min-width="180" align="center">
|
||||
<template #default="{ row }">{{ termText(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="colStatus" width="90" align="center">
|
||||
<!-- <el-table-column :label="colStatus" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="termStatusTag(row.termStatus)" size="small">{{ termStatusText(row.termStatus) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table-column> -->
|
||||
<el-table-column prop="weightCount" :label="colWeight" width="80" align="center" />
|
||||
<el-table-column prop="surchargeCount" :label="colSc" width="80" align="center" />
|
||||
<el-table-column :label="colBoundChannels" min-width="180" show-overflow-tooltip>
|
||||
@@ -34,16 +34,29 @@
|
||||
</el-table-column>
|
||||
<el-table-column :label="colAction" width="160" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openProduct(row)" v-hasPermi="['price:label:query']">查看</el-button>
|
||||
<el-button link type="primary" @click="openProduct(row)" v-hasPermi="['price:label:query', 'price:label:edit']">编辑</el-button>
|
||||
<el-button link type="primary" @click="openBindList(row)" v-hasPermi="['price:label:edit']">绑定</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-drawer v-model="detailVisible" :title="detailTitle" size="78%" destroy-on-close append-to-body>
|
||||
<pay-config-detail v-loading="detailLoading" :product="detail" :editable="false" />
|
||||
<pay-config-detail
|
||||
v-loading="detailLoading"
|
||||
:product="detail"
|
||||
:editable="canEditDetail"
|
||||
:saving-row-key="savingRowKey"
|
||||
@save-row="saveDetailRow"
|
||||
/>
|
||||
<template #footer>
|
||||
<el-button @click="detailVisible = false">关闭</el-button>
|
||||
<el-button
|
||||
v-if="canEditDetail"
|
||||
type="primary"
|
||||
:loading="detailSaving"
|
||||
v-hasPermi="['price:label:edit']"
|
||||
@click="saveDetail"
|
||||
>保存明细</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
|
||||
@@ -125,9 +138,11 @@
|
||||
import {
|
||||
listLabelProducts,
|
||||
getLabelProduct,
|
||||
updateLabelProduct,
|
||||
listLabelProductChannels,
|
||||
replaceLabelProductChannels
|
||||
} from '@/api/price/label'
|
||||
import { checkPermi } from '@/utils/permission'
|
||||
import { listChannelOptions } from '@/api/masterdata/serviceChannel'
|
||||
import PayConfigDetail from '@/views/price/basic/pay/components/PayConfigDetail.vue'
|
||||
|
||||
@@ -144,7 +159,10 @@ const rows = ref([])
|
||||
const keyword = ref('')
|
||||
const detailVisible = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const detailSaving = ref(false)
|
||||
const savingRowKey = ref('')
|
||||
const detail = ref(null)
|
||||
const canEditDetail = computed(() => checkPermi(['price:label:edit']))
|
||||
|
||||
const bindListVisible = ref(false)
|
||||
const bindListLoading = ref(false)
|
||||
@@ -258,9 +276,66 @@ async function load() {
|
||||
}
|
||||
}
|
||||
|
||||
function cleanMoney(product) {
|
||||
if (!product) return
|
||||
;(product.weightBands || []).forEach(row => {
|
||||
if (row && row.price === '') row.price = null
|
||||
})
|
||||
;(product.surcharges || []).forEach(row => {
|
||||
if (row && row.amount === '') row.amount = null
|
||||
})
|
||||
}
|
||||
|
||||
async function persistDetail() {
|
||||
if (!detail.value?.productId) {
|
||||
throw new Error('NO_PRODUCT')
|
||||
}
|
||||
cleanMoney(detail.value)
|
||||
await updateLabelProduct(detail.value)
|
||||
const termId = detail.value.termId
|
||||
const res = await getLabelProduct(detail.value.productId, termId)
|
||||
detail.value = res.data || detail.value
|
||||
await load()
|
||||
emit('changed')
|
||||
}
|
||||
|
||||
async function saveDetail() {
|
||||
if (!detail.value || detailSaving.value || savingRowKey.value) return
|
||||
detailSaving.value = true
|
||||
try {
|
||||
await persistDetail()
|
||||
proxy.$modal.msgSuccess('明细已保存')
|
||||
} catch (e) {
|
||||
// request 拦截器已提示
|
||||
} finally {
|
||||
detailSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function saveDetailRow(payload) {
|
||||
if (!detail.value || savingRowKey.value || detailSaving.value) {
|
||||
payload?.done?.(false)
|
||||
return
|
||||
}
|
||||
savingRowKey.value = payload?.rowKey || 'row'
|
||||
let ok = false
|
||||
try {
|
||||
await persistDetail()
|
||||
ok = true
|
||||
proxy.$modal.msgSuccess('该行已保存')
|
||||
} catch (e) {
|
||||
ok = false
|
||||
} finally {
|
||||
savingRowKey.value = ''
|
||||
payload?.done?.(ok)
|
||||
}
|
||||
}
|
||||
|
||||
async function openProduct(row) {
|
||||
detailVisible.value = true
|
||||
detailLoading.value = true
|
||||
detailSaving.value = false
|
||||
savingRowKey.value = ''
|
||||
detail.value = null
|
||||
try {
|
||||
const res = await getLabelProduct(row.productId, row.termId)
|
||||
|
||||
@@ -38,11 +38,11 @@
|
||||
<el-table-column label="失效时间" min-width="170" align="center">
|
||||
<template #default="{ row }">{{ formatHour(row.expireTime) || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<!-- <el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusTag(row.status)" size="small">{{ statusText(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table-column> -->
|
||||
<el-table-column label="产品数" width="90" align="center">
|
||||
<template #default="{ row }">{{ row.productCount || 0 }}</template>
|
||||
</el-table-column>
|
||||
|
||||
@@ -100,7 +100,6 @@
|
||||
<pay-config-detail
|
||||
:product="detailProduct"
|
||||
:editable="true"
|
||||
:bill-select="true"
|
||||
:saving-row-key="savingRowKey"
|
||||
@save-row="saveDetailRow"
|
||||
/>
|
||||
@@ -220,8 +219,7 @@ async function loadEditProducts() {
|
||||
}
|
||||
}
|
||||
}))
|
||||
// 库中已落库的附加费默认勾选(保留原计费项)
|
||||
productRows.value = markSurchargeBillFlags(details, true)
|
||||
productRows.value = details
|
||||
selectedProducts.value = []
|
||||
importMeta.sourceFile = details[0]?.sourceFile || ''
|
||||
importMeta.supplierName = details[0]?.supplierName || ''
|
||||
@@ -235,23 +233,6 @@ async function openImport() {
|
||||
importRef.value?.open()
|
||||
}
|
||||
|
||||
function markSurchargeBillFlags(products, defaultNeedBill) {
|
||||
return (products || []).map(product => ({
|
||||
...product,
|
||||
surcharges: (product.surcharges || []).map(fee => ({
|
||||
...fee,
|
||||
needBill: fee?.needBill === true ? true : (fee?.needBill === false ? false : !!defaultNeedBill)
|
||||
}))
|
||||
}))
|
||||
}
|
||||
|
||||
function withBillableSurcharges(products) {
|
||||
return (products || []).map(product => ({
|
||||
...product,
|
||||
surcharges: (product.surcharges || []).filter(fee => fee && fee.needBill === true)
|
||||
}))
|
||||
}
|
||||
|
||||
function onProductSelect(rows) {
|
||||
selectedProducts.value = rows || []
|
||||
}
|
||||
@@ -297,8 +278,7 @@ async function removeSelectedProducts() {
|
||||
}
|
||||
|
||||
function onImportConfirm(payload) {
|
||||
// 价卡解析行默认不计费,需手动勾选
|
||||
productRows.value = markSurchargeBillFlags(payload?.products || [], false)
|
||||
productRows.value = payload?.products || []
|
||||
selectedProducts.value = []
|
||||
importMeta.sourceFile = payload?.sourceFile || ''
|
||||
importMeta.supplierName = payload?.supplierName || ''
|
||||
@@ -414,7 +394,7 @@ async function finish() {
|
||||
try {
|
||||
const payload = {
|
||||
overwrite: editVersionMode.value ? true : importMeta.overwrite,
|
||||
products: withBillableSurcharges(productRows.value)
|
||||
products: productRows.value
|
||||
}
|
||||
if (editVersionMode.value) {
|
||||
payload.priceSetId = priceSetId.value
|
||||
|
||||
Reference in New Issue
Block a user