2026-08-05 99c00652cf3895009eacdb05025e02967a670367
feat(mes): 实现生产报工批次属性管理和SRM投标优化功能

- 新增仓库选择组件,实现仓库列表缓存和自动填充仓库编码
- 添加商机价格支持,动态切换产品编辑表格价格列显示
- 优化SRM投标详情页面,实现供应商和投标记录联动筛选
- 实现生产报工批次属性管理,新增批次输入弹窗和验证机制
- 完善报工审批流程,集成批次需求检查和属性收集
- 移除物料分类选择组件中的禁用标记逻辑
- 添加采购申请审批详情查看验证,防止未提交申请被访问
- 新增仓库简单列表API接口和数据模型定义
已修改12个文件
已添加2个文件
535 ■■■■ 文件已修改
src/api/crm/business/index.ts 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mdm/warehouse/index.ts 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/feedback/index.ts 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/mdm/data.ts 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/product/components/data.ts 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/product/components/edit-table.vue 29 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/request/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/md/item/type/components/select.vue 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/feedback/index.vue 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/feedback/modules/batch-input-dialog.vue 177 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/feedback/modules/form.vue 46 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/workbench/components/tabs/FeedbackHistoryTab.vue 40 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/workbench/components/tabs/FeedbackTab.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/tender/detail.vue 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/crm/business/index.ts
@@ -38,11 +38,14 @@
  /** å•†æœºäº§å“ä¿¡æ¯ */
  export interface BusinessProduct {
    id: number;
    productId: number;
    productName: string;
    productNo: string;
    productUnit: number;
    productPrice: number;
    itemId: number;
    itemName: string;
    itemCode: string;
    barCode: string;
    itemSpecification: string;
    itemUnitId: number;
    itemUnitName: string;
    itemPrice: number;
    businessPrice: number;
    count: number;
    totalPrice: number;
src/api/mdm/warehouse/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
import { requestClient } from '#/api/request';
export namespace MdmWarehouseApi {
  export interface Warehouse {
    id?: number;
    code?: string;
    name?: string;
    address?: string;
    type?: number;
    status?: number;
  }
}
/** æŸ¥è¯¢å…¨éƒ¨å¯ç”¨çš„仓库列表 */
export function getWarehouseSimpleList() {
  return requestClient.get<MdmWarehouseApi.Warehouse[]>(
    '/mdm/warehouse/list-all-simple',
  );
}
src/api/mes/pro/feedback/index.ts
@@ -110,8 +110,37 @@
}
/** å®¡æ‰¹ç”Ÿäº§æŠ¥å·¥ï¼ˆè¿”回是否已审批完成) */
export function approveFeedback(id: number) {
  return requestClient.put<boolean>(`/mes/pro/feedback/approve?id=${id}`);
export function approveFeedback(data: { id: number; [key: string]: unknown }) {
  return requestClient.put<boolean>(`/mes/pro/feedback/approve`, data);
}
/** æ‰¹æ¬¡éœ€æ±‚检查响应 */
export interface BatchCheckResp {
  needBatchInput?: boolean;
  isBatchManaged?: boolean;
  produceDateFlag?: boolean;
  expireDateFlag?: boolean;
  receiptDateFlag?: boolean;
  vendorFlag?: boolean;
  clientFlag?: boolean;
  salesOrderCodeFlag?: boolean;
  purchaseOrderCodeFlag?: boolean;
  workOrderFlag?: boolean;
  taskFlag?: boolean;
  workstationFlag?: boolean;
  toolFlag?: boolean;
  moldFlag?: boolean;
  lotNumberFlag?: boolean;
  qualityStatusFlag?: boolean;
  autoFilledFields?: string[];
}
/** æ£€æŸ¥æŠ¥å·¥å®¡æ‰¹å‰æ˜¯å¦éœ€è¦å¡«å†™æ‰¹æ¬¡å±žæ€§ */
export function getBatchRequirements(feedbackId: number) {
  return requestClient.get<BatchCheckResp>(
    `/mes/pro/feedback/batch-requirements`,
    { params: { id: feedbackId } },
  );
}
/** å¿«é€ŸæŠ¥å·¥è¯·æ±‚ */
src/views/basicData/mdm/data.ts
@@ -11,12 +11,17 @@
import { MesAutoCodeRuleCode } from '@vben/constants';
import { getItemTypeSimpleList } from '#/api/mes/md/item/type';
import { getUnitPage } from '#/api/mdm/unit';
import { getWarehouseSimpleList } from '#/api/mes/wm/warehouse';
import { Button } from 'ant-design-vue';
// ç¼“存单位列表
let unitListCache: any[] | null = null;
let unitListPromise: Promise<any[]> | null = null;
// ç¼“存仓库列表
let warehouseListCache: any[] | null = null;
let warehouseListPromise: Promise<any[]> | null = null;
/** å°†æ‰å¹³åˆ†ç±»åˆ—表转为 TreeSelect æ‰€éœ€çš„æ ‘形结构 */
export function buildCategoryTree(list: any[]): any[] {
@@ -53,9 +58,27 @@
  return unitListPromise;
}
/** èŽ·å–ä»“åº“åˆ—è¡¨ï¼ˆå¸¦ç¼“å­˜ï¼‰ */
async function getWarehouseListCached() {
  if (warehouseListCache) {
    return warehouseListCache;
  }
  if (warehouseListPromise) {
    return warehouseListPromise;
  }
  warehouseListPromise = getWarehouseSimpleList().then((res) => {
    warehouseListCache = res || [];
    return warehouseListCache;
  }).finally(() => {
    warehouseListPromise = null;
  });
  return warehouseListPromise;
}
/** æ¸…除单位列表缓存 */
export function clearUnitListCache() {
  unitListCache = null;
  warehouseListCache = null;
}
/** æ–°å¢ž/修改的表单 */
@@ -188,11 +211,29 @@
      },
    },
    {
      component: 'Input',
      component: 'ApiSelect',
      fieldName: 'warehouseId',
      label: '默认仓库',
      componentProps: {
        placeholder: '请输入默认仓库ID',
        placeholder: '请选择默认仓库',
        allowClear: true,
        api: getWarehouseListCached,
        labelField: 'name',
        valueField: 'id',
        onChange: async (value: number | undefined) => {
          const code = value
            ? ((await getWarehouseListCached()).find((w: any) => w.id === value)?.code || '')
            : '';
          formApi?.setFieldValue('warehouseCode', code);
        },
      },
    },
    {
      component: 'Input',
      fieldName: 'warehouseCode',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
src/views/crm/product/components/data.ts
@@ -14,7 +14,7 @@
      title: '物料编码',
    },
    {
      field: 'itemBarCode',
      field: 'barCode',
      title: '物料条码',
    },
    {
@@ -52,7 +52,22 @@
}
/** äº§å“ç¼–辑表格的列定义 */
export function useProductEditTableColumns(): VxeTableGridOptions['columns'] {
export function useProductEditTableColumns(
  showBusinessPrice: boolean = false,
): VxeTableGridOptions['columns'] {
  const priceColumn = showBusinessPrice
    ? {
        field: 'businessPrice',
        title: '商机价格(元)',
        minWidth: 100,
        slots: { default: 'businessPrice' },
      }
    : {
        field: 'contractPrice',
        title: '合同价(元)',
        minWidth: 100,
        slots: { default: 'contractPrice' },
      };
  return [
    { type: 'seq', title: '序号', minWidth: 50 },
    {
@@ -87,12 +102,7 @@
      minWidth: 100,
      formatter: 'formatAmount2',
    },
    {
      field: 'contractPrice',
      title: '合同价(元)',
      minWidth: 100,
      slots: { default: 'contractPrice' },
    },
    priceColumn,
    {
      field: 'count',
      title: '数量',
src/views/crm/product/components/edit-table.vue
@@ -3,7 +3,7 @@
import type { CrmContractApi } from '#/api/crm/contract';
import type { MdmItemApi } from '#/api/mdm/item';
import { nextTick, ref, watch } from 'vue';
import { computed, nextTick, ref, watch } from 'vue';
import { erpPriceMultiply } from '@vben/utils';
@@ -24,6 +24,12 @@
}>();
const emit = defineEmits(['update:products']);
/** æ˜¯å¦ä¸ºå•†æœºç±»åž‹ */
const isBusiness = computed(() => props.bizType === BizTypeEnum.CRM_BUSINESS);
/** å½“前业务类型对应的价格字段名 */
const priceField = computed(() => isBusiness.value ? 'businessPrice' : 'contractPrice');
/** è¡¨æ ¼å†…部数据 */
const tableData = ref<any[]>([]);
@@ -46,28 +52,29 @@
  row.itemId = product.id;
  row.itemName = product.name;
  row.itemCode = product.code;
  row.itemBarCode = product.barCode;
  row.barCode = product.barCode;
  row.itemUnitId = product.unitMeasureId;
  row.itemUnitName = product.unitMeasureName;
  row.itemUnitName2 = product.unitMeasureName2;
  row.itemUnitName3 = product.unitMeasureName3;
  row.itemSpecification = product.specification;
  row.itemPrice = product.salesPrice;
  row.contractPrice = product.salesPrice;
  row[priceField.value] = product.salesPrice;
  row.count = row.count || 1;
  row.totalPrice = erpPriceMultiply(row.contractPrice, row.count) ?? 0;
  row.totalPrice = erpPriceMultiply(row[priceField.value], row.count) ?? 0;
  handleUpdateValue(row);
}
/** é‡‘额变动时重新计算合计 */
function handlePriceChange(row: any) {
  row.totalPrice = erpPriceMultiply(row.contractPrice, row.count) ?? 0;
  row.totalPrice = erpPriceMultiply(row[priceField.value], row.count) ?? 0;
  handleUpdateValue(row);
}
/** å°†æœ€æ–°æ•°æ®å†™å›žå¹¶é€šçŸ¥çˆ¶ç»„ä»¶ */
function handleUpdateValue(row: any) {
  const index = tableData.value.findIndex((item) => item.id === row.id);
  row.totalPrice = erpPriceMultiply(row.contractPrice, row.count) ?? 0;
  row.totalPrice = erpPriceMultiply(row[priceField.value], row.count) ?? 0;
  if (index === -1) {
    row.id = tableData.value.length + 1;
    tableData.value.push(row);
@@ -83,7 +90,7 @@
      trigger: 'click',
      mode: 'cell',
    },
    columns: useProductEditTableColumns(),
    columns: useProductEditTableColumns(isBusiness.value),
    data: tableData.value,
    border: true,
    showOverflow: true,
@@ -137,6 +144,14 @@
        @change="handlePriceChange(row)"
      />
    </template>
    <template #businessPrice="{ row }">
      <InputNumber
        v-model:value="row.businessPrice"
        :min="0.001"
        :precision="2"
        @change="handlePriceChange(row)"
      />
    </template>
    <template #count="{ row }">
      <InputNumber
        v-model:value="row.count"
src/views/erp/purchase/request/index.vue
@@ -122,6 +122,10 @@
/** æŸ¥çœ‹å®¡æ‰¹è¯¦æƒ… */
function handleViewProcess(row: ErpPurchaseRequestApi.PurchaseRequest) {
  if (!row.processInstanceId) {
    message.warning('该申请尚未提交审批');
    return;
  }
  router.push({ name: 'BpmProcessInstanceDetail', query: { id: row.processInstanceId } });
}
src/views/mes/md/item/type/components/select.vue
@@ -48,22 +48,6 @@
  },
});
/** é€’归将有子节点的分支节点标记为 disabled */
function markParentsDisabled(
  nodes: MesMdItemTypeApi.ItemType[],
): ItemTypeNode[] {
  return nodes.map((node) => {
    const children = node.children?.length
      ? markParentsDisabled(node.children)
      : undefined;
    return {
      ...node,
      children,
      disabled: Boolean(children?.length),
    };
  });
}
/** æ ¹æ®å½“前值同步 tooltip å±•示的分类详情 */
function syncSelectedItem(value: number | undefined) {
  selectedItem.value =
@@ -81,7 +65,7 @@
/** æŸ¥è¯¢ç‰©æ–™åˆ†ç±»æ ‘ */
async function getItemTypeTree() {
  allList.value = await getItemTypeSimpleList();
  itemTypeTree.value = markParentsDisabled(handleTree(allList.value));
  itemTypeTree.value = handleTree(allList.value) as ItemTypeNode[];
  syncSelectedItem(props.modelValue);
}
src/views/mes/pro/feedback/index.vue
@@ -22,8 +22,6 @@
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
import QuickFeedbackForm from '../workorder-center/modules/quick-feedback.vue';
import BatchFeedbackForm from './modules/batch-feedback.vue';
import ArchiveDetail from '#/views/mes/pd/archive/modules/detail.vue';
const userStore = useUserStore();
@@ -31,16 +29,6 @@
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
const [QuickFeedbackModal, quickFeedbackModalApi] = useVbenModal({
  connectedComponent: QuickFeedbackForm,
  destroyOnClose: true,
});
const [BatchFeedbackModal, batchFeedbackModalApi] = useVbenModal({
  connectedComponent: BatchFeedbackForm,
  destroyOnClose: true,
});
@@ -108,16 +96,6 @@
  }
}
/** å¿«é€ŸæŠ¥å·¥ */
function handleQuickCreate() {
  quickFeedbackModalApi.setData({}).open();
}
/** æ‰¹é‡æŠ¥å·¥ */
function handleBatchCreate() {
  batchFeedbackModalApi.setData({}).open();
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: { schema: useGridFormSchema() },
  gridOptions: {
@@ -144,8 +122,6 @@
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <QuickFeedbackModal @success="handleRefresh" />
    <BatchFeedbackModal @success="handleRefresh" />
    <Grid table-title="生产报工列表">
      <template #toolbar-tools>
        <TableAction
@@ -156,19 +132,6 @@
              icon: ACTION_ICON.ADD,
              auth: ['mes:pro-feedback:create'],
              onClick: handleCreate,
            },
            {
              label: '快速报工',
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['mes:pro-feedback:create'],
              onClick: handleQuickCreate,
            },
            {
              label: '批量报工',
              type: 'primary',
              auth: ['mes:pro-feedback:create'],
              onClick: handleBatchCreate,
            },
            {
              label: $t('ui.actionTitle.export'),
src/views/mes/pro/feedback/modules/batch-input-dialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,177 @@
<script lang="ts" setup>
import type { BatchCheckResp } from '#/api/mes/pro/feedback';
import { computed, ref } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { useVbenModal } from '@vben/common-ui';
import { useVbenForm } from '#/adapter/form';
const emit = defineEmits<{
  confirm: [data: Record<string, unknown>];
}>();
const checkResult = ref<BatchCheckResp>({});
const batchData = ref<Record<string, unknown>>({});
const skipFieldSet = computed<Set<string>>(() => {
  const autoFields = checkResult.value.autoFilledFields ?? [];
  return new Set([...autoFields, 'workOrderId', 'taskId', 'workstationId', 'produceDate']);
});
const needTool = computed(() => checkResult.value.toolFlag && !skipFieldSet.value.has('toolId'));
const needMold = computed(() => checkResult.value.moldFlag && !skipFieldSet.value.has('moldId'));
const needQualityStatus = computed(() => checkResult.value.qualityStatusFlag && !skipFieldSet.value.has('qualityStatus'));
const needExpireDate = computed(() => checkResult.value.expireDateFlag && !skipFieldSet.value.has('expireDate'));
const needReceiptDate = computed(() => checkResult.value.receiptDateFlag && !skipFieldSet.value.has('receiptDate'));
const needLotNumber = computed(() => checkResult.value.lotNumberFlag && !skipFieldSet.value.has('lotNumber'));
const needVendor = computed(() => checkResult.value.vendorFlag && !skipFieldSet.value.has('vendorId'));
const needClient = computed(() => checkResult.value.clientFlag && !skipFieldSet.value.has('clientId'));
const needSalesOrderCode = computed(() => checkResult.value.salesOrderCodeFlag && !skipFieldSet.value.has('salesOrderCode'));
const needPurchaseOrderCode = computed(() => checkResult.value.purchaseOrderCodeFlag && !skipFieldSet.value.has('purchaseOrderCode'));
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: { class: 'w-full' },
    formItemClass: 'col-span-1',
    labelWidth: 110,
  },
  layout: 'horizontal',
  schema: [],
  showDefaultActions: false,
  wrapperClass: 'grid-cols-2',
});
const [Modal, modalApi] = useVbenModal({
  onConfirm: async () => {
    const { valid } = await formApi.validate();
    if (!valid) return;
    const values = await formApi.getValues();
    emit('confirm', values as Record<string, unknown>);
    await modalApi.close();
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) return;
    const data = modalApi.getData<{ checkResult: BatchCheckResp }>();
    checkResult.value = data.checkResult ?? {};
    batchData.value = {};
    // åŠ¨æ€æž„å»ºè¡¨å• schema
    const schemas: any[] = [];
    if (needExpireDate.value) {
      schemas.push({
        fieldName: 'expireDate',
        label: '有效期',
        component: 'DatePicker',
        componentProps: { class: 'w-full', format: 'YYYY-MM-DD', valueFormat: 'YYYY-MM-DD HH:mm:ss', showTime: true, placeholder: '请选择有效期' },
        rules: 'required',
      });
    }
    if (needReceiptDate.value) {
      schemas.push({
        fieldName: 'receiptDate',
        label: '入库日期',
        component: 'DatePicker',
        componentProps: { class: 'w-full', format: 'YYYY-MM-DD', valueFormat: 'YYYY-MM-DD HH:mm:ss', showTime: true, placeholder: '请选择入库日期' },
        rules: 'required',
      });
    }
    if (needLotNumber.value) {
      schemas.push({
        fieldName: 'lotNumber',
        label: '生产批号',
        component: 'Input',
        componentProps: { placeholder: '请输入生产批号' },
        rules: 'required',
      });
    }
    if (needTool.value) {
      schemas.push({
        fieldName: 'toolId',
        label: '工具',
        component: 'Input',
      });
    }
    if (needMold.value) {
      schemas.push({
        fieldName: 'moldId',
        label: '模具',
        component: 'Input',
      });
    }
    if (needQualityStatus.value) {
      schemas.push({
        fieldName: 'qualityStatus',
        label: '质量状态',
        component: 'Select',
        componentProps: {
          options: getDictOptions(DICT_TYPE.MES_WM_QUALITY_STATUS, 'number'),
          placeholder: '请选择质量状态',
        },
        rules: 'required',
      });
    }
    if (needVendor.value) {
      schemas.push({
        fieldName: 'vendorId',
        label: '供应商',
        component: 'Input',
        componentProps: { placeholder: '请输入供应商ID' },
        rules: 'required',
      });
    }
    if (needClient.value) {
      schemas.push({
        fieldName: 'clientId',
        label: '客户',
        component: 'Input',
        componentProps: { placeholder: '请输入客户ID' },
        rules: 'required',
      });
    }
    if (needSalesOrderCode.value) {
      schemas.push({
        fieldName: 'salesOrderCode',
        label: '销售订单编号',
        component: 'Input',
        componentProps: { placeholder: '请输入销售订单编号' },
        rules: 'required',
      });
    }
    if (needPurchaseOrderCode.value) {
      schemas.push({
        fieldName: 'purchaseOrderCode',
        label: '采购订单编号',
        component: 'Input',
        componentProps: { placeholder: '请输入采购订单编号' },
        rules: 'required',
      });
    }
    formApi.setState({ schema: schemas });
    // é»˜è®¤å€¼
    const defaults: Record<string, unknown> = {};
    if (needExpireDate.value) defaults.expireDate = undefined;
    if (needReceiptDate.value) defaults.receiptDate = undefined;
    await formApi.setValues(defaults);
  },
});
defineExpose({
  open(data: { checkResult: BatchCheckResp }) {
    modalApi.setData(data);
    modalApi.open();
  },
});
</script>
<template>
  <Modal title="填写批次属性" class="w-2/5">
    <div class="mb-3 text-sm text-gray-500">
      è¯¥ç‰©æ–™éœ€è¦æ‰¹æ¬¡ç®¡ç†ï¼Œè¯·å¡«å†™ä»¥ä¸‹å¿…填批次属性后继续审批。
    </div>
    <Form class="mx-4" />
  </Modal>
</template>
src/views/mes/pro/feedback/modules/form.vue
@@ -5,6 +5,8 @@
import { computed, ref } from 'vue';
import dayjs from 'dayjs';
import { useVbenModal } from '@vben/common-ui';
import { MesAutoCodeRuleCode, MesProFeedbackStatusEnum } from '@vben/constants';
import { useUserStore } from '@vben/stores';
@@ -16,6 +18,7 @@
import {
  approveFeedback,
  createFeedback,
  getBatchRequirements,
  getFeedback,
  rejectFeedback,
  submitFeedback,
@@ -25,6 +28,7 @@
import { $t } from '#/locales';
import { useFormSchema } from '../data';
import BatchInputDialog from './batch-input-dialog.vue';
import ItemConsumeList from './item-consume-list.vue';
import ProductProduceList from './product-produce-list.vue';
@@ -161,14 +165,17 @@
  }
}
/** å®¡æ‰¹é€šè¿‡ */
async function handleApprove() {
  if (!formData.value?.id) {
    return;
  }
const batchDialogRef = ref<InstanceType<typeof BatchInputDialog>>();
/** æ‰§è¡Œå®¡æ‰¹ï¼ˆå«æ‰¹æ¬¡å±žæ€§ï¼‰ */
async function doApprove(batchFields?: Record<string, unknown>) {
  if (!formData.value?.id) return;
  modalApi.lock();
  try {
    const finished = await approveFeedback(formData.value.id);
    const finished = await approveFeedback({
      id: formData.value.id,
      ...batchFields,
    });
    await modalApi.close();
    emit('success');
    message.success(
@@ -177,6 +184,30 @@
  } finally {
    modalApi.unlock();
  }
}
/** å®¡æ‰¹é€šè¿‡ */
async function handleApprove() {
  if (!formData.value?.id) {
    return;
  }
  try {
    const checkResult = await getBatchRequirements(formData.value.id);
    if (checkResult?.needBatchInput) {
      // éœ€è¦å¡«å†™æ‰¹æ¬¡å±žæ€§ï¼Œå¼¹çª—收集
      batchDialogRef.value?.open({ checkResult });
    } else {
      await doApprove();
    }
  } catch {
    // æ£€æŸ¥å¤±è´¥æ—¶ç›´æŽ¥å®¡æ‰¹ï¼ˆå…¼å®¹æ—§é€»è¾‘)
    await doApprove();
  }
}
/** æ‰¹æ¬¡å¼¹çª—确认回调 */
function handleBatchConfirm(batchData: Record<string, unknown>) {
  doApprove(batchData);
}
/** å®¡æ‰¹ä¸é€šè¿‡ */
@@ -248,7 +279,7 @@
      );
      await formApi.setValues({
        code,
        feedbackTime: Date.now(),
        feedbackTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
        feedbackUserId: userStore.userInfo?.id,
      });
      // è®°å½•初始快照(含数量对齐结果),submit æ—¶ç”¨äºŽè·³è¿‡æ— å˜æ›´çš„保存请求
@@ -314,5 +345,6 @@
        </Popconfirm>
      </div>
    </template>
    <BatchInputDialog ref="batchDialogRef" @confirm="handleBatchConfirm" />
  </Modal>
</template>
src/views/mes/workbench/components/tabs/FeedbackHistoryTab.vue
@@ -16,6 +16,7 @@
import {
  approveFeedback,
  deleteFeedback,
  getBatchRequirements,
  getFeedbackPage,
  rejectFeedback,
  submitFeedback,
@@ -23,6 +24,7 @@
import { $t } from '#/locales';
import FeedbackForm from '../../../pro/feedback/modules/form.vue';
import BatchInputDialog from '../../../pro/feedback/modules/batch-input-dialog.vue';
defineOptions({ name: 'WorkbenchFeedbackHistoryTab' });
@@ -38,6 +40,19 @@
  connectedComponent: FeedbackForm,
  destroyOnClose: true,
});
const batchDialogRef = ref<InstanceType<typeof BatchInputDialog>>();
const approvingRow = ref<MesProFeedbackApi.Feedback>();
/** æ‰§è¡Œå®¡æ‰¹ï¼ˆå«æ‰¹æ¬¡å±žæ€§ï¼‰ */
async function doApprove(row: MesProFeedbackApi.Feedback, batchFields?: Record<string, unknown>) {
  const finished = await approveFeedback({ id: row.id!, ...batchFields });
  message.success(
    finished ? '报工单已审批完成' : '报工成功,请等待质量检验完成!',
  );
  refreshGrid();
  emit('reported');
}
/** æŸ¥çœ‹è¯¦æƒ… */
function handleDetail(row: MesProFeedbackApi.Feedback) {
@@ -67,12 +82,24 @@
/** å®¡æ‰¹é€šè¿‡ï¼ˆä»…审批中) */
async function handleApprove(row: MesProFeedbackApi.Feedback) {
  const finished = await approveFeedback(row.id!);
  message.success(
    finished ? '报工单已审批完成' : '报工成功,请等待质量检验完成!',
  );
  refreshGrid();
  emit('reported');
  try {
    const checkResult = await getBatchRequirements(row.id!);
    if (checkResult?.needBatchInput) {
      approvingRow.value = row;
      batchDialogRef.value?.open({ checkResult });
    } else {
      await doApprove(row);
    }
  } catch {
    await doApprove(row);
  }
}
/** æ‰¹æ¬¡å¼¹çª—确认回调 */
function handleBatchConfirm(batchData: Record<string, unknown>) {
  if (approvingRow.value) {
    doApprove(approvingRow.value, batchData);
  }
}
/** é©³å›žï¼ˆä»…审批中) */
@@ -202,6 +229,7 @@
<template>
  <div class="feedback-history-tab">
    <FormModal @success="refreshGrid" />
    <BatchInputDialog ref="batchDialogRef" @confirm="handleBatchConfirm" />
    <div v-if="!taskId" class="feedback-history-tab__empty">
      è¯·ä»Žå·¦ä¾§å·¥å•列表选择一条任务查看报工记录
src/views/mes/workbench/components/tabs/FeedbackTab.vue
@@ -4,6 +4,8 @@
import { computed, ref, watch } from 'vue';
import dayjs from 'dayjs';
import {
  MesAutoCodeRuleCode,
  MesProFeedbackStatusEnum,
@@ -116,7 +118,7 @@
  await formApi.setValues({
    code,
    type: 1,
    feedbackTime: Date.now(),
    feedbackTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
    feedbackUserId: userStore.userInfo?.id,
    taskId: task.id,
    workOrderId: task.workOrderId,
@@ -242,7 +244,7 @@
/** å®¡æ‰¹é€šè¿‡ */
async function handleApprove() {
  if (!feedbackId.value) return;
  const finished = await approveFeedback(feedbackId.value);
  const finished = await approveFeedback({ id: feedbackId.value });
  message.success(
    finished ? '报订单已审批完成' : '报工成功,请等待质量检验完成!',
  );
src/views/srm/tender/detail.vue
@@ -253,6 +253,40 @@
const awardFormVisible = ref(false);
const awardForm = ref<SrmTenderApi.BidAwardVO>({ tenderProjectId: projectId, supplierId: undefined!, bidId: undefined!, awardNo: '' });
/** å·²æŠ•标且未撤标的供应商列表(从投标记录中提取) */
const bidSuppliers = computed(() => {
  const activeBids = bidList.value.filter((b) => b.bidStatus !== BID_STATUS.WITHDRAWN);
  const seen = new Map<number, { id: number; name: string }>();
  activeBids.forEach((b) => {
    if (b.supplierId && !seen.has(b.supplierId)) {
      seen.set(b.supplierId, { id: b.supplierId, name: b.supplierName || '' });
    }
  });
  return [...seen.values()];
});
/** å¯é€‰æŠ•标记录:排除撤标,且如果选了供应商则只显示该供应商的投标 */
const activeBids = computed(() => {
  return bidList.value.filter((b) => {
    if (b.bidStatus === BID_STATUS.WITHDRAWN) return false;
    if (awardForm.value.supplierId && b.supplierId !== awardForm.value.supplierId) return false;
    return true;
  });
});
/** é€‰æ‹©ä¾›åº”商时清空已选的投标 */
function onAwardSupplierChange() {
  awardForm.value.bidId = undefined!;
}
/** é€‰æ‹©æŠ•标时自动带入该投标的报价金额 */
function onAwardBidChange(bidId: number) {
  const bid = bidList.value.find((b) => b.id === bidId);
  if (bid?.bidTotalAmount != null) {
    awardForm.value.awardAmount = bid.bidTotalAmount;
  }
}
async function loadAward() {
  try { award.value = await getAwardByProject(projectId); } catch { award.value = undefined; }
}
@@ -859,15 +893,15 @@
          <a-row :gutter="12">
            <a-col :span="12">
              <a-form-item label="供应商" required>
                <a-select v-model:value="awardForm.supplierId" show-search placeholder="选择供应商" :filter-option="filterOption">
                  <a-select-option v-for="s in supplierList" :key="s.id" :value="s.id" :label="s.name">{{ s.name }}</a-select-option>
                <a-select v-model:value="awardForm.supplierId" show-search placeholder="选择供应商" :filter-option="filterOption" @change="onAwardSupplierChange">
                  <a-select-option v-for="s in bidSuppliers" :key="s.id" :value="s.id" :label="s.name">{{ s.name }}</a-select-option>
                </a-select>
              </a-form-item>
            </a-col>
            <a-col :span="12">
              <a-form-item label="投标" required>
                <a-select v-model:value="awardForm.bidId" show-search placeholder="选择投标" :filter-option="filterOption">
                  <a-select-option v-for="b in bidList" :key="b.id" :value="b.id" :label="`${b.bidNo} - ${b.supplierName}`">{{ b.bidNo }} - {{ b.supplierName }}</a-select-option>
                <a-select v-model:value="awardForm.bidId" show-search placeholder="选择投标" :filter-option="filterOption" :disabled="!awardForm.supplierId" @change="onAwardBidChange">
                  <a-select-option v-for="b in activeBids" :key="b.id" :value="b.id" :label="`${b.bidNo} - ${b.supplierName}`">{{ b.bidNo }} - {{ b.supplierName }}</a-select-option>
                </a-select>
              </a-form-item>
            </a-col>