2043 lines
63 KiB
Vue
2043 lines
63 KiB
Vue
<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>
|