订单数据源增加订单对比功能
This commit is contained in:
@@ -13,6 +13,35 @@ export function listOrderSource(query) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 比对缺失转单号回填搜索专用(不走普通列表) */
|
||||
export function listOrderSourceByTransferNos(query) {
|
||||
const { pageNum, pageSize, ...filters } = query || {}
|
||||
return request({
|
||||
url: '/monitor/orderSource/listByTransferNos',
|
||||
method: 'post',
|
||||
params: { pageNum, pageSize },
|
||||
data: filters,
|
||||
timeout: 300000
|
||||
})
|
||||
}
|
||||
|
||||
/** 订单比对(异步):按客户+提货时间比对转单号 */
|
||||
export function compareOrderSourceByFilter(data) {
|
||||
return request({
|
||||
url: '/monitor/orderSource/compareByFilter',
|
||||
method: 'post',
|
||||
data,
|
||||
timeout: 60000
|
||||
})
|
||||
}
|
||||
|
||||
export function getOrderSourceCompareProgress(asyncTaskId) {
|
||||
return request({
|
||||
url: '/monitor/orderSource/compareTask/' + asyncTaskId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function syncOrderSourceByFilter(data) {
|
||||
return request({
|
||||
url: '/monitor/orderSource/syncByFilter',
|
||||
|
||||
@@ -65,6 +65,26 @@ export function listOrderBill(query) {
|
||||
})
|
||||
}
|
||||
|
||||
/** Order-bill product check: package sales products missing from catalog */
|
||||
export function checkOrderBillProducts(data) {
|
||||
return request({
|
||||
url: '/price/billing/orderBill/checkProducts',
|
||||
method: 'post',
|
||||
data,
|
||||
timeout: 180000
|
||||
})
|
||||
}
|
||||
|
||||
/** Drill-down order bills for one product under product-check filters */
|
||||
export function listOrderBillsByProduct(data) {
|
||||
return request({
|
||||
url: '/price/billing/orderBill/checkProducts/orders',
|
||||
method: 'post',
|
||||
data,
|
||||
timeout: 120000
|
||||
})
|
||||
}
|
||||
|
||||
/** Receivable bill detail */
|
||||
export function getBill(billId) {
|
||||
return request({
|
||||
|
||||
@@ -48,27 +48,11 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="产品" prop="productCodeList">
|
||||
<el-select
|
||||
<el-input
|
||||
v-model="form.productCodeList"
|
||||
:multiple="!isEdit"
|
||||
filterable
|
||||
remote
|
||||
clearable
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
reserve-keyword
|
||||
:placeholder="isEdit ? '空=全部产品' : '多选将按产品展开为多行,空=全部'"
|
||||
:remote-method="searchProduct"
|
||||
:loading="productLoading"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="p in productOptions"
|
||||
:key="p.productCode"
|
||||
:label="(p.productName || '') + ' (' + p.productCode + ')'"
|
||||
:value="p.productCode"
|
||||
/>
|
||||
</el-select>
|
||||
:placeholder="isEdit ? '产品代码,空=全部' : '产品代码,多个用逗号/换行分隔,空=全部'"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="国家" prop="countryCodeList">
|
||||
<el-select
|
||||
@@ -140,7 +124,6 @@
|
||||
|
||||
<script setup name="BizRuleConfig">
|
||||
import { listBizRule, getBizRule, addBizRule, updateBizRule, delBizRule } from '@/api/data/bizRule'
|
||||
import { listProductOptions } from '@/api/price/product'
|
||||
import { listRegionOptions } from '@/api/data/region'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
@@ -151,8 +134,6 @@ const formRef = ref()
|
||||
const open = ref(false)
|
||||
const title = ref('')
|
||||
const isEdit = ref(false)
|
||||
const productLoading = ref(false)
|
||||
const productOptions = ref([])
|
||||
const countryOptions = ref([])
|
||||
|
||||
const statusOptions = [
|
||||
@@ -223,12 +204,21 @@ function toList(v) {
|
||||
return [v]
|
||||
}
|
||||
|
||||
function parseCodes(v) {
|
||||
if (Array.isArray(v)) return v.map(x => String(x).trim()).filter(Boolean)
|
||||
if (v == null || v === '') return []
|
||||
return String(v)
|
||||
.split(/[,;\r\n,;、]+/)
|
||||
.map(x => x.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
form.value = {
|
||||
ruleId: undefined,
|
||||
ruleName: '',
|
||||
ruleType: 'THREE_SIDE',
|
||||
productCodeList: [],
|
||||
productCodeList: '',
|
||||
countryCodeList: [],
|
||||
customerCodeList: [],
|
||||
weightMin: undefined,
|
||||
@@ -238,7 +228,6 @@ function resetForm() {
|
||||
status: '0',
|
||||
remark: ''
|
||||
}
|
||||
productOptions.value = []
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
@@ -260,10 +249,6 @@ function handleUpdate(row) {
|
||||
countryCodeList: d.countryCode || '',
|
||||
customerCodeList: d.customerCode || ''
|
||||
}
|
||||
if (d.productCode) {
|
||||
productOptions.value = [{ productCode: d.productCode, productName: d.productCode }]
|
||||
searchProduct(d.productCode)
|
||||
}
|
||||
title.value = '修改规则'
|
||||
open.value = true
|
||||
loadCountries()
|
||||
@@ -272,7 +257,7 @@ function handleUpdate(row) {
|
||||
|
||||
function buildPayload() {
|
||||
const f = form.value
|
||||
const products = toList(f.productCodeList)
|
||||
const products = parseCodes(f.productCodeList)
|
||||
const countries = toList(f.countryCodeList)
|
||||
const customers = toList(f.customerCodeList)
|
||||
const base = {
|
||||
@@ -325,36 +310,6 @@ function handleDelete(ids) {
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
function searchProduct(keyword) {
|
||||
productLoading.value = true
|
||||
const kw = (keyword || '').trim()
|
||||
const base = { status: '1' }
|
||||
// product_code / product_name are separate likes; query both and merge
|
||||
const reqs = kw
|
||||
? [
|
||||
listProductOptions({ ...base, productCode: kw }),
|
||||
listProductOptions({ ...base, productName: kw })
|
||||
]
|
||||
: [listProductOptions(base)]
|
||||
Promise.all(reqs).then(results => {
|
||||
const map = new Map()
|
||||
results.forEach(res => {
|
||||
;(res.data || res.rows || []).forEach(p => {
|
||||
if (p && p.productCode) map.set(p.productCode, p)
|
||||
})
|
||||
})
|
||||
// keep already-selected options visible
|
||||
toList(form.value.productCodeList).forEach(code => {
|
||||
if (code && !map.has(code)) {
|
||||
const hit = productOptions.value.find(p => p.productCode === code)
|
||||
map.set(code, hit || { productCode: code, productName: code })
|
||||
}
|
||||
})
|
||||
productOptions.value = Array.from(map.values())
|
||||
}).finally(() => {
|
||||
productLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function loadCountries() {
|
||||
if (countryOptions.value.length) return
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="visible"
|
||||
size="720px"
|
||||
:title="drawerTitle"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
@closed="onClosed"
|
||||
>
|
||||
<div v-loading="starting">
|
||||
<el-descriptions :column="2" border size="small" class="mb12">
|
||||
<el-descriptions-item label="任务ID" :span="2">{{ asyncTaskId || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="客户">{{ filterText.customers }}</el-descriptions-item>
|
||||
<el-descriptions-item label="提货时间">{{ filterText.time }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag size="small" :type="statusTagType">{{ statusLabel }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="进度">{{ progress.percent || 0 }}%</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-progress
|
||||
:percentage="Math.min(100, Math.max(0, Number(progress.percent) || 0))"
|
||||
:status="progressStatus"
|
||||
:stroke-width="12"
|
||||
striped
|
||||
:striped-flow="running"
|
||||
class="mb12"
|
||||
/>
|
||||
|
||||
<div class="summary-line">
|
||||
已扫描 {{ progress.scanned || 0 }}
|
||||
/ 本地已有 {{ progress.matched || 0 }}
|
||||
/ 本地缺失 {{ progress.missing || 0 }}
|
||||
</div>
|
||||
<div v-if="progress.message" class="msg-line">{{ progress.message }}</div>
|
||||
<div v-if="progress.error" class="err-line">{{ progress.error }}</div>
|
||||
|
||||
<el-alert
|
||||
class="mb12"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="仅比对转单号:上游 TRANSFER_ORDER_ID 有、本地 transfer_no 没有。异步分页扫描,结果已写入操作记录。"
|
||||
/>
|
||||
|
||||
<div class="json-panel__head">
|
||||
<span>缺失转单号(样本)</span>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
:disabled="!canFillSearch"
|
||||
@click="fillToSearch"
|
||||
>回填到单号搜索</el-button>
|
||||
</div>
|
||||
<el-table :data="missingRows" border size="small" max-height="360">
|
||||
<el-table-column type="index" width="55" label="#" />
|
||||
<el-table-column prop="transferNo" label="转单号" min-width="220" show-overflow-tooltip />
|
||||
</el-table>
|
||||
<el-empty v-if="!running && !(progress.missingTransferNos || []).length" description="暂无缺失转单号" />
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { compareOrderSourceByFilter, getOrderSourceCompareProgress } from '@/api/monitor/orderSource'
|
||||
|
||||
const emit = defineEmits(['fill-search'])
|
||||
|
||||
const visible = ref(false)
|
||||
const starting = ref(false)
|
||||
const asyncTaskId = ref('')
|
||||
const query = ref({})
|
||||
const progress = ref({})
|
||||
let timer = null
|
||||
|
||||
const filterText = computed(() => {
|
||||
const q = query.value || {}
|
||||
const codes = Array.isArray(q.customerCodes) ? q.customerCodes.filter(Boolean) : []
|
||||
const keywords = Array.isArray(q.customerKeywords) ? q.customerKeywords.filter(Boolean) : []
|
||||
return {
|
||||
customers: keywords.join(', ') || codes.join(', ') || '-',
|
||||
time: (q.beginTime || '-') + ' ~ ' + (q.endTime || '-')
|
||||
}
|
||||
})
|
||||
|
||||
const running = computed(() => {
|
||||
const st = (progress.value?.status || '').toUpperCase()
|
||||
return st === 'PENDING' || st === 'RUNNING' || starting.value
|
||||
})
|
||||
|
||||
const drawerTitle = computed(() => {
|
||||
if (running.value) return '订单比对任务进行中'
|
||||
const st = (progress.value?.status || '').toUpperCase()
|
||||
if (st === 'SUCCESS') return '订单比对完成'
|
||||
if (st === 'FAIL') return '订单比对失败'
|
||||
return '订单比对任务'
|
||||
})
|
||||
|
||||
const statusLabel = computed(() => {
|
||||
const st = (progress.value?.status || '').toUpperCase()
|
||||
if (st === 'PENDING') return '排队中'
|
||||
if (st === 'RUNNING') return '扫描中'
|
||||
if (st === 'SUCCESS') return '完成'
|
||||
if (st === 'FAIL') return '失败'
|
||||
return st || '-'
|
||||
})
|
||||
|
||||
const statusTagType = computed(() => {
|
||||
const st = (progress.value?.status || '').toUpperCase()
|
||||
if (st === 'SUCCESS') return 'success'
|
||||
if (st === 'FAIL') return 'danger'
|
||||
if (st === 'RUNNING' || st === 'PENDING') return 'warning'
|
||||
return 'info'
|
||||
})
|
||||
|
||||
const progressStatus = computed(() => {
|
||||
const st = (progress.value?.status || '').toUpperCase()
|
||||
if (st === 'SUCCESS') return 'success'
|
||||
if (st === 'FAIL') return 'exception'
|
||||
return undefined
|
||||
})
|
||||
|
||||
const missingRows = computed(() => {
|
||||
const list = progress.value?.missingTransferNos || []
|
||||
return list.map(tn => ({ transferNo: tn }))
|
||||
})
|
||||
|
||||
const canFillSearch = computed(() => {
|
||||
return !running.value && (progress.value?.missingTransferNos || []).length > 0
|
||||
})
|
||||
|
||||
function fillToSearch() {
|
||||
const list = (progress.value?.missingTransferNos || [])
|
||||
.map(s => String(s || '').trim())
|
||||
.filter(Boolean)
|
||||
if (!list.length) {
|
||||
ElMessage.warning('暂无可回填的缺失转单号')
|
||||
return
|
||||
}
|
||||
const truncated = !!(progress.value?.missingTruncated) || (Number(progress.value?.missing || 0) > list.length)
|
||||
emit('fill-search', { transferNos: list, truncated })
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
function clearTimer() {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
}
|
||||
|
||||
function poll() {
|
||||
if (!asyncTaskId.value) return
|
||||
getOrderSourceCompareProgress(asyncTaskId.value).then(res => {
|
||||
progress.value = res.data || {}
|
||||
const st = (progress.value.status || '').toUpperCase()
|
||||
if (st === 'SUCCESS' || st === 'FAIL') {
|
||||
clearTimer()
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
function open(q) {
|
||||
query.value = q || {}
|
||||
asyncTaskId.value = ''
|
||||
progress.value = {}
|
||||
visible.value = true
|
||||
starting.value = true
|
||||
clearTimer()
|
||||
compareOrderSourceByFilter(query.value).then(res => {
|
||||
asyncTaskId.value = res.data?.asyncTaskId || ''
|
||||
if (!asyncTaskId.value) {
|
||||
ElMessage.error('启动比对任务失败')
|
||||
return
|
||||
}
|
||||
progress.value = { status: 'PENDING', percent: 0, message: 'queued', scanned: 0, matched: 0, missing: 0 }
|
||||
poll()
|
||||
timer = setInterval(poll, 1500)
|
||||
}).catch(() => {
|
||||
ElMessage.error('启动比对任务失败')
|
||||
}).finally(() => {
|
||||
starting.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function onClosed() {
|
||||
clearTimer()
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mb12 { margin-bottom: 12px; }
|
||||
.summary-line {
|
||||
margin-bottom: 6px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.msg-line {
|
||||
margin-bottom: 8px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
.err-line {
|
||||
margin-bottom: 8px;
|
||||
color: #f56c6c;
|
||||
word-break: break-all;
|
||||
}
|
||||
.json-panel__head {
|
||||
margin: 12px 0 6px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -109,6 +109,10 @@ function actionLabel(row) {
|
||||
}
|
||||
if (action === 'ONE') return '单条同步'
|
||||
if (action === 'ONE_FAIL') return '同步失败'
|
||||
if (action === 'COMPARE') return '订单比对'
|
||||
if (action === 'COMPARE_DONE') {
|
||||
return row.status === 'FAIL' ? '比对完成(失败)' : '比对完成'
|
||||
}
|
||||
return action || '-'
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,15 @@
|
||||
@click="openFilterSync()"
|
||||
>同步计费</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="warning"
|
||||
plain
|
||||
icon="Search"
|
||||
v-hasPermi="['monitor:orderSource:list']"
|
||||
@click="openOrderCompare()"
|
||||
>订单比对</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="success"
|
||||
@@ -77,7 +86,7 @@
|
||||
<template #col-orderStatus="{ row }">
|
||||
{{ orderStatusLabel(row.orderStatus) }}
|
||||
</template>
|
||||
<template #col-action="{ row }">
|
||||
<!-- <template #col-action="{ row }">
|
||||
<el-button
|
||||
link
|
||||
type="warning"
|
||||
@@ -86,7 +95,7 @@
|
||||
v-hasPermi="['monitor:orderSource:sync']"
|
||||
@click="handleSyncOne(row)"
|
||||
>同步计费</el-button>
|
||||
</template>
|
||||
</template> -->
|
||||
</basic-table>
|
||||
|
||||
<order-source-sync-dialog
|
||||
@@ -96,25 +105,30 @@
|
||||
/>
|
||||
<order-source-sync-log-drawer ref="logDrawerRef" />
|
||||
<order-source-product-check-drawer ref="productCheckDrawerRef" />
|
||||
<order-source-compare-drawer ref="compareDrawerRef" @fill-search="onCompareFillSearch" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="OrderSource">
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { listOrderSource, syncOrderSourceSelected } from '@/api/monitor/orderSource'
|
||||
import { listOrderSource, listOrderSourceByTransferNos, syncOrderSourceSelected } from '@/api/monitor/orderSource'
|
||||
import OrderSourceSyncDialog from './components/OrderSourceSyncDialog.vue'
|
||||
import OrderSourceSyncLogDrawer from './components/OrderSourceSyncLogDrawer.vue'
|
||||
import OrderSourceProductCheckDrawer from './components/OrderSourceProductCheckDrawer.vue'
|
||||
import OrderSourceCompareDrawer from './components/OrderSourceCompareDrawer.vue'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const tableRef = ref()
|
||||
const syncDialogRef = ref()
|
||||
const logDrawerRef = ref()
|
||||
const productCheckDrawerRef = ref()
|
||||
const compareDrawerRef = ref()
|
||||
const checkLoading = ref(false)
|
||||
const syncingKey = ref(null)
|
||||
const batchLoading = ref(false)
|
||||
const syncFloat = ref(null)
|
||||
/** 比对回填搜索模式:走专用转单号接口,不影响普通列表 */
|
||||
const transferFillMode = ref(false)
|
||||
|
||||
const searchColumns = [
|
||||
{
|
||||
@@ -168,8 +182,8 @@ const columns = [
|
||||
{ label: '计费重', prop: 'dimensionalWeightFactor', align: 'center', width: 100 },
|
||||
{ label: '体积重', prop: 'volumeWeight', align: 'center', width: 100 },
|
||||
{ label: '实重', prop: 'weight', align: 'center', width: 100 },
|
||||
{ label: '提货时间', prop: 'pickupDate', align: 'center', width: 170 },
|
||||
{ label: '操作', prop: 'action', align: 'center', width: 120, fixed: 'right', slot: 'action', showInToolbar: false }
|
||||
{ label: '提货时间', prop: 'pickupDate', align: 'center', width: 170 }
|
||||
// { label: '操作', prop: 'action', align: 'center', width: 120, fixed: 'right', slot: 'action', showInToolbar: false }
|
||||
]
|
||||
|
||||
const orderStatusMap = {
|
||||
@@ -183,7 +197,8 @@ const orderStatusMap = {
|
||||
7: '已上网',
|
||||
8: '已签收',
|
||||
9: '已退件',
|
||||
10: '已删除'
|
||||
10: '已删除',
|
||||
11: '已复核'
|
||||
}
|
||||
|
||||
const syncFloatVisible = computed(() => {
|
||||
@@ -247,12 +262,29 @@ function buildFilterQuery(params) {
|
||||
}
|
||||
|
||||
function queryList(params) {
|
||||
const src = params || tableRef.value?.getQueryParams?.() || {}
|
||||
const identifiers = splitLines(src.orderIdentifiers)
|
||||
if (!identifiers.length) transferFillMode.value = false
|
||||
|
||||
// 回填搜索:走专用接口,不走普通 /list
|
||||
if (transferFillMode.value && identifiers.length) {
|
||||
if (identifiers.length > MAX_ORDER_IDENTIFIERS) {
|
||||
ElMessage.warning(`单次最多查询 ${MAX_ORDER_IDENTIFIERS} 个单号,当前 ${identifiers.length} 个,请减少后再查`)
|
||||
return Promise.resolve({ rows: [], total: 0 })
|
||||
}
|
||||
const customers = splitLines(src.customerKeywords)
|
||||
const fillQuery = { orderIdentifiers: identifiers }
|
||||
if (customers.length) fillQuery.customerKeywords = customers
|
||||
fillQuery.pageNum = params.pageNum
|
||||
fillQuery.pageSize = params.pageSize
|
||||
return listOrderSourceByTransferNos(fillQuery)
|
||||
}
|
||||
|
||||
const query = buildFilterQuery(params)
|
||||
const identifierCount = (query.orderIdentifiers || []).length
|
||||
const hasIdentifier = identifierCount > 0
|
||||
const hasCustomer = !!(query.customerKeywords && query.customerKeywords.length)
|
||||
const hasTime = !!query.beginTime && !!query.endTime
|
||||
// 有任一单号即可查询;无单号时必须客户+提货时间
|
||||
if (!hasIdentifier && !(hasCustomer && hasTime)) {
|
||||
ElMessage.warning('请输入单号,或选择客户和提货时间')
|
||||
return Promise.resolve({ rows: [], total: 0 })
|
||||
@@ -266,6 +298,52 @@ function queryList(params) {
|
||||
return listOrderSource(query)
|
||||
}
|
||||
|
||||
function openOrderCompare() {
|
||||
const q = buildFilterQuery()
|
||||
const hasCustomer = !!(q.customerKeywords && q.customerKeywords.length)
|
||||
const hasTime = !!q.beginTime && !!q.endTime
|
||||
if (!hasCustomer || !hasTime) {
|
||||
ElMessage.warning('订单比对需先输入客户并选择提货时间')
|
||||
return
|
||||
}
|
||||
proxy.$modal.confirm(
|
||||
'将按当前客户与提货时间,异步分页比对上游转单号与本地订单库(仅转单号),确认继续?',
|
||||
'二次确认'
|
||||
).then(() => {
|
||||
// 比对只按客户+时间;不带单号框内容(回填后可能有数千转单号,带上会把比对拖死)
|
||||
const compareQuery = { ...q }
|
||||
delete compareQuery.orderIdentifiers
|
||||
delete compareQuery.orderId
|
||||
delete compareQuery.orgOrderId
|
||||
delete compareQuery.transferOrderId
|
||||
compareDrawerRef.value?.open(compareQuery)
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
/** 比对缺失转单号回填到列表「单号」搜索框并查询 */
|
||||
function onCompareFillSearch(payload) {
|
||||
const list = (payload && payload.transferNos) || []
|
||||
if (!list.length) {
|
||||
ElMessage.warning('暂无可回填的转单号')
|
||||
return
|
||||
}
|
||||
let nos = list
|
||||
if (nos.length > MAX_ORDER_IDENTIFIERS) {
|
||||
nos = nos.slice(0, MAX_ORDER_IDENTIFIERS)
|
||||
ElMessage.warning(`单次最多查询 ${MAX_ORDER_IDENTIFIERS} 个单号,已截取前 ${MAX_ORDER_IDENTIFIERS} 个`)
|
||||
} else if (payload && payload.truncated) {
|
||||
ElMessage.warning(`缺失转单号较多,仅回填样本 ${nos.length} 个`)
|
||||
} else {
|
||||
ElMessage.success(`已回填 ${nos.length} 个转单号并开始搜索`)
|
||||
}
|
||||
transferFillMode.value = true
|
||||
tableRef.value?.setQueryParams?.({
|
||||
orderIdentifiers: nos.join('\n'),
|
||||
pageNum: 1
|
||||
})
|
||||
tableRef.value?.handleQuery?.()
|
||||
}
|
||||
|
||||
function openFilterSync() {
|
||||
const q = buildFilterQuery()
|
||||
const identifierCount = (q.orderIdentifiers || []).length
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<el-drawer v-model="visible" size="920px" title="产品库比对(套餐销售价)" append-to-body destroy-on-close>
|
||||
<div v-loading="loading">
|
||||
<el-empty v-if="!loading && !result" description="暂无结果" />
|
||||
<template v-else-if="result">
|
||||
<el-descriptions :column="3" border size="small" class="mb12">
|
||||
<el-descriptions-item label="筛选订单数">{{ result.orderTotal ?? 0 }}</el-descriptions-item>
|
||||
<el-descriptions-item label="套餐截止时间">{{ result.asOfTime || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="套餐产品数">{{ result.packageProductCount ?? 0 }}</el-descriptions-item>
|
||||
<el-descriptions-item label="库内/缺失">
|
||||
<el-tag type="success" size="small">{{ result.matchedCount ?? 0 }}</el-tag>
|
||||
<span class="sep">/</span>
|
||||
<el-tag type="danger" size="small">{{ result.missingCount ?? 0 }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane :label="missingTabLabel" name="missing">
|
||||
<el-table :data="result.missingProducts || []" border size="small" max-height="520">
|
||||
<el-table-column prop="customerCode" label="客户代码" min-width="110" show-overflow-tooltip />
|
||||
<el-table-column prop="productCode" label="套餐产品代码" min-width="130" show-overflow-tooltip />
|
||||
<el-table-column prop="productName" label="产品名称" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="orderCount" label="订单数" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" :title="'点击查订单'" @click="openOrders(row)">{{ row.orderCount ?? 0 }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="!(result.missingProducts || []).length" description="未发现产品库缺失" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="packageTabLabel" name="package">
|
||||
<el-table :data="result.packageProducts || []" border size="small" max-height="520">
|
||||
<el-table-column prop="customerCode" label="客户代码" min-width="110" show-overflow-tooltip />
|
||||
<el-table-column prop="productCode" label="套餐产品代码" min-width="130" show-overflow-tooltip />
|
||||
<el-table-column prop="productName" label="产品名称" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="产品库" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.inLibrary" size="small" type="success">在库</el-tag>
|
||||
<el-tag v-else size="small" type="danger">不在库</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="orderCount" label="订单数" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openOrders(row)">{{ row.orderCount ?? 0 }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="!(result.packageProducts || []).length" description="客户未绑定套餐产品" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="orderDialogVisible" :title="orderDialogTitle" width="860px" append-to-body destroy-on-close>
|
||||
<el-table v-loading="orderLoading" :data="orderRows" border size="small" max-height="480">
|
||||
<el-table-column prop="orderNo" label="订单号" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="trackingNo" label="运单号" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="transferNo" label="转单号" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="bizDate" label="计费时间" width="170" align="center" />
|
||||
</el-table>
|
||||
<el-empty v-if="!orderLoading && !orderRows.length" description="未查到订单" />
|
||||
</el-dialog>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { checkOrderBillProducts, listOrderBillsByProduct } from '@/api/price/billing'
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const result = ref(null)
|
||||
const activeTab = ref('missing')
|
||||
const lastQuery = ref(null)
|
||||
|
||||
const orderDialogVisible = ref(false)
|
||||
const orderLoading = ref(false)
|
||||
const orderRows = ref([])
|
||||
const orderDialogTitle = ref('产品订单')
|
||||
|
||||
const missingTabLabel = computed(() => {
|
||||
const n = result.value?.missingCount ?? 0
|
||||
return '缺失产品 (' + n + ')'
|
||||
})
|
||||
const packageTabLabel = computed(() => {
|
||||
const n = result.value?.packageProductCount ?? 0
|
||||
return '套餐产品 (' + n + ')'
|
||||
})
|
||||
|
||||
function openOrders(row) {
|
||||
if (!row || !row.productCode || !lastQuery.value) {
|
||||
ElMessage.warning('请先选择客户并选择计费时间')
|
||||
return
|
||||
}
|
||||
const q = {
|
||||
...lastQuery.value,
|
||||
customerCodes: row.customerCode ? [row.customerCode] : lastQuery.value.customerCodes,
|
||||
productCode: row.productCode
|
||||
}
|
||||
orderDialogTitle.value = '产品订单: ' + row.productCode
|
||||
orderDialogVisible.value = true
|
||||
orderRows.value = []
|
||||
orderLoading.value = true
|
||||
listOrderBillsByProduct(q).then(res => {
|
||||
orderRows.value = Array.isArray(res.data) ? res.data : []
|
||||
}).catch(() => {
|
||||
orderRows.value = []
|
||||
}).finally(() => {
|
||||
orderLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function open(query) {
|
||||
const hasCustomer = !!(query && Array.isArray(query.customerCodes) && query.customerCodes.length)
|
||||
const begin = query?.params?.beginBizDate
|
||||
const end = query?.params?.endBizDate
|
||||
if (!query || !hasCustomer || !begin || !end) {
|
||||
ElMessage.warning('请先选择客户并选择计费时间')
|
||||
return
|
||||
}
|
||||
lastQuery.value = { ...query }
|
||||
visible.value = true
|
||||
activeTab.value = 'missing'
|
||||
result.value = null
|
||||
loading.value = true
|
||||
checkOrderBillProducts(query).then(res => {
|
||||
result.value = res.data || null
|
||||
if ((result.value?.missingCount || 0) > 0) {
|
||||
activeTab.value = 'missing'
|
||||
} else {
|
||||
activeTab.value = 'package'
|
||||
}
|
||||
}).catch(() => {
|
||||
result.value = null
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mb12 { margin-bottom: 12px; }
|
||||
.sep { margin: 0 6px; color: var(--el-text-color-secondary); }
|
||||
</style>
|
||||
@@ -24,6 +24,16 @@
|
||||
@click="openFilterRebill()"
|
||||
>重新计费</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="success"
|
||||
plain
|
||||
icon="Search"
|
||||
:loading="checkLoading"
|
||||
v-hasPermi="['price:orderBill:list']"
|
||||
@click="handleCheckProducts()"
|
||||
>检测产品</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="info" plain icon="Document" @click="logDrawerRef.open()">操作记录</el-button>
|
||||
</el-col>
|
||||
@@ -83,6 +93,7 @@
|
||||
</basic-table>
|
||||
<order-source-sync-dialog ref="syncDialogRef" @success="onRebillSuccess" />
|
||||
<order-source-sync-log-drawer ref="logDrawerRef" />
|
||||
<order-bill-product-check-drawer ref="productCheckDrawerRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -92,10 +103,13 @@ import { listOrderBill, listOrderNoOptions, listTrackingNoOptions, listTransferN
|
||||
import { syncOrderSourceSelected } from '@/api/monitor/orderSource'
|
||||
import OrderSourceSyncDialog from '../../monitor/orderSource/components/OrderSourceSyncDialog.vue'
|
||||
import OrderSourceSyncLogDrawer from '../../monitor/orderSource/components/OrderSourceSyncLogDrawer.vue'
|
||||
import OrderBillProductCheckDrawer from './components/OrderBillProductCheckDrawer.vue'
|
||||
|
||||
const tableRef = ref()
|
||||
const syncDialogRef = ref()
|
||||
const logDrawerRef = ref()
|
||||
const productCheckDrawerRef = ref()
|
||||
const checkLoading = ref(false)
|
||||
const batchLoading = ref(false)
|
||||
const tableHeight = ref(480)
|
||||
let tableResizeObserver = null
|
||||
@@ -183,6 +197,16 @@ const searchColumns = [
|
||||
defaultTime: [new Date(2000, 0, 1, 0, 0, 0), new Date(2000, 0, 1, 23, 59, 59)]
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '提货时间',
|
||||
prop: 'pickupDateRange',
|
||||
component: 'datetimerange',
|
||||
colSpan: 2,
|
||||
props: {
|
||||
clearable: true,
|
||||
defaultTime: [new Date(2000, 0, 1, 0, 0, 0), new Date(2000, 0, 1, 23, 59, 59)]
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '订单状态',
|
||||
prop: 'orderStatuses',
|
||||
@@ -205,17 +229,26 @@ const searchColumns = [
|
||||
}
|
||||
]
|
||||
|
||||
function queryList(params) {
|
||||
const { bizDateRange, ...rest } = params
|
||||
function applySearchRanges(params) {
|
||||
const { bizDateRange, pickupDateRange, ...rest } = params
|
||||
const query = { ...rest }
|
||||
const rangeParams = {}
|
||||
if (Array.isArray(bizDateRange) && (bizDateRange[0] || bizDateRange[1])) {
|
||||
query.params = {
|
||||
...(query.params || {}),
|
||||
beginBizDate: bizDateRange[0] || null,
|
||||
endBizDate: bizDateRange[1] || null
|
||||
}
|
||||
rangeParams.beginBizDate = bizDateRange[0] || null
|
||||
rangeParams.endBizDate = bizDateRange[1] || null
|
||||
}
|
||||
return listOrderBill(query)
|
||||
if (Array.isArray(pickupDateRange) && (pickupDateRange[0] || pickupDateRange[1])) {
|
||||
rangeParams.beginPickupDate = pickupDateRange[0] || null
|
||||
rangeParams.endPickupDate = pickupDateRange[1] || null
|
||||
}
|
||||
if (Object.keys(rangeParams).length) {
|
||||
query.params = { ...(query.params || {}), ...rangeParams }
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
function queryList(params) {
|
||||
return listOrderBill(applySearchRanges(params))
|
||||
}
|
||||
|
||||
|
||||
@@ -237,6 +270,29 @@ function buildRebillFilter() {
|
||||
return filter
|
||||
}
|
||||
|
||||
|
||||
function buildCheckQuery() {
|
||||
const q = tableRef.value?.getQueryParams?.() || {}
|
||||
return applySearchRanges(q)
|
||||
}
|
||||
|
||||
function handleCheckProducts() {
|
||||
const q = buildCheckQuery()
|
||||
const hasCustomer = Array.isArray(q.customerCodes) && q.customerCodes.length > 0
|
||||
const begin = q.params?.beginBizDate
|
||||
const end = q.params?.endBizDate
|
||||
if (!hasCustomer || !begin || !end) {
|
||||
ElMessage.warning('检测产品需先选择客户并选择计费时间')
|
||||
return
|
||||
}
|
||||
checkLoading.value = true
|
||||
try {
|
||||
productCheckDrawerRef.value?.open(q)
|
||||
} finally {
|
||||
checkLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openFilterRebill() {
|
||||
const identifiers = currentIdentifiers()
|
||||
if (!identifiers.length) {
|
||||
|
||||
Reference in New Issue
Block a user