Files
BMS/bms-front/src/views/price/basic/pay/components/PayImportDialog.vue
T
2026-11-10 15:21:03 +08:00

363 lines
11 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>
<el-dialog v-model="visible" title="导入应付价卡" width="1080px" append-to-body destroy-on-close @closed="reset">
<el-form label-width="88px" class="import-form">
<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
:key="uploadKey"
drag
accept=".xlsx"
:auto-upload="false"
:show-file-list="false"
:disabled="recognizing"
@change="onFile"
>
<el-icon class="upload-icon"><UploadFilled /></el-icon>
<div class="el-upload__text">将 xlsx 价卡拖到此处,或<em>点击选择</em></div>
<template #tip>
<div class="upload-tip">选一个产品就只读对应工作表。不选时,一个文件里有多张表,只读取名称能对上字典的工作表。</div>
</template>
</el-upload>
<div v-if="recognizing" class="parse-progress">
<el-progress
:percentage="parsePercent"
:stroke-width="16"
:status="parseStatus"
striped
striped-flow
:duration="12"
/>
<div class="parse-progress-msg">{{ parseMessage }}</div>
</div>
<div v-if="preview" v-loading="recognizing" class="preview-box">
<el-alert
:title="`${preview.sourceFile || ''} 归属 ${preview.supplierName || '-'} 生效 ${preview.effectiveDate || '-'} 共 ${products.length} 个产品`"
type="info"
:closable="false"
show-icon
/>
<div class="preview-bar">
<el-switch v-model="overwrite" active-text="已存在时另存为新版本" />
<span class="preview-hint">未开启时,已存在的产品不能勾选,保存时会跳过。开启后产品编号不变,旧价格留在历史里,新价格从今天起生效。</span>
</div>
<el-table
ref="tableRef"
:data="products"
border
size="small"
max-height="280"
row-key="rowKey"
@selection-change="onSelect"
>
<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="countryScope" label="国家/口岸" min-width="110" show-overflow-tooltip />
<el-table-column prop="relatedSheet" label="关联分区表" min-width="120" show-overflow-tooltip />
<el-table-column label="生效日期" width="110">
<template #default="{ row }">{{ row.effectiveDate || preview.effectiveDate || '-' }}</template>
</el-table-column>
<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>
<div v-if="skippedText" class="upload-tip">未匹配字典、已跳过:{{ skippedText }}</div>
<div class="sheet-scroll">
<button
v-for="item in products"
:key="item.rowKey"
type="button"
class="sheet-tab"
:class="{ active: activeKey === item.rowKey }"
@click="activeKey = item.rowKey"
>{{ item.sheetName || item.productName }}</button>
</div>
<div class="preview-detail">
<pay-config-detail v-if="activeProduct" :product="activeProduct" />
</div>
</div>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :disabled="!selected.length" :loading="saving" @click="save">保存</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { ElMessageBox } from 'element-plus'
import { UploadFilled } from '@element-plus/icons-vue'
import { previewPay, payParseProgress, savePay } from '@/api/price/pay'
import { useDict } from '@/utils/dict'
import PayConfigDetail from './PayConfigDetail.vue'
const emit = defineEmits(['saved'])
const { proxy } = getCurrentInstance()
const { payment_product_type } = useDict('payment_product_type')
const visible = ref(false)
const recognizing = ref(false)
const saving = ref(false)
const overwrite = ref(false)
const dictValue = ref('')
const preview = ref(null)
const selected = ref([])
const tableRef = ref()
const activeKey = ref('')
const uploadKey = ref(0)
const parsePercent = ref(0)
const parseMessage = ref('')
const parseStatus = ref(undefined)
const products = computed(() => preview.value?.products || [])
const activeProduct = computed(() => products.value.find(item => item.rowKey === activeKey.value) || products.value[0])
const skippedText = computed(() => (preview.value?.skippedSheets || []).filter(Boolean).join('、'))
let parseSeq = 0
let progressTimer = null
function open() {
reset()
visible.value = true
}
function reset() {
parseSeq += 1
preview.value = null
selected.value = []
overwrite.value = false
dictValue.value = ''
activeKey.value = ''
recognizing.value = false
saving.value = false
parsePercent.value = 0
parseMessage.value = ''
parseStatus.value = undefined
stopProgress()
uploadKey.value += 1
}
async function onFile(uploadFile) {
const raw = uploadFile?.raw
if (!raw) return
if (!raw.name.toLowerCase().endsWith('.xlsx')) {
proxy.$modal.msgError('请上传xlsx价卡')
return
}
const seq = ++parseSeq
const progressId = createProgressId()
recognizing.value = true
parsePercent.value = 0
parseMessage.value = '正在上传价卡…'
parseStatus.value = undefined
startProgress(seq, progressId)
try {
const res = await previewPay(raw, dictValue.value, progressId, event => {
if (seq !== parseSeq || !event?.total) return
const uploaded = Math.min(8, Math.round(event.loaded * 8 / event.total))
if (uploaded > parsePercent.value) {
parsePercent.value = uploaded
parseMessage.value = '正在上传价卡…'
}
})
if (seq !== parseSeq) return
parsePercent.value = 100
parseMessage.value = '解析完成'
parseStatus.value = 'success'
const data = res.data || {}
data.products = (data.products || []).map((item, index) => ({
...item,
rowKey: `${item.sheetName || ''}-${item.productName || ''}-${index}`
}))
preview.value = data
activeKey.value = data.products[0]?.rowKey || ''
const picked = data.products.filter(row => canSelect(row))
selected.value = picked
await nextTick()
tableRef.value?.clearSelection()
picked.forEach(row => tableRef.value?.toggleRowSelection(row, true))
} catch (error) {
if (seq !== parseSeq) return
parseStatus.value = 'exception'
parseMessage.value = error?.message || '价卡解析失败'
} finally {
if (seq === parseSeq) {
stopProgress()
recognizing.value = false
}
}
}
function createProgressId() {
if (window.crypto?.randomUUID) return window.crypto.randomUUID()
return `pay${Date.now().toString(16)}${Math.random().toString(16).slice(2, 10)}`
}
function startProgress(seq, progressId) {
stopProgress()
progressTimer = window.setInterval(async () => {
if (seq !== parseSeq) return
try {
const res = await payParseProgress(progressId)
if (seq !== parseSeq) return
const data = res.data || {}
if (!data.status || data.status === 'PENDING') return
const server = Math.min(99, Number(data.percent) || 0)
if (server >= parsePercent.value) {
parsePercent.value = server
if (data.message) parseMessage.value = data.message
}
if (data.status === 'FAIL') parseStatus.value = 'exception'
} catch {
// 进度查询失败时继续等预览结果
}
}, 400)
}
function stopProgress() {
if (progressTimer) {
window.clearInterval(progressTimer)
progressTimer = null
}
}
function canSelect(row) {
if (row.parseStatus === 'MISSING') return false
return overwrite.value || !row.duplicated
}
watch(overwrite, async () => {
if (!visible.value || !preview.value) return
const picked = products.value.filter(row => canSelect(row))
selected.value = picked
await nextTick()
tableRef.value?.clearSelection()
picked.forEach(row => tableRef.value?.toggleRowSelection(row, true))
})
function onSelect(rows) {
if (!visible.value) return
selected.value = rows || []
}
async function save() {
if (!selected.value.length) {
proxy.$modal.msgError('请勾选要保存的产品')
return
}
const duplicated = selected.value.filter(row => row.duplicated).length
const tip = overwrite.value && duplicated
? `将保存 ${selected.value.length} 个产品,其中 ${duplicated} 个会保留旧版本并生成新版本。`
: `将保存 ${selected.value.length} 个产品。`
try {
await proxy.$modal.confirm(tip)
} catch {
return
}
saving.value = true
try {
const res = await savePay({ overwrite: overwrite.value, products: selected.value })
const message = res.msg || '保存成功'
reset()
visible.value = false
ElMessageBox.close()
proxy.$modal.msgSuccess(message)
emit('saved')
} finally {
saving.value = false
}
}
function statusText(status) {
return { OK: '已识别', EMPTY: '未识别到配置', MISSING: '缺少工作表' }[status] || status || '-'
}
function statusType(status) {
if (status === 'OK') return 'success'
if (status === 'EMPTY') return 'warning'
if (status === 'MISSING') return 'danger'
return 'info'
}
defineExpose({ open })
</script>
<style scoped>
.upload-icon {
font-size: 42px;
color: var(--el-text-color-secondary);
}
.upload-tip {
margin-top: 6px;
color: var(--el-text-color-secondary);
font-size: 12px;
}
.preview-box {
margin-top: 14px;
}
.preview-bar {
display: flex;
align-items: center;
gap: 12px;
margin: 10px 0;
}
.preview-hint {
color: var(--el-text-color-secondary);
font-size: 12px;
}
.preview-detail {
max-height: 460px;
margin-top: 14px;
overflow: auto;
}
.import-form {
margin-bottom: 8px;
}
.parse-progress {
margin-top: 14px;
}
.parse-progress-msg {
margin-top: 8px;
color: var(--el-text-color-regular);
font-size: 13px;
}
.sheet-scroll {
display: flex;
gap: 8px;
margin-top: 12px;
overflow-x: auto;
padding-bottom: 6px;
}
.sheet-tab {
flex: 0 0 auto;
border: 1px solid var(--el-border-color);
background: #fff;
border-radius: 4px;
padding: 6px 12px;
cursor: pointer;
white-space: nowrap;
}
.sheet-tab.active {
color: var(--el-color-primary);
border-color: var(--el-color-primary);
background: var(--el-color-primary-light-9);
}
</style>