liu
3 天以前 d3230d4bb80fbc7c2adb63c588c580ab94234ee8
Merge remote-tracking branch 'origin/dev_pro2.0' into dev_pro2.0

# Conflicts:
# src/views/erp/purchase/invoice/modules/form.vue
已添加2个文件
已修改25个文件
1068 ■■■■■ 文件已修改
.idea/mom-pro2-before.iml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
WTXC.bat 451 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
company.config.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/erp/purchase/order/index.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/feedback/index.ts 23 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/workorder/index.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/user/index.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/routes/modules/mes.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/finance/payment/data.ts 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/finance/payment/modules/form.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/invoice/data.ts 48 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/invoice/modules/form.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/invoice/modules/purchase-order-select.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/order/data.ts 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/sale/order/data.ts 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/feedback/data.ts 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/feedback/detail/index.vue 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/feedback/index.vue 30 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/feedback/modules/form.vue 63 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/task/data.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/task/modules/task-form.vue 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/workorder/data.ts 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/workorder/modules/form.vue 21 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/workbench/components/tabs/FeedbackHistoryTab.vue 32 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/workbench/components/tabs/FeedbackTab.vue 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/workbench/index.vue 29 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/workbench/types.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.idea/mom-pro2-before.iml
@@ -7,6 +7,8 @@
      <excludeFolder url="file://$MODULE_DIR$/.cache" />
      <excludeFolder url="file://$MODULE_DIR$/dist" />
      <excludeFolder url="file://$MODULE_DIR$/build" />
      <excludeFolder url="file://$MODULE_DIR$/.agents" />
      <excludeFolder url="file://$MODULE_DIR$/.codex" />
    </content>
    <orderEntry type="inheritedJdk" />
    <orderEntry type="sourceFolder" forTests="false" />
WTXC.bat
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,451 @@
@echo off
setlocal EnableExtensions EnableDelayedExpansion
REM ============================================================
REM Vue Frontend Deploy Script
REM ============================================================
REM =========================
REM Configuration
REM =========================
set "COMPANY=WTXC"
set "REMOTE_USER=root"
set "REMOTE_HOST=42.63.71.140"
set "REMOTE_PORT=22"
set "PASSWORD=Nxwtxc123."
set "ZIP_FILE=dist.zip"
set "REMOTE_DIR=/home/vue/dist"
set "SSH_OPTS=-o StrictHostKeyChecking=no -o ConnectTimeout=10 -o ServerAliveInterval=15 -o ServerAliveCountMax=3"
set "RETRY_COUNT=3"
set "SSHPASS_DIR=%~dp0bin"
REM ============================================================
REM Start
REM ============================================================
echo.
echo ============================================================
echo              Vue Dist Auto Deploy
echo ============================================================
echo.
echo Company    : %COMPANY%
echo Server     : %REMOTE_USER%@%REMOTE_HOST%
echo Port       : %REMOTE_PORT%
echo Remote Dir : %REMOTE_DIR%
echo Package    : %ZIP_FILE%
echo.
REM ============================================================
REM Check sshpass
REM ============================================================
echo [1/7] Checking sshpass...
if exist "%SSHPASS_DIR%\sshpass.exe" (
    set "PATH=%SSHPASS_DIR%;%PATH%"
)
where sshpass >nul 2>&1
if not errorlevel 1 (
    echo [OK] sshpass found.
    goto CHECK_PNPM
)
echo [WARN] sshpass not found.
if not exist "%SSHPASS_DIR%" (
    mkdir "%SSHPASS_DIR%" >nul 2>&1
)
if exist "%SSHPASS_DIR%\sshpass.exe" (
    set "PATH=%SSHPASS_DIR%;%PATH%"
    echo [OK] sshpass found.
    goto CHECK_PNPM
)
echo [INFO] Downloading sshpass...
powershell -NoProfile -ExecutionPolicy Bypass -Command "$url='https://github.com/xhcoding/sshpass-win32/releases/download/v1.0.3/sshpass.exe'; $out='%SSHPASS_DIR%\sshpass.exe'; Invoke-WebRequest -Uri $url -OutFile $out"
if errorlevel 1 (
    echo [ERROR] Failed to download sshpass.
    echo [ERROR] Please install sshpass manually.
    goto ERROR_EXIT
)
set "PATH=%SSHPASS_DIR%;%PATH%"
echo [OK] sshpass installed.
REM ============================================================
REM Check pnpm
REM ============================================================
:CHECK_PNPM
echo.
echo [2/7] Checking pnpm...
where pnpm >nul 2>&1
if errorlevel 1 (
    echo [ERROR] pnpm not found.
    echo [HINT] npm install -g pnpm
    goto ERROR_EXIT
)
call pnpm --version
if errorlevel 1 (
    echo [ERROR] pnpm execution failed.
    goto ERROR_EXIT
)
REM ============================================================
REM Check node
REM ============================================================
echo.
echo [3/7] Checking Node.js...
where node >nul 2>&1
if errorlevel 1 (
    echo [ERROR] Node.js not found.
    goto ERROR_EXIT
)
call node --version
if errorlevel 1 (
    echo [ERROR] Node.js execution failed.
    goto ERROR_EXIT
)
REM ============================================================
REM Check node_modules
REM ============================================================
if not exist "node_modules" (
    echo.
    echo [INFO] node_modules not found.
    echo [INFO] Running pnpm install...
    call pnpm install
    if errorlevel 1 (
        echo [ERROR] pnpm install failed.
        goto ERROR_EXIT
    )
    echo [OK] pnpm install completed.
) else (
    echo [OK] node_modules exists.
)
REM ============================================================
REM Build
REM ============================================================
echo.
echo [4/7] Building project...
if exist "%ZIP_FILE%" (
    echo.
    echo [WARN] %ZIP_FILE% already exists.
    set "REBUILD="
    set /p "REBUILD=Rebuild? Y/N (default Y): "
    if /I "!REBUILD!"=="N" (
        echo [INFO] Skip build.
        goto CHECK_ZIP
    )
)
echo [INFO] Company: %COMPANY%
echo [INFO] Command: pnpm run build
echo.
set "VITE_COMPANY=%COMPANY%"
call pnpm run build
if errorlevel 1 (
    echo.
    echo [ERROR] Build failed.
    goto ERROR_EXIT
)
echo.
echo [OK] Build completed.
REM ============================================================
REM Check ZIP
REM ============================================================
:CHECK_ZIP
echo.
echo [5/7] Checking package...
if not exist "%ZIP_FILE%" (
    echo [ERROR] %ZIP_FILE% not found.
    echo [HINT] Please check whether your build generates dist.zip.
    goto ERROR_EXIT
)
for %%F in ("%ZIP_FILE%") do set "ZIP_BYTES=%%~zF"
set /a ZIP_MB=ZIP_BYTES/1024/1024
echo [OK] Package found.
echo [INFO] File size: !ZIP_BYTES! bytes
echo [INFO] File size: !ZIP_MB! MB
REM ============================================================
REM Set sshpass password
REM ============================================================
set "SSHPASS=%PASSWORD%"
REM ============================================================
REM Test SSH
REM ============================================================
echo.
echo [6/7] Testing SSH connection...
sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "echo SSH_OK"
if errorlevel 1 (
    echo [ERROR] SSH connection failed.
    goto ERROR_EXIT
)
echo [OK] SSH connection successful.
REM ============================================================
REM Create remote directory
REM ============================================================
echo.
echo [INFO] Creating remote directory...
sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "mkdir -p '%REMOTE_DIR%'"
if errorlevel 1 (
    echo [ERROR] Failed to create remote directory.
    goto ERROR_EXIT
)
echo [OK] Remote directory ready.
REM ============================================================
REM Upload temporary file
REM ============================================================
echo.
echo [INFO] Uploading package...
set "REMOTE_TMP=%REMOTE_DIR%/.dist.zip.uploading"
sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "rm -f '%REMOTE_TMP%'"
sshpass -e scp -P %REMOTE_PORT% %SSH_OPTS% "%ZIP_FILE%" %REMOTE_USER%@%REMOTE_HOST%:"%REMOTE_TMP%"
if errorlevel 1 (
    echo [ERROR] Upload failed.
    goto ERROR_EXIT
)
echo [OK] Upload completed.
REM ============================================================
REM Check remote file
REM ============================================================
echo.
echo [INFO] Checking remote package...
sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "test -f '%REMOTE_TMP%'"
if errorlevel 1 (
    echo [ERROR] Remote package does not exist.
    goto ERROR_EXIT
)
echo [OK] Remote package exists.
REM ============================================================
REM Compare file size
REM ============================================================
echo.
echo [INFO] Checking package size...
sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "stat -c %%s '%REMOTE_TMP%'"
echo.
REM ============================================================
REM Rename temporary file
REM ============================================================
echo [INFO] Activating package...
sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "mv -f '%REMOTE_TMP%' '%REMOTE_DIR%/%ZIP_FILE%'"
if errorlevel 1 (
    echo [ERROR] Failed to activate package.
    goto ERROR_EXIT
)
echo [OK] Package activated.
REM ============================================================
REM Check unzip
REM ============================================================
echo.
echo [INFO] Checking unzip...
sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "command -v unzip"
if errorlevel 1 (
    echo [ERROR] unzip is not installed on server.
    echo.
    echo CentOS/RHEL:
    echo yum install -y unzip
    echo.
    echo Ubuntu/Debian:
    echo apt install -y unzip
    goto ERROR_EXIT
)
echo [OK] unzip found.
REM ============================================================
REM Test ZIP
REM ============================================================
echo.
echo [INFO] Testing remote ZIP...
sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "cd '%REMOTE_DIR%' && unzip -t '%ZIP_FILE%'"
if errorlevel 1 (
    echo.
    echo [ERROR] ZIP test failed.
    echo [INFO] Remote ZIP has been kept.
    goto ERROR_EXIT
)
echo [OK] ZIP test passed.
REM ============================================================
REM Extract
REM ============================================================
echo.
echo [INFO] Extracting package...
sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "cd '%REMOTE_DIR%' && unzip -o '%ZIP_FILE%'"
if errorlevel 1 (
    echo.
    echo [ERROR] Remote unzip failed.
    echo [INFO] Remote ZIP has been kept.
    goto ERROR_EXIT
)
echo.
echo [OK] Extract completed.
REM ============================================================
REM Remove ZIP
REM ============================================================
echo.
echo [INFO] Removing remote ZIP...
sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "rm -f '%REMOTE_DIR%/%ZIP_FILE%'"
if errorlevel 1 (
    echo [WARN] Failed to remove remote ZIP.
) else (
    echo [OK] Remote ZIP removed.
)
REM ============================================================
REM Success
REM ============================================================
echo.
echo ============================================================
echo                 DEPLOY SUCCESS
echo ============================================================
echo.
echo Company    : %COMPANY%
echo Server     : %REMOTE_HOST%:%REMOTE_PORT%
echo Remote Dir : %REMOTE_DIR%
echo Package    : %ZIP_FILE%
echo Size       : !ZIP_MB! MB
echo.
echo Deployment completed successfully.
echo.
goto END
REM ============================================================
REM Error
REM ============================================================
:ERROR_EXIT
echo.
echo ============================================================
echo                  DEPLOY FAILED
echo ============================================================
echo.
echo Please check the error message above.
echo.
goto END
REM ============================================================
REM End
REM ============================================================
:END
pause
endlocal
company.config.ts
@@ -35,4 +35,10 @@
    favicon: '/logos/YZFXLogo.ico',
    apiUrl: 'http://42.63.70.231:9001/admin-api',
  },
  WTXC: {
    name: '宁夏万通新材',
    logo: '/logos/YZFXLogo.png',
    favicon: '/logos/YZFXLogo.ico',
    apiUrl: 'http://42.63.71.140:9001/admin-api',
  },
};
src/api/erp/purchase/order/index.ts
@@ -13,6 +13,7 @@
    totalCount?: number; // åˆè®¡æ•°é‡
    totalPrice?: number; // åˆè®¡é‡‘额,单位:元
    totalProductPrice?: number; // äº§å“é‡‘额,单位:元
    totalTaxPrice?: number; // åˆè®¡ç¨Žé¢ï¼Œå•位:元
    discountPercent?: number; // ä¼˜æƒ çŽ‡ï¼Œç™¾åˆ†æ¯”
    discountPrice?: number; // ä¼˜æƒ é‡‘额,单位:元
    depositPrice?: number; // å®šé‡‘金额,单位:元
src/api/mes/pro/feedback/index.ts
@@ -1,8 +1,23 @@
import type { PageParam, PageResult } from '@vben/request';
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace MesProFeedbackApi {
  export interface FeedbackSubmitReqVO {
    id: number;
    produceDate?: string;
    expireDate?: string;
    receiptDate?: string;
    vendorId?: number;
    clientId?: number;
    purchaseOrderCode?: string;
    salesOrderCode?: string;
    toolId?: number;
    moldId?: number;
    lotNumber?: string;
    qualityStatus?: number;
  }
  /** MES ç”Ÿäº§æŠ¥å·¥ */
  export interface Feedback {
    id?: number;
@@ -47,6 +62,8 @@
    remark?: string; // å¤‡æ³¨
    creator?: string; // åˆ›å»ºäºº
    createTime?: number; // åˆ›å»ºæ—¶é—´
    processInstanceId?: string; // BPM æµç¨‹å®žä¾‹ç¼–号
    processDefinitionKey?: string; // BPM æµç¨‹å®šä¹‰ Key
  }
  /** MES ç”Ÿäº§æŠ¥å·¥åˆ†é¡µæŸ¥è¯¢å‚æ•° */
@@ -100,8 +117,8 @@
}
/** æäº¤ç”Ÿäº§æŠ¥å·¥ */
export function submitFeedback(id: number) {
  return requestClient.put(`/mes/pro/feedback/submit?id=${id}`);
export function submitFeedback(data: MesProFeedbackApi.FeedbackSubmitReqVO) {
  return requestClient.put(`/mes/pro/feedback/submit`, data);
}
/** é©³å›žç”Ÿäº§æŠ¥å·¥ */
src/api/mes/pro/workorder/index.ts
@@ -40,6 +40,9 @@
    completionProgress?: number; // å®Œæˆè¿›åº¦
    remark?: string; // å¤‡æ³¨
    createTime?: number; // åˆ›å»ºæ—¶é—´
    feedbackApprove?: boolean; // æ˜¯å¦æŠ¥å·¥å®¡æ‰¹
    processDefinitionKey?: string; // æŠ¥å·¥å®¡æ‰¹æµç¨‹å®šä¹‰ Key
    feedbackRole?: string; // æŠ¥å·¥è§’色
  }
  /** MES ç”Ÿäº§è®¢å•分页查询参数 */
@@ -159,3 +162,10 @@
    `/mes/pro/work-order/check-auto-finish/${id}`,
  );
}
/** èŽ·å¾—ç”Ÿäº§è®¢å•å®¡æ‰¹æµç¨‹åˆ—è¡¨ */
export function getWorkOrderApproveProcessList() {
  return requestClient.get<Array<{ id: string; name: string; key: string }>>(
    '/mes/pro/work-order/approve-process-list',
  );
}
src/api/system/user/index.ts
@@ -116,8 +116,8 @@
}
/** é€šè¿‡è§’色标识获取用户精简信息列表 */
export function getSimpleUserListByRoleCode(code: string) {
export function getSimpleUserListByRoleCode(code: Object) {
  return requestClient.get<SystemUserApi.UserSimple[]>('/system/user/list-by-role-code', {
    params: { code },
    params:  code ,
  });
}
src/router/routes/modules/mes.ts
@@ -29,6 +29,16 @@
        },
        component: () => import('#/views/mes/pd/project/detail/index.vue'),
      },
      {
        path: 'pro/feedback/detail/:id',
        name: 'MesProFeedbackDetail',
        meta: {
          title: '生产报工详情',
          activePath: '/mes/pro/feedback',
        },
        props: true,
        component: () => import('#/views/mes/pro/feedback/detail/index.vue'),
      },
    ],
  },
];
src/views/erp/finance/payment/data.ts
@@ -152,18 +152,14 @@
      formItemClass: 'col-span-3',
    },
    {
      fieldName: 'accountId',
      fieldName: 'accountName',
      label: '付款账户',
      component: 'ApiSelect',
      component: 'Input',
      rules: 'required',
      componentProps: {
        placeholder: '请选择付款账户',
        placeholder: '请输入付款账户',
        allowClear: true,
        showSearch: true,
        api: getAccountSimpleList,
        labelField: 'name',
        valueField: 'id',
      },
      help: '付款账户数据来自【财务管理 â†’ ç»“算账户】菜单维护的开启状态账户,无选项时请先在结算账户页面新增。',
    },
    {
      fieldName: 'totalPrice',
src/views/erp/finance/payment/modules/form.vue
@@ -29,7 +29,7 @@
  id: undefined,
  no: '',
  supplierId: undefined,
  accountId: undefined,
  accountName: undefined,
  financeUserId: undefined,
  paymentTime: undefined,
  remark: '',
@@ -176,11 +176,11 @@
    formApi.setDisabled(formType.value === 'detail');
    formApi.updateSchema(useFormSchema(formType.value));
    if (!data || !data.id) {
      // æ–°å¢žæ—¶ï¼Œé»˜è®¤é€‰ä¸­è´¦æˆ·
      // æ–°å¢žæ—¶ï¼Œé»˜è®¤å¡«å…¥é»˜è®¤ç»“算账户名称
      const accountList = await getAccountSimpleList();
      const defaultAccount = accountList.find((item) => item.defaultStatus);
      if (defaultAccount) {
        await formApi.setValues({ accountId: defaultAccount.id });
        await formApi.setValues({ accountName: defaultAccount.name });
      }
      return;
    }
src/views/erp/purchase/invoice/data.ts
@@ -1,7 +1,10 @@
import type { Ref } from 'vue';
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { getSupplierSimpleList } from '#/api/srm/supplier';
import { z } from '#/adapter/form';
import { markRaw } from 'vue';
@@ -11,7 +14,9 @@
export type FormType = 'create' | 'detail' | 'edit';
/** æ–°å¢ž/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
export function useFormSchema(
  orderTotalTaxPrice?: Ref<number | undefined>,
): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
@@ -41,8 +46,15 @@
      },
      componentProps: (_values, form) => ({
        placeholder: '请选择采购订单',
        onChange: (order: { supplierName?: string } | undefined) => {
        onChange: (
          order:
            | { supplierName?: string; totalTaxPrice?: number }
            | undefined,
        ) => {
          form.setFieldValue('supplierName', order?.supplierName);
          if (orderTotalTaxPrice) {
            orderTotalTaxPrice.value = order?.totalTaxPrice;
          }
        },
      }),
    },
@@ -78,11 +90,37 @@
      label: '来票金额(元)',
      component: 'InputNumber',
      rules: 'required',
      description: () =>
        orderTotalTaxPrice?.value != null
          ? `采购订单未开票金额:${Number(orderTotalTaxPrice.value).toFixed(2)} å…ƒ`
          : '',
      componentProps: {
        class: '!w-full',
        placeholder: '请输入来票金额',
        min: 0,
        precision: 2,
      },
      dependencies: {
        triggerFields: ['purchaseOrderId'],
        componentProps: () => ({
          class: '!w-full',
          placeholder: '请输入来票金额',
          min: 0,
          max: orderTotalTaxPrice?.value ?? undefined,
          precision: 2,
        }),
        rules: () => {
          const max = orderTotalTaxPrice?.value;
          if (max != null) {
            return z
              .number({ message: '请输入来票金额' })
              .min(0, '来票金额不能小于 0')
              .max(max, `来票金额不能大于未开票金额 ${Number(max).toFixed(2)}`);
          }
          return z
            .number({ message: '请输入来票金额' })
            .min(0, '来票金额不能小于 0');
        },
      },
    },
    {
@@ -180,12 +218,6 @@
    {
      title: '来票金额(元)',
      field: 'price',
      minWidth: 150,
      formatter: 'formatAmount2',
    },
    {
      title: '剩余可付金额',
      field: 'remainingInvoicePrice',
      minWidth: 150,
      formatter: 'formatAmount2',
    },
src/views/erp/purchase/invoice/modules/form.vue
@@ -24,6 +24,7 @@
const formData = ref<ErpPurchaseInvoiceApi.PurchaseInvoice>();
const formType = ref<FormType>('create');
const ocrSupplierName = ref('');
const orderTotalTaxPrice = ref<number>(); // æ‰€é€‰é‡‡è´­è®¢å•合计税额(未开票金额,用于提示与校验)
const getTitle = computed(() => {
  if (formType.value === 'create') {
@@ -44,7 +45,7 @@
  },
  wrapperClass: 'grid-cols-2',
  layout: 'horizontal',
  schema: useFormSchema(),
  schema: useFormSchema(orderTotalTaxPrice),
  showDefaultActions: false,
});
@@ -74,6 +75,7 @@
    if (!isOpen) {
      formData.value = undefined;
      ocrSupplierName.value = '';
      orderTotalTaxPrice.value = undefined;
      return;
    }
    const data = modalApi.getData<{
@@ -83,7 +85,7 @@
    }>();
    formType.value = data.formType;
    formApi.setDisabled(formType.value === 'detail');
    formApi.updateSchema(useFormSchema());
    formApi.updateSchema(useFormSchema(orderTotalTaxPrice));
    await formApi.resetForm();
    // OCR è¯†åˆ«æ•°æ®é¢„填(新建场景,编辑/详情不处理)
    const ocr = data?.ocrData;
src/views/erp/purchase/invoice/modules/purchase-order-select.vue
@@ -92,6 +92,12 @@
        minWidth: 120,
      },
      {
        field: 'totalTaxPrice',
        title: '未开票金额',
        formatter: 'formatAmount2',
        minWidth: 120,
      },
      {
        field: 'status',
        title: '状态',
        minWidth: 100,
src/views/erp/purchase/order/data.ts
@@ -65,14 +65,27 @@
    },
    {
      component: 'InputNumber',
      componentProps: {
      componentProps: (values) => ({
        class: '!w-full',
        placeholder: '请输入支付订金',
        precision: 2,
        min: 0,
      },
        max: values.totalPrice ?? undefined,
      }),
      fieldName: 'depositPrice',
      label: '支付订金',
      dependencies: {
        triggerFields: ['totalPrice'],
        rules(values) {
          if (values.totalPrice != null) {
            return z
              .number()
              .min(0, '订金不能小于0')
              .max(values.totalPrice, '订金不能大于优惠后金额');
          }
          return z.number().min(0, '订金不能小于0');
        },
      },
      rules: 'required',
    },
    {
src/views/erp/sale/order/data.ts
@@ -181,14 +181,27 @@
    },
    {
      component: 'InputNumber',
      componentProps: {
      componentProps: (values) => ({
        class: '!w-full',
        placeholder: '请输入收取订金',
        precision: 2,
        min: 0,
      },
        max: values.totalPrice ?? undefined,
      }),
      fieldName: 'depositPrice',
      label: '收取订金',
      dependencies: {
        triggerFields: ['totalPrice'],
        rules(values) {
          if (values.totalPrice != null) {
            return z
              .number()
              .min(0, '订金不能小于0')
              .max(values.totalPrice, '收取订金不能大于优惠后金额');
          }
          return z.number().min(0, '订金不能小于0').optional();
        },
      },
      rules: z.number().min(0).optional(),
    },
  ];
src/views/mes/pro/feedback/data.ts
@@ -2,6 +2,7 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProFeedbackApi } from '#/api/mes/pro/feedback';
import type { MesProTaskApi } from '#/api/mes/pro/task';
import type { DescriptionItemSchema } from '#/components/description';
import { h, markRaw } from 'vue';
@@ -23,6 +24,7 @@
import { MdWorkstationSelect } from '#/views/mes/md/workstation/components';
import { ProTaskSelect } from '#/views/mes/pro/task/components';
import { ProWorkOrderSelect } from '#/views/mes/pro/workorder/components';
import { DictTag } from '#/components/dict-tag';
/** ç”Ÿäº§æŠ¥å·¥è¡¨å•类型 */
export type FormType = 'approve' | 'create' | 'detail' | 'submit' | 'update';
@@ -601,3 +603,34 @@
    },
  ];
}
/** ç”Ÿäº§æŠ¥å·¥è¯¦æƒ…摘要 */
export function useDetailSchema(): DescriptionItemSchema[] {
  return [
    { field: 'code', label: '报订单号' },
    {
      field: 'type',
      label: '报工类型',
      render: (val) =>
        h(DictTag, { type: DICT_TYPE.MES_PRO_FEEDBACK_TYPE, value: val }),
    },
    { field: 'workOrderId', label: '生产订单', render: (_val, data) => data.workOrderCode },
    { field: 'taskId', label: '生产任务', render: (_val, data) => data.taskCode },
    { field: 'workstationName', label: '工作站' },
    { field: 'itemCode', label: '产品编码' },
    { field: 'itemName', label: '产品名称' },
    { field: 'unitMeasureName', label: '单位' },
    { field: 'itemSpecification', label: '规格' },
    { field: 'reportableQuantity', label: '可报工数量' },
    { field: 'feedbackQuantity', label: '报工数量' },
    { field: 'qualifiedQuantity', label: '合格品数量' },
    { field: 'unqualifiedQuantity', label: '不良品数量' },
    { field: 'laborScrapQuantity', label: '工废数量' },
    { field: 'materialScrapQuantity', label: '料废数量' },
    { field: 'otherScrapQuantity', label: '其他废品' },
    { field: 'feedbackUserNickname', label: '报工人' },
    { field: 'feedbackTime', label: '报工时间' },
    { field: 'approveUserNickname', label: '审核人' },
    { field: 'remark', label: '备注' },
  ];
}
src/views/mes/pro/feedback/detail/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,67 @@
<script lang="ts" setup>
import type { MesProFeedbackApi } from '#/api/mes/pro/feedback';
import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { Page } from '@vben/common-ui';
import { useTabs } from '@vben/hooks';
import { Card, Tabs } from 'ant-design-vue';
import { TableAction } from '#/adapter/vxe-table';
import { getFeedback } from '#/api/mes/pro/feedback';
import { useDescription } from '#/components/description';
import { useDetailSchema } from '../data';
import ItemConsumeList from '../modules/item-consume-list.vue';
import ProductProduceList from '../modules/product-produce-list.vue';
const props = defineProps<{ id: string }>();
const router = useRouter();
const tabs = useTabs();
const loading = ref(false);
const feedbackId = ref(0);
const feedback = ref<MesProFeedbackApi.Feedback>({});
const activeTab = ref('itemConsume');
const [Descriptions] = useDescription({
  bordered: false,
  column: 4,
  schema: useDetailSchema(),
});
async function loadFeedbackDetail() {
  loading.value = true;
  try {
    feedback.value = await getFeedback(feedbackId.value);
  } finally {
    loading.value = false;
  }
}
function handleBack() {
  tabs.closeCurrentTab();
  router.push({ path: '/mes/pro/feedback' });
}
onMounted(() => {
  feedbackId.value = Number(props.id);
  loadFeedbackDetail();
});
</script>
<template>
  <Page
    auto-content-height
    :loading="loading"
    :title="feedback.code ? `生产报工详情 - ${feedback.code}` : '生产报工详情'"
  >
    <Card class="min-h-[10%]">
      <Descriptions :data="feedback" />
    </Card>
  </Page>
</template>
src/views/mes/pro/feedback/index.vue
@@ -19,6 +19,7 @@
import { $t } from '#/locales';
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
@@ -26,6 +27,7 @@
const userStore = useUserStore();
const currentUserId = userStore.userInfo?.id; // å½“前登录用户 ID,用于审批人权限判断
const router = useRouter();
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
@@ -57,6 +59,15 @@
/** å®¡æ‰¹ç”Ÿäº§æŠ¥å·¥ */
function handleApprove(row: MesProFeedbackApi.Feedback) {
  formModalApi.setData({ formType: 'approve', id: row.id }).open();
}
/** æŸ¥çœ‹å®¡æ‰¹è¯¦æƒ… */
function handleViewProcess(row: MesProFeedbackApi.Feedback) {
  if (!row.processInstanceId) {
    message.warning('该报工尚未提交审批');
    return;
  }
  router.push({ path: '/bpm/process-instance/detail', query: { id: row.processInstanceId } });
}
/** è¯¦æƒ…生产报工 */
@@ -165,14 +176,21 @@
              ifShow: () => row.status === MesProFeedbackStatusEnum.PREPARE,
              onClick: handleSubmit.bind(null, row),
            },
            // {
            //   label: $t('common.approve'),
            //   type: 'link',
            //   auth: ['mes:pro-feedback:approve'],
            //   ifShow: () =>
            //     row.status === MesProFeedbackStatusEnum.APPROVING &&
            //     row.approveUserId === currentUserId,
            //   onClick: handleApprove.bind(null, row),
            // },
            {
              label: $t('common.approve'),
              label: '查看审批',
              type: 'link',
              auth: ['mes:pro-feedback:approve'],
              ifShow: () =>
                row.status === MesProFeedbackStatusEnum.APPROVING &&
                row.approveUserId === currentUserId,
              onClick: handleApprove.bind(null, row),
              icon: ACTION_ICON.VIEW,
              ifShow: () => row.status === MesProFeedbackStatusEnum.APPROVING,
              onClick: handleViewProcess.bind(null, row),
            },
            {
              label: $t('common.delete'),
src/views/mes/pro/feedback/modules/form.vue
@@ -1,4 +1,4 @@
<script lang="ts" setup>
<script lang="ts" setup>
import type { FormType } from '../data';
import type { MesProFeedbackApi } from '#/api/mes/pro/feedback';
@@ -25,6 +25,7 @@
  updateFeedback,
} from '#/api/mes/pro/feedback';
import { getRouteProcessByRouteAndProcess } from '#/api/mes/pro/route/process';
import { getWorkOrder } from '#/api/mes/pro/workorder';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
@@ -142,6 +143,23 @@
  }
}
const batchDialogRef = ref<InstanceType<typeof BatchInputDialog>>();
const batchActionType = ref<'submit' | 'approve'>('');
/** æ‰§è¡Œæäº¤ï¼ˆå«æ‰¹æ¬¡å±žæ€§ï¼‰ */
async function doSubmit(batchFields?: Record<string, unknown>) {
  if (!formData.value?.id) return;
  modalApi.lock();
  try {
    await submitFeedback({ id: formData.value.id, ...batchFields });
    await modalApi.close();
    emit('success');
    message.success('报工单已提交');
  } finally {
    modalApi.unlock();
  }
}
/** æäº¤ï¼šä»…当表单较快照有变更时才先保存,再调用提交接口 */
async function handleSubmit() {
  const { valid } = await formApi.validate();
@@ -156,16 +174,40 @@
      await updateFeedback(data);
      originalSnapshot.value = JSON.stringify(data);
    }
    await submitFeedback(formData.value!.id!);
    await modalApi.close();
    emit('success');
    message.success('报订单已提交');
    // æ£€æŸ¥æ˜¯å¦éœ€è¦å®¡æ‰¹ï¼Œè‹¥éœ€è¦å®¡æ‰¹ï¼Œåˆ™ä¸éœ€è¦åœ¨æ­¤é˜¶æ®µå¡«å†™æ‰¹æ¬¡å±žæ€§ï¼Œç›´æŽ¥æäº¤
    let feedbackApprove = false;
    if (formData.value?.workOrderId) {
      try {
        const workOrder = await getWorkOrder(formData.value.workOrderId);
        feedbackApprove = workOrder?.feedbackApprove ?? false;
      } catch {
        // ignore
      }
    }
    if (feedbackApprove) {
      await doSubmit();
      return;
    }
    let checkResult;
    try {
      checkResult = await getBatchRequirements(formData.value!.id!);
    } catch {
      // ignore
    }
    if (checkResult?.needBatchInput) {
      batchActionType.value = 'submit';
      batchDialogRef.value?.open({ checkResult });
    } else {
      await doSubmit();
    }
  } finally {
    modalApi.unlock();
  }
}
const batchDialogRef = ref<InstanceType<typeof BatchInputDialog>>();
/** æ‰§è¡Œå®¡æ‰¹ï¼ˆå«æ‰¹æ¬¡å±žæ€§ï¼‰ */
async function doApprove(batchFields?: Record<string, unknown>) {
@@ -195,6 +237,7 @@
    const checkResult = await getBatchRequirements(formData.value.id);
    if (checkResult?.needBatchInput) {
      // éœ€è¦å¡«å†™æ‰¹æ¬¡å±žæ€§ï¼Œå¼¹çª—收集
      batchActionType.value = 'approve';
      batchDialogRef.value?.open({ checkResult });
    } else {
      await doApprove();
@@ -207,7 +250,11 @@
/** æ‰¹æ¬¡å¼¹çª—确认回调 */
function handleBatchConfirm(batchData: Record<string, unknown>) {
  doApprove(batchData);
  if (batchActionType.value === 'submit') {
    doSubmit(batchData);
  } else {
    doApprove(batchData);
  }
}
/** å®¡æ‰¹ä¸é€šè¿‡ */
src/views/mes/pro/task/data.ts
@@ -393,7 +393,7 @@
}
/** ç”Ÿäº§ä»»åŠ¡æ–°å¢ž/修改的表单 */
export function useTaskFormSchema(formApi?: VbenFormApi, workOrderQuantity?: Ref<number | undefined>): VbenFormSchema[] {
export function useTaskFormSchema(formApi?: VbenFormApi, workOrderQuantity?: Ref<number | undefined>, feedbackRole?: Ref<string | undefined>): VbenFormSchema[] {
  return [
    {
      fieldName: 'workstationId',
@@ -476,7 +476,7 @@
      componentProps: {
        placeholder: '请选择报工人',
        api: getSimpleUserListByRoleCode,
        params: { code: 'production_submit_work' },
        params: { code: feedbackRole?.value || 'production_submit_work' },
        labelField: 'nickname',
        valueField: 'id',
        mode: 'multiple',
src/views/mes/pro/task/modules/task-form.vue
@@ -9,6 +9,7 @@
import { useVbenForm } from '#/adapter/form';
import { createTask, getTask, updateTask } from '#/api/mes/pro/task';
import { getWorkOrder } from '#/api/mes/pro/workorder';
import { $t } from '#/locales';
import { useTaskFormSchema } from '../data';
@@ -23,6 +24,7 @@
  routeId?: number;
  workOrderId?: number;
}>({}); // æ–°å¢žä»»åŠ¡æ—¶çš„è®¢å• / å·¥åºä¸Šä¸‹æ–‡
const feedbackRole = ref<string>();
const getTitle = computed(() => {
  return formData.value?.id
@@ -72,8 +74,6 @@
      context.value = {};
      return;
    }
    formApi.setState({ schema: useTaskFormSchema(formApi, workOrderQuantity) });
    // åŠ è½½æ•°æ®
    const data = modalApi.getData<{
      colorCode?: string;
      id?: number;
@@ -83,30 +83,46 @@
      routeId?: number;
      workOrderId?: number;
    }>();
    let wid: number | undefined;
    if (data?.id) {
      modalApi.lock();
      try {
        formData.value = await getTask(data.id);
        wid = formData.value.workOrderId;
        // è®¾ç½®åˆ° values
        await formApi.setValues(formData.value);
      } finally {
        modalApi.unlock();
      }
      return;
    } else {
      context.value = {
        colorCode: data.colorCode,
        itemId: data.itemId,
        processId: data.processId,
        routeId: data.routeId,
        workOrderId: data.workOrderId,
      };
      wid = data.workOrderId;
      workOrderQuantity.value = data.quantity;
      await formApi.setValues({
        colorCode: data.colorCode || '#00AEF3',
        duration: 1,
        quantity: data.quantity,
      });
    }
    context.value = {
      colorCode: data.colorCode,
      itemId: data.itemId,
      processId: data.processId,
      routeId: data.routeId,
      workOrderId: data.workOrderId,
    };
    workOrderQuantity.value = data.quantity;
    await formApi.setValues({
      colorCode: data.colorCode || '#00AEF3',
      duration: 1,
      quantity: data.quantity,
    });
    if (wid) {
      try {
        const wo = await getWorkOrder(wid);
        feedbackRole.value = wo.feedbackRole;
      } catch {
        // ignore
      }
    }
    formApi.setState({ schema: useTaskFormSchema(formApi, workOrderQuantity, feedbackRole) });
  },
});
</script>
src/views/mes/pro/workorder/data.ts
@@ -1,6 +1,7 @@
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder';
import { type MesProWorkOrderApi, getWorkOrderApproveProcessList } from '#/api/mes/pro/workorder';
import { getSimpleRoleList } from '#/api/system/role';
import type { MesProWorkOrderBomApi } from '#/api/mes/pro/workorder/bom';
import type { MesProWorkOrderProcessApi } from '#/api/mes/pro/workorder/process';
@@ -237,6 +238,56 @@
      rules: 'required',
    },
    {
      fieldName: 'feedbackRole',
      label: '报工角色',
      component: 'ApiSelect',
      componentProps: {
        api: getSimpleRoleList,
        disabled: headerReadonly,
        labelField: 'name',
        valueField: 'code',
        mode: 'multiple',
        placeholder: '请选择报工角色',
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'feedbackApprove',
      label: '是否报工审批',
      component: 'Switch',
      componentProps: {
        class: '!w-auto',
        checkedChildren: '是',
        unCheckedChildren: '否',
        disabled: headerReadonly,
      },
      defaultValue: false,
      rules: 'required',
    },
    {
      fieldName: 'processDefinitionKey',
      label: '审批流程',
      component: 'ApiSelect',
      componentProps: {
        api: getWorkOrderApproveProcessList,
        disabled: headerReadonly,
        labelField: 'name',
        valueField: 'key',
        placeholder: '请选择审批流程',
      },
      dependencies: {
        triggerFields: ['feedbackApprove'],
        show: (values) => !!values.feedbackApprove,
        rules(values) {
          if (values.feedbackApprove) {
            return 'selectRequired';
          }
          return undefined;
        },
      },
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
src/views/mes/pro/workorder/modules/form.vue
@@ -127,7 +127,10 @@
    if (!valid || !formData.value?.id) return;
    modalApi.lock();
    try {
      const data = (await formApi.getValues()) as MesProWorkOrderApi.WorkOrder;
      const data = (await formApi.getValues()) as any;
      if (Array.isArray(data.feedbackRole)) {
        data.feedbackRole = data.feedbackRole.join(',');
      }
      await updateWorkOrder({ ...formData.value, ...data });
      if (tempProcessList.value.length > 0) {
        const processList = tempProcessList.value.map(item => ({
@@ -180,7 +183,10 @@
          if (isEditable.value) {
            const current = JSON.stringify(await formApi.getValues());
            if (current !== originalSnapshot.value) {
              const data = (await formApi.getValues()) as MesProWorkOrderApi.WorkOrder;
              const data = (await formApi.getValues()) as any;
              if (Array.isArray(data.feedbackRole)) {
                data.feedbackRole = data.feedbackRole.join(',');
              }
              await updateWorkOrder({ ...formData.value, ...data });
              originalSnapshot.value = current;
            }
@@ -242,7 +248,10 @@
      const { valid } = await formApi.validate();
      if (!valid) return;
      modalApi.lock();
      const data = (await formApi.getValues()) as MesProWorkOrderApi.WorkOrder;
      const data = (await formApi.getValues()) as any;
      if (Array.isArray(data.feedbackRole)) {
        data.feedbackRole = data.feedbackRole.join(',');
      }
      try {
        if (formData.value?.id) {
          // ç¼–辑模式
@@ -306,7 +315,11 @@
        modalApi.lock();
        try {
          formData.value = await getWorkOrder(data.id);
          await formApi.setValues(formData.value);
          const formValues: any = { ...formData.value };
          if (formValues.feedbackRole && typeof formValues.feedbackRole === 'string') {
            formValues.feedbackRole = formValues.feedbackRole.split(',');
          }
          await formApi.setValues(formValues);
          currentProductId.value = formData.value.productId;
        } finally {
          modalApi.unlock();
src/views/mes/workbench/components/tabs/FeedbackHistoryTab.vue
@@ -3,12 +3,14 @@
import type { MesProFeedbackApi } from '#/api/mes/pro/feedback';
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import {
  DICT_TYPE,
  MesProFeedbackStatusEnum,
} from '@vben/constants';
import { useVbenModal } from '@vben/common-ui';
import { useAccess } from '@vben/access';
import { Button, message, Popconfirm, Space } from 'ant-design-vue';
@@ -40,6 +42,9 @@
  connectedComponent: FeedbackForm,
  destroyOnClose: true,
});
const { hasAccessByCodes } = useAccess();
const router = useRouter();
const batchDialogRef = ref<InstanceType<typeof BatchInputDialog>>();
const approvingRow = ref<MesProFeedbackApi.Feedback>();
@@ -74,7 +79,7 @@
/** æäº¤ï¼ˆä»…草稿) */
async function handleSubmit(row: MesProFeedbackApi.Feedback) {
  await submitFeedback(row.id!);
  await submitFeedback({ id: row.id! });
  message.success('报工单已提交');
  refreshGrid();
  emit('reported');
@@ -108,6 +113,15 @@
  message.success('报工单已驳回');
  refreshGrid();
  emit('reported');
}
/** æŸ¥çœ‹å®¡æ‰¹è¯¦æƒ… */
function handleViewProcess(row: MesProFeedbackApi.Feedback) {
  if (!row.processInstanceId) {
    message.warning('该报工尚未提交审批');
    return;
  }
  router.push({ path: '/bpm/process-instance/detail', query: { id: row.processInstanceId } });
}
/** çŠ¶æ€å¸¸é‡ä¾›æ¨¡æ¿ä½¿ç”¨ */
@@ -274,9 +288,18 @@
                åˆ é™¤
              </Button>
            </Popconfirm>
            <!-- å®¡æ‰¹ä¸­ï¼šå®¡æ‰¹é€šè¿‡ã€é©³å›ž -->
            <Popconfirm
            <!-- å®¡æ‰¹ä¸­ï¼šæŸ¥çœ‹å®¡æ‰¹ -->
            <Button
              v-if="row.status === STATUS_APPROVING"
              type="link"
              size="small"
              @click="handleViewProcess(row)"
            >
              æŸ¥çœ‹å®¡æ‰¹
            </Button>
            <!--
            <Popconfirm
              v-if="row.status === STATUS_APPROVING && hasAccessByCodes(['mes:pro-feedback:approve'])"
              title="确认审批通过该报工单?"
              @confirm="handleApprove(row)"
            >
@@ -285,7 +308,7 @@
              </Button>
            </Popconfirm>
            <Popconfirm
              v-if="row.status === STATUS_APPROVING"
              v-if="row.status === STATUS_APPROVING && hasAccessByCodes(['mes:pro-feedback:approve'])"
              title="确认驳回该报工单?"
              @confirm="handleReject(row)"
            >
@@ -293,6 +316,7 @@
                é©³å›ž
              </Button>
            </Popconfirm>
            -->
          </Space>
        </template>
      </Grid>
src/views/mes/workbench/components/tabs/FeedbackTab.vue
@@ -18,7 +18,7 @@
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import {
  approveFeedback,
  createFeedback,
  createFeedback, getBatchRequirements,
  getFeedback,
  getFeedbackPage,
  rejectFeedback,
@@ -29,7 +29,10 @@
import { $t } from '#/locales';
import { useFormSchema } from '../../../pro/feedback/data';
import BatchInputDialog from '../../../pro/feedback/modules/batch-input-dialog.vue';
const batchDialogRef = ref<InstanceType<typeof BatchInputDialog>>();
const batchActionType = ref<'submit' | 'approve'>('');
const pendingSubmitMode = ref<'saveAndSubmit' | 'submit'>('submit');
defineOptions({ name: 'WorkbenchFeedbackTab' });
const props = defineProps<{
@@ -131,9 +134,9 @@
    itemSpecification: task.itemSpecification,
    unitMeasureName: task.unitMeasureName,
    scheduledQuantity: task.quantity,
    feedbackApprove: task.feedbackApprove,
    checkFlag,
  });
  originalSnapshot.value = JSON.stringify(await getAlignedData());
}
@@ -168,6 +171,7 @@
  if (props.task) {
    await resetForTask(props.task);
  }
  emit('reported');
}
@@ -203,8 +207,12 @@
    return;
  }
  pendingSubmitMode.value = 'saveAndSubmit';
  // 4. è°ƒç”¨æäº¤æŽ¥å£
  await submitFeedback(record.id!);
  const isDone = await handleOnSubmit(feedbackId.value!,props.task.feedbackApprove);
  if (!isDone) return;
  feedbackId.value = record.id;
  feedbackStatus.value = record.status;
@@ -232,13 +240,60 @@
    await updateFeedback(data);
    originalSnapshot.value = JSON.stringify(data);
  }
  await submitFeedback(feedbackId.value!);
  message.success('报订单已提交');
  pendingSubmitMode.value = 'submit';
  const isDone = await handleOnSubmit(feedbackId.value!,props.task.feedbackApprove);
  if (!isDone) return;
  message.success('报工单已提交');
  // é‡ç½®è¡¨å•为新建模式
  if (props.task) {
    await resetForTask(props.task);
  }
  emit('reported');
}
/** æäº¤æ–¹æ³• */
async function handleOnSubmit(id:any,feedbackRate:any): Promise<boolean> {
  if (feedbackRate&&feedbackRate==true){
    await submitFeedback({ id: feedbackId.value! });
    return true;
  }
  const checkResult = await getBatchRequirements(id);
  if (checkResult?.needBatchInput) {
    batchActionType.value = 'submit';
    batchDialogRef.value?.open({ checkResult });
    return false;
  } else {
    await submitFeedback({ id: feedbackId.value! });
    return true;
  }
}
/** æ‰¹æ¬¡ç¡®è®¤å›žè°ƒ */
async function handleBatchConfirm(batchData: Record<string, unknown>) {
  if (batchActionType.value === 'submit') {
    await submitFeedback({ id: feedbackId.value!, ...batchData });
    if (pendingSubmitMode.value === 'saveAndSubmit') {
      const data = await getAlignedData();
      const code = data.code as string;
      Modal.success({
        title: '保存并提交成功',
        content: `报工单 ${code} å·²æäº¤`,
        centered: true,
      });
    } else {
      message.success('报工单已提交');
    }
    if (props.task) {
      await resetForTask(props.task);
    }
    emit('reported');
  }
}
/** å®¡æ‰¹é€šè¿‡ */
@@ -294,6 +349,7 @@
<template>
  <div class="feedback-tab">
    <BatchInputDialog ref="batchDialogRef" @confirm="handleBatchConfirm" />
    <div v-if="!task" class="feedback-tab__empty">
      è¯·ä»Žå·¦ä¾§å·¥å•列表选择一条任务
    </div>
src/views/mes/workbench/index.vue
@@ -17,6 +17,7 @@
} from '#/api/mes/pro/workrecord';
import { getPendingFeedbackPage } from '#/api/mes/pro/task';
import { router } from '#/router';
import { useAccess } from '@vben/access';
import TaskListPanel from './components/TaskListPanel.vue';
import WorkstationPanel from './components/WorkstationPanel.vue';
@@ -33,16 +34,36 @@
// --- Tab é…ç½® ---
const tabItems: WorkbenchTabItem[] = [
  { key: 'detail', label: '工单详情', icon: 'lucide:file-text' },
  { key: 'feedback', label: '报工', icon: 'lucide:edit-3' },
  { key: 'history', label: '报工记录', icon: 'lucide:history' },
  { key: 'feedback', label: '报工', icon: 'lucide:edit-3', auth: ['mes:pro-feedback:create'] },
  { key: 'history', label: '报工记录', icon: 'lucide:history' ,auth: ['mes:pro-feedback:query']},
  { key: 'record-param', label: '生产记录参数', icon: 'lucide:file-cog' },
  { key: 'product-issue', label: '领料', icon: 'lucide:clipboard-list' },
  { key: 'return-issue', label: '退料', icon: 'lucide:rotate-ccw' },
];
const { hasAccessByCodes } = useAccess();
const availableTabs = computed(() => {
  return tabItems.filter((tab) => {
    if (!tab.auth || tab.auth.length === 0) return true;
    return hasAccessByCodes(tab.auth);
  });
});
const activeTab = ref<WorkbenchTabKey>('detail');
const activeTabLabel = computed(
  () => tabItems.find((t) => t.key === activeTab.value)?.label ?? '',
  () => availableTabs.value.find((t) => t.key === activeTab.value)?.label ?? '',
);
// å½“可用 tab å˜åŒ–时,如果当前选中的 tab ä¸å¯ç”¨ï¼Œåˆ™è‡ªåŠ¨é€‰æ‹©ç¬¬ä¸€ä¸ªå¯ç”¨çš„ tab
watch(
  availableTabs,
  (tabs) => {
    if (tabs.length > 0 && !tabs.find((t) => t.key === activeTab.value)) {
      activeTab.value = tabs[0]!.key;
    }
  },
  { immediate: true },
);
// --- å·¥ä½œç«™ ---
@@ -176,7 +197,7 @@
      <nav class="workbench-nav">
        <button
          v-for="tab in tabItems"
          v-for="tab in availableTabs"
          :key="tab.key"
          type="button"
          class="workbench-nav__item"
src/views/mes/workbench/types.ts
@@ -8,6 +8,7 @@
  key: WorkbenchTabKey;
  label: string;
  icon: string;
  auth?: string[];
}
/** å¾…报工任务(从 API ç±»åž‹é‡å¯¼å‡ºï¼Œä¾¿äºŽç»„件引用) */