feat(mes): 迁移 wm 里的 产品收货(wm_product_recpt)、、销售出库(wm_product_sales)、销售发货通知(wm_sales_notice)、领料出库(wm_production_issue)、采购入库(wm_item_recpt)、到货通知(wm_arrival_notice)

This commit is contained in:
YunaiV
2026-05-30 11:09:48 +08:00
parent f5fbd85e26
commit 83482781d6
96 changed files with 19161 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
export { default as WmArrivalNoticeLineSelect } from './wm-arrival-notice-line-select.vue';
export { default as WmArrivalNoticeSelect } from './wm-arrival-notice-select.vue';

View File

@@ -0,0 +1,203 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmArrivalNoticeLineApi } from '#/api/mes/wm/arrivalnotice/line';
import { nextTick, ref } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { message, Modal } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getArrivalNoticeLinePage } from '#/api/mes/wm/arrivalnotice/line';
const emit = defineEmits<{
selected: [rows: MesWmArrivalNoticeLineApi.ArrivalNoticeLine[]];
}>();
const open = ref(false); // 弹窗是否打开
const noticeId = ref<number>(); // 所属通知单编号
const syncingSingleSelection = ref(false); // 是否同步单选勾选状态
const selectedRows = ref<MesWmArrivalNoticeLineApi.ArrivalNoticeLine[]>([]); // 已选行列表
const preSelectedIds = ref<number[]>([]); // 预选行编号列表
/** 表格字段 */
function useGridColumns(): VxeTableGridOptions<MesWmArrivalNoticeLineApi.ArrivalNoticeLine>['columns'] {
return [
{
type: 'checkbox',
width: 50,
},
{
field: 'itemCode',
title: '物料编码',
minWidth: 120,
},
{
field: 'itemName',
title: '物料名称',
minWidth: 140,
},
{
field: 'specification',
title: '规格型号',
minWidth: 120,
},
{
field: 'unitMeasureName',
title: '单位',
width: 80,
},
{
field: 'arrivalQuantity',
title: '到货数量',
width: 100,
},
{
field: 'iqcCheckFlag',
title: '是否检验',
width: 90,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
},
},
{
field: 'qualifiedQuantity',
title: '合格数量',
width: 100,
},
{
field: 'remark',
title: '备注',
minWidth: 120,
},
];
}
/** 单选模式下同步 VXE 勾选状态 */
async function syncSingleSelection(
row?: MesWmArrivalNoticeLineApi.ArrivalNoticeLine,
) {
syncingSingleSelection.value = true;
await nextTick();
await gridApi.grid.clearCheckboxRow();
if (row) {
await gridApi.grid.setCheckboxRow(row, true);
}
await nextTick();
syncingSingleSelection.value = false;
}
/** 处理勾选变化,只保留最后一条 */
async function handleCheckboxChange({
checked,
row,
}: {
checked: boolean;
row?: MesWmArrivalNoticeLineApi.ArrivalNoticeLine;
}) {
if (syncingSingleSelection.value) {
return;
}
const selected = checked && row ? [row] : [];
selectedRows.value = selected;
await syncSingleSelection(selected[0]);
}
/** 回显预选行 */
function applyPreSelection() {
if (preSelectedIds.value.length === 0) {
return;
}
const rows =
gridApi.grid.getData() as MesWmArrivalNoticeLineApi.ArrivalNoticeLine[];
for (const row of rows) {
if (row.id && preSelectedIds.value.includes(row.id)) {
gridApi.grid.setCheckboxRow(row, true);
selectedRows.value = [row];
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useGridColumns(),
height: 460,
keepSource: true,
checkboxConfig: {
highlight: true,
reserve: true,
},
proxyConfig: {
ajax: {
query: async ({ page }) => {
if (!noticeId.value) {
return { list: [], total: 0 };
}
return await getArrivalNoticeLinePage({
noticeId: noticeId.value,
pageNo: page.currentPage,
pageSize: page.pageSize,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
},
} as VxeTableGridOptions<MesWmArrivalNoticeLineApi.ArrivalNoticeLine>,
gridEvents: {
checkboxChange: handleCheckboxChange,
},
});
/** 打开行选择弹窗 */
async function openModal(id: number | undefined, selectedIds?: number[]) {
open.value = true;
noticeId.value = id;
preSelectedIds.value = selectedIds || [];
selectedRows.value = [];
await nextTick();
await gridApi.grid.clearCheckboxRow();
await gridApi.query();
await nextTick();
applyPreSelection();
}
/** 关闭弹窗 */
async function closeModal() {
open.value = false;
selectedRows.value = [];
await gridApi.grid.clearCheckboxRow();
}
/** 确认选择行 */
function handleConfirm() {
if (selectedRows.value.length === 0) {
message.warning('请选择一条数据');
return;
}
emit('selected', [selectedRows.value[0]!]);
open.value = false;
}
defineExpose({ open: openModal });
</script>
<template>
<Modal
v-model:open="open"
title="到货通知单行选择"
width="70%"
:destroy-on-close="true"
@cancel="closeModal"
@ok="handleConfirm"
>
<Grid table-title="到货通知单行列表" />
</Modal>
</template>

View File

@@ -0,0 +1,151 @@
<script lang="ts" setup>
import type { MesWmArrivalNoticeLineApi } from '#/api/mes/wm/arrivalnotice/line';
import { computed, ref, useAttrs, watch } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { Input, Tooltip } from 'ant-design-vue';
import { getArrivalNoticeLine } from '#/api/mes/wm/arrivalnotice/line';
import WmArrivalNoticeLineSelectDialog from './wm-arrival-notice-line-select-dialog.vue';
defineOptions({ name: 'WmArrivalNoticeLineSelect', inheritAttrs: false });
const props = withDefaults(
defineProps<{
allowClear?: boolean;
disabled?: boolean;
modelValue?: number;
noticeId?: number; // 所属到货通知单编号
placeholder?: string;
}>(),
{
allowClear: true,
disabled: false,
modelValue: undefined,
noticeId: undefined,
placeholder: '请选择到货通知单行',
},
);
const emit = defineEmits<{
change: [item: MesWmArrivalNoticeLineApi.ArrivalNoticeLine | undefined];
'update:modelValue': [value: number | undefined];
}>();
const attrs = useAttrs();
const dialogRef = ref<InstanceType<typeof WmArrivalNoticeLineSelectDialog>>();
const hovering = ref(false);
const selectedItem = ref<MesWmArrivalNoticeLineApi.ArrivalNoticeLine>();
const displayLabel = computed(() => {
const item = selectedItem.value;
if (!item) {
return '';
}
return `${item.itemCode ?? ''} - ${item.itemName ?? ''}`;
});
const showClear = computed(
() =>
props.allowClear &&
!props.disabled &&
hovering.value &&
props.modelValue != null,
);
/** 根据编号单条查询行信息(用于编辑回显) */
async function resolveItemById(id: number | undefined) {
if (id == null) {
selectedItem.value = undefined;
return;
}
if (selectedItem.value?.id === id) {
return;
}
selectedItem.value = await getArrivalNoticeLine(id);
}
watch(() => props.modelValue, resolveItemById, { immediate: true });
/** noticeId 变化时清空选中(关联的行已失效) */
watch(
() => props.noticeId,
() => {
selectedItem.value = undefined;
emit('update:modelValue', undefined);
emit('change', undefined);
},
);
/** 清空已选行 */
function clearSelected() {
selectedItem.value = undefined;
emit('update:modelValue', undefined);
emit('change', undefined);
}
/** 打开行选择弹窗 */
function handleClick(event: MouseEvent) {
if (props.disabled || !props.noticeId) {
return;
}
const target = event.target as HTMLElement;
if (showClear.value && target.closest('.ant-input-suffix')) {
event.stopPropagation();
clearSelected();
return;
}
const selectedIds = props.modelValue == null ? [] : [props.modelValue];
dialogRef.value?.open(props.noticeId, selectedIds);
}
/** 弹窗选中回调 */
function handleSelected(rows: MesWmArrivalNoticeLineApi.ArrivalNoticeLine[]) {
const item = rows[0];
if (!item) {
return;
}
selectedItem.value = item;
emit('update:modelValue', item.id);
emit('change', item);
}
</script>
<template>
<div
v-bind="attrs"
class="w-full"
:class="disabled ? 'cursor-not-allowed' : 'cursor-pointer'"
@click="handleClick"
@mouseenter="hovering = true"
@mouseleave="hovering = false"
>
<Tooltip :mouse-enter-delay="0.5" :open="selectedItem ? undefined : false">
<template #title>
<div v-if="selectedItem" class="leading-6">
<div>物料编码{{ selectedItem.itemCode || '-' }}</div>
<div>物料名称{{ selectedItem.itemName || '-' }}</div>
<div>规格型号{{ selectedItem.specification || '-' }}</div>
<div>到货数量{{ selectedItem.arrivalQuantity ?? '-' }}</div>
</div>
</template>
<Input
:disabled="disabled"
:placeholder="placeholder"
:value="displayLabel"
readonly
>
<template #suffix>
<IconifyIcon
class="size-4"
:icon="showClear ? 'lucide:circle-x' : 'lucide:search'"
/>
</template>
</Input>
</Tooltip>
</div>
<WmArrivalNoticeLineSelectDialog ref="dialogRef" @selected="handleSelected" />
</template>

View File

@@ -0,0 +1,262 @@
<script lang="ts" setup>
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmArrivalNoticeApi } from '#/api/mes/wm/arrivalnotice';
import { nextTick, ref } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { message, Modal } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getArrivalNoticePage } from '#/api/mes/wm/arrivalnotice';
const emit = defineEmits<{
selected: [rows: MesWmArrivalNoticeApi.ArrivalNotice[]];
}>();
const open = ref(false); // 弹窗是否打开
const multiple = ref(false); // 是否多选
const fixedStatus = ref<number>(); // 固定状态筛选
const syncingSingleSelection = ref(false); // 是否同步单选勾选状态
const selectedRows = ref<MesWmArrivalNoticeApi.ArrivalNotice[]>([]); // 已选通知单列表
const preSelectedIds = ref<number[]>([]); // 预选通知单编号列表
/** 搜索表单 */
function useSearchSchema(): VbenFormSchema[] {
return [
{
fieldName: 'code',
label: '通知单编号',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入通知单编号',
},
},
{
fieldName: 'name',
label: '通知单名称',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入通知单名称',
},
},
{
fieldName: 'purchaseOrderCode',
label: '采购订单编号',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入采购订单编号',
},
},
];
}
/** 表格字段 */
function useGridColumns(): VxeTableGridOptions<MesWmArrivalNoticeApi.ArrivalNotice>['columns'] {
return [
{
type: 'checkbox',
width: 50,
},
{
field: 'code',
title: '通知单编号',
minWidth: 160,
},
{
field: 'name',
title: '通知单名称',
minWidth: 150,
},
{
field: 'purchaseOrderCode',
title: '采购订单编号',
minWidth: 140,
},
{
field: 'vendorName',
title: '供应商名称',
minWidth: 120,
},
{
field: 'arrivalDate',
title: '到货日期',
width: 120,
formatter: 'formatDate',
},
{
field: 'status',
title: '单据状态',
width: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.MES_WM_ARRIVAL_NOTICE_STATUS },
},
},
];
}
/** 单选模式下同步 VXE 勾选状态,避免跨页残留多选 */
async function syncSingleSelection(row?: MesWmArrivalNoticeApi.ArrivalNotice) {
syncingSingleSelection.value = true;
await nextTick();
await gridApi.grid.clearCheckboxRow();
if (row) {
await gridApi.grid.setCheckboxRow(row, true);
}
await nextTick();
syncingSingleSelection.value = false;
}
/** 处理勾选变化,单选模式只保留最后一条 */
async function handleCheckboxChange({
checked,
records,
row,
}: {
checked: boolean;
records: MesWmArrivalNoticeApi.ArrivalNotice[];
row?: MesWmArrivalNoticeApi.ArrivalNotice;
}) {
if (syncingSingleSelection.value) {
return;
}
if (!multiple.value) {
const selected = checked && row ? [row] : [];
selectedRows.value = selected;
await syncSingleSelection(selected[0]);
return;
}
selectedRows.value = records;
}
/** 处理全选变化 */
function handleCheckboxAll({
records,
}: {
records: MesWmArrivalNoticeApi.ArrivalNotice[];
}) {
if (syncingSingleSelection.value) {
return;
}
selectedRows.value = records;
}
/** 回显预选通知单 */
function applyPreSelection() {
if (preSelectedIds.value.length === 0) {
return;
}
const rows = gridApi.grid.getData() as MesWmArrivalNoticeApi.ArrivalNotice[];
for (const row of rows) {
if (row.id && preSelectedIds.value.includes(row.id)) {
gridApi.grid.setCheckboxRow(row, true);
if (!multiple.value) {
selectedRows.value = [row];
}
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useSearchSchema(),
},
gridOptions: {
columns: useGridColumns(),
height: 520,
keepSource: true,
checkboxConfig: {
highlight: true,
range: true,
reserve: true,
},
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getArrivalNoticePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
status: fixedStatus.value,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
search: true,
},
} as VxeTableGridOptions<MesWmArrivalNoticeApi.ArrivalNotice>,
gridEvents: {
checkboxAll: handleCheckboxAll,
checkboxChange: handleCheckboxChange,
},
});
/** 重置查询和选择状态 */
async function resetQueryState() {
selectedRows.value = [];
await gridApi.grid.clearCheckboxRow();
await gridApi.formApi.resetForm();
}
/** 打开通知单选择弹窗 */
async function openModal(
selectedIds?: number[],
options?: { multiple?: boolean; status?: number },
) {
open.value = true;
multiple.value = options?.multiple ?? false;
fixedStatus.value = options?.status;
preSelectedIds.value = selectedIds || [];
await nextTick();
await resetQueryState();
await gridApi.query();
await nextTick();
applyPreSelection();
}
/** 关闭通知单选择弹窗 */
async function closeModal() {
open.value = false;
await resetQueryState();
}
/** 确认选择通知单 */
function handleConfirm() {
if (selectedRows.value.length === 0) {
message.warning(multiple.value ? '请至少选择一条数据' : '请选择一条数据');
return;
}
emit(
'selected',
multiple.value ? selectedRows.value : [selectedRows.value[0]!],
);
open.value = false;
}
defineExpose({ open: openModal });
</script>
<template>
<Modal
v-model:open="open"
title="到货通知单选择"
width="70%"
:destroy-on-close="true"
@cancel="closeModal"
@ok="handleConfirm"
>
<Grid table-title="到货通知单列表" />
</Modal>
</template>

View File

@@ -0,0 +1,138 @@
<script lang="ts" setup>
import type { MesWmArrivalNoticeApi } from '#/api/mes/wm/arrivalnotice';
import { computed, ref, useAttrs, watch } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { Input, Tooltip } from 'ant-design-vue';
import { getArrivalNotice } from '#/api/mes/wm/arrivalnotice';
import WmArrivalNoticeSelectDialog from './wm-arrival-notice-select-dialog.vue';
defineOptions({ name: 'WmArrivalNoticeSelect', inheritAttrs: false });
const props = withDefaults(
defineProps<{
allowClear?: boolean;
disabled?: boolean;
modelValue?: number;
placeholder?: string;
status?: number; // 固定状态筛选
}>(),
{
allowClear: true,
disabled: false,
modelValue: undefined,
placeholder: '请选择到货通知单',
status: undefined,
},
);
const emit = defineEmits<{
change: [item: MesWmArrivalNoticeApi.ArrivalNotice | undefined];
'update:modelValue': [value: number | undefined];
}>();
const attrs = useAttrs();
const dialogRef = ref<InstanceType<typeof WmArrivalNoticeSelectDialog>>();
const hovering = ref(false);
const selectedItem = ref<MesWmArrivalNoticeApi.ArrivalNotice>();
const displayLabel = computed(() => selectedItem.value?.code ?? '');
const showClear = computed(
() =>
props.allowClear &&
!props.disabled &&
hovering.value &&
props.modelValue != null,
);
/** 根据编号单条查询通知单信息(用于编辑回显) */
async function resolveItemById(id: number | undefined) {
if (id == null) {
selectedItem.value = undefined;
return;
}
if (selectedItem.value?.id === id) {
return;
}
selectedItem.value = await getArrivalNotice(id);
}
watch(() => props.modelValue, resolveItemById, { immediate: true });
/** 清空已选通知单 */
function clearSelected() {
selectedItem.value = undefined;
emit('update:modelValue', undefined);
emit('change', undefined);
}
/** 打开通知单选择弹窗 */
function handleClick(event: MouseEvent) {
if (props.disabled) {
return;
}
const target = event.target as HTMLElement;
if (showClear.value && target.closest('.ant-input-suffix')) {
event.stopPropagation();
clearSelected();
return;
}
const selectedIds = props.modelValue == null ? [] : [props.modelValue];
dialogRef.value?.open(selectedIds, {
multiple: false,
status: props.status,
});
}
/** 弹窗选中回调 */
function handleSelected(rows: MesWmArrivalNoticeApi.ArrivalNotice[]) {
const item = rows[0];
if (!item) {
return;
}
selectedItem.value = item;
emit('update:modelValue', item.id);
emit('change', item);
}
</script>
<template>
<div
v-bind="attrs"
class="w-full"
:class="disabled ? 'cursor-not-allowed' : 'cursor-pointer'"
@click="handleClick"
@mouseenter="hovering = true"
@mouseleave="hovering = false"
>
<Tooltip :mouse-enter-delay="0.5" :open="selectedItem ? undefined : false">
<template #title>
<div v-if="selectedItem" class="leading-6">
<div>编号{{ selectedItem.code || '-' }}</div>
<div>名称{{ selectedItem.name || '-' }}</div>
<div>供应商{{ selectedItem.vendorName || '-' }}</div>
<div>采购订单{{ selectedItem.purchaseOrderCode || '-' }}</div>
</div>
</template>
<Input
:disabled="disabled"
:placeholder="placeholder"
:value="displayLabel"
readonly
>
<template #suffix>
<IconifyIcon
class="size-4"
:icon="showClear ? 'lucide:circle-x' : 'lucide:search'"
/>
</template>
</Input>
</Tooltip>
</div>
<WmArrivalNoticeSelectDialog ref="dialogRef" @selected="handleSelected" />
</template>

View File

@@ -0,0 +1,353 @@
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmArrivalNoticeApi } from '#/api/mes/wm/arrivalnotice';
import type { MesWmArrivalNoticeLineApi } from '#/api/mes/wm/arrivalnotice/line';
import { h, markRaw } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { Button } from 'ant-design-vue';
import { z } from '#/adapter/form';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { getRangePickerDefaultProps } from '#/utils';
import MdItemSelect from '#/views/mes/md/item/components/md-item-select.vue';
import MdVendorSelect from '#/views/mes/md/vendor/components/md-vendor-select.vue';
import { MesAutoCodeRuleCode } from '#/views/mes/utils/constants';
/** 表单类型 */
export type FormType = 'create' | 'detail' | 'update';
/** 新增/修改的表单 */
export function useFormSchema(
formType: FormType,
formApi?: VbenFormApi,
): VbenFormSchema[] {
return [
{
fieldName: 'id',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'status',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'code',
label: '通知单编号',
component: 'Input',
componentProps: {
placeholder: '请输入通知单编号',
},
rules: 'required',
suffix:
formType === 'detail'
? undefined
: () =>
h(
Button,
{
type: 'default',
onClick: async () => {
const code = await generateAutoCode(
MesAutoCodeRuleCode.WM_ARRIVAL_NOTICE_CODE,
);
await formApi?.setFieldValue('code', code);
},
},
{ default: () => '生成' },
),
},
{
fieldName: 'name',
label: '通知单名称',
component: 'Input',
componentProps: {
placeholder: '请输入通知单名称',
},
rules: 'required',
},
{
fieldName: 'purchaseOrderCode',
label: '采购订单编号',
component: 'Input',
componentProps: {
placeholder: '请输入采购订单编号',
},
},
{
fieldName: 'vendorId',
label: '供应商',
component: markRaw(MdVendorSelect),
componentProps: {
placeholder: '请选择供应商',
},
rules: 'selectRequired',
},
{
fieldName: 'arrivalDate',
label: '到货日期',
component: 'DatePicker',
componentProps: {
class: '!w-full',
format: 'YYYY-MM-DD',
placeholder: '请选择到货日期',
valueFormat: 'x',
},
rules: 'required',
},
{
fieldName: 'contactName',
label: '联系人',
component: 'Input',
componentProps: {
placeholder: '请输入联系人',
},
},
{
fieldName: 'contactTelephone',
label: '联系方式',
component: 'Input',
componentProps: {
placeholder: '请输入联系方式',
},
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
formItemClass: 'col-span-3',
componentProps: {
placeholder: '请输入备注',
rows: 3,
},
},
];
}
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'code',
label: '通知单编号',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入通知单编号',
},
},
{
fieldName: 'name',
label: '通知单名称',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入通知单名称',
},
},
{
fieldName: 'purchaseOrderCode',
label: '采购订单编号',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入采购订单编号',
},
},
{
fieldName: 'vendorId',
label: '供应商',
component: markRaw(MdVendorSelect),
componentProps: {
placeholder: '请选择供应商',
},
},
{
fieldName: 'arrivalDate',
label: '到货日期',
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
allowClear: true,
},
},
];
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions<MesWmArrivalNoticeApi.ArrivalNotice>['columns'] {
return [
{
field: 'code',
title: '通知单编号',
minWidth: 160,
slots: { default: 'code' },
},
{
field: 'name',
title: '通知单名称',
minWidth: 150,
},
{
field: 'purchaseOrderCode',
title: '采购订单编号',
minWidth: 140,
},
{
field: 'vendorName',
title: '供应商名称',
minWidth: 120,
},
{
field: 'contactName',
title: '联系人',
minWidth: 100,
},
{
field: 'contactTelephone',
title: '联系方式',
minWidth: 120,
},
{
field: 'arrivalDate',
title: '到货日期',
width: 180,
formatter: 'formatDate',
},
{
field: 'status',
title: '单据状态',
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.MES_WM_ARRIVAL_NOTICE_STATUS },
},
},
{
title: '操作',
width: 180,
fixed: 'right',
slots: { default: 'actions' },
},
];
}
/** 通知单行子表的字段 */
export function useLineGridColumns(
editable: boolean,
): VxeTableGridOptions<MesWmArrivalNoticeLineApi.ArrivalNoticeLine>['columns'] {
return [
{
field: 'itemCode',
title: '物料编码',
minWidth: 120,
},
{
field: 'itemName',
title: '物料名称',
minWidth: 140,
},
{
field: 'specification',
title: '规格型号',
minWidth: 120,
},
{
field: 'unitMeasureName',
title: '单位',
width: 80,
},
{
field: 'arrivalQuantity',
title: '到货数量',
width: 100,
},
{
field: 'iqcCheckFlag',
title: '是否检验',
width: 90,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
},
},
{
field: 'qualifiedQuantity',
title: '合格数量',
width: 100,
},
{
field: 'iqcCode',
title: '检验单号',
minWidth: 140,
},
{
field: 'remark',
title: '备注',
minWidth: 120,
},
...(editable
? [
{
title: '操作',
width: 120,
fixed: 'right',
slots: { default: 'actions' },
} as const,
]
: []),
];
}
/** 通知单行新增/修改的表单 */
export function useLineFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'itemId',
label: '物料',
component: markRaw(MdItemSelect),
componentProps: {
placeholder: '请选择物料',
},
rules: 'selectRequired',
},
{
fieldName: 'arrivalQuantity',
label: '到货数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0.01,
placeholder: '请输入到货数量',
precision: 2,
},
rules: 'required',
},
{
fieldName: 'iqcCheckFlag',
label: '是否检验',
component: 'Switch',
rules: z.boolean().default(false),
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
formItemClass: 'col-span-3',
componentProps: {
placeholder: '请输入备注',
rows: 3,
},
},
];
}

View File

@@ -0,0 +1,187 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmArrivalNoticeApi } from '#/api/mes/wm/arrivalnotice';
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteArrivalNotice,
exportArrivalNotice,
getArrivalNoticePage,
} from '#/api/mes/wm/arrivalnotice';
import { $t } from '#/locales';
import { MesWmArrivalNoticeStatusEnum } from '#/views/mes/utils/constants';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/** 刷新表格 */
function handleRefresh() {
gridApi.query();
}
/** 创建到货通知单 */
function handleCreate() {
formModalApi.setData({ formType: 'create' }).open();
}
/** 查看到货通知单 */
function handleDetail(row: MesWmArrivalNoticeApi.ArrivalNotice) {
formModalApi.setData({ formType: 'detail', id: row.id }).open();
}
/** 编辑到货通知单 */
function handleEdit(row: MesWmArrivalNoticeApi.ArrivalNotice) {
formModalApi.setData({ formType: 'update', id: row.id }).open();
}
/** 删除到货通知单 */
async function handleDelete(row: MesWmArrivalNoticeApi.ArrivalNotice) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.code]),
duration: 0,
});
try {
await deleteArrivalNotice(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.code]));
handleRefresh();
} finally {
hideLoading();
}
}
/** 提示前往质检模块 */
function handleQc() {
message.info('请前往【质量管理 - 待检任务】中进行来料检验操作');
}
/** 提示前往采购入库模块 */
function handleReceipt() {
message.info('请前往【仓库管理 - 采购入库】中进行入库操作');
}
/** 导出表格 */
async function handleExport() {
const data = await exportArrivalNotice(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '到货通知单.xls', source: data });
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getArrivalNoticePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
search: true,
},
} as VxeTableGridOptions<MesWmArrivalNoticeApi.ArrivalNotice>,
});
</script>
<template>
<Page auto-content-height>
<template #doc>
<DocAlert
title="【仓库】到货通知、采购入库、采购退货"
url="https://doc.iocoder.cn/mes/wm/purchase-in/"
/>
</template>
<FormModal @success="handleRefresh" />
<Grid table-title="到货通知单列表">
<template #toolbar-tools>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['到货通知单']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['mes:wm-arrival-notice:create'],
onClick: handleCreate,
},
{
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
auth: ['mes:wm-arrival-notice:export'],
onClick: handleExport,
},
]"
/>
</template>
<template #code="{ row }">
<Button type="link" @click="handleDetail(row)">
{{ row.code }}
</Button>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['mes:wm-arrival-notice:update'],
ifShow: row.status === MesWmArrivalNoticeStatusEnum.PREPARE,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['mes:wm-arrival-notice:delete'],
ifShow: row.status === MesWmArrivalNoticeStatusEnum.PREPARE,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.code]),
confirm: handleDelete.bind(null, row),
},
},
{
label: '执行质检',
type: 'link',
ifShow: row.status === MesWmArrivalNoticeStatusEnum.PENDING_QC,
onClick: handleQc,
},
{
label: '执行入库',
type: 'link',
ifShow:
row.status === MesWmArrivalNoticeStatusEnum.PENDING_RECEIPT,
onClick: handleReceipt,
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,168 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { MesWmArrivalNoticeApi } from '#/api/mes/wm/arrivalnotice';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Button, Divider, message, Popconfirm } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
createArrivalNotice,
getArrivalNotice,
submitArrivalNotice,
updateArrivalNotice,
} from '#/api/mes/wm/arrivalnotice';
import { $t } from '#/locales';
import { MesWmArrivalNoticeStatusEnum } from '#/views/mes/utils/constants';
import { useFormSchema } from '../data';
import LineList from './line-list.vue';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const formData = ref<MesWmArrivalNoticeApi.ArrivalNotice>();
const originalSnapshot = ref(''); // 表单原始数据快照,用于提交时跳过未变更的保存请求
const isEditable = computed(() => // 是否为编辑模式(可保存)
['create', 'update'].includes(formType.value),
);
const canSubmit = computed(() => // 是否可提交
formType.value === 'update' &&
formData.value?.status === MesWmArrivalNoticeStatusEnum.PREPARE,
);
const getTitle = computed(() => {
if (formType.value === 'detail') {
return $t('ui.actionTitle.view', ['到货通知单']);
}
return formType.value === 'update'
? $t('ui.actionTitle.edit', ['到货通知单'])
: $t('ui.actionTitle.create', ['到货通知单']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-1',
labelWidth: 110,
},
layout: 'horizontal',
schema: [],
showDefaultActions: false,
wrapperClass: 'grid-cols-3',
});
/** 提交通知单:表单有修改时先保存,再调用提交接口 */
async function handleSubmit() {
const { valid } = await formApi.validate();
if (!valid || !formData.value?.id) {
return;
}
modalApi.lock();
try {
const current = JSON.stringify(await formApi.getValues());
if (current !== originalSnapshot.value) {
const data =
(await formApi.getValues()) as MesWmArrivalNoticeApi.ArrivalNotice;
await updateArrivalNotice({ ...formData.value, ...data });
originalSnapshot.value = current;
}
await submitArrivalNotice(formData.value.id);
message.success('提交成功');
await modalApi.close();
emit('success');
} finally {
modalApi.unlock();
}
}
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
if (!isEditable.value) {
await modalApi.close();
return;
}
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data =
(await formApi.getValues()) as MesWmArrivalNoticeApi.ArrivalNotice;
try {
if (formData.value?.id) {
await updateArrivalNotice({ ...formData.value, ...data });
formData.value = { ...formData.value, ...data };
} else {
const id = await createArrivalNotice(data);
formData.value = {
...data,
id,
status: MesWmArrivalNoticeStatusEnum.PREPARE,
};
await formApi.setFieldValue('id', id);
await formApi.setFieldValue('status', formData.value.status);
formType.value = 'update';
}
originalSnapshot.value = JSON.stringify(await formApi.getValues());
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
originalSnapshot.value = '';
return;
}
// 加载数据
const data = modalApi.getData<{ formType: FormType; id?: number }>();
formType.value = data.formType;
formApi.setState({ schema: useFormSchema(formType.value, formApi) });
formApi.setDisabled(!isEditable.value);
modalApi.setState({ showConfirmButton: isEditable.value });
if (data?.id) {
modalApi.lock();
try {
formData.value = await getArrivalNotice(data.id);
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
}
originalSnapshot.value = JSON.stringify(await formApi.getValues());
},
});
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" />
<!-- 非新建模式展示物料信息 -->
<template v-if="formData?.id">
<Divider>物料信息</Divider>
<div class="mx-4">
<LineList :form-type="formType" :notice-id="formData.id" />
</div>
</template>
<template #prepend-footer>
<div class="flex flex-auto items-center gap-2">
<Popconfirm
v-if="canSubmit"
title="确认提交该到货通知单?【提交后将不能修改】"
@confirm="handleSubmit"
>
<Button type="primary">提交</Button>
</Popconfirm>
</div>
</template>
</Modal>
</template>

View File

@@ -0,0 +1,94 @@
<script lang="ts" setup>
import type { MesWmArrivalNoticeLineApi } from '#/api/mes/wm/arrivalnotice/line';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
createArrivalNoticeLine,
getArrivalNoticeLine,
updateArrivalNoticeLine,
} from '#/api/mes/wm/arrivalnotice/line';
import { $t } from '#/locales';
import { useLineFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<MesWmArrivalNoticeLineApi.ArrivalNoticeLine>();
const noticeId = ref<number>(); // 所属通知单编号
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['到货通知单行'])
: $t('ui.actionTitle.create', ['到货通知单行']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-1',
labelWidth: 90,
},
layout: 'horizontal',
schema: useLineFormSchema(),
showDefaultActions: false,
wrapperClass: 'grid-cols-3',
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data =
(await formApi.getValues()) as MesWmArrivalNoticeLineApi.ArrivalNoticeLine;
data.noticeId = noticeId.value;
try {
await (formData.value?.id
? updateArrivalNoticeLine({ ...data, id: formData.value.id })
: createArrivalNoticeLine(data));
// 关闭并提示
await modalApi.close();
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
// 加载数据
const data = modalApi.getData<{ id?: number; noticeId: number }>();
noticeId.value = data.noticeId;
if (!data.id) {
return;
}
modalApi.lock();
try {
formData.value = await getArrivalNoticeLine(data.id);
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
},
});
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" />
</Modal>
</template>

View File

@@ -0,0 +1,139 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmArrivalNoticeLineApi } from '#/api/mes/wm/arrivalnotice/line';
import { computed } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteArrivalNoticeLine,
getArrivalNoticeLinePage,
} from '#/api/mes/wm/arrivalnotice/line';
import { $t } from '#/locales';
import { type FormType, useLineGridColumns } from '../data';
import LineForm from './line-form.vue';
const props = defineProps<{
formType: FormType;
noticeId: number;
}>();
const isEditable = computed(() => // 是否可编辑明细行
['create', 'update'].includes(props.formType),
);
const [LineFormModal, lineFormModalApi] = useVbenModal({
connectedComponent: LineForm,
destroyOnClose: true,
});
/** 刷新表格 */
function handleRefresh() {
gridApi.query();
}
/** 添加物料 */
function handleCreate() {
lineFormModalApi.setData({ noticeId: props.noticeId }).open();
}
/** 编辑物料 */
function handleEdit(row: MesWmArrivalNoticeLineApi.ArrivalNoticeLine) {
lineFormModalApi.setData({ id: row.id, noticeId: props.noticeId }).open();
}
/** 删除物料 */
async function handleDelete(
row: MesWmArrivalNoticeLineApi.ArrivalNoticeLine,
) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.itemName]),
duration: 0,
});
try {
await deleteArrivalNoticeLine(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.itemName]));
handleRefresh();
} finally {
hideLoading();
}
}
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useLineGridColumns(isEditable.value),
height: 400,
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }) => {
if (!props.noticeId) {
return { list: [], total: 0 };
}
return await getArrivalNoticeLinePage({
noticeId: props.noticeId,
pageNo: page.currentPage,
pageSize: page.pageSize,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
},
} as VxeTableGridOptions<MesWmArrivalNoticeLineApi.ArrivalNoticeLine>,
});
</script>
<template>
<div>
<LineFormModal @success="handleRefresh" />
<Grid table-title="物料信息">
<template v-if="isEditable" #toolbar-tools>
<TableAction
:actions="[
{
label: '添加物料',
type: 'primary',
icon: ACTION_ICON.ADD,
onClick: handleCreate,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
ifShow: isEditable,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
ifShow: isEditable,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.itemName]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</div>
</template>

View File

@@ -0,0 +1,530 @@
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmArrivalNoticeApi } from '#/api/mes/wm/arrivalnotice';
import type { MesWmArrivalNoticeLineApi } from '#/api/mes/wm/arrivalnotice/line';
import type { MesWmItemReceiptApi } from '#/api/mes/wm/itemreceipt';
import type { MesWmItemReceiptDetailApi } from '#/api/mes/wm/itemreceipt/detail';
import type { MesWmItemReceiptLineApi } from '#/api/mes/wm/itemreceipt/line';
import { h, markRaw } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { Button } from 'ant-design-vue';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { getRangePickerDefaultProps } from '#/utils';
import MdItemSelect from '#/views/mes/md/item/components/md-item-select.vue';
import MdVendorSelect from '#/views/mes/md/vendor/components/md-vendor-select.vue';
import {
MesAutoCodeRuleCode,
MesWmArrivalNoticeStatusEnum,
} from '#/views/mes/utils/constants';
import {
WmArrivalNoticeLineSelect,
WmArrivalNoticeSelect,
} from '#/views/mes/wm/arrivalnotice/components';
import {
WmWarehouseAreaSelect,
WmWarehouseLocationSelect,
WmWarehouseSelect,
} from '#/views/mes/wm/warehouse/components';
/** 表单类型 */
export type FormType = 'create' | 'detail' | 'finish' | 'stock' | 'update';
/** 表单头部是否只读(上架、详情、入库态) */
function isHeaderReadonly(formType: FormType): boolean {
return (
formType === 'detail' || formType === 'finish' || formType === 'stock'
);
}
/** 新增/修改的表单 */
export function useFormSchema(
formType: FormType,
formApi?: VbenFormApi,
): VbenFormSchema[] {
return [
{
fieldName: 'id',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'status',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'code',
label: '入库单编号',
component: 'Input',
componentProps: {
placeholder: '请输入入库单编号',
},
rules: 'required',
suffix: isHeaderReadonly(formType)
? undefined
: () =>
h(
Button,
{
type: 'default',
onClick: async () => {
const code = await generateAutoCode(
MesAutoCodeRuleCode.WM_ITEM_RECEIPT_CODE,
);
await formApi?.setFieldValue('code', code);
},
},
{ default: () => '生成' },
),
},
{
fieldName: 'name',
label: '入库单名称',
component: 'Input',
componentProps: {
placeholder: '请输入入库单名称',
},
rules: 'required',
},
{
fieldName: 'receiptDate',
label: '入库日期',
component: 'DatePicker',
componentProps: {
class: '!w-full',
format: 'YYYY-MM-DD',
placeholder: '请选择入库日期',
valueFormat: 'x',
},
rules: 'required',
},
{
fieldName: 'noticeId',
label: '到货通知单',
component: markRaw(WmArrivalNoticeSelect),
componentProps: {
// 选择到货通知单后,自动回填供应商和采购订单号
onChange: async (notice?: MesWmArrivalNoticeApi.ArrivalNotice) => {
await formApi?.setValues({
purchaseOrderCode: notice?.purchaseOrderCode,
vendorId: notice?.vendorId,
});
},
status: MesWmArrivalNoticeStatusEnum.PENDING_RECEIPT,
},
},
{
fieldName: 'vendorId',
label: '供应商',
component: markRaw(MdVendorSelect),
componentProps: {
placeholder: '请选择供应商',
},
rules: 'selectRequired',
},
{
fieldName: 'purchaseOrderCode',
label: '采购订单号',
component: 'Input',
componentProps: {
placeholder: '请输入采购订单号',
},
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
formItemClass: 'col-span-3',
componentProps: {
placeholder: '请输入备注',
rows: 3,
},
},
];
}
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'code',
label: '入库单编号',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入入库单编号',
},
},
{
fieldName: 'name',
label: '入库单名称',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入入库单名称',
},
},
{
fieldName: 'vendorId',
label: '供应商',
component: markRaw(MdVendorSelect),
componentProps: {
placeholder: '请选择供应商',
},
},
{
fieldName: 'receiptDate',
label: '入库日期',
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
allowClear: true,
},
},
];
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions<MesWmItemReceiptApi.ItemReceipt>['columns'] {
return [
{
field: 'code',
title: '入库单编号',
minWidth: 160,
slots: { default: 'code' },
},
{
field: 'name',
title: '入库单名称',
minWidth: 150,
},
{
field: 'purchaseOrderCode',
title: '采购订单号',
minWidth: 140,
},
{
field: 'vendorName',
title: '供应商名称',
minWidth: 120,
},
{
field: 'receiptDate',
title: '入库日期',
width: 180,
formatter: 'formatDate',
},
{
field: 'status',
title: '单据状态',
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.MES_WM_ITEM_RECEIPT_STATUS },
},
},
{
title: '操作',
width: 240,
fixed: 'right',
slots: { default: 'actions' },
},
];
}
/** 入库单行子表的字段 */
export function useLineGridColumns(
editable: boolean,
stockable: boolean,
): VxeTableGridOptions<MesWmItemReceiptLineApi.ItemReceiptLine>['columns'] {
return [
{
type: 'expand',
width: 48,
slots: { content: 'detail' },
},
{
field: 'itemCode',
title: '物料编码',
minWidth: 120,
},
{
field: 'itemName',
title: '物料名称',
minWidth: 140,
},
{
field: 'specification',
title: '规格型号',
minWidth: 120,
},
{
field: 'unitMeasureName',
title: '单位',
width: 80,
},
{
field: 'receivedQuantity',
title: '入库数量',
width: 100,
},
{
field: 'batchCode',
title: '批次号',
minWidth: 120,
},
...(editable || stockable
? [
{
title: '操作',
width: 200,
fixed: 'right',
slots: { default: 'actions' },
} as const,
]
: []),
];
}
/** 入库单行新增/修改的表单 */
export function useLineFormSchema(
hasNotice: boolean,
formApi?: VbenFormApi,
): VbenFormSchema[] {
return [
{
fieldName: 'noticeId',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'arrivalNoticeLineId',
label: '到货通知单行',
component: markRaw(WmArrivalNoticeLineSelect),
componentProps: {
// 选择到货通知单行后,自动回填物料和入库数量
onChange: async (
line?: MesWmArrivalNoticeLineApi.ArrivalNoticeLine,
) => {
await formApi?.setValues({
itemId: line?.itemId,
receivedQuantity: line?.arrivalQuantity,
});
},
},
dependencies: {
triggerFields: ['noticeId'],
if: () => hasNotice,
componentProps: (values) => ({
noticeId: values.noticeId,
}),
},
},
{
fieldName: 'itemId',
label: '物料',
component: markRaw(MdItemSelect),
componentProps: {
placeholder: '请选择物料',
},
rules: 'selectRequired',
dependencies: {
triggerFields: ['arrivalNoticeLineId'],
componentProps: (values) => ({
disabled: !!values.arrivalNoticeLineId,
placeholder: '请选择物料',
}),
},
},
{
fieldName: 'receivedQuantity',
label: '入库数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
placeholder: '请输入入库数量',
precision: 2,
},
rules: 'required',
},
{
fieldName: 'productionDate',
label: '生产日期',
component: 'DatePicker',
componentProps: {
class: '!w-full',
format: 'YYYY-MM-DD',
placeholder: '请选择生产日期',
valueFormat: 'x',
},
},
{
fieldName: 'expireDate',
label: '有效期',
component: 'DatePicker',
componentProps: {
class: '!w-full',
format: 'YYYY-MM-DD',
placeholder: '请选择有效期',
valueFormat: 'x',
},
},
{
fieldName: 'lotNumber',
label: '生产批号',
component: 'Input',
componentProps: {
placeholder: '请输入生产批号',
},
},
{
fieldName: 'batchCode',
label: '批次号',
component: 'Input',
componentProps: {
disabled: true,
placeholder: '由填写信息自动生成',
},
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
formItemClass: 'col-span-3',
componentProps: {
placeholder: '请输入备注',
rows: 3,
},
},
];
}
/** 上架明细子表的字段 */
export function useDetailGridColumns(
stockable: boolean,
): VxeTableGridOptions<MesWmItemReceiptDetailApi.ItemReceiptDetail>['columns'] {
return [
{
field: 'warehouseName',
title: '仓库名称',
minWidth: 100,
},
{
field: 'locationName',
title: '库区名称',
minWidth: 100,
},
{
field: 'areaName',
title: '库位名称',
minWidth: 100,
},
{
field: 'quantity',
title: '数量',
width: 100,
},
...(stockable
? [
{
title: '操作',
width: 120,
fixed: 'right',
slots: { default: 'actions' },
} as const,
]
: []),
];
}
/** 上架明细新增/修改的表单 */
export function useDetailFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
return [
{
fieldName: 'itemId',
label: '物料',
component: markRaw(MdItemSelect),
componentProps: {
disabled: true,
},
},
{
fieldName: 'warehouseId',
label: '入库仓库',
component: markRaw(WmWarehouseSelect),
componentProps: {
// 切换仓库后清空库区和库位
onChange: async () => {
await formApi?.setValues({
areaId: undefined,
locationId: undefined,
});
},
placeholder: '请选择仓库',
},
rules: 'selectRequired',
},
{
fieldName: 'locationId',
label: '库区',
component: markRaw(WmWarehouseLocationSelect),
componentProps: {
placeholder: '请选择库区',
},
rules: 'selectRequired',
dependencies: {
triggerFields: ['warehouseId'],
componentProps: (values) => ({
// 切换库区后清空库位
onChange: async () => {
await formApi?.setFieldValue('areaId', undefined);
},
placeholder: '请选择库区',
warehouseId: values.warehouseId,
}),
},
},
{
fieldName: 'areaId',
label: '库位',
component: markRaw(WmWarehouseAreaSelect),
componentProps: {
placeholder: '请选择库位',
},
rules: 'selectRequired',
dependencies: {
triggerFields: ['locationId'],
componentProps: (values) => ({
locationId: values.locationId,
placeholder: '请选择库位',
}),
},
},
{
fieldName: 'quantity',
label: '数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
placeholder: '请输入数量',
precision: 2,
},
rules: 'required',
},
];
}

View File

@@ -0,0 +1,209 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmItemReceiptApi } from '#/api/mes/wm/itemreceipt';
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
cancelItemReceipt,
deleteItemReceipt,
exportItemReceipt,
getItemReceiptPage,
} from '#/api/mes/wm/itemreceipt';
import { $t } from '#/locales';
import { MesWmItemReceiptStatusEnum } from '#/views/mes/utils/constants';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/** 刷新表格 */
function handleRefresh() {
gridApi.query();
}
/** 创建采购入库单 */
function handleCreate() {
formModalApi.setData({ formType: 'create' }).open();
}
/** 查看采购入库单 */
function handleDetail(row: MesWmItemReceiptApi.ItemReceipt) {
formModalApi.setData({ formType: 'detail', id: row.id }).open();
}
/** 编辑采购入库单 */
function handleEdit(row: MesWmItemReceiptApi.ItemReceipt) {
formModalApi.setData({ formType: 'update', id: row.id }).open();
}
/** 执行上架 */
function handleStock(row: MesWmItemReceiptApi.ItemReceipt) {
formModalApi.setData({ formType: 'stock', id: row.id }).open();
}
/** 执行入库 */
function handleFinish(row: MesWmItemReceiptApi.ItemReceipt) {
formModalApi.setData({ formType: 'finish', id: row.id }).open();
}
/** 删除采购入库单 */
async function handleDelete(row: MesWmItemReceiptApi.ItemReceipt) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.code]),
duration: 0,
});
try {
await deleteItemReceipt(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.code]));
handleRefresh();
} finally {
hideLoading();
}
}
/** 取消采购入库单 */
async function handleCancel(row: MesWmItemReceiptApi.ItemReceipt) {
await cancelItemReceipt(row.id!);
message.success('取消成功');
handleRefresh();
}
/** 导出表格 */
async function handleExport() {
const data = await exportItemReceipt(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '采购入库单.xls', source: data });
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getItemReceiptPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
search: true,
},
} as VxeTableGridOptions<MesWmItemReceiptApi.ItemReceipt>,
});
</script>
<template>
<Page auto-content-height>
<template #doc>
<DocAlert
title="【仓库】到货通知、采购入库、采购退货"
url="https://doc.iocoder.cn/mes/wm/purchase-in/"
/>
</template>
<FormModal @success="handleRefresh" />
<Grid table-title="采购入库单列表">
<template #toolbar-tools>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['采购入库单']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['mes:wm-item-receipt:create'],
onClick: handleCreate,
},
{
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
auth: ['mes:wm-item-receipt:export'],
onClick: handleExport,
},
]"
/>
</template>
<template #code="{ row }">
<Button type="link" @click="handleDetail(row)">
{{ row.code }}
</Button>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['mes:wm-item-receipt:update'],
ifShow: row.status === MesWmItemReceiptStatusEnum.PREPARE,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['mes:wm-item-receipt:delete'],
ifShow: row.status === MesWmItemReceiptStatusEnum.PREPARE,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.code]),
confirm: handleDelete.bind(null, row),
},
},
{
label: '执行上架',
type: 'link',
auth: ['mes:wm-item-receipt:update'],
ifShow: row.status === MesWmItemReceiptStatusEnum.APPROVING,
onClick: handleStock.bind(null, row),
},
{
label: '执行入库',
type: 'link',
auth: ['mes:wm-item-receipt:finish'],
ifShow: row.status === MesWmItemReceiptStatusEnum.APPROVED,
onClick: handleFinish.bind(null, row),
},
{
label: '取消',
type: 'link',
danger: true,
auth: ['mes:wm-item-receipt:update'],
ifShow:
row.status === MesWmItemReceiptStatusEnum.APPROVING ||
row.status === MesWmItemReceiptStatusEnum.APPROVED,
popConfirm: {
title: '确认取消该采购入库单?取消后不可恢复。',
confirm: handleCancel.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,104 @@
<script lang="ts" setup>
import type { MesWmItemReceiptDetailApi } from '#/api/mes/wm/itemreceipt/detail';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
createItemReceiptDetail,
getItemReceiptDetail,
updateItemReceiptDetail,
} from '#/api/mes/wm/itemreceipt/detail';
import { $t } from '#/locales';
import { useDetailFormSchema } from '../data';
const emit = defineEmits<{ success: [lineId: number] }>();
const formData = ref<MesWmItemReceiptDetailApi.ItemReceiptDetail>();
const receiptId = ref<number>(); // 所属入库单编号
const lineId = ref<number>(); // 所属入库单行编号
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['上架明细'])
: $t('ui.actionTitle.create', ['上架明细']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-1',
labelWidth: 90,
},
layout: 'horizontal',
schema: useDetailFormSchema(),
showDefaultActions: false,
wrapperClass: 'grid-cols-3',
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data =
(await formApi.getValues()) as MesWmItemReceiptDetailApi.ItemReceiptDetail;
data.receiptId = receiptId.value;
data.lineId = lineId.value;
try {
await (formData.value?.id
? updateItemReceiptDetail({ ...data, id: formData.value.id })
: createItemReceiptDetail(data));
// 关闭并提示
await modalApi.close();
emit('success', lineId.value!);
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
formApi.setState({ schema: useDetailFormSchema(formApi) });
// 加载数据
const data = modalApi.getData<{
detailId?: number;
itemId?: number;
lineId: number;
receiptId: number;
}>();
receiptId.value = data.receiptId;
lineId.value = data.lineId;
if (data.detailId) {
modalApi.lock();
try {
formData.value = await getItemReceiptDetail(data.detailId);
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
} else if (data.itemId) {
await formApi.setFieldValue('itemId', data.itemId);
}
},
});
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" />
</Modal>
</template>

View File

@@ -0,0 +1,103 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmItemReceiptDetailApi } from '#/api/mes/wm/itemreceipt/detail';
import { computed, watch } from 'vue';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteItemReceiptDetail } from '#/api/mes/wm/itemreceipt/detail';
import { $t } from '#/locales';
import { type FormType, useDetailGridColumns } from '../data';
const props = defineProps<{
details: MesWmItemReceiptDetailApi.ItemReceiptDetail[];
formType: FormType;
}>();
const emit = defineEmits<{
edit: [detailId: number];
refresh: [];
}>();
const isStock = computed(() => props.formType === 'stock'); // 是否为上架模式
/** 编辑上架明细 */
function handleEdit(row: MesWmItemReceiptDetailApi.ItemReceiptDetail) {
emit('edit', row.id!);
}
/** 删除上架明细 */
async function handleDelete(
row: MesWmItemReceiptDetailApi.ItemReceiptDetail,
) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.warehouseName]),
duration: 0,
});
try {
await deleteItemReceiptDetail(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.warehouseName]));
emit('refresh');
} finally {
hideLoading();
}
}
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
border: true,
columns: useDetailGridColumns(isStock.value),
data: props.details,
pagerConfig: {
enabled: false,
},
rowConfig: {
keyField: 'id',
isHover: true,
},
size: 'small',
toolbarConfig: {
enabled: false,
},
} as VxeTableGridOptions<MesWmItemReceiptDetailApi.ItemReceiptDetail>,
});
watch(
() => props.details,
(details) => gridApi.setGridOptions({ data: details }),
);
</script>
<template>
<div class="px-4 py-2">
<Grid>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [
row.warehouseName,
]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</div>
</template>

View File

@@ -0,0 +1,238 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { MesWmItemReceiptApi } from '#/api/mes/wm/itemreceipt';
import { computed, ref } from 'vue';
import { confirm, useVbenModal } from '@vben/common-ui';
import { Button, Divider, message, Popconfirm } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
createItemReceipt,
finishItemReceipt,
getItemReceipt,
stockItemReceipt,
submitItemReceipt,
updateItemReceipt,
} from '#/api/mes/wm/itemreceipt';
import { $t } from '#/locales';
import { MesWmItemReceiptStatusEnum } from '#/views/mes/utils/constants';
import { useFormSchema } from '../data';
import LineList from './line-list.vue';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const formData = ref<MesWmItemReceiptApi.ItemReceipt>();
const originalSnapshot = ref(''); // 表单原始数据快照,用于提交时跳过未变更的保存请求
const isEditable = computed(() => // 是否为编辑模式(可保存)
['create', 'update'].includes(formType.value),
);
const isStock = computed(() => formType.value === 'stock'); // 是否为上架模式
const isFinish = computed(() => formType.value === 'finish'); // 是否为执行入库模式
const canSubmit = computed(() => // 是否可提交
formType.value === 'update' &&
formData.value?.status === MesWmItemReceiptStatusEnum.PREPARE,
);
const getTitle = computed(() => {
if (formType.value === 'detail') {
return $t('ui.actionTitle.view', ['采购入库单']);
}
if (formType.value === 'stock') {
return '执行上架';
}
if (formType.value === 'finish') {
return '执行入库';
}
return formType.value === 'update'
? $t('ui.actionTitle.edit', ['采购入库单'])
: $t('ui.actionTitle.create', ['采购入库单']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-1',
labelWidth: 110,
},
layout: 'horizontal',
schema: [],
showDefaultActions: false,
wrapperClass: 'grid-cols-3',
});
/** 提交入库单:表单有修改时先保存,再调用提交接口 */
async function handleSubmit() {
const { valid } = await formApi.validate();
if (!valid || !formData.value?.id) {
return;
}
modalApi.lock();
try {
const current = JSON.stringify(await formApi.getValues());
if (current !== originalSnapshot.value) {
const data = (await formApi.getValues()) as MesWmItemReceiptApi.ItemReceipt;
await updateItemReceipt({ ...formData.value, ...data });
originalSnapshot.value = current;
}
await submitItemReceipt(formData.value.id);
message.success('提交成功');
await modalApi.close();
emit('success');
} finally {
modalApi.unlock();
}
}
/** 执行上架 */
async function handleStock() {
if (!formData.value?.id) {
return;
}
modalApi.lock();
try {
await stockItemReceipt(formData.value.id);
message.success('上架成功');
await modalApi.close();
emit('success');
} finally {
modalApi.unlock();
}
}
/** 执行入库 */
async function handleFinish() {
if (!formData.value?.id) {
return;
}
modalApi.lock();
try {
await finishItemReceipt(formData.value.id);
message.success('入库成功');
await modalApi.close();
emit('success');
} finally {
modalApi.unlock();
}
}
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
if (!isEditable.value) {
await modalApi.close();
return;
}
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data = (await formApi.getValues()) as MesWmItemReceiptApi.ItemReceipt;
try {
if (formData.value?.id) {
await updateItemReceipt({ ...formData.value, ...data });
formData.value = { ...formData.value, ...data };
} else {
const id = await createItemReceipt(data);
formData.value = {
...data,
id,
status: MesWmItemReceiptStatusEnum.PREPARE,
};
await formApi.setFieldValue('id', id);
await formApi.setFieldValue('status', formData.value.status);
formType.value = 'update';
}
originalSnapshot.value = JSON.stringify(await formApi.getValues());
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
originalSnapshot.value = '';
return;
}
// 加载数据
const data = modalApi.getData<{ formType: FormType; id?: number }>();
formType.value = data.formType;
formApi.setState({ schema: useFormSchema(formType.value, formApi) });
formApi.setDisabled(!isEditable.value);
modalApi.setState({ showConfirmButton: isEditable.value });
if (data?.id) {
modalApi.lock();
try {
formData.value = await getItemReceipt(data.id);
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
}
originalSnapshot.value = JSON.stringify(await formApi.getValues());
},
});
/** 上架前确认 */
async function confirmStock() {
try {
await confirm('确认执行上架?');
} catch {
return;
}
await handleStock();
}
/** 入库前确认 */
async function confirmFinish() {
try {
await confirm('确认执行入库?执行后将更新库存台账。');
} catch {
return;
}
await handleFinish();
}
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" />
<!-- 非新建模式展示物料信息 -->
<template v-if="formData?.id">
<Divider>物料信息</Divider>
<div class="mx-4">
<LineList
:form-type="formType"
:notice-id="formData.noticeId"
:receipt-id="formData.id"
/>
</div>
</template>
<template #prepend-footer>
<div class="flex flex-auto items-center gap-2">
<Popconfirm
v-if="canSubmit"
title="确认提交该采购入库单?【提交后将不能修改】"
@confirm="handleSubmit"
>
<Button type="primary">提交</Button>
</Popconfirm>
<Button v-if="isStock" type="primary" @click="confirmStock">
执行上架
</Button>
<Button v-if="isFinish" type="primary" @click="confirmFinish">
执行入库
</Button>
</div>
</template>
</Modal>
</template>

View File

@@ -0,0 +1,104 @@
<script lang="ts" setup>
import type { MesWmItemReceiptLineApi } from '#/api/mes/wm/itemreceipt/line';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
createItemReceiptLine,
getItemReceiptLine,
updateItemReceiptLine,
} from '#/api/mes/wm/itemreceipt/line';
import { $t } from '#/locales';
import { useLineFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<MesWmItemReceiptLineApi.ItemReceiptLine>();
const receiptId = ref<number>(); // 所属入库单编号
const noticeId = ref<number>(); // 所属到货通知单编号
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['物料入库单行'])
: $t('ui.actionTitle.create', ['物料入库单行']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-1',
labelWidth: 110,
},
layout: 'horizontal',
schema: [],
showDefaultActions: false,
wrapperClass: 'grid-cols-3',
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data =
(await formApi.getValues()) as MesWmItemReceiptLineApi.ItemReceiptLine;
data.receiptId = receiptId.value;
try {
await (formData.value?.id
? updateItemReceiptLine({ ...data, id: formData.value.id })
: createItemReceiptLine(data));
// 关闭并提示
await modalApi.close();
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
// 加载数据
const data = modalApi.getData<{
id?: number;
noticeId?: number;
receiptId: number;
}>();
receiptId.value = data.receiptId;
noticeId.value = data.noticeId;
formApi.setState({ schema: useLineFormSchema(!!data.noticeId, formApi) });
if (data.noticeId) {
await formApi.setFieldValue('noticeId', data.noticeId);
}
if (!data.id) {
return;
}
modalApi.lock();
try {
formData.value = await getItemReceiptLine(data.id);
// 设置到 values
await formApi.setValues({ ...formData.value, noticeId: noticeId.value });
} finally {
modalApi.unlock();
}
},
});
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" />
</Modal>
</template>

View File

@@ -0,0 +1,238 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmItemReceiptDetailApi } from '#/api/mes/wm/itemreceipt/detail';
import type { MesWmItemReceiptLineApi } from '#/api/mes/wm/itemreceipt/line';
import { computed, reactive } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getItemReceiptDetailListByLineId } from '#/api/mes/wm/itemreceipt/detail';
import {
deleteItemReceiptLine,
getItemReceiptLinePage,
} from '#/api/mes/wm/itemreceipt/line';
import { $t } from '#/locales';
import { PrinterLabel } from '#/views/mes/wm/barcode/components';
import { type FormType, useLineGridColumns } from '../data';
import DetailForm from './detail-form.vue';
import DetailList from './detail-list.vue';
import LineForm from './line-form.vue';
const props = defineProps<{
formType: FormType;
noticeId?: number;
receiptId: number;
}>();
const isEditable = computed(() => // 是否可编辑明细行
['create', 'update'].includes(props.formType),
);
const isStock = computed(() => props.formType === 'stock'); // 是否为上架模式
const detailMap = reactive<
Record<number, MesWmItemReceiptDetailApi.ItemReceiptDetail[]>
>({}); // 已展开行的上架明细缓存
const [LineFormModal, lineFormModalApi] = useVbenModal({
connectedComponent: LineForm,
destroyOnClose: true,
});
const [DetailFormModal, detailFormModalApi] = useVbenModal({
connectedComponent: DetailForm,
destroyOnClose: true,
});
/** 刷新表格 */
function handleRefresh() {
for (const id of Object.keys(detailMap)) {
delete detailMap[Number(id)];
}
gridApi.query();
}
/** 添加物料 */
function handleCreate() {
lineFormModalApi
.setData({ noticeId: props.noticeId, receiptId: props.receiptId })
.open();
}
/** 编辑物料 */
function handleEdit(row: MesWmItemReceiptLineApi.ItemReceiptLine) {
lineFormModalApi
.setData({
id: row.id,
noticeId: props.noticeId,
receiptId: props.receiptId,
})
.open();
}
/** 删除物料 */
async function handleDelete(row: MesWmItemReceiptLineApi.ItemReceiptLine) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.itemName]),
duration: 0,
});
try {
await deleteItemReceiptLine(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.itemName]));
handleRefresh();
} finally {
hideLoading();
}
}
/** 上架:直接打开明细创建表单 */
function handleStock(row: MesWmItemReceiptLineApi.ItemReceiptLine) {
openDetailForm(row.id!, row.itemId);
}
/** 打开上架明细表单 */
function openDetailForm(lineId: number, itemId?: number, detailId?: number) {
detailFormModalApi
.setData({ detailId, itemId, lineId, receiptId: props.receiptId })
.open();
}
/** 获取已展开行的上架明细 */
function getExpandedDetails(row: MesWmItemReceiptLineApi.ItemReceiptLine) {
return detailMap[row.id!] || [];
}
/** 加载指定行的上架明细 */
async function loadLineDetails(lineId: number) {
detailMap[lineId] = await getItemReceiptDetailListByLineId(lineId);
}
/** 展开行时懒加载上架明细 */
async function handleExpandChange(
row: MesWmItemReceiptLineApi.ItemReceiptLine,
expanded: boolean,
) {
if (!expanded) {
return;
}
await loadLineDetails(row.id!);
}
/** 明细表单提交成功后,刷新对应行已展开的明细 */
async function handleDetailSuccess(lineId: number) {
await loadLineDetails(lineId);
}
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useLineGridColumns(isEditable.value, isStock.value),
expandConfig: {
padding: true,
},
height: 400,
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }) => {
if (!props.receiptId) {
return { list: [], total: 0 };
}
return await getItemReceiptLinePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
receiptId: props.receiptId,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
},
} as VxeTableGridOptions<MesWmItemReceiptLineApi.ItemReceiptLine>,
gridEvents: {
toggleRowExpand: ({
expanded,
row,
}: {
expanded: boolean;
row: MesWmItemReceiptLineApi.ItemReceiptLine;
}) => {
handleExpandChange(row, expanded);
},
},
});
</script>
<template>
<div>
<LineFormModal @success="handleRefresh" />
<DetailFormModal @success="handleDetailSuccess" />
<Grid table-title="物料信息">
<template v-if="isEditable" #toolbar-tools>
<TableAction
:actions="[
{
label: '添加物料',
type: 'primary',
icon: ACTION_ICON.ADD,
onClick: handleCreate,
},
]"
/>
</template>
<template #detail="{ row }">
<DetailList
:details="getExpandedDetails(row)"
:form-type="formType"
@edit="(detailId) => openDetailForm(row.id!, row.itemId, detailId)"
@refresh="loadLineDetails(row.id!)"
/>
</template>
<template #actions="{ row }">
<div class="flex items-center justify-center">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
ifShow: isEditable,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
ifShow: isEditable,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.itemName]),
confirm: handleDelete.bind(null, row),
},
},
{
label: '上架',
type: 'link',
ifShow: isStock,
onClick: handleStock.bind(null, row),
},
]"
/>
<PrinterLabel
v-if="isStock"
:biz-code="row.batchCode"
:biz-id="row.batchId"
biz-type="ITEM_BATCH"
/>
</div>
</template>
</Grid>
</div>
</template>

View File

@@ -0,0 +1,474 @@
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmMaterialStockApi } from '#/api/mes/wm/materialstock';
import type { MesWmProductIssueApi } from '#/api/mes/wm/productissue';
import type { MesWmProductIssueDetailApi } from '#/api/mes/wm/productissue/detail';
import type { MesWmProductIssueLineApi } from '#/api/mes/wm/productissue/line';
import { h, markRaw } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { Button } from 'ant-design-vue';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { getRangePickerDefaultProps } from '#/utils';
import MdItemSelect from '#/views/mes/md/item/components/md-item-select.vue';
import MdWorkstationSelect from '#/views/mes/md/workstation/components/md-workstation-select.vue';
import ProWorkOrderSelect from '#/views/mes/pro/workorder/components/pro-work-order-select.vue';
import {
MesAutoCodeRuleCode,
MesProWorkOrderStatusEnum,
} from '#/views/mes/utils/constants';
import { WmMaterialStockSelect } from '#/views/mes/wm/materialstock/components';
import {
WmWarehouseAreaSelect,
WmWarehouseLocationSelect,
WmWarehouseSelect,
} from '#/views/mes/wm/warehouse/components';
/** 表单类型 */
export type FormType = 'create' | 'detail' | 'finish' | 'stock' | 'update';
/** 表单头部是否只读(拣货、详情、完成态) */
function isHeaderReadonly(formType: FormType): boolean {
return (
formType === 'detail' || formType === 'finish' || formType === 'stock'
);
}
/** 新增/修改的表单 */
export function useFormSchema(
formType: FormType,
formApi?: VbenFormApi,
): VbenFormSchema[] {
return [
{
fieldName: 'id',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'status',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'code',
label: '领料单编号',
component: 'Input',
componentProps: {
placeholder: '请输入领料单编号',
},
rules: 'required',
suffix: isHeaderReadonly(formType)
? undefined
: () =>
h(
Button,
{
type: 'default',
onClick: async () => {
const code = await generateAutoCode(
MesAutoCodeRuleCode.WM_PRODUCT_ISSUE_CODE,
);
await formApi?.setFieldValue('code', code);
},
},
{ default: () => '生成' },
),
},
{
fieldName: 'name',
label: '领料单名称',
component: 'Input',
componentProps: {
placeholder: '请输入领料单名称',
},
rules: 'required',
},
{
fieldName: 'requiredTime',
label: '需求时间',
component: 'DatePicker',
componentProps: {
class: '!w-full',
format: 'YYYY-MM-DD HH:mm:ss',
placeholder: '请选择需求时间',
showTime: true,
valueFormat: 'x',
},
rules: 'required',
},
{
fieldName: 'workOrderId',
label: '生产工单',
component: markRaw(ProWorkOrderSelect),
componentProps: {
status: MesProWorkOrderStatusEnum.CONFIRMED,
},
rules: 'selectRequired',
},
{
fieldName: 'workstationId',
label: '工作站',
component: markRaw(MdWorkstationSelect),
componentProps: {
placeholder: '请选择工作站',
},
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
formItemClass: 'col-span-3',
componentProps: {
placeholder: '请输入备注',
rows: 3,
},
},
];
}
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'code',
label: '领料单编号',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入领料单编号',
},
},
{
fieldName: 'name',
label: '领料单名称',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入领料单名称',
},
},
{
fieldName: 'issueDate',
label: '领料日期',
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
allowClear: true,
},
},
{
fieldName: 'status',
label: '单据状态',
component: 'Select',
componentProps: {
allowClear: true,
options: getDictOptions(DICT_TYPE.MES_WM_PRODUCT_ISSUE_STATUS, 'number'),
placeholder: '请选择单据状态',
},
},
];
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions<MesWmProductIssueApi.ProductIssue>['columns'] {
return [
{
field: 'code',
title: '领料单编号',
minWidth: 160,
slots: { default: 'code' },
},
{
field: 'name',
title: '领料单名称',
minWidth: 150,
},
{
field: 'workOrderCode',
title: '生产工单',
minWidth: 140,
},
{
field: 'workstationName',
title: '工作站',
minWidth: 120,
},
{
field: 'requiredTime',
title: '需求时间',
width: 180,
formatter: 'formatDateTime',
},
{
field: 'status',
title: '单据状态',
minWidth: 110,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.MES_WM_PRODUCT_ISSUE_STATUS },
},
},
{
title: '操作',
width: 240,
fixed: 'right',
slots: { default: 'actions' },
},
];
}
/** 领料单行子表的字段 */
export function useLineGridColumns(
editable: boolean,
stockable: boolean,
): VxeTableGridOptions<MesWmProductIssueLineApi.ProductIssueLine>['columns'] {
return [
{
type: 'expand',
width: 48,
slots: { content: 'detail' },
},
{
field: 'itemCode',
title: '物料编码',
minWidth: 120,
},
{
field: 'itemName',
title: '物料名称',
minWidth: 140,
},
{
field: 'specification',
title: '规格型号',
minWidth: 120,
},
{
field: 'unitMeasureName',
title: '单位',
width: 80,
},
{
field: 'quantity',
title: '领料数量',
width: 100,
},
...(editable || stockable
? [
{
title: '操作',
width: 160,
fixed: 'right',
slots: { default: 'actions' },
} as const,
]
: []),
];
}
/** 领料单行新增/修改的表单 */
export function useLineFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'itemId',
label: '产品物料',
component: markRaw(MdItemSelect),
componentProps: {
placeholder: '请选择产品物料',
},
rules: 'selectRequired',
},
{
fieldName: 'quantity',
label: '领料数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
placeholder: '请输入领料数量',
precision: 2,
},
rules: 'required',
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
formItemClass: 'col-span-3',
componentProps: {
placeholder: '请输入备注',
rows: 3,
},
},
];
}
/** 拣货明细子表的字段 */
export function useDetailGridColumns(
stockable: boolean,
): VxeTableGridOptions<MesWmProductIssueDetailApi.ProductIssueDetail>['columns'] {
return [
{
field: 'batchCode',
title: '批次号',
minWidth: 120,
},
{
field: 'warehouseName',
title: '仓库名称',
minWidth: 100,
},
{
field: 'locationName',
title: '库区名称',
minWidth: 100,
},
{
field: 'areaName',
title: '库位名称',
minWidth: 100,
},
{
field: 'quantity',
title: '数量',
width: 100,
},
...(stockable
? [
{
title: '操作',
width: 120,
fixed: 'right',
slots: { default: 'actions' },
} as const,
]
: []),
];
}
/** 拣货明细新增/修改的表单 */
export function useDetailFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
return [
{
fieldName: 'quantityMax',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'itemId',
label: '物料',
component: markRaw(MdItemSelect),
componentProps: {
disabled: true,
},
},
{
fieldName: 'materialStockId',
label: '库存记录',
component: markRaw(WmMaterialStockSelect),
componentProps: {
// 选择库存记录后,自动回填仓库/库区/库位/批次/数量
onChange: async (stock?: MesWmMaterialStockApi.MaterialStock) => {
await formApi?.setValues({
areaId: stock?.areaId,
batchCode: stock?.batchCode,
batchId: stock?.batchId,
locationId: stock?.locationId,
quantity: stock?.quantity,
quantityMax: stock?.quantity,
warehouseId: stock?.warehouseId,
});
},
},
rules: 'selectRequired',
dependencies: {
triggerFields: ['itemId'],
componentProps: (values) => ({
itemId: values.itemId,
}),
},
},
{
fieldName: 'quantity',
label: '数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
placeholder: '请输入数量',
precision: 2,
},
rules: 'required',
dependencies: {
triggerFields: ['quantityMax'],
componentProps: (values) => ({
class: '!w-full',
max: values.quantityMax,
min: 0,
placeholder: '请输入数量',
precision: 2,
}),
},
},
{
fieldName: 'warehouseId',
label: '出库仓库',
component: markRaw(WmWarehouseSelect),
componentProps: {
disabled: true,
},
},
{
fieldName: 'locationId',
label: '库区',
component: markRaw(WmWarehouseLocationSelect),
componentProps: {
disabled: true,
},
dependencies: {
triggerFields: ['warehouseId'],
componentProps: (values) => ({
disabled: true,
warehouseId: values.warehouseId,
}),
},
},
{
fieldName: 'areaId',
label: '库位',
component: markRaw(WmWarehouseAreaSelect),
componentProps: {
disabled: true,
},
dependencies: {
triggerFields: ['locationId'],
componentProps: (values) => ({
disabled: true,
locationId: values.locationId,
}),
},
},
{
fieldName: 'batchCode',
label: '批次号',
component: 'Input',
componentProps: {
disabled: true,
},
},
];
}

View File

@@ -0,0 +1,227 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmProductIssueApi } from '#/api/mes/wm/productissue';
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
cancelProductIssue,
deleteProductIssue,
exportProductIssue,
getProductIssuePage,
submitProductIssue,
} from '#/api/mes/wm/productissue';
import { $t } from '#/locales';
import { MesWmProductIssueStatusEnum } from '#/views/mes/utils/constants';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/** 刷新表格 */
function handleRefresh() {
gridApi.query();
}
/** 创建领料出库单 */
function handleCreate() {
formModalApi.setData({ formType: 'create' }).open();
}
/** 查看领料出库单 */
function handleDetail(row: MesWmProductIssueApi.ProductIssue) {
formModalApi.setData({ formType: 'detail', id: row.id }).open();
}
/** 编辑领料出库单 */
function handleEdit(row: MesWmProductIssueApi.ProductIssue) {
formModalApi.setData({ formType: 'update', id: row.id }).open();
}
/** 执行拣货 */
function handleStock(row: MesWmProductIssueApi.ProductIssue) {
formModalApi.setData({ formType: 'stock', id: row.id }).open();
}
/** 完成领料出库 */
function handleFinish(row: MesWmProductIssueApi.ProductIssue) {
formModalApi.setData({ formType: 'finish', id: row.id }).open();
}
/** 提交领料出库单 */
async function handleSubmit(row: MesWmProductIssueApi.ProductIssue) {
await submitProductIssue(row.id!);
message.success('提交成功');
handleRefresh();
}
/** 删除领料出库单 */
async function handleDelete(row: MesWmProductIssueApi.ProductIssue) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.code]),
duration: 0,
});
try {
await deleteProductIssue(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.code]));
handleRefresh();
} finally {
hideLoading();
}
}
/** 取消领料出库单 */
async function handleCancel(row: MesWmProductIssueApi.ProductIssue) {
await cancelProductIssue(row.id!);
message.success('取消成功');
handleRefresh();
}
/** 导出表格 */
async function handleExport() {
const data = await exportProductIssue(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '领料出库单.xls', source: data });
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getProductIssuePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
search: true,
},
} as VxeTableGridOptions<MesWmProductIssueApi.ProductIssue>,
});
</script>
<template>
<Page auto-content-height>
<template #doc>
<DocAlert
title="【仓库】生产领料、生产退料、物料消耗"
url="https://doc.iocoder.cn/mes/wm/issue-return/"
/>
</template>
<FormModal @success="handleRefresh" />
<Grid table-title="领料出库单列表">
<template #toolbar-tools>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['领料出库单']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['mes:wm-product-issue:create'],
onClick: handleCreate,
},
{
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
auth: ['mes:wm-product-issue:export'],
onClick: handleExport,
},
]"
/>
</template>
<template #code="{ row }">
<Button type="link" @click="handleDetail(row)">
{{ row.code }}
</Button>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['mes:wm-product-issue:update'],
ifShow: row.status === MesWmProductIssueStatusEnum.PREPARE,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.submit'),
type: 'link',
auth: ['mes:wm-product-issue:update'],
ifShow: row.status === MesWmProductIssueStatusEnum.PREPARE,
popConfirm: {
title: '确认提交该领料出库单?提交后将不能修改。',
confirm: handleSubmit.bind(null, row),
},
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['mes:wm-product-issue:delete'],
ifShow: row.status === MesWmProductIssueStatusEnum.PREPARE,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.code]),
confirm: handleDelete.bind(null, row),
},
},
{
label: '执行拣货',
type: 'link',
auth: ['mes:wm-product-issue:update'],
ifShow: row.status === MesWmProductIssueStatusEnum.APPROVING,
onClick: handleStock.bind(null, row),
},
{
label: '完成',
type: 'link',
auth: ['mes:wm-product-issue:finish'],
ifShow: row.status === MesWmProductIssueStatusEnum.APPROVED,
onClick: handleFinish.bind(null, row),
},
{
label: '取消',
type: 'link',
danger: true,
auth: ['mes:wm-product-issue:update'],
ifShow:
row.status === MesWmProductIssueStatusEnum.APPROVING ||
row.status === MesWmProductIssueStatusEnum.APPROVED,
popConfirm: {
title: '确认取消该领料出库单?取消后不可恢复。',
confirm: handleCancel.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,104 @@
<script lang="ts" setup>
import type { MesWmProductIssueDetailApi } from '#/api/mes/wm/productissue/detail';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
createProductIssueDetail,
getProductIssueDetail,
updateProductIssueDetail,
} from '#/api/mes/wm/productissue/detail';
import { $t } from '#/locales';
import { useDetailFormSchema } from '../data';
const emit = defineEmits<{ success: [lineId: number] }>();
const formData = ref<MesWmProductIssueDetailApi.ProductIssueDetail>();
const issueId = ref<number>(); // 所属领料单编号
const lineId = ref<number>(); // 所属领料单行编号
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['拣货明细'])
: $t('ui.actionTitle.create', ['拣货明细']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-1',
labelWidth: 90,
},
layout: 'horizontal',
schema: useDetailFormSchema(),
showDefaultActions: false,
wrapperClass: 'grid-cols-3',
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data =
(await formApi.getValues()) as MesWmProductIssueDetailApi.ProductIssueDetail;
data.issueId = issueId.value;
data.lineId = lineId.value;
try {
await (formData.value?.id
? updateProductIssueDetail({ ...data, id: formData.value.id })
: createProductIssueDetail(data));
// 关闭并提示
await modalApi.close();
emit('success', lineId.value!);
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
formApi.setState({ schema: useDetailFormSchema(formApi) });
// 加载数据
const data = modalApi.getData<{
detailId?: number;
issueId: number;
itemId?: number;
lineId: number;
}>();
issueId.value = data.issueId;
lineId.value = data.lineId;
if (data.detailId) {
modalApi.lock();
try {
formData.value = await getProductIssueDetail(data.detailId);
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
} else if (data.itemId) {
await formApi.setFieldValue('itemId', data.itemId);
}
},
});
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" />
</Modal>
</template>

View File

@@ -0,0 +1,103 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmProductIssueDetailApi } from '#/api/mes/wm/productissue/detail';
import { computed, watch } from 'vue';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteProductIssueDetail } from '#/api/mes/wm/productissue/detail';
import { $t } from '#/locales';
import { type FormType, useDetailGridColumns } from '../data';
const props = defineProps<{
details: MesWmProductIssueDetailApi.ProductIssueDetail[];
formType: FormType;
}>();
const emit = defineEmits<{
edit: [detailId: number];
refresh: [];
}>();
const isStock = computed(() => props.formType === 'stock'); // 是否为拣货模式
/** 编辑拣货明细 */
function handleEdit(row: MesWmProductIssueDetailApi.ProductIssueDetail) {
emit('edit', row.id!);
}
/** 删除拣货明细 */
async function handleDelete(
row: MesWmProductIssueDetailApi.ProductIssueDetail,
) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.warehouseName]),
duration: 0,
});
try {
await deleteProductIssueDetail(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.warehouseName]));
emit('refresh');
} finally {
hideLoading();
}
}
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
border: true,
columns: useDetailGridColumns(isStock.value),
data: props.details,
pagerConfig: {
enabled: false,
},
rowConfig: {
keyField: 'id',
isHover: true,
},
size: 'small',
toolbarConfig: {
enabled: false,
},
} as VxeTableGridOptions<MesWmProductIssueDetailApi.ProductIssueDetail>,
});
watch(
() => props.details,
(details) => gridApi.setGridOptions({ data: details }),
);
</script>
<template>
<div class="px-4 py-2">
<Grid>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [
row.warehouseName,
]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</div>
</template>

View File

@@ -0,0 +1,229 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { MesWmProductIssueApi } from '#/api/mes/wm/productissue';
import { computed, ref } from 'vue';
import { confirm, useVbenModal } from '@vben/common-ui';
import { Button, Divider, message, Popconfirm } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
checkProductIssueQuantity,
createProductIssue,
finishProductIssue,
getProductIssue,
stockProductIssue,
submitProductIssue,
updateProductIssue,
} from '#/api/mes/wm/productissue';
import { $t } from '#/locales';
import { MesWmProductIssueStatusEnum } from '#/views/mes/utils/constants';
import { useFormSchema } from '../data';
import LineList from './line-list.vue';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const formData = ref<MesWmProductIssueApi.ProductIssue>();
const originalSnapshot = ref(''); // 表单原始数据快照,用于提交时跳过未变更的保存请求
const isEditable = computed(() => // 是否为编辑模式(可保存)
['create', 'update'].includes(formType.value),
);
const isStock = computed(() => formType.value === 'stock'); // 是否为拣货模式
const isFinish = computed(() => formType.value === 'finish'); // 是否为完成出库模式
const canSubmit = computed(() => // 是否可提交
formType.value === 'update' &&
formData.value?.status === MesWmProductIssueStatusEnum.PREPARE,
);
const getTitle = computed(() => {
if (formType.value === 'detail') {
return $t('ui.actionTitle.view', ['领料出库单']);
}
if (formType.value === 'stock') {
return '执行拣货';
}
if (formType.value === 'finish') {
return '完成领料出库';
}
return formType.value === 'update'
? $t('ui.actionTitle.edit', ['领料出库单'])
: $t('ui.actionTitle.create', ['领料出库单']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-1',
labelWidth: 110,
},
layout: 'horizontal',
schema: [],
showDefaultActions: false,
wrapperClass: 'grid-cols-3',
});
/** 提交领料单:表单有修改时先保存,再调用提交接口 */
async function handleSubmit() {
const { valid } = await formApi.validate();
if (!valid || !formData.value?.id) {
return;
}
modalApi.lock();
try {
const current = JSON.stringify(await formApi.getValues());
if (current !== originalSnapshot.value) {
const data =
(await formApi.getValues()) as MesWmProductIssueApi.ProductIssue;
await updateProductIssue({ ...formData.value, ...data });
originalSnapshot.value = current;
}
await submitProductIssue(formData.value.id);
message.success('提交成功');
await modalApi.close();
emit('success');
} finally {
modalApi.unlock();
}
}
/** 执行拣货:领料数量与拣货数量不一致时二次确认 */
async function handleStock() {
if (!formData.value?.id) {
return;
}
const quantityMatch = await checkProductIssueQuantity(formData.value.id);
if (!quantityMatch) {
try {
await confirm('领料数量与拣货数量不一致,确认执行拣货?');
} catch {
return;
}
}
modalApi.lock();
try {
await stockProductIssue(formData.value.id);
message.success('拣货成功');
await modalApi.close();
emit('success');
} finally {
modalApi.unlock();
}
}
/** 完成领料出库 */
async function handleFinish() {
if (!formData.value?.id) {
return;
}
modalApi.lock();
try {
await finishProductIssue(formData.value.id);
message.success('完成成功');
await modalApi.close();
emit('success');
} finally {
modalApi.unlock();
}
}
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
if (!isEditable.value) {
await modalApi.close();
return;
}
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data =
(await formApi.getValues()) as MesWmProductIssueApi.ProductIssue;
try {
if (formData.value?.id) {
await updateProductIssue({ ...formData.value, ...data });
formData.value = { ...formData.value, ...data };
} else {
const id = await createProductIssue(data);
formData.value = {
...data,
id,
status: MesWmProductIssueStatusEnum.PREPARE,
};
await formApi.setFieldValue('id', id);
await formApi.setFieldValue('status', formData.value.status);
formType.value = 'update';
}
originalSnapshot.value = JSON.stringify(await formApi.getValues());
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
originalSnapshot.value = '';
return;
}
// 加载数据
const data = modalApi.getData<{ formType: FormType; id?: number }>();
formType.value = data.formType;
formApi.setState({ schema: useFormSchema(formType.value, formApi) });
formApi.setDisabled(!isEditable.value);
modalApi.setState({ showConfirmButton: isEditable.value });
if (data?.id) {
modalApi.lock();
try {
formData.value = await getProductIssue(data.id);
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
}
originalSnapshot.value = JSON.stringify(await formApi.getValues());
},
});
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" />
<!-- 非新建模式展示物料信息 -->
<template v-if="formData?.id">
<Divider>物料信息</Divider>
<div class="mx-4">
<LineList :form-type="formType" :issue-id="formData.id" />
</div>
</template>
<template #prepend-footer>
<div class="flex flex-auto items-center gap-2">
<Popconfirm
v-if="canSubmit"
title="确认提交该领料出库单?【提交后将不能修改】"
@confirm="handleSubmit"
>
<Button type="primary">提交</Button>
</Popconfirm>
<Button v-if="isStock" type="primary" @click="handleStock">
执行拣货
</Button>
<Popconfirm
v-if="isFinish"
title="确认完成该领料单并执行出库吗?"
@confirm="handleFinish"
>
<Button type="primary">完成</Button>
</Popconfirm>
</div>
</template>
</Modal>
</template>

View File

@@ -0,0 +1,94 @@
<script lang="ts" setup>
import type { MesWmProductIssueLineApi } from '#/api/mes/wm/productissue/line';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
createProductIssueLine,
getProductIssueLine,
updateProductIssueLine,
} from '#/api/mes/wm/productissue/line';
import { $t } from '#/locales';
import { useLineFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<MesWmProductIssueLineApi.ProductIssueLine>();
const issueId = ref<number>(); // 所属领料单编号
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['领料出库单行'])
: $t('ui.actionTitle.create', ['领料出库单行']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-1',
labelWidth: 90,
},
layout: 'horizontal',
schema: useLineFormSchema(),
showDefaultActions: false,
wrapperClass: 'grid-cols-3',
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data =
(await formApi.getValues()) as MesWmProductIssueLineApi.ProductIssueLine;
data.issueId = issueId.value;
try {
await (formData.value?.id
? updateProductIssueLine({ ...data, id: formData.value.id })
: createProductIssueLine(data));
// 关闭并提示
await modalApi.close();
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
// 加载数据
const data = modalApi.getData<{ id?: number; issueId: number }>();
issueId.value = data.issueId;
if (!data.id) {
return;
}
modalApi.lock();
try {
formData.value = await getProductIssueLine(data.id);
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
},
});
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" />
</Modal>
</template>

View File

@@ -0,0 +1,220 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmProductIssueDetailApi } from '#/api/mes/wm/productissue/detail';
import type { MesWmProductIssueLineApi } from '#/api/mes/wm/productissue/line';
import { computed, reactive } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getProductIssueDetailListByLineId } from '#/api/mes/wm/productissue/detail';
import {
deleteProductIssueLine,
getProductIssueLinePage,
} from '#/api/mes/wm/productissue/line';
import { $t } from '#/locales';
import { type FormType, useLineGridColumns } from '../data';
import DetailForm from './detail-form.vue';
import DetailList from './detail-list.vue';
import LineForm from './line-form.vue';
const props = defineProps<{
formType: FormType;
issueId: number;
}>();
const isEditable = computed(() => // 是否可编辑明细行
['create', 'update'].includes(props.formType),
);
const isStock = computed(() => props.formType === 'stock'); // 是否为拣货模式
const detailMap = reactive<
Record<number, MesWmProductIssueDetailApi.ProductIssueDetail[]>
>({}); // 已展开行的拣货明细缓存
const [LineFormModal, lineFormModalApi] = useVbenModal({
connectedComponent: LineForm,
destroyOnClose: true,
});
const [DetailFormModal, detailFormModalApi] = useVbenModal({
connectedComponent: DetailForm,
destroyOnClose: true,
});
/** 刷新表格 */
function handleRefresh() {
for (const id of Object.keys(detailMap)) {
delete detailMap[Number(id)];
}
gridApi.query();
}
/** 添加物料 */
function handleCreate() {
lineFormModalApi.setData({ issueId: props.issueId }).open();
}
/** 编辑物料 */
function handleEdit(row: MesWmProductIssueLineApi.ProductIssueLine) {
lineFormModalApi.setData({ id: row.id, issueId: props.issueId }).open();
}
/** 删除物料 */
async function handleDelete(row: MesWmProductIssueLineApi.ProductIssueLine) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.itemName]),
duration: 0,
});
try {
await deleteProductIssueLine(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.itemName]));
handleRefresh();
} finally {
hideLoading();
}
}
/** 拣货:直接打开明细创建表单 */
function handlePicking(row: MesWmProductIssueLineApi.ProductIssueLine) {
openDetailForm(row.id!, row.itemId);
}
/** 打开拣货明细表单 */
function openDetailForm(lineId: number, itemId?: number, detailId?: number) {
detailFormModalApi
.setData({ detailId, issueId: props.issueId, itemId, lineId })
.open();
}
/** 获取已展开行的拣货明细 */
function getExpandedDetails(row: MesWmProductIssueLineApi.ProductIssueLine) {
return detailMap[row.id!] || [];
}
/** 加载指定行的拣货明细 */
async function loadLineDetails(lineId: number) {
detailMap[lineId] = await getProductIssueDetailListByLineId(lineId);
}
/** 展开行时懒加载拣货明细 */
async function handleExpandChange(
row: MesWmProductIssueLineApi.ProductIssueLine,
expanded: boolean,
) {
if (!expanded) {
return;
}
await loadLineDetails(row.id!);
}
/** 明细表单提交成功后,刷新对应行已展开的明细 */
async function handleDetailSuccess(lineId: number) {
await loadLineDetails(lineId);
}
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useLineGridColumns(isEditable.value, isStock.value),
expandConfig: {
padding: true,
},
height: 400,
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }) => {
if (!props.issueId) {
return { list: [], total: 0 };
}
return await getProductIssueLinePage({
issueId: props.issueId,
pageNo: page.currentPage,
pageSize: page.pageSize,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
},
} as VxeTableGridOptions<MesWmProductIssueLineApi.ProductIssueLine>,
gridEvents: {
toggleRowExpand: ({
expanded,
row,
}: {
expanded: boolean;
row: MesWmProductIssueLineApi.ProductIssueLine;
}) => {
handleExpandChange(row, expanded);
},
},
});
</script>
<template>
<div>
<LineFormModal @success="handleRefresh" />
<DetailFormModal @success="handleDetailSuccess" />
<Grid table-title="物料信息">
<template v-if="isEditable" #toolbar-tools>
<TableAction
:actions="[
{
label: '添加物料',
type: 'primary',
icon: ACTION_ICON.ADD,
onClick: handleCreate,
},
]"
/>
</template>
<template #detail="{ row }">
<DetailList
:details="getExpandedDetails(row)"
:form-type="formType"
@edit="(detailId) => openDetailForm(row.id!, row.itemId, detailId)"
@refresh="loadLineDetails(row.id!)"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
ifShow: isEditable,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
ifShow: isEditable,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.itemName]),
confirm: handleDelete.bind(null, row),
},
},
{
label: '拣货',
type: 'link',
ifShow: isStock,
onClick: handlePicking.bind(null, row),
},
]"
/>
</template>
</Grid>
</div>
</template>

View File

@@ -0,0 +1,454 @@
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmMaterialStockApi } from '#/api/mes/wm/materialstock';
import type { MesWmProductReceiptApi } from '#/api/mes/wm/productreceipt';
import type { MesWmProductReceiptDetailApi } from '#/api/mes/wm/productreceipt/detail';
import type { MesWmProductReceiptLineApi } from '#/api/mes/wm/productreceipt/line';
import { h, markRaw } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { Button } from 'ant-design-vue';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { getRangePickerDefaultProps } from '#/utils';
import MdItemSelect from '#/views/mes/md/item/components/md-item-select.vue';
import ProWorkOrderSelect from '#/views/mes/pro/workorder/components/pro-work-order-select.vue';
import {
MesAutoCodeRuleCode,
MesProWorkOrderStatusEnum,
} from '#/views/mes/utils/constants';
import { WmMaterialStockSelect } from '#/views/mes/wm/materialstock/components';
import {
WmWarehouseAreaSelect,
WmWarehouseLocationSelect,
WmWarehouseSelect,
} from '#/views/mes/wm/warehouse/components';
/** 表单类型 */
export type FormType = 'create' | 'detail' | 'finish' | 'stock' | 'update';
/** 表单头部是否只读(上架、详情、入库态) */
function isHeaderReadonly(formType: FormType): boolean {
return (
formType === 'detail' || formType === 'finish' || formType === 'stock'
);
}
/** 新增/修改的表单 */
export function useFormSchema(
formType: FormType,
formApi?: VbenFormApi,
): VbenFormSchema[] {
return [
{
fieldName: 'id',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'status',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'code',
label: '入库单编号',
component: 'Input',
componentProps: {
placeholder: '请输入入库单编号',
},
rules: 'required',
suffix: isHeaderReadonly(formType)
? undefined
: () =>
h(
Button,
{
type: 'default',
onClick: async () => {
const code = await generateAutoCode(
MesAutoCodeRuleCode.PRODUCTRECPT_CODE,
);
await formApi?.setFieldValue('code', code);
},
},
{ default: () => '生成' },
),
},
{
fieldName: 'name',
label: '入库单名称',
component: 'Input',
componentProps: {
placeholder: '请输入入库单名称',
},
rules: 'required',
},
{
fieldName: 'receiptDate',
label: '入库日期',
component: 'DatePicker',
componentProps: {
class: '!w-full',
format: 'YYYY-MM-DD',
placeholder: '请选择入库日期',
valueFormat: 'x',
},
rules: 'required',
},
{
fieldName: 'workOrderId',
label: '生产工单',
component: markRaw(ProWorkOrderSelect),
componentProps: {
status: MesProWorkOrderStatusEnum.CONFIRMED,
},
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
formItemClass: 'col-span-3',
componentProps: {
placeholder: '请输入备注',
rows: 3,
},
},
];
}
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'code',
label: '入库单编号',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入入库单编号',
},
},
{
fieldName: 'name',
label: '入库单名称',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入入库单名称',
},
},
{
fieldName: 'receiptDate',
label: '入库日期',
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
allowClear: true,
},
},
];
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions<MesWmProductReceiptApi.ProductReceipt>['columns'] {
return [
{
field: 'code',
title: '入库单编号',
minWidth: 160,
slots: { default: 'code' },
},
{
field: 'name',
title: '入库单名称',
minWidth: 150,
},
{
field: 'workOrderCode',
title: '生产工单',
minWidth: 140,
},
{
field: 'receiptDate',
title: '入库日期',
width: 180,
formatter: 'formatDate',
},
{
field: 'status',
title: '单据状态',
minWidth: 110,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.MES_WM_PRODUCT_RECEIPT_STATUS },
},
},
{
title: '操作',
width: 240,
fixed: 'right',
slots: { default: 'actions' },
},
];
}
/** 入库单行子表的字段 */
export function useLineGridColumns(
editable: boolean,
stockable: boolean,
): VxeTableGridOptions<MesWmProductReceiptLineApi.ProductReceiptLine>['columns'] {
return [
{
type: 'expand',
width: 48,
slots: { content: 'detail' },
},
{
field: 'itemCode',
title: '产品物料编码',
minWidth: 120,
},
{
field: 'itemName',
title: '产品物料名称',
minWidth: 140,
},
{
field: 'specification',
title: '规格型号',
minWidth: 120,
},
{
field: 'unitMeasureName',
title: '单位',
width: 80,
},
{
field: 'quantity',
title: '入库数量',
width: 100,
},
{
field: 'batchCode',
title: '批次号',
minWidth: 120,
},
...(editable || stockable
? [
{
title: '操作',
width: 200,
fixed: 'right',
slots: { default: 'actions' },
} as const,
]
: []),
];
}
/** 入库单行新增/修改的表单 */
export function useLineFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
return [
{
fieldName: 'materialStockId',
label: '库存记录',
component: markRaw(WmMaterialStockSelect),
componentProps: {
// 选择库存记录后,自动回填物料/批次/数量
onChange: async (stock?: MesWmMaterialStockApi.MaterialStock) => {
await formApi?.setValues({
batchCode: stock?.batchCode,
batchId: stock?.batchId,
itemId: stock?.itemId,
quantity: stock?.quantity,
quantityMax: stock?.quantity,
});
},
virtualFilter: 'only',
},
rules: 'selectRequired',
},
{
fieldName: 'quantityMax',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'quantity',
label: '入库数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
placeholder: '请输入入库数量',
precision: 2,
},
rules: 'required',
dependencies: {
triggerFields: ['quantityMax'],
componentProps: (values) => ({
class: '!w-full',
max: values.quantityMax,
min: 0,
placeholder: '请输入入库数量',
precision: 2,
}),
},
},
{
fieldName: 'batchCode',
label: '批次号',
component: 'Input',
componentProps: {
disabled: true,
placeholder: '选择库存后自动带出',
},
},
{
fieldName: 'itemId',
label: '物料',
component: markRaw(MdItemSelect),
componentProps: {
disabled: true,
},
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
formItemClass: 'col-span-3',
componentProps: {
placeholder: '请输入备注',
rows: 3,
},
},
];
}
/** 上架明细子表的字段 */
export function useDetailGridColumns(
stockable: boolean,
): VxeTableGridOptions<MesWmProductReceiptDetailApi.ProductReceiptDetail>['columns'] {
return [
{
field: 'warehouseName',
title: '仓库名称',
minWidth: 100,
},
{
field: 'locationName',
title: '库区名称',
minWidth: 100,
},
{
field: 'areaName',
title: '库位名称',
minWidth: 100,
},
{
field: 'quantity',
title: '数量',
width: 100,
},
...(stockable
? [
{
title: '操作',
width: 120,
fixed: 'right',
slots: { default: 'actions' },
} as const,
]
: []),
];
}
/** 上架明细新增/修改的表单 */
export function useDetailFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
return [
{
fieldName: 'itemId',
label: '物料',
component: markRaw(MdItemSelect),
componentProps: {
disabled: true,
},
},
{
fieldName: 'warehouseId',
label: '入库仓库',
component: markRaw(WmWarehouseSelect),
componentProps: {
// 切换仓库后清空库区和库位
onChange: async () => {
await formApi?.setValues({
areaId: undefined,
locationId: undefined,
});
},
placeholder: '请选择仓库',
},
rules: 'selectRequired',
},
{
fieldName: 'locationId',
label: '库区',
component: markRaw(WmWarehouseLocationSelect),
componentProps: {
placeholder: '请选择库区',
},
rules: 'selectRequired',
dependencies: {
triggerFields: ['warehouseId'],
componentProps: (values) => ({
// 切换库区后清空库位
onChange: async () => {
await formApi?.setFieldValue('areaId', undefined);
},
placeholder: '请选择库区',
warehouseId: values.warehouseId,
}),
},
},
{
fieldName: 'areaId',
label: '库位',
component: markRaw(WmWarehouseAreaSelect),
componentProps: {
placeholder: '请选择库位',
},
rules: 'selectRequired',
dependencies: {
triggerFields: ['locationId'],
componentProps: (values) => ({
locationId: values.locationId,
placeholder: '请选择库位',
}),
},
},
{
fieldName: 'quantity',
label: '数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
placeholder: '请输入数量',
precision: 2,
},
rules: 'required',
},
];
}

View File

@@ -0,0 +1,209 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmProductReceiptApi } from '#/api/mes/wm/productreceipt';
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
cancelProductReceipt,
deleteProductReceipt,
exportProductReceipt,
getProductReceiptPage,
} from '#/api/mes/wm/productreceipt';
import { $t } from '#/locales';
import { MesWmProductReceiptStatusEnum } from '#/views/mes/utils/constants';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/** 刷新表格 */
function handleRefresh() {
gridApi.query();
}
/** 创建产品入库单 */
function handleCreate() {
formModalApi.setData({ formType: 'create' }).open();
}
/** 查看产品入库单 */
function handleDetail(row: MesWmProductReceiptApi.ProductReceipt) {
formModalApi.setData({ formType: 'detail', id: row.id }).open();
}
/** 编辑产品入库单 */
function handleEdit(row: MesWmProductReceiptApi.ProductReceipt) {
formModalApi.setData({ formType: 'update', id: row.id }).open();
}
/** 执行上架 */
function handleStock(row: MesWmProductReceiptApi.ProductReceipt) {
formModalApi.setData({ formType: 'stock', id: row.id }).open();
}
/** 执行入库 */
function handleFinish(row: MesWmProductReceiptApi.ProductReceipt) {
formModalApi.setData({ formType: 'finish', id: row.id }).open();
}
/** 删除产品入库单 */
async function handleDelete(row: MesWmProductReceiptApi.ProductReceipt) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.code]),
duration: 0,
});
try {
await deleteProductReceipt(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.code]));
handleRefresh();
} finally {
hideLoading();
}
}
/** 取消产品入库单 */
async function handleCancel(row: MesWmProductReceiptApi.ProductReceipt) {
await cancelProductReceipt(row.id!);
message.success('取消成功');
handleRefresh();
}
/** 导出表格 */
async function handleExport() {
const data = await exportProductReceipt(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '产品入库单.xls', source: data });
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getProductReceiptPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
search: true,
},
} as VxeTableGridOptions<MesWmProductReceiptApi.ProductReceipt>,
});
</script>
<template>
<Page auto-content-height>
<template #doc>
<DocAlert
title="【仓库】生产入库、生产退料"
url="https://doc.iocoder.cn/mes/wm/produce-in/"
/>
</template>
<FormModal @success="handleRefresh" />
<Grid table-title="产品入库单列表">
<template #toolbar-tools>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['产品入库单']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['mes:wm-product-receipt:create'],
onClick: handleCreate,
},
{
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
auth: ['mes:wm-product-receipt:export'],
onClick: handleExport,
},
]"
/>
</template>
<template #code="{ row }">
<Button type="link" @click="handleDetail(row)">
{{ row.code }}
</Button>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['mes:wm-product-receipt:update'],
ifShow: row.status === MesWmProductReceiptStatusEnum.PREPARE,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['mes:wm-product-receipt:delete'],
ifShow: row.status === MesWmProductReceiptStatusEnum.PREPARE,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.code]),
confirm: handleDelete.bind(null, row),
},
},
{
label: '执行上架',
type: 'link',
auth: ['mes:wm-product-receipt:update'],
ifShow: row.status === MesWmProductReceiptStatusEnum.APPROVING,
onClick: handleStock.bind(null, row),
},
{
label: '执行入库',
type: 'link',
auth: ['mes:wm-product-receipt:finish'],
ifShow: row.status === MesWmProductReceiptStatusEnum.APPROVED,
onClick: handleFinish.bind(null, row),
},
{
label: '取消',
type: 'link',
danger: true,
auth: ['mes:wm-product-receipt:update'],
ifShow:
row.status === MesWmProductReceiptStatusEnum.APPROVING ||
row.status === MesWmProductReceiptStatusEnum.APPROVED,
popConfirm: {
title: '确认取消该产品入库单?取消后不可恢复。',
confirm: handleCancel.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,104 @@
<script lang="ts" setup>
import type { MesWmProductReceiptDetailApi } from '#/api/mes/wm/productreceipt/detail';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
createProductReceiptDetail,
getProductReceiptDetail,
updateProductReceiptDetail,
} from '#/api/mes/wm/productreceipt/detail';
import { $t } from '#/locales';
import { useDetailFormSchema } from '../data';
const emit = defineEmits<{ success: [lineId: number] }>();
const formData = ref<MesWmProductReceiptDetailApi.ProductReceiptDetail>();
const receiptId = ref<number>(); // 所属入库单编号
const lineId = ref<number>(); // 所属入库单行编号
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['上架明细'])
: $t('ui.actionTitle.create', ['上架明细']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-1',
labelWidth: 90,
},
layout: 'horizontal',
schema: useDetailFormSchema(),
showDefaultActions: false,
wrapperClass: 'grid-cols-3',
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data =
(await formApi.getValues()) as MesWmProductReceiptDetailApi.ProductReceiptDetail;
data.receiptId = receiptId.value;
data.lineId = lineId.value;
try {
await (formData.value?.id
? updateProductReceiptDetail({ ...data, id: formData.value.id })
: createProductReceiptDetail(data));
// 关闭并提示
await modalApi.close();
emit('success', lineId.value!);
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
formApi.setState({ schema: useDetailFormSchema(formApi) });
// 加载数据
const data = modalApi.getData<{
detailId?: number;
itemId?: number;
lineId: number;
receiptId: number;
}>();
receiptId.value = data.receiptId;
lineId.value = data.lineId;
if (data.detailId) {
modalApi.lock();
try {
formData.value = await getProductReceiptDetail(data.detailId);
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
} else if (data.itemId) {
await formApi.setFieldValue('itemId', data.itemId);
}
},
});
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" />
</Modal>
</template>

View File

@@ -0,0 +1,103 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmProductReceiptDetailApi } from '#/api/mes/wm/productreceipt/detail';
import { computed, watch } from 'vue';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteProductReceiptDetail } from '#/api/mes/wm/productreceipt/detail';
import { $t } from '#/locales';
import { type FormType, useDetailGridColumns } from '../data';
const props = defineProps<{
details: MesWmProductReceiptDetailApi.ProductReceiptDetail[];
formType: FormType;
}>();
const emit = defineEmits<{
edit: [detailId: number];
refresh: [];
}>();
const isStock = computed(() => props.formType === 'stock'); // 是否为上架模式
/** 编辑上架明细 */
function handleEdit(row: MesWmProductReceiptDetailApi.ProductReceiptDetail) {
emit('edit', row.id!);
}
/** 删除上架明细 */
async function handleDelete(
row: MesWmProductReceiptDetailApi.ProductReceiptDetail,
) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.warehouseName]),
duration: 0,
});
try {
await deleteProductReceiptDetail(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.warehouseName]));
emit('refresh');
} finally {
hideLoading();
}
}
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
border: true,
columns: useDetailGridColumns(isStock.value),
data: props.details,
pagerConfig: {
enabled: false,
},
rowConfig: {
keyField: 'id',
isHover: true,
},
size: 'small',
toolbarConfig: {
enabled: false,
},
} as VxeTableGridOptions<MesWmProductReceiptDetailApi.ProductReceiptDetail>,
});
watch(
() => props.details,
(details) => gridApi.setGridOptions({ data: details }),
);
</script>
<template>
<div class="px-4 py-2">
<Grid>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [
row.warehouseName,
]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</div>
</template>

View File

@@ -0,0 +1,235 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { MesWmProductReceiptApi } from '#/api/mes/wm/productreceipt';
import { computed, ref } from 'vue';
import { confirm, useVbenModal } from '@vben/common-ui';
import { Button, Divider, message, Popconfirm } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
checkProductReceiptQuantity,
createProductReceipt,
finishProductReceipt,
getProductReceipt,
stockProductReceipt,
submitProductReceipt,
updateProductReceipt,
} from '#/api/mes/wm/productreceipt';
import { $t } from '#/locales';
import { MesWmProductReceiptStatusEnum } from '#/views/mes/utils/constants';
import { useFormSchema } from '../data';
import LineList from './line-list.vue';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const formData = ref<MesWmProductReceiptApi.ProductReceipt>();
const originalSnapshot = ref(''); // 表单原始数据快照,用于提交时跳过未变更的保存请求
const isEditable = computed(() => // 是否为编辑模式(可保存)
['create', 'update'].includes(formType.value),
);
const isStock = computed(() => formType.value === 'stock'); // 是否为上架模式
const isFinish = computed(() => formType.value === 'finish'); // 是否为执行入库模式
const canSubmit = computed(() => // 是否可提交
formType.value === 'update' &&
formData.value?.status === MesWmProductReceiptStatusEnum.PREPARE,
);
const getTitle = computed(() => {
if (formType.value === 'detail') {
return $t('ui.actionTitle.view', ['产品入库单']);
}
if (formType.value === 'stock') {
return '执行上架';
}
if (formType.value === 'finish') {
return '执行入库';
}
return formType.value === 'update'
? $t('ui.actionTitle.edit', ['产品入库单'])
: $t('ui.actionTitle.create', ['产品入库单']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-1',
labelWidth: 110,
},
layout: 'horizontal',
schema: [],
showDefaultActions: false,
wrapperClass: 'grid-cols-3',
});
/** 提交入库单:表单有修改时先保存,再调用提交接口 */
async function handleSubmit() {
const { valid } = await formApi.validate();
if (!valid || !formData.value?.id) {
return;
}
modalApi.lock();
try {
const current = JSON.stringify(await formApi.getValues());
if (current !== originalSnapshot.value) {
const data =
(await formApi.getValues()) as MesWmProductReceiptApi.ProductReceipt;
await updateProductReceipt({ ...formData.value, ...data });
originalSnapshot.value = current;
}
await submitProductReceipt(formData.value.id);
message.success('提交成功');
await modalApi.close();
emit('success');
} finally {
modalApi.unlock();
}
}
/** 执行上架:明细数量与行收货数量不一致时二次确认 */
async function handleStock() {
if (!formData.value?.id) {
return;
}
const quantityMatch = await checkProductReceiptQuantity(formData.value.id);
if (!quantityMatch) {
try {
await confirm('明细数量与行收货数量不一致,确认执行上架?');
} catch {
return;
}
}
modalApi.lock();
try {
await stockProductReceipt(formData.value.id);
message.success('上架成功');
await modalApi.close();
emit('success');
} finally {
modalApi.unlock();
}
}
/** 执行入库 */
async function handleFinish() {
if (!formData.value?.id) {
return;
}
modalApi.lock();
try {
await finishProductReceipt(formData.value.id);
message.success('入库成功');
await modalApi.close();
emit('success');
} finally {
modalApi.unlock();
}
}
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
if (!isEditable.value) {
await modalApi.close();
return;
}
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data =
(await formApi.getValues()) as MesWmProductReceiptApi.ProductReceipt;
try {
if (formData.value?.id) {
await updateProductReceipt({ ...formData.value, ...data });
formData.value = { ...formData.value, ...data };
} else {
const id = await createProductReceipt(data);
formData.value = {
...data,
id,
status: MesWmProductReceiptStatusEnum.PREPARE,
};
await formApi.setFieldValue('id', id);
await formApi.setFieldValue('status', formData.value.status);
formType.value = 'update';
}
originalSnapshot.value = JSON.stringify(await formApi.getValues());
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
originalSnapshot.value = '';
return;
}
// 加载数据
const data = modalApi.getData<{ formType: FormType; id?: number }>();
formType.value = data.formType;
formApi.setState({ schema: useFormSchema(formType.value, formApi) });
formApi.setDisabled(!isEditable.value);
modalApi.setState({ showConfirmButton: isEditable.value });
if (data?.id) {
modalApi.lock();
try {
formData.value = await getProductReceipt(data.id);
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
}
originalSnapshot.value = JSON.stringify(await formApi.getValues());
},
});
/** 入库前确认 */
async function confirmFinish() {
try {
await confirm('确认执行入库?执行后将更新库存台账。');
} catch {
return;
}
await handleFinish();
}
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" />
<!-- 非新建模式展示物料信息 -->
<template v-if="formData?.id">
<Divider>物料信息</Divider>
<div class="mx-4">
<LineList :form-type="formType" :receipt-id="formData.id" />
</div>
</template>
<template #prepend-footer>
<div class="flex flex-auto items-center gap-2">
<Popconfirm
v-if="canSubmit"
title="确认提交该产品入库单?【提交后将不能修改】"
@confirm="handleSubmit"
>
<Button type="primary">提交</Button>
</Popconfirm>
<Button v-if="isStock" type="primary" @click="handleStock">
执行上架
</Button>
<Button v-if="isFinish" type="primary" @click="confirmFinish">
执行入库
</Button>
</div>
</template>
</Modal>
</template>

View File

@@ -0,0 +1,95 @@
<script lang="ts" setup>
import type { MesWmProductReceiptLineApi } from '#/api/mes/wm/productreceipt/line';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
createProductReceiptLine,
getProductReceiptLine,
updateProductReceiptLine,
} from '#/api/mes/wm/productreceipt/line';
import { $t } from '#/locales';
import { useLineFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<MesWmProductReceiptLineApi.ProductReceiptLine>();
const receiptId = ref<number>(); // 所属入库单编号
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['物料入库单行'])
: $t('ui.actionTitle.create', ['物料入库单行']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-1',
labelWidth: 110,
},
layout: 'horizontal',
schema: [],
showDefaultActions: false,
wrapperClass: 'grid-cols-3',
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data =
(await formApi.getValues()) as MesWmProductReceiptLineApi.ProductReceiptLine;
data.receiptId = receiptId.value;
try {
await (formData.value?.id
? updateProductReceiptLine({ ...data, id: formData.value.id })
: createProductReceiptLine(data));
// 关闭并提示
await modalApi.close();
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
formApi.setState({ schema: useLineFormSchema(formApi) });
// 加载数据
const data = modalApi.getData<{ id?: number; receiptId: number }>();
receiptId.value = data.receiptId;
if (!data.id) {
return;
}
modalApi.lock();
try {
formData.value = await getProductReceiptLine(data.id);
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
},
});
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" />
</Modal>
</template>

View File

@@ -0,0 +1,249 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmProductReceiptDetailApi } from '#/api/mes/wm/productreceipt/detail';
import type { MesWmProductReceiptLineApi } from '#/api/mes/wm/productreceipt/line';
import { computed, reactive, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getProductReceiptDetailListByLineId } from '#/api/mes/wm/productreceipt/detail';
import {
deleteProductReceiptLine,
getProductReceiptLinePage,
} from '#/api/mes/wm/productreceipt/line';
import { $t } from '#/locales';
import { BarcodeBizTypeEnum } from '#/views/mes/utils/constants';
import { BarcodeDetail, PrinterLabel } from '#/views/mes/wm/barcode/components';
import { type FormType, useLineGridColumns } from '../data';
import DetailForm from './detail-form.vue';
import DetailList from './detail-list.vue';
import LineForm from './line-form.vue';
const props = defineProps<{
formType: FormType;
receiptId: number;
}>();
const isEditable = computed(() => // 是否可编辑明细行
['create', 'update'].includes(props.formType),
);
const isStock = computed(() => props.formType === 'stock'); // 是否为上架模式
const detailMap = reactive<
Record<number, MesWmProductReceiptDetailApi.ProductReceiptDetail[]>
>({}); // 已展开行的上架明细缓存
const barcodeDetailRef = ref(); // 条码详情弹窗实例
const [LineFormModal, lineFormModalApi] = useVbenModal({
connectedComponent: LineForm,
destroyOnClose: true,
});
const [DetailFormModal, detailFormModalApi] = useVbenModal({
connectedComponent: DetailForm,
destroyOnClose: true,
});
/** 刷新表格 */
function handleRefresh() {
for (const id of Object.keys(detailMap)) {
delete detailMap[Number(id)];
}
gridApi.query();
}
/** 添加物料 */
function handleCreate() {
lineFormModalApi.setData({ receiptId: props.receiptId }).open();
}
/** 编辑物料 */
function handleEdit(row: MesWmProductReceiptLineApi.ProductReceiptLine) {
lineFormModalApi.setData({ id: row.id, receiptId: props.receiptId }).open();
}
/** 删除物料 */
async function handleDelete(
row: MesWmProductReceiptLineApi.ProductReceiptLine,
) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.itemName]),
duration: 0,
});
try {
await deleteProductReceiptLine(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.itemName]));
handleRefresh();
} finally {
hideLoading();
}
}
/** 上架:直接打开明细创建表单 */
function handleStock(row: MesWmProductReceiptLineApi.ProductReceiptLine) {
openDetailForm(row.id!, row.itemId);
}
/** 查看物料条码 */
function handleBarcode(row: MesWmProductReceiptLineApi.ProductReceiptLine) {
barcodeDetailRef.value?.openByBusiness(
row.itemId,
BarcodeBizTypeEnum.ITEM,
row.itemCode,
row.itemName,
);
}
/** 打开上架明细表单 */
function openDetailForm(lineId: number, itemId?: number, detailId?: number) {
detailFormModalApi
.setData({ detailId, itemId, lineId, receiptId: props.receiptId })
.open();
}
/** 获取已展开行的上架明细 */
function getExpandedDetails(row: MesWmProductReceiptLineApi.ProductReceiptLine) {
return detailMap[row.id!] || [];
}
/** 加载指定行的上架明细 */
async function loadLineDetails(lineId: number) {
detailMap[lineId] = await getProductReceiptDetailListByLineId(lineId);
}
/** 展开行时懒加载上架明细 */
async function handleExpandChange(
row: MesWmProductReceiptLineApi.ProductReceiptLine,
expanded: boolean,
) {
if (!expanded) {
return;
}
await loadLineDetails(row.id!);
}
/** 明细表单提交成功后,刷新对应行已展开的明细 */
async function handleDetailSuccess(lineId: number) {
await loadLineDetails(lineId);
}
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useLineGridColumns(isEditable.value, isStock.value),
expandConfig: {
padding: true,
},
height: 400,
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }) => {
if (!props.receiptId) {
return { list: [], total: 0 };
}
return await getProductReceiptLinePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
receiptId: props.receiptId,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
},
} as VxeTableGridOptions<MesWmProductReceiptLineApi.ProductReceiptLine>,
gridEvents: {
toggleRowExpand: ({
expanded,
row,
}: {
expanded: boolean;
row: MesWmProductReceiptLineApi.ProductReceiptLine;
}) => {
handleExpandChange(row, expanded);
},
},
});
</script>
<template>
<div>
<LineFormModal @success="handleRefresh" />
<DetailFormModal @success="handleDetailSuccess" />
<Grid table-title="物料信息">
<template v-if="isEditable" #toolbar-tools>
<TableAction
:actions="[
{
label: '添加物料',
type: 'primary',
icon: ACTION_ICON.ADD,
onClick: handleCreate,
},
]"
/>
</template>
<template #detail="{ row }">
<DetailList
:details="getExpandedDetails(row)"
:form-type="formType"
@edit="(detailId) => openDetailForm(row.id!, row.itemId, detailId)"
@refresh="loadLineDetails(row.id!)"
/>
</template>
<template #actions="{ row }">
<div class="flex items-center justify-center">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
ifShow: isEditable,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
ifShow: isEditable,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.itemName]),
confirm: handleDelete.bind(null, row),
},
},
{
label: '上架',
type: 'link',
ifShow: isStock,
onClick: handleStock.bind(null, row),
},
{
label: '条码',
type: 'link',
onClick: handleBarcode.bind(null, row),
},
]"
/>
<PrinterLabel
v-if="isStock"
:biz-code="row.batchCode"
:biz-id="row.batchId"
biz-type="BATCH"
/>
</div>
</template>
</Grid>
<BarcodeDetail ref="barcodeDetailRef" />
</div>
</template>

View File

@@ -0,0 +1,688 @@
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmMaterialStockApi } from '#/api/mes/wm/materialstock';
import type { MesWmProductSalesApi } from '#/api/mes/wm/productsales';
import type { MesWmProductSalesDetailApi } from '#/api/mes/wm/productsales/detail';
import type { MesWmProductSalesLineApi } from '#/api/mes/wm/productsales/line';
import type { MesWmSalesNoticeApi } from '#/api/mes/wm/salesnotice';
import type { MesWmSalesNoticeLineApi } from '#/api/mes/wm/salesnotice/line';
import { h, markRaw } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { Button } from 'ant-design-vue';
import { z } from '#/adapter/form';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { getRangePickerDefaultProps } from '#/utils';
import MdClientSelect from '#/views/mes/md/client/components/md-client-select.vue';
import MdItemSelect from '#/views/mes/md/item/components/md-item-select.vue';
import {
MesAutoCodeRuleCode,
MesWmSalesNoticeStatusEnum,
} from '#/views/mes/utils/constants';
import { WmMaterialStockSelect } from '#/views/mes/wm/materialstock/components';
import {
WmSalesNoticeLineSelect,
WmSalesNoticeSelect,
} from '#/views/mes/wm/salesnotice/components';
import {
WmWarehouseAreaSelect,
WmWarehouseLocationSelect,
WmWarehouseSelect,
} from '#/views/mes/wm/warehouse/components';
/** 表单类型 */
export type FormType =
| 'create'
| 'detail'
| 'finish'
| 'shipping'
| 'stock'
| 'update';
/** 表单头部是否只读(拣货、填写运单、出库、详情态) */
function isHeaderReadonly(formType: FormType): boolean {
return (
formType === 'detail' ||
formType === 'finish' ||
formType === 'shipping' ||
formType === 'stock'
);
}
/** 是否展示运输信息 */
export function showShippingInfo(formType: FormType): boolean {
return (
formType === 'shipping' || formType === 'detail' || formType === 'finish'
);
}
/** 新增/修改的表单 */
export function useFormSchema(
formType: FormType,
formApi?: VbenFormApi,
): VbenFormSchema[] {
return [
{
fieldName: 'id',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'status',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'code',
label: '出库单编号',
component: 'Input',
componentProps: {
placeholder: '请输入出库单编号',
},
rules: 'required',
suffix: isHeaderReadonly(formType)
? undefined
: () =>
h(
Button,
{
type: 'default',
onClick: async () => {
const code = await generateAutoCode(
MesAutoCodeRuleCode.WM_PRODUCT_SALES_CODE,
);
await formApi?.setFieldValue('code', code);
},
},
{ default: () => '生成' },
),
},
{
fieldName: 'name',
label: '出库单名称',
component: 'Input',
componentProps: {
placeholder: '请输入出库单名称',
},
rules: 'required',
},
{
fieldName: 'noticeId',
label: '发货通知单',
component: markRaw(WmSalesNoticeSelect),
componentProps: {
// 选择发货通知单后,自动回填销售订单号、客户、收货人信息
onChange: async (notice?: MesWmSalesNoticeApi.SalesNotice) => {
await formApi?.setValues({
clientId: notice?.clientId,
contactAddress: notice?.recipientAddress,
contactName: notice?.recipientName,
contactTelephone: notice?.recipientTelephone,
salesOrderCode: notice?.salesOrderCode,
});
},
status: MesWmSalesNoticeStatusEnum.APPROVED,
},
},
{
fieldName: 'salesOrderCode',
label: '销售订单编号',
component: 'Input',
componentProps: {
placeholder: '请输入销售订单编号',
},
},
{
fieldName: 'salesDate',
label: '出库日期',
component: 'DatePicker',
componentProps: {
class: '!w-full',
format: 'YYYY-MM-DD',
placeholder: '请选择出库日期',
valueFormat: 'x',
},
rules: 'required',
},
{
fieldName: 'clientId',
label: '客户',
component: markRaw(MdClientSelect),
componentProps: {
placeholder: '请选择客户',
},
rules: 'selectRequired',
},
{
fieldName: 'contactName',
label: '收货人',
component: 'Input',
componentProps: {
placeholder: '请输入收货人',
},
},
{
fieldName: 'contactTelephone',
label: '联系方式',
component: 'Input',
componentProps: {
placeholder: '请输入联系方式',
},
},
{
fieldName: 'contactAddress',
label: '收货地址',
component: 'Input',
componentProps: {
placeholder: '请输入收货地址',
},
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
formItemClass: 'col-span-3',
componentProps: {
placeholder: '请输入备注',
rows: 3,
},
},
{
fieldName: 'carrier',
label: '承运商',
component: 'Input',
componentProps: {
disabled: formType !== 'shipping',
placeholder: '请输入承运商',
},
dependencies: {
triggerFields: [''],
if: () => showShippingInfo(formType),
},
},
{
fieldName: 'shippingNumber',
label: '运输单号',
component: 'Input',
componentProps: {
disabled: formType !== 'shipping',
placeholder: '请输入运输单号',
},
dependencies: {
triggerFields: [''],
if: () => showShippingInfo(formType),
},
},
];
}
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'code',
label: '出库单编号',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入出库单编号',
},
},
{
fieldName: 'name',
label: '出库单名称',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入出库单名称',
},
},
{
fieldName: 'salesOrderCode',
label: '销售订单编号',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入销售订单编号',
},
},
{
fieldName: 'clientId',
label: '客户',
component: markRaw(MdClientSelect),
componentProps: {
placeholder: '请选择客户',
},
},
{
fieldName: 'salesDate',
label: '出库日期',
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
allowClear: true,
},
},
{
fieldName: 'status',
label: '单据状态',
component: 'Select',
componentProps: {
allowClear: true,
options: getDictOptions(DICT_TYPE.MES_WM_PRODUCT_SALES_STATUS, 'number'),
placeholder: '请选择单据状态',
},
},
];
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions<MesWmProductSalesApi.ProductSales>['columns'] {
return [
{
field: 'code',
title: '出库单编号',
minWidth: 160,
slots: { default: 'code' },
},
{
field: 'name',
title: '出库单名称',
minWidth: 150,
},
{
field: 'noticeCode',
title: '发货通知单号',
minWidth: 160,
},
{
field: 'salesOrderCode',
title: '销售订单编号',
minWidth: 160,
},
{
field: 'clientCode',
title: '客户编码',
minWidth: 120,
},
{
field: 'clientName',
title: '客户名称',
minWidth: 120,
},
{
field: 'contactName',
title: '收货人',
minWidth: 100,
},
{
field: 'carrier',
title: '承运商',
minWidth: 120,
},
{
field: 'shippingNumber',
title: '运输单号',
minWidth: 160,
},
{
field: 'salesDate',
title: '出库日期',
width: 180,
formatter: 'formatDate',
},
{
field: 'status',
title: '单据状态',
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.MES_WM_PRODUCT_SALES_STATUS },
},
},
{
title: '操作',
width: 240,
fixed: 'right',
slots: { default: 'actions' },
},
];
}
/** 出库单行子表的字段 */
export function useLineGridColumns(
editable: boolean,
stockable: boolean,
): VxeTableGridOptions<MesWmProductSalesLineApi.ProductSalesLine>['columns'] {
return [
{
type: 'expand',
width: 48,
slots: { content: 'detail' },
},
{
field: 'itemCode',
title: '产品编码',
minWidth: 120,
},
{
field: 'itemName',
title: '产品名称',
minWidth: 140,
},
{
field: 'specification',
title: '规格型号',
minWidth: 120,
},
{
field: 'unitMeasureName',
title: '单位',
width: 80,
},
{
field: 'quantity',
title: '出库数量',
width: 100,
},
{
field: 'batchCode',
title: '批次号',
minWidth: 120,
},
{
field: 'oqcCheckFlag',
title: '是否校验',
width: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
},
},
{
field: 'remark',
title: '备注',
minWidth: 150,
},
...(editable || stockable
? [
{
title: '操作',
width: 160,
fixed: 'right',
slots: { default: 'actions' },
} as const,
]
: []),
];
}
/** 出库单行新增/修改的表单 */
export function useLineFormSchema(
hasNotice: boolean,
formApi?: VbenFormApi,
): VbenFormSchema[] {
return [
{
fieldName: 'noticeId',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'noticeLineId',
label: '发货通知单行',
component: markRaw(WmSalesNoticeLineSelect),
componentProps: {
// 选择发货通知单行后,自动回填物料、数量、批次、是否检验
onChange: async (line?: MesWmSalesNoticeLineApi.SalesNoticeLine) => {
await formApi?.setValues({
batchCode: line?.batchCode,
itemId: line?.itemId,
oqcCheckFlag: line?.oqcCheckFlag ?? false,
quantity: line?.quantity,
});
},
},
dependencies: {
triggerFields: ['noticeId'],
if: () => hasNotice,
componentProps: (values) => ({
noticeId: values.noticeId,
}),
},
},
{
fieldName: 'itemId',
label: '产品',
component: markRaw(MdItemSelect),
componentProps: {
placeholder: '请选择产品',
},
rules: 'selectRequired',
dependencies: {
triggerFields: ['noticeLineId'],
componentProps: (values) => ({
disabled: !!values.noticeLineId,
placeholder: '请选择产品',
}),
},
},
{
fieldName: 'batchCode',
label: '批次号',
component: 'Input',
componentProps: {
placeholder: '请输入批次号',
},
},
{
fieldName: 'quantity',
label: '出库数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
placeholder: '请输入出库数量',
precision: 2,
},
rules: 'required',
},
{
fieldName: 'oqcCheckFlag',
label: '是否校验',
component: 'RadioGroup',
componentProps: {
buttonStyle: 'solid',
optionType: 'button',
options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
},
rules: z.boolean().default(false),
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
formItemClass: 'col-span-3',
componentProps: {
placeholder: '请输入备注',
rows: 3,
},
},
];
}
/** 拣货明细子表的字段 */
export function useDetailGridColumns(
stockable: boolean,
): VxeTableGridOptions<MesWmProductSalesDetailApi.ProductSalesDetail>['columns'] {
return [
{
field: 'warehouseName',
title: '仓库名称',
minWidth: 100,
},
{
field: 'locationName',
title: '库区名称',
minWidth: 100,
},
{
field: 'areaName',
title: '库位名称',
minWidth: 100,
},
{
field: 'quantity',
title: '数量',
width: 100,
},
...(stockable
? [
{
title: '操作',
width: 120,
fixed: 'right',
slots: { default: 'actions' },
} as const,
]
: []),
];
}
/** 拣货明细新增/修改的表单 */
export function useDetailFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
return [
{
fieldName: 'quantityMax',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'batchId',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'itemId',
label: '物料',
component: markRaw(MdItemSelect),
componentProps: {
disabled: true,
},
},
{
fieldName: 'materialStockId',
label: '选择库存',
component: markRaw(WmMaterialStockSelect),
componentProps: {
// 选择库存记录后,自动回填仓库/库区/库位/批次/数量
onChange: async (stock?: MesWmMaterialStockApi.MaterialStock) => {
await formApi?.setValues({
areaId: stock?.areaId,
batchCode: stock?.batchCode,
batchId: stock?.batchId,
locationId: stock?.locationId,
quantity: stock?.quantity,
quantityMax: stock?.quantity,
warehouseId: stock?.warehouseId,
});
},
},
rules: 'selectRequired',
dependencies: {
triggerFields: ['itemId', 'batchId'],
componentProps: (values) => ({
batchId: values.batchId,
itemId: values.itemId,
}),
},
},
{
fieldName: 'quantity',
label: '数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
placeholder: '请输入数量',
precision: 2,
},
rules: 'required',
dependencies: {
triggerFields: ['quantityMax'],
componentProps: (values) => ({
class: '!w-full',
max: values.quantityMax,
min: 0,
placeholder: '请输入数量',
precision: 2,
}),
},
},
{
fieldName: 'warehouseId',
label: '出库仓库',
component: markRaw(WmWarehouseSelect),
componentProps: {
disabled: true,
},
},
{
fieldName: 'locationId',
label: '库区',
component: markRaw(WmWarehouseLocationSelect),
componentProps: {
disabled: true,
},
dependencies: {
triggerFields: ['warehouseId'],
componentProps: (values) => ({
disabled: true,
warehouseId: values.warehouseId,
}),
},
},
{
fieldName: 'areaId',
label: '库位',
component: markRaw(WmWarehouseAreaSelect),
componentProps: {
disabled: true,
},
dependencies: {
triggerFields: ['locationId'],
componentProps: (values) => ({
disabled: true,
locationId: values.locationId,
}),
},
},
{
fieldName: 'batchCode',
label: '批次号',
component: 'Input',
componentProps: {
disabled: true,
},
},
];
}

View File

@@ -0,0 +1,223 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmProductSalesApi } from '#/api/mes/wm/productsales';
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
cancelProductSales,
deleteProductSales,
exportProductSales,
getProductSalesPage,
} from '#/api/mes/wm/productsales';
import { $t } from '#/locales';
import { MesWmProductSalesStatusEnum } from '#/views/mes/utils/constants';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/** 刷新表格 */
function handleRefresh() {
gridApi.query();
}
/** 创建销售出库单 */
function handleCreate() {
formModalApi.setData({ formType: 'create' }).open();
}
/** 查看销售出库单 */
function handleDetail(row: MesWmProductSalesApi.ProductSales) {
formModalApi.setData({ formType: 'detail', id: row.id }).open();
}
/** 编辑销售出库单 */
function handleEdit(row: MesWmProductSalesApi.ProductSales) {
formModalApi.setData({ formType: 'update', id: row.id }).open();
}
/** 执行拣货 */
function handleStock(row: MesWmProductSalesApi.ProductSales) {
formModalApi.setData({ formType: 'stock', id: row.id }).open();
}
/** 填写运单 */
function handleShipping(row: MesWmProductSalesApi.ProductSales) {
formModalApi.setData({ formType: 'shipping', id: row.id }).open();
}
/** 执行出库 */
function handleFinish(row: MesWmProductSalesApi.ProductSales) {
formModalApi.setData({ formType: 'finish', id: row.id }).open();
}
/** 删除销售出库单 */
async function handleDelete(row: MesWmProductSalesApi.ProductSales) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.code]),
duration: 0,
});
try {
await deleteProductSales(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.code]));
handleRefresh();
} finally {
hideLoading();
}
}
/** 取消销售出库单 */
async function handleCancel(row: MesWmProductSalesApi.ProductSales) {
await cancelProductSales(row.id!);
message.success('取消成功');
handleRefresh();
}
/** 导出表格 */
async function handleExport() {
const data = await exportProductSales(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '销售出库单.xls', source: data });
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getProductSalesPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
search: true,
},
} as VxeTableGridOptions<MesWmProductSalesApi.ProductSales>,
});
</script>
<template>
<Page auto-content-height>
<template #doc>
<DocAlert
title="【仓库】发货通知、销售出库、销售退货"
url="https://doc.iocoder.cn/mes/wm/sales-out/"
/>
</template>
<FormModal @success="handleRefresh" />
<Grid table-title="销售出库单列表">
<template #toolbar-tools>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['销售出库单']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['mes:wm-product-sales:create'],
onClick: handleCreate,
},
{
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
auth: ['mes:wm-product-sales:export'],
onClick: handleExport,
},
]"
/>
</template>
<template #code="{ row }">
<Button type="link" @click="handleDetail(row)">
{{ row.code }}
</Button>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['mes:wm-product-sales:update'],
ifShow: row.status === MesWmProductSalesStatusEnum.PREPARE,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['mes:wm-product-sales:delete'],
ifShow: row.status === MesWmProductSalesStatusEnum.PREPARE,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.code]),
confirm: handleDelete.bind(null, row),
},
},
{
label: '拣货',
type: 'link',
auth: ['mes:wm-product-sales:stock'],
ifShow: row.status === MesWmProductSalesStatusEnum.APPROVING,
onClick: handleStock.bind(null, row),
},
{
label: '填写运单',
type: 'link',
auth: ['mes:wm-product-sales:shipping'],
ifShow: row.status === MesWmProductSalesStatusEnum.SHIPPING,
onClick: handleShipping.bind(null, row),
},
{
label: '执行出库',
type: 'link',
auth: ['mes:wm-product-sales:finish'],
ifShow: row.status === MesWmProductSalesStatusEnum.APPROVED,
onClick: handleFinish.bind(null, row),
},
{
label: '取消',
type: 'link',
danger: true,
auth: ['mes:wm-product-sales:cancel'],
ifShow:
row.status === MesWmProductSalesStatusEnum.CONFIRMED ||
row.status === MesWmProductSalesStatusEnum.APPROVING ||
row.status === MesWmProductSalesStatusEnum.SHIPPING ||
row.status === MesWmProductSalesStatusEnum.APPROVED,
popConfirm: {
title: '确认取消该销售出库单?取消后不可恢复。',
confirm: handleCancel.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,110 @@
<script lang="ts" setup>
import type { MesWmProductSalesDetailApi } from '#/api/mes/wm/productsales/detail';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
createProductSalesDetail,
getProductSalesDetail,
updateProductSalesDetail,
} from '#/api/mes/wm/productsales/detail';
import { $t } from '#/locales';
import { useDetailFormSchema } from '../data';
const emit = defineEmits<{ success: [lineId: number] }>();
const formData = ref<MesWmProductSalesDetailApi.ProductSalesDetail>();
const salesId = ref<number>(); // 所属出库单编号
const lineId = ref<number>(); // 所属出库单行编号
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['拣货明细'])
: $t('ui.actionTitle.create', ['拣货明细']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-1',
labelWidth: 90,
},
layout: 'horizontal',
schema: useDetailFormSchema(),
showDefaultActions: false,
wrapperClass: 'grid-cols-3',
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data =
(await formApi.getValues()) as MesWmProductSalesDetailApi.ProductSalesDetail;
data.salesId = salesId.value;
data.lineId = lineId.value;
try {
await (formData.value?.id
? updateProductSalesDetail({ ...data, id: formData.value.id })
: createProductSalesDetail(data));
// 关闭并提示
await modalApi.close();
emit('success', lineId.value!);
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
formApi.setState({ schema: useDetailFormSchema(formApi) });
// 加载数据
const data = modalApi.getData<{
batchId?: number;
detailId?: number;
itemId?: number;
lineId: number;
salesId: number;
}>();
salesId.value = data.salesId;
lineId.value = data.lineId;
if (data.detailId) {
modalApi.lock();
try {
formData.value = await getProductSalesDetail(data.detailId);
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
} else {
if (data.itemId) {
await formApi.setFieldValue('itemId', data.itemId);
}
if (data.batchId) {
await formApi.setFieldValue('batchId', data.batchId);
}
}
},
});
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" />
</Modal>
</template>

View File

@@ -0,0 +1,103 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmProductSalesDetailApi } from '#/api/mes/wm/productsales/detail';
import { computed, watch } from 'vue';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteProductSalesDetail } from '#/api/mes/wm/productsales/detail';
import { $t } from '#/locales';
import { type FormType, useDetailGridColumns } from '../data';
const props = defineProps<{
details: MesWmProductSalesDetailApi.ProductSalesDetail[];
formType: FormType;
}>();
const emit = defineEmits<{
edit: [detailId: number];
refresh: [];
}>();
const isStock = computed(() => props.formType === 'stock'); // 是否为拣货模式
/** 编辑拣货明细 */
function handleEdit(row: MesWmProductSalesDetailApi.ProductSalesDetail) {
emit('edit', row.id!);
}
/** 删除拣货明细 */
async function handleDelete(
row: MesWmProductSalesDetailApi.ProductSalesDetail,
) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.warehouseName]),
duration: 0,
});
try {
await deleteProductSalesDetail(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.warehouseName]));
emit('refresh');
} finally {
hideLoading();
}
}
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
border: true,
columns: useDetailGridColumns(isStock.value),
data: props.details,
pagerConfig: {
enabled: false,
},
rowConfig: {
keyField: 'id',
isHover: true,
},
size: 'small',
toolbarConfig: {
enabled: false,
},
} as VxeTableGridOptions<MesWmProductSalesDetailApi.ProductSalesDetail>,
});
watch(
() => props.details,
(details) => gridApi.setGridOptions({ data: details }),
);
</script>
<template>
<div class="px-4 py-2">
<Grid>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [
row.warehouseName,
]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</div>
</template>

View File

@@ -0,0 +1,266 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { MesWmProductSalesApi } from '#/api/mes/wm/productsales';
import { computed, ref } from 'vue';
import { confirm, useVbenModal } from '@vben/common-ui';
import { Button, Divider, message, Popconfirm } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
checkProductSalesQuantity,
createProductSales,
finishProductSales,
getProductSales,
shippingProductSales,
stockProductSales,
submitProductSales,
updateProductSales,
} from '#/api/mes/wm/productsales';
import { $t } from '#/locales';
import { MesWmProductSalesStatusEnum } from '#/views/mes/utils/constants';
import { useFormSchema } from '../data';
import LineList from './line-list.vue';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const formData = ref<MesWmProductSalesApi.ProductSales>();
const originalSnapshot = ref(''); // 表单原始数据快照,用于提交时跳过未变更的保存请求
const isEditable = computed(() => // 是否为编辑模式(可保存)
['create', 'update'].includes(formType.value),
);
const isStock = computed(() => formType.value === 'stock'); // 是否为拣货模式
const isShipping = computed(() => formType.value === 'shipping'); // 是否为填写运单模式
const isFinish = computed(() => formType.value === 'finish'); // 是否为执行出库模式
const canSubmit = computed(() => // 是否可提交
formType.value === 'update' &&
formData.value?.status === MesWmProductSalesStatusEnum.PREPARE,
);
const getTitle = computed(() => {
if (formType.value === 'detail') {
return $t('ui.actionTitle.view', ['销售出库单']);
}
if (formType.value === 'stock') {
return '执行拣货';
}
if (formType.value === 'shipping') {
return '填写运单';
}
if (formType.value === 'finish') {
return '执行出库';
}
return formType.value === 'update'
? $t('ui.actionTitle.edit', ['销售出库单'])
: $t('ui.actionTitle.create', ['销售出库单']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-1',
labelWidth: 110,
},
layout: 'horizontal',
schema: [],
showDefaultActions: false,
wrapperClass: 'grid-cols-3',
});
/** 提交出库单:表单有修改时先保存,再调用提交接口 */
async function handleSubmit() {
const { valid } = await formApi.validate();
if (!valid || !formData.value?.id) {
return;
}
modalApi.lock();
try {
const current = JSON.stringify(await formApi.getValues());
if (current !== originalSnapshot.value) {
const data =
(await formApi.getValues()) as MesWmProductSalesApi.ProductSales;
await updateProductSales({ ...formData.value, ...data });
originalSnapshot.value = current;
}
await submitProductSales(formData.value.id);
message.success('提交成功');
await modalApi.close();
emit('success');
} finally {
modalApi.unlock();
}
}
/** 执行拣货:出库数量与拣货数量不一致时二次确认 */
async function handleStock() {
if (!formData.value?.id) {
return;
}
const quantityMatch = await checkProductSalesQuantity(formData.value.id);
if (!quantityMatch) {
try {
await confirm('出库数量与拣货数量不一致,确认执行拣货?');
} catch {
return;
}
}
modalApi.lock();
try {
await stockProductSales(formData.value.id);
message.success('拣货成功');
await modalApi.close();
emit('success');
} finally {
modalApi.unlock();
}
}
/** 填写运单 */
async function handleShipping() {
if (!formData.value?.id) {
return;
}
const values = (await formApi.getValues()) as MesWmProductSalesApi.ProductSales;
modalApi.lock();
try {
await shippingProductSales({
carrier: values.carrier,
id: formData.value.id,
shippingNumber: values.shippingNumber,
});
message.success('运单信息填写成功');
await modalApi.close();
emit('success');
} finally {
modalApi.unlock();
}
}
/** 执行出库 */
async function handleFinish() {
if (!formData.value?.id) {
return;
}
modalApi.lock();
try {
await finishProductSales(formData.value.id);
message.success('出库成功');
await modalApi.close();
emit('success');
} finally {
modalApi.unlock();
}
}
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
if (!isEditable.value) {
await modalApi.close();
return;
}
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data =
(await formApi.getValues()) as MesWmProductSalesApi.ProductSales;
try {
if (formData.value?.id) {
await updateProductSales({ ...formData.value, ...data });
formData.value = { ...formData.value, ...data };
} else {
const id = await createProductSales(data);
formData.value = {
...data,
id,
status: MesWmProductSalesStatusEnum.PREPARE,
};
await formApi.setFieldValue('id', id);
await formApi.setFieldValue('status', formData.value.status);
formType.value = 'update';
}
originalSnapshot.value = JSON.stringify(await formApi.getValues());
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
originalSnapshot.value = '';
return;
}
// 加载数据
const data = modalApi.getData<{ formType: FormType; id?: number }>();
formType.value = data.formType;
formApi.setState({ schema: useFormSchema(formType.value, formApi) });
formApi.setDisabled(!isEditable.value && !isShipping.value);
modalApi.setState({ showConfirmButton: isEditable.value });
if (data?.id) {
modalApi.lock();
try {
formData.value = await getProductSales(data.id);
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
}
originalSnapshot.value = JSON.stringify(await formApi.getValues());
},
});
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" />
<!-- 非新建模式展示物料信息 -->
<template v-if="formData?.id">
<Divider>物料信息</Divider>
<div class="mx-4">
<LineList
:form-type="formType"
:notice-id="formData.noticeId"
:sales-id="formData.id"
/>
</div>
</template>
<template #prepend-footer>
<div class="flex flex-auto items-center gap-2">
<Popconfirm
v-if="canSubmit"
title="确认提交该销售出库单?【提交后将不能修改】"
@confirm="handleSubmit"
>
<Button type="primary">提交</Button>
</Popconfirm>
<Button v-if="isStock" type="primary" @click="handleStock">
执行拣货
</Button>
<Popconfirm
v-if="isShipping"
title="确认提交运单信息?"
@confirm="handleShipping"
>
<Button type="primary">确认填写</Button>
</Popconfirm>
<Popconfirm
v-if="isFinish"
title="确认执行出库?执行后将扣减库存。"
@confirm="handleFinish"
>
<Button type="primary">确认出库</Button>
</Popconfirm>
</div>
</template>
</Modal>
</template>

View File

@@ -0,0 +1,104 @@
<script lang="ts" setup>
import type { MesWmProductSalesLineApi } from '#/api/mes/wm/productsales/line';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
createProductSalesLine,
getProductSalesLine,
updateProductSalesLine,
} from '#/api/mes/wm/productsales/line';
import { $t } from '#/locales';
import { useLineFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<MesWmProductSalesLineApi.ProductSalesLine>();
const salesId = ref<number>(); // 所属出库单编号
const noticeId = ref<number>(); // 所属发货通知单编号
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['销售出库单行'])
: $t('ui.actionTitle.create', ['销售出库单行']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-1',
labelWidth: 110,
},
layout: 'horizontal',
schema: [],
showDefaultActions: false,
wrapperClass: 'grid-cols-3',
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data =
(await formApi.getValues()) as MesWmProductSalesLineApi.ProductSalesLine;
data.salesId = salesId.value;
try {
await (formData.value?.id
? updateProductSalesLine({ ...data, id: formData.value.id })
: createProductSalesLine(data));
// 关闭并提示
await modalApi.close();
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
// 加载数据
const data = modalApi.getData<{
id?: number;
noticeId?: number;
salesId: number;
}>();
salesId.value = data.salesId;
noticeId.value = data.noticeId;
formApi.setState({ schema: useLineFormSchema(!!data.noticeId, formApi) });
if (data.noticeId) {
await formApi.setFieldValue('noticeId', data.noticeId);
}
if (!data.id) {
return;
}
modalApi.lock();
try {
formData.value = await getProductSalesLine(data.id);
// 设置到 values
await formApi.setValues({ ...formData.value, noticeId: noticeId.value });
} finally {
modalApi.unlock();
}
},
});
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" />
</Modal>
</template>

View File

@@ -0,0 +1,233 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmProductSalesDetailApi } from '#/api/mes/wm/productsales/detail';
import type { MesWmProductSalesLineApi } from '#/api/mes/wm/productsales/line';
import { computed, reactive } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getProductSalesDetailListByLineId } from '#/api/mes/wm/productsales/detail';
import {
deleteProductSalesLine,
getProductSalesLinePage,
} from '#/api/mes/wm/productsales/line';
import { $t } from '#/locales';
import { type FormType, useLineGridColumns } from '../data';
import DetailForm from './detail-form.vue';
import DetailList from './detail-list.vue';
import LineForm from './line-form.vue';
const props = defineProps<{
formType: FormType;
noticeId?: number;
salesId: number;
}>();
const isEditable = computed(() => // 是否可编辑明细行
['create', 'update'].includes(props.formType),
);
const isStock = computed(() => props.formType === 'stock'); // 是否为拣货模式
const detailMap = reactive<
Record<number, MesWmProductSalesDetailApi.ProductSalesDetail[]>
>({}); // 已展开行的拣货明细缓存
const [LineFormModal, lineFormModalApi] = useVbenModal({
connectedComponent: LineForm,
destroyOnClose: true,
});
const [DetailFormModal, detailFormModalApi] = useVbenModal({
connectedComponent: DetailForm,
destroyOnClose: true,
});
/** 刷新表格 */
function handleRefresh() {
for (const id of Object.keys(detailMap)) {
delete detailMap[Number(id)];
}
gridApi.query();
}
/** 添加物料 */
function handleCreate() {
lineFormModalApi
.setData({ noticeId: props.noticeId, salesId: props.salesId })
.open();
}
/** 编辑物料 */
function handleEdit(row: MesWmProductSalesLineApi.ProductSalesLine) {
lineFormModalApi
.setData({ id: row.id, noticeId: props.noticeId, salesId: props.salesId })
.open();
}
/** 删除物料 */
async function handleDelete(row: MesWmProductSalesLineApi.ProductSalesLine) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.itemName]),
duration: 0,
});
try {
await deleteProductSalesLine(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.itemName]));
handleRefresh();
} finally {
hideLoading();
}
}
/** 拣货:直接打开明细创建表单 */
function handlePicking(row: MesWmProductSalesLineApi.ProductSalesLine) {
openDetailForm(row.id!, row.itemId, row.batchId);
}
/** 打开拣货明细表单 */
function openDetailForm(
lineId: number,
itemId?: number,
batchId?: number,
detailId?: number,
) {
detailFormModalApi
.setData({ batchId, detailId, itemId, lineId, salesId: props.salesId })
.open();
}
/** 获取已展开行的拣货明细 */
function getExpandedDetails(row: MesWmProductSalesLineApi.ProductSalesLine) {
return detailMap[row.id!] || [];
}
/** 加载指定行的拣货明细 */
async function loadLineDetails(lineId: number) {
detailMap[lineId] = await getProductSalesDetailListByLineId(lineId);
}
/** 展开行时懒加载拣货明细 */
async function handleExpandChange(
row: MesWmProductSalesLineApi.ProductSalesLine,
expanded: boolean,
) {
if (!expanded) {
return;
}
await loadLineDetails(row.id!);
}
/** 明细表单提交成功后,刷新对应行已展开的明细 */
async function handleDetailSuccess(lineId: number) {
await loadLineDetails(lineId);
}
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useLineGridColumns(isEditable.value, isStock.value),
expandConfig: {
padding: true,
},
height: 400,
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }) => {
if (!props.salesId) {
return { list: [], total: 0 };
}
return await getProductSalesLinePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
salesId: props.salesId,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
},
} as VxeTableGridOptions<MesWmProductSalesLineApi.ProductSalesLine>,
gridEvents: {
toggleRowExpand: ({
expanded,
row,
}: {
expanded: boolean;
row: MesWmProductSalesLineApi.ProductSalesLine;
}) => {
handleExpandChange(row, expanded);
},
},
});
</script>
<template>
<div>
<LineFormModal @success="handleRefresh" />
<DetailFormModal @success="handleDetailSuccess" />
<Grid table-title="物料信息">
<template v-if="isEditable" #toolbar-tools>
<TableAction
:actions="[
{
label: '添加物料',
type: 'primary',
icon: ACTION_ICON.ADD,
onClick: handleCreate,
},
]"
/>
</template>
<template #detail="{ row }">
<DetailList
:details="getExpandedDetails(row)"
:form-type="formType"
@edit="
(detailId) =>
openDetailForm(row.id!, row.itemId, row.batchId, detailId)
"
@refresh="loadLineDetails(row.id!)"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
ifShow: isEditable,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
ifShow: isEditable,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.itemName]),
confirm: handleDelete.bind(null, row),
},
},
{
label: '拣货',
type: 'link',
ifShow: isStock,
onClick: handlePicking.bind(null, row),
},
]"
/>
</template>
</Grid>
</div>
</template>

View File

@@ -0,0 +1,2 @@
export { default as WmSalesNoticeLineSelect } from './wm-sales-notice-line-select.vue';
export { default as WmSalesNoticeSelect } from './wm-sales-notice-select.vue';

View File

@@ -0,0 +1,198 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmSalesNoticeLineApi } from '#/api/mes/wm/salesnotice/line';
import { nextTick, ref } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { message, Modal } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getSalesNoticeLinePage } from '#/api/mes/wm/salesnotice/line';
const emit = defineEmits<{
selected: [rows: MesWmSalesNoticeLineApi.SalesNoticeLine[]];
}>();
const open = ref(false); // 弹窗是否打开
const noticeId = ref<number>(); // 所属通知单编号
const syncingSingleSelection = ref(false); // 是否同步单选勾选状态
const selectedRows = ref<MesWmSalesNoticeLineApi.SalesNoticeLine[]>([]); // 已选行列表
const preSelectedIds = ref<number[]>([]); // 预选行编号列表
/** 表格字段 */
function useGridColumns(): VxeTableGridOptions<MesWmSalesNoticeLineApi.SalesNoticeLine>['columns'] {
return [
{
type: 'checkbox',
width: 50,
},
{
field: 'itemCode',
title: '物料编码',
minWidth: 120,
},
{
field: 'itemName',
title: '物料名称',
minWidth: 140,
},
{
field: 'specification',
title: '规格型号',
minWidth: 120,
},
{
field: 'unitMeasureName',
title: '单位',
width: 80,
},
{
field: 'quantity',
title: '发货数量',
width: 100,
},
{
field: 'batchCode',
title: '批次号',
minWidth: 120,
},
{
field: 'oqcCheckFlag',
title: '是否检验',
width: 90,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
},
},
];
}
/** 单选模式下同步 VXE 勾选状态 */
async function syncSingleSelection(
row?: MesWmSalesNoticeLineApi.SalesNoticeLine,
) {
syncingSingleSelection.value = true;
await nextTick();
await gridApi.grid.clearCheckboxRow();
if (row) {
await gridApi.grid.setCheckboxRow(row, true);
}
await nextTick();
syncingSingleSelection.value = false;
}
/** 处理勾选变化,只保留最后一条 */
async function handleCheckboxChange({
checked,
row,
}: {
checked: boolean;
row?: MesWmSalesNoticeLineApi.SalesNoticeLine;
}) {
if (syncingSingleSelection.value) {
return;
}
const selected = checked && row ? [row] : [];
selectedRows.value = selected;
await syncSingleSelection(selected[0]);
}
/** 回显预选行 */
function applyPreSelection() {
if (preSelectedIds.value.length === 0) {
return;
}
const rows =
gridApi.grid.getData() as MesWmSalesNoticeLineApi.SalesNoticeLine[];
for (const row of rows) {
if (row.id && preSelectedIds.value.includes(row.id)) {
gridApi.grid.setCheckboxRow(row, true);
selectedRows.value = [row];
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useGridColumns(),
height: 460,
keepSource: true,
checkboxConfig: {
highlight: true,
reserve: true,
},
proxyConfig: {
ajax: {
query: async ({ page }) => {
if (!noticeId.value) {
return { list: [], total: 0 };
}
return await getSalesNoticeLinePage({
noticeId: noticeId.value,
pageNo: page.currentPage,
pageSize: page.pageSize,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
},
} as VxeTableGridOptions<MesWmSalesNoticeLineApi.SalesNoticeLine>,
gridEvents: {
checkboxChange: handleCheckboxChange,
},
});
/** 打开行选择弹窗 */
async function openModal(id: number | undefined, selectedIds?: number[]) {
open.value = true;
noticeId.value = id;
preSelectedIds.value = selectedIds || [];
selectedRows.value = [];
await nextTick();
await gridApi.grid.clearCheckboxRow();
await gridApi.query();
await nextTick();
applyPreSelection();
}
/** 关闭弹窗 */
async function closeModal() {
open.value = false;
selectedRows.value = [];
await gridApi.grid.clearCheckboxRow();
}
/** 确认选择行 */
function handleConfirm() {
if (selectedRows.value.length === 0) {
message.warning('请选择一条数据');
return;
}
emit('selected', [selectedRows.value[0]!]);
open.value = false;
}
defineExpose({ open: openModal });
</script>
<template>
<Modal
v-model:open="open"
title="发货通知单行选择"
width="70%"
:destroy-on-close="true"
@cancel="closeModal"
@ok="handleConfirm"
>
<Grid table-title="发货通知单行列表" />
</Modal>
</template>

View File

@@ -0,0 +1,151 @@
<script lang="ts" setup>
import type { MesWmSalesNoticeLineApi } from '#/api/mes/wm/salesnotice/line';
import { computed, ref, useAttrs, watch } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { Input, Tooltip } from 'ant-design-vue';
import { getSalesNoticeLine } from '#/api/mes/wm/salesnotice/line';
import WmSalesNoticeLineSelectDialog from './wm-sales-notice-line-select-dialog.vue';
defineOptions({ name: 'WmSalesNoticeLineSelect', inheritAttrs: false });
const props = withDefaults(
defineProps<{
allowClear?: boolean;
disabled?: boolean;
modelValue?: number;
noticeId?: number; // 所属发货通知单编号
placeholder?: string;
}>(),
{
allowClear: true,
disabled: false,
modelValue: undefined,
noticeId: undefined,
placeholder: '请选择发货通知单行',
},
);
const emit = defineEmits<{
change: [item: MesWmSalesNoticeLineApi.SalesNoticeLine | undefined];
'update:modelValue': [value: number | undefined];
}>();
const attrs = useAttrs();
const dialogRef = ref<InstanceType<typeof WmSalesNoticeLineSelectDialog>>();
const hovering = ref(false);
const selectedItem = ref<MesWmSalesNoticeLineApi.SalesNoticeLine>();
const displayLabel = computed(() => {
const item = selectedItem.value;
if (!item) {
return '';
}
return `${item.itemCode ?? ''} - ${item.itemName ?? ''}`;
});
const showClear = computed(
() =>
props.allowClear &&
!props.disabled &&
hovering.value &&
props.modelValue != null,
);
/** 根据编号单条查询行信息(用于编辑回显) */
async function resolveItemById(id: number | undefined) {
if (id == null) {
selectedItem.value = undefined;
return;
}
if (selectedItem.value?.id === id) {
return;
}
selectedItem.value = await getSalesNoticeLine(id);
}
watch(() => props.modelValue, resolveItemById, { immediate: true });
/** noticeId 变化时清空选中(关联的行已失效) */
watch(
() => props.noticeId,
() => {
selectedItem.value = undefined;
emit('update:modelValue', undefined);
emit('change', undefined);
},
);
/** 清空已选行 */
function clearSelected() {
selectedItem.value = undefined;
emit('update:modelValue', undefined);
emit('change', undefined);
}
/** 打开行选择弹窗 */
function handleClick(event: MouseEvent) {
if (props.disabled || !props.noticeId) {
return;
}
const target = event.target as HTMLElement;
if (showClear.value && target.closest('.ant-input-suffix')) {
event.stopPropagation();
clearSelected();
return;
}
const selectedIds = props.modelValue == null ? [] : [props.modelValue];
dialogRef.value?.open(props.noticeId, selectedIds);
}
/** 弹窗选中回调 */
function handleSelected(rows: MesWmSalesNoticeLineApi.SalesNoticeLine[]) {
const item = rows[0];
if (!item) {
return;
}
selectedItem.value = item;
emit('update:modelValue', item.id);
emit('change', item);
}
</script>
<template>
<div
v-bind="attrs"
class="w-full"
:class="disabled ? 'cursor-not-allowed' : 'cursor-pointer'"
@click="handleClick"
@mouseenter="hovering = true"
@mouseleave="hovering = false"
>
<Tooltip :mouse-enter-delay="0.5" :open="selectedItem ? undefined : false">
<template #title>
<div v-if="selectedItem" class="leading-6">
<div>物料编码{{ selectedItem.itemCode || '-' }}</div>
<div>物料名称{{ selectedItem.itemName || '-' }}</div>
<div>规格型号{{ selectedItem.specification || '-' }}</div>
<div>发货数量{{ selectedItem.quantity ?? '-' }}</div>
</div>
</template>
<Input
:disabled="disabled"
:placeholder="placeholder"
:value="displayLabel"
readonly
>
<template #suffix>
<IconifyIcon
class="size-4"
:icon="showClear ? 'lucide:circle-x' : 'lucide:search'"
/>
</template>
</Input>
</Tooltip>
</div>
<WmSalesNoticeLineSelectDialog ref="dialogRef" @selected="handleSelected" />
</template>

View File

@@ -0,0 +1,262 @@
<script lang="ts" setup>
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmSalesNoticeApi } from '#/api/mes/wm/salesnotice';
import { nextTick, ref } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { message, Modal } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getSalesNoticePage } from '#/api/mes/wm/salesnotice';
const emit = defineEmits<{
selected: [rows: MesWmSalesNoticeApi.SalesNotice[]];
}>();
const open = ref(false); // 弹窗是否打开
const multiple = ref(false); // 是否多选
const fixedStatus = ref<number>(); // 固定状态筛选
const syncingSingleSelection = ref(false); // 是否同步单选勾选状态
const selectedRows = ref<MesWmSalesNoticeApi.SalesNotice[]>([]); // 已选通知单列表
const preSelectedIds = ref<number[]>([]); // 预选通知单编号列表
/** 搜索表单 */
function useSearchSchema(): VbenFormSchema[] {
return [
{
fieldName: 'code',
label: '通知单编号',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入通知单编号',
},
},
{
fieldName: 'name',
label: '通知单名称',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入通知单名称',
},
},
{
fieldName: 'salesOrderCode',
label: '销售订单编号',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入销售订单编号',
},
},
];
}
/** 表格字段 */
function useGridColumns(): VxeTableGridOptions<MesWmSalesNoticeApi.SalesNotice>['columns'] {
return [
{
type: 'checkbox',
width: 50,
},
{
field: 'code',
title: '通知单编号',
minWidth: 160,
},
{
field: 'name',
title: '通知单名称',
minWidth: 150,
},
{
field: 'salesOrderCode',
title: '销售订单编号',
minWidth: 140,
},
{
field: 'clientName',
title: '客户名称',
minWidth: 120,
},
{
field: 'salesDate',
title: '发货日期',
width: 120,
formatter: 'formatDate',
},
{
field: 'status',
title: '单据状态',
width: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.MES_WM_SALES_NOTICE_STATUS },
},
},
];
}
/** 单选模式下同步 VXE 勾选状态,避免跨页残留多选 */
async function syncSingleSelection(row?: MesWmSalesNoticeApi.SalesNotice) {
syncingSingleSelection.value = true;
await nextTick();
await gridApi.grid.clearCheckboxRow();
if (row) {
await gridApi.grid.setCheckboxRow(row, true);
}
await nextTick();
syncingSingleSelection.value = false;
}
/** 处理勾选变化,单选模式只保留最后一条 */
async function handleCheckboxChange({
checked,
records,
row,
}: {
checked: boolean;
records: MesWmSalesNoticeApi.SalesNotice[];
row?: MesWmSalesNoticeApi.SalesNotice;
}) {
if (syncingSingleSelection.value) {
return;
}
if (!multiple.value) {
const selected = checked && row ? [row] : [];
selectedRows.value = selected;
await syncSingleSelection(selected[0]);
return;
}
selectedRows.value = records;
}
/** 处理全选变化 */
function handleCheckboxAll({
records,
}: {
records: MesWmSalesNoticeApi.SalesNotice[];
}) {
if (syncingSingleSelection.value) {
return;
}
selectedRows.value = records;
}
/** 回显预选通知单 */
function applyPreSelection() {
if (preSelectedIds.value.length === 0) {
return;
}
const rows = gridApi.grid.getData() as MesWmSalesNoticeApi.SalesNotice[];
for (const row of rows) {
if (row.id && preSelectedIds.value.includes(row.id)) {
gridApi.grid.setCheckboxRow(row, true);
if (!multiple.value) {
selectedRows.value = [row];
}
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useSearchSchema(),
},
gridOptions: {
columns: useGridColumns(),
height: 520,
keepSource: true,
checkboxConfig: {
highlight: true,
range: true,
reserve: true,
},
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getSalesNoticePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
status: fixedStatus.value,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
search: true,
},
} as VxeTableGridOptions<MesWmSalesNoticeApi.SalesNotice>,
gridEvents: {
checkboxAll: handleCheckboxAll,
checkboxChange: handleCheckboxChange,
},
});
/** 重置查询和选择状态 */
async function resetQueryState() {
selectedRows.value = [];
await gridApi.grid.clearCheckboxRow();
await gridApi.formApi.resetForm();
}
/** 打开通知单选择弹窗 */
async function openModal(
selectedIds?: number[],
options?: { multiple?: boolean; status?: number },
) {
open.value = true;
multiple.value = options?.multiple ?? false;
fixedStatus.value = options?.status;
preSelectedIds.value = selectedIds || [];
await nextTick();
await resetQueryState();
await gridApi.query();
await nextTick();
applyPreSelection();
}
/** 关闭通知单选择弹窗 */
async function closeModal() {
open.value = false;
await resetQueryState();
}
/** 确认选择通知单 */
function handleConfirm() {
if (selectedRows.value.length === 0) {
message.warning(multiple.value ? '请至少选择一条数据' : '请选择一条数据');
return;
}
emit(
'selected',
multiple.value ? selectedRows.value : [selectedRows.value[0]!],
);
open.value = false;
}
defineExpose({ open: openModal });
</script>
<template>
<Modal
v-model:open="open"
title="发货通知单选择"
width="70%"
:destroy-on-close="true"
@cancel="closeModal"
@ok="handleConfirm"
>
<Grid table-title="发货通知单列表" />
</Modal>
</template>

View File

@@ -0,0 +1,138 @@
<script lang="ts" setup>
import type { MesWmSalesNoticeApi } from '#/api/mes/wm/salesnotice';
import { computed, ref, useAttrs, watch } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { Input, Tooltip } from 'ant-design-vue';
import { getSalesNotice } from '#/api/mes/wm/salesnotice';
import WmSalesNoticeSelectDialog from './wm-sales-notice-select-dialog.vue';
defineOptions({ name: 'WmSalesNoticeSelect', inheritAttrs: false });
const props = withDefaults(
defineProps<{
allowClear?: boolean;
disabled?: boolean;
modelValue?: number;
placeholder?: string;
status?: number; // 固定状态筛选
}>(),
{
allowClear: true,
disabled: false,
modelValue: undefined,
placeholder: '请选择发货通知单',
status: undefined,
},
);
const emit = defineEmits<{
change: [item: MesWmSalesNoticeApi.SalesNotice | undefined];
'update:modelValue': [value: number | undefined];
}>();
const attrs = useAttrs();
const dialogRef = ref<InstanceType<typeof WmSalesNoticeSelectDialog>>();
const hovering = ref(false);
const selectedItem = ref<MesWmSalesNoticeApi.SalesNotice>();
const displayLabel = computed(() => selectedItem.value?.name ?? '');
const showClear = computed(
() =>
props.allowClear &&
!props.disabled &&
hovering.value &&
props.modelValue != null,
);
/** 根据编号单条查询通知单信息(用于编辑回显) */
async function resolveItemById(id: number | undefined) {
if (id == null) {
selectedItem.value = undefined;
return;
}
if (selectedItem.value?.id === id) {
return;
}
selectedItem.value = await getSalesNotice(id);
}
watch(() => props.modelValue, resolveItemById, { immediate: true });
/** 清空已选通知单 */
function clearSelected() {
selectedItem.value = undefined;
emit('update:modelValue', undefined);
emit('change', undefined);
}
/** 打开通知单选择弹窗 */
function handleClick(event: MouseEvent) {
if (props.disabled) {
return;
}
const target = event.target as HTMLElement;
if (showClear.value && target.closest('.ant-input-suffix')) {
event.stopPropagation();
clearSelected();
return;
}
const selectedIds = props.modelValue == null ? [] : [props.modelValue];
dialogRef.value?.open(selectedIds, {
multiple: false,
status: props.status,
});
}
/** 弹窗选中回调 */
function handleSelected(rows: MesWmSalesNoticeApi.SalesNotice[]) {
const item = rows[0];
if (!item) {
return;
}
selectedItem.value = item;
emit('update:modelValue', item.id);
emit('change', item);
}
</script>
<template>
<div
v-bind="attrs"
class="w-full"
:class="disabled ? 'cursor-not-allowed' : 'cursor-pointer'"
@click="handleClick"
@mouseenter="hovering = true"
@mouseleave="hovering = false"
>
<Tooltip :mouse-enter-delay="0.5" :open="selectedItem ? undefined : false">
<template #title>
<div v-if="selectedItem" class="leading-6">
<div>编号{{ selectedItem.code || '-' }}</div>
<div>名称{{ selectedItem.name || '-' }}</div>
<div>客户{{ selectedItem.clientName || '-' }}</div>
<div>销售订单{{ selectedItem.salesOrderCode || '-' }}</div>
</div>
</template>
<Input
:disabled="disabled"
:placeholder="placeholder"
:value="displayLabel"
readonly
>
<template #suffix>
<IconifyIcon
class="size-4"
:icon="showClear ? 'lucide:circle-x' : 'lucide:search'"
/>
</template>
</Input>
</Tooltip>
</div>
<WmSalesNoticeSelectDialog ref="dialogRef" @selected="handleSelected" />
</template>

View File

@@ -0,0 +1,363 @@
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmSalesNoticeApi } from '#/api/mes/wm/salesnotice';
import type { MesWmSalesNoticeLineApi } from '#/api/mes/wm/salesnotice/line';
import { h, markRaw } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { Button } from 'ant-design-vue';
import { z } from '#/adapter/form';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import MdClientSelect from '#/views/mes/md/client/components/md-client-select.vue';
import MdItemSelect from '#/views/mes/md/item/components/md-item-select.vue';
import { MesAutoCodeRuleCode } from '#/views/mes/utils/constants';
/** 表单类型 */
export type FormType = 'create' | 'detail' | 'finish' | 'update';
/** 表单头部是否只读(详情、执行出库态) */
function isHeaderReadonly(formType: FormType): boolean {
return formType === 'detail' || formType === 'finish';
}
/** 新增/修改的表单 */
export function useFormSchema(
formType: FormType,
formApi?: VbenFormApi,
): VbenFormSchema[] {
return [
{
fieldName: 'id',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'status',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'code',
label: '通知单编号',
component: 'Input',
componentProps: {
placeholder: '请输入通知单编号',
},
rules: 'required',
suffix: isHeaderReadonly(formType)
? undefined
: () =>
h(
Button,
{
type: 'default',
onClick: async () => {
const code = await generateAutoCode(
MesAutoCodeRuleCode.WM_SALES_NOTICE_CODE,
);
await formApi?.setFieldValue('code', code);
},
},
{ default: () => '生成' },
),
},
{
fieldName: 'name',
label: '通知单名称',
component: 'Input',
componentProps: {
placeholder: '请输入通知单名称',
},
rules: 'required',
},
{
fieldName: 'salesOrderCode',
label: '销售订单编号',
component: 'Input',
componentProps: {
placeholder: '请输入销售订单编号',
},
},
{
fieldName: 'clientId',
label: '客户',
component: markRaw(MdClientSelect),
componentProps: {
placeholder: '请选择客户',
},
rules: 'selectRequired',
},
{
fieldName: 'salesDate',
label: '发货日期',
component: 'DatePicker',
componentProps: {
class: '!w-full',
format: 'YYYY-MM-DD',
placeholder: '请选择发货日期',
valueFormat: 'x',
},
rules: 'required',
},
{
fieldName: 'recipientName',
label: '收货人',
component: 'Input',
componentProps: {
placeholder: '请输入收货人',
},
},
{
fieldName: 'recipientTelephone',
label: '联系方式',
component: 'Input',
componentProps: {
placeholder: '请输入联系方式',
},
},
{
fieldName: 'recipientAddress',
label: '收货地址',
component: 'Input',
componentProps: {
placeholder: '请输入收货地址',
},
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
formItemClass: 'col-span-3',
componentProps: {
placeholder: '请输入备注',
rows: 3,
},
},
];
}
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'code',
label: '通知单编号',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入通知单编号',
},
},
{
fieldName: 'name',
label: '通知单名称',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入通知单名称',
},
},
{
fieldName: 'salesOrderCode',
label: '销售订单编号',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入销售订单编号',
},
},
{
fieldName: 'clientId',
label: '客户',
component: markRaw(MdClientSelect),
componentProps: {
placeholder: '请选择客户',
},
},
];
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions<MesWmSalesNoticeApi.SalesNotice>['columns'] {
return [
{
field: 'code',
title: '通知单编号',
minWidth: 160,
slots: { default: 'code' },
},
{
field: 'name',
title: '通知单名称',
minWidth: 150,
},
{
field: 'salesOrderCode',
title: '销售订单编号',
minWidth: 140,
},
{
field: 'clientName',
title: '客户名称',
minWidth: 120,
},
{
field: 'salesDate',
title: '发货日期',
width: 180,
formatter: 'formatDate',
},
{
field: 'recipientName',
title: '收货人',
minWidth: 100,
},
{
field: 'recipientTelephone',
title: '联系方式',
minWidth: 120,
},
{
field: 'recipientAddress',
title: '收货地址',
minWidth: 200,
},
{
field: 'status',
title: '单据状态',
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.MES_WM_SALES_NOTICE_STATUS },
},
},
{
title: '操作',
width: 200,
fixed: 'right',
slots: { default: 'actions' },
},
];
}
/** 通知单行子表的字段 */
export function useLineGridColumns(
editable: boolean,
): VxeTableGridOptions<MesWmSalesNoticeLineApi.SalesNoticeLine>['columns'] {
return [
{
field: 'itemCode',
title: '物料编码',
minWidth: 120,
},
{
field: 'itemName',
title: '物料名称',
minWidth: 140,
},
{
field: 'specification',
title: '规格型号',
minWidth: 120,
},
{
field: 'unitMeasureName',
title: '单位',
width: 80,
},
{
field: 'quantity',
title: '发货数量',
width: 100,
},
{
field: 'batchCode',
title: '批次号',
minWidth: 120,
},
{
field: 'oqcCheckFlag',
title: '是否检验',
width: 90,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
},
},
{
field: 'remark',
title: '备注',
minWidth: 120,
},
...(editable
? [
{
title: '操作',
width: 120,
fixed: 'right',
slots: { default: 'actions' },
} as const,
]
: []),
];
}
/** 通知单行新增/修改的表单 */
export function useLineFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'itemId',
label: '物料',
component: markRaw(MdItemSelect),
componentProps: {
placeholder: '请选择物料',
},
rules: 'selectRequired',
},
{
fieldName: 'quantity',
label: '发货数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0.01,
placeholder: '请输入发货数量',
precision: 2,
},
rules: 'required',
},
{
fieldName: 'batchCode',
label: '批次号',
component: 'Input',
componentProps: {
placeholder: '请输入批次号',
},
},
{
fieldName: 'oqcCheckFlag',
label: '是否检验',
component: 'Switch',
rules: z.boolean().default(true),
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
formItemClass: 'col-span-3',
componentProps: {
placeholder: '请输入备注',
rows: 3,
},
},
];
}

View File

@@ -0,0 +1,176 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmSalesNoticeApi } from '#/api/mes/wm/salesnotice';
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteSalesNotice,
exportSalesNotice,
getSalesNoticePage,
} from '#/api/mes/wm/salesnotice';
import { $t } from '#/locales';
import { MesWmSalesNoticeStatusEnum } from '#/views/mes/utils/constants';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/** 刷新表格 */
function handleRefresh() {
gridApi.query();
}
/** 创建发货通知单 */
function handleCreate() {
formModalApi.setData({ formType: 'create' }).open();
}
/** 查看发货通知单 */
function handleDetail(row: MesWmSalesNoticeApi.SalesNotice) {
formModalApi.setData({ formType: 'detail', id: row.id }).open();
}
/** 编辑发货通知单 */
function handleEdit(row: MesWmSalesNoticeApi.SalesNotice) {
formModalApi.setData({ formType: 'update', id: row.id }).open();
}
/** 执行出库 */
function handleFinish(row: MesWmSalesNoticeApi.SalesNotice) {
formModalApi.setData({ formType: 'finish', id: row.id }).open();
}
/** 删除发货通知单 */
async function handleDelete(row: MesWmSalesNoticeApi.SalesNotice) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.code]),
duration: 0,
});
try {
await deleteSalesNotice(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.code]));
handleRefresh();
} finally {
hideLoading();
}
}
/** 导出表格 */
async function handleExport() {
const data = await exportSalesNotice(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '发货通知单.xls', source: data });
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getSalesNoticePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
search: true,
},
} as VxeTableGridOptions<MesWmSalesNoticeApi.SalesNotice>,
});
</script>
<template>
<Page auto-content-height>
<template #doc>
<DocAlert
title="【仓库】发货通知、销售出库、销售退货"
url="https://doc.iocoder.cn/mes/wm/sales-out/"
/>
</template>
<FormModal @success="handleRefresh" />
<Grid table-title="发货通知单列表">
<template #toolbar-tools>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['发货通知单']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['mes:wm-sales-notice:create'],
onClick: handleCreate,
},
{
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
auth: ['mes:wm-sales-notice:export'],
onClick: handleExport,
},
]"
/>
</template>
<template #code="{ row }">
<Button type="link" @click="handleDetail(row)">
{{ row.code }}
</Button>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['mes:wm-sales-notice:update'],
ifShow: row.status === MesWmSalesNoticeStatusEnum.PREPARE,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['mes:wm-sales-notice:delete'],
ifShow: row.status === MesWmSalesNoticeStatusEnum.PREPARE,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.code]),
confirm: handleDelete.bind(null, row),
},
},
{
label: '执行出库',
type: 'link',
auth: ['mes:wm-sales-notice:update'],
ifShow: row.status === MesWmSalesNoticeStatusEnum.APPROVED,
onClick: handleFinish.bind(null, row),
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,179 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { MesWmSalesNoticeApi } from '#/api/mes/wm/salesnotice';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Button, Divider, message, Popconfirm } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
createSalesNotice,
getSalesNotice,
submitSalesNotice,
updateSalesNotice,
} from '#/api/mes/wm/salesnotice';
import { $t } from '#/locales';
import { MesWmSalesNoticeStatusEnum } from '#/views/mes/utils/constants';
import { useFormSchema } from '../data';
import LineList from './line-list.vue';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const formData = ref<MesWmSalesNoticeApi.SalesNotice>();
const originalSnapshot = ref(''); // 表单原始数据快照,用于提交时跳过未变更的保存请求
const isEditable = computed(() => // 是否为编辑模式(可保存)
['create', 'update'].includes(formType.value),
);
const isFinish = computed(() => formType.value === 'finish'); // 是否为执行出库模式
const canSubmit = computed(() => // 是否可提交
formType.value === 'update' &&
formData.value?.status === MesWmSalesNoticeStatusEnum.PREPARE,
);
const getTitle = computed(() => {
if (formType.value === 'detail') {
return $t('ui.actionTitle.view', ['发货通知单']);
}
if (formType.value === 'finish') {
return '执行出库';
}
return formType.value === 'update'
? $t('ui.actionTitle.edit', ['发货通知单'])
: $t('ui.actionTitle.create', ['发货通知单']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-1',
labelWidth: 110,
},
layout: 'horizontal',
schema: [],
showDefaultActions: false,
wrapperClass: 'grid-cols-3',
});
/** 提交通知单:表单有修改时先保存,再调用提交接口 */
async function handleSubmit() {
const { valid } = await formApi.validate();
if (!valid || !formData.value?.id) {
return;
}
modalApi.lock();
try {
const current = JSON.stringify(await formApi.getValues());
if (current !== originalSnapshot.value) {
const data =
(await formApi.getValues()) as MesWmSalesNoticeApi.SalesNotice;
await updateSalesNotice({ ...formData.value, ...data });
originalSnapshot.value = current;
}
await submitSalesNotice(formData.value.id);
message.success('提交成功');
await modalApi.close();
emit('success');
} finally {
modalApi.unlock();
}
}
/** 执行出库(后端暂未实现,提示用户) */
function handleFinish() {
message.info('执行出库功能暂时不支持,敬请期待!');
}
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
if (!isEditable.value) {
await modalApi.close();
return;
}
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data = (await formApi.getValues()) as MesWmSalesNoticeApi.SalesNotice;
try {
if (formData.value?.id) {
await updateSalesNotice({ ...formData.value, ...data });
formData.value = { ...formData.value, ...data };
} else {
const id = await createSalesNotice(data);
formData.value = {
...data,
id,
status: MesWmSalesNoticeStatusEnum.PREPARE,
};
await formApi.setFieldValue('id', id);
await formApi.setFieldValue('status', formData.value.status);
formType.value = 'update';
}
originalSnapshot.value = JSON.stringify(await formApi.getValues());
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
originalSnapshot.value = '';
return;
}
// 加载数据
const data = modalApi.getData<{ formType: FormType; id?: number }>();
formType.value = data.formType;
formApi.setState({ schema: useFormSchema(formType.value, formApi) });
formApi.setDisabled(!isEditable.value);
modalApi.setState({ showConfirmButton: isEditable.value });
if (data?.id) {
modalApi.lock();
try {
formData.value = await getSalesNotice(data.id);
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
}
originalSnapshot.value = JSON.stringify(await formApi.getValues());
},
});
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" />
<!-- 非新建模式展示物料信息 -->
<template v-if="formData?.id">
<Divider>物料信息</Divider>
<div class="mx-4">
<LineList :form-type="formType" :notice-id="formData.id" />
</div>
</template>
<template #prepend-footer>
<div class="flex flex-auto items-center gap-2">
<Popconfirm
v-if="canSubmit"
title="确认提交该发货通知单?【提交后将不能修改】"
@confirm="handleSubmit"
>
<Button type="primary">提交</Button>
</Popconfirm>
<Button v-if="isFinish" type="primary" @click="handleFinish">
执行出库
</Button>
</div>
</template>
</Modal>
</template>

View File

@@ -0,0 +1,94 @@
<script lang="ts" setup>
import type { MesWmSalesNoticeLineApi } from '#/api/mes/wm/salesnotice/line';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
createSalesNoticeLine,
getSalesNoticeLine,
updateSalesNoticeLine,
} from '#/api/mes/wm/salesnotice/line';
import { $t } from '#/locales';
import { useLineFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<MesWmSalesNoticeLineApi.SalesNoticeLine>();
const noticeId = ref<number>(); // 所属通知单编号
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['发货通知单行'])
: $t('ui.actionTitle.create', ['发货通知单行']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-1',
labelWidth: 90,
},
layout: 'horizontal',
schema: useLineFormSchema(),
showDefaultActions: false,
wrapperClass: 'grid-cols-3',
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data =
(await formApi.getValues()) as MesWmSalesNoticeLineApi.SalesNoticeLine;
data.noticeId = noticeId.value;
try {
await (formData.value?.id
? updateSalesNoticeLine({ ...data, id: formData.value.id })
: createSalesNoticeLine(data));
// 关闭并提示
await modalApi.close();
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
// 加载数据
const data = modalApi.getData<{ id?: number; noticeId: number }>();
noticeId.value = data.noticeId;
if (!data.id) {
return;
}
modalApi.lock();
try {
formData.value = await getSalesNoticeLine(data.id);
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
},
});
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" />
</Modal>
</template>

View File

@@ -0,0 +1,137 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmSalesNoticeLineApi } from '#/api/mes/wm/salesnotice/line';
import { computed } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteSalesNoticeLine,
getSalesNoticeLinePage,
} from '#/api/mes/wm/salesnotice/line';
import { $t } from '#/locales';
import { type FormType, useLineGridColumns } from '../data';
import LineForm from './line-form.vue';
const props = defineProps<{
formType: FormType;
noticeId: number;
}>();
const isEditable = computed(() => // 是否可编辑明细行
['create', 'update'].includes(props.formType),
);
const [LineFormModal, lineFormModalApi] = useVbenModal({
connectedComponent: LineForm,
destroyOnClose: true,
});
/** 刷新表格 */
function handleRefresh() {
gridApi.query();
}
/** 添加物料 */
function handleCreate() {
lineFormModalApi.setData({ noticeId: props.noticeId }).open();
}
/** 编辑物料 */
function handleEdit(row: MesWmSalesNoticeLineApi.SalesNoticeLine) {
lineFormModalApi.setData({ id: row.id, noticeId: props.noticeId }).open();
}
/** 删除物料 */
async function handleDelete(row: MesWmSalesNoticeLineApi.SalesNoticeLine) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.itemName]),
duration: 0,
});
try {
await deleteSalesNoticeLine(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.itemName]));
handleRefresh();
} finally {
hideLoading();
}
}
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useLineGridColumns(isEditable.value),
height: 400,
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }) => {
if (!props.noticeId) {
return { list: [], total: 0 };
}
return await getSalesNoticeLinePage({
noticeId: props.noticeId,
pageNo: page.currentPage,
pageSize: page.pageSize,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
},
} as VxeTableGridOptions<MesWmSalesNoticeLineApi.SalesNoticeLine>,
});
</script>
<template>
<div>
<LineFormModal @success="handleRefresh" />
<Grid table-title="物料信息">
<template v-if="isEditable" #toolbar-tools>
<TableAction
:actions="[
{
label: '添加物料',
type: 'primary',
icon: ACTION_ICON.ADD,
onClick: handleCreate,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
ifShow: isEditable,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
ifShow: isEditable,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.itemName]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</div>
</template>

View File

@@ -0,0 +1,2 @@
export { default as WmArrivalNoticeLineSelect } from './wm-arrival-notice-line-select.vue';
export { default as WmArrivalNoticeSelect } from './wm-arrival-notice-select.vue';

View File

@@ -0,0 +1,206 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmArrivalNoticeLineApi } from '#/api/mes/wm/arrivalnotice/line';
import { nextTick, ref } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { ElButton, ElDialog, ElMessage } from 'element-plus';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getArrivalNoticeLinePage } from '#/api/mes/wm/arrivalnotice/line';
const emit = defineEmits<{
selected: [rows: MesWmArrivalNoticeLineApi.ArrivalNoticeLine[]];
}>();
const open = ref(false); // 弹窗是否打开
const noticeId = ref<number>(); // 所属通知单编号
const syncingSingleSelection = ref(false); // 是否同步单选勾选状态
const selectedRows = ref<MesWmArrivalNoticeLineApi.ArrivalNoticeLine[]>([]); // 已选行列表
const preSelectedIds = ref<number[]>([]); // 预选行编号列表
/** 表格字段 */
function useGridColumns(): VxeTableGridOptions<MesWmArrivalNoticeLineApi.ArrivalNoticeLine>['columns'] {
return [
{
type: 'checkbox',
width: 50,
},
{
field: 'itemCode',
title: '物料编码',
minWidth: 120,
},
{
field: 'itemName',
title: '物料名称',
minWidth: 140,
},
{
field: 'specification',
title: '规格型号',
minWidth: 120,
},
{
field: 'unitMeasureName',
title: '单位',
width: 80,
},
{
field: 'arrivalQuantity',
title: '到货数量',
width: 100,
},
{
field: 'iqcCheckFlag',
title: '是否检验',
width: 90,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
},
},
{
field: 'qualifiedQuantity',
title: '合格数量',
width: 100,
},
{
field: 'remark',
title: '备注',
minWidth: 120,
},
];
}
/** 单选模式下同步 VXE 勾选状态 */
async function syncSingleSelection(
row?: MesWmArrivalNoticeLineApi.ArrivalNoticeLine,
) {
syncingSingleSelection.value = true;
await nextTick();
await gridApi.grid.clearCheckboxRow();
if (row) {
await gridApi.grid.setCheckboxRow(row, true);
}
await nextTick();
syncingSingleSelection.value = false;
}
/** 处理勾选变化,只保留最后一条 */
async function handleCheckboxChange({
checked,
row,
}: {
checked: boolean;
row?: MesWmArrivalNoticeLineApi.ArrivalNoticeLine;
}) {
if (syncingSingleSelection.value) {
return;
}
const selected = checked && row ? [row] : [];
selectedRows.value = selected;
await syncSingleSelection(selected[0]);
}
/** 回显预选行 */
function applyPreSelection() {
if (preSelectedIds.value.length === 0) {
return;
}
const rows =
gridApi.grid.getData() as MesWmArrivalNoticeLineApi.ArrivalNoticeLine[];
for (const row of rows) {
if (row.id && preSelectedIds.value.includes(row.id)) {
gridApi.grid.setCheckboxRow(row, true);
selectedRows.value = [row];
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useGridColumns(),
height: 460,
keepSource: true,
checkboxConfig: {
highlight: true,
reserve: true,
},
proxyConfig: {
ajax: {
query: async ({ page }) => {
if (!noticeId.value) {
return { list: [], total: 0 };
}
return await getArrivalNoticeLinePage({
noticeId: noticeId.value,
pageNo: page.currentPage,
pageSize: page.pageSize,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
},
} as VxeTableGridOptions<MesWmArrivalNoticeLineApi.ArrivalNoticeLine>,
gridEvents: {
checkboxChange: handleCheckboxChange,
},
});
/** 打开行选择弹窗 */
async function openModal(id: number | undefined, selectedIds?: number[]) {
open.value = true;
noticeId.value = id;
preSelectedIds.value = selectedIds || [];
selectedRows.value = [];
await nextTick();
await gridApi.grid.clearCheckboxRow();
await gridApi.query();
await nextTick();
applyPreSelection();
}
/** 关闭弹窗 */
async function closeModal() {
open.value = false;
selectedRows.value = [];
await gridApi.grid.clearCheckboxRow();
}
/** 确认选择行 */
function handleConfirm() {
if (selectedRows.value.length === 0) {
ElMessage.warning('请选择一条数据');
return;
}
emit('selected', [selectedRows.value[0]!]);
open.value = false;
}
defineExpose({ open: openModal });
</script>
<template>
<ElDialog
v-model="open"
title="到货通知单行选择"
width="70%"
destroy-on-close
@close="closeModal"
>
<Grid table-title="到货通知单行列表" />
<template #footer>
<ElButton @click="closeModal">取消</ElButton>
<ElButton type="primary" @click="handleConfirm">确定</ElButton>
</template>
</ElDialog>
</template>

View File

@@ -0,0 +1,149 @@
<script lang="ts" setup>
import type { MesWmArrivalNoticeLineApi } from '#/api/mes/wm/arrivalnotice/line';
import { computed, ref, useAttrs, watch } from 'vue';
import { CircleX, Search } from '@vben/icons';
import { ElInput, ElTooltip } from 'element-plus';
import { getArrivalNoticeLine } from '#/api/mes/wm/arrivalnotice/line';
import WmArrivalNoticeLineSelectDialog from './wm-arrival-notice-line-select-dialog.vue';
defineOptions({ name: 'WmArrivalNoticeLineSelect', inheritAttrs: false });
const props = withDefaults(
defineProps<{
clearable?: boolean;
disabled?: boolean;
modelValue?: number;
noticeId?: number; // 所属到货通知单编号
placeholder?: string;
}>(),
{
clearable: true,
disabled: false,
modelValue: undefined,
noticeId: undefined,
placeholder: '请选择到货通知单行',
},
);
const emit = defineEmits<{
change: [item: MesWmArrivalNoticeLineApi.ArrivalNoticeLine | undefined];
'update:modelValue': [value: number | undefined];
}>();
const attrs = useAttrs();
const dialogRef = ref<InstanceType<typeof WmArrivalNoticeLineSelectDialog>>();
const hovering = ref(false);
const selectedItem = ref<MesWmArrivalNoticeLineApi.ArrivalNoticeLine>();
const displayLabel = computed(() => {
const item = selectedItem.value;
if (!item) {
return '';
}
return `${item.itemCode ?? ''} - ${item.itemName ?? ''}`;
});
const showClear = computed(
() =>
props.clearable &&
!props.disabled &&
hovering.value &&
props.modelValue != null,
);
/** 根据编号单条查询行信息(用于编辑回显) */
async function resolveItemById(id: number | undefined) {
if (id == null) {
selectedItem.value = undefined;
return;
}
if (selectedItem.value?.id === id) {
return;
}
selectedItem.value = await getArrivalNoticeLine(id);
}
watch(() => props.modelValue, resolveItemById, { immediate: true });
/** noticeId 变化时清空选中(关联的行已失效) */
watch(
() => props.noticeId,
() => {
selectedItem.value = undefined;
emit('update:modelValue', undefined);
emit('change', undefined);
},
);
/** 清空已选行 */
function clearSelected() {
selectedItem.value = undefined;
emit('update:modelValue', undefined);
emit('change', undefined);
}
/** 打开行选择弹窗 */
function handleClick(event: MouseEvent) {
if (props.disabled || !props.noticeId) {
return;
}
const target = event.target as HTMLElement;
if (showClear.value && target.closest('.el-input__suffix')) {
event.stopPropagation();
clearSelected();
return;
}
const selectedIds = props.modelValue == null ? [] : [props.modelValue];
dialogRef.value?.open(props.noticeId, selectedIds);
}
/** 弹窗选中回调 */
function handleSelected(rows: MesWmArrivalNoticeLineApi.ArrivalNoticeLine[]) {
const item = rows[0];
if (!item) {
return;
}
selectedItem.value = item;
emit('update:modelValue', item.id);
emit('change', item);
}
</script>
<template>
<div
v-bind="attrs"
class="w-full"
:class="disabled ? 'cursor-not-allowed' : 'cursor-pointer'"
@click="handleClick"
@mouseenter="hovering = true"
@mouseleave="hovering = false"
>
<ElTooltip :disabled="!selectedItem" placement="top" :show-after="500">
<template #content>
<div v-if="selectedItem" class="leading-6">
<div>物料编码{{ selectedItem.itemCode || '-' }}</div>
<div>物料名称{{ selectedItem.itemName || '-' }}</div>
<div>规格型号{{ selectedItem.specification || '-' }}</div>
<div>到货数量{{ selectedItem.arrivalQuantity ?? '-' }}</div>
</div>
</template>
<ElInput
:disabled="disabled"
:model-value="displayLabel"
:placeholder="placeholder"
readonly
>
<template #suffix>
<CircleX v-if="showClear" class="size-4" />
<Search v-else class="size-4" />
</template>
</ElInput>
</ElTooltip>
</div>
<WmArrivalNoticeLineSelectDialog ref="dialogRef" @selected="handleSelected" />
</template>

View File

@@ -0,0 +1,265 @@
<script lang="ts" setup>
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmArrivalNoticeApi } from '#/api/mes/wm/arrivalnotice';
import { nextTick, ref } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { ElButton, ElDialog, ElMessage } from 'element-plus';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getArrivalNoticePage } from '#/api/mes/wm/arrivalnotice';
const emit = defineEmits<{
selected: [rows: MesWmArrivalNoticeApi.ArrivalNotice[]];
}>();
const open = ref(false); // 弹窗是否打开
const multiple = ref(false); // 是否多选
const fixedStatus = ref<number>(); // 固定状态筛选
const syncingSingleSelection = ref(false); // 是否同步单选勾选状态
const selectedRows = ref<MesWmArrivalNoticeApi.ArrivalNotice[]>([]); // 已选通知单列表
const preSelectedIds = ref<number[]>([]); // 预选通知单编号列表
/** 搜索表单 */
function useSearchSchema(): VbenFormSchema[] {
return [
{
fieldName: 'code',
label: '通知单编号',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入通知单编号',
},
},
{
fieldName: 'name',
label: '通知单名称',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入通知单名称',
},
},
{
fieldName: 'purchaseOrderCode',
label: '采购订单编号',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入采购订单编号',
},
},
];
}
/** 表格字段 */
function useGridColumns(): VxeTableGridOptions<MesWmArrivalNoticeApi.ArrivalNotice>['columns'] {
return [
{
type: 'checkbox',
width: 50,
},
{
field: 'code',
title: '通知单编号',
minWidth: 160,
},
{
field: 'name',
title: '通知单名称',
minWidth: 150,
},
{
field: 'purchaseOrderCode',
title: '采购订单编号',
minWidth: 140,
},
{
field: 'vendorName',
title: '供应商名称',
minWidth: 120,
},
{
field: 'arrivalDate',
title: '到货日期',
width: 120,
formatter: 'formatDate',
},
{
field: 'status',
title: '单据状态',
width: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.MES_WM_ARRIVAL_NOTICE_STATUS },
},
},
];
}
/** 单选模式下同步 VXE 勾选状态,避免跨页残留多选 */
async function syncSingleSelection(row?: MesWmArrivalNoticeApi.ArrivalNotice) {
syncingSingleSelection.value = true;
await nextTick();
await gridApi.grid.clearCheckboxRow();
if (row) {
await gridApi.grid.setCheckboxRow(row, true);
}
await nextTick();
syncingSingleSelection.value = false;
}
/** 处理勾选变化,单选模式只保留最后一条 */
async function handleCheckboxChange({
checked,
records,
row,
}: {
checked: boolean;
records: MesWmArrivalNoticeApi.ArrivalNotice[];
row?: MesWmArrivalNoticeApi.ArrivalNotice;
}) {
if (syncingSingleSelection.value) {
return;
}
if (!multiple.value) {
const selected = checked && row ? [row] : [];
selectedRows.value = selected;
await syncSingleSelection(selected[0]);
return;
}
selectedRows.value = records;
}
/** 处理全选变化 */
function handleCheckboxAll({
records,
}: {
records: MesWmArrivalNoticeApi.ArrivalNotice[];
}) {
if (syncingSingleSelection.value) {
return;
}
selectedRows.value = records;
}
/** 回显预选通知单 */
function applyPreSelection() {
if (preSelectedIds.value.length === 0) {
return;
}
const rows = gridApi.grid.getData() as MesWmArrivalNoticeApi.ArrivalNotice[];
for (const row of rows) {
if (row.id && preSelectedIds.value.includes(row.id)) {
gridApi.grid.setCheckboxRow(row, true);
if (!multiple.value) {
selectedRows.value = [row];
}
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useSearchSchema(),
},
gridOptions: {
columns: useGridColumns(),
height: 520,
keepSource: true,
checkboxConfig: {
highlight: true,
range: true,
reserve: true,
},
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getArrivalNoticePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
status: fixedStatus.value,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
search: true,
},
} as VxeTableGridOptions<MesWmArrivalNoticeApi.ArrivalNotice>,
gridEvents: {
checkboxAll: handleCheckboxAll,
checkboxChange: handleCheckboxChange,
},
});
/** 重置查询和选择状态 */
async function resetQueryState() {
selectedRows.value = [];
await gridApi.grid.clearCheckboxRow();
await gridApi.formApi.resetForm();
}
/** 打开通知单选择弹窗 */
async function openModal(
selectedIds?: number[],
options?: { multiple?: boolean; status?: number },
) {
open.value = true;
multiple.value = options?.multiple ?? false;
fixedStatus.value = options?.status;
preSelectedIds.value = selectedIds || [];
await nextTick();
await resetQueryState();
await gridApi.query();
await nextTick();
applyPreSelection();
}
/** 关闭通知单选择弹窗 */
async function closeModal() {
open.value = false;
await resetQueryState();
}
/** 确认选择通知单 */
function handleConfirm() {
if (selectedRows.value.length === 0) {
ElMessage.warning(multiple.value ? '请至少选择一条数据' : '请选择一条数据');
return;
}
emit(
'selected',
multiple.value ? selectedRows.value : [selectedRows.value[0]!],
);
open.value = false;
}
defineExpose({ open: openModal });
</script>
<template>
<ElDialog
v-model="open"
title="到货通知单选择"
width="70%"
destroy-on-close
@close="closeModal"
>
<Grid table-title="到货通知单列表" />
<template #footer>
<ElButton @click="closeModal">取消</ElButton>
<ElButton type="primary" @click="handleConfirm">确定</ElButton>
</template>
</ElDialog>
</template>

View File

@@ -0,0 +1,136 @@
<script lang="ts" setup>
import type { MesWmArrivalNoticeApi } from '#/api/mes/wm/arrivalnotice';
import { computed, ref, useAttrs, watch } from 'vue';
import { CircleX, Search } from '@vben/icons';
import { ElInput, ElTooltip } from 'element-plus';
import { getArrivalNotice } from '#/api/mes/wm/arrivalnotice';
import WmArrivalNoticeSelectDialog from './wm-arrival-notice-select-dialog.vue';
defineOptions({ name: 'WmArrivalNoticeSelect', inheritAttrs: false });
const props = withDefaults(
defineProps<{
clearable?: boolean;
disabled?: boolean;
modelValue?: number;
placeholder?: string;
status?: number; // 固定状态筛选
}>(),
{
clearable: true,
disabled: false,
modelValue: undefined,
placeholder: '请选择到货通知单',
status: undefined,
},
);
const emit = defineEmits<{
change: [item: MesWmArrivalNoticeApi.ArrivalNotice | undefined];
'update:modelValue': [value: number | undefined];
}>();
const attrs = useAttrs();
const dialogRef = ref<InstanceType<typeof WmArrivalNoticeSelectDialog>>();
const hovering = ref(false);
const selectedItem = ref<MesWmArrivalNoticeApi.ArrivalNotice>();
const displayLabel = computed(() => selectedItem.value?.code ?? '');
const showClear = computed(
() =>
props.clearable &&
!props.disabled &&
hovering.value &&
props.modelValue != null,
);
/** 根据编号单条查询通知单信息(用于编辑回显) */
async function resolveItemById(id: number | undefined) {
if (id == null) {
selectedItem.value = undefined;
return;
}
if (selectedItem.value?.id === id) {
return;
}
selectedItem.value = await getArrivalNotice(id);
}
watch(() => props.modelValue, resolveItemById, { immediate: true });
/** 清空已选通知单 */
function clearSelected() {
selectedItem.value = undefined;
emit('update:modelValue', undefined);
emit('change', undefined);
}
/** 打开通知单选择弹窗 */
function handleClick(event: MouseEvent) {
if (props.disabled) {
return;
}
const target = event.target as HTMLElement;
if (showClear.value && target.closest('.el-input__suffix')) {
event.stopPropagation();
clearSelected();
return;
}
const selectedIds = props.modelValue == null ? [] : [props.modelValue];
dialogRef.value?.open(selectedIds, {
multiple: false,
status: props.status,
});
}
/** 弹窗选中回调 */
function handleSelected(rows: MesWmArrivalNoticeApi.ArrivalNotice[]) {
const item = rows[0];
if (!item) {
return;
}
selectedItem.value = item;
emit('update:modelValue', item.id);
emit('change', item);
}
</script>
<template>
<div
v-bind="attrs"
class="w-full"
:class="disabled ? 'cursor-not-allowed' : 'cursor-pointer'"
@click="handleClick"
@mouseenter="hovering = true"
@mouseleave="hovering = false"
>
<ElTooltip :disabled="!selectedItem" placement="top" :show-after="500">
<template #content>
<div v-if="selectedItem" class="leading-6">
<div>编号{{ selectedItem.code || '-' }}</div>
<div>名称{{ selectedItem.name || '-' }}</div>
<div>供应商{{ selectedItem.vendorName || '-' }}</div>
<div>采购订单{{ selectedItem.purchaseOrderCode || '-' }}</div>
</div>
</template>
<ElInput
:disabled="disabled"
:model-value="displayLabel"
:placeholder="placeholder"
readonly
>
<template #suffix>
<CircleX v-if="showClear" class="size-4" />
<Search v-else class="size-4" />
</template>
</ElInput>
</ElTooltip>
</div>
<WmArrivalNoticeSelectDialog ref="dialogRef" @selected="handleSelected" />
</template>

View File

@@ -0,0 +1,354 @@
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmArrivalNoticeApi } from '#/api/mes/wm/arrivalnotice';
import type { MesWmArrivalNoticeLineApi } from '#/api/mes/wm/arrivalnotice/line';
import { h, markRaw } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { ElButton } from 'element-plus';
import { z } from '#/adapter/form';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { getRangePickerDefaultProps } from '#/utils';
import MdItemSelect from '#/views/mes/md/item/components/md-item-select.vue';
import MdVendorSelect from '#/views/mes/md/vendor/components/md-vendor-select.vue';
import { MesAutoCodeRuleCode } from '#/views/mes/utils/constants';
/** 表单类型 */
export type FormType = 'create' | 'detail' | 'update';
/** 新增/修改的表单 */
export function useFormSchema(
formType: FormType,
formApi?: VbenFormApi,
): VbenFormSchema[] {
return [
{
fieldName: 'id',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'status',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'code',
label: '通知单编号',
component: 'Input',
componentProps: {
placeholder: '请输入通知单编号',
},
rules: 'required',
suffix:
formType === 'detail'
? undefined
: () =>
h(
ElButton,
{
onClick: async () => {
const code = await generateAutoCode(
MesAutoCodeRuleCode.WM_ARRIVAL_NOTICE_CODE,
);
await formApi?.setFieldValue('code', code);
},
},
{ default: () => '生成' },
),
},
{
fieldName: 'name',
label: '通知单名称',
component: 'Input',
componentProps: {
placeholder: '请输入通知单名称',
},
rules: 'required',
},
{
fieldName: 'purchaseOrderCode',
label: '采购订单编号',
component: 'Input',
componentProps: {
placeholder: '请输入采购订单编号',
},
},
{
fieldName: 'vendorId',
label: '供应商',
component: markRaw(MdVendorSelect),
componentProps: {
placeholder: '请选择供应商',
},
rules: 'selectRequired',
},
{
fieldName: 'arrivalDate',
label: '到货日期',
component: 'DatePicker',
componentProps: {
class: '!w-full',
format: 'YYYY-MM-DD',
placeholder: '请选择到货日期',
type: 'date',
valueFormat: 'x',
},
rules: 'required',
},
{
fieldName: 'contactName',
label: '联系人',
component: 'Input',
componentProps: {
placeholder: '请输入联系人',
},
},
{
fieldName: 'contactTelephone',
label: '联系方式',
component: 'Input',
componentProps: {
placeholder: '请输入联系方式',
},
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
formItemClass: 'col-span-3',
componentProps: {
placeholder: '请输入备注',
rows: 3,
},
},
];
}
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'code',
label: '通知单编号',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入通知单编号',
},
},
{
fieldName: 'name',
label: '通知单名称',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入通知单名称',
},
},
{
fieldName: 'purchaseOrderCode',
label: '采购订单编号',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入采购订单编号',
},
},
{
fieldName: 'vendorId',
label: '供应商',
component: markRaw(MdVendorSelect),
componentProps: {
placeholder: '请选择供应商',
},
},
{
fieldName: 'arrivalDate',
label: '到货日期',
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
clearable: true,
},
},
];
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions<MesWmArrivalNoticeApi.ArrivalNotice>['columns'] {
return [
{
field: 'code',
title: '通知单编号',
minWidth: 160,
slots: { default: 'code' },
},
{
field: 'name',
title: '通知单名称',
minWidth: 150,
},
{
field: 'purchaseOrderCode',
title: '采购订单编号',
minWidth: 140,
},
{
field: 'vendorName',
title: '供应商名称',
minWidth: 120,
},
{
field: 'contactName',
title: '联系人',
minWidth: 100,
},
{
field: 'contactTelephone',
title: '联系方式',
minWidth: 120,
},
{
field: 'arrivalDate',
title: '到货日期',
width: 180,
formatter: 'formatDate',
},
{
field: 'status',
title: '单据状态',
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.MES_WM_ARRIVAL_NOTICE_STATUS },
},
},
{
title: '操作',
width: 180,
fixed: 'right',
slots: { default: 'actions' },
},
];
}
/** 通知单行子表的字段 */
export function useLineGridColumns(
editable: boolean,
): VxeTableGridOptions<MesWmArrivalNoticeLineApi.ArrivalNoticeLine>['columns'] {
return [
{
field: 'itemCode',
title: '物料编码',
minWidth: 120,
},
{
field: 'itemName',
title: '物料名称',
minWidth: 140,
},
{
field: 'specification',
title: '规格型号',
minWidth: 120,
},
{
field: 'unitMeasureName',
title: '单位',
width: 80,
},
{
field: 'arrivalQuantity',
title: '到货数量',
width: 100,
},
{
field: 'iqcCheckFlag',
title: '是否检验',
width: 90,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
},
},
{
field: 'qualifiedQuantity',
title: '合格数量',
width: 100,
},
{
field: 'iqcCode',
title: '检验单号',
minWidth: 140,
},
{
field: 'remark',
title: '备注',
minWidth: 120,
},
...(editable
? [
{
title: '操作',
width: 120,
fixed: 'right',
slots: { default: 'actions' },
} as const,
]
: []),
];
}
/** 通知单行新增/修改的表单 */
export function useLineFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'itemId',
label: '物料',
component: markRaw(MdItemSelect),
componentProps: {
placeholder: '请选择物料',
},
rules: 'selectRequired',
},
{
fieldName: 'arrivalQuantity',
label: '到货数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
controlsPosition: 'right',
min: 0.01,
placeholder: '请输入到货数量',
precision: 2,
},
rules: 'required',
},
{
fieldName: 'iqcCheckFlag',
label: '是否检验',
component: 'Switch',
rules: z.boolean().default(false),
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
formItemClass: 'col-span-3',
componentProps: {
placeholder: '请输入备注',
rows: 3,
},
},
];
}

View File

@@ -0,0 +1,189 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmArrivalNoticeApi } from '#/api/mes/wm/arrivalnotice';
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { ElButton, ElLoading, ElMessage } from 'element-plus';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteArrivalNotice,
exportArrivalNotice,
getArrivalNoticePage,
} from '#/api/mes/wm/arrivalnotice';
import { $t } from '#/locales';
import { MesWmArrivalNoticeStatusEnum } from '#/views/mes/utils/constants';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/** 刷新表格 */
function handleRefresh() {
gridApi.query();
}
/** 创建到货通知单 */
function handleCreate() {
formModalApi.setData({ formType: 'create' }).open();
}
/** 查看到货通知单 */
function handleDetail(row: MesWmArrivalNoticeApi.ArrivalNotice) {
formModalApi.setData({ formType: 'detail', id: row.id }).open();
}
/** 编辑到货通知单 */
function handleEdit(row: MesWmArrivalNoticeApi.ArrivalNotice) {
formModalApi.setData({ formType: 'update', id: row.id }).open();
}
/** 删除到货通知单 */
async function handleDelete(row: MesWmArrivalNoticeApi.ArrivalNotice) {
const loadingInstance = ElLoading.service({
text: $t('ui.actionMessage.deleting', [row.code]),
});
try {
await deleteArrivalNotice(row.id!);
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.code]));
handleRefresh();
} finally {
loadingInstance.close();
}
}
/** 提示前往质检模块 */
function handleQc() {
ElMessage.info('请前往【质量管理 - 待检任务】中进行来料检验操作');
}
/** 提示前往采购入库模块 */
function handleReceipt() {
ElMessage.info('请前往【仓库管理 - 采购入库】中进行入库操作');
}
/** 导出表格 */
async function handleExport() {
const data = await exportArrivalNotice(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '到货通知单.xls', source: data });
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getArrivalNoticePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
search: true,
},
} as VxeTableGridOptions<MesWmArrivalNoticeApi.ArrivalNotice>,
});
</script>
<template>
<Page auto-content-height>
<template #doc>
<DocAlert
title="【仓库】到货通知、采购入库、采购退货"
url="https://doc.iocoder.cn/mes/wm/purchase-in/"
/>
</template>
<FormModal @success="handleRefresh" />
<Grid table-title="到货通知单列表">
<template #toolbar-tools>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['到货通知单']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['mes:wm-arrival-notice:create'],
onClick: handleCreate,
},
{
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
auth: ['mes:wm-arrival-notice:export'],
onClick: handleExport,
},
]"
/>
</template>
<template #code="{ row }">
<ElButton link type="primary" @click="handleDetail(row)">
{{ row.code }}
</ElButton>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'primary',
link: true,
icon: ACTION_ICON.EDIT,
auth: ['mes:wm-arrival-notice:update'],
ifShow: row.status === MesWmArrivalNoticeStatusEnum.PREPARE,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'danger',
link: true,
icon: ACTION_ICON.DELETE,
auth: ['mes:wm-arrival-notice:delete'],
ifShow: row.status === MesWmArrivalNoticeStatusEnum.PREPARE,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.code]),
confirm: handleDelete.bind(null, row),
},
},
{
label: '执行质检',
type: 'primary',
link: true,
ifShow: row.status === MesWmArrivalNoticeStatusEnum.PENDING_QC,
onClick: handleQc,
},
{
label: '执行入库',
type: 'primary',
link: true,
ifShow:
row.status === MesWmArrivalNoticeStatusEnum.PENDING_RECEIPT,
onClick: handleReceipt,
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,171 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { MesWmArrivalNoticeApi } from '#/api/mes/wm/arrivalnotice';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { ElButton, ElDivider, ElMessage, ElPopconfirm } from 'element-plus';
import { useVbenForm } from '#/adapter/form';
import {
createArrivalNotice,
getArrivalNotice,
submitArrivalNotice,
updateArrivalNotice,
} from '#/api/mes/wm/arrivalnotice';
import { $t } from '#/locales';
import { MesWmArrivalNoticeStatusEnum } from '#/views/mes/utils/constants';
import { useFormSchema } from '../data';
import LineList from './line-list.vue';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const formData = ref<MesWmArrivalNoticeApi.ArrivalNotice>();
const originalSnapshot = ref(''); // 表单原始数据快照,用于提交时跳过未变更的保存请求
const isEditable = computed(() => // 是否为编辑模式(可保存)
['create', 'update'].includes(formType.value),
);
const canSubmit = computed(() => // 是否可提交
formType.value === 'update' &&
formData.value?.status === MesWmArrivalNoticeStatusEnum.PREPARE,
);
const getTitle = computed(() => {
if (formType.value === 'detail') {
return $t('ui.actionTitle.view', ['到货通知单']);
}
return formType.value === 'update'
? $t('ui.actionTitle.edit', ['到货通知单'])
: $t('ui.actionTitle.create', ['到货通知单']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-1',
labelWidth: 110,
},
layout: 'horizontal',
schema: [],
showDefaultActions: false,
wrapperClass: 'grid-cols-3',
});
/** 提交通知单:表单有修改时先保存,再调用提交接口 */
async function handleSubmit() {
const { valid } = await formApi.validate();
if (!valid || !formData.value?.id) {
return;
}
modalApi.lock();
try {
const current = JSON.stringify(await formApi.getValues());
if (current !== originalSnapshot.value) {
const data =
(await formApi.getValues()) as MesWmArrivalNoticeApi.ArrivalNotice;
await updateArrivalNotice({ ...formData.value, ...data });
originalSnapshot.value = current;
}
await submitArrivalNotice(formData.value.id);
ElMessage.success('提交成功');
await modalApi.close();
emit('success');
} finally {
modalApi.unlock();
}
}
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
if (!isEditable.value) {
await modalApi.close();
return;
}
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data =
(await formApi.getValues()) as MesWmArrivalNoticeApi.ArrivalNotice;
try {
if (formData.value?.id) {
await updateArrivalNotice({ ...formData.value, ...data });
formData.value = { ...formData.value, ...data };
} else {
const id = await createArrivalNotice(data);
formData.value = {
...data,
id,
status: MesWmArrivalNoticeStatusEnum.PREPARE,
};
await formApi.setFieldValue('id', id);
await formApi.setFieldValue('status', formData.value.status);
formType.value = 'update';
}
originalSnapshot.value = JSON.stringify(await formApi.getValues());
emit('success');
ElMessage.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
originalSnapshot.value = '';
return;
}
// 加载数据
const data = modalApi.getData<{ formType: FormType; id?: number }>();
formType.value = data.formType;
formApi.setState({ schema: useFormSchema(formType.value, formApi) });
formApi.setDisabled(!isEditable.value);
modalApi.setState({ showConfirmButton: isEditable.value });
if (data?.id) {
modalApi.lock();
try {
formData.value = await getArrivalNotice(data.id);
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
}
originalSnapshot.value = JSON.stringify(await formApi.getValues());
},
});
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" />
<!-- 非新建模式展示物料信息 -->
<template v-if="formData?.id">
<ElDivider>物料信息</ElDivider>
<div class="mx-4">
<LineList :form-type="formType" :notice-id="formData.id" />
</div>
</template>
<template #prepend-footer>
<div class="flex flex-auto items-center gap-2">
<ElPopconfirm
v-if="canSubmit"
title="确认提交该到货通知单?【提交后将不能修改】"
width="260"
@confirm="handleSubmit"
>
<template #reference>
<ElButton type="primary">提交</ElButton>
</template>
</ElPopconfirm>
</div>
</template>
</Modal>
</template>

View File

@@ -0,0 +1,94 @@
<script lang="ts" setup>
import type { MesWmArrivalNoticeLineApi } from '#/api/mes/wm/arrivalnotice/line';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { ElMessage } from 'element-plus';
import { useVbenForm } from '#/adapter/form';
import {
createArrivalNoticeLine,
getArrivalNoticeLine,
updateArrivalNoticeLine,
} from '#/api/mes/wm/arrivalnotice/line';
import { $t } from '#/locales';
import { useLineFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<MesWmArrivalNoticeLineApi.ArrivalNoticeLine>();
const noticeId = ref<number>(); // 所属通知单编号
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['到货通知单行'])
: $t('ui.actionTitle.create', ['到货通知单行']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-1',
labelWidth: 90,
},
layout: 'horizontal',
schema: useLineFormSchema(),
showDefaultActions: false,
wrapperClass: 'grid-cols-3',
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data =
(await formApi.getValues()) as MesWmArrivalNoticeLineApi.ArrivalNoticeLine;
data.noticeId = noticeId.value;
try {
await (formData.value?.id
? updateArrivalNoticeLine({ ...data, id: formData.value.id })
: createArrivalNoticeLine(data));
// 关闭并提示
await modalApi.close();
emit('success');
ElMessage.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
// 加载数据
const data = modalApi.getData<{ id?: number; noticeId: number }>();
noticeId.value = data.noticeId;
if (!data.id) {
return;
}
modalApi.lock();
try {
formData.value = await getArrivalNoticeLine(data.id);
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
},
});
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" />
</Modal>
</template>

View File

@@ -0,0 +1,139 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmArrivalNoticeLineApi } from '#/api/mes/wm/arrivalnotice/line';
import { computed } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { ElLoading, ElMessage } from 'element-plus';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteArrivalNoticeLine,
getArrivalNoticeLinePage,
} from '#/api/mes/wm/arrivalnotice/line';
import { $t } from '#/locales';
import { type FormType, useLineGridColumns } from '../data';
import LineForm from './line-form.vue';
const props = defineProps<{
formType: FormType;
noticeId: number;
}>();
const isEditable = computed(() => // 是否可编辑明细行
['create', 'update'].includes(props.formType),
);
const [LineFormModal, lineFormModalApi] = useVbenModal({
connectedComponent: LineForm,
destroyOnClose: true,
});
/** 刷新表格 */
function handleRefresh() {
gridApi.query();
}
/** 添加物料 */
function handleCreate() {
lineFormModalApi.setData({ noticeId: props.noticeId }).open();
}
/** 编辑物料 */
function handleEdit(row: MesWmArrivalNoticeLineApi.ArrivalNoticeLine) {
lineFormModalApi.setData({ id: row.id, noticeId: props.noticeId }).open();
}
/** 删除物料 */
async function handleDelete(
row: MesWmArrivalNoticeLineApi.ArrivalNoticeLine,
) {
const loadingInstance = ElLoading.service({
text: $t('ui.actionMessage.deleting', [row.itemName]),
});
try {
await deleteArrivalNoticeLine(row.id!);
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.itemName]));
handleRefresh();
} finally {
loadingInstance.close();
}
}
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useLineGridColumns(isEditable.value),
height: 400,
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }) => {
if (!props.noticeId) {
return { list: [], total: 0 };
}
return await getArrivalNoticeLinePage({
noticeId: props.noticeId,
pageNo: page.currentPage,
pageSize: page.pageSize,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
},
} as VxeTableGridOptions<MesWmArrivalNoticeLineApi.ArrivalNoticeLine>,
});
</script>
<template>
<div>
<LineFormModal @success="handleRefresh" />
<Grid table-title="物料信息">
<template v-if="isEditable" #toolbar-tools>
<TableAction
:actions="[
{
label: '添加物料',
type: 'primary',
icon: ACTION_ICON.ADD,
onClick: handleCreate,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'primary',
link: true,
icon: ACTION_ICON.EDIT,
ifShow: isEditable,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'danger',
link: true,
icon: ACTION_ICON.DELETE,
ifShow: isEditable,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.itemName]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</div>
</template>

View File

@@ -0,0 +1,534 @@
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmArrivalNoticeApi } from '#/api/mes/wm/arrivalnotice';
import type { MesWmArrivalNoticeLineApi } from '#/api/mes/wm/arrivalnotice/line';
import type { MesWmItemReceiptApi } from '#/api/mes/wm/itemreceipt';
import type { MesWmItemReceiptDetailApi } from '#/api/mes/wm/itemreceipt/detail';
import type { MesWmItemReceiptLineApi } from '#/api/mes/wm/itemreceipt/line';
import { h, markRaw } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { ElButton } from 'element-plus';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { getRangePickerDefaultProps } from '#/utils';
import MdItemSelect from '#/views/mes/md/item/components/md-item-select.vue';
import MdVendorSelect from '#/views/mes/md/vendor/components/md-vendor-select.vue';
import {
MesAutoCodeRuleCode,
MesWmArrivalNoticeStatusEnum,
} from '#/views/mes/utils/constants';
import {
WmArrivalNoticeLineSelect,
WmArrivalNoticeSelect,
} from '#/views/mes/wm/arrivalnotice/components';
import {
WmWarehouseAreaSelect,
WmWarehouseLocationSelect,
WmWarehouseSelect,
} from '#/views/mes/wm/warehouse/components';
/** 表单类型 */
export type FormType = 'create' | 'detail' | 'finish' | 'stock' | 'update';
/** 表单头部是否只读(上架、详情、入库态) */
function isHeaderReadonly(formType: FormType): boolean {
return (
formType === 'detail' || formType === 'finish' || formType === 'stock'
);
}
/** 新增/修改的表单 */
export function useFormSchema(
formType: FormType,
formApi?: VbenFormApi,
): VbenFormSchema[] {
return [
{
fieldName: 'id',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'status',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'code',
label: '入库单编号',
component: 'Input',
componentProps: {
placeholder: '请输入入库单编号',
},
rules: 'required',
suffix: isHeaderReadonly(formType)
? undefined
: () =>
h(
ElButton,
{
onClick: async () => {
const code = await generateAutoCode(
MesAutoCodeRuleCode.WM_ITEM_RECEIPT_CODE,
);
await formApi?.setFieldValue('code', code);
},
},
{ default: () => '生成' },
),
},
{
fieldName: 'name',
label: '入库单名称',
component: 'Input',
componentProps: {
placeholder: '请输入入库单名称',
},
rules: 'required',
},
{
fieldName: 'receiptDate',
label: '入库日期',
component: 'DatePicker',
componentProps: {
class: '!w-full',
format: 'YYYY-MM-DD',
placeholder: '请选择入库日期',
type: 'date',
valueFormat: 'x',
},
rules: 'required',
},
{
fieldName: 'noticeId',
label: '到货通知单',
component: markRaw(WmArrivalNoticeSelect),
componentProps: {
// 选择到货通知单后,自动回填供应商和采购订单号
onChange: async (notice?: MesWmArrivalNoticeApi.ArrivalNotice) => {
await formApi?.setValues({
purchaseOrderCode: notice?.purchaseOrderCode,
vendorId: notice?.vendorId,
});
},
status: MesWmArrivalNoticeStatusEnum.PENDING_RECEIPT,
},
},
{
fieldName: 'vendorId',
label: '供应商',
component: markRaw(MdVendorSelect),
componentProps: {
placeholder: '请选择供应商',
},
rules: 'selectRequired',
},
{
fieldName: 'purchaseOrderCode',
label: '采购订单号',
component: 'Input',
componentProps: {
placeholder: '请输入采购订单号',
},
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
formItemClass: 'col-span-3',
componentProps: {
placeholder: '请输入备注',
rows: 3,
},
},
];
}
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'code',
label: '入库单编号',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入入库单编号',
},
},
{
fieldName: 'name',
label: '入库单名称',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入入库单名称',
},
},
{
fieldName: 'vendorId',
label: '供应商',
component: markRaw(MdVendorSelect),
componentProps: {
placeholder: '请选择供应商',
},
},
{
fieldName: 'receiptDate',
label: '入库日期',
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
clearable: true,
},
},
];
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions<MesWmItemReceiptApi.ItemReceipt>['columns'] {
return [
{
field: 'code',
title: '入库单编号',
minWidth: 160,
slots: { default: 'code' },
},
{
field: 'name',
title: '入库单名称',
minWidth: 150,
},
{
field: 'purchaseOrderCode',
title: '采购订单号',
minWidth: 140,
},
{
field: 'vendorName',
title: '供应商名称',
minWidth: 120,
},
{
field: 'receiptDate',
title: '入库日期',
width: 180,
formatter: 'formatDate',
},
{
field: 'status',
title: '单据状态',
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.MES_WM_ITEM_RECEIPT_STATUS },
},
},
{
title: '操作',
width: 240,
fixed: 'right',
slots: { default: 'actions' },
},
];
}
/** 入库单行子表的字段 */
export function useLineGridColumns(
editable: boolean,
stockable: boolean,
): VxeTableGridOptions<MesWmItemReceiptLineApi.ItemReceiptLine>['columns'] {
return [
{
type: 'expand',
width: 48,
slots: { content: 'detail' },
},
{
field: 'itemCode',
title: '物料编码',
minWidth: 120,
},
{
field: 'itemName',
title: '物料名称',
minWidth: 140,
},
{
field: 'specification',
title: '规格型号',
minWidth: 120,
},
{
field: 'unitMeasureName',
title: '单位',
width: 80,
},
{
field: 'receivedQuantity',
title: '入库数量',
width: 100,
},
{
field: 'batchCode',
title: '批次号',
minWidth: 120,
},
...(editable || stockable
? [
{
title: '操作',
width: 200,
fixed: 'right',
slots: { default: 'actions' },
} as const,
]
: []),
];
}
/** 入库单行新增/修改的表单 */
export function useLineFormSchema(
hasNotice: boolean,
formApi?: VbenFormApi,
): VbenFormSchema[] {
return [
{
fieldName: 'noticeId',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'arrivalNoticeLineId',
label: '到货通知单行',
component: markRaw(WmArrivalNoticeLineSelect),
componentProps: {
// 选择到货通知单行后,自动回填物料和入库数量
onChange: async (
line?: MesWmArrivalNoticeLineApi.ArrivalNoticeLine,
) => {
await formApi?.setValues({
itemId: line?.itemId,
receivedQuantity: line?.arrivalQuantity,
});
},
},
dependencies: {
triggerFields: ['noticeId'],
if: () => hasNotice,
componentProps: (values) => ({
noticeId: values.noticeId,
}),
},
},
{
fieldName: 'itemId',
label: '物料',
component: markRaw(MdItemSelect),
componentProps: {
placeholder: '请选择物料',
},
rules: 'selectRequired',
dependencies: {
triggerFields: ['arrivalNoticeLineId'],
componentProps: (values) => ({
disabled: !!values.arrivalNoticeLineId,
placeholder: '请选择物料',
}),
},
},
{
fieldName: 'receivedQuantity',
label: '入库数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
controlsPosition: 'right',
min: 0,
placeholder: '请输入入库数量',
precision: 2,
},
rules: 'required',
},
{
fieldName: 'productionDate',
label: '生产日期',
component: 'DatePicker',
componentProps: {
class: '!w-full',
format: 'YYYY-MM-DD',
placeholder: '请选择生产日期',
type: 'date',
valueFormat: 'x',
},
},
{
fieldName: 'expireDate',
label: '有效期',
component: 'DatePicker',
componentProps: {
class: '!w-full',
format: 'YYYY-MM-DD',
placeholder: '请选择有效期',
type: 'date',
valueFormat: 'x',
},
},
{
fieldName: 'lotNumber',
label: '生产批号',
component: 'Input',
componentProps: {
placeholder: '请输入生产批号',
},
},
{
fieldName: 'batchCode',
label: '批次号',
component: 'Input',
componentProps: {
disabled: true,
placeholder: '由填写信息自动生成',
},
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
formItemClass: 'col-span-3',
componentProps: {
placeholder: '请输入备注',
rows: 3,
},
},
];
}
/** 上架明细子表的字段 */
export function useDetailGridColumns(
stockable: boolean,
): VxeTableGridOptions<MesWmItemReceiptDetailApi.ItemReceiptDetail>['columns'] {
return [
{
field: 'warehouseName',
title: '仓库名称',
minWidth: 100,
},
{
field: 'locationName',
title: '库区名称',
minWidth: 100,
},
{
field: 'areaName',
title: '库位名称',
minWidth: 100,
},
{
field: 'quantity',
title: '数量',
width: 100,
},
...(stockable
? [
{
title: '操作',
width: 120,
fixed: 'right',
slots: { default: 'actions' },
} as const,
]
: []),
];
}
/** 上架明细新增/修改的表单 */
export function useDetailFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
return [
{
fieldName: 'itemId',
label: '物料',
component: markRaw(MdItemSelect),
componentProps: {
disabled: true,
},
},
{
fieldName: 'warehouseId',
label: '入库仓库',
component: markRaw(WmWarehouseSelect),
componentProps: {
// 切换仓库后清空库区和库位
onChange: async () => {
await formApi?.setValues({
areaId: undefined,
locationId: undefined,
});
},
placeholder: '请选择仓库',
},
rules: 'selectRequired',
},
{
fieldName: 'locationId',
label: '库区',
component: markRaw(WmWarehouseLocationSelect),
componentProps: {
placeholder: '请选择库区',
},
rules: 'selectRequired',
dependencies: {
triggerFields: ['warehouseId'],
componentProps: (values) => ({
// 切换库区后清空库位
onChange: async () => {
await formApi?.setFieldValue('areaId', undefined);
},
placeholder: '请选择库区',
warehouseId: values.warehouseId,
}),
},
},
{
fieldName: 'areaId',
label: '库位',
component: markRaw(WmWarehouseAreaSelect),
componentProps: {
placeholder: '请选择库位',
},
rules: 'selectRequired',
dependencies: {
triggerFields: ['locationId'],
componentProps: (values) => ({
locationId: values.locationId,
placeholder: '请选择库位',
}),
},
},
{
fieldName: 'quantity',
label: '数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
controlsPosition: 'right',
min: 0,
placeholder: '请输入数量',
precision: 2,
},
rules: 'required',
},
];
}

View File

@@ -0,0 +1,211 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmItemReceiptApi } from '#/api/mes/wm/itemreceipt';
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { ElButton, ElLoading, ElMessage } from 'element-plus';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
cancelItemReceipt,
deleteItemReceipt,
exportItemReceipt,
getItemReceiptPage,
} from '#/api/mes/wm/itemreceipt';
import { $t } from '#/locales';
import { MesWmItemReceiptStatusEnum } from '#/views/mes/utils/constants';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/** 刷新表格 */
function handleRefresh() {
gridApi.query();
}
/** 创建采购入库单 */
function handleCreate() {
formModalApi.setData({ formType: 'create' }).open();
}
/** 查看采购入库单 */
function handleDetail(row: MesWmItemReceiptApi.ItemReceipt) {
formModalApi.setData({ formType: 'detail', id: row.id }).open();
}
/** 编辑采购入库单 */
function handleEdit(row: MesWmItemReceiptApi.ItemReceipt) {
formModalApi.setData({ formType: 'update', id: row.id }).open();
}
/** 执行上架 */
function handleStock(row: MesWmItemReceiptApi.ItemReceipt) {
formModalApi.setData({ formType: 'stock', id: row.id }).open();
}
/** 执行入库 */
function handleFinish(row: MesWmItemReceiptApi.ItemReceipt) {
formModalApi.setData({ formType: 'finish', id: row.id }).open();
}
/** 删除采购入库单 */
async function handleDelete(row: MesWmItemReceiptApi.ItemReceipt) {
const loadingInstance = ElLoading.service({
text: $t('ui.actionMessage.deleting', [row.code]),
});
try {
await deleteItemReceipt(row.id!);
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.code]));
handleRefresh();
} finally {
loadingInstance.close();
}
}
/** 取消采购入库单 */
async function handleCancel(row: MesWmItemReceiptApi.ItemReceipt) {
await cancelItemReceipt(row.id!);
ElMessage.success('取消成功');
handleRefresh();
}
/** 导出表格 */
async function handleExport() {
const data = await exportItemReceipt(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '采购入库单.xls', source: data });
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getItemReceiptPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
search: true,
},
} as VxeTableGridOptions<MesWmItemReceiptApi.ItemReceipt>,
});
</script>
<template>
<Page auto-content-height>
<template #doc>
<DocAlert
title="【仓库】到货通知、采购入库、采购退货"
url="https://doc.iocoder.cn/mes/wm/purchase-in/"
/>
</template>
<FormModal @success="handleRefresh" />
<Grid table-title="采购入库单列表">
<template #toolbar-tools>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['采购入库单']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['mes:wm-item-receipt:create'],
onClick: handleCreate,
},
{
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
auth: ['mes:wm-item-receipt:export'],
onClick: handleExport,
},
]"
/>
</template>
<template #code="{ row }">
<ElButton link type="primary" @click="handleDetail(row)">
{{ row.code }}
</ElButton>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'primary',
link: true,
icon: ACTION_ICON.EDIT,
auth: ['mes:wm-item-receipt:update'],
ifShow: row.status === MesWmItemReceiptStatusEnum.PREPARE,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'danger',
link: true,
icon: ACTION_ICON.DELETE,
auth: ['mes:wm-item-receipt:delete'],
ifShow: row.status === MesWmItemReceiptStatusEnum.PREPARE,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.code]),
confirm: handleDelete.bind(null, row),
},
},
{
label: '执行上架',
type: 'primary',
link: true,
auth: ['mes:wm-item-receipt:update'],
ifShow: row.status === MesWmItemReceiptStatusEnum.APPROVING,
onClick: handleStock.bind(null, row),
},
{
label: '执行入库',
type: 'primary',
link: true,
auth: ['mes:wm-item-receipt:finish'],
ifShow: row.status === MesWmItemReceiptStatusEnum.APPROVED,
onClick: handleFinish.bind(null, row),
},
{
label: '取消',
type: 'danger',
link: true,
auth: ['mes:wm-item-receipt:update'],
ifShow:
row.status === MesWmItemReceiptStatusEnum.APPROVING ||
row.status === MesWmItemReceiptStatusEnum.APPROVED,
popConfirm: {
title: '确认取消该采购入库单?取消后不可恢复。',
confirm: handleCancel.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,104 @@
<script lang="ts" setup>
import type { MesWmItemReceiptDetailApi } from '#/api/mes/wm/itemreceipt/detail';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { ElMessage } from 'element-plus';
import { useVbenForm } from '#/adapter/form';
import {
createItemReceiptDetail,
getItemReceiptDetail,
updateItemReceiptDetail,
} from '#/api/mes/wm/itemreceipt/detail';
import { $t } from '#/locales';
import { useDetailFormSchema } from '../data';
const emit = defineEmits<{ success: [lineId: number] }>();
const formData = ref<MesWmItemReceiptDetailApi.ItemReceiptDetail>();
const receiptId = ref<number>(); // 所属入库单编号
const lineId = ref<number>(); // 所属入库单行编号
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['上架明细'])
: $t('ui.actionTitle.create', ['上架明细']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-1',
labelWidth: 90,
},
layout: 'horizontal',
schema: useDetailFormSchema(),
showDefaultActions: false,
wrapperClass: 'grid-cols-3',
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data =
(await formApi.getValues()) as MesWmItemReceiptDetailApi.ItemReceiptDetail;
data.receiptId = receiptId.value;
data.lineId = lineId.value;
try {
await (formData.value?.id
? updateItemReceiptDetail({ ...data, id: formData.value.id })
: createItemReceiptDetail(data));
// 关闭并提示
await modalApi.close();
emit('success', lineId.value!);
ElMessage.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
formApi.setState({ schema: useDetailFormSchema(formApi) });
// 加载数据
const data = modalApi.getData<{
detailId?: number;
itemId?: number;
lineId: number;
receiptId: number;
}>();
receiptId.value = data.receiptId;
lineId.value = data.lineId;
if (data.detailId) {
modalApi.lock();
try {
formData.value = await getItemReceiptDetail(data.detailId);
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
} else if (data.itemId) {
await formApi.setFieldValue('itemId', data.itemId);
}
},
});
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" />
</Modal>
</template>

View File

@@ -0,0 +1,103 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmItemReceiptDetailApi } from '#/api/mes/wm/itemreceipt/detail';
import { computed, watch } from 'vue';
import { ElLoading, ElMessage } from 'element-plus';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteItemReceiptDetail } from '#/api/mes/wm/itemreceipt/detail';
import { $t } from '#/locales';
import { type FormType, useDetailGridColumns } from '../data';
const props = defineProps<{
details: MesWmItemReceiptDetailApi.ItemReceiptDetail[];
formType: FormType;
}>();
const emit = defineEmits<{
edit: [detailId: number];
refresh: [];
}>();
const isStock = computed(() => props.formType === 'stock'); // 是否为上架模式
/** 编辑上架明细 */
function handleEdit(row: MesWmItemReceiptDetailApi.ItemReceiptDetail) {
emit('edit', row.id!);
}
/** 删除上架明细 */
async function handleDelete(
row: MesWmItemReceiptDetailApi.ItemReceiptDetail,
) {
const loadingInstance = ElLoading.service({
text: $t('ui.actionMessage.deleting', [row.warehouseName]),
});
try {
await deleteItemReceiptDetail(row.id!);
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.warehouseName]));
emit('refresh');
} finally {
loadingInstance.close();
}
}
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
border: true,
columns: useDetailGridColumns(isStock.value),
data: props.details,
pagerConfig: {
enabled: false,
},
rowConfig: {
keyField: 'id',
isHover: true,
},
size: 'small',
toolbarConfig: {
enabled: false,
},
} as VxeTableGridOptions<MesWmItemReceiptDetailApi.ItemReceiptDetail>,
});
watch(
() => props.details,
(details) => gridApi.setGridOptions({ data: details }),
);
</script>
<template>
<div class="px-4 py-2">
<Grid>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'primary',
link: true,
icon: ACTION_ICON.EDIT,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'danger',
link: true,
icon: ACTION_ICON.DELETE,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [
row.warehouseName,
]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</div>
</template>

View File

@@ -0,0 +1,241 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { MesWmItemReceiptApi } from '#/api/mes/wm/itemreceipt';
import { computed, ref } from 'vue';
import { confirm, useVbenModal } from '@vben/common-ui';
import { ElButton, ElDivider, ElMessage, ElPopconfirm } from 'element-plus';
import { useVbenForm } from '#/adapter/form';
import {
createItemReceipt,
finishItemReceipt,
getItemReceipt,
stockItemReceipt,
submitItemReceipt,
updateItemReceipt,
} from '#/api/mes/wm/itemreceipt';
import { $t } from '#/locales';
import { MesWmItemReceiptStatusEnum } from '#/views/mes/utils/constants';
import { useFormSchema } from '../data';
import LineList from './line-list.vue';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const formData = ref<MesWmItemReceiptApi.ItemReceipt>();
const originalSnapshot = ref(''); // 表单原始数据快照,用于提交时跳过未变更的保存请求
const isEditable = computed(() => // 是否为编辑模式(可保存)
['create', 'update'].includes(formType.value),
);
const isStock = computed(() => formType.value === 'stock'); // 是否为上架模式
const isFinish = computed(() => formType.value === 'finish'); // 是否为执行入库模式
const canSubmit = computed(() => // 是否可提交
formType.value === 'update' &&
formData.value?.status === MesWmItemReceiptStatusEnum.PREPARE,
);
const getTitle = computed(() => {
if (formType.value === 'detail') {
return $t('ui.actionTitle.view', ['采购入库单']);
}
if (formType.value === 'stock') {
return '执行上架';
}
if (formType.value === 'finish') {
return '执行入库';
}
return formType.value === 'update'
? $t('ui.actionTitle.edit', ['采购入库单'])
: $t('ui.actionTitle.create', ['采购入库单']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-1',
labelWidth: 110,
},
layout: 'horizontal',
schema: [],
showDefaultActions: false,
wrapperClass: 'grid-cols-3',
});
/** 提交入库单:表单有修改时先保存,再调用提交接口 */
async function handleSubmit() {
const { valid } = await formApi.validate();
if (!valid || !formData.value?.id) {
return;
}
modalApi.lock();
try {
const current = JSON.stringify(await formApi.getValues());
if (current !== originalSnapshot.value) {
const data = (await formApi.getValues()) as MesWmItemReceiptApi.ItemReceipt;
await updateItemReceipt({ ...formData.value, ...data });
originalSnapshot.value = current;
}
await submitItemReceipt(formData.value.id);
ElMessage.success('提交成功');
await modalApi.close();
emit('success');
} finally {
modalApi.unlock();
}
}
/** 执行上架 */
async function handleStock() {
if (!formData.value?.id) {
return;
}
modalApi.lock();
try {
await stockItemReceipt(formData.value.id);
ElMessage.success('上架成功');
await modalApi.close();
emit('success');
} finally {
modalApi.unlock();
}
}
/** 执行入库 */
async function handleFinish() {
if (!formData.value?.id) {
return;
}
modalApi.lock();
try {
await finishItemReceipt(formData.value.id);
ElMessage.success('入库成功');
await modalApi.close();
emit('success');
} finally {
modalApi.unlock();
}
}
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
if (!isEditable.value) {
await modalApi.close();
return;
}
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data = (await formApi.getValues()) as MesWmItemReceiptApi.ItemReceipt;
try {
if (formData.value?.id) {
await updateItemReceipt({ ...formData.value, ...data });
formData.value = { ...formData.value, ...data };
} else {
const id = await createItemReceipt(data);
formData.value = {
...data,
id,
status: MesWmItemReceiptStatusEnum.PREPARE,
};
await formApi.setFieldValue('id', id);
await formApi.setFieldValue('status', formData.value.status);
formType.value = 'update';
}
originalSnapshot.value = JSON.stringify(await formApi.getValues());
emit('success');
ElMessage.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
originalSnapshot.value = '';
return;
}
// 加载数据
const data = modalApi.getData<{ formType: FormType; id?: number }>();
formType.value = data.formType;
formApi.setState({ schema: useFormSchema(formType.value, formApi) });
formApi.setDisabled(!isEditable.value);
modalApi.setState({ showConfirmButton: isEditable.value });
if (data?.id) {
modalApi.lock();
try {
formData.value = await getItemReceipt(data.id);
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
}
originalSnapshot.value = JSON.stringify(await formApi.getValues());
},
});
/** 上架前确认 */
async function confirmStock() {
try {
await confirm('确认执行上架?');
} catch {
return;
}
await handleStock();
}
/** 入库前确认 */
async function confirmFinish() {
try {
await confirm('确认执行入库?执行后将更新库存台账。');
} catch {
return;
}
await handleFinish();
}
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" />
<!-- 非新建模式展示物料信息 -->
<template v-if="formData?.id">
<ElDivider>物料信息</ElDivider>
<div class="mx-4">
<LineList
:form-type="formType"
:notice-id="formData.noticeId"
:receipt-id="formData.id"
/>
</div>
</template>
<template #prepend-footer>
<div class="flex flex-auto items-center gap-2">
<ElPopconfirm
v-if="canSubmit"
title="确认提交该采购入库单?【提交后将不能修改】"
width="260"
@confirm="handleSubmit"
>
<template #reference>
<ElButton type="primary">提交</ElButton>
</template>
</ElPopconfirm>
<ElButton v-if="isStock" type="primary" @click="confirmStock">
执行上架
</ElButton>
<ElButton v-if="isFinish" type="primary" @click="confirmFinish">
执行入库
</ElButton>
</div>
</template>
</Modal>
</template>

View File

@@ -0,0 +1,104 @@
<script lang="ts" setup>
import type { MesWmItemReceiptLineApi } from '#/api/mes/wm/itemreceipt/line';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { ElMessage } from 'element-plus';
import { useVbenForm } from '#/adapter/form';
import {
createItemReceiptLine,
getItemReceiptLine,
updateItemReceiptLine,
} from '#/api/mes/wm/itemreceipt/line';
import { $t } from '#/locales';
import { useLineFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<MesWmItemReceiptLineApi.ItemReceiptLine>();
const receiptId = ref<number>(); // 所属入库单编号
const noticeId = ref<number>(); // 所属到货通知单编号
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['物料入库单行'])
: $t('ui.actionTitle.create', ['物料入库单行']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-1',
labelWidth: 110,
},
layout: 'horizontal',
schema: [],
showDefaultActions: false,
wrapperClass: 'grid-cols-3',
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data =
(await formApi.getValues()) as MesWmItemReceiptLineApi.ItemReceiptLine;
data.receiptId = receiptId.value;
try {
await (formData.value?.id
? updateItemReceiptLine({ ...data, id: formData.value.id })
: createItemReceiptLine(data));
// 关闭并提示
await modalApi.close();
emit('success');
ElMessage.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
// 加载数据
const data = modalApi.getData<{
id?: number;
noticeId?: number;
receiptId: number;
}>();
receiptId.value = data.receiptId;
noticeId.value = data.noticeId;
formApi.setState({ schema: useLineFormSchema(!!data.noticeId, formApi) });
if (data.noticeId) {
await formApi.setFieldValue('noticeId', data.noticeId);
}
if (!data.id) {
return;
}
modalApi.lock();
try {
formData.value = await getItemReceiptLine(data.id);
// 设置到 values
await formApi.setValues({ ...formData.value, noticeId: noticeId.value });
} finally {
modalApi.unlock();
}
},
});
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" />
</Modal>
</template>

View File

@@ -0,0 +1,239 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmItemReceiptDetailApi } from '#/api/mes/wm/itemreceipt/detail';
import type { MesWmItemReceiptLineApi } from '#/api/mes/wm/itemreceipt/line';
import { computed, reactive } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { ElLoading, ElMessage } from 'element-plus';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getItemReceiptDetailListByLineId } from '#/api/mes/wm/itemreceipt/detail';
import {
deleteItemReceiptLine,
getItemReceiptLinePage,
} from '#/api/mes/wm/itemreceipt/line';
import { $t } from '#/locales';
import { PrinterLabel } from '#/views/mes/wm/barcode/components';
import { type FormType, useLineGridColumns } from '../data';
import DetailForm from './detail-form.vue';
import DetailList from './detail-list.vue';
import LineForm from './line-form.vue';
const props = defineProps<{
formType: FormType;
noticeId?: number;
receiptId: number;
}>();
const isEditable = computed(() => // 是否可编辑明细行
['create', 'update'].includes(props.formType),
);
const isStock = computed(() => props.formType === 'stock'); // 是否为上架模式
const detailMap = reactive<
Record<number, MesWmItemReceiptDetailApi.ItemReceiptDetail[]>
>({}); // 已展开行的上架明细缓存
const [LineFormModal, lineFormModalApi] = useVbenModal({
connectedComponent: LineForm,
destroyOnClose: true,
});
const [DetailFormModal, detailFormModalApi] = useVbenModal({
connectedComponent: DetailForm,
destroyOnClose: true,
});
/** 刷新表格 */
function handleRefresh() {
for (const id of Object.keys(detailMap)) {
delete detailMap[Number(id)];
}
gridApi.query();
}
/** 添加物料 */
function handleCreate() {
lineFormModalApi
.setData({ noticeId: props.noticeId, receiptId: props.receiptId })
.open();
}
/** 编辑物料 */
function handleEdit(row: MesWmItemReceiptLineApi.ItemReceiptLine) {
lineFormModalApi
.setData({
id: row.id,
noticeId: props.noticeId,
receiptId: props.receiptId,
})
.open();
}
/** 删除物料 */
async function handleDelete(row: MesWmItemReceiptLineApi.ItemReceiptLine) {
const loadingInstance = ElLoading.service({
text: $t('ui.actionMessage.deleting', [row.itemName]),
});
try {
await deleteItemReceiptLine(row.id!);
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.itemName]));
handleRefresh();
} finally {
loadingInstance.close();
}
}
/** 上架:直接打开明细创建表单 */
function handleStock(row: MesWmItemReceiptLineApi.ItemReceiptLine) {
openDetailForm(row.id!, row.itemId);
}
/** 打开上架明细表单 */
function openDetailForm(lineId: number, itemId?: number, detailId?: number) {
detailFormModalApi
.setData({ detailId, itemId, lineId, receiptId: props.receiptId })
.open();
}
/** 获取已展开行的上架明细 */
function getExpandedDetails(row: MesWmItemReceiptLineApi.ItemReceiptLine) {
return detailMap[row.id!] || [];
}
/** 加载指定行的上架明细 */
async function loadLineDetails(lineId: number) {
detailMap[lineId] = await getItemReceiptDetailListByLineId(lineId);
}
/** 展开行时懒加载上架明细 */
async function handleExpandChange(
row: MesWmItemReceiptLineApi.ItemReceiptLine,
expanded: boolean,
) {
if (!expanded) {
return;
}
await loadLineDetails(row.id!);
}
/** 明细表单提交成功后,刷新对应行已展开的明细 */
async function handleDetailSuccess(lineId: number) {
await loadLineDetails(lineId);
}
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useLineGridColumns(isEditable.value, isStock.value),
expandConfig: {
padding: true,
},
height: 400,
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }) => {
if (!props.receiptId) {
return { list: [], total: 0 };
}
return await getItemReceiptLinePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
receiptId: props.receiptId,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
},
} as VxeTableGridOptions<MesWmItemReceiptLineApi.ItemReceiptLine>,
gridEvents: {
toggleRowExpand: ({
expanded,
row,
}: {
expanded: boolean;
row: MesWmItemReceiptLineApi.ItemReceiptLine;
}) => {
handleExpandChange(row, expanded);
},
},
});
</script>
<template>
<div>
<LineFormModal @success="handleRefresh" />
<DetailFormModal @success="handleDetailSuccess" />
<Grid table-title="物料信息">
<template v-if="isEditable" #toolbar-tools>
<TableAction
:actions="[
{
label: '添加物料',
type: 'primary',
icon: ACTION_ICON.ADD,
onClick: handleCreate,
},
]"
/>
</template>
<template #detail="{ row }">
<DetailList
:details="getExpandedDetails(row)"
:form-type="formType"
@edit="(detailId) => openDetailForm(row.id!, row.itemId, detailId)"
@refresh="loadLineDetails(row.id!)"
/>
</template>
<template #actions="{ row }">
<div class="flex items-center justify-center">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'primary',
link: true,
icon: ACTION_ICON.EDIT,
ifShow: isEditable,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'danger',
link: true,
icon: ACTION_ICON.DELETE,
ifShow: isEditable,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.itemName]),
confirm: handleDelete.bind(null, row),
},
},
{
label: '上架',
type: 'primary',
link: true,
ifShow: isStock,
onClick: handleStock.bind(null, row),
},
]"
/>
<PrinterLabel
v-if="isStock"
:biz-code="row.batchCode"
:biz-id="row.batchId"
biz-type="ITEM_BATCH"
/>
</div>
</template>
</Grid>
</div>
</template>

View File

@@ -0,0 +1,476 @@
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmMaterialStockApi } from '#/api/mes/wm/materialstock';
import type { MesWmProductIssueApi } from '#/api/mes/wm/productissue';
import type { MesWmProductIssueDetailApi } from '#/api/mes/wm/productissue/detail';
import type { MesWmProductIssueLineApi } from '#/api/mes/wm/productissue/line';
import { h, markRaw } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { ElButton } from 'element-plus';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { getRangePickerDefaultProps } from '#/utils';
import MdItemSelect from '#/views/mes/md/item/components/md-item-select.vue';
import MdWorkstationSelect from '#/views/mes/md/workstation/components/md-workstation-select.vue';
import ProWorkOrderSelect from '#/views/mes/pro/workorder/components/pro-work-order-select.vue';
import {
MesAutoCodeRuleCode,
MesProWorkOrderStatusEnum,
} from '#/views/mes/utils/constants';
import { WmMaterialStockSelect } from '#/views/mes/wm/materialstock/components';
import {
WmWarehouseAreaSelect,
WmWarehouseLocationSelect,
WmWarehouseSelect,
} from '#/views/mes/wm/warehouse/components';
/** 表单类型 */
export type FormType = 'create' | 'detail' | 'finish' | 'stock' | 'update';
/** 表单头部是否只读(拣货、详情、完成态) */
function isHeaderReadonly(formType: FormType): boolean {
return (
formType === 'detail' || formType === 'finish' || formType === 'stock'
);
}
/** 新增/修改的表单 */
export function useFormSchema(
formType: FormType,
formApi?: VbenFormApi,
): VbenFormSchema[] {
return [
{
fieldName: 'id',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'status',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'code',
label: '领料单编号',
component: 'Input',
componentProps: {
placeholder: '请输入领料单编号',
},
rules: 'required',
suffix: isHeaderReadonly(formType)
? undefined
: () =>
h(
ElButton,
{
onClick: async () => {
const code = await generateAutoCode(
MesAutoCodeRuleCode.WM_PRODUCT_ISSUE_CODE,
);
await formApi?.setFieldValue('code', code);
},
},
{ default: () => '生成' },
),
},
{
fieldName: 'name',
label: '领料单名称',
component: 'Input',
componentProps: {
placeholder: '请输入领料单名称',
},
rules: 'required',
},
{
fieldName: 'requiredTime',
label: '需求时间',
component: 'DatePicker',
componentProps: {
class: '!w-full',
format: 'YYYY-MM-DD HH:mm:ss',
placeholder: '请选择需求时间',
type: 'datetime',
valueFormat: 'x',
},
rules: 'required',
},
{
fieldName: 'workOrderId',
label: '生产工单',
component: markRaw(ProWorkOrderSelect),
componentProps: {
status: MesProWorkOrderStatusEnum.CONFIRMED,
},
rules: 'selectRequired',
},
{
fieldName: 'workstationId',
label: '工作站',
component: markRaw(MdWorkstationSelect),
componentProps: {
placeholder: '请选择工作站',
},
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
formItemClass: 'col-span-3',
componentProps: {
placeholder: '请输入备注',
rows: 3,
},
},
];
}
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'code',
label: '领料单编号',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入领料单编号',
},
},
{
fieldName: 'name',
label: '领料单名称',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入领料单名称',
},
},
{
fieldName: 'issueDate',
label: '领料日期',
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
clearable: true,
},
},
{
fieldName: 'status',
label: '单据状态',
component: 'Select',
componentProps: {
clearable: true,
options: getDictOptions(DICT_TYPE.MES_WM_PRODUCT_ISSUE_STATUS, 'number'),
placeholder: '请选择单据状态',
},
},
];
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions<MesWmProductIssueApi.ProductIssue>['columns'] {
return [
{
field: 'code',
title: '领料单编号',
minWidth: 160,
slots: { default: 'code' },
},
{
field: 'name',
title: '领料单名称',
minWidth: 150,
},
{
field: 'workOrderCode',
title: '生产工单',
minWidth: 140,
},
{
field: 'workstationName',
title: '工作站',
minWidth: 120,
},
{
field: 'requiredTime',
title: '需求时间',
width: 180,
formatter: 'formatDateTime',
},
{
field: 'status',
title: '单据状态',
minWidth: 110,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.MES_WM_PRODUCT_ISSUE_STATUS },
},
},
{
title: '操作',
width: 240,
fixed: 'right',
slots: { default: 'actions' },
},
];
}
/** 领料单行子表的字段 */
export function useLineGridColumns(
editable: boolean,
stockable: boolean,
): VxeTableGridOptions<MesWmProductIssueLineApi.ProductIssueLine>['columns'] {
return [
{
type: 'expand',
width: 48,
slots: { content: 'detail' },
},
{
field: 'itemCode',
title: '物料编码',
minWidth: 120,
},
{
field: 'itemName',
title: '物料名称',
minWidth: 140,
},
{
field: 'specification',
title: '规格型号',
minWidth: 120,
},
{
field: 'unitMeasureName',
title: '单位',
width: 80,
},
{
field: 'quantity',
title: '领料数量',
width: 100,
},
...(editable || stockable
? [
{
title: '操作',
width: 160,
fixed: 'right',
slots: { default: 'actions' },
} as const,
]
: []),
];
}
/** 领料单行新增/修改的表单 */
export function useLineFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'itemId',
label: '产品物料',
component: markRaw(MdItemSelect),
componentProps: {
placeholder: '请选择产品物料',
},
rules: 'selectRequired',
},
{
fieldName: 'quantity',
label: '领料数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
controlsPosition: 'right',
min: 0,
placeholder: '请输入领料数量',
precision: 2,
},
rules: 'required',
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
formItemClass: 'col-span-3',
componentProps: {
placeholder: '请输入备注',
rows: 3,
},
},
];
}
/** 拣货明细子表的字段 */
export function useDetailGridColumns(
stockable: boolean,
): VxeTableGridOptions<MesWmProductIssueDetailApi.ProductIssueDetail>['columns'] {
return [
{
field: 'batchCode',
title: '批次号',
minWidth: 120,
},
{
field: 'warehouseName',
title: '仓库名称',
minWidth: 100,
},
{
field: 'locationName',
title: '库区名称',
minWidth: 100,
},
{
field: 'areaName',
title: '库位名称',
minWidth: 100,
},
{
field: 'quantity',
title: '数量',
width: 100,
},
...(stockable
? [
{
title: '操作',
width: 120,
fixed: 'right',
slots: { default: 'actions' },
} as const,
]
: []),
];
}
/** 拣货明细新增/修改的表单 */
export function useDetailFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
return [
{
fieldName: 'quantityMax',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'itemId',
label: '物料',
component: markRaw(MdItemSelect),
componentProps: {
disabled: true,
},
},
{
fieldName: 'materialStockId',
label: '库存记录',
component: markRaw(WmMaterialStockSelect),
componentProps: {
// 选择库存记录后,自动回填仓库/库区/库位/批次/数量
onChange: async (stock?: MesWmMaterialStockApi.MaterialStock) => {
await formApi?.setValues({
areaId: stock?.areaId,
batchCode: stock?.batchCode,
batchId: stock?.batchId,
locationId: stock?.locationId,
quantity: stock?.quantity,
quantityMax: stock?.quantity,
warehouseId: stock?.warehouseId,
});
},
},
rules: 'selectRequired',
dependencies: {
triggerFields: ['itemId'],
componentProps: (values) => ({
itemId: values.itemId,
}),
},
},
{
fieldName: 'quantity',
label: '数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
controlsPosition: 'right',
min: 0,
placeholder: '请输入数量',
precision: 2,
},
rules: 'required',
dependencies: {
triggerFields: ['quantityMax'],
componentProps: (values) => ({
class: '!w-full',
controlsPosition: 'right',
max: values.quantityMax,
min: 0,
placeholder: '请输入数量',
precision: 2,
}),
},
},
{
fieldName: 'warehouseId',
label: '出库仓库',
component: markRaw(WmWarehouseSelect),
componentProps: {
disabled: true,
},
},
{
fieldName: 'locationId',
label: '库区',
component: markRaw(WmWarehouseLocationSelect),
componentProps: {
disabled: true,
},
dependencies: {
triggerFields: ['warehouseId'],
componentProps: (values) => ({
disabled: true,
warehouseId: values.warehouseId,
}),
},
},
{
fieldName: 'areaId',
label: '库位',
component: markRaw(WmWarehouseAreaSelect),
componentProps: {
disabled: true,
},
dependencies: {
triggerFields: ['locationId'],
componentProps: (values) => ({
disabled: true,
locationId: values.locationId,
}),
},
},
{
fieldName: 'batchCode',
label: '批次号',
component: 'Input',
componentProps: {
disabled: true,
},
},
];
}

View File

@@ -0,0 +1,230 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmProductIssueApi } from '#/api/mes/wm/productissue';
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { ElButton, ElLoading, ElMessage } from 'element-plus';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
cancelProductIssue,
deleteProductIssue,
exportProductIssue,
getProductIssuePage,
submitProductIssue,
} from '#/api/mes/wm/productissue';
import { $t } from '#/locales';
import { MesWmProductIssueStatusEnum } from '#/views/mes/utils/constants';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/** 刷新表格 */
function handleRefresh() {
gridApi.query();
}
/** 创建领料出库单 */
function handleCreate() {
formModalApi.setData({ formType: 'create' }).open();
}
/** 查看领料出库单 */
function handleDetail(row: MesWmProductIssueApi.ProductIssue) {
formModalApi.setData({ formType: 'detail', id: row.id }).open();
}
/** 编辑领料出库单 */
function handleEdit(row: MesWmProductIssueApi.ProductIssue) {
formModalApi.setData({ formType: 'update', id: row.id }).open();
}
/** 执行拣货 */
function handleStock(row: MesWmProductIssueApi.ProductIssue) {
formModalApi.setData({ formType: 'stock', id: row.id }).open();
}
/** 完成领料出库 */
function handleFinish(row: MesWmProductIssueApi.ProductIssue) {
formModalApi.setData({ formType: 'finish', id: row.id }).open();
}
/** 提交领料出库单 */
async function handleSubmit(row: MesWmProductIssueApi.ProductIssue) {
await submitProductIssue(row.id!);
ElMessage.success('提交成功');
handleRefresh();
}
/** 删除领料出库单 */
async function handleDelete(row: MesWmProductIssueApi.ProductIssue) {
const loadingInstance = ElLoading.service({
text: $t('ui.actionMessage.deleting', [row.code]),
});
try {
await deleteProductIssue(row.id!);
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.code]));
handleRefresh();
} finally {
loadingInstance.close();
}
}
/** 取消领料出库单 */
async function handleCancel(row: MesWmProductIssueApi.ProductIssue) {
await cancelProductIssue(row.id!);
ElMessage.success('取消成功');
handleRefresh();
}
/** 导出表格 */
async function handleExport() {
const data = await exportProductIssue(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '领料出库单.xls', source: data });
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getProductIssuePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
search: true,
},
} as VxeTableGridOptions<MesWmProductIssueApi.ProductIssue>,
});
</script>
<template>
<Page auto-content-height>
<template #doc>
<DocAlert
title="【仓库】生产领料、生产退料、物料消耗"
url="https://doc.iocoder.cn/mes/wm/issue-return/"
/>
</template>
<FormModal @success="handleRefresh" />
<Grid table-title="领料出库单列表">
<template #toolbar-tools>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['领料出库单']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['mes:wm-product-issue:create'],
onClick: handleCreate,
},
{
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
auth: ['mes:wm-product-issue:export'],
onClick: handleExport,
},
]"
/>
</template>
<template #code="{ row }">
<ElButton link type="primary" @click="handleDetail(row)">
{{ row.code }}
</ElButton>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'primary',
link: true,
icon: ACTION_ICON.EDIT,
auth: ['mes:wm-product-issue:update'],
ifShow: row.status === MesWmProductIssueStatusEnum.PREPARE,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.submit'),
type: 'primary',
link: true,
auth: ['mes:wm-product-issue:update'],
ifShow: row.status === MesWmProductIssueStatusEnum.PREPARE,
popConfirm: {
title: '确认提交该领料出库单?提交后将不能修改。',
confirm: handleSubmit.bind(null, row),
},
},
{
label: $t('common.delete'),
type: 'danger',
link: true,
icon: ACTION_ICON.DELETE,
auth: ['mes:wm-product-issue:delete'],
ifShow: row.status === MesWmProductIssueStatusEnum.PREPARE,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.code]),
confirm: handleDelete.bind(null, row),
},
},
{
label: '执行拣货',
type: 'primary',
link: true,
auth: ['mes:wm-product-issue:update'],
ifShow: row.status === MesWmProductIssueStatusEnum.APPROVING,
onClick: handleStock.bind(null, row),
},
{
label: '完成',
type: 'primary',
link: true,
auth: ['mes:wm-product-issue:finish'],
ifShow: row.status === MesWmProductIssueStatusEnum.APPROVED,
onClick: handleFinish.bind(null, row),
},
{
label: '取消',
type: 'danger',
link: true,
auth: ['mes:wm-product-issue:update'],
ifShow:
row.status === MesWmProductIssueStatusEnum.APPROVING ||
row.status === MesWmProductIssueStatusEnum.APPROVED,
popConfirm: {
title: '确认取消该领料出库单?取消后不可恢复。',
confirm: handleCancel.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,104 @@
<script lang="ts" setup>
import type { MesWmProductIssueDetailApi } from '#/api/mes/wm/productissue/detail';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { ElMessage } from 'element-plus';
import { useVbenForm } from '#/adapter/form';
import {
createProductIssueDetail,
getProductIssueDetail,
updateProductIssueDetail,
} from '#/api/mes/wm/productissue/detail';
import { $t } from '#/locales';
import { useDetailFormSchema } from '../data';
const emit = defineEmits<{ success: [lineId: number] }>();
const formData = ref<MesWmProductIssueDetailApi.ProductIssueDetail>();
const issueId = ref<number>(); // 所属领料单编号
const lineId = ref<number>(); // 所属领料单行编号
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['拣货明细'])
: $t('ui.actionTitle.create', ['拣货明细']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-1',
labelWidth: 90,
},
layout: 'horizontal',
schema: useDetailFormSchema(),
showDefaultActions: false,
wrapperClass: 'grid-cols-3',
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data =
(await formApi.getValues()) as MesWmProductIssueDetailApi.ProductIssueDetail;
data.issueId = issueId.value;
data.lineId = lineId.value;
try {
await (formData.value?.id
? updateProductIssueDetail({ ...data, id: formData.value.id })
: createProductIssueDetail(data));
// 关闭并提示
await modalApi.close();
emit('success', lineId.value!);
ElMessage.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
formApi.setState({ schema: useDetailFormSchema(formApi) });
// 加载数据
const data = modalApi.getData<{
detailId?: number;
issueId: number;
itemId?: number;
lineId: number;
}>();
issueId.value = data.issueId;
lineId.value = data.lineId;
if (data.detailId) {
modalApi.lock();
try {
formData.value = await getProductIssueDetail(data.detailId);
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
} else if (data.itemId) {
await formApi.setFieldValue('itemId', data.itemId);
}
},
});
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" />
</Modal>
</template>

View File

@@ -0,0 +1,103 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmProductIssueDetailApi } from '#/api/mes/wm/productissue/detail';
import { computed, watch } from 'vue';
import { ElLoading, ElMessage } from 'element-plus';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteProductIssueDetail } from '#/api/mes/wm/productissue/detail';
import { $t } from '#/locales';
import { type FormType, useDetailGridColumns } from '../data';
const props = defineProps<{
details: MesWmProductIssueDetailApi.ProductIssueDetail[];
formType: FormType;
}>();
const emit = defineEmits<{
edit: [detailId: number];
refresh: [];
}>();
const isStock = computed(() => props.formType === 'stock'); // 是否为拣货模式
/** 编辑拣货明细 */
function handleEdit(row: MesWmProductIssueDetailApi.ProductIssueDetail) {
emit('edit', row.id!);
}
/** 删除拣货明细 */
async function handleDelete(
row: MesWmProductIssueDetailApi.ProductIssueDetail,
) {
const loadingInstance = ElLoading.service({
text: $t('ui.actionMessage.deleting', [row.warehouseName]),
});
try {
await deleteProductIssueDetail(row.id!);
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.warehouseName]));
emit('refresh');
} finally {
loadingInstance.close();
}
}
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
border: true,
columns: useDetailGridColumns(isStock.value),
data: props.details,
pagerConfig: {
enabled: false,
},
rowConfig: {
keyField: 'id',
isHover: true,
},
size: 'small',
toolbarConfig: {
enabled: false,
},
} as VxeTableGridOptions<MesWmProductIssueDetailApi.ProductIssueDetail>,
});
watch(
() => props.details,
(details) => gridApi.setGridOptions({ data: details }),
);
</script>
<template>
<div class="px-4 py-2">
<Grid>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'primary',
link: true,
icon: ACTION_ICON.EDIT,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'danger',
link: true,
icon: ACTION_ICON.DELETE,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [
row.warehouseName,
]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</div>
</template>

View File

@@ -0,0 +1,235 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { MesWmProductIssueApi } from '#/api/mes/wm/productissue';
import { computed, ref } from 'vue';
import { confirm, useVbenModal } from '@vben/common-ui';
import { ElButton, ElDivider, ElMessage, ElPopconfirm } from 'element-plus';
import { useVbenForm } from '#/adapter/form';
import {
checkProductIssueQuantity,
createProductIssue,
finishProductIssue,
getProductIssue,
stockProductIssue,
submitProductIssue,
updateProductIssue,
} from '#/api/mes/wm/productissue';
import { $t } from '#/locales';
import { MesWmProductIssueStatusEnum } from '#/views/mes/utils/constants';
import { useFormSchema } from '../data';
import LineList from './line-list.vue';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const formData = ref<MesWmProductIssueApi.ProductIssue>();
const originalSnapshot = ref(''); // 表单原始数据快照,用于提交时跳过未变更的保存请求
const isEditable = computed(() => // 是否为编辑模式(可保存)
['create', 'update'].includes(formType.value),
);
const isStock = computed(() => formType.value === 'stock'); // 是否为拣货模式
const isFinish = computed(() => formType.value === 'finish'); // 是否为完成出库模式
const canSubmit = computed(() => // 是否可提交
formType.value === 'update' &&
formData.value?.status === MesWmProductIssueStatusEnum.PREPARE,
);
const getTitle = computed(() => {
if (formType.value === 'detail') {
return $t('ui.actionTitle.view', ['领料出库单']);
}
if (formType.value === 'stock') {
return '执行拣货';
}
if (formType.value === 'finish') {
return '完成领料出库';
}
return formType.value === 'update'
? $t('ui.actionTitle.edit', ['领料出库单'])
: $t('ui.actionTitle.create', ['领料出库单']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-1',
labelWidth: 110,
},
layout: 'horizontal',
schema: [],
showDefaultActions: false,
wrapperClass: 'grid-cols-3',
});
/** 提交领料单:表单有修改时先保存,再调用提交接口 */
async function handleSubmit() {
const { valid } = await formApi.validate();
if (!valid || !formData.value?.id) {
return;
}
modalApi.lock();
try {
const current = JSON.stringify(await formApi.getValues());
if (current !== originalSnapshot.value) {
const data =
(await formApi.getValues()) as MesWmProductIssueApi.ProductIssue;
await updateProductIssue({ ...formData.value, ...data });
originalSnapshot.value = current;
}
await submitProductIssue(formData.value.id);
ElMessage.success('提交成功');
await modalApi.close();
emit('success');
} finally {
modalApi.unlock();
}
}
/** 执行拣货:领料数量与拣货数量不一致时二次确认 */
async function handleStock() {
if (!formData.value?.id) {
return;
}
const quantityMatch = await checkProductIssueQuantity(formData.value.id);
if (!quantityMatch) {
try {
await confirm('领料数量与拣货数量不一致,确认执行拣货?');
} catch {
return;
}
}
modalApi.lock();
try {
await stockProductIssue(formData.value.id);
ElMessage.success('拣货成功');
await modalApi.close();
emit('success');
} finally {
modalApi.unlock();
}
}
/** 完成领料出库 */
async function handleFinish() {
if (!formData.value?.id) {
return;
}
modalApi.lock();
try {
await finishProductIssue(formData.value.id);
ElMessage.success('完成成功');
await modalApi.close();
emit('success');
} finally {
modalApi.unlock();
}
}
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
if (!isEditable.value) {
await modalApi.close();
return;
}
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data =
(await formApi.getValues()) as MesWmProductIssueApi.ProductIssue;
try {
if (formData.value?.id) {
await updateProductIssue({ ...formData.value, ...data });
formData.value = { ...formData.value, ...data };
} else {
const id = await createProductIssue(data);
formData.value = {
...data,
id,
status: MesWmProductIssueStatusEnum.PREPARE,
};
await formApi.setFieldValue('id', id);
await formApi.setFieldValue('status', formData.value.status);
formType.value = 'update';
}
originalSnapshot.value = JSON.stringify(await formApi.getValues());
emit('success');
ElMessage.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
originalSnapshot.value = '';
return;
}
// 加载数据
const data = modalApi.getData<{ formType: FormType; id?: number }>();
formType.value = data.formType;
formApi.setState({ schema: useFormSchema(formType.value, formApi) });
formApi.setDisabled(!isEditable.value);
modalApi.setState({ showConfirmButton: isEditable.value });
if (data?.id) {
modalApi.lock();
try {
formData.value = await getProductIssue(data.id);
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
}
originalSnapshot.value = JSON.stringify(await formApi.getValues());
},
});
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" />
<!-- 非新建模式展示物料信息 -->
<template v-if="formData?.id">
<ElDivider>物料信息</ElDivider>
<div class="mx-4">
<LineList :form-type="formType" :issue-id="formData.id" />
</div>
</template>
<template #prepend-footer>
<div class="flex flex-auto items-center gap-2">
<ElPopconfirm
v-if="canSubmit"
title="确认提交该领料出库单?【提交后将不能修改】"
width="260"
@confirm="handleSubmit"
>
<template #reference>
<ElButton type="primary">提交</ElButton>
</template>
</ElPopconfirm>
<ElButton v-if="isStock" type="primary" @click="handleStock">
执行拣货
</ElButton>
<ElPopconfirm
v-if="isFinish"
title="确认完成该领料单并执行出库吗?"
width="260"
@confirm="handleFinish"
>
<template #reference>
<ElButton type="primary">完成</ElButton>
</template>
</ElPopconfirm>
</div>
</template>
</Modal>
</template>

View File

@@ -0,0 +1,94 @@
<script lang="ts" setup>
import type { MesWmProductIssueLineApi } from '#/api/mes/wm/productissue/line';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { ElMessage } from 'element-plus';
import { useVbenForm } from '#/adapter/form';
import {
createProductIssueLine,
getProductIssueLine,
updateProductIssueLine,
} from '#/api/mes/wm/productissue/line';
import { $t } from '#/locales';
import { useLineFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<MesWmProductIssueLineApi.ProductIssueLine>();
const issueId = ref<number>(); // 所属领料单编号
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['领料出库单行'])
: $t('ui.actionTitle.create', ['领料出库单行']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-1',
labelWidth: 90,
},
layout: 'horizontal',
schema: useLineFormSchema(),
showDefaultActions: false,
wrapperClass: 'grid-cols-3',
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data =
(await formApi.getValues()) as MesWmProductIssueLineApi.ProductIssueLine;
data.issueId = issueId.value;
try {
await (formData.value?.id
? updateProductIssueLine({ ...data, id: formData.value.id })
: createProductIssueLine(data));
// 关闭并提示
await modalApi.close();
emit('success');
ElMessage.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
// 加载数据
const data = modalApi.getData<{ id?: number; issueId: number }>();
issueId.value = data.issueId;
if (!data.id) {
return;
}
modalApi.lock();
try {
formData.value = await getProductIssueLine(data.id);
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
},
});
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" />
</Modal>
</template>

View File

@@ -0,0 +1,221 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmProductIssueDetailApi } from '#/api/mes/wm/productissue/detail';
import type { MesWmProductIssueLineApi } from '#/api/mes/wm/productissue/line';
import { computed, reactive } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { ElLoading, ElMessage } from 'element-plus';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getProductIssueDetailListByLineId } from '#/api/mes/wm/productissue/detail';
import {
deleteProductIssueLine,
getProductIssueLinePage,
} from '#/api/mes/wm/productissue/line';
import { $t } from '#/locales';
import { type FormType, useLineGridColumns } from '../data';
import DetailForm from './detail-form.vue';
import DetailList from './detail-list.vue';
import LineForm from './line-form.vue';
const props = defineProps<{
formType: FormType;
issueId: number;
}>();
const isEditable = computed(() => // 是否可编辑明细行
['create', 'update'].includes(props.formType),
);
const isStock = computed(() => props.formType === 'stock'); // 是否为拣货模式
const detailMap = reactive<
Record<number, MesWmProductIssueDetailApi.ProductIssueDetail[]>
>({}); // 已展开行的拣货明细缓存
const [LineFormModal, lineFormModalApi] = useVbenModal({
connectedComponent: LineForm,
destroyOnClose: true,
});
const [DetailFormModal, detailFormModalApi] = useVbenModal({
connectedComponent: DetailForm,
destroyOnClose: true,
});
/** 刷新表格 */
function handleRefresh() {
for (const id of Object.keys(detailMap)) {
delete detailMap[Number(id)];
}
gridApi.query();
}
/** 添加物料 */
function handleCreate() {
lineFormModalApi.setData({ issueId: props.issueId }).open();
}
/** 编辑物料 */
function handleEdit(row: MesWmProductIssueLineApi.ProductIssueLine) {
lineFormModalApi.setData({ id: row.id, issueId: props.issueId }).open();
}
/** 删除物料 */
async function handleDelete(row: MesWmProductIssueLineApi.ProductIssueLine) {
const loadingInstance = ElLoading.service({
text: $t('ui.actionMessage.deleting', [row.itemName]),
});
try {
await deleteProductIssueLine(row.id!);
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.itemName]));
handleRefresh();
} finally {
loadingInstance.close();
}
}
/** 拣货:直接打开明细创建表单 */
function handlePicking(row: MesWmProductIssueLineApi.ProductIssueLine) {
openDetailForm(row.id!, row.itemId);
}
/** 打开拣货明细表单 */
function openDetailForm(lineId: number, itemId?: number, detailId?: number) {
detailFormModalApi
.setData({ detailId, issueId: props.issueId, itemId, lineId })
.open();
}
/** 获取已展开行的拣货明细 */
function getExpandedDetails(row: MesWmProductIssueLineApi.ProductIssueLine) {
return detailMap[row.id!] || [];
}
/** 加载指定行的拣货明细 */
async function loadLineDetails(lineId: number) {
detailMap[lineId] = await getProductIssueDetailListByLineId(lineId);
}
/** 展开行时懒加载拣货明细 */
async function handleExpandChange(
row: MesWmProductIssueLineApi.ProductIssueLine,
expanded: boolean,
) {
if (!expanded) {
return;
}
await loadLineDetails(row.id!);
}
/** 明细表单提交成功后,刷新对应行已展开的明细 */
async function handleDetailSuccess(lineId: number) {
await loadLineDetails(lineId);
}
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useLineGridColumns(isEditable.value, isStock.value),
expandConfig: {
padding: true,
},
height: 400,
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }) => {
if (!props.issueId) {
return { list: [], total: 0 };
}
return await getProductIssueLinePage({
issueId: props.issueId,
pageNo: page.currentPage,
pageSize: page.pageSize,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
},
} as VxeTableGridOptions<MesWmProductIssueLineApi.ProductIssueLine>,
gridEvents: {
toggleRowExpand: ({
expanded,
row,
}: {
expanded: boolean;
row: MesWmProductIssueLineApi.ProductIssueLine;
}) => {
handleExpandChange(row, expanded);
},
},
});
</script>
<template>
<div>
<LineFormModal @success="handleRefresh" />
<DetailFormModal @success="handleDetailSuccess" />
<Grid table-title="物料信息">
<template v-if="isEditable" #toolbar-tools>
<TableAction
:actions="[
{
label: '添加物料',
type: 'primary',
icon: ACTION_ICON.ADD,
onClick: handleCreate,
},
]"
/>
</template>
<template #detail="{ row }">
<DetailList
:details="getExpandedDetails(row)"
:form-type="formType"
@edit="(detailId) => openDetailForm(row.id!, row.itemId, detailId)"
@refresh="loadLineDetails(row.id!)"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'primary',
link: true,
icon: ACTION_ICON.EDIT,
ifShow: isEditable,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'danger',
link: true,
icon: ACTION_ICON.DELETE,
ifShow: isEditable,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.itemName]),
confirm: handleDelete.bind(null, row),
},
},
{
label: '拣货',
type: 'primary',
link: true,
ifShow: isStock,
onClick: handlePicking.bind(null, row),
},
]"
/>
</template>
</Grid>
</div>
</template>

View File

@@ -0,0 +1,457 @@
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmMaterialStockApi } from '#/api/mes/wm/materialstock';
import type { MesWmProductReceiptApi } from '#/api/mes/wm/productreceipt';
import type { MesWmProductReceiptDetailApi } from '#/api/mes/wm/productreceipt/detail';
import type { MesWmProductReceiptLineApi } from '#/api/mes/wm/productreceipt/line';
import { h, markRaw } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { ElButton } from 'element-plus';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { getRangePickerDefaultProps } from '#/utils';
import MdItemSelect from '#/views/mes/md/item/components/md-item-select.vue';
import ProWorkOrderSelect from '#/views/mes/pro/workorder/components/pro-work-order-select.vue';
import {
MesAutoCodeRuleCode,
MesProWorkOrderStatusEnum,
} from '#/views/mes/utils/constants';
import { WmMaterialStockSelect } from '#/views/mes/wm/materialstock/components';
import {
WmWarehouseAreaSelect,
WmWarehouseLocationSelect,
WmWarehouseSelect,
} from '#/views/mes/wm/warehouse/components';
/** 表单类型 */
export type FormType = 'create' | 'detail' | 'finish' | 'stock' | 'update';
/** 表单头部是否只读(上架、详情、入库态) */
function isHeaderReadonly(formType: FormType): boolean {
return (
formType === 'detail' || formType === 'finish' || formType === 'stock'
);
}
/** 新增/修改的表单 */
export function useFormSchema(
formType: FormType,
formApi?: VbenFormApi,
): VbenFormSchema[] {
return [
{
fieldName: 'id',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'status',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'code',
label: '入库单编号',
component: 'Input',
componentProps: {
placeholder: '请输入入库单编号',
},
rules: 'required',
suffix: isHeaderReadonly(formType)
? undefined
: () =>
h(
ElButton,
{
onClick: async () => {
const code = await generateAutoCode(
MesAutoCodeRuleCode.PRODUCTRECPT_CODE,
);
await formApi?.setFieldValue('code', code);
},
},
{ default: () => '生成' },
),
},
{
fieldName: 'name',
label: '入库单名称',
component: 'Input',
componentProps: {
placeholder: '请输入入库单名称',
},
rules: 'required',
},
{
fieldName: 'receiptDate',
label: '入库日期',
component: 'DatePicker',
componentProps: {
class: '!w-full',
format: 'YYYY-MM-DD',
placeholder: '请选择入库日期',
type: 'date',
valueFormat: 'x',
},
rules: 'required',
},
{
fieldName: 'workOrderId',
label: '生产工单',
component: markRaw(ProWorkOrderSelect),
componentProps: {
status: MesProWorkOrderStatusEnum.CONFIRMED,
},
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
formItemClass: 'col-span-3',
componentProps: {
placeholder: '请输入备注',
rows: 3,
},
},
];
}
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'code',
label: '入库单编号',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入入库单编号',
},
},
{
fieldName: 'name',
label: '入库单名称',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入入库单名称',
},
},
{
fieldName: 'receiptDate',
label: '入库日期',
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
clearable: true,
},
},
];
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions<MesWmProductReceiptApi.ProductReceipt>['columns'] {
return [
{
field: 'code',
title: '入库单编号',
minWidth: 160,
slots: { default: 'code' },
},
{
field: 'name',
title: '入库单名称',
minWidth: 150,
},
{
field: 'workOrderCode',
title: '生产工单',
minWidth: 140,
},
{
field: 'receiptDate',
title: '入库日期',
width: 180,
formatter: 'formatDate',
},
{
field: 'status',
title: '单据状态',
minWidth: 110,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.MES_WM_PRODUCT_RECEIPT_STATUS },
},
},
{
title: '操作',
width: 240,
fixed: 'right',
slots: { default: 'actions' },
},
];
}
/** 入库单行子表的字段 */
export function useLineGridColumns(
editable: boolean,
stockable: boolean,
): VxeTableGridOptions<MesWmProductReceiptLineApi.ProductReceiptLine>['columns'] {
return [
{
type: 'expand',
width: 48,
slots: { content: 'detail' },
},
{
field: 'itemCode',
title: '产品物料编码',
minWidth: 120,
},
{
field: 'itemName',
title: '产品物料名称',
minWidth: 140,
},
{
field: 'specification',
title: '规格型号',
minWidth: 120,
},
{
field: 'unitMeasureName',
title: '单位',
width: 80,
},
{
field: 'quantity',
title: '入库数量',
width: 100,
},
{
field: 'batchCode',
title: '批次号',
minWidth: 120,
},
...(editable || stockable
? [
{
title: '操作',
width: 200,
fixed: 'right',
slots: { default: 'actions' },
} as const,
]
: []),
];
}
/** 入库单行新增/修改的表单 */
export function useLineFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
return [
{
fieldName: 'materialStockId',
label: '库存记录',
component: markRaw(WmMaterialStockSelect),
componentProps: {
// 选择库存记录后,自动回填物料/批次/数量
onChange: async (stock?: MesWmMaterialStockApi.MaterialStock) => {
await formApi?.setValues({
batchCode: stock?.batchCode,
batchId: stock?.batchId,
itemId: stock?.itemId,
quantity: stock?.quantity,
quantityMax: stock?.quantity,
});
},
virtualFilter: 'only',
},
rules: 'selectRequired',
},
{
fieldName: 'quantityMax',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'quantity',
label: '入库数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
controlsPosition: 'right',
min: 0,
placeholder: '请输入入库数量',
precision: 2,
},
rules: 'required',
dependencies: {
triggerFields: ['quantityMax'],
componentProps: (values) => ({
class: '!w-full',
controlsPosition: 'right',
max: values.quantityMax,
min: 0,
placeholder: '请输入入库数量',
precision: 2,
}),
},
},
{
fieldName: 'batchCode',
label: '批次号',
component: 'Input',
componentProps: {
disabled: true,
placeholder: '选择库存后自动带出',
},
},
{
fieldName: 'itemId',
label: '物料',
component: markRaw(MdItemSelect),
componentProps: {
disabled: true,
},
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
formItemClass: 'col-span-3',
componentProps: {
placeholder: '请输入备注',
rows: 3,
},
},
];
}
/** 上架明细子表的字段 */
export function useDetailGridColumns(
stockable: boolean,
): VxeTableGridOptions<MesWmProductReceiptDetailApi.ProductReceiptDetail>['columns'] {
return [
{
field: 'warehouseName',
title: '仓库名称',
minWidth: 100,
},
{
field: 'locationName',
title: '库区名称',
minWidth: 100,
},
{
field: 'areaName',
title: '库位名称',
minWidth: 100,
},
{
field: 'quantity',
title: '数量',
width: 100,
},
...(stockable
? [
{
title: '操作',
width: 120,
fixed: 'right',
slots: { default: 'actions' },
} as const,
]
: []),
];
}
/** 上架明细新增/修改的表单 */
export function useDetailFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
return [
{
fieldName: 'itemId',
label: '物料',
component: markRaw(MdItemSelect),
componentProps: {
disabled: true,
},
},
{
fieldName: 'warehouseId',
label: '入库仓库',
component: markRaw(WmWarehouseSelect),
componentProps: {
// 切换仓库后清空库区和库位
onChange: async () => {
await formApi?.setValues({
areaId: undefined,
locationId: undefined,
});
},
placeholder: '请选择仓库',
},
rules: 'selectRequired',
},
{
fieldName: 'locationId',
label: '库区',
component: markRaw(WmWarehouseLocationSelect),
componentProps: {
placeholder: '请选择库区',
},
rules: 'selectRequired',
dependencies: {
triggerFields: ['warehouseId'],
componentProps: (values) => ({
// 切换库区后清空库位
onChange: async () => {
await formApi?.setFieldValue('areaId', undefined);
},
placeholder: '请选择库区',
warehouseId: values.warehouseId,
}),
},
},
{
fieldName: 'areaId',
label: '库位',
component: markRaw(WmWarehouseAreaSelect),
componentProps: {
placeholder: '请选择库位',
},
rules: 'selectRequired',
dependencies: {
triggerFields: ['locationId'],
componentProps: (values) => ({
locationId: values.locationId,
placeholder: '请选择库位',
}),
},
},
{
fieldName: 'quantity',
label: '数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
controlsPosition: 'right',
min: 0,
placeholder: '请输入数量',
precision: 2,
},
rules: 'required',
},
];
}

View File

@@ -0,0 +1,211 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmProductReceiptApi } from '#/api/mes/wm/productreceipt';
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { ElButton, ElLoading, ElMessage } from 'element-plus';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
cancelProductReceipt,
deleteProductReceipt,
exportProductReceipt,
getProductReceiptPage,
} from '#/api/mes/wm/productreceipt';
import { $t } from '#/locales';
import { MesWmProductReceiptStatusEnum } from '#/views/mes/utils/constants';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/** 刷新表格 */
function handleRefresh() {
gridApi.query();
}
/** 创建产品入库单 */
function handleCreate() {
formModalApi.setData({ formType: 'create' }).open();
}
/** 查看产品入库单 */
function handleDetail(row: MesWmProductReceiptApi.ProductReceipt) {
formModalApi.setData({ formType: 'detail', id: row.id }).open();
}
/** 编辑产品入库单 */
function handleEdit(row: MesWmProductReceiptApi.ProductReceipt) {
formModalApi.setData({ formType: 'update', id: row.id }).open();
}
/** 执行上架 */
function handleStock(row: MesWmProductReceiptApi.ProductReceipt) {
formModalApi.setData({ formType: 'stock', id: row.id }).open();
}
/** 执行入库 */
function handleFinish(row: MesWmProductReceiptApi.ProductReceipt) {
formModalApi.setData({ formType: 'finish', id: row.id }).open();
}
/** 删除产品入库单 */
async function handleDelete(row: MesWmProductReceiptApi.ProductReceipt) {
const loadingInstance = ElLoading.service({
text: $t('ui.actionMessage.deleting', [row.code]),
});
try {
await deleteProductReceipt(row.id!);
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.code]));
handleRefresh();
} finally {
loadingInstance.close();
}
}
/** 取消产品入库单 */
async function handleCancel(row: MesWmProductReceiptApi.ProductReceipt) {
await cancelProductReceipt(row.id!);
ElMessage.success('取消成功');
handleRefresh();
}
/** 导出表格 */
async function handleExport() {
const data = await exportProductReceipt(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '产品入库单.xls', source: data });
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getProductReceiptPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
search: true,
},
} as VxeTableGridOptions<MesWmProductReceiptApi.ProductReceipt>,
});
</script>
<template>
<Page auto-content-height>
<template #doc>
<DocAlert
title="【仓库】生产入库、生产退料"
url="https://doc.iocoder.cn/mes/wm/produce-in/"
/>
</template>
<FormModal @success="handleRefresh" />
<Grid table-title="产品入库单列表">
<template #toolbar-tools>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['产品入库单']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['mes:wm-product-receipt:create'],
onClick: handleCreate,
},
{
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
auth: ['mes:wm-product-receipt:export'],
onClick: handleExport,
},
]"
/>
</template>
<template #code="{ row }">
<ElButton link type="primary" @click="handleDetail(row)">
{{ row.code }}
</ElButton>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'primary',
link: true,
icon: ACTION_ICON.EDIT,
auth: ['mes:wm-product-receipt:update'],
ifShow: row.status === MesWmProductReceiptStatusEnum.PREPARE,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'danger',
link: true,
icon: ACTION_ICON.DELETE,
auth: ['mes:wm-product-receipt:delete'],
ifShow: row.status === MesWmProductReceiptStatusEnum.PREPARE,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.code]),
confirm: handleDelete.bind(null, row),
},
},
{
label: '执行上架',
type: 'primary',
link: true,
auth: ['mes:wm-product-receipt:update'],
ifShow: row.status === MesWmProductReceiptStatusEnum.APPROVING,
onClick: handleStock.bind(null, row),
},
{
label: '执行入库',
type: 'primary',
link: true,
auth: ['mes:wm-product-receipt:finish'],
ifShow: row.status === MesWmProductReceiptStatusEnum.APPROVED,
onClick: handleFinish.bind(null, row),
},
{
label: '取消',
type: 'danger',
link: true,
auth: ['mes:wm-product-receipt:update'],
ifShow:
row.status === MesWmProductReceiptStatusEnum.APPROVING ||
row.status === MesWmProductReceiptStatusEnum.APPROVED,
popConfirm: {
title: '确认取消该产品入库单?取消后不可恢复。',
confirm: handleCancel.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,104 @@
<script lang="ts" setup>
import type { MesWmProductReceiptDetailApi } from '#/api/mes/wm/productreceipt/detail';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { ElMessage } from 'element-plus';
import { useVbenForm } from '#/adapter/form';
import {
createProductReceiptDetail,
getProductReceiptDetail,
updateProductReceiptDetail,
} from '#/api/mes/wm/productreceipt/detail';
import { $t } from '#/locales';
import { useDetailFormSchema } from '../data';
const emit = defineEmits<{ success: [lineId: number] }>();
const formData = ref<MesWmProductReceiptDetailApi.ProductReceiptDetail>();
const receiptId = ref<number>(); // 所属入库单编号
const lineId = ref<number>(); // 所属入库单行编号
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['上架明细'])
: $t('ui.actionTitle.create', ['上架明细']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-1',
labelWidth: 90,
},
layout: 'horizontal',
schema: useDetailFormSchema(),
showDefaultActions: false,
wrapperClass: 'grid-cols-3',
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data =
(await formApi.getValues()) as MesWmProductReceiptDetailApi.ProductReceiptDetail;
data.receiptId = receiptId.value;
data.lineId = lineId.value;
try {
await (formData.value?.id
? updateProductReceiptDetail({ ...data, id: formData.value.id })
: createProductReceiptDetail(data));
// 关闭并提示
await modalApi.close();
emit('success', lineId.value!);
ElMessage.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
formApi.setState({ schema: useDetailFormSchema(formApi) });
// 加载数据
const data = modalApi.getData<{
detailId?: number;
itemId?: number;
lineId: number;
receiptId: number;
}>();
receiptId.value = data.receiptId;
lineId.value = data.lineId;
if (data.detailId) {
modalApi.lock();
try {
formData.value = await getProductReceiptDetail(data.detailId);
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
} else if (data.itemId) {
await formApi.setFieldValue('itemId', data.itemId);
}
},
});
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" />
</Modal>
</template>

View File

@@ -0,0 +1,103 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmProductReceiptDetailApi } from '#/api/mes/wm/productreceipt/detail';
import { computed, watch } from 'vue';
import { ElLoading, ElMessage } from 'element-plus';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteProductReceiptDetail } from '#/api/mes/wm/productreceipt/detail';
import { $t } from '#/locales';
import { type FormType, useDetailGridColumns } from '../data';
const props = defineProps<{
details: MesWmProductReceiptDetailApi.ProductReceiptDetail[];
formType: FormType;
}>();
const emit = defineEmits<{
edit: [detailId: number];
refresh: [];
}>();
const isStock = computed(() => props.formType === 'stock'); // 是否为上架模式
/** 编辑上架明细 */
function handleEdit(row: MesWmProductReceiptDetailApi.ProductReceiptDetail) {
emit('edit', row.id!);
}
/** 删除上架明细 */
async function handleDelete(
row: MesWmProductReceiptDetailApi.ProductReceiptDetail,
) {
const loadingInstance = ElLoading.service({
text: $t('ui.actionMessage.deleting', [row.warehouseName]),
});
try {
await deleteProductReceiptDetail(row.id!);
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.warehouseName]));
emit('refresh');
} finally {
loadingInstance.close();
}
}
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
border: true,
columns: useDetailGridColumns(isStock.value),
data: props.details,
pagerConfig: {
enabled: false,
},
rowConfig: {
keyField: 'id',
isHover: true,
},
size: 'small',
toolbarConfig: {
enabled: false,
},
} as VxeTableGridOptions<MesWmProductReceiptDetailApi.ProductReceiptDetail>,
});
watch(
() => props.details,
(details) => gridApi.setGridOptions({ data: details }),
);
</script>
<template>
<div class="px-4 py-2">
<Grid>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'primary',
link: true,
icon: ACTION_ICON.EDIT,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'danger',
link: true,
icon: ACTION_ICON.DELETE,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [
row.warehouseName,
]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</div>
</template>

View File

@@ -0,0 +1,238 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { MesWmProductReceiptApi } from '#/api/mes/wm/productreceipt';
import { computed, ref } from 'vue';
import { confirm, useVbenModal } from '@vben/common-ui';
import { ElButton, ElDivider, ElMessage, ElPopconfirm } from 'element-plus';
import { useVbenForm } from '#/adapter/form';
import {
checkProductReceiptQuantity,
createProductReceipt,
finishProductReceipt,
getProductReceipt,
stockProductReceipt,
submitProductReceipt,
updateProductReceipt,
} from '#/api/mes/wm/productreceipt';
import { $t } from '#/locales';
import { MesWmProductReceiptStatusEnum } from '#/views/mes/utils/constants';
import { useFormSchema } from '../data';
import LineList from './line-list.vue';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const formData = ref<MesWmProductReceiptApi.ProductReceipt>();
const originalSnapshot = ref(''); // 表单原始数据快照,用于提交时跳过未变更的保存请求
const isEditable = computed(() => // 是否为编辑模式(可保存)
['create', 'update'].includes(formType.value),
);
const isStock = computed(() => formType.value === 'stock'); // 是否为上架模式
const isFinish = computed(() => formType.value === 'finish'); // 是否为执行入库模式
const canSubmit = computed(() => // 是否可提交
formType.value === 'update' &&
formData.value?.status === MesWmProductReceiptStatusEnum.PREPARE,
);
const getTitle = computed(() => {
if (formType.value === 'detail') {
return $t('ui.actionTitle.view', ['产品入库单']);
}
if (formType.value === 'stock') {
return '执行上架';
}
if (formType.value === 'finish') {
return '执行入库';
}
return formType.value === 'update'
? $t('ui.actionTitle.edit', ['产品入库单'])
: $t('ui.actionTitle.create', ['产品入库单']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-1',
labelWidth: 110,
},
layout: 'horizontal',
schema: [],
showDefaultActions: false,
wrapperClass: 'grid-cols-3',
});
/** 提交入库单:表单有修改时先保存,再调用提交接口 */
async function handleSubmit() {
const { valid } = await formApi.validate();
if (!valid || !formData.value?.id) {
return;
}
modalApi.lock();
try {
const current = JSON.stringify(await formApi.getValues());
if (current !== originalSnapshot.value) {
const data =
(await formApi.getValues()) as MesWmProductReceiptApi.ProductReceipt;
await updateProductReceipt({ ...formData.value, ...data });
originalSnapshot.value = current;
}
await submitProductReceipt(formData.value.id);
ElMessage.success('提交成功');
await modalApi.close();
emit('success');
} finally {
modalApi.unlock();
}
}
/** 执行上架:明细数量与行收货数量不一致时二次确认 */
async function handleStock() {
if (!formData.value?.id) {
return;
}
const quantityMatch = await checkProductReceiptQuantity(formData.value.id);
if (!quantityMatch) {
try {
await confirm('明细数量与行收货数量不一致,确认执行上架?');
} catch {
return;
}
}
modalApi.lock();
try {
await stockProductReceipt(formData.value.id);
ElMessage.success('上架成功');
await modalApi.close();
emit('success');
} finally {
modalApi.unlock();
}
}
/** 执行入库 */
async function handleFinish() {
if (!formData.value?.id) {
return;
}
modalApi.lock();
try {
await finishProductReceipt(formData.value.id);
ElMessage.success('入库成功');
await modalApi.close();
emit('success');
} finally {
modalApi.unlock();
}
}
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
if (!isEditable.value) {
await modalApi.close();
return;
}
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data =
(await formApi.getValues()) as MesWmProductReceiptApi.ProductReceipt;
try {
if (formData.value?.id) {
await updateProductReceipt({ ...formData.value, ...data });
formData.value = { ...formData.value, ...data };
} else {
const id = await createProductReceipt(data);
formData.value = {
...data,
id,
status: MesWmProductReceiptStatusEnum.PREPARE,
};
await formApi.setFieldValue('id', id);
await formApi.setFieldValue('status', formData.value.status);
formType.value = 'update';
}
originalSnapshot.value = JSON.stringify(await formApi.getValues());
emit('success');
ElMessage.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
originalSnapshot.value = '';
return;
}
// 加载数据
const data = modalApi.getData<{ formType: FormType; id?: number }>();
formType.value = data.formType;
formApi.setState({ schema: useFormSchema(formType.value, formApi) });
formApi.setDisabled(!isEditable.value);
modalApi.setState({ showConfirmButton: isEditable.value });
if (data?.id) {
modalApi.lock();
try {
formData.value = await getProductReceipt(data.id);
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
}
originalSnapshot.value = JSON.stringify(await formApi.getValues());
},
});
/** 入库前确认 */
async function confirmFinish() {
try {
await confirm('确认执行入库?执行后将更新库存台账。');
} catch {
return;
}
await handleFinish();
}
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" />
<!-- 非新建模式展示物料信息 -->
<template v-if="formData?.id">
<ElDivider>物料信息</ElDivider>
<div class="mx-4">
<LineList :form-type="formType" :receipt-id="formData.id" />
</div>
</template>
<template #prepend-footer>
<div class="flex flex-auto items-center gap-2">
<ElPopconfirm
v-if="canSubmit"
title="确认提交该产品入库单?【提交后将不能修改】"
width="260"
@confirm="handleSubmit"
>
<template #reference>
<ElButton type="primary">提交</ElButton>
</template>
</ElPopconfirm>
<ElButton v-if="isStock" type="primary" @click="handleStock">
执行上架
</ElButton>
<ElButton v-if="isFinish" type="primary" @click="confirmFinish">
执行入库
</ElButton>
</div>
</template>
</Modal>
</template>

View File

@@ -0,0 +1,95 @@
<script lang="ts" setup>
import type { MesWmProductReceiptLineApi } from '#/api/mes/wm/productreceipt/line';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { ElMessage } from 'element-plus';
import { useVbenForm } from '#/adapter/form';
import {
createProductReceiptLine,
getProductReceiptLine,
updateProductReceiptLine,
} from '#/api/mes/wm/productreceipt/line';
import { $t } from '#/locales';
import { useLineFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<MesWmProductReceiptLineApi.ProductReceiptLine>();
const receiptId = ref<number>(); // 所属入库单编号
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['物料入库单行'])
: $t('ui.actionTitle.create', ['物料入库单行']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-1',
labelWidth: 110,
},
layout: 'horizontal',
schema: [],
showDefaultActions: false,
wrapperClass: 'grid-cols-3',
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data =
(await formApi.getValues()) as MesWmProductReceiptLineApi.ProductReceiptLine;
data.receiptId = receiptId.value;
try {
await (formData.value?.id
? updateProductReceiptLine({ ...data, id: formData.value.id })
: createProductReceiptLine(data));
// 关闭并提示
await modalApi.close();
emit('success');
ElMessage.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
formApi.setState({ schema: useLineFormSchema(formApi) });
// 加载数据
const data = modalApi.getData<{ id?: number; receiptId: number }>();
receiptId.value = data.receiptId;
if (!data.id) {
return;
}
modalApi.lock();
try {
formData.value = await getProductReceiptLine(data.id);
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
},
});
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" />
</Modal>
</template>

View File

@@ -0,0 +1,251 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmProductReceiptDetailApi } from '#/api/mes/wm/productreceipt/detail';
import type { MesWmProductReceiptLineApi } from '#/api/mes/wm/productreceipt/line';
import { computed, reactive, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { ElLoading, ElMessage } from 'element-plus';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getProductReceiptDetailListByLineId } from '#/api/mes/wm/productreceipt/detail';
import {
deleteProductReceiptLine,
getProductReceiptLinePage,
} from '#/api/mes/wm/productreceipt/line';
import { $t } from '#/locales';
import { BarcodeBizTypeEnum } from '#/views/mes/utils/constants';
import { BarcodeDetail, PrinterLabel } from '#/views/mes/wm/barcode/components';
import { type FormType, useLineGridColumns } from '../data';
import DetailForm from './detail-form.vue';
import DetailList from './detail-list.vue';
import LineForm from './line-form.vue';
const props = defineProps<{
formType: FormType;
receiptId: number;
}>();
const isEditable = computed(() => // 是否可编辑明细行
['create', 'update'].includes(props.formType),
);
const isStock = computed(() => props.formType === 'stock'); // 是否为上架模式
const detailMap = reactive<
Record<number, MesWmProductReceiptDetailApi.ProductReceiptDetail[]>
>({}); // 已展开行的上架明细缓存
const barcodeDetailRef = ref(); // 条码详情弹窗实例
const [LineFormModal, lineFormModalApi] = useVbenModal({
connectedComponent: LineForm,
destroyOnClose: true,
});
const [DetailFormModal, detailFormModalApi] = useVbenModal({
connectedComponent: DetailForm,
destroyOnClose: true,
});
/** 刷新表格 */
function handleRefresh() {
for (const id of Object.keys(detailMap)) {
delete detailMap[Number(id)];
}
gridApi.query();
}
/** 添加物料 */
function handleCreate() {
lineFormModalApi.setData({ receiptId: props.receiptId }).open();
}
/** 编辑物料 */
function handleEdit(row: MesWmProductReceiptLineApi.ProductReceiptLine) {
lineFormModalApi.setData({ id: row.id, receiptId: props.receiptId }).open();
}
/** 删除物料 */
async function handleDelete(
row: MesWmProductReceiptLineApi.ProductReceiptLine,
) {
const loadingInstance = ElLoading.service({
text: $t('ui.actionMessage.deleting', [row.itemName]),
});
try {
await deleteProductReceiptLine(row.id!);
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.itemName]));
handleRefresh();
} finally {
loadingInstance.close();
}
}
/** 上架:直接打开明细创建表单 */
function handleStock(row: MesWmProductReceiptLineApi.ProductReceiptLine) {
openDetailForm(row.id!, row.itemId);
}
/** 查看物料条码 */
function handleBarcode(row: MesWmProductReceiptLineApi.ProductReceiptLine) {
barcodeDetailRef.value?.openByBusiness(
row.itemId,
BarcodeBizTypeEnum.ITEM,
row.itemCode,
row.itemName,
);
}
/** 打开上架明细表单 */
function openDetailForm(lineId: number, itemId?: number, detailId?: number) {
detailFormModalApi
.setData({ detailId, itemId, lineId, receiptId: props.receiptId })
.open();
}
/** 获取已展开行的上架明细 */
function getExpandedDetails(row: MesWmProductReceiptLineApi.ProductReceiptLine) {
return detailMap[row.id!] || [];
}
/** 加载指定行的上架明细 */
async function loadLineDetails(lineId: number) {
detailMap[lineId] = await getProductReceiptDetailListByLineId(lineId);
}
/** 展开行时懒加载上架明细 */
async function handleExpandChange(
row: MesWmProductReceiptLineApi.ProductReceiptLine,
expanded: boolean,
) {
if (!expanded) {
return;
}
await loadLineDetails(row.id!);
}
/** 明细表单提交成功后,刷新对应行已展开的明细 */
async function handleDetailSuccess(lineId: number) {
await loadLineDetails(lineId);
}
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useLineGridColumns(isEditable.value, isStock.value),
expandConfig: {
padding: true,
},
height: 400,
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }) => {
if (!props.receiptId) {
return { list: [], total: 0 };
}
return await getProductReceiptLinePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
receiptId: props.receiptId,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
},
} as VxeTableGridOptions<MesWmProductReceiptLineApi.ProductReceiptLine>,
gridEvents: {
toggleRowExpand: ({
expanded,
row,
}: {
expanded: boolean;
row: MesWmProductReceiptLineApi.ProductReceiptLine;
}) => {
handleExpandChange(row, expanded);
},
},
});
</script>
<template>
<div>
<LineFormModal @success="handleRefresh" />
<DetailFormModal @success="handleDetailSuccess" />
<Grid table-title="物料信息">
<template v-if="isEditable" #toolbar-tools>
<TableAction
:actions="[
{
label: '添加物料',
type: 'primary',
icon: ACTION_ICON.ADD,
onClick: handleCreate,
},
]"
/>
</template>
<template #detail="{ row }">
<DetailList
:details="getExpandedDetails(row)"
:form-type="formType"
@edit="(detailId) => openDetailForm(row.id!, row.itemId, detailId)"
@refresh="loadLineDetails(row.id!)"
/>
</template>
<template #actions="{ row }">
<div class="flex items-center justify-center">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'primary',
link: true,
icon: ACTION_ICON.EDIT,
ifShow: isEditable,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'danger',
link: true,
icon: ACTION_ICON.DELETE,
ifShow: isEditable,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.itemName]),
confirm: handleDelete.bind(null, row),
},
},
{
label: '上架',
type: 'primary',
link: true,
ifShow: isStock,
onClick: handleStock.bind(null, row),
},
{
label: '条码',
type: 'primary',
link: true,
onClick: handleBarcode.bind(null, row),
},
]"
/>
<PrinterLabel
v-if="isStock"
:biz-code="row.batchCode"
:biz-id="row.batchId"
biz-type="BATCH"
/>
</div>
</template>
</Grid>
<BarcodeDetail ref="barcodeDetailRef" />
</div>
</template>

View File

@@ -0,0 +1,689 @@
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmMaterialStockApi } from '#/api/mes/wm/materialstock';
import type { MesWmProductSalesApi } from '#/api/mes/wm/productsales';
import type { MesWmProductSalesDetailApi } from '#/api/mes/wm/productsales/detail';
import type { MesWmProductSalesLineApi } from '#/api/mes/wm/productsales/line';
import type { MesWmSalesNoticeApi } from '#/api/mes/wm/salesnotice';
import type { MesWmSalesNoticeLineApi } from '#/api/mes/wm/salesnotice/line';
import { h, markRaw } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { ElButton } from 'element-plus';
import { z } from '#/adapter/form';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { getRangePickerDefaultProps } from '#/utils';
import MdClientSelect from '#/views/mes/md/client/components/md-client-select.vue';
import MdItemSelect from '#/views/mes/md/item/components/md-item-select.vue';
import {
MesAutoCodeRuleCode,
MesWmSalesNoticeStatusEnum,
} from '#/views/mes/utils/constants';
import { WmMaterialStockSelect } from '#/views/mes/wm/materialstock/components';
import {
WmSalesNoticeLineSelect,
WmSalesNoticeSelect,
} from '#/views/mes/wm/salesnotice/components';
import {
WmWarehouseAreaSelect,
WmWarehouseLocationSelect,
WmWarehouseSelect,
} from '#/views/mes/wm/warehouse/components';
/** 表单类型 */
export type FormType =
| 'create'
| 'detail'
| 'finish'
| 'shipping'
| 'stock'
| 'update';
/** 表单头部是否只读(拣货、填写运单、出库、详情态) */
function isHeaderReadonly(formType: FormType): boolean {
return (
formType === 'detail' ||
formType === 'finish' ||
formType === 'shipping' ||
formType === 'stock'
);
}
/** 是否展示运输信息 */
export function showShippingInfo(formType: FormType): boolean {
return (
formType === 'shipping' || formType === 'detail' || formType === 'finish'
);
}
/** 新增/修改的表单 */
export function useFormSchema(
formType: FormType,
formApi?: VbenFormApi,
): VbenFormSchema[] {
return [
{
fieldName: 'id',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'status',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'code',
label: '出库单编号',
component: 'Input',
componentProps: {
placeholder: '请输入出库单编号',
},
rules: 'required',
suffix: isHeaderReadonly(formType)
? undefined
: () =>
h(
ElButton,
{
onClick: async () => {
const code = await generateAutoCode(
MesAutoCodeRuleCode.WM_PRODUCT_SALES_CODE,
);
await formApi?.setFieldValue('code', code);
},
},
{ default: () => '生成' },
),
},
{
fieldName: 'name',
label: '出库单名称',
component: 'Input',
componentProps: {
placeholder: '请输入出库单名称',
},
rules: 'required',
},
{
fieldName: 'noticeId',
label: '发货通知单',
component: markRaw(WmSalesNoticeSelect),
componentProps: {
// 选择发货通知单后,自动回填销售订单号、客户、收货人信息
onChange: async (notice?: MesWmSalesNoticeApi.SalesNotice) => {
await formApi?.setValues({
clientId: notice?.clientId,
contactAddress: notice?.recipientAddress,
contactName: notice?.recipientName,
contactTelephone: notice?.recipientTelephone,
salesOrderCode: notice?.salesOrderCode,
});
},
status: MesWmSalesNoticeStatusEnum.APPROVED,
},
},
{
fieldName: 'salesOrderCode',
label: '销售订单编号',
component: 'Input',
componentProps: {
placeholder: '请输入销售订单编号',
},
},
{
fieldName: 'salesDate',
label: '出库日期',
component: 'DatePicker',
componentProps: {
class: '!w-full',
format: 'YYYY-MM-DD',
placeholder: '请选择出库日期',
type: 'date',
valueFormat: 'x',
},
rules: 'required',
},
{
fieldName: 'clientId',
label: '客户',
component: markRaw(MdClientSelect),
componentProps: {
placeholder: '请选择客户',
},
rules: 'selectRequired',
},
{
fieldName: 'contactName',
label: '收货人',
component: 'Input',
componentProps: {
placeholder: '请输入收货人',
},
},
{
fieldName: 'contactTelephone',
label: '联系方式',
component: 'Input',
componentProps: {
placeholder: '请输入联系方式',
},
},
{
fieldName: 'contactAddress',
label: '收货地址',
component: 'Input',
componentProps: {
placeholder: '请输入收货地址',
},
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
formItemClass: 'col-span-3',
componentProps: {
placeholder: '请输入备注',
rows: 3,
},
},
{
fieldName: 'carrier',
label: '承运商',
component: 'Input',
componentProps: {
disabled: formType !== 'shipping',
placeholder: '请输入承运商',
},
dependencies: {
triggerFields: [''],
if: () => showShippingInfo(formType),
},
},
{
fieldName: 'shippingNumber',
label: '运输单号',
component: 'Input',
componentProps: {
disabled: formType !== 'shipping',
placeholder: '请输入运输单号',
},
dependencies: {
triggerFields: [''],
if: () => showShippingInfo(formType),
},
},
];
}
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'code',
label: '出库单编号',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入出库单编号',
},
},
{
fieldName: 'name',
label: '出库单名称',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入出库单名称',
},
},
{
fieldName: 'salesOrderCode',
label: '销售订单编号',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入销售订单编号',
},
},
{
fieldName: 'clientId',
label: '客户',
component: markRaw(MdClientSelect),
componentProps: {
placeholder: '请选择客户',
},
},
{
fieldName: 'salesDate',
label: '出库日期',
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
clearable: true,
},
},
{
fieldName: 'status',
label: '单据状态',
component: 'Select',
componentProps: {
clearable: true,
options: getDictOptions(DICT_TYPE.MES_WM_PRODUCT_SALES_STATUS, 'number'),
placeholder: '请选择单据状态',
},
},
];
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions<MesWmProductSalesApi.ProductSales>['columns'] {
return [
{
field: 'code',
title: '出库单编号',
minWidth: 160,
slots: { default: 'code' },
},
{
field: 'name',
title: '出库单名称',
minWidth: 150,
},
{
field: 'noticeCode',
title: '发货通知单号',
minWidth: 160,
},
{
field: 'salesOrderCode',
title: '销售订单编号',
minWidth: 160,
},
{
field: 'clientCode',
title: '客户编码',
minWidth: 120,
},
{
field: 'clientName',
title: '客户名称',
minWidth: 120,
},
{
field: 'contactName',
title: '收货人',
minWidth: 100,
},
{
field: 'carrier',
title: '承运商',
minWidth: 120,
},
{
field: 'shippingNumber',
title: '运输单号',
minWidth: 160,
},
{
field: 'salesDate',
title: '出库日期',
width: 180,
formatter: 'formatDate',
},
{
field: 'status',
title: '单据状态',
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.MES_WM_PRODUCT_SALES_STATUS },
},
},
{
title: '操作',
width: 240,
fixed: 'right',
slots: { default: 'actions' },
},
];
}
/** 出库单行子表的字段 */
export function useLineGridColumns(
editable: boolean,
stockable: boolean,
): VxeTableGridOptions<MesWmProductSalesLineApi.ProductSalesLine>['columns'] {
return [
{
type: 'expand',
width: 48,
slots: { content: 'detail' },
},
{
field: 'itemCode',
title: '产品编码',
minWidth: 120,
},
{
field: 'itemName',
title: '产品名称',
minWidth: 140,
},
{
field: 'specification',
title: '规格型号',
minWidth: 120,
},
{
field: 'unitMeasureName',
title: '单位',
width: 80,
},
{
field: 'quantity',
title: '出库数量',
width: 100,
},
{
field: 'batchCode',
title: '批次号',
minWidth: 120,
},
{
field: 'oqcCheckFlag',
title: '是否校验',
width: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
},
},
{
field: 'remark',
title: '备注',
minWidth: 150,
},
...(editable || stockable
? [
{
title: '操作',
width: 160,
fixed: 'right',
slots: { default: 'actions' },
} as const,
]
: []),
];
}
/** 出库单行新增/修改的表单 */
export function useLineFormSchema(
hasNotice: boolean,
formApi?: VbenFormApi,
): VbenFormSchema[] {
return [
{
fieldName: 'noticeId',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'noticeLineId',
label: '发货通知单行',
component: markRaw(WmSalesNoticeLineSelect),
componentProps: {
// 选择发货通知单行后,自动回填物料、数量、批次、是否检验
onChange: async (line?: MesWmSalesNoticeLineApi.SalesNoticeLine) => {
await formApi?.setValues({
batchCode: line?.batchCode,
itemId: line?.itemId,
oqcCheckFlag: line?.oqcCheckFlag ?? false,
quantity: line?.quantity,
});
},
},
dependencies: {
triggerFields: ['noticeId'],
if: () => hasNotice,
componentProps: (values) => ({
noticeId: values.noticeId,
}),
},
},
{
fieldName: 'itemId',
label: '产品',
component: markRaw(MdItemSelect),
componentProps: {
placeholder: '请选择产品',
},
rules: 'selectRequired',
dependencies: {
triggerFields: ['noticeLineId'],
componentProps: (values) => ({
disabled: !!values.noticeLineId,
placeholder: '请选择产品',
}),
},
},
{
fieldName: 'batchCode',
label: '批次号',
component: 'Input',
componentProps: {
placeholder: '请输入批次号',
},
},
{
fieldName: 'quantity',
label: '出库数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
controlsPosition: 'right',
min: 0,
placeholder: '请输入出库数量',
precision: 2,
},
rules: 'required',
},
{
fieldName: 'oqcCheckFlag',
label: '是否校验',
component: 'RadioGroup',
componentProps: {
options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
},
rules: z.boolean().default(false),
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
formItemClass: 'col-span-3',
componentProps: {
placeholder: '请输入备注',
rows: 3,
},
},
];
}
/** 拣货明细子表的字段 */
export function useDetailGridColumns(
stockable: boolean,
): VxeTableGridOptions<MesWmProductSalesDetailApi.ProductSalesDetail>['columns'] {
return [
{
field: 'warehouseName',
title: '仓库名称',
minWidth: 100,
},
{
field: 'locationName',
title: '库区名称',
minWidth: 100,
},
{
field: 'areaName',
title: '库位名称',
minWidth: 100,
},
{
field: 'quantity',
title: '数量',
width: 100,
},
...(stockable
? [
{
title: '操作',
width: 120,
fixed: 'right',
slots: { default: 'actions' },
} as const,
]
: []),
];
}
/** 拣货明细新增/修改的表单 */
export function useDetailFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
return [
{
fieldName: 'quantityMax',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'batchId',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'itemId',
label: '物料',
component: markRaw(MdItemSelect),
componentProps: {
disabled: true,
},
},
{
fieldName: 'materialStockId',
label: '选择库存',
component: markRaw(WmMaterialStockSelect),
componentProps: {
// 选择库存记录后,自动回填仓库/库区/库位/批次/数量
onChange: async (stock?: MesWmMaterialStockApi.MaterialStock) => {
await formApi?.setValues({
areaId: stock?.areaId,
batchCode: stock?.batchCode,
batchId: stock?.batchId,
locationId: stock?.locationId,
quantity: stock?.quantity,
quantityMax: stock?.quantity,
warehouseId: stock?.warehouseId,
});
},
},
rules: 'selectRequired',
dependencies: {
triggerFields: ['itemId', 'batchId'],
componentProps: (values) => ({
batchId: values.batchId,
itemId: values.itemId,
}),
},
},
{
fieldName: 'quantity',
label: '数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
controlsPosition: 'right',
min: 0,
placeholder: '请输入数量',
precision: 2,
},
rules: 'required',
dependencies: {
triggerFields: ['quantityMax'],
componentProps: (values) => ({
class: '!w-full',
controlsPosition: 'right',
max: values.quantityMax,
min: 0,
placeholder: '请输入数量',
precision: 2,
}),
},
},
{
fieldName: 'warehouseId',
label: '出库仓库',
component: markRaw(WmWarehouseSelect),
componentProps: {
disabled: true,
},
},
{
fieldName: 'locationId',
label: '库区',
component: markRaw(WmWarehouseLocationSelect),
componentProps: {
disabled: true,
},
dependencies: {
triggerFields: ['warehouseId'],
componentProps: (values) => ({
disabled: true,
warehouseId: values.warehouseId,
}),
},
},
{
fieldName: 'areaId',
label: '库位',
component: markRaw(WmWarehouseAreaSelect),
componentProps: {
disabled: true,
},
dependencies: {
triggerFields: ['locationId'],
componentProps: (values) => ({
disabled: true,
locationId: values.locationId,
}),
},
},
{
fieldName: 'batchCode',
label: '批次号',
component: 'Input',
componentProps: {
disabled: true,
},
},
];
}

View File

@@ -0,0 +1,226 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmProductSalesApi } from '#/api/mes/wm/productsales';
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { ElButton, ElLoading, ElMessage } from 'element-plus';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
cancelProductSales,
deleteProductSales,
exportProductSales,
getProductSalesPage,
} from '#/api/mes/wm/productsales';
import { $t } from '#/locales';
import { MesWmProductSalesStatusEnum } from '#/views/mes/utils/constants';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/** 刷新表格 */
function handleRefresh() {
gridApi.query();
}
/** 创建销售出库单 */
function handleCreate() {
formModalApi.setData({ formType: 'create' }).open();
}
/** 查看销售出库单 */
function handleDetail(row: MesWmProductSalesApi.ProductSales) {
formModalApi.setData({ formType: 'detail', id: row.id }).open();
}
/** 编辑销售出库单 */
function handleEdit(row: MesWmProductSalesApi.ProductSales) {
formModalApi.setData({ formType: 'update', id: row.id }).open();
}
/** 执行拣货 */
function handleStock(row: MesWmProductSalesApi.ProductSales) {
formModalApi.setData({ formType: 'stock', id: row.id }).open();
}
/** 填写运单 */
function handleShipping(row: MesWmProductSalesApi.ProductSales) {
formModalApi.setData({ formType: 'shipping', id: row.id }).open();
}
/** 执行出库 */
function handleFinish(row: MesWmProductSalesApi.ProductSales) {
formModalApi.setData({ formType: 'finish', id: row.id }).open();
}
/** 删除销售出库单 */
async function handleDelete(row: MesWmProductSalesApi.ProductSales) {
const loadingInstance = ElLoading.service({
text: $t('ui.actionMessage.deleting', [row.code]),
});
try {
await deleteProductSales(row.id!);
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.code]));
handleRefresh();
} finally {
loadingInstance.close();
}
}
/** 取消销售出库单 */
async function handleCancel(row: MesWmProductSalesApi.ProductSales) {
await cancelProductSales(row.id!);
ElMessage.success('取消成功');
handleRefresh();
}
/** 导出表格 */
async function handleExport() {
const data = await exportProductSales(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '销售出库单.xls', source: data });
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getProductSalesPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
search: true,
},
} as VxeTableGridOptions<MesWmProductSalesApi.ProductSales>,
});
</script>
<template>
<Page auto-content-height>
<template #doc>
<DocAlert
title="【仓库】发货通知、销售出库、销售退货"
url="https://doc.iocoder.cn/mes/wm/sales-out/"
/>
</template>
<FormModal @success="handleRefresh" />
<Grid table-title="销售出库单列表">
<template #toolbar-tools>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['销售出库单']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['mes:wm-product-sales:create'],
onClick: handleCreate,
},
{
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
auth: ['mes:wm-product-sales:export'],
onClick: handleExport,
},
]"
/>
</template>
<template #code="{ row }">
<ElButton link type="primary" @click="handleDetail(row)">
{{ row.code }}
</ElButton>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'primary',
link: true,
icon: ACTION_ICON.EDIT,
auth: ['mes:wm-product-sales:update'],
ifShow: row.status === MesWmProductSalesStatusEnum.PREPARE,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'danger',
link: true,
icon: ACTION_ICON.DELETE,
auth: ['mes:wm-product-sales:delete'],
ifShow: row.status === MesWmProductSalesStatusEnum.PREPARE,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.code]),
confirm: handleDelete.bind(null, row),
},
},
{
label: '拣货',
type: 'primary',
link: true,
auth: ['mes:wm-product-sales:stock'],
ifShow: row.status === MesWmProductSalesStatusEnum.APPROVING,
onClick: handleStock.bind(null, row),
},
{
label: '填写运单',
type: 'primary',
link: true,
auth: ['mes:wm-product-sales:shipping'],
ifShow: row.status === MesWmProductSalesStatusEnum.SHIPPING,
onClick: handleShipping.bind(null, row),
},
{
label: '执行出库',
type: 'primary',
link: true,
auth: ['mes:wm-product-sales:finish'],
ifShow: row.status === MesWmProductSalesStatusEnum.APPROVED,
onClick: handleFinish.bind(null, row),
},
{
label: '取消',
type: 'danger',
link: true,
auth: ['mes:wm-product-sales:cancel'],
ifShow:
row.status === MesWmProductSalesStatusEnum.CONFIRMED ||
row.status === MesWmProductSalesStatusEnum.APPROVING ||
row.status === MesWmProductSalesStatusEnum.SHIPPING ||
row.status === MesWmProductSalesStatusEnum.APPROVED,
popConfirm: {
title: '确认取消该销售出库单?取消后不可恢复。',
confirm: handleCancel.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,110 @@
<script lang="ts" setup>
import type { MesWmProductSalesDetailApi } from '#/api/mes/wm/productsales/detail';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { ElMessage } from 'element-plus';
import { useVbenForm } from '#/adapter/form';
import {
createProductSalesDetail,
getProductSalesDetail,
updateProductSalesDetail,
} from '#/api/mes/wm/productsales/detail';
import { $t } from '#/locales';
import { useDetailFormSchema } from '../data';
const emit = defineEmits<{ success: [lineId: number] }>();
const formData = ref<MesWmProductSalesDetailApi.ProductSalesDetail>();
const salesId = ref<number>(); // 所属出库单编号
const lineId = ref<number>(); // 所属出库单行编号
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['拣货明细'])
: $t('ui.actionTitle.create', ['拣货明细']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-1',
labelWidth: 90,
},
layout: 'horizontal',
schema: useDetailFormSchema(),
showDefaultActions: false,
wrapperClass: 'grid-cols-3',
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data =
(await formApi.getValues()) as MesWmProductSalesDetailApi.ProductSalesDetail;
data.salesId = salesId.value;
data.lineId = lineId.value;
try {
await (formData.value?.id
? updateProductSalesDetail({ ...data, id: formData.value.id })
: createProductSalesDetail(data));
// 关闭并提示
await modalApi.close();
emit('success', lineId.value!);
ElMessage.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
formApi.setState({ schema: useDetailFormSchema(formApi) });
// 加载数据
const data = modalApi.getData<{
batchId?: number;
detailId?: number;
itemId?: number;
lineId: number;
salesId: number;
}>();
salesId.value = data.salesId;
lineId.value = data.lineId;
if (data.detailId) {
modalApi.lock();
try {
formData.value = await getProductSalesDetail(data.detailId);
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
} else {
if (data.itemId) {
await formApi.setFieldValue('itemId', data.itemId);
}
if (data.batchId) {
await formApi.setFieldValue('batchId', data.batchId);
}
}
},
});
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" />
</Modal>
</template>

View File

@@ -0,0 +1,103 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmProductSalesDetailApi } from '#/api/mes/wm/productsales/detail';
import { computed, watch } from 'vue';
import { ElLoading, ElMessage } from 'element-plus';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteProductSalesDetail } from '#/api/mes/wm/productsales/detail';
import { $t } from '#/locales';
import { type FormType, useDetailGridColumns } from '../data';
const props = defineProps<{
details: MesWmProductSalesDetailApi.ProductSalesDetail[];
formType: FormType;
}>();
const emit = defineEmits<{
edit: [detailId: number];
refresh: [];
}>();
const isStock = computed(() => props.formType === 'stock'); // 是否为拣货模式
/** 编辑拣货明细 */
function handleEdit(row: MesWmProductSalesDetailApi.ProductSalesDetail) {
emit('edit', row.id!);
}
/** 删除拣货明细 */
async function handleDelete(
row: MesWmProductSalesDetailApi.ProductSalesDetail,
) {
const loadingInstance = ElLoading.service({
text: $t('ui.actionMessage.deleting', [row.warehouseName]),
});
try {
await deleteProductSalesDetail(row.id!);
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.warehouseName]));
emit('refresh');
} finally {
loadingInstance.close();
}
}
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
border: true,
columns: useDetailGridColumns(isStock.value),
data: props.details,
pagerConfig: {
enabled: false,
},
rowConfig: {
keyField: 'id',
isHover: true,
},
size: 'small',
toolbarConfig: {
enabled: false,
},
} as VxeTableGridOptions<MesWmProductSalesDetailApi.ProductSalesDetail>,
});
watch(
() => props.details,
(details) => gridApi.setGridOptions({ data: details }),
);
</script>
<template>
<div class="px-4 py-2">
<Grid>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'primary',
link: true,
icon: ACTION_ICON.EDIT,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'danger',
link: true,
icon: ACTION_ICON.DELETE,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [
row.warehouseName,
]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</div>
</template>

View File

@@ -0,0 +1,275 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { MesWmProductSalesApi } from '#/api/mes/wm/productsales';
import { computed, ref } from 'vue';
import { confirm, useVbenModal } from '@vben/common-ui';
import { ElButton, ElDivider, ElMessage, ElPopconfirm } from 'element-plus';
import { useVbenForm } from '#/adapter/form';
import {
checkProductSalesQuantity,
createProductSales,
finishProductSales,
getProductSales,
shippingProductSales,
stockProductSales,
submitProductSales,
updateProductSales,
} from '#/api/mes/wm/productsales';
import { $t } from '#/locales';
import { MesWmProductSalesStatusEnum } from '#/views/mes/utils/constants';
import { useFormSchema } from '../data';
import LineList from './line-list.vue';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const formData = ref<MesWmProductSalesApi.ProductSales>();
const originalSnapshot = ref(''); // 表单原始数据快照,用于提交时跳过未变更的保存请求
const isEditable = computed(() => // 是否为编辑模式(可保存)
['create', 'update'].includes(formType.value),
);
const isStock = computed(() => formType.value === 'stock'); // 是否为拣货模式
const isShipping = computed(() => formType.value === 'shipping'); // 是否为填写运单模式
const isFinish = computed(() => formType.value === 'finish'); // 是否为执行出库模式
const canSubmit = computed(() => // 是否可提交
formType.value === 'update' &&
formData.value?.status === MesWmProductSalesStatusEnum.PREPARE,
);
const getTitle = computed(() => {
if (formType.value === 'detail') {
return $t('ui.actionTitle.view', ['销售出库单']);
}
if (formType.value === 'stock') {
return '执行拣货';
}
if (formType.value === 'shipping') {
return '填写运单';
}
if (formType.value === 'finish') {
return '执行出库';
}
return formType.value === 'update'
? $t('ui.actionTitle.edit', ['销售出库单'])
: $t('ui.actionTitle.create', ['销售出库单']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-1',
labelWidth: 110,
},
layout: 'horizontal',
schema: [],
showDefaultActions: false,
wrapperClass: 'grid-cols-3',
});
/** 提交出库单:表单有修改时先保存,再调用提交接口 */
async function handleSubmit() {
const { valid } = await formApi.validate();
if (!valid || !formData.value?.id) {
return;
}
modalApi.lock();
try {
const current = JSON.stringify(await formApi.getValues());
if (current !== originalSnapshot.value) {
const data =
(await formApi.getValues()) as MesWmProductSalesApi.ProductSales;
await updateProductSales({ ...formData.value, ...data });
originalSnapshot.value = current;
}
await submitProductSales(formData.value.id);
ElMessage.success('提交成功');
await modalApi.close();
emit('success');
} finally {
modalApi.unlock();
}
}
/** 执行拣货:出库数量与拣货数量不一致时二次确认 */
async function handleStock() {
if (!formData.value?.id) {
return;
}
const quantityMatch = await checkProductSalesQuantity(formData.value.id);
if (!quantityMatch) {
try {
await confirm('出库数量与拣货数量不一致,确认执行拣货?');
} catch {
return;
}
}
modalApi.lock();
try {
await stockProductSales(formData.value.id);
ElMessage.success('拣货成功');
await modalApi.close();
emit('success');
} finally {
modalApi.unlock();
}
}
/** 填写运单 */
async function handleShipping() {
if (!formData.value?.id) {
return;
}
const values = (await formApi.getValues()) as MesWmProductSalesApi.ProductSales;
modalApi.lock();
try {
await shippingProductSales({
carrier: values.carrier,
id: formData.value.id,
shippingNumber: values.shippingNumber,
});
ElMessage.success('运单信息填写成功');
await modalApi.close();
emit('success');
} finally {
modalApi.unlock();
}
}
/** 执行出库 */
async function handleFinish() {
if (!formData.value?.id) {
return;
}
modalApi.lock();
try {
await finishProductSales(formData.value.id);
ElMessage.success('出库成功');
await modalApi.close();
emit('success');
} finally {
modalApi.unlock();
}
}
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
if (!isEditable.value) {
await modalApi.close();
return;
}
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data =
(await formApi.getValues()) as MesWmProductSalesApi.ProductSales;
try {
if (formData.value?.id) {
await updateProductSales({ ...formData.value, ...data });
formData.value = { ...formData.value, ...data };
} else {
const id = await createProductSales(data);
formData.value = {
...data,
id,
status: MesWmProductSalesStatusEnum.PREPARE,
};
await formApi.setFieldValue('id', id);
await formApi.setFieldValue('status', formData.value.status);
formType.value = 'update';
}
originalSnapshot.value = JSON.stringify(await formApi.getValues());
emit('success');
ElMessage.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
originalSnapshot.value = '';
return;
}
// 加载数据
const data = modalApi.getData<{ formType: FormType; id?: number }>();
formType.value = data.formType;
formApi.setState({ schema: useFormSchema(formType.value, formApi) });
formApi.setDisabled(!isEditable.value && !isShipping.value);
modalApi.setState({ showConfirmButton: isEditable.value });
if (data?.id) {
modalApi.lock();
try {
formData.value = await getProductSales(data.id);
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
}
originalSnapshot.value = JSON.stringify(await formApi.getValues());
},
});
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" />
<!-- 非新建模式展示物料信息 -->
<template v-if="formData?.id">
<ElDivider>物料信息</ElDivider>
<div class="mx-4">
<LineList
:form-type="formType"
:notice-id="formData.noticeId"
:sales-id="formData.id"
/>
</div>
</template>
<template #prepend-footer>
<div class="flex flex-auto items-center gap-2">
<ElPopconfirm
v-if="canSubmit"
title="确认提交该销售出库单?【提交后将不能修改】"
width="260"
@confirm="handleSubmit"
>
<template #reference>
<ElButton type="primary">提交</ElButton>
</template>
</ElPopconfirm>
<ElButton v-if="isStock" type="primary" @click="handleStock">
执行拣货
</ElButton>
<ElPopconfirm
v-if="isShipping"
title="确认提交运单信息?"
width="260"
@confirm="handleShipping"
>
<template #reference>
<ElButton type="primary">确认填写</ElButton>
</template>
</ElPopconfirm>
<ElPopconfirm
v-if="isFinish"
title="确认执行出库?执行后将扣减库存。"
width="260"
@confirm="handleFinish"
>
<template #reference>
<ElButton type="primary">确认出库</ElButton>
</template>
</ElPopconfirm>
</div>
</template>
</Modal>
</template>

View File

@@ -0,0 +1,104 @@
<script lang="ts" setup>
import type { MesWmProductSalesLineApi } from '#/api/mes/wm/productsales/line';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { ElMessage } from 'element-plus';
import { useVbenForm } from '#/adapter/form';
import {
createProductSalesLine,
getProductSalesLine,
updateProductSalesLine,
} from '#/api/mes/wm/productsales/line';
import { $t } from '#/locales';
import { useLineFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<MesWmProductSalesLineApi.ProductSalesLine>();
const salesId = ref<number>(); // 所属出库单编号
const noticeId = ref<number>(); // 所属发货通知单编号
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['销售出库单行'])
: $t('ui.actionTitle.create', ['销售出库单行']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-1',
labelWidth: 110,
},
layout: 'horizontal',
schema: [],
showDefaultActions: false,
wrapperClass: 'grid-cols-3',
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data =
(await formApi.getValues()) as MesWmProductSalesLineApi.ProductSalesLine;
data.salesId = salesId.value;
try {
await (formData.value?.id
? updateProductSalesLine({ ...data, id: formData.value.id })
: createProductSalesLine(data));
// 关闭并提示
await modalApi.close();
emit('success');
ElMessage.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
// 加载数据
const data = modalApi.getData<{
id?: number;
noticeId?: number;
salesId: number;
}>();
salesId.value = data.salesId;
noticeId.value = data.noticeId;
formApi.setState({ schema: useLineFormSchema(!!data.noticeId, formApi) });
if (data.noticeId) {
await formApi.setFieldValue('noticeId', data.noticeId);
}
if (!data.id) {
return;
}
modalApi.lock();
try {
formData.value = await getProductSalesLine(data.id);
// 设置到 values
await formApi.setValues({ ...formData.value, noticeId: noticeId.value });
} finally {
modalApi.unlock();
}
},
});
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" />
</Modal>
</template>

View File

@@ -0,0 +1,234 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmProductSalesDetailApi } from '#/api/mes/wm/productsales/detail';
import type { MesWmProductSalesLineApi } from '#/api/mes/wm/productsales/line';
import { computed, reactive } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { ElLoading, ElMessage } from 'element-plus';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getProductSalesDetailListByLineId } from '#/api/mes/wm/productsales/detail';
import {
deleteProductSalesLine,
getProductSalesLinePage,
} from '#/api/mes/wm/productsales/line';
import { $t } from '#/locales';
import { type FormType, useLineGridColumns } from '../data';
import DetailForm from './detail-form.vue';
import DetailList from './detail-list.vue';
import LineForm from './line-form.vue';
const props = defineProps<{
formType: FormType;
noticeId?: number;
salesId: number;
}>();
const isEditable = computed(() => // 是否可编辑明细行
['create', 'update'].includes(props.formType),
);
const isStock = computed(() => props.formType === 'stock'); // 是否为拣货模式
const detailMap = reactive<
Record<number, MesWmProductSalesDetailApi.ProductSalesDetail[]>
>({}); // 已展开行的拣货明细缓存
const [LineFormModal, lineFormModalApi] = useVbenModal({
connectedComponent: LineForm,
destroyOnClose: true,
});
const [DetailFormModal, detailFormModalApi] = useVbenModal({
connectedComponent: DetailForm,
destroyOnClose: true,
});
/** 刷新表格 */
function handleRefresh() {
for (const id of Object.keys(detailMap)) {
delete detailMap[Number(id)];
}
gridApi.query();
}
/** 添加物料 */
function handleCreate() {
lineFormModalApi
.setData({ noticeId: props.noticeId, salesId: props.salesId })
.open();
}
/** 编辑物料 */
function handleEdit(row: MesWmProductSalesLineApi.ProductSalesLine) {
lineFormModalApi
.setData({ id: row.id, noticeId: props.noticeId, salesId: props.salesId })
.open();
}
/** 删除物料 */
async function handleDelete(row: MesWmProductSalesLineApi.ProductSalesLine) {
const loadingInstance = ElLoading.service({
text: $t('ui.actionMessage.deleting', [row.itemName]),
});
try {
await deleteProductSalesLine(row.id!);
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.itemName]));
handleRefresh();
} finally {
loadingInstance.close();
}
}
/** 拣货:直接打开明细创建表单 */
function handlePicking(row: MesWmProductSalesLineApi.ProductSalesLine) {
openDetailForm(row.id!, row.itemId, row.batchId);
}
/** 打开拣货明细表单 */
function openDetailForm(
lineId: number,
itemId?: number,
batchId?: number,
detailId?: number,
) {
detailFormModalApi
.setData({ batchId, detailId, itemId, lineId, salesId: props.salesId })
.open();
}
/** 获取已展开行的拣货明细 */
function getExpandedDetails(row: MesWmProductSalesLineApi.ProductSalesLine) {
return detailMap[row.id!] || [];
}
/** 加载指定行的拣货明细 */
async function loadLineDetails(lineId: number) {
detailMap[lineId] = await getProductSalesDetailListByLineId(lineId);
}
/** 展开行时懒加载拣货明细 */
async function handleExpandChange(
row: MesWmProductSalesLineApi.ProductSalesLine,
expanded: boolean,
) {
if (!expanded) {
return;
}
await loadLineDetails(row.id!);
}
/** 明细表单提交成功后,刷新对应行已展开的明细 */
async function handleDetailSuccess(lineId: number) {
await loadLineDetails(lineId);
}
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useLineGridColumns(isEditable.value, isStock.value),
expandConfig: {
padding: true,
},
height: 400,
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }) => {
if (!props.salesId) {
return { list: [], total: 0 };
}
return await getProductSalesLinePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
salesId: props.salesId,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
},
} as VxeTableGridOptions<MesWmProductSalesLineApi.ProductSalesLine>,
gridEvents: {
toggleRowExpand: ({
expanded,
row,
}: {
expanded: boolean;
row: MesWmProductSalesLineApi.ProductSalesLine;
}) => {
handleExpandChange(row, expanded);
},
},
});
</script>
<template>
<div>
<LineFormModal @success="handleRefresh" />
<DetailFormModal @success="handleDetailSuccess" />
<Grid table-title="物料信息">
<template v-if="isEditable" #toolbar-tools>
<TableAction
:actions="[
{
label: '添加物料',
type: 'primary',
icon: ACTION_ICON.ADD,
onClick: handleCreate,
},
]"
/>
</template>
<template #detail="{ row }">
<DetailList
:details="getExpandedDetails(row)"
:form-type="formType"
@edit="
(detailId) =>
openDetailForm(row.id!, row.itemId, row.batchId, detailId)
"
@refresh="loadLineDetails(row.id!)"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'primary',
link: true,
icon: ACTION_ICON.EDIT,
ifShow: isEditable,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'danger',
link: true,
icon: ACTION_ICON.DELETE,
ifShow: isEditable,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.itemName]),
confirm: handleDelete.bind(null, row),
},
},
{
label: '拣货',
type: 'primary',
link: true,
ifShow: isStock,
onClick: handlePicking.bind(null, row),
},
]"
/>
</template>
</Grid>
</div>
</template>

View File

@@ -0,0 +1,2 @@
export { default as WmSalesNoticeLineSelect } from './wm-sales-notice-line-select.vue';
export { default as WmSalesNoticeSelect } from './wm-sales-notice-select.vue';

View File

@@ -0,0 +1,201 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmSalesNoticeLineApi } from '#/api/mes/wm/salesnotice/line';
import { nextTick, ref } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { ElButton, ElDialog, ElMessage } from 'element-plus';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getSalesNoticeLinePage } from '#/api/mes/wm/salesnotice/line';
const emit = defineEmits<{
selected: [rows: MesWmSalesNoticeLineApi.SalesNoticeLine[]];
}>();
const open = ref(false); // 弹窗是否打开
const noticeId = ref<number>(); // 所属通知单编号
const syncingSingleSelection = ref(false); // 是否同步单选勾选状态
const selectedRows = ref<MesWmSalesNoticeLineApi.SalesNoticeLine[]>([]); // 已选行列表
const preSelectedIds = ref<number[]>([]); // 预选行编号列表
/** 表格字段 */
function useGridColumns(): VxeTableGridOptions<MesWmSalesNoticeLineApi.SalesNoticeLine>['columns'] {
return [
{
type: 'checkbox',
width: 50,
},
{
field: 'itemCode',
title: '物料编码',
minWidth: 120,
},
{
field: 'itemName',
title: '物料名称',
minWidth: 140,
},
{
field: 'specification',
title: '规格型号',
minWidth: 120,
},
{
field: 'unitMeasureName',
title: '单位',
width: 80,
},
{
field: 'quantity',
title: '发货数量',
width: 100,
},
{
field: 'batchCode',
title: '批次号',
minWidth: 120,
},
{
field: 'oqcCheckFlag',
title: '是否检验',
width: 90,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
},
},
];
}
/** 单选模式下同步 VXE 勾选状态 */
async function syncSingleSelection(
row?: MesWmSalesNoticeLineApi.SalesNoticeLine,
) {
syncingSingleSelection.value = true;
await nextTick();
await gridApi.grid.clearCheckboxRow();
if (row) {
await gridApi.grid.setCheckboxRow(row, true);
}
await nextTick();
syncingSingleSelection.value = false;
}
/** 处理勾选变化,只保留最后一条 */
async function handleCheckboxChange({
checked,
row,
}: {
checked: boolean;
row?: MesWmSalesNoticeLineApi.SalesNoticeLine;
}) {
if (syncingSingleSelection.value) {
return;
}
const selected = checked && row ? [row] : [];
selectedRows.value = selected;
await syncSingleSelection(selected[0]);
}
/** 回显预选行 */
function applyPreSelection() {
if (preSelectedIds.value.length === 0) {
return;
}
const rows =
gridApi.grid.getData() as MesWmSalesNoticeLineApi.SalesNoticeLine[];
for (const row of rows) {
if (row.id && preSelectedIds.value.includes(row.id)) {
gridApi.grid.setCheckboxRow(row, true);
selectedRows.value = [row];
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useGridColumns(),
height: 460,
keepSource: true,
checkboxConfig: {
highlight: true,
reserve: true,
},
proxyConfig: {
ajax: {
query: async ({ page }) => {
if (!noticeId.value) {
return { list: [], total: 0 };
}
return await getSalesNoticeLinePage({
noticeId: noticeId.value,
pageNo: page.currentPage,
pageSize: page.pageSize,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
},
} as VxeTableGridOptions<MesWmSalesNoticeLineApi.SalesNoticeLine>,
gridEvents: {
checkboxChange: handleCheckboxChange,
},
});
/** 打开行选择弹窗 */
async function openModal(id: number | undefined, selectedIds?: number[]) {
open.value = true;
noticeId.value = id;
preSelectedIds.value = selectedIds || [];
selectedRows.value = [];
await nextTick();
await gridApi.grid.clearCheckboxRow();
await gridApi.query();
await nextTick();
applyPreSelection();
}
/** 关闭弹窗 */
async function closeModal() {
open.value = false;
selectedRows.value = [];
await gridApi.grid.clearCheckboxRow();
}
/** 确认选择行 */
function handleConfirm() {
if (selectedRows.value.length === 0) {
ElMessage.warning('请选择一条数据');
return;
}
emit('selected', [selectedRows.value[0]!]);
open.value = false;
}
defineExpose({ open: openModal });
</script>
<template>
<ElDialog
v-model="open"
title="发货通知单行选择"
width="70%"
destroy-on-close
@close="closeModal"
>
<Grid table-title="发货通知单行列表" />
<template #footer>
<ElButton @click="closeModal">取消</ElButton>
<ElButton type="primary" @click="handleConfirm">确定</ElButton>
</template>
</ElDialog>
</template>

View File

@@ -0,0 +1,149 @@
<script lang="ts" setup>
import type { MesWmSalesNoticeLineApi } from '#/api/mes/wm/salesnotice/line';
import { computed, ref, useAttrs, watch } from 'vue';
import { CircleX, Search } from '@vben/icons';
import { ElInput, ElTooltip } from 'element-plus';
import { getSalesNoticeLine } from '#/api/mes/wm/salesnotice/line';
import WmSalesNoticeLineSelectDialog from './wm-sales-notice-line-select-dialog.vue';
defineOptions({ name: 'WmSalesNoticeLineSelect', inheritAttrs: false });
const props = withDefaults(
defineProps<{
clearable?: boolean;
disabled?: boolean;
modelValue?: number;
noticeId?: number; // 所属发货通知单编号
placeholder?: string;
}>(),
{
clearable: true,
disabled: false,
modelValue: undefined,
noticeId: undefined,
placeholder: '请选择发货通知单行',
},
);
const emit = defineEmits<{
change: [item: MesWmSalesNoticeLineApi.SalesNoticeLine | undefined];
'update:modelValue': [value: number | undefined];
}>();
const attrs = useAttrs();
const dialogRef = ref<InstanceType<typeof WmSalesNoticeLineSelectDialog>>();
const hovering = ref(false);
const selectedItem = ref<MesWmSalesNoticeLineApi.SalesNoticeLine>();
const displayLabel = computed(() => {
const item = selectedItem.value;
if (!item) {
return '';
}
return `${item.itemCode ?? ''} - ${item.itemName ?? ''}`;
});
const showClear = computed(
() =>
props.clearable &&
!props.disabled &&
hovering.value &&
props.modelValue != null,
);
/** 根据编号单条查询行信息(用于编辑回显) */
async function resolveItemById(id: number | undefined) {
if (id == null) {
selectedItem.value = undefined;
return;
}
if (selectedItem.value?.id === id) {
return;
}
selectedItem.value = await getSalesNoticeLine(id);
}
watch(() => props.modelValue, resolveItemById, { immediate: true });
/** noticeId 变化时清空选中(关联的行已失效) */
watch(
() => props.noticeId,
() => {
selectedItem.value = undefined;
emit('update:modelValue', undefined);
emit('change', undefined);
},
);
/** 清空已选行 */
function clearSelected() {
selectedItem.value = undefined;
emit('update:modelValue', undefined);
emit('change', undefined);
}
/** 打开行选择弹窗 */
function handleClick(event: MouseEvent) {
if (props.disabled || !props.noticeId) {
return;
}
const target = event.target as HTMLElement;
if (showClear.value && target.closest('.el-input__suffix')) {
event.stopPropagation();
clearSelected();
return;
}
const selectedIds = props.modelValue == null ? [] : [props.modelValue];
dialogRef.value?.open(props.noticeId, selectedIds);
}
/** 弹窗选中回调 */
function handleSelected(rows: MesWmSalesNoticeLineApi.SalesNoticeLine[]) {
const item = rows[0];
if (!item) {
return;
}
selectedItem.value = item;
emit('update:modelValue', item.id);
emit('change', item);
}
</script>
<template>
<div
v-bind="attrs"
class="w-full"
:class="disabled ? 'cursor-not-allowed' : 'cursor-pointer'"
@click="handleClick"
@mouseenter="hovering = true"
@mouseleave="hovering = false"
>
<ElTooltip :disabled="!selectedItem" placement="top" :show-after="500">
<template #content>
<div v-if="selectedItem" class="leading-6">
<div>物料编码{{ selectedItem.itemCode || '-' }}</div>
<div>物料名称{{ selectedItem.itemName || '-' }}</div>
<div>规格型号{{ selectedItem.specification || '-' }}</div>
<div>发货数量{{ selectedItem.quantity ?? '-' }}</div>
</div>
</template>
<ElInput
:disabled="disabled"
:model-value="displayLabel"
:placeholder="placeholder"
readonly
>
<template #suffix>
<CircleX v-if="showClear" class="size-4" />
<Search v-else class="size-4" />
</template>
</ElInput>
</ElTooltip>
</div>
<WmSalesNoticeLineSelectDialog ref="dialogRef" @selected="handleSelected" />
</template>

View File

@@ -0,0 +1,265 @@
<script lang="ts" setup>
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmSalesNoticeApi } from '#/api/mes/wm/salesnotice';
import { nextTick, ref } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { ElButton, ElDialog, ElMessage } from 'element-plus';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getSalesNoticePage } from '#/api/mes/wm/salesnotice';
const emit = defineEmits<{
selected: [rows: MesWmSalesNoticeApi.SalesNotice[]];
}>();
const open = ref(false); // 弹窗是否打开
const multiple = ref(false); // 是否多选
const fixedStatus = ref<number>(); // 固定状态筛选
const syncingSingleSelection = ref(false); // 是否同步单选勾选状态
const selectedRows = ref<MesWmSalesNoticeApi.SalesNotice[]>([]); // 已选通知单列表
const preSelectedIds = ref<number[]>([]); // 预选通知单编号列表
/** 搜索表单 */
function useSearchSchema(): VbenFormSchema[] {
return [
{
fieldName: 'code',
label: '通知单编号',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入通知单编号',
},
},
{
fieldName: 'name',
label: '通知单名称',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入通知单名称',
},
},
{
fieldName: 'salesOrderCode',
label: '销售订单编号',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入销售订单编号',
},
},
];
}
/** 表格字段 */
function useGridColumns(): VxeTableGridOptions<MesWmSalesNoticeApi.SalesNotice>['columns'] {
return [
{
type: 'checkbox',
width: 50,
},
{
field: 'code',
title: '通知单编号',
minWidth: 160,
},
{
field: 'name',
title: '通知单名称',
minWidth: 150,
},
{
field: 'salesOrderCode',
title: '销售订单编号',
minWidth: 140,
},
{
field: 'clientName',
title: '客户名称',
minWidth: 120,
},
{
field: 'salesDate',
title: '发货日期',
width: 120,
formatter: 'formatDate',
},
{
field: 'status',
title: '单据状态',
width: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.MES_WM_SALES_NOTICE_STATUS },
},
},
];
}
/** 单选模式下同步 VXE 勾选状态,避免跨页残留多选 */
async function syncSingleSelection(row?: MesWmSalesNoticeApi.SalesNotice) {
syncingSingleSelection.value = true;
await nextTick();
await gridApi.grid.clearCheckboxRow();
if (row) {
await gridApi.grid.setCheckboxRow(row, true);
}
await nextTick();
syncingSingleSelection.value = false;
}
/** 处理勾选变化,单选模式只保留最后一条 */
async function handleCheckboxChange({
checked,
records,
row,
}: {
checked: boolean;
records: MesWmSalesNoticeApi.SalesNotice[];
row?: MesWmSalesNoticeApi.SalesNotice;
}) {
if (syncingSingleSelection.value) {
return;
}
if (!multiple.value) {
const selected = checked && row ? [row] : [];
selectedRows.value = selected;
await syncSingleSelection(selected[0]);
return;
}
selectedRows.value = records;
}
/** 处理全选变化 */
function handleCheckboxAll({
records,
}: {
records: MesWmSalesNoticeApi.SalesNotice[];
}) {
if (syncingSingleSelection.value) {
return;
}
selectedRows.value = records;
}
/** 回显预选通知单 */
function applyPreSelection() {
if (preSelectedIds.value.length === 0) {
return;
}
const rows = gridApi.grid.getData() as MesWmSalesNoticeApi.SalesNotice[];
for (const row of rows) {
if (row.id && preSelectedIds.value.includes(row.id)) {
gridApi.grid.setCheckboxRow(row, true);
if (!multiple.value) {
selectedRows.value = [row];
}
}
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useSearchSchema(),
},
gridOptions: {
columns: useGridColumns(),
height: 520,
keepSource: true,
checkboxConfig: {
highlight: true,
range: true,
reserve: true,
},
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getSalesNoticePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
status: fixedStatus.value,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
search: true,
},
} as VxeTableGridOptions<MesWmSalesNoticeApi.SalesNotice>,
gridEvents: {
checkboxAll: handleCheckboxAll,
checkboxChange: handleCheckboxChange,
},
});
/** 重置查询和选择状态 */
async function resetQueryState() {
selectedRows.value = [];
await gridApi.grid.clearCheckboxRow();
await gridApi.formApi.resetForm();
}
/** 打开通知单选择弹窗 */
async function openModal(
selectedIds?: number[],
options?: { multiple?: boolean; status?: number },
) {
open.value = true;
multiple.value = options?.multiple ?? false;
fixedStatus.value = options?.status;
preSelectedIds.value = selectedIds || [];
await nextTick();
await resetQueryState();
await gridApi.query();
await nextTick();
applyPreSelection();
}
/** 关闭通知单选择弹窗 */
async function closeModal() {
open.value = false;
await resetQueryState();
}
/** 确认选择通知单 */
function handleConfirm() {
if (selectedRows.value.length === 0) {
ElMessage.warning(multiple.value ? '请至少选择一条数据' : '请选择一条数据');
return;
}
emit(
'selected',
multiple.value ? selectedRows.value : [selectedRows.value[0]!],
);
open.value = false;
}
defineExpose({ open: openModal });
</script>
<template>
<ElDialog
v-model="open"
title="发货通知单选择"
width="70%"
destroy-on-close
@close="closeModal"
>
<Grid table-title="发货通知单列表" />
<template #footer>
<ElButton @click="closeModal">取消</ElButton>
<ElButton type="primary" @click="handleConfirm">确定</ElButton>
</template>
</ElDialog>
</template>

View File

@@ -0,0 +1,136 @@
<script lang="ts" setup>
import type { MesWmSalesNoticeApi } from '#/api/mes/wm/salesnotice';
import { computed, ref, useAttrs, watch } from 'vue';
import { CircleX, Search } from '@vben/icons';
import { ElInput, ElTooltip } from 'element-plus';
import { getSalesNotice } from '#/api/mes/wm/salesnotice';
import WmSalesNoticeSelectDialog from './wm-sales-notice-select-dialog.vue';
defineOptions({ name: 'WmSalesNoticeSelect', inheritAttrs: false });
const props = withDefaults(
defineProps<{
clearable?: boolean;
disabled?: boolean;
modelValue?: number;
placeholder?: string;
status?: number; // 固定状态筛选
}>(),
{
clearable: true,
disabled: false,
modelValue: undefined,
placeholder: '请选择发货通知单',
status: undefined,
},
);
const emit = defineEmits<{
change: [item: MesWmSalesNoticeApi.SalesNotice | undefined];
'update:modelValue': [value: number | undefined];
}>();
const attrs = useAttrs();
const dialogRef = ref<InstanceType<typeof WmSalesNoticeSelectDialog>>();
const hovering = ref(false);
const selectedItem = ref<MesWmSalesNoticeApi.SalesNotice>();
const displayLabel = computed(() => selectedItem.value?.name ?? '');
const showClear = computed(
() =>
props.clearable &&
!props.disabled &&
hovering.value &&
props.modelValue != null,
);
/** 根据编号单条查询通知单信息(用于编辑回显) */
async function resolveItemById(id: number | undefined) {
if (id == null) {
selectedItem.value = undefined;
return;
}
if (selectedItem.value?.id === id) {
return;
}
selectedItem.value = await getSalesNotice(id);
}
watch(() => props.modelValue, resolveItemById, { immediate: true });
/** 清空已选通知单 */
function clearSelected() {
selectedItem.value = undefined;
emit('update:modelValue', undefined);
emit('change', undefined);
}
/** 打开通知单选择弹窗 */
function handleClick(event: MouseEvent) {
if (props.disabled) {
return;
}
const target = event.target as HTMLElement;
if (showClear.value && target.closest('.el-input__suffix')) {
event.stopPropagation();
clearSelected();
return;
}
const selectedIds = props.modelValue == null ? [] : [props.modelValue];
dialogRef.value?.open(selectedIds, {
multiple: false,
status: props.status,
});
}
/** 弹窗选中回调 */
function handleSelected(rows: MesWmSalesNoticeApi.SalesNotice[]) {
const item = rows[0];
if (!item) {
return;
}
selectedItem.value = item;
emit('update:modelValue', item.id);
emit('change', item);
}
</script>
<template>
<div
v-bind="attrs"
class="w-full"
:class="disabled ? 'cursor-not-allowed' : 'cursor-pointer'"
@click="handleClick"
@mouseenter="hovering = true"
@mouseleave="hovering = false"
>
<ElTooltip :disabled="!selectedItem" placement="top" :show-after="500">
<template #content>
<div v-if="selectedItem" class="leading-6">
<div>编号{{ selectedItem.code || '-' }}</div>
<div>名称{{ selectedItem.name || '-' }}</div>
<div>客户{{ selectedItem.clientName || '-' }}</div>
<div>销售订单{{ selectedItem.salesOrderCode || '-' }}</div>
</div>
</template>
<ElInput
:disabled="disabled"
:model-value="displayLabel"
:placeholder="placeholder"
readonly
>
<template #suffix>
<CircleX v-if="showClear" class="size-4" />
<Search v-else class="size-4" />
</template>
</ElInput>
</ElTooltip>
</div>
<WmSalesNoticeSelectDialog ref="dialogRef" @selected="handleSelected" />
</template>

View File

@@ -0,0 +1,364 @@
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmSalesNoticeApi } from '#/api/mes/wm/salesnotice';
import type { MesWmSalesNoticeLineApi } from '#/api/mes/wm/salesnotice/line';
import { h, markRaw } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { ElButton } from 'element-plus';
import { z } from '#/adapter/form';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import MdClientSelect from '#/views/mes/md/client/components/md-client-select.vue';
import MdItemSelect from '#/views/mes/md/item/components/md-item-select.vue';
import { MesAutoCodeRuleCode } from '#/views/mes/utils/constants';
/** 表单类型 */
export type FormType = 'create' | 'detail' | 'finish' | 'update';
/** 表单头部是否只读(详情、执行出库态) */
function isHeaderReadonly(formType: FormType): boolean {
return formType === 'detail' || formType === 'finish';
}
/** 新增/修改的表单 */
export function useFormSchema(
formType: FormType,
formApi?: VbenFormApi,
): VbenFormSchema[] {
return [
{
fieldName: 'id',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'status',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'code',
label: '通知单编号',
component: 'Input',
componentProps: {
placeholder: '请输入通知单编号',
},
rules: 'required',
suffix: isHeaderReadonly(formType)
? undefined
: () =>
h(
ElButton,
{
onClick: async () => {
const code = await generateAutoCode(
MesAutoCodeRuleCode.WM_SALES_NOTICE_CODE,
);
await formApi?.setFieldValue('code', code);
},
},
{ default: () => '生成' },
),
},
{
fieldName: 'name',
label: '通知单名称',
component: 'Input',
componentProps: {
placeholder: '请输入通知单名称',
},
rules: 'required',
},
{
fieldName: 'salesOrderCode',
label: '销售订单编号',
component: 'Input',
componentProps: {
placeholder: '请输入销售订单编号',
},
},
{
fieldName: 'clientId',
label: '客户',
component: markRaw(MdClientSelect),
componentProps: {
placeholder: '请选择客户',
},
rules: 'selectRequired',
},
{
fieldName: 'salesDate',
label: '发货日期',
component: 'DatePicker',
componentProps: {
class: '!w-full',
format: 'YYYY-MM-DD',
placeholder: '请选择发货日期',
type: 'date',
valueFormat: 'x',
},
rules: 'required',
},
{
fieldName: 'recipientName',
label: '收货人',
component: 'Input',
componentProps: {
placeholder: '请输入收货人',
},
},
{
fieldName: 'recipientTelephone',
label: '联系方式',
component: 'Input',
componentProps: {
placeholder: '请输入联系方式',
},
},
{
fieldName: 'recipientAddress',
label: '收货地址',
component: 'Input',
componentProps: {
placeholder: '请输入收货地址',
},
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
formItemClass: 'col-span-3',
componentProps: {
placeholder: '请输入备注',
rows: 3,
},
},
];
}
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'code',
label: '通知单编号',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入通知单编号',
},
},
{
fieldName: 'name',
label: '通知单名称',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入通知单名称',
},
},
{
fieldName: 'salesOrderCode',
label: '销售订单编号',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入销售订单编号',
},
},
{
fieldName: 'clientId',
label: '客户',
component: markRaw(MdClientSelect),
componentProps: {
placeholder: '请选择客户',
},
},
];
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions<MesWmSalesNoticeApi.SalesNotice>['columns'] {
return [
{
field: 'code',
title: '通知单编号',
minWidth: 160,
slots: { default: 'code' },
},
{
field: 'name',
title: '通知单名称',
minWidth: 150,
},
{
field: 'salesOrderCode',
title: '销售订单编号',
minWidth: 140,
},
{
field: 'clientName',
title: '客户名称',
minWidth: 120,
},
{
field: 'salesDate',
title: '发货日期',
width: 180,
formatter: 'formatDate',
},
{
field: 'recipientName',
title: '收货人',
minWidth: 100,
},
{
field: 'recipientTelephone',
title: '联系方式',
minWidth: 120,
},
{
field: 'recipientAddress',
title: '收货地址',
minWidth: 200,
},
{
field: 'status',
title: '单据状态',
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.MES_WM_SALES_NOTICE_STATUS },
},
},
{
title: '操作',
width: 200,
fixed: 'right',
slots: { default: 'actions' },
},
];
}
/** 通知单行子表的字段 */
export function useLineGridColumns(
editable: boolean,
): VxeTableGridOptions<MesWmSalesNoticeLineApi.SalesNoticeLine>['columns'] {
return [
{
field: 'itemCode',
title: '物料编码',
minWidth: 120,
},
{
field: 'itemName',
title: '物料名称',
minWidth: 140,
},
{
field: 'specification',
title: '规格型号',
minWidth: 120,
},
{
field: 'unitMeasureName',
title: '单位',
width: 80,
},
{
field: 'quantity',
title: '发货数量',
width: 100,
},
{
field: 'batchCode',
title: '批次号',
minWidth: 120,
},
{
field: 'oqcCheckFlag',
title: '是否检验',
width: 90,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
},
},
{
field: 'remark',
title: '备注',
minWidth: 120,
},
...(editable
? [
{
title: '操作',
width: 120,
fixed: 'right',
slots: { default: 'actions' },
} as const,
]
: []),
];
}
/** 通知单行新增/修改的表单 */
export function useLineFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'itemId',
label: '物料',
component: markRaw(MdItemSelect),
componentProps: {
placeholder: '请选择物料',
},
rules: 'selectRequired',
},
{
fieldName: 'quantity',
label: '发货数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
controlsPosition: 'right',
min: 0.01,
placeholder: '请输入发货数量',
precision: 2,
},
rules: 'required',
},
{
fieldName: 'batchCode',
label: '批次号',
component: 'Input',
componentProps: {
placeholder: '请输入批次号',
},
},
{
fieldName: 'oqcCheckFlag',
label: '是否检验',
component: 'Switch',
rules: z.boolean().default(true),
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
formItemClass: 'col-span-3',
componentProps: {
placeholder: '请输入备注',
rows: 3,
},
},
];
}

View File

@@ -0,0 +1,177 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmSalesNoticeApi } from '#/api/mes/wm/salesnotice';
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { ElButton, ElLoading, ElMessage } from 'element-plus';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteSalesNotice,
exportSalesNotice,
getSalesNoticePage,
} from '#/api/mes/wm/salesnotice';
import { $t } from '#/locales';
import { MesWmSalesNoticeStatusEnum } from '#/views/mes/utils/constants';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/** 刷新表格 */
function handleRefresh() {
gridApi.query();
}
/** 创建发货通知单 */
function handleCreate() {
formModalApi.setData({ formType: 'create' }).open();
}
/** 查看发货通知单 */
function handleDetail(row: MesWmSalesNoticeApi.SalesNotice) {
formModalApi.setData({ formType: 'detail', id: row.id }).open();
}
/** 编辑发货通知单 */
function handleEdit(row: MesWmSalesNoticeApi.SalesNotice) {
formModalApi.setData({ formType: 'update', id: row.id }).open();
}
/** 执行出库 */
function handleFinish(row: MesWmSalesNoticeApi.SalesNotice) {
formModalApi.setData({ formType: 'finish', id: row.id }).open();
}
/** 删除发货通知单 */
async function handleDelete(row: MesWmSalesNoticeApi.SalesNotice) {
const loadingInstance = ElLoading.service({
text: $t('ui.actionMessage.deleting', [row.code]),
});
try {
await deleteSalesNotice(row.id!);
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.code]));
handleRefresh();
} finally {
loadingInstance.close();
}
}
/** 导出表格 */
async function handleExport() {
const data = await exportSalesNotice(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '发货通知单.xls', source: data });
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getSalesNoticePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
search: true,
},
} as VxeTableGridOptions<MesWmSalesNoticeApi.SalesNotice>,
});
</script>
<template>
<Page auto-content-height>
<template #doc>
<DocAlert
title="【仓库】发货通知、销售出库、销售退货"
url="https://doc.iocoder.cn/mes/wm/sales-out/"
/>
</template>
<FormModal @success="handleRefresh" />
<Grid table-title="发货通知单列表">
<template #toolbar-tools>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['发货通知单']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['mes:wm-sales-notice:create'],
onClick: handleCreate,
},
{
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
auth: ['mes:wm-sales-notice:export'],
onClick: handleExport,
},
]"
/>
</template>
<template #code="{ row }">
<ElButton link type="primary" @click="handleDetail(row)">
{{ row.code }}
</ElButton>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'primary',
link: true,
icon: ACTION_ICON.EDIT,
auth: ['mes:wm-sales-notice:update'],
ifShow: row.status === MesWmSalesNoticeStatusEnum.PREPARE,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'danger',
link: true,
icon: ACTION_ICON.DELETE,
auth: ['mes:wm-sales-notice:delete'],
ifShow: row.status === MesWmSalesNoticeStatusEnum.PREPARE,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.code]),
confirm: handleDelete.bind(null, row),
},
},
{
label: '执行出库',
type: 'primary',
link: true,
auth: ['mes:wm-sales-notice:update'],
ifShow: row.status === MesWmSalesNoticeStatusEnum.APPROVED,
onClick: handleFinish.bind(null, row),
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@@ -0,0 +1,182 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { MesWmSalesNoticeApi } from '#/api/mes/wm/salesnotice';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { ElButton, ElDivider, ElMessage, ElPopconfirm } from 'element-plus';
import { useVbenForm } from '#/adapter/form';
import {
createSalesNotice,
getSalesNotice,
submitSalesNotice,
updateSalesNotice,
} from '#/api/mes/wm/salesnotice';
import { $t } from '#/locales';
import { MesWmSalesNoticeStatusEnum } from '#/views/mes/utils/constants';
import { useFormSchema } from '../data';
import LineList from './line-list.vue';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const formData = ref<MesWmSalesNoticeApi.SalesNotice>();
const originalSnapshot = ref(''); // 表单原始数据快照,用于提交时跳过未变更的保存请求
const isEditable = computed(() => // 是否为编辑模式(可保存)
['create', 'update'].includes(formType.value),
);
const isFinish = computed(() => formType.value === 'finish'); // 是否为执行出库模式
const canSubmit = computed(() => // 是否可提交
formType.value === 'update' &&
formData.value?.status === MesWmSalesNoticeStatusEnum.PREPARE,
);
const getTitle = computed(() => {
if (formType.value === 'detail') {
return $t('ui.actionTitle.view', ['发货通知单']);
}
if (formType.value === 'finish') {
return '执行出库';
}
return formType.value === 'update'
? $t('ui.actionTitle.edit', ['发货通知单'])
: $t('ui.actionTitle.create', ['发货通知单']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-1',
labelWidth: 110,
},
layout: 'horizontal',
schema: [],
showDefaultActions: false,
wrapperClass: 'grid-cols-3',
});
/** 提交通知单:表单有修改时先保存,再调用提交接口 */
async function handleSubmit() {
const { valid } = await formApi.validate();
if (!valid || !formData.value?.id) {
return;
}
modalApi.lock();
try {
const current = JSON.stringify(await formApi.getValues());
if (current !== originalSnapshot.value) {
const data =
(await formApi.getValues()) as MesWmSalesNoticeApi.SalesNotice;
await updateSalesNotice({ ...formData.value, ...data });
originalSnapshot.value = current;
}
await submitSalesNotice(formData.value.id);
ElMessage.success('提交成功');
await modalApi.close();
emit('success');
} finally {
modalApi.unlock();
}
}
/** 执行出库(后端暂未实现,提示用户) */
function handleFinish() {
ElMessage.info('执行出库功能暂时不支持,敬请期待!');
}
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
if (!isEditable.value) {
await modalApi.close();
return;
}
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data = (await formApi.getValues()) as MesWmSalesNoticeApi.SalesNotice;
try {
if (formData.value?.id) {
await updateSalesNotice({ ...formData.value, ...data });
formData.value = { ...formData.value, ...data };
} else {
const id = await createSalesNotice(data);
formData.value = {
...data,
id,
status: MesWmSalesNoticeStatusEnum.PREPARE,
};
await formApi.setFieldValue('id', id);
await formApi.setFieldValue('status', formData.value.status);
formType.value = 'update';
}
originalSnapshot.value = JSON.stringify(await formApi.getValues());
emit('success');
ElMessage.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
originalSnapshot.value = '';
return;
}
// 加载数据
const data = modalApi.getData<{ formType: FormType; id?: number }>();
formType.value = data.formType;
formApi.setState({ schema: useFormSchema(formType.value, formApi) });
formApi.setDisabled(!isEditable.value);
modalApi.setState({ showConfirmButton: isEditable.value });
if (data?.id) {
modalApi.lock();
try {
formData.value = await getSalesNotice(data.id);
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
}
originalSnapshot.value = JSON.stringify(await formApi.getValues());
},
});
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" />
<!-- 非新建模式展示物料信息 -->
<template v-if="formData?.id">
<ElDivider>物料信息</ElDivider>
<div class="mx-4">
<LineList :form-type="formType" :notice-id="formData.id" />
</div>
</template>
<template #prepend-footer>
<div class="flex flex-auto items-center gap-2">
<ElPopconfirm
v-if="canSubmit"
title="确认提交该发货通知单?【提交后将不能修改】"
width="260"
@confirm="handleSubmit"
>
<template #reference>
<ElButton type="primary">提交</ElButton>
</template>
</ElPopconfirm>
<ElButton v-if="isFinish" type="primary" @click="handleFinish">
执行出库
</ElButton>
</div>
</template>
</Modal>
</template>

View File

@@ -0,0 +1,94 @@
<script lang="ts" setup>
import type { MesWmSalesNoticeLineApi } from '#/api/mes/wm/salesnotice/line';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { ElMessage } from 'element-plus';
import { useVbenForm } from '#/adapter/form';
import {
createSalesNoticeLine,
getSalesNoticeLine,
updateSalesNoticeLine,
} from '#/api/mes/wm/salesnotice/line';
import { $t } from '#/locales';
import { useLineFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<MesWmSalesNoticeLineApi.SalesNoticeLine>();
const noticeId = ref<number>(); // 所属通知单编号
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['发货通知单行'])
: $t('ui.actionTitle.create', ['发货通知单行']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-1',
labelWidth: 90,
},
layout: 'horizontal',
schema: useLineFormSchema(),
showDefaultActions: false,
wrapperClass: 'grid-cols-3',
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data =
(await formApi.getValues()) as MesWmSalesNoticeLineApi.SalesNoticeLine;
data.noticeId = noticeId.value;
try {
await (formData.value?.id
? updateSalesNoticeLine({ ...data, id: formData.value.id })
: createSalesNoticeLine(data));
// 关闭并提示
await modalApi.close();
emit('success');
ElMessage.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
return;
}
// 加载数据
const data = modalApi.getData<{ id?: number; noticeId: number }>();
noticeId.value = data.noticeId;
if (!data.id) {
return;
}
modalApi.lock();
try {
formData.value = await getSalesNoticeLine(data.id);
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
},
});
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" />
</Modal>
</template>

View File

@@ -0,0 +1,137 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmSalesNoticeLineApi } from '#/api/mes/wm/salesnotice/line';
import { computed } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { ElLoading, ElMessage } from 'element-plus';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteSalesNoticeLine,
getSalesNoticeLinePage,
} from '#/api/mes/wm/salesnotice/line';
import { $t } from '#/locales';
import { type FormType, useLineGridColumns } from '../data';
import LineForm from './line-form.vue';
const props = defineProps<{
formType: FormType;
noticeId: number;
}>();
const isEditable = computed(() => // 是否可编辑明细行
['create', 'update'].includes(props.formType),
);
const [LineFormModal, lineFormModalApi] = useVbenModal({
connectedComponent: LineForm,
destroyOnClose: true,
});
/** 刷新表格 */
function handleRefresh() {
gridApi.query();
}
/** 添加物料 */
function handleCreate() {
lineFormModalApi.setData({ noticeId: props.noticeId }).open();
}
/** 编辑物料 */
function handleEdit(row: MesWmSalesNoticeLineApi.SalesNoticeLine) {
lineFormModalApi.setData({ id: row.id, noticeId: props.noticeId }).open();
}
/** 删除物料 */
async function handleDelete(row: MesWmSalesNoticeLineApi.SalesNoticeLine) {
const loadingInstance = ElLoading.service({
text: $t('ui.actionMessage.deleting', [row.itemName]),
});
try {
await deleteSalesNoticeLine(row.id!);
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.itemName]));
handleRefresh();
} finally {
loadingInstance.close();
}
}
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useLineGridColumns(isEditable.value),
height: 400,
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }) => {
if (!props.noticeId) {
return { list: [], total: 0 };
}
return await getSalesNoticeLinePage({
noticeId: props.noticeId,
pageNo: page.currentPage,
pageSize: page.pageSize,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
},
} as VxeTableGridOptions<MesWmSalesNoticeLineApi.SalesNoticeLine>,
});
</script>
<template>
<div>
<LineFormModal @success="handleRefresh" />
<Grid table-title="物料信息">
<template v-if="isEditable" #toolbar-tools>
<TableAction
:actions="[
{
label: '添加物料',
type: 'primary',
icon: ACTION_ICON.ADD,
onClick: handleCreate,
},
]"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'primary',
link: true,
icon: ACTION_ICON.EDIT,
ifShow: isEditable,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
type: 'danger',
link: true,
icon: ACTION_ICON.DELETE,
ifShow: isEditable,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.itemName]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</div>
</template>