feat(wms):增加 receipt 功能、评审

pull/345/head
YunaiV 2026-05-17 21:39:15 +08:00
parent 8710da9383
commit 4933180560
2 changed files with 675 additions and 0 deletions

View File

@ -0,0 +1,334 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { markRaw } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { z } from '#/adapter/form';
import { getSimpleUserList } from '#/api/system/user';
import { getRangePickerDefaultProps } from '#/utils';
import { WmsMerchantSelect } from '#/views/wms/md/merchant/components';
import { WmsWarehouseSelect } from '#/views/wms/md/warehouse/components';
import {
PRICE_PRECISION,
QUANTITY_PRECISION,
} from '#/views/wms/utils/format';
import NumberRangeInput from './modules/number-range-input.vue';
function splitNumberRange(minFieldName: string, maxFieldName: string) {
return (
value: [number | undefined, number | undefined] | undefined,
setValue: (fieldName: string, value: any) => void,
) => {
setValue(minFieldName, value?.[0]);
setValue(maxFieldName, value?.[1]);
return undefined;
};
}
function buildNumberRangeSchema(
label: string,
fieldName: string,
minFieldName: string,
maxFieldName: string,
precision: number,
): VbenFormSchema {
return {
component: markRaw(NumberRangeInput),
componentProps: {
min: 0,
precision,
},
fieldName,
label,
valueFormat: splitNumberRange(minFieldName, maxFieldName),
};
}
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入入库单号',
},
fieldName: 'no',
label: '入库单号',
},
{
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入业务单号',
},
fieldName: 'bizOrderNo',
label: '业务单号',
},
{
component: 'Select',
componentProps: {
allowClear: 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(),
allowClear: true,
},
fieldName: 'orderTime',
label: '单据日期',
},
buildNumberRangeSchema(
'数量',
'totalQuantityRange',
'totalQuantityMin',
'totalQuantityMax',
QUANTITY_PRECISION,
),
buildNumberRangeSchema(
'总金额',
'totalPriceRange',
'totalPriceMin',
'totalPriceMax',
PRICE_PRECISION,
),
{
component: 'Select',
componentProps: {
allowClear: true,
options: getDictOptions(DICT_TYPE.WMS_RECEIPT_ORDER_TYPE, 'number'),
placeholder: '请选择入库类型',
},
fieldName: 'type',
label: '入库类型',
},
{
component: 'ApiSelect',
componentProps: {
allowClear: true,
api: getSimpleUserList,
labelField: 'nickname',
placeholder: '请选择创建用户',
showSearch: true,
valueField: 'id',
},
fieldName: 'creator',
label: '创建用户',
},
{
component: 'ApiSelect',
componentProps: {
allowClear: true,
api: getSimpleUserList,
labelField: 'nickname',
placeholder: '请选择更新用户',
showSearch: true,
valueField: 'id',
},
fieldName: 'updater',
label: '更新用户',
},
{
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
allowClear: true,
},
fieldName: 'createTime',
label: '创建时间',
},
{
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
allowClear: 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 useFormSchema(
handleWarehouseChange: () => Promise<void> | void,
): 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: {
allowClear: true,
options: getDictOptions(DICT_TYPE.WMS_RECEIPT_ORDER_TYPE, 'number'),
placeholder: '请选择入库类型',
},
fieldName: 'type',
label: '入库类型',
rules: 'required',
},
{
component: markRaw(WmsWarehouseSelect),
componentProps: {
onChange: handleWarehouseChange,
},
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: '备注',
},
];
}

View File

@ -0,0 +1,341 @@
<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';
// TODO @AI system user index.vue function
// TODO @AIrowid
import { reactive, ref } from 'vue';
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart, formatDateTime } from '@vben/utils';
import { message } from 'ant-design-vue';
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 handleRefresh() {
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 handlePrint(row: WmsReceiptOrderApi.ReceiptOrder) {
// TODO @AI
if (row.id) {
printRef.value?.print(row.id);
}
}
/** 计算单据明细金额 */
function getDetailTotalPrice(
detail: WmsReceiptOrderDetailApi.ReceiptOrderDetail,
) {
return detail.totalPrice ?? multiplyPrice(detail.quantity, detail.price);
}
function getExpandedDetails(row: WmsReceiptOrderApi.ReceiptOrder) {
return row.id ? detailMap[row.id] || [] : [];
}
/** 展开列表行时懒加载入库明细 */
// TODO @AI >
async function handleExpandChange(
row: WmsReceiptOrderApi.ReceiptOrder,
expanded: boolean,
) {
if (!row.id || !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) {
if (!row.id) {
return;
}
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.no]),
duration: 0,
});
try {
await deleteReceiptOrder(row.id);
message.success($t('ui.actionMessage.deleteSuccess', [row.no]));
handleRefresh();
} finally {
hideLoading();
}
}
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: 'row',
},
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: 'link',
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: 'link',
danger: 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: 'link',
auth: ['wms:receipt-order:query'],
onClick: handlePrint.bind(null, row),
},
]"
/>
</template>
</Grid>
</Page>
</template>