liu
3 天以前 f11bd0d8d9c1190340a445a67df9e43cdbde0a3e
feat(mes): 基础参数与工序绑定前端 + 报工填写参数字段

- 工序设置页新增「工艺参数」绑定弹窗(添加/必填/排序/备注)
- 基础参数页新增「关联工序」入口
- 报工表单按任务参数记录渲染参数字段、必填星号、客户端校验
- 报工详情/列表回显参数值

Co-Authored-By: Claude <noreply@anthropic.com>
已添加4个文件
已修改7个文件
493 ■■■■■ 文件已修改
src/api/mes/pro/feedback/index.ts 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/processdesign/processparam/index.ts 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/workorderparamrecord/index.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/feedback/data.ts 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/feedback/detail/index.vue 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/feedback/modules/form.vue 37 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/feedback/param-state.ts 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/parameter/index.vue 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/parameter/modules/process-link-dialog.vue 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/process/index.vue 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/process/modules/param-manage.vue 193 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/feedback/index.ts
@@ -70,6 +70,13 @@
  export interface ParamValue {
    detailId: number;
    value?: string;
    // ---- å›žæ˜¾å­—段(报工详情/列表返回) ----
    paramCode?: string;
    paramName?: string;
    paramType?: number;
    unitMeasureName?: string;
    paramValue?: string;
    required?: boolean;
  }
  /** MES ç”Ÿäº§æŠ¥å·¥åˆ†é¡µæŸ¥è¯¢å‚æ•° */
src/api/mes/pro/processdesign/processparam/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,52 @@
import { requestClient } from '#/api/request';
export namespace MesProProcessParamApi {
  /** å·¥åº-工艺参数绑定 */
  export interface ProcessParam {
    id?: number;
    processId?: number;
    processParamId?: number;
    paramCode?: string;
    paramName?: string;
    paramType?: number;
    unitMeasureId?: number;
    unitMeasureName?: string;
    required?: boolean;
    sort?: number;
    remark?: string;
  }
  /** ä¿å­˜ç»‘定项 */
  export interface SaveItem {
    processParamId: number;
    required?: boolean;
    sort?: number;
    remark?: string;
  }
}
/** æŸ¥è¯¢å·¥åºç»‘定的工艺参数列表 */
export function getProcessParamListByProcess(processId: number) {
  return requestClient.get<MesProProcessParamApi.ProcessParam[]>(
    '/mes/pro/process-param/list-by-process',
    { params: { processId } },
  );
}
/** å…¨é‡è¦†ç›–保存工序绑定的工艺参数 */
export function updateProcessParams(
  processId: number,
  items: MesProProcessParamApi.SaveItem[],
) {
  return requestClient.post('/mes/pro/process-param/update-bindings', {
    processId,
    items,
  });
}
/** æŸ¥è¯¢æŸå·¥è‰ºå‚数被哪些工序绑定 */
export function getProcessListByParam(paramId: number) {
  return requestClient.get<
    { id: number; code?: string; name?: string; attention?: string }[]
  >('/mes/pro/process-param/list-by-param', { params: { paramId } });
}
src/api/mes/pro/workorderparamrecord/index.ts
@@ -13,6 +13,7 @@
    unitMeasureId?: number;
    unitMeasureName?: string;
    paramValue?: string;
    required?: boolean;
    remark?: string;
  }
src/views/mes/pro/feedback/data.ts
@@ -18,8 +18,8 @@
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { getRouteProcessByRouteAndProcess } from '#/api/mes/pro/route/process';
import { getWorkOrderParamRecordPage } from '#/api/mes/pro/workorderparamrecord';
import { getSimpleUserList } from '#/api/system/user';
import { loadParamDetails } from './param-state';
import { getRangePickerDefaultProps } from '#/utils';
import { MdItemSelect } from '#/views/mes/md/item/components';
import { MdWorkstationSelect } from '#/views/mes/md/workstation/components';
@@ -287,9 +287,10 @@
          workstationId: values.workstationId,
        }),
      },
      // ä»»åŠ¡å˜æ›´ï¼šè‡ªåŠ¨å¡«å……å…³è”å­—æ®µã€äº§å“ä¿¡æ¯ã€checkFlag
      // ä»»åŠ¡å˜æ›´ï¼šè‡ªåŠ¨å¡«å……å…³è”å­—æ®µã€äº§å“ä¿¡æ¯ã€checkFlag,并加载任务绑定的工艺参数
      componentProps: {
        onChange: async (task?: MesProTaskApi.Task) => {
          await loadParamDetails(task?.id);
          if (!task) {
            return;
          }
src/views/mes/pro/feedback/detail/index.vue
@@ -7,15 +7,27 @@
import { Page } from '@vben/common-ui';
import { useTabs } from '@vben/hooks';
import { Card, Tabs } from 'ant-design-vue';
import { DICT_TYPE } from '@vben/constants';
import { Card, Table, Tabs } from 'ant-design-vue';
import { TableAction } from '#/adapter/vxe-table';
import { getFeedback } from '#/api/mes/pro/feedback';
import { DictTag } from '#/components/dict-tag';
import { useDescription } from '#/components/description';
import { useDetailSchema } from '../data';
import ItemConsumeList from '../modules/item-consume-list.vue';
import ProductProduceList from '../modules/product-produce-list.vue';
const paramColumns = [
  { title: '参数名称', dataIndex: 'paramName', key: 'paramName' },
  { title: '参数编码', dataIndex: 'paramCode', key: 'paramCode' },
  { title: '参数类型', dataIndex: 'paramType', key: 'paramType' },
  { title: '单位', dataIndex: 'unitMeasureName', key: 'unitMeasureName' },
  { title: '参数值', dataIndex: 'paramValue', key: 'paramValue' },
  { title: '是否必填', dataIndex: 'required', key: 'required' },
];
const props = defineProps<{ id: string }>();
@@ -62,6 +74,31 @@
    <Card class="min-h-[10%]">
      <Descriptions :data="feedback" />
    </Card>
    <Card v-if="feedback.paramValues && feedback.paramValues.length > 0" class="mt-3" title="工艺参数">
      <Table
        :columns="paramColumns"
        :data-source="feedback.paramValues"
        :pagination="false"
        row-key="detailId"
        size="small"
      >
        <template #bodyCell="{ column, record }">
          <template v-if="column.key === 'paramType'">
            <DictTag
              v-if="record.paramType !== undefined && record.paramType !== null"
              :type="DICT_TYPE.MES_PD_PARAM_TYPE"
              :value="record.paramType"
            />
          </template>
          <template v-else-if="column.key === 'required'">
            <DictTag
              :type="DICT_TYPE.INFRA_BOOLEAN_STRING"
              :value="record.required ? 'true' : 'false'"
            />
          </template>
        </template>
      </Table>
    </Card>
  </Page>
</template>
src/views/mes/pro/feedback/modules/form.vue
@@ -26,10 +26,10 @@
} from '#/api/mes/pro/feedback';
import { getRouteProcessByRouteAndProcess } from '#/api/mes/pro/route/process';
import { getWorkOrder } from '#/api/mes/pro/workorder';
import { getWorkOrderParamRecordPage } from '#/api/mes/pro/workorderparamrecord';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
import { findMissingRequiredParams, loadParamDetails, paramDetails } from '../param-state';
import BatchInputDialog from './batch-input-dialog.vue';
import ItemConsumeList from './item-consume-list.vue';
import ProductProduceList from './product-produce-list.vue';
@@ -41,7 +41,6 @@
const userStore = useUserStore();
const subTabsName = ref('itemConsume');
const originalSnapshot = ref(''); // è¡¨å•原始数据快照,用于 submit æ—¶è·³è¿‡æœªå˜æ›´çš„保存请求
const paramDetails = ref<Array<{ id: number; paramName?: string; paramCode?: string; unitMeasureName?: string; paramValue?: string }>>([]);
const isEditable = computed(() =>
  ['create', 'submit', 'update'].includes(formType.value),
@@ -118,6 +117,9 @@
  if (!valid) {
    return;
  }
  if (!checkRequiredParams()) {
    return;
  }
  modalApi.lock();
  try {
    const data = await getAlignedData();
@@ -149,25 +151,21 @@
const batchDialogRef = ref<InstanceType<typeof BatchInputDialog>>();
const batchActionType = ref<'submit' | 'approve'>('');
async function loadParamDetails(taskId?: number) {
  paramDetails.value = [];
  if (!taskId) return;
  const page = await getWorkOrderParamRecordPage({ taskId, pageNo: 1, pageSize: 1 });
  const record = page.list?.[0];
  paramDetails.value = (record?.details ?? []).map((detail) => ({
    id: detail.id!,
    paramCode: detail.paramCode,
    paramName: detail.paramName,
    unitMeasureName: detail.unitMeasureName,
    paramValue: detail.paramValue,
  }));
}
function syncParamValues(data: MesProFeedbackApi.Feedback) {
  data.paramValues = paramDetails.value.map((detail) => ({
    detailId: detail.id,
    value: detail.paramValue,
  }));
}
/** æ ¡éªŒå¿…填参数是否填写完整 */
function checkRequiredParams(): boolean {
  const missing = findMissingRequiredParams();
  if (missing.length > 0) {
    message.warning(`必填参数未填写:${missing.join('、')}`);
    return false;
  }
  return true;
}
/** æ‰§è¡Œæäº¤ï¼ˆå«æ‰¹æ¬¡å±žæ€§ï¼‰ */
@@ -188,6 +186,9 @@
async function handleSubmit() {
  const { valid } = await formApi.validate();
  if (!valid) {
    return;
  }
  if (!checkRequiredParams()) {
    return;
  }
  modalApi.lock();
@@ -326,6 +327,7 @@
      formData.value = undefined;
      subTabsName.value = 'itemConsume';
      originalSnapshot.value = '';
      paramDetails.value = [];
      return;
    }
    // åŠ è½½æ•°æ®
@@ -383,7 +385,8 @@
      <div class="mb-3 font-medium">工艺参数</div>
      <div class="grid grid-cols-2 gap-3">
        <div v-for="detail in paramDetails" :key="detail.id" class="flex items-center gap-2">
          <span class="w-40 text-right text-sm text-gray-600">
          <span class="w-44 text-right text-sm text-gray-600">
            <span v-if="detail.required" class="text-red-500">*&nbsp;</span>
            {{ detail.paramName || detail.paramCode }}{{ detail.unitMeasureName ? `(${detail.unitMeasureName})` : '' }}
          </span>
          <Input v-model:value="detail.paramValue" :disabled="!isEditable" placeholder="请输入参数值" />
src/views/mes/pro/feedback/param-state.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,47 @@
import { ref } from 'vue';
import { getWorkOrderParamRecordPage } from '#/api/mes/pro/workorderparamrecord';
/** æŠ¥å·¥è¡¨å•要填写的参数项(来自任务参数记录快照) */
export interface ParamDetailItem {
  id: number;
  paramCode?: string;
  paramName?: string;
  paramType?: number;
  unitMeasureName?: string;
  paramValue?: string;
  required?: boolean;
}
/** å½“前报工表单的参数明细(共享给表单与任务选择联动) */
export const paramDetails = ref<ParamDetailItem[]>([]);
/** æŒ‰ä»»åŠ¡åŠ è½½å‚æ•°æ˜Žç»†ï¼ˆä»»åŠ¡æ— å‚æ•°è®°å½•æ—¶ä¸ºç©ºæ•°ç»„ï¼‰ */
export async function loadParamDetails(taskId?: number) {
  paramDetails.value = [];
  if (!taskId) {
    return;
  }
  const page = await getWorkOrderParamRecordPage({
    taskId,
    pageNo: 1,
    pageSize: 1,
  });
  const record = page.list?.[0];
  paramDetails.value = (record?.details ?? []).map((detail) => ({
    id: detail.id!,
    paramCode: detail.paramCode,
    paramName: detail.paramName,
    paramType: detail.paramType,
    unitMeasureName: detail.unitMeasureName,
    paramValue: detail.paramValue,
    required: detail.required,
  }));
}
/** æ ¡éªŒå¿…填参数是否都已填写;返回未填写的参数名 */
export function findMissingRequiredParams(): string[] {
  return paramDetails.value
    .filter((detail) => detail.required && !detail.paramValue?.trim())
    .map((detail) => detail.paramName || detail.paramCode || '');
}
src/views/mes/process-design/parameter/index.vue
@@ -17,15 +17,28 @@
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
import ProcessLinkDialog from './modules/process-link-dialog.vue';
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
const [ProcessLinkModal, processLinkModalApi] = useVbenModal({
  connectedComponent: ProcessLinkDialog,
  destroyOnClose: true,
});
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** æŸ¥çœ‹è¯¥å‚数被哪些工序绑定 */
function handleViewProcessLink(row: MesPdParameterApi.Parameter) {
  processLinkModalApi
    .setData({ paramId: row.id, paramName: row.paramName })
    .open();
}
/** åˆ›å»ºå·¥è‰ºå‚æ•° */
@@ -98,6 +111,7 @@
<template>
  <Page auto-content-height><FormModal @success="handleRefresh" />
    <ProcessLinkModal />
    <Grid table-title="工艺参数列表">
      <template #toolbar-tools>
        <TableAction
@@ -126,6 +140,13 @@
        <TableAction
          :actions="[
            {
              label: '关联工序',
              type: 'link',
              icon: ACTION_ICON.LINK,
              auth: ['mes:pd:parameter:update'],
              onClick: handleViewProcessLink.bind(null, row),
            },
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
src/views/mes/process-design/parameter/modules/process-link-dialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,70 @@
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Table } from 'ant-design-vue';
import { getProcessListByParam } from '#/api/mes/pro/processdesign/processparam';
const emit = defineEmits(['success']);
const paramId = ref<number>();
const paramName = ref<string>('');
const loading = ref(false);
const processList = ref<{ id: number; code?: string; name?: string; attention?: string }[]>([]);
const getTitle = computed(() => `关联工序 - ${paramName.value || ''}`);
async function loadData() {
  if (!paramId.value) {
    return;
  }
  loading.value = true;
  try {
    processList.value = await getProcessListByParam(paramId.value);
  } finally {
    loading.value = false;
  }
}
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    await modalApi.close();
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      paramId.value = undefined;
      processList.value = [];
      return;
    }
    const data = modalApi.getData<{ paramId: number; paramName?: string }>();
    paramId.value = data.paramId;
    paramName.value = data.paramName || '';
    await loadData();
  },
});
const columns = [
  { title: '工序编码', dataIndex: 'code', key: 'code' },
  { title: '工序名称', dataIndex: 'name', key: 'name' },
  { title: '工艺要求', dataIndex: 'attention', key: 'attention' },
];
</script>
<template>
  <Modal :title="getTitle" class="w-2/5">
    <Table
      :columns="columns"
      :data-source="processList"
      :loading="loading"
      :pagination="false"
      row-key="id"
      size="small"
    >
      <template #emptyText>
        {{ loading ? '加载中…' : '该参数暂未被任何工序绑定' }}
      </template>
    </Table>
  </Modal>
</template>
src/views/mes/process-design/process/index.vue
@@ -17,15 +17,28 @@
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
import ParamManage from './modules/param-manage.vue';
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
const [ParamManageModal, paramManageModalApi] = useVbenModal({
  connectedComponent: ParamManage,
  destroyOnClose: true,
});
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** ç®¡ç†å·¥åºç»‘定的工艺参数 */
function handleParamManage(row: MesProProcessApi.Process) {
  paramManageModalApi
    .setData({ processId: row.id, processName: row.name })
    .open();
}
/** åˆ›å»ºç”Ÿäº§å·¥åº */
@@ -97,6 +110,7 @@
<template>
  <Page auto-content-height><FormModal @success="handleRefresh" />
    <ParamManageModal @success="handleRefresh" />
    <Grid table-title="生产工序列表">
      <template #toolbar-tools>
        <TableAction
@@ -125,6 +139,13 @@
        <TableAction
          :actions="[
            {
              label: '工艺参数',
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['mes:pro-process:update'],
              onClick: handleParamManage.bind(null, row),
            },
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
src/views/mes/process-design/process/modules/param-manage.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,193 @@
<script lang="ts" setup>
import type { MesPdParameterApi } from '#/api/mes/pro/processdesign/parameter';
import type { MesProProcessParamApi } from '#/api/mes/pro/processdesign/processparam';
import { computed, ref } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { useVbenModal } from '@vben/common-ui';
import { Button, Input, InputNumber, message, Switch } from 'ant-design-vue';
import {
  getProcessParamListByProcess,
  updateProcessParams,
} from '#/api/mes/pro/processdesign/processparam';
import { $t } from '#/locales';
import { DictTag } from '#/components/dict-tag';
import ParameterSelectDialog from '../../template/modules/parameter-select-dialog.vue';
const emit = defineEmits(['success']);
const processId = ref<number>();
const processName = ref<string>('');
const loading = ref(false);
const boundList = ref<MesProProcessParamApi.ProcessParam[]>([]);
const parameterSelectDialogRef =
  ref<InstanceType<typeof ParameterSelectDialog>>();
const getTitle = computed(() => `工艺参数绑定 - ${processName.value || ''}`);
/** åŠ è½½å·¥åºå·²ç»‘å®šçš„å‚æ•° */
async function loadData() {
  if (!processId.value) {
    return;
  }
  loading.value = true;
  try {
    boundList.value = await getProcessParamListByProcess(processId.value);
  } finally {
    loading.value = false;
  }
}
/** æ‰“开工艺参数选择弹窗 */
function handleSelectParameters() {
  parameterSelectDialogRef.value?.open(
    boundList.value
      .map((item) => item.processParamId)
      .filter((id): id is number => id !== undefined),
  );
}
/** æŽ¥æ”¶é€‰ä¸­çš„参数,追加到绑定列表(必填/排序走默认值) */
function onParametersSelected(rows: MesPdParameterApi.Parameter[]) {
  const existingIds = new Set(
    boundList.value
      .map((item) => item.processParamId)
      .filter((id): id is number => id !== undefined),
  );
  const newRows = rows
    .filter((row) => row.id !== undefined && !existingIds.has(row.id!))
    .map((row, index) => ({
      processParamId: row.id,
      paramCode: row.paramCode,
      paramName: row.paramName,
      paramType: row.paramType,
      unitMeasureName: row.unitMeasureName,
      required: row.required,
      sort: boundList.value.length + index + 1,
      remark: row.remark,
    }));
  boundList.value = [...boundList.value, ...newRows];
}
/** ç§»é™¤ç»‘定 */
function removeItem(index: number) {
  boundList.value.splice(index, 1);
}
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    if (processId.value === undefined) {
      return;
    }
    modalApi.lock();
    try {
      await updateProcessParams(
        processId.value,
        boundList.value.map((item, index) => ({
          processParamId: item.processParamId!,
          required: item.required,
          sort: item.sort ?? index + 1,
          remark: item.remark,
        })),
      );
      message.success($t('ui.actionMessage.operationSuccess'));
      await modalApi.close();
      emit('success');
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      processId.value = undefined;
      boundList.value = [];
      return;
    }
    const data = modalApi.getData<{ processId: number; processName?: string }>();
    processId.value = data.processId;
    processName.value = data.processName || '';
    await loadData();
  },
});
</script>
<template>
  <Modal :title="getTitle" class="w-4/5">
    <div class="mx-4">
      <div class="mb-2 flex items-center justify-between">
        <span class="text-sm font-medium">已绑定工艺参数</span>
        <Button type="primary" size="small" @click="handleSelectParameters">
          æ·»åŠ å‚æ•°
        </Button>
      </div>
      <div v-if="boundList.length > 0" class="rounded border border-gray-200">
        <div
          class="grid grid-cols-12 gap-2 border-b border-gray-200 bg-gray-50 p-2 text-xs font-medium"
        >
          <div class="col-span-3">参数名称</div>
          <div class="col-span-2">参数编码</div>
          <div class="col-span-2">参数类型</div>
          <div class="col-span-1">单位</div>
          <div class="col-span-1 text-center">是否必填</div>
          <div class="col-span-1">排序</div>
          <div class="col-span-1">备注</div>
          <div class="col-span-1 text-center">操作</div>
        </div>
        <div
          v-for="(item, index) in boundList"
          :key="item.processParamId || index"
          class="grid grid-cols-12 items-center gap-2 border-b border-gray-100 p-2"
        >
          <div class="col-span-3 text-sm">{{ item.paramName }}</div>
          <div class="col-span-2 text-sm text-gray-500">{{ item.paramCode }}</div>
          <div class="col-span-2">
            <DictTag
              v-if="item.paramType !== undefined"
              :type="DICT_TYPE.MES_PD_PARAM_TYPE"
              :value="item.paramType"
            />
          </div>
          <div class="col-span-1 text-sm">{{ item.unitMeasureName }}</div>
          <div class="col-span-1 flex items-center justify-center">
            <Switch v-model:checked="item.required" size="small" />
          </div>
          <div class="col-span-1">
            <InputNumber
              v-model:value="item.sort"
              :min="1"
              size="small"
              class="w-full"
            />
          </div>
          <div class="col-span-1">
            <Input
              v-model:value="item.remark"
              placeholder="备注"
              size="small"
            />
          </div>
          <div class="col-span-1 flex items-center justify-center">
            <Button type="text" danger size="small" @click="removeItem(index)">
              åˆ é™¤
            </Button>
          </div>
        </div>
      </div>
      <div
        v-else
        class="rounded border border-dashed border-gray-300 p-4 text-center text-gray-400"
      >
        {{ loading ? '加载中…' : '暂未绑定参数' }}
      </div>
    </div>
    <ParameterSelectDialog
      ref="parameterSelectDialogRef"
      @selected="onParametersSelected"
    />
  </Modal>
</template>