云
昨天 346a988f537228ae44731cd31007f1412b482e83
refactor(mdm): 移除物料主数据中废弃的字段配置

- 移除了辅单位相关字段(辅单位1、辅单位2及其换算比率)
- 移除了条码、价格、库存等业务字段
- 移除了批次管理和有效期等配置选项
- 移除了备注字段和批次属性配置分组
- 优化了表单数据处理逻辑,保留历史字段避免清空

feat(batch): 支持生产批次绑定吨袋功能

- 新增吨袋绑定功能,与托盘绑定互斥
- 添加了吨袋选择器组件和相关依赖逻辑
- 实现了吨袋绑定时的数据联动和校验
- 更新了表单架构以支持托盘和吨袋两种模式
- 添加了吨袋数据获取API接口

fix(batch): 完善批次绑定验证和状态处理

- 添加了托盘和吨袋互斥绑定的前端验证
- 修复了表单初始化时未清空残留值的问题
- 优化了绑定模式切换时的状态管理
- 增强了批次数据提交前的校验机制

refactor(oqc): 简化出货检验流程处理逻辑

- 移除了销售质检相关的审批流程代码
- 删除了复检、审批和流程记录功能模块
- 移除了工作流状态相关的字段和显示
- 简化了页面组件结构和交互逻辑

feat(seedQuality): 增强种子质检指标管理功能

- 新增种子指标范围配置的获取和保存功能
- 添加了指标合格范围的动态加载机制
- 优化了指标录入界面的验证和提示信息
- 更新了质检流程描述和操作反馈文案

refactor(qc): 调整质检流程和界面布局

- 移除了销售质检独立页面模块
- 重构了种子质检的界面布局和交互流程
- 优化了质量检验结果的提交和状态更新
- 改进了页面响应式设计和用户体验
已添加5个文件
已删除1个文件
已修改14个文件
1455 ■■■■■ 文件已修改
src/api/mes/pro/bagCode/index.ts 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/batch/index.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/qc/approverConfig/index.ts 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/qc/oqc/index.ts 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/qc/seedQuality/index.ts 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/mdm/data.ts 276 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/mdm/modules/form.vue 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/batch/data.ts 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/batch/modules/form.vue 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/approverConfig/data.ts 199 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/approverConfig/index.vue 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/approverConfig/modules/form.vue 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/oqc/index.vue 129 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/oqc/modules/form.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/salesQuality/index.vue 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/seedQuality/index.vue 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/seedQuality/record.vue 155 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/seedRange/index.vue 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mobile/quality-records.vue 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mobile/quality.vue 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/bagCode/index.ts
@@ -222,6 +222,11 @@
  );
}
/** å¯ç»‘定批次的吨袋(无托盘且未绑定批次) */
export function getUnboundTonBags() {
  return requestClient.get<MesProBagCodeApi.BagCode[]>('/mes/pro/bag-code/unbound-ton-bags');
}
/** æ‰¹é‡å¯¼å‡ºè¢‹ç  Excel(交付印刷) */
export function exportBagCodeExcel(params: MesProBagCodeApi.PageParams) {
  return requestClient.download('/mes/pro/bag-code/export-excel', { params });
src/api/mes/pro/batch/index.ts
@@ -18,9 +18,10 @@
  /** ç”Ÿäº§æ‰¹æ¬¡ */
  export interface Batch {
    id?: number; // ç¼–号
    itemId?: number; // æ‰˜ç›˜å¸¦å…¥çš„品种编号
    itemId?: number; // æ‰˜ç›˜æˆ–吨袋带入的品种编号
    itemCode?: string;
    itemName?: string;
    bindingMode?: 'pallet' | 'tonBag'; // æ‰¹æ¬¡å·²æœ‰ç»‘定方式
    businessCode?: string; // å†…部出入库业务码
    code?: string; // æ‰¹æ¬¡å·ï¼ˆè¡¥å½•正式信息后自动生成)
    tempCode?: string; // ä¸´æ—¶æ‰¹æ¬¡ç ï¼ˆé¢„生成二维码阶段使用,扫码追溯兼容)
@@ -39,6 +40,7 @@
    createTime?: string; // åˆ›å»ºæ—¶é—´
    workers?: BatchWorker[]; // ç”Ÿäº§äººå‘˜é…ç½®åˆ—表
    palletIds?: number[]; // ç»‘定托盘编号列表(仅新增/补录提交时传入,绑定时批次直接进入生产中)
    tonBagIds?: number[]; // ç»‘定吨袋编号列表;与托盘互斥
  }
  /** ç”Ÿäº§æ‰¹æ¬¡æ‰«ç è¿½æº¯ */
src/api/mes/qc/approverConfig/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,53 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace MesQcApproverConfigApi {
  /** MES è´¨æ£€å®¡æ ¸äººé…ç½® */
  export interface ApproverConfig {
    id?: number; // ç¼–号
    qcType?: number; // è´¨æ£€ç±»åž‹ï¼š1种子质检
    approveNode?: number; // å®¡æ ¸èŠ‚ç‚¹ï¼š1加工部审批 3公司质检审批
    userId?: number; // å®¡æ ¸äººç”¨æˆ· ID
    userNickname?: string; // å®¡æ ¸äººæ˜µç§°ï¼ˆå…³è”查询)
    status?: number; // çŠ¶æ€ï¼š0启用 1停用
    remark?: string; // å¤‡æ³¨
    createTime?: Date; // åˆ›å»ºæ—¶é—´
  }
}
/** æŸ¥è¯¢è´¨æ£€å®¡æ ¸äººé…ç½®åˆ†é¡µ */
export function getApproverConfigPage(params: PageParam) {
  return requestClient.get<PageResult<MesQcApproverConfigApi.ApproverConfig>>(
    '/mes/qc/approver-config/page',
    { params },
  );
}
/** æŸ¥è¯¢è´¨æ£€å®¡æ ¸äººé…ç½®è¯¦æƒ… */
export function getApproverConfig(id: number) {
  return requestClient.get<MesQcApproverConfigApi.ApproverConfig>(
    `/mes/qc/approver-config/get?id=${id}`,
  );
}
/** æ–°å¢žè´¨æ£€å®¡æ ¸äººé…ç½® */
export function createApproverConfig(
  data: MesQcApproverConfigApi.ApproverConfig,
) {
  return requestClient.post('/mes/qc/approver-config/create', data);
}
/** ä¿®æ”¹è´¨æ£€å®¡æ ¸äººé…ç½® */
export function updateApproverConfig(
  data: MesQcApproverConfigApi.ApproverConfig,
) {
  return requestClient.put('/mes/qc/approver-config/update', data);
}
/** åˆ é™¤è´¨æ£€å®¡æ ¸äººé…ç½® */
export function deleteApproverConfig(id: number) {
  return requestClient.delete(
    `/mes/qc/approver-config/delete?id=${id}`,
  );
}
src/api/mes/qc/oqc/index.ts
@@ -39,7 +39,6 @@
    inspectorUserId?: number; // æ£€æµ‹äººå‘˜ç”¨æˆ· ID
    inspectorNickname?: string; // æ£€æµ‹äººå‘˜æ˜µç§°ï¼ˆå…³è”查询)
    status?: number; // çŠ¶æ€
    workflowStatus?: number; // é”€å”®è´¨æ£€å¤æ£€å’Œå®¡æ‰¹çŠ¶æ€
    remark?: string; // å¤‡æ³¨
  }
}
@@ -69,16 +68,6 @@
/** å®Œæˆå‡ºè´§æ£€éªŒå• */
export function finishOqc(id: number) {
  return requestClient.put(`/mes/qc/oqc/finish?id=${id}`);
}
export function retestOqc(data: { id: number; checkQuantity: number; qualifiedQuantity: number; unqualifiedQuantity: number; checkResult: number; opinion?: string }) {
  return requestClient.post('/mes/qc/oqc/retest', data);
}
export function approveOqc(stage: 'sales' | 'company', data: { id: number; approved: boolean; opinion: string }) {
  return requestClient.post(`/mes/qc/oqc/${stage}-approve`, data);
}
export interface OqcAction { id: number; action: string; result?: number; opinion?: string; quantitiesJson?: string; operatorId?: number; createTime?: string }
export function getOqcActions(id: number) {
  return requestClient.get<OqcAction[]>('/mes/qc/oqc/actions', { params: { id } });
}
/** åˆ é™¤å‡ºè´§æ£€éªŒå• */
src/api/mes/qc/seedQuality/index.ts
@@ -1,5 +1,20 @@
import { requestClient } from '#/api/request';
export interface SeedIndicatorRange {
  indicatorCode: string;
  minValue: number;
  maxValue: number;
  updateTime?: string;
}
export function getSeedIndicatorRanges() {
  return requestClient.get<SeedIndicatorRange[]>('/mes/qc/seed-quality/ranges');
}
export function saveSeedIndicatorRanges(ranges: SeedIndicatorRange[]) {
  return requestClient.put('/mes/qc/seed-quality/ranges', { ranges });
}
export interface SeedQualityPageParams {
  pageNo?: number;
  pageSize?: number;
@@ -12,6 +27,7 @@
export interface SeedQualityRecord {
  id: number;
  code: string;
  palletCode?: string;
  batchCode?: string;
  result: number;
  workflowStatus: number;
@@ -23,7 +39,7 @@
}
export interface SeedQualityIndicator {
  indicatorCode: string;
  value: number;
  value?: number;
  minValue: number;
  maxValue: number;
}
@@ -45,6 +61,33 @@
export function retestSeedQuality(id: number, data: Omit<MesQcSeedQualityApi.SubmitParams, 'qualifiedWarehouseId'>) {
  return requestClient.post('/mes/qc/seed-quality/retest', data, { params: { id } });
}
export function retestSeedQualityBatch(data: {
  ids: number[];
  reason?: string;
  indicators: MesQcSeedQualityApi.Indicator[];
}) {
  return requestClient.post('/mes/qc/seed-quality/retest-batch', data);
}
export interface SeedRetestScope {
  code: string;
  scanType: 'PALLET' | 'BATCH';
  batchCode?: string;
  totalCount: number;
  retestIds: number[];
  initialRecheckIds: number[];
  pendingProcessCount: number;
  pendingCompanyCount: number;
}
export function resolveSeedRetestScope(code: string, scanType: 'PALLET' | 'BATCH') {
  return requestClient.get<SeedRetestScope>('/mes/qc/seed-quality/retest-scope/resolve', { params: { code, scanType } });
}
export function submitSeedRetestScope(data: {
  code: string; scanType: 'PALLET' | 'BATCH'; workflowStatus: 10 | 50;
  qualityIds: number[]; indicators: MesQcSeedQualityApi.Indicator[]; reason?: string;
}) {
  return requestClient.post<{ processedCount: number }>('/mes/qc/seed-quality/retest-scope', data);
}
export function approveSeedQuality(stage: 'process' | 'company', data: { id: number; approved: boolean; opinion: string }) {
  return requestClient.post(`/mes/qc/seed-quality/${stage}-approve`, data);
}
src/views/basicData/mdm/data.ts
@@ -129,56 +129,6 @@
      },
    },
    {
      component: "ApiSelect",
      fieldName: "unitMeasureId2",
      label: "辅单位1",
      formItemClass: "col-span-1",
      componentProps: {
        placeholder: "请选择辅单位1",
        allowClear: true,
        api: getUnitListCached,
        labelField: "name",
        valueField: "id",
      },
    },
    {
      component: "InputNumber",
      fieldName: "unitMeasureRate1",
      label: "辅单位1换算比率",
      formItemClass: "col-span-1",
      componentProps: {
        class: "!w-full",
        min: 0,
        precision: 6,
        placeholder: "请输入换算比率",
      },
    },
    {
      component: "ApiSelect",
      fieldName: "unitMeasureId3",
      label: "辅单位2",
      formItemClass: "col-span-1",
      componentProps: {
        placeholder: "请选择辅单位2",
        allowClear: true,
        api: getUnitListCached,
        labelField: "name",
        valueField: "id",
      },
    },
    {
      component: "InputNumber",
      fieldName: "unitMeasureRate2",
      label: "辅单位2换算比率",
      formItemClass: "col-span-1",
      componentProps: {
        class: "!w-full",
        min: 0,
        precision: 6,
        placeholder: "请输入换算比率",
      },
    },
    {
      component: 'ApiSelect',
      fieldName: 'warehouseId',
      label: '默认仓库',
@@ -205,102 +155,6 @@
      },
    },
    {
      component: "Input",
      fieldName: "barCode",
      label: "条码",
      componentProps: {
        placeholder: "请输入条码",
      },
    },
    {
      component: "InputNumber",
      fieldName: "salesPrice",
      label: "销售价",
      componentProps: {
        class: "!w-full",
        min: 0,
        precision: 2,
        placeholder: "请输入销售价",
      },
    },
    {
      component: "InputNumber",
      fieldName: "costPrice",
      label: "成本价",
      componentProps: {
        class: "!w-full",
        min: 0,
        precision: 2,
        placeholder: "请输入成本价",
      },
    },
    {
      component: "InputNumber",
      fieldName: "safetyStock",
      label: "安全库存",
      componentProps: {
        class: "!w-full",
        min: 0,
        precision: 2,
        placeholder: "请输入安全库存",
      },
    },
    {
      component: "InputNumber",
      fieldName: "minStock",
      label: "最低库存",
      componentProps: {
        class: "!w-full",
        min: 0,
        precision: 2,
        placeholder: "请输入最低库存",
      },
    },
    {
      component: "InputNumber",
      fieldName: "maxStock",
      label: "最高库存",
      componentProps: {
        class: "!w-full",
        min: 0,
        precision: 2,
        placeholder: "请输入最高库存",
      },
    },
    {
      component: 'Switch',
      fieldName: 'isBatchManaged',
      label: '批次管理',
      defaultValue: false,
      componentProps: {
        class: "w-auto",
        checkedChildren: "是",
        unCheckedChildren: "否",
      },
      rules: z.boolean().default(false),
    },
    {
      component: "InputNumber",
      fieldName: "expiryDay",
      label: "有效期(天)",
      componentProps: {
        class: "!w-full",
        min: 0,
        placeholder: "请输入有效期",
      },
    },
    {
      component: 'Switch',
      fieldName: 'syncMes',
      label: '同步到MES',
      defaultValue: true,
      componentProps: {
        checkedChildren: '是',
        unCheckedChildren: '否',
      },
      rules: z.boolean().default(true),
    },
    {
      component: "RadioGroup",
      fieldName: "status",
      label: "状态",
@@ -310,136 +164,6 @@
        optionType: "button",
      },
      rules: z.number().default(CommonStatusEnum.ENABLE),
    },
    {
      component: "Textarea",
      fieldName: "remark",
      label: "备注",
      formItemClass: "col-span-2",
      componentProps: {
        placeholder: "请输入备注",
        rows: 3,
      },
    },
    // ===== æ‰¹æ¬¡å±žæ€§é…ç½®ï¼ˆisBatchManaged = true æ—¶æ˜¾ç¤ºï¼‰ =====
    {
      component: "Divider",
      fieldName: "batchConfigDivider",
      label: "批次属性配置",
      formItemClass: "col-span-2",
      componentProps: {
        orientation: "left",
        plain: true,
      },
      dependencies: {
        triggerFields: ["isBatchManaged"],
        show: (values) => values.isBatchManaged === true,
      },
    },
    {
      component: "Switch",
      fieldName: "produceDateFlag",
      label: "生产日期必填",
      defaultValue: false,
      componentProps: {
        class: "w-auto",
        checkedChildren: "是",
        unCheckedChildren: "否",
      },
      dependencies: {
        triggerFields: ["isBatchManaged"],
        show: (values) => values.isBatchManaged === true,
      },
    },
    {
      component: "Switch",
      fieldName: "expireDateFlag",
      label: "有效期必填",
      defaultValue: false,
      componentProps: {
        class: "w-auto",
        checkedChildren: "是",
        unCheckedChildren: "否",
      },
      dependencies: {
        triggerFields: ["isBatchManaged"],
        show: (values) => values.isBatchManaged === true,
      },
    },
    {
      component: "Switch",
      fieldName: "receiptDateFlag",
      label: "入库日期必填",
      defaultValue: false,
      componentProps: {
        class: "w-auto",
        checkedChildren: "是",
        unCheckedChildren: "否",
      },
      dependencies: {
        triggerFields: ["isBatchManaged"],
        show: (values) => values.isBatchManaged === true,
      },
    },
    {
      component: "Switch",
      fieldName: "vendorFlag",
      label: "供应商必填",
      defaultValue: false,
      componentProps: {
        class: "w-auto",
        checkedChildren: "是",
        unCheckedChildren: "否",
      },
      dependencies: {
        triggerFields: ["isBatchManaged"],
        show: (values) => values.isBatchManaged === true,
      },
    },
    {
      component: "Switch",
      fieldName: "purchaseOrderCodeFlag",
      label: "采购订单号必填",
      defaultValue: false,
      componentProps: {
        class: "w-auto",
        checkedChildren: "是",
        unCheckedChildren: "否",
      },
      dependencies: {
        triggerFields: ["isBatchManaged"],
        show: (values) => values.isBatchManaged === true,
      },
    },
    {
      component: "Switch",
      fieldName: "lotNumberFlag",
      label: "生产批号必填",
      defaultValue: false,
      componentProps: {
        class: "w-auto",
        checkedChildren: "是",
        unCheckedChildren: "否",
      },
      dependencies: {
        triggerFields: ["isBatchManaged"],
        show: (values) => values.isBatchManaged === true,
      },
    },
    {
      component: "Switch",
      fieldName: "qualityStatusFlag",
      label: "质量状态必填",
      defaultValue: false,
      componentProps: {
        class: "w-auto",
        checkedChildren: "是",
        unCheckedChildren: "否",
      },
      dependencies: {
        triggerFields: ["isBatchManaged"],
        show: (values) => values.isBatchManaged === true,
      },
    },
  ];
}
src/views/basicData/mdm/modules/form.vue
@@ -38,7 +38,12 @@
      return;
    }
    modalApi.lock();
    const data = (await formApi.getValues()) as MdmItemApi.Item;
    // ç¼–辑时保留列表未展示的历史字段,避免保存精简表单时清空它们。
    const data = {
      ...formData.value,
      ...(await formApi.getValues()),
      syncMes: formData.value?.syncedMes ?? true,
    } as MdmItemApi.Item;
    try {
      await (formData.value?.id ? updateItem(data) : createItem(data));
      await modalApi.close();
@@ -63,8 +68,6 @@
      modalApi.lock();
      try {
        formData.value = await getItem(data.id);
        // åŒæ­¥çŠ¶æ€æ˜ å°„ï¼šè¯¦æƒ…æŽ¥å£è¿”å›ž syncedMes,表单字段为 syncMes
        formData.value.syncMes = formData.value.syncedMes ?? true;
        await formApi.setValues(formData.value);
      } finally {
        modalApi.unlock();
src/views/mes/pro/batch/data.ts
@@ -23,11 +23,16 @@
  itemCode?: string;
}
export type TonBagOption = PalletOption;
/** æ–°å¢ž/修改的表单 */
export function useFormSchema(
  formType: FormType,
  palletOptions: PalletOption[] = [],
  onPalletChange?: (ids: number[]) => void,
  tonBagOptions: TonBagOption[] = [],
  onTonBagChange?: (ids: number[]) => void,
  existingBinding?: 'pallet' | 'tonBag',
): VbenFormSchema[] {
  const disabled = formType === 'detail';
  return [
@@ -141,11 +146,11 @@
    },
    {
      fieldName: 'itemName', label: '品种名称', component: 'Input',
      componentProps: { disabled: true, placeholder: '选择托盘后自动带入' },
      componentProps: { disabled: true, placeholder: '选择托盘或吨袋后自动带入' },
    },
    {
      fieldName: 'itemCode', label: '品种编码', component: 'Input',
      componentProps: { disabled: true, placeholder: '选择托盘后自动带入' },
      componentProps: { disabled: true, placeholder: '选择托盘或吨袋后自动带入' },
    },
    {
      fieldName: 'palletIds',
@@ -165,17 +170,47 @@
        showSearch: true,
      },
      dependencies: {
        triggerFields: ['itemId'],
        triggerFields: ['itemId', 'tonBagIds'],
        show: () => formType !== 'detail',
        componentProps: (values) => ({
          options: palletOptions.map(option => ({ ...option,
            disabled: !option.itemId || (!!values.itemId && option.itemId !== values.itemId),
            disabled: existingBinding === 'tonBag' || !!values.tonBagIds?.length || !option.itemId || (!!values.itemId && option.itemId !== values.itemId),
          })),
          disabled: disabled || existingBinding === 'tonBag' || !!values.tonBagIds?.length,
        }),
      },
      help: '只能绑定相同品种的托盘,品种信息自动带入;缺少品种的托盘请先装袋。绑定后批次直接进入生产中:托盘生成正式托盘码,托盘下未录批次的袋码自动级联录入本批次(袋码品种须一致);编辑时仅需勾选新增托盘,已绑定的不可解绑',
    },
    {
      fieldName: 'tonBagIds',
      label: '绑定吨袋',
      component: 'Select',
      componentProps: {
        allowClear: true,
        disabled,
        maxTagCount: 3,
        dropdownMatchSelectWidth: false,
        dropdownStyle: { minWidth: '380px', maxWidth: 'calc(100vw - 32px)' },
        mode: 'multiple',
        optionFilterProp: 'label',
        options: tonBagOptions,
        onChange: onTonBagChange,
        placeholder: '请选择相同品种的吨袋',
        showSearch: true,
      },
      dependencies: {
        triggerFields: ['itemId', 'palletIds'],
        show: () => formType !== 'detail',
        componentProps: (values) => ({
          options: tonBagOptions.map(option => ({ ...option,
            disabled: existingBinding === 'pallet' || !!values.palletIds?.length || (!!values.itemId && option.itemId !== values.itemId),
          })),
          disabled: disabled || existingBinding === 'pallet' || !!values.palletIds?.length,
        }),
      },
      help: '仅可绑定计量单位为吨、无托盘且未录入批次的袋码;与绑定托盘互斥。编辑时仅选择新增吨袋,已绑定的不可解绑',
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
src/views/mes/pro/batch/modules/form.vue
@@ -1,5 +1,5 @@
<script lang="ts" setup>
  import type { FormType, PalletOption } from "../data";
  import type { FormType, PalletOption, TonBagOption } from "../data";
  import type { VxeTableGridOptions } from "#/adapter/vxe-table";
  import type { MesProBatchApi } from "#/api/mes/pro/batch";
@@ -22,6 +22,7 @@
    updateBatch,
  } from "#/api/mes/pro/batch";
  import { getUnboundPalletList } from "#/api/mes/pro/pallet";
  import { getUnboundTonBags } from "#/api/mes/pro/bagCode";
  import { $t } from "#/locales";
  import { UserSelect } from "#/views/system/user/components";
@@ -38,12 +39,37 @@
  const formType = ref<FormType>("create");
  const formData = ref<MesProBatchApi.Batch>();
  let palletOptions: PalletOption[] = [];
  let tonBagOptions: TonBagOption[] = [];
  async function handlePalletChange(ids: number[] = []) {
    const values = await formApi.getValues();
    if (ids.length && (values.tonBagIds?.length || formData.value?.bindingMode === 'tonBag')) {
      message.warning('绑定托盘和绑定吨袋只能选择一种');
      await formApi.setFieldValue('palletIds', []);
      return;
    }
    const selected = palletOptions.filter(option => ids.includes(option.value));
    const source = formData.value?.itemId ? formData.value : selected[0];
    if (selected.some(option => !option.itemId || (source?.itemId && option.itemId !== source.itemId))) {
      message.warning('生产批次只能绑定相同品种的托盘');
      await formApi.setFieldValue('palletIds', selected.filter(option => option.itemId === source?.itemId).map(option => option.value));
    }
    await formApi.setFieldValue('itemId', source?.itemId);
    await formApi.setFieldValue('itemName', source?.itemName);
    await formApi.setFieldValue('itemCode', source?.itemCode);
  }
  async function handleTonBagChange(ids: number[] = []) {
    const values = await formApi.getValues();
    if (ids.length && (values.palletIds?.length || formData.value?.bindingMode === 'pallet')) {
      message.warning('绑定托盘和绑定吨袋只能选择一种');
      await formApi.setFieldValue('tonBagIds', []);
      return;
    }
    const selected = tonBagOptions.filter(option => ids.includes(option.value));
    const source = formData.value?.itemId ? formData.value : selected[0];
    if (selected.some(option => option.itemId !== source?.itemId)) {
      message.warning('生产批次只能绑定相同品种的吨袋');
      await formApi.setFieldValue('tonBagIds', selected.filter(option => option.itemId === source?.itemId).map(option => option.value));
    }
    await formApi.setFieldValue('itemId', source?.itemId);
    await formApi.setFieldValue('itemName', source?.itemName);
@@ -198,6 +224,10 @@
      modalApi.lock();
      try {
        const data = (await formApi.getValues()) as MesProBatchApi.Batch;
        if (data.palletIds?.length && data.tonBagIds?.length) {
          message.warning('绑定托盘和绑定吨袋只能选择一种');
          return;
        }
        if (formType.value === "fill") {
          await fillBatchInfo({ ...data, id: formData.value!.id, workers });
        } else if (formData.value?.id) {
@@ -221,18 +251,25 @@
      formType.value = data.formType;
      // æ–°å¢ž/补录/编辑态加载未绑定批次的托盘,供「绑定托盘」多选下拉使用
      palletOptions = [];
      tonBagOptions = [];
      if (formType.value !== "detail") {
        const pallets = await getUnboundPalletList();
        const [pallets, tonBags] = await Promise.all([getUnboundPalletList(), getUnboundTonBags()]);
        palletOptions = pallets.map((pallet) => ({
          label: `${pallet.tempCode ?? `托盘${pallet.id}`} Â· ${pallet.itemName || "待完善品种"}(已装 ${pallet.bagCount ?? 0} è¢‹ï¼‰`,
          value: pallet.id as number,
          itemId: pallet.itemId, itemName: pallet.itemName, itemCode: pallet.itemCode,
        }));
        tonBagOptions = tonBags.map((bag) => ({
          label: `${bag.code ?? `吨袋${bag.id}`} Â· ${bag.itemName || '待完善品种'}`,
          value: bag.id as number,
          itemId: bag.itemId, itemName: bag.itemName, itemCode: bag.itemCode,
        }));
      }
      formApi.setState({ schema: useFormSchema(formType.value, palletOptions, handlePalletChange) });
      formApi.setState({ schema: useFormSchema(formType.value, palletOptions, handlePalletChange, tonBagOptions, handleTonBagChange) });
      if (formType.value !== "detail") {
        // æ¸…空上次打开时勾选的托盘,避免残留值误触发「批次进入生产中」
        await formApi.setFieldValue("palletIds", undefined);
        await formApi.setFieldValue("tonBagIds", undefined);
      }
      // æ ¹æ®è¡¨å•态切换人员子表列
      workerGridApi.setGridOptions({
@@ -244,6 +281,8 @@
        modalApi.lock();
        try {
          formData.value = await getBatch(data.id);
          const existingBinding = formData.value.bindingMode;
          formApi.setState({ schema: useFormSchema(formType.value, palletOptions, handlePalletChange, tonBagOptions, handleTonBagChange, existingBinding) });
          await formApi.setValues(formData.value);
          const workers = (formData.value.workers ?? []).map(
            (worker, index) => ({ ...worker, _rowKey: worker.id ?? index })
src/views/mes/qc/approverConfig/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,199 @@
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesQcApproverConfigApi } from '#/api/mes/qc/approverConfig';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { getSimpleUserList } from '#/api/system/user';
/** è´¨æ£€ç±»åž‹ */
export const QC_TYPE_OPTIONS = [{ label: '种子质检', value: 1 }];
/** å®¡æ ¸èŠ‚ç‚¹ */
export const APPROVE_NODE_OPTIONS = [
  { label: '初检审核', value: 1 },
  { label: '复检审核', value: 3 },
];
/** æŒ‰è´¨æ£€ç±»åž‹è¿”回可选的审核节点 */
export function getNodeOptions(_qcType?: number) {
  return APPROVE_NODE_OPTIONS;
}
export function getQcTypeLabel(qcType?: number): string {
  return QC_TYPE_OPTIONS.find((item) => item.value === qcType)?.label || '—';
}
export function getApproveNodeLabel(approveNode?: number): string {
  return (
    APPROVE_NODE_OPTIONS.find((item) => item.value === approveNode)?.label ||
    '—'
  );
}
/** æ–°å¢ž/修改的表单 */
export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'qcType',
      label: '质检类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: QC_TYPE_OPTIONS,
        placeholder: '请选择质检类型',
        onChange: async () => {
          await formApi?.setFieldValue('approveNode', undefined);
        },
      },
      defaultValue: 1,
      rules: 'required',
    },
    {
      fieldName: 'approveNode',
      label: '审核节点',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: APPROVE_NODE_OPTIONS,
        placeholder: '请选择审核节点',
      },
      dependencies: {
        triggerFields: ['qcType'],
        componentProps: (values) => ({
          allowClear: true,
          options: getNodeOptions(values.qcType),
          placeholder: '请选择审核节点',
        }),
      },
      rules: 'required',
    },
    {
      fieldName: 'userId',
      label: '审核人',
      component: 'ApiSelect',
      componentProps: {
        allowClear: true,
        api: getSimpleUserList,
        labelField: 'nickname',
        placeholder: '请选择审核人',
        valueField: 'id',
      },
      rules: 'selectRequired',
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'Select',
      componentProps: {
        options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
        placeholder: '请选择状态',
      },
      defaultValue: 0,
      rules: 'required',
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      componentProps: {
        placeholder: '请输入备注',
        rows: 3,
      },
    },
  ];
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'qcType',
      label: '质检类型',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: QC_TYPE_OPTIONS,
        placeholder: '请选择质检类型',
      },
    },
    {
      fieldName: 'approveNode',
      label: '审核节点',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: APPROVE_NODE_OPTIONS,
        placeholder: '请选择审核节点',
      },
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
        placeholder: '请选择状态',
      },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<MesQcApproverConfigApi.ApproverConfig>['columns'] {
  return [
    {
      field: 'qcType',
      title: '质检类型',
      width: 140,
      slots: { default: 'qcType' },
    },
    {
      field: 'approveNode',
      title: '审核节点',
      width: 160,
      slots: { default: 'approveNode' },
    },
    {
      field: 'userNickname',
      title: '审核人',
      width: 140,
    },
    {
      field: 'status',
      title: '状态',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.COMMON_STATUS },
      },
    },
    {
      field: 'remark',
      title: '备注',
      minWidth: 180,
    },
    {
      field: 'createTime',
      title: '创建时间',
      width: 180,
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
      width: 160,
      fixed: 'right',
      slots: { default: 'actions' },
    },
  ];
}
src/views/mes/qc/approverConfig/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,141 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesQcApproverConfigApi } from '#/api/mes/qc/approverConfig';
import { Page, useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  deleteApproverConfig,
  getApproverConfigPage,
} from '#/api/mes/qc/approverConfig';
import { $t } from '#/locales';
import {
  getApproveNodeLabel,
  getQcTypeLabel,
  useGridColumns,
  useGridFormSchema,
} from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** åˆ›å»ºå®¡æ ¸äººé…ç½® */
function handleCreate() {
  formModalApi.setData(null).open();
}
/** ç¼–辑审核人配置 */
function handleEdit(row: MesQcApproverConfigApi.ApproverConfig) {
  formModalApi.setData(row).open();
}
/** åˆ é™¤å®¡æ ¸äººé…ç½® */
async function handleDelete(row: MesQcApproverConfigApi.ApproverConfig) {
  const hideLoading = message.loading({
    content: $t('ui.actionMessage.deleting', [row.userNickname || '']),
    duration: 0,
  });
  try {
    await deleteApproverConfig(row.id!);
    message.success($t('ui.actionMessage.deleteSuccess', [row.userNickname || '']));
    handleRefresh();
  } finally {
    hideLoading();
  }
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) => {
          return await getApproverConfigPage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          });
        },
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<MesQcApproverConfigApi.ApproverConfig>,
});
</script>
<template>
  <Page auto-content-height><FormModal @success="handleRefresh" />
    <Grid table-title="质检审核人配置">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['审核人配置']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['mes:qc-approver:create'],
              onClick: handleCreate,
            },
          ]"
        />
      </template>
      <template #qcType="{ row }">
        <span>{{ getQcTypeLabel(row.qcType) }}</span>
      </template>
      <template #approveNode="{ row }">
        <span>{{ getApproveNodeLabel(row.approveNode) }}</span>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['mes:qc-approver:update'],
              onClick: handleEdit.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['mes:qc-approver:delete'],
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [
                  row.userNickname || '',
                ]),
                confirm: handleDelete.bind(null, row),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/mes/qc/approverConfig/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,92 @@
<script lang="ts" setup>
import type { MesQcApproverConfigApi } from '#/api/mes/qc/approverConfig';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
  createApproverConfig,
  getApproverConfig,
  updateApproverConfig,
} from '#/api/mes/qc/approverConfig';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<MesQcApproverConfigApi.ApproverConfig>();
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 MesQcApproverConfigApi.ApproverConfig;
    try {
      await (formData.value?.id
        ? updateApproverConfig(data)
        : createApproverConfig(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<MesQcApproverConfigApi.ApproverConfig>();
    if (!data || !data.id) {
      await formApi.resetForm();
      await formApi.setValues({ qcType: 1, status: 0 });
      return;
    }
    modalApi.lock();
    try {
      formData.value = await getApproverConfig(data.id);
      // è®¾ç½®åˆ° values
      await formApi.setValues(formData.value);
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal :title="getTitle" class="w-2/5">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/mes/qc/oqc/index.vue
@@ -1,95 +1,19 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesQcOqcApi } from '#/api/mes/qc/oqc';
import type { OqcAction } from '#/api/mes/qc/oqc';
import { ref } from 'vue';
import { useAccess } from '@vben/access';
import { Page, useVbenModal } from '@vben/common-ui';
import { MesBizTypeEnum, MesQcStatusEnum } from '@vben/constants';
import { MesQcStatusEnum } from '@vben/constants';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { approveOqc, deleteOqc, exportOqc, getOqcActions, getOqcPage, retestOqc } from '#/api/mes/qc/oqc';
import { deleteOqc, exportOqc, getOqcPage } from '#/api/mes/qc/oqc';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const props = withDefaults(defineProps<{ salesMode?: boolean }>(), { salesMode: false });
const { hasAccessByCodes } = useAccess();
const workflowLabels: Record<number, string> = {
  0: '草稿', 10: '待复检', 20: '待销售部审批', 30: '待公司质检审批',
  40: '已放行', 50: '待销售部不合格评审', 60: '待公司不合格审批', 70: '已拒绝出货',
};
const active = ref<MesQcOqcApi.Oqc>();
const retestOpen = ref(false);
const retestBusy = ref(false);
const retestForm = ref({ checkQuantity: 0, qualifiedQuantity: 0, unqualifiedQuantity: 0, opinion: '' });
function openRetest(row: MesQcOqcApi.Oqc) {
  active.value = row;
  retestForm.value = {
    checkQuantity: Number(row.checkQuantity ?? 0), qualifiedQuantity: Number(row.qualifiedQuantity ?? 0),
    unqualifiedQuantity: Number(row.unqualifiedQuantity ?? 0), opinion: '',
  };
  retestOpen.value = true;
}
async function saveRetest() {
  if (!active.value?.id) return;
  const form = retestForm.value;
  if (form.checkQuantity < 0 || form.qualifiedQuantity < 0 || form.unqualifiedQuantity < 0
    || form.checkQuantity !== form.qualifiedQuantity + form.unqualifiedQuantity) {
    message.warning('检测数量必须等于合格与不合格数量之和');
    return;
  }
  if (form.unqualifiedQuantity > 0 && !form.opinion.trim()) {
    message.warning('复检不合格请填写原因');
    return;
  }
  retestBusy.value = true;
  try {
    await retestOqc({ id: active.value.id, ...form, checkResult: form.unqualifiedQuantity === 0 ? 1 : 2 });
    message.success('复检结果已提交,待销售部评审');
    retestOpen.value = false;
    handleRefresh();
  } catch (error) {
    message.error((error as Error).message || '复检失败');
  } finally { retestBusy.value = false; }
}
const reviewOpen = ref(false);
const reviewBusy = ref(false);
const reviewStage = ref<'sales' | 'company'>('sales');
const reviewApproved = ref(true);
const reviewOpinion = ref('');
function openReview(row: MesQcOqcApi.Oqc, stage: 'sales' | 'company') {
  active.value = row; reviewStage.value = stage; reviewApproved.value = true;
  reviewOpinion.value = ''; reviewOpen.value = true;
}
async function saveReview() {
  if (!active.value?.id) return;
  if (!reviewOpinion.value.trim()) { message.warning('请填写审批意见'); return; }
  reviewBusy.value = true;
  try {
    await approveOqc(reviewStage.value, { id: active.value.id, approved: reviewApproved.value, opinion: reviewOpinion.value.trim() });
    message.success('审批完成'); reviewOpen.value = false; handleRefresh();
  } catch (error) { message.error((error as Error).message || '审批失败'); }
  finally { reviewBusy.value = false; }
}
const historyOpen = ref(false);
const historyActions = ref<OqcAction[]>([]);
const actionLabels: Record<string, string> = {
  INITIAL: '初检', RETEST: '复检', SALES_APPROVE: '销售部通过', SALES_REJECT: '销售部驳回',
  COMPANY_APPROVE: '公司质检通过', COMPANY_REJECT: '公司质检驳回',
};
async function openHistory(row: MesQcOqcApi.Oqc) {
  if (!row.id) return;
  active.value = row;
  try { historyActions.value = await getOqcActions(row.id); historyOpen.value = true; }
  catch (error) { message.error((error as Error).message || '读取流程记录失败'); }
}
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
@@ -134,23 +58,15 @@
/** å¯¼å‡ºè¡¨æ ¼ */
async function handleExport() {
  const data = await exportOqc(await gridApi.formApi.getValues());
  downloadFileFromBlobPart({ fileName: props.salesMode ? '销售质检单.xls' : '出货检验单.xls', source: data });
  downloadFileFromBlobPart({ fileName: '出货检验单.xls', source: data });
}
const columns = useGridColumns();
if (props.salesMode && columns) {
  columns.splice(columns.length - 1, 0, {
    field: 'workflowStatus', title: '销售质检流程', width: 170, slots: { default: 'workflow' },
  });
  const actions = columns.at(-1);
  if (actions) actions.width = 320;
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns,
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
@@ -160,7 +76,6 @@
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
            sourceDocType: props.salesMode ? MesBizTypeEnum.WM_PRODUCT_SALES : undefined,
          });
        },
      },
@@ -180,7 +95,7 @@
<template>
  <Page auto-content-height><FormModal @success="handleRefresh" />
    <Grid :table-title="props.salesMode ? '销售质检单列表' : '出货检验单列表'">
    <Grid table-title="出货检验单列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
@@ -205,9 +120,6 @@
        <Button type="link" @click="handleDetail(row)">
          {{ row.code }}
        </Button>
      </template>
      <template #workflow="{ row }">
        <span>{{ workflowLabels[row.workflowStatus ?? 0] || '未知状态' }}</span>
      </template>
      <template #actions="{ row }">
        <TableAction
@@ -241,38 +153,7 @@
            },
          ]"
        />
        <Button v-if="props.salesMode && row.workflowStatus === 10 && hasAccessByCodes(['mes:qc-oqc:retest'])" type="link" @click="openRetest(row)">复检</Button>
        <Button v-if="props.salesMode && [20, 50].includes(row.workflowStatus ?? 0) && hasAccessByCodes(['mes:qc-oqc:sales-approve'])" type="link" @click="openReview(row, 'sales')">销售部审批</Button>
        <Button v-if="props.salesMode && [30, 60].includes(row.workflowStatus ?? 0) && hasAccessByCodes(['mes:qc-oqc:company-approve'])" type="link" @click="openReview(row, 'company')">公司质检审批</Button>
        <Button v-if="props.salesMode && hasAccessByCodes(['mes:qc-oqc:query'])" type="link" @click="openHistory(row)">流程记录</Button>
      </template>
    </Grid>
    <a-modal v-if="props.salesMode" v-model:open="retestOpen" title="销售质检复检" :confirm-loading="retestBusy" ok-text="提交复检" @ok="saveRetest">
      <p>检验单:{{ active?.code }}</p>
      <a-form layout="vertical">
        <a-form-item label="本次检测数量"><a-input-number v-model:value="retestForm.checkQuantity" :min="0" style="width: 100%" /></a-form-item>
        <a-form-item label="合格数量"><a-input-number v-model:value="retestForm.qualifiedQuantity" :min="0" style="width: 100%" /></a-form-item>
        <a-form-item label="不合格数量"><a-input-number v-model:value="retestForm.unqualifiedQuantity" :min="0" style="width: 100%" /></a-form-item>
        <a-form-item label="复检意见"><a-textarea v-model:value="retestForm.opinion" :rows="3" :maxlength="500" /></a-form-item>
      </a-form>
    </a-modal>
    <a-modal v-if="props.salesMode" v-model:open="reviewOpen" :title="reviewStage === 'sales' ? '销售部审批' : '公司质检审批'" :confirm-loading="reviewBusy" ok-text="提交审批" @ok="saveReview">
      <p>{{ active?.code }} Â· {{ workflowLabels[active?.workflowStatus ?? 0] }}</p>
      <a-radio-group v-model:value="reviewApproved" style="display: flex; gap: 16px; margin: 12px 0 18px">
        <a-radio :value="true">同意{{ reviewStage === 'company' ? active?.workflowStatus === 30 ? '出货放行' : '不合格处置' : '上报公司质检' }}</a-radio>
        <a-radio :value="false">驳回重检</a-radio>
      </a-radio-group>
      <a-textarea v-model:value="reviewOpinion" :rows="3" :maxlength="500" placeholder="请填写审批意见" />
    </a-modal>
    <a-modal v-if="props.salesMode" v-model:open="historyOpen" :title="`流程记录 Â· ${active?.code || ''}`" :footer="null">
      <a-timeline v-if="historyActions.length">
        <a-timeline-item v-for="action in historyActions" :key="action.id">
          <strong>{{ actionLabels[action.action] || action.action }}</strong>
          <span> Â· {{ action.createTime || '—' }} Â· æ“ä½œäºº {{ action.operatorId || '—' }}</span>
          <div v-if="action.opinion">{{ action.opinion }}</div>
        </a-timeline-item>
      </a-timeline>
      <a-empty v-else description="暂无流程记录" />
    </a-modal>
  </Page>
</template>
src/views/mes/qc/oqc/modules/form.vue
@@ -105,7 +105,7 @@
    return;
  }
  try {
    await confirm('是否提交出货检验结论?提交后进入审批流程,初检不合格需先复检。');
    await confirm('是否提交出货检验结论?');
  } catch {
    return;
  }
@@ -126,7 +126,7 @@
      originalSnapshot.value = current;
    }
    await finishOqc(id);
    message.success('检验已提交,请在销售质检菜单继续复检或审批');
    message.success('检验已提交');
    await modalApi.close();
    emit('success');
  } finally {
src/views/mes/qc/salesQuality/index.vue
ÎļþÒÑɾ³ý
src/views/mes/qc/seedQuality/index.vue
@@ -11,6 +11,7 @@
import {
  resolveSeedQuality,
  submitSeedQualityGroup,
  getSeedIndicatorRanges,
} from '#/api/mes/qc/seedQuality';
import { getWarehouseSimpleList } from '#/api/mes/wm/warehouse';
@@ -59,6 +60,17 @@
  { code: 'germination_rate', label: '发芽率', unit: '%' },
  { code: 'moisture', label: '水分', unit: '%' },
]);
const rangesReady = computed(() => indicators.value.every((item) =>
  item.minValue !== undefined && item.maxValue !== undefined));
async function loadRanges() {
  const ranges = await getSeedIndicatorRanges();
  for (const item of indicators.value) {
    const range = ranges.find((row) => row.indicatorCode === item.code);
    item.minValue = range?.minValue === undefined ? undefined : Number(range.minValue);
    item.maxValue = range?.maxValue === undefined ? undefined : Number(range.maxValue);
  }
}
const pendingCount = computed(() => target.value?.pendingBagIds?.length ?? 0);
const completedCount = computed(() => target.value?.completedBagIds?.length ?? 0);
@@ -102,8 +114,6 @@
  reason.value = '';
  indicators.value.forEach((item) => {
    item.value = undefined;
    item.minValue = undefined;
    item.maxValue = undefined;
  });
}
@@ -132,6 +142,7 @@
  resolving.value = true;
  submittedCount.value = undefined;
  try {
    await loadRanges().catch(() => undefined);
    const data = await resolveSeedQuality(value, scanType.value);
    if (data.canceled) {
      target.value = undefined;
@@ -166,7 +177,7 @@
    return;
  }
  if (!allEntered.value) {
    message.warning('请填写四项指标的实测值及合格上下限');
    message.warning('请填写四项指标的实测值和本次合格上下限');
    return;
  }
  if (indicators.value.some((item) => item.minValue === undefined || item.maxValue === undefined || item.minValue > item.maxValue)) {
@@ -190,8 +201,8 @@
    Modal.confirm({
      title: overallPassed.value ? '确认提交合格结果?' : '确认提交不合格结果?',
      content: overallPassed.value
        ? `本次 ${count} è¢‹å°†ç»Ÿä¸€è®°å½•为初检合格,待加工部和公司质检审批后才转入合格库。${scopeNote}`
        : `本次 ${count} è¢‹å°†è®°å½•为初检不合格,保留暂存库存并等待复检。${scopeNote}`,
        ? `本次 ${count} è¢‹å°†ç»Ÿä¸€è®°å½•为初检合格,待初检审核通过后复检;复检审核通过后才转入合格库。${scopeNote}`
        : `本次 ${count} è¢‹å°†è®°å½•为初检不合格,保留暂存库存;初检审核后决定是否进入复检。${scopeNote}`,
      okText: '确认提交',
      cancelText: '返回修改',
      okButtonProps: { danger: !overallPassed.value },
@@ -219,7 +230,7 @@
      })),
    });
    submittedCount.value = result.processedCount;
    message.success(`本次 ${result.processedCount} è¢‹å·²æäº¤ï¼š${passed ? '初检合格,待两级审批' : '初检不合格,待复检'}`);
    message.success(`本次 ${result.processedCount} è¢‹å·²æäº¤åˆæ£€ï¼Œå¾…初检审核`);
    if (pageRoot.value && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
      gsap.fromTo('.result-card', { scale: 0.97, autoAlpha: 0.5 }, { scale: 1, autoAlpha: 1, duration: 0.35, ease: 'power2.out' });
    }
@@ -235,6 +246,11 @@
}
onMounted(async () => {
  try {
    await loadRanges();
  } catch (error) {
    message.error(errorText(error));
  }
  try {
    const warehouses = await getWarehouseSimpleList();
    qualifiedWarehouses.value = (warehouses ?? []).filter(
@@ -266,7 +282,7 @@
      <div>
        <div class="eyebrow">MES / è´¨é‡ç®¡ç†</div>
        <h1>种子质检</h1>
        <p>识别待检范围并录入四项指标;合格经加工部与公司质检审批后入库,不合格先复检</p>
        <p>识别待检范围并录入四项指标;初检审核后复检,复检审核后入库或处置</p>
      </div>
      <div class="header-badge"><IconifyIcon icon="ep:checked" /> è¢‹ç  / æ‰˜ç›˜ç  / æ‰¹æ¬¡ç </div>
    </header>
@@ -318,6 +334,7 @@
            </a-form-item>
          </div>
          <p v-if="!qualifiedWarehouses.length" class="target-warning">暂无可用合格库,请联系管理员配置后再提交质检。</p>
          <a-alert v-if="!rangesReady" class="target-empty" type="info" show-icon message="本次请手动填写合格范围" description="管理员尚未配置完整默认范围;仍可手动填写四项指标的合格上下限。" />
          <a-divider orientation="left">指标结果</a-divider>
          <div class="indicator-list">
@@ -325,9 +342,9 @@
              <div class="indicator-name"><span class="indicator-dot" :class="isPassed(item) ? 'pass' : 'fail'" /><strong>{{ item.label }}</strong><small>{{ item.unit }}</small></div>
              <a-input-number v-model:value="item.value" class="indicator-input" :disabled="loading" :min="0" :precision="3" placeholder="实测值" />
              <span class="range-separator">范围</span>
              <a-input-number v-model:value="item.minValue" class="range-input" :disabled="loading" :min="0" :precision="3" placeholder="下限" />
              <a-input-number v-model:value="item.minValue" class="range-input" :disabled="loading" :min="0" :max="100" :precision="3" placeholder="下限" />
              <span>—</span>
              <a-input-number v-model:value="item.maxValue" class="range-input" :disabled="loading" :min="0" :precision="3" placeholder="上限" />
              <a-input-number v-model:value="item.maxValue" class="range-input" :disabled="loading" :min="0" :max="100" :precision="3" placeholder="上限" />
              <a-tag :color="!allEntered ? 'default' : isPassed(item) ? 'success' : 'error'">{{ !allEntered ? '待录入' : isPassed(item) ? '合格' : '超出范围' }}</a-tag>
            </div>
          </div>
@@ -341,7 +358,7 @@
        <template #title><div class="card-title"><IconifyIcon icon="ep:data-analysis" />判定说明</div></template>
        <div class="result-hero" :class="overallPassed ? 'passed' : allEntered ? 'failed-result' : 'pending-result'"><IconifyIcon :icon="overallPassed ? 'ep:circle-check-filled' : allEntered ? 'ep:warning-filled' : 'ep:edit-pen'" /><strong>{{ overallPassed ? '当前结果合格' : allEntered ? '当前存在不合格项' : '等待录入指标' }}</strong><span>{{ allEntered ? '实测值需处于最小值与最大值之间' : `已录入 ${enteredCount} / 4 é¡¹å®žæµ‹å€¼` }}</span></div>
        <div class="rule-list"><div><IconifyIcon icon="ep:check" /><span>先识别码值再录入指标,四项指标必须全部提交</span></div><div><IconifyIcon icon="ep:check" /><span>边界值按合格处理</span></div><div><IconifyIcon icon="ep:check" /><span>托盘码、批次码的结论应用于本次全部待检袋</span></div><div><IconifyIcon icon="ep:check" /><span>不合格结果需保留原因</span></div><div><IconifyIcon icon="ep:check" /><span>质检员与检验时间由服务端记录</span></div></div>
        <a-alert message="审批完成后流转库存" description="初检不改变库存;不合格待复检。复检结论经加工部和公司质检审批后,合格袋才转入合格库,不合格袋才报废。" type="info" show-icon />
        <a-alert message="复检审核完成后流转库存" description="初检及复检期间袋码留在暂存库。初检审核通过后进入复检;复检合格并审核通过才转入合格库,复检不合格经确认后才报废。" type="info" show-icon />
        <div v-if="submittedCount" class="success-note"><IconifyIcon icon="ep:success-filled" /><span>最近提交质检:<strong>{{ submittedCount }} è¢‹</strong></span></div>
      </a-card>
    </section>
src/views/mes/qc/seedQuality/record.vue
@@ -10,12 +10,16 @@
  getSeedQualityPage,
  getSeedQualityIndicators,
  getSeedQualityActions,
  getSeedIndicatorRanges,
  retestSeedQuality,
  resolveSeedRetestScope,
  submitSeedRetestScope,
  approveSeedQuality,
  approveSeedQualityBatch,
  type SeedQualityRecord,
  type SeedQualityIndicator,
  type SeedQualityAction,
  type SeedRetestScope,
} from '#/api/mes/qc/seedQuality';
const records = ref<SeedQualityRecord[]>([]);
@@ -37,13 +41,13 @@
);
const { hasAccessByCodes } = useAccess();
const workflowLabels: Record<number, string> = {
  10: '待复检', 20: '待加工部审批', 30: '待公司质检审批',
  40: '已放行', 50: '待加工部不合格评审', 60: '待公司不合格审批', 70: '已报废',
  10: '待复检', 20: '待初检审核', 25: '待初检不合格处理', 30: '待复检审核',
  40: '已放行', 50: '初检驳回待重检', 60: '待复检不合格处理', 70: '已报废',
};
const actionLabels: Record<string, string> = {
  INITIAL: '初检', RETEST: '复检', PROCESS_APPROVE: '加工部通过',
  PROCESS_REJECT: '加工部驳回', COMPANY_APPROVE: '公司质检通过',
  COMPANY_REJECT: '公司质检驳回',
  INITIAL: '初检', INITIAL_RECHECK: '重新初检', RETEST: '复检', PROCESS_APPROVE: '初检审核通过',
  PROCESS_REJECT: '初检审核驳回', COMPANY_APPROVE: '复检审核通过',
  COMPANY_REJECT: '复检审核驳回',
};
const indicatorLabels: Record<string, string> = {
  purity: '纯度', cleanliness: '净度', germination_rate: '发芽率', moisture: '水分',
@@ -73,6 +77,13 @@
const retestBusy = ref(false);
const retestIndicators = ref<SeedQualityIndicator[]>([]);
const retestReason = ref('');
const scopeOpen = ref(false);
const scopeType = ref<'PALLET' | 'BATCH'>('PALLET');
const scopeCode = ref('');
const retestScope = ref<SeedRetestScope>();
const scopeStatus = ref<10 | 50>(10);
const scopeBusy = ref(false);
const scopeIds = computed(() => scopeStatus.value === 10 ? retestScope.value?.retestIds || [] : retestScope.value?.initialRecheckIds || []);
const reviewOpen = ref(false);
const reviewBusy = ref(false);
const reviewStage = ref<'process' | 'company'>('process');
@@ -84,7 +95,7 @@
const selectableStage = computed(() => {
  if (!selectedRows.value.length) return undefined;
  if (!selectedRows.value.every((record) => record.workflowStatus === selectedRows.value[0]?.workflowStatus)) return undefined;
  if (selectedRows.value.every((record) => [20, 50].includes(record.workflowStatus))) return 'process';
  if (selectedRows.value.every((record) => [20, 25].includes(record.workflowStatus))) return 'process';
  if (selectedRows.value.every((record) => [30, 60].includes(record.workflowStatus))) return 'company';
  return undefined;
});
@@ -92,13 +103,65 @@
async function openRetest(record: SeedQualityRecord) {
  activeRecord.value = record;
  try {
    retestIndicators.value = await getSeedQualityIndicators(record.id);
    const previous = await getSeedQualityIndicators(record.id);
    const ranges = await getSeedIndicatorRanges().catch(() => []);
    retestIndicators.value = previous.map((item) => {
      const range = ranges.find((row) => row.indicatorCode === item.indicatorCode);
      return { ...item, minValue: range ? Number(range.minValue) : Number(item.minValue), maxValue: range ? Number(range.maxValue) : Number(item.maxValue) };
    });
    if (retestIndicators.value.length !== 4) throw new Error('质检指标不完整');
    retestIndicators.value = retestIndicators.value.map((item) => ({ ...item, value: undefined }));
    retestReason.value = '';
    retestOpen.value = true;
  } catch (error) {
    message.error((error as Error).message || '读取质检指标失败');
  }
}
function openScopeRetest() {
  scopeOpen.value = true; retestScope.value = undefined; scopeCode.value = ''; retestReason.value = '';
}
async function identifyScope() {
  if (!scopeCode.value.trim()) { message.warning('请扫描或输入托盘码 / ç”Ÿäº§æ‰¹æ¬¡ç '); return; }
  scopeBusy.value = true; retestScope.value = undefined;
  try {
    const data = await resolveSeedRetestScope(scopeCode.value.trim(), scopeType.value);
    if (!data.retestIds.length && !data.initialRecheckIds.length) {
      const pending = [
        data.pendingProcessCount ? `${data.pendingProcessCount} æ¡å¾…初检审核或不合格处理` : '',
        data.pendingCompanyCount ? `${data.pendingCompanyCount} æ¡å¾…复检审核或不合格处理` : '',
      ].filter(Boolean).join('、');
      throw new Error(pending
        ? `当前范围有${pending},请先完成对应审核或处理`
        : '当前范围没有待复检或待重新初检的袋码');
    }
    retestScope.value = data; scopeStatus.value = data.retestIds.length ? 10 : 50;
    const ranges = await getSeedIndicatorRanges().catch(() => []);
    retestIndicators.value = Object.entries(indicatorLabels).map(([indicatorCode]) => {
      const range = ranges.find((row) => row.indicatorCode === indicatorCode);
      return { indicatorCode, value: undefined, minValue: range ? Number(range.minValue) : undefined as unknown as number,
        maxValue: range ? Number(range.maxValue) : undefined as unknown as number };
    });
    retestReason.value = '';
  } catch (error) { message.error((error as Error).message || '识别失败'); }
  finally { scopeBusy.value = false; }
}
async function saveScopeRetest() {
  if (!retestScope.value || !scopeIds.value.length) return;
  if (retestIndicators.value.some(item => item.value === undefined || item.value === null || item.minValue === undefined || item.maxValue === undefined
    || Number(item.minValue) < 0 || Number(item.maxValue) > 100 || Number(item.minValue) > Number(item.maxValue))) {
    message.warning('请填写四项指标的实测值与本次合格范围'); return;
  }
  const passed = retestIndicators.value.every(item => Number(item.value) >= Number(item.minValue) && Number(item.value) <= Number(item.maxValue));
  if (!passed && !retestReason.value.trim()) { message.warning('不合格请填写原因'); return; }
  scopeBusy.value = true;
  try {
    const result = await submitSeedRetestScope({ code: retestScope.value.code, scanType: retestScope.value.scanType, workflowStatus: scopeStatus.value,
      qualityIds: scopeIds.value, reason: retestReason.value.trim(), indicators: retestIndicators.value.map(item => ({
        code: item.indicatorCode, value: Number(item.value), minValue: Number(item.minValue), maxValue: Number(item.maxValue),
      })) });
    message.success(`已完成 ${result.processedCount} è¢‹è´¨æ£€`); scopeOpen.value = false; refreshAll();
  } catch (error) { message.error((error as Error).message || '批量质检失败'); retestScope.value = undefined; }
  finally { scopeBusy.value = false; }
}
async function saveRetest() {
  const record = activeRecord.value;
@@ -106,27 +169,30 @@
  if (retestIndicators.value.some((item) => item.value === null || item.value === undefined
    || item.minValue === null || item.minValue === undefined || item.maxValue === null || item.maxValue === undefined
    || Number(item.minValue) > Number(item.maxValue))) {
    message.warning('请填写完整的实测值及有效上下限');
    message.warning('请填写完整的实测值');
    return;
  }
  const passed = retestIndicators.value.every((item) =>
    Number(item.value) >= Number(item.minValue) && Number(item.value) <= Number(item.maxValue));
  if (!passed && !retestReason.value.trim()) {
    message.warning('复检不合格请填写原因');
    message.warning('检验不合格请填写原因');
    return;
  }
  retestBusy.value = true;
  try {
    await retestSeedQuality(record.id, {
      code: record.code,
    const payload = {
      reason: retestReason.value.trim(),
      indicators: retestIndicators.value.map((item) => ({
        code: item.indicatorCode, value: Number(item.value),
        minValue: Number(item.minValue), maxValue: Number(item.maxValue),
      })),
    });
    message.success(passed ? '复检合格,待加工部审批' : '复检不合格,待加工部评审');
    };
    await retestSeedQuality(record.id, { ...payload, code: record.code });
    message.success(record.workflowStatus === 50
      ? passed ? '重新初检合格,待初检审核' : '重新初检不合格,待不合格处理'
      : passed ? '复检合格,待复检审核' : '复检不合格,待不合格处理');
    retestOpen.value = false;
    selectedIds.value = [];
    refreshAll();
  } catch (error) {
    message.error((error as Error).message || '复检提交失败');
@@ -154,8 +220,8 @@
}
async function saveReview() {
  if (!reviewIds.value.length) return;
  if (!reviewOpinion.value.trim()) {
    message.warning('请填写审批意见');
  if (!reviewApproved.value && !reviewOpinion.value.trim()) {
    message.warning('驳回时请填写审批意见');
    return;
  }
  reviewBusy.value = true;
@@ -254,7 +320,7 @@
      <div class="header-copy">
        <div class="eyebrow">MES / è´¨é‡ç®¡ç†</div>
        <h1>质检记录</h1>
        <p>跟踪初检、复检、加工部审批和公司质检审批;完成审批后才流转库存</p>
        <p>初检审核通过后进行复检,复检审核通过后才放行入库或处置不合格品</p>
      </div>
      <div class="header-badge">
        <span class="badge-pulse" />
@@ -291,7 +357,8 @@
          <span class="count-chip">{{ total.toLocaleString() }} æ¡</span>
        </div>
        <div class="filters">
          <a-button v-if="selectableStage && hasAccessByCodes([`mes:qc-seed-quality:${selectableStage === 'process' ? 'process-approve' : 'company-approve'}`])" @click="openBatchReview">批量{{ selectableStage === 'process' ? '加工部审批' : '公司质检审批' }}({{ selectedRows.length }})</a-button>
          <a-button v-if="hasAccessByCodes(['mes:qc-seed-quality:retest'])" type="primary" @click="openScopeRetest">按托盘 / æ‰¹æ¬¡æ‰¹é‡å¤æ£€</a-button>
          <a-button v-if="selectableStage && hasAccessByCodes([`mes:qc-seed-quality:${selectableStage === 'process' ? 'process-approve' : 'company-approve'}`])" @click="openBatchReview">批量{{ selectableStage === 'process' ? selectedRows[0]?.workflowStatus === 25 ? '初检不合格处理' : '初检审核' : selectedRows[0]?.workflowStatus === 60 ? '复检不合格处理' : '复检审核' }}({{ selectedRows.length }})</a-button>
          <a-input v-model:value="code" allow-clear placeholder="袋码" @press-enter="searchRecords">
            <template #prefix><IconifyIcon icon="ep:box" /></template>
          </a-input>
@@ -320,7 +387,7 @@
        :data-source="records"
        :row-selection="{ selectedRowKeys: selectedIds, onChange: (keys: (string | number)[]) => { selectedIds = keys.map(Number); } }"
        :loading="loading"
        :scroll="{ x: 1120 }"
        :scroll="{ x: 1900 }"
        :pagination="{
          current: page,
          pageSize,
@@ -343,6 +410,9 @@
        </a-table-column>
        <a-table-column key="code" title="袋码" data-index="code" min-width="220">
          <template #default="{ record }"><span class="mono strong">{{ record.code }}</span></template>
        </a-table-column>
        <a-table-column key="palletCode" title="托盘码" data-index="palletCode" min-width="190">
          <template #default="{ record }"><span class="mono">{{ record.palletCode || '—' }}</span></template>
        </a-table-column>
        <a-table-column key="batchCode" title="生产批次" data-index="batchCode" min-width="190">
          <template #default="{ record }">{{ record.batchCode || '—' }}</template>
@@ -373,32 +443,53 @@
        <a-table-column key="actions" title="操作" width="260" fixed="right">
          <template #default="{ record }">
            <a-space>
              <a-button v-if="record.workflowStatus === 10 && hasAccessByCodes(['mes:qc-seed-quality:retest'])" type="link" @click="openRetest(record)">复检</a-button>
              <a-button v-if="[20, 50].includes(record.workflowStatus) && hasAccessByCodes(['mes:qc-seed-quality:process-approve'])" type="link" @click="openReview(record, 'process')">加工部审批</a-button>
              <a-button v-if="[30, 60].includes(record.workflowStatus) && hasAccessByCodes(['mes:qc-seed-quality:company-approve'])" type="link" @click="openReview(record, 'company')">公司质检审批</a-button>
              <a-button v-if="[10, 50].includes(record.workflowStatus) && hasAccessByCodes(['mes:qc-seed-quality:retest'])" type="link" @click="openRetest(record)">{{ record.workflowStatus === 50 ? '重新初检' : '复检' }}</a-button>
              <a-button v-if="[20, 25].includes(record.workflowStatus) && hasAccessByCodes(['mes:qc-seed-quality:process-approve'])" type="link" @click="openReview(record, 'process')">{{ record.workflowStatus === 25 ? '不合格处理' : '初检审核' }}</a-button>
              <a-button v-if="[30, 60].includes(record.workflowStatus) && hasAccessByCodes(['mes:qc-seed-quality:company-approve'])" type="link" @click="openReview(record, 'company')">{{ record.workflowStatus === 60 ? '不合格处理' : '复检审核' }}</a-button>
              <a-button v-if="hasAccessByCodes(['mes:qc-seed-quality:query'])" type="link" @click="openHistory(record)">流程记录</a-button>
            </a-space>
          </template>
        </a-table-column>
      </a-table>
    </section>
    <a-modal v-model:open="retestOpen" title="种子质检复检" :confirm-loading="retestBusy" ok-text="提交复检" @ok="saveRetest">
      <p class="muted">袋码:{{ activeRecord?.code }}。原指标范围已带入,请录入本次实测值并核对上下限。</p>
    <a-modal v-model:open="retestOpen" :title="activeRecord?.workflowStatus === 50 ? '重新初检' : '种子质检复检'" :confirm-loading="retestBusy" :ok-text="activeRecord?.workflowStatus === 50 ? '提交初检' : '提交复检'" @ok="saveRetest">
      <p class="muted">袋码:{{ activeRecord?.code }}。四项指标与初检一致;请录入本次实测值并核对合格范围。</p>
      <div v-for="item in retestIndicators" :key="item.indicatorCode" class="retest-row">
        <strong>{{ indicatorLabels[item.indicatorCode] || item.indicatorCode }}</strong>
        <a-input-number v-model:value="item.value" :min="0" :precision="3" placeholder="实测值" />
        <a-input-number v-model:value="item.minValue" :min="0" :precision="3" placeholder="下限" />
        <a-input-number v-model:value="item.maxValue" :min="0" :precision="3" placeholder="上限" />
        <a-input-number v-model:value="item.value" :min="0" :precision="3" placeholder="本次实测值" />
        <a-input-number v-model:value="item.minValue" :min="0" :max="100" :precision="3" placeholder="下限" />
        <a-input-number v-model:value="item.maxValue" :min="0" :max="100" :precision="3" placeholder="上限" />
      </div>
      <a-textarea v-model:value="retestReason" :rows="3" :maxlength="500" placeholder="复检不合格时填写原因" />
      <a-textarea v-model:value="retestReason" :rows="3" :maxlength="500" placeholder="本次检验不合格时填写原因" />
    </a-modal>
    <a-modal v-model:open="reviewOpen" :title="reviewStage === 'process' ? '加工部审批' : '公司质检审批'" :confirm-loading="reviewBusy" ok-text="提交审批" @ok="saveReview">
    <a-modal v-model:open="scopeOpen" title="按托盘 / ç”Ÿäº§æ‰¹æ¬¡æ‰¹é‡å¤æ£€æˆ–重新初检" :confirm-loading="scopeBusy" ok-text="提交本阶段全部袋码" :ok-button-props="{ disabled: !scopeIds.length }" @ok="saveScopeRetest">
      <a-space class="scope-scan">
        <a-select v-model:value="scopeType" style="width: 130px" :options="[{ label: '托盘码', value: 'PALLET' }, { label: '生产批次码', value: 'BATCH' }]" @change="retestScope = undefined" />
        <a-input-search v-model:value="scopeCode" placeholder="扫描或输入码值" enter-button="识别" :loading="scopeBusy" @search="identifyScope" />
      </a-space>
      <template v-if="retestScope">
        <p>已识别 {{ retestScope.code }} Â· æœ‰æ•ˆè¢‹ {{ retestScope.totalCount }} Â· ç”Ÿäº§æ‰¹æ¬¡ {{ retestScope.batchCode || '—' }}</p>
        <a-radio-group v-model:value="scopeStatus">
          <a-radio-button v-if="retestScope.retestIds.length" :value="10">复检 {{ retestScope.retestIds.length }} è¢‹</a-radio-button>
          <a-radio-button v-if="retestScope.initialRecheckIds.length" :value="50">重新初检 {{ retestScope.initialRecheckIds.length }} è¢‹</a-radio-button>
        </a-radio-group>
        <p class="muted">同一组指标将应用于本阶段全部 {{ scopeIds.length }} è¢‹ã€‚模板范围可以修改。</p>
        <div v-for="item in retestIndicators" :key="item.indicatorCode" class="retest-row">
          <strong>{{ indicatorLabels[item.indicatorCode] }}</strong>
          <a-input-number v-model:value="item.value" :min="0" :max="100" :precision="3" placeholder="实测值" />
          <a-input-number v-model:value="item.minValue" :min="0" :max="100" :precision="3" placeholder="下限" />
          <a-input-number v-model:value="item.maxValue" :min="0" :max="100" :precision="3" placeholder="上限" />
        </div>
        <a-textarea v-model:value="retestReason" :rows="3" :maxlength="500" placeholder="不合格时填写原因" />
      </template>
    </a-modal>
    <a-modal v-model:open="reviewOpen" :title="[25, 60].includes(activeRecord?.workflowStatus ?? 0) ? '不合格处理' : reviewStage === 'process' ? '初检审核' : '复检审核'" :confirm-loading="reviewBusy" ok-text="提交审核" @ok="saveReview">
      <p>{{ reviewIds.length > 1 ? `共 ${reviewIds.length} æ¡è®°å½•` : activeRecord?.code }} Â· {{ workflowLabels[activeRecord?.workflowStatus ?? 0] }}</p>
      <a-radio-group v-model:value="reviewApproved" class="review-choice">
        <a-radio :value="true">同意{{ reviewStage === 'company' ? activeRecord?.workflowStatus === 30 ? '放行入库' : '报废处置' : '上报公司质检' }}</a-radio>
        <a-radio :value="false">驳回重检</a-radio>
        <a-radio :value="true">{{ reviewStage === 'process' ? activeRecord?.workflowStatus === 25 ? '确认不合格,安排复检' : '通过,进入复检' : activeRecord?.workflowStatus === 30 ? '通过,放行入库' : '确认不合格,报废处置' }}</a-radio>
        <a-radio :value="false">{{ reviewStage === 'process' ? '驳回,重新初检' : '驳回,重新复检' }}</a-radio>
      </a-radio-group>
      <a-textarea v-model:value="reviewOpinion" :rows="3" :maxlength="500" placeholder="请填写审批意见" />
      <a-textarea v-model:value="reviewOpinion" :rows="3" :maxlength="500" :placeholder="reviewApproved ? '审批意见(选填)' : '请填写驳回意见'" />
    </a-modal>
    <a-modal v-model:open="historyOpen" :title="`流程记录 Â· ${activeRecord?.code || ''}`" :footer="null">
      <a-timeline v-if="historyActions.length">
@@ -458,6 +549,8 @@
.filters :deep(.ant-btn .iconify) { font-size: 14px; }
.retest-row { display: grid; grid-template-columns: 60px repeat(3, 1fr); gap: 8px; align-items: center; margin-bottom: 10px; }
.retest-row :deep(.ant-input-number) { width: 100%; }
.scope-scan { display: flex; width: 100%; margin-bottom: 16px; }
.scope-scan :deep(.ant-space-item:last-child) { flex: 1; }
.review-choice { display: flex; gap: 16px; margin: 10px 0 18px; }
/* ===== å•元格 ===== */
src/views/mes/qc/seedRange/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,91 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import { message } from 'ant-design-vue';
import { useAccess } from '@vben/access';
import { getSeedIndicatorRanges, saveSeedIndicatorRanges } from '#/api/mes/qc/seedQuality';
const definitions = [
  { indicatorCode: 'purity', name: '纯度' },
  { indicatorCode: 'cleanliness', name: '净度' },
  { indicatorCode: 'germination_rate', name: '发芽率' },
  { indicatorCode: 'moisture', name: '水分' },
];
const ranges = ref(definitions.map((item) => ({ ...item, minValue: undefined as number | undefined, maxValue: undefined as number | undefined })));
const loading = ref(false);
const saving = ref(false);
const { hasAccessByCodes } = useAccess();
const complete = computed(() => ranges.value.every((item) =>
  item.minValue !== undefined && item.maxValue !== undefined && item.minValue <= item.maxValue));
async function load() {
  loading.value = true;
  try {
    const data = await getSeedIndicatorRanges();
    ranges.value = definitions.map((item) => {
      const saved = data.find((row) => row.indicatorCode === item.indicatorCode);
      return { ...item, minValue: saved ? Number(saved.minValue) : undefined,
        maxValue: saved ? Number(saved.maxValue) : undefined };
    });
  } catch (error) {
    message.error((error as Error).message || '读取合格范围失败');
  } finally {
    loading.value = false;
  }
}
async function save() {
  if (!complete.value || ranges.value.some((item) => item.minValue! < 0 || item.maxValue! > 100)) {
    message.warning('请为四项指标配置 0–100% çš„æœ‰æ•ˆä¸Šä¸‹é™');
    return;
  }
  saving.value = true;
  try {
    await saveSeedIndicatorRanges(ranges.value.map((item) => ({
      indicatorCode: item.indicatorCode,
      minValue: item.minValue!, maxValue: item.maxValue!,
    })));
    message.success('默认合格范围已保存,检验时仍可按实际情况调整');
    await load();
  } catch (error) {
    message.error((error as Error).message || '保存合格范围失败');
  } finally {
    saving.value = false;
  }
}
onMounted(load);
</script>
<template>
  <div class="range-page">
    <header class="page-header">
      <div><div class="eyebrow">MES / è´¨é‡ç®¡ç†</div><h1>质检项合格范围配置</h1>
        <p>为初检和复检提供四项指标的默认范围。</p></div>
    </header>
    <a-alert type="info" show-icon message="这里配置的是填写模板" description="检验员可以修改本次合格上下限;实际提交的上下限和实测值一同保存,历史记录不受模板修改影响。" />
    <a-card class="range-card" :loading="loading" :bordered="false">
      <div class="grid-head"><span>质检项</span><span>合格下限</span><span>合格上限</span><span>单位</span></div>
      <div v-for="item in ranges" :key="item.indicatorCode" class="range-row">
        <strong>{{ item.name }}</strong>
        <a-input-number v-model:value="item.minValue" :min="0" :max="100" :precision="3" :disabled="!hasAccessByCodes(['mes:qc-seed-range:update'])" placeholder="下限" />
        <a-input-number v-model:value="item.maxValue" :min="0" :max="100" :precision="3" :disabled="!hasAccessByCodes(['mes:qc-seed-range:update'])" placeholder="上限" />
        <span>%</span>
      </div>
      <div class="actions">
        <a-button @click="load">刷新</a-button>
        <a-button v-if="hasAccessByCodes(['mes:qc-seed-range:update'])" type="primary" :loading="saving" :disabled="!complete" @click="save">保存合格范围</a-button>
      </div>
    </a-card>
  </div>
</template>
<style scoped>
.range-page { padding: 24px; min-height: 100%; background: #f4f7fb; }
.page-header { margin-bottom: 20px; }.eyebrow { color: #1677ff; font-size: 12px; font-weight: 700; letter-spacing: .08em; }
h1 { margin: 6px 0; color: #172033; font-size: 28px; }.page-header p { margin: 0; color: #718096; }
.range-card { margin-top: 16px; border-radius: 10px; box-shadow: 0 3px 12px #243b5310; }
.grid-head, .range-row { display: grid; grid-template-columns: minmax(120px, 1fr) minmax(120px, 1fr) minmax(120px, 1fr) 70px; align-items: center; gap: 16px; padding: 12px 16px; }
.grid-head { color: #718096; background: #f7f9fc; font-weight: 600; }.range-row { border-bottom: 1px solid #e8edf5; }.range-row :deep(.ant-input-number) { width: 100%; }
.actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px; }
@media (max-width: 640px) { .range-page { padding: 14px; }.grid-head { display: none; }.range-row { grid-template-columns: 1fr 1fr; }.range-row strong { grid-column: 1 / -1; } }
</style>
src/views/mobile/quality-records.vue
@@ -1,11 +1,58 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { getSeedQualityPage, type SeedQualityRecord } from '#/api/mes/qc/seedQuality';
import { computed, onMounted, ref } from 'vue';
import { useAccess } from '@vben/access';
import { getSeedIndicatorRanges, getSeedQualityPage, resolveSeedRetestScope, submitSeedRetestScope, type SeedQualityRecord, type SeedRetestScope } from '#/api/mes/qc/seedQuality';
import { errorText } from '#/mobile/api';
import { extractCode } from '#/mobile/access';
import Scanner from './scanner.vue';
const rows = ref<SeedQualityRecord[]>([]), busy = ref(false), error = ref('');
const { hasAccessByCodes } = useAccess();
const canRetest = computed(() => hasAccessByCodes(['mes:qc-seed-quality:retest']));
const scanType = ref<'PALLET' | 'BATCH'>('PALLET'), scope = ref<SeedRetestScope>();
const workflowStatus = ref<10 | 50>(10), reason = ref(''), notice = ref('');
const indicators = ref([
  { code: 'purity', name: '纯度', value: '', minValue: '', maxValue: '' },
  { code: 'cleanliness', name: '净度', value: '', minValue: '', maxValue: '' },
  { code: 'germination_rate', name: '发芽率', value: '', minValue: '', maxValue: '' },
  { code: 'moisture', name: '水分', value: '', minValue: '', maxValue: '' },
]);
const targetIds = computed(() => workflowStatus.value === 10 ? scope.value?.retestIds || [] : scope.value?.initialRecheckIds || []);
const complete = computed(() => indicators.value.every(i => [i.value, i.minValue, i.maxValue].every(v => v.trim() !== '' && Number.isFinite(Number(v)))
  && Number(i.value) >= 0 && Number(i.value) <= 100 && Number(i.minValue) >= 0 && Number(i.maxValue) <= 100 && Number(i.minValue) <= Number(i.maxValue)));
const passed = computed(() => complete.value && indicators.value.every(i => Number(i.value) >= Number(i.minValue) && Number(i.value) <= Number(i.maxValue)));
async function identifyRetest(raw: string) {
  if (busy.value) return;
  busy.value = true; error.value = ''; notice.value = ''; scope.value = undefined;
  try {
    const data = await resolveSeedRetestScope(extractCode(raw), scanType.value);
    if (!data.retestIds.length && !data.initialRecheckIds.length) throw new Error('当前托盘或批次没有待复检或待重新初检的记录');
    scope.value = data;
    workflowStatus.value = data.retestIds.length ? 10 : 50;
    indicators.value.forEach(i => { i.value = ''; i.minValue = ''; i.maxValue = ''; }); reason.value = '';
    try {
      const ranges = await getSeedIndicatorRanges();
      indicators.value.forEach(i => { const range = ranges.find(r => r.indicatorCode === i.code); if (range) { i.minValue = String(range.minValue); i.maxValue = String(range.maxValue); } });
    } catch { /* å¯æ‰‹åŠ¨å¡«å†™æœ¬æ¬¡èŒƒå›´ */ }
  } catch (e) { error.value = errorText(e); } finally { busy.value = false; }
}
async function submitRetest() {
  if (busy.value || !scope.value || !targetIds.value.length || !complete.value) return;
  if (!passed.value && !reason.value.trim()) { error.value = '检验不合格请填写原因'; return; }
  const count = targetIds.value.length;
  if (!window.confirm(`确认将本次指标用于 ${count} è¢‹${workflowStatus.value === 10 ? '复检' : '重新初检'}?`)) return;
  busy.value = true; error.value = '';
  try {
    const result = await submitSeedRetestScope({ code: scope.value.code, scanType: scope.value.scanType, workflowStatus: workflowStatus.value,
      qualityIds: targetIds.value, reason: reason.value.trim() || undefined,
      indicators: indicators.value.map(i => ({ code: i.code, value: Number(i.value), minValue: Number(i.minValue), maxValue: Number(i.maxValue) })) });
    notice.value = `已完成 ${result.processedCount} è¢‹æ£€éªŒï¼Œç­‰å¾…后续审核或不合格处理。`;
    scope.value = undefined;
  } catch (e) { error.value = errorText(e) + '。请重新扫码核对清单后再提交。'; scope.value = undefined; }
  finally { busy.value = false; if (notice.value) void load(); }
}
const code = ref(''), batchCode = ref(''), result = ref(''), page = ref(0), total = ref(0);
const statusNames: Record<number, string> = { 10: '待复检', 20: '待初检审核', 25: '待初检不合格处理', 30: '待复检审核', 40: '已放行', 50: '待重新初检', 60: '待复检不合格处理', 70: '已报废' };
let filters: { code?: string; batchCode?: string; result?: number } = {};
async function load(reset = true) {
  if (busy.value) return;
@@ -21,6 +68,25 @@
onMounted(() => load());
</script>
<template>
  <section v-if="canRetest" class="m-panel">
    <h2 class="m-title">按托盘 / æ‰¹æ¬¡æ‰¹é‡è´¨æ£€</h2>
    <p class="m-muted">扫描托盘码或生产批次码,系统会列出待复检和待重新初检的袋数。</p>
    <label class="m-field"><span>扫码类型</span><select v-model="scanType" :disabled="busy" @change="scope = undefined"><option value="PALLET">托盘码</option><option value="BATCH">生产批次码</option></select></label>
    <Scanner :disabled="busy" @code="identifyRetest" />
    <form v-if="scope" @submit.prevent="submitRetest">
      <p class="m-muted">{{scope.code}} Â· {{scope.batchCode || '未标注批次'}} Â· æœ‰æ•ˆè¢‹ {{scope.totalCount}}</p>
      <label class="m-field"><span>质检阶段</span><select v-model="workflowStatus" :disabled="busy"><option v-if="scope.retestIds.length" :value="10">复检({{scope.retestIds.length}} è¢‹ï¼‰</option><option v-if="scope.initialRecheckIds.length" :value="50">重新初检({{scope.initialRecheckIds.length}} è¢‹ï¼‰</option></select></label>
      <p class="m-warning">同一组指标将用于本阶段全部 {{targetIds.length}} è¢‹ã€‚请核对抽样结果是否适用。</p>
      <fieldset v-for="i in indicators" :key="i.code" class="m-panel m-quality-indicator" :disabled="busy"><legend>{{i.name}}(%)</legend>
        <label class="m-field"><span>实测值</span><input v-model="i.value" type="number" inputmode="decimal" min="0" max="100" step="any" required /></label>
        <div class="m-grid"><label class="m-field"><span>合格下限</span><input v-model="i.minValue" type="number" inputmode="decimal" min="0" max="100" step="any" required /></label><label class="m-field"><span>合格上限</span><input v-model="i.maxValue" type="number" inputmode="decimal" min="0" max="100" step="any" required /></label></div>
      </fieldset>
      <p class="m-muted">上下限从模板带入,可按本次检验标准调整。</p>
      <label class="m-field"><span>{{complete && !passed ? '不合格原因(必填)' : '检验备注'}}</span><textarea v-model="reason" :required="complete && !passed" maxlength="500" rows="3" /></label>
      <button class="m-primary" :disabled="busy || !complete || !targetIds.length">{{busy ? '正在提交…' : `提交 ${targetIds.length} è¢‹è´¨æ£€`}}</button>
    </form>
  </section>
  <p v-if="notice" class="m-success" role="status">{{notice}}</p>
  <p class="m-muted">查看质检结果、批次与不合格原因。</p>
  <Scanner :disabled="busy" @code="value => { code = value; load(); }" />
  <form class="m-panel" @submit.prevent="load()">
@@ -34,7 +100,7 @@
  <p v-if="!rows.length && !busy && !error" class="m-empty">暂无匹配的质检记录</p>
  <article v-for="row in rows" :key="row.id" class="m-panel">
    <strong :class="['m-status', row.result === 1 ? 'success' : row.result === 2 ? 'failed' : '']">{{row.result === 1 ? '合格' : row.result === 2 ? '不合格' : '处理中'}}</strong>
    <dl><div class="m-row"><dt>追溯码</dt><dd>{{row.code}}</dd></div><div class="m-row"><dt>生产批次</dt><dd>{{row.batchCode || '—'}}</dd></div><div class="m-row"><dt>检验时间</dt><dd>{{row.inspectedAt || row.createTime || '—'}}</dd></div><div v-if="row.reason" class="m-row"><dt>原因 / å¤‡æ³¨</dt><dd>{{row.reason}}</dd></div></dl>
    <dl><div class="m-row"><dt>追溯码</dt><dd>{{row.code}}</dd></div><div class="m-row"><dt>生产批次</dt><dd>{{row.batchCode || '—'}}</dd></div><div class="m-row"><dt>流程状态</dt><dd>{{statusNames[row.workflowStatus] || '未知状态'}}</dd></div><div class="m-row"><dt>检验时间</dt><dd>{{row.inspectedAt || row.createTime || '—'}}</dd></div><div v-if="row.reason" class="m-row"><dt>原因 / å¤‡æ³¨</dt><dd>{{row.reason}}</dd></div></dl>
  </article>
  <button v-if="rows.length < total" class="m-secondary" :disabled="busy" @click="load(false)">{{busy ? '正在加载…' : '加载更多'}}</button>
</template>
src/views/mobile/quality.vue
@@ -2,7 +2,7 @@
import { computed, onMounted, ref } from 'vue';
import { onBeforeRouteLeave } from 'vue-router';
import { useUserStore } from '@vben/stores';
import { resolveSeedQuality, submitSeedQualityGroup, type SeedQualityScan } from '#/api/mes/qc/seedQuality';
import { getSeedIndicatorRanges, resolveSeedQuality, submitSeedQualityGroup, type SeedQualityScan } from '#/api/mes/qc/seedQuality';
import { getWarehouseSimpleList, type MesWmWarehouseApi } from '#/api/mes/wm/warehouse';
import { allowedMobilePath, extractCode } from '#/mobile/access';
import { errorText } from '#/mobile/api';
@@ -18,10 +18,10 @@
const pendingType = ref<ScanType>('BAG'), pendingIds = ref<number[]>([]);
const storageKey = 'mom-mobile-quality-' + useUserStore().userInfo?.id;
const indicators = ref([
  { code: 'purity', name: '纯度', value: '', minValue: '0', maxValue: '100' },
  { code: 'cleanliness', name: '净度', value: '', minValue: '0', maxValue: '100' },
  { code: 'germination_rate', name: '发芽率', value: '', minValue: '0', maxValue: '100' },
  { code: 'moisture', name: '水分', value: '', minValue: '0', maxValue: '100' },
  { code: 'purity', name: '纯度', value: '', minValue: '', maxValue: '' },
  { code: 'cleanliness', name: '净度', value: '', minValue: '', maxValue: '' },
  { code: 'germination_rate', name: '发芽率', value: '', minValue: '', maxValue: '' },
  { code: 'moisture', name: '水分', value: '', minValue: '', maxValue: '' },
]);
const complete = computed(() => indicators.value.every(i => [i.value, i.minValue, i.maxValue].every(v => String(v).trim() !== '' && Number.isFinite(Number(v))) && Number(i.minValue) >= 0 && Number(i.maxValue) <= 100 && Number(i.value) >= 0 && Number(i.value) <= 100 && Number(i.minValue) <= Number(i.maxValue)));
const passed = computed(() => complete.value && indicators.value.every(i => Number(i.value) >= Number(i.minValue) && Number(i.value) <= Number(i.maxValue)));
@@ -33,20 +33,24 @@
    const data = await resolveSeedQuality(extractCode(raw), scanType.value);
    if (data.canceled || !data.inbound) throw new Error('该对象已作废或有效袋码尚未全部入库,不能质检');
    if (!data.pendingBagIds?.length) throw new Error('该对象没有待质检袋码,请在质检记录中查看已有结果');
    bag.value = data; indicators.value.forEach(i => i.value = ''); reason.value = '';
    bag.value = data; indicators.value.forEach(i => { i.value = ''; i.minValue = ''; i.maxValue = ''; }); reason.value = '';
    try {
      const ranges = await getSeedIndicatorRanges();
      indicators.value.forEach(i => { const range = ranges.find(r => r.indicatorCode === i.code); if (range) { i.minValue = String(range.minValue); i.maxValue = String(range.maxValue); } });
    } catch { /* æ¨¡æ¿ä¸å¯ç”¨æ—¶ä»å…è®¸æ‰‹åŠ¨å¡«å†™æœ¬æ¬¡èŒƒå›´ */ }
  } catch (e) { error.value = errorText(e); } finally { busy.value = false; }
}
async function submit() {
  if (busy.value || pendingCode.value || !bag.value || !complete.value || !warehouseId.value) return;
  if (!passed.value && !reason.value.trim()) { error.value = '请填写不合格原因'; return; }
  const count = bag.value.pendingBagIds!.length;
  if (!window.confirm(passed.value ? `确认对本次 ${count} è¢‹ç»Ÿä¸€åˆ¤å®šåˆæ ¼ï¼Ÿå…¨éƒ¨å°†è½¬å…¥æ‰€é€‰åˆæ ¼åº“。` : `确认对本次 ${count} è¢‹ç»Ÿä¸€åˆ¤å®šä¸åˆæ ¼ï¼Ÿå…¨éƒ¨å°†ä»Žæš‚存库存移除并标记报废。`)) return;
  if (!window.confirm(passed.value ? `确认对本次 ${count} è¢‹ç»Ÿä¸€æäº¤åˆæ£€åˆæ ¼ï¼Ÿå®¡æ ¸é€šè¿‡åŽè¿›å…¥å¤æ£€ã€‚` : `确认对本次 ${count} è¢‹ç»Ÿä¸€æäº¤åˆæ£€ä¸åˆæ ¼ï¼Ÿå°†è¿›å…¥ä¸åˆæ ¼å¤„理。`)) return;
  error.value = ''; notice.value = '';
  pendingCode.value = bag.value.code; pendingType.value = scanType.value; pendingIds.value = [...bag.value.pendingBagIds!];
  sessionStorage.setItem(storageKey, JSON.stringify({ code: pendingCode.value, scanType: pendingType.value, bagIds: pendingIds.value })); busy.value = true;
  try {
    const result = await submitSeedQualityGroup({ code: bag.value.code, scanType: scanType.value, bagIds: pendingIds.value, qualifiedWarehouseId: warehouseId.value, reason: reason.value.trim() || undefined, indicators: indicators.value.map(i => ({ code: i.code, value: Number(i.value), minValue: Number(i.minValue), maxValue: Number(i.maxValue) })) });
    clearPending(); notice.value = `本次 ${result.processedCount} è¢‹` + (passed.value ? '质检合格,已转入合格库。' : '质检不合格,已记录原因并移除暂存库存。');
    clearPending(); notice.value = `本次 ${result.processedCount} è¢‹` + (passed.value ? '初检合格,等待审核。' : '初检不合格,等待不合格处理。');
    bag.value = undefined; reason.value = ''; indicators.value.forEach(i => i.value = '');
  } catch (e) { error.value = '提交结果待确认:' + errorText(e) + '。请核实后再操作下一袋。'; } finally { busy.value = false; }
}
@@ -87,8 +91,8 @@
        <label class="m-field"><span>实测值</span><input v-model="i.value" type="number" inputmode="decimal" min="0" max="100" step="any" required placeholder="输入实测百分比" /></label>
        <div class="m-grid"><label class="m-field"><span>合格下限</span><input v-model="i.minValue" type="number" inputmode="decimal" min="0" max="100" step="any" required /></label><label class="m-field"><span>合格上限</span><input v-model="i.maxValue" type="number" inputmode="decimal" min="0" max="100" step="any" required /></label></div>
      </fieldset>
      <div class="m-panel"><p class="m-muted">上下限沿用现有质检规则录入方式,请按该品种检验标准核对。</p><Picker v-model="warehouseId" label="目标合格库" :options="warehouses" :disabled="busy" /><p v-if="!warehouses.length && !busy" class="m-warning">暂无可用合格库,请联系管理员配置。</p><label class="m-field"><span>{{complete && !passed ? '不合格原因(必填)' : '检验备注'}}</span><textarea v-model="reason" :disabled="busy" :required="complete && !passed" rows="3" maxlength="255" /></label></div>
      <div class="m-action-bar"><p v-if="complete" :class="passed ? 'm-success' : 'm-warning'">预判:{{passed ? '合格,提交后转入合格库' : '不合格,提交后移除暂存库存'}}。最终以服务端结果为准。</p><button class="m-primary" :disabled="busy || !complete || !warehouseId">{{busy ? '正在提交…' : '确认提交质检'}}</button></div>
      <div class="m-panel"><p class="m-muted">合格范围从模板带入,可在本次质检中修改;请按该品种标准核对。</p><Picker v-model="warehouseId" label="目标合格库" :options="warehouses" :disabled="busy" /><p v-if="!warehouses.length && !busy" class="m-warning">暂无可用合格库,请联系管理员配置。</p><label class="m-field"><span>{{complete && !passed ? '不合格原因(必填)' : '检验备注'}}</span><textarea v-model="reason" :disabled="busy" :required="complete && !passed" rows="3" maxlength="255" /></label></div>
      <div class="m-action-bar"><p v-if="complete" :class="passed ? 'm-success' : 'm-warning'">预判:{{passed ? '初检合格,等待初检审核' : '初检不合格,进入不合格处理'}}。最终以服务端结果为准。</p><button class="m-primary" :disabled="busy || !complete || !warehouseId">{{busy ? '正在提交…' : '确认提交质检'}}</button></div>
    </form>
  </template>
  <RouterLink v-if="allowedMobilePath('/mobile/quality-records')" to="/mobile/quality-records" class="m-link">查看质检记录</RouterLink>