应付账单初始化
This commit is contained in:
@@ -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'
|
||||
})
|
||||
}
|
||||
@@ -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'
|
||||
})
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user