front/src/views/order/pickupOrder/index.vue
xiaolipro a0dfdffa14
Some checks failed
Build and Deploy Front / deploy (push) Failing after 2m17s
fix:编码乱码
2026-09-07 14:36:45 +08:00

2043 lines
63 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.

<script setup lang="ts">
import { computed, h, nextTick, onMounted, reactive, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
import dayjs from 'dayjs'
import { Modal } from '@arco-design/web-vue'
import { IconInfoCircle } from '@arco-design/web-vue/es/icon'
import HandsontablePro from '@/components/HandsontablePro/index.vue'
import PaginationPro from '@/components/PaginationPro/index.vue'
import ModalPro from '@/components/ModalPro/index.vue'
import useModal from '@/components/ModalPro/useModal'
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
import { useWarehouseAdminCustomer } from '@/composables/useWarehouseAdminCustomer'
import {
addPickupOrderOrders,
cancelPickupOrder,
confirmPickupFreight,
getPickupOrderDetail,
getPickupOrderList,
getPickupOrderStatusCounts,
markPickupInbound,
markPickupPicked,
payPickupOrder,
payPickupOrderBatch,
removePickupOrderOrders,
reschedulePickupOrder,
schedulePickupOrder,
} from '@/api/order/pickupOrder'
import { CustomerService } from '@/api/customer'
import { resolveCurrentCustomer } from '@/api/customer/resolve'
import { $baseMessage } from '@/utils'
import { hasPermission } from '@/utils/rbac-permission'
import { formatDateTime, normalizeListRow, pickField } from '@/utils/warehouseListMapper'
import { notifyCustomerBalanceChanged } from '@/api/customer/accountFlow'
import { renderWarehouseActionBtn, wrapWarehouseActions } from '@/utils/warehouseActionRenderer'
import {
loadDomesticPickupProductOptions,
type DomesticPickupProductOption,
} from '@/utils/domesticPickupProducts'
import CompleteContactModal from '@/components/CompleteContactModal/index.vue'
import type { CustomerProfileCompleteness } from '@/api/customer'
import CreatePickupSheetModal from './components/CreatePickupSheetModal.vue'
import type { CreatePickupSuccessPayload } from './components/CreatePickupSheetModal.vue'
import BindableOrderPicker from './components/BindableOrderPicker.vue'
import type { BindableOrderRow } from './components/BindableOrderPicker.vue'
import { getOrderStatusMeta, resolveOrderStatusCode } from '@/constants/orderStatus'
import { useRbacUserStore } from '@/store/modules/rbac-user'
import { getMapIdFromAccessToken } from '@/utils/jwt'
defineOptions({ name: 'OrderPickupOrder' })
const { t } = useI18n()
const route = useRoute()
const router = useRouter()
const rbacUserStore = useRbacUserStore()
const { tableContainerRef, tableHeight } = useTableContainerHeight()
const {
isAdmin,
customerId: adminCustomerId,
customerOptions,
withCustomerId,
} = useWarehouseAdminCustomer()
const createModalRef = ref<InstanceType<typeof CreatePickupSheetModal> | null>(null)
const contactVisible = ref(false)
const contactProfile = ref<CustomerProfileCompleteness | null>(null)
const keyword = ref('')
const STATUS_TAB_ALL = 'all'
const listLoading = ref(false)
const tableRows = ref<any[]>([])
const currentPage = ref(1)
const pageSize = ref(50)
const totalCount = ref(0)
const statusCounts = ref<Record<string, number>>({
[STATUS_TAB_ALL]: 0,
pendingPayment: 0,
})
const detailLoading = ref(false)
const detail = ref<any>(null)
const [registerDetailModal, { openModal: openDetailModal, closeModal: closeDetailModal }] = useModal() as any
const [registerFreightModal, { openModal: openFreightModal, closeModal: closeFreightModal }] = useModal() as any
const [registerScheduleModal, { openModal: openScheduleModal, closeModal: closeScheduleModal }] = useModal() as any
const [registerRescheduleModal, { openModal: openRescheduleModal, closeModal: closeRescheduleModal }] = useModal() as any
const [registerRebindModal, { openModal: openRebindModal, closeModal: closeRebindModal }] = useModal() as any
const [registerBatchFreightModal, { openModal: openBatchFreightModal, closeModal: closeBatchFreightModal }] =
useModal() as any
const freightForm = reactive({ id: 0, freightAmount: undefined as number | undefined, otherFeeAmount: undefined as number | undefined })
const scheduleForm = reactive({
id: 0,
carrierService: '',
carrierWaybillNo: '',
expectPickupTime: '',
})
const rescheduleForm = reactive({
id: 0,
expectPickupTime: '',
})
const batchFreightForm = reactive({
freightAmount: undefined as number | undefined,
otherFeeAmount: undefined as number | undefined,
})
const rebindForm = reactive({
id: 0,
removeIds: [] as number[],
addOrderIds: [] as number[],
})
const rebindPickerRef = ref<InstanceType<typeof BindableOrderPicker> | null>(null)
const rebindAddRows = ref<BindableOrderRow[]>([])
const carrierServiceOptions = ref<DomesticPickupProductOption[]>([])
const actionLoading = ref(false)
const selectedRows = ref<any[]>([])
const batchPaying = ref(false)
const batchFreighting = ref(false)
const batchScheduling = ref(false)
/** batch schedule uses same schedule modal with multiple ids */
const batchScheduleIds = ref<number[]>([])
const selectedRowCount = computed(() => selectedRows.value.length)
const hasSelectedRows = computed(() => selectedRowCount.value > 0)
const batchPayCandidates = computed(() =>
selectedRows.value.filter((row) => {
const payStatus = String(row?.payStatus || '')
const status = String(row?.status || '')
const amount = Number(row?.receivableAmount)
return payStatus !== 'paid' && status !== 'cancelled' && status !== 'failed' && Number.isFinite(amount) && amount > 0
}),
)
const batchPayAmount = computed(() =>
batchPayCandidates.value.reduce((sum, row) => sum + (Number(row?.receivableAmount) || 0), 0),
)
const batchFreightCandidates = computed(() =>
selectedRows.value.filter((row) => {
const payStatus = String(row?.payStatus || '')
const status = String(row?.status || '')
return payStatus !== 'paid' && status !== 'cancelled' && status !== 'failed'
}),
)
const batchScheduleCandidates = computed(() =>
selectedRows.value.filter((row) => canShowSchedule(String(row?.status || ''))),
)
const statusTabs = [
{ value: STATUS_TAB_ALL, labelKey: 'common.all' },
{ value: 'pendingPayment', labelKey: 'order.pickupOrder.filter.pendingPayment' },
{ value: 'draft', labelKey: 'order.pickupOrder.status.draft' },
{ value: 'scheduled', labelKey: 'order.pickupOrder.status.scheduled' },
{ value: 'dispatched', labelKey: 'order.pickupOrder.status.dispatched' },
{ value: 'picked', labelKey: 'order.pickupOrder.status.picked' },
{ value: 'inbound', labelKey: 'order.pickupOrder.status.inbound' },
{ value: 'cancelled', labelKey: 'order.pickupOrder.status.cancelled' },
{ value: 'failed', labelKey: 'order.pickupOrder.status.failed' },
]
const validStatusSet = new Set(statusTabs.map((tab) => tab.value))
function resolveStatusFromQuery(): string {
const raw = route.query.status
const status = Array.isArray(raw) ? raw[0] : raw
if (typeof status === 'string' && validStatusSet.has(status)) {
return status
}
return STATUS_TAB_ALL
}
const statusFilter = ref(resolveStatusFromQuery())
function tabCount(value: string): number {
return Number(statusCounts.value[value] ?? 0) || 0
}
const slidingStatusTabs = computed(() =>
statusTabs.map((tab) => ({
key: tab.value,
label: t(tab.labelKey),
badge: tabCount(tab.value),
})),
)
function canShowPay(row: { status?: string; payStatus?: string; receivableAmount?: unknown; Status?: string; PayStatus?: string; ReceivableAmount?: unknown }) {
const status = String(row?.status ?? row?.Status ?? '')
const payStatus = String(row?.payStatus ?? row?.PayStatus ?? '')
if (payStatus === 'paid') return false
if (status === 'cancelled' || status === 'failed') return false
const amount = Number(row?.receivableAmount ?? row?.ReceivableAmount)
return Number.isFinite(amount) && amount > 0
}
function canShowSchedule(status: string) {
return status === 'draft' || status === 'scheduled'
}
function canShowRebind(status: string) {
return status === 'draft' || status === 'scheduled'
}
function canShowCancel(status: string, payStatus: string) {
if (payStatus === 'paid') return false
return status === 'draft' || status === 'scheduled'
}
function canShowMarkPicked(status: string) {
return status === 'scheduled' || status === 'dispatched'
}
function canShowMarkInbound(status: string) {
return status === 'picked'
}
const statusLabel = (code: string) => {
const key = `order.pickupOrder.status.${code}`
const text = t(key)
return text === key ? code : text
}
const payStatusLabel = (code: string) => {
const key = `order.pickupOrder.payStatus.${code}`
const text = t(key)
return text === key ? code : text
}
const settlementStatusLabel = (code: string) => {
const key = `order.pickupOrder.settlementStatus.${code || 'none'}`
const text = t(key)
return text === key ? code || '-' : text
}
const hotColumns = computed(() => [
{ key: 'pickupNo', label: t('order.pickupOrder.table.pickupNo'), width: 160 },
{ key: 'carrierWaybillNo', label: t('order.pickupOrder.table.carrierWaybillNo'), width: 160 },
{ key: 'customerName', label: t('order.pickupOrder.table.customer'), width: 120 },
{ key: 'warehouseName', label: t('order.pickupOrder.table.warehouse'), width: 120 },
{ key: 'expectPickupTime', label: t('order.pickupOrder.table.expectTime'), width: 160 },
{ key: 'parcelCount', label: t('order.pickupOrder.table.parcelCount'), width: 80 },
{ key: 'statusName', label: t('order.pickupOrder.table.status'), width: 100 },
{ key: 'payStatusName', label: t('order.pickupOrder.table.payStatus'), width: 90 },
{ key: 'settlementStatusName', label: t('order.pickupOrder.table.settlement'), width: 90 },
{ key: 'receivableAmount', label: t('order.pickupOrder.table.receivable'), width: 100 },
{ key: 'carrierService', label: t('order.pickupOrder.table.carrierService'), width: 140 },
{ key: 'createTime', label: t('order.pickupOrder.table.createTime'), width: 160 },
])
function mapRow(raw: any) {
const row = normalizeListRow(raw)
const id = pickField(row, 'id', 'Id')
const status = String(pickField(row, 'status', 'Status') || '')
const payStatus = String(pickField(row, 'payStatus', 'PayStatus') || '')
const settlementStatus = String(pickField(row, 'settlementStatus', 'SettlementStatus') || 'none')
const receivableRaw = pickField(row, 'receivableAmount', 'ReceivableAmount')
return {
id,
customerId: Number(pickField(row, 'customerId', 'CustomerId') || 0) || undefined,
pickupNo: pickField(row, 'pickupNo', 'PickupNo'),
carrierWaybillNo: pickField(row, 'carrierWaybillNo', 'CarrierWaybillNo') || '-',
customerName: pickField(row, 'customerName', 'CustomerName') || '-',
warehouseName: pickField(row, 'warehouseName', 'WarehouseName') || '-',
expectPickupTime: formatDateTime(pickField(row, 'expectPickupTime', 'ExpectPickupTime')),
parcelCount: pickField(row, 'parcelCount', 'ParcelCount') ?? 0,
status,
statusName: statusLabel(status),
payStatus,
payStatusName: payStatusLabel(payStatus),
settlementStatus,
settlementStatusName: settlementStatusLabel(settlementStatus),
receivableAmount: receivableRaw ?? '-',
carrierService: carrierServiceLabel(String(pickField(row, 'carrierService', 'CarrierService') || '')),
createTime: formatDateTime(pickField(row, 'createTime', 'CreateTime')),
}
}
async function ensureCarrierServiceOptions() {
if (carrierServiceOptions.value.length) return
carrierServiceOptions.value = await loadDomesticPickupProductOptions()
}
function carrierServiceLabel(code: string) {
const hit = carrierServiceOptions.value.find((x) => x.value === code)
return hit?.label || code || '-'
}
function actionRenderer(rowData: any) {
if (!hasPermission('Order.Pickup.Create')) {
return wrapWarehouseActions([
renderWarehouseActionBtn('detail', t('order.pickupOrder.actions.detail'), 'view'),
])
}
const status = String(rowData?.status || '')
const payStatus = String(rowData?.payStatus || '')
const parts: string[] = [
renderWarehouseActionBtn('detail', t('order.pickupOrder.actions.detail'), 'view'),
]
if (canShowPay(rowData)) {
parts.push(renderWarehouseActionBtn('pay', t('order.pickupOrder.actions.pay'), 'pay'))
}
if (canShowSchedule(status)) {
parts.push(renderWarehouseActionBtn('schedule', t('order.pickupOrder.actions.draftForecast'), 'publish'))
}
if (canShowRebind(status)) {
parts.push(renderWarehouseActionBtn('rebind', t('order.pickupOrder.actions.rebind'), 'rebind'))
}
if (canShowCancel(status, payStatus)) {
parts.push(renderWarehouseActionBtn('cancel', t('order.pickupOrder.actions.cancel'), 'cancel'))
}
return wrapWarehouseActions(parts)
}
async function loadStatusCounts() {
try {
const params = withCustomerId({})
const res: any = await getPickupOrderStatusCounts(
params.customerId != null ? { customerId: params.customerId } : undefined,
)
const countsRaw = res?.counts ?? res?.Counts ?? {}
const next: Record<string, number> = {
[STATUS_TAB_ALL]: Number(res?.total ?? res?.Total ?? 0) || 0,
pendingPayment: Number(res?.pendingPayment ?? res?.PendingPayment ?? 0) || 0,
}
for (const tab of statusTabs) {
if (tab.value === STATUS_TAB_ALL || tab.value === 'pendingPayment') continue
next[tab.value] = Number(countsRaw[tab.value] ?? countsRaw[tab.value] ?? 0) || 0
}
statusCounts.value = next
} catch {
/* 角标失败不影响列?*/
}
}
async function loadList() {
listLoading.value = true
try {
const filter = statusFilter.value
const body = withCustomerId({
page: currentPage.value,
pageSize: pageSize.value,
keyword: keyword.value.trim() || undefined,
status: filter !== STATUS_TAB_ALL && filter !== 'pendingPayment' ? filter : undefined,
payStatus: filter === 'pendingPayment' ? 'unpaid' : undefined,
})
const [res] = await Promise.all([getPickupOrderList(body), loadStatusCounts()])
tableRows.value = (res.items || []).map(mapRow)
totalCount.value = Number(res.total ?? 0)
selectedRows.value = []
} catch (e: any) {
tableRows.value = []
totalCount.value = 0
$baseMessage(e?.message || t('order.pickupOrder.message.loadFailed'), 'error')
} finally {
listLoading.value = false
}
}
function handleSearch() {
currentPage.value = 1
selectedRows.value = []
void loadList()
}
function handleOpenCreate() {
if (!hasPermission('Order.Pickup.Create')) {
$baseMessage(t('order.pickupOrder.message.noPermission'), 'warning')
return
}
createModalRef.value?.open(adminCustomerId.value)
}
function onCreateSuccess(payload: CreatePickupSuccessPayload = {}) {
currentPage.value = 1
void loadList()
showCreateNextSteps(payload)
}
function showCreateNextSteps(payload: CreatePickupSuccessPayload) {
const id = payload.id || payload.pickupOrderId
const status = String(payload.status || 'draft')
const payStatus = String(payload.payStatus || '')
const amount = Number(payload.receivableAmount)
const canSchedule = canShowSchedule(status)
const canPay =
payStatus !== 'paid' &&
status !== 'cancelled' &&
status !== 'failed' &&
Number.isFinite(amount) &&
amount > 0
Modal.open({
title: t('order.pickupOrder.createNext.title'),
content: () =>
h('div', { class: 'pk-create-next' }, [
h(
'p',
{ style: 'margin:0 0 12px;color:var(--color-text-2);line-height:1.5' },
t('order.pickupOrder.createNext.desc', {
no: payload.pickupNo || payload.serviceNo || '',
}),
),
h(
'div',
{ style: 'display:flex;flex-wrap:wrap;gap:8px;justify-content:flex-end' },
[
canSchedule
? h(
'button',
{
class: 'arco-btn arco-btn-secondary arco-btn-size-medium',
style: 'height:32px',
onClick: () => {
Modal.destroyAll()
if (id) void openScheduleById(id)
},
},
t('order.pickupOrder.createNext.goSchedule'),
)
: null,
canPay
? h(
'button',
{
class: 'arco-btn arco-btn-secondary arco-btn-size-medium',
style: 'height:32px',
onClick: () => {
Modal.destroyAll()
if (id) void handlePayById(id)
},
},
t('order.pickupOrder.createNext.goPay'),
)
: null,
h(
'button',
{
class: 'arco-btn arco-btn-primary arco-btn-size-medium',
style: 'height:32px',
onClick: () => {
Modal.destroyAll()
if (id) void openDetail(id)
},
},
t('order.pickupOrder.createNext.viewDetail'),
),
].filter(Boolean),
),
]),
footer: false,
simple: true,
})
}
function onNeedContact(profile: CustomerProfileCompleteness | null) {
contactProfile.value = profile
contactVisible.value = true
}
function onContactSuccess() {
contactVisible.value = false
createModalRef.value?.open(adminCustomerId.value)
}
function handleStatusTabChange() {
handleSearch()
}
function handleSelectionChange(rows: any[]) {
selectedRows.value = rows || []
}
async function loadCustomerBalance(customerId?: number): Promise<number | null> {
try {
if (isAdmin.value && customerId && customerId > 0) {
const data = await CustomerService.getById(customerId)
const raw = data?.accountBalance ?? data?.balance
const n = Number(raw)
return Number.isFinite(n) ? n : 0
}
const mapId = getMapIdFromAccessToken(rbacUserStore.accessToken)
const account = rbacUserStore.userInfo?.account?.trim() || ''
const data = await resolveCurrentCustomer({ mapId, account, force: true })
const raw = data?.accountBalance ?? data?.balance
const n = Number(raw)
return Number.isFinite(n) ? n : 0
} catch {
return null
}
}
function confirmPayWithBalance(opts: {
title: string
amount: number
balance: number | null
onOk: () => void | Promise<void>
}) {
const amount = Number(opts.amount) || 0
const balance = opts.balance
const hasBalance = balance != null && Number.isFinite(balance)
const shortfall = hasBalance ? Math.max(0, amount - (balance as number)) : 0
const insufficient = hasBalance && shortfall > 0
const lines = [
t('order.pickupOrder.payCheck.amount', { amount: amount.toFixed(2) }),
hasBalance
? t('order.pickupOrder.payCheck.balance', { balance: (balance as number).toFixed(2) })
: t('order.pickupOrder.payCheck.balanceUnknown'),
]
if (insufficient) {
lines.push(t('order.pickupOrder.payCheck.shortfall', { shortfall: shortfall.toFixed(2) }))
}
Modal.confirm({
title: opts.title,
content: () =>
h(
'div',
{ style: 'line-height:1.7;color:var(--color-text-2)' },
lines.map((text) => h('div', text)),
),
okText: insufficient
? t('order.pickupOrder.payCheck.payAnyway')
: t('order.pickupOrder.actions.pay'),
okButtonProps: insufficient ? { status: 'warning' } : undefined,
onOk: () => opts.onOk(),
})
}
function handleBatchPay() {
if (!hasPermission('Order.Pickup.Create')) {
$baseMessage(t('order.pickupOrder.message.noPermission'), 'warning')
return
}
if (!selectedRowCount.value) {
$baseMessage(t('order.pickupOrder.message.batchPayNeedSelect'), 'warning')
return
}
if (!batchPayCandidates.value.length) {
$baseMessage(t('order.pickupOrder.message.batchPayNoPayable'), 'warning')
return
}
void (async () => {
const customerIds = [
...new Set(
batchPayCandidates.value
.map((r) => Number(r.customerId) || 0)
.filter((id) => id > 0),
),
]
const balance = await loadCustomerBalance(customerIds.length === 1 ? customerIds[0] : undefined)
confirmPayWithBalance({
title: t('order.pickupOrder.actions.batchPay'),
amount: batchPayAmount.value,
balance,
onOk: () => doBatchPay(),
})
})()
}
async function doBatchPay() {
const ids = batchPayCandidates.value.map((row) => row.id).filter((id) => id != null)
if (!ids.length) return
batchPaying.value = true
try {
const res: any = await payPickupOrderBatch(ids)
notifyCustomerBalanceChanged()
const successCount = Number(res?.successCount ?? res?.SuccessCount ?? 0)
const failCount = Number(res?.failCount ?? res?.FailCount ?? 0)
const message =
res?.message ||
res?.Message ||
t('order.pickupOrder.message.batchPayResult', { success: successCount, fail: failCount })
if (failCount > 0) {
const failItems = (res?.items || res?.Items || [])
.filter((x: any) => !(x?.success ?? x?.Success))
.slice(0, 5)
.map((x: any) => `${x?.pickupNo || x?.PickupNo || x?.id || x?.Id}: ${x?.message || x?.Message || ''}`)
.join('')
$baseMessage(failItems ? `${message}(${failItems}` : message, 'warning')
} else {
$baseMessage(message, 'success')
}
selectedRows.value = []
void loadList()
} catch (e: any) {
$baseMessage(e?.message || t('order.pickupOrder.message.actionFailed'), 'error')
} finally {
batchPaying.value = false
}
}
function handleBatchConfirmFreight() {
if (!hasPermission('Order.Pickup.Create')) {
$baseMessage(t('order.pickupOrder.message.noPermission'), 'warning')
return
}
if (!batchFreightCandidates.value.length) {
$baseMessage(t('order.pickupOrder.message.batchFreightNoCandidates'), 'warning')
return
}
batchFreightForm.freightAmount = undefined
batchFreightForm.otherFeeAmount = undefined
openBatchFreightModal(true)
}
async function submitBatchFreight() {
if (batchFreightForm.freightAmount == null || batchFreightForm.freightAmount < 0) {
$baseMessage(t('order.pickupOrder.message.freightRequired'), 'warning')
return
}
const rows = [...batchFreightCandidates.value]
batchFreighting.value = true
let success = 0
let fail = 0
try {
for (const row of rows) {
try {
await confirmPickupFreight({
id: row.id,
freightAmount: batchFreightForm.freightAmount!,
otherFeeAmount: batchFreightForm.otherFeeAmount,
})
success += 1
} catch {
fail += 1
}
}
closeBatchFreightModal()
$baseMessage(
t('order.pickupOrder.message.batchFreightResult', { success, fail }),
fail > 0 ? 'warning' : 'success',
)
selectedRows.value = []
void loadList()
} finally {
batchFreighting.value = false
}
}
async function handleBatchSchedule() {
if (!hasPermission('Order.Pickup.Create')) {
$baseMessage(t('order.pickupOrder.message.noPermission'), 'warning')
return
}
if (!batchScheduleCandidates.value.length) {
$baseMessage(t('order.pickupOrder.message.batchScheduleNoCandidates'), 'warning')
return
}
await ensureCarrierServiceOptions()
batchScheduleIds.value = batchScheduleCandidates.value
.map((r) => Number(r.id))
.filter((id) => id > 0)
scheduleForm.id = 0
scheduleForm.carrierService = ''
scheduleForm.carrierWaybillNo = ''
scheduleForm.expectPickupTime = dayjs().add(1, 'day').hour(10).minute(0).second(0).format('YYYY-MM-DD HH:mm:ss')
openScheduleModal(true)
}
function handlePageChange(page: number) {
currentPage.value = page
void loadList()
}
function handlePageSizeChange(size: number) {
pageSize.value = size
currentPage.value = 1
void loadList()
}
function handleRowAction(row: any, _rowIndex: number, action?: string) {
if (row?.id == null) return
const key = action || 'detail'
if (key === 'detail') {
void openDetail(row.id)
return
}
if (key === 'schedule') {
void openScheduleById(row.id)
return
}
if (key === 'rebind') {
void openRebind(row.id)
return
}
if (key === 'cancel') {
confirmCancel(row.id)
return
}
if (key === 'pay') {
void handlePayById(row.id)
return
}
if (key === 'markPicked') {
void handleMarkPickedById(row.id)
return
}
if (key === 'markInbound') {
void handleMarkInboundById(row.id)
}
}
async function openDetail(id: number | string) {
detailLoading.value = true
detail.value = null
openDetailModal(true)
try {
detail.value = await getPickupOrderDetail(id)
} catch (e: any) {
$baseMessage(e?.message || t('order.pickupOrder.message.detailFailed'), 'error')
closeDetailModal()
} finally {
detailLoading.value = false
}
}
function openConfirmFreight() {
if (!detail.value) return
freightForm.id = Number(detail.value.id ?? detail.value.Id)
freightForm.freightAmount = Number(detail.value.freightAmount ?? detail.value.FreightAmount ?? 0) || undefined
freightForm.otherFeeAmount = Number(detail.value.otherFeeAmount ?? detail.value.OtherFeeAmount ?? 0) || undefined
openFreightModal(true)
}
async function submitFreight() {
if (freightForm.freightAmount == null || freightForm.freightAmount < 0) {
$baseMessage(t('order.pickupOrder.message.freightRequired'), 'warning')
return
}
actionLoading.value = true
try {
detail.value = await confirmPickupFreight({
id: freightForm.id,
freightAmount: freightForm.freightAmount,
otherFeeAmount: freightForm.otherFeeAmount,
})
closeFreightModal()
$baseMessage(t('order.pickupOrder.message.freightOk'), 'success')
void loadList()
} catch (e: any) {
$baseMessage(e?.message || t('order.pickupOrder.message.actionFailed'), 'error')
} finally {
actionLoading.value = false
}
}
async function handlePay() {
if (!detail.value) return
await handlePayById(detail.value.id ?? detail.value.Id)
}
async function handlePayById(id: number | string) {
actionLoading.value = true
try {
let amount = 0
let customerId: number | undefined
if (detail.value && Number(detail.value.id ?? detail.value.Id) === Number(id)) {
amount = Number(detail.value.receivableAmount ?? detail.value.ReceivableAmount) || 0
customerId = Number(detail.value.customerId ?? detail.value.CustomerId) || undefined
} else {
const row = tableRows.value.find((r) => Number(r.id) === Number(id))
if (row) {
amount = Number(row.receivableAmount) || 0
customerId = row.customerId
} else {
const raw = await getPickupOrderDetail(id)
amount = Number(raw?.receivableAmount ?? raw?.ReceivableAmount) || 0
customerId = Number(raw?.customerId ?? raw?.CustomerId) || undefined
}
}
const balance = await loadCustomerBalance(customerId)
actionLoading.value = false
confirmPayWithBalance({
title: t('order.pickupOrder.actions.pay'),
amount,
balance,
onOk: () => doPayById(id),
})
} catch (e: any) {
actionLoading.value = false
$baseMessage(e?.message || t('order.pickupOrder.message.actionFailed'), 'error')
}
}
async function doPayById(id: number | string) {
actionLoading.value = true
try {
const res = await payPickupOrder(id)
if (detail.value && Number(detail.value.id ?? detail.value.Id) === Number(id)) {
detail.value = res
}
notifyCustomerBalanceChanged()
$baseMessage(t('order.pickupOrder.message.payOk'), 'success')
void loadList()
} catch (e: any) {
$baseMessage(e?.message || t('order.pickupOrder.message.actionFailed'), 'error')
} finally {
actionLoading.value = false
}
}
function fillScheduleForm(raw: any) {
scheduleForm.id = Number(raw.id ?? raw.Id)
scheduleForm.carrierService = String(raw.carrierService ?? raw.CarrierService ?? '')
scheduleForm.carrierWaybillNo = String(raw.carrierWaybillNo ?? raw.CarrierWaybillNo ?? '')
const t0 = raw.expectPickupTime ?? raw.ExpectPickupTime
scheduleForm.expectPickupTime = t0
? dayjs(t0).format('YYYY-MM-DD HH:mm:ss')
: dayjs().add(1, 'day').hour(10).minute(0).second(0).format('YYYY-MM-DD HH:mm:ss')
}
async function openSchedule() {
if (!detail.value) return
batchScheduleIds.value = []
await ensureCarrierServiceOptions()
fillScheduleForm(detail.value)
openScheduleModal(true)
}
async function openScheduleById(id: number | string) {
actionLoading.value = true
batchScheduleIds.value = []
try {
await ensureCarrierServiceOptions()
const raw = await getPickupOrderDetail(id)
fillScheduleForm(raw)
openScheduleModal(true)
} catch (e: any) {
$baseMessage(e?.message || t('order.pickupOrder.message.detailFailed'), 'error')
} finally {
actionLoading.value = false
}
}
async function submitSchedule() {
if (!scheduleForm.carrierService.trim()) {
$baseMessage(t('order.pickupOrder.message.carrierServiceRequired'), 'warning')
return
}
const ids =
batchScheduleIds.value.length > 0
? [...batchScheduleIds.value]
: scheduleForm.id > 0
? [scheduleForm.id]
: []
if (!ids.length) return
actionLoading.value = true
batchScheduling.value = batchScheduleIds.value.length > 0
let success = 0
let fail = 0
try {
for (const id of ids) {
try {
const res = await schedulePickupOrder({
id,
carrierService: scheduleForm.carrierService.trim(),
carrierWaybillNo: scheduleForm.carrierWaybillNo.trim() || undefined,
expectPickupTime: scheduleForm.expectPickupTime || undefined,
})
if (detail.value && Number(detail.value.id ?? detail.value.Id) === id) {
detail.value = res
}
success += 1
} catch {
fail += 1
}
}
closeScheduleModal()
batchScheduleIds.value = []
if (ids.length > 1) {
$baseMessage(
t('order.pickupOrder.message.batchScheduleResult', { success, fail }),
fail > 0 ? 'warning' : 'success',
)
} else if (fail > 0) {
$baseMessage(t('order.pickupOrder.message.actionFailed'), 'error')
} else {
$baseMessage(t('order.pickupOrder.message.scheduleOk'), 'success')
}
selectedRows.value = []
void loadList()
} finally {
actionLoading.value = false
batchScheduling.value = false
}
}
function openReschedule() {
if (!detail.value) return
rescheduleForm.id = Number(detail.value.id ?? detail.value.Id)
const t0 = detail.value.expectPickupTime ?? detail.value.ExpectPickupTime
rescheduleForm.expectPickupTime = t0
? dayjs(t0).format('YYYY-MM-DD HH:mm:ss')
: dayjs().add(1, 'day').hour(10).minute(0).second(0).format('YYYY-MM-DD HH:mm:ss')
openRescheduleModal(true)
}
async function submitReschedule() {
if (!rescheduleForm.expectPickupTime) {
$baseMessage(t('order.pickupOrder.message.pickupTimeRequired'), 'warning')
return
}
actionLoading.value = true
try {
const res = await reschedulePickupOrder({
id: rescheduleForm.id,
expectPickupTime: rescheduleForm.expectPickupTime,
})
detail.value = res
closeRescheduleModal()
$baseMessage(t('order.pickupOrder.message.rescheduleOk'), 'success')
void loadList()
} catch (e: any) {
$baseMessage(e?.message || t('order.pickupOrder.message.actionFailed'), 'error')
} finally {
actionLoading.value = false
}
}
function confirmCancel(id: number | string) {
Modal.confirm({
title: t('order.pickupOrder.actions.cancel'),
content: t('order.pickupOrder.message.cancelConfirm'),
onOk: () => doCancel(id),
})
}
async function doCancel(id: number | string) {
actionLoading.value = true
try {
const res = await cancelPickupOrder(id)
if (detail.value && Number(detail.value.id ?? detail.value.Id) === Number(id)) {
detail.value = res
}
$baseMessage(t('order.pickupOrder.message.cancelOk'), 'success')
void loadList()
} catch (e: any) {
$baseMessage(e?.message || t('order.pickupOrder.message.actionFailed'), 'error')
} finally {
actionLoading.value = false
}
}
async function handleCancel() {
if (!detail.value) return
confirmCancel(detail.value.id ?? detail.value.Id)
}
async function openRebind(id: number | string) {
actionLoading.value = true
try {
const raw = await getPickupOrderDetail(id)
detail.value = raw
rebindForm.id = Number(raw.id ?? raw.Id)
rebindForm.removeIds = []
rebindForm.addOrderIds = []
rebindAddRows.value = []
openRebindModal(true)
await nextTick()
rebindPickerRef.value?.clear()
await rebindPickerRef.value?.reload()
} catch (e: any) {
$baseMessage(e?.message || t('order.pickupOrder.message.detailFailed'), 'error')
} finally {
actionLoading.value = false
}
}
async function openRebindFromDetail() {
if (!detail.value) return
rebindForm.id = Number(detail.value.id ?? detail.value.Id)
rebindForm.removeIds = []
rebindForm.addOrderIds = []
rebindAddRows.value = []
openRebindModal(true)
await nextTick()
rebindPickerRef.value?.clear()
await rebindPickerRef.value?.reload()
}
function onRebindAddChange(rows: BindableOrderRow[]) {
rebindAddRows.value = rows
rebindForm.addOrderIds = rows.map((x) => x.id)
}
async function submitRebind() {
const removeIds = [...rebindForm.removeIds]
const addIds = [...rebindForm.addOrderIds]
if (!removeIds.length && !addIds.length) {
$baseMessage(t('order.pickupOrder.message.rebindEmpty'), 'warning')
return
}
actionLoading.value = true
try {
if (removeIds.length) {
detail.value = await removePickupOrderOrders({ id: rebindForm.id, orderIds: removeIds })
}
if (addIds.length) {
detail.value = await addPickupOrderOrders({ id: rebindForm.id, orderIds: addIds })
}
closeRebindModal()
$baseMessage(t('order.pickupOrder.message.rebindOk'), 'success')
void loadList()
} catch (e: any) {
$baseMessage(e?.message || t('order.pickupOrder.message.actionFailed'), 'error')
} finally {
actionLoading.value = false
}
}
async function handleMarkPicked() {
if (!detail.value) return
await handleMarkPickedById(detail.value.id ?? detail.value.Id)
}
async function handleMarkPickedById(id: number | string) {
actionLoading.value = true
try {
const res = await markPickupPicked(id)
if (detail.value && Number(detail.value.id ?? detail.value.Id) === Number(id)) {
detail.value = res
}
$baseMessage(t('order.pickupOrder.message.pickedOk'), 'success')
void loadList()
} catch (e: any) {
$baseMessage(e?.message || t('order.pickupOrder.message.actionFailed'), 'error')
} finally {
actionLoading.value = false
}
}
async function handleMarkInbound() {
if (!detail.value) return
await handleMarkInboundById(detail.value.id ?? detail.value.Id)
}
async function handleMarkInboundById(id: number | string) {
actionLoading.value = true
try {
const res = await markPickupInbound(id)
if (detail.value && Number(detail.value.id ?? detail.value.Id) === Number(id)) {
detail.value = res
}
$baseMessage(t('order.pickupOrder.message.inboundOk'), 'success')
void loadList()
} catch (e: any) {
$baseMessage(e?.message || t('order.pickupOrder.message.actionFailed'), 'error')
} finally {
actionLoading.value = false
}
}
const detailOrders = computed(() => {
const list = detail.value?.orders ?? detail.value?.Orders ?? []
if (!Array.isArray(list)) return []
return list.map((raw: any) => {
const row = normalizeListRow(raw)
const rawStatus = String(pickField(row, 'orderStatus', 'OrderStatus', 'status', 'Status') || '')
const statusCode = resolveOrderStatusCode({ status: rawStatus, orderStatus: rawStatus })
const meta = getOrderStatusMeta(statusCode || rawStatus)
return {
orderId: Number(pickField(row, 'orderId', 'OrderId', 'id', 'Id') || 0),
serviceNo: pickField(row, 'serviceNo', 'ServiceNo') || '-',
clientNo: pickField(row, 'clientNo', 'ClientNo') || '-',
orderStatus: meta ? t(meta.labelKey) : rawStatus || '-',
}
})
})
const rebindExcludeOrderIds = computed(() =>
detailOrders.value.map((x) => x.orderId).filter((id) => id > 0),
)
const rebindCustomerId = computed(() => {
const raw = detail.value
if (!raw) return adminCustomerId.value
const id = Number(pickField(normalizeListRow(raw), 'customerId', 'CustomerId') || 0)
return id > 0 ? id : adminCustomerId.value
})
const rebindWarehouseId = computed(() => {
const raw = detail.value
if (!raw) return undefined
const id = Number(pickField(normalizeListRow(raw), 'warehouseId', 'WarehouseId') || 0)
return id > 0 ? id : undefined
})
function goLinkedOrder(orderId: number) {
if (!orderId) return
const path = rbacUserStore.isCustomerPortal() ? '/order/myOrders' : '/order/waybillManage'
router.push({ path, query: { orderId: String(orderId) } })
}
async function copyAllServiceNos() {
const nos = detailOrders.value
.map((x) => String(x.serviceNo || '').trim())
.filter((x) => x && x !== '-')
if (!nos.length) {
$baseMessage(t('order.pickupOrder.message.noServiceNos'), 'warning')
return
}
try {
await navigator.clipboard.writeText(nos.join('\n'))
$baseMessage(t('order.pickupOrder.message.copyServiceNosOk'), 'success')
} catch {
$baseMessage(t('order.pickupOrder.message.copyFailed'), 'error')
}
}
const detailOrderColumns = computed(() => [
{
title: t('order.pickupOrder.table.serviceNo'),
dataIndex: 'serviceNo',
slotName: 'serviceNo',
},
{ title: t('order.pickupOrder.table.clientNo'), dataIndex: 'clientNo' },
{ title: t('order.pickupOrder.table.status'), dataIndex: 'orderStatus' },
])
const detailStatus = computed(() => String(detail.value?.status ?? detail.value?.Status ?? ''))
const detailPayStatus = computed(() => String(detail.value?.payStatus ?? detail.value?.PayStatus ?? ''))
const detailCanManage = computed(() => hasPermission('Order.Pickup.Create') && !!detail.value)
const detailFlags = computed(() => {
if (!detail.value) {
return {
pay: false,
schedule: false,
reschedule: false,
rebind: false,
cancel: false,
markPicked: false,
markInbound: false,
}
}
return {
pay: detailCanManage.value && canShowPay(detail.value),
schedule: detailCanManage.value && canShowSchedule(detailStatus.value),
reschedule: detailCanManage.value && canShowSchedule(detailStatus.value),
rebind: detailCanManage.value && canShowRebind(detailStatus.value),
cancel: detailCanManage.value && canShowCancel(detailStatus.value, detailPayStatus.value),
markPicked: detailCanManage.value && canShowMarkPicked(detailStatus.value),
markInbound: detailCanManage.value && canShowMarkInbound(detailStatus.value),
}
})
const detailTimeline = computed(() => {
if (!detail.value) return []
const raw = detail.value
const items: { key: string; label: string; time: string }[] = []
const push = (key: string, labelKey: string, value: unknown) => {
if (value == null || value === '') return
const time = formatDateTime(value)
if (!time || time === '-') return
items.push({ key, label: t(labelKey), time })
}
push('create', 'order.pickupOrder.timeline.createTime', raw.createTime ?? raw.CreateTime)
push('pay', 'order.pickupOrder.timeline.payTime', raw.payTime ?? raw.PayTime)
push(
'pickup',
'order.pickupOrder.timeline.actualPickupTime',
raw.actualPickupTime ?? raw.ActualPickupTime,
)
push('update', 'order.pickupOrder.timeline.updateTime', raw.updateTime ?? raw.UpdateTime)
return items
})
const detailSenderText = computed(() => {
if (!detail.value) return '-'
const name = detail.value.senderName || detail.value.SenderName || ''
const phone = detail.value.senderPhone || detail.value.SenderPhone || ''
const addr = [
detail.value.senderProvince || detail.value.SenderProvince,
detail.value.senderCity || detail.value.SenderCity,
detail.value.senderStreetAddress1 || detail.value.SenderStreetAddress1,
]
.filter(Boolean)
.join(' ')
return [name, phone, addr].filter(Boolean).join(' · ') || '-'
})
const scheduleModalTitle = computed(() =>
batchScheduleIds.value.length > 1
? t('order.pickupOrder.actions.batchSchedule')
: t('order.pickupOrder.actions.draftForecast'),
)
watch(
() => route.query.status,
() => {
statusFilter.value = resolveStatusFromQuery()
currentPage.value = 1
void loadList()
},
)
onMounted(async () => {
await ensureCarrierServiceOptions()
void loadList()
})
</script>
<template>
<div class="pickup-order-container warehouse-list-page">
<CompleteContactModal
v-model:visible="contactVisible"
:profile="contactProfile"
@success="onContactSuccess"
/>
<CreatePickupSheetModal
ref="createModalRef"
@success="onCreateSuccess"
@need-contact="onNeedContact"
/>
<div class="warehouse-list-page-card">
<div class="tabs-actions-section">
<div class="tabs-slot">
<SlidingTabs
v-model="statusFilter"
:items="slidingStatusTabs"
@change="handleStatusTabChange"
/>
</div>
<div class="toolbar-left">
<div class="list-filters">
<a-select
v-if="isAdmin"
v-model="adminCustomerId"
allow-search
allow-clear
:placeholder="t('order.pickupOrder.search.customer')"
style="width: 200px"
@change="handleSearch"
>
<a-option v-for="opt in customerOptions" :key="opt.value" :value="opt.value">
{{ opt.label }}
</a-option>
</a-select>
<a-input
v-model="keyword"
allow-clear
:placeholder="t('order.pickupOrder.search.keyword')"
style="width: 220px"
@press-enter="handleSearch"
/>
<a-button class="app-btn-search" @click="handleSearch">
{{ t('order.pickupOrder.actions.search') }}
</a-button>
</div>
<a-tooltip :content="t('order.pickupOrder.hint.createFromMyOrders')">
<span class="page-hint" tabindex="0" role="img" :aria-label="t('order.pickupOrder.hint.createFromMyOrders')">
<IconInfoCircle />
</span>
</a-tooltip>
</div>
</div>
<div class="table-toolbar">
<div class="actions-right">
<a-button
v-if="hasPermission('Order.Pickup.Create')"
class="btn-add app-btn-primary-soft"
@click="handleOpenCreate"
>
{{ t('order.pickupOrder.actions.create') }}
</a-button>
<a-button
v-if="hasPermission('Order.Pickup.Create')"
class="app-btn-toolbar"
:loading="batchPaying"
:disabled="!hasSelectedRows || batchPaying"
@click="handleBatchPay"
>
{{ t('order.pickupOrder.actions.batchPay') }}
</a-button>
<a-button
v-if="hasPermission('Order.Pickup.Create')"
class="app-btn-toolbar"
:loading="batchFreighting"
:disabled="!hasSelectedRows || batchFreighting"
@click="handleBatchConfirmFreight"
>
{{ t('order.pickupOrder.actions.batchConfirmFreight') }}
</a-button>
<a-button
v-if="hasPermission('Order.Pickup.Create')"
class="app-btn-toolbar"
:loading="batchScheduling"
:disabled="!hasSelectedRows || batchScheduling"
@click="handleBatchSchedule"
>
{{ t('order.pickupOrder.actions.batchSchedule') }}
</a-button>
</div>
<div class="toolbar-right">
<span class="selected-count">
<i18n-t keypath="order.pickupOrder.selectedCount" tag="span">
<template #count>
<span class="count-number">{{ selectedRowCount }}</span>
</template>
</i18n-t>
</span>
<PaginationPro
:total-count="totalCount"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 30, 50, 100]"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
/>
</div>
</div>
<div ref="tableContainerRef" class="list-wrap">
<HandsontablePro
:data="tableRows"
:columns="hotColumns"
:height="tableHeight"
:loading="listLoading"
stretch-columns
show-action-column
:action-renderer="actionRenderer"
@action-click="handleRowAction"
@selection-change="handleSelectionChange"
/>
</div>
</div>
<ModalPro
@register="registerDetailModal"
:title="t('order.pickupOrder.detail.title')"
width="800px"
:mask-closable="false"
>
<a-spin :loading="detailLoading" style="width: 100%">
<template v-if="detail">
<div class="pk-detail">
<div class="pk-detail-header">
<div class="pk-detail-header__main">
<div class="pk-detail-title">
<span class="pk-detail-no">{{ detail.pickupNo || detail.PickupNo }}</span>
<a-tag size="small" :color="detailStatus === 'cancelled' ? 'gray' : 'arcoblue'">
{{ statusLabel(detailStatus) }}
</a-tag>
<a-tag
size="small"
:color="detailPayStatus === 'paid' ? 'green' : 'orangered'"
>
{{ payStatusLabel(detailPayStatus) }}
</a-tag>
</div>
<div class="pk-detail-sub">
{{ t('order.pickupOrder.table.createTime') }}?
{{ formatDateTime(detail.createTime || detail.CreateTime) }}
</div>
</div>
<div class="pk-detail-amount">
<span>{{ t('order.pickupOrder.table.receivable') }}</span>
<b>¥ {{ detail.receivableAmount ?? detail.ReceivableAmount ?? '-' }}</b>
</div>
</div>
<div class="pk-detail-section">
<div class="pk-detail-section__title">{{ t('order.pickupOrder.detail.sectionInfo') }}</div>
<div class="pk-detail-grid">
<div class="pk-detail-field">
<span>{{ t('order.pickupOrder.table.carrierWaybillNo') }}</span>
<b>{{ detail.carrierWaybillNo || detail.CarrierWaybillNo || '-' }}</b>
</div>
<div class="pk-detail-field">
<span>{{ t('order.pickupOrder.table.warehouse') }}</span>
<b>{{ detail.warehouseName || detail.WarehouseName || '-' }}</b>
</div>
<div class="pk-detail-field">
<span>{{ t('order.pickupOrder.table.expectTime') }}</span>
<b>{{ formatDateTime(detail.expectPickupTime || detail.ExpectPickupTime) }}</b>
</div>
<div class="pk-detail-field">
<span>{{ t('order.pickupOrder.table.carrierService') }}</span>
<b>{{ carrierServiceLabel(String(detail.carrierService || detail.CarrierService || '')) }}</b>
</div>
<div class="pk-detail-field">
<span>{{ t('order.pickupOrder.table.settlement') }}</span>
<b>{{
settlementStatusLabel(
String(detail.settlementStatus || detail.SettlementStatus || 'none'),
)
}}</b>
</div>
<div class="pk-detail-field pk-detail-grid-span">
<span>{{ t('order.myOrders.createPickup.senderAddress') }}</span>
<b>{{ detailSenderText }}</b>
</div>
</div>
</div>
<div class="pk-detail-section">
<div class="pk-detail-section__title">
{{ t('order.pickupOrder.detail.orders') }}
<em>{{ detailOrders.length }}</em>
<a-button
v-if="detailOrders.length"
type="text"
size="mini"
class="pk-copy-btn"
@click="copyAllServiceNos"
>
{{ t('order.pickupOrder.actions.copyServiceNos') }}
</a-button>
</div>
<a-table
:data="detailOrders"
:pagination="false"
size="small"
:columns="detailOrderColumns"
:scroll="{ y: 220 }"
>
<template #serviceNo="{ record }">
<a
v-if="record.orderId"
class="pk-link"
href="javascript:;"
@click.prevent="goLinkedOrder(record.orderId)"
>
{{ record.serviceNo }}
</a>
<span v-else>{{ record.serviceNo }}</span>
</template>
</a-table>
</div>
<div v-if="detailTimeline.length" class="pk-detail-section">
<div class="pk-detail-section__title">
{{ t('order.pickupOrder.detail.timeline') }}
</div>
<div class="pk-timeline">
<div v-for="item in detailTimeline" :key="item.key" class="pk-timeline__item">
<span class="pk-timeline__dot" />
<div class="pk-timeline__body">
<div class="pk-timeline__label">{{ item.label }}</div>
<div class="pk-timeline__time">{{ item.time }}</div>
</div>
</div>
</div>
</div>
</div>
</template>
</a-spin>
<template #footer>
<div class="pk-detail-footer">
<div class="pk-detail-footer__left">
<a-button
v-if="detailFlags.cancel"
status="danger"
:loading="actionLoading"
@click="handleCancel"
>
{{ t('order.pickupOrder.actions.cancel') }}
</a-button>
</div>
<div class="pk-detail-footer__right">
<a-button @click="closeDetailModal">{{ t('order.pickupOrder.detail.close') }}</a-button>
<a-button
v-if="detailFlags.rebind"
@click="openRebindFromDetail"
>
{{ t('order.pickupOrder.actions.rebind') }}
</a-button>
<a-button
v-if="detailFlags.reschedule"
@click="openReschedule"
>
{{ t('order.pickupOrder.actions.reschedule') }}
</a-button>
<a-button
v-if="detailFlags.schedule && (detailFlags.pay || detailFlags.markPicked || detailFlags.markInbound)"
@click="openSchedule"
>
{{ t('order.pickupOrder.actions.draftForecast') }}
</a-button>
<a-button
v-if="detailFlags.pay"
@click="openConfirmFreight"
>
{{ t('order.pickupOrder.actions.confirmFreight') }}
</a-button>
<a-button
v-if="detailFlags.markPicked && detailFlags.markInbound"
:loading="actionLoading"
@click="handleMarkPicked"
>
{{ t('order.pickupOrder.actions.markPicked') }}
</a-button>
<a-button
v-if="detailFlags.schedule && !detailFlags.pay && !detailFlags.markPicked && !detailFlags.markInbound"
type="primary"
@click="openSchedule"
>
{{ t('order.pickupOrder.actions.draftForecast') }}
</a-button>
<a-button
v-if="detailFlags.markPicked && !detailFlags.markInbound && !detailFlags.pay"
type="primary"
:loading="actionLoading"
@click="handleMarkPicked"
>
{{ t('order.pickupOrder.actions.markPicked') }}
</a-button>
<a-button
v-if="detailFlags.markInbound && !detailFlags.pay"
type="primary"
:loading="actionLoading"
@click="handleMarkInbound"
>
{{ t('order.pickupOrder.actions.markInbound') }}
</a-button>
<a-button
v-if="detailFlags.pay"
type="primary"
:loading="actionLoading"
@click="handlePay"
>
{{ t('order.pickupOrder.actions.pay') }}
</a-button>
</div>
</div>
</template>
</ModalPro>
<ModalPro
@register="registerFreightModal"
:title="t('order.pickupOrder.actions.confirmFreight')"
:confirm-loading="actionLoading"
@ok="submitFreight"
>
<a-form layout="vertical">
<a-form-item :label="t('order.pickupOrder.form.freight')" required>
<a-input-number v-model="freightForm.freightAmount" :min="0" :precision="2" style="width: 100%" />
</a-form-item>
<a-form-item :label="t('order.pickupOrder.form.otherFee')">
<a-input-number v-model="freightForm.otherFeeAmount" :min="0" :precision="2" style="width: 100%" />
</a-form-item>
</a-form>
</ModalPro>
<ModalPro
@register="registerBatchFreightModal"
:title="t('order.pickupOrder.actions.batchConfirmFreight')"
:confirm-loading="batchFreighting"
@ok="submitBatchFreight"
>
<a-alert type="info" show-icon style="margin-bottom: 12px">
{{
t('order.pickupOrder.message.batchFreightHint', {
count: batchFreightCandidates.length,
})
}}
</a-alert>
<a-form layout="vertical">
<a-form-item :label="t('order.pickupOrder.form.freight')" required>
<a-input-number v-model="batchFreightForm.freightAmount" :min="0" :precision="2" style="width: 100%" />
</a-form-item>
<a-form-item :label="t('order.pickupOrder.form.otherFee')">
<a-input-number v-model="batchFreightForm.otherFeeAmount" :min="0" :precision="2" style="width: 100%" />
</a-form-item>
</a-form>
</ModalPro>
<ModalPro
@register="registerScheduleModal"
:title="scheduleModalTitle"
:confirm-loading="actionLoading || batchScheduling"
@ok="submitSchedule"
>
<a-alert
v-if="batchScheduleIds.length > 1"
type="info"
show-icon
style="margin-bottom: 12px"
>
{{ t('order.pickupOrder.message.batchScheduleHint', { count: batchScheduleIds.length }) }}
</a-alert>
<a-form layout="vertical">
<a-form-item :label="t('order.myOrders.createPickup.carrierService')" required>
<a-select
v-model="scheduleForm.carrierService"
allow-search
allow-clear
:placeholder="t('order.myOrders.createPickup.carrierServicePh')"
>
<a-option v-for="opt in carrierServiceOptions" :key="opt.value" :value="opt.value">
{{ opt.label }}
</a-option>
</a-select>
</a-form-item>
<a-form-item :label="t('order.pickupOrder.table.carrierWaybillNo')">
<a-input
v-model="scheduleForm.carrierWaybillNo"
:placeholder="t('order.pickupOrder.form.carrierWaybillNoPh')"
/>
</a-form-item>
<a-form-item :label="t('order.myOrders.createPickup.expectTime')">
<a-date-picker
v-model="scheduleForm.expectPickupTime"
show-time
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
/>
</a-form-item>
</a-form>
</ModalPro>
<ModalPro
@register="registerRescheduleModal"
:title="t('order.pickupOrder.actions.reschedule')"
:confirm-loading="actionLoading"
@ok="submitReschedule"
>
<a-form layout="vertical">
<a-form-item :label="t('order.myOrders.createPickup.expectTime')" required>
<a-date-picker
v-model="rescheduleForm.expectPickupTime"
show-time
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
/>
</a-form-item>
</a-form>
</ModalPro>
<ModalPro
@register="registerRebindModal"
:title="t('order.pickupOrder.actions.rebind')"
:confirm-loading="actionLoading"
width="780px"
@ok="submitRebind"
>
<div class="pk-rebind-modal">
<div class="pk-rebind-section">
<div class="pk-rebind-section__title">{{ t('order.pickupOrder.rebind.remove') }}</div>
<a-checkbox-group v-model="rebindForm.removeIds" direction="vertical" class="pk-rebind-remove">
<a-checkbox
v-for="row in detailOrders"
:key="row.orderId"
:value="row.orderId"
:disabled="!row.orderId"
>
{{ row.serviceNo }} / {{ row.clientNo }}
</a-checkbox>
</a-checkbox-group>
<div v-if="!detailOrders.length" class="pk-rebind-empty">
{{ t('order.pickupOrder.rebind.emptyBound') }}
</div>
</div>
<a-divider class="pk-rebind-divider" />
<div class="pk-rebind-section">
<div class="pk-rebind-section__title">{{ t('order.pickupOrder.rebind.add') }}</div>
<p class="pk-rebind-hint">{{ t('order.pickupOrder.rebind.addHint') }}</p>
<BindableOrderPicker
ref="rebindPickerRef"
:customer-id="rebindCustomerId"
:warehouse-id="rebindWarehouseId"
:exclude-order-ids="rebindExcludeOrderIds"
:auto-load="false"
:scroll-y="220"
@change="onRebindAddChange"
/>
</div>
</div>
</ModalPro>
</div>
</template>
<style lang="less" scoped>
@import '@/styles/warehouse-list-page.less';
.tabs-actions-section {
background: transparent;
border-radius: 0;
padding: 10px 16px;
margin-bottom: 0;
border-bottom: 1px solid var(--color-border-2);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: nowrap;
gap: 12px;
flex-shrink: 0;
min-height: 52px;
box-sizing: border-box;
.tabs-slot {
flex: 1 1 auto;
min-width: 0;
display: flex;
align-items: center;
overflow: hidden;
}
:deep(.t-tabs) {
flex: 0 1 auto;
min-width: 0;
max-width: 100%;
width: max-content;
}
.toolbar-left {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
height: 32px;
max-width: 100%;
}
.list-filters {
display: inline-flex;
align-items: center;
gap: 8px;
height: 32px;
:deep(.arco-select-view),
:deep(.arco-input-wrapper),
:deep(.arco-btn) {
height: 32px;
line-height: 32px;
}
:deep(.arco-select-view-single) {
display: inline-flex;
align-items: center;
}
:deep(.arco-input-wrapper) {
display: inline-flex;
align-items: center;
padding-top: 0;
padding-bottom: 0;
}
:deep(.arco-input) {
height: 100%;
line-height: 1.2;
}
}
}
.page-hint {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 4px;
color: var(--app-primary-color);
cursor: help;
flex-shrink: 0;
:deep(svg) {
font-size: 16px;
}
&:hover {
background: var(--color-fill-2);
}
}
.table-toolbar {
.actions-right {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
flex: 1;
min-width: 0;
}
.toolbar-right {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
justify-content: flex-end;
.selected-count {
font-size: 13px;
color: var(--color-text-3);
white-space: nowrap;
line-height: 32px;
}
}
}
.pk-detail-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin-bottom: 16px;
padding-bottom: 14px;
border-bottom: 1px solid var(--color-border-2);
}
.pk-detail-header__main {
min-width: 0;
flex: 1;
}
.pk-detail-title {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
}
.pk-detail-no {
font-size: 16px;
font-weight: 600;
color: var(--color-text-1);
}
.pk-detail-sub {
margin-top: 6px;
font-size: 12px;
color: var(--color-text-3);
line-height: 1.4;
}
.pk-detail-amount {
text-align: right;
flex-shrink: 0;
padding: 8px 12px;
border-radius: 4px;
background: color-mix(in srgb, var(--app-primary-color) 8%, #fff);
span {
display: block;
font-size: 12px;
color: var(--color-text-3);
}
b {
font-size: 20px;
font-weight: 600;
color: var(--app-primary-color);
line-height: 1.3;
}
}
.pk-detail-section {
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
}
.pk-detail-section__title {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
font-size: 14px;
font-weight: 500;
color: var(--color-text-1);
em {
font-style: normal;
min-width: 18px;
height: 18px;
padding: 0 6px;
border-radius: 9px;
background: var(--color-fill-2);
color: var(--color-text-3);
font-size: 12px;
line-height: 18px;
text-align: center;
}
}
.pk-copy-btn {
margin-left: auto;
height: 24px;
color: var(--app-primary-color);
}
.pk-link {
color: var(--app-primary-color);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
.pk-detail-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px 16px;
padding: 12px;
border-radius: 4px;
background: var(--color-fill-1);
}
.pk-detail-field {
min-width: 0;
span {
display: block;
margin-bottom: 4px;
font-size: 12px;
color: var(--color-text-3);
}
b {
display: block;
font-size: 14px;
font-weight: 500;
color: var(--color-text-1);
line-height: 1.4;
word-break: break-all;
}
}
.pk-detail-grid-span {
grid-column: 1 / -1;
}
.pk-timeline {
padding: 4px 0 4px 4px;
}
.pk-timeline__item {
display: flex;
gap: 12px;
position: relative;
padding-bottom: 14px;
&:last-child {
padding-bottom: 0;
.pk-timeline__dot::after {
display: none;
}
}
}
.pk-timeline__dot {
position: relative;
flex-shrink: 0;
width: 10px;
height: 10px;
margin-top: 4px;
border-radius: 50%;
background: var(--app-primary-color);
&::after {
content: '';
position: absolute;
left: 4px;
top: 12px;
width: 2px;
bottom: -14px;
background: var(--color-border-2);
}
}
.pk-timeline__body {
min-width: 0;
}
.pk-timeline__label {
font-size: 13px;
color: var(--color-text-1);
font-weight: 500;
}
.pk-timeline__time {
margin-top: 2px;
font-size: 12px;
color: var(--color-text-3);
}
.pk-detail-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
}
.pk-detail-footer__left,
.pk-detail-footer__right {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
.pk-detail-footer__right {
margin-left: auto;
justify-content: flex-end;
}
.pk-rebind-modal {
padding-top: 4px;
}
.pk-rebind-section__title {
margin-bottom: 8px;
font-size: 14px;
font-weight: 500;
color: var(--color-text-1);
}
.pk-rebind-hint {
margin: 0 0 10px;
font-size: 12px;
color: var(--color-text-3);
line-height: 1.5;
}
.pk-rebind-remove {
width: 100%;
max-height: 140px;
overflow: auto;
padding: 4px 0;
}
.pk-rebind-divider {
margin: 16px 0;
}
.pk-rebind-empty {
color: var(--color-text-3);
font-size: 13px;
}
</style>