Files
BMS/bms-front/src/views/monitor/orderSource/index.vue
T

421 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>