gaoluyang
2026-08-07 a3057955f855b8a266c017df00645845e7471410
银川
1.不合格品处理页面开发联调
2.招投标管理 - 可配置版前端联调方案
已修改20个文件
已添加6个文件
1486 ■■■■■ 文件已修改
src/api/mes/qc/ncr/index.ts 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/srm/tender-config/index.ts 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/srm/tender/index.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/constants/src/biz-mes-enum.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/constants/src/dict-enum.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/sale/order/data.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/sale/order/modules/form.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/ipqc/data.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/ipqc/modules/form.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/iqc/data.ts 21 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/iqc/modules/form.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/ncr/data.ts 447 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/ncr/index.vue 339 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/ncr/modules/form.vue 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/oqc/data.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/oqc/modules/form.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/rqc/data.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/rqc/modules/form.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/enums.ts 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/tender/config/index.vue 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/tender/data.ts 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/tender/detail.vue 120 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/tender/index.vue 107 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/srm/tender/modules/form.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/stocktaking/plan/components/condition-value-input.vue 30 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/stocktaking/plan/data.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/qc/ncr/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,90 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace MesQcNcrApi {
  /** MES ä¸åˆæ ¼å“å¤„理单 */
  export interface Ncr {
    id?: number;
    code?: string;
    name?: string;
    qcType?: number;
    qcId?: number;
    qcCode?: string;
    itemId?: number;
    itemCode?: string;
    itemName?: string;
    itemSpecification?: string;
    defectLevel?: number;
    unqualifiedQuantity?: number;
    defectDescription?: string;
    disposition?: number;
    responsibleDeptId?: number;
    responsibleDeptName?: string;
    reviewerUserId?: number;
    reviewerNickname?: string;
    reviewDate?: string;
    reviewOpinion?: string;
    handlerUserId?: number;
    handlerNickname?: string;
    handleDate?: string;
    handleResult?: string;
    rootCause?: string;
    correctiveAction?: string;
    preventiveAction?: string;
    closeUserId?: number;
    closeUserNickname?: string;
    closeDate?: string;
    closeOpinion?: string;
    status?: number;
    remark?: string;
    createTime?: string;
  }
}
/** æŸ¥è¯¢ä¸åˆæ ¼å“å¤„理单分页 */
export function getNcrPage(params: PageParam) {
  return requestClient.get<PageResult<MesQcNcrApi.Ncr>>('/mes/qc/ncr/page', {
    params,
  });
}
/** æŸ¥è¯¢ä¸åˆæ ¼å“å¤„理单详情 */
export function getNcr(id: number) {
  return requestClient.get<MesQcNcrApi.Ncr>(`/mes/qc/ncr/get?id=${id}`);
}
/** æ–°å¢žä¸åˆæ ¼å“å¤„理单 */
export function createNcr(data: MesQcNcrApi.Ncr) {
  return requestClient.post('/mes/qc/ncr/create', data);
}
/** ä¿®æ”¹ä¸åˆæ ¼å“å¤„理单 */
export function updateNcr(data: MesQcNcrApi.Ncr) {
  return requestClient.put('/mes/qc/ncr/update', data);
}
/** åˆ é™¤ä¸åˆæ ¼å“å¤„理单 */
export function deleteNcr(id: number) {
  return requestClient.delete(`/mes/qc/ncr/delete?id=${id}`);
}
/** è¯„审不合格品处理单 */
export function reviewNcr(data: { defectLevel?: number; disposition: number; id: number; responsibleDeptId?: number; reviewOpinion?: string }) {
  return requestClient.put('/mes/qc/ncr/review', data);
}
/** å¤„置不合格品处理单 */
export function disposeNcr(data: { handleResult?: string; handlerUserId?: number; id: number }) {
  return requestClient.put('/mes/qc/ncr/dispose', data);
}
/** å…³é—­ä¸åˆæ ¼å“å¤„理单 */
export function closeNcr(data: { closeOpinion?: string; id: number }) {
  return requestClient.put('/mes/qc/ncr/close', data);
}
/** å¯¼å‡ºä¸åˆæ ¼å“å¤„理单 */
export function exportNcr(params: any) {
  return requestClient.download('/mes/qc/ncr/export-excel', { params });
}
src/api/srm/tender-config/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
import { requestClient } from '#/api/request';
export namespace SrmTenderConfigApi {
  export interface TenderConfigVO {
    id?: number;
    defaultBiddingMode?: string;
  }
}
export function getTenderConfig() {
  return requestClient.get<SrmTenderConfigApi.TenderConfigVO>('/srm/tender-config/get');
}
export function saveTenderConfig(data: SrmTenderConfigApi.TenderConfigVO) {
  return requestClient.put('/srm/tender-config/save', data);
}
src/api/srm/tender/index.ts
@@ -21,6 +21,7 @@
    purchaseRequestId?: number;
    awardId?: number;
    purchaseOrderNo?: string;
    biddingMode?: string;
    createTime?: string;
  }
@@ -149,6 +150,14 @@
  return requestClient.put(`/srm/tender-project/start-evaluation?id=${id}`);
}
export function startPriceComparing(id: number) {
  return requestClient.put(`/srm/tender-project/start-price-comparing?id=${id}`);
}
export function confirmTenderProject(data: { id: number; bidId: number }) {
  return requestClient.put('/srm/tender-project/confirm', data);
}
// ========== ç‰©æ–™å­è¡¨ ==========
export function getMaterialList(tenderProjectId: number) {
src/packages/constants/src/biz-mes-enum.ts
@@ -484,6 +484,12 @@
  RQC: 4, // é€€è´§æ£€éªŒ
} as const;
/** MES è´¨æ£€æ£€æµ‹ç»“果枚举 */
export const MesQcCheckResultEnum = {
  PASS: 1, // æ ¡éªŒé€šè¿‡
  FAIL: 2, // æ ¡éªŒä¸é€šè¿‡
} as const;
/** MES è´¨æ£€å•状态枚举 */
export const MesQcStatusEnum = {
  DRAFT: MesOrderStatusConstants.DRAFT,
src/packages/constants/src/dict-enum.ts
@@ -286,6 +286,8 @@
  MES_WM_TRANSFER_TYPE: 'mes_wm_transfer_type', // MES è½¬ç§»å•类型
  MES_PD_PARAM_TYPE: 'mes_pd_param_type', // MES å·¥è‰ºå‚数类型
  MES_PD_TEMPLATE_TYPE: 'mes_pd_template_type', // MES å·¥è‰ºæ¨¡æ¿ç±»åž‹
  MES_NCR_STATUS: 'mes_ncr_status', // MES ä¸åˆæ ¼å“å¤„理单状态
  MES_NCR_DISPOSITION: 'mes_ncr_disposition', // MES ä¸åˆæ ¼å“å¤„置方式
} as const;
/** ========== WMS - ä»“储管理模块 ========== */
src/views/erp/sale/order/data.ts
@@ -216,6 +216,7 @@
      title: '库存',
      minWidth: 80,
      formatter: 'formatAmount3',
      visible: false,
    },
    {
      field: 'productUnitName',
@@ -226,11 +227,13 @@
      field: 'productUnitName2',
      title: '辅单位1',
      minWidth: 80,
      visible: false,
    },
    {
      field: 'productUnitName3',
      title: '辅单位2',
      minWidth: 80,
      visible: false,
    },
    {
      field: 'needProduction',
@@ -243,6 +246,7 @@
      title: '备注',
      minWidth: 150,
      slots: { default: 'remark' },
      visible: false,
    },
    {
      field: 'count',
src/views/erp/sale/order/modules/form.vue
@@ -154,7 +154,7 @@
<template>
  <Modal
    :title="getTitle"
    class="w-3/4"
    class="w-5/6"
    :show-confirm-button="formType !== 'detail'"
  >
    <Form class="mx-3">
src/views/mes/qc/ipqc/data.ts
@@ -250,6 +250,7 @@
        precision: 2,
      },
      rules: 'required',
      help: '不合格品数量大于0时,保存后将自动生成不合格处理单',
    },
    {
      fieldName: 'laborScrapQuantity',
src/views/mes/qc/ipqc/modules/form.vue
@@ -6,7 +6,7 @@
import { computed, ref } from 'vue';
import { confirm, useVbenModal } from '@vben/common-ui';
import { MesQcStatusEnum, MesQcTypeEnum } from '@vben/constants';
import { MesQcCheckResultEnum, MesQcStatusEnum, MesQcTypeEnum } from '@vben/constants';
import { Button, Descriptions, message, Tabs } from 'ant-design-vue';
@@ -69,6 +69,13 @@
    return false;
  }
  const data = (await formApi.getValues()) as MesQcIpqcApi.Ipqc;
  if (
    (data.unqualifiedQuantity == null || data.unqualifiedQuantity === 0) &&
    data.checkResult === MesQcCheckResultEnum.FAIL
  ) {
    message.warning('不合格品数量为0时,检测结果不能为"校验不通过"');
    return false;
  }
  if (formData.value?.id) {
    await updateIpqc({ ...data, id: formData.value.id });
    formData.value = { ...formData.value, ...data };
@@ -108,6 +115,13 @@
    const current = JSON.stringify(await formApi.getValues());
    if (current !== originalSnapshot.value) {
      const data = (await formApi.getValues()) as MesQcIpqcApi.Ipqc;
      if (
        (data.unqualifiedQuantity == null || data.unqualifiedQuantity === 0) &&
        data.checkResult === MesQcCheckResultEnum.FAIL
      ) {
        message.warning('不合格品数量为0时,检测结果不能为"校验不通过"');
        return;
      }
      await updateIpqc({ ...data, id });
      formData.value = { ...formData.value, ...data };
      originalSnapshot.value = current;
src/views/mes/qc/iqc/data.ts
@@ -11,10 +11,11 @@
import { Button } from 'ant-design-vue';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { getSupplierSimpleList } from '#/api/srm/supplier';
import { getSimpleUserList } from '#/api/system/user';
import { getRangePickerDefaultProps } from '#/utils';
import { MdmItemSelect } from '#/views/basicData/mdm/components';
import { MdVendorSelect } from '#/views/mes/md/vendor/components';
/** è¡¨å•类型 */
export type FormType = 'create' | 'detail' | 'update';
@@ -136,16 +137,23 @@
    {
      fieldName: 'vendorId',
      label: '供应商',
      component: markRaw(MdVendorSelect),
      component: 'ApiSelect',
      componentProps: {
        allowClear: true,
        api: getSupplierSimpleList,
        labelField: 'name',
        placeholder: '请选择供应商',
        valueField: 'id',
      },
      rules: 'selectRequired',
      dependencies: {
        triggerFields: ['sourceDocId'],
        componentProps: (values) => ({
          disabled: !!values.sourceDocId,
          allowClear: true,
          api: getSupplierSimpleList,
          labelField: 'name',
          placeholder: '请选择供应商',
          valueField: 'id',
        }),
      },
    },
@@ -200,6 +208,7 @@
        placeholder: '请输入不合格品数量',
      },
      rules: 'required',
      help: '不合格品数量大于0时,保存后将自动生成不合格处理单',
    },
    {
      fieldName: 'receiveDate',
@@ -274,9 +283,13 @@
    {
      fieldName: 'vendorId',
      label: '供应商',
      component: markRaw(MdVendorSelect),
      component: 'ApiSelect',
      componentProps: {
        allowClear: true,
        api: getSupplierSimpleList,
        labelField: 'name',
        placeholder: '请选择供应商',
        valueField: 'id',
      },
    },
    {
src/views/mes/qc/iqc/modules/form.vue
@@ -6,7 +6,7 @@
import { computed, ref } from 'vue';
import { confirm, useVbenModal } from '@vben/common-ui';
import { MesQcStatusEnum, MesQcTypeEnum } from '@vben/constants';
import { MesQcCheckResultEnum, MesQcStatusEnum, MesQcTypeEnum } from '@vben/constants';
import { Button, Descriptions, message, Tabs } from 'ant-design-vue';
@@ -69,6 +69,13 @@
    return false;
  }
  const data = (await formApi.getValues()) as MesQcIqcApi.Iqc;
  if (
    (data.unqualifiedQuantity == null || data.unqualifiedQuantity === 0) &&
    data.checkResult === MesQcCheckResultEnum.FAIL
  ) {
    message.warning('不合格品数量为0时,检测结果不能为"校验不通过"');
    return false;
  }
  if (formData.value?.id) {
    await updateIqc({ ...data, id: formData.value.id });
    formData.value = { ...formData.value, ...data };
@@ -108,6 +115,13 @@
    const current = JSON.stringify(await formApi.getValues());
    if (current !== originalSnapshot.value) {
      const data = (await formApi.getValues()) as MesQcIqcApi.Iqc;
      if (
        (data.unqualifiedQuantity == null || data.unqualifiedQuantity === 0) &&
        data.checkResult === MesQcCheckResultEnum.FAIL
      ) {
        message.warning('不合格品数量为0时,检测结果不能为"校验不通过"');
        return;
      }
      await updateIqc({ ...data, id });
      formData.value = { ...formData.value, ...data };
      originalSnapshot.value = current;
src/views/mes/qc/ncr/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,447 @@
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesQcNcrApi } from '#/api/mes/qc/ncr';
import { h, markRaw } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { Button } from 'ant-design-vue';
import { getSimpleUserList } from '#/api/system/user';
import { DeptTreeSelect } from '#/views/system/dept/components';
import { MdItemSelect } from '#/views/mes/md/item/components';
/** NCR çŠ¶æ€æžšä¸¾ */
export const NCR_STATUS = {
  PENDING_REVIEW: 0,
  REVIEWED: 1,
  DISPOSED: 2,
  CLOSED: 3,
} as const;
export const NCR_STATUS_OPTIONS = [
  { label: '待评审', value: 0 },
  { label: '已评审', value: 1 },
  { label: '已处置', value: 2 },
  { label: '已关闭', value: 3 },
];
/** æ£€éªŒç±»åž‹æžšä¸¾ */
export const QC_TYPE_OPTIONS = [
  { label: 'IQC', value: 1 },
  { label: 'IPQC', value: 2 },
  { label: 'OQC', value: 3 },
  { label: 'RQC', value: 4 },
];
/** æ–°å¢ž/修改的表单 */
export function useFormSchema(
  formApi?: VbenFormApi,
): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'qcId',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'status',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'code',
      label: 'NCR单号',
      component: 'Input',
      componentProps: {
        placeholder: '请输入NCR单号',
      },
      rules: 'required',
      suffix: () =>
        h(
          Button,
          {
            type: 'default',
            onClick: async () => {
              const now = new Date();
              const pad = (n: number) => String(n).padStart(2, '0');
              const code = `NCR${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
              await formApi?.setFieldValue('code', code);
            },
          },
          { default: () => '自动生成' },
        ),
    },
    {
      fieldName: 'name',
      label: 'NCR名称',
      component: 'Input',
      componentProps: {
        placeholder: '请输入NCR名称',
      },
      rules: 'required',
    },
    {
      fieldName: 'qcType',
      label: '检验类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: QC_TYPE_OPTIONS,
        placeholder: '请选择检验类型',
      },
    },
    {
      fieldName: 'itemId',
      label: '不合格物料',
      component: markRaw(MdItemSelect),
      componentProps: {
        placeholder: '请选择不合格物料',
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'defectLevel',
      label: '缺陷等级',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_DEFECT_LEVEL, 'number'),
        placeholder: '请选择缺陷等级',
      },
    },
    {
      fieldName: 'unqualifiedQuantity',
      label: '不合格数量',
      component: 'InputNumber',
      componentProps: {
        class: '!w-full',
        min: 0,
        placeholder: '请输入不合格数量',
        precision: 2,
      },
    },
    {
      fieldName: 'defectDescription',
      label: '不合格描述',
      component: 'Textarea',
      componentProps: {
        placeholder: '请输入不合格描述',
        rows: 3,
      },
    },
    {
      fieldName: 'rootCause',
      label: '根本原因',
      component: 'Textarea',
      componentProps: {
        placeholder: '请输入根本原因',
        rows: 2,
      },
    },
    {
      fieldName: 'correctiveAction',
      label: '纠正措施',
      component: 'Textarea',
      componentProps: {
        placeholder: '请输入纠正措施',
        rows: 2,
      },
    },
    {
      fieldName: 'preventiveAction',
      label: '预防措施',
      component: 'Textarea',
      componentProps: {
        placeholder: '请输入预防措施',
        rows: 2,
      },
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      componentProps: {
        placeholder: '请输入备注',
        rows: 2,
      },
    },
  ];
}
/** è¯„审表单 */
export function useReviewFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'disposition',
      label: '处置方式',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_NCR_DISPOSITION, 'number'),
        placeholder: '请选择处置方式',
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'defectLevel',
      label: '缺陷等级',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_DEFECT_LEVEL, 'number'),
        placeholder: '请选择缺陷等级',
      },
    },
    {
      fieldName: 'responsibleDeptId',
      label: '责任部门',
      component: markRaw(DeptTreeSelect),
      componentProps: {
        placeholder: '请选择责任部门',
      },
    },
    {
      fieldName: 'reviewOpinion',
      label: '评审意见',
      component: 'Textarea',
      componentProps: {
        placeholder: '请输入评审意见',
        rows: 3,
      },
    },
  ];
}
/** å¤„置表单 */
export function useDisposeFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'handlerUserId',
      label: '处理人',
      component: 'ApiSelect',
      componentProps: {
        allowClear: true,
        api: getSimpleUserList,
        labelField: 'nickname',
        placeholder: '请选择处理人',
        valueField: 'id',
      },
    },
    {
      fieldName: 'handleResult',
      label: '处理结果说明',
      component: 'Textarea',
      componentProps: {
        placeholder: '请输入处理结果说明',
        rows: 3,
      },
    },
  ];
}
/** å…³é—­è¡¨å• */
export function useCloseFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'closeOpinion',
      label: '关闭意见',
      component: 'Textarea',
      componentProps: {
        placeholder: '请输入关闭意见',
        rows: 3,
      },
    },
  ];
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'code',
      label: 'NCR单号',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入NCR单号',
      },
    },
    {
      fieldName: 'qcType',
      label: '检验类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: QC_TYPE_OPTIONS,
        placeholder: '请选择检验类型',
      },
    },
    {
      fieldName: 'itemId',
      label: '不合格物料',
      component: markRaw(MdItemSelect),
      componentProps: {
        placeholder: '请选择物料',
      },
    },
    {
      fieldName: 'defectLevel',
      label: '缺陷等级',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_DEFECT_LEVEL, 'number'),
        placeholder: '请选择缺陷等级',
      },
    },
    {
      fieldName: 'disposition',
      label: '处置方式',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_NCR_DISPOSITION, 'number'),
        placeholder: '请选择处置方式',
      },
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: NCR_STATUS_OPTIONS,
        placeholder: '请选择状态',
      },
    },
    {
      fieldName: 'createTime',
      label: '创建时间',
      component: 'DatePicker',
      componentProps: {
        allowClear: true,
        class: '!w-full',
        placeholder: '请选择创建时间',
        showTime: true,
        type: 'date',
      },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<MesQcNcrApi.Ncr>['columns'] {
  return [
    {
      field: 'code',
      title: 'NCR单号',
      width: 200,
    },
    {
      field: 'qcCode',
      title: '来源检验单编号',
      width: 180,
    },
    {
      field: 'itemName',
      title: '物料名称',
      minWidth: 150,
    },
    {
      field: 'itemSpecification',
      title: '规格型号',
      width: 130,
    },
    {
      field: 'qcType',
      title: '检验类型',
      width: 100,
      slots: { default: 'qcType' },
    },
    {
      field: 'defectLevel',
      title: '缺陷等级',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_DEFECT_LEVEL },
      },
    },
    {
      field: 'unqualifiedQuantity',
      title: '不合格数量',
      width: 110,
    },
    {
      field: 'disposition',
      title: '处置方式',
      width: 110,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_NCR_DISPOSITION },
      },
    },
    {
      field: 'status',
      title: '状态',
      width: 90,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_NCR_STATUS },
      },
    },
    {
      field: 'createTime',
      title: '创建时间',
      width: 180,
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
      width: 220,
      fixed: 'right',
      slots: { default: 'actions' },
    },
  ];
}
src/views/mes/qc/ncr/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,339 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesQcNcrApi } from '#/api/mes/qc/ncr';
import { Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart } from '@vben/utils';
import { message, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { useVbenForm } from '#/adapter/form';
import {
  closeNcr,
  deleteNcr,
  disposeNcr,
  exportNcr,
  getNcrPage,
  reviewNcr,
} from '#/api/mes/qc/ncr';
import { $t } from '#/locales';
import {
  NCR_STATUS,
  QC_TYPE_OPTIONS,
  useCloseFormSchema,
  useDisposeFormSchema,
  useGridColumns,
  useGridFormSchema,
  useReviewFormSchema,
} from './data';
import Form from './modules/form.vue';
defineOptions({ name: 'NcrIndex' });
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
/** è¯„审弹窗 */
const [ReviewForm, reviewFormApi] = useVbenForm({
  commonConfig: {
    componentProps: { class: 'w-full' },
    formItemClass: 'col-span-2',
    labelWidth: 120,
  },
  layout: 'horizontal',
  schema: useReviewFormSchema(),
  showDefaultActions: false,
});
const [ReviewModal, reviewModalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await reviewFormApi.validate();
    if (!valid) return;
    reviewModalApi.lock();
    try {
      await reviewNcr(
        (await reviewFormApi.getValues()) as {
          defectLevel?: number;
          disposition: number;
          id: number;
          responsibleDeptId?: number;
          reviewOpinion?: string;
        },
      );
      await reviewModalApi.close();
      message.success('评审成功');
      handleRefresh();
    } finally {
      reviewModalApi.unlock();
    }
  },
  onOpenChange(isOpen: boolean) {
    if (!isOpen) return;
    const data = reviewModalApi.getData<MesQcNcrApi.Ncr>();
    reviewFormApi.setValues({ id: data?.id });
  },
});
/** å¤„置弹窗 */
const [DisposeForm, disposeFormApi] = useVbenForm({
  commonConfig: {
    componentProps: { class: 'w-full' },
    formItemClass: 'col-span-2',
    labelWidth: 120,
  },
  layout: 'horizontal',
  schema: useDisposeFormSchema(),
  showDefaultActions: false,
});
const [DisposeModal, disposeModalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await disposeFormApi.validate();
    if (!valid) return;
    disposeModalApi.lock();
    try {
      await disposeNcr(
        (await disposeFormApi.getValues()) as {
          handleResult?: string;
          handlerUserId?: number;
          id: number;
        },
      );
      await disposeModalApi.close();
      message.success('处置成功');
      handleRefresh();
    } finally {
      disposeModalApi.unlock();
    }
  },
  onOpenChange(isOpen: boolean) {
    if (!isOpen) return;
    const data = disposeModalApi.getData<MesQcNcrApi.Ncr>();
    disposeFormApi.setValues({ id: data?.id });
  },
});
/** å…³é—­å¼¹çª— */
const [CloseForm, closeFormApi] = useVbenForm({
  commonConfig: {
    componentProps: { class: 'w-full' },
    formItemClass: 'col-span-2',
    labelWidth: 120,
  },
  layout: 'horizontal',
  schema: useCloseFormSchema(),
  showDefaultActions: false,
});
const [CloseModal, closeModalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await closeFormApi.validate();
    if (!valid) return;
    closeModalApi.lock();
    try {
      await closeNcr(
        (await closeFormApi.getValues()) as {
          closeOpinion?: string;
          id: number;
        },
      );
      await closeModalApi.close();
      message.success('关闭成功');
      handleRefresh();
    } finally {
      closeModalApi.unlock();
    }
  },
  onOpenChange(isOpen: boolean) {
    if (!isOpen) return;
    const data = closeModalApi.getData<MesQcNcrApi.Ncr>();
    closeFormApi.setValues({ id: data?.id });
  },
});
function getQcTypeLabel(type?: number): string {
  return QC_TYPE_OPTIONS.find((o) => o.value === type)?.label || '';
}
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** åˆ›å»º */
function handleCreate() {
  formModalApi.setData(null).open();
}
/** ç¼–辑 */
function handleEdit(row: MesQcNcrApi.Ncr) {
  formModalApi.setData(row).open();
}
/** åˆ é™¤ */
async function handleDelete(row: MesQcNcrApi.Ncr) {
  const hideLoading = message.loading({
    content: $t('ui.actionMessage.deleting', [row.name]),
    duration: 0,
  });
  try {
    await deleteNcr(row.id!);
    message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
    handleRefresh();
  } finally {
    hideLoading();
  }
}
/** è¯„审 */
function handleReview(row: MesQcNcrApi.Ncr) {
  reviewModalApi.setData(row).open();
}
/** å¤„ç½® */
function handleDispose(row: MesQcNcrApi.Ncr) {
  disposeModalApi.setData(row).open();
}
/** å…³é—­ */
function handleClose(row: MesQcNcrApi.Ncr) {
  closeModalApi.setData(row).open();
}
/** å¯¼å‡º */
async function handleExport() {
  const data = await exportNcr(await gridApi.formApi.getValues());
  downloadFileFromBlobPart({ fileName: '不合格品处理单.xls', source: data });
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          return await getNcrPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          });
        },
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<MesQcNcrApi.Ncr>,
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <ReviewModal title="评审不合格品处理单" class="w-2/5">
      <ReviewForm class="mx-4" />
    </ReviewModal>
    <DisposeModal title="处置不合格品处理单" class="w-2/5">
      <DisposeForm class="mx-4" />
    </DisposeModal>
    <CloseModal title="关闭不合格品处理单" class="w-2/5">
      <CloseForm class="mx-4" />
    </CloseModal>
    <Grid table-title="不合格品处理">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['不合格品处理单']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['mes:qc-ncr:create'],
              onClick: handleCreate,
            },
            {
              label: $t('ui.actionTitle.export'),
              type: 'primary',
              icon: ACTION_ICON.DOWNLOAD,
              auth: ['mes:qc-ncr:export'],
              onClick: handleExport,
            },
          ]"
        />
      </template>
      <template #qcType="{ row }">
        <Tag v-if="row.qcType !== undefined && row.qcType !== null">
          {{ getQcTypeLabel(row.qcType) }}
        </Tag>
      </template>
      <template #actions="{ row }">
        <TableAction
          v-if="row.status === NCR_STATUS.PENDING_REVIEW"
          :actions="[
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['mes:qc-ncr:update'],
              onClick: handleEdit.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['mes:qc-ncr:delete'],
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [row.name]),
                confirm: handleDelete.bind(null, row),
              },
            },
            {
              label: '评审',
              type: 'link',
              icon: 'lucide:clipboard-check',
              auth: ['mes:qc-ncr:review'],
              onClick: handleReview.bind(null, row),
            },
          ]"
        />
        <TableAction
          v-else-if="row.status === NCR_STATUS.REVIEWED"
          :actions="[
            {
              label: '处置',
              type: 'link',
              icon: 'lucide:play',
              auth: ['mes:qc-ncr:dispose'],
              onClick: handleDispose.bind(null, row),
            },
          ]"
        />
        <TableAction
          v-else-if="row.status === NCR_STATUS.DISPOSED"
          :actions="[
            {
              label: '关闭',
              type: 'link',
              icon: 'lucide:check-circle',
              auth: ['mes:qc-ncr:close'],
              onClick: handleClose.bind(null, row),
            },
          ]"
        />
        <span v-else class="text-gray-400">--</span>
      </template>
    </Grid>
  </Page>
</template>
src/views/mes/qc/ncr/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,81 @@
<script lang="ts" setup>
import type { MesQcNcrApi } from '#/api/mes/qc/ncr';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createNcr, getNcr, updateNcr } from '#/api/mes/qc/ncr';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
defineOptions({ name: 'NcrForm' });
const emit = defineEmits(['success']);
const formData = ref<MesQcNcrApi.Ncr>();
const getTitle = computed(() => {
  return formData.value?.id
    ? $t('ui.actionTitle.edit', ['不合格品处理单'])
    : $t('ui.actionTitle.create', ['不合格品处理单']);
});
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    formItemClass: 'col-span-2',
    labelWidth: 120,
  },
  layout: 'horizontal',
  schema: [],
  showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    const data = (await formApi.getValues()) as MesQcNcrApi.Ncr;
    try {
      await (formData.value?.id ? updateNcr(data) : createNcr(data));
      await modalApi.close();
      emit('success');
      message.success($t('ui.actionMessage.operationSuccess'));
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      formData.value = undefined;
      return;
    }
    formApi.setState({ schema: useFormSchema(formApi) });
    const data = modalApi.getData<MesQcNcrApi.Ncr>();
    if (!data || !data.id) {
      return;
    }
    modalApi.lock();
    try {
      formData.value = await getNcr(data.id);
      await formApi.setValues(formData.value);
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal :title="getTitle" class="w-3/5">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/mes/qc/oqc/data.ts
@@ -233,6 +233,7 @@
        placeholder: '请输入不合格品数量',
      },
      rules: 'required',
      help: '不合格品数量大于0时,保存后将自动生成不合格处理单',
    },
    {
      fieldName: 'inspectorUserId',
src/views/mes/qc/oqc/modules/form.vue
@@ -6,7 +6,7 @@
import { computed, ref } from 'vue';
import { confirm, useVbenModal } from '@vben/common-ui';
import { MesQcStatusEnum, MesQcTypeEnum } from '@vben/constants';
import { MesQcCheckResultEnum, MesQcStatusEnum, MesQcTypeEnum } from '@vben/constants';
import { Button, Descriptions, message, Tabs } from 'ant-design-vue';
@@ -69,6 +69,13 @@
    return false;
  }
  const data = (await formApi.getValues()) as MesQcOqcApi.Oqc;
  if (
    (data.unqualifiedQuantity == null || data.unqualifiedQuantity === 0) &&
    data.checkResult === MesQcCheckResultEnum.FAIL
  ) {
    message.warning('不合格品数量为0时,检测结果不能为"校验不通过"');
    return false;
  }
  if (formData.value?.id) {
    await updateOqc({ ...data, id: formData.value.id });
    formData.value = { ...formData.value, ...data };
@@ -107,6 +114,13 @@
    const current = JSON.stringify(await formApi.getValues());
    if (current !== originalSnapshot.value) {
      const data = (await formApi.getValues()) as MesQcOqcApi.Oqc;
      if (
        (data.unqualifiedQuantity == null || data.unqualifiedQuantity === 0) &&
        data.checkResult === MesQcCheckResultEnum.FAIL
      ) {
        message.warning('不合格品数量为0时,检测结果不能为"校验不通过"');
        return;
      }
      await updateOqc({ ...data, id });
      formData.value = { ...formData.value, ...data };
      originalSnapshot.value = current;
src/views/mes/qc/rqc/data.ts
@@ -210,6 +210,7 @@
        precision: 2,
      },
      rules: 'required',
      help: '不合格品数量大于0时,保存后将自动生成不合格处理单',
    },
    {
      fieldName: 'inspectDate',
src/views/mes/qc/rqc/modules/form.vue
@@ -6,7 +6,7 @@
import { computed, ref } from 'vue';
import { confirm, useVbenModal } from '@vben/common-ui';
import { MesQcStatusEnum, MesQcTypeEnum } from '@vben/constants';
import { MesQcCheckResultEnum, MesQcStatusEnum, MesQcTypeEnum } from '@vben/constants';
import { Button, Descriptions, message, Tabs } from 'ant-design-vue';
@@ -69,6 +69,13 @@
    return false;
  }
  const data = (await formApi.getValues()) as MesQcRqcApi.Rqc;
  if (
    (data.unqualifiedQuantity == null || data.unqualifiedQuantity === 0) &&
    data.checkResult === MesQcCheckResultEnum.FAIL
  ) {
    message.warning('不合格品数量为0时,检测结果不能为"校验不通过"');
    return false;
  }
  if (formData.value?.id) {
    await updateRqc({ ...data, id: formData.value.id });
    formData.value = { ...formData.value, ...data };
@@ -107,6 +114,13 @@
    const current = JSON.stringify(await formApi.getValues());
    if (current !== originalSnapshot.value) {
      const data = (await formApi.getValues()) as MesQcRqcApi.Rqc;
      if (
        (data.unqualifiedQuantity == null || data.unqualifiedQuantity === 0) &&
        data.checkResult === MesQcCheckResultEnum.FAIL
      ) {
        message.warning('不合格品数量为0时,检测结果不能为"校验不通过"');
        return;
      }
      await updateRqc({ ...data, id });
      formData.value = { ...formData.value, ...data };
      originalSnapshot.value = current;
src/views/srm/enums.ts
@@ -172,7 +172,7 @@
  labels: { 1: '公开招标', 2: '邀请招标', 3: '竞争性谈判' },
};
/** æ‹›æ ‡é¡¹ç›®çŠ¶æ€ â€” å¯¹é½ TenderStatusEnum (0=草稿 1=发布中 2=投标中 3=评标中 4=已定标 5=已关闭) */
/** æ‹›æ ‡é¡¹ç›®çŠ¶æ€ â€” å¯¹é½ TenderStatusEnum (0=草稿 1=发布中 2=投标中 3=评标中 4=已定标 5=已关闭 10=比价中 20=已确认) */
export const TENDER_STATUS = {
  DRAFT: 0,
  PUBLISHED: 1,
@@ -180,6 +180,8 @@
  EVALUATING: 3,
  AWARDED: 4,
  CLOSED: 5,
  PRICE_COMPARING: 10,
  CONFIRMED: 20,
} as const;
export const TENDER_STATUS_OPTIONS: EnumOption[] = [
@@ -189,6 +191,8 @@
  { label: '评标中', value: 3 },
  { label: '已定标', value: 4 },
  { label: '已关闭', value: 5 },
  { label: '比价中', value: 10 },
  { label: '已确认', value: 20 },
];
export const TENDER_STATUS_CELLTAG: { data: CellTagItem[]; labels: Record<number, string> } = {
@@ -199,8 +203,10 @@
    { value: 3, color: 'orange' },
    { value: 4, color: 'green' },
    { value: 5, color: 'red' },
    { value: 10, color: 'purple' },
    { value: 20, color: 'green' },
  ],
  labels: { 0: '草稿', 1: '发布中', 2: '投标中', 3: '评标中', 4: '已定标', 5: '已关闭' },
  labels: { 0: '草稿', 1: '发布中', 2: '投标中', 3: '评标中', 4: '已定标', 5: '已关闭', 10: '比价中', 20: '已确认' },
};
/** æ‹›æ ‡çŠ¶æ€ Ant Design Tag é¢œè‰²æ˜ å°„ */
@@ -211,6 +217,25 @@
  [TENDER_STATUS.EVALUATING]: 'orange',
  [TENDER_STATUS.AWARDED]: 'green',
  [TENDER_STATUS.CLOSED]: 'red',
  [TENDER_STATUS.PRICE_COMPARING]: 'purple',
  [TENDER_STATUS.CONFIRMED]: 'green',
};
// ========== æ‹›æŠ•标模式 â€” FULL=完整版 SIMPLE=简易版 ==========
export const BIDDING_MODE = {
  FULL: 'FULL',
  SIMPLE: 'SIMPLE',
} as const;
export const BIDDING_MODE_OPTIONS: EnumOption[] = [
  { label: '完整版', value: 'FULL' },
  { label: '简易版', value: 'SIMPLE' },
];
export const BIDDING_MODE_MAP: Record<string, string> = {
  FULL: '完整版',
  SIMPLE: '简易版',
};
// ========== æŠ•æ ‡ â€” å¯¹é½ BidStatusEnum (0=报名 10=已投标 20=撤标 30=中标 40=未中标) ==========
src/views/srm/tender/config/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,65 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { Page } from '@vben/common-ui';
import { message, Radio, Button, Card, Spin } from 'ant-design-vue';
import { getTenderConfig, saveTenderConfig } from '#/api/srm/tender-config';
import { BIDDING_MODE, BIDDING_MODE_OPTIONS } from '#/views/srm/enums';
defineOptions({ name: 'SrmTenderConfig' });
const loading = ref(false);
const saving = ref(false);
const defaultBiddingMode = ref<string>(BIDDING_MODE.FULL);
async function loadConfig() {
  loading.value = true;
  try {
    const res = await getTenderConfig();
    if (res?.defaultBiddingMode) {
      defaultBiddingMode.value = res.defaultBiddingMode;
    }
  } finally {
    loading.value = false;
  }
}
async function handleSave() {
  saving.value = true;
  try {
    await saveTenderConfig({ defaultBiddingMode: defaultBiddingMode.value });
    message.success('保存成功');
    await loadConfig();
  } finally {
    saving.value = false;
  }
}
loadConfig();
</script>
<template>
  <Page>
    <Spin :spinning="loading">
      <Card title="招投标默认模式配置" class="max-w-lg">
        <div class="mb-4 text-gray-500 text-sm">
          è®¾ç½®æ–°å»ºæ‹›æ ‡é¡¹ç›®æ—¶çš„默认招投标模式,用户在创建项目时仍可手动更改。
        </div>
        <div class="flex items-center gap-4">
          <span class="font-medium">默认模式:</span>
          <Radio.Group
            v-model:value="defaultBiddingMode"
            :options="BIDDING_MODE_OPTIONS"
          />
        </div>
        <div class="mt-6">
          <Button type="primary" :loading="saving" @click="handleSave">
            ä¿å­˜
          </Button>
        </div>
      </Card>
    </Spin>
  </Page>
</template>
src/views/srm/tender/data.ts
@@ -2,6 +2,8 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import {
  BIDDING_MODE_MAP,
  BIDDING_MODE_OPTIONS,
  TENDER_STATUS_CELLTAG,
  TENDER_STATUS_OPTIONS,
  TENDER_TYPE_CELLTAG,
@@ -146,6 +148,25 @@
      },
    },
    {
      fieldName: 'biddingMode',
      label: '招投标模式',
      component: 'Select',
      rules: 'required',
      componentProps: {
        placeholder: '请选择招投标模式',
        options: BIDDING_MODE_OPTIONS,
      },
      dependencies: {
        triggerFields: ['_partial', 'id'],
        show: (values) => !values._partial,
        componentProps: (values) => ({
          disabled: !!values.id,
          placeholder: '请选择招投标模式',
          options: BIDDING_MODE_OPTIONS,
        }),
      },
    },
    {
      fieldName: 'tenderDesc',
      label: '招标说明',
      component: 'Textarea',
@@ -197,6 +218,16 @@
        options: TENDER_STATUS_OPTIONS,
      },
    },
    {
      fieldName: 'biddingMode',
      label: '招投标模式',
      component: 'Select',
      componentProps: {
        placeholder: '请选择招投标模式',
        allowClear: true,
        options: BIDDING_MODE_OPTIONS,
      },
    },
  ];
}
@@ -217,6 +248,12 @@
      minWidth: 100,
      cellRender: { name: 'CellTag', props: TENDER_STATUS_CELLTAG },
    },
    {
      field: 'biddingMode',
      title: '招标模式',
      minWidth: 100,
      formatter: ({ cellValue }) => BIDDING_MODE_MAP[cellValue] || cellValue,
    },
    { field: 'budgetAmount', title: '预算金额', minWidth: 120 },
    { field: 'purchaseOrg', title: '采购组织', minWidth: 100 },
    { field: 'startTime', title: '开始时间', minWidth: 160 },
src/views/srm/tender/detail.vue
@@ -17,6 +17,7 @@
  createBidOpen, getBidOpen,
  getEvaluationList, createBidEvaluation, updateBidEvaluation, calculateRanking,
  createAward, deleteAward, getAward, getAwardByProject, approveAward, generatePurchaseOrder,
  confirmTenderProject,
} from '#/api/srm/tender';
import { getSupplierSimpleList } from '#/api/srm/supplier';
import { getItemSimpleList } from '#/api/mdm/item';
@@ -31,6 +32,7 @@
  AWARD_STATUS,
  AWARD_STATUS_MAP,
  getLabel,
  BIDDING_MODE,
} from '../enums';
defineOptions({ name: 'SrmTenderDetail' });
@@ -318,6 +320,36 @@
  loadProject();
}
// ========== ç®€æ˜“模式:确认中标 ==========
const confirmBidId = ref<number>();
const confirmModalVisible = ref(false);
async function handleConfirmBidAsWinner(bidId: number) {
  await confirmTenderProject({ id: projectId, bidId });
  message.success('确认中标成功,采购订单已自动生成');
  loadProject();
  loadBids();
  loadAward();
}
function openSimpleConfirmModal() {
  confirmBidId.value = undefined;
  confirmModalVisible.value = true;
}
async function handleSimpleConfirm() {
  if (!confirmBidId.value) {
    message.warning('请选择中标的投标记录');
    return;
  }
  await confirmTenderProject({ id: projectId, bidId: confirmBidId.value });
  message.success('确认中标成功,采购订单已自动生成');
  confirmModalVisible.value = false;
  loadProject();
  loadBids();
  loadAward();
}
// ========== åˆå§‹åŒ– ==========
loadProject();
loadMaterials();
@@ -344,23 +376,37 @@
const canApproveAward = computed(() => award.value?.awardStatus === AWARD_STATUS.PENDING_APPROVAL);
const canGeneratePO = computed(() => award.value?.awardStatus === AWARD_STATUS.AWARDED);
const stepItems = [
  { title: '草稿', status: TENDER_STATUS.DRAFT },
  { title: '发布', status: TENDER_STATUS.PUBLISHED },
  { title: '投标中', status: TENDER_STATUS.BIDDING },
  { title: '评标中', status: TENDER_STATUS.EVALUATING },
  { title: '已定标', status: TENDER_STATUS.AWARDED },
];
// ç®€æ˜“模式
const isSimpleMode = computed(() => project.value?.biddingMode === BIDDING_MODE.SIMPLE);
const isPriceComparing = computed(() => project.value?.tenderStatus === TENDER_STATUS.PRICE_COMPARING);
const stepItems = computed(() => {
  if (isSimpleMode.value) {
    return [
      { title: '草稿', status: TENDER_STATUS.DRAFT },
      { title: '比价中', status: TENDER_STATUS.PRICE_COMPARING },
      { title: '已确认', status: TENDER_STATUS.CONFIRMED },
    ];
  }
  return [
    { title: '草稿', status: TENDER_STATUS.DRAFT },
    { title: '发布', status: TENDER_STATUS.PUBLISHED },
    { title: '投标中', status: TENDER_STATUS.BIDDING },
    { title: '评标中', status: TENDER_STATUS.EVALUATING },
    { title: '已定标', status: TENDER_STATUS.AWARDED },
  ];
});
const currentStep = computed(() => {
  const s = project.value?.tenderStatus ?? 0;
  if (s === TENDER_STATUS.CLOSED) return 4;
  const idx = stepItems.findIndex((item) => item.status >= s);
  const items = stepItems.value;
  if (s === TENDER_STATUS.CLOSED) return items.length - 1;
  const idx = items.findIndex((item) => item.status >= s);
  return idx === -1 ? 0 : idx;
});
const stepStatus = computed(() => {
  const s = project.value?.tenderStatus;
  if (s === TENDER_STATUS.CLOSED) return 'error';
  if (s === TENDER_STATUS.AWARDED) return 'finish';
  if (s === TENDER_STATUS.AWARDED || s === TENDER_STATUS.CONFIRMED) return 'finish';
  return 'process';
});
@@ -395,7 +441,7 @@
  { title: '投标金额', dataIndex: 'bidTotalAmount', width: 120 },
  { title: '投标时间', dataIndex: 'bidTime', width: 160 },
  { title: '状态', dataIndex: 'bidStatus', width: 100, customRender: ({ text }: any) => BID_STATUS_MAP[text] || text },
  { title: '操作', key: 'actions', width: 180 },
  { title: '操作', key: 'actions', width: 240 },
];
const quoteColumns = [
@@ -438,7 +484,16 @@
        <span class="text-gray-400 text-xs">{{ project.tenderNo }}</span>
        <a-tag :color="statusColor">{{ statusLabel }}</a-tag>
        <a-tag color="blue">{{ typeLabel }}</a-tag>
        <a-tag v-if="isSimpleMode" color="purple">简易版</a-tag>
        <div class="flex-1" />
        <a-button
          v-if="isPriceComparing"
          type="primary"
          size="small"
          @click="openSimpleConfirmModal"
        >
          ç¡®è®¤ä¸­æ ‡
        </a-button>
        <a-button size="small" @click="loadProject(); loadMaterials(); loadBids(); loadEvaluations(); loadAward();">
          <template #icon><IconifyIcon icon="ant-design:reload-outlined" /></template>
          åˆ·æ–°
@@ -539,7 +594,7 @@
              </template>
              <div class="flex items-center justify-between mb-3">
                <span class="text-xs text-gray-400">共 <b class="text-gray-600">{{ bidCount }}</b> ä¸ªæŠ•æ ‡</span>
                <a-button v-if="isPublishedOrBidding" type="primary" size="small" @click="bidForm = { tenderProjectId: projectId, supplierId: undefined!, bidNo: '' }; bidFormVisible = true">
                <a-button v-if="isPublishedOrBidding || isDraft || isPriceComparing" type="primary" size="small" @click="bidForm = { tenderProjectId: projectId, supplierId: undefined!, bidNo: '' }; bidFormVisible = true">
                  <template #icon><IconifyIcon icon="ant-design:plus-outlined" /></template>
                  æ–°å¢žæŠ•æ ‡
                </a-button>
@@ -557,7 +612,10 @@
                  <template v-if="column.key === 'actions'">
                    <a-space>
                      <a-button size="small" type="link" @click="loadQuotes(record.id); activeTab = 'quote'"><IconifyIcon icon="ant-design:dollar-outlined" />报价</a-button>
                      <a-popconfirm v-if="isPublishedOrBidding && record.bidStatus === BID_STATUS.REGISTERED" title="确认撤标?" @confirm="handleWithdrawBid(record.id)">
                      <a-popconfirm v-if="isPriceComparing && record.bidStatus !== BID_STATUS.WITHDRAWN" title="确认选择该投标为中标?将自动生成采购订单。" ok-text="确认中标" cancel-text="取消" @confirm="handleConfirmBidAsWinner(record.id)">
                        <a-button size="small" type="link" style="color: #52c41a;"><IconifyIcon icon="ant-design:check-circle-outlined" />确认中标</a-button>
                      </a-popconfirm>
                      <a-popconfirm v-if="(isPublishedOrBidding || isDraft || isPriceComparing) && record.bidStatus === BID_STATUS.REGISTERED" title="确认撤标?" @confirm="handleWithdrawBid(record.id)">
                        <a-button size="small" type="link" danger><IconifyIcon icon="ant-design:close-circle-outlined" />撤标</a-button>
                      </a-popconfirm>
                    </a-space>
@@ -580,7 +638,7 @@
                  <a-button size="small" type="link" class="ml-auto" @click="activeTab = 'bid'">切换投标</a-button>
                </div>
                <div class="text-right mb-3">
                  <a-button v-if="isPublishedOrBidding && selectedBid.bidStatus !== BID_STATUS.WITHDRAWN" type="primary" size="small" @click="handleAddQuote">
                  <a-button v-if="(isPublishedOrBidding || isDraft || isPriceComparing) && selectedBid?.bidStatus !== BID_STATUS.WITHDRAWN" type="primary" size="small" @click="handleAddQuote">
                    <template #icon><IconifyIcon icon="ant-design:plus-outlined" /></template>
                    æ–°å¢žæŠ¥ä»·
                  </a-button>
@@ -600,7 +658,7 @@
                      {{ record.warrantyPeriod != null ? `${record.warrantyPeriod} ä¸ªæœˆ` : '-' }}
                    </template>
                    <template v-if="column.key === 'actions'">
                      <a-button v-if="isPublishedOrBidding && selectedBid?.bidStatus !== BID_STATUS.WITHDRAWN" size="small" type="link" @click="handleEditQuote(record)"><IconifyIcon icon="ant-design:edit-outlined" />编辑</a-button>
                      <a-button v-if="(isPublishedOrBidding || isDraft || isPriceComparing) && selectedBid?.bidStatus !== BID_STATUS.WITHDRAWN" size="small" type="link" @click="handleEditQuote(record)"><IconifyIcon icon="ant-design:edit-outlined" />编辑</a-button>
                    </template>
                  </template>
                </a-table>
@@ -612,7 +670,7 @@
            </a-tab-pane>
            <!-- Tab4: å¼€æ ‡ -->
            <a-tab-pane key="bidOpen">
            <a-tab-pane v-if="!isSimpleMode" key="bidOpen">
              <template #tab>
                <a-space :size="4"><IconifyIcon icon="ant-design:unlock-outlined" />开标</a-space>
              </template>
@@ -639,7 +697,7 @@
            </a-tab-pane>
            <!-- Tab5: è¯„æ ‡ -->
            <a-tab-pane key="evaluation">
            <a-tab-pane v-if="!isSimpleMode" key="evaluation">
              <template #tab>
                <a-space :size="4"><IconifyIcon icon="ant-design:trophy-outlined" />评标</a-space>
              </template>
@@ -685,7 +743,7 @@
            </a-tab-pane>
            <!-- Tab6: å®šæ ‡ -->
            <a-tab-pane key="award">
            <a-tab-pane v-if="!isSimpleMode" key="award">
              <template #tab>
                <a-space :size="4"><IconifyIcon icon="ant-design:check-circle-outlined" />定标</a-space>
              </template>
@@ -886,6 +944,32 @@
        </a-form>
      </a-modal>
      <!-- ç®€æ˜“模式:确认中标弹窗 -->
      <a-modal v-model:open="confirmModalVisible" title="确认中标" :width="460" @ok="handleSimpleConfirm">
        <a-form layout="vertical" class="mt-4">
          <a-form-item label="中标投标记录" required>
            <a-select
              v-model:value="confirmBidId"
              show-search
              placeholder="选择中标的投标记录"
              :filter-option="filterOption"
            >
              <a-select-option
                v-for="b in bidList.filter((i) => i.bidStatus !== BID_STATUS.WITHDRAWN)"
                :key="b.id"
                :value="b.id"
                :label="`${b.supplierName} - ${b.bidNo}`"
              >
                {{ b.supplierName }} - {{ b.bidNo }} (Â¥{{ b.bidTotalAmount?.toLocaleString?.() ?? b.bidTotalAmount }})
              </a-select-option>
            </a-select>
          </a-form-item>
          <div class="text-gray-400 text-xs">
            ç¡®è®¤åŽå°†è‡ªåŠ¨ç”Ÿæˆé‡‡è´­è®¢å•ï¼Œè¯¥æ“ä½œä¸å¯æ’¤é”€ã€‚
          </div>
        </a-form>
      </a-modal>
      <!-- å®šæ ‡è¡¨å• -->
      <a-modal v-model:open="awardFormVisible" title="创建定标" :width="500" @ok="handleCreateAward">
        <a-form layout="vertical" class="mt-4">
src/views/srm/tender/index.vue
@@ -4,24 +4,28 @@
import { Page, useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { message, Select } from 'ant-design-vue';
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  closeTender,
  confirmTenderProject,
  deleteTenderProject,
  generatePurchaseOrder,
  getBidListByProject,
  getTenderProjectPage,
  publishTender,
  startBidding,
  startEvaluation,
  startPriceComparing,
} from '#/api/srm/tender';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import TenderForm from './modules/form.vue';
import { TENDER_STATUS } from '../enums';
import { BIDDING_MODE, TENDER_STATUS } from '../enums';
defineOptions({ name: 'SrmTender' });
@@ -88,6 +92,64 @@
  handleRefresh();
}
async function handleStartPriceComparing(row: SrmTenderApi.TenderProjectVO) {
  await startPriceComparing(row.id!);
  message.success('已开始比价');
  handleRefresh();
}
// ç¡®è®¤ä¸­æ ‡ç›¸å…³
const confirmTarget = ref<SrmTenderApi.TenderProjectVO | null>(null);
const confirmBidId = ref<number>();
const confirmBidOptions = ref<{ label: string; value: number }[]>([]);
const confirmLoading = ref(false);
async function openConfirmModal(row: SrmTenderApi.TenderProjectVO) {
  confirmTarget.value = row;
  confirmBidId.value = undefined;
  confirmLoading.value = true;
  try {
    const bids = await getBidListByProject(row.id!);
    confirmBidOptions.value = (bids || []).map((b: any) => ({
      label: `${b.supplierName} - ${b.bidNo} (Â¥${b.bidTotalAmount ?? 0})`,
      value: b.id!,
    }));
  } finally {
    confirmLoading.value = false;
  }
  confirmModalApi.open();
}
async function handleConfirmBid() {
  if (!confirmTarget.value || !confirmBidId.value) {
    message.warning('请选择中标供应商的投标记录');
    return;
  }
  confirmLoading.value = true;
  try {
    await confirmTenderProject({
      id: confirmTarget.value.id!,
      bidId: confirmBidId.value,
    });
    message.success('确认中标成功,采购订单已自动生成');
    confirmModalApi.close();
    handleRefresh();
  } finally {
    confirmLoading.value = false;
  }
}
const [ConfirmModal, confirmModalApi] = useVbenModal({
  title: '确认中标',
  onConfirm: handleConfirmBid,
  onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      confirmTarget.value = null;
      confirmBidId.value = undefined;
    }
  },
});
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: TenderForm,
  destroyOnClose: true,
@@ -121,6 +183,18 @@
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <ConfirmModal>
      <div class="flex items-center gap-2">
        <span class="font-medium whitespace-nowrap">中标投标记录:</span>
        <Select
          v-model:value="confirmBidId"
          :options="confirmBidOptions"
          :loading="confirmLoading"
          placeholder="请选择中标的投标记录"
          class="flex-1"
        />
      </div>
    </ConfirmModal>
    <Grid table-title="招标项目列表">
      <template #toolbar-tools>
        <TableAction
@@ -148,7 +222,7 @@
              label: '发布',
              type: 'link',
              auth: ['srm:tender-project:update'],
              ifShow: row.tenderStatus === TENDER_STATUS.DRAFT,
              ifShow: row.tenderStatus === TENDER_STATUS.DRAFT && row.biddingMode !== BIDDING_MODE.SIMPLE,
              popConfirm: {
                title: '确认要发布该项目吗?发布后无法修改。',
                confirm: handlePublish.bind(null, row),
@@ -167,7 +241,7 @@
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['srm:tender-project:update'],
              ifShow: row.tenderStatus === TENDER_STATUS.PUBLISHED || row.tenderStatus === TENDER_STATUS.BIDDING,
              ifShow: (row.tenderStatus === TENDER_STATUS.PUBLISHED || row.tenderStatus === TENDER_STATUS.BIDDING) && row.biddingMode !== BIDDING_MODE.SIMPLE,
              onClick: handleEdit.bind(null, row),
            },
            {
@@ -186,7 +260,7 @@
              label: '开始投标',
              type: 'link',
              auth: ['srm:tender-project:update'],
              ifShow: row.tenderStatus === TENDER_STATUS.PUBLISHED,
              ifShow: row.tenderStatus === TENDER_STATUS.PUBLISHED && row.biddingMode !== BIDDING_MODE.SIMPLE,
              popConfirm: {
                title: '确认要开始投标吗?',
                confirm: handleStartBidding.bind(null, row),
@@ -197,7 +271,7 @@
              type: 'link',
              danger: true,
              auth: ['srm:tender-project:update'],
              ifShow: row.tenderStatus === TENDER_STATUS.PUBLISHED || row.tenderStatus === TENDER_STATUS.BIDDING || row.tenderStatus === TENDER_STATUS.EVALUATING,
              ifShow: row.tenderStatus === TENDER_STATUS.PUBLISHED || row.tenderStatus === TENDER_STATUS.BIDDING || row.tenderStatus === TENDER_STATUS.EVALUATING || row.tenderStatus === TENDER_STATUS.PRICE_COMPARING,
              popConfirm: {
                title: '确认要关闭该项目吗?',
                confirm: handleClose.bind(null, row),
@@ -207,7 +281,7 @@
              label: '开始评标',
              type: 'link',
              auth: ['srm:tender-project:update'],
              ifShow: row.tenderStatus === TENDER_STATUS.BIDDING,
              ifShow: row.tenderStatus === TENDER_STATUS.BIDDING && row.biddingMode !== BIDDING_MODE.SIMPLE,
              popConfirm: {
                title: '确认要开始评标吗?',
                confirm: handleStartEvaluation.bind(null, row),
@@ -217,12 +291,29 @@
              label: '生成采购订单',
              type: 'link',
              auth: ['srm:bid-award:update'],
              ifShow: !!row.awardId && !row.purchaseOrderNo,
              ifShow: !!row.awardId && !row.purchaseOrderNo && row.biddingMode !== BIDDING_MODE.SIMPLE,
              popConfirm: {
                title: '确认要生成采购订单吗?',
                confirm: handleGeneratePurchaseOrder.bind(null, row),
              },
            },
            {
              label: '开始比价',
              type: 'link',
              auth: ['srm:tender-project:update'],
              ifShow: row.biddingMode === BIDDING_MODE.SIMPLE && row.tenderStatus === TENDER_STATUS.DRAFT,
              popConfirm: {
                title: '确认要开始比价吗?',
                confirm: handleStartPriceComparing.bind(null, row),
              },
            },
            {
              label: '确认中标',
              type: 'link',
              auth: ['srm:tender-project:update'],
              ifShow: row.biddingMode === BIDDING_MODE.SIMPLE && row.tenderStatus === TENDER_STATUS.PRICE_COMPARING,
              onClick: openConfirmModal.bind(null, row),
            },
          ]"
        />
      </template>
src/views/srm/tender/modules/form.vue
@@ -14,6 +14,7 @@
  updateTenderProject,
  updateTenderProjectPartial,
} from '#/api/srm/tender';
import { getTenderConfig } from '#/api/srm/tender-config';
import { $t } from '#/locales';
import { TENDER_STATUS } from '../../enums';
@@ -77,6 +78,15 @@
    }
    const data = modalApi.getData<SrmTenderApi.TenderProjectVO>();
    if (!data || !data.id) {
      // æ–°å»ºæ¨¡å¼ï¼šåŠ è½½é…ç½®èŽ·å–é»˜è®¤æ‹›æŠ•æ ‡æ¨¡å¼
      try {
        const config = await getTenderConfig();
        if (config?.defaultBiddingMode) {
          await formApi.setFieldValue('biddingMode', config.defaultBiddingMode);
        }
      } catch {
        // é…ç½®åŠ è½½å¤±è´¥ï¼Œä½¿ç”¨é»˜è®¤å€¼
      }
      return;
    }
    modalApi.lock();
src/views/wls/stocktaking/plan/components/condition-value-input.vue
@@ -1,7 +1,7 @@
<script lang="ts" setup>
import type { NumberDictDataType } from '@vben/hooks';
import { ref, watch } from 'vue';
import { computed, ref, watch } from 'vue';
import { DICT_TYPE, WlsStockTakingParamTypeEnum } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
@@ -24,16 +24,19 @@
  defineProps<{
    modelValue?: number;
    type?: number;
    value?: number;
    valueCode?: string;
  }>(),
  {
    modelValue: undefined,
    type: undefined,
    value: undefined,
    valueCode: undefined,
  },
);
const emit = defineEmits<{
  'update:modelValue': [value?: number];
  'update:value': [value?: number];
  valueChange: [
    payload: { valueCode?: string; valueId?: number; valueName?: string },
  ];
@@ -44,6 +47,9 @@
  'number',
) as NumberDictDataType[];
/** å…¼å®¹ VbenForm é»˜è®¤ v-model prop ('value') å’Œ Vue 3 æ ‡å‡† ('modelValue') */
const effectiveValue = computed(() => props.value ?? props.modelValue);
const locationWarehouseId = ref<number>(); // åº“区选择器临时数据:选仓库后传给库区选择器
const areaWarehouseId = ref<number>(); // åº“位选择器临时数据:选仓库后传给库区选择器
const areaLocationId = ref<number>(); // åº“位选择器临时数据:选库区后传给库位选择器
@@ -53,6 +59,7 @@
/** é€šç”¨é€‰æ‹©å™¨å˜åŒ–:回填条件值编码、名称 */
function handleSelectorChange(item?: any) {
  emit('update:modelValue', item?.id);
  emit('update:value', item?.id);
  emit('valueChange', {
    valueId: item?.id,
    valueCode: item?.code || '',
@@ -63,6 +70,7 @@
/** æ‰¹æ¬¡é€‰æ‹©å™¨å˜åŒ– */
function handleBatchChange(batch?: any) {
  emit('update:modelValue', batch?.id);
  emit('update:value', batch?.id);
  emit('valueChange', {
    valueId: batch?.id,
    valueCode: batch?.code || '',
@@ -75,6 +83,7 @@
  qualityStatusValue.value = value;
  const selected = qualityStatusOptions.find((item) => item.value === value);
  emit('update:modelValue', undefined);
  emit('update:value', undefined);
  emit('valueChange', {
    valueId: undefined,
    valueCode: value === null ? '' : String(value),
@@ -85,6 +94,7 @@
/** åº“区仓库选择回调:清空库区 */
function handleLocationWarehouseChange() {
  emit('update:modelValue', undefined);
  emit('update:value', undefined);
  emit('valueChange', { valueId: undefined, valueCode: '', valueName: '' });
}
@@ -92,12 +102,14 @@
function handleAreaWarehouseChange() {
  areaLocationId.value = undefined;
  emit('update:modelValue', undefined);
  emit('update:value', undefined);
  emit('valueChange', { valueId: undefined, valueCode: '', valueName: '' });
}
/** åº“位库区选择回调:清空库位 */
function handleAreaLocationChange() {
  emit('update:modelValue', undefined);
  emit('update:value', undefined);
  emit('valueChange', { valueId: undefined, valueCode: '', valueName: '' });
}
@@ -109,14 +121,14 @@
      : undefined;
    return;
  }
  if (!props.modelValue) {
  if (!effectiveValue.value) {
    return;
  }
  if (props.type === WlsStockTakingParamTypeEnum.LOCATION) {
    const location = await getWarehouseLocation(props.modelValue);
    const location = await getWarehouseLocation(effectiveValue.value);
    locationWarehouseId.value = location?.warehouseId;
  } else if (props.type === WlsStockTakingParamTypeEnum.AREA) {
    const area = await getWarehouseArea(props.modelValue);
    const area = await getWarehouseArea(effectiveValue.value);
    areaWarehouseId.value = area?.warehouseId;
    areaLocationId.value = area?.locationId;
  }
@@ -139,7 +151,7 @@
<template>
  <WmWarehouseSelect
    v-if="type === WlsStockTakingParamTypeEnum.WAREHOUSE"
    :model-value="modelValue"
    :model-value="effectiveValue"
    @change="handleSelectorChange"
  />
  <div
@@ -153,7 +165,7 @@
    />
    <WmWarehouseLocationSelect
      v-if="locationWarehouseId"
      :model-value="modelValue"
      :model-value="effectiveValue"
      :warehouse-id="locationWarehouseId"
      placeholder="请选择库区"
      @change="handleSelectorChange"
@@ -177,7 +189,7 @@
    />
    <WmWarehouseAreaSelect
      v-if="areaLocationId"
      :model-value="modelValue"
      :model-value="effectiveValue"
      :location-id="areaLocationId"
      placeholder="请选择库位"
      @change="handleSelectorChange"
@@ -185,12 +197,12 @@
  </div>
  <MdItemSelect
    v-else-if="type === WlsStockTakingParamTypeEnum.ITEM"
    :model-value="modelValue"
    :model-value="effectiveValue"
    @change="handleSelectorChange"
  />
  <WmBatchSelect
    v-else-if="type === WlsStockTakingParamTypeEnum.BATCH"
    :model-value="modelValue"
    :model-value="effectiveValue"
    @change="handleBatchChange"
  />
  <Select
src/views/wls/stocktaking/plan/data.ts
@@ -326,8 +326,8 @@
      component: markRaw(StockTakingPlanConditionValueInput),
      // æ¡ä»¶å€¼æŽ§ä»¶å†…部按条件类型切换选择器,仅选择类型后展示
      dependencies: {
        triggerFields: ['type'],
        if: (values) => values.type !== null,
        triggerFields: ['type', 'valueCode'],
        show: (values) => values.type != null,
        componentProps: (values) => ({
          type: values.type,
          valueCode: values.valueCode,