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 { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProCardApi } from '#/api/mes/pro/card'; 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 { 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 { 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[] { 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 ProTaskSelectDialog } from './pro-task-select-dialog.vue';
export { default as ProTaskSelect } from './pro-task-select.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 { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProTaskApi } from '#/api/mes/pro/task'; import type { MesProTaskApi } from '#/api/mes/pro/task';
import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder';
import { markRaw } from 'vue'; import { markRaw } from 'vue';
import { DICT_TYPE } from '@vben/constants'; 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 { MdWorkstationSelect } from '#/views/mes/md/workstation/components';
import { ProProcessSelect } from '#/views/mes/pro/process/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'; 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[] { export function useTaskSelectGridFormSchema(): VbenFormSchema[] {
return [ 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'; export { default as ProWorkOrderSelect } from './pro-work-order-select.vue';

View File

@ -1,20 +1,16 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder'; 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 }); defineOptions({ name: 'ProWorkOrderSelect', inheritAttrs: false });
const props = withDefaults( const props = withDefaults(
@ -22,7 +18,6 @@ const props = withDefaults(
allowClear?: boolean; allowClear?: boolean;
disabled?: boolean; disabled?: boolean;
modelValue?: number; modelValue?: number;
pageSize?: number;
placeholder?: string; placeholder?: string;
status?: number; status?: number;
type?: number; type?: number;
@ -31,7 +26,6 @@ const props = withDefaults(
allowClear: true, allowClear: true,
disabled: false, disabled: false,
modelValue: undefined, modelValue: undefined,
pageSize: 100,
placeholder: '请选择工单', placeholder: '请选择工单',
status: undefined, status: undefined,
type: undefined, type: undefined,
@ -43,102 +37,104 @@ const emit = defineEmits<{
'update:modelValue': [value: number | undefined]; 'update:modelValue': [value: number | undefined];
}>(); }>();
const allList = ref<MesProWorkOrderApi.WorkOrder[]>([]); const attrs = useAttrs(); //
const selectedItem = ref<MesProWorkOrderApi.WorkOrder>(); const dialogRef = ref<InstanceType<typeof ProWorkOrderSelectDialog>>(); //
const hovering = ref(false); //
const selectedItem = ref<MesProWorkOrderApi.WorkOrder>(); //
const selectValue = computed({ const displayLabel = computed(() => selectedItem.value?.code ?? ''); //
get: () => props.modelValue, const showClear = computed(() => //
set: (value: number | undefined) => { props.allowClear &&
emit('update:modelValue', value); !props.disabled &&
}, hovering.value &&
}); props.modelValue != null,
);
/** 前端过滤:按工单编码或名称模糊匹配 */ /** 根据编号单条查询工单信息(用于编辑回显) */
function handleFilter(input: string, option: any) { async function resolveItemById(id: number | undefined) {
const keyword = input.toLowerCase(); if (id == null) {
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) {
selectedItem.value = undefined; selectedItem.value = undefined;
return; return;
} }
const found = allList.value.find((item) => item.id === value); if (selectedItem.value?.id === id) {
if (found) {
selectedItem.value = found;
return; return;
} }
try { selectedItem.value = await getWorkOrder(id);
selectedItem.value = await getWorkOrder(value); }
} catch (error) {
console.error('[ProWorkOrderSelect] resolveItemById failed:', error); 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) { function handleSelected(rows: MesProWorkOrderApi.WorkOrder[]) {
const nextValue = value === undefined ? undefined : Number(value); const item = rows[0];
syncSelectedItem(nextValue); if (!item) {
emit('change', selectedItem.value); 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> </script>
<template> <template>
<Tooltip :mouse-enter-delay="0.5" :open="selectedItem ? undefined : false"> <div
<template #title> v-bind="attrs"
<div v-if="selectedItem" class="leading-6"> class="w-full"
<div>编码{{ selectedItem.code || '-' }}</div> :class="disabled ? 'cursor-not-allowed' : 'cursor-pointer'"
<div>名称{{ selectedItem.name || '-' }}</div> @click="handleClick"
<div>产品{{ selectedItem.productName || '-' }}</div> @mouseenter="hovering = true"
<div>数量{{ selectedItem.quantity ?? '-' }}</div> @mouseleave="hovering = false"
</div> >
</template> <Tooltip :mouse-enter-delay="0.5" :open="selectedItem ? undefined : false">
<Select <template #title>
v-bind="$attrs" <div v-if="selectedItem" class="leading-6">
v-model:value="selectValue" <div>编码{{ selectedItem.code || '-' }}</div>
:allow-clear="allowClear" <div>名称{{ selectedItem.name || '-' }}</div>
:disabled="disabled" <div>产品{{ selectedItem.productName || '-' }}</div>
:filter-option="handleFilter" <div>数量{{ selectedItem.quantity ?? '-' }}</div>
: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> </div>
</Select.Option> </template>
</Select> <Input
</Tooltip> :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> </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 WmBatchSelectDialog } from './wm-batch-select-dialog.vue';
export { default as WmBatchSelect } from './wm-batch-select.vue'; export { default as WmBatchSelect } from './wm-batch-select.vue';

View File

@ -1,16 +1,21 @@
import type { VbenFormSchema } from '#/adapter/form'; import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmBatchApi } from '#/api/mes/wm/batch'; 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 { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks'; 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 MdClientSelect from '#/views/mes/md/client/components/md-client-select.vue';
import MdItemSelect from '#/views/mes/md/item/components/md-item-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 MdVendorSelect from '#/views/mes/md/vendor/components/md-vendor-select.vue';
import { MdWorkstationSelect } from '#/views/mes/md/workstation/components'; 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 { ProWorkOrderSelect } from '#/views/mes/pro/workorder/components';
import { TmToolSelect } from '#/views/mes/tm/tool/components'; import { TmToolSelect } from '#/views/mes/tm/tool/components';
@ -66,6 +71,14 @@ export function useBatchSelectGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择工作站', placeholder: '请选择工作站',
}, },
}, },
{
fieldName: 'taskId',
label: '生产任务',
component: markRaw(ProTaskSelect),
componentProps: {
placeholder: '请选择生产任务',
},
},
{ {
fieldName: 'toolId', fieldName: 'toolId',
label: '工具', label: '工具',
@ -74,6 +87,15 @@ export function useBatchSelectGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择工具', placeholder: '请选择工具',
}, },
}, },
{
fieldName: 'moldId',
label: '模具编号',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入模具编号',
},
},
{ {
fieldName: 'salesOrderCode', fieldName: 'salesOrderCode',
label: '销售订单编号', label: '销售订单编号',
@ -111,6 +133,33 @@ export function useBatchSelectGridFormSchema(): VbenFormSchema[] {
placeholder: '请选择质量状态', 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 open = ref(false);
const multiple = ref(true); const multiple = ref(false); // 使
const syncingSingleSelection = ref(false);
const selectedRows = ref<MesWmMaterialStockApi.MaterialStock[]>([]); const selectedRows = ref<MesWmMaterialStockApi.MaterialStock[]>([]);
const preSelectedIds = ref<number[]>([]); const preSelectedIds = ref<number[]>([]);
const searchItemTypeId = ref<number>(); const searchItemTypeId = ref<number>();
@ -64,50 +63,37 @@ const alertTitle = computed(() => {
return `已按${parts.join('/')}预过滤`; return `已按${parts.join('/')}预过滤`;
}); });
/** 单选模式同步 VXE 勾选状态 */ /** 获取多选记录,包含 VXE reserve 跨页记录 */
async function syncSingleSelection(row?: MesWmMaterialStockApi.MaterialStock) { function getMultipleSelectedRows() {
syncingSingleSelection.value = true; const selectedMap = new Map<number, MesWmMaterialStockApi.MaterialStock>();
await nextTick(); const records = [
await gridApi.grid.clearCheckboxRow(); ...(gridApi.grid.getCheckboxReserveRecords?.() ?? []),
if (row) { ...(gridApi.grid.getCheckboxRecords?.() ?? []),
await gridApi.grid.setCheckboxRow(row, true); ] as MesWmMaterialStockApi.MaterialStock[];
} records.forEach((row) => {
await nextTick(); const rowId = row.id;
syncingSingleSelection.value = false; if (rowId !== undefined) {
selectedMap.set(rowId, row);
}
});
return [...selectedMap.values()];
} }
/** 处理勾选变化 */ /** 处理多选勾选变化 */
async function handleCheckboxChange({ function handleCheckboxSelectChange() {
checked, selectedRows.value = getMultipleSelectedRows();
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 handleCheckboxAll({ function handleRadioChange(row: MesWmMaterialStockApi.MaterialStock) {
records, selectedRows.value = [row];
}: { }
records: MesWmMaterialStockApi.MaterialStock[];
}) { /** 多选模式下切换行勾选 */
if (syncingSingleSelection.value) { async function toggleMultipleRow(row: MesWmMaterialStockApi.MaterialStock) {
return; const selected = gridApi.grid.isCheckedByCheckboxRow(row);
} await gridApi.grid.setCheckboxRow(row, !selected);
selectedRows.value = records; selectedRows.value = getMultipleSelectedRows();
} }
/** 双击行:单选直接确认;多选切换勾选 */ /** 双击行:单选直接确认;多选切换勾选 */
@ -117,33 +103,34 @@ async function handleRowDblclick({
row: MesWmMaterialStockApi.MaterialStock; row: MesWmMaterialStockApi.MaterialStock;
}) { }) {
if (multiple.value) { if (multiple.value) {
const checked = !gridApi.grid.isCheckedByCheckboxRow(row); await toggleMultipleRow(row);
await gridApi.grid.setCheckboxRow(row, checked);
handleCheckboxChange({
checked,
records: gridApi.grid.getCheckboxRecords() as MesWmMaterialStockApi.MaterialStock[],
row,
});
return; return;
} }
selectedRows.value = [row]; selectedRows.value = [row];
await syncSingleSelection(row); await gridApi.grid.setRadioRow(row);
handleConfirm(); handleConfirm();
} }
/** 回显预选 */ /** 回显预选 */
function applyPreSelection() { async function applyPreSelection() {
if (preSelectedIds.value.length === 0) { if (preSelectedIds.value.length === 0) {
return; return;
} }
const rows = gridApi.grid.getData() as MesWmMaterialStockApi.MaterialStock[]; const rows = gridApi.grid.getData() as MesWmMaterialStockApi.MaterialStock[];
for (const row of rows) { for (const row of rows) {
if (row.id && preSelectedIds.value.includes(row.id)) { if (row.id === undefined || !preSelectedIds.value.includes(row.id)) {
gridApi.grid.setCheckboxRow(row, true); continue;
if (!multiple.value) {
selectedRows.value = [row];
}
} }
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(), schema: useSelectGridFormSchema(),
}, },
gridOptions: { gridOptions: {
columns: useSelectGridColumns(), columns: useSelectGridColumns(false),
height: 480, height: 480,
keepSource: true, keepSource: true,
checkboxConfig: { highlight: true, range: true, reserve: true }, checkboxConfig: { highlight: true, range: true, reserve: true },
radioConfig: { highlight: true, trigger: 'row' },
proxyConfig: { proxyConfig: {
ajax: { ajax: {
query: async ({ page }, formValues) => { query: async ({ page }, formValues) => {
@ -186,8 +174,11 @@ const [Grid, gridApi] = useVbenVxeGrid({
} as VxeTableGridOptions<MesWmMaterialStockApi.MaterialStock>, } as VxeTableGridOptions<MesWmMaterialStockApi.MaterialStock>,
gridEvents: { gridEvents: {
cellDblclick: handleRowDblclick, cellDblclick: handleRowDblclick,
checkboxAll: handleCheckboxAll, checkboxAll: handleCheckboxSelectChange,
checkboxChange: handleCheckboxChange, checkboxChange: handleCheckboxSelectChange,
radioChange: ({ row }: { row: MesWmMaterialStockApi.MaterialStock }) => {
handleRadioChange(row);
},
}, },
}); });
@ -202,6 +193,8 @@ async function resetQueryState() {
selectedRows.value = []; selectedRows.value = [];
searchItemTypeId.value = undefined; searchItemTypeId.value = undefined;
await gridApi.grid.clearCheckboxRow(); await gridApi.grid.clearCheckboxRow();
await gridApi.grid.clearCheckboxReserve();
await gridApi.grid.clearRadioRow();
await gridApi.formApi.resetForm(); await gridApi.formApi.resetForm();
} }
@ -211,13 +204,16 @@ async function openModal(
options?: { multiple?: boolean }, options?: { multiple?: boolean },
) { ) {
open.value = true; open.value = true;
multiple.value = options?.multiple ?? true; multiple.value = options?.multiple ?? false;
preSelectedIds.value = selectedIds || []; preSelectedIds.value = selectedIds || [];
await nextTick(); await nextTick();
gridApi.setGridOptions({
columns: useSelectGridColumns(multiple.value),
});
await resetQueryState(); await resetQueryState();
await gridApi.query(); await gridApi.query();
await nextTick(); await nextTick();
applyPreSelection(); await applyPreSelection();
} }
/** 关闭弹窗 */ /** 关闭弹窗 */
@ -228,14 +224,12 @@ async function closeModal() {
/** 确认选择 */ /** 确认选择 */
function handleConfirm() { function handleConfirm() {
if (selectedRows.value.length === 0) { const rows = multiple.value ? getMultipleSelectedRows() : selectedRows.value;
if (rows.length === 0) {
message.warning(multiple.value ? '请至少选择一条数据' : '请选择一条数据'); message.warning(multiple.value ? '请至少选择一条数据' : '请选择一条数据');
return; return;
} }
emit( emit('selected', multiple.value ? rows : [rows[0]!]);
'selected',
multiple.value ? selectedRows.value : [selectedRows.value[0]!],
);
open.value = false; open.value = false;
} }

View File

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

View File

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

View File

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

View File

@ -39,6 +39,15 @@ const routes: RouteRecordRaw[] = [
}, },
component: () => import('#/views/mes/wm/barcode/config/index.vue'), 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 ProTaskSelectDialog } from './pro-task-select-dialog.vue';
export { default as ProTaskSelect } from './pro-task-select.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 { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProTaskApi } from '#/api/mes/pro/task'; import type { MesProTaskApi } from '#/api/mes/pro/task';
import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder';
import { markRaw } from 'vue'; import { markRaw } from 'vue';
import { DICT_TYPE } from '@vben/constants'; 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 { MdWorkstationSelect } from '#/views/mes/md/workstation/components';
import { ProProcessSelect } from '#/views/mes/pro/process/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'; 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[] { export function useTaskSelectGridFormSchema(): VbenFormSchema[] {
return [ 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 WmBatchSelectDialog } from './wm-batch-select-dialog.vue';
export { default as WmBatchSelect } from './wm-batch-select.vue'; export { default as WmBatchSelect } from './wm-batch-select.vue';

View File

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

View File

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

View File

@ -73,7 +73,10 @@ export function useGridFormSchema(): VbenFormSchema[] {
/** 列表的字段 */ /** 列表的字段 */
export function useGridColumns( export function useGridColumns(
onFrozenChange: (row: MesWmMaterialStockApi.MaterialStock) => void, onFrozenChange: (
newFrozen: boolean,
row: MesWmMaterialStockApi.MaterialStock,
) => Promise<boolean | undefined>,
): VxeTableGridOptions<MesWmMaterialStockApi.MaterialStock>['columns'] { ): VxeTableGridOptions<MesWmMaterialStockApi.MaterialStock>['columns'] {
return [ 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 [ return [
{ {
type: 'checkbox', type: multiple ? 'checkbox' : 'radio',
width: 50, width: 50,
}, },
{ {

View File

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

View File

@ -285,6 +285,9 @@ catalogs:
defu: defu:
specifier: ^6.1.7 specifier: ^6.1.7
version: 6.1.7 version: 6.1.7
dhtmlx-gantt:
specifier: ^9.1.1
version: 9.1.4
diagram-js: diagram-js:
specifier: ^12.8.1 specifier: ^12.8.1
version: 12.8.1 version: 12.8.1
@ -525,6 +528,9 @@ catalogs:
tw-animate-css: tw-animate-css:
specifier: ^1.4.0 specifier: ^1.4.0
version: 1.4.0 version: 1.4.0
tyme4ts:
specifier: ^1.5.0
version: 1.5.0
typescript: typescript:
specifier: ^6.0.3 specifier: ^6.0.3
version: 6.0.3 version: 6.0.3
@ -819,6 +825,9 @@ importers:
dayjs: dayjs:
specifier: 'catalog:' specifier: 'catalog:'
version: 1.11.20 version: 1.11.20
dhtmlx-gantt:
specifier: 'catalog:'
version: 9.1.4
diagram-js: diagram-js:
specifier: 'catalog:' specifier: 'catalog:'
version: 12.8.1 version: 12.8.1
@ -1071,6 +1080,9 @@ importers:
dayjs: dayjs:
specifier: 'catalog:' specifier: 'catalog:'
version: 1.11.20 version: 1.11.20
dhtmlx-gantt:
specifier: 'catalog:'
version: 9.1.4
diagram-js: diagram-js:
specifier: 'catalog:' specifier: 'catalog:'
version: 12.8.1 version: 12.8.1
@ -1293,14 +1305,14 @@ importers:
version: 2.9.8(vue@3.5.34(typescript@6.0.3)) version: 2.9.8(vue@3.5.34(typescript@6.0.3))
vitepress-plugin-group-icons: vitepress-plugin-group-icons:
specifier: 'catalog:' 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: devDependencies:
'@nolebase/vitepress-plugin-git-changelog': '@nolebase/vitepress-plugin-git-changelog':
specifier: 'catalog:' 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)) 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': '@tailwindcss/vite':
specifier: 'catalog:' 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': '@vben/tailwind-config':
specifier: workspace:* specifier: workspace:*
version: link:../internal/tailwind-config version: link:../internal/tailwind-config
@ -1309,7 +1321,7 @@ importers:
version: link:../internal/vite-config version: link:../internal/vite-config
'@vite-pwa/vitepress': '@vite-pwa/vitepress':
specifier: 'catalog:' 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: vitepress:
specifier: 'catalog:' 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) 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: devlop@1.1.0:
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} 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: diagram-js-direct-editing@3.3.0:
resolution: {integrity: sha512-EjXYb35J3qBU8lLz5U81hn7wNykVmF7U5DXZ7BvPok2IX7rmPz+ZyaI5AEMiqaC6lpSnHqPxFcPgKEiJcAiv5w==} resolution: {integrity: sha512-EjXYb35J3qBU8lLz5U81hn7wNykVmF7U5DXZ7BvPok2IX7rmPz+ZyaI5AEMiqaC6lpSnHqPxFcPgKEiJcAiv5w==}
peerDependencies: peerDependencies:
@ -15694,6 +15709,13 @@ snapshots:
tailwindcss: 4.3.0 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) 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/store@0.11.0': {}
'@tanstack/virtual-core@3.15.0': {} '@tanstack/virtual-core@3.15.0': {}
@ -16590,9 +16612,9 @@ snapshots:
global: 4.4.0 global: 4.4.0
is-function: 1.0.2 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: 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))': '@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: dependencies:
@ -18132,6 +18154,8 @@ snapshots:
dependencies: dependencies:
dequal: 2.0.3 dequal: 2.0.3
dhtmlx-gantt@9.1.4: {}
diagram-js-direct-editing@3.3.0(diagram-js@14.11.3): diagram-js-direct-editing@3.3.0(diagram-js@14.11.3):
dependencies: dependencies:
diagram-js: 14.11.3 diagram-js: 14.11.3
@ -22536,6 +22560,17 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - 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)): 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: dependencies:
'@vue/devtools-core': 8.1.2(vue@3.5.34(typescript@6.0.3)) '@vue/devtools-core': 8.1.2(vue@3.5.34(typescript@6.0.3))
@ -22620,13 +22655,13 @@ snapshots:
terser: 5.48.0 terser: 5.48.0
yaml: 2.9.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: dependencies:
'@iconify-json/logos': 1.2.11 '@iconify-json/logos': 1.2.11
'@iconify-json/vscode-icons': 1.2.50 '@iconify-json/vscode-icons': 1.2.50
'@iconify/utils': 3.1.3 '@iconify/utils': 3.1.3
optionalDependencies: 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): 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: dependencies: