云
8 天以前 964cfa12263cf51b2a3d1ef281130d0e808e9078
feat: 完善业务导入及工艺流程界面
已添加1个文件
已修改30个文件
387 ■■■■■ 文件已修改
src/api/crm/saleQuotation/index.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mdm/item/index.ts 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/feedback/index.ts 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/process/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/route/index.ts 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/route/product/index.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/wm/materialstock/index.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/import-modal/import-modal.vue 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/mdm/data.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/mdm/index.vue 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/saleQuotation/index.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/order/index.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/request/index.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/sale/order/index.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/employee/modules/import-form.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/feedback/components/FeedbackParamFields.vue 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/feedback/data.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/feedback/modules/batch-feedback.vue 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/feedback/modules/form.vue 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/process/data.ts 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/route/data.ts 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/route/modules/product-list.vue 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/route/modules/route-configurator.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/workorder-center/modules/quick-feedback.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/process/data.ts 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/process/modules/form.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/route/data.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/route/index.vue 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/workbench/components/tabs/FeedbackTab.vue 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/materialstock/index.vue 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/transaction/data.ts 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/crm/saleQuotation/index.ts
@@ -115,4 +115,13 @@
  return requestClient.post<number>('/crm/sale-quotation/convert-contract', null, {
    params: { id },
  });
}
/** ä¸‹è½½æŠ¥ä»·æ˜Žç»†å¯¼å…¥æ¨¡æ¿ */
export function importSaleQuotationTemplate() {
  return requestClient.download('/crm/sale-quotation/get-import-template');
}
/** å¯¼å…¥æŠ¥ä»·ï¼ˆä»…新增) */
export function importSaleQuotation(file: File, updateSupport: boolean) {
  return requestClient.upload('/crm/sale-quotation/import', { file, updateSupport });
}
src/api/mdm/item/index.ts
@@ -6,6 +6,7 @@
  /** ç‰©æ–™ä¿¡æ¯ */
  export interface Item {
    id?: number;
    sort?: number;
    code: string;
    name: string;
    barCode?: string;
src/api/mes/pro/feedback/index.ts
@@ -23,6 +23,8 @@
    workOrderCode?: string; // è®¢å•编码
    workOrderName?: string; // è®¢å•名称
    taskId?: number; // ç”Ÿäº§ä»»åŠ¡ç¼–å·
    paramValues?: ParamValue[];
    paramSnapshot?: string;
    taskCode?: string; // ä»»åŠ¡ç¼–ç 
    itemId?: number; // äº§å“ç‰©æ–™ç¼–号
    itemCode?: string; // ç‰©æ–™ç¼–码
@@ -49,6 +51,12 @@
    createTime?: number; // åˆ›å»ºæ—¶é—´
  }
  export interface ParamValue { detailId: number; value?: string }
  export interface ParamDetail { id: number; paramCode?: string; paramName: string; paramType?: number; required?: boolean; unitMeasureId?: number; unitMeasureName?: string }
  export interface ParamTemplate { templateId: number; templateCode?: string; templateName?: string; details: ParamDetail[] }
  export interface ParamSnapshot { template: ParamTemplate; values: ParamValue[] }
  export interface ParamConfig { mode: number; template?: ParamTemplate; shared?: ParamSnapshot }
  /** MES ç”Ÿäº§æŠ¥å·¥åˆ†é¡µæŸ¥è¯¢å‚æ•° */
  export interface PageParams extends PageParam {
    code?: string;
@@ -62,6 +70,11 @@
    status?: number;
    feedbackTime?: string[];
  }
}
/** èŽ·å¾—ä»»åŠ¡ä¸‹è¾¾æ—¶å†»ç»“çš„å‚æ•°é…ç½® */
export function getFeedbackParamConfig(taskId: number) {
  return requestClient.get<MesProFeedbackApi.ParamConfig>('/mes/pro/feedback/param-config', { params: { taskId } });
}
/** æŸ¥è¯¢ç”Ÿäº§æŠ¥å·¥åˆ†é¡µ */
@@ -159,6 +172,7 @@
/** å¿«é€ŸæŠ¥å·¥è¯·æ±‚ */
export interface QuickCreateReq {
  taskId: number;
  paramValues?: MesProFeedbackApi.ParamValue[];
  feedbackQuantity: number;
  qualifiedQuantity?: number;
  unqualifiedQuantity?: number;
src/api/mes/pro/process/index.ts
@@ -10,6 +10,8 @@
    name?: string;
    type?: number; // ç”µåŠ›å·¥è‰ºåˆ†ç±»
    attention?: string;
    paramCollectMode?: number; // 0 ä¸é‡‡é›†ï¼›1 æ¯æ¬¡æŠ¥å·¥ï¼›2 ä»»åŠ¡å…±ç”¨
    paramTemplateId?: number;
    status?: number;
    remark?: string;
    createTime?: Date;
src/api/mes/pro/route/index.ts
@@ -8,6 +8,8 @@
    id?: number;
    code?: string;
    name?: string;
    referenceProductCode?: string;
    referenceProductName?: string;
    description?: string;
    status?: number;
    remark?: string;
@@ -62,6 +64,16 @@
  return requestClient.download('/mes/pro/route/export-excel', { params });
}
/** ä¸‹è½½åŒ…含启用路线参考产品及工序、产出、投料的完整模板 */
export function downloadFullRouteTemplate(routeId: number) {
  return requestClient.download('/mes/pro/route/get-full-import-template', { params: { routeId } });
}
/** æŒ‰è·¯çº¿å¯¼å…¥å®Œæ•´å·¥è‰ºæµç¨‹ */
export function importFullRoute(file: File, updateSupport: boolean) {
  return requestClient.upload(`/mes/pro/route/import-full?updateSupport=${updateSupport}`, { file });
}
/** ä¸‹è½½å·¥è‰ºè·¯çº¿å¯¼å…¥æ¨¡æ¿ */
export function importRouteTemplate() {
  return requestClient.download('/mes/pro/route/get-import-template');
src/api/mes/pro/route/product/index.ts
@@ -6,6 +6,7 @@
    id?: number;
    routeId?: number;
    itemId?: number;
    templateReference?: boolean;
    itemCode?: string;
    itemName?: string;
    warehouseId?: number;
@@ -31,6 +32,11 @@
  );
}
/** è®¾ç½®è·¯çº¿çš„æ¨¡æ¿å‚考产品 */
export function setRouteProductTemplateReference(id: number) {
  return requestClient.put('/mes/pro/route-product/set-template-reference', null, { params: { id } });
}
/** æŸ¥è¯¢å·¥è‰ºè·¯çº¿äº§å“è¯¦æƒ… */
export function getRouteProduct(id: number) {
  return requestClient.get<MesProRouteProductApi.RouteProduct>(
src/api/mes/wm/materialstock/index.ts
@@ -49,6 +49,15 @@
  );
}
/** ä¸‹è½½åº“存增量导入模板 */
export function importMaterialStockTemplate() {
  return requestClient.download('/mes/wm/material-stock/get-import-template');
}
/** å¯¼å…¥åº“存增量 */
export function importMaterialStock(file: File, updateSupport: boolean) {
  return requestClient.upload('/mes/wm/material-stock/import', { file, updateSupport });
}
/** æ›´æ–°åº“存冻结状态 */
export function updateMaterialStockFrozen(data: {
  frozen: boolean;
src/components/import-modal/import-modal.vue
@@ -44,8 +44,13 @@
  fileName: string;
  /** æ‰§è¡Œå¯¼å…¥çš„æŽ¥å£å‡½æ•° (file, updateSupport) => Promise */
  importFn: (file: File, updateSupport: boolean) => Promise<unknown>;
  /** æ˜¯å¦å…è®¸è¦†ç›–已有数据 */
  allowUpdate?: boolean;
  uploadHint?: string;
  /** ä¸æä¾›æ¨¡æ¿ä¸‹è½½æ—¶éšè—å¼¹çª—内的下载按钮。 */
  showTemplateDownload?: boolean;
  /** ä¸‹è½½æ¨¡æ¿çš„æŽ¥å£å‡½æ•° () => Promise(blob) */
  templateFn: () => Promise<BlobPart>;
  templateFn?: () => Promise<BlobPart>;
}
const emit = defineEmits(['success']);
@@ -69,10 +74,11 @@
      label: '数据文件',
      component: 'Upload',
      rules: 'required',
      help: '仅允许导入 xls、xlsx æ ¼å¼æ–‡ä»¶',
      help: () => config.value.uploadHint || '仅允许导入 xls、xlsx æ ¼å¼æ–‡ä»¶',
    },
    {
      fieldName: 'updateSupport',
      dependencies: { triggerFields: ['file'], show: () => config.value.allowUpdate !== false },
      label: '是否覆盖',
      component: 'Switch',
      componentProps: {
@@ -288,6 +294,7 @@
/** ä¸‹è½½æ¨¡ç‰ˆ */
async function handleDownload() {
  if (!config.value.templateFn) return;
  const data = await config.value.templateFn();
  downloadFileFromBlobPart({ fileName: config.value.fileName, source: data });
}
@@ -309,7 +316,7 @@
      </template>
    </Form>
    <template #prepend-footer>
      <div class="flex flex-auto items-center">
      <div v-if="config.showTemplateDownload !== false" class="flex flex-auto items-center">
        <Button @click="handleDownload"> ä¸‹è½½å¯¼å…¥æ¨¡æ¿ </Button>
      </div>
    </template>
src/views/basicData/mdm/data.ts
@@ -93,6 +93,13 @@
      },
    },
    {
      component: 'InputNumber',
      fieldName: 'sort',
      label: '排序',
      defaultValue: 0,
      componentProps: { min: 0, max: 2147483647, precision: 0, placeholder: '数值越小越靠前' },
    },
    {
      component: "Input",
      fieldName: "code",
      label: "物料编码",
@@ -525,6 +532,7 @@
export function useGridColumns(): VxeTableGridOptions["columns"] {
  return [
    { type: "checkbox", width: 40 },
    { field: 'sort', title: '排序', width: 80 },
    {
      field: "code",
      title: "物料编码",
src/views/basicData/mdm/index.vue
@@ -102,6 +102,7 @@
      fileName: '物料导入模板.xls',
      importFn: importItem,
      templateFn: importItemTemplate,
      uploadHint: '红色表头为必填。分类和单位须已存在;新增物料同步 MES,覆盖时保留原同步状态。',
    })
    .open();
}
src/views/crm/saleQuotation/index.vue
@@ -14,11 +14,14 @@
  convertSaleQuotationToContract,
  deleteSaleQuotation,
  getSaleQuotationPage,
  importSaleQuotation,
  importSaleQuotationTemplate,
} from '#/api/crm/saleQuotation';
import { $t } from '#/locales';
import { QUOTATION_STATUS, useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
import ImportModalComponent from '#/components/import-modal';
const { push } = useRouter();
@@ -26,6 +29,12 @@
  connectedComponent: Form,
  destroyOnClose: true,
});
const [ImportModal, importModalApi] = useVbenModal({ connectedComponent: ImportModalComponent, destroyOnClose: true });
function handleImport() {
  importModalApi.setData({ title: '导入报价单', fileName: '销售报价导入模板.xls', importFn: importSaleQuotation, templateFn: importSaleQuotationTemplate, allowUpdate: false, uploadHint: '请下载模板;同一客户和报价名称的多行会合并为一张报价单,仅支持新增。' }).open();
}
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
@@ -38,6 +47,7 @@
}
const toolbarActions: ActionItem[] = [
  { label: '导入报价单', icon: ACTION_ICON.UPLOAD, auth: ['crm:sale-quotation:create'], onClick: handleImport },
  {
    label: $t('ui.actionTitle.create', ['报价单']),
    type: 'primary',
@@ -138,6 +148,7 @@
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <ImportModal @success="handleRefresh" />
    <Grid>
      <template #toolbar-tools>
        <TableAction :actions="toolbarActions" />
src/views/erp/purchase/order/index.vue
@@ -80,6 +80,8 @@
      fileName: '采购订单导入模板.xls',
      importFn: importPurchaseOrder,
      templateFn: importPurchaseOrderTemplate,
      allowUpdate: false,
      uploadHint: '红色表头为必填。同一单号的多行合并为一张订单;正式单号由系统生成,仅支持新增,请勿重复导入。需关联采购申请时,请在已审批申请中生成采购订单。',
    })
    .open();
}
src/views/erp/purchase/request/index.vue
@@ -102,6 +102,8 @@
      fileName: '采购申请导入模板.xls',
      importFn: importPurchaseRequest,
      templateFn: importPurchaseRequestTemplate,
      allowUpdate: false,
      uploadHint: '红色表头为必填。同一单号的多行合并为一张申请;正式单号由系统生成,仅支持新增,请勿重复导入。',
    })
    .open();
}
src/views/erp/sale/order/index.vue
@@ -80,6 +80,8 @@
      fileName: '销售订单导入模板.xls',
      importFn: importSaleOrder,
      templateFn: importSaleOrderTemplate,
      allowUpdate: false,
      uploadHint: '同一单号的多行合并为一张订单,系统生成正式单号。合同单号可选;填写时须为已审批合同且明细一致。仅支持新增,请勿重复导入。',
    })
    .open();
}
src/views/hrm/employee/modules/import-form.vue
@@ -39,7 +39,11 @@
      stopMockProgress();
      progressPercent.value = 100;
      await delay(500);
      message.success('导入完成');
      if (Object.keys(importResult.value?.failureNames ?? {}).length > 0) {
        message.warning('导入完成,部分员工未导入,请查看失败原因');
      } else {
        message.success('导入完成');
      }
      emit('success');
    } finally {
      stopMockProgress();
@@ -113,7 +117,7 @@
        <Switch v-model:checked="updateSupport" />
      </div>
      <div class="mt-2 text-gray-500">
        ä»…允许导入 xls、xlsx æ ¼å¼æ–‡ä»¶ï¼›æ¨¡æ¿ä¸­"部门名称/岗位名称/用户角色"填写名称,角色多个用逗号分隔
        ä»…支持 xls、xlsx;红色表头为必填。部门、岗位和角色名称须唯一;角色多个用逗号分隔。新增时需填写性别,覆盖已有员工时请将初始密码和角色留空,岗位须与原档案一致。
      </div>
      <!-- å¯¼å…¥è¿›åº¦æ¡ -->
      <div v-if="importing" class="mt-4">
src/views/mes/pro/feedback/components/FeedbackParamFields.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,74 @@
<script lang="ts" setup>
import type { MesProFeedbackApi } from '#/api/mes/pro/feedback';
import { computed, ref, watch } from 'vue';
import { message } from 'ant-design-vue';
import { getFeedbackParamConfig } from '#/api/mes/pro/feedback';
const props = defineProps<{ taskId?: number; snapshot?: string; readonly?: boolean }>();
const config = ref<MesProFeedbackApi.ParamConfig>();
const values = ref<Record<number, string>>({});
const loading = ref(false);
const error = ref('');
let loadVersion = 0;
const shared = computed(() => config.value?.mode === 2 && !!config.value?.shared);
const details = computed(() => config.value?.template?.details ?? []);
watch(() => [props.taskId, props.snapshot] as const, async ([taskId, raw]) => {
  const version = ++loadVersion;
  config.value = undefined;
  values.value = {};
  error.value = '';
  if (!taskId) return;
  loading.value = true;
  try {
    const result = await getFeedbackParamConfig(taskId);
    if (version !== loadVersion) return;
    config.value = result;
    let previous: MesProFeedbackApi.ParamSnapshot | undefined;
    if (raw) previous = JSON.parse(raw) as MesProFeedbackApi.ParamSnapshot;
    const existing = props.readonly ? (previous ?? result.shared) : (result.shared ?? previous);
    for (const entry of existing?.values ?? []) values.value[entry.detailId] = entry.value ?? '';
  } catch {
    if (version === loadVersion) error.value = '参数配置加载失败,请重新选择任务';
  } finally {
    if (version === loadVersion) loading.value = false;
  }
}, { immediate: true });
function getValues() {
  if (loading.value || error.value || (props.taskId && !config.value)) {
    message.error(error.value || '参数配置正在加载,请稍后再试');
    throw new Error('参数配置未就绪');
  }
  if (!config.value?.mode || shared.value || props.readonly) return undefined;
  return details.value.map((d) => ({ detailId: d.id, value: values.value[d.id] ?? '' }));
}
defineExpose({ getValues });
</script>
<template>
  <section v-if="taskId && (loading || error || config?.mode)" class="param-fields">
    <h4>工艺参数</h4>
    <p v-if="loading">正在加载参数配置…</p>
    <p v-else-if="error" role="alert">{{ error }}</p>
    <template v-else-if="config?.mode">
      <p>{{ config.template?.templateName }} Â· {{ config.mode === 1 ? '每次报工填写' : '每个任务填写一次' }}</p>
      <p v-if="shared">此任务已填写参数,报工将沿用已保存的值。</p>
      <div v-for="detail in details" :key="detail.id" class="param-fields__item">
        <label :for="`feedback-param-${detail.id}`">{{ detail.paramName }}<span v-if="detail.unitMeasureName">({{ detail.unitMeasureName }})</span><span v-if="detail.required">(必填)</span></label>
        <a-input :id="`feedback-param-${detail.id}`" v-model:value="values[detail.id]"
          :disabled="readonly || shared" :maxlength="100"
          :type="detail.paramType === 4 ? 'time' : detail.paramType === 5 ? 'date' : 'text'"
          :placeholder="`请输入${detail.paramName}`" />
      </div>
    </template>
  </section>
</template>
<style scoped>
.param-fields { margin: 16px; padding: 16px; border: 1px solid var(--ant-color-border, #d9d9d9); border-radius: 6px; }
.param-fields h4 { margin: 0 0 8px; font-weight: 600; }
.param-fields p { margin: 0 0 12px; }
.param-fields__item { display: grid; grid-template-columns: minmax(120px, 1fr) 2fr; align-items: center; gap: 12px; margin-top: 10px; }
@media (max-width: 600px) { .param-fields__item { grid-template-columns: 1fr; gap: 4px; } }
</style>
src/views/mes/pro/feedback/data.ts
@@ -178,6 +178,7 @@
export function useFormSchema(
  formType: FormType,
  formApi?: VbenFormApi,
  onTaskChange?: (taskId?: number) => void,
): VbenFormSchema[] {
  const isHeaderReadonly = ['approve', 'detail', 'submit'].includes(formType);
  return [
@@ -253,6 +254,7 @@
        status: MesProWorkOrderStatusEnum.CONFIRMED,
        // è®¢å•变更:清空任务及任务带出的产品信息、数量区域控制位
        onChange: async () => {
          onTaskChange?.(undefined);
          await formApi?.setValues({
            checkFlag: true,
            itemCode: undefined,
@@ -287,6 +289,7 @@
      // ä»»åŠ¡å˜æ›´ï¼šè‡ªåŠ¨å¡«å……å…³è”å­—æ®µã€äº§å“ä¿¡æ¯ã€checkFlag
      componentProps: {
        onChange: async (task?: MesProTaskApi.Task) => {
          onTaskChange?.(task?.id);
          if (!task) {
            return;
          }
src/views/mes/pro/feedback/modules/batch-feedback.vue
@@ -5,7 +5,7 @@
import { useVbenModal } from '@vben/common-ui';
import { Button, message, Tag } from 'ant-design-vue';
import { Button, message, Modal as AntModal, Tag } from 'ant-design-vue';
import { batchCreateFeedback } from '#/api/mes/pro/feedback';
import ProTaskSelectDialog from '#/views/mes/pro/task/components/select-dialog.vue';
@@ -73,12 +73,18 @@
      remark: formData.value.remark || undefined,
    });
    if (result.failCount) {
      message.warning(
        `批量报工完成:成功 ${result.successCount} ä¸ªï¼Œå¤±è´¥ ${result.failCount} ä¸ª`,
      );
    } else {
      message.success(`批量报工成功,共 ${result.successCount} ä¸ª`);
      const failures = (result.results ?? []).filter((row) => !row.success);
      const names = new Map(selectedTasks.value.map((task) => [task.id, task.code]));
      AntModal.warning({
        title: `成功 ${result.successCount} ä¸ªï¼Œå¤±è´¥ ${result.failCount} ä¸ª`,
        content: failures.map((row) => `${names.get(row.taskId) ?? row.taskId}:${row.errorMsg ?? '报工失败'}`).join(';'),
      });
      const failedIds = new Set(failures.map((row) => row.taskId));
      selectedTasks.value = selectedTasks.value.filter((task) => failedIds.has(task.id!));
      emit('success');
      return;
    }
    message.success(`批量报工成功,共 ${result.successCount} ä¸ª`);
    modalApi.close();
    emit('success');
  } finally {
src/views/mes/pro/feedback/modules/form.vue
@@ -31,6 +31,7 @@
import BatchInputDialog from './batch-input-dialog.vue';
import ItemConsumeList from './item-consume-list.vue';
import ProductProduceList from './product-produce-list.vue';
import FeedbackParamFields from '../components/FeedbackParamFields.vue';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
@@ -39,6 +40,10 @@
const userStore = useUserStore();
const subTabsName = ref('itemConsume');
const originalSnapshot = ref(''); // è¡¨å•原始数据快照,用于 submit æ—¶è·³è¿‡æœªå˜æ›´çš„保存请求
const paramTaskId = ref<number>();
const paramSnapshot = ref<string>();
const paramFields = ref<InstanceType<typeof FeedbackParamFields>>();
function onParamTaskChange(taskId?: number) { paramTaskId.value = taskId; paramSnapshot.value = undefined; }
const isEditable = computed(() =>
  ['create', 'submit', 'update'].includes(formType.value),
@@ -102,9 +107,10 @@
}
/** è¯»å–并按提交规则对齐后的表单数据,用于快照和保存 */
async function getAlignedData(): Promise<MesProFeedbackApi.Feedback> {
async function getAlignedData(includeParams = true): Promise<MesProFeedbackApi.Feedback> {
  const data = (await formApi.getValues()) as MesProFeedbackApi.Feedback;
  alignQuantity(data);
  if (includeParams) data.paramValues = paramFields.value?.getValues();
  return data;
}
@@ -125,7 +131,7 @@
        status: MesProFeedbackStatusEnum.PREPARE,
      };
      formType.value = 'update';
      formApi.setState({ schema: useFormSchema(formType.value, formApi) });
      formApi.setState({ schema: useFormSchema(formType.value, formApi, onParamTaskChange) });
      await formApi.setFieldValue('id', id);
      message.success($t('common.createSuccess'));
    } else {
@@ -253,6 +259,8 @@
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      formData.value = undefined;
      paramTaskId.value = undefined;
      paramSnapshot.value = undefined;
      subTabsName.value = 'itemConsume';
      originalSnapshot.value = '';
      return;
@@ -261,7 +269,7 @@
    const data = modalApi.getData<{ formType: FormType; id?: number; showSubmitButton?: boolean }>();
    formType.value = data.formType;
    showSubmitButton.value = data.showSubmitButton !== false;
    formApi.setState({ schema: useFormSchema(formType.value, formApi) });
    formApi.setState({ schema: useFormSchema(formType.value, formApi, onParamTaskChange) });
    // å®¡æ‰¹/详情整表禁用,避免审核人误改未提交的字段
    formApi.setDisabled(
      formType.value === 'approve' || formType.value === 'detail',
@@ -283,12 +291,14 @@
        feedbackUserId: userStore.userInfo?.id,
      });
      // è®°å½•初始快照(含数量对齐结果),submit æ—¶ç”¨äºŽè·³è¿‡æ— å˜æ›´çš„保存请求
      originalSnapshot.value = JSON.stringify(await getAlignedData());
      originalSnapshot.value = JSON.stringify(await getAlignedData(false));
      return;
    }
    modalApi.lock();
    try {
      formData.value = await getFeedback(data.id);
      paramTaskId.value = formData.value.taskId;
      paramSnapshot.value = formData.value.paramSnapshot;
      const checkFlag = await resolveCheckFlag(
        formData.value.routeId,
        formData.value.processId,
@@ -296,7 +306,7 @@
      // è®¾ç½®åˆ° values
      await formApi.setValues({ ...formData.value, checkFlag });
      // è®°å½•初始快照(含数量对齐结果),submit æ—¶ç”¨äºŽè·³è¿‡æ— å˜æ›´çš„保存请求
      originalSnapshot.value = JSON.stringify(await getAlignedData());
      originalSnapshot.value = JSON.stringify(await getAlignedData(false));
    } finally {
      modalApi.unlock();
    }
@@ -307,6 +317,7 @@
<template>
  <Modal :title="getTitle" class="w-3/5">
    <Form class="mx-4" />
    <FeedbackParamFields ref="paramFields" :task-id="paramTaskId" :snapshot="paramSnapshot" :readonly="!isEditable" />
    <Tabs
      v-if="showSubTabs"
      v-model:active-key="subTabsName"
src/views/mes/pro/process/data.ts
@@ -16,6 +16,7 @@
import { z } from '#/adapter/form';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { getTemplatePage } from '#/api/mes/pro/processdesign/template';
/** è¡¨å•类型 */
export type FormType = 'create' | 'detail' | 'update';
@@ -77,6 +78,31 @@
      rules: z.number().default(CommonStatusEnum.ENABLE),
    },
    {
      fieldName: 'paramCollectMode',
      label: '参数采集方式',
      component: 'Select',
      defaultValue: 0,
      componentProps: {
        options: [
          { label: '不采集', value: 0 },
          { label: '每次报工填写', value: 1 },
          { label: '每个任务填写一次', value: 2 },
        ],
      },
    },
    {
      fieldName: 'paramTemplateId',
      label: '参数模板',
      component: 'ApiSelect',
      componentProps: {
        api: getTemplatePage,
        params: { pageNo: 1, pageSize: 100 },
        resultField: 'list', labelField: 'templateName', valueField: 'id',
        showSearch: true, optionFilterProp: 'label', allowClear: true,
        placeholder: '启用参数采集时请选择模板',
      },
    },
    {
      fieldName: 'attention',
      label: '工序说明',
      component: 'Textarea',
src/views/mes/pro/route/data.ts
@@ -395,6 +395,7 @@
  return [
    { field: 'itemCode', title: '产出编码', width: 150 },
    { field: 'itemName', title: '产出名称', width: 150 },
    { field: 'templateReference', title: '模板引用', width: 100, slots: { default: 'templateReference' } },
    { field: 'specification', title: '规格型号', width: 150 },
    { field: 'unitName', title: '单位', width: 80 },
    { field: 'quantity', title: '生产数量', width: 100 },
src/views/mes/pro/route/modules/product-list.vue
@@ -7,15 +7,17 @@
import { computed, ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { useAccess } from '@vben/access';
import { DICT_TYPE } from '@vben/constants';
import { getDictLabel } from '@vben/hooks';
import { message } from 'ant-design-vue';
import { message, Radio } from 'ant-design-vue';
import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  deleteRouteProduct,
  getRouteProductListByRoute,
  setRouteProductTemplateReference,
} from '#/api/mes/pro/route/product';
import { $t } from '#/locales';
@@ -29,8 +31,11 @@
const emit = defineEmits<{
  change: [list: MesProRouteProductApi.RouteProduct[]];
  referenceChange: [];
}>();
const { hasAccessByCodes } = useAccess();
const canSetTemplateReference = hasAccessByCodes(['mes:pro-route:update']);
const isEditable = computed(() => props.formType !== 'detail'); // æ˜¯å¦å¯ç¼–辑
const list = ref<MesProRouteProductApi.RouteProduct[]>([]); // å·¥è‰ºè·¯çº¿äº§å“åˆ—表
@@ -75,6 +80,15 @@
  productFormModalApi.setData({ id: row.id, routeId: props.routeId }).open();
}
/** æ¯æ¡è·¯çº¿ä½¿ç”¨ä¸€ä¸ªäº§å‡ºäº§å“ä½œä¸ºå¯¼å…¥æ¨¡æ¿å‚考。 */
async function handleTemplateReference(row: MesProRouteProductApi.RouteProduct) {
  if (!canSetTemplateReference || !row.id || row.templateReference) return;
  await setRouteProductTemplateReference(row.id);
  message.success('已设置模板引用产品');
  await getList();
  emit('referenceChange');
}
/** åˆ é™¤å·¥è‰ºè·¯çº¿äº§å“ */
async function handleDelete(row: MesProRouteProductApi.RouteProduct) {
  await deleteRouteProduct(row.id!);
@@ -107,6 +121,11 @@
    />
  </div>
  <Grid class="w-full" table-title="产出产品及报工入库位置">
    <template #templateReference="{ row }">
      <Radio :checked="!!row.templateReference" :disabled="!canSetTemplateReference" @change="handleTemplateReference(row)">
        {{ row.templateReference ? '已选' : '选择' }}
      </Radio>
    </template>
    <template #productionTime="{ row }">
      <span v-if="row.productionTime">
        {{ row.productionTime }}
src/views/mes/pro/route/modules/route-configurator.vue
@@ -296,7 +296,7 @@
          <Alert
            class="mb-4"
            message="先维护所有可能的工序产出"
            description="实际形成的中间半成品和末道工序的成品都要加入,并分别指定报工后进入的仓库、库区和库位。"
            description="实际形成的中间半成品和末道工序的成品都要加入,并指定入库仓位;选择一个成品作为模板引用,供工艺流程模板下载使用。"
            show-icon
            type="info"
          />
@@ -304,6 +304,7 @@
            :form-type="formType"
            :route-id="routeId"
            @change="productList = $event"
            @reference-change="emit('success')"
          />
        </section>
src/views/mes/pro/workorder-center/modules/quick-feedback.vue
@@ -10,6 +10,7 @@
import { quickCreateFeedback } from '#/api/mes/pro/feedback';
import ProTaskSelect from '#/views/mes/pro/task/components/select.vue';
import FeedbackParamFields from '#/views/mes/pro/feedback/components/FeedbackParamFields.vue';
defineOptions({ name: 'QuickFeedbackForm' });
@@ -44,6 +45,7 @@
});
const formRef = ref<FormInstance>();
const paramFields = ref<InstanceType<typeof FeedbackParamFields>>();
const selectedTaskId = ref<number | undefined>(props.taskId);
const selectedTask = ref<MesProTaskApi.Task>();
const formData = ref({
@@ -86,6 +88,7 @@
  try {
    await quickCreateFeedback({
      taskId,
      paramValues: paramFields.value?.getValues(),
      feedbackQuantity: formData.value.feedbackQuantity,
      qualifiedQuantity: formData.value.qualifiedQuantity,
      unqualifiedQuantity: formData.value.unqualifiedQuantity,
@@ -146,6 +149,7 @@
          class="!w-full"
        />
      </a-form-item>
      <FeedbackParamFields ref="paramFields" :task-id="selectedTaskId" />
      <a-form-item label="备注">
        <a-textarea
          v-model:value="formData.remark"
src/views/mes/process-design/process/data.ts
@@ -16,6 +16,7 @@
import { z } from '#/adapter/form';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { getTemplatePage } from '#/api/mes/pro/processdesign/template';
/** è¡¨å•类型 */
export type FormType = 'create' | 'detail' | 'update';
@@ -85,6 +86,35 @@
        options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
      },
      rules: z.number().default(CommonStatusEnum.ENABLE),
    },
    {
      fieldName: 'paramCollectMode',
      label: '参数采集方式',
      component: 'Select',
      defaultValue: 0,
      componentProps: {
        options: [
          { label: '不采集', value: 0 },
          { label: '每次报工填写', value: 1 },
          { label: '每个任务填写一次', value: 2 },
        ],
      },
    },
    {
      fieldName: 'paramTemplateId',
      label: '参数模板',
      component: 'ApiSelect',
      componentProps: {
        api: getTemplatePage,
        params: { pageNo: 1, pageSize: 100 },
        resultField: 'list',
        labelField: 'templateName',
        valueField: 'id',
        showSearch: true,
        optionFilterProp: 'label',
        allowClear: true,
        placeholder: '启用参数采集时请选择模板',
      },
    },
    {
      fieldName: 'attention',
@@ -175,6 +205,13 @@
        props: { type: DICT_TYPE.COMMON_STATUS },
      },
    },
    {
      field: 'paramCollectMode',
      title: '参数采集方式',
      minWidth: 150,
      formatter: ({ cellValue }) =>
        (['不采集', '每次报工填写', '每个任务填写一次'])[Number(cellValue ?? 0)] ?? '未知',
    },
    { field: 'remark', title: '备注', minWidth: 180 },
    {
      field: 'createTime',
src/views/mes/process-design/process/modules/form.vue
@@ -61,6 +61,11 @@
    modalApi.lock();
    // æäº¤è¡¨å•
    const data = (await formApi.getValues()) as MesProProcessApi.Process;
    if ((data.paramCollectMode ?? 0) !== 0 && !data.paramTemplateId) {
      message.warning('启用参数采集时,请选择参数模板');
      modalApi.unlock();
      return;
    }
    try {
      await (formData.value?.id ? updateProcess(data) : createProcess(data));
      // å…³é—­å¹¶æç¤º
src/views/mes/process-design/route/data.ts
@@ -133,6 +133,7 @@
      slots: { default: "code" },
    },
    { field: "name", title: "路线名称", minWidth: 180 },
    { field: "referenceProductName", title: "启用产品", minWidth: 160 },
    { field: "description", title: "路线说明", minWidth: 200 },
    {
      field: "status",
@@ -158,7 +159,7 @@
    },
    {
      title: "操作",
      width: 160,
      width: 260,
      fixed: "right",
      slots: { default: "actions" },
    },
src/views/mes/process-design/route/index.vue
@@ -13,10 +13,10 @@
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  deleteRoute,
  downloadFullRouteTemplate,
  importFullRoute,
  exportRoute,
  getRoutePage,
  importRoute,
  importRouteTemplate,
  updateRouteStatus,
} from '#/api/mes/pro/route';
import { $t } from '#/locales';
@@ -50,11 +50,23 @@
  importModalApi
    .setData({
      title: $t('ui.actionTitle.import', ['工艺路线']),
      fileName: '工艺路线导入模板.xls',
      importFn: importRoute,
      templateFn: importRouteTemplate,
      fileName: '工艺流程完整导入模板.xlsx',
      importFn: importFullRoute,
      showTemplateDownload: false,
      uploadHint: '请先从路线操作列下载模板。表格分为基本信息、产品与仓位、工序、工序产出、工序投料;复制产品时只需修改模板产品行的产品编码,“本产品”会自动关联;名称仅供参考,导入以编码为准。更新路线请先停用并勾选覆盖;未列出的数据不会删除。',
    })
    .open();
}
/** ä¸‹è½½æŒ‡å®šè·¯çº¿çš„æ¨¡æ¿å¼•用产品及完整工艺流程。 */
async function handleDownloadTemplate(row: MesProRouteApi.Route) {
  if (!row.id) return;
  if (!row.referenceProductCode) {
    message.warning('请先在路线的产出产品中选择模板引用');
    return;
  }
  const data = await downloadFullRouteTemplate(row.id);
  downloadFileFromBlobPart({ fileName: `工艺流程模板-${row.code || row.id}.xlsx`, source: data });
}
/** åˆ›å»ºå·¥è‰ºè·¯çº¿ */
@@ -146,7 +158,7 @@
    <Alert
      class="mb-3"
      message="路线配置顺序"
      description="基本信息 â†’ äº§å‡ºäº§å“åŠå…¥åº“仓 â†’ å·¥åºåŠå„工序产出 â†’ å·¥åºæŠ•æ–™ â†’ æ ¡éªŒå¯ç”¨"
      description="先在产出产品中选择模板引用;在目标路线的操作列下载该产品的工序、产出和投料,路线启用或停用均可下载。"
      show-icon
      type="info"
    />
@@ -182,6 +194,11 @@
        <Button type="link" @click="handleDetail(row)">{{ row.code }}</Button>
      </template>
      <template #actions="{ row }">
        <Button
          v-if="row.referenceProductCode && hasAccessByCodes(['mes:pro-route:import'])"
          type="link"
          @click="handleDownloadTemplate(row)"
        >下载模板</Button>
        <Tooltip
          :open="row.status === CommonStatusEnum.DISABLE ? false : undefined"
          title="仅停用状态,才可以操作"
src/views/mes/workbench/components/tabs/FeedbackTab.vue
@@ -29,6 +29,7 @@
import { $t } from '#/locales';
import { useFormSchema } from '../../../pro/feedback/data';
import FeedbackParamFields from '../../../pro/feedback/components/FeedbackParamFields.vue';
defineOptions({ name: 'WorkbenchFeedbackTab' });
@@ -47,6 +48,9 @@
const feedbackId = ref<number>();
const feedbackStatus = ref<number>();
const originalSnapshot = ref('');
const paramFields = ref<InstanceType<typeof FeedbackParamFields>>();
const paramSnapshot = ref<string>();
const paramRenderKey = ref(0);
const isEditable = computed(() =>
  ['create', 'submit', 'update'].includes(formType.value),
@@ -86,13 +90,14 @@
  }
}
async function getAlignedData() {
async function getAlignedData(includeParams = true) {
  const data = (await formApi.getValues()) as any;
  // æ–°å»ºæ—¶ä¸èƒ½æºå¸¦ä¸Šä¸€å¼ æŠ¥å·¥å•残留的 id,否则后端按主键插入会冲突
  if (formType.value === 'create') {
    delete data.id;
  }
  alignQuantity(data);
  if (includeParams) data.paramValues = paramFields.value?.getValues();
  return data;
}
@@ -113,6 +118,8 @@
  feedbackId.value = undefined;
  feedbackStatus.value = undefined;
  originalSnapshot.value = '';
  paramSnapshot.value = undefined;
  paramRenderKey.value++;
  formApi.setState({ schema: useFormSchema(formType.value, formApi) });
@@ -139,7 +146,7 @@
    checkFlag,
  });
  originalSnapshot.value = JSON.stringify(await getAlignedData());
  originalSnapshot.value = JSON.stringify(await getAlignedData(false));
}
/** ä¿å­˜ */
@@ -274,12 +281,13 @@
async function loadFeedback(id: number) {
  const data = await getFeedback(id);
  feedbackId.value = data.id;
  paramSnapshot.value = data.paramSnapshot;
  feedbackStatus.value = data.status;
  formType.value = 'update';
  formApi.setState({ schema: useFormSchema(formType.value, formApi) });
  const checkFlag = await resolveCheckFlag(data.routeId, data.processId);
  await formApi.setValues({ ...data, checkFlag });
  originalSnapshot.value = JSON.stringify(await getAlignedData());
  originalSnapshot.value = JSON.stringify(await getAlignedData(false));
}
// ç›‘听任务变更(仅当任务 ID çœŸæ­£å˜åŒ–时才重置表单)
@@ -313,6 +321,7 @@
      </div>
      <Form class="feedback-tab__form" />
      <FeedbackParamFields :key="paramRenderKey" ref="paramFields" :task-id="task.id" :snapshot="paramSnapshot" :readonly="!isEditable" />
      <div class="feedback-tab__actions">
        <Button
src/views/wls/materialstock/index.vue
@@ -13,6 +13,8 @@
import {
  exportMaterialStock,
  getMaterialStockPage,
  importMaterialStock,
  importMaterialStockTemplate,
  updateMaterialStockFrozen,
} from '#/api/mes/wm/materialstock';
import { $t } from '#/locales';
@@ -20,6 +22,7 @@
import { WmBatchDetail } from '#/views/wls/batch/components';
import AreaForm from '#/views/wls/warehouse/area/modules/form.vue';
import { WmStockAdjustModal, WmStockMoveModal } from './components';
import ImportModalComponent from '#/components/import-modal';
import { useGridColumns, useGridFormSchema } from './data';
@@ -38,6 +41,21 @@
  destroyOnClose: true,
});
const [StockImportModal, stockImportModalApi] = useVbenModal({
  connectedComponent: ImportModalComponent,
  destroyOnClose: true,
});
function handleImportStock() {
  stockImportModalApi.setData({
    title: '导入库存增量',
    fileName: '库存导入模板.xls',
    importFn: importMaterialStock,
    templateFn: importMaterialStockTemplate,
    allowUpdate: false,
    uploadHint: '数量按物料主单位累加;每行使用唯一导入明细编号,重复编号不会再次入账。批次须预先建立且合格;相同库存维度存在多个供应商时须填写供应商编码。序列号物料不可批量导入。',
  }).open();
}
const batchDetailRef = ref<InstanceType<typeof WmBatchDetail>>();
/** åˆ·æ–°è¡¨æ ¼ */
@@ -154,6 +172,7 @@
  <Page auto-content-height><AreaModal />
    <WmBatchDetail ref="batchDetailRef" />
    <StockAdjustModal @success="handleStockAdjustSuccess" />
    <StockImportModal @success="handleRefresh" />
    <StockMoveModal @success="handleStockMoveSuccess" />
    <div class="flex h-full w-full">
@@ -171,6 +190,13 @@
            <TableAction
              :actions="[
                {
                  label: '导入库存',
                  type: 'default',
                  icon: ACTION_ICON.UPLOAD,
                  auth: ['mes:wm-material-stock:adjust'],
                  onClick: handleImportStock,
                },
                {
                  label: '库存调整',
                  type: 'default',
                  icon: ACTION_ICON.EDIT,
src/views/wls/transaction/data.ts
@@ -13,6 +13,7 @@
/** ä¸šåŠ¡ç±»åž‹é€‰é¡¹ */
const bizTypeOptions = [
  { label: '库存导入入库', value: 128 },
  { label: '自定义入库', value: 126 },
  { label: '自定义出库', value: 127 },
  { label: '采购入库', value: 110 },