498 lines
13 KiB
Vue
498 lines
13 KiB
Vue
<template>
|
|
<div class="waybill-container">
|
|
<div class="waybill-layout">
|
|
<div class="form-section">
|
|
<div class="form-section-toolbar">
|
|
<a-checkbox v-model="keepScanFocus" size="small">
|
|
{{ $t('operation.common.keepScanFocus') }}
|
|
</a-checkbox>
|
|
<a-checkbox v-model="voiceEnabled" size="small">
|
|
{{ $t('operation.common.voiceBroadcast') }}
|
|
</a-checkbox>
|
|
</div>
|
|
<a-form :model="formData" layout="vertical" class="waybill-form">
|
|
<a-form-item
|
|
v-if="isAdmin"
|
|
:label="t('warehouse.common.customer')"
|
|
class="form-item-large"
|
|
>
|
|
<a-select
|
|
v-model="adminCustomerId"
|
|
allow-clear
|
|
allow-search
|
|
:placeholder="t('warehouse.common.selectCustomer')"
|
|
:options="customerOptions"
|
|
size="large"
|
|
class="large-input"
|
|
/>
|
|
</a-form-item>
|
|
<a-form-item :label="t('warehouse.parcelArrival.form.trackingNo')" class="form-item-large">
|
|
<a-input
|
|
ref="scanInputRef"
|
|
v-model="formData.trackingNo"
|
|
:placeholder="t('warehouse.parcelArrival.form.placeholder.scanOrInput')"
|
|
size="large"
|
|
class="large-input"
|
|
@press-enter="() => void handleSubmit()"
|
|
/>
|
|
</a-form-item>
|
|
<a-form-item :label="t('warehouse.common.warehouse')" class="form-item-large">
|
|
<a-input
|
|
:model-value="currentWarehouseLabel"
|
|
readonly
|
|
class="large-input readonly-field"
|
|
:placeholder="t('warehouse.common.noCurrentWarehouse')"
|
|
/>
|
|
</a-form-item>
|
|
<a-form-item>
|
|
<div class="form-actions">
|
|
<a-button class="reset-btn" @click="handleReset">
|
|
{{ t('warehouse.parcelArrival.actions.reset') }}
|
|
</a-button>
|
|
<a-button
|
|
v-if="hasPermission('Operation.ParcelArrival.Save')"
|
|
type="primary"
|
|
class="submit-btn"
|
|
:loading="submitting"
|
|
@click="handleSubmit"
|
|
>
|
|
{{ t('warehouse.parcelArrival.actions.arrive') }}
|
|
</a-button>
|
|
</div>
|
|
</a-form-item>
|
|
</a-form>
|
|
</div>
|
|
|
|
<div class="recent-orders-section">
|
|
<div class="list-toolbar">
|
|
<div class="toolbar-left">
|
|
<a-space wrap :size="8">
|
|
<a-input
|
|
v-model="keyword"
|
|
allow-clear
|
|
:placeholder="t('warehouse.parcelArrival.search.trackingNo')"
|
|
style="width: 200px"
|
|
@press-enter="handleSearch"
|
|
/>
|
|
<a-button type="primary" @click="handleSearch">
|
|
{{ t('warehouse.common.search') }}
|
|
</a-button>
|
|
</a-space>
|
|
<p
|
|
v-if="feedbackMessage"
|
|
class="scan-feedback-text"
|
|
:class="`is-${feedbackType}`"
|
|
>
|
|
{{ feedbackMessage }}
|
|
</p>
|
|
</div>
|
|
<div class="toolbar-right">
|
|
<div class="pagination-controls">
|
|
<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>
|
|
<div ref="tableContainerRef" class="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-column-width="72"
|
|
:action-renderer="actionRenderer"
|
|
@action-click="handleActionClick"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
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 { useWarehouseAdminCustomer } from '@/composables/useWarehouseAdminCustomer'
|
|
import {
|
|
createWarehouseParcelArrival,
|
|
deleteWarehouseParcelArrival,
|
|
getWarehouseParcelArrivalList,
|
|
} from '@/api/warehouse'
|
|
import { useCurrentWarehouseStore } from '@/store'
|
|
import { hasPermission } from '@/utils/rbac-permission'
|
|
import { formatDateTime, normalizeListRow, pickField } from '@/utils/warehouseListMapper'
|
|
import { renderWarehouseActionBtn, wrapWarehouseActions } from '@/utils/warehouseActionRenderer'
|
|
|
|
defineOptions({ name: 'WarehouseParcelArrival' })
|
|
|
|
const { t } = useI18n()
|
|
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 {
|
|
isAdmin,
|
|
customerId: adminCustomerId,
|
|
customerOptions,
|
|
withCustomerId,
|
|
ensureMutationCustomerId,
|
|
} = useWarehouseAdminCustomer()
|
|
const currentWarehouseStore = useCurrentWarehouseStore()
|
|
|
|
const submitting = ref(false)
|
|
const listLoading = ref(false)
|
|
const keyword = ref('')
|
|
const tableRows = ref<any[]>([])
|
|
const currentPage = ref(1)
|
|
const pageSize = ref(30)
|
|
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(() => [
|
|
{ key: 'trackingNo', label: t('warehouse.parcelArrival.table.trackingNo'), width: 200 },
|
|
{ key: 'warehouseName', label: t('warehouse.parcelArrival.table.warehouse'), width: 160 },
|
|
{ key: 'createTime', label: t('warehouse.parcelArrival.table.createTime'), width: 180 },
|
|
{ key: 'createBy', label: t('warehouse.parcelArrival.table.createBy'), width: 120 },
|
|
])
|
|
|
|
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') ?? '-',
|
|
createTime: formatDateTime(pickField(row, 'createTime', 'CreateTime')),
|
|
createBy: pickField(row, 'createBy', 'CreateBy') ?? '-',
|
|
_raw: row,
|
|
}
|
|
}
|
|
|
|
async function loadList() {
|
|
listLoading.value = true
|
|
try {
|
|
const { items, total } = await getWarehouseParcelArrivalList(
|
|
withCustomerId({
|
|
page: currentPage.value,
|
|
pageSize: pageSize.value,
|
|
keyword: keyword.value || 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 handlePageChange(page: number) {
|
|
currentPage.value = page
|
|
loadList()
|
|
}
|
|
|
|
function handlePageSizeChange(size: number) {
|
|
pageSize.value = size
|
|
currentPage.value = 1
|
|
loadList()
|
|
}
|
|
|
|
function handleSearch() {
|
|
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
|
|
}
|
|
if (isAdmin.value && ensureMutationCustomerId() == null) {
|
|
showWarning(t('warehouse.common.selectCustomerRequired'))
|
|
maybeRefocusScan()
|
|
return
|
|
}
|
|
submitting.value = true
|
|
try {
|
|
await createWarehouseParcelArrival(
|
|
withCustomerId({
|
|
warehouseId,
|
|
trackingNo,
|
|
}),
|
|
)
|
|
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>
|
|
.waybill-container {
|
|
padding: 16px;
|
|
background-color: #f0f3f4;
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.waybill-layout {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: stretch;
|
|
gap: 16px;
|
|
flex: 1;
|
|
min-height: 0;
|
|
min-width: 0;
|
|
}
|
|
|
|
.form-section {
|
|
background: #fff;
|
|
border-radius: 4px;
|
|
padding: 24px;
|
|
margin-bottom: 0;
|
|
flex: 3 1 0;
|
|
min-width: 0;
|
|
overflow-x: hidden;
|
|
overflow-y: auto;
|
|
align-self: stretch;
|
|
}
|
|
|
|
.form-section-toolbar {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
align-items: center;
|
|
gap: 16px;
|
|
margin: -4px 0 12px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.waybill-form {
|
|
.form-item-large {
|
|
:deep(.arco-form-item-label) {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: #262626;
|
|
}
|
|
}
|
|
|
|
.large-input {
|
|
width: 100%;
|
|
max-width: 100%;
|
|
height: 48px;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.readonly-field {
|
|
:deep(.arco-input) {
|
|
background-color: #f7f8fa;
|
|
color: #4e5969;
|
|
cursor: default;
|
|
}
|
|
}
|
|
|
|
.form-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
margin-top: 8px;
|
|
|
|
.reset-btn,
|
|
.submit-btn {
|
|
min-width: 100px;
|
|
height: 32px;
|
|
border-radius: 4px;
|
|
}
|
|
}
|
|
}
|
|
|
|
.recent-orders-section {
|
|
background: #fff;
|
|
border-radius: 4px;
|
|
padding: 0;
|
|
flex: 7 1 0;
|
|
min-width: 0;
|
|
min-height: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
|
|
.list-toolbar {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
padding: 16px 16px 0;
|
|
margin-bottom: 16px;
|
|
flex-shrink: 0;
|
|
flex-wrap: nowrap;
|
|
gap: 12px;
|
|
|
|
.toolbar-left {
|
|
flex: 1;
|
|
min-width: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.scan-feedback-text {
|
|
margin: 0;
|
|
font-size: 16px;
|
|
line-height: 1.5;
|
|
word-break: break-word;
|
|
|
|
&.is-error {
|
|
color: #f53f3f;
|
|
font-weight: 700;
|
|
}
|
|
|
|
&.is-success {
|
|
color: #00b42a;
|
|
font-weight: 600;
|
|
}
|
|
|
|
&.is-warning {
|
|
color: #d46b08;
|
|
font-weight: 600;
|
|
}
|
|
}
|
|
|
|
.toolbar-right .pagination-controls {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
white-space: nowrap;
|
|
flex-wrap: nowrap;
|
|
}
|
|
}
|
|
|
|
.list-wrap {
|
|
width: 100%;
|
|
flex: 1;
|
|
min-height: 0;
|
|
padding: 0 16px 16px;
|
|
box-sizing: border-box;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
overscroll-behavior: contain;
|
|
touch-action: pan-x pan-y;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 992px) {
|
|
.waybill-layout {
|
|
flex-direction: column;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.form-section {
|
|
flex: 0 0 auto;
|
|
max-width: none;
|
|
width: 100%;
|
|
min-width: 0;
|
|
}
|
|
|
|
.recent-orders-section {
|
|
flex: 1;
|
|
min-height: 360px;
|
|
}
|
|
}
|
|
</style>
|