新增打单产品渠道查询与替换功能

This commit is contained in:
wuweihua
2026-11-11 14:50:34 +08:00
parent 41a80f9794
commit 7e1041391a
20 changed files with 603 additions and 109 deletions
+16
View File
@@ -162,3 +162,19 @@ export function removeLabelProducts(priceSetId, productIds) {
})
}
export function listLabelProductChannels(productId, payType = 'AR') {
return request({
url: '/price/label/product/' + productId + '/channels',
method: 'get',
params: { payType }
})
}
export function replaceLabelProductChannels(productId, data) {
return request({
url: '/price/label/product/' + productId + '/channels',
method: 'put',
data
})
}
+2 -1
View File
@@ -62,7 +62,8 @@ export const BILLING_BILL_EXPORT_FIELDS = [
{ prop: 'receiveWeight', label: '收货重' },
{ prop: 'incomeTotal', label: '应收总额' },
{ prop: 'payTotal', label: '应付总额' },
{ prop: 'matchSourceLabel', label: '计价来源' },
{ prop: 'productFloorCode', label: '渠道编码' },
{ prop: 'productFloor', label: '渠道名称' },
{ prop: 'orderStatusLabel', label: '订单状态' },
{ prop: 'billingFailStatusLabel', label: '计费状态' },
{ prop: 'customerActiveStatusLabel', label: '客户状态' },
@@ -80,9 +80,8 @@
<el-descriptions-item label="原计费重(kg)">{{ formatWeight(detail.weight) }}</el-descriptions-item>
<el-descriptions-item label="重量段">{{ detail.weightLabel || '-' }}</el-descriptions-item>
<el-descriptions-item label="计费时间">{{ detail.bizDate || '-' }}</el-descriptions-item>
<el-descriptions-item label="计价来源">
<el-tag size="small" :type="sourceTagType(detail.matchSource)">{{ sourceLabel(detail.matchSource) }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="渠道编码">{{ detail.productFloorCode || '-' }}</el-descriptions-item>
<el-descriptions-item label="渠道名称">{{ detail.productFloor || '-' }}</el-descriptions-item>
<el-descriptions-item label="计费状态">
<el-tag size="small" :type="billingFailTagType(detail.billingFailStatus)">{{ billingFailLabel(detail.billingFailStatus) }}</el-tag>
</el-descriptions-item>
@@ -96,10 +95,6 @@
<el-descriptions-item label="运费单价" >{{ formatRate(detail.freightRate) }}</el-descriptions-item>
<el-descriptions-item label="挂号费" >{{ formatMoney(detail.registrationFee) }}</el-descriptions-item>
</el-descriptions>
<div v-if="detail.matchMessage" class="match-note">
<span class="match-note__label">命中说明</span>
<span>{{ detail.matchMessage }}</span>
</div>
</section>
<section class="detail-block">
@@ -422,18 +417,6 @@ function billingFailTagType(status) {
return 'success'
}
function sourceLabel(source) {
const map = {
PACKAGE_FIXED: '客户固定价套餐',
PACKAGE_DISCOUNT: '客户折扣套餐',
PRODUCT_DEFAULT: '产品默认价',
BILLING_FAIL: '计费失败',
FIXED: '客户固定价套餐',
DISCOUNT: '客户折扣套餐'
}
return map[source] || source || '-'
}
function roundModeLabel(mode) {
const map = { UP: '向上取值', DOWN: '向下取整', HALF_UP: '四舍五入' }
return map[mode] || mode || '四舍五入'
@@ -466,14 +449,6 @@ function displayFeeName(row) {
return name || typeLabel || row.feeType || '-'
}
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'
if (source === 'BILLING_FAIL') return 'danger'
return ''
}
function refundLabel(status) {
const map = { NONE: '未退费', REFUNDED: '已退费' }
return map[status] || status || '未退费'
@@ -8,8 +8,7 @@
@close="emit('update:visible', false)"
>
<div class="toolbar">
<el-input v-model="keyword" clearable placeholder="产品名称 / 工作表 / 国家" style="width: 260px" />
<el-button type="danger" plain :disabled="!selectedIds.length" @click="handleRemove" v-hasPermi="['price:label:remove']">移除</el-button>
<el-input v-model="keyword" clearable :placeholder="phSearch" style="width: 300px" />
</div>
<el-table
v-loading="loading"
@@ -17,30 +16,26 @@
border
height="calc(100vh - 220px)"
row-key="termId"
@selection-change="onSelect"
>
<el-table-column type="selection" width="48" align="center" />
<el-table-column prop="productId" label="产品编号" width="90" align="center" />
<el-table-column prop="productName" label="产品名称" min-width="150" show-overflow-tooltip />
<el-table-column prop="supplierName" label="价卡归属" width="100" align="center" />
<el-table-column prop="countryScope" label="国家/口岸" min-width="120" show-overflow-tooltip />
<el-table-column prop="chargeType" label="费用类型" width="110" align="center" />
<el-table-column prop="sheetName" label="报价工作表" min-width="140" show-overflow-tooltip />
<el-table-column prop="versionNo" label="版本" width="70" align="center" />
<el-table-column label="有效期" min-width="180" align="center">
<el-table-column prop="productId" :label="colId" width="90" align="center" />
<el-table-column prop="productName" :label="colName" min-width="150" show-overflow-tooltip />
<el-table-column :label="colTerm" min-width="180" align="center">
<template #default="{ row }">{{ termText(row) }}</template>
</el-table-column>
<el-table-column label="状态" 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 prop="weightCount" label="重量段" width="80" align="center" />
<el-table-column prop="surchargeCount" label="附加费" width="80" align="center" />
<el-table-column label="操作" width="160" align="center" fixed="right">
<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>
<template #default="{ row }">{{ boundChannelText(row) }}</template>
</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="danger" @click="handleRemove([row.productId])" v-hasPermi="['price:label:remove']">移除</el-button>
<el-button link type="primary" @click="openBindList(row)" v-hasPermi="['price:label:edit']">绑定</el-button>
</template>
</el-table-column>
</el-table>
@@ -51,11 +46,89 @@
<el-button @click="detailVisible = false">关闭</el-button>
</template>
</el-drawer>
<el-drawer
v-model="bindListVisible"
:title="bindListTitle"
size="520px"
append-to-body
destroy-on-close
@closed="onBindListClosed"
>
<div class="bind-list-toolbar">
<el-button type="primary" plain @click="openAddChannels" v-hasPermi="['price:label:edit']">添加渠道</el-button>
</div>
<el-table
v-loading="bindListLoading"
:data="boundChannels"
border
height="calc(100vh - 240px)"
:empty-text="emptyBound"
>
<el-table-column prop="channelCode" :label="colChannelCode" min-width="120" show-overflow-tooltip />
<el-table-column prop="channelName" :label="colChannelName" min-width="160" show-overflow-tooltip />
<el-table-column :label="colAction" width="90" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="danger" @click="removeBoundChannel(row)" v-hasPermi="['price:label:edit']">移除</el-button>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="bindListVisible = false">关闭</el-button>
</template>
</el-drawer>
<el-dialog
v-model="addVisible"
:title="addTitle"
width="560px"
append-to-body
destroy-on-close
@closed="onAddClosed"
>
<div v-loading="addLoading">
<el-form label-width="88px">
<el-form-item :label="lblProduct">
<span>{{ bindProductName }}</span>
</el-form-item>
<el-form-item :label="lblChannels">
<el-select
v-model="addChannelIds"
multiple
filterable
clearable
collapse-tags
collapse-tags-tooltip
:placeholder="phChannels"
style="width: 100%"
>
<el-option
v-for="item in addableChannelOptions"
:key="item.id"
:label="channelOptionLabel(item)"
:value="String(item.id)"
/>
</el-select>
</el-form-item>
<div class="bind-hint">{{ bindHint }}</div>
</el-form>
</div>
<template #footer>
<el-button @click="addVisible = false">取消</el-button>
<el-button type="primary" :loading="addSaving" @click="submitAddChannels">保存</el-button>
</template>
</el-dialog>
</el-drawer>
</template>
<script setup>
import { listLabelProducts, getLabelProduct, removeLabelProducts } from '@/api/price/label'
import {
listLabelProducts,
getLabelProduct,
listLabelProductChannels,
replaceLabelProductChannels
} from '@/api/price/label'
import { listChannelOptions } from '@/api/masterdata/serviceChannel'
import PayConfigDetail from '@/views/price/basic/pay/components/PayConfigDetail.vue'
const props = defineProps({
@@ -69,11 +142,40 @@ const { proxy } = getCurrentInstance()
const loading = ref(false)
const rows = ref([])
const keyword = ref('')
const selectedIds = ref([])
const detailVisible = ref(false)
const detailLoading = ref(false)
const detail = ref(null)
const bindListVisible = ref(false)
const bindListLoading = ref(false)
const bindProductId = ref(null)
const bindProductName = ref('')
const boundChannels = ref([])
const channelOptions = ref([])
const addVisible = ref(false)
const addLoading = ref(false)
const addSaving = ref(false)
const addChannelIds = ref([])
const phSearch = '产品名称 / 绑定渠道'
const colId = '产品编号'
const colName = '产品名称'
const colTerm = '有效期'
const colStatus = '状态'
const colWeight = '重量段'
const colSc = '附加费'
const colBoundChannels = '绑定渠道'
const colAction = '操作'
const colChannelCode = '渠道编码'
const colChannelName = '渠道名称'
const lblProduct = '产品'
const lblChannels = '添加渠道'
const phChannels = '请选择要添加的渠道(可多选)'
const bindHint = '绑定为收款(AR),落库 srv_channel_product_map;一个渠道同一收付类型仅保留一条产品映射。'
const emptyBound = '暂无绑定渠道'
const addTitle = '添加渠道'
const title = computed(() => {
const v = props.version || {}
const label = v.versionLabel || (v.versionNo != null ? `V${v.versionNo}` : '')
@@ -86,15 +188,24 @@ const detailTitle = computed(() => {
return `${detail.value.productName || '产品'} ${no}`.trim()
})
const bindListTitle = computed(() => {
return bindProductName.value ? `绑定渠道 · ${bindProductName.value}` : '绑定渠道'
})
const filteredRows = computed(() => {
const kw = keyword.value.trim().toLowerCase()
if (!kw) return rows.value
return rows.value.filter(row => {
const text = [row.productName, row.sheetName, row.countryScope, row.supplierName].join(' ').toLowerCase()
const text = [row.productName, boundChannelText(row)].join(' ').toLowerCase()
return text.includes(kw)
})
})
const addableChannelOptions = computed(() => {
const bound = new Set((boundChannels.value || []).map(item => String(item.channelId)))
return (channelOptions.value || []).filter(item => item && item.id != null && !bound.has(String(item.id)))
})
function formatHour(val) {
if (!val) return ''
const text = String(val).replace('T', ' ')
@@ -120,8 +231,17 @@ function termStatusTag(status) {
return map[String(status)] || 'info'
}
function onSelect(selection) {
selectedIds.value = (selection || []).map(item => item.productId).filter(Boolean)
function channelOptionLabel(item) {
const code = item?.channelCode || item?.id || ''
const name = item?.channelName || ''
if (code && name && code !== name) return code + ' / ' + name
return code || name || '-'
}
function boundChannelText(row) {
if (!row) return '-'
if (row.boundChannelText) return row.boundChannelText
return '-'
}
async function load() {
@@ -150,16 +270,96 @@ async function openProduct(row) {
}
}
function handleRemove(ids) {
const list = (Array.isArray(ids) ? ids : selectedIds.value).filter(Boolean)
if (!list.length || !props.priceSetId) return
proxy.$modal.confirm(`确认从本套餐移除选中的 ${list.length} 个产品?`).then(() => {
return removeLabelProducts(props.priceSetId, list.join(','))
}).then(() => {
proxy.$modal.msgSuccess('已移除')
load()
emit('changed')
}).catch(() => {})
async function ensureChannelOptions() {
if (channelOptions.value.length) return
const res = await listChannelOptions({ blOpen: '1' })
channelOptions.value = res.data || []
}
async function loadBoundChannels() {
if (!bindProductId.value) {
boundChannels.value = []
return
}
bindListLoading.value = true
try {
const res = await listLabelProductChannels(bindProductId.value, 'AR')
boundChannels.value = res.data || []
} finally {
bindListLoading.value = false
}
}
async function openBindList(row) {
if (!row?.productId) return
bindProductId.value = row.productId
bindProductName.value = row.productName || String(row.productId)
boundChannels.value = []
bindListVisible.value = true
await loadBoundChannels()
}
function onBindListClosed() {
bindProductId.value = null
bindProductName.value = ''
boundChannels.value = []
addVisible.value = false
addChannelIds.value = []
}
async function openAddChannels() {
addChannelIds.value = []
addVisible.value = true
addLoading.value = true
try {
await ensureChannelOptions()
} finally {
addLoading.value = false
}
}
function onAddClosed() {
addChannelIds.value = []
}
function currentBoundIds() {
return (boundChannels.value || []).map(item => String(item.channelId)).filter(Boolean)
}
function submitAddChannels() {
if (!bindProductId.value) return
const selected = (addChannelIds.value || []).map(String).filter(Boolean)
if (!selected.length) {
proxy.$modal.msgWarning('请先选择要添加的渠道')
return
}
const merged = Array.from(new Set(currentBoundIds().concat(selected)))
addSaving.value = true
replaceLabelProductChannels(bindProductId.value, {
payType: 'AR',
channelIds: merged
}).then((res) => {
proxy.$modal.msgSuccess((res && res.msg) || '添加成功')
addVisible.value = false
return loadBoundChannels()
}).then(() => load()).catch(() => {}).finally(() => {
addSaving.value = false
})
}
function removeBoundChannel(row) {
if (!bindProductId.value || !row?.channelId) return
const label = channelOptionLabel(row)
proxy.$modal.confirm(`确认移除渠道「${label}」?`).then(() => {
const remain = currentBoundIds().filter(id => id !== String(row.channelId))
return replaceLabelProductChannels(bindProductId.value, {
payType: 'AR',
channelIds: remain
})
}).then((res) => {
proxy.$modal.msgSuccess((res && res.msg) || '已移除')
return loadBoundChannels()
}).then(() => load()).catch(() => {})
}
watch(() => [props.visible, props.priceSetId, props.version?.versionNo], ([vis]) => {
@@ -174,4 +374,15 @@ watch(() => [props.visible, props.priceSetId, props.version?.versionNo], ([vis])
margin-bottom: 12px;
align-items: center;
}
.bind-list-toolbar {
display: flex;
justify-content: flex-end;
margin-bottom: 12px;
}
.bind-hint {
margin: -4px 0 0 88px;
color: #909399;
font-size: 12px;
line-height: 1.5;
}
</style>