front/src/views/operation/inboundScan/index.vue
2026-05-06 12:28:03 +08:00

531 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="waybill-container">
<div class="waybill-layout">
<!-- 表单区域 -->
<div class="form-section">
<a-form :model="formData" layout="vertical" class="waybill-form">
<!-- 扫描单号 -->
<a-form-item :label="$t('operation.inbound.form.scanNumber')" class="form-item-large">
<a-input
v-model="formData.waybillNumber"
:placeholder="$t('operation.inbound.form.placeholder.scanOrInput')"
size="large"
class="large-input"
@press-enter="handleScan"
/>
</a-form-item>
<!-- 收货实重、长宽高、销售产品、目的国家 同一行 -->
<div class="form-row-inline">
<div class="form-row-fields">
<div class="inline-field">
<label class="inline-label">{{ $t('operation.inbound.form.receivedWeight') }}</label>
<div class="input-with-unit">
<a-input
v-model="formData.weight"
:placeholder="$t('operation.inbound.form.placeholder.input')"
class="weight-input"
/>
<span class="unit-text">KG</span>
</div>
</div>
<div class="inline-field">
<label class="inline-label">{{ $t('operation.inbound.form.dimensions') }}</label>
<div class="dimension-inputs">
<a-input
v-model="formData.length"
:placeholder="$t('operation.inbound.form.placeholder.length')"
class="dimension-input"
/>
<span class="separator">×</span>
<a-input
v-model="formData.width"
:placeholder="$t('operation.inbound.form.placeholder.width')"
class="dimension-input"
/>
<span class="separator">×</span>
<a-input
v-model="formData.height"
:placeholder="$t('operation.inbound.form.placeholder.height')"
class="dimension-input"
/>
</div>
</div>
<div class="inline-field">
<label class="inline-label">{{ $t('operation.inbound.form.salesProduct') }}</label>
<a-select
v-model="formData.salesProduct"
:placeholder="$t('operation.inbound.form.placeholder.select')"
class="select-input-inline"
>
<a-option
v-for="product in salesProductOptions"
:key="product.value"
:value="product.value"
>
{{ product.label }}
</a-option>
</a-select>
</div>
<div class="inline-field">
<label class="inline-label">{{ $t('operation.inbound.form.destinationCountry') }}</label>
<a-select
v-model="formData.destinationCountry"
:placeholder="$t('operation.inbound.form.placeholder.select')"
class="select-input-inline"
>
<a-option
v-for="country in countryOptions"
:key="country.value"
:value="country.value"
>
{{ country.label }}
</a-option>
</a-select>
</div>
</div>
</div>
<!-- 操作按钮 -->
<a-form-item>
<div class="form-actions">
<a-button class="reset-btn" @click="handleReset">{{ $t('operation.inbound.actions.reset') }}</a-button>
<a-button type="primary" class="submit-btn" @click="handleSubmit">{{ $t('operation.inbound.actions.inbound') }}</a-button>
</div>
</a-form-item>
</a-form>
</div>
<!-- 最近操作的订单 -->
<div class="recent-orders-section">
<div class="list-toolbar">
<div class="toolbar-left">
</div>
<div class="toolbar-right">
<div class="pagination-controls">
<PaginationPro
:total-count="totalCount"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
/>
</div>
</div>
</div>
<div ref="tableContainerRef" class="list-wrap">
<HandsontablePro
ref="handsontableRef"
:columns="hotColumns"
:data="displayedData"
:total="totalCount"
:current-page="currentPage"
:page-size="pageSize"
:height="tableHeight"
:read-only="true"
:stretch-columns="true"
/>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { useI18n } from 'vue-i18n';
import { ref, reactive, computed, onMounted } from 'vue';
import { $baseMessage } from '@/utils';
import HandsontablePro from '@/components/HandsontablePro/index.vue';
import PaginationPro from '@/components/PaginationPro/index.vue';
import { useTableContainerHeight } from '@/composables/useTableContainerHeight';
import { inboundCreate, inboundRecentList } from '@/api/operation';
import { getSalesProductList } from '@/api/channel/salesProduct';
const { t } = useI18n();
const { tableContainerRef, tableHeight } = useTableContainerHeight();
defineOptions({
name: 'InboundScan',
});
// 表单数据
const formData = reactive({
waybillNumber: '',
weight: '',
length: '',
width: '',
height: '',
salesProduct: undefined as number | undefined,
destinationCountry: undefined as string | undefined,
});
const salesProductOptions = ref<Array<{ label: string; value: number }>>([]);
const countryOptions = ref([
{ label: '美国', value: 'US' },
{ label: '英国', value: 'UK' },
{ label: '德国', value: 'DE' },
{ label: '法国', value: 'FR' },
]);
// HandsontablePro 列定义(序号由组件内部生成)
const hotColumns = computed(() => [
{ key: 'waybillNumber', label: t('operation.inbound.table.waybillNumber'), width: 180 },
{ key: 'weight', label: t('operation.inbound.table.weight'), width: 100 },
{ key: 'dimensions', label: t('operation.inbound.table.dimensions'), width: 150 },
{ key: 'product', label: t('operation.inbound.table.product'), width: 120 },
{ key: 'country', label: t('operation.inbound.table.country'), width: 120 },
{ key: 'warehouse', label: t('operation.inbound.table.warehouse'), width: 100 },
{ key: 'operateTime', label: t('operation.inbound.table.operateTime'), width: 180 },
{ key: 'operator', label: t('operation.inbound.table.operator'), width: 100 },
]);
const recentOrders = ref<any[]>([]);
const handsontableRef = ref<InstanceType<typeof HandsontablePro>>();
const currentPage = ref(1);
const pageSize = ref(50);
const totalCount = computed(() => recentOrders.value.length);
const displayedData = computed(() => {
const start = (currentPage.value - 1) * pageSize.value;
return recentOrders.value.slice(start, start + pageSize.value);
});
async function loadSalesProducts() {
try {
const { items } = await getSalesProductList({ page: 1, pageSize: 500, isActive: 1 });
salesProductOptions.value = (items || []).map((p: any) => ({
label: p.productName || p.productCode || String(p.id),
value: Number(p.id),
}));
} catch {
salesProductOptions.value = [];
}
}
async function loadRecent() {
try {
const { items } = await inboundRecentList({ limit: 200 });
recentOrders.value = (items || []).map((row: any) => ({
waybillNumber: row.waybillNo ?? row.waybillNumber ?? '',
weight: row.weight != null ? String(row.weight) : '-',
dimensions:
row.length != null && row.width != null && row.height != null
? `${row.length}×${row.width}×${row.height}`
: '-',
product: row.salesProductName ?? '-',
country: row.destinationCountry ?? '-',
warehouse: row.warehouseName ?? '-',
operateTime: row.operateTime ?? row.createTime ?? '',
operator: row.operatorName ?? row.operator ?? '-',
}));
} catch (e: any) {
$baseMessage(e?.message || '加载最近入库记录失败', 'error');
recentOrders.value = [];
}
}
function handlePageChange(page: number) {
currentPage.value = page;
}
function handlePageSizeChange(size: number) {
pageSize.value = size;
currentPage.value = 1;
}
// 处理扫描
function handleScan() {
if (!formData.waybillNumber) {
$baseMessage(t('operation.inbound.message.waybillNumberRequired'), 'warning');
return;
}
// TODO: 实现扫描逻辑
}
async function handleSubmit() {
if (!formData.waybillNumber?.trim()) {
$baseMessage(t('operation.inbound.message.waybillNumberRequired'), 'warning');
return;
}
try {
await inboundCreate({
waybillNo: formData.waybillNumber.trim(),
weight: formData.weight ? Number(formData.weight) : undefined,
length: formData.length ? Number(formData.length) : undefined,
width: formData.width ? Number(formData.width) : undefined,
height: formData.height ? Number(formData.height) : undefined,
salesProductId: formData.salesProduct,
destinationCountry: formData.destinationCountry,
});
$baseMessage(t('operation.inbound.message.inboundSuccess'), 'success');
handleReset();
await loadRecent();
} catch (e: any) {
$baseMessage(e?.message || '入库失败', 'error');
}
}
// 重置表单
function handleReset() {
formData.waybillNumber = '';
formData.weight = '';
formData.length = '';
formData.width = '';
formData.height = '';
formData.salesProduct = undefined;
formData.destinationCountry = undefined;
}
onMounted(() => {
loadSalesProducts();
loadRecent();
});
</script>
<style lang="less" scoped>
.waybill-container {
padding: 24px;
background-color: #f5f5f5;
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;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
flex-shrink: 0;
.header-left {
.page-title {
margin: 0;
font-size: 20px;
font-weight: 600;
color: #262626;
}
}
.header-right {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
.sound-icon {
font-size: 18px;
color: #595959;
}
.broadcast-text {
font-size: 14px;
color: #595959;
}
}
}
.form-section {
background: #fff;
border-radius: 8px;
padding: 24px;
margin-bottom: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
flex: 3 1 0;
min-width: 0;
overflow-x: hidden;
overflow-y: auto;
align-self: stretch;
}
.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;
}
.input-with-unit {
display: flex;
align-items: center;
gap: 8px;
.weight-input {
flex: 1;
}
.unit-text {
font-size: 14px;
color: #595959;
min-width: 30px;
}
}
.form-row-inline {
margin-bottom: 16px;
}
.form-row-fields {
display: flex;
align-items: flex-end;
gap: 16px;
flex-wrap: wrap;
}
.inline-field {
flex: 1 1 100%;
min-width: 0;
.inline-label {
display: block;
font-size: 14px;
font-weight: 500;
color: #262626;
margin-bottom: 8px;
}
}
.dimension-inputs {
display: flex;
align-items: center;
gap: 6px;
.dimension-input {
width: 100px;
min-width: 100px;
}
.separator {
font-size: 14px;
color: #595959;
padding: 0 2px;
}
}
.select-input-inline {
width: 100%;
min-width: 120px;
}
.form-actions {
display: flex;
gap: 16px;
margin-top: 8px;
.reset-btn {
min-width: 100px;
}
.submit-btn {
min-width: 100px;
}
}
}
.recent-orders-section {
background: #fff;
border-radius: 8px;
padding: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
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: center;
padding: 16px 16px 0;
margin-bottom: 16px;
flex-shrink: 0;
flex-wrap: nowrap;
.toolbar-left .section-subtitle {
font-size: 12px;
color: #8c8c8c;
}
.toolbar-right .pagination-controls {
display: flex;
align-items: center;
gap: 8px;
white-space: nowrap;
flex-wrap: nowrap;
}
.toolbar-right .total-records,
.toolbar-right .go-to-text,
.toolbar-right .page-size-text,
.toolbar-right .page-text {
font-size: 12px;
color: #4e5969;
}
.toolbar-right .current-page {
font-size: 12px;
font-weight: 500;
color: #1d2129;
}
}
.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>