增加一个渠道绑定

This commit is contained in:
wuweihua
2026-11-10 16:21:07 +08:00
parent e6a9fcf0a2
commit 1a0483c7fe
20 changed files with 940 additions and 147 deletions
+29
View File
@@ -93,6 +93,13 @@ export function activateLabelVersion(priceSetId, versionNo, data) {
})
}
export function deleteLabelVersion(priceSetId, versionNo) {
return request({
url: '/price/label/' + priceSetId + '/versions/' + versionNo,
method: 'delete'
})
}
export function listLabelProducts(priceSetId, versionNo) {
return request({
url: '/price/label/' + priceSetId + '/products',
@@ -154,3 +161,25 @@ export function removeLabelProducts(priceSetId, productIds) {
method: 'delete'
})
}
export function listLabelChannels(priceSetId) {
return request({
url: '/price/label/' + priceSetId + '/channels',
method: 'get'
})
}
export function bindLabelChannels(priceSetId, data) {
return request({
url: '/price/label/' + priceSetId + '/channels',
method: 'put',
data
})
}
export function unbindLabelChannel(priceSetId, channelId) {
return request({
url: '/price/label/' + priceSetId + '/channels/' + channelId,
method: 'delete'
})
}
+12 -1
View File
@@ -51,7 +51,9 @@ export function updatePay(data) {
return request({
url: '/price/pay',
method: 'put',
data
data,
headers: { repeatSubmit: false },
timeout: 120000
})
}
@@ -72,6 +74,15 @@ export function bindChannelPay(channelId, productId, payType = 'AP') {
})
}
/** 批量绑定渠道应收/应付产品;至少传一个产品 */
export function batchBindChannelPay(data) {
return request({
url: '/price/pay/channel/batchBind',
method: 'put',
data
})
}
export function comparePay(leftId, rightId) {
return request({
url: '/price/pay/compare',
@@ -21,6 +21,16 @@
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['data:serviceChannel:export']">导出</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="Link"
:disabled="!ids.length"
@click="openBatchBind(selection, ids)"
v-hasPermi="['data:serviceChannel:edit']"
>批量绑定产品</el-button>
</el-col>
</template>
<template #col-blOpen="{ row }">
@@ -165,6 +175,29 @@
</template>
</el-dialog>
<el-dialog v-model="batchBindOpen" title="批量绑定应收/应付产品" width="520px" append-to-body @closed="onBatchBindClosed">
<el-form label-width="100px">
<el-form-item label="已选渠道">
<span>{{ batchBindIds.length }} 个</span>
</el-form-item>
<el-form-item label="应付产品">
<el-select v-model="batchApId" filterable clearable placeholder="可选,不选则不改应付绑定" style="width: 100%">
<el-option v-for="item in payOptions" :key="'ap-' + item.productId" :label="payOptionLabel(item)" :value="item.productId" />
</el-select>
</el-form-item>
<el-form-item label="应收产品">
<el-select v-model="batchArId" filterable clearable placeholder="可选,不选则不改应收绑定" style="width: 100%">
<el-option v-for="item in arOptions" :key="'ar-' + item.productId" :label="payOptionLabel(item)" :value="item.productId" />
</el-select>
</el-form-item>
<div class="pay-tip">至少选择一个产品;将覆盖已选渠道对应收付类型的绑定。</div>
</el-form>
<template #footer>
<el-button @click="batchBindOpen = false">取消</el-button>
<el-button type="primary" :loading="batchBindSaving" @click="submitBatchBind">确定</el-button>
</template>
</el-dialog>
</div>
</template>
@@ -174,7 +207,7 @@ import {
listServiceChannel, getServiceChannel, addServiceChannel,
updateServiceChannel, delServiceChannel, changeServiceChannelStatus
} from '@/api/masterdata/serviceChannel'
import { payOptions as listPayOptions, getChannelPay, bindChannelPay } from '@/api/price/pay'
import { payOptions as listPayOptions, getChannelPay, bindChannelPay, batchBindChannelPay } from '@/api/price/pay'
const { proxy } = getCurrentInstance()
const tableRef = ref()
@@ -190,6 +223,11 @@ const payProduct = ref(null)
const arProduct = ref(null)
const syncingPay = ref(false)
const syncingAr = ref(false)
const batchBindOpen = ref(false)
const batchBindSaving = ref(false)
const batchBindIds = ref([])
const batchApId = ref()
const batchArId = ref()
const payTip = computed(() => {
if (viewing.value) {
if (payProduct.value?.versionNo) return `已绑定第 ${payProduct.value.versionNo} 版。`
@@ -293,8 +331,7 @@ function resetPay() {
}
function payOptionLabel(item) {
const version = item.versionNo > 1 ? `(当前第 ${item.versionNo} 版)` : ''
return `${item.productName}${version}`
return item?.productName || ''
}
function loadPayOptions() {
@@ -447,6 +484,46 @@ function handleExport() {
...tableRef.value.getQueryParams()
}, `service_channel_${new Date().getTime()}.xlsx`)
}
function openBatchBind(selection, ids) {
const list = Array.isArray(ids) ? ids.filter(Boolean) : []
if (!list.length) {
proxy.$modal.msgWarning('请先勾选渠道')
return
}
batchBindIds.value = list.map(String)
batchApId.value = undefined
batchArId.value = undefined
loadPayOptions()
batchBindOpen.value = true
}
function onBatchBindClosed() {
batchBindIds.value = []
batchApId.value = undefined
batchArId.value = undefined
batchBindSaving.value = false
}
function submitBatchBind() {
if (!batchBindIds.value.length) return
if (!batchApId.value && !batchArId.value) {
proxy.$modal.msgWarning('请至少选择应付或应收产品')
return
}
batchBindSaving.value = true
batchBindChannelPay({
channelIds: batchBindIds.value,
apProductId: batchApId.value || null,
arProductId: batchArId.value || null
}).then(res => {
proxy.$modal.msgSuccess(res.msg || res.data || '批量绑定成功')
batchBindOpen.value = false
tableRef.value?.refresh()
}).finally(() => {
batchBindSaving.value = false
})
}
</script>
<style scoped>
@@ -23,24 +23,21 @@
<el-tabs v-model="pane.tab">
<el-tab-pane :label="`重量段(${(pane.item.weightBands || []).length})`" name="band">
<el-input v-model="pane.bandKeyword" clearable placeholder="筛选国家、分区、重量段、费用项" class="pay-filter" @input="pane.bandPageNo = 1" />
<div v-if="editable" class="pay-edit-bar">
<el-button size="small" type="primary" plain @click="addBand(pane)">添加重量段</el-button>
</div>
<el-table :data="pageBands(pane)" border size="small" max-height="360" empty-text="没有重量段或起重配置">
<el-table :data="pageBands(pane)" border size="small" empty-text="没有重量段或起重配置">
<el-table-column prop="sectionName" label="区块" min-width="110" show-overflow-tooltip>
<template #default="{ row }"><cell-text v-model="row.sectionName" :edit="editable" /></template>
<template #default="{ row }"><cell-text v-model="row.sectionName" :edit="isRowEditing(row)" /></template>
</el-table-column>
<el-table-column prop="countryName" label="国家" min-width="120" show-overflow-tooltip>
<template #default="{ row }"><cell-text v-model="row.countryName" :edit="editable" /></template>
<template #default="{ row }"><cell-text v-model="row.countryName" :edit="isRowEditing(row)" /></template>
</el-table-column>
<el-table-column prop="countryCode" label="代码" width="80" show-overflow-tooltip>
<template #default="{ row }"><cell-text v-model="row.countryCode" :edit="editable" /></template>
<template #default="{ row }"><cell-text v-model="row.countryCode" :edit="isRowEditing(row)" /></template>
</el-table-column>
<el-table-column prop="zoneName" label="分区" min-width="140" show-overflow-tooltip>
<template #default="{ row }"><cell-text v-model="row.zoneName" :edit="editable" /></template>
<template #default="{ row }"><cell-text v-model="row.zoneName" :edit="isRowEditing(row)" /></template>
</el-table-column>
<el-table-column prop="weightLabel" label="重量段" min-width="120" show-overflow-tooltip>
<template #default="{ row }"><cell-text v-model="row.weightLabel" :edit="editable" /></template>
<template #default="{ row }"><cell-text v-model="row.weightLabel" :edit="isRowEditing(row)" /></template>
</el-table-column>
<el-table-column label="下限KG" width="90" align="right">
<template #default="{ row }">{{ money(row.weightMin) }}</template>
@@ -49,21 +46,22 @@
<template #default="{ row }">{{ money(row.weightMax) }}</template>
</el-table-column>
<el-table-column prop="chargeName" label="费用项" min-width="120" show-overflow-tooltip>
<template #default="{ row }"><cell-text v-model="row.chargeName" :edit="editable" /></template>
<template #default="{ row }"><cell-text v-model="row.chargeName" :edit="isRowEditing(row)" /></template>
</el-table-column>
<el-table-column label="单价" width="110" align="right">
<template #default="{ row }">
<el-input v-if="editable" v-model="row.price" size="small" />
<el-input v-if="isRowEditing(row)" v-model="row.price" size="small" />
<span v-else>{{ money(row.price) }}</span>
</template>
</el-table-column>
<el-table-column prop="priceUnit" label="单位" width="110" show-overflow-tooltip>
<template #default="{ row }"><cell-text v-model="row.priceUnit" :edit="editable" /></template>
<template #default="{ row }"><cell-text v-model="row.priceUnit" :edit="isRowEditing(row)" /></template>
</el-table-column>
<el-table-column v-if="editable" label="操作" width="120" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" :loading="isRowSaving(row)" @click="saveBandRow(pane, row)">保存</el-button>
<el-button link type="danger" :disabled="!!savingRowKey" @click="removeRow(pane.item.weightBands, row)">删除</el-button>
<el-button v-if="!isRowEditing(row)" link type="primary" :disabled="rowBusy" @click="startEdit(row)">修改</el-button>
<el-button v-else link type="primary" :disabled="rowBusy" @click="saveBandRow(pane, row)">保存</el-button>
<el-button link type="danger" :disabled="rowBusy" @click="removeRow(pane.item.weightBands, row)">删除</el-button>
</template>
</el-table-column>
</el-table>
@@ -82,11 +80,11 @@
<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="addFee(pane)">添加附加费</el-button>
</div>
<el-table :data="pageFees(pane)" border size="small" max-height="360" empty-text="没有附加费">
<el-table :data="pageFees(pane)" border size="small" empty-text="没有附加费">
<el-table-column prop="feeName" label="附加费用" min-width="180" show-overflow-tooltip>
<template #default="{ row }">
<el-select
v-if="editable"
v-if="isRowEditing(row)"
v-model="row.feeName"
filterable
clearable
@@ -106,18 +104,18 @@
</template>
</el-table-column>
<el-table-column prop="zoneName" label="地区" min-width="160" show-overflow-tooltip>
<template #default="{ row }"><cell-text v-model="row.zoneName" :edit="editable" /></template>
<template #default="{ row }"><cell-text v-model="row.zoneName" :edit="isRowEditing(row)" /></template>
</el-table-column>
<el-table-column label="价格" width="110" align="right">
<template #default="{ row }">
<el-input v-if="editable" v-model="row.amount" size="small" />
<el-input v-if="isRowEditing(row)" v-model="row.amount" size="small" />
<span v-else>{{ money(row.amount) }}</span>
</template>
</el-table-column>
<el-table-column label="币种" width="110" show-overflow-tooltip>
<template #default="{ row }">
<el-select
v-if="editable"
v-if="isRowEditing(row)"
:model-value="feeCurrencyOf(row)"
filterable
allow-create
@@ -141,7 +139,7 @@
<el-table-column label="单位" width="110" show-overflow-tooltip>
<template #default="{ row }">
<el-select
v-if="editable"
v-if="isRowEditing(row)"
:model-value="feeUnitOf(row)"
filterable
allow-create
@@ -164,16 +162,16 @@
</el-table-column>
<el-table-column label="收费标准" min-width="180" show-overflow-tooltip>
<template #default="{ row }">
<cell-text v-if="editable && (!row.chargeText || feeNote(row))" v-model="row.chargeText" :edit="true" />
<cell-text v-if="isRowEditing(row) && (!row.chargeText || feeNote(row))" v-model="row.chargeText" :edit="true" />
<span v-else>{{ feeNote(row) || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="feeDesc" label="费用解释" min-width="240" show-overflow-tooltip>
<template #default="{ row }"><cell-text v-model="row.feeDesc" :edit="editable" /></template>
<template #default="{ row }"><cell-text v-model="row.feeDesc" :edit="isRowEditing(row)" /></template>
</el-table-column>
<el-table-column prop="calcRule" label="计费条件" width="140">
<template #default="{ row }">
<el-select v-if="editable" v-model="row.calcRule" size="small" placeholder="人工确认">
<el-select v-if="isRowEditing(row)" v-model="row.calcRule" size="small" placeholder="人工确认">
<el-option v-for="item in calcRules" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<span v-else>{{ calcRuleLabel(row.calcRule) }}</span>
@@ -181,7 +179,7 @@
</el-table-column>
<el-table-column prop="calcParam" label="条件参数" min-width="220" show-overflow-tooltip>
<template #default="{ row }">
<span v-if="!editable">{{ feeCondition(row) }}</span>
<span v-if="!isRowEditing(row)">{{ feeCondition(row) }}</span>
<template v-else>
<div v-if="conditionHint(row)" class="pay-cond-hint">{{ conditionHint(row) }}</div>
<cell-text
@@ -194,8 +192,9 @@
</el-table-column>
<el-table-column v-if="editable" label="操作" width="120" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" :loading="isRowSaving(row)" @click="saveFeeRow(pane, row)">保存</el-button>
<el-button link type="danger" :disabled="!!savingRowKey" @click="removeRow(pane.item.surcharges, row)">删除</el-button>
<el-button v-if="!isRowEditing(row)" link type="primary" :disabled="rowBusy" @click="startEdit(row)">修改</el-button>
<el-button v-else link type="primary" :disabled="rowBusy" @click="saveFeeRow(pane, row)">保存</el-button>
<el-button link type="danger" :disabled="rowBusy" @click="removeRow(pane.item.surcharges, row)">删除</el-button>
</template>
</el-table-column>
</el-table>
@@ -212,7 +211,7 @@
<el-tab-pane v-if="postcodeLines(pane).length" :label="`邮编分区(${postcodeLines(pane).length})`" name="postcode">
<p class="pay-hint">{{ postcodeHint(pane) }}</p>
<el-input v-model="pane.postKeyword" clearable placeholder="筛选邮编、州、城市或分区" class="pay-filter" @input="pane.postPageNo = 1" />
<el-table :data="pagePostcodes(pane)" border size="small" max-height="360" empty-text="没有邮编">
<el-table :data="pagePostcodes(pane)" border size="small" empty-text="没有邮编">
<el-table-column v-for="(title, index) in postcodeTitles(pane)" :key="title" :label="title" min-width="110" show-overflow-tooltip>
<template #default="{ row }">{{ row.parts[index] || '-' }}</template>
</el-table-column>
@@ -233,7 +232,7 @@
<script setup>
// 重量段和附加费。传入 products 时一次展示全部产品。
import { h, onMounted } from 'vue'
import { h, onMounted, watch } from 'vue'
import { ElInput } from 'element-plus'
import { listSurchargeOptions } from '@/api/price/surcharge'
@@ -258,6 +257,12 @@ const props = defineProps({
const emit = defineEmits(['save-row'])
const editingKeys = ref(new Set())
watch(() => props.product, () => {
editingKeys.value = new Set()
})
function rowKeyOf(row) {
if (!row) return ''
if (row.surchargeId != null) return 'fee-' + row.surchargeId
@@ -267,16 +272,58 @@ function rowKeyOf(row) {
return row.__rowKey
}
function isRowSaving(row) {
return props.savingRowKey && String(props.savingRowKey) === String(rowKeyOf(row))
function isRowEditing(row) {
return editingKeys.value.has(rowKeyOf(row))
}
function startEdit(row) {
const next = new Set(editingKeys.value)
next.add(rowKeyOf(row))
editingKeys.value = next
}
function stopEdit(row) {
const next = new Set(editingKeys.value)
next.delete(rowKeyOf(row))
editingKeys.value = next
}
const localSavingKey = ref('')
const rowBusy = computed(() => !!(props.savingRowKey || localSavingKey.value))
function finishSave(row, ok) {
localSavingKey.value = ''
if (ok !== false) stopEdit(row)
}
function runSave(kind, pane, row) {
if (localSavingKey.value || props.savingRowKey) return
const rowKey = rowKeyOf(row)
localSavingKey.value = rowKey
let settled = false
const done = (ok) => {
if (settled) return
settled = true
finishSave(row, ok)
}
try {
emit('save-row', { kind, product: pane.item, row, rowKey, done })
} catch (e) {
done(false)
return
}
// Parent must call done(); hard stop UI lock if it does not.
window.setTimeout(() => {
if (!settled) done(false)
}, 8000)
}
function saveBandRow(pane, row) {
emit('save-row', { kind: 'band', product: pane.item, row, rowKey: rowKeyOf(row) })
runSave('band', pane, row)
}
function saveFeeRow(pane, row) {
emit('save-row', { kind: 'fee', product: pane.item, row, rowKey: rowKeyOf(row) })
runSave('fee', pane, row)
}
const surchargeOptions = ref([])
@@ -470,16 +517,10 @@ function filteredFees(pane) {
return filterRows(pane.item.surcharges || [], pane.feeKeyword, ['feeName', 'zoneName', 'chargeText', 'amount', 'currency', 'priceUnit', 'feeDesc', 'calcRule', 'calcParam', 'postcodeParam'])
}
function addBand(pane) {
if (!pane.item.weightBands) pane.item.weightBands = []
pane.item.weightBands.push({ sectionName: '', countryName: '', countryCode: '', zoneName: '', weightLabel: '', chargeName: '', price: '', priceUnit: '' })
pane.bandPageNo = Math.ceil(pane.item.weightBands.length / pageSize) || 1
}
function addFee(pane) {
if (!pane.item.surcharges) pane.item.surcharges = []
const currency = String(pane.item.currency || 'EUR').trim().toUpperCase() || 'EUR'
pane.item.surcharges.push({
const row = {
feeName: '',
scId: undefined,
zoneName: '',
@@ -490,7 +531,9 @@ function addFee(pane) {
feeDesc: '',
calcRule: 'MANUAL',
calcParam: ''
})
}
pane.item.surcharges.push(row)
startEdit(row)
pane.feePageNo = Math.ceil(pane.item.surcharges.length / pageSize) || 1
}
+22 -23
View File
@@ -140,25 +140,17 @@
size="78%"
append-to-body
:close-on-click-modal="false"
@closed="editing = false"
>
<pay-config-detail
v-loading="detailLoading || !!savingRowKey"
v-loading="detailLoading"
:product="detail"
:editable="editing"
:editable="true"
:saving-row-key="savingRowKey"
@save-row="saveProductRow"
/>
<template #footer>
<el-button @click="detailVisible = false">关闭</el-button>
<el-button v-if="detail" @click="downloadSource(detail)" v-hasPermi="['price:pay:query']">下载源文件</el-button>
<el-button
v-if="!editing"
type="primary"
@click="editing = true"
v-hasPermi="['price:pay:import']"
>编辑</el-button>
<el-button v-else @click="editing = false">完成</el-button>
</template>
</el-drawer>
</div>
@@ -174,7 +166,6 @@ const tableRef = ref()
const importRef = ref()
const detailVisible = ref(false)
const detailLoading = ref(false)
const editing = ref(false)
const savingRowKey = ref('')
const detail = ref(null)
const historyVisible = ref(false)
@@ -292,7 +283,6 @@ function handleDelete(ids) {
function openDetail(row) {
detailVisible.value = true
editing.value = false
savingRowKey.value = ''
detailLoading.value = true
detail.value = null
@@ -303,25 +293,34 @@ function openDetail(row) {
})
}
function saveProductRow(payload) {
if (!detail.value || savingRowKey.value) return
async function saveProductRow(payload) {
if (!detail.value || savingRowKey.value) {
payload?.done?.(false)
return
}
cleanMoney(detail.value)
;(detail.value.surcharges || []).forEach(row => {
if (row.amount === '') row.amount = null
})
const productId = detail.value.productId
savingRowKey.value = payload?.rowKey || 'row'
updatePay(detail.value).then(() => {
let ok = false
try {
await updatePay(detail.value)
ok = true
proxy.$modal.msgSuccess('该行已保存')
if (!productId) return
return getPay(productId).then(res => {
if (res.data) detail.value = res.data
})
}).then(() => {
tableRef.value?.refresh()
}).finally(() => {
} catch (e) {
ok = false
} finally {
savingRowKey.value = ''
})
payload?.done?.(ok)
}
if (!ok || !productId) return
try {
const res = await getPay(productId)
if (res.data) detail.value = res.data
tableRef.value?.refresh()
} catch (e) {}
}
function cleanMoney(product) {
@@ -0,0 +1,182 @@
<template>
<set-section-card
:title="title"
tone="blue"
:count="rows.length"
:sub="sub"
>
<template #action>
<el-button
type="primary"
size="small"
icon="Plus"
:disabled="!priceSetId"
@click="openBind"
v-hasPermi="['price:label:edit']"
>添加渠道</el-button>
</template>
<el-table v-loading="loading" :data="rows" border empty-text="暂未绑定渠道">
<el-table-column label="渠道编码" prop="channelCode" min-width="140" show-overflow-tooltip />
<el-table-column label="渠道名称" prop="channelName" min-width="180" show-overflow-tooltip />
<el-table-column label="应收产品" prop="productName" min-width="160" show-overflow-tooltip />
<el-table-column label="操作" width="100" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="danger" @click="unbind(row)" v-hasPermi="['price:label:edit']">解绑</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog v-model="bindOpen" title="绑定渠道" width="560px" append-to-body @closed="onClosed">
<el-form label-width="110px">
<el-form-item label="套餐产品" required>
<el-select v-model="form.productId" filterable clearable placeholder="选择本套餐内应收产品" style="width: 100%">
<el-option
v-for="item in productOptions"
:key="item.productId"
:label="item.productName"
:value="item.productId"
/>
</el-select>
</el-form-item>
<el-form-item label="渠道" required>
<el-select
v-model="form.channelIds"
multiple
filterable
clearable
collapse-tags
collapse-tags-tooltip
placeholder="可多选渠道"
style="width: 100%"
>
<el-option
v-for="item in channelOptions"
:key="item.id"
:label="channelLabel(item)"
:value="item.id"
/>
</el-select>
</el-form-item>
<div class="hint">绑定后会写入渠道应收产品,并关联本打单套餐。</div>
</el-form>
<template #footer>
<el-button @click="bindOpen = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="submit">确定</el-button>
</template>
</el-dialog>
</set-section-card>
</template>
<script setup>
import SetSectionCard from '../../set/components/detail/SetSectionCard.vue'
import { listLabelChannels, bindLabelChannels, unbindLabelChannel, listLabelProducts } from '@/api/price/label'
import { listChannelOptions } from '@/api/masterdata/serviceChannel'
const props = defineProps({
priceSetId: { type: [Number, String], default: null }
})
const { proxy } = getCurrentInstance()
const title = '绑定渠道'
const sub = '渠道计费时按应收产品 + 本套餐取价'
const loading = ref(false)
const rows = ref([])
const bindOpen = ref(false)
const saving = ref(false)
const productOptions = ref([])
const channelOptions = ref([])
const form = reactive({
productId: undefined,
channelIds: []
})
function channelLabel(item) {
const code = item.channelCode || item.id || ''
const name = item.channelName || ''
return name ? (code + ' / ' + name) : String(code)
}
async function loadRows() {
if (!props.priceSetId) {
rows.value = []
return
}
loading.value = true
try {
const res = await listLabelChannels(props.priceSetId)
rows.value = res.data || []
} finally {
loading.value = false
}
}
async function openBind() {
if (!props.priceSetId) return
form.productId = undefined
form.channelIds = []
const [prodRes, chRes] = await Promise.all([
listLabelProducts(props.priceSetId),
listChannelOptions({ blOpen: '1' })
])
productOptions.value = prodRes.data || []
channelOptions.value = chRes.data || []
if (!productOptions.value.length) {
proxy.$modal.msgWarning('请先导入套餐产品价卡')
return
}
bindOpen.value = true
}
function onClosed() {
form.productId = undefined
form.channelIds = []
saving.value = false
}
function submit() {
if (!form.productId) {
proxy.$modal.msgWarning('请选择套餐内应收产品')
return
}
if (!form.channelIds.length) {
proxy.$modal.msgWarning('请选择渠道')
return
}
saving.value = true
bindLabelChannels(props.priceSetId, {
productId: form.productId,
channelIds: form.channelIds
}).then(res => {
proxy.$modal.msgSuccess(res.msg || res.data || '绑定成功')
bindOpen.value = false
loadRows()
}).finally(() => {
saving.value = false
})
}
function unbind(row) {
if (!row?.channelId) return
const name = row.channelName || row.channelCode || row.channelId
proxy.$modal.confirm('确认解绑渠道「' + name + '」?').then(() => {
return unbindLabelChannel(props.priceSetId, row.channelId)
}).then(() => {
proxy.$modal.msgSuccess('已解绑')
loadRows()
}).catch(() => {})
}
watch(() => props.priceSetId, () => loadRows(), { immediate: true })
defineExpose({ reload: loadRows })
</script>
<style scoped>
.hint {
margin: -4px 0 0 110px;
color: #909399;
font-size: 12px;
line-height: 1.5;
}
</style>
@@ -95,11 +95,19 @@ const filteredRows = computed(() => {
})
})
function formatHour(val) {
if (!val) return ''
const text = String(val).replace('T', ' ')
if (text.length >= 16) return text.slice(0, 13) + ':00'
if (text.length >= 10) return text.slice(0, 10) + ' 00:00'
return text
}
function termText(row) {
const start = row.termStart || row.effectiveDate || ''
const end = row.termEnd || '不限'
const start = formatHour(row.termStart || row.effectiveDate || '')
const end = row.termEnd ? formatHour(row.termEnd) : '不限'
if (!start) return '未设置'
return `${String(start).slice(0, 10)} ~ ${String(end).slice(0, 10)}`
return start + ' ~ ' + end
}
function termStatusText(status) {
@@ -28,15 +28,15 @@
<el-table-column label="版本号" width="100" align="center">
<template #default="{ row }">{{ row.versionLabel || ('V' + (row.versionNo || 1)) }}</template>
</el-table-column>
<el-table-column label="生效时间" min-width="160" align="center">
<el-table-column label="生效时间" min-width="170" align="center">
<template #default="{ row }">
<el-button link type="primary" @click="$emit('open-details', row)">
{{ formatDay(row.effectiveTime) || '未设置' }}
{{ formatHour(row.effectiveTime) || '未设置' }}
</el-button>
</template>
</el-table-column>
<el-table-column label="失效时间" min-width="160" align="center">
<template #default="{ row }">{{ formatDay(row.expireTime) || '-' }}</template>
<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">
<template #default="{ row }">
@@ -49,7 +49,7 @@
<el-table-column label="创建时间" min-width="170" align="center">
<template #default="{ row }">{{ row.createTime || '-' }}</template>
</el-table-column>
<el-table-column label="操作" width="280" align="center" fixed="right">
<el-table-column label="操作" width="340" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="$emit('open-details', row)">查看详情</el-button>
<el-button
@@ -66,6 +66,13 @@
@click="$emit('reschedule', row)"
v-hasPermi="['price:label:edit']"
>修改生效时间</el-button>
<el-button
v-if="canDelete(row)"
link
type="danger"
@click="$emit('remove', row)"
v-hasPermi="['price:label:remove']"
>删除</el-button>
</template>
</el-table-column>
</el-table>
@@ -76,7 +83,7 @@
const props = defineProps({
versions: { type: Array, default: () => [] }
})
defineEmits(['edit', 'reschedule', 'open-details', 'create-version'])
defineEmits(['edit', 'reschedule', 'remove', 'open-details', 'create-version'])
const statusTab = ref('all')
@@ -103,9 +110,12 @@ const filteredVersions = computed(() => {
return list.filter(v => String(v.status) === statusTab.value)
})
function formatDay(val) {
function formatHour(val) {
if (!val) return ''
return String(val).slice(0, 10)
const text = String(val).replace('T', ' ')
if (text.length >= 16) return text.slice(0, 13) + ':00'
if (text.length >= 10) return text.slice(0, 10) + ' 00:00'
return text
}
function statusText(status) {
@@ -118,13 +128,19 @@ function statusTag(status) {
return map[String(status)] || 'info'
}
/** 待生效:可编辑价卡 */
/** 待生效、已生效:可编辑价卡 */
function canEdit(row) {
const s = row ? String(row.status) : ''
return s === '0' || s === '1'
}
/** 仅待生效可改生效时间 */
function canReschedule(row) {
return row && String(row.status) === '0'
}
/** 仅待生效可改生效时间;已生效不可改 */
function canReschedule(row) {
/** 仅待生效可删除整版 */
function canDelete(row) {
return row && String(row.status) === '0'
}
</script>
+35 -26
View File
@@ -76,12 +76,12 @@
/>
<el-drawer v-model="detailVisible" :title="detailTitle" size="78%" destroy-on-close append-to-body @closed="onDetailClosed">
<pay-config-detail :product="detailProduct" :editable="detailEditing" />
<template #footer>
<el-button v-if="!detailEditing" type="primary" icon="Edit" @click="startEdit">修改</el-button>
<el-button v-else type="primary" @click="applyDetail">保存</el-button>
<el-button @click="detailVisible = false">关闭</el-button>
</template>
<pay-config-detail
:product="detailProduct"
:editable="true"
:saving-row-key="savingRowKey"
@save-row="saveDetailRow"
/>
</el-drawer>
</div>
</template>
@@ -111,9 +111,9 @@ const importMeta = reactive({
overwrite: false
})
const detailVisible = ref(false)
const detailEditing = ref(false)
const detailProduct = ref(null)
const detailRowKey = ref('')
const savingRowKey = ref('')
const detailTitle = computed(() => detailProduct.value?.productName || detailProduct.value?.sheetName || '产品明细')
const priceSetId = computed(() => {
@@ -135,13 +135,13 @@ const cardTitle = computed(() => {
const pageBanner = computed(() => {
const name = form.basic.priceSetName || ''
const code = form.basic.priceSetCode || ''
if (editVersionMode.value) return `编辑待生效版本 V${versionNo.value}:${name}(${code})`
if (editVersionMode.value) return `编辑版本 V${versionNo.value}:${name}(${code})`
return `新建版本:${name}(${code})`
})
const headerTip = computed(() => {
if (editVersionMode.value) return '可重新导入或修改明细;完成后覆盖本待生效版本,不新增版本号'
if (editVersionMode.value) return '可重新导入或修改明细;完成后覆盖本版本,不新增版本号'
if (newVersionMode.value) return '导入后生成待生效新版本;生效时间请回详情页设置'
return '打单价卡与应付独立;解析后回填到此表,查看后点修改可改明细'
return '打单价卡与应付独立;解析后回填到此表,查看明细后可在行内保存修改'
})
function goBack() {
@@ -219,28 +219,21 @@ function onImportConfirm(payload) {
function openDetail(row) {
if (!row) return
detailEditing.value = false
savingRowKey.value = ''
detailRowKey.value = row.rowKey || `${row.sheetName || ''}-${row.productName || ''}`
detailProduct.value = JSON.parse(JSON.stringify(row))
detailVisible.value = true
}
function startEdit() {
detailEditing.value = true
}
function onDetailClosed() {
detailEditing.value = false
detailProduct.value = null
detailRowKey.value = ''
savingRowKey.value = ''
}
function applyDetail() {
function applyEditedProduct() {
const edited = detailProduct.value
if (!edited) {
detailVisible.value = false
return
}
if (!edited) return false
const key = detailRowKey.value
const idx = productRows.value.findIndex(item => {
const itemKey = item.rowKey || `${item.sheetName || ''}-${item.productName || ''}`
@@ -248,7 +241,7 @@ function applyDetail() {
})
if (idx < 0) {
proxy.$modal.msgError('未找到对应产品行')
return
return false
}
const bands = edited.weightBands || []
const fees = edited.surcharges || []
@@ -261,9 +254,25 @@ function applyDetail() {
extraCount: extras.length,
parseStatus: bands.length + fees.length + extras.length === 0 ? 'EMPTY' : (edited.parseStatus || 'OK')
})
detailEditing.value = false
detailVisible.value = false
proxy.$modal.msgSuccess('已更新该产品明细')
return true
}
function saveDetailRow(payload) {
if (savingRowKey.value) {
payload?.done?.(false)
return
}
savingRowKey.value = payload?.rowKey || 'row'
let ok = false
try {
ok = !!applyEditedProduct()
if (ok) proxy.$modal.msgSuccess('该行已保存')
} catch (e) {
ok = false
} finally {
savingRowKey.value = ''
payload?.done?.(ok)
}
}
function statusType(status) {
@@ -295,7 +304,7 @@ async function finish() {
return
}
const tip = editVersionMode.value
? `将覆盖待生效版本 V${versionNo.value} 的 ${productRows.value.length} 个产品,确认继续?`
? `将覆盖版本 V${versionNo.value} 的 ${productRows.value.length} 个产品,确认继续?`
: newVersionMode.value
? `将为套餐「${form.basic.priceSetName || ''}」新建待生效版本并保存 ${productRows.value.length} 个产品,确认继续?`
: `将创建打单套餐并保存 ${productRows.value.length} 个应收产品,确认继续?`
+31 -5
View File
@@ -14,10 +14,13 @@
<set-customer-table :set="setInfo" @refresh="loadHeader" />
<label-channel-table ref="channelTableRef" :price-set-id="priceSetId" />
<label-version-table
:versions="versions"
@edit="openEditVersion"
@reschedule="openReschedule"
@remove="removeVersion"
@open-details="openVersionDetails"
@create-version="openCreateVersion"
/>
@@ -45,13 +48,14 @@
<el-form-item label="生效时间" prop="effectiveTime">
<el-date-picker
v-model="activateForm.effectiveTime"
type="date"
value-format="YYYY-MM-DD"
type="datetime"
value-format="YYYY-MM-DD HH:00:00"
format="YYYY-MM-DD HH:00"
placeholder="请选择生效时间"
style="width: 100%"
/>
</el-form-item>
<div class="activate-hint">仅待生效版本可修改;未到生效日仍为待生效,到日后自动按生效处理。</div>
<div class="activate-hint">仅待生效版本可修改;精确到小时。未到生效时间仍为待生效,到时自动按生效处理。</div>
</el-form>
<template #footer>
<el-button @click="activateVisible = false">取消</el-button>
@@ -66,6 +70,7 @@ import {
getLabelPackage,
listLabelVersions,
activateLabelVersion,
deleteLabelVersion,
listLabelCustomer,
addLabelCustomer,
batchAddLabelCustomer,
@@ -78,6 +83,7 @@ import SetCustomerTable from '../set/components/detail/SetCustomerTable.vue'
import LabelImportDialog from './components/LabelImportDialog.vue'
import LabelVersionTable from './components/LabelVersionTable.vue'
import LabelVersionProductDrawer from './components/LabelVersionProductDrawer.vue'
import LabelChannelTable from './components/LabelChannelTable.vue'
const labelModuleApi = {
...priceSetApi,
@@ -127,6 +133,7 @@ const setInfo = reactive({
})
const versions = ref([])
const importRef = ref()
const channelTableRef = ref()
const productDrawerVisible = ref(false)
const activeVersion = ref(null)
@@ -176,6 +183,7 @@ async function loadVersions() {
async function reloadAll() {
await loadHeader()
await loadVersions()
channelTableRef.value?.reload?.()
}
function openVersionDetails(row) {
@@ -196,7 +204,7 @@ function openCreateVersion() {
})
}
/** 编辑待生效版本价卡 */
/** 编辑待生效/已生效版本价卡 */
function openEditVersion(row) {
if (!priceSetId.value || !row?.versionNo) return
proxy.$router.push({
@@ -209,10 +217,28 @@ function openEditVersion(row) {
})
}
function removeVersion(row) {
if (!priceSetId.value || !row?.versionNo) return
const label = row.versionLabel || ('V' + row.versionNo)
proxy.$modal.confirm('确认删除待生效版本 ' + label + '?该版产品价卡将一起清除。').then(() => {
return deleteLabelVersion(priceSetId.value, row.versionNo)
}).then(() => {
proxy.$modal.msgSuccess('版本已删除')
reloadAll()
}).catch(() => {})
}
function toHourValue(val) {
const text = String(val || '').replace('T', ' ')
if (text.length >= 13) return text.slice(0, 13) + ':00:00'
if (text.length >= 10) return text.slice(0, 10) + ' 00:00:00'
return undefined
}
function openReschedule(row) {
activateForm.versionNo = row.versionNo
activateForm.versionLabel = row.versionLabel || ('V' + row.versionNo)
activateForm.effectiveTime = row.effectiveTime ? String(row.effectiveTime).slice(0, 10) : undefined
activateForm.effectiveTime = row.effectiveTime ? toHourValue(row.effectiveTime) : undefined
activateVisible.value = true
}