421 lines
14 KiB
Vue
421 lines
14 KiB
Vue
<template>
|
||
<div class="app-container order-source-page">
|
||
<div v-if="syncFloatVisible" class="retry-status-float">
|
||
<el-tooltip :content="syncFloatTooltip" placement="bottom">
|
||
<el-button class="retry-status-button" type="primary" plain size="small" @click="openFilterSync()">
|
||
<span class="retry-progress-ring" :style="{ '--retry-progress': syncProgressDegree + 'deg' }" aria-hidden="true">
|
||
<span class="retry-progress-value">{{ syncProgressText }}</span>
|
||
</span>
|
||
<span class="retry-status-text">
|
||
<strong>{{ syncFloatTitle }}</strong>
|
||
<small>{{ syncFloatSummary }}</small>
|
||
</span>
|
||
</el-button>
|
||
</el-tooltip>
|
||
</div>
|
||
|
||
<basic-table
|
||
ref="tableRef"
|
||
:columns="columns"
|
||
:search-columns="searchColumns"
|
||
:query="queryList"
|
||
row-key="orderId"
|
||
:auto-load="false"
|
||
selection
|
||
search-action-align="center"
|
||
:search-per-row="4"
|
||
>
|
||
<template #toolbar="{ selection }">
|
||
<el-col :span="1.5">
|
||
<el-button
|
||
type="primary"
|
||
plain
|
||
icon="Refresh"
|
||
v-hasPermi="['monitor:orderSource:sync']"
|
||
@click="openFilterSync()"
|
||
>同步计费</el-button>
|
||
</el-col>
|
||
<el-col :span="1.5">
|
||
<el-button
|
||
type="success"
|
||
plain
|
||
icon="Search"
|
||
:loading="checkLoading"
|
||
v-hasPermi="['monitor:orderSource: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>
|
||
<el-col :span="1.5">
|
||
<el-button
|
||
type="warning"
|
||
plain
|
||
icon="Refresh"
|
||
:disabled="!selection.length"
|
||
:loading="batchLoading"
|
||
v-hasPermi="['monitor:orderSource:sync']"
|
||
@click="handleBatchSync(selection)"
|
||
>同步选中</el-button>
|
||
</el-col>
|
||
</template>
|
||
<template #col-product="{ row }">
|
||
{{ row.productName || '-' }} ({{ row.productCode || '-' }})
|
||
</template>
|
||
<template #col-country="{ row }">
|
||
{{ row.countryName || '-' }} ({{ row.country || '-' }})
|
||
</template>
|
||
<template #col-orderStatus="{ row }">
|
||
{{ orderStatusLabel(row.orderStatus) }}
|
||
</template>
|
||
<template #col-action="{ row }">
|
||
<el-button
|
||
link
|
||
type="warning"
|
||
icon="Refresh"
|
||
:loading="syncingKey === rowKey(row)"
|
||
v-hasPermi="['monitor:orderSource:sync']"
|
||
@click="handleSyncOne(row)"
|
||
>同步计费</el-button>
|
||
</template>
|
||
</basic-table>
|
||
|
||
<order-source-sync-dialog
|
||
ref="syncDialogRef"
|
||
@progress="onSyncProgress"
|
||
@success="onSyncSuccess"
|
||
/>
|
||
<order-source-sync-log-drawer ref="logDrawerRef" />
|
||
<order-source-product-check-drawer ref="productCheckDrawerRef" />
|
||
</div>
|
||
</template>
|
||
|
||
<script setup name="OrderSource">
|
||
import { ElMessage } from 'element-plus'
|
||
import { listOrderSource, syncOrderSourceSelected } from '@/api/monitor/orderSource'
|
||
import OrderSourceSyncDialog from './components/OrderSourceSyncDialog.vue'
|
||
import OrderSourceSyncLogDrawer from './components/OrderSourceSyncLogDrawer.vue'
|
||
import OrderSourceProductCheckDrawer from './components/OrderSourceProductCheckDrawer.vue'
|
||
|
||
const { proxy } = getCurrentInstance()
|
||
const tableRef = ref()
|
||
const syncDialogRef = ref()
|
||
const logDrawerRef = ref()
|
||
const productCheckDrawerRef = ref()
|
||
const checkLoading = ref(false)
|
||
const syncingKey = ref(null)
|
||
const batchLoading = ref(false)
|
||
const syncFloat = ref(null)
|
||
|
||
const searchColumns = [
|
||
{
|
||
// 客户名称或客户代码混合录入,一行一个
|
||
label: '客户',
|
||
prop: 'customerKeywords',
|
||
component: 'textarea',
|
||
placeholder: '客户名称/客户代码,一行一个',
|
||
props: {
|
||
rows: 2,
|
||
autosize: { minRows: 1, maxRows: 4 },
|
||
clearable: true
|
||
}
|
||
},
|
||
{
|
||
// 订单号/运单号/转单号混合录入,一行一个;有单号即可查询,不要求客户+提货时间
|
||
label: '单号',
|
||
prop: 'orderIdentifiers',
|
||
component: 'textarea',
|
||
placeholder: '订单号/运单号/转单号,一行一个,支持数万个',
|
||
props: {
|
||
rows: 2,
|
||
autosize: { minRows: 1, maxRows: 4 },
|
||
clearable: true
|
||
}
|
||
},
|
||
{
|
||
label: '提货时间',
|
||
prop: 'dateRange',
|
||
component: 'datetimerange',
|
||
colSpan: 2,
|
||
props: {
|
||
clearable: true,
|
||
defaultTime: [new Date(2000, 0, 1, 0, 0, 0), new Date(2000, 0, 1, 23, 59, 59)]
|
||
}
|
||
}
|
||
]
|
||
|
||
const columns = [
|
||
{ label: '客户订单号', prop: 'orderId', minWidth: 160, showOverflowTooltip: true },
|
||
{ label: '运单号', prop: 'orgOrderId', minWidth: 150, showOverflowTooltip: true },
|
||
{ label: '转单号', prop: 'transferOrderId', minWidth: 160, showOverflowTooltip: true },
|
||
{ label: '轨迹ID', prop: 'trackId', minWidth: 140, showOverflowTooltip: true },
|
||
{ label: '订单状态', prop: 'orderStatus', align: 'center', width: 110, slot: 'orderStatus' },
|
||
{ label: '客户代码', prop: 'customerCode', minWidth: 110, showOverflowTooltip: true },
|
||
{ label: '客户名称', prop: 'customerName', minWidth: 140, showOverflowTooltip: true },
|
||
{ label: '产品', prop: 'product', minWidth: 180, slot: 'product', showOverflowTooltip: true },
|
||
{ label: '产品底级代码', prop: 'productFloorCode', minWidth: 130, showOverflowTooltip: true },
|
||
{ label: '国家', prop: 'country', minWidth: 140, slot: 'country', showOverflowTooltip: true },
|
||
{ label: '收件人邮编', prop: 'consigneePostcode', minWidth: 120, showOverflowTooltip: true },
|
||
{ 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 }
|
||
]
|
||
|
||
const orderStatusMap = {
|
||
0: '预报订单',
|
||
1: '确认订单',
|
||
2: '已中转',
|
||
3: '已收货',
|
||
4: '已装箱',
|
||
5: '已出库',
|
||
6: '转运中',
|
||
7: '已上网',
|
||
8: '已签收',
|
||
9: '已退件',
|
||
10: '已删除'
|
||
}
|
||
|
||
const syncFloatVisible = computed(() => {
|
||
const p = syncFloat.value
|
||
return !!(p && (p.loading || p.finished))
|
||
})
|
||
const syncProgressDegree = computed(() => Math.round(((syncFloat.value?.percent || 0) / 100) * 360))
|
||
const syncProgressText = computed(() => (syncFloat.value?.percent || 0) + '%')
|
||
const syncFloatTitle = computed(() => {
|
||
const st = (syncFloat.value?.status || '').toUpperCase()
|
||
if (syncFloat.value?.loading) return '异步同步计费中'
|
||
if (st === 'SUCCESS') return '同步计费完成'
|
||
if (st === 'FAIL') return '同步计费失败'
|
||
return '异步同步计费'
|
||
})
|
||
const syncFloatSummary = computed(() => {
|
||
const p = syncFloat.value || {}
|
||
return '成功 ' + (p.ok || 0) + ' / 失败 ' + (p.fail || 0) + ' / 总数 ' + (p.total || 0)
|
||
})
|
||
const syncFloatTooltip = computed(() => syncFloat.value?.message || '点击查看详情')
|
||
|
||
function orderStatusLabel(status) {
|
||
if (status === null || status === undefined || status === '') return '-'
|
||
const key = Number(status)
|
||
if (Object.prototype.hasOwnProperty.call(orderStatusMap, key)) {
|
||
return orderStatusMap[key]
|
||
}
|
||
return String(status)
|
||
}
|
||
|
||
function rowKey(row) {
|
||
return (row && (row.orderId || row.transferOrderId || row.orgOrderId)) || ''
|
||
}
|
||
|
||
/** 单次查询可接受的单号上限(后端按 900/批分批走索引,过高会明显增加等待时间) */
|
||
const MAX_ORDER_IDENTIFIERS = 50000
|
||
/** 产品检测走单条 SQL(Oracle IN 上限 1000),与后端保持一致 */
|
||
const MAX_PRODUCT_CHECK_IDENTIFIERS = 1000
|
||
|
||
/** 长文本拆成数组:一行一条,兼容逗号/分号/顿号分隔;不按空格拆(客户名称可能含空格) */
|
||
function splitLines(text) {
|
||
return String(text || '')
|
||
.split(/[\r\n,;,;、]+/)
|
||
.map(s => s.trim())
|
||
.filter(Boolean)
|
||
}
|
||
|
||
function buildFilterQuery(params) {
|
||
const src = params || tableRef.value?.getQueryParams?.() || {}
|
||
const { dateRange, pageNum, pageSize, orderIdentifiers, customerKeywords, ...rest } = src
|
||
const query = { ...rest }
|
||
const identifiers = splitLines(orderIdentifiers)
|
||
const customers = splitLines(customerKeywords)
|
||
if (identifiers.length) query.orderIdentifiers = identifiers
|
||
if (customers.length) query.customerKeywords = customers
|
||
if (Array.isArray(dateRange) && dateRange[0] && dateRange[1]) {
|
||
query.beginTime = dateRange[0]
|
||
query.endTime = dateRange[1]
|
||
}
|
||
return query
|
||
}
|
||
|
||
function queryList(params) {
|
||
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 })
|
||
}
|
||
if (identifierCount > MAX_ORDER_IDENTIFIERS) {
|
||
ElMessage.warning(`单次最多查询 ${MAX_ORDER_IDENTIFIERS} 个单号,当前 ${identifierCount} 个,请减少后再查`)
|
||
return Promise.resolve({ rows: [], total: 0 })
|
||
}
|
||
query.pageNum = params.pageNum
|
||
query.pageSize = params.pageSize
|
||
return listOrderSource(query)
|
||
}
|
||
|
||
function openFilterSync() {
|
||
const q = buildFilterQuery()
|
||
if (!(q.customerKeywords && q.customerKeywords.length) || !q.beginTime || !q.endTime) {
|
||
ElMessage.warning('按条件同步计费需先输入客户并选择提货时间')
|
||
return
|
||
}
|
||
proxy.$modal.confirm('确认按当前筛选分页查询并异步同步计费?', '二次确认').then(() => {
|
||
syncDialogRef.value?.open(q, true)
|
||
}).catch(() => {})
|
||
}
|
||
|
||
function onSyncProgress(payload) {
|
||
syncFloat.value = payload
|
||
if (payload && payload.finished && !payload.loading) {
|
||
window.setTimeout(() => {
|
||
if (syncFloat.value && syncFloat.value.finished && !syncFloat.value.loading) {
|
||
syncFloat.value = null
|
||
}
|
||
}, 8000)
|
||
}
|
||
}
|
||
|
||
function onSyncSuccess() {
|
||
tableRef.value?.refresh()
|
||
}
|
||
|
||
function toSyncRow(row) {
|
||
return {
|
||
orderId: row.orderId,
|
||
orgOrderId: row.orgOrderId,
|
||
transferOrderId: row.transferOrderId,
|
||
customerCode: row.customerCode,
|
||
pickupDate: row.pickupDate
|
||
}
|
||
}
|
||
|
||
|
||
function handleSyncOne(row) {
|
||
proxy.$modal.confirm('确认同步计费该订单?', '二次确认').then(() => {
|
||
syncingKey.value = rowKey(row)
|
||
return syncOrderSourceSelected([toSyncRow(row)])
|
||
}).then(res => {
|
||
const data = res.data || {}
|
||
if ((data.ok || 0) > 0 && !(data.fail || 0)) {
|
||
proxy.$modal.msgSuccess('同步成功')
|
||
} else {
|
||
proxy.$modal.msgError((data.details && data.details[0] && data.details[0].errorMessage) || '同步失败')
|
||
}
|
||
tableRef.value.refresh()
|
||
}).catch(() => {}).finally(() => {
|
||
syncingKey.value = null
|
||
})
|
||
}
|
||
|
||
|
||
function handleCheckProducts() {
|
||
const q = buildFilterQuery()
|
||
if (!(q.customerKeywords && q.customerKeywords.length) || !q.beginTime || !q.endTime) {
|
||
ElMessage.warning('检测产品需先输入客户并选择提货时间')
|
||
return
|
||
}
|
||
const identifierCount = (q.orderIdentifiers || []).length
|
||
if (identifierCount > MAX_PRODUCT_CHECK_IDENTIFIERS) {
|
||
ElMessage.warning(`检测产品最多支持 ${MAX_PRODUCT_CHECK_IDENTIFIERS} 个单号,请减少单号或直接在列表勾选同步`)
|
||
return
|
||
}
|
||
checkLoading.value = true
|
||
productCheckDrawerRef.value?.open(q)
|
||
window.setTimeout(() => { checkLoading.value = false }, 300)
|
||
}
|
||
|
||
function handleBatchSync(selection) {
|
||
if (!selection || !selection.length) return
|
||
proxy.$modal.confirm('确认同步选中的 ' + selection.length + ' 条订单?', '二次确认').then(() => {
|
||
batchLoading.value = true
|
||
return syncOrderSourceSelected(selection.map(toSyncRow))
|
||
}).then(res => {
|
||
const data = res.data || {}
|
||
proxy.$modal.msgSuccess('完成:成功 ' + (data.ok || 0) + ' / 失败 ' + (data.fail || 0))
|
||
tableRef.value.refresh()
|
||
}).catch(() => {}).finally(() => {
|
||
batchLoading.value = false
|
||
})
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.order-source-page .retry-status-float {
|
||
position: fixed;
|
||
top: 92px;
|
||
right: 24px;
|
||
z-index: 3001;
|
||
}
|
||
|
||
.order-source-page .retry-status-button {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
min-width: 200px;
|
||
min-height: 68px;
|
||
padding: 6px 16px 6px 7px;
|
||
border-radius: 36px;
|
||
}
|
||
|
||
.order-source-page .retry-progress-ring {
|
||
position: relative;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex: 0 0 54px;
|
||
width: 54px;
|
||
height: 54px;
|
||
border-radius: 50%;
|
||
background: conic-gradient(
|
||
var(--el-color-primary) 0deg var(--retry-progress, 45deg),
|
||
var(--el-color-primary-light-8) var(--retry-progress, 45deg) 360deg
|
||
);
|
||
}
|
||
|
||
.order-source-page .retry-progress-ring::after {
|
||
position: absolute;
|
||
inset: 5px;
|
||
content: '';
|
||
border-radius: 50%;
|
||
background: var(--el-bg-color);
|
||
}
|
||
|
||
.order-source-page .retry-progress-value {
|
||
position: relative;
|
||
z-index: 1;
|
||
color: var(--el-color-primary);
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
line-height: 1;
|
||
}
|
||
|
||
.order-source-page .retry-status-text {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 3px;
|
||
line-height: 1.2;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.order-source-page .retry-status-text strong {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.order-source-page .retry-status-text small {
|
||
color: var(--el-text-color-secondary);
|
||
font-size: 11px;
|
||
}
|
||
</style>
|