feat(wms):新增 ele 的 order receipt 迁移
parent
3135b28211
commit
41d5aa93d6
|
|
@ -0,0 +1,423 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { DescriptionItemSchema } from '#/components/description';
|
||||
|
||||
import { h, markRaw } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { formatDate, formatDateTime } from '@vben/utils';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { buildNumberRangeSchema } from '#/components/number-range-input';
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
import { WmsMerchantSelect } from '#/views/wms/md/merchant/components';
|
||||
import { WmsWarehouseSelect } from '#/views/wms/md/warehouse/components';
|
||||
import {
|
||||
formatPrice,
|
||||
formatQuantity,
|
||||
formatSumPrice,
|
||||
formatSumQuantity,
|
||||
PRICE_PRECISION,
|
||||
QUANTITY_PRECISION,
|
||||
} from '#/views/wms/utils/format';
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
placeholder: '请输入入库单号',
|
||||
},
|
||||
fieldName: 'no',
|
||||
label: '入库单号',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
placeholder: '请输入业务单号',
|
||||
},
|
||||
fieldName: 'bizOrderNo',
|
||||
label: '业务单号',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
options: getDictOptions(DICT_TYPE.WMS_ORDER_STATUS, 'number'),
|
||||
placeholder: '请选择单据状态',
|
||||
},
|
||||
fieldName: 'status',
|
||||
label: '单据状态',
|
||||
},
|
||||
{
|
||||
component: markRaw(WmsWarehouseSelect),
|
||||
fieldName: 'warehouseId',
|
||||
label: '仓库',
|
||||
},
|
||||
{
|
||||
component: markRaw(WmsMerchantSelect),
|
||||
componentProps: {
|
||||
placeholder: '请选择供应商',
|
||||
supplier: true,
|
||||
},
|
||||
fieldName: 'merchantId',
|
||||
label: '供应商',
|
||||
},
|
||||
{
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
clearable: true,
|
||||
},
|
||||
fieldName: 'orderTime',
|
||||
label: '单据日期',
|
||||
},
|
||||
buildNumberRangeSchema(
|
||||
'数量',
|
||||
'totalQuantityRange',
|
||||
'totalQuantityMin',
|
||||
'totalQuantityMax',
|
||||
QUANTITY_PRECISION,
|
||||
),
|
||||
buildNumberRangeSchema(
|
||||
'总金额',
|
||||
'totalPriceRange',
|
||||
'totalPriceMin',
|
||||
'totalPriceMax',
|
||||
PRICE_PRECISION,
|
||||
),
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
options: getDictOptions(DICT_TYPE.WMS_RECEIPT_ORDER_TYPE, 'number'),
|
||||
placeholder: '请选择入库类型',
|
||||
},
|
||||
fieldName: 'type',
|
||||
label: '入库类型',
|
||||
},
|
||||
{
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSimpleUserList,
|
||||
clearable: true,
|
||||
filterable: true,
|
||||
labelField: 'nickname',
|
||||
placeholder: '请选择创建用户',
|
||||
valueField: 'id',
|
||||
},
|
||||
fieldName: 'creator',
|
||||
label: '创建用户',
|
||||
},
|
||||
{
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSimpleUserList,
|
||||
clearable: true,
|
||||
filterable: true,
|
||||
labelField: 'nickname',
|
||||
placeholder: '请选择更新用户',
|
||||
valueField: 'id',
|
||||
},
|
||||
fieldName: 'updater',
|
||||
label: '更新用户',
|
||||
},
|
||||
{
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
clearable: true,
|
||||
},
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
},
|
||||
{
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
clearable: true,
|
||||
},
|
||||
fieldName: 'updateTime',
|
||||
label: '更新时间',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表表格列 */
|
||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
fixed: 'left',
|
||||
slots: { content: 'expand_content' },
|
||||
type: 'expand',
|
||||
width: 48,
|
||||
},
|
||||
{
|
||||
field: 'no',
|
||||
fixed: 'left',
|
||||
slots: { default: 'no' },
|
||||
title: '单号/业务单号',
|
||||
width: 260,
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.WMS_ORDER_STATUS },
|
||||
},
|
||||
field: 'status',
|
||||
fixed: 'left',
|
||||
title: '入库状态',
|
||||
width: 110,
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.WMS_RECEIPT_ORDER_TYPE },
|
||||
},
|
||||
field: 'type',
|
||||
title: '入库类型',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
field: 'warehouseName',
|
||||
minWidth: 180,
|
||||
title: '仓库',
|
||||
},
|
||||
{
|
||||
field: 'quantityAmount',
|
||||
minWidth: 180,
|
||||
slots: { default: 'quantityAmount' },
|
||||
title: '总数量/总金额(元)',
|
||||
},
|
||||
{
|
||||
field: 'merchantName',
|
||||
minWidth: 160,
|
||||
title: '供应商',
|
||||
},
|
||||
{
|
||||
field: 'operateInfo',
|
||||
minWidth: 260,
|
||||
slots: { default: 'operateInfo' },
|
||||
title: '操作信息',
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
minWidth: 160,
|
||||
title: '备注',
|
||||
},
|
||||
{
|
||||
field: 'actions',
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
title: '操作',
|
||||
width: 220,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 详情的字段 */
|
||||
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
{
|
||||
field: 'no',
|
||||
label: '入库单号',
|
||||
render: (val) => val || '-',
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
label: '入库类型',
|
||||
render: (val) =>
|
||||
val === undefined || val === null
|
||||
? '-'
|
||||
: h(DictTag, {
|
||||
type: DICT_TYPE.WMS_RECEIPT_ORDER_TYPE,
|
||||
value: val,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'warehouseName',
|
||||
label: '仓库',
|
||||
render: (val) => val || '-',
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
label: '单据状态',
|
||||
render: (val) =>
|
||||
val === undefined || val === null
|
||||
? '-'
|
||||
: h(DictTag, {
|
||||
type: DICT_TYPE.WMS_ORDER_STATUS,
|
||||
value: val,
|
||||
}),
|
||||
},
|
||||
{
|
||||
field: 'orderTime',
|
||||
label: '单据日期',
|
||||
render: (val) => formatDate(val, 'YYYY-MM-DD') || '-',
|
||||
},
|
||||
{
|
||||
field: 'merchantName',
|
||||
label: '供应商',
|
||||
render: (val) => val || '-',
|
||||
},
|
||||
{
|
||||
field: 'bizOrderNo',
|
||||
label: '业务单号',
|
||||
render: (val) => val || '-',
|
||||
},
|
||||
{
|
||||
field: 'totalQuantity',
|
||||
label: '总数量',
|
||||
render: (val) => formatQuantity(val) || '-',
|
||||
},
|
||||
{
|
||||
field: 'totalPrice',
|
||||
label: '总金额',
|
||||
render: (val) => formatPrice(val) || '-',
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
label: '创建时间',
|
||||
render: (val) => formatDateTime(val) || '-',
|
||||
},
|
||||
{
|
||||
field: 'creatorName',
|
||||
label: '创建人',
|
||||
render: (val, data) => val || data?.creator || '-',
|
||||
},
|
||||
{
|
||||
field: 'updateTime',
|
||||
label: '更新时间',
|
||||
render: (val) => formatDateTime(val) || '-',
|
||||
},
|
||||
{
|
||||
field: 'updaterName',
|
||||
label: '更新人',
|
||||
render: (val, data) => val || data?.updater || '-',
|
||||
},
|
||||
{
|
||||
field: 'remark',
|
||||
label: '备注',
|
||||
render: (val) => val || '-',
|
||||
span: 2,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 表单的配置项 */
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
show: () => false,
|
||||
triggerFields: [''],
|
||||
},
|
||||
fieldName: 'id',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
maxLength: 64,
|
||||
placeholder: '请输入入库单号',
|
||||
},
|
||||
fieldName: 'no',
|
||||
label: '入库单号',
|
||||
rules: z.string().min(1, '入库单号不能为空').max(64),
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
clearable: true,
|
||||
options: getDictOptions(DICT_TYPE.WMS_RECEIPT_ORDER_TYPE, 'number'),
|
||||
placeholder: '请选择入库类型',
|
||||
},
|
||||
fieldName: 'type',
|
||||
label: '入库类型',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: markRaw(WmsWarehouseSelect),
|
||||
fieldName: 'warehouseId',
|
||||
label: '仓库',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'DatePicker',
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
format: 'YYYY-MM-DD',
|
||||
placeholder: '请选择单据日期',
|
||||
valueFormat: 'x',
|
||||
},
|
||||
fieldName: 'orderTime',
|
||||
label: '单据日期',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: markRaw(WmsMerchantSelect),
|
||||
componentProps: {
|
||||
placeholder: '请选择供应商',
|
||||
supplier: true,
|
||||
},
|
||||
fieldName: 'merchantId',
|
||||
label: '供应商',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
maxLength: 64,
|
||||
placeholder: '请输入业务单号',
|
||||
},
|
||||
fieldName: 'bizOrderNo',
|
||||
label: '业务单号',
|
||||
},
|
||||
{
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
maxLength: 255,
|
||||
placeholder: '请输入备注',
|
||||
},
|
||||
fieldName: 'remark',
|
||||
formItemClass: 'col-span-2',
|
||||
label: '备注',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
interface ReceiptOrderDetailFooterRow {
|
||||
quantity?: number;
|
||||
totalPrice?: number;
|
||||
}
|
||||
|
||||
/** 明细表格的合计行 */
|
||||
export function getDetailFooter({
|
||||
columns,
|
||||
data,
|
||||
}: {
|
||||
columns: Array<{ field?: string }>;
|
||||
data: ReceiptOrderDetailFooterRow[];
|
||||
}) {
|
||||
return [
|
||||
columns.map((column, index) => {
|
||||
if (index === 0) {
|
||||
return '合计';
|
||||
}
|
||||
if (column.field === 'quantity') {
|
||||
return formatSumQuantity(data, (detail) => detail.quantity);
|
||||
}
|
||||
if (column.field === 'totalPrice') {
|
||||
return formatSumPrice(data, (detail) => detail.totalPrice);
|
||||
}
|
||||
return '';
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,347 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { WmsReceiptOrderApi } from '#/api/wms/order/receipt';
|
||||
import type { WmsReceiptOrderDetailApi } from '#/api/wms/order/receipt/detail';
|
||||
|
||||
import { reactive, ref } from 'vue';
|
||||
|
||||
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart, formatDateTime } from '@vben/utils';
|
||||
|
||||
import { ElLoading, ElMessage } from 'element-plus';
|
||||
|
||||
import {
|
||||
ACTION_ICON,
|
||||
TableAction,
|
||||
useVbenVxeGrid,
|
||||
VxeColumn,
|
||||
VxeTable,
|
||||
} from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteReceiptOrder,
|
||||
exportReceiptOrder,
|
||||
getReceiptOrderDetailListByOrderId,
|
||||
getReceiptOrderPage,
|
||||
} from '#/api/wms/order/receipt';
|
||||
import { $t } from '#/locales';
|
||||
import {
|
||||
OrderDeleteStatusList,
|
||||
OrderStatusEnum,
|
||||
OrderUpdateStatusList,
|
||||
} from '#/views/wms/utils/constants';
|
||||
import { formatPrice, formatQuantity, multiplyPrice } from '#/views/wms/utils/format';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import ReceiptOrderDetail from './modules/detail.vue';
|
||||
import ReceiptOrderForm from './modules/form.vue';
|
||||
import ReceiptOrderPrint from './modules/print.vue';
|
||||
|
||||
defineOptions({ name: 'WmsReceiptOrder' });
|
||||
|
||||
const printRef = ref<InstanceType<typeof ReceiptOrderPrint>>();
|
||||
const detailMap = reactive<
|
||||
Record<number, WmsReceiptOrderDetailApi.ReceiptOrderDetail[]>
|
||||
>({});
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: ReceiptOrderForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
const [DetailModal, detailModalApi] = useVbenModal({
|
||||
connectedComponent: ReceiptOrderDetail,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 清空展开明细缓存 */
|
||||
function clearDetailMap() {
|
||||
for (const id of Object.keys(detailMap)) {
|
||||
delete detailMap[Number(id)];
|
||||
}
|
||||
}
|
||||
|
||||
/** 刷新表格 */
|
||||
function handleRefresh() {
|
||||
clearDetailMap();
|
||||
gridApi.query();
|
||||
}
|
||||
|
||||
/** 创建入库单 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData({ type: 'create' }).open();
|
||||
}
|
||||
|
||||
/** 编辑入库单 */
|
||||
function handleEdit(row: WmsReceiptOrderApi.ReceiptOrder) {
|
||||
formModalApi.setData({ id: row.id!, type: 'update' }).open();
|
||||
}
|
||||
|
||||
/** 查看入库单详情 */
|
||||
function handleDetail(row: WmsReceiptOrderApi.ReceiptOrder) {
|
||||
detailModalApi.setData({ id: row.id! }).open();
|
||||
}
|
||||
|
||||
/** 计算单据明细金额 */
|
||||
function getDetailTotalPrice(
|
||||
detail: WmsReceiptOrderDetailApi.ReceiptOrderDetail,
|
||||
) {
|
||||
return detail.totalPrice ?? multiplyPrice(detail.quantity, detail.price);
|
||||
}
|
||||
|
||||
/** 获取已展开行的明细 */
|
||||
function getExpandedDetails(row: WmsReceiptOrderApi.ReceiptOrder) {
|
||||
return detailMap[row.id!] || [];
|
||||
}
|
||||
|
||||
/** 展开列表行时懒加载入库明细 */
|
||||
async function handleExpandChange(
|
||||
row: WmsReceiptOrderApi.ReceiptOrder,
|
||||
expanded: boolean,
|
||||
) {
|
||||
if (!expanded) {
|
||||
return;
|
||||
}
|
||||
delete detailMap[row.id!];
|
||||
detailMap[row.id!] = await getReceiptOrderDetailListByOrderId(row.id!);
|
||||
}
|
||||
|
||||
/** 判断入库单是否可修改 */
|
||||
function canUpdateReceiptOrder(status?: number) {
|
||||
return status !== undefined && OrderUpdateStatusList.includes(status);
|
||||
}
|
||||
|
||||
/** 判断入库单是否可删除 */
|
||||
function canDeleteReceiptOrder(status?: number) {
|
||||
return status !== undefined && OrderDeleteStatusList.includes(status);
|
||||
}
|
||||
|
||||
/** 获取修改按钮禁用提示 */
|
||||
function getReceiptOrderUpdateTip(status?: number) {
|
||||
if (canUpdateReceiptOrder(status)) {
|
||||
return undefined;
|
||||
}
|
||||
if (status === OrderStatusEnum.FINISHED) {
|
||||
return '已入库,无法修改';
|
||||
}
|
||||
if (status === OrderStatusEnum.CANCELED) {
|
||||
return '已作废,无法修改';
|
||||
}
|
||||
return '当前状态无法修改';
|
||||
}
|
||||
|
||||
/** 获取删除按钮禁用提示 */
|
||||
function getReceiptOrderDeleteTip(status?: number) {
|
||||
if (canDeleteReceiptOrder(status)) {
|
||||
return undefined;
|
||||
}
|
||||
if (status === OrderStatusEnum.FINISHED) {
|
||||
return '已入库,无法删除';
|
||||
}
|
||||
return '当前状态无法删除';
|
||||
}
|
||||
|
||||
/** 删除入库单 */
|
||||
async function handleDelete(row: WmsReceiptOrderApi.ReceiptOrder) {
|
||||
const loadingInstance = ElLoading.service({
|
||||
text: $t('ui.actionMessage.deleting', [row.no]),
|
||||
});
|
||||
try {
|
||||
await deleteReceiptOrder(row.id!);
|
||||
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.no]));
|
||||
handleRefresh();
|
||||
} finally {
|
||||
loadingInstance.close();
|
||||
}
|
||||
}
|
||||
|
||||
/** 导出入库单 */
|
||||
async function handleExport() {
|
||||
const data = await exportReceiptOrder(await gridApi.formApi.getValues());
|
||||
downloadFileFromBlobPart({ fileName: '入库单.xls', source: data });
|
||||
}
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
collapsed: true,
|
||||
schema: useGridFormSchema(),
|
||||
},
|
||||
gridOptions: {
|
||||
columns: useGridColumns(),
|
||||
expandConfig: {
|
||||
padding: true,
|
||||
trigger: 'default',
|
||||
},
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues) => {
|
||||
return await getReceiptOrderPage({
|
||||
pageNo: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
isHover: true,
|
||||
keyField: 'id',
|
||||
},
|
||||
toolbarConfig: {
|
||||
refresh: true,
|
||||
search: true,
|
||||
},
|
||||
} as VxeTableGridOptions<WmsReceiptOrderApi.ReceiptOrder>,
|
||||
gridEvents: {
|
||||
toggleRowExpand: ({
|
||||
expanded,
|
||||
row,
|
||||
}: {
|
||||
expanded: boolean;
|
||||
row: WmsReceiptOrderApi.ReceiptOrder;
|
||||
}) => {
|
||||
handleExpandChange(row, expanded);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<template #doc>
|
||||
<DocAlert title="【单据】入库" url="https://doc.iocoder.cn/wms/order/receipt/" />
|
||||
</template>
|
||||
<FormModal @success="handleRefresh" />
|
||||
<DetailModal />
|
||||
<ReceiptOrderPrint ref="printRef" />
|
||||
|
||||
<Grid table-title="入库单列表">
|
||||
<template #expand_content="{ row }">
|
||||
<VxeTable
|
||||
:data="getExpandedDetails(row)"
|
||||
border
|
||||
:show-overflow="true"
|
||||
size="small"
|
||||
>
|
||||
<VxeColumn title="商品信息" min-width="220">
|
||||
<template #default="{ row: detail }">
|
||||
<div>{{ detail.itemName || '-' }}</div>
|
||||
<div v-if="detail.itemCode" class="text-xs text-gray-500">
|
||||
商品编号:{{ detail.itemCode }}
|
||||
</div>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="规格信息" min-width="220">
|
||||
<template #default="{ row: detail }">
|
||||
<div>{{ detail.skuName || '-' }}</div>
|
||||
<div v-if="detail.skuCode" class="text-xs text-gray-500">
|
||||
规格编号:{{ detail.skuCode }}
|
||||
</div>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="入库数量" align="right" width="120">
|
||||
<template #default="{ row: detail }">
|
||||
{{ formatQuantity(detail.quantity) }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="单价(元)" align="right" width="120">
|
||||
<template #default="{ row: detail }">
|
||||
{{ formatPrice(detail.price) || '-' }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="金额(元)" align="right" width="120">
|
||||
<template #default="{ row: detail }">
|
||||
{{ formatPrice(getDetailTotalPrice(detail)) || '-' }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
</VxeTable>
|
||||
</template>
|
||||
<template #toolbar-tools>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('ui.actionTitle.create', ['入库单']),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.ADD,
|
||||
auth: ['wms:receipt-order:create'],
|
||||
onClick: handleCreate,
|
||||
},
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
icon: ACTION_ICON.DOWNLOAD,
|
||||
auth: ['wms:receipt-order:export'],
|
||||
onClick: handleExport,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
<template #no="{ row }">
|
||||
<div>
|
||||
单号:
|
||||
<a class="text-primary" @click="handleDetail(row)">{{ row.no }}</a>
|
||||
</div>
|
||||
<div v-if="row.bizOrderNo" class="text-xs text-gray-500">
|
||||
业务:{{ row.bizOrderNo }}
|
||||
</div>
|
||||
</template>
|
||||
<template #quantityAmount="{ row }">
|
||||
<div class="flex items-center justify-between">
|
||||
<span>数量:</span>
|
||||
<span>{{ formatQuantity(row.totalQuantity) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>金额:</span>
|
||||
<span>{{ formatPrice(row.totalPrice) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #operateInfo="{ row }">
|
||||
<div>
|
||||
创建:{{ formatDateTime(row.createTime) || '-' }} /
|
||||
{{ row.creatorName || row.creator || '-' }}
|
||||
</div>
|
||||
<div>
|
||||
更新:{{ formatDateTime(row.updateTime) || '-' }} /
|
||||
{{ row.updaterName || row.updater || '-' }}
|
||||
</div>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: $t('common.edit'),
|
||||
type: 'primary',
|
||||
link: true,
|
||||
icon: ACTION_ICON.EDIT,
|
||||
disabled: !canUpdateReceiptOrder(row.status),
|
||||
tooltip: getReceiptOrderUpdateTip(row.status),
|
||||
auth: ['wms:receipt-order:update'],
|
||||
onClick: handleEdit.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: $t('common.delete'),
|
||||
type: 'danger',
|
||||
link: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
disabled: !canDeleteReceiptOrder(row.status),
|
||||
tooltip: getReceiptOrderDeleteTip(row.status),
|
||||
auth: ['wms:receipt-order:delete'],
|
||||
popConfirm: {
|
||||
title: $t('ui.actionMessage.deleteConfirm', [row.no]),
|
||||
confirm: handleDelete.bind(null, row),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '打印',
|
||||
type: 'primary',
|
||||
link: true,
|
||||
auth: ['wms:receipt-order:query'],
|
||||
onClick: () => printRef?.print(row.id!),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
<script lang="ts" setup>
|
||||
import type { WmsReceiptOrderApi } from '#/api/wms/order/receipt';
|
||||
import type { WmsReceiptOrderDetailApi } from '#/api/wms/order/receipt/detail';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
|
||||
import {
|
||||
getReceiptOrder,
|
||||
getReceiptOrderDetailListByOrderId,
|
||||
} from '#/api/wms/order/receipt';
|
||||
import { useDescription } from '#/components/description';
|
||||
import {
|
||||
formatPrice,
|
||||
formatQuantity,
|
||||
formatSumPrice,
|
||||
formatSumQuantity,
|
||||
multiplyPrice,
|
||||
} from '#/views/wms/utils/format';
|
||||
|
||||
import { useDetailSchema } from '../data';
|
||||
|
||||
interface DetailRow extends WmsReceiptOrderDetailApi.ReceiptOrderDetail {
|
||||
totalPrice?: number;
|
||||
}
|
||||
|
||||
defineOptions({ name: 'WmsReceiptOrderDetail' });
|
||||
|
||||
const detailData = ref<WmsReceiptOrderApi.ReceiptOrder>({});
|
||||
|
||||
const detailRows = computed<DetailRow[]>(() =>
|
||||
(detailData.value.details || []).map((detail) => ({
|
||||
...detail,
|
||||
totalPrice: detail.totalPrice ?? multiplyPrice(detail.quantity, detail.price),
|
||||
})),
|
||||
);
|
||||
|
||||
const [Descriptions] = useDescription({
|
||||
border: true,
|
||||
column: 2,
|
||||
schema: useDetailSchema(),
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
detailData.value = {};
|
||||
return;
|
||||
}
|
||||
const data = modalApi.getData<{ id?: number }>();
|
||||
if (!data?.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
const order = await getReceiptOrder(data.id);
|
||||
const details =
|
||||
order.details || (await getReceiptOrderDetailListByOrderId(data.id));
|
||||
detailData.value = { ...order, details };
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
title="入库单详情"
|
||||
class="w-2/3"
|
||||
:show-cancel-button="false"
|
||||
:show-confirm-button="false"
|
||||
>
|
||||
<div class="mx-4 space-y-4">
|
||||
<Descriptions :data="detailData" />
|
||||
<VxeTable
|
||||
:data="detailRows"
|
||||
border
|
||||
empty-text="暂无商品明细"
|
||||
:show-overflow="true"
|
||||
size="small"
|
||||
>
|
||||
<VxeColumn title="商品信息" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.itemName || '-' }}</div>
|
||||
<div v-if="row.itemCode" class="text-xs text-gray-500">
|
||||
商品编号:{{ row.itemCode }}
|
||||
</div>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="规格信息" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.skuName || '-' }}</div>
|
||||
<div v-if="row.skuCode" class="text-xs text-gray-500">
|
||||
规格编号:{{ row.skuCode }}
|
||||
</div>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="数量" align="right" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ formatQuantity(row.quantity) || '-' }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="unit" title="单位" align="center" width="100" />
|
||||
<VxeColumn title="单价" align="right" width="140">
|
||||
<template #default="{ row }">
|
||||
{{ formatPrice(row.price) || '-' }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="总价" align="right" width="140">
|
||||
<template #default="{ row }">
|
||||
{{ formatPrice(row.totalPrice) || '-' }}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
</VxeTable>
|
||||
<div class="flex justify-end gap-6 text-sm">
|
||||
<span>合计数量:{{ formatSumQuantity(detailRows, (detail) => detail.quantity) }}</span>
|
||||
<span>合计金额:{{ formatSumPrice(detailRows, (detail) => detail.totalPrice) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,473 @@
|
|||
<script lang="ts" setup>
|
||||
import type { VxeTableInstance } from '#/adapter/vxe-table';
|
||||
import type { WmsItemSkuApi } from '#/api/wms/md/item/sku';
|
||||
import type { WmsReceiptOrderApi } from '#/api/wms/order/receipt';
|
||||
import type { WmsReceiptOrderDetailApi } from '#/api/wms/order/receipt/detail';
|
||||
|
||||
import { computed, nextTick, ref } from 'vue';
|
||||
|
||||
import { confirm, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElInputNumber, ElMessage } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { TableAction, VxeColumn, VxeTable } from '#/adapter/vxe-table';
|
||||
import {
|
||||
cancelReceiptOrder,
|
||||
completeReceiptOrder,
|
||||
createReceiptOrder,
|
||||
getReceiptOrder,
|
||||
getReceiptOrderDetailListByOrderId,
|
||||
updateReceiptOrder,
|
||||
} from '#/api/wms/order/receipt';
|
||||
import { $t } from '#/locales';
|
||||
import { WmsItemSkuSelect } from '#/views/wms/md/item/sku/components';
|
||||
import {
|
||||
OrderStatusEnum,
|
||||
OrderUpdateStatusList,
|
||||
} from '#/views/wms/utils/constants';
|
||||
import {
|
||||
dividePrice,
|
||||
multiplyPrice,
|
||||
PRICE_PRECISION,
|
||||
QUANTITY_PRECISION,
|
||||
} from '#/views/wms/utils/format';
|
||||
import { generateOrderNo } from '#/views/wms/utils/order';
|
||||
|
||||
import { getDetailFooter, useFormSchema } from '../data';
|
||||
|
||||
interface DetailRow extends WmsReceiptOrderDetailApi.ReceiptOrderDetail {
|
||||
seq: number;
|
||||
}
|
||||
|
||||
type FormMode = 'create' | 'update';
|
||||
|
||||
defineOptions({ name: 'WmsReceiptOrderForm' });
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const formData = ref<WmsReceiptOrderApi.ReceiptOrder>();
|
||||
const formMode = ref<FormMode>('create');
|
||||
const originalSubmitData = ref('');
|
||||
const details = ref<DetailRow[]>([]);
|
||||
const detailTableRef = ref<VxeTableInstance>();
|
||||
const skuSelectRef = ref<InstanceType<typeof WmsItemSkuSelect>>();
|
||||
let detailSeq = 0; // 明细行可能还没有后端 id,使用本地序号作为 VXE 行操作的稳定标识
|
||||
|
||||
const getTitle = computed(() => {
|
||||
return formMode.value === 'update'
|
||||
? $t('ui.actionTitle.edit', ['入库单'])
|
||||
: $t('ui.actionTitle.create', ['入库单']);
|
||||
});
|
||||
|
||||
const isPrepareOrder = computed(() => {
|
||||
return (
|
||||
!formData.value?.id ||
|
||||
(formData.value.status !== undefined &&
|
||||
OrderUpdateStatusList.includes(formData.value.status))
|
||||
);
|
||||
});
|
||||
const isSavedPrepareOrder = computed(() => {
|
||||
return (
|
||||
!!formData.value?.id &&
|
||||
formData.value.status !== undefined &&
|
||||
OrderUpdateStatusList.includes(formData.value.status)
|
||||
);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
labelWidth: 100,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
wrapperClass: 'grid-cols-3',
|
||||
});
|
||||
|
||||
/** 标准化明细行,补齐本地序号和金额 */
|
||||
function normalizeDetail(
|
||||
detail: WmsReceiptOrderDetailApi.ReceiptOrderDetail,
|
||||
): DetailRow {
|
||||
detailSeq += 1;
|
||||
return {
|
||||
...detail,
|
||||
seq: detailSeq,
|
||||
totalPrice: detail.totalPrice ?? multiplyPrice(detail.quantity, detail.price),
|
||||
};
|
||||
}
|
||||
|
||||
/** 根据商品 SKU 构建新的入库明细 */
|
||||
function buildDetail(sku: WmsItemSkuApi.ItemSku): DetailRow {
|
||||
return normalizeDetail({
|
||||
id: undefined,
|
||||
itemCode: sku.itemCode,
|
||||
itemId: sku.itemId,
|
||||
itemName: sku.itemName,
|
||||
price: undefined,
|
||||
quantity: undefined,
|
||||
skuCode: sku.code,
|
||||
skuId: sku.id,
|
||||
skuName: sku.name,
|
||||
totalPrice: undefined,
|
||||
unit: sku.unit,
|
||||
});
|
||||
}
|
||||
|
||||
/** 设置入库明细 */
|
||||
function setDetails(list?: WmsReceiptOrderDetailApi.ReceiptOrderDetail[]) {
|
||||
details.value = (list || []).map((detail) => normalizeDetail(detail));
|
||||
void refreshDetailFooter();
|
||||
}
|
||||
|
||||
/** 刷新明细合计行 */
|
||||
async function refreshDetailFooter() {
|
||||
await nextTick();
|
||||
await detailTableRef.value?.updateFooter();
|
||||
}
|
||||
|
||||
/** 获取已选择的 SKU 编号,避免重复选择 */
|
||||
function getSelectedSkuIds() {
|
||||
return details.value
|
||||
.map((detail) => detail.skuId)
|
||||
.filter((id): id is number => !!id);
|
||||
}
|
||||
|
||||
/** 添加商品明细 */
|
||||
async function handleAddDetail() {
|
||||
const values = (await formApi.getValues()) as WmsReceiptOrderApi.ReceiptOrder;
|
||||
if (!values.warehouseId) {
|
||||
ElMessage.warning('请先选择仓库');
|
||||
return;
|
||||
}
|
||||
skuSelectRef.value?.open(getSelectedSkuIds());
|
||||
}
|
||||
|
||||
/** 选择商品 SKU */
|
||||
function handleSelectSku(skus: WmsItemSkuApi.ItemSku[]) {
|
||||
if (skus.length === 0) {
|
||||
return;
|
||||
}
|
||||
const selectedSkuIds = new Set(getSelectedSkuIds());
|
||||
let changed = false;
|
||||
for (const sku of skus) {
|
||||
if (!sku.id || selectedSkuIds.has(sku.id)) {
|
||||
continue;
|
||||
}
|
||||
details.value.push(buildDetail(sku));
|
||||
selectedSkuIds.add(sku.id);
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
void refreshDetailFooter();
|
||||
}
|
||||
}
|
||||
|
||||
/** 删除商品明细 */
|
||||
function handleDeleteDetail(row: DetailRow) {
|
||||
const index = details.value.findIndex((detail) => detail.seq === row.seq);
|
||||
if (index !== -1) {
|
||||
details.value.splice(index, 1);
|
||||
void refreshDetailFooter();
|
||||
}
|
||||
}
|
||||
|
||||
/** 明细数量变化 */
|
||||
function handleDetailQuantityChange(detail: DetailRow) {
|
||||
if (detail.price !== undefined && detail.price !== null) {
|
||||
detail.totalPrice = multiplyPrice(detail.quantity, detail.price);
|
||||
void refreshDetailFooter();
|
||||
return;
|
||||
}
|
||||
detail.price = dividePrice(detail.totalPrice, detail.quantity);
|
||||
void refreshDetailFooter();
|
||||
}
|
||||
|
||||
/** 明细单价变化 */
|
||||
function handleDetailPriceChange(detail: DetailRow) {
|
||||
detail.totalPrice = multiplyPrice(detail.quantity, detail.price);
|
||||
void refreshDetailFooter();
|
||||
}
|
||||
|
||||
/** 明细金额变化 */
|
||||
function handleDetailTotalPriceChange(detail: DetailRow) {
|
||||
detail.price = dividePrice(detail.totalPrice, detail.quantity);
|
||||
void refreshDetailFooter();
|
||||
}
|
||||
|
||||
/** 校验商品明细 */
|
||||
function validateDetails(required = false) {
|
||||
if (details.value.length === 0) {
|
||||
if (required) {
|
||||
ElMessage.error('至少包含一条入库明细');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
for (let index = 0; index < details.value.length; index += 1) {
|
||||
const detail = details.value[index]!;
|
||||
if (!detail.skuId) {
|
||||
ElMessage.error(`第 ${index + 1} 行明细请选择商品规格`);
|
||||
return false;
|
||||
}
|
||||
if (!detail.quantity || detail.quantity <= 0) {
|
||||
ElMessage.error(`第 ${index + 1} 行明细入库数量必须大于 0`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 构建提交用的明细数据 */
|
||||
function buildSubmitDetails() {
|
||||
return details.value.map((row) => {
|
||||
const { seq: _seq, ...detail } = row;
|
||||
return detail;
|
||||
});
|
||||
}
|
||||
|
||||
/** 构建提交用的单据数据 */
|
||||
async function buildSubmitData(): Promise<WmsReceiptOrderApi.ReceiptOrder> {
|
||||
const values = (await formApi.getValues()) as WmsReceiptOrderApi.ReceiptOrder;
|
||||
const {
|
||||
details: _details,
|
||||
totalPrice: _totalPrice,
|
||||
totalQuantity: _totalQuantity,
|
||||
...order
|
||||
} = formData.value || {};
|
||||
return {
|
||||
...order,
|
||||
...values,
|
||||
details: buildSubmitDetails(),
|
||||
};
|
||||
}
|
||||
|
||||
/** 完成入库 */
|
||||
async function handleFormComplete() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid || !validateDetails(true) || !formData.value?.id) {
|
||||
return;
|
||||
}
|
||||
await confirm('确认完成入库?完成后将更新库存。');
|
||||
modalApi.lock();
|
||||
try {
|
||||
const data = await buildSubmitData();
|
||||
if (JSON.stringify(data) !== originalSubmitData.value) {
|
||||
await updateReceiptOrder(data);
|
||||
}
|
||||
await completeReceiptOrder(formData.value.id);
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
ElMessage.success('入库成功');
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/** 作废入库单 */
|
||||
async function handleFormCancel() {
|
||||
if (!formData.value?.id) {
|
||||
return;
|
||||
}
|
||||
await confirm('确认作废该入库单?作废后不可恢复。');
|
||||
modalApi.lock();
|
||||
try {
|
||||
await cancelReceiptOrder(formData.value.id);
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
ElMessage.success('作废成功');
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid || !validateDetails(false) || !isPrepareOrder.value) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = await buildSubmitData();
|
||||
try {
|
||||
await (formMode.value === 'update'
|
||||
? updateReceiptOrder(data)
|
||||
: createReceiptOrder(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
originalSubmitData.value = '';
|
||||
setDetails([]);
|
||||
return;
|
||||
}
|
||||
await formApi.resetForm();
|
||||
const data = modalApi.getData<{
|
||||
id?: number;
|
||||
type?: FormMode;
|
||||
}>();
|
||||
formMode.value = data?.type || (data?.id ? 'update' : 'create');
|
||||
if (data?.id) {
|
||||
modalApi.lock();
|
||||
try {
|
||||
// 加载数据
|
||||
const order = await getReceiptOrder(data.id);
|
||||
const orderDetails =
|
||||
order.details || (await getReceiptOrderDetailListByOrderId(data.id));
|
||||
formData.value = { ...order, details: orderDetails };
|
||||
setDetails(orderDetails);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
originalSubmitData.value = JSON.stringify(await buildSubmitData());
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 初始化新增表单
|
||||
formData.value = {
|
||||
details: [],
|
||||
no: generateOrderNo('RK'),
|
||||
status: OrderStatusEnum.PREPARE,
|
||||
};
|
||||
setDetails([]);
|
||||
await formApi.setValues(formData.value);
|
||||
originalSubmitData.value = JSON.stringify(await buildSubmitData());
|
||||
await nextTick();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle" class="w-3/4">
|
||||
<div class="mx-4">
|
||||
<Form />
|
||||
<div class="mt-4">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<span class="text-sm font-semibold">入库明细</span>
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '添加商品',
|
||||
onClick: handleAddDetail,
|
||||
type: 'primary',
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
<VxeTable
|
||||
ref="detailTableRef"
|
||||
:data="details"
|
||||
border
|
||||
empty-text="暂无商品明细"
|
||||
:footer-method="getDetailFooter"
|
||||
:show-overflow="true"
|
||||
show-footer
|
||||
size="small"
|
||||
>
|
||||
<VxeColumn title="商品信息" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.itemName || '-' }}</div>
|
||||
<div v-if="row.itemCode" class="text-xs text-gray-500">
|
||||
商品编号:{{ row.itemCode }}
|
||||
</div>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="规格信息" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.skuName || '-' }}</div>
|
||||
<div v-if="row.skuCode" class="text-xs text-gray-500">
|
||||
规格编号:{{ row.skuCode }}
|
||||
</div>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="quantity" title="入库数量" width="150">
|
||||
<template #default="{ row }">
|
||||
<ElInputNumber
|
||||
v-model="row.quantity"
|
||||
:controls="false"
|
||||
:min="0"
|
||||
:precision="QUANTITY_PRECISION"
|
||||
class="!w-full"
|
||||
placeholder="数量"
|
||||
@change="handleDetailQuantityChange(row)"
|
||||
/>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="price" title="单价(元)" width="150">
|
||||
<template #default="{ row }">
|
||||
<ElInputNumber
|
||||
v-model="row.price"
|
||||
:controls="false"
|
||||
:min="0"
|
||||
:precision="PRICE_PRECISION"
|
||||
class="!w-full"
|
||||
placeholder="单价"
|
||||
@change="handleDetailPriceChange(row)"
|
||||
/>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="totalPrice" title="金额(元)" width="150">
|
||||
<template #default="{ row }">
|
||||
<ElInputNumber
|
||||
v-model="row.totalPrice"
|
||||
:controls="false"
|
||||
:min="0"
|
||||
:precision="PRICE_PRECISION"
|
||||
class="!w-full"
|
||||
placeholder="金额"
|
||||
@change="handleDetailTotalPriceChange(row)"
|
||||
/>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn title="操作" align="center" fixed="right" width="90">
|
||||
<template #default="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '删除',
|
||||
type: 'danger',
|
||||
link: true,
|
||||
onClick: handleDeleteDetail.bind(null, row),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
</VxeTable>
|
||||
</div>
|
||||
</div>
|
||||
<template #prepend-footer>
|
||||
<div v-if="isSavedPrepareOrder" class="flex flex-auto items-center gap-2">
|
||||
<TableAction
|
||||
:actions="[
|
||||
{
|
||||
label: '完成入库',
|
||||
type: 'primary',
|
||||
auth: ['wms:receipt-order:complete'],
|
||||
onClick: handleFormComplete,
|
||||
},
|
||||
{
|
||||
label: '作废',
|
||||
type: 'danger',
|
||||
auth: ['wms:receipt-order:cancel'],
|
||||
onClick: handleFormCancel,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<WmsItemSkuSelect ref="skuSelectRef" @change="handleSelectSku" />
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,231 @@
|
|||
<script lang="ts" setup>
|
||||
import type { WmsReceiptOrderApi } from '#/api/wms/order/receipt';
|
||||
import type { WmsReceiptOrderDetailApi } from '#/api/wms/order/receipt/detail';
|
||||
|
||||
import { computed, nextTick, ref } from 'vue';
|
||||
|
||||
import { Barcode, BarcodeFormatEnum } from '@vben/common-ui';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictLabel } from '@vben/hooks';
|
||||
import { formatDate, formatDateTime } from '@vben/utils';
|
||||
|
||||
import {
|
||||
getReceiptOrder,
|
||||
getReceiptOrderDetailListByOrderId,
|
||||
} from '#/api/wms/order/receipt';
|
||||
import {
|
||||
formatPrice,
|
||||
formatQuantity,
|
||||
formatSumPrice,
|
||||
formatSumQuantity,
|
||||
multiplyPrice,
|
||||
} from '#/views/wms/utils/format';
|
||||
|
||||
interface PrintRow extends WmsReceiptOrderDetailApi.ReceiptOrderDetail {
|
||||
totalPrice?: number;
|
||||
}
|
||||
|
||||
defineOptions({ name: 'WmsReceiptOrderPrint' });
|
||||
|
||||
const printData = ref<WmsReceiptOrderApi.ReceiptOrder>({});
|
||||
const tableColumnCount = 5;
|
||||
|
||||
const printRows = computed<PrintRow[]>(() =>
|
||||
(printData.value.details || []).map((detail) => ({
|
||||
...detail,
|
||||
totalPrice: detail.totalPrice ?? multiplyPrice(detail.quantity, detail.price),
|
||||
})),
|
||||
);
|
||||
|
||||
/** 等待条码和打印 DOM 完成绘制,避免浏览器打印到旧内容 */
|
||||
function waitForPaint() {
|
||||
return new Promise<void>((resolve) => {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => resolve());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** 退出打印模式,恢复当前页面显示 */
|
||||
function removePrintMode() {
|
||||
document.body.classList.remove('wms-receipt-order-printing');
|
||||
}
|
||||
|
||||
/** 获取打印用字典文案,空值统一显示为横杠 */
|
||||
function getPrintDictLabel(dictType: string, value?: number) {
|
||||
if (value === undefined || value === null) {
|
||||
return '-';
|
||||
}
|
||||
return getDictLabel(dictType, value) || '-';
|
||||
}
|
||||
|
||||
/** 打印入库单:加载数据后只展示打印区域,再调用浏览器打印 */
|
||||
async function print(id: number) {
|
||||
const order = await getReceiptOrder(id);
|
||||
const details = order.details || (await getReceiptOrderDetailListByOrderId(id));
|
||||
printData.value = { ...order, details };
|
||||
await nextTick();
|
||||
await waitForPaint();
|
||||
document.body.classList.add('wms-receipt-order-printing');
|
||||
window.addEventListener('afterprint', removePrintMode, { once: true });
|
||||
window.print();
|
||||
}
|
||||
|
||||
defineExpose({ print });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
id="wmsReceiptOrderPrint"
|
||||
class="wms-receipt-order-print pointer-events-none fixed left-0 top-0 z-[-1] w-full bg-white text-[#303133] opacity-0"
|
||||
>
|
||||
<div class="relative mb-2">
|
||||
<h2 class="m-0 text-center text-[1.5em] font-bold leading-[1.2]">
|
||||
入库单
|
||||
</h2>
|
||||
<div v-if="printData.no" class="absolute right-0 top-0">
|
||||
<Barcode
|
||||
:content="printData.no"
|
||||
:display-value="false"
|
||||
:format="BarcodeFormatEnum.CODE39"
|
||||
:height="40"
|
||||
:width="180"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3 grid grid-cols-3 gap-x-6 gap-y-2 text-sm leading-[1.5]">
|
||||
<div>入库单号:{{ printData.no || '-' }}</div>
|
||||
<div>
|
||||
入库类型:{{ getPrintDictLabel(DICT_TYPE.WMS_RECEIPT_ORDER_TYPE, printData.type) }}
|
||||
</div>
|
||||
<div>仓库:{{ printData.warehouseName || '-' }}</div>
|
||||
<div>
|
||||
入库状态:{{ getPrintDictLabel(DICT_TYPE.WMS_ORDER_STATUS, printData.status) }}
|
||||
</div>
|
||||
<div>单据日期:{{ formatDate(printData.orderTime, 'YYYY-MM-DD') || '-' }}</div>
|
||||
<div>供应商:{{ printData.merchantName || '-' }}</div>
|
||||
<div>业务单号:{{ printData.bizOrderNo || '-' }}</div>
|
||||
<div>总数量:{{ formatQuantity(printData.totalQuantity) || '-' }}</div>
|
||||
<div>总金额:{{ formatPrice(printData.totalPrice) || '-' }}</div>
|
||||
<div class="col-span-3 grid grid-cols-2 gap-x-6">
|
||||
<div>
|
||||
创建:{{ formatDateTime(printData.createTime) || '-' }} /
|
||||
{{ printData.creatorName || printData.creator || '-' }}
|
||||
</div>
|
||||
<div>
|
||||
更新:{{ formatDateTime(printData.updateTime) || '-' }} /
|
||||
{{ printData.updaterName || printData.updater || '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-3">备注:{{ printData.remark || '-' }}</div>
|
||||
</div>
|
||||
<table class="w-full border-collapse text-[13px] leading-[1.5]">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold">
|
||||
商品信息
|
||||
</th>
|
||||
<th class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold">
|
||||
规格信息
|
||||
</th>
|
||||
<th class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold">
|
||||
数量
|
||||
</th>
|
||||
<th class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold">
|
||||
单价(元)
|
||||
</th>
|
||||
<th class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold">
|
||||
金额(元)
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="detail in printRows" :key="detail.id || detail.skuId">
|
||||
<td class="border border-solid border-[#dcdfe6] p-2">
|
||||
<div>{{ detail.itemName || '-' }}</div>
|
||||
<div v-if="detail.itemCode" class="text-xs">
|
||||
编号:{{ detail.itemCode }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="border border-solid border-[#dcdfe6] p-2">
|
||||
<div>{{ detail.skuName || '-' }}</div>
|
||||
<div v-if="detail.skuCode" class="text-xs">
|
||||
编号:{{ detail.skuCode }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="border border-solid border-[#dcdfe6] p-2 text-right">
|
||||
{{ formatQuantity(detail.quantity) || '-' }}
|
||||
</td>
|
||||
<td class="border border-solid border-[#dcdfe6] p-2 text-right">
|
||||
{{ formatPrice(detail.price) || '-' }}
|
||||
</td>
|
||||
<td class="border border-solid border-[#dcdfe6] p-2 text-right">
|
||||
{{ formatPrice(detail.totalPrice) || '-' }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="printRows.length > 0">
|
||||
<td
|
||||
class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2"
|
||||
colspan="2"
|
||||
>
|
||||
合计
|
||||
</td>
|
||||
<td class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right">
|
||||
{{ formatSumQuantity(printRows, (detail) => detail.quantity) }}
|
||||
</td>
|
||||
<td class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right"></td>
|
||||
<td class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right">
|
||||
{{ formatSumPrice(printRows, (detail) => detail.totalPrice) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="printRows.length === 0">
|
||||
<td
|
||||
class="border border-solid border-[#dcdfe6] p-2 text-center"
|
||||
:colspan="tableColumnCount"
|
||||
>
|
||||
暂无明细
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@page {
|
||||
margin: 8mm 10mm;
|
||||
}
|
||||
|
||||
@media print {
|
||||
:global(body.wms-receipt-order-printing) {
|
||||
-webkit-print-color-adjust: exact;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
print-color-adjust: exact;
|
||||
}
|
||||
|
||||
:global(body.wms-receipt-order-printing *) {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
:global(body.wms-receipt-order-printing .wms-receipt-order-print),
|
||||
:global(body.wms-receipt-order-printing .wms-receipt-order-print *) {
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
:global(body.wms-receipt-order-printing .wms-receipt-order-print) {
|
||||
pointer-events: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: auto;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue