front/src/views/warehouse/parcelArrival/index.vue
2026-09-05 18:37:50 +08:00

325 lines
10 KiB
Vue
Raw Blame History

<template>
<div class="ops-page">
<div class="ops-col ops-col--main">
<section class="ops-card ops-card--scan">
<div class="ops-card__body ops-scan">
<div class="ops-scan__row">
<a-input
ref="scanInputRef"
v-model="formData.trackingNo"
:placeholder="t('warehouse.parcelArrival.form.placeholder.scanOrInput')"
class="ops-scan-input"
allow-clear
@press-enter="() => void handleSubmit()"
/>
</div>
<div class="ops-scan__row ops-scan__row--tools">
<div class="ops-scan__opts">
<a-checkbox v-model="keepScanFocus" class="ops-opt">
{{ $t('operation.common.keepScanFocus') }}
</a-checkbox>
<a-checkbox v-model="voiceEnabled" class="ops-opt">
{{ $t('operation.common.voiceBroadcast') }}
</a-checkbox>
</div>
<div class="ops-scan__actions">
<a-button class="ops-btn ops-btn--ghost" @click="handleReset()">
{{ t('warehouse.parcelArrival.actions.reset') }}
</a-button>
<a-button
v-if="hasPermission('Operation.ParcelArrival.Save')"
type="primary"
class="ops-btn ops-btn--primary"
:loading="submitting"
@click="handleSubmit"
>
{{ t('warehouse.parcelArrival.actions.arrive') }}
</a-button>
</div>
</div>
<p class="ops-scan-hint">{{ t('warehouse.parcelArrival.tip') }}</p>
</div>
</section>
<section class="ops-card ops-card--verify">
<div class="ops-card__body ops-verify">
<p
v-if="feedbackMessage"
class="ops-feedback"
:class="`is-${feedbackType}`"
>
{{ feedbackMessage }}
</p>
<div class="ops-status-grid">
<div class="ops-status-item">
<span>{{ t('warehouse.common.warehouse') }}</span>
<strong>{{ currentWarehouseLabel || t('warehouse.common.noCurrentWarehouse') }}</strong>
</div>
<div class="ops-status-item">
<span>{{ t('warehouse.parcelArrival.form.trackingNo') }}</span>
<strong>{{ formData.trackingNo?.trim() || '-' }}</strong>
</div>
</div>
<p class="ops-scan-hint" style="margin-top: 16px">
{{ t('warehouse.parcelArrival.preview.empty') }}
</p>
</div>
</section>
</div>
<aside class="ops-card ops-card--recent-wide">
<div class="ops-list-toolbar">
<div />
<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 ref="tableContainerRef" class="ops-list-wrap">
<HandsontablePro
:columns="hotColumns"
:data="tableRows"
:total="totalCount"
:current-page="currentPage"
:page-size="pageSize"
:height="tableHeight"
:loading="listLoading"
:read-only="true"
:stretch-columns="true"
show-action-column
:action-renderer="actionRenderer"
:search-params="headerSearchParams"
@search="handleHeaderSearch"
@action-click="handleActionClick"
/>
</div>
</aside>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { Modal } from '@arco-design/web-vue'
import HandsontablePro from '@/components/HandsontablePro/index.vue'
import PaginationPro from '@/components/PaginationPro/index.vue'
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
import { useOperationScanFocus } from '@/composables/useOperationScanFocus'
import { useOperationScanFeedback } from '@/composables/useOperationScanFeedback'
import {
createWarehouseParcelArrival,
deleteWarehouseParcelArrival,
getWarehouseParcelArrivalList,
} from '@/api/warehouse'
import { useCurrentWarehouseStore } from '@/store'
import { hasPermission } from '@/utils/rbac-permission'
import {
adaptKeywordHeaderFilters,
withServerFilterColumns,
} from '@/utils/headerFilterAdapter'
import { formatDateTime, normalizeListRow, pickField } from '@/utils/warehouseListMapper'
import { renderWarehouseActionBtn, wrapWarehouseActions } from '@/utils/warehouseActionRenderer'
defineOptions({ name: 'WarehouseParcelArrival' })
const { t } = useI18n()
const router = useRouter()
const { tableContainerRef, tableHeight } = useTableContainerHeight()
const scanInputRef = ref<{ focus?: () => void } | null>(null)
const { keepScanFocus, maybeRefocusScan } = useOperationScanFocus('parcelArrival', scanInputRef)
const {
voiceEnabled,
feedbackType,
feedbackMessage,
clearFeedback,
showError,
showSuccess,
showWarning,
} = useOperationScanFeedback('parcelArrival')
const currentWarehouseStore = useCurrentWarehouseStore()
const submitting = ref(false)
const listLoading = ref(false)
const headerSearchParams = reactive<Record<string, string>>({})
const ARRIVAL_SERVER_FILTER_KEYS = ['trackingNo'] as const
const tableRows = ref<any[]>([])
const currentPage = ref(1)
const pageSize = ref(50)
const totalCount = ref(0)
const formData = reactive({
trackingNo: '',
})
const currentWarehouseLabel = computed(() => {
const id = currentWarehouseStore.warehouseId
if (!id) return ''
return currentWarehouseStore.warehouseName || String(id)
})
const hotColumns = computed(() =>
withServerFilterColumns(
[
{ key: 'trackingNo', label: t('warehouse.parcelArrival.table.trackingNo'), width: 200 },
{ key: 'warehouseName', label: t('warehouse.parcelArrival.table.warehouse'), width: 140 },
{ key: 'operateTime', label: t('warehouse.parcelArrival.table.operateTime'), width: 170 },
{ key: 'operator', label: t('warehouse.parcelArrival.table.operator'), width: 120 },
],
ARRIVAL_SERVER_FILTER_KEYS,
),
)
function mapRow(raw: Record<string, unknown>) {
const row = normalizeListRow(raw)
return {
id: pickField<number>(row, 'id', 'Id'),
trackingNo: pickField(row, 'trackingNo', 'TrackingNo') ?? '-',
warehouseName: pickField(row, 'warehouseName', 'WarehouseName') ?? '-',
operateTime: formatDateTime(pickField(row, 'createTime', 'CreateTime')),
operator: pickField(row, 'createBy', 'CreateBy') ?? '-',
_raw: row,
}
}
async function loadList() {
listLoading.value = true
try {
const keyword = adaptKeywordHeaderFilters(headerSearchParams, [...ARRIVAL_SERVER_FILTER_KEYS])
const { items, total } = await getWarehouseParcelArrivalList({
page: currentPage.value,
pageSize: pageSize.value,
keyword: keyword || undefined,
warehouseId: currentWarehouseStore.warehouseId,
})
tableRows.value = (items || []).map((item: Record<string, unknown>) => mapRow(item))
totalCount.value = Number(total || 0)
} catch (e: any) {
showError(e?.message || t('warehouse.parcelArrival.message.loadFailed'))
tableRows.value = []
totalCount.value = 0
} finally {
listLoading.value = false
}
}
function handleHeaderSearch() {
currentPage.value = 1
loadList()
}
function handlePageChange(page: number) {
currentPage.value = page
loadList()
}
function handlePageSizeChange(size: number) {
pageSize.value = size
currentPage.value = 1
loadList()
}
function handleReset(clearStatus = true) {
formData.trackingNo = ''
if (clearStatus) clearFeedback()
maybeRefocusScan()
}
async function handleSubmit() {
if (!hasPermission('Operation.ParcelArrival.Save')) return
const trackingNo = formData.trackingNo?.trim()
if (!trackingNo) {
showWarning(t('warehouse.parcelArrival.message.trackingRequired'))
maybeRefocusScan()
return
}
const warehouseId = currentWarehouseStore.warehouseId
if (!warehouseId) {
showWarning(t('warehouse.common.noCurrentWarehouse'))
maybeRefocusScan()
return
}
submitting.value = true
try {
const result = await createWarehouseParcelArrival({
warehouseId,
trackingNo,
})
const linked = (result as any)?.linkedOrders || (result as any)?.LinkedOrders || []
const serviceNos = linked
.map((o: any) => o.serviceNo || o.ServiceNo)
.filter(Boolean)
.join('<27><>?)
if (serviceNos) {
showSuccess(t('warehouse.parcelArrival.message.arriveSuccessWithOrders', { orders: serviceNos }))
if (hasPermission('Operation.FulfillPack')) {
Modal.confirm({
title: t('warehouse.parcelArrival.message.arriveSuccess'),
content: t('warehouse.parcelArrival.message.openFulfillPackHint'),
okText: t('warehouse.parcelArrival.actions.openFulfillPack'),
cancelText: t('common.cancel'),
onOk: () => {
void router.push({ name: 'FulfillPack' })
},
})
}
} else {
showSuccess(t('warehouse.parcelArrival.message.arriveSuccess'))
}
handleReset(false)
currentPage.value = 1
await loadList()
maybeRefocusScan()
} catch (e: any) {
showError(e?.message || t('warehouse.parcelArrival.message.arriveFailed'))
maybeRefocusScan()
} finally {
submitting.value = false
}
}
function handleDelete(row: any) {
if (!hasPermission('Operation.ParcelArrival.Delete')) return
Modal.confirm({
title: t('warehouse.parcelArrival.actions.delete'),
content: row.trackingNo,
onOk: async () => {
await deleteWarehouseParcelArrival(row.id)
showSuccess(t('warehouse.parcelArrival.message.deleteSuccess'))
await loadList()
},
})
}
function actionRenderer(row: any) {
if (!hasPermission('Operation.ParcelArrival.Delete') || !row?.id) return ''
return wrapWarehouseActions([
renderWarehouseActionBtn('delete', t('warehouse.common.delete')),
])
}
function handleActionClick(row: any, _i: number, action?: string) {
if (action === 'delete') handleDelete(row)
}
watch(
() => currentWarehouseStore.warehouseId,
() => {
currentPage.value = 1
loadList()
},
)
onMounted(() => {
loadList()
})
</script>
<style lang="less" scoped>
@import '@/views/operation/styles/ops-scan-page.less';
</style>