应付账单初始化

This commit is contained in:
caojia
2026-11-04 16:04:38 +08:00
committed by liuminghui
parent 04be15deab
commit 51a89b18e1
90 changed files with 15661 additions and 6 deletions
+53
View File
@@ -0,0 +1,53 @@
import request from '@/utils/request'
/** 应付邮编配置:导入整本 Excel,列表只展示合并后的分区和附加费 */
export function listPayPostcode(query) {
return request({
url: '/data/payPostcode/list',
method: 'get',
params: query
})
}
export function getPayPostcode(groupId) {
return request({
url: '/data/payPostcode/' + groupId,
method: 'get'
})
}
export function updatePayPostcode(data) {
return request({
url: '/data/payPostcode',
method: 'put',
data
})
}
export function listPayPostcodeLine(query) {
return request({
url: '/data/payPostcode/line',
method: 'get',
params: query
})
}
export function importPayPostcode(file) {
const data = new FormData()
data.append('file', file)
return request({
url: '/data/payPostcode/import',
method: 'post',
data,
headers: { 'Content-Type': 'multipart/form-data', repeatSubmit: false },
timeout: 10 * 60 * 1000
})
}
export function delPayPostcode(groupIds) {
return request({
url: '/data/payPostcode/' + groupIds,
method: 'delete'
})
}
+149
View File
@@ -0,0 +1,149 @@
import request from '@/utils/request'
// 应付价卡配置。预览不入库,保存才写入 pay_ 表。
/** 分页查询已保存的应付产品 */
export function listPay(query) {
return request({
url: '/price/pay/list',
method: 'get',
params: query
})
}
/** 产品详情,含重量段、附加费和其他配置 */
export function getPay(productId) {
return request({
url: '/price/pay/' + productId,
method: 'get'
})
}
/** 按字典匹配工作表,返回预览,不写数据库 */
export function previewPay(file, dictValue) {
const data = new FormData()
data.append('file', file)
if (dictValue) data.append('dictValue', dictValue)
return request({
url: '/price/pay/preview',
method: 'post',
data,
// 必须声明 multipart,否则全局 application/json 会把文件流改写成空 JSON
headers: { 'Content-Type': 'multipart/form-data', repeatSubmit: false },
timeout: 10 * 60 * 1000
})
}
/** 修改已保存的公共模板 */
export function updatePay(data) {
return request({
url: '/price/pay',
method: 'put',
data
})
}
/** 已导入模板,供渠道下拉。payType 默认应付 AP */
export function payOptions(payType = 'AP') {
return request({
url: '/price/pay/options',
method: 'get',
params: { payType }
})
}
export function bindChannelPay(channelId, productId, payType = 'AP') {
return request({
url: '/price/pay/channel/' + channelId + '/bind',
method: 'put',
data: { productId, payType }
})
}
export function comparePay(leftId, rightId) {
return request({
url: '/price/pay/compare',
method: 'get',
params: { leftId, rightId }
})
}
export function getChannelPay(channelId) {
return request({
url: '/price/pay/channel/' + channelId,
method: 'get'
})
}
export function saveChannelPay(channelId, data) {
return request({
url: '/price/pay/channel/' + channelId,
method: 'post',
data
})
}
export function reimportChannelPay(channelId, file) {
const data = new FormData()
data.append('file', file)
return request({
url: '/price/pay/channel/' + channelId + '/reimport',
method: 'post',
data,
headers: { 'Content-Type': 'multipart/form-data', repeatSubmit: false },
timeout: 10 * 60 * 1000
})
}
/** 保存预览里勾选的产品 */
export function savePay(data) {
return request({
url: '/price/pay/save',
method: 'post',
data,
timeout: 120000
})
}
/** 一个产品的全部价格版本 */
export function listPayTerm(productId) {
return request({
url: '/price/pay/term/list',
method: 'get',
params: { productId }
})
}
/** 修改某一版的生效起止 */
export function updatePayTerm(data) {
return request({
url: '/price/pay/term',
method: 'put',
data
})
}
/** 生效或失效 */
export function changePayTermStatus(data) {
return request({
url: '/price/pay/term/status',
method: 'put',
data
})
}
/** 按订单渠道上的应付产品生成应付账单 */
export function generatePayBill(billId) {
return request({
url: '/price/pay/order/' + billId,
method: 'post'
})
}
/** 删除产品及其重量段、附加费、其他配置 */
export function delPay(productIds) {
return request({
url: '/price/pay/' + productIds,
method: 'delete'
})
}
+29
View File
@@ -0,0 +1,29 @@
import request from '@/utils/request'
export function listPayBill(query) {
return request({ url: '/price/payBill/list', method: 'get', params: query })
}
export function getPayBill(billId) {
return request({ url: '/price/payBill/' + billId, method: 'get' })
}
export function previewPayBill(data) {
return request({ url: '/price/payBill/preview', method: 'post', data })
}
export function generatePayBill(data) {
return request({ url: '/price/payBill', method: 'post', data, timeout: 10 * 60 * 1000 })
}
export function confirmPayBill(billId) {
return request({ url: '/price/payBill/confirm/' + billId, method: 'put' })
}
export function payPayBill(billId) {
return request({ url: '/price/payBill/paid/' + billId, method: 'put' })
}
export function voidPayBill(data) {
return request({ url: '/price/payBill/void', method: 'put', data })
}
+22 -3
View File
@@ -25,7 +25,7 @@
</slot>
</el-form-item>
</template>
<el-form-item v-if="$slots.default" :label-width="actionLabelWidth" :class="actionAlign === 'center' ? 'basic-form-action--center' : ''">
<el-form-item v-if="$slots.default" :label-width="actionLabelWidth" :class="actionClass">
<slot />
</el-form-item>
</template>
@@ -51,7 +51,7 @@
</el-form-item>
</el-col>
<el-col :span="actionSpan" v-if="$slots.default">
<el-form-item :label-width="actionLabelWidth" :class="actionAlign === 'center' ? 'basic-form-action--center' : ''">
<el-form-item :label-width="actionLabelWidth" :class="actionClass">
<slot />
</el-form-item>
</el-col>
@@ -159,13 +159,19 @@ const props = defineProps({
type: [String, Number],
default: '0'
},
// 操作区(默认插槽)对齐方式:left / center(center 时独占一行并水平居中)
// 操作区(默认插槽)对齐方式:left / center / right
actionAlign: {
type: String,
default: 'left'
}
})
const actionClass = computed(() => {
if (props.actionAlign === 'center') return 'basic-form-action--center'
if (props.actionAlign === 'right') return 'basic-form-action--right'
return ''
})
const { proxy } = getCurrentInstance()
const formRef = ref()
@@ -276,4 +282,17 @@ defineExpose({
display: flex;
justify-content: center;
}
/* 操作区靠右:独占整行,按钮贴在右侧 */
.basic-form--perrow .basic-form-action--right {
grid-column: 1 / -1;
}
.basic-form .basic-form-action--right {
width: 100%;
flex: 0 0 100%;
margin-right: 0;
}
.basic-form-action--right :deep(.el-form-item__content) {
display: flex;
justify-content: flex-end;
}
</style>
@@ -208,7 +208,7 @@ const props = defineProps({
type: [String, Number],
default: 'auto'
},
// 搜索按钮区对齐:left / center(搜索项较多按钮换行时可用 center 让按钮居中)
// 搜索按钮区对齐:left / center / right
searchActionAlign: {
type: String,
default: 'left'
@@ -0,0 +1,280 @@
<template>
<div class="app-container">
<basic-table
ref="tableRef"
:columns="columns"
:search-columns="searchColumns"
:query="queryList"
row-key="groupId"
selection
search-action-align="right"
>
<template #toolbar="{ ids, selection }">
<el-col :span="1.5">
<el-button type="primary" plain icon="Upload" @click="open = true" v-hasPermi="['data:payPostcode:import']">导入</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="Edit" :disabled="selection.length !== 1" @click="handleUpdate(selection[0])" v-hasPermi="['data:payPostcode:edit']">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="!ids.length" @click="handleDelete(ids)" v-hasPermi="['data:payPostcode:remove']">删除</el-button>
</el-col>
</template>
<template #col-configKind="{ row }">
<el-tag size="small" :type="row.configKind === 'FEE' ? 'warning' : 'success'">{{ row.configKind === 'FEE' ? '附加费' : '邮编分区' }}</el-tag>
</template>
<template #col-coverage="{ row }">
<el-tag v-if="row.coverage" size="small" :type="coverageType(row.coverage)">{{ coverageLabel(row.coverage) }}</el-tag>
</template>
<template #col-action="{ row }">
<el-button link type="primary" @click="openLines(row)" v-hasPermi="['data:payPostcode:query']">邮编</el-button>
<el-button link type="primary" icon="Edit" @click="handleUpdate(row)" v-hasPermi="['data:payPostcode:edit']">修改</el-button>
<el-button link type="danger" @click="handleDelete([row.groupId])" v-hasPermi="['data:payPostcode:remove']">删除</el-button>
</template>
</basic-table>
<el-dialog v-model="open" title="导入应付邮编" width="480px" append-to-body destroy-on-close>
<p class="import-tip">表头里带邮编或附加费的工作表会被导入。目录里的分区表会带上服务类型、国家、销售产品和报价表。可达、可派记为可达,不可达记为不可达,偏远和岛屿记为偏远。渠道名称从标题里识别,例如 Royal Mail、EVRI。邮编末尾的 * 会去掉,同一国家、同一渠道、同一附加费、金额相同只保留一条。</p>
<el-upload :auto-upload="false" :limit="1" accept=".xlsx" :on-change="onFile" :on-remove="() => file = null">
<el-button type="primary" plain>选择 xlsx</el-button>
</el-upload>
<template #footer>
<el-button type="primary" :loading="importing" @click="submitImport">开始导入</el-button>
<el-button @click="open = false">取消</el-button>
</template>
</el-dialog>
<el-dialog title="修改应付邮编" v-model="editOpen" width="560px" append-to-body destroy-on-close>
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="服务类型" prop="serviceType">
<el-input v-model="form.serviceType" maxlength="100" placeholder="目录中的服务类型" />
</el-form-item>
<el-form-item label="目录国家" prop="catalogCountry">
<el-input v-model="form.catalogCountry" maxlength="100" placeholder="目录中的国家" />
</el-form-item>
<el-form-item label="销售产品" prop="productName">
<el-input v-model="form.productName" maxlength="200" placeholder="目录中的销售产品" />
</el-form-item>
<el-form-item label="报价表" prop="quoteSheet">
<el-input v-model="form.quoteSheet" maxlength="200" placeholder="目录中的报价工作表" />
</el-form-item>
<el-form-item label="渠道名称" prop="channelName">
<el-input v-model="form.channelName" maxlength="100" placeholder="如 Royal Mail、EVRI" />
</el-form-item>
<el-form-item label="类型" prop="configKind">
<el-select v-model="form.configKind" style="width: 100%">
<el-option v-for="item in kindOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="邮编属性" prop="coverage">
<el-select v-model="form.coverage" clearable placeholder="普通分区" style="width: 100%">
<el-option v-for="item in coverageOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="国家" prop="countryName">
<el-input v-model="form.countryName" maxlength="100" />
</el-form-item>
<el-form-item label="国家二字码" prop="countryCode">
<el-input v-model="form.countryCode" maxlength="20" />
</el-form-item>
<el-form-item label="附加费" prop="feeName">
<el-input v-model="form.feeName" maxlength="200" />
</el-form-item>
<el-form-item label="金额原文" prop="feeText">
<el-input v-model="form.feeText" maxlength="200" />
</el-form-item>
<el-form-item label="区块标题" prop="blockTitle">
<el-input v-model="form.blockTitle" maxlength="200" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="2" maxlength="500" />
</el-form-item>
</el-form>
<template #footer>
<el-button type="primary" @click="submitEdit">确定</el-button>
<el-button @click="editOpen = false">取消</el-button>
</template>
</el-dialog>
<el-drawer v-model="lineOpen" :title="lineTitle" size="72%" append-to-body>
<el-input v-model="lineQuery.postcode" clearable placeholder="筛选邮编" class="line-filter" @change="loadLines" />
<el-table :data="lineRows" border size="small" max-height="640" empty-text="没有邮编">
<el-table-column prop="countryName" label="国家" width="90" show-overflow-tooltip />
<el-table-column prop="regionName" label="地区" min-width="140" show-overflow-tooltip />
<el-table-column prop="regionNameCn" label="地区中文" min-width="120" show-overflow-tooltip />
<el-table-column prop="cityName" label="城市" min-width="120" show-overflow-tooltip />
<el-table-column prop="regionCode" label="地区代码" width="100" show-overflow-tooltip />
<el-table-column prop="zoneName" label="分区" min-width="140" show-overflow-tooltip />
<el-table-column prop="postcode" label="邮编" min-width="120" show-overflow-tooltip />
<el-table-column prop="postcodeRaw" label="邮编原文" min-width="120" show-overflow-tooltip />
<el-table-column prop="postcodeEnd" label="截止邮编" width="120" show-overflow-tooltip />
<el-table-column prop="extraText" label="其余列" min-width="180" show-overflow-tooltip />
</el-table>
<el-pagination
v-if="lineTotal > lineQuery.pageSize"
class="line-page"
layout="total, prev, pager, next"
:total="lineTotal"
:page-size="lineQuery.pageSize"
v-model:current-page="lineQuery.pageNum"
@current-change="loadLines"
/>
</el-drawer>
</div>
</template>
<script setup name="PayPostcode">
import { listPayPostcode, getPayPostcode, updatePayPostcode, listPayPostcodeLine, importPayPostcode, delPayPostcode } from '@/api/data/payPostcode'
const { proxy } = getCurrentInstance()
const tableRef = ref()
const formRef = ref()
const open = ref(false)
const editOpen = ref(false)
const importing = ref(false)
const file = ref(null)
const lineOpen = ref(false)
const lineTitle = ref('邮编')
const lineRows = ref([])
const lineTotal = ref(0)
const lineQuery = reactive({ groupId: undefined, postcode: '', pageNum: 1, pageSize: 20 })
const kindOptions = [
{ label: '附加费', value: 'FEE' },
{ label: '邮编分区', value: 'ZONE' }
]
const coverageOptions = [
{ label: '可达', value: 'REACH' },
{ label: '不可达', value: 'UNREACH' },
{ label: '偏远', value: 'REMOTE' }
]
function coverageLabel(value) {
return coverageOptions.find(item => item.value === value)?.label || ''
}
function coverageType(value) {
if (value === 'UNREACH') return 'danger'
if (value === 'REMOTE') return 'warning'
return 'success'
}
const searchColumns = [
{ label: '销售产品', prop: 'productName', component: 'input', placeholder: '销售产品', width: '140px' },
{ label: '报价表', prop: 'quoteSheet', component: 'input', placeholder: '报价表', width: '160px' },
{ label: '来源文件', prop: 'sourceFile', component: 'input', placeholder: '文件名', width: '180px' },
{ label: '工作表', prop: 'sheetName', component: 'input', placeholder: '工作表', width: '160px' },
{ label: '国家', prop: 'countryName', component: 'input', placeholder: '国家或二字码', width: '140px' },
{ label: '渠道', prop: 'channelName', component: 'input', placeholder: '渠道名称', width: '140px' },
{ label: '类型', prop: 'configKind', component: 'select', options: kindOptions, placeholder: '全部', width: '130px', props: { clearable: true } },
{ label: '邮编属性', prop: 'coverage', component: 'select', options: coverageOptions, placeholder: '全部', width: '130px', props: { clearable: true } },
{ label: '附加费', prop: 'feeName', component: 'input', placeholder: '附加费名称', width: '160px' },
{ label: '邮编', prop: 'postcode', component: 'input', placeholder: '邮编', width: '140px' }
]
const columns = [
{ label: '类型', prop: 'configKind', width: 100, align: 'center', slot: 'configKind' },
{ label: '邮编属性', prop: 'coverage', width: 100, align: 'center', slot: 'coverage' },
{ label: '服务类型', prop: 'serviceType', width: 100, showOverflowTooltip: true },
{ label: '目录国家', prop: 'catalogCountry', width: 100, showOverflowTooltip: true },
{ label: '销售产品', prop: 'productName', minWidth: 120, showOverflowTooltip: true },
{ label: '报价表', prop: 'quoteSheet', minWidth: 140, showOverflowTooltip: true },
{ label: '渠道名称', prop: 'channelName', minWidth: 120, showOverflowTooltip: true },
{ label: '国家', prop: 'countryName', width: 90, showOverflowTooltip: true },
{ label: '工作表', prop: 'sheetName', minWidth: 140, showOverflowTooltip: true },
{ label: '区块', prop: 'blockTitle', minWidth: 180, showOverflowTooltip: true },
{ label: '附加费', prop: 'feeName', minWidth: 140, showOverflowTooltip: true },
{ label: '金额原文', prop: 'feeText', minWidth: 140, showOverflowTooltip: true },
{ label: '邮编行数', prop: 'lineCount', width: 100, align: 'right' },
{ label: '来源文件', prop: 'sourceFile', minWidth: 220, showOverflowTooltip: true },
{ label: '导入时间', prop: 'createTime', width: 170, align: 'center' },
{ label: '操作', prop: 'action', width: 170, align: 'center', fixed: 'right', slot: 'action', showInToolbar: false }
]
const form = ref({})
const rules = {
configKind: [{ required: true, message: '请选择类型', trigger: 'change' }]
}
function queryList(params) {
return listPayPostcode(params)
}
function onFile(uploadFile) {
file.value = uploadFile.raw
}
function submitImport() {
if (!file.value) {
proxy.$modal.msgWarning('请选择 xlsx 文件')
return
}
importing.value = true
importPayPostcode(file.value).then(res => {
proxy.$modal.msgSuccess(res.data?.message || res.msg || '导入完成')
open.value = false
file.value = null
tableRef.value?.reload?.() || tableRef.value?.getList?.()
}).finally(() => {
importing.value = false
})
}
function handleUpdate(row) {
getPayPostcode(row.groupId).then(res => {
form.value = Object.assign({}, res.data || {})
editOpen.value = true
})
}
function submitEdit() {
formRef.value.validate(valid => {
if (!valid) return
updatePayPostcode(form.value).then(() => {
proxy.$modal.msgSuccess('保存成功')
editOpen.value = false
tableRef.value?.reload?.()
})
})
}
function openLines(row) {
lineTitle.value = `${coverageLabel(row.coverage)} ${row.countryName || ''} ${row.feeName || row.blockTitle || row.sheetName}`.trim()
lineQuery.groupId = row.groupId
lineQuery.postcode = ''
lineQuery.pageNum = 1
lineOpen.value = true
loadLines()
}
function loadLines() {
listPayPostcodeLine(lineQuery).then(res => {
lineRows.value = res.rows || []
lineTotal.value = res.total || 0
})
}
function handleDelete(ids) {
proxy.$modal.confirm('确认删除选中的邮编配置吗?邮编明细会一起删除。').then(() => delPayPostcode(ids)).then(() => {
proxy.$modal.msgSuccess('删除成功')
tableRef.value?.reload?.() || tableRef.value?.getList?.()
}).catch(() => {})
}
</script>
<style scoped>
.import-tip {
margin: 0 0 12px;
color: var(--el-text-color-secondary);
line-height: 1.5;
}
.line-filter {
width: 240px;
margin-bottom: 8px;
}
.line-page {
margin-top: 8px;
justify-content: flex-end;
}
</style>
@@ -140,6 +140,14 @@
<el-input v-model="form.remarks" type="textarea" :rows="2" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="应付产品">
<el-select v-model="payOriginId" filterable clearable placeholder="选择已导入的应付产品" style="width: 280px" :disabled="!form.id" @change="onPayOrigin">
<el-option v-for="item in payOptions" :key="item.productId" :label="payOptionLabel(item)" :value="item.productId" />
</el-select>
<div class="pay-tip">{{ payTip }}</div>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
@@ -147,6 +155,7 @@
<el-button @click="open = false">取 消</el-button>
</template>
</el-dialog>
</div>
</template>
@@ -156,12 +165,22 @@ import {
listServiceChannel, getServiceChannel, addServiceChannel,
updateServiceChannel, delServiceChannel, changeServiceChannelStatus
} from '@/api/masterdata/serviceChannel'
import { payOptions as listPayOptions, getChannelPay, bindChannelPay } from '@/api/price/pay'
const { proxy } = getCurrentInstance()
const tableRef = ref()
const formRef = ref()
const open = ref(false)
const title = ref('')
const payOptions = ref([])
const payOriginId = ref()
const payProduct = ref(null)
const syncingPay = ref(false)
const payTip = computed(() => {
if (!form.value?.id) return '先保存渠道,再选择应付产品。渠道绑定的是产品编号,更新价格不会换编号。'
if (payProduct.value?.versionNo) return `当前按有效期使用第 ${payProduct.value.versionNo} 版。历史订单按下单或计费时间取当时的版本。`
return '选择产品后即绑定,按应付 AP 保存。'
})
const openOptions = [
{ label: '启用', value: '1' },
@@ -233,8 +252,42 @@ function emptyForm() {
}
}
function resetPay() {
syncingPay.value = true
payOriginId.value = undefined
payProduct.value = null
nextTick(() => {
syncingPay.value = false
})
}
function payOptionLabel(item) {
const version = item.versionNo > 1 ? `(当前第 ${item.versionNo} 版)` : ''
return `${item.productName}${version}`
}
function loadPayOptions() {
listPayOptions('AP').then(res => {
payOptions.value = res.data || []
})
}
function loadChannelPay(channelId) {
syncingPay.value = true
getChannelPay(channelId).then(res => {
payProduct.value = res.data || null
payOriginId.value = res.data?.originId || res.data?.productId
}).finally(() => {
nextTick(() => {
syncingPay.value = false
})
})
}
function handleAdd() {
form.value = emptyForm()
resetPay()
loadPayOptions()
title.value = '新增渠道'
open.value = true
nextTick(() => formRef.value && formRef.value.clearValidate())
@@ -243,12 +296,28 @@ function handleAdd() {
function handleUpdate(row) {
getServiceChannel(row.id).then(res => {
form.value = Object.assign(emptyForm(), res.data || {})
resetPay()
loadPayOptions()
loadChannelPay(row.id)
title.value = '修改渠道'
open.value = true
nextTick(() => formRef.value && formRef.value.clearValidate())
})
}
function onPayOrigin(id) {
if (!form.value.id || syncingPay.value) return
if (!id) {
payProduct.value = null
bindChannelPay(form.value.id, null, 'AP')
return
}
bindChannelPay(form.value.id, id, 'AP').then(res => {
payProduct.value = res.data || null
payOriginId.value = res.data?.originId || id
})
}
function submitForm() {
formRef.value.validate(valid => {
if (!valid) return
@@ -302,3 +371,12 @@ function handleExport() {
}, `service_channel_${new Date().getTime()}.xlsx`)
}
</script>
<style scoped>
.pay-tip {
margin-top: 6px;
color: var(--el-text-color-secondary);
font-size: 12px;
line-height: 1.5;
}
</style>
@@ -0,0 +1,341 @@
<template>
<div v-if="panes.length" class="pay-detail">
<section v-for="pane in panes" :key="pane.key" class="pay-product">
<div class="pay-product-title">{{ pane.item.productName || '未命名产品' }}</div>
<el-descriptions :column="3" border size="small" class="pay-summary">
<el-descriptions-item label="价卡归属">{{ pane.item.supplierName || '-' }}</el-descriptions-item>
<el-descriptions-item label="生效日期">{{ pane.item.effectiveDate || '-' }}</el-descriptions-item>
<el-descriptions-item label="国家/口岸">{{ pane.item.countryScope || '-' }}</el-descriptions-item>
<el-descriptions-item label="费用类型">{{ pane.item.chargeType || '-' }}</el-descriptions-item>
<el-descriptions-item label="每公斤加价">{{ pane.item.extraPerKg || '-' }}</el-descriptions-item>
<el-descriptions-item label="服务类型">{{ pane.item.serviceType || '-' }}</el-descriptions-item>
<el-descriptions-item label="分类">{{ pane.item.classification || '-' }}</el-descriptions-item>
<el-descriptions-item label="币种">{{ pane.item.currency || '-' }}</el-descriptions-item>
<el-descriptions-item label="报价工作表">{{ pane.item.sheetName || '-' }}</el-descriptions-item>
<el-descriptions-item label="关联分区表">{{ pane.item.relatedSheet || '-' }}</el-descriptions-item>
<el-descriptions-item label="识别结果">{{ statusText(pane.item.parseStatus) }}</el-descriptions-item>
<el-descriptions-item label="重量限制">{{ pane.item.weightLimit || '-' }}</el-descriptions-item>
<el-descriptions-item label="更新内容" :span="2">{{ pane.item.updateContent || '-' }}</el-descriptions-item>
<el-descriptions-item label="目录备注" :span="3">{{ pane.item.catalogRemark || '-' }}</el-descriptions-item>
<el-descriptions-item v-if="pane.item.remark" label="备注" :span="3">{{ pane.item.remark }}</el-descriptions-item>
</el-descriptions>
<el-tabs v-model="pane.tab">
<el-tab-pane :label="`重量段(${(pane.item.weightBands || []).length})`" name="band">
<el-input v-model="pane.bandKeyword" clearable placeholder="筛选国家、分区、重量段、费用项" class="pay-filter" @input="pane.bandPageNo = 1" />
<div v-if="editable" class="pay-edit-bar">
<el-button size="small" type="primary" plain @click="addBand(pane)">添加重量段</el-button>
</div>
<el-table :data="pageBands(pane)" border size="small" max-height="360" empty-text="没有重量段或起重配置">
<el-table-column prop="sectionName" label="区块" min-width="110" show-overflow-tooltip>
<template #default="{ row }"><cell-text v-model="row.sectionName" :edit="editable" /></template>
</el-table-column>
<el-table-column prop="countryName" label="国家" min-width="120" show-overflow-tooltip>
<template #default="{ row }"><cell-text v-model="row.countryName" :edit="editable" /></template>
</el-table-column>
<el-table-column prop="countryCode" label="代码" width="80" show-overflow-tooltip>
<template #default="{ row }"><cell-text v-model="row.countryCode" :edit="editable" /></template>
</el-table-column>
<el-table-column prop="zoneName" label="分区" min-width="140" show-overflow-tooltip>
<template #default="{ row }"><cell-text v-model="row.zoneName" :edit="editable" /></template>
</el-table-column>
<el-table-column prop="weightLabel" label="重量段" min-width="120" show-overflow-tooltip>
<template #default="{ row }"><cell-text v-model="row.weightLabel" :edit="editable" /></template>
</el-table-column>
<el-table-column label="下限KG" width="90" align="right">
<template #default="{ row }">{{ money(row.weightMin) }}</template>
</el-table-column>
<el-table-column label="上限KG" width="90" align="right">
<template #default="{ row }">{{ money(row.weightMax) }}</template>
</el-table-column>
<el-table-column prop="chargeName" label="费用项" min-width="120" show-overflow-tooltip>
<template #default="{ row }"><cell-text v-model="row.chargeName" :edit="editable" /></template>
</el-table-column>
<el-table-column label="单价" width="110" align="right">
<template #default="{ row }">
<el-input v-if="editable" v-model="row.price" size="small" />
<span v-else>{{ money(row.price) }}</span>
</template>
</el-table-column>
<el-table-column prop="priceUnit" label="单位" width="110" show-overflow-tooltip>
<template #default="{ row }"><cell-text v-model="row.priceUnit" :edit="editable" /></template>
</el-table-column>
<el-table-column v-if="editable" label="操作" width="70" align="center">
<template #default="{ row }">
<el-button link type="danger" @click="removeRow(pane.item.weightBands, row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-if="filteredBands(pane).length > pageSize"
class="pay-page"
layout="total, prev, pager, next"
:total="filteredBands(pane).length"
:page-size="pageSize"
v-model:current-page="pane.bandPageNo"
/>
</el-tab-pane>
<el-tab-pane :label="`附加费(${(pane.item.surcharges || []).length})`" name="fee">
<div class="pay-edit-bar">
<el-input v-model="pane.feeKeyword" clearable placeholder="筛选附加费用、地区、收费标准或费用解释" class="pay-filter" @input="pane.feePageNo = 1" />
<el-button v-if="editable" size="small" type="primary" plain @click="addFee(pane)">添加附加费</el-button>
</div>
<el-table :data="pageFees(pane)" border size="small" max-height="360" empty-text="没有附加费">
<el-table-column prop="feeName" label="附加费用" min-width="160" show-overflow-tooltip>
<template #default="{ row }"><cell-text v-model="row.feeName" :edit="editable" /></template>
</el-table-column>
<el-table-column prop="zoneName" label="地区" min-width="160" show-overflow-tooltip>
<template #default="{ row }"><cell-text v-model="row.zoneName" :edit="editable" /></template>
</el-table-column>
<el-table-column prop="chargeText" label="收费标准" min-width="180" show-overflow-tooltip>
<template #default="{ row }"><cell-text v-model="row.chargeText" :edit="editable" /></template>
</el-table-column>
<el-table-column prop="feeDesc" label="费用解释" min-width="240" show-overflow-tooltip>
<template #default="{ row }"><cell-text v-model="row.feeDesc" :edit="editable" /></template>
</el-table-column>
<el-table-column prop="calcRule" label="计费条件" width="140">
<template #default="{ row }">
<el-select v-if="editable" v-model="row.calcRule" size="small" placeholder="人工确认">
<el-option v-for="item in calcRules" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<span v-else>{{ calcRuleLabel(row.calcRule) }}</span>
</template>
</el-table-column>
<el-table-column prop="calcParam" label="条件参数" min-width="180" show-overflow-tooltip>
<template #default="{ row }"><cell-text v-model="row.calcParam" :edit="editable" /></template>
</el-table-column>
<el-table-column v-if="editable" label="操作" width="70" align="center">
<template #default="{ row }">
<el-button link type="danger" @click="removeRow(pane.item.surcharges, row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-if="filteredFees(pane).length > pageSize"
class="pay-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="pay-hint">{{ postcodeHint(pane) }}</p>
<el-input v-model="pane.postKeyword" clearable placeholder="筛选邮编、州、城市或分区" class="pay-filter" @input="pane.postPageNo = 1" />
<el-table :data="pagePostcodes(pane)" border size="small" max-height="360" empty-text="没有邮编">
<el-table-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="pay-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>
</div>
</template>
<script setup>
// 重量段和附加费。传入 products 时一次展示全部产品。
import { h } from 'vue'
import { ElInput } from 'element-plus'
const props = defineProps({
product: {
type: Object,
default: null
},
products: {
type: Array,
default: () => []
},
editable: {
type: Boolean,
default: false
}
})
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 || '-')
}
}
const calcRules = [
{ value: 'ALWAYS', label: '每票都收' },
{ value: 'POSTCODE', label: '命中邮编' },
{ value: 'WEIGHT', label: '超重' },
{ value: 'SIZE', label: '超尺寸' },
{ value: 'RETURN', label: '退件' },
{ value: 'MANUAL', label: '人工确认' }
]
function calcRuleLabel(value) {
return calcRules.find(item => item.value === value)?.label || '人工确认'
}
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, ['sectionName', 'countryName', 'countryCode', 'zoneName', 'weightLabel', 'chargeName', 'priceUnit'])
}
function filteredFees(pane) {
return filterRows(pane.item.surcharges || [], pane.feeKeyword, ['feeName', 'zoneName', 'chargeText', 'feeDesc', 'calcRule', 'calcParam'])
}
function addBand(pane) {
if (!pane.item.weightBands) pane.item.weightBands = []
pane.item.weightBands.push({ sectionName: '', countryName: '', countryCode: '', zoneName: '', weightLabel: '', chargeName: '', price: '', priceUnit: '' })
pane.bandPageNo = Math.ceil(pane.item.weightBands.length / pageSize) || 1
}
function addFee(pane) {
if (!pane.item.surcharges) pane.item.surcharges = []
pane.item.surcharges.push({ feeName: '', zoneName: '', chargeText: '', feeDesc: '', calcRule: 'MANUAL', calcParam: '' })
pane.feePageNo = Math.ceil(pane.item.surcharges.length / pageSize) || 1
}
function removeRow(list, row) {
const index = list.indexOf(row)
if (index >= 0) list.splice(index, 1)
}
function pageBands(pane) {
return pageOf(filteredBands(pane), pane.bandPageNo)
}
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 money(value) {
if (value === null || value === undefined || value === '') return '-'
const num = Number(value)
return Number.isNaN(num) ? value : String(num)
}
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>
.pay-product + .pay-product {
margin-top: 18px;
padding-top: 12px;
border-top: 1px solid var(--el-border-color-lighter);
}
.pay-product-title {
margin-bottom: 8px;
font-size: 15px;
font-weight: 600;
}
.pay-summary {
margin-bottom: 12px;
}
.pay-filter {
width: 320px;
}
.pay-edit-bar {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.pay-hint {
margin: 0 0 8px;
color: var(--el-text-color-secondary);
font-size: 13px;
line-height: 1.5;
}
.pay-page {
margin-top: 8px;
justify-content: flex-end;
}
.limit-tag {
margin-right: 6px;
}
</style>
@@ -0,0 +1,286 @@
<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="支付类型" required>
<el-select v-model="productType" filterable placeholder="请选择支付类型" style="width: 420px">
<el-option v-for="item in bms_pay_type" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</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
: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="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, 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, bms_pay_type } = useDict('payment_product_type', 'bms_pay_type')
const visible = ref(false)
const recognizing = ref(false)
const saving = ref(false)
const overwrite = ref(false)
const dictValue = ref('')
const productType = ref('AP')
const preview = ref(null)
const selected = ref([])
const tableRef = ref()
const activeKey = ref('')
const uploadKey = ref(0)
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('、'))
function open() {
reset()
visible.value = true
}
function reset() {
preview.value = null
selected.value = []
overwrite.value = false
dictValue.value = ''
productType.value = 'AP'
activeKey.value = ''
recognizing.value = false
saving.value = false
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
}
recognizing.value = true
try {
const res = await previewPay(raw, dictValue.value)
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))
} finally {
recognizing.value = false
}
}
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 (!productType.value) {
proxy.$modal.msgError('请选择支付类型')
return
}
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 products = selected.value.map(row => ({ ...row, productType: productType.value }))
const res = await savePay({ overwrite: overwrite.value, products })
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;
}
.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>
@@ -0,0 +1,396 @@
<template>
<div class="app-container">
<basic-table
ref="tableRef"
:columns="columns"
:search-columns="searchColumns"
:query="queryList"
:search-per-row="4"
search-action-align="right"
row-key="productId"
>
<template #toolbar="{ selection, ids }">
<el-col :span="1.5">
<el-button type="primary" plain icon="Upload" @click="importRef.open()" v-hasPermi="['price:pay:import']">导入价卡</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="!selection.length" @click="handleDelete(ids)" v-hasPermi="['price:pay:remove']">删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['price:pay:export']">导出列表</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExportDetail" v-hasPermi="['price:pay:export']">导出明细</el-button>
</el-col>
</template>
<template #col-parseStatus="{ row }">
<el-tag :type="statusType(row.parseStatus)" size="small">{{ statusText(row.parseStatus) }}</el-tag>
</template>
<template #col-termRange="{ row }">
<span>{{ termText(row) }}</span>
</template>
<template #col-termStatus="{ row }">
<el-switch
v-if="row.termId"
:model-value="row.termStatus === '1' ? '1' : '0'"
active-value="0"
inactive-value="1"
inline-prompt
active-text="生效"
inactive-text="失效"
@change="(val) => changeStatus(row, 'termStatus', val)"
v-hasPermi="['price:pay:edit']"
/>
</template>
<template #col-action="{ row }">
<el-button link type="primary" icon="View" @click="openDetail(row)" v-hasPermi="['price:pay:query']">查看</el-button>
<el-button link type="primary" icon="Calendar" @click="openTermDialog(row)" v-hasPermi="['price:pay:edit']">有效期</el-button>
<el-button link type="primary" icon="Clock" @click="openHistory(row)" v-hasPermi="['price:pay:query']">历史</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(row.productId)" v-hasPermi="['price:pay:remove']">删除</el-button>
</template>
</basic-table>
<pay-import-dialog ref="importRef" @saved="tableRef.refresh()" />
<el-drawer v-model="historyVisible" :title="historyTitle" size="860px" destroy-on-close>
<div class="history-bar">
<el-button type="primary" plain :disabled="historyPicked.length !== 2" @click="openCompare">比对所选版本</el-button>
<span>勾选两个版本后比对。历史订单按生效时间取价,不看当前开关。</span>
</div>
<el-table v-loading="historyLoading" :data="historyRows" border size="small" empty-text="还没有价格版本" @selection-change="onHistoryPick">
<el-table-column type="selection" width="42" align="center" />
<el-table-column prop="versionNo" label="版本" width="70" align="center" />
<el-table-column prop="sourceFile" label="来源文件" min-width="160" show-overflow-tooltip />
<el-table-column prop="weightCount" label="重量段" width="80" align="center" />
<el-table-column prop="surchargeCount" label="附加费" width="80" align="center" />
<el-table-column label="生效开始" width="110" align="center">
<template #default="{ row }">{{ dayText(row.effectiveStart) }}</template>
</el-table-column>
<el-table-column label="生效结束" width="110" align="center">
<template #default="{ row }">{{ dayText(row.effectiveEnd) || '不限' }}</template>
</el-table-column>
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-switch
v-if="row.termId"
:model-value="row.status === '1' ? '1' : '0'"
active-value="0"
inactive-value="1"
inline-prompt
active-text="生效"
inactive-text="失效"
@change="(val) => changeStatus(row, 'status', val)"
v-hasPermi="['price:pay:edit']"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="150" align="center">
<template #default="{ row }">
<el-button link type="primary" @click="openTermDialog(row)" v-hasPermi="['price:pay:edit']">修改有效期</el-button>
<el-button link type="primary" @click="openDetail(row)" v-hasPermi="['price:pay:query']">查看</el-button>
</template>
</el-table-column>
</el-table>
</el-drawer>
<el-dialog v-model="termVisible" title="修改有效期" width="460px" append-to-body>
<el-form label-width="90px">
<el-form-item label="生效开始" required>
<el-date-picker v-model="termForm.effectiveStart" type="date" value-format="YYYY-MM-DD" placeholder="生效开始" style="width: 100%" />
</el-form-item>
<el-form-item label="生效结束">
<el-date-picker v-model="termForm.effectiveEnd" type="date" value-format="YYYY-MM-DD" placeholder="不填表示不限" style="width: 100%" clearable />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="termVisible = false">取消</el-button>
<el-button type="primary" :loading="termSaving" @click="saveTerm">保存</el-button>
</template>
</el-dialog>
<el-drawer v-model="compareVisible" :title="compareTitle" size="88%" append-to-body destroy-on-close>
<el-table v-loading="compareLoading" :data="compareRows" border size="small" max-height="640" empty-text="没有差异">
<el-table-column prop="kind" label="类型" width="90" align="center" />
<el-table-column prop="change" label="变化" width="80" align="center" />
<el-table-column prop="name" label="项目" min-width="160" show-overflow-tooltip />
<el-table-column prop="zoneName" label="地区" min-width="140" show-overflow-tooltip />
<el-table-column prop="oldValue" label="旧版" min-width="160" show-overflow-tooltip />
<el-table-column prop="newValue" label="新版" min-width="160" show-overflow-tooltip />
</el-table>
</el-drawer>
<el-drawer v-model="detailVisible" :title="detailTitle" size="78%" destroy-on-close>
<pay-config-detail v-loading="detailLoading" :product="detail" :editable="editing" />
<template #footer>
<el-button v-if="!editing" type="primary" @click="editing = true" v-hasPermi="['price:pay:import']">编辑</el-button>
<el-button v-else type="primary" :loading="saving" @click="saveDetail">保存</el-button>
<el-button @click="detailVisible = false">关闭</el-button>
</template>
</el-drawer>
</div>
</template>
<script setup name="PricePay">
import { listPay, getPay, delPay, updatePay, listPayTerm, updatePayTerm, changePayTermStatus, comparePay } from '@/api/price/pay'
import PayImportDialog from './components/PayImportDialog.vue'
import PayConfigDetail from './components/PayConfigDetail.vue'
const { proxy } = getCurrentInstance()
const tableRef = ref()
const importRef = ref()
const detailVisible = ref(false)
const detailLoading = ref(false)
const editing = ref(false)
const saving = ref(false)
const detail = ref(null)
const historyVisible = ref(false)
const historyLoading = ref(false)
const historyRows = ref([])
const historyTitle = ref('价格版本')
const historyProductId = ref(null)
const historyPicked = ref([])
const compareVisible = ref(false)
const compareLoading = ref(false)
const compareTitle = ref('版本比对')
const compareRows = ref([])
const detailTitle = computed(() => {
if (!detail.value) return '应付配置'
const no = detail.value.versionNo ? `第 ${detail.value.versionNo} 版` : ''
return `${detail.value.productName || '应付配置'} ${no}`.trim()
})
const termVisible = ref(false)
const termSaving = ref(false)
const termForm = reactive({ termId: null, productId: null, effectiveStart: '', effectiveEnd: '' })
const statusOptions = [
{ label: '已识别', value: 'OK' },
{ label: '未识别到配置', value: 'EMPTY' },
{ label: '缺少工作表', value: 'MISSING' }
]
const columns = [
{ label: '产品编号', prop: 'productId', align: 'center', width: 90 },
{ label: '产品名称', prop: 'productName', align: 'center', minWidth: 150, showOverflowTooltip: true },
{ label: '价卡归属', prop: 'supplierName', align: 'center', width: 100 },
{ label: '国家/口岸', prop: 'countryScope', align: 'center', minWidth: 120, showOverflowTooltip: true },
{ label: '费用类型', prop: 'chargeType', align: 'center', width: 110 },
{ label: '每公斤加价', prop: 'extraPerKg', align: 'center', width: 120 },
{ label: '报价工作表', prop: 'sheetName', align: 'center', minWidth: 140, showOverflowTooltip: true },
{ label: '关联分区表', prop: 'relatedSheet', align: 'center', minWidth: 140, showOverflowTooltip: true },
{ label: '版本', prop: 'versionNo', align: 'center', width: 70 },
{ label: '有效期', prop: 'termRange', align: 'center', minWidth: 180, showOverflowTooltip: true },
{ label: '状态', prop: 'termStatus', align: 'center', width: 100 },
{ label: '币种', prop: 'currency', align: 'center', width: 80 },
{ label: '重量段', prop: 'weightCount', align: 'center', width: 80 },
{ label: '附加费', prop: 'surchargeCount', align: 'center', width: 80 },
{ label: '识别结果', prop: 'parseStatus', align: 'center', width: 120 },
{ label: '来源文件', prop: 'sourceFile', align: 'center', minWidth: 180, showOverflowTooltip: true },
{ label: '创建时间', prop: 'createTime', align: 'center', width: 160, parseTime: true },
{ label: '操作', prop: 'action', align: 'center', width: 280, fixed: 'right', showInToolbar: false }
]
const searchColumns = [
{ label: '产品名称', prop: 'productName', component: 'input', placeholder: '产品名称', width: '180px' },
{ label: '价卡归属', prop: 'supplierName', component: 'input', placeholder: '威狮、易德、GWE', width: '160px' },
{ label: '来源文件', prop: 'sourceFile', component: 'input', placeholder: '文件名', width: '180px' },
{ label: '报价工作表', prop: 'sheetName', component: 'input', placeholder: '工作表', width: '160px' },
{ label: '国家/口岸', prop: 'countryScope', component: 'input', placeholder: '国家或口岸', width: '160px' },
{ label: '服务类型', prop: 'serviceType', component: 'input', placeholder: '服务类型', width: '150px' },
{ label: '重量段', prop: 'weightKeyword', component: 'input', placeholder: '重量段、分区或费用项', width: '180px' },
{ label: '附加费', prop: 'surchargeName', component: 'input', placeholder: '名称、说明或时效', width: '180px' },
{ label: '生效日期', prop: 'effectiveRange', component: 'daterange', width: '280px' },
{ label: '识别结果', prop: 'parseStatus', component: 'select', options: statusOptions, placeholder: '全部', width: '150px' }
]
function queryList(params) {
return listPay({ ...toQuery(params), templateOnly: true, currentOnly: true })
}
function toQuery(params) {
const query = { ...params }
if (query.effectiveRange?.length === 2) {
query.effectiveBegin = query.effectiveRange[0]
query.effectiveEnd = query.effectiveRange[1]
}
delete query.effectiveRange
return query
}
function currentQuery() {
const query = toQuery(tableRef.value?.getQueryParams() || {})
delete query.pageNum
delete query.pageSize
query.templateOnly = true
query.currentOnly = true
return query
}
function handleExport() {
proxy.download('/price/pay/export', currentQuery(), `应付配置_${Date.now()}.xlsx`)
}
function handleExportDetail() {
proxy.download('/price/pay/exportDetail', currentQuery(), `应付配置明细_${Date.now()}.xlsx`)
}
function handleDelete(ids) {
const idText = Array.isArray(ids) ? ids.join(',') : ids
if (!idText) return
proxy.$modal.confirm('确认删除所选应付产品?重量段和附加费会一起删除。').then(() => {
return delPay(idText)
}).then(() => {
proxy.$modal.msgSuccess('删除成功')
tableRef.value.refresh()
}).catch(() => {})
}
function openDetail(row) {
detailVisible.value = true
editing.value = false
detailLoading.value = true
detail.value = null
getPay(row.versionProductId || row.productId).then(res => {
detail.value = res.data
}).finally(() => {
detailLoading.value = false
})
}
function saveDetail() {
cleanMoney(detail.value)
saving.value = true
updatePay(detail.value).then(() => {
proxy.$modal.msgSuccess('已保存这一版。产品编号不变,绑定该产品的渠道按有效期取价')
editing.value = false
tableRef.value.refresh()
}).finally(() => {
saving.value = false
})
}
function cleanMoney(product) {
;(product?.weightBands || []).forEach(row => {
if (row.price === '') row.price = null
})
}
function dayText(value) {
if (!value) return ''
return typeof value === 'string' ? value.slice(0, 10) : proxy.parseTime(value, '{y}-{m}-{d}')
}
function termText(row) {
const start = dayText(row.termStart || row.effectiveDate)
const end = dayText(row.termEnd)
if (!start && !end) return '-'
return `${start || '不限'} 至 ${end || '不限'}`
}
function openHistory(row) {
historyProductId.value = row.productId
historyTitle.value = `${row.productName || '产品'}(编号 ${row.productId})的价格版本`
historyPicked.value = []
historyVisible.value = true
loadHistory()
}
function onHistoryPick(rows) {
historyPicked.value = rows || []
}
function openCompare() {
if (historyPicked.value.length !== 2) return
const [first, second] = historyPicked.value
compareVisible.value = true
compareLoading.value = true
compareRows.value = []
comparePay(first.productId, second.productId).then(res => {
const data = res.data || {}
const older = data.older?.versionNo || '-'
const newer = data.newer?.versionNo || '-'
compareTitle.value = `第 ${older} 版 对比 第 ${newer} 版`
const weights = (data.weightDiffs || []).map(row => ({ ...row, kind: '重量段' }))
const fees = (data.surchargeDiffs || []).map(row => ({ ...row, kind: '附加费' }))
compareRows.value = weights.concat(fees).filter(row => row.change !== '相同')
}).finally(() => {
compareLoading.value = false
})
}
function loadHistory() {
if (!historyProductId.value) return
historyLoading.value = true
listPayTerm(historyProductId.value).then(res => {
historyRows.value = res.data || []
}).finally(() => {
historyLoading.value = false
})
}
function openTermDialog(row) {
termForm.termId = row.termId
termForm.productId = row.productId
termForm.effectiveStart = dayText(row.termStart || row.effectiveStart || row.effectiveDate)
termForm.effectiveEnd = dayText(row.termEnd || row.effectiveEnd)
termVisible.value = true
}
function saveTerm() {
if (!termForm.effectiveStart) {
proxy.$modal.msgError('请填写生效开始')
return
}
termSaving.value = true
updatePayTerm({
termId: termForm.termId,
productId: termForm.productId,
effectiveStart: termForm.effectiveStart,
effectiveEnd: termForm.effectiveEnd || null
}).then(() => {
proxy.$modal.msgSuccess('有效期已保存')
termVisible.value = false
tableRef.value.refresh()
if (historyVisible.value) loadHistory()
}).finally(() => {
termSaving.value = false
})
}
function changeStatus(row, field, status) {
if (!row?.termId || (status !== '0' && status !== '1') || row[field] === status) return
const previous = row[field]
row[field] = status
changePayTermStatus({ termId: row.termId, productId: row.productId, status }).then(() => {
proxy.$modal.msgSuccess(status === '0' ? '已设为生效' : '已设为失效')
tableRef.value.refresh()
if (historyVisible.value) loadHistory()
}).catch(() => {
row[field] = previous
})
}
function statusText(status) {
return statusOptions.find(item => item.value === status)?.label || status || '-'
}
function statusType(status) {
if (status === 'OK') return 'success'
if (status === 'EMPTY') return 'warning'
if (status === 'MISSING') return 'danger'
return 'info'
}
</script>
<style scoped>
.history-bar {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
color: var(--el-text-color-secondary);
}
</style>
@@ -164,6 +164,8 @@
</template>
<template #col-action="{ row }">
<el-button link type="primary" icon="View" @click="handleView(row)">查看</el-button>
<el-button link type="primary" icon="Money" v-hasPermi="['price:billing:bill']" @click="handlePayBill(row)">生成应付</el-button>
<el-button
<!-- <el-button
link
type="primary"
@@ -276,6 +278,7 @@
<script setup name="BillingBill">
import { listBill, getBill, refundBill, updateCreditArrears, importChargeWeightAsync, downloadChargeWeightTemplate, recalcBillPriceAsync } from '@/api/price/billing'
import { generatePayBill } from '@/api/price/pay'
import { exportAsyncFromTable } from '@/utils/excelExport'
import ExcelExportFieldDialog from '@/components/ExcelExportFieldDialog/index.vue'
import { BILLING_BILL_EXPORT_FIELDS } from '@/utils/exportFieldDefs'
@@ -461,6 +464,7 @@ const columns = [
{ label: '销售员', prop: 'belongSaler', minWidth: 100, showOverflowTooltip: true },
{ label: '结算员', prop: 'settlementMan', minWidth: 100, showOverflowTooltip: true },
{ label: '客服员', prop: 'serviceMan', minWidth: 100, showOverflowTooltip: true },
{ label: '操作', prop: 'action', align: 'center', width: 320, fixed: 'right', slot: 'action', showInToolbar: false, showOverflowTooltip: false }
{ label: '操作', prop: 'action', align: 'center', width: 150, fixed: 'right', slot: 'action', showInToolbar: false, showOverflowTooltip: false }
]
@@ -597,6 +601,23 @@ function handleUpdateArrears(ids, selection) {
}).catch(() => {})
}
function handlePayBill(row) {
if (!row || !row.billId) return
ElMessageBox.confirm(
'按订单的渠道服务商和已配置的应付产品计算应付?',
'生成应付',
{ type: 'warning' }
).then(() => generatePayBill(row.billId)).then(res => {
const data = res.data || {}
if (data.skipped && data.skipped.length) {
ElMessageBox.alert(data.skipped.join('\n'), data.message || '应付已生成', { type: 'warning' })
} else {
ElMessage.success(data.message || '应付已生成')
}
tableRef.value?.refresh?.()
}).catch(() => {})
}
function handleRefund(row) {
if (!row || !row.billId) return
if (row.refundStatus === 'REFUNDED') {
@@ -137,6 +137,9 @@
<el-empty v-else description="暂无应收明细" :image-size="64" />
</el-tab-pane>
<el-tab-pane label="应付" name="pay">
<div v-if="!readonly" class="pay-generate">
<el-button type="primary" plain size="small" v-hasPermi="['price:billing:bill']" @click="handlePayBill">生成应付</el-button>
</div>
<el-table v-if="payFees.length" :data="payFees" border size="small" class="fee-table">
<el-table-column label="费用名称" min-width="140" show-overflow-tooltip>
<template #default="{ row }">{{ displayFeeName(row) }}</template>
@@ -190,7 +193,9 @@
<script setup>
import AddIncomeFeeDialog from './AddIncomeFeeDialog.vue'
import EditChargeWeightDialog from './EditChargeWeightDialog.vue'
import { deleteIncomeBillFee } from '@/api/price/billing'
import { deleteIncomeBillFee, getBill } from '@/api/price/billing'
import { generatePayBill } from '@/api/price/pay'
import { ElMessage, ElMessageBox } from 'element-plus'
const { proxy } = getCurrentInstance()
@@ -226,6 +231,26 @@ watch(() => props.detail, () => {
feeTab.value = 'income'
})
function handlePayBill() {
const billId = props.detail?.billId
if (!billId) return
ElMessageBox.confirm('按订单的渠道服务商和已配置的应付产品计算应付?', '生成应付', { type: 'warning' })
.then(() => generatePayBill(billId))
.then(res => {
const data = res.data || {}
if (data.skipped && data.skipped.length) {
ElMessageBox.alert(data.skipped.join('\n'), data.message || '应付已生成', { type: 'warning' })
} else {
ElMessage.success(data.message || '应付已生成')
}
return getBill(billId)
})
.then(res => {
if (res && res.data) emit('updated', res.data)
})
.catch(() => {})
}
function canEditManualFee(row) {
return row && String(row.manualFlag) === '1' && row.arBillStatus !== 'BILLED'
}
@@ -439,6 +464,9 @@ function refundLabel(status) {
.bill-detail-wrap {
min-height: 240px;
}
.pay-generate {
margin-bottom: 8px;
}
.bill-detail {
padding-bottom: 8px;
}
@@ -0,0 +1,288 @@
<template>
<div class="app-container">
<el-form :inline="true" :model="query" class="mb8">
<el-form-item label="账单号">
<el-input v-model="query.billNo" clearable placeholder="账单号" style="width: 180px" />
</el-form-item>
<el-form-item label="供应商">
<el-input v-model="query.supplierName" clearable placeholder="供应商" style="width: 180px" />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="query.status" clearable placeholder="全部" style="width: 140px">
<el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="loadList">查询</el-button>
<el-button type="primary" plain icon="Plus" @click="openCreate" v-hasPermi="['price:payBill:add']">生成账单</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="rows" border>
<el-table-column prop="billNo" label="账单号" min-width="170" />
<el-table-column prop="supplierName" label="供应商" min-width="140" show-overflow-tooltip />
<el-table-column prop="currency" label="币种" width="80" align="center" />
<el-table-column prop="orderCount" label="订单数" width="80" align="center" />
<el-table-column prop="totalAmount" label="应付总额" width="120" align="right" />
<el-table-column label="来源" width="90" align="center">
<template #default="{ row }">{{ sourceText(row.sourceType) }}</template>
</el-table-column>
<el-table-column label="状态" width="90" align="center">
<template #default="{ row }">{{ statusText(row.status) }}</template>
</el-table-column>
<el-table-column prop="createTime" label="生成时间" width="170" />
<el-table-column label="操作" width="260" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openDetail(row)">查看</el-button>
<el-button v-if="row.status === 'DRAFT'" link type="primary" @click="confirmRow(row)" v-hasPermi="['price:payBill:confirm']">确认</el-button>
<el-button v-if="row.status === 'CONFIRMED'" link type="primary" @click="payRow(row)" v-hasPermi="['price:payBill:confirm']">登记付款</el-button>
<el-button v-if="row.status !== 'VOID' && row.status !== 'PAID'" link type="danger" @click="openVoid(row)" v-hasPermi="['price:payBill:void']">作废</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="query.pageNum" v-model:limit="query.pageSize" @pagination="loadList" />
<el-dialog v-model="createVisible" title="生成应付账单" width="1180px" append-to-body>
<el-form label-width="90px">
<el-form-item label="选出方式">
<el-radio-group v-model="command.sourceType">
<el-radio value="ORDER">订单号</el-radio>
<el-radio value="TIME">计费日</el-radio>
<el-radio value="LADING">提单</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="command.sourceType === 'ORDER'" label="订单号">
<el-input v-model="command.orderText" type="textarea" :rows="4" placeholder="可用逗号、空格或换行分隔" />
</el-form-item>
<el-form-item v-if="command.sourceType === 'TIME'" label="计费日" class="pay-bill-date">
<el-date-picker v-model="dateRange" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始" end-placeholder="结束" />
</el-form-item>
<el-form-item v-if="command.sourceType === 'LADING'" label="提单号">
<el-input v-model="command.ladingText" type="textarea" :rows="4" placeholder="多张提单可用逗号、空格或换行分隔" />
</el-form-item>
<el-form-item label="供应商" required>
<el-select v-model="command.supplierId" filterable placeholder="选择供应商" style="width: 280px">
<el-option v-for="item in suppliers" :key="item.supplierId" :label="item.supplierName" :value="item.supplierId" />
</el-select>
</el-form-item>
</el-form>
<div class="mb8">
<el-button @click="runPreview">查询订单</el-button>
<span v-if="previewRows.length" class="preview-msg">查询到 {{ previewRows.length }} 票,件数 {{ piecesTotal }},可生成 {{ preview.orders.length }} 票</span>
<span v-if="preview.message" class="preview-msg">{{ preview.message }}</span>
</div>
<el-table :data="previewRows" border size="small" max-height="360" empty-text="先查询订单">
<el-table-column prop="orderNo" label="订单号" min-width="140" show-overflow-tooltip />
<el-table-column prop="trackingNo" label="运单号" min-width="130" show-overflow-tooltip />
<el-table-column prop="ladingNo" label="提单号" min-width="120" show-overflow-tooltip />
<el-table-column prop="supplierName" label="供应商" min-width="120" show-overflow-tooltip />
<el-table-column label="目的地" min-width="140" show-overflow-tooltip>
<template #default="{ row }">{{ destText(row) }}</template>
</el-table-column>
<el-table-column prop="pieces" label="件数" width="70" align="center" />
<el-table-column prop="chargeWeight" label="计费重" width="90" align="right" />
<el-table-column prop="channelName" label="渠道" min-width="120" show-overflow-tooltip />
<el-table-column prop="productName" label="产品" min-width="120" show-overflow-tooltip />
<el-table-column prop="feeText" label="费用" min-width="180" show-overflow-tooltip />
<el-table-column prop="currency" label="币种" width="70" align="center" />
<el-table-column prop="payTotal" label="应付总额" width="100" align="right" />
<el-table-column prop="bizDate" label="计费日" width="160" />
<el-table-column label="状态" width="150" show-overflow-tooltip>
<template #default="{ row }">{{ row.occupiedBillNo ? '已入账 ' + row.occupiedBillNo : '可生成' }}</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="createVisible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="runGenerate">生成</el-button>
</template>
</el-dialog>
<el-drawer v-model="detailVisible" :title="detail?.billNo || '应付账单'" size="70%">
<template v-if="detail">
<el-descriptions :column="3" border size="small" class="mb8">
<el-descriptions-item label="供应商">{{ detail.supplierName }}</el-descriptions-item>
<el-descriptions-item label="币种">{{ detail.currency }}</el-descriptions-item>
<el-descriptions-item label="状态">{{ statusText(detail.status) }}</el-descriptions-item>
<el-descriptions-item label="应付总额">{{ detail.totalAmount }}</el-descriptions-item>
<el-descriptions-item label="订单数">{{ detail.orderCount }}</el-descriptions-item>
<el-descriptions-item label="作废原因">{{ detail.voidReason || '-' }}</el-descriptions-item>
</el-descriptions>
<el-table :data="detail.lines || []" border size="small" max-height="320">
<el-table-column prop="orderNo" label="订单号" min-width="130" />
<el-table-column prop="ladingNo" label="提单号" min-width="120" />
<el-table-column prop="feeName" label="费用" min-width="120" />
<el-table-column prop="amount" label="金额" width="100" align="right" />
<el-table-column prop="currency" label="币种" width="70" />
<el-table-column prop="versionNo" label="版本" width="70" />
</el-table>
<h4>操作记录</h4>
<el-table :data="detail.logs || []" border size="small">
<el-table-column prop="createTime" label="时间" width="170" />
<el-table-column prop="operatorName" label="操作人" width="100" />
<el-table-column prop="operationType" label="动作" width="100" />
<el-table-column prop="fromStatus" label="原状态" width="110" />
<el-table-column prop="toStatus" label="新状态" width="110" />
<el-table-column prop="operationDesc" label="说明" min-width="180" show-overflow-tooltip />
</el-table>
</template>
</el-drawer>
<el-dialog v-model="voidVisible" title="作废应付账单" width="460px" append-to-body>
<el-input v-model="voidReason" type="textarea" :rows="3" placeholder="作废原因" />
<template #footer>
<el-button @click="voidVisible = false">取消</el-button>
<el-button type="danger" @click="runVoid">确认作废</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup name="PayBill">
import { listSupplier } from '@/api/customersuppliermanagement/supplierinfo/supplier'
import { listPayBill, getPayBill, previewPayBill, generatePayBill, confirmPayBill, payPayBill, voidPayBill } from '@/api/price/payBill'
const { proxy } = getCurrentInstance()
const loading = ref(false)
const rows = ref([])
const total = ref(0)
const query = reactive({ pageNum: 1, pageSize: 10, billNo: '', supplierName: '', status: '' })
const statusOptions = [
{ label: '草稿', value: 'DRAFT' },
{ label: '已确认', value: 'CONFIRMED' },
{ label: '已付款', value: 'PAID' },
{ label: '已作废', value: 'VOID' }
]
const createVisible = ref(false)
const saving = ref(false)
const suppliers = ref([])
const dateRange = ref([])
const command = reactive({ sourceType: 'ORDER', orderText: '', ladingText: '', supplierId: null, beginDate: '', endDate: '' })
const preview = reactive({ orders: [], occupied: [], missingNos: [], message: '' })
const detailVisible = ref(false)
const detail = ref(null)
const voidVisible = ref(false)
const voidReason = ref('')
const voidId = ref(null)
const previewRows = computed(() => [...(preview.orders || []), ...(preview.occupied || [])])
const piecesTotal = computed(() => previewRows.value.reduce((sum, item) => sum + (Number(item.pieces) || 0), 0))
function destText(row) {
const country = [row.countryName, row.countryCode].filter(Boolean).join(' ')
return [country, row.destPostcode].filter(Boolean).join(' / ') || '-'
}
function statusText(value) {
return statusOptions.find(item => item.value === value)?.label || value || '-'
}
function sourceText(value) {
return { ORDER: '订单号', TIME: '计费日', LADING: '提单' }[value] || value || '-'
}
function loadList() {
loading.value = true
listPayBill(query).then(res => {
rows.value = res.rows || []
total.value = res.total || 0
}).finally(() => {
loading.value = false
})
}
function openCreate() {
preview.orders = []
preview.occupied = []
preview.missingNos = []
preview.message = ''
createVisible.value = true
if (!suppliers.value.length) {
listSupplier({ pageNum: 1, pageSize: 500 }).then(res => {
suppliers.value = res.rows || []
})
}
}
function buildCommand() {
const data = { ...command }
if (command.sourceType === 'TIME' && dateRange.value?.length === 2) {
data.beginDate = dateRange.value[0]
data.endDate = dateRange.value[1]
}
return data
}
function runPreview() {
previewPayBill(buildCommand()).then(res => {
const data = res.data || {}
preview.orders = data.orders || []
preview.occupied = data.occupied || []
preview.missingNos = data.missingNos || []
preview.message = data.message || ''
})
}
function runGenerate() {
saving.value = true
generatePayBill(buildCommand()).then(() => {
proxy.$modal.msgSuccess('应付账单已生成')
createVisible.value = false
loadList()
}).finally(() => {
saving.value = false
})
}
function openDetail(row) {
getPayBill(row.billId).then(res => {
detail.value = res.data
detailVisible.value = true
})
}
function confirmRow(row) {
proxy.$modal.confirm('确认这张应付账单?确认后金额锁定。').then(() => confirmPayBill(row.billId)).then(() => {
proxy.$modal.msgSuccess('已确认')
loadList()
}).catch(() => {})
}
function payRow(row) {
proxy.$modal.confirm('登记这张账单已付款?已付款后不能直接作废。').then(() => payPayBill(row.billId)).then(() => {
proxy.$modal.msgSuccess('已登记付款')
loadList()
}).catch(() => {})
}
function openVoid(row) {
voidId.value = row.billId
voidReason.value = ''
voidVisible.value = true
}
function runVoid() {
if (!voidReason.value.trim()) {
proxy.$modal.msgWarning('请填写作废原因')
return
}
voidPayBill({ billId: voidId.value, voidReason: voidReason.value.trim() }).then(() => {
proxy.$modal.msgSuccess('已作废,历史账单仍可查看')
voidVisible.value = false
loadList()
})
}
loadList()
</script>
<style scoped>
.preview-msg { margin-left: 12px; color: #909399; }
.mb8 { margin-bottom: 8px; }
.pay-bill-date :deep(.el-date-editor) {
width: 280px !important;
max-width: 280px;
flex: 0 0 280px;
--el-date-editor-width: 280px;
}
</style>