462 lines
15 KiB
Vue
462 lines
15 KiB
Vue
<template>
|
||
<div class="app-container">
|
||
<div v-if="showBasic" class="create-section">
|
||
<step-basic-info ref="basicRef" v-model="form.basic" />
|
||
</div>
|
||
|
||
<div v-else class="create-section">
|
||
<el-alert
|
||
:title="pageBanner"
|
||
type="info"
|
||
:closable="false"
|
||
show-icon
|
||
/>
|
||
</div>
|
||
|
||
<div class="create-section">
|
||
<el-card shadow="never" class="step-card">
|
||
<template #header>
|
||
<div class="card-header">
|
||
<div>
|
||
<span>{{ cardTitle }}</span>
|
||
<span class="header-tip">{{ headerTip }}</span>
|
||
</div>
|
||
<div class="header-actions">
|
||
<el-button
|
||
type="danger"
|
||
plain
|
||
icon="Delete"
|
||
:disabled="!selectedProducts.length"
|
||
@click="removeSelectedProducts"
|
||
>批量删除</el-button>
|
||
<el-button type="primary" plain icon="Upload" @click="openImport" v-hasPermi="['price:label:import', 'price:label:add']">
|
||
导入价卡
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<el-alert
|
||
v-if="importMeta.sourceFile"
|
||
:title="`${importMeta.sourceFile} 归属 ${importMeta.supplierName || '-'} 生效 ${importMeta.effectiveDate || '-'} 已选 ${productRows.length} 个产品`"
|
||
type="info"
|
||
:closable="false"
|
||
show-icon
|
||
class="meta-alert"
|
||
/>
|
||
|
||
<el-table
|
||
ref="productTableRef"
|
||
v-loading="loadingProducts"
|
||
:data="productRows"
|
||
border
|
||
size="small"
|
||
max-height="420"
|
||
row-key="rowKey"
|
||
empty-text="请点击右上角「导入价卡」"
|
||
@selection-change="onProductSelect"
|
||
>
|
||
<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="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>
|
||
<el-button link type="danger" icon="Delete" @click="removeProduct(row)">删除</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</el-card>
|
||
</div>
|
||
|
||
<div class="wizard-actions">
|
||
<el-button @click="goBack">取消</el-button>
|
||
<el-button type="primary" :loading="saving" @click="finish">完成</el-button>
|
||
</div>
|
||
|
||
<label-import-dialog
|
||
ref="importRef"
|
||
:price-set-id="priceSetId"
|
||
:auto-fill-on-parse="true"
|
||
@confirm="onImportConfirm"
|
||
/>
|
||
|
||
<el-drawer v-model="detailVisible" :title="detailTitle" size="78%" destroy-on-close append-to-body @closed="onDetailClosed">
|
||
<pay-config-detail
|
||
:product="detailProduct"
|
||
:editable="true"
|
||
:saving-row-key="savingRowKey"
|
||
@save-row="saveDetailRow"
|
||
/>
|
||
</el-drawer>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup name="LabelPackageCreate">
|
||
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'
|
||
|
||
const { proxy } = getCurrentInstance()
|
||
const route = useRoute()
|
||
const basicRef = ref()
|
||
const importRef = ref()
|
||
const saving = ref(false)
|
||
const loadingProducts = ref(false)
|
||
const form = reactive({
|
||
basic: createEmptyBasicInfo()
|
||
})
|
||
const productRows = ref([])
|
||
const productTableRef = ref()
|
||
const selectedProducts = ref([])
|
||
const importMeta = reactive({
|
||
sourceFile: '',
|
||
supplierName: '',
|
||
effectiveDate: '',
|
||
overwrite: false
|
||
})
|
||
const detailVisible = ref(false)
|
||
const detailProduct = ref(null)
|
||
const detailRowKey = ref('')
|
||
const savingRowKey = ref('')
|
||
const detailTitle = computed(() => detailProduct.value?.productName || detailProduct.value?.sheetName || '产品明细')
|
||
|
||
const priceSetId = computed(() => {
|
||
const id = Number(route.query.priceSetId)
|
||
return Number.isFinite(id) && id > 0 ? id : null
|
||
})
|
||
const versionNo = computed(() => {
|
||
const no = Number(route.query.versionNo)
|
||
return Number.isFinite(no) && no > 0 ? no : null
|
||
})
|
||
const newVersionMode = computed(() => String(route.query.newVersion || '') === '1' && !!priceSetId.value)
|
||
const editVersionMode = computed(() => String(route.query.editVersion || '') === '1' && !!priceSetId.value && !!versionNo.value)
|
||
const showBasic = computed(() => !newVersionMode.value && !editVersionMode.value)
|
||
const cardTitle = computed(() => {
|
||
if (editVersionMode.value) return `编辑版本 V${versionNo.value}`
|
||
if (newVersionMode.value) return '新版本价卡'
|
||
return '产品价卡'
|
||
})
|
||
const pageBanner = computed(() => {
|
||
const name = form.basic.priceSetName || ''
|
||
const code = form.basic.priceSetCode || ''
|
||
if (editVersionMode.value) return `编辑版本 V${versionNo.value}:${name}(${code})`
|
||
return `新建版本:${name}(${code})`
|
||
})
|
||
const headerTip = computed(() => {
|
||
if (editVersionMode.value) return '可重新导入或修改明细;完成后覆盖本版本,不新增版本号'
|
||
if (newVersionMode.value) return '导入后生成待生效新版本;生效时间请回详情页设置'
|
||
return '打单价卡与应付独立;解析后回填到此表,可删除不需要的产品,编辑明细后点完成保存'
|
||
})
|
||
|
||
function goBack() {
|
||
const obj = (newVersionMode.value || editVersionMode.value) && priceSetId.value
|
||
? { path: '/customer/labelDetail', query: { priceSetId: priceSetId.value } }
|
||
: { path: '/customer/label' }
|
||
proxy.$tab.closeOpenPage(obj)
|
||
}
|
||
|
||
async function loadPackageHeader() {
|
||
if ((!newVersionMode.value && !editVersionMode.value) || !priceSetId.value) return
|
||
const res = await getLabelPackage(priceSetId.value)
|
||
const data = res.data || {}
|
||
form.basic.priceSetId = data.priceSetId
|
||
form.basic.priceSetCode = data.priceSetCode || ''
|
||
form.basic.priceSetName = data.priceSetName || ''
|
||
form.basic.priceSetNote = data.priceSetNote || ''
|
||
}
|
||
|
||
async function loadEditProducts() {
|
||
if (!editVersionMode.value || !priceSetId.value || !versionNo.value) return
|
||
loadingProducts.value = true
|
||
try {
|
||
const res = await listLabelProducts(priceSetId.value, versionNo.value)
|
||
const list = res.data || []
|
||
if (!list.length) {
|
||
productRows.value = []
|
||
return
|
||
}
|
||
const details = await Promise.all(list.map(async item => {
|
||
try {
|
||
const detailRes = await getLabelProduct(item.productId, 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 || ''}`,
|
||
weightCount: bands.length || detail.weightCount || 0,
|
||
surchargeCount: fees.length || detail.surchargeCount || 0,
|
||
extraCount: extras.length || detail.extraCount || 0,
|
||
parseStatus: bands.length + fees.length + extras.length === 0 ? 'EMPTY' : 'OK',
|
||
duplicated: true
|
||
}
|
||
} catch {
|
||
return {
|
||
...item,
|
||
rowKey: `${item.sheetName || ''}-${item.productName || ''}-${item.termId || item.productId || ''}`,
|
||
parseStatus: 'OK',
|
||
duplicated: true
|
||
}
|
||
}
|
||
}))
|
||
productRows.value = details
|
||
selectedProducts.value = []
|
||
importMeta.sourceFile = details[0]?.sourceFile || ''
|
||
importMeta.supplierName = details[0]?.supplierName || ''
|
||
importMeta.overwrite = true
|
||
} finally {
|
||
loadingProducts.value = false
|
||
}
|
||
}
|
||
|
||
async function openImport() {
|
||
importRef.value?.open()
|
||
}
|
||
|
||
function onProductSelect(rows) {
|
||
selectedProducts.value = rows || []
|
||
}
|
||
|
||
function productKey(row) {
|
||
return row?.rowKey || `${row?.sheetName || ''}-${row?.productName || ''}`
|
||
}
|
||
|
||
async function removeProduct(row) {
|
||
if (!row) return
|
||
try {
|
||
await proxy.$modal.confirm(`确认从本次导入中移除产品「${row.productName || row.sheetName || ''}」?`)
|
||
} catch {
|
||
return
|
||
}
|
||
const key = productKey(row)
|
||
productRows.value = productRows.value.filter(item => productKey(item) !== key)
|
||
selectedProducts.value = selectedProducts.value.filter(item => productKey(item) !== key)
|
||
if (detailRowKey.value === key) {
|
||
detailVisible.value = false
|
||
}
|
||
proxy.$modal.msgSuccess('已删除')
|
||
}
|
||
|
||
async function removeSelectedProducts() {
|
||
const rows = selectedProducts.value || []
|
||
if (!rows.length) {
|
||
proxy.$modal.msgWarning('请先勾选要删除的产品')
|
||
return
|
||
}
|
||
try {
|
||
await proxy.$modal.confirm(`确认删除所选 ${rows.length} 个产品?`)
|
||
} catch {
|
||
return
|
||
}
|
||
const keys = new Set(rows.map(productKey))
|
||
productRows.value = productRows.value.filter(item => !keys.has(productKey(item)))
|
||
selectedProducts.value = []
|
||
if (keys.has(detailRowKey.value)) {
|
||
detailVisible.value = false
|
||
}
|
||
proxy.$modal.msgSuccess('已批量删除')
|
||
}
|
||
|
||
function onImportConfirm(payload) {
|
||
productRows.value = payload?.products || []
|
||
selectedProducts.value = []
|
||
importMeta.sourceFile = payload?.sourceFile || ''
|
||
importMeta.supplierName = payload?.supplierName || ''
|
||
importMeta.effectiveDate = payload?.effectiveDate || ''
|
||
importMeta.overwrite = editVersionMode.value ? true : !!payload?.overwrite
|
||
}
|
||
|
||
function openDetail(row) {
|
||
if (!row) return
|
||
savingRowKey.value = ''
|
||
detailRowKey.value = row.rowKey || `${row.sheetName || ''}-${row.productName || ''}`
|
||
detailProduct.value = JSON.parse(JSON.stringify(row))
|
||
detailVisible.value = true
|
||
}
|
||
|
||
function onDetailClosed() {
|
||
// 关闭抽屉时回写勾选结果(不依赖行内「保存」)
|
||
if (detailProduct.value && detailRowKey.value) {
|
||
applyEditedProduct()
|
||
}
|
||
detailProduct.value = null
|
||
detailRowKey.value = ''
|
||
savingRowKey.value = ''
|
||
}
|
||
|
||
function applyEditedProduct() {
|
||
const edited = detailProduct.value
|
||
if (!edited) return false
|
||
const key = detailRowKey.value
|
||
const idx = productRows.value.findIndex(item => {
|
||
const itemKey = item.rowKey || `${item.sheetName || ''}-${item.productName || ''}`
|
||
return itemKey === key
|
||
})
|
||
if (idx < 0) {
|
||
proxy.$modal.msgError('未找到对应产品行')
|
||
return false
|
||
}
|
||
const bands = edited.weightBands || []
|
||
const fees = edited.surcharges || []
|
||
const extras = edited.extraLines || []
|
||
productRows.value.splice(idx, 1, {
|
||
...edited,
|
||
rowKey: productRows.value[idx].rowKey || key,
|
||
weightCount: bands.length,
|
||
surchargeCount: fees.length,
|
||
extraCount: extras.length,
|
||
parseStatus: bands.length + fees.length + extras.length === 0 ? 'EMPTY' : (edited.parseStatus || 'OK')
|
||
})
|
||
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) {
|
||
if (status === 'OK') return 'success'
|
||
if (status === 'EMPTY') return 'warning'
|
||
return 'danger'
|
||
}
|
||
|
||
function statusText(status) {
|
||
if (status === 'OK') return '已识别'
|
||
if (status === 'EMPTY') return '未识别到配置'
|
||
if (status === 'MISSING') return '缺少工作表'
|
||
return status || '-'
|
||
}
|
||
|
||
async function finish() {
|
||
if (showBasic.value) {
|
||
const ok = await basicRef.value?.validate()
|
||
if (!ok) {
|
||
proxy.$modal.msgWarning('请先完善基本信息')
|
||
return
|
||
}
|
||
} else if (!priceSetId.value) {
|
||
proxy.$modal.msgWarning(editVersionMode.value ? '缺少套餐编号,无法编辑版本' : '缺少套餐编号,无法新建版本')
|
||
return
|
||
}
|
||
if (!productRows.value.length) {
|
||
proxy.$modal.msgWarning('请先导入并确认价卡产品')
|
||
return
|
||
}
|
||
// 抽屉未关闭时先回写勾选,避免丢失
|
||
if (detailVisible.value && detailProduct.value && detailRowKey.value) {
|
||
applyEditedProduct()
|
||
}
|
||
const tip = editVersionMode.value
|
||
? `将覆盖版本 V${versionNo.value} 的 ${productRows.value.length} 个产品,确认继续?`
|
||
: newVersionMode.value
|
||
? `将为套餐「${form.basic.priceSetName || ''}」新建待生效版本并保存 ${productRows.value.length} 个产品,确认继续?`
|
||
: `将创建打单套餐并保存 ${productRows.value.length} 个应收产品,确认继续?`
|
||
try {
|
||
await proxy.$modal.confirm(tip)
|
||
} catch {
|
||
return
|
||
}
|
||
saving.value = true
|
||
try {
|
||
const payload = {
|
||
overwrite: editVersionMode.value ? true : importMeta.overwrite,
|
||
products: productRows.value
|
||
}
|
||
if (editVersionMode.value) {
|
||
payload.priceSetId = priceSetId.value
|
||
payload.versionNo = versionNo.value
|
||
} else if (newVersionMode.value) {
|
||
payload.priceSetId = priceSetId.value
|
||
} else {
|
||
payload.priceSetName = form.basic.priceSetName
|
||
payload.priceSetNote = form.basic.priceSetNote
|
||
}
|
||
const res = await saveLabelImport(payload)
|
||
const data = res.data || {}
|
||
ElMessageBox.close()
|
||
proxy.$modal.msgSuccess(data.message || res.msg || '保存成功')
|
||
const id = data.priceSetId || priceSetId.value
|
||
if (id) {
|
||
const obj = { path: '/customer/labelDetail', query: { priceSetId: id } }
|
||
proxy.$tab.closeOpenPage(obj)
|
||
} else {
|
||
goBack()
|
||
}
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
watch([priceSetId, versionNo, () => route.query.editVersion, () => route.query.newVersion], async () => {
|
||
await loadPackageHeader()
|
||
await loadEditProducts()
|
||
}, { immediate: true })
|
||
</script>
|
||
|
||
<style scoped>
|
||
.create-section {
|
||
margin-bottom: 16px;
|
||
}
|
||
.card-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
width: 100%;
|
||
}
|
||
.header-tip {
|
||
margin-left: 12px;
|
||
font-size: 12px;
|
||
color: #909399;
|
||
font-weight: normal;
|
||
}
|
||
.header-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
.meta-alert {
|
||
margin-bottom: 12px;
|
||
}
|
||
.wizard-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 12px;
|
||
padding: 8px 0 24px;
|
||
}
|
||
</style>
|