772 lines
29 KiB
Vue
772 lines
29 KiB
Vue
<template>
|
||
<el-card class="shipping-order-manager">
|
||
<template #header>
|
||
<div class="card-header">发货单管理</div>
|
||
</template>
|
||
<div class="filter-bar">
|
||
<el-input v-model="searchParams.keyword" placeholder="发货单号" clearable style="width: 200px"
|
||
@keyup.enter="handleSearch">
|
||
<template #prefix>
|
||
<el-icon>
|
||
<Search />
|
||
</el-icon>
|
||
</template>
|
||
</el-input>
|
||
<el-select v-model="searchParams.status" placeholder="状态" clearable style="width: 120px">
|
||
<el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||
</el-select>
|
||
<el-select v-model="searchParams.warehouse_id" placeholder="仓库" clearable style="width: 140px">
|
||
<el-option v-for="item in warehouseOptions" :key="item.id" :label="item.name" :value="item.id" />
|
||
</el-select>
|
||
<el-select v-model="searchParams.customer_id" placeholder="客户" clearable filterable style="width: 160px">
|
||
<el-option v-for="item in customerOptions" :key="item.id" :label="item.name" :value="item.id" />
|
||
</el-select>
|
||
<el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
|
||
<el-button :icon="Refresh" @click="resetSearch">重置</el-button>
|
||
</div>
|
||
|
||
<el-table :data="tableData" v-loading="loading" border stripe style="width: 100%"
|
||
@expand-change="handleExpandChange">
|
||
|
||
<!-- 发货单详情展开行 -->
|
||
<el-table-column type="expand">
|
||
<template #default="{ row }">
|
||
<div v-if="detailCache[row.id]" style="padding: 12px 20px">
|
||
<h4 style="margin: 0 0 10px; font-size: 14px; color: #303133">发货明细</h4>
|
||
<el-table :data="detailCache[row.id].items || []" border size="small">
|
||
<el-table-column prop="product_name" label="商品名称" min-width="120" show-overflow-tooltip align="center" />
|
||
<el-table-column prop="product_code" label="ISBN/条码" min-width="100" show-overflow-tooltip
|
||
align="center" />
|
||
<el-table-column label="库位" min-width="100" align="center">
|
||
<template #default="{ row: item }">
|
||
{{ locationMap[item.warehouse_code] || item.warehouse_code || '-' }}##{{
|
||
locationMap[item.location_name]
|
||
|| item.location_name || '-' }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="销售单号" min-width="140" align="center">
|
||
<template #default="{ row: item }">
|
||
<a v-if="item.sales_order_no" style="color: #409eff; cursor: pointer; text-decoration: underline;"
|
||
@click.stop="navigateToSalesOrder(item.sales_order_no)">
|
||
{{ item.sales_order_no }}
|
||
</a>
|
||
<span v-else>-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="出库单号" min-width="140" align="center">
|
||
<template #default="{ row: item }">
|
||
<a v-if="item.outbound_order_no" style="color: #409eff; cursor: pointer; text-decoration: underline;"
|
||
@click.stop="navigateToOutbound(item.outbound_order_no)">
|
||
{{ item.outbound_order_no }}
|
||
</a>
|
||
<span v-else>-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="association_order_no" label="平台单号" min-width="90" align="center">
|
||
<template #default="{ row: item }">
|
||
<span style="display: inline-flex; align-items: center; gap: 4px;">
|
||
<span v-if="item.association_order_no" style="color: #409eff; text-decoration: underline;">{{ item.association_order_no }}</span>
|
||
<span v-else>-</span>
|
||
<el-button v-if="item.association_order_no" type="primary" size="small" link
|
||
@click="copyToClipboard(item.association_order_no, '平台单号已复制')">
|
||
<el-icon>
|
||
<CopyDocument />
|
||
</el-icon>
|
||
</el-button>
|
||
</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="销售时间" min-width="100" align="center">
|
||
<template #default="{ row: item }">{{ formatDateForSale(item.sales_order_created_at) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="售价" min-width="40" align="center">
|
||
<template #default="{ row: item }">
|
||
<span style="color: #e6a23c; font-weight: 600">{{ formatAmount(item.unit_price) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="quantity" label="出库数量" min-width="60" align="center">
|
||
<template #default="{ row: item }">
|
||
<span style="color: #409eff; font-weight: 600">{{ item.quantity }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="快递公司" min-width="100" align="center">
|
||
<template #default="{ row: item }">{{ logisticsCompanyMap[item.logistics_company] ||
|
||
item.logistics_company || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="logistics_no" label="快递单号" min-width="100" align="center">
|
||
<template #default="{ row: item }">{{ item.logistics_no || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="receiver_address" label="收货地址" min-width="100" align="center">
|
||
<template #default="{ row: item }">{{ item.receiver_address || '-' }}</template>
|
||
</el-table-column>
|
||
|
||
</el-table>
|
||
</div>
|
||
<div v-else style="padding: 20px; text-align: center; color: #909399">
|
||
<el-icon class="is-loading" style="margin-right: 6px">
|
||
<Loading />
|
||
</el-icon>加载中...
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
<el-table-column prop="shipping_no" label="发货单号" min-width="160" show-overflow-tooltip align="center" />
|
||
<el-table-column label="客户" min-width="160" align="center">
|
||
<template #default="{ row }">
|
||
<template v-if="row.shop_list && row.shop_list.length > 0">
|
||
<div v-for="(shop, idx) in row.shop_list" :key="idx">
|
||
{{ shop.shop_name }}({{ shop.shop_type_text }})
|
||
</div>
|
||
</template>
|
||
<span v-else>-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="status_text" label="状态" min-width="80" show-overflow-tooltip align="center" />
|
||
<el-table-column prop="operator" label="操作员" min-width="90" align="center">
|
||
<template #default="{ row }">{{ row.operator || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="创建时间" min-width="150" align="center">
|
||
<template #default="{ row }">{{ formatTimestamp(row.created_at) }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="remark" label="备注" min-width="140" show-overflow-tooltip align="center">
|
||
<template #default="{ row }">{{ row.remark || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" align="center" width="100">
|
||
<template #default="{ row }">
|
||
<el-button type="primary" size="small" link @click="handleStartShipping(row)">开始发货</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="" align="center" width="70">
|
||
<template #default="{ row }">
|
||
<el-button type="primary" size="small" link>打单</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
|
||
</el-table>
|
||
|
||
<div class="pagination-wrapper">
|
||
<el-pagination v-model:current-page="pagination.current" v-model:page-size="pagination.pageSize"
|
||
:page-sizes="[10, 20, 50, 100]" :total="pagination.total" layout="total, sizes, prev, pager, next, jumper"
|
||
@size-change="handleSizeChange" @current-change="handleCurrentChange" />
|
||
</div>
|
||
|
||
<!-- 扫码发货弹窗 -->
|
||
<el-dialog v-model="scanDialogVisible" title="扫码发货" width="1000px" :close-on-click-modal="false"
|
||
@opened="onScanDialogOpened" @closed="onScanDialogClosed">
|
||
<div style="padding: 10px">
|
||
<p style="margin-bottom: 12px; color: #606266; text-align: center">
|
||
共 <strong style="color:#409eff">{{ currentScanTotal }}</strong> 件商品,已扫描 <strong style="color:#67c23a">{{
|
||
currentScanIndex }}</strong> 件
|
||
</p>
|
||
|
||
<el-table :data="scanList" border size="small" style="width: 100%" max-height="340"
|
||
:row-class-name="scanRowClassName">
|
||
<el-table-column type="index" label="#" width="40" align="center" />
|
||
<el-table-column prop="product_name" label="商品名称" min-width="130" show-overflow-tooltip />
|
||
<el-table-column prop="product_code" label="ISBN" width="120" align="center" />
|
||
<el-table-column prop="quantity" label="数量" width="60" align="center" />
|
||
<el-table-column prop="association_order_no" label="平台单号" min-width="150" align="center"
|
||
show-overflow-tooltip>
|
||
<template #default="{ row }">
|
||
<span v-if="row.association_order_no" style="display: inline-flex; align-items: center; gap: 4px;">
|
||
<span style="color: #409eff; text-decoration: underline;">{{ row.association_order_no }}</span>
|
||
<el-button type="primary" size="small" link
|
||
@click="copyToClipboard(row.association_order_no, '平台单号已复制')">
|
||
<el-icon>
|
||
<CopyDocument />
|
||
</el-icon>
|
||
</el-button>
|
||
</span>
|
||
<span v-else>-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="快递单号" min-width="120" align="center">
|
||
<template #default="{ row, $index }">
|
||
<span v-if="$index < currentScanIndex && row.logistics_no" style="color: #67c23a;">{{ row.logistics_no
|
||
}}</span>
|
||
<span v-else>-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="售价" width="80" align="center">
|
||
<template #default="{ row }">
|
||
<span style="color: #e6a23c; font-weight: 600">{{ formatAmount(row.unit_price) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="状态" width="90" align="center">
|
||
<template #default="{ row, $index }">
|
||
<template v-if="$index < currentScanIndex || row.logistics_no">
|
||
<el-tag type="success" size="small">已扫</el-tag>
|
||
</template>
|
||
<template v-else-if="$index === processingIndex">
|
||
<el-tag type="warning" size="small" effect="dark">扫描中</el-tag>
|
||
</template>
|
||
<template v-else>
|
||
<el-tag type="info" size="small">待扫</el-tag>
|
||
</template>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<el-input ref="scanInputRef" v-model="scanCode" placeholder="请用扫码枪扫描 ISBN..." @keyup.enter="handleScanSubmit"
|
||
class="hidden-scan-input" />
|
||
<p style="margin-top: 8px; color: #c0c4cc; font-size: 12px; text-align: center">请使用扫码枪扫描商品条码</p>
|
||
</div>
|
||
</el-dialog>
|
||
</el-card>
|
||
</template>
|
||
|
||
<script lang="ts">
|
||
import { defineComponent, ref, reactive, onMounted, computed, nextTick } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { ElMessage } from 'element-plus'
|
||
import { Search, Refresh, View, Loading, CopyDocument } from '@element-plus/icons-vue'
|
||
import dayjs from 'dayjs'
|
||
import axios from 'axios'
|
||
import { copyToClipboard } from '@/utils/clipboard'
|
||
import { fetchShippingOrderList, fetchShippingOrderDetail, updateShippingOrderLogistics } from '@/api/shipping-order'
|
||
import { fetchWarehouseList } from '@/api/warehouse'
|
||
import { createPrintTask } from '@/api/print'
|
||
|
||
/** 状态映射 */
|
||
const STATUS_MAP: Record<number, { label: string; type: string }> = {
|
||
1: { label: '已创建', type: 'info' },
|
||
2: { label: '拣货中', type: 'warning' },
|
||
3: { label: '已完成', type: 'success' },
|
||
4: { label: '已取消', type: 'danger' }
|
||
}
|
||
|
||
export default defineComponent({
|
||
name: 'ShippingOrder',
|
||
setup() {
|
||
const router = useRouter()
|
||
const loading = ref<boolean>(false)
|
||
const tableData = ref<any[]>([])
|
||
|
||
/** 导航到销售订单页面 */
|
||
const navigateToSalesOrder = (salesOrderNo: string) => {
|
||
router.push({ name: 'sales-order', query: { keyword: salesOrderNo } })
|
||
}
|
||
|
||
/** 导航到出库单页面 */
|
||
const navigateToOutbound = (outboundOrderNo: string) => {
|
||
router.push({ name: 'outbound', query: { keyword: outboundOrderNo } })
|
||
}
|
||
|
||
const statusOptions = Object.entries(STATUS_MAP).map(([value, { label }]) => ({
|
||
value: Number(value),
|
||
label
|
||
}))
|
||
|
||
const searchParams = reactive<{
|
||
keyword: string
|
||
status: number | null
|
||
warehouse_id: number | null
|
||
customer_id: number | null
|
||
}>({
|
||
keyword: '',
|
||
status: null,
|
||
warehouse_id: null,
|
||
customer_id: null
|
||
})
|
||
|
||
const pagination = reactive({
|
||
current: 1,
|
||
pageSize: 10,
|
||
total: 0
|
||
})
|
||
|
||
const detailVisible = ref<boolean>(false)
|
||
const detailData = ref<any>(null)
|
||
|
||
// 下拉选项
|
||
const warehouseOptions = ref<any[]>([])
|
||
const customerOptions = ref<any[]>([])
|
||
|
||
// ID→名称映射
|
||
const warehouseMap = ref<Record<string, string>>({})
|
||
const customerMap = ref<Record<string, string>>({})
|
||
const salesOrderMap = ref<Record<string, string>>({})
|
||
const waveTaskMap = ref<Record<string, string>>({})
|
||
const locationMap = ref<Record<string, string>>({})
|
||
|
||
// 详情缓存
|
||
const detailCache = ref<Record<number, any>>({})
|
||
|
||
// ========== 扫码发货相关 ==========
|
||
const scanDialogVisible = ref(false)
|
||
const scanCode = ref('')
|
||
const scanInputRef = ref<any>(null)
|
||
const scanList = ref<any[]>([]) // 当前发货单的明细列表
|
||
const currentScanIndex = ref(0) // 当前扫描到的索引
|
||
const currentScanTotal = ref(0) // 总数量
|
||
const scanFocusTimer = ref<ReturnType<typeof setInterval> | null>(null)
|
||
|
||
const currentScanItem = computed(() => scanList.value[currentScanIndex.value] || null)
|
||
|
||
const formatAmount = (amount: number): string => {
|
||
if (!amount && amount !== 0) return '¥0.00'
|
||
return '¥' + (Number(amount) / 100).toFixed(2)
|
||
}
|
||
|
||
// 扫描防并发锁
|
||
const isProcessing = ref(false)
|
||
const processingIndex = ref(-1)
|
||
|
||
/** 已扫描完成的行添加背景色 */
|
||
const scanRowClassName = ({ row, rowIndex }: { row: any; rowIndex: number }) => {
|
||
if (rowIndex < currentScanIndex.value || row.logistics_no) {
|
||
return 'scan-completed-row'
|
||
}
|
||
return ''
|
||
}
|
||
|
||
/** 点击 "开始发货" */
|
||
const handleStartShipping = async (row: any) => {
|
||
// 优先用缓存,否则重新请求
|
||
let detail = detailCache.value[row.id]
|
||
if (!detail || !detail.items) {
|
||
try {
|
||
detail = await fetchShippingOrderDetail(row.id)
|
||
detailCache.value[row.id] = detail
|
||
} catch (e) {
|
||
ElMessage.error('加载发货明细失败')
|
||
return
|
||
}
|
||
}
|
||
const items = detail?.items || []
|
||
if (items.length === 0) {
|
||
ElMessage.warning('该发货单没有明细')
|
||
return
|
||
}
|
||
scanList.value = items.map((item: any) => ({ ...item, shipping_order_id: row.id }))
|
||
// 根据实际数据设置初始状态:已有物流单号的视为已扫描
|
||
const firstUnscanned = scanList.value.findIndex((item: any) => !item.logistics_no)
|
||
currentScanIndex.value = firstUnscanned >= 0 ? firstUnscanned : scanList.value.length
|
||
currentScanTotal.value = items.length
|
||
scanCode.value = ''
|
||
scanDialogVisible.value = true
|
||
}
|
||
|
||
/** 弹窗打开后聚焦并锁定焦点到隐藏 input */
|
||
const onScanDialogOpened = () => {
|
||
const focusInput = () => {
|
||
scanInputRef.value?.focus?.()
|
||
scanInputRef.value?.$el?.querySelector?.('input')?.focus?.()
|
||
}
|
||
// 立刻聚焦一次
|
||
nextTick(() => { focusInput() })
|
||
// 每隔 200ms 强制保持焦点(直到弹窗关闭)
|
||
const intervalId = setInterval(() => {
|
||
if (!scanDialogVisible.value) {
|
||
clearInterval(intervalId)
|
||
return
|
||
}
|
||
// 检查当前焦点是否已不在该输入框上,是则重新聚焦
|
||
const active = document.activeElement
|
||
const inputEl = scanInputRef.value?.$el?.querySelector?.('input')
|
||
if (inputEl && active !== inputEl) {
|
||
inputEl.focus()
|
||
}
|
||
}, 200)
|
||
// dialog 关闭时清理 interval
|
||
const origClose = onScanDialogClosed
|
||
const cleanup = () => {
|
||
clearInterval(intervalId)
|
||
origClose()
|
||
}
|
||
}
|
||
|
||
/** 弹窗关闭时清理 */
|
||
const onScanDialogClosed = () => {
|
||
scanCode.value = ''
|
||
scanList.value = []
|
||
currentScanIndex.value = 0
|
||
currentScanTotal.value = 0
|
||
}
|
||
|
||
/** 扫码回车处理 */
|
||
const handleScanSubmit = async () => {
|
||
const code = scanCode.value.trim()
|
||
if (!code) return
|
||
|
||
// 如果正在处理中,忽略新的扫码
|
||
if (isProcessing.value) return
|
||
|
||
// 锁定
|
||
isProcessing.value = true
|
||
processingIndex.value = currentScanIndex.value
|
||
|
||
// 在整个发货单明细中查找 ISBN 匹配的商品
|
||
const matchIndex = scanList.value.findIndex(
|
||
(item: any) => item.product_code === code
|
||
)
|
||
|
||
if (matchIndex === -1) {
|
||
ElMessage.warning(`未找到 ISBN 为 ${code} 的商品`)
|
||
isProcessing.value = false
|
||
processingIndex.value = -1
|
||
scanCode.value = ''
|
||
return
|
||
}
|
||
|
||
// 匹配成功:交换当前扫描位与匹配位(把匹配商品提到当前位置)
|
||
if (matchIndex !== currentScanIndex.value) {
|
||
const temp = scanList.value[currentScanIndex.value]
|
||
scanList.value[currentScanIndex.value] = scanList.value[matchIndex]
|
||
scanList.value[matchIndex] = temp
|
||
}
|
||
|
||
const currentItem = scanList.value[currentScanIndex.value]
|
||
if (!currentItem) {
|
||
isProcessing.value = false
|
||
processingIndex.value = -1
|
||
return
|
||
}
|
||
|
||
const salesPersonId = currentItem.sales_person_id
|
||
if (!salesPersonId) {
|
||
ElMessage.warning('当前商品缺少 sales_person_id')
|
||
isProcessing.value = false
|
||
processingIndex.value = -1
|
||
scanCode.value = ''
|
||
return
|
||
}
|
||
|
||
try {
|
||
// 第一步:获取快递账号列表
|
||
const res = await axios.get('https://api.buzhiyushu.cn/zhishu/fastMail/listApi', {
|
||
params: { shopId: salesPersonId }
|
||
})
|
||
console.log('fastMail/listApi 响应:', res.data)
|
||
|
||
// 第二步:提取 fastMailType=1 的成员,调用创建面单接口
|
||
const data = res.data?.data
|
||
if (!Array.isArray(data) || data.length === 0) {
|
||
ElMessage.error('未获取到快递账号配置')
|
||
isProcessing.value = false
|
||
processingIndex.value = -1
|
||
scanCode.value = ''
|
||
return
|
||
}
|
||
|
||
const fastMail = data.find((item: any) => item.fastMailType === '1')
|
||
if (!fastMail || !currentItem.association_order_no) {
|
||
ElMessage.error('未找到默认快递配置或缺少平台单号')
|
||
isProcessing.value = false
|
||
processingIndex.value = -1
|
||
scanCode.value = ''
|
||
return
|
||
}
|
||
|
||
const formData = new FormData()
|
||
formData.append('type', fastMail.type)
|
||
formData.append('partnerId', fastMail.partnerId)
|
||
formData.append('secret', fastMail.secret)
|
||
formData.append('orderSn', currentItem.association_order_no)
|
||
formData.append('contact', currentItem.warehouse_contact_person)
|
||
formData.append('phoneNumber', currentItem.warehouse_contact_phone)
|
||
formData.append('province', currentItem.warehouse_province)
|
||
formData.append('city', currentItem.warehouse_city)
|
||
formData.append('area', currentItem.warehouse_district)
|
||
formData.append('town', currentItem.warehouse_address)
|
||
const createRes = await axios.post('https://print.buzhiyushu.cn/api/print/createOrderBatch', formData)
|
||
console.log('createOrderBatch 响应:', createRes.data)
|
||
|
||
// 第三步:获取打印PDF信息
|
||
const createData = await axios.get('https://print.buzhiyushu.cn/api/print/createBmOrderDaYin', {
|
||
params: {
|
||
mailno: createRes.data.data[0].mail_no || createRes.data.data[0].mailno,
|
||
partnerId: fastMail.partnerId,
|
||
secret: fastMail.secret
|
||
}
|
||
})
|
||
console.log('createBmOrderDaYin 响应:', createData.data)
|
||
|
||
const printData = createRes.data.data[0]
|
||
printData.pdf_info = createData.data.pdfInfo
|
||
printData.itemList = createRes.data.erpGoodsOrderList[0].itemList;
|
||
console.log('createBmOrderDaYin 响应:', JSON.stringify(printData))
|
||
|
||
// 调用createPrintTask接口创建打印任务并打印(失败会阻断后续回填操作)
|
||
console.log('正在创建打印任务并打印...')
|
||
const LODOP = await createPrintTask('yunda', printData) as any
|
||
LODOP.SET_PRINTER_INDEX(localStorage.getItem('printer_express'));
|
||
const printResult = LODOP.PRINT()
|
||
console.log('打印结果:', printResult)
|
||
|
||
// 调用updateShippingOrderLogistics更新物流信息(失败会阻断后续回填操作)
|
||
if (currentItem.sales_order_item_id) {
|
||
await updateShippingOrderLogistics({
|
||
shipping_order_id: currentItem.shipping_order_id,
|
||
sales_order_item_id: currentItem.sales_order_item_id,
|
||
logistics_company: fastMail.type,
|
||
logistics_no: createRes.data.data[0].mail_no || createRes.data.data[0].mailno
|
||
})
|
||
}
|
||
|
||
console.log('正在回填快递单号并更新状态...')
|
||
const submitCompany = await axios.post('https://api.buzhiyushu.cn/zhishu/orderExternalGoods/submitCompanyOrder', {
|
||
code: fastMail.type,
|
||
orderNo: createRes.data.data[0].mail_no || createRes.data.data[0].mailno,
|
||
erpOrderId: createRes.data.erpGoodsOrderList[0].id.toString()
|
||
}, {
|
||
headers: { 'Content-Type': 'application/json' }
|
||
});
|
||
console.log('回填快递单号 响应:', submitCompany.data)
|
||
|
||
// 回填快递单号到当前行
|
||
scanList.value[currentScanIndex.value].logistics_no = createRes.data.data[0].mail_no || createRes.data.data[0].mailno
|
||
} catch (err) {
|
||
console.error('发货处理失败:', err)
|
||
ElMessage.error('发货处理失败,请重试')
|
||
isProcessing.value = false
|
||
processingIndex.value = -1
|
||
scanCode.value = ''
|
||
return
|
||
}
|
||
|
||
// 解锁,前进到下一个商品
|
||
isProcessing.value = false
|
||
processingIndex.value = -1
|
||
currentScanIndex.value++
|
||
scanCode.value = ''
|
||
|
||
if (currentScanIndex.value >= scanList.value.length) {
|
||
ElMessage.success('全部扫描完成!')
|
||
scanDialogVisible.value = false
|
||
}
|
||
}
|
||
|
||
const statusLabel = (status: number): string => STATUS_MAP[status]?.label || '未知'
|
||
const statusTagType = (status: number): string => STATUS_MAP[status]?.type || 'info'
|
||
|
||
const formatTimestamp = (timestamp?: number | string | null): string => {
|
||
if (!timestamp && timestamp !== 0) return '-'
|
||
return dayjs.unix(Number(timestamp)).format('YYYY-MM-DD HH:mm')
|
||
}
|
||
|
||
const formatDate = (date?: number | string | null): string => {
|
||
if (!date) return '-'
|
||
if (typeof date === 'number' && date < 10000000000) {
|
||
return dayjs.unix(date).format('YYYY-MM-DD')
|
||
}
|
||
return dayjs(date).format('YYYY-MM-DD')
|
||
}
|
||
|
||
|
||
const formatDateForSale = (date?: number | string | null): string => {
|
||
if (!date) return '-'
|
||
if (typeof date === 'number' && date < 10000000000) {
|
||
return dayjs.unix(date).format('YYYY-MM-DD HH:mm')
|
||
}
|
||
return dayjs(date).format('YYYY-MM-DD HH:mm')
|
||
}
|
||
|
||
/** 快递公司映射 */
|
||
const logisticsCompanyMap: Record<string, string> = {
|
||
YUNDA: '韵达快递'
|
||
}
|
||
|
||
/** 加载下拉选项和名称映射 */
|
||
const loadOptions = async (): Promise<void> => {
|
||
try {
|
||
// 加载仓库
|
||
const warehouseRes = await fetchWarehouseList({ keyword: '', page: 1, pageSize: 9999 })
|
||
warehouseOptions.value = warehouseRes.list || []
|
||
const wMap: Record<string, string> = {}
|
||
for (const w of warehouseRes.list) {
|
||
wMap[String(w.id)] = w.name || w.code || String(w.id)
|
||
}
|
||
warehouseMap.value = wMap
|
||
|
||
// TODO: 加载客户、销售订单、波次任务、库位选项(需要对应API)
|
||
customerOptions.value = []
|
||
customerMap.value = {}
|
||
} catch (error) {
|
||
// console.error('加载选项失败:', error)
|
||
}
|
||
}
|
||
|
||
/** 加载发货单列表 */
|
||
const loadList = async (): Promise<void> => {
|
||
loading.value = true
|
||
try {
|
||
const res = await fetchShippingOrderList({
|
||
check_no: searchParams.keyword,
|
||
status: searchParams.status || undefined,
|
||
warehouse_id: searchParams.warehouse_id || undefined,
|
||
customer_id: searchParams.customer_id || undefined,
|
||
page: pagination.current,
|
||
pageSize: pagination.pageSize
|
||
})
|
||
tableData.value = res.list || []
|
||
pagination.total = res.total || 0
|
||
} catch (error) {
|
||
// console.error('加载发货单列表失败:', error)
|
||
ElMessage.error('加载发货单列表失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
/** 搜索 */
|
||
const handleSearch = (): void => {
|
||
pagination.current = 1
|
||
loadList()
|
||
}
|
||
|
||
/** 重置搜索 */
|
||
const resetSearch = (): void => {
|
||
searchParams.keyword = ''
|
||
searchParams.status = null
|
||
searchParams.warehouse_id = null
|
||
searchParams.customer_id = null
|
||
pagination.current = 1
|
||
loadList()
|
||
}
|
||
|
||
/** 分页大小变化 */
|
||
const handleSizeChange = (size: number): void => {
|
||
pagination.pageSize = size
|
||
loadList()
|
||
}
|
||
|
||
/** 页码变化 */
|
||
const handleCurrentChange = (page: number): void => {
|
||
pagination.current = page
|
||
loadList()
|
||
}
|
||
|
||
/** 展开行时加载详情 */
|
||
const handleExpandChange = async (row: any, expandedRows: any[]): Promise<void> => {
|
||
if (expandedRows.find((r: any) => r.id === row.id) && !detailCache.value[row.id]) {
|
||
try {
|
||
const detail = await fetchShippingOrderDetail(row.id)
|
||
detailCache.value[row.id] = detail
|
||
} catch (error) {
|
||
// console.error('加载发货单详情失败:', error)
|
||
detailCache.value[row.id] = { items: [] }
|
||
}
|
||
}
|
||
}
|
||
|
||
/** 查看发货单详情 */
|
||
const handleView = async (row: any): Promise<void> => {
|
||
try {
|
||
const detail = await fetchShippingOrderDetail(row.id)
|
||
detailData.value = detail
|
||
detailVisible.value = true
|
||
} catch (error) {
|
||
// console.error('加载发货单详情失败:', error)
|
||
ElMessage.error('加载详情失败')
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadOptions()
|
||
loadList()
|
||
})
|
||
|
||
return {
|
||
loading,
|
||
tableData,
|
||
statusOptions,
|
||
searchParams,
|
||
pagination,
|
||
detailVisible,
|
||
detailData,
|
||
warehouseOptions,
|
||
customerOptions,
|
||
warehouseMap,
|
||
customerMap,
|
||
salesOrderMap,
|
||
waveTaskMap,
|
||
locationMap,
|
||
detailCache,
|
||
scanDialogVisible,
|
||
scanInputRef,
|
||
scanCode,
|
||
scanList,
|
||
currentScanIndex,
|
||
currentScanTotal,
|
||
currentScanItem,
|
||
isProcessing,
|
||
processingIndex,
|
||
formatAmount,
|
||
scanRowClassName,
|
||
handleStartShipping,
|
||
onScanDialogOpened,
|
||
onScanDialogClosed,
|
||
handleScanSubmit,
|
||
statusLabel,
|
||
statusTagType,
|
||
formatTimestamp,
|
||
formatDate,
|
||
formatDateForSale,
|
||
logisticsCompanyMap,
|
||
handleSearch,
|
||
resetSearch,
|
||
handleSizeChange,
|
||
handleCurrentChange,
|
||
handleExpandChange,
|
||
handleView,
|
||
navigateToSalesOrder,
|
||
navigateToOutbound,
|
||
copyToClipboard,
|
||
Search,
|
||
Refresh,
|
||
View,
|
||
Loading,
|
||
CopyDocument
|
||
}
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.shipping-order-manager {
|
||
width: 100%;
|
||
}
|
||
|
||
.card-header {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
}
|
||
|
||
.filter-bar {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.pagination-wrapper {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
margin-top: 16px;
|
||
}
|
||
|
||
:deep(.el-table__expanded-cell) {
|
||
padding: 0;
|
||
}
|
||
|
||
/* 隐藏扫码输入框,仅保留焦点功能 */
|
||
.hidden-scan-input {
|
||
position: absolute;
|
||
left: -9999px;
|
||
opacity: 0;
|
||
width: 1px;
|
||
height: 1px;
|
||
}
|
||
|
||
/* 已完成扫描的行背景色 */
|
||
:deep(.el-table__row.scan-completed-row) {
|
||
background-color: #ecf5ff;
|
||
}
|
||
|
||
:deep(.el-table__row.scan-completed-row:hover > td) {
|
||
background-color: #ecf5ff;
|
||
}
|
||
</style>
|