feat(mes): 迁移“生产报工(pro_feedback)”的 ele 功能

This commit is contained in:
YunaiV
2026-05-26 21:18:36 +08:00
parent 44b62e14ac
commit 5a1100aed4
19 changed files with 2451 additions and 110 deletions

View File

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

View File

@@ -0,0 +1,245 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProTaskApi } from '#/api/mes/pro/task';
import { computed, nextTick, ref } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictLabel } from '@vben/hooks';
import { Alert, Button, message, Modal } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getTaskPage } from '#/api/mes/pro/task';
import { useTaskSelectGridColumns, useTaskSelectGridFormSchema } from '../data';
const props = withDefaults(
defineProps<{
statuses?: number[];
}>(),
{
statuses: undefined,
},
);
const emit = defineEmits<{
selected: [rows: MesProTaskApi.Task[]];
}>();
const open = ref(false); // 弹窗是否打开
const multiple = ref(false); // 是否多选;默认按单选选择器使用
const selectedRows = ref<MesProTaskApi.Task[]>([]); // 已选任务列表
const preSelectedIds = ref<number[]>([]); // 预选任务编号列表
const externalWorkOrderId = ref<number>(); // 外部传入的默认工单过滤
const externalWorkstationId = ref<number>(); // 外部传入的默认工位过滤
const statusTip = computed(() => {
if (!props.statuses?.length) {
return '';
}
const labels = props.statuses
.map((value) => getDictLabel(DICT_TYPE.MES_PRO_TASK_STATUS, value))
.filter(Boolean)
.join('、');
return `仅展示状态为【${labels}】的任务`;
});
/** 获取多选记录,包含 VXE reserve 跨页记录 */
function getMultipleSelectedRows() {
const selectedMap = new Map<number, MesProTaskApi.Task>();
const records = [
...(gridApi.grid.getCheckboxReserveRecords?.() ?? []),
...(gridApi.grid.getCheckboxRecords?.() ?? []),
] as MesProTaskApi.Task[];
records.forEach((row) => {
const rowId = row.id;
if (rowId !== undefined) {
selectedMap.set(rowId, row);
}
});
return [...selectedMap.values()];
}
/** 处理多选勾选变化 */
function handleCheckboxSelectChange() {
selectedRows.value = getMultipleSelectedRows();
}
/** 处理单选切换 */
function handleRadioChange(row: MesProTaskApi.Task) {
selectedRows.value = [row];
}
/** 多选模式下切换行勾选 */
async function toggleMultipleRow(row: MesProTaskApi.Task) {
const selected = gridApi.grid.isCheckedByCheckboxRow(row);
await gridApi.grid.setCheckboxRow(row, !selected);
selectedRows.value = getMultipleSelectedRows();
}
/** 处理行双击:单选直接确认,多选切换勾选 */
async function handleCellDblclick({ row }: { row: MesProTaskApi.Task }) {
if (multiple.value) {
await toggleMultipleRow(row);
return;
}
selectedRows.value = [row];
await gridApi.grid.setRadioRow(row);
handleConfirm();
}
/** 回显预选任务 */
async function applyPreSelection() {
if (preSelectedIds.value.length === 0) {
return;
}
const rows = gridApi.grid.getData() as MesProTaskApi.Task[];
for (const row of rows) {
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();
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useTaskSelectGridFormSchema(),
},
gridOptions: {
columns: useTaskSelectGridColumns(false),
height: 520,
keepSource: true,
checkboxConfig: {
highlight: true,
range: true,
reserve: true,
},
radioConfig: {
highlight: true,
trigger: 'row',
},
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getTaskPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
statuses: props.statuses,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
search: true,
},
} as VxeTableGridOptions<MesProTaskApi.Task>,
gridEvents: {
cellDblclick: handleCellDblclick,
checkboxAll: handleCheckboxSelectChange,
checkboxChange: handleCheckboxSelectChange,
radioChange: ({ row }: { row: MesProTaskApi.Task }) => {
handleRadioChange(row);
},
},
});
/** 重置查询和选择状态,保留外部传入的工单/工位默认过滤 */
async function resetQueryState() {
selectedRows.value = [];
await gridApi.grid.clearCheckboxRow();
await gridApi.grid.clearCheckboxReserve();
await gridApi.grid.clearRadioRow();
await gridApi.formApi.resetForm();
if (externalWorkOrderId.value) {
await gridApi.formApi.setFieldValue('workOrderId', externalWorkOrderId.value);
}
if (externalWorkstationId.value) {
await gridApi.formApi.setFieldValue(
'workstationId',
externalWorkstationId.value,
);
}
}
/** 打开任务选择弹窗 */
async function openModal(
selectedIds?: number[],
options?: {
multiple?: boolean;
workOrderId?: number;
workstationId?: number;
},
) {
open.value = true;
multiple.value = options?.multiple ?? false;
preSelectedIds.value = selectedIds || [];
externalWorkOrderId.value = options?.workOrderId;
externalWorkstationId.value = options?.workstationId;
await nextTick();
gridApi.setGridOptions({
columns: useTaskSelectGridColumns(multiple.value),
});
await resetQueryState();
await gridApi.query();
await nextTick();
await applyPreSelection();
}
/** 关闭任务选择弹窗 */
function closeModal() {
open.value = false;
}
/** 确认选择任务 */
function handleConfirm() {
const rows = multiple.value ? getMultipleSelectedRows() : selectedRows.value;
if (rows.length === 0) {
message.warning(multiple.value ? '请至少选择一条数据' : '请选择一条数据');
return;
}
emit('selected', multiple.value ? rows : [rows[0]!]);
open.value = false;
}
defineExpose({ open: openModal });
</script>
<template>
<Modal
v-model:open="open"
title="生产任务选择"
width="80%"
:destroy-on-close="true"
@ok="handleConfirm"
@cancel="closeModal"
>
<Alert
v-if="statusTip"
:message="statusTip"
type="info"
show-icon
class="!mb-3"
/>
<Grid table-title="生产任务列表" />
<template #footer>
<Button @click="closeModal">取消</Button>
<Button type="primary" @click="handleConfirm">确定</Button>
</template>
</Modal>
</template>

View File

@@ -1,21 +1,16 @@
<script lang="ts" setup>
import type { MesProTaskApi } from '#/api/mes/pro/task';
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 { getTask, getTaskPage } from '#/api/mes/pro/task';
import { Input, Tooltip } from 'ant-design-vue';
import { getTask } from '#/api/mes/pro/task';
import ProTaskSelectDialog from './pro-task-select-dialog.vue';
// TODO @AI直接完整迁移
/**
* MES 生产任务选择器(轻量版)
*
* 当前用于生产报工等只需要单选任务 ID 的业务页面:
* - 默认按 `workOrderId` / `workstationId` / `statuses` 过滤拉取首页 100 条任务作为下拉
* - 编辑回显走 `getTask(id)`
* - 后续 `mes/pro/task` 完整迁移后,可替换为带弹窗的复杂选择器
*/
defineOptions({ name: 'ProTaskSelect', inheritAttrs: false });
const props = withDefaults(
@@ -23,7 +18,6 @@ const props = withDefaults(
allowClear?: boolean;
disabled?: boolean;
modelValue?: number;
pageSize?: number;
placeholder?: string;
statuses?: number[];
workOrderId?: number;
@@ -33,131 +27,131 @@ const props = withDefaults(
allowClear: true,
disabled: false,
modelValue: undefined,
pageSize: 100,
placeholder: '请选择任务',
statuses: undefined,
workOrderId: undefined,
workstationId: undefined,
},
);
const emit = defineEmits<{
change: [item: MesProTaskApi.Task | undefined];
'update:modelValue': [value: number | undefined];
}>();
const attrs = useAttrs(); // 透传属性
const dialogRef = ref<InstanceType<typeof ProTaskSelectDialog>>(); // 任务选择弹窗
const hovering = ref(false); // 是否悬停
const selectedItem = ref<MesProTaskApi.Task>(); // 选中的任务
const allList = ref<MesProTaskApi.Task[]>([]);
const selectedItem = ref<MesProTaskApi.Task>();
const displayLabel = computed(() => selectedItem.value?.code ?? ''); // 选择器展示编号
const showClear = computed( // 是否显示清空图标
() =>
props.allowClear &&
!props.disabled &&
hovering.value &&
props.modelValue !== undefined,
);
const selectValue = computed({
get: () => props.modelValue,
set: (value: number | undefined) => {
emit('update:modelValue', value);
},
});
/** 前端过滤:按任务编号或名称模糊匹配 */
function handleFilter(input: string, option: any) {
const keyword = input.toLowerCase();
const item = option?.item as MesProTaskApi.Task | 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 === undefined) {
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 getTask(value);
selectedItem.value = await getTask(id);
} catch (error) {
console.error('[ProTaskSelect] resolveItemById failed:', error);
}
}
/** 除 v-model 外,额外抛出完整任务对象给业务表单使用 */
function handleChange(value: any) {
const nextValue = value === undefined ? undefined : Number(value);
syncSelectedItem(nextValue);
emit('change', selectedItem.value);
}
/** 重新拉取候选任务列表 */
async function loadList() {
const data = await getTaskPage({
pageNo: 1,
pageSize: props.pageSize,
statuses: props.statuses,
workOrderId: props.workOrderId,
workstationId: props.workstationId,
});
allList.value = data.list ?? [];
}
watch(
() => props.modelValue,
(value) => {
syncSelectedItem(value);
resolveItemById(value);
},
{ immediate: true },
);
watch(
() => [props.workOrderId, props.workstationId],
async () => {
await loadList();
syncSelectedItem(props.modelValue);
},
);
/** 清空已选任务 */
function clearSelected() {
selectedItem.value = undefined;
emit('update:modelValue', undefined);
emit('change', undefined);
}
onMounted(async () => {
await loadList();
syncSelectedItem(props.modelValue);
});
/** 打开任务选择弹窗 */
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 === undefined ? [] : [props.modelValue];
dialogRef.value?.open(selectedIds, {
multiple: false,
workOrderId: props.workOrderId,
workstationId: props.workstationId,
});
}
/** 回填选中的任务 */
function handleSelected(rows: MesProTaskApi.Task[]) {
const item = rows[0];
if (!item) {
return;
}
selectedItem.value = item;
emit('update:modelValue', item.id);
emit('change', item);
}
</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.processName || '-' }}</div>
<div>工作站{{ selectedItem.workstationName || '-' }}</div>
<div>物料{{ selectedItem.itemName || '-' }}</div>
<div>规格{{ selectedItem.itemSpecification || '-' }}</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.itemName" color="default">{{ item.itemName }}</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.processName || '-' }}</div>
<div>工作站{{ selectedItem.workstationName || '-' }}</div>
<div>物料{{ selectedItem.itemName || '-' }}</div>
<div>规格{{ selectedItem.itemSpecification || '-' }}</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>
<ProTaskSelectDialog
ref="dialogRef"
:statuses="statuses"
@selected="handleSelected"
/>
</template>

View File

@@ -0,0 +1,112 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProTaskApi } from '#/api/mes/pro/task';
import { markRaw } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { MdWorkstationSelect } from '#/views/mes/md/workstation/components';
import { ProProcessSelect } from '#/views/mes/pro/process/components';
import { ProWorkOrderSelect } from '#/views/mes/pro/workorder/components';
/** 任务选择弹窗的搜索表单 */
export function useTaskSelectGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'workOrderId',
label: '生产工单',
component: markRaw(ProWorkOrderSelect),
componentProps: {
allowClear: true,
placeholder: '请选择生产工单',
},
},
{
fieldName: 'processId',
label: '所属工序',
component: markRaw(ProProcessSelect),
componentProps: {
allowClear: true,
placeholder: '请选择工序',
},
},
{
fieldName: 'workstationId',
label: '工作站',
component: markRaw(MdWorkstationSelect),
componentProps: {
allowClear: true,
placeholder: '请选择工作站',
},
},
{
fieldName: 'code',
label: '任务编号',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入任务编号',
},
},
{
fieldName: 'name',
label: '任务名称',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入任务名称',
},
},
];
}
/** 任务选择弹窗的字段 */
export function useTaskSelectGridColumns(
multiple = false,
): VxeTableGridOptions<MesProTaskApi.Task>['columns'] {
return [
{ type: multiple ? 'checkbox' : 'radio', width: 50 },
{ field: 'code', title: '任务编号', width: 180 },
{ field: 'name', title: '任务名称', minWidth: 140 },
{ field: 'workstationCode', title: '工作站编码', width: 140 },
{ field: 'workstationName', title: '工作站名称', width: 140 },
{ field: 'processName', title: '工序', width: 120 },
{
field: 'checkFlag',
title: '是否质检',
width: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
},
},
{ field: 'itemCode', title: '物料编码', width: 140 },
{ field: 'itemName', title: '物料名称', width: 140 },
{ field: 'itemSpecification', title: '规格型号', width: 120 },
{ field: 'quantity', title: '排产数量', width: 100 },
{ field: 'producedQuantity', title: '已生产数量', width: 110 },
{
field: 'startTime',
title: '开始生产时间',
width: 170,
formatter: 'formatDateTime',
},
{ field: 'duration', title: '生产时长', width: 100 },
{
field: 'endTime',
title: '预计完成时间',
width: 170,
formatter: 'formatDateTime',
},
{
field: 'status',
title: '任务状态',
width: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.MES_PRO_TASK_STATUS },
},
},
];
}

View File

@@ -166,10 +166,10 @@ export const MesProTaskStatusEnum = {
/** MES 生产报工状态枚举 */
export const MesProFeedbackStatusEnum = {
PREPARE: MesOrderStatusConstants.DRAFT,
CONFIRMED: MesOrderStatusConstants.CONFIRMED,
APPROVING: MesOrderStatusConstants.APPROVING,
UNCHECK: MesOrderStatusConstants.APPROVED,
FINISHED: MesOrderStatusConstants.FINISHED,
CANCELLED: MesOrderStatusConstants.CANCELLED,
CANCELED: MesOrderStatusConstants.CANCELLED,
} as const;
/** MES 流转卡状态枚举 */