325 lines
10 KiB
Vue
325 lines
10 KiB
Vue
<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>
|