Files
BMS/bms-front/src/views/price/label/create.vue
T

462 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>