应付计费邮编改造

This commit is contained in:
liuminghui
2026-11-13 18:26:21 +08:00
parent 880b4751c5
commit bde1d7d86e
30 changed files with 1761 additions and 254 deletions
+36
View File
@@ -51,3 +51,39 @@ export function delPayPostcode(groupIds) {
method: 'delete'
})
}
export function listPayPostcodeZone(query) {
return request({ url: '/data/payPostcode/zones', method: 'get', params: query })
}
export function addPayPostcodeZone(data) {
return request({ url: '/data/payPostcode/zones', method: 'post', data })
}
export function updatePayPostcodeZone(data) {
return request({ url: '/data/payPostcode/zones', method: 'put', data })
}
export function delPayPostcodeZone(query) {
return request({ url: '/data/payPostcode/zones', method: 'delete', params: query })
}
export function updatePayPostcodeCountry(data) {
return request({ url: '/data/payPostcode/country', method: 'put', data })
}
export function delPayPostcodeCountry(query) {
return request({ url: '/data/payPostcode/country', method: 'delete', params: query })
}
export function addPayPostcodeLine(data) {
return request({ url: '/data/payPostcode/lines', method: 'post', data })
}
export function updatePayPostcodeLine(data) {
return request({ url: '/data/payPostcode/lines', method: 'put', data })
}
export function delPayPostcodeLine(lineIds) {
return request({ url: '/data/payPostcode/lines/' + lineIds, method: 'delete' })
}
@@ -0,0 +1,124 @@
<template>
<el-drawer v-model="visible" size="760px" append-to-body>
<template #header>
<div class="drawer-head">
<span class="drawer-title">邮编明细</span>
<span v-if="zone" class="drawer-sub">{{ zone.zoneName }}({{ total }} 条)</span>
</div>
</template>
<div class="drawer-toolbar">
<el-input v-model="query.postcode" clearable placeholder="搜索邮编" style="width: 200px" @keyup.enter="loadList" />
<el-button type="primary" plain icon="Search" size="small" @click="loadList">搜索</el-button>
<el-button type="primary" plain icon="Plus" size="small" @click="handleAdd" v-hasPermi="['data:payPostcode:edit']">新增邮编</el-button>
</div>
<el-table v-loading="loading" :data="list" border row-key="lineId" empty-text="暂无邮编">
<el-table-column label="地段" prop="cityName" min-width="140" show-overflow-tooltip>
<template #default="{ row }">{{ row.cityName || row.regionName || '-' }}</template>
</el-table-column>
<el-table-column label="邮编" prop="postcode" min-width="140" show-overflow-tooltip />
<el-table-column label="截止邮编" prop="postcodeEnd" width="140" show-overflow-tooltip />
<el-table-column label="操作" width="160" align="center">
<template #default="{ row }">
<el-button link type="primary" icon="Edit" @click="handleEdit(row)" v-hasPermi="['data:payPostcode:edit']">编辑</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(row)" v-hasPermi="['data:payPostcode:remove']">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="drawer-pager">
<el-pagination
v-model:current-page="query.pageNum"
v-model:page-size="query.pageSize"
:total="total"
:page-sizes="[20, 50, 100, 200]"
layout="total, sizes, prev, pager, next"
@size-change="loadList"
@current-change="loadList"
/>
</div>
<pay-postcode-form-dialog ref="formRef" @success="onChanged" />
</el-drawer>
</template>
<script setup name="PayPostcodeDrawer">
import { listPayPostcodeLine, delPayPostcodeLine } from '@/api/data/payPostcode'
import PayPostcodeFormDialog from './PayPostcodeFormDialog.vue'
const { proxy } = getCurrentInstance()
const emit = defineEmits(['changed'])
const visible = ref(false)
const loading = ref(false)
const country = ref(null)
const zone = ref(null)
const list = ref([])
const total = ref(0)
const query = ref({ pageNum: 1, pageSize: 50, postcode: '' })
const formRef = ref()
function open(countryRow, zoneRow) {
country.value = countryRow
zone.value = zoneRow
query.value = { pageNum: 1, pageSize: 50, postcode: '' }
visible.value = true
loadList()
}
function loadList() {
if (!zone.value) {
list.value = []
total.value = 0
return
}
loading.value = true
listPayPostcodeLine({
countryCode: country.value?.countryCode,
countryName: country.value?.countryName,
zoneName: zone.value.zoneName,
postcode: query.value.postcode || undefined,
pageNum: query.value.pageNum,
pageSize: query.value.pageSize
}).then(res => {
list.value = res.rows || []
total.value = res.total || 0
}).catch(() => {
list.value = []
total.value = 0
}).finally(() => {
loading.value = false
})
}
function onChanged() {
loadList()
emit('changed')
}
function handleAdd() {
formRef.value.open(country.value, zone.value)
}
function handleEdit(row) {
formRef.value.open(country.value, zone.value, row)
}
function handleDelete(row) {
proxy.$modal.confirm('删除邮编「' + row.postcode + '」?').then(() => delPayPostcodeLine(row.lineId)).then(() => {
proxy.$modal.msgSuccess('删除成功')
onChanged()
}).catch(() => {})
}
defineExpose({ open })
</script>
<style scoped>
.drawer-head { display: flex; align-items: center; gap: 10px; }
.drawer-title { font-size: 16px; font-weight: 600; }
.drawer-sub { color: #909399; font-size: 13px; }
.drawer-toolbar { margin-bottom: 12px; display: flex; align-items: center; gap: 8px; }
.drawer-pager { margin-top: 16px; display: flex; justify-content: flex-end; }
</style>
@@ -0,0 +1,78 @@
<template>
<el-dialog :title="title" v-model="visible" width="460px" append-to-body>
<el-form ref="formRef" :model="form" :rules="rules" label-width="90px">
<el-form-item label="分区">
<span>{{ zone?.zoneName || '-' }}</span>
</el-form-item>
<el-form-item label="地段" prop="cityName">
<el-input v-model="form.cityName" maxlength="200" placeholder="例如罗马,可空" />
</el-form-item>
<el-form-item label="邮编" prop="postcode">
<el-input v-model="form.postcode" maxlength="200" placeholder="邮编或前缀" />
</el-form-item>
<el-form-item label="截止邮编" prop="postcodeEnd">
<el-input v-model="form.postcodeEnd" maxlength="200" placeholder="区间结束,可空" />
</el-form-item>
</el-form>
<template #footer>
<el-button type="primary" @click="submit">确定</el-button>
<el-button @click="visible = false">取消</el-button>
</template>
</el-dialog>
</template>
<script setup name="PayPostcodeFormDialog">
import { addPayPostcodeLine, updatePayPostcodeLine } from '@/api/data/payPostcode'
const { proxy } = getCurrentInstance()
const emit = defineEmits(['success'])
const visible = ref(false)
const title = ref('')
const formRef = ref()
const form = ref({})
const zone = ref(null)
const rules = {
postcode: [{ required: true, message: '请填写邮编', trigger: 'blur' }]
}
function open(country, zoneRow, row) {
zone.value = zoneRow
form.value = row
? {
lineId: row.lineId,
countryCode: country?.countryCode,
countryName: country?.countryName,
zoneName: zoneRow?.zoneName,
cityName: row.cityName || row.regionName || '',
postcode: row.postcode,
postcodeEnd: row.postcodeEnd || ''
}
: {
countryCode: country?.countryCode,
countryName: country?.countryName,
zoneName: zoneRow?.zoneName,
cityName: '',
postcode: '',
postcodeEnd: ''
}
title.value = row ? '编辑邮编' : '新增邮编'
visible.value = true
nextTick(() => formRef.value?.clearValidate())
}
function submit() {
formRef.value.validate(valid => {
if (!valid) return
const payload = { ...form.value, postcode: String(form.value.postcode || '').trim() }
const req = payload.lineId ? updatePayPostcodeLine(payload) : addPayPostcodeLine(payload)
req.then(() => {
proxy.$modal.msgSuccess(payload.lineId ? '修改成功' : '新增成功')
visible.value = false
emit('success')
})
})
}
defineExpose({ open })
</script>
@@ -0,0 +1,146 @@
<template>
<el-drawer v-model="visible" size="860px" append-to-body>
<template #header>
<div class="drawer-head">
<span class="drawer-title">国家分区</span>
<span v-if="country" class="drawer-sub">
{{ country.countryName || '未填写国家' }}
<template v-if="country.countryCode">({{ country.countryCode }})</template>
,共 {{ zoneList.length }} 个分区
</span>
</div>
</template>
<div v-loading="loading">
<div class="zone-toolbar">
<el-button type="primary" plain icon="Plus" size="small" :disabled="!country" @click="handleAdd" v-hasPermi="['data:payPostcode:edit']">新增分区</el-button>
</div>
<el-table :data="zoneList" border row-key="zoneName" :empty-text="country ? '这个国家还没有分区' : '请选择国家'">
<el-table-column label="分区" min-width="180" align="center">
<template #default="{ row }">
<div class="zone-country-cell">
<div class="zone-title">{{ row.zoneName }}</div>
<el-button link type="primary" class="zone-click" @click="openLines(row)" v-hasPermi="['data:payPostcode:query']">分区点击</el-button>
</div>
</template>
</el-table-column>
<el-table-column label="邮编属性" width="120" align="center">
<template #default="{ row }">
<el-tag v-if="row.coverage" size="small" :type="coverageType(firstPart(row.coverage))">{{ coverageLabel(firstPart(row.coverage)) }}</el-tag>
<el-tag v-else size="small" type="info">普通</el-tag>
</template>
</el-table-column>
<el-table-column label="邮编数" prop="lineCount" width="100" align="center" />
<el-table-column label="操作" width="220" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" icon="View" @click="openLines(row)" v-hasPermi="['data:payPostcode:query']">明细</el-button>
<el-button link type="primary" icon="Edit" @click="handleEdit(row)" v-hasPermi="['data:payPostcode:edit']">编辑</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(row)" v-hasPermi="['data:payPostcode:remove']">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<pay-zone-form-dialog ref="formRef" :country="country" @success="onChanged" />
<pay-postcode-drawer ref="lineRef" @changed="onChanged" />
</el-drawer>
</template>
<script setup name="PayZoneDrawer">
import { listPayPostcodeZone, delPayPostcodeZone } from '@/api/data/payPostcode'
import PayZoneFormDialog from './PayZoneFormDialog.vue'
import PayPostcodeDrawer from './PayPostcodeDrawer.vue'
const { proxy } = getCurrentInstance()
const emit = defineEmits(['changed'])
const visible = ref(false)
const loading = ref(false)
const country = ref(null)
const zoneList = ref([])
const formRef = ref()
const lineRef = ref()
const coverageOptions = [
{ label: '可达', value: 'REACH' },
{ label: '不可达', value: 'UNREACH' },
{ label: '偏远', value: 'REMOTE' }
]
function firstPart(value) {
return String(value || '').split('、').map(item => item.trim()).filter(Boolean)[0] || ''
}
function coverageLabel(value) {
return coverageOptions.find(item => item.value === value)?.label || value
}
function coverageType(value) {
if (value === 'UNREACH') return 'danger'
if (value === 'REMOTE') return 'warning'
return 'success'
}
function open(row) {
country.value = row
visible.value = true
loadList()
}
function loadList() {
if (!country.value) {
zoneList.value = []
return
}
loading.value = true
listPayPostcodeZone({ countryCode: country.value.countryCode, countryName: country.value.countryName }).then(res => {
zoneList.value = res.data || []
}).catch(() => {
zoneList.value = []
}).finally(() => {
loading.value = false
})
}
function onChanged() {
loadList()
emit('changed')
}
function handleAdd() {
formRef.value.open()
}
function handleEdit(row) {
formRef.value.open(row)
}
function handleDelete(row) {
proxy.$modal.confirm('删除分区「' + row.zoneName + '」及其邮编?').then(() => {
return delPayPostcodeZone({
countryCode: country.value.countryCode,
countryName: country.value.countryName,
zoneName: row.zoneName
})
}).then(() => {
proxy.$modal.msgSuccess('删除成功')
onChanged()
}).catch(() => {})
}
function openLines(row) {
lineRef.value.open(country.value, row)
}
defineExpose({ open })
</script>
<style scoped>
.drawer-head { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.drawer-title { font-size: 16px; font-weight: 600; }
.drawer-sub { color: #909399; font-size: 13px; }
.zone-toolbar { display: flex; align-items: center; margin-bottom: 12px; }
.zone-country-cell { display: flex; flex-direction: column; align-items: center; gap: 2px; padding: 4px 0; }
.zone-title { font-weight: 500; }
.zone-click { padding: 0; height: auto; font-size: 12px; }
</style>
@@ -0,0 +1,83 @@
<template>
<el-dialog :title="title" v-model="visible" width="480px" append-to-body>
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="国家">
<span>{{ country?.countryName || '-' }}<template v-if="country?.countryCode">({{ country.countryCode }})</template></span>
</el-form-item>
<el-form-item label="分区" prop="zoneName">
<el-input v-model="form.zoneName" maxlength="200" placeholder="例如 mainland" />
</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>
<template #footer>
<el-button type="primary" @click="submit">确定</el-button>
<el-button @click="visible = false">取消</el-button>
</template>
</el-dialog>
</template>
<script setup name="PayZoneFormDialog">
import { addPayPostcodeZone, updatePayPostcodeZone } from '@/api/data/payPostcode'
const { proxy } = getCurrentInstance()
const props = defineProps({
country: { type: Object, default: null }
})
const emit = defineEmits(['success'])
const visible = ref(false)
const title = ref('')
const formRef = ref()
const form = ref({})
const rules = {
zoneName: [{ required: true, message: '请填写分区', trigger: 'blur' }]
}
const coverageOptions = [
{ label: '可达', value: 'REACH' },
{ label: '不可达', value: 'UNREACH' },
{ label: '偏远', value: 'REMOTE' }
]
function open(row) {
form.value = row
? {
countryCode: props.country?.countryCode,
countryName: props.country?.countryName,
oldZoneName: row.zoneName,
zoneName: row.zoneName,
coverage: String(row.coverage || '').split('、')[0] || ''
}
: {
countryCode: props.country?.countryCode,
countryName: props.country?.countryName,
zoneName: '',
coverage: ''
}
title.value = row ? '编辑分区' : '新增分区'
visible.value = true
nextTick(() => formRef.value?.clearValidate())
}
function submit() {
formRef.value.validate(valid => {
if (!valid) return
const payload = {
...form.value,
zoneName: String(form.value.zoneName || '').trim(),
coverage: form.value.coverage || ''
}
const req = payload.oldZoneName ? updatePayPostcodeZone(payload) : addPayPostcodeZone(payload)
req.then(() => {
proxy.$modal.msgSuccess(payload.oldZoneName ? '修改成功' : '新增成功')
visible.value = false
emit('success')
})
})
}
defineExpose({ open })
</script>
+84 -166
View File
@@ -1,40 +1,41 @@
<template>
<div class="app-container">
<el-alert
type="info"
:closable="false"
show-icon
class="mb16"
title="每个国家一行。点「分区」维护分区和邮编。偏远分区按目的国家参与应付偏远附加费;某个产品的报价表自己已经挂了偏远名单时,仍只用那一份。"
/>
<basic-table
ref="tableRef"
:columns="columns"
:search-columns="searchColumns"
:query="queryList"
row-key="groupId"
selection
search-action-align="right"
>
<template #toolbar="{ ids, selection }">
<template #toolbar>
<el-col :span="1.5">
<el-button type="primary" plain icon="Upload" @click="open = true" v-hasPermi="['data:payPostcode:import']">导入</el-button>
<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 v-if="coverageParts(row.coverage).length">
<el-tag v-for="item in coverageParts(row.coverage)" :key="item" size="small" class="cov-tag" :type="coverageType(item)">{{ coverageLabel(item) }}</el-tag>
</template>
<el-tag v-else size="small" type="info">普通</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>
<el-button link type="primary" icon="List" @click.stop="openZones(row)" v-hasPermi="['data:payPostcode:query']">分区</el-button>
<el-button link type="primary" icon="Edit" @click.stop="handleUpdate(row)" v-hasPermi="['data:payPostcode:edit']">修改</el-button>
<el-button link type="danger" icon="Delete" @click.stop="handleClear(row)" 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>
<p class="import-tip">导入后按国家归成一行。表头里带邮编或附加费的工作表会被导入。可达、可派记为可达,不可达记为不可达,偏远和岛屿记为偏远。</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>
@@ -44,105 +45,44 @@
</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-dialog title="修改国家邮编" v-model="editOpen" width="520px" append-to-body destroy-on-close>
<el-form ref="formRef" :model="form" label-width="100px">
<el-form-item label="国家">
<span>{{ form.countryName || '-' }}<template v-if="form.countryCode">({{ form.countryCode }})</template></span>
</el-form-item>
<el-form-item label="目录国家" prop="catalogCountry">
<el-input v-model="form.catalogCountry" maxlength="100" placeholder="目录中的国家" />
<el-form-item label="服务类型">
<el-input v-model="form.serviceType" maxlength="100" />
</el-form-item>
<el-form-item label="销售产品" prop="productName">
<el-input v-model="form.productName" maxlength="200" placeholder="目录中的销售产品" />
<el-form-item label="销售产品">
<el-input v-model="form.productName" maxlength="200" />
</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-form-item label="渠道名称">
<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>
<p class="import-tip">保存后,这个国家下的分区都使用这里的服务类型、销售产品和渠道。报价表保持不变。偏远附加费按目的国家的偏远邮编匹配,再用价卡上的分区或邮编前缀选定费用档。</p>
<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>
<pay-zone-drawer ref="zoneDrawerRef" @changed="reload" />
</div>
</template>
<script setup name="PayPostcode">
import { listPayPostcode, getPayPostcode, updatePayPostcode, listPayPostcodeLine, importPayPostcode, delPayPostcode } from '@/api/data/payPostcode'
import { listPayPostcode, importPayPostcode, updatePayPostcodeCountry, delPayPostcodeCountry } from '@/api/data/payPostcode'
import PayZoneDrawer from './components/PayZoneDrawer.vue'
const { proxy } = getCurrentInstance()
const tableRef = ref()
const formRef = ref()
const zoneDrawerRef = 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 form = ref({})
const coverageOptions = [
{ label: '可达', value: 'REACH' },
@@ -150,8 +90,12 @@ const coverageOptions = [
{ label: '偏远', value: 'REMOTE' }
]
function coverageParts(value) {
return String(value || '').split('、').map(item => item.trim()).filter(Boolean)
}
function coverageLabel(value) {
return coverageOptions.find(item => item.value === value)?.label || ''
return coverageOptions.find(item => item.value === value)?.label || value
}
function coverageType(value) {
@@ -160,47 +104,39 @@ function coverageType(value) {
return 'success'
}
function firstPart(value) {
return coverageParts(value)[0] || ''
}
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: 'countryName', component: 'input', placeholder: '国家或二字码', width: '160px' },
{ label: '服务类型', prop: 'serviceType', component: 'input', placeholder: '服务类型', width: '140px' },
{ label: '销售产品', prop: 'productName', component: 'input', placeholder: '销售产品', width: '160px' },
{ 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: 'countryName', minWidth: 120, showOverflowTooltip: true },
{ label: '邮编属性', prop: 'coverage', minWidth: 140, align: 'center', slot: 'coverage' },
{ label: '服务类型', prop: 'serviceType', minWidth: 120, showOverflowTooltip: true },
{ label: '销售产品', prop: 'productName', minWidth: 140, showOverflowTooltip: true },
{ label: '渠道名称', prop: 'channelName', minWidth: 140, showOverflowTooltip: true },
{ label: '分区数', prop: 'zoneCount', width: 90, align: 'center' },
{ label: '邮编总数', prop: 'lineCount', width: 100, align: 'center' },
{ label: '导入时间', prop: 'createTime', width: 170, align: 'center' },
{ label: '操作', prop: 'action', width: 170, align: 'center', fixed: 'right', slot: 'action', showInToolbar: false }
{ label: '操作', prop: 'action', width: 200, 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 reload() {
tableRef.value?.refresh?.() || tableRef.value?.reload?.()
}
function onFile(uploadFile) {
file.value = uploadFile.raw
}
@@ -215,66 +151,48 @@ function submitImport() {
proxy.$modal.msgSuccess(res.data?.message || res.msg || '导入完成')
open.value = false
file.value = null
tableRef.value?.reload?.() || tableRef.value?.getList?.()
reload()
}).finally(() => {
importing.value = false
})
}
function openZones(row) {
zoneDrawerRef.value.open(row)
}
function handleUpdate(row) {
getPayPostcode(row.groupId).then(res => {
form.value = Object.assign({}, res.data || {})
editOpen.value = true
})
form.value = {
countryCode: row.countryCode,
countryName: row.countryName,
serviceType: firstPart(row.serviceType),
productName: firstPart(row.productName),
channelName: firstPart(row.channelName)
}
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?.()
})
updatePayPostcodeCountry(form.value).then(() => {
proxy.$modal.msgSuccess('保存成功')
editOpen.value = false
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?.()
function handleClear(row) {
const name = row.countryName || row.countryCode
proxy.$modal.confirm('清空「' + name + '」下的全部分区和邮编?计费将不再使用这些邮编。').then(() => {
return delPayPostcodeCountry({ countryCode: row.countryCode, countryName: row.countryName })
}).then(() => {
proxy.$modal.msgSuccess('已清空')
reload()
}).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;
}
.mb16 { margin-bottom: 16px; }
.import-tip { margin: 0 0 12px; color: var(--el-text-color-secondary); line-height: 1.5; }
.cov-tag { margin: 0 4px 4px 0; }
</style>
@@ -420,6 +420,7 @@ const keyword = ref('')
const found = ref([])
const products = ref([])
const feeOptions = ref([])
const feeCounts = ref({})
const zoneOptions = ref([])
const enabled = reactive({})
const groups = reactive({})
@@ -455,6 +456,7 @@ function reset() {
found.value = []
products.value = []
feeOptions.value = []
feeCounts.value = {}
zoneOptions.value = []
blankGroups()
saving.value = false
@@ -504,24 +506,32 @@ function loadFees() {
const bound = products.value.slice()
if (!bound.length) {
feeOptions.value = []
feeCounts.value = {}
zoneOptions.value = keepZones([])
return
return Promise.resolve()
}
Promise.all(bound.map(item => getPay(item.feeProductId || item.productId).catch(() => null))).then(results => {
return Promise.all(bound.map(item => getPay(item.feeProductId || item.productId).catch(() => null))).then(results => {
if (token !== feeToken) return
const map = new Map()
const counts = {}
const zones = new Set()
results.forEach(res => {
const local = {}
const rows = res?.data?.surcharges || []
rows.forEach(fee => {
const name = String(fee.feeName || '').trim()
const zone = String(fee.zoneName || '').trim()
if (zone) zones.add(zone)
if (!name || map.has(name)) return
map.set(name, { feeName: name, scId: fee.scId || '' })
if (!name) return
local[name] = (local[name] || 0) + 1
if (!map.has(name)) map.set(name, { feeName: name, scId: fee.scId || '' })
})
Object.keys(local).forEach(name => {
counts[name] = Math.max(counts[name] || 0, local[name])
})
})
zoneOptions.value = keepZones([...zones])
feeCounts.value = counts
for (const row of groups.ALWAYS || []) {
const name = String(row.feeName || '').trim()
if (name && !map.has(name)) map.set(name, { feeName: name, scId: row.scId || '' })
@@ -659,35 +669,48 @@ function save() {
}
}
if (enabled.ALWAYS) {
const names = new Set()
for (const row of groups.ALWAYS) {
const name = String(row.feeName || '').trim()
if (!name) {
if (!String(row.feeName || '').trim()) {
proxy.$modal.msgError('每票都收要选择费用项')
return
}
if (names.has(name)) {
proxy.$modal.msgError('每票都收的费用项不能重复:' + name)
return
}
names.add(name)
}
}
saving.value = true
savePaySurchargeRule({
products: products.value.map(item => ({
productId: item.productId,
productName: item.productName
})),
rules: payloadRules()
}).then(() => {
proxy.$modal.msgSuccess('已保存,计算时按当前生效版本上的附加费执行')
visible.value = false
loadFees().then(() => {
const duplicated = sameTypeFees()
if (duplicated.length) {
proxy.$modal.alert('存在两笔同类型的附加费:' + duplicated.join('、'))
return
}
return savePaySurchargeRule({
products: products.value.map(item => ({
productId: item.productId,
productName: item.productName
})),
rules: payloadRules()
}).then(() => {
proxy.$modal.msgSuccess('已保存,计算时按当前生效版本上的附加费执行')
visible.value = false
})
}).finally(() => {
saving.value = false
})
}
function sameTypeFees() {
if (!enabled.ALWAYS) return []
const seen = new Set()
const duplicated = []
for (const row of groups.ALWAYS) {
const name = String(row.feeName || '').trim()
if (!name) continue
if (seen.has(name) || Number(feeCounts.value[name] || 0) >= 2) duplicated.push(name)
seen.add(name)
}
return [...new Set(duplicated)]
}
defineExpose({ open })
</script>
@@ -281,10 +281,12 @@
<script setup name="PayBill">
import { getToken } from '@/utils/auth'
import useUserStore from '@/store/modules/user'
import { listSupplier } from '@/api/customersuppliermanagement/supplierinfo/supplier'
import { listPayBill, getPayBill, previewPayBill, generatePayBill, voidPayBill, submitPayBill, approvePayBill, rejectPayBill, savePayBillDiff, acceptPayBillDiff, removePayBillLine, savePayBillFile, deletePayBillFile, redPayBill, savePayFinance, exportPayStatement, importPaySupplier } from '@/api/price/payBill'
const { proxy } = getCurrentInstance()
const userStore = useUserStore()
const loading = ref(false)
const rows = ref([])
const total = ref(0)
@@ -480,7 +482,8 @@ function submitRow(row) {
}
function approveRow(row) {
proxy.$modal.confirm('审核通过?蓝字需要发票价税合计、税额和账单一致,审核人不能是提交人。').then(() => approvePayBill(row.billId)).then(() => {
const limit = String(userStore.id) === '1' ? '' : ',审核人不能是提交人'
proxy.$modal.confirm('审核通过?蓝字需要发票价税合计、税额和账单一致' + limit + '。').then(() => approvePayBill(row.billId)).then(() => {
proxy.$modal.msgSuccess('已审核通过')
loadList()
if (detail.value?.billId === row.billId) reloadDetail()
@@ -558,6 +561,11 @@ function onFileUploaded(res) {
fileUrl: res.url || res.fileName
}).then(() => {
proxy.$modal.msgSuccess('凭证已上传')
fileForm.fileType = 'INVOICE'
fileForm.invoiceNo = ''
fileForm.amount = null
fileForm.taxAmount = 0
fileForm.sellerName = ''
reloadDetail()
})
}
@@ -38,8 +38,8 @@
<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:payPayment:confirm']">确认付款</el-button>
<el-button v-if="row.status === 'DRAFT'" link type="danger" @click="voidRow(row)" v-hasPermi="['price:payPayment:void']">作废</el-button>
<el-button v-if="row.status === 'PAID'" link type="warning" @click="reverseRow(row)" v-hasPermi="['price:payPayment:confirm']">反核销</el-button>
<el-button v-if="row.status === 'PAID'" link type="danger" @click="refundRow(row)" v-hasPermi="['price:payPayment:confirm']">退款</el-button>
<el-button v-if="canAct(row)" link type="warning" @click="openAct(row, 'reverse')" v-hasPermi="['price:payPayment:confirm']">反核销</el-button>
<el-button v-if="canAct(row)" link type="danger" @click="openAct(row, 'refund')" v-hasPermi="['price:payPayment:confirm']">退款</el-button>
</template>
</el-table-column>
</el-table>
@@ -119,14 +119,55 @@
</el-table>
</template>
</el-drawer>
<el-dialog v-model="actVisible" :title="actTitle" width="920px" append-to-body>
<div v-loading="actLoading">
<template v-if="actPayment">
<el-descriptions :column="3" border size="small" class="mb8">
<el-descriptions-item label="付款单号">{{ actPayment.paymentNo }}</el-descriptions-item>
<el-descriptions-item label="供应商">{{ actPayment.supplierName }}</el-descriptions-item>
<el-descriptions-item label="币种">{{ actPayment.currency }}</el-descriptions-item>
<el-descriptions-item label="付款金额">{{ actPayment.amount }}</el-descriptions-item>
<el-descriptions-item label="已核销">{{ actPayment.allocAmount || 0 }}</el-descriptions-item>
<el-descriptions-item label="手续费">{{ actPayment.bankFee }}</el-descriptions-item>
<el-descriptions-item label="付款日">{{ actPayment.payDate }}</el-descriptions-item>
<el-descriptions-item label="方式">{{ methodText(actPayment.payMethod) }}</el-descriptions-item>
<el-descriptions-item label="账户">{{ actPayment.bankAccount || '-' }}</el-descriptions-item>
</el-descriptions>
<div class="mb8">核销账单</div>
<el-table :data="actAllocs" border size="small" class="mb8" empty-text="没有仍有效的核销">
<el-table-column prop="billNo" label="账单号" min-width="160" />
<el-table-column prop="allocAmount" label="核销金额" width="120" align="right" />
<el-table-column prop="currency" label="币种" width="80" align="center" />
</el-table>
<div class="mb8">费用明细</div>
<el-table :data="actFees" border size="small" max-height="280" empty-text="没有费用明细">
<el-table-column prop="billNo" label="账单号" min-width="150" />
<el-table-column prop="orderNo" label="订单号" min-width="140" />
<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="110" align="right" />
<el-table-column prop="currency" label="币种" width="80" align="center" />
</el-table>
<div class="act-hint">{{ actHint }}</div>
</template>
</div>
<template #footer>
<el-button @click="actVisible = false">取消</el-button>
<el-button :type="actType === 'refund' ? 'danger' : 'warning'" :loading="actSaving" :disabled="!actPayment" @click="runAct">确认</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup name="PayPayment">
import useUserStore from '@/store/modules/user'
import { listSupplier } from '@/api/customersuppliermanagement/supplierinfo/supplier'
import { listPayPayment, getPayPayment, openPayBills, createPayPayment, confirmPayPayment, voidPayPayment, reversePayPayment, refundPayPayment } from '@/api/price/payPayment'
import { getPayBill } from '@/api/price/payBill'
const { proxy } = getCurrentInstance()
const userStore = useUserStore()
const { bms_currency } = useDict('bms_currency')
const loading = ref(false)
const saving = ref(false)
@@ -145,8 +186,23 @@ const openBills = ref([])
const form = reactive({ supplierId: null, currency: 'CNY', payDate: '', payMethod: 'BANK', bankAccount: '', amount: null, bankFee: 0, exchangeRate: 1 })
const detailVisible = ref(false)
const detail = ref(null)
const actVisible = ref(false)
const actLoading = ref(false)
const actSaving = ref(false)
const actType = ref('reverse')
const actPayment = ref(null)
const actFees = ref([])
const allocTotal = computed(() => openBills.value.reduce((sum, item) => sum + (Number(item.allocAmount) || 0), 0).toFixed(2))
const actTitle = computed(() => actType.value === 'refund' ? '应付退款' : '应付反核销')
const actAllocs = computed(() => (actPayment.value?.allocs || []).filter(item => item.allocStatus !== 'REVERSED'))
const actHint = computed(() => {
const admin = String(userStore.id) === '1'
if (actType.value === 'refund') {
return admin ? '确认后这些核销会冲回,付款单变为已退款。' : '确认后这些核销会冲回,付款单变为已退款。确认人不能自己退款。'
}
return admin ? '确认后这些核销会冲回,账单未付金额会回来。' : '确认后这些核销会冲回,账单未付金额会回来。确认人不能自己反核销。'
})
function statusText(value) {
return statusOptions.find(item => item.value === value)?.label || value || '-'
@@ -156,6 +212,10 @@ function methodText(value) {
return { BANK: '银行转账', CASH: '现金', OTHER: '其他' }[value] || value || '-'
}
function canAct(row) {
return row.status === 'PAID' && Number(row.allocAmount) > 0
}
function loadList() {
loading.value = true
listPayPayment(query).then(res => {
@@ -205,22 +265,46 @@ function openDetail(row) {
})
}
function reverseRow(row) {
proxy.$modal.confirm('反核销这张付款?账单未付金额会回来。确认人不能自己反核销。').then(() => reversePayPayment(row.paymentId)).then(() => {
proxy.$modal.msgSuccess('已反核销')
loadList()
}).catch(() => {})
function openAct(row, type) {
actType.value = type
actPayment.value = null
actFees.value = []
actVisible.value = true
actLoading.value = true
getPayPayment(row.paymentId).then(res => {
actPayment.value = res.data
const allocs = (res.data?.allocs || []).filter(item => item.allocStatus !== 'REVERSED' && item.billId)
return Promise.all(allocs.map(alloc => getPayBill(alloc.billId).then(billRes => {
const bill = billRes.data || {}
return (bill.lines || []).map(line => ({
billNo: alloc.billNo || bill.billNo,
orderNo: line.orderNo,
ladingNo: line.ladingNo,
feeName: line.feeName,
amount: line.amount,
currency: line.currency || bill.currency
}))
}).catch(() => [])))
}).then(groups => {
if (Array.isArray(groups)) actFees.value = groups.flat()
}).finally(() => { actLoading.value = false })
}
function refundRow(row) {
proxy.$modal.confirm('把这笔付款退回?已核销金额会冲回。').then(() => refundPayPayment(row.paymentId)).then(() => {
proxy.$modal.msgSuccess('已退款')
function runAct() {
if (!actPayment.value) return
const request = actType.value === 'refund' ? refundPayPayment : reversePayPayment
actSaving.value = true
request(actPayment.value.paymentId).then(() => {
proxy.$modal.msgSuccess(actType.value === 'refund' ? '已退款' : '已反核销')
actVisible.value = false
loadList()
}).catch(() => {})
if (detail.value?.paymentId === actPayment.value.paymentId) openDetail(actPayment.value)
}).finally(() => { actSaving.value = false })
}
function confirmRow(row) {
proxy.$modal.confirm('确认这张付款?登记人和确认人不能是同一人。').then(() => confirmPayPayment(row.paymentId)).then(() => {
const limit = String(userStore.id) === '1' ? '' : '登记人和确认人不能是同一人。'
proxy.$modal.confirm('确认这张付款?' + limit).then(() => confirmPayPayment(row.paymentId)).then(() => {
proxy.$modal.msgSuccess('已确认付款')
loadList()
}).catch(() => {})
@@ -238,4 +322,5 @@ loadList()
<style scoped>
.mb8 { margin-bottom: 8px; }
.act-hint { margin-top: 8px; }
</style>