Merge remote-tracking branch 'origin/test' into test
This commit is contained in:
@@ -69,15 +69,76 @@ export function labelParseProgress(progressId) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 提交异步保存,返回 { taskId } */
|
||||
export function saveLabelImport(data) {
|
||||
return request({
|
||||
url: '/price/label/importSave',
|
||||
method: 'post',
|
||||
data,
|
||||
timeout: 10 * 60 * 1000
|
||||
headers: { repeatSubmit: false },
|
||||
timeout: 60 * 1000
|
||||
})
|
||||
}
|
||||
|
||||
export function labelSaveProgress(taskId) {
|
||||
return request({
|
||||
url: '/price/label/importSave/task/' + taskId,
|
||||
method: 'get',
|
||||
headers: { repeatSubmit: false }
|
||||
})
|
||||
}
|
||||
|
||||
function pickLabelSaveTaskId(res) {
|
||||
if (!res) return ''
|
||||
const d = res.data
|
||||
if (d && typeof d === 'object' && d.taskId) return String(d.taskId)
|
||||
if (typeof d === 'string' && d.trim()) return d.trim()
|
||||
if (res.taskId) return String(res.taskId)
|
||||
if (typeof res.msg === 'string') {
|
||||
const m = res.msg.trim()
|
||||
if (/^[a-f0-9]{32}$/i.test(m)) return m
|
||||
if (/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(m)) {
|
||||
return m.replace(/-/g, '')
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/** 提交并轮询直到成功/失败;onProgress({ percent, message, status }) */
|
||||
export async function saveLabelImportAndWait(data, onProgress) {
|
||||
const res = await saveLabelImport(data)
|
||||
const taskId = pickLabelSaveTaskId(res)
|
||||
if (!taskId) {
|
||||
throw new Error('未返回保存任务编号')
|
||||
}
|
||||
const started = Date.now()
|
||||
const timeoutMs = 10 * 60 * 1000
|
||||
while (Date.now() - started < timeoutMs) {
|
||||
const prog = await labelSaveProgress(taskId)
|
||||
const task = (prog && prog.data) || prog || {}
|
||||
if (typeof onProgress === 'function') {
|
||||
onProgress({
|
||||
percent: Number(task.percent) || 0,
|
||||
message: task.message || '',
|
||||
status: task.status || 'PENDING',
|
||||
priceSetId: task.priceSetId,
|
||||
error: task.error
|
||||
})
|
||||
}
|
||||
if (task.status === 'SUCCESS') {
|
||||
return {
|
||||
message: task.message || '保存成功',
|
||||
priceSetId: task.priceSetId
|
||||
}
|
||||
}
|
||||
if (task.status === 'FAIL') {
|
||||
throw new Error(task.error || task.message || '保存失败')
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 400))
|
||||
}
|
||||
throw new Error('保存超时,请稍后在列表中查看')
|
||||
}
|
||||
|
||||
export function listLabelVersions(priceSetId) {
|
||||
return request({
|
||||
url: '/price/label/' + priceSetId + '/versions',
|
||||
@@ -147,11 +208,11 @@ export function delLabelCustomer(ids) {
|
||||
})
|
||||
}
|
||||
|
||||
export function getLabelProduct(productId, termId) {
|
||||
export function getLabelProduct(productId, versionId) {
|
||||
return request({
|
||||
url: '/price/label/product/' + productId,
|
||||
method: 'get',
|
||||
params: termId ? { termId } : undefined
|
||||
params: versionId ? { versionId } : undefined
|
||||
})
|
||||
}
|
||||
|
||||
@@ -166,6 +227,26 @@ export function updateLabelProduct(data) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 仅保存当前重量段行 */
|
||||
export function updateLabelWeightBand(data) {
|
||||
return request({
|
||||
url: '/price/label/product/band',
|
||||
method: 'put',
|
||||
data,
|
||||
headers: { repeatSubmit: false }
|
||||
})
|
||||
}
|
||||
|
||||
/** 仅保存当前附加费行 */
|
||||
export function updateLabelSurcharge(data) {
|
||||
return request({
|
||||
url: '/price/label/product/surcharge',
|
||||
method: 'put',
|
||||
data,
|
||||
headers: { repeatSubmit: false }
|
||||
})
|
||||
}
|
||||
|
||||
export function removeLabelProducts(priceSetId, productIds) {
|
||||
return request({
|
||||
url: '/price/label/' + priceSetId + '/products/' + productIds,
|
||||
|
||||
@@ -0,0 +1,906 @@
|
||||
<template>
|
||||
<div v-if="panes.length" class="label-detail">
|
||||
<section v-for="pane in panes" :key="pane.key" class="label-product">
|
||||
<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="label-filter" />
|
||||
<el-table
|
||||
:data="displayBands(pane)"
|
||||
border
|
||||
size="small"
|
||||
empty-text="没有重量段配置"
|
||||
max-height="calc(100vh - 220px)"
|
||||
:span-method="arg => bandSpanMethod(pane, arg)"
|
||||
>
|
||||
<el-table-column label="国家" min-width="140" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span>{{ pairText(row.countryName, row.countryCode) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="分区" min-width="150" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span>{{ pairText(row.zoneName, row.zoneCode) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="weightLabel" label="重量段" min-width="110" show-overflow-tooltip>
|
||||
<template #default="{ row }"><span>{{ row.weightLabel || '-' }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="费用类型" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-select v-if="isRowEditing(row)" v-model="row.feeType" size="small" style="width: 100%">
|
||||
<el-option label="运费" value="FREIGHT" />
|
||||
<el-option label="处理费" value="REGISTRATION" />
|
||||
<el-option label="偏远费" value="REMOTE" />
|
||||
</el-select>
|
||||
<span v-else>{{ feeTypeText(row.feeType) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="计费条件" width="130">
|
||||
<template #default="{ row }">
|
||||
<el-select v-if="isRowEditing(row)" v-model="row.chargeMode" size="small" style="width: 100%" clearable>
|
||||
<el-option v-for="d in lbl_charge_mode" :key="d.value" :label="d.label" :value="d.value" />
|
||||
</el-select>
|
||||
<span v-else>{{ dictLabel(lbl_charge_mode, row.chargeMode) || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额" min-width="130" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<div v-if="isRowEditing(row)" class="label-cell-inline">
|
||||
<el-input v-model="row.price" size="small" placeholder="金额" class="label-price-input" />
|
||||
<span class="label-unit-text">{{ row.priceUnit || '-' }}</span>
|
||||
</div>
|
||||
<span v-else>{{ moneyUnit(row.price, row.priceUnit) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="续重" min-width="130" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<div v-if="isRowEditing(row)" class="label-cell-inline">
|
||||
<el-input v-model="row.extraPrice" size="small" placeholder="续重价" class="label-price-input" />
|
||||
<span class="label-unit-text">{{ row.extraPriceUnit || '-' }}</span>
|
||||
</div>
|
||||
<span v-else>{{ moneyUnit(row.extraPrice, row.extraPriceUnit) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="calcParam" label="条件参数" min-width="100" show-overflow-tooltip>
|
||||
<template #default="{ row }"><cell-text v-model="row.calcParam" :edit="isRowEditing(row)" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="postcodeParam" label="邮编" min-width="110" show-overflow-tooltip>
|
||||
<template #default="{ row }"><cell-text v-model="row.postcodeParam" :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 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>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane :label="feeTabLabel(pane)" name="fee">
|
||||
<div class="label-edit-bar">
|
||||
<el-input v-model="pane.feeKeyword" clearable placeholder="筛选附加费用、地区、价格、货币或费用解释" class="label-filter" @input="pane.feePageNo = 1" />
|
||||
<el-button v-if="editable" size="small" type="primary" plain @click="openFeePicker(pane)">添加附加费</el-button>
|
||||
<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="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._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>
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
v-if="isRowEditing(row)"
|
||||
v-model="row.feeName"
|
||||
filterable
|
||||
clearable
|
||||
size="small"
|
||||
style="width: 100%"
|
||||
:placeholder="'请选择附加费'"
|
||||
@change="name => onFeeNameChange(row, name)"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in feeNameOptions(row)"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item"
|
||||
/>
|
||||
</el-select>
|
||||
<span v-else>{{ row.feeName || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="zoneCode" label="分区编码" width="110" show-overflow-tooltip>
|
||||
<template #default="{ row }"><cell-text v-model="row.zoneCode" :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="isRowEditing(row)" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="价格" width="110" align="right">
|
||||
<template #default="{ row }">
|
||||
<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="isRowEditing(row)"
|
||||
:model-value="feeCurrencyOf(row)"
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
clearable
|
||||
size="small"
|
||||
style="width: 100%"
|
||||
:placeholder="'币种'"
|
||||
@update:model-value="value => setFeeCurrency(row, value)"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in currencyOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<span v-else>{{ feeCurrencyOf(row) || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="单位" width="110" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
v-if="isRowEditing(row)"
|
||||
:model-value="feeUnitOf(row)"
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
clearable
|
||||
size="small"
|
||||
style="width: 100%"
|
||||
:placeholder="'单位'"
|
||||
@update:model-value="value => setFeeUnit(row, value)"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in feeUnitOptions"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item"
|
||||
/>
|
||||
</el-select>
|
||||
<span v-else>{{ feeUnitOf(row) || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="收费标准" min-width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<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="isRowEditing(row)" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="calcRule" label="计费条件" width="140">
|
||||
<template #default="{ row }">
|
||||
<el-select v-if="isRowEditing(row)" :model-value="feeBillFlag(row)" size="small" @change="value => setFeeBill(row, value)">
|
||||
<el-option label="需要计费" value="BILL" />
|
||||
<el-option label="暂不计费" value="MANUAL" />
|
||||
</el-select>
|
||||
<span v-else>{{ feeBillFlag(row) === 'BILL' ? '需要计费' : '暂不计费' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="editable" label="操作" width="120" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<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>
|
||||
<el-pagination
|
||||
v-if="filteredFees(pane).length > pageSize"
|
||||
class="label-page"
|
||||
layout="total, prev, pager, next"
|
||||
:total="filteredFees(pane).length"
|
||||
:page-size="pageSize"
|
||||
v-model:current-page="pane.feePageNo"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane v-if="postcodeLines(pane).length" :label="`邮编分区(${postcodeLines(pane).length})`" name="postcode">
|
||||
<p class="label-hint">{{ postcodeHint(pane) }}</p>
|
||||
<el-input v-model="pane.postKeyword" clearable placeholder="筛选邮编、州、城市或分区" class="label-filter" @input="pane.postPageNo = 1" />
|
||||
<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>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
v-if="filteredPostcodes(pane).length > pageSize"
|
||||
class="label-page"
|
||||
layout="total, prev, pager, next"
|
||||
:total="filteredPostcodes(pane).length"
|
||||
:page-size="pageSize"
|
||||
v-model:current-page="pane.postPageNo"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</section>
|
||||
|
||||
<pay-surcharge-picker-dialog
|
||||
v-model="feePickerVisible"
|
||||
:exclude-sc-ids="feePickerExcludeIds"
|
||||
@confirm="onFeePickerConfirm"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 打单价卡明细:feeType / chargeMode(=calc_rule) / calcParam / extraPrice。
|
||||
import { h, onMounted, watch } from 'vue'
|
||||
import { ElInput } from 'element-plus'
|
||||
import { listSurchargeOptions } from '@/api/price/surcharge'
|
||||
import PaySurchargePickerDialog from '@/views/price/basic/pay/components/PaySurchargePickerDialog.vue'
|
||||
|
||||
const props = defineProps({
|
||||
product: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
products: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
savingRowKey: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
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
|
||||
if (row.bandId != null) return 'band-' + row.bandId
|
||||
if (row.__rowKey) return row.__rowKey
|
||||
row.__rowKey = 'tmp-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8)
|
||||
return row.__rowKey
|
||||
}
|
||||
|
||||
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) {
|
||||
runSave('band', pane, row)
|
||||
}
|
||||
|
||||
function saveFeeRow(pane, row) {
|
||||
runSave('fee', pane, row)
|
||||
}
|
||||
|
||||
const surchargeOptions = ref([])
|
||||
const { bms_currency, lbl_charge_mode } = useDict('bms_currency', 'lbl_charge_mode')
|
||||
const currencyOptions = computed(() => (bms_currency.value || []).map(item => ({
|
||||
label: item.label || item.value,
|
||||
value: item.value
|
||||
})))
|
||||
const feeUnitOptions = ['ITEM', 'KG', 'PCS', 'PARCEL', '%']
|
||||
|
||||
function parseFeeUnit(priceUnit) {
|
||||
const text = String(priceUnit || '').trim()
|
||||
if (!text) return ''
|
||||
if (text === '%') return '%'
|
||||
const slash = text.lastIndexOf('/')
|
||||
if (slash >= 0) {
|
||||
return text.slice(slash + 1).trim().toUpperCase()
|
||||
}
|
||||
if (/^[A-Za-z]{3}$/.test(text)) return ''
|
||||
return text.toUpperCase()
|
||||
}
|
||||
|
||||
function feeCurrencyOf(row) {
|
||||
const currency = String(row?.currency || '').trim()
|
||||
if (currency) return currency.toUpperCase()
|
||||
const text = String(row?.priceUnit || '').trim()
|
||||
const match = text.match(/^([A-Za-z]{3})(?:\/|$)/)
|
||||
return match ? match[1].toUpperCase() : ''
|
||||
}
|
||||
|
||||
function feeUnitOf(row) {
|
||||
return parseFeeUnit(row?.priceUnit)
|
||||
}
|
||||
|
||||
function syncFeePriceUnit(row, unitOverride) {
|
||||
const currency = feeCurrencyOf(row)
|
||||
const unit = unitOverride != null ? String(unitOverride || '').trim().toUpperCase() : feeUnitOf(row)
|
||||
if (unit === '%') {
|
||||
row.priceUnit = '%'
|
||||
return
|
||||
}
|
||||
if (currency && unit) {
|
||||
row.priceUnit = currency + '/' + unit
|
||||
return
|
||||
}
|
||||
if (currency) {
|
||||
row.priceUnit = currency
|
||||
return
|
||||
}
|
||||
row.priceUnit = unit || ''
|
||||
}
|
||||
|
||||
function setFeeCurrency(row, value) {
|
||||
const currency = String(value || '').trim().toUpperCase()
|
||||
row.currency = currency
|
||||
syncFeePriceUnit(row)
|
||||
}
|
||||
|
||||
function setFeeUnit(row, value) {
|
||||
syncFeePriceUnit(row, value)
|
||||
}
|
||||
|
||||
function loadSurchargeOptions() {
|
||||
return listSurchargeOptions({ ssCode: 'on' }).then(res => {
|
||||
surchargeOptions.value = res.data || []
|
||||
}).catch(() => {
|
||||
surchargeOptions.value = []
|
||||
})
|
||||
}
|
||||
|
||||
function feeNameOptions(row) {
|
||||
const names = []
|
||||
const seen = new Set()
|
||||
for (const item of surchargeOptions.value || []) {
|
||||
const name = String(item?.scName || '').trim()
|
||||
if (!name || seen.has(name)) continue
|
||||
seen.add(name)
|
||||
names.push(name)
|
||||
}
|
||||
const current = String(row?.feeName || '').trim()
|
||||
if (current && !seen.has(current)) {
|
||||
names.unshift(current)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
function onFeeNameChange(row, name) {
|
||||
const matched = (surchargeOptions.value || []).find(item => String(item?.scName || '').trim() === String(name || '').trim())
|
||||
if (!matched) {
|
||||
row.scId = undefined
|
||||
return
|
||||
}
|
||||
row.scId = matched.scId
|
||||
const currency = matched.currency || feeCurrencyOf(row)
|
||||
if (currency) {
|
||||
setFeeCurrency(row, currency)
|
||||
}
|
||||
if (!feeUnitOf(row)) {
|
||||
setFeeUnit(row, 'ITEM')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadSurchargeOptions()
|
||||
})
|
||||
|
||||
const CellText = {
|
||||
props: { modelValue: { default: '' }, edit: Boolean },
|
||||
emits: ['update:modelValue'],
|
||||
setup(props, { emit }) {
|
||||
return () => props.edit
|
||||
? h(ElInput, {
|
||||
modelValue: props.modelValue ?? '',
|
||||
size: 'small',
|
||||
'onUpdate:modelValue': value => emit('update:modelValue', value)
|
||||
})
|
||||
: h('span', props.modelValue || '-')
|
||||
}
|
||||
}
|
||||
|
||||
function feeBillFlag(row) {
|
||||
return row?.calcRule && row.calcRule !== 'MANUAL' ? 'BILL' : 'MANUAL'
|
||||
}
|
||||
|
||||
function setFeeBill(row, value) {
|
||||
if (value === 'MANUAL') {
|
||||
row.calcRule = 'MANUAL'
|
||||
return
|
||||
}
|
||||
if (!row.calcRule || row.calcRule === 'MANUAL') row.calcRule = 'ALWAYS'
|
||||
}
|
||||
|
||||
const pageSize = 20
|
||||
const panes = ref([])
|
||||
|
||||
const source = computed(() => (props.products?.length ? props.products : (props.product ? [props.product] : [])))
|
||||
|
||||
watch(source, (list) => {
|
||||
panes.value = list.map((item, index) => ({
|
||||
key: item.productId || item.rowKey || `${item.sheetName || ''}-${item.productName || ''}-${index}`,
|
||||
item,
|
||||
tab: 'band',
|
||||
bandKeyword: '',
|
||||
feeKeyword: '',
|
||||
postKeyword: '',
|
||||
bandPageNo: 1,
|
||||
feePageNo: 1,
|
||||
postPageNo: 1
|
||||
}))
|
||||
}, { immediate: true })
|
||||
|
||||
function filteredBands(pane) {
|
||||
return filterRows(pane.item.weightBands || [], pane.bandKeyword, ['countryName', 'countryCode', 'zoneCode', 'zoneName', 'postcodeParam', 'weightLabel', 'feeType', 'chargeMode', 'chargeName', 'calcParam', 'priceUnit', 'extraPriceUnit'])
|
||||
}
|
||||
|
||||
const FEE_TYPE_ORDER = { FREIGHT: 1, REGISTRATION: 2, REMOTE: 3 }
|
||||
const bandViewCache = new WeakMap()
|
||||
|
||||
function bandCountryKey(row) {
|
||||
return pairText(row?.countryName, row?.countryCode)
|
||||
}
|
||||
|
||||
function bandZoneKey(row) {
|
||||
return bandCountryKey(row) + '|' + pairText(row?.zoneName, row?.zoneCode)
|
||||
}
|
||||
|
||||
function bandWeightKey(row) {
|
||||
return bandZoneKey(row) + '|' + String(row?.weightLabel || '-').trim()
|
||||
}
|
||||
|
||||
function feeTypeOrder(row) {
|
||||
return FEE_TYPE_ORDER[String(row?.feeType || '').toUpperCase()] || 9
|
||||
}
|
||||
|
||||
function bandViewSignature(pane) {
|
||||
const bands = pane?.item?.weightBands || []
|
||||
const keyword = pane?.bandKeyword || ''
|
||||
let hash = bands.length + ':' + keyword
|
||||
for (let i = 0; i < bands.length; i++) {
|
||||
const row = bands[i]
|
||||
hash += '|' + (row?.bandId ?? i) + ':' + (row?.feeType || '') + ':' + (row?.countryCode || row?.countryName || '') + ':' + (row?.zoneCode || row?.zoneName || '') + ':' + (row?.weightLabel || '')
|
||||
}
|
||||
return hash
|
||||
}
|
||||
|
||||
/** 缓存排序与 rowspan,避免每次渲染重排导致卡顿 */
|
||||
function weightSortValue(row) {
|
||||
const min = Number(row?.weightMin)
|
||||
if (Number.isFinite(min)) return min
|
||||
const label = String(row?.weightLabel || '')
|
||||
const m = label.match(/(\d+(?:\.\d+)?)/)
|
||||
return m ? Number(m[1]) : Number.POSITIVE_INFINITY
|
||||
}
|
||||
|
||||
function buildBandSpans(rows, keyOf) {
|
||||
const spans = new Array(rows.length).fill(1)
|
||||
let i = 0
|
||||
while (i < rows.length) {
|
||||
let j = i + 1
|
||||
const key = keyOf(rows[i])
|
||||
while (j < rows.length && keyOf(rows[j]) === key) j++
|
||||
spans[i] = j - i
|
||||
for (let k = i + 1; k < j; k++) spans[k] = 0
|
||||
i = j
|
||||
}
|
||||
return spans
|
||||
}
|
||||
|
||||
function ensureBandView(pane) {
|
||||
const sig = bandViewSignature(pane)
|
||||
const hit = bandViewCache.get(pane)
|
||||
if (hit && hit.sig === sig) return hit
|
||||
const rows = filteredBands(pane).slice()
|
||||
rows.sort((a, b) => {
|
||||
const c = bandCountryKey(a).localeCompare(bandCountryKey(b), 'zh')
|
||||
if (c) return c
|
||||
const z = pairText(a?.zoneName, a?.zoneCode).localeCompare(pairText(b?.zoneName, b?.zoneCode), 'zh')
|
||||
if (z) return z
|
||||
const w = weightSortValue(a) - weightSortValue(b)
|
||||
if (w) return w
|
||||
const wl = String(a?.weightLabel || '').localeCompare(String(b?.weightLabel || ''), 'zh')
|
||||
if (wl) return wl
|
||||
const f = feeTypeOrder(a) - feeTypeOrder(b)
|
||||
if (f) return f
|
||||
return Number(a?.bandId || 0) - Number(b?.bandId || 0)
|
||||
})
|
||||
const view = {
|
||||
sig,
|
||||
rows,
|
||||
countrySpans: buildBandSpans(rows, bandCountryKey),
|
||||
zoneSpans: buildBandSpans(rows, bandZoneKey),
|
||||
weightSpans: buildBandSpans(rows, bandWeightKey)
|
||||
}
|
||||
bandViewCache.set(pane, view)
|
||||
return view
|
||||
}
|
||||
|
||||
function displayBands(pane) {
|
||||
return ensureBandView(pane).rows
|
||||
}
|
||||
|
||||
function spanOf(spans, rowIndex) {
|
||||
const rowspan = spans[rowIndex] || 0
|
||||
return rowspan > 0 ? { rowspan, colspan: 1 } : { rowspan: 0, colspan: 0 }
|
||||
}
|
||||
|
||||
function bandSpanMethod(pane, { columnIndex, rowIndex }) {
|
||||
const view = ensureBandView(pane)
|
||||
if (columnIndex === 0) return spanOf(view.countrySpans, rowIndex)
|
||||
if (columnIndex === 1) return spanOf(view.zoneSpans, rowIndex)
|
||||
if (columnIndex === 2) return spanOf(view.weightSpans, rowIndex)
|
||||
return { rowspan: 1, colspan: 1 }
|
||||
}
|
||||
|
||||
function invalidateBandView(pane) {
|
||||
if (pane) bandViewCache.delete(pane)
|
||||
}
|
||||
|
||||
function filteredFees(pane) {
|
||||
return filterRows(pane.item.surcharges || [], pane.feeKeyword, ['feeName', 'zoneCode', 'zoneName', 'chargeText', 'amount', 'currency', 'priceUnit', 'feeDesc', 'calcRule', 'calcParam', 'postcodeParam'])
|
||||
}
|
||||
|
||||
function feeTabLabel(pane) {
|
||||
const fees = pane.item.surcharges || []
|
||||
return '附加费(' + fees.length + ')'
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
const feePickerExcludeIds = computed(() => {
|
||||
const fees = feePickerPane.value?.item?.surcharges || []
|
||||
return fees.map(fee => fee?.scId).filter(id => id != null)
|
||||
})
|
||||
|
||||
function openFeePicker(pane) {
|
||||
feePickerPane.value = pane
|
||||
feePickerVisible.value = true
|
||||
}
|
||||
|
||||
function toCalcRule(chargeType) {
|
||||
const t = String(chargeType || '').trim().toUpperCase()
|
||||
if (!t || t === 'RATE') return 'MANUAL'
|
||||
if (t === 'FIXED') return 'ALWAYS'
|
||||
const allowed = ['ALWAYS', 'POSTCODE', 'ZONE', 'EXTRA_KG', 'WEIGHT', 'SIZE', 'RETURN', 'MANUAL']
|
||||
return allowed.includes(t) ? t : 'MANUAL'
|
||||
}
|
||||
|
||||
function configToFeeRow(cfg, product) {
|
||||
const currency = String(cfg?.currency || product?.currency || 'EUR').trim().toUpperCase() || 'EUR'
|
||||
const amount = cfg?.chargeValue
|
||||
const isPercent = String(cfg?.chargeType || '').toUpperCase() === 'RATE'
|
||||
return {
|
||||
feeName: String(cfg?.scName || '').trim(),
|
||||
scId: cfg?.scId,
|
||||
zoneName: String(cfg?.blockTitle || cfg?.countryCode || '').trim(),
|
||||
chargeText: '',
|
||||
amount: amount == null ? '' : amount,
|
||||
currency,
|
||||
priceUnit: isPercent ? '%' : (currency + '/ITEM'),
|
||||
feeDesc: String(cfg?.scNote || '').trim(),
|
||||
calcRule: toCalcRule(cfg?.chargeType),
|
||||
calcParam: ''
|
||||
}
|
||||
}
|
||||
|
||||
function onFeePickerConfirm(list) {
|
||||
const pane = feePickerPane.value
|
||||
if (!pane?.item) return
|
||||
if (!pane.item.surcharges) pane.item.surcharges = []
|
||||
const existed = new Set(
|
||||
(pane.item.surcharges || []).map(fee => fee?.scId).filter(id => id != null).map(id => String(id))
|
||||
)
|
||||
let added = 0
|
||||
let skipped = 0
|
||||
for (const cfg of list || []) {
|
||||
if (cfg?.scId != null && existed.has(String(cfg.scId))) {
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
const row = configToFeeRow(cfg, pane.item)
|
||||
if (!row.feeName) continue
|
||||
pane.item.surcharges.push(row)
|
||||
if (row.scId != null) existed.add(String(row.scId))
|
||||
added++
|
||||
}
|
||||
if (added > 0) {
|
||||
pane.feePageNo = Math.ceil(pane.item.surcharges.length / pageSize) || 1
|
||||
pane.tab = 'fee'
|
||||
}
|
||||
if (added && skipped) {
|
||||
proxy.$modal.msgSuccess(`已添加 ${added} 项,跳过重复 ${skipped} 项`)
|
||||
} else if (added) {
|
||||
proxy.$modal.msgSuccess(`已添加 ${added} 项附加费`)
|
||||
} else {
|
||||
proxy.$modal.msgWarning('没有可添加的附加费(可能已全部存在)')
|
||||
}
|
||||
}
|
||||
|
||||
function removeRow(list, row) {
|
||||
const index = list.indexOf(row)
|
||||
if (index >= 0) list.splice(index, 1)
|
||||
for (const pane of panes.value) {
|
||||
if (pane?.item?.weightBands === list || pane?.item?.surcharges === list) invalidateBandView(pane)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function pageFees(pane) {
|
||||
return pageOf(filteredFees(pane), pane.feePageNo)
|
||||
}
|
||||
|
||||
function postcodeLines(pane) {
|
||||
const related = pane.item.relatedSheet || ''
|
||||
if (!related.includes('邮编')) return []
|
||||
return (pane.item.extraLines || []).filter(line => line.lineType === '邮编')
|
||||
}
|
||||
|
||||
function postcodeTitles(pane) {
|
||||
const sheet = `${pane.item.sheetName || ''}${pane.item.productName || ''}`
|
||||
if (sheet.includes('DE-iMile')) return ['国家', '产品', '邮编', '状态']
|
||||
return ['州/地区', '城市', '地区代码', '邮编', '分区']
|
||||
}
|
||||
|
||||
function postcodeHint(pane) {
|
||||
const sheet = `${pane.item.sheetName || ''}${pane.item.productName || ''}`
|
||||
if (sheet.includes('DE-iMile')) {
|
||||
return '德国这块没有州和城市。状态为 Active 的邮编属于价卡里的德国邮编清单,走同一套重量价格。'
|
||||
}
|
||||
return '用邮编对应州和城市。分区已换成价卡上的列:city(dense) 为 City,province(no dense) 为 Province,rural(remote) 为 Rural。'
|
||||
}
|
||||
|
||||
function filteredPostcodes(pane) {
|
||||
const text = (pane.postKeyword || '').trim().toLowerCase()
|
||||
const rows = postcodeLines(pane).map(line => ({ parts: String(line.lineText || '').split(' | '), raw: line.lineText || '' }))
|
||||
if (!text) return rows
|
||||
return rows.filter(row => row.raw.toLowerCase().includes(text))
|
||||
}
|
||||
|
||||
function pagePostcodes(pane) {
|
||||
return pageOf(filteredPostcodes(pane), pane.postPageNo)
|
||||
}
|
||||
|
||||
function filterRows(rows, keyword, fields) {
|
||||
const text = (keyword || '').trim().toLowerCase()
|
||||
if (!text) return rows
|
||||
return rows.filter(row => fields.some(field => String(row?.[field] ?? '').toLowerCase().includes(text)))
|
||||
}
|
||||
|
||||
function pageOf(rows, pageNo) {
|
||||
const start = (pageNo - 1) * pageSize
|
||||
return rows.slice(start, start + pageSize)
|
||||
}
|
||||
|
||||
function feeTypeText(type) {
|
||||
const map = {
|
||||
FREIGHT: '运费',
|
||||
REGISTRATION: '处理费',
|
||||
REMOTE: '偏远费'
|
||||
}
|
||||
return map[String(type || '')] || (type || '-')
|
||||
}
|
||||
|
||||
function pairText(name, code) {
|
||||
const a = String(name ?? '').trim()
|
||||
const b = String(code ?? '').trim()
|
||||
if (!a) return b || '-'
|
||||
if (!b || a === b) return a
|
||||
const compact = a.replace(/\s/g, '').toUpperCase()
|
||||
const codeU = b.toUpperCase()
|
||||
// 名称已含码(如 奥地利(AT))不再拼
|
||||
if (compact.includes('(' + codeU + ')') || compact.includes('/' + codeU) || compact.endsWith(codeU)) return a
|
||||
return `${a} (${b})`
|
||||
}
|
||||
|
||||
function dictLabel(options, value) {
|
||||
const v = String(value ?? '').trim()
|
||||
if (!v) return ''
|
||||
const hit = (options || []).find(d => String(d.value) === v)
|
||||
return hit ? (hit.label || v) : v
|
||||
}
|
||||
|
||||
function moneyUnit(amount, unit) {
|
||||
const a = money(amount)
|
||||
const u = String(unit ?? '').trim()
|
||||
if (a === '-' && !u) return '-'
|
||||
if (a === '-') return u || '-'
|
||||
return u ? `${a} ${u}` : a
|
||||
}
|
||||
|
||||
function money(value) {
|
||||
if (value === null || value === undefined || value === '') return '-'
|
||||
const num = Number(value)
|
||||
return Number.isNaN(num) ? value : String(num)
|
||||
}
|
||||
|
||||
/** 价格和货币已经拆开时,收费标准只留说明,不再重复「0.19 EUR/ITEM」。 */
|
||||
function feeNote(row) {
|
||||
const text = (row.chargeText || '').trim()
|
||||
if (!text || row.amount === null || row.amount === undefined || row.amount === '') return text
|
||||
const amount = Number(row.amount)
|
||||
if (!Number.isFinite(amount)) return text
|
||||
const match = text.match(/^\s*(\d+(?:\.\d+)?)\s*(?:EUR|GBP|USD|RMB|CNY|欧元|英镑|人民币|美元)?\s*(?:\/\s*[A-Za-z\u4e00-\u9fa5]+)?\s*([\s\S]*)$/i)
|
||||
if (!match || Number(match[1]) !== amount) return text
|
||||
const rest = (match[2] || '').trim()
|
||||
if (!rest || rest === '%' || rest === (row.priceUnit || '') || /^[。..、,,]+$/.test(rest)) return ''
|
||||
return rest
|
||||
}
|
||||
|
||||
function periodLabel(row) {
|
||||
if (row.effectiveStart || row.effectiveEnd) {
|
||||
return `${row.effectiveStart || '…'} 至 ${row.effectiveEnd || '…'}`
|
||||
}
|
||||
return row.periodText || row.chargeTiming || '-'
|
||||
}
|
||||
|
||||
function statusText(status) {
|
||||
return { OK: '已识别', EMPTY: '未识别到配置', MISSING: '缺少工作表' }[status] || status || '-'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.label-product + .label-product {
|
||||
margin-top: 18px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
.label-filter {
|
||||
width: 320px;
|
||||
}
|
||||
.label-edit-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.label-hint {
|
||||
margin: 0 0 8px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.label-page {
|
||||
margin-top: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.label-cond-hint {
|
||||
line-height: 1.5;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
.limit-tag {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.label-cell-pair {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.label-cell-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.label-price-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.label-unit-text {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
padding: 0 2px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -13,9 +13,13 @@
|
||||
:price-set-id="priceSetId"
|
||||
@parsed="onParsed"
|
||||
/>
|
||||
<div v-if="saving" class="save-progress">
|
||||
<el-progress :percentage="savePercent" :stroke-width="14" :status="saveStatus" striped striped-flow :duration="12" />
|
||||
<div class="save-progress-msg">{{ saveMessage }}</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">{{ autoFillOnParse ? '关闭' : '取消' }}</el-button>
|
||||
<el-button v-if="!autoFillOnParse" type="primary" :loading="saving" @click="save">保存</el-button>
|
||||
<el-button :disabled="saving" @click="visible = false">{{ autoFillOnParse ? '关闭' : '取消' }}</el-button>
|
||||
<el-button v-if="!autoFillOnParse" type="primary" :loading="saving" :disabled="saving" @click="save">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
@@ -38,6 +42,9 @@ const { proxy } = getCurrentInstance()
|
||||
|
||||
const visible = ref(false)
|
||||
const saving = ref(false)
|
||||
const savePercent = ref(0)
|
||||
const saveMessage = ref('')
|
||||
const saveStatus = ref(undefined)
|
||||
const panelRef = ref()
|
||||
const autoFilled = ref(false)
|
||||
|
||||
@@ -50,6 +57,9 @@ function open() {
|
||||
function onClosed() {
|
||||
panelRef.value?.reset()
|
||||
saving.value = false
|
||||
savePercent.value = 0
|
||||
saveMessage.value = ''
|
||||
saveStatus.value = undefined
|
||||
autoFilled.value = false
|
||||
}
|
||||
|
||||
@@ -76,12 +86,30 @@ async function save() {
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
savePercent.value = 1
|
||||
saveStatus.value = undefined
|
||||
saveMessage.value = '正在提交保存任务…'
|
||||
try {
|
||||
const result = await panelRef.value.submit({ priceSetId: props.priceSetId })
|
||||
const result = await panelRef.value.submit({
|
||||
priceSetId: props.priceSetId,
|
||||
onProgress: prog => {
|
||||
const pct = Math.min(99, Number(prog.percent) || 0)
|
||||
if (pct >= savePercent.value) savePercent.value = pct
|
||||
if (prog.message) saveMessage.value = prog.message
|
||||
if (prog.status === 'FAIL') saveStatus.value = 'exception'
|
||||
}
|
||||
})
|
||||
savePercent.value = 100
|
||||
saveStatus.value = 'success'
|
||||
saveMessage.value = result.message || '保存成功'
|
||||
visible.value = false
|
||||
ElMessageBox.close()
|
||||
proxy.$modal.msgSuccess(result.message || '保存成功')
|
||||
emit('saved', { priceSetId: result.priceSetId || props.priceSetId })
|
||||
} catch (e) {
|
||||
saveStatus.value = 'exception'
|
||||
saveMessage.value = e?.message || '保存失败'
|
||||
proxy.$modal.msgError(e?.message || '保存失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
@@ -89,3 +117,8 @@ async function save() {
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.save-progress { margin: 8px 0 0; padding: 8px 4px 0; }
|
||||
.save-progress-msg { margin-top: 6px; color: #606266; font-size: 13px; }
|
||||
</style>
|
||||
|
||||
@@ -5,11 +5,6 @@
|
||||
<el-tag type="success">应收 AR</el-tag>
|
||||
<span class="form-tip">打单套餐固定应收,不显示建价选项,直接 Excel 导入</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="产品">
|
||||
<el-select v-model="dictValue" clearable filterable placeholder="不选则按工作表名称匹配全部字典产品" style="width: 420px">
|
||||
<el-option v-for="item in payment_product_type" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-upload
|
||||
@@ -27,7 +22,7 @@
|
||||
<div class="upload-tip">
|
||||
{{ autoFillOnParse
|
||||
? '解析成功后自动回填到创建页产品价卡列表;打单保存与应付独立,弹窗内不展示表格。'
|
||||
: '模板以威狮 B2C 末端派送价卡为准。选一个产品就只读对应工作表。' }}
|
||||
: '模板以威狮 B2C 末端派送价卡为准;按工作表名称匹配字典产品。' }}
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
@@ -47,7 +42,7 @@
|
||||
/>
|
||||
<div class="preview-bar">
|
||||
<el-switch v-model="overwrite" active-text="本套餐已存在时另存为新版本" />
|
||||
<span class="preview-hint">仅本打单套餐维度;与应付公共价卡互不影响。开启后同产品编号不变,旧价留在历史 term。</span>
|
||||
<span class="preview-hint">仅本打单套餐维度;与应付公共价卡互不影响。开启后同产品编号不变,旧价留在历史版本。</span>
|
||||
</div>
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
@@ -60,22 +55,10 @@
|
||||
>
|
||||
<el-table-column type="selection" width="48" :selectable="canSelect" />
|
||||
<el-table-column prop="productName" label="产品" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="sheetName" label="报价工作表" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="chargeType" label="费用类型" min-width="110" show-overflow-tooltip />
|
||||
<el-table-column prop="countryScope" label="国家/口岸" min-width="110" show-overflow-tooltip />
|
||||
<el-table-column prop="relatedSheet" label="关联分区表" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="weightCount" label="重量段" width="80" align="right" />
|
||||
<el-table-column prop="surchargeCount" label="附加费" width="80" align="right" />
|
||||
<el-table-column label="识别" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType(row.parseStatus)" size="small">{{ statusText(row.parseStatus) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="库中" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.duplicated" type="warning" size="small">已存在</el-tag>
|
||||
<span v-else>新记录</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="90" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" icon="View" @click="openDetail(row)">查看</el-button>
|
||||
@@ -87,7 +70,7 @@
|
||||
</div>
|
||||
|
||||
<el-drawer v-model="detailVisible" :title="detailTitle" size="78%" destroy-on-close append-to-body>
|
||||
<pay-config-detail :product="detailProduct" :editable="false" />
|
||||
<label-config-detail :product="detailProduct" :editable="false" />
|
||||
<template #footer>
|
||||
<el-button @click="detailVisible = false">关闭</el-button>
|
||||
</template>
|
||||
@@ -97,9 +80,8 @@
|
||||
|
||||
<script setup>
|
||||
import { UploadFilled } from '@element-plus/icons-vue'
|
||||
import { previewLabel, labelParseProgress, saveLabelImport } from '@/api/price/label'
|
||||
import { useDict } from '@/utils/dict'
|
||||
import PayConfigDetail from '@/views/price/basic/pay/components/PayConfigDetail.vue'
|
||||
import { previewLabel, labelParseProgress, saveLabelImportAndWait } from '@/api/price/label'
|
||||
import LabelConfigDetail from './LabelConfigDetail.vue'
|
||||
|
||||
const props = defineProps({
|
||||
/** 仅创建页开启:解析成功后立刻抛出勾选结果,其它入口保持预览后手动保存 */
|
||||
@@ -109,11 +91,8 @@ const props = defineProps({
|
||||
})
|
||||
const emit = defineEmits(['parsed'])
|
||||
const { proxy } = getCurrentInstance()
|
||||
const { payment_product_type } = useDict('payment_product_type')
|
||||
|
||||
const recognizing = ref(false)
|
||||
const overwrite = ref(false)
|
||||
const dictValue = ref('')
|
||||
const preview = ref(null)
|
||||
const selected = ref([])
|
||||
const tableRef = ref()
|
||||
@@ -141,7 +120,6 @@ function reset() {
|
||||
preview.value = null
|
||||
selected.value = []
|
||||
overwrite.value = false
|
||||
dictValue.value = ''
|
||||
recognizing.value = false
|
||||
parsePercent.value = 0
|
||||
parseMessage.value = ''
|
||||
@@ -172,7 +150,7 @@ async function onFile(uploadFile) {
|
||||
parseStatus.value = undefined
|
||||
startProgress(seq, progressId)
|
||||
try {
|
||||
const res = await previewLabel(raw, dictValue.value, progressId, event => {
|
||||
const res = await previewLabel(raw, '', progressId, event => {
|
||||
if (seq !== parseSeq || !event?.total) return
|
||||
const uploaded = Math.min(8, Math.round(event.loaded * 8 / event.total))
|
||||
if (uploaded > parsePercent.value) {
|
||||
@@ -202,8 +180,8 @@ async function onFile(uploadFile) {
|
||||
} else {
|
||||
proxy.$modal.msgWarning(
|
||||
props.priceSetId
|
||||
? '没有可导入的产品,请调整产品筛选或开启「本套餐已存在时另存为新版本」后重试'
|
||||
: '没有可导入的产品,请调整产品筛选或检查价卡工作表是否匹配字典'
|
||||
? '没有可导入的产品,请开启「本套餐已存在时另存为新版本」后重试'
|
||||
: '没有可导入的产品,请检查价卡工作表是否匹配字典'
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -314,16 +292,15 @@ async function submit(payload = {}) {
|
||||
...product,
|
||||
productType: product.productType || 'AR'
|
||||
}))
|
||||
const res = await saveLabelImport({
|
||||
const data = await saveLabelImportAndWait({
|
||||
priceSetId: payload.priceSetId || null,
|
||||
priceSetName: String(payload.priceSetName || '').trim(),
|
||||
priceSetNote: payload.priceSetNote || '',
|
||||
overwrite: payload.overwrite != null ? payload.overwrite : overwrite.value,
|
||||
products: productsForSave
|
||||
})
|
||||
const data = res.data || {}
|
||||
}, payload.onProgress)
|
||||
return {
|
||||
message: data.message || res.msg || '保存成功',
|
||||
message: data.message || '保存成功',
|
||||
priceSetId: data.priceSetId || payload.priceSetId
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,12 +15,13 @@
|
||||
:data="filteredRows"
|
||||
border
|
||||
height="calc(100vh - 220px)"
|
||||
row-key="termId"
|
||||
row-key="versionId"
|
||||
>
|
||||
<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 prop="chargeType" :label="colChargeType" min-width="110" show-overflow-tooltip />
|
||||
<el-table-column :label="colTerm" min-width="180" align="center">
|
||||
<template #default="{ row }">{{ termText(row) }}</template>
|
||||
<template #default="{ row }">{{ versionText(row) }}</template>
|
||||
</el-table-column>
|
||||
<!-- <el-table-column :label="colStatus" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
@@ -41,23 +42,13 @@
|
||||
</el-table>
|
||||
|
||||
<el-drawer v-model="detailVisible" :title="detailTitle" size="78%" destroy-on-close append-to-body>
|
||||
<pay-config-detail
|
||||
<label-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>
|
||||
|
||||
<el-drawer
|
||||
@@ -139,12 +130,14 @@ import {
|
||||
listLabelProducts,
|
||||
getLabelProduct,
|
||||
updateLabelProduct,
|
||||
updateLabelWeightBand,
|
||||
updateLabelSurcharge,
|
||||
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'
|
||||
import LabelConfigDetail from './LabelConfigDetail.vue'
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false },
|
||||
@@ -180,6 +173,7 @@ const phSearch = '产品名称 / 绑定渠道'
|
||||
const colId = '产品编号'
|
||||
const colName = '产品名称'
|
||||
const colTerm = '有效期'
|
||||
const colChargeType = '费用类型'
|
||||
const colStatus = '状态'
|
||||
const colWeight = '重量段'
|
||||
const colSc = '附加费'
|
||||
@@ -232,9 +226,9 @@ function formatHour(val) {
|
||||
return text
|
||||
}
|
||||
|
||||
function termText(row) {
|
||||
const start = formatHour(row.termStart || row.effectiveDate || '')
|
||||
const end = row.termEnd ? formatHour(row.termEnd) : '不限'
|
||||
function versionText(row) {
|
||||
const start = formatHour(row.versionStart || row.effectiveDate || '')
|
||||
const end = row.versionEnd ? formatHour(row.versionEnd) : '不限'
|
||||
if (!start) return '未设置'
|
||||
return start + ' ~ ' + end
|
||||
}
|
||||
@@ -279,7 +273,11 @@ async function load() {
|
||||
function cleanMoney(product) {
|
||||
if (!product) return
|
||||
;(product.weightBands || []).forEach(row => {
|
||||
if (row && row.price === '') row.price = null
|
||||
if (!row) return
|
||||
if (row.price === '') row.price = null
|
||||
if (row.extraPrice === '') row.extraPrice = null
|
||||
if (row.weightMin === '') row.weightMin = null
|
||||
if (row.weightMax === '') row.weightMax = null
|
||||
})
|
||||
;(product.surcharges || []).forEach(row => {
|
||||
if (row && row.amount === '') row.amount = null
|
||||
@@ -292,26 +290,13 @@ async function persistDetail() {
|
||||
}
|
||||
cleanMoney(detail.value)
|
||||
await updateLabelProduct(detail.value)
|
||||
const termId = detail.value.termId
|
||||
const res = await getLabelProduct(detail.value.productId, termId)
|
||||
const versionId = detail.value.versionId || detail.value.termId
|
||||
const res = await getLabelProduct(detail.value.productId, versionId)
|
||||
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)
|
||||
@@ -320,7 +305,27 @@ async function saveDetailRow(payload) {
|
||||
savingRowKey.value = payload?.rowKey || 'row'
|
||||
let ok = false
|
||||
try {
|
||||
await persistDetail()
|
||||
const row = payload?.row
|
||||
if (payload?.kind === 'band') {
|
||||
if (!row?.bandId) throw new Error('NO_BAND')
|
||||
if (row.price === '') row.price = null
|
||||
if (row.extraPrice === '') row.extraPrice = null
|
||||
await updateLabelWeightBand({
|
||||
bandId: row.bandId,
|
||||
feeType: row.feeType,
|
||||
chargeMode: row.chargeMode,
|
||||
calcParam: row.calcParam,
|
||||
postcodeParam: row.postcodeParam,
|
||||
price: row.price,
|
||||
extraPrice: row.extraPrice
|
||||
})
|
||||
} else if (payload?.kind === 'fee') {
|
||||
if (!row?.surchargeId) throw new Error('NO_FEE')
|
||||
if (row.amount === '') row.amount = null
|
||||
await updateLabelSurcharge(row)
|
||||
} else {
|
||||
await persistDetail()
|
||||
}
|
||||
ok = true
|
||||
proxy.$modal.msgSuccess('该行已保存')
|
||||
} catch (e) {
|
||||
@@ -338,7 +343,7 @@ async function openProduct(row) {
|
||||
savingRowKey.value = ''
|
||||
detail.value = null
|
||||
try {
|
||||
const res = await getLabelProduct(row.productId, row.termId)
|
||||
const res = await getLabelProduct(row.productId, row.versionId || row.termId)
|
||||
detail.value = res.data || null
|
||||
} finally {
|
||||
detailLoading.value = false
|
||||
|
||||
@@ -59,6 +59,13 @@
|
||||
@click="$emit('edit', row)"
|
||||
v-hasPermi="['price:label:edit']"
|
||||
>编辑</el-button>
|
||||
<el-button
|
||||
v-if="canActivate(row)"
|
||||
link
|
||||
type="success"
|
||||
@click="$emit('activate', row)"
|
||||
v-hasPermi="['price:label:edit']"
|
||||
>启动</el-button>
|
||||
<el-button
|
||||
v-if="canReschedule(row)"
|
||||
link
|
||||
@@ -83,7 +90,7 @@
|
||||
const props = defineProps({
|
||||
versions: { type: Array, default: () => [] }
|
||||
})
|
||||
defineEmits(['edit', 'reschedule', 'remove', 'open-details', 'create-version'])
|
||||
defineEmits(['edit', 'activate', 'reschedule', 'remove', 'open-details', 'create-version'])
|
||||
|
||||
const statusTab = ref('all')
|
||||
|
||||
@@ -133,9 +140,18 @@ function canEdit(row) {
|
||||
return !!row
|
||||
}
|
||||
|
||||
/** 仅待生效可改生效时间 */
|
||||
function hasEffectiveTime(row) {
|
||||
return !!(row && row.effectiveTime)
|
||||
}
|
||||
|
||||
/** 待生效且未设生效时间:启动(启动时设置) */
|
||||
function canActivate(row) {
|
||||
return row && String(row.status) === '0' && !hasEffectiveTime(row)
|
||||
}
|
||||
|
||||
/** 待生效且已设生效时间:仅修改时间 */
|
||||
function canReschedule(row) {
|
||||
return row && String(row.status) === '0'
|
||||
return row && String(row.status) === '0' && hasEffectiveTime(row)
|
||||
}
|
||||
|
||||
/** 仅待生效可删除整版 */
|
||||
|
||||
@@ -58,22 +58,10 @@
|
||||
>
|
||||
<el-table-column type="selection" width="48" align="center" />
|
||||
<el-table-column prop="productName" label="产品" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="sheetName" label="报价工作表" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="chargeType" label="费用类型" min-width="110" show-overflow-tooltip />
|
||||
<el-table-column prop="countryScope" label="国家/口岸" min-width="110" show-overflow-tooltip />
|
||||
<el-table-column prop="relatedSheet" label="关联分区表" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="weightCount" label="重量段" width="80" align="right" />
|
||||
<el-table-column prop="surchargeCount" label="附加费" width="80" align="right" />
|
||||
<el-table-column label="识别" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType(row.parseStatus)" size="small">{{ statusText(row.parseStatus) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="库中" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.duplicated" type="warning" size="small">已存在</el-tag>
|
||||
<span v-else>新记录</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="140" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" icon="Edit" @click="openDetail(row)">编辑</el-button>
|
||||
@@ -84,9 +72,21 @@
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<div v-if="saving" class="save-progress">
|
||||
<el-progress
|
||||
:percentage="savePercent"
|
||||
:stroke-width="16"
|
||||
:status="saveStatus"
|
||||
striped
|
||||
striped-flow
|
||||
:duration="12"
|
||||
/>
|
||||
<div class="save-progress-msg">{{ saveMessage }}</div>
|
||||
</div>
|
||||
|
||||
<div class="wizard-actions">
|
||||
<el-button @click="goBack">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="finish">完成</el-button>
|
||||
<el-button :disabled="saving" @click="goBack">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" :disabled="saving" @click="finish">完成</el-button>
|
||||
</div>
|
||||
|
||||
<label-import-dialog
|
||||
@@ -97,7 +97,7 @@
|
||||
/>
|
||||
|
||||
<el-drawer v-model="detailVisible" :title="detailTitle" size="78%" destroy-on-close append-to-body @closed="onDetailClosed">
|
||||
<pay-config-detail
|
||||
<label-config-detail
|
||||
:product="detailProduct"
|
||||
:editable="true"
|
||||
:saving-row-key="savingRowKey"
|
||||
@@ -112,14 +112,17 @@ import { ElMessageBox } from 'element-plus'
|
||||
import StepBasicInfo from '../set/components/create/StepBasicInfo.vue'
|
||||
import { createEmptyBasicInfo } from '../set/utils/priceSet'
|
||||
import LabelImportDialog from './components/LabelImportDialog.vue'
|
||||
import PayConfigDetail from '@/views/price/basic/pay/components/PayConfigDetail.vue'
|
||||
import { getLabelPackage, getLabelProduct, listLabelProducts, saveLabelImport } from '@/api/price/label'
|
||||
import LabelConfigDetail from './components/LabelConfigDetail.vue'
|
||||
import { getLabelPackage, getLabelProduct, listLabelProducts, saveLabelImportAndWait } from '@/api/price/label'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const route = useRoute()
|
||||
const basicRef = ref()
|
||||
const importRef = ref()
|
||||
const saving = ref(false)
|
||||
const savePercent = ref(0)
|
||||
const saveMessage = ref('')
|
||||
const saveStatus = ref(undefined)
|
||||
const loadingProducts = ref(false)
|
||||
const form = reactive({
|
||||
basic: createEmptyBasicInfo()
|
||||
@@ -196,14 +199,14 @@ async function loadEditProducts() {
|
||||
}
|
||||
const details = await Promise.all(list.map(async item => {
|
||||
try {
|
||||
const detailRes = await getLabelProduct(item.productId, item.termId)
|
||||
const detailRes = await getLabelProduct(item.productId, item.versionId || item.termId)
|
||||
const detail = detailRes.data || item
|
||||
const bands = detail.weightBands || []
|
||||
const fees = detail.surcharges || []
|
||||
const extras = detail.extraLines || []
|
||||
return {
|
||||
...detail,
|
||||
rowKey: `${detail.sheetName || ''}-${detail.productName || ''}-${detail.termId || detail.productId || ''}`,
|
||||
rowKey: `${detail.sheetName || ''}-${detail.productName || ''}-${detail.versionId || detail.termId || detail.productId || ''}`,
|
||||
weightCount: bands.length || detail.weightCount || 0,
|
||||
surchargeCount: fees.length || detail.surchargeCount || 0,
|
||||
extraCount: extras.length || detail.extraCount || 0,
|
||||
@@ -213,7 +216,7 @@ async function loadEditProducts() {
|
||||
} catch {
|
||||
return {
|
||||
...item,
|
||||
rowKey: `${item.sheetName || ''}-${item.productName || ''}-${item.termId || item.productId || ''}`,
|
||||
rowKey: `${item.sheetName || ''}-${item.productName || ''}-${item.versionId || item.termId || item.productId || ''}`,
|
||||
parseStatus: 'OK',
|
||||
duplicated: true
|
||||
}
|
||||
@@ -384,13 +387,16 @@ async function finish() {
|
||||
? `将覆盖版本 V${versionNo.value} 的 ${productRows.value.length} 个产品,确认继续?`
|
||||
: newVersionMode.value
|
||||
? `将为套餐「${form.basic.priceSetName || ''}」新建待生效版本并保存 ${productRows.value.length} 个产品,确认继续?`
|
||||
: `将创建打单套餐并保存 ${productRows.value.length} 个应收产品,确认继续?`
|
||||
: `将创建打单套餐并保存 ${productRows.value.length} 个产品,确认继续?`
|
||||
try {
|
||||
await proxy.$modal.confirm(tip)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
savePercent.value = 1
|
||||
saveStatus.value = undefined
|
||||
saveMessage.value = '正在提交保存任务…'
|
||||
try {
|
||||
const payload = {
|
||||
overwrite: editVersionMode.value ? true : importMeta.overwrite,
|
||||
@@ -405,10 +411,17 @@ async function finish() {
|
||||
payload.priceSetName = form.basic.priceSetName
|
||||
payload.priceSetNote = form.basic.priceSetNote
|
||||
}
|
||||
const res = await saveLabelImport(payload)
|
||||
const data = res.data || {}
|
||||
const data = await saveLabelImportAndWait(payload, prog => {
|
||||
const pct = Math.min(99, Number(prog.percent) || 0)
|
||||
if (pct >= savePercent.value) savePercent.value = pct
|
||||
if (prog.message) saveMessage.value = prog.message
|
||||
if (prog.status === 'FAIL') saveStatus.value = 'exception'
|
||||
})
|
||||
savePercent.value = 100
|
||||
saveStatus.value = 'success'
|
||||
saveMessage.value = data.message || '保存成功'
|
||||
ElMessageBox.close()
|
||||
proxy.$modal.msgSuccess(data.message || res.msg || '保存成功')
|
||||
proxy.$modal.msgSuccess(data.message || '保存成功')
|
||||
const id = data.priceSetId || priceSetId.value
|
||||
if (id) {
|
||||
const obj = { path: '/customer/labelDetail', query: { priceSetId: id } }
|
||||
@@ -416,6 +429,10 @@ async function finish() {
|
||||
} else {
|
||||
goBack()
|
||||
}
|
||||
} catch (e) {
|
||||
saveStatus.value = 'exception'
|
||||
saveMessage.value = e?.message || '保存失败'
|
||||
proxy.$modal.msgError(e?.message || '保存失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
@@ -458,4 +475,15 @@ watch([priceSetId, versionNo, () => route.query.editVersion, () => route.query.n
|
||||
gap: 12px;
|
||||
padding: 8px 0 24px;
|
||||
}
|
||||
.save-progress {
|
||||
margin: 8px 0 4px;
|
||||
padding: 12px 16px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.save-progress-msg {
|
||||
margin-top: 8px;
|
||||
color: #606266;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,6 +7,28 @@
|
||||
<el-tag size="small" type="warning" effect="plain">打单 / LABEL</el-tag>
|
||||
<el-tag v-if="setInfo.status" size="small" :type="setStatusTag" effect="plain" class="status-tag">{{ setStatusText }}</el-tag>
|
||||
</template>
|
||||
<template #extra>
|
||||
<el-button
|
||||
v-if="canStartPackage"
|
||||
type="success"
|
||||
v-hasPermi="['price:label:edit']"
|
||||
@click="startPackage"
|
||||
>启动套餐</el-button>
|
||||
<el-button
|
||||
v-if="setInfo.status === 'E'"
|
||||
type="danger"
|
||||
plain
|
||||
v-hasPermi="['price:label:edit']"
|
||||
@click="onDisablePackage"
|
||||
>停用</el-button>
|
||||
<el-button
|
||||
v-if="setInfo.status === 'S'"
|
||||
type="success"
|
||||
plain
|
||||
v-hasPermi="['price:label:edit']"
|
||||
@click="onEnablePackage"
|
||||
>启用</el-button>
|
||||
</template>
|
||||
</el-page-header>
|
||||
|
||||
<set-customer-table :set="setInfo" @refresh="loadHeader" />
|
||||
@@ -14,6 +36,7 @@
|
||||
<label-version-table
|
||||
:versions="versions"
|
||||
@edit="openEditVersion"
|
||||
@activate="openActivate"
|
||||
@reschedule="openReschedule"
|
||||
@remove="removeVersion"
|
||||
@open-details="openVersionDetails"
|
||||
@@ -29,7 +52,7 @@
|
||||
|
||||
<el-dialog
|
||||
v-model="activateVisible"
|
||||
title="修改生效时间"
|
||||
:title="activateDialogTitle"
|
||||
width="460px"
|
||||
append-to-body
|
||||
@closed="onActivateClosed"
|
||||
@@ -48,11 +71,11 @@
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<div class="activate-hint">仅待生效版本可修改;精确到小时。未到生效时间仍为待生效,到时自动按生效处理。</div>
|
||||
<div class="activate-hint">{{ activateDialogHint }}</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="activateVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="activateSubmitting" @click="submitActivate">确认修改</el-button>
|
||||
<el-button type="primary" :loading="activateSubmitting" @click="submitActivate">{{ activateConfirmText }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
@@ -64,6 +87,8 @@ import {
|
||||
listLabelVersions,
|
||||
activateLabelVersion,
|
||||
deleteLabelVersion,
|
||||
enableLabelPackage,
|
||||
disableLabelPackage,
|
||||
listLabelCustomer,
|
||||
addLabelCustomer,
|
||||
batchAddLabelCustomer,
|
||||
@@ -135,14 +160,66 @@ const setStatusTag = computed(() => {
|
||||
return map[setInfo.status] || 'info'
|
||||
})
|
||||
|
||||
/** 待启动:有待生效且未设生效时间的版本 */
|
||||
const startableVersion = computed(() =>
|
||||
(versions.value || []).find(v => v && String(v.status) === '0' && !v.effectiveTime)
|
||||
)
|
||||
const canStartPackage = computed(() =>
|
||||
setInfo.status !== 'E' && setInfo.status !== 'S' && !!startableVersion.value
|
||||
)
|
||||
|
||||
function startPackage() {
|
||||
const row = startableVersion.value
|
||||
if (!row) {
|
||||
proxy.$modal.msgWarning('没有可启动的版本,请先导入价卡')
|
||||
return
|
||||
}
|
||||
openActivate(row)
|
||||
}
|
||||
|
||||
function onDisablePackage() {
|
||||
if (!priceSetId.value) return
|
||||
const name = setInfo.priceSetName || ''
|
||||
proxy.$modal.confirm('停用后该打单套餐不再参与计费命中,确认停用【' + name + '】?').then(() => {
|
||||
return disableLabelPackage(priceSetId.value)
|
||||
}).then(() => {
|
||||
proxy.$modal.msgSuccess('已停用')
|
||||
reloadAll()
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
function onEnablePackage() {
|
||||
if (!priceSetId.value) return
|
||||
const name = setInfo.priceSetName || ''
|
||||
proxy.$modal.confirm('启用后该打单套餐将重新参与计费命中,确认启用【' + name + '】?').then(() => {
|
||||
return enableLabelPackage(priceSetId.value)
|
||||
}).then(() => {
|
||||
proxy.$modal.msgSuccess('已启用')
|
||||
reloadAll()
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
const activateVisible = ref(false)
|
||||
const activateSubmitting = ref(false)
|
||||
const activateFormRef = ref()
|
||||
/** activate=启动(首次设生效时间);reschedule=修改已设时间 */
|
||||
const activateMode = ref('activate')
|
||||
const activateForm = reactive({
|
||||
versionNo: null,
|
||||
versionLabel: '',
|
||||
effectiveTime: undefined
|
||||
})
|
||||
const activateDialogTitle = computed(() =>
|
||||
activateMode.value === 'reschedule' ? '修改生效时间' : '启动版本'
|
||||
)
|
||||
const activateConfirmText = computed(() =>
|
||||
activateMode.value === 'reschedule' ? '确认修改' : '确认启动'
|
||||
)
|
||||
const activateDialogHint = computed(() =>
|
||||
activateMode.value === 'reschedule'
|
||||
? '仅待生效版本可修改;精确到小时。未到生效时间仍为待生效,到时自动按生效处理。'
|
||||
: '启动时设置生效时间;精确到小时。若晚于当前则为待生效,到时自动生效并使套餐进入生效状态。'
|
||||
)
|
||||
const activateRules = {
|
||||
effectiveTime: [{ required: true, message: '请选择生效时间', trigger: 'change' }]
|
||||
}
|
||||
@@ -223,18 +300,41 @@ function toHourValue(val) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
function openReschedule(row) {
|
||||
function formatNowHour() {
|
||||
const d = new Date()
|
||||
const pad = n => String(n).padStart(2, '0')
|
||||
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate())
|
||||
+ ' ' + pad(d.getHours()) + ':00:00'
|
||||
}
|
||||
|
||||
function openActivateDialog(row, mode) {
|
||||
activateMode.value = mode
|
||||
activateForm.versionNo = row.versionNo
|
||||
activateForm.versionLabel = row.versionLabel || ('V' + row.versionNo)
|
||||
activateForm.effectiveTime = row.effectiveTime ? toHourValue(row.effectiveTime) : undefined
|
||||
if (mode === 'reschedule' && row.effectiveTime) {
|
||||
activateForm.effectiveTime = toHourValue(row.effectiveTime)
|
||||
} else {
|
||||
activateForm.effectiveTime = formatNowHour()
|
||||
}
|
||||
activateVisible.value = true
|
||||
}
|
||||
|
||||
/** 首次启动:设置生效时间 */
|
||||
function openActivate(row) {
|
||||
openActivateDialog(row, 'activate')
|
||||
}
|
||||
|
||||
/** 已设生效时间后修改 */
|
||||
function openReschedule(row) {
|
||||
openActivateDialog(row, 'reschedule')
|
||||
}
|
||||
|
||||
function onActivateClosed() {
|
||||
activateForm.versionNo = null
|
||||
activateForm.versionLabel = ''
|
||||
activateForm.effectiveTime = undefined
|
||||
activateSubmitting.value = false
|
||||
activateMode.value = 'activate'
|
||||
}
|
||||
|
||||
function submitActivate() {
|
||||
@@ -245,7 +345,8 @@ function submitActivate() {
|
||||
effectiveTime: activateForm.effectiveTime
|
||||
}).then(res => {
|
||||
const data = res.data || {}
|
||||
proxy.$modal.msgSuccess(data.message || res.msg || '操作成功')
|
||||
const fallback = activateMode.value === 'reschedule' ? '生效时间已修改' : '版本已启动'
|
||||
proxy.$modal.msgSuccess(data.message || res.msg || fallback)
|
||||
activateVisible.value = false
|
||||
reloadAll()
|
||||
}).finally(() => {
|
||||
@@ -262,6 +363,9 @@ watch(priceSetId, async (id) => {
|
||||
|
||||
<style scoped>
|
||||
.page-header { margin-bottom: 16px; }
|
||||
.page-header :deep(.el-page-header__extra) {
|
||||
margin-left: auto;
|
||||
}
|
||||
.title { font-weight: 600; margin-right: 8px; }
|
||||
.code-tag { margin-right: 8px; }
|
||||
.status-tag { margin-left: 8px; }
|
||||
|
||||
@@ -95,7 +95,6 @@ const columns = [
|
||||
{ label: '套餐编码', prop: 'priceSetCode', width: 110 },
|
||||
{ label: '套餐名称', prop: 'priceSetName', minWidth: 160 },
|
||||
{ label: '类型', prop: 'setType', width: 140 },
|
||||
{ label: '产品数', prop: 'productCount', width: 90, align: 'center' },
|
||||
{ label: '说明', prop: 'priceSetNote', minWidth: 140 },
|
||||
{ label: '状态', prop: 'status', width: 90 },
|
||||
{ label: '操作', prop: 'action', width: 200, showInToolbar: false }
|
||||
|
||||
Reference in New Issue
Block a user