feat(mes): 优化 materialstock 的代码实现风格

pull/350/head
YunaiV 2026-05-30 09:35:09 +08:00
parent 79af870afe
commit e313de09c4
20 changed files with 1800 additions and 266 deletions

View File

@ -1,11 +1,427 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProCardApi } from '#/api/mes/pro/card';
import type { MesProCardProcessApi } from '#/api/mes/pro/card/process';
import { markRaw } from 'vue';
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 { MdItemSelect } from '#/views/mes/md/item/components';
import { MdWorkstationSelect } from '#/views/mes/md/workstation/components';
import { ProProcessSelect } from '#/views/mes/pro/process/components';
import { ProWorkOrderSelect } from '#/views/mes/pro/workorder/components';
import {
MesAutoCodeRuleCode,
MesProWorkOrderStatusEnum,
} from '#/views/mes/utils/constants';
import { UserSelect } from '#/views/system/user/components';
/** 表单类型 */
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[] {
const headerReadonly = isHeaderReadonly(formType);
return [
{
fieldName: 'id',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'status',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'code',
label: '流转卡编码',
component: 'Input',
componentProps: {
disabled: headerReadonly,
placeholder: '请输入流转卡编码',
},
rules: 'required',
suffix:
formType === 'create' || formType === 'update'
? () =>
h(
Button,
{
type: 'default',
onClick: async () => {
const code = await generateAutoCode(
MesAutoCodeRuleCode.PRO_CARD_CODE,
);
await formApi?.setFieldValue('code', code);
},
},
{ default: () => '生成' },
)
: undefined,
},
{
fieldName: 'workOrderId',
label: '生产工单',
component: markRaw(ProWorkOrderSelect),
componentProps: {
disabled: headerReadonly,
placeholder: '请选择生产工单',
status: MesProWorkOrderStatusEnum.CONFIRMED,
},
rules: 'selectRequired',
},
{
fieldName: 'itemId',
label: '产品',
component: markRaw(MdItemSelect),
componentProps: {
disabled: headerReadonly,
placeholder: '请选择产品',
},
rules: 'selectRequired',
},
{
fieldName: 'transferedQuantity',
label: '流转数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
disabled: headerReadonly,
min: 0,
placeholder: '请输入流转数量',
precision: 2,
},
rules: 'required',
},
{
fieldName: 'batchCode',
label: '批次号',
component: 'Input',
componentProps: {
disabled: headerReadonly,
placeholder: '请输入批次号',
},
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
formItemClass: 'col-span-3',
componentProps: {
disabled: headerReadonly,
placeholder: '请输入备注',
rows: 3,
},
},
];
}
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'code',
label: '流转卡编码',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入流转卡编码',
},
},
{
fieldName: 'workOrderId',
label: '生产工单',
component: markRaw(ProWorkOrderSelect),
componentProps: {
placeholder: '请选择生产工单',
},
},
{
fieldName: 'itemId',
label: '产品',
component: markRaw(MdItemSelect),
componentProps: {
placeholder: '请选择产品',
},
},
{
fieldName: 'batchCode',
label: '批次号',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入批次号',
},
},
];
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions<MesProCardApi.Card>['columns'] {
return [
{
field: 'code',
title: '流转卡编码',
width: 160,
slots: { default: 'code' },
},
{
field: 'workOrderCode',
title: '生产工单编号',
width: 160,
},
{
field: 'workOrderName',
title: '工单名称',
minWidth: 150,
},
{
field: 'batchCode',
title: '批次号',
width: 120,
},
{
field: 'itemCode',
title: '产品物料编码',
width: 140,
},
{
field: 'itemName',
title: '产品物料名称',
minWidth: 120,
},
{
field: 'specification',
title: '规格型号',
width: 120,
},
{
field: 'unitMeasureName',
title: '单位',
width: 80,
},
{
field: 'transferedQuantity',
title: '流转数量',
width: 100,
},
{
field: 'status',
title: '单据状态',
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.MES_PRO_WORK_ORDER_STATUS },
},
},
{
title: '操作',
width: 240,
fixed: 'right',
slots: { default: 'actions' },
},
];
}
/** 工序记录子表的字段 */
export function useProcessGridColumns(
editable: boolean,
): VxeTableGridOptions<MesProCardProcessApi.CardProcess>['columns'] {
return [
{
field: 'sort',
title: '序号',
width: 60,
},
{
field: 'processName',
title: '工序名称',
minWidth: 120,
},
{
field: 'processCode',
title: '工序编码',
width: 120,
},
{
field: 'inputTime',
title: '进入工序时间',
width: 180,
formatter: 'formatDateTime',
},
{
field: 'outputTime',
title: '出工序时间',
width: 180,
formatter: 'formatDateTime',
},
{
field: 'inputQuantity',
title: '投入数量',
width: 100,
},
{
field: 'outputQuantity',
title: '产出数量',
width: 100,
},
{
field: 'unqualifiedQuantity',
title: '不良品数量',
width: 100,
},
{
field: 'workstationCode',
title: '工位编码',
width: 120,
},
{
field: 'workstationName',
title: '工位名称',
minWidth: 120,
},
{
field: 'nickname',
title: '操作人',
width: 100,
},
...(editable
? [
{
title: '操作',
width: 160,
fixed: 'right',
slots: { default: 'actions' },
} as const,
]
: []),
];
}
/** 工序记录新增/修改的表单 */
export function useProcessFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'sort',
label: '序号',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
placeholder: '请输入序号',
precision: 0,
},
},
{
fieldName: 'processId',
label: '工序',
component: markRaw(ProProcessSelect),
componentProps: {
placeholder: '请选择工序',
},
},
{
fieldName: 'inputTime',
label: '进入工序时间',
component: 'DatePicker',
componentProps: {
class: '!w-full',
placeholder: '请选择进入工序时间',
showTime: true,
valueFormat: 'x',
},
},
{
fieldName: 'outputTime',
label: '出工序时间',
component: 'DatePicker',
componentProps: {
class: '!w-full',
placeholder: '请选择出工序时间',
showTime: true,
valueFormat: 'x',
},
},
{
fieldName: 'inputQuantity',
label: '投入数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
placeholder: '请输入投入数量',
precision: 2,
},
},
{
fieldName: 'outputQuantity',
label: '产出数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
placeholder: '请输入产出数量',
precision: 2,
},
},
{
fieldName: 'unqualifiedQuantity',
label: '不合格数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0,
placeholder: '请输入不合格数量',
precision: 2,
},
},
{
fieldName: 'workstationId',
label: '工位',
component: markRaw(MdWorkstationSelect),
componentProps: {
placeholder: '请选择工位',
},
},
{
fieldName: 'userId',
label: '操作人',
component: markRaw(UserSelect),
componentProps: {
placeholder: '请选择操作人',
},
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
formItemClass: 'col-span-2',
componentProps: {
placeholder: '请输入备注',
rows: 3,
},
},
];
}
/** 流转卡选择弹窗的搜索表单 */
export function useCardSelectGridFormSchema(): VbenFormSchema[] {

View File

@ -1,2 +1,3 @@
export { default as GanttChart } from './gantt-chart.vue';
export { default as ProTaskSelectDialog } from './pro-task-select-dialog.vue';
export { default as ProTaskSelect } from './pro-task-select.vue';

View File

@ -1,15 +1,464 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProTaskApi } from '#/api/mes/pro/task';
import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder';
import { markRaw } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { getRangePickerDefaultProps } from '#/utils';
import MdClientSelect from '#/views/mes/md/client/components/md-client-select.vue';
import { MdItemSelect } from '#/views/mes/md/item/components';
import { MdWorkstationSelect } from '#/views/mes/md/workstation/components';
import { ProProcessSelect } from '#/views/mes/pro/process/components';
import { RouteColorPicker } from '#/views/mes/pro/route/components';
import { ProWorkOrderSelect } from '#/views/mes/pro/workorder/components';
/** 待排产工单列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'code',
label: '工单编码',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入工单编码',
},
},
{
fieldName: 'name',
label: '工单名称',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入工单名称',
},
},
{
fieldName: 'orderSourceCode',
label: '来源单据',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入来源单据编号',
},
},
{
fieldName: 'productId',
label: '产品',
component: markRaw(MdItemSelect),
componentProps: {
placeholder: '请选择产品',
},
},
{
fieldName: 'clientId',
label: '客户',
component: markRaw(MdClientSelect),
componentProps: {
placeholder: '请选择客户',
},
},
{
fieldName: 'requestDate',
label: '需求日期',
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
allowClear: true,
},
},
];
}
/** 待排产工单列表的字段 */
export function useGridColumns(): VxeTableGridOptions<MesProWorkOrderApi.WorkOrder>['columns'] {
return [
{
field: 'code',
title: '工单编码',
fixed: 'left',
width: 200,
treeNode: true,
slots: { default: 'code' },
},
{
field: 'name',
title: '工单名称',
minWidth: 150,
},
{
field: 'orderSourceType',
title: '工单来源',
width: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.MES_PRO_WORK_ORDER_SOURCE_TYPE },
},
},
{
field: 'orderSourceCode',
title: '来源单据编号',
width: 140,
},
{
field: 'productCode',
title: '产品编码',
width: 120,
},
{
field: 'productName',
title: '产品名称',
minWidth: 120,
},
{
field: 'productSpecification',
title: '规格型号',
width: 120,
},
{
field: 'unitMeasureName',
title: '单位',
width: 80,
},
{
field: 'quantity',
title: '工单数量',
width: 100,
},
{
field: 'quantityChanged',
title: '调整数量',
width: 100,
},
{
field: 'quantityProduced',
title: '已生产数量',
width: 100,
},
{
field: 'clientCode',
title: '客户编码',
width: 120,
},
{
field: 'clientName',
title: '客户名称',
width: 120,
},
{
field: 'requestDate',
title: '需求日期',
width: 120,
formatter: 'formatDate',
},
{
field: 'status',
title: '排产状态',
width: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.MES_PRO_WORK_ORDER_STATUS },
},
},
{
title: '操作',
width: 100,
fixed: 'right',
slots: { default: 'actions' },
},
];
}
/** 排产对话框只读工单信息的表单 */
export function useScheduleFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'code',
label: '工单编码',
component: 'Input',
componentProps: {
disabled: true,
},
},
{
fieldName: 'name',
label: '工单名称',
component: 'Input',
componentProps: {
disabled: true,
},
},
{
fieldName: 'orderSourceType',
label: '工单来源',
component: 'Select',
componentProps: {
disabled: true,
options: getDictOptions(DICT_TYPE.MES_PRO_WORK_ORDER_SOURCE_TYPE, 'number'),
},
},
{
fieldName: 'orderSourceCode',
label: '来源单据编号',
component: 'Input',
componentProps: {
disabled: true,
},
},
{
fieldName: 'type',
label: '工单类型',
component: 'Select',
componentProps: {
disabled: true,
options: getDictOptions(DICT_TYPE.MES_PRO_WORK_ORDER_TYPE, 'number'),
},
},
{
fieldName: 'productId',
label: '产品',
component: markRaw(MdItemSelect),
componentProps: {
disabled: true,
},
},
{
fieldName: 'productSpecification',
label: '规格型号',
component: 'Input',
componentProps: {
disabled: true,
},
},
{
fieldName: 'unitMeasureName',
label: '单位',
component: 'Input',
componentProps: {
disabled: true,
},
},
{
fieldName: 'quantity',
label: '工单数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
disabled: true,
precision: 2,
},
},
{
fieldName: 'clientId',
label: '客户',
component: markRaw(MdClientSelect),
componentProps: {
disabled: true,
},
},
{
fieldName: 'batchCode',
label: '批次号',
component: 'Input',
componentProps: {
disabled: true,
},
},
{
fieldName: 'requestDate',
label: '需求日期',
component: 'DatePicker',
componentProps: {
class: '!w-full',
disabled: true,
format: 'YYYY-MM-DD',
valueFormat: 'x',
},
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
formItemClass: 'col-span-3',
componentProps: {
disabled: true,
rows: 2,
},
},
];
}
/** 生产任务子表的字段 */
export function useTaskGridColumns(
editable: boolean,
): VxeTableGridOptions<MesProTaskApi.Task>['columns'] {
return [
{
field: 'code',
title: '任务编码',
width: 140,
},
{
field: 'name',
title: '任务名称',
minWidth: 150,
},
{
field: 'workstationCode',
title: '工作站编号',
width: 120,
},
{
field: 'workstationName',
title: '工作站名称',
width: 120,
},
{
field: 'quantity',
title: '排产数量',
width: 100,
},
{
field: 'producedQuantity',
title: '已生产数量',
width: 100,
},
{
field: 'startTime',
title: '开始生产时间',
width: 170,
formatter: 'formatDateTime',
},
{
field: 'duration',
title: '生产时长',
width: 80,
},
{
field: 'endTime',
title: '预计完成时间',
width: 170,
formatter: 'formatDateTime',
},
{
field: 'colorCode',
title: '显示颜色',
width: 100,
slots: { default: 'colorCode' },
},
...(editable
? [
{
title: '操作',
width: 160,
fixed: 'right',
slots: { default: 'actions' },
} as const,
]
: []),
];
}
/** 生产任务新增/修改的表单 */
export function useTaskFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
return [
{
fieldName: 'workstationId',
label: '工作站',
component: markRaw(MdWorkstationSelect),
componentProps: {
placeholder: '请选择工作站',
},
rules: 'selectRequired',
},
{
fieldName: 'quantity',
label: '排产数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 0.01,
placeholder: '请输入排产数量',
precision: 2,
},
rules: 'required',
},
{
fieldName: 'colorCode',
label: '甘特颜色',
component: markRaw(RouteColorPicker),
},
{
fieldName: 'startTime',
label: '开始时间',
component: 'DatePicker',
componentProps: {
class: '!w-full',
placeholder: '请选择开始时间',
showTime: true,
valueFormat: 'x',
// 开始时间变更:重新计算结束时间
onChange: () => recalcEndTime(formApi),
},
rules: 'required',
},
{
fieldName: 'duration',
label: '生产时长',
component: 'InputNumber',
componentProps: {
class: '!w-full',
min: 1,
placeholder: '请输入生产时长',
precision: 0,
// 生产时长变更:重新计算结束时间
onChange: () => recalcEndTime(formApi),
},
rules: 'required',
},
{
fieldName: 'endTime',
label: '结束时间',
component: 'DatePicker',
componentProps: {
class: '!w-full',
disabled: true,
showTime: true,
valueFormat: 'x',
},
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
formItemClass: 'col-span-3',
componentProps: {
placeholder: '请输入备注',
rows: 2,
},
},
];
}
/** 计算结束时间:开始时间 + 生产时长 × 8 小时 */
async function recalcEndTime(formApi?: VbenFormApi) {
if (!formApi) {
return;
}
const values = await formApi.getValues();
if (values.startTime && values.duration) {
const start = Number(values.startTime);
await formApi.setFieldValue(
'endTime',
start + values.duration * 8 * 3600 * 1000,
);
}
}
/** 任务选择弹窗的搜索表单 */
export function useTaskSelectGridFormSchema(): VbenFormSchema[] {
return [

View File

@ -1 +1,2 @@
export { default as ProWorkOrderSelectDialog } from './pro-work-order-select-dialog.vue';
export { default as ProWorkOrderSelect } from './pro-work-order-select.vue';

View File

@ -1,20 +1,16 @@
<script lang="ts" setup>
import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder';
import { computed, onMounted, ref, watch } from 'vue';
import { computed, ref, useAttrs, watch } from 'vue';
import { Select, Tag, Tooltip } from 'ant-design-vue';
import { IconifyIcon } from '@vben/icons';
import { getWorkOrder, getWorkOrderPage } from '#/api/mes/pro/workorder';
import { Input, Tooltip } from 'ant-design-vue';
import { getWorkOrder } from '#/api/mes/pro/workorder';
import ProWorkOrderSelectDialog from './pro-work-order-select-dialog.vue';
/**
* MES 生产工单选择器轻量版
*
* 当前用于安灯记录等只需要单选工单 ID 的业务页面
* - 默认按 `status` 过滤拉取首页 100 条工单作为下拉
* - 编辑回显走 `getWorkOrder(id)`
* - 后续 `mes/pro/workorder` 完整迁移后可替换为带弹窗的复杂选择器
*/
defineOptions({ name: 'ProWorkOrderSelect', inheritAttrs: false });
const props = withDefaults(
@ -22,7 +18,6 @@ const props = withDefaults(
allowClear?: boolean;
disabled?: boolean;
modelValue?: number;
pageSize?: number;
placeholder?: string;
status?: number;
type?: number;
@ -31,7 +26,6 @@ const props = withDefaults(
allowClear: true,
disabled: false,
modelValue: undefined,
pageSize: 100,
placeholder: '请选择工单',
status: undefined,
type: undefined,
@ -43,102 +37,104 @@ const emit = defineEmits<{
'update:modelValue': [value: number | undefined];
}>();
const allList = ref<MesProWorkOrderApi.WorkOrder[]>([]);
const selectedItem = ref<MesProWorkOrderApi.WorkOrder>();
const attrs = useAttrs(); //
const dialogRef = ref<InstanceType<typeof ProWorkOrderSelectDialog>>(); //
const hovering = ref(false); //
const selectedItem = ref<MesProWorkOrderApi.WorkOrder>(); //
const selectValue = computed({
get: () => props.modelValue,
set: (value: number | undefined) => {
emit('update:modelValue', value);
},
});
const displayLabel = computed(() => selectedItem.value?.code ?? ''); //
const showClear = computed(() => //
props.allowClear &&
!props.disabled &&
hovering.value &&
props.modelValue != null,
);
/** 前端过滤:按工单编码或名称模糊匹配 */
function handleFilter(input: string, option: any) {
const keyword = input.toLowerCase();
const item = option?.item as MesProWorkOrderApi.WorkOrder | undefined;
return Boolean(
item?.code?.toLowerCase().includes(keyword) ||
item?.name?.toLowerCase().includes(keyword),
);
}
/** 同步选中工单详情,未在列表内时单独拉取 */
async function syncSelectedItem(value: number | undefined) {
if (value === undefined) {
/** 根据编号单条查询工单信息(用于编辑回显) */
async function resolveItemById(id: number | undefined) {
if (id == null) {
selectedItem.value = undefined;
return;
}
const found = allList.value.find((item) => item.id === value);
if (found) {
selectedItem.value = found;
if (selectedItem.value?.id === id) {
return;
}
try {
selectedItem.value = await getWorkOrder(value);
} catch (error) {
console.error('[ProWorkOrderSelect] resolveItemById failed:', error);
selectedItem.value = await getWorkOrder(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 });
}
/** 除 v-model 外,额外抛出完整工单对象给业务表单使用 */
function handleChange(value: any) {
const nextValue = value === undefined ? undefined : Number(value);
syncSelectedItem(nextValue);
emit('change', selectedItem.value);
/** 弹窗选中回调 */
function handleSelected(rows: MesProWorkOrderApi.WorkOrder[]) {
const item = rows[0];
if (!item) {
return;
}
selectedItem.value = item;
emit('update:modelValue', item.id);
emit('change', item);
}
watch(
() => props.modelValue,
(value) => {
syncSelectedItem(value);
},
);
onMounted(async () => {
const data = await getWorkOrderPage({
pageNo: 1,
pageSize: props.pageSize,
status: props.status,
type: props.type,
});
allList.value = data.list ?? [];
syncSelectedItem(props.modelValue);
});
</script>
<template>
<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.productName || '-' }}</div>
<div>数量{{ selectedItem.quantity ?? '-' }}</div>
</div>
</template>
<Select
v-bind="$attrs"
v-model:value="selectValue"
:allow-clear="allowClear"
:disabled="disabled"
:filter-option="handleFilter"
:placeholder="placeholder"
class="w-full"
show-search
@change="handleChange"
>
<Select.Option
v-for="item in allList"
:key="item.id"
:item="item"
:value="item.id"
>
<div class="flex items-center gap-2">
<span>{{ item.code }}</span>
<Tag v-if="item.name" color="default">{{ item.name }}</Tag>
<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.productName || '-' }}</div>
<div>数量{{ selectedItem.quantity ?? '-' }}</div>
</div>
</Select.Option>
</Select>
</Tooltip>
</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>
<ProWorkOrderSelectDialog
ref="dialogRef"
:status="status"
:type="type"
@selected="handleSelected"
/>
</template>

View File

@ -1,2 +1,3 @@
export { default as WmBatchDetail } from './wm-batch-detail.vue';
export { default as WmBatchSelectDialog } from './wm-batch-select-dialog.vue';
export { default as WmBatchSelect } from './wm-batch-select.vue';

View File

@ -1,16 +1,21 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmBatchApi } from '#/api/mes/wm/batch';
import type { DescriptionItemSchema } from '#/components/description';
import { markRaw } from 'vue';
import { h, markRaw } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { formatDate } from '@vben/utils';
import { DictTag } from '#/components/dict-tag';
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 MdVendorSelect from '#/views/mes/md/vendor/components/md-vendor-select.vue';
import { MdWorkstationSelect } from '#/views/mes/md/workstation/components';
import { ProTaskSelect } from '#/views/mes/pro/task/components';
import { ProWorkOrderSelect } from '#/views/mes/pro/workorder/components';
import { TmToolSelect } from '#/views/mes/tm/tool/components';
@ -66,6 +71,14 @@ export function useBatchSelectGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择工作站',
},
},
{
fieldName: 'taskId',
label: '生产任务',
component: markRaw(ProTaskSelect),
componentProps: {
placeholder: '请选择生产任务',
},
},
{
fieldName: 'toolId',
label: '工具',
@ -74,6 +87,15 @@ export function useBatchSelectGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择工具',
},
},
{
fieldName: 'moldId',
label: '模具编号',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入模具编号',
},
},
{
fieldName: 'salesOrderCode',
label: '销售订单编号',
@ -111,6 +133,33 @@ export function useBatchSelectGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择质量状态',
},
},
{
fieldName: 'produceDate',
label: '生产日期',
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
allowClear: true,
},
},
{
fieldName: 'expireDate',
label: '有效期',
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
allowClear: true,
},
},
{
fieldName: 'receiptDate',
label: '入库日期',
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
allowClear: true,
},
},
];
}
@ -232,3 +281,92 @@ export function useBatchSelectGridColumns(
},
];
}
/** 批次详情的描述字段 */
export function useDetailSchema(): DescriptionItemSchema[] {
return [
{
field: 'code',
label: '批次编号',
},
{
field: 'itemCode',
label: '物料编码',
},
{
field: 'itemName',
label: '物料名称',
},
{
field: 'itemSpecification',
label: '规格型号',
},
{
field: 'unitName',
label: '单位',
},
{
field: 'lotNumber',
label: '生产批号',
},
{
field: 'produceDate',
label: '生产日期',
render: (value) => (value ? formatDate(value, 'YYYY-MM-DD') : '-'),
},
{
field: 'expireDate',
label: '有效期',
render: (value) => (value ? formatDate(value, 'YYYY-MM-DD') : '-'),
},
{
field: 'receiptDate',
label: '入库日期',
render: (value) => (value ? formatDate(value, 'YYYY-MM-DD') : '-'),
},
{
field: 'vendorName',
label: '供应商',
render: (value) => value || '-',
},
{
field: 'clientName',
label: '客户',
render: (value) => value || '-',
},
{
field: 'workstationCode',
label: '工作站',
render: (value) => value || '-',
},
{
field: 'purchaseOrderCode',
label: '采购订单编号',
render: (value) => value || '-',
},
{
field: 'salesOrderCode',
label: '销售订单编号',
render: (value) => value || '-',
},
{
field: 'workOrderCode',
label: '生产工单',
render: (value) => value || '-',
},
{
field: 'qualityStatus',
label: '质量状态',
render: (value) =>
value == null
? '-'
: h(DictTag, { type: DICT_TYPE.MES_WM_QUALITY_STATUS, value }),
},
{
field: 'remark',
label: '备注',
span: 3,
render: (value) => value || '-',
},
];
}

View File

@ -35,8 +35,7 @@ const emit = defineEmits<{
}>();
const open = ref(false);
const multiple = ref(true);
const syncingSingleSelection = ref(false);
const multiple = ref(false); // 使
const selectedRows = ref<MesWmMaterialStockApi.MaterialStock[]>([]);
const preSelectedIds = ref<number[]>([]);
const searchItemTypeId = ref<number>();
@ -64,50 +63,37 @@ const alertTitle = computed(() => {
return `已按${parts.join('/')}预过滤`;
});
/** 单选模式同步 VXE 勾选状态 */
async function syncSingleSelection(row?: MesWmMaterialStockApi.MaterialStock) {
syncingSingleSelection.value = true;
await nextTick();
await gridApi.grid.clearCheckboxRow();
if (row) {
await gridApi.grid.setCheckboxRow(row, true);
}
await nextTick();
syncingSingleSelection.value = false;
/** 获取多选记录,包含 VXE reserve 跨页记录 */
function getMultipleSelectedRows() {
const selectedMap = new Map<number, MesWmMaterialStockApi.MaterialStock>();
const records = [
...(gridApi.grid.getCheckboxReserveRecords?.() ?? []),
...(gridApi.grid.getCheckboxRecords?.() ?? []),
] as MesWmMaterialStockApi.MaterialStock[];
records.forEach((row) => {
const rowId = row.id;
if (rowId !== undefined) {
selectedMap.set(rowId, row);
}
});
return [...selectedMap.values()];
}
/** 处理勾选变化 */
async function handleCheckboxChange({
checked,
records,
row,
}: {
checked: boolean;
records: MesWmMaterialStockApi.MaterialStock[];
row?: MesWmMaterialStockApi.MaterialStock;
}) {
if (syncingSingleSelection.value) {
return;
}
if (!multiple.value) {
const selected = checked && row ? [row] : [];
selectedRows.value = selected;
await syncSingleSelection(selected[0]);
return;
}
selectedRows.value = records;
/** 处理多选勾选变化 */
function handleCheckboxSelectChange() {
selectedRows.value = getMultipleSelectedRows();
}
/** 处理全选变化 */
function handleCheckboxAll({
records,
}: {
records: MesWmMaterialStockApi.MaterialStock[];
}) {
if (syncingSingleSelection.value) {
return;
}
selectedRows.value = records;
/** 处理单选切换 */
function handleRadioChange(row: MesWmMaterialStockApi.MaterialStock) {
selectedRows.value = [row];
}
/** 多选模式下切换行勾选 */
async function toggleMultipleRow(row: MesWmMaterialStockApi.MaterialStock) {
const selected = gridApi.grid.isCheckedByCheckboxRow(row);
await gridApi.grid.setCheckboxRow(row, !selected);
selectedRows.value = getMultipleSelectedRows();
}
/** 双击行:单选直接确认;多选切换勾选 */
@ -117,33 +103,34 @@ async function handleRowDblclick({
row: MesWmMaterialStockApi.MaterialStock;
}) {
if (multiple.value) {
const checked = !gridApi.grid.isCheckedByCheckboxRow(row);
await gridApi.grid.setCheckboxRow(row, checked);
handleCheckboxChange({
checked,
records: gridApi.grid.getCheckboxRecords() as MesWmMaterialStockApi.MaterialStock[],
row,
});
await toggleMultipleRow(row);
return;
}
selectedRows.value = [row];
await syncSingleSelection(row);
await gridApi.grid.setRadioRow(row);
handleConfirm();
}
/** 回显预选 */
function applyPreSelection() {
async function applyPreSelection() {
if (preSelectedIds.value.length === 0) {
return;
}
const rows = gridApi.grid.getData() as MesWmMaterialStockApi.MaterialStock[];
for (const row of rows) {
if (row.id && preSelectedIds.value.includes(row.id)) {
gridApi.grid.setCheckboxRow(row, true);
if (!multiple.value) {
selectedRows.value = [row];
}
if (row.id === undefined || !preSelectedIds.value.includes(row.id)) {
continue;
}
if (multiple.value) {
await gridApi.grid.setCheckboxRow(row, true);
} else {
await gridApi.grid.setRadioRow(row);
selectedRows.value = [row];
return;
}
}
if (multiple.value) {
selectedRows.value = getMultipleSelectedRows();
}
}
@ -152,10 +139,11 @@ const [Grid, gridApi] = useVbenVxeGrid({
schema: useSelectGridFormSchema(),
},
gridOptions: {
columns: useSelectGridColumns(),
columns: useSelectGridColumns(false),
height: 480,
keepSource: true,
checkboxConfig: { highlight: true, range: true, reserve: true },
radioConfig: { highlight: true, trigger: 'row' },
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
@ -186,8 +174,11 @@ const [Grid, gridApi] = useVbenVxeGrid({
} as VxeTableGridOptions<MesWmMaterialStockApi.MaterialStock>,
gridEvents: {
cellDblclick: handleRowDblclick,
checkboxAll: handleCheckboxAll,
checkboxChange: handleCheckboxChange,
checkboxAll: handleCheckboxSelectChange,
checkboxChange: handleCheckboxSelectChange,
radioChange: ({ row }: { row: MesWmMaterialStockApi.MaterialStock }) => {
handleRadioChange(row);
},
},
});
@ -202,6 +193,8 @@ async function resetQueryState() {
selectedRows.value = [];
searchItemTypeId.value = undefined;
await gridApi.grid.clearCheckboxRow();
await gridApi.grid.clearCheckboxReserve();
await gridApi.grid.clearRadioRow();
await gridApi.formApi.resetForm();
}
@ -211,13 +204,16 @@ async function openModal(
options?: { multiple?: boolean },
) {
open.value = true;
multiple.value = options?.multiple ?? true;
multiple.value = options?.multiple ?? false;
preSelectedIds.value = selectedIds || [];
await nextTick();
gridApi.setGridOptions({
columns: useSelectGridColumns(multiple.value),
});
await resetQueryState();
await gridApi.query();
await nextTick();
applyPreSelection();
await applyPreSelection();
}
/** 关闭弹窗 */
@ -228,14 +224,12 @@ async function closeModal() {
/** 确认选择 */
function handleConfirm() {
if (selectedRows.value.length === 0) {
const rows = multiple.value ? getMultipleSelectedRows() : selectedRows.value;
if (rows.length === 0) {
message.warning(multiple.value ? '请至少选择一条数据' : '请选择一条数据');
return;
}
emit(
'selected',
multiple.value ? selectedRows.value : [selectedRows.value[0]!],
);
emit('selected', multiple.value ? rows : [rows[0]!]);
open.value = false;
}

View File

@ -71,11 +71,7 @@ async function resolveItemById(id: number | undefined) {
if (selectedItem.value?.id === id) {
return;
}
try {
selectedItem.value = await getMaterialStock(id);
} catch (error) {
console.error('[WmMaterialStockSelect] resolveItemById failed:', error);
}
selectedItem.value = await getMaterialStock(id);
}
watch(() => props.modelValue, resolveItemById, { immediate: true });

View File

@ -72,8 +72,12 @@ export function useGridFormSchema(): VbenFormSchema[] {
}
/** 列表的字段 */
// TODO @AI看看别的模块是不是会叫 onFrozenChange还是一般叫什么梗合适
export function useGridColumns(
onFrozenChange: (row: MesWmMaterialStockApi.MaterialStock) => void,
onFrozenChange: (
newFrozen: boolean,
row: MesWmMaterialStockApi.MaterialStock,
) => Promise<boolean | undefined>,
): VxeTableGridOptions<MesWmMaterialStockApi.MaterialStock>['columns'] {
return [
{
@ -206,10 +210,12 @@ export function useSelectGridFormSchema(): VbenFormSchema[] {
}
/** 选择弹窗的字段 */
export function useSelectGridColumns(): VxeTableGridOptions<MesWmMaterialStockApi.MaterialStock>['columns'] {
export function useSelectGridColumns(
multiple = false,
): VxeTableGridOptions<MesWmMaterialStockApi.MaterialStock>['columns'] {
return [
{
type: 'checkbox',
type: multiple ? 'checkbox' : 'radio',
width: 50,
},
{

View File

@ -17,6 +17,7 @@ import {
} from '#/api/mes/wm/materialstock';
import { $t } from '#/locales';
import MdItemTypeTree from '#/views/mes/md/item/type/components/md-item-type-tree.vue';
import { WmBatchDetail } from '#/views/mes/wm/batch/components';
import AreaForm from '#/views/mes/wm/warehouse/area/modules/form.vue';
import { useGridColumns, useGridFormSchema } from './data';
@ -26,6 +27,8 @@ const [AreaModal, areaModalApi] = useVbenModal({
destroyOnClose: true,
});
const batchDetailRef = ref<InstanceType<typeof WmBatchDetail>>();
/** 刷新表格 */
function handleRefresh() {
gridApi.query();
@ -55,11 +58,20 @@ function handleOpenAreaDetail(row: MesWmMaterialStockApi.MaterialStock) {
areaModalApi.setData({ formType: 'detail', id: row.areaId }).open();
}
/** 打开批次详情弹窗 */
function handleOpenBatchDetail(row: MesWmMaterialStockApi.MaterialStock) {
if (!row.batchId) {
return;
}
batchDetailRef.value?.open(row.batchId);
}
/** 处理冻结状态切换 */
async function handleFrozenChange(
newFrozen: boolean,
row: MesWmMaterialStockApi.MaterialStock,
): Promise<boolean | undefined> {
const text = row.frozen ? '冻结' : '解冻';
const text = newFrozen ? '冻结' : '解冻';
try {
await confirm(`确认要"${text}"该库存记录吗?`);
} catch {
@ -68,7 +80,7 @@ async function handleFrozenChange(
//
await updateMaterialStockFrozen({
id: row.id!,
frozen: row.frozen!,
frozen: newFrozen,
});
//
message.success(`${text}成功`);
@ -117,6 +129,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
</template>
<AreaModal />
<WmBatchDetail ref="batchDetailRef" />
<div class="flex h-full w-full">
<!-- 左侧物料分类树 -->
@ -140,9 +153,15 @@ const [Grid, gridApi] = useVbenVxeGrid({
/>
</template>
<template #batchCode="{ row }">
<span v-if="row.batchId" :title="row.batchCode">
<Button
v-if="row.batchId"
:title="row.batchCode"
size="small"
type="link"
@click="handleOpenBatchDetail(row)"
>
{{ row.batchCode }}
</span>
</Button>
<span v-else>-</span>
</template>
<template #areaName="{ row }">

View File

@ -39,6 +39,15 @@ const routes: RouteRecordRaw[] = [
},
component: () => import('#/views/mes/wm/barcode/config/index.vue'),
},
{
path: 'pro/task/edit',
name: 'MesProTaskGanttEdit',
meta: {
title: '甘特图编辑',
activePath: '/mes/pro/task',
},
component: () => import('#/views/mes/pro/task/edit/index.vue'),
},
],
},
];

View File

@ -1,2 +1,3 @@
export { default as GanttChart } from './gantt-chart.vue';
export { default as ProTaskSelectDialog } from './pro-task-select-dialog.vue';
export { default as ProTaskSelect } from './pro-task-select.vue';

View File

@ -1,15 +1,467 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProTaskApi } from '#/api/mes/pro/task';
import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder';
import { markRaw } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { getRangePickerDefaultProps } from '#/utils';
import MdClientSelect from '#/views/mes/md/client/components/md-client-select.vue';
import { MdItemSelect } from '#/views/mes/md/item/components';
import { MdWorkstationSelect } from '#/views/mes/md/workstation/components';
import { ProProcessSelect } from '#/views/mes/pro/process/components';
import { RouteColorPicker } from '#/views/mes/pro/route/components';
import { ProWorkOrderSelect } from '#/views/mes/pro/workorder/components';
/** 待排产工单列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'code',
label: '工单编码',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入工单编码',
},
},
{
fieldName: 'name',
label: '工单名称',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入工单名称',
},
},
{
fieldName: 'orderSourceCode',
label: '来源单据',
component: 'Input',
componentProps: {
clearable: true,
placeholder: '请输入来源单据编号',
},
},
{
fieldName: 'productId',
label: '产品',
component: markRaw(MdItemSelect),
componentProps: {
placeholder: '请选择产品',
},
},
{
fieldName: 'clientId',
label: '客户',
component: markRaw(MdClientSelect),
componentProps: {
placeholder: '请选择客户',
},
},
{
fieldName: 'requestDate',
label: '需求日期',
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
clearable: true,
},
},
];
}
/** 待排产工单列表的字段 */
export function useGridColumns(): VxeTableGridOptions<MesProWorkOrderApi.WorkOrder>['columns'] {
return [
{
field: 'code',
title: '工单编码',
fixed: 'left',
width: 200,
treeNode: true,
slots: { default: 'code' },
},
{
field: 'name',
title: '工单名称',
minWidth: 150,
},
{
field: 'orderSourceType',
title: '工单来源',
width: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.MES_PRO_WORK_ORDER_SOURCE_TYPE },
},
},
{
field: 'orderSourceCode',
title: '来源单据编号',
width: 140,
},
{
field: 'productCode',
title: '产品编码',
width: 120,
},
{
field: 'productName',
title: '产品名称',
minWidth: 120,
},
{
field: 'productSpecification',
title: '规格型号',
width: 120,
},
{
field: 'unitMeasureName',
title: '单位',
width: 80,
},
{
field: 'quantity',
title: '工单数量',
width: 100,
},
{
field: 'quantityChanged',
title: '调整数量',
width: 100,
},
{
field: 'quantityProduced',
title: '已生产数量',
width: 100,
},
{
field: 'clientCode',
title: '客户编码',
width: 120,
},
{
field: 'clientName',
title: '客户名称',
width: 120,
},
{
field: 'requestDate',
title: '需求日期',
width: 120,
formatter: 'formatDate',
},
{
field: 'status',
title: '排产状态',
width: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.MES_PRO_WORK_ORDER_STATUS },
},
},
{
title: '操作',
width: 100,
fixed: 'right',
slots: { default: 'actions' },
},
];
}
/** 排产对话框只读工单信息的表单 */
export function useScheduleFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'code',
label: '工单编码',
component: 'Input',
componentProps: {
disabled: true,
},
},
{
fieldName: 'name',
label: '工单名称',
component: 'Input',
componentProps: {
disabled: true,
},
},
{
fieldName: 'orderSourceType',
label: '工单来源',
component: 'Select',
componentProps: {
disabled: true,
options: getDictOptions(DICT_TYPE.MES_PRO_WORK_ORDER_SOURCE_TYPE, 'number'),
},
},
{
fieldName: 'orderSourceCode',
label: '来源单据编号',
component: 'Input',
componentProps: {
disabled: true,
},
},
{
fieldName: 'type',
label: '工单类型',
component: 'Select',
componentProps: {
disabled: true,
options: getDictOptions(DICT_TYPE.MES_PRO_WORK_ORDER_TYPE, 'number'),
},
},
{
fieldName: 'productId',
label: '产品',
component: markRaw(MdItemSelect),
componentProps: {
disabled: true,
},
},
{
fieldName: 'productSpecification',
label: '规格型号',
component: 'Input',
componentProps: {
disabled: true,
},
},
{
fieldName: 'unitMeasureName',
label: '单位',
component: 'Input',
componentProps: {
disabled: true,
},
},
{
fieldName: 'quantity',
label: '工单数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
controlsPosition: 'right',
disabled: true,
precision: 2,
},
},
{
fieldName: 'clientId',
label: '客户',
component: markRaw(MdClientSelect),
componentProps: {
disabled: true,
},
},
{
fieldName: 'batchCode',
label: '批次号',
component: 'Input',
componentProps: {
disabled: true,
},
},
{
fieldName: 'requestDate',
label: '需求日期',
component: 'DatePicker',
componentProps: {
class: '!w-full',
disabled: true,
type: 'date',
valueFormat: 'x',
},
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
formItemClass: 'col-span-3',
componentProps: {
disabled: true,
rows: 2,
},
},
];
}
/** 生产任务子表的字段 */
export function useTaskGridColumns(
editable: boolean,
): VxeTableGridOptions<MesProTaskApi.Task>['columns'] {
return [
{
field: 'code',
title: '任务编码',
width: 140,
},
{
field: 'name',
title: '任务名称',
minWidth: 150,
},
{
field: 'workstationCode',
title: '工作站编号',
width: 120,
},
{
field: 'workstationName',
title: '工作站名称',
width: 120,
},
{
field: 'quantity',
title: '排产数量',
width: 100,
},
{
field: 'producedQuantity',
title: '已生产数量',
width: 100,
},
{
field: 'startTime',
title: '开始生产时间',
width: 170,
formatter: 'formatDateTime',
},
{
field: 'duration',
title: '生产时长',
width: 80,
},
{
field: 'endTime',
title: '预计完成时间',
width: 170,
formatter: 'formatDateTime',
},
{
field: 'colorCode',
title: '显示颜色',
width: 100,
slots: { default: 'colorCode' },
},
...(editable
? [
{
title: '操作',
width: 160,
fixed: 'right',
slots: { default: 'actions' },
} as const,
]
: []),
];
}
/** 生产任务新增/修改的表单 */
export function useTaskFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
return [
{
fieldName: 'workstationId',
label: '工作站',
component: markRaw(MdWorkstationSelect),
componentProps: {
placeholder: '请选择工作站',
},
rules: 'selectRequired',
},
{
fieldName: 'quantity',
label: '排产数量',
component: 'InputNumber',
componentProps: {
class: '!w-full',
controlsPosition: 'right',
min: 0.01,
placeholder: '请输入排产数量',
precision: 2,
},
rules: 'required',
},
{
fieldName: 'colorCode',
label: '甘特颜色',
component: markRaw(RouteColorPicker),
},
{
fieldName: 'startTime',
label: '开始时间',
component: 'DatePicker',
componentProps: {
class: '!w-full',
placeholder: '请选择开始时间',
type: 'datetime',
valueFormat: 'x',
// 开始时间变更:重新计算结束时间
onChange: () => recalcEndTime(formApi),
},
rules: 'required',
},
{
fieldName: 'duration',
label: '生产时长',
component: 'InputNumber',
componentProps: {
class: '!w-full',
controlsPosition: 'right',
min: 1,
placeholder: '请输入生产时长',
precision: 0,
// 生产时长变更:重新计算结束时间
onChange: () => recalcEndTime(formApi),
},
rules: 'required',
},
{
fieldName: 'endTime',
label: '结束时间',
component: 'DatePicker',
componentProps: {
class: '!w-full',
disabled: true,
type: 'datetime',
valueFormat: 'x',
},
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
formItemClass: 'col-span-3',
componentProps: {
placeholder: '请输入备注',
rows: 2,
},
},
];
}
/** 计算结束时间:开始时间 + 生产时长 × 8 小时 */
async function recalcEndTime(formApi?: VbenFormApi) {
if (!formApi) {
return;
}
const values = await formApi.getValues();
if (values.startTime && values.duration) {
const start = Number(values.startTime);
await formApi.setFieldValue(
'endTime',
start + values.duration * 8 * 3600 * 1000,
);
}
}
/** 任务选择弹窗的搜索表单 */
export function useTaskSelectGridFormSchema(): VbenFormSchema[] {
return [

View File

@ -1,2 +1,3 @@
export { default as WmBatchDetail } from './wm-batch-detail.vue';
export { default as WmBatchSelectDialog } from './wm-batch-select-dialog.vue';
export { default as WmBatchSelect } from './wm-batch-select.vue';

View File

@ -35,8 +35,7 @@ const emit = defineEmits<{
}>();
const open = ref(false);
const multiple = ref(true);
const syncingSingleSelection = ref(false);
const multiple = ref(false); // 使
const selectedRows = ref<MesWmMaterialStockApi.MaterialStock[]>([]);
const preSelectedIds = ref<number[]>([]);
const searchItemTypeId = ref<number>();
@ -64,50 +63,37 @@ const alertTitle = computed(() => {
return `已按${parts.join('/')}预过滤`;
});
/** 单选模式同步 VXE 勾选状态 */
async function syncSingleSelection(row?: MesWmMaterialStockApi.MaterialStock) {
syncingSingleSelection.value = true;
await nextTick();
await gridApi.grid.clearCheckboxRow();
if (row) {
await gridApi.grid.setCheckboxRow(row, true);
}
await nextTick();
syncingSingleSelection.value = false;
/** 获取多选记录,包含 VXE reserve 跨页记录 */
function getMultipleSelectedRows() {
const selectedMap = new Map<number, MesWmMaterialStockApi.MaterialStock>();
const records = [
...(gridApi.grid.getCheckboxReserveRecords?.() ?? []),
...(gridApi.grid.getCheckboxRecords?.() ?? []),
] as MesWmMaterialStockApi.MaterialStock[];
records.forEach((row) => {
const rowId = row.id;
if (rowId !== undefined) {
selectedMap.set(rowId, row);
}
});
return [...selectedMap.values()];
}
/** 处理勾选变化 */
async function handleCheckboxChange({
checked,
records,
row,
}: {
checked: boolean;
records: MesWmMaterialStockApi.MaterialStock[];
row?: MesWmMaterialStockApi.MaterialStock;
}) {
if (syncingSingleSelection.value) {
return;
}
if (!multiple.value) {
const selected = checked && row ? [row] : [];
selectedRows.value = selected;
await syncSingleSelection(selected[0]);
return;
}
selectedRows.value = records;
/** 处理多选勾选变化 */
function handleCheckboxSelectChange() {
selectedRows.value = getMultipleSelectedRows();
}
/** 处理全选变化 */
function handleCheckboxAll({
records,
}: {
records: MesWmMaterialStockApi.MaterialStock[];
}) {
if (syncingSingleSelection.value) {
return;
}
selectedRows.value = records;
/** 处理单选切换 */
function handleRadioChange(row: MesWmMaterialStockApi.MaterialStock) {
selectedRows.value = [row];
}
/** 多选模式下切换行勾选 */
async function toggleMultipleRow(row: MesWmMaterialStockApi.MaterialStock) {
const selected = gridApi.grid.isCheckedByCheckboxRow(row);
await gridApi.grid.setCheckboxRow(row, !selected);
selectedRows.value = getMultipleSelectedRows();
}
/** 双击行:单选直接确认;多选切换勾选 */
@ -117,34 +103,34 @@ async function handleRowDblclick({
row: MesWmMaterialStockApi.MaterialStock;
}) {
if (multiple.value) {
const checked = !gridApi.grid.isCheckedByCheckboxRow(row);
await gridApi.grid.setCheckboxRow(row, checked);
handleCheckboxChange({
checked,
records:
gridApi.grid.getCheckboxRecords() as MesWmMaterialStockApi.MaterialStock[],
row,
});
await toggleMultipleRow(row);
return;
}
selectedRows.value = [row];
await syncSingleSelection(row);
await gridApi.grid.setRadioRow(row);
handleConfirm();
}
/** 回显预选 */
function applyPreSelection() {
async function applyPreSelection() {
if (preSelectedIds.value.length === 0) {
return;
}
const rows = gridApi.grid.getData() as MesWmMaterialStockApi.MaterialStock[];
for (const row of rows) {
if (row.id && preSelectedIds.value.includes(row.id)) {
gridApi.grid.setCheckboxRow(row, true);
if (!multiple.value) {
selectedRows.value = [row];
}
if (row.id === undefined || !preSelectedIds.value.includes(row.id)) {
continue;
}
if (multiple.value) {
await gridApi.grid.setCheckboxRow(row, true);
} else {
await gridApi.grid.setRadioRow(row);
selectedRows.value = [row];
return;
}
}
if (multiple.value) {
selectedRows.value = getMultipleSelectedRows();
}
}
@ -153,7 +139,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
schema: useSelectGridFormSchema(),
},
gridOptions: {
columns: useSelectGridColumns(),
columns: useSelectGridColumns(false),
height: 480,
keepSource: true,
checkboxConfig: {
@ -161,6 +147,10 @@ const [Grid, gridApi] = useVbenVxeGrid({
range: true,
reserve: true,
},
radioConfig: {
highlight: true,
trigger: 'row',
},
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
@ -191,8 +181,11 @@ const [Grid, gridApi] = useVbenVxeGrid({
} as VxeTableGridOptions<MesWmMaterialStockApi.MaterialStock>,
gridEvents: {
cellDblclick: handleRowDblclick,
checkboxAll: handleCheckboxAll,
checkboxChange: handleCheckboxChange,
checkboxAll: handleCheckboxSelectChange,
checkboxChange: handleCheckboxSelectChange,
radioChange: ({ row }: { row: MesWmMaterialStockApi.MaterialStock }) => {
handleRadioChange(row);
},
},
});
@ -207,6 +200,8 @@ async function resetQueryState() {
selectedRows.value = [];
searchItemTypeId.value = undefined;
await gridApi.grid.clearCheckboxRow();
await gridApi.grid.clearCheckboxReserve();
await gridApi.grid.clearRadioRow();
await gridApi.formApi.resetForm();
}
@ -216,13 +211,16 @@ async function openModal(
options?: { multiple?: boolean },
) {
open.value = true;
multiple.value = options?.multiple ?? true;
multiple.value = options?.multiple ?? false;
preSelectedIds.value = selectedIds || [];
await nextTick();
gridApi.setGridOptions({
columns: useSelectGridColumns(multiple.value),
});
await resetQueryState();
await gridApi.query();
await nextTick();
applyPreSelection();
await applyPreSelection();
}
/** 关闭弹窗 */
@ -233,14 +231,12 @@ async function closeModal() {
/** 确认选择 */
function handleConfirm() {
if (selectedRows.value.length === 0) {
const rows = multiple.value ? getMultipleSelectedRows() : selectedRows.value;
if (rows.length === 0) {
ElMessage.warning(multiple.value ? '请至少选择一条数据' : '请选择一条数据');
return;
}
emit(
'selected',
multiple.value ? selectedRows.value : [selectedRows.value[0]!],
);
emit('selected', multiple.value ? rows : [rows[0]!]);
open.value = false;
}

View File

@ -71,11 +71,7 @@ async function resolveItemById(id: number | undefined) {
if (selectedItem.value?.id === id) {
return;
}
try {
selectedItem.value = await getMaterialStock(id);
} catch (error) {
console.error('[WmMaterialStockSelect] resolveItemById failed:', error);
}
selectedItem.value = await getMaterialStock(id);
}
watch(() => props.modelValue, resolveItemById, { immediate: true });

View File

@ -73,7 +73,10 @@ export function useGridFormSchema(): VbenFormSchema[] {
/** 列表的字段 */
export function useGridColumns(
onFrozenChange: (row: MesWmMaterialStockApi.MaterialStock) => void,
onFrozenChange: (
newFrozen: boolean,
row: MesWmMaterialStockApi.MaterialStock,
) => Promise<boolean | undefined>,
): VxeTableGridOptions<MesWmMaterialStockApi.MaterialStock>['columns'] {
return [
{
@ -206,10 +209,12 @@ export function useSelectGridFormSchema(): VbenFormSchema[] {
}
/** 选择弹窗的字段 */
export function useSelectGridColumns(): VxeTableGridOptions<MesWmMaterialStockApi.MaterialStock>['columns'] {
export function useSelectGridColumns(
multiple = false,
): VxeTableGridOptions<MesWmMaterialStockApi.MaterialStock>['columns'] {
return [
{
type: 'checkbox',
type: multiple ? 'checkbox' : 'radio',
width: 50,
},
{

View File

@ -17,6 +17,7 @@ import {
} from '#/api/mes/wm/materialstock';
import { $t } from '#/locales';
import MdItemTypeTree from '#/views/mes/md/item/type/components/md-item-type-tree.vue';
import { WmBatchDetail } from '#/views/mes/wm/batch/components';
import AreaForm from '#/views/mes/wm/warehouse/area/modules/form.vue';
import { useGridColumns, useGridFormSchema } from './data';
@ -26,9 +27,14 @@ const [AreaModal, areaModalApi] = useVbenModal({
destroyOnClose: true,
});
const batchDetailRef = ref<InstanceType<typeof WmBatchDetail>>();
/** 处理冻结状态切换 */
async function handleFrozenChange(row: MesWmMaterialStockApi.MaterialStock) {
const text = row.frozen ? '冻结' : '解冻';
async function handleFrozenChange(
newFrozen: boolean,
row: MesWmMaterialStockApi.MaterialStock,
): Promise<boolean | undefined> {
const text = newFrozen ? '冻结' : '解冻';
try {
await confirm(`确认要"${text}"该库存记录吗?`);
} catch {
@ -36,7 +42,7 @@ async function handleFrozenChange(row: MesWmMaterialStockApi.MaterialStock) {
}
await updateMaterialStockFrozen({
id: row.id!,
frozen: row.frozen!,
frozen: newFrozen,
});
ElMessage.success(`${text}成功`);
return true;
@ -90,6 +96,14 @@ function handleOpenAreaDetail(row: MesWmMaterialStockApi.MaterialStock) {
areaModalApi.setData({ formType: 'detail', id: row.areaId }).open();
}
/** 打开批次详情弹窗 */
function handleOpenBatchDetail(row: MesWmMaterialStockApi.MaterialStock) {
if (!row.batchId) {
return;
}
batchDetailRef.value?.open(row.batchId);
}
/** 导出表格 */
async function handleExport() {
const data = await exportMaterialStock({
@ -110,6 +124,7 @@ async function handleExport() {
</template>
<AreaModal />
<WmBatchDetail ref="batchDetailRef" />
<div class="flex h-full gap-3">
<div class="bg-card w-1/6 rounded p-3">
@ -131,9 +146,16 @@ async function handleExport() {
/>
</template>
<template #batchCode="{ row }">
<span v-if="row.batchId" :title="row.batchCode">
<ElButton
v-if="row.batchId"
link
size="small"
:title="row.batchCode"
type="primary"
@click="handleOpenBatchDetail(row)"
>
{{ row.batchCode }}
</span>
</ElButton>
<span v-else>-</span>
</template>
<template #areaName="{ row }">

View File

@ -285,6 +285,9 @@ catalogs:
defu:
specifier: ^6.1.7
version: 6.1.7
dhtmlx-gantt:
specifier: ^9.1.1
version: 9.1.4
diagram-js:
specifier: ^12.8.1
version: 12.8.1
@ -525,6 +528,9 @@ catalogs:
tw-animate-css:
specifier: ^1.4.0
version: 1.4.0
tyme4ts:
specifier: ^1.5.0
version: 1.5.0
typescript:
specifier: ^6.0.3
version: 6.0.3
@ -819,6 +825,9 @@ importers:
dayjs:
specifier: 'catalog:'
version: 1.11.20
dhtmlx-gantt:
specifier: 'catalog:'
version: 9.1.4
diagram-js:
specifier: 'catalog:'
version: 12.8.1
@ -1071,6 +1080,9 @@ importers:
dayjs:
specifier: 'catalog:'
version: 1.11.20
dhtmlx-gantt:
specifier: 'catalog:'
version: 9.1.4
diagram-js:
specifier: 'catalog:'
version: 12.8.1
@ -1293,14 +1305,14 @@ importers:
version: 2.9.8(vue@3.5.34(typescript@6.0.3))
vitepress-plugin-group-icons:
specifier: 'catalog:'
version: 1.7.5(vite@8.0.10(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0))
version: 1.7.5(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0))
devDependencies:
'@nolebase/vitepress-plugin-git-changelog':
specifier: 'catalog:'
version: 2.18.2(vitepress@2.0.0-alpha.17(@types/node@25.9.1)(async-validator@4.2.5)(axios@1.16.1)(change-case@5.4.4)(jiti@2.7.0)(less@4.6.4)(lightningcss@1.32.0)(nprogress@0.2.0)(postcss@8.5.15)(qrcode@1.5.4)(sass-embedded@1.100.0)(sass@1.100.0)(sortablejs@1.15.7)(terser@5.48.0)(typescript@6.0.3)(yaml@2.9.0))(vue@3.5.34(typescript@6.0.3))
'@tailwindcss/vite':
specifier: 'catalog:'
version: 4.3.0(vite@8.0.10(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0))
version: 4.3.0(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0))
'@vben/tailwind-config':
specifier: workspace:*
version: link:../internal/tailwind-config
@ -1309,7 +1321,7 @@ importers:
version: link:../internal/vite-config
'@vite-pwa/vitepress':
specifier: 'catalog:'
version: 1.1.0(vite-plugin-pwa@1.3.0(vite@8.0.10(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1)(workbox-window@7.4.1))
version: 1.1.0(vite-plugin-pwa@1.3.0(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1)(workbox-window@7.4.1))
vitepress:
specifier: 'catalog:'
version: 2.0.0-alpha.17(@types/node@25.9.1)(async-validator@4.2.5)(axios@1.16.1)(change-case@5.4.4)(jiti@2.7.0)(less@4.6.4)(lightningcss@1.32.0)(nprogress@0.2.0)(postcss@8.5.15)(qrcode@1.5.4)(sass-embedded@1.100.0)(sass@1.100.0)(sortablejs@1.15.7)(terser@5.48.0)(typescript@6.0.3)(yaml@2.9.0)
@ -7944,6 +7956,9 @@ packages:
devlop@1.1.0:
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
dhtmlx-gantt@9.1.4:
resolution: {integrity: sha512-XCNA5QUiuV79Xq1ykNpH9LFNR2IVpDZMqnmBV6dsBeOkHyPMOpkyQ/gqAPCcK2GAvYHoN2nGAMYb2LldCWhMuQ==}
diagram-js-direct-editing@3.3.0:
resolution: {integrity: sha512-EjXYb35J3qBU8lLz5U81hn7wNykVmF7U5DXZ7BvPok2IX7rmPz+ZyaI5AEMiqaC6lpSnHqPxFcPgKEiJcAiv5w==}
peerDependencies:
@ -15694,6 +15709,13 @@ snapshots:
tailwindcss: 4.3.0
vite: 8.0.10(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0)
'@tailwindcss/vite@4.3.0(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0))':
dependencies:
'@tailwindcss/node': 4.3.0
'@tailwindcss/oxide': 4.3.0
tailwindcss: 4.3.0
vite: 8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0)
'@tanstack/store@0.11.0': {}
'@tanstack/virtual-core@3.15.0': {}
@ -16590,9 +16612,9 @@ snapshots:
global: 4.4.0
is-function: 1.0.2
'@vite-pwa/vitepress@1.1.0(vite-plugin-pwa@1.3.0(vite@8.0.10(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1)(workbox-window@7.4.1))':
'@vite-pwa/vitepress@1.1.0(vite-plugin-pwa@1.3.0(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1)(workbox-window@7.4.1))':
dependencies:
vite-plugin-pwa: 1.3.0(vite@8.0.10(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1)(workbox-window@7.4.1)
vite-plugin-pwa: 1.3.0(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1)(workbox-window@7.4.1)
'@vitejs/plugin-vue-jsx@5.1.5(vite@8.0.10(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0))(vue@3.5.34(typescript@6.0.3))':
dependencies:
@ -18132,6 +18154,8 @@ snapshots:
dependencies:
dequal: 2.0.3
dhtmlx-gantt@9.1.4: {}
diagram-js-direct-editing@3.3.0(diagram-js@14.11.3):
dependencies:
diagram-js: 14.11.3
@ -22536,6 +22560,17 @@ snapshots:
transitivePeerDependencies:
- supports-color
vite-plugin-pwa@1.3.0(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1)(workbox-window@7.4.1):
dependencies:
debug: 4.4.3
pretty-bytes: 6.1.1
tinyglobby: 0.2.16
vite: 8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0)
workbox-build: 7.4.1
workbox-window: 7.4.1
transitivePeerDependencies:
- supports-color
vite-plugin-vue-devtools@8.1.2(vite@8.0.10(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0))(vue@3.5.34(typescript@6.0.3)):
dependencies:
'@vue/devtools-core': 8.1.2(vue@3.5.34(typescript@6.0.3))
@ -22620,13 +22655,13 @@ snapshots:
terser: 5.48.0
yaml: 2.9.0
vitepress-plugin-group-icons@1.7.5(vite@8.0.10(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0)):
vitepress-plugin-group-icons@1.7.5(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0)):
dependencies:
'@iconify-json/logos': 1.2.11
'@iconify-json/vscode-icons': 1.2.50
'@iconify/utils': 3.1.3
optionalDependencies:
vite: 8.0.10(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0)
vite: 8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0)
vitepress@2.0.0-alpha.17(@types/node@25.9.1)(async-validator@4.2.5)(axios@1.16.1)(change-case@5.4.4)(jiti@2.7.0)(less@4.6.4)(lightningcss@1.32.0)(nprogress@0.2.0)(postcss@8.5.15)(qrcode@1.5.4)(sass-embedded@1.100.0)(sass@1.100.0)(sortablejs@1.15.7)(terser@5.48.0)(typescript@6.0.3)(yaml@2.9.0):
dependencies: