531 lines
14 KiB
Vue
531 lines
14 KiB
Vue
<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>
|
||
|