yuan
2026-05-18 e5ed471a20179fef875aed7341a5ae6d5b79a8c7
Merge remote-tracking branch 'origin/dev_NEW_pro' into dev_鹤壁_强信宇_pro

# Conflicts:
# multiple/assets/favicon/QXYfavicon.ico
# multiple/config.json
已添加21个文件
已修改75个文件
4453 ■■■■ 文件已修改
multiple/assets/favicon/HYJCfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/HYLQfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/JHYfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/JXJHfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/XCDQfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/XSWHfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/YTJZfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/ZQSYfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/HYJCLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/HYLQLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/JHYLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/JXJHLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/XCDQLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/XSWHLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/YTJZLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/ZQSYLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/config.json 87 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
multiple/multiple-build.js 192 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/equipmentManagement/repair.js 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inventoryManagement/stockInventory.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/styles/sidebar.scss 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AIChatSidebar/assistants/index.js 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AIChatSidebar/assistants/productionAssistant.js 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AIChatSidebar/assistants/salesAssistant.js 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AIChatSidebar/index.vue 1137 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/ProcessParamListDialog.vue 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Sidebar/index.vue 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/aiIndustrialBrain/MAINTAIN_RULES.md 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/aiIndustrialBrain/components/AiAssistantWorkspace.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/aiIndustrialBrain/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/product/index.vue 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/supplierManage/components/BlacklistTab.vue 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/supplierManage/components/HomeTab.vue 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/components/infoFormDia.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/index.vue 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/sealManagement/index.vue 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/inspectionManagement/components/formDia.vue 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/inspectionManagement/index.vue 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/measurementEquipment/components/formDia.vue 34 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/measurementEquipment/index.vue 48 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/repair/Modal/AcceptanceModal.vue 144 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/repair/Modal/RepairModal.vue 38 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/repair/index.vue 36 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/Form/MaintenanceModal.vue 357 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/Form/PlanModal.vue 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/Form/formDia.vue 22 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/index.vue 29 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/assets/fixedAssets.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/assets/intangibleAssets.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/generalLedger/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/voucher/detailLedger.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/voucher/generalLedger.vue 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/voucher/index.vue 107 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/BatchNoQtyDetail.vue 227 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/Record.vue 354 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/contractManagement/index.vue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/employeeRecord/index.vue 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/procurementLedger/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/processRouteItem/index.vue 79 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productStructure/Detail/index.vue 118 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionProcess/index.vue 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionTraceability/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/workOrderManagement/index.vue 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/finalInspection/components/formDia.vue 180 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/finalInspection/index.vue 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/nonconformingManagement/components/formDia.vue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/nonconformingManagement/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/processInspection/components/formDia.vue 120 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/processInspection/index.vue 25 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/rawMaterialInspection/components/formDia.vue 86 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/rawMaterialInspection/index.vue 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/components/center-bottom.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/components/center-center.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/components/center-top.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/components/left-bottom.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/components/left-top.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/index.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/center-bottom.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/center-top.vue 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/left-bottom.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/left-top.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/right-bottom.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/right-top.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/index.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/center-bottom.vue 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/center-center.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/center-top.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/left-bottom.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/left-top.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/right-bottom.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/right-top.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/index.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safeProduction/safeWorkApproval/components/infoFormDia.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/index.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesQuotation/index.vue 77 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/HYJCfavicon.ico
multiple/assets/favicon/HYLQfavicon.ico
multiple/assets/favicon/JHYfavicon.ico
multiple/assets/favicon/JXJHfavicon.ico
multiple/assets/favicon/XCDQfavicon.ico
multiple/assets/favicon/XSWHfavicon.ico
multiple/assets/favicon/YTJZfavicon.ico
multiple/assets/favicon/ZQSYfavicon.ico
multiple/assets/logo/HYJCLogo.png
multiple/assets/logo/HYLQLogo.png
multiple/assets/logo/JHYLogo.png
multiple/assets/logo/JXJHLogo.png
multiple/assets/logo/XCDQLogo.png
multiple/assets/logo/XSWHLogo.png
multiple/assets/logo/YTJZLogo.png
multiple/assets/logo/ZQSYLogo.png
multiple/config.json
@@ -44,9 +44,9 @@
  },
  "BTYX": {
    "env": {
      "VITE_APP_TITLE": "河南帮太优选进出口有限公司",
      "VITE_BASE_API": "http://127.0.0.1:9001",
      "VITE_JAVA_API": "http://127.0.0.1:9000"
      "VITE_APP_TITLE": "河南帮太优选食品有限公司",
      "VITE_BASE_API": "http://1.15.17.182:9056",
      "VITE_JAVA_API": "http://1.15.17.182:9057"
    },
    "logo": "logo/BTYXLogo.png",
    "favicon": "favicon/BTYXfavicon.ico"
@@ -59,15 +59,6 @@
    },
    "logo": "logo/ZXZNLogo.png",
    "favicon": "favicon/ZXZNfavicon.ico"
  },
  "QXY": {
    "env": {
      "VITE_APP_TITLE": "强信宇电器管理系统",
      "VITE_BASE_API": "http://36.134.154.10:9001",
      "VITE_JAVA_API": "http://36.134.154.10:9000"
  },
    "logo": "logo/QXYLogo.png",
    "favicon": "favicon/QXYfavicon.ico"
  },
  "HYZC": {
    "env": {
@@ -105,6 +96,78 @@
    "logo": "logo/DYKJLogo.png",
    "favicon": "favicon/DYKJfavicon.ico"
  },
  "ZQSY": {
    "env": {
      "VITE_APP_TITLE": "泽淇实业",
      "VITE_BASE_API": "http://36.213.128.159:9000",
      "VITE_JAVA_API": "http://36.213.128.159:9001"
    },
    "logo": "logo/ZQSYLogo.png",
    "favicon": "favicon/ZQSYfavicon.ico"
  },
  "JXJH": {
    "env": {
      "VITE_APP_TITLE": "浚县江海水泥制品有限公司",
      "VITE_BASE_API": "http://36.139.201.20:9000",
      "VITE_JAVA_API": "http://36.139.201.20:9001"
    },
    "logo": "logo/JXJHLogo.png",
    "favicon": "favicon/JXJHfavicon.ico"
  },
  "YTJZ": {
    "env": {
      "VITE_APP_TITLE": "豫泰建筑材料有限公司",
      "VITE_BASE_API": "http://36.139.201.181:9000",
      "VITE_JAVA_API": "http://36.139.201.181:9001"
    },
    "logo": "logo/YTJZLogo.png",
    "favicon": "favicon/YTJZfavicon.ico"
  },
  "HYLQ": {
    "env": {
      "VITE_APP_TITLE": "航逸路桥工程有限公司",
      "VITE_BASE_API": "http://36.139.202.111:9000",
      "VITE_JAVA_API": "http://36.139.202.111:9001"
    },
    "logo": "logo/HYLQLogo.png",
    "favicon": "favicon/HYLQfavicon.ico"
  },
  "QXY": {
    "env": {
      "VITE_APP_TITLE": "强信宇电器云主机",
      "VITE_BASE_API": "http://36.134.154.10:9000",
      "VITE_JAVA_API": "http://36.134.154.10:9001"
    },
    "logo": "logo/QXYLogo.png",
    "favicon": "favicon/QXYfavicon.ico"
  },
  "HYJC": {
    "env": {
      "VITE_APP_TITLE": "恒洋建材",
      "VITE_BASE_API": "http://36.138.94.178:9000",
      "VITE_JAVA_API": "http://36.138.94.178:9001"
    },
    "logo": "logo/HYJCLogo.png",
    "favicon": "favicon/HYJCfavicon.ico"
  },
  "JHY": {
    "env": {
      "VITE_APP_TITLE": "山西省榆社县晋和园食品有限公司",
      "VITE_BASE_API": "http://223.15.233.27:9001",
      "VITE_JAVA_API": "http://223.15.233.27:9002"
    },
    "logo": "logo/JHYLogo.png",
    "favicon": "favicon/JHYfavicon.ico"
  },
  "XCDQ": {
    "env": {
      "VITE_APP_TITLE": "旭晨电器管理系统",
      "VITE_BASE_API": "http://36.133.45.183:9001",
      "VITE_JAVA_API": "http://36.133.45.183:9002"
    },
    "logo": "logo/XCDQLogo.png",
    "favicon": "favicon/XCDQfavicon.ico"
  },
  "logo": "/src/assets/logo/logo.png",
  "favicon": "/public/favicon.ico"
}
multiple/multiple-build.js
@@ -1,98 +1,152 @@
import fs from 'fs/promises';
import fsSync from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import fs from "fs/promises";
import fsSync from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { execSync } from "child_process";
// èŽ·å– __dirname
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// è¯»å– JSON é…ç½®
const data = await fs.readFile(path.join(__dirname, 'config.json'), 'utf-8');
const data = await fs.readFile(path.join(__dirname, "config.json"), "utf-8");
const config = JSON.parse(data);
// é¡¹ç›®è·¯å¾„
const rootPath = path.resolve(__dirname, '..');
const resourcePath = path.join(rootPath, 'multiple', 'assets');
const replacePath = path.join(rootPath, 'replace');
const rootPath = path.resolve(__dirname, "..");
const resourcePath = path.join(rootPath, "multiple", "assets");
const replacePath = path.join(rootPath, "replace");
const envFilePath = path.join(rootPath, ".env.production.local");
// èŽ·å–å‘½ä»¤è¡Œå‚æ•°
const params = parseArgs(process.argv);
const company = params["company"] ?? "default";
const company = resolveCompany(params);
const companyMap = config[company];
const envFilePath = path.join(process.cwd(), '.env.production.local');
if (!companyMap) {
  const availableCompanies = Object.entries(config)
    .filter(([, value]) => value && typeof value === "object" && value.env)
    .map(([key]) => key)
    .sort();
  throw new Error(
    `未知 company: "${company}"。可选值: ${availableCompanies.join(", ")}`
  );
}
console.log(`当前 company: ${company}`);
async function copyFileWithOverwrite(src, dest) {
    await fs.mkdir(path.dirname(dest), { recursive: true });
    if (fsSync.existsSync(dest)) {
        try {
            await fs.chmod(dest, 0o666);
        } catch {
            // Ignore chmod failure and try delete directly.
        }
        await fs.rm(dest, { force: true });
  await fs.mkdir(path.dirname(dest), { recursive: true });
  if (fsSync.existsSync(dest)) {
    try {
      await fs.chmod(dest, 0o666);
    } catch {
      // Ignore chmod failure and continue.
    }
    await fs.copyFile(src, dest);
    await fs.rm(dest, { force: true });
  }
  await fs.copyFile(src, dest);
}
try {
    // 1️⃣ ç”Ÿæˆ .env
    console.log("=======生成.env=======");
    const envContent = Object.entries(companyMap.env)
        .map(([key, value]) => `${key}='${value}'`)
        .join('\n') + '\n';
    await fs.writeFile(envFilePath, envContent, 'utf-8');
  console.log("=======生成.env=======");
  const envContent =
    Object.entries(companyMap.env)
      .map(([key, value]) => `${key}='${value}'`)
      .join("\n") + "\n";
  await fs.writeFile(envFilePath, envContent, "utf-8");
    // 2️⃣ å¤‡ä»½åŽŸå§‹èµ„æºå¹¶æ›¿æ¢
    console.log("=======修改资源=======");
    for (const [key, value] of Object.entries(companyMap)) {
        if (key === 'env') continue;
  console.log("=======修改资源=======");
  for (const [key] of Object.entries(companyMap)) {
    if (key === "env") continue;
        const originFile = path.join(rootPath, config[key]);
        const backupFile = path.join(replacePath, config[key]);
        const replaceFile = path.join(resourcePath, companyMap[key]);
    const originFile = path.join(rootPath, config[key]);
    const backupFile = path.join(replacePath, config[key]);
    const replaceFile = path.join(resourcePath, companyMap[key]);
        await copyFileWithOverwrite(originFile, backupFile);
        await copyFileWithOverwrite(replaceFile, originFile);
    }
    await copyFileWithOverwrite(originFile, backupFile);
    await copyFileWithOverwrite(replaceFile, originFile);
  }
    console.log("=====开始打包======");
    execSync("vite build", { stdio: "inherit" });
    console.log("=====打包完成======");
  console.log("=====开始打包=====");
  const buildEnv = createBuildEnv(companyMap.env);
  execSync("vite build", { stdio: "inherit", cwd: rootPath, env: buildEnv });
  console.log("=====打包完成======");
} finally {
    console.log("=====恢复资源======");
  console.log("=====恢复资源======");
    // åˆ é™¤ä¸´æ—¶ .env æ–‡ä»¶
    if (fsSync.existsSync(envFilePath)) {
        await fs.unlink(envFilePath);
        console.log(`🗑️ å·²åˆ é™¤ ${envFilePath}`);
  if (fsSync.existsSync(envFilePath)) {
    await fs.unlink(envFilePath);
    console.log(`🗑️ å·²åˆ é™¤ ${envFilePath}`);
  }
  if (fsSync.existsSync(replacePath)) {
    for (const [key] of Object.entries(companyMap)) {
      if (key === "env") continue;
      const originFile = path.join(rootPath, config[key]);
      const backupFile = path.join(replacePath, config[key]);
      await copyFileWithOverwrite(backupFile, originFile);
    }
    // æ¢å¤èµ„源文件
    if (fsSync.existsSync(replacePath)) {
        for (const [key, value] of Object.entries(companyMap)) {
            if (key === 'env') continue;
            const originFile = path.join(rootPath, config[key]);
            const backupFile = path.join(replacePath, config[key]);
            await copyFileWithOverwrite(backupFile, originFile);
        }
        await fs.rm(replacePath, { recursive: true, force: true });
        console.log(`🗑️ å·²åˆ é™¤ ${replacePath}`);
    }
    await fs.rm(replacePath, { recursive: true, force: true });
    console.log(`🗑️ å·²åˆ é™¤ ${replacePath}`);
  }
}
// ç®€å•命令行参数解析
function parseArgs(argv) {
    const params = {};
    for (const arg of argv.slice(2)) {
        if (arg.startsWith('--')) {
            const [key, value] = arg.slice(2).split('=');
            params[key] = value ?? true;
        }
  const params = {};
  for (let index = 2; index < argv.length; index++) {
    const arg = argv[index];
    if (!arg.startsWith("--")) continue;
    const normalized = arg.slice(2);
    const equalIndex = normalized.indexOf("=");
    if (equalIndex >= 0) {
      const key = normalized.slice(0, equalIndex);
      const value = normalized.slice(equalIndex + 1);
      params[key] = value || true;
      continue;
    }
    return params;
    const nextArg = argv[index + 1];
    if (nextArg && !nextArg.startsWith("--")) {
      params[normalized] = nextArg;
      index += 1;
      continue;
    }
    params[normalized] = true;
  }
  return params;
}
function resolveCompany(parsedParams) {
  const fromArg = parseValue(parsedParams.company);
  if (fromArg) return fromArg;
  const fromNpmConfig = parseValue(process.env.npm_config_company);
  if (fromNpmConfig) return fromNpmConfig;
  const fromEnv = parseValue(process.env.COMPANY ?? process.env.company);
  if (fromEnv) return fromEnv;
  return "default";
}
function parseValue(value) {
  if (value == null || value === true) return undefined;
  if (typeof value !== "string") return undefined;
  const trimmed = value.trim();
  if (!trimmed) return undefined;
  return trimmed.replace(/^["']|["']$/g, "");
}
function createBuildEnv(companyEnv) {
  const env = { ...process.env };
  for (const key of Object.keys(env)) {
    if (key.startsWith("VITE_")) {
      delete env[key];
    }
  }
  return {
    ...env,
    ...companyEnv,
    VITE_APP_ENV: "production",
  };
}
src/api/equipmentManagement/repair.js
@@ -70,3 +70,16 @@
    data,
  });
};
/**
 * @desc éªŒæ”¶å®¡æ‰¹
 * @param {验收参数} data
 * @returns
 */
export const repairAcceptance = (data) => {
  return request({
    url: `/device/repair/acceptance`,
    method: "post",
    data,
  });
};
src/api/inventoryManagement/stockInventory.js
@@ -17,6 +17,14 @@
    });
};
export const getStockInventoryBatchNoQty = (params) => {
    return request({
        url: "/stockInventory/getBatchNoQty",
        method: "get",
        params,
    });
};
// åˆ›å»ºåº“存记录
export const createStockInventory = (params) => {
    return request({
src/assets/styles/sidebar.scss
@@ -225,14 +225,7 @@
            width: 0;
            overflow: hidden;
            visibility: hidden;
            display: inline-block;
          }
          & > i {
            height: 0;
            width: 0;
            overflow: hidden;
            visibility: hidden;
            display: inline-block;
            display: inline-block;
          }
        }
      }
src/components/AIChatSidebar/assistants/index.js
@@ -1,6 +1,15 @@
import { generalAssistant } from './generalAssistant'
import { purchaseAssistant } from './purchaseAssistant'
import { productionAssistant } from './productionAssistant'
import { salesAssistant } from './salesAssistant'
export { generalAssistant, purchaseAssistant }
export { generalAssistant, purchaseAssistant, productionAssistant, salesAssistant }
export const builtInAssistants = [generalAssistant, purchaseAssistant]
export const assistantRegistry = {
  general: generalAssistant,
  sales: salesAssistant,
  purchase: purchaseAssistant,
  production: productionAssistant
}
export const builtInAssistants = [generalAssistant, salesAssistant, purchaseAssistant, productionAssistant]
src/components/AIChatSidebar/assistants/productionAssistant.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,27 @@
import { Operation } from '@element-plus/icons-vue'
export const productionAssistant = {
  key: 'production',
  label: '生产助理',
  title: '生产智能助理',
  tooltip: '生产智能助手',
  icon: Operation,
  apiBase: '/manufacturing-ai',
  storageKey: 'production_ai_chat_uuid',
  placeholder: '请输入生产相关问题... (Enter å‘送, Shift+Enter æ¢è¡Œ)',
  welcomeMessage: '你好',
  description: '我可以围绕生产现场、计划、工单、设备、质量、物料、异常处理提供查询、预警、分析和办理建议。',
  allowFileUpload: false,
  emptySessionText: '暂无生产会话',
  quickPrompts: [
    '查询本月生产计划',
    '查看最近10条工单',
    '查设备A-01的维修情况',
    '查质量不合格记录',
    '查低库存物料',
    '查近7天异常处理',
    '生成制造预警看板',
    '分析本月生产完成率和异常率',
    '给出工单逾期和设备待修的办理建议'
  ]
}
src/components/AIChatSidebar/assistants/salesAssistant.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
import { TrendCharts } from '@element-plus/icons-vue'
export const salesAssistant = {
  key: 'sales',
  label: '销售助手',
  title: '销售智能助手',
  tooltip: '销售智能助手',
  icon: TrendCharts,
  apiBase: '/sales-ai',
  storageKey: 'sales_ai_chat_uuid',
  placeholder: '请输入销售相关问题... (Enter å‘送 / Shift+Enter æ¢è¡Œ)',
  welcomeMessage: '你好',
  description: '我可以协助你查询客户档案、销售报价、销售台账、销售退货、客户往来、发货台账,并重点分析客户流失风险及回款/报价策略。',
  allowFileUpload: false,
  emptySessionText: '暂无销售会话',
  quickPrompts: [
    '查询私海客户档案前10条',
    '查询公海客户档案',
    '查询本月销售报价',
    '查询本月销售台账',
    '查询近30天销售退货',
    '查询近30天客户回款往来',
    '查询本月发货台账',
    '查看销售指标统计',
    '帮我做客户流失风险分析,近30天,前20条',
    '生成回款与报价策略建议,优先高风险客户'
  ]
}
src/components/AIChatSidebar/index.vue
@@ -242,7 +242,253 @@
                  </el-table>
                </div>
                <!-- æ‰“字中动画 -->
                <div v-if="message.manufacturingData" class="manufacturing-card">
                  <div class="manufacturing-card__title">{{ getManufacturingTypeLabel(message.type) }}</div>
                  <div
                      v-if="message.manufacturingData.summaryEntries?.length || message.manufacturingData.coreMetrics?.length"
                      class="manufacturing-summary-grid"
                  >
                    <div
                        v-for="(entry, entryIndex) in message.manufacturingData.summaryEntries"
                        :key="`summary-${entry.key}-${entryIndex}`"
                        class="manufacturing-summary-item"
                    >
                      <span class="manufacturing-summary-label">{{ entry.label }}</span>
                      <strong class="manufacturing-summary-value">{{ entry.value }}</strong>
                    </div>
                    <div
                        v-for="(metric, metricIndex) in message.manufacturingData.coreMetrics"
                        :key="`core-${metric.key}-${metricIndex}`"
                        class="manufacturing-summary-item manufacturing-summary-item--core"
                    >
                      <span class="manufacturing-summary-label">{{ metric.label }}</span>
                      <strong class="manufacturing-summary-value">{{ metric.value }}</strong>
                    </div>
                  </div>
                  <div v-if="message.manufacturingData.warningItems?.length" class="manufacturing-warning-list">
                    <div
                        v-for="(warning, warningIndex) in message.manufacturingData.warningItems"
                        :key="`warning-${warning.title || warningIndex}`"
                        class="manufacturing-warning-item"
                    >
                      <div class="manufacturing-warning-item__head">
                        <el-tag size="small" :type="getManufacturingWarningLevelType(warning.level)">
                          {{ getManufacturingWarningLevelLabel(warning.level) }}
                        </el-tag>
                        <strong>{{ warning.title || `预警 ${warningIndex + 1}` }}</strong>
                        <span v-if="warning.count !== '' && warning.count !== null && warning.count !== undefined" class="manufacturing-warning-count">
                          {{ warning.count }}
                        </span>
                      </div>
                      <p v-if="warning.detail" class="manufacturing-warning-detail">{{ warning.detail }}</p>
                    </div>
                  </div>
                  <div
                      v-if="message.manufacturingData.listItems?.length && message.manufacturingData.columns?.length"
                      class="table-wrapper manufacturing-table-wrapper"
                  >
                    <el-table :data="message.manufacturingData.listItems" border stripe size="small" style="width: 100%">
                      <el-table-column
                          v-for="col in message.manufacturingData.columns"
                          :key="col"
                          :label="getStructuredFieldLabel(col)"
                          min-width="140"
                          show-overflow-tooltip
                      >
                        <template #default="{ row }">
                          {{ formatStructuredValue(row[col]) }}
                        </template>
                      </el-table-column>
                    </el-table>
                  </div>
                  <div v-if="message.manufacturingData.actionCards?.length" class="manufacturing-action-list">
                    <div
                        v-for="(card, cardIndex) in message.manufacturingData.actionCards"
                        :key="card.runtimeKey || `${card.code}-${card.targetApi}-${cardIndex}`"
                        class="manufacturing-action-card"
                    >
                      <div class="manufacturing-action-card__head">
                        <strong>{{ card.name || `动作 ${cardIndex + 1}` }}</strong>
                        <el-tag size="small" type="info">{{ getNormalizedRequestMethod(card.method) }}</el-tag>
                      </div>
                      <div class="manufacturing-action-card__meta">
                        <span>{{ card.code || '--' }}</span>
                        <span>{{ card.targetApi || '--' }}</span>
                      </div>
                      <p v-if="card.description" class="manufacturing-action-card__desc">{{ card.description }}</p>
                      <div v-if="card.requiredFields?.length" class="manufacturing-required-fields">
                        <span>必填字段</span>
                        <el-tag
                            v-for="field in card.requiredFields"
                            :key="field"
                            size="small"
                            type="warning"
                        >
                          {{ getStructuredPathLabel(field) }}
                        </el-tag>
                      </div>
                      <el-input
                          v-model="card.payloadText"
                          type="textarea"
                          :rows="6"
                          resize="vertical"
                          :disabled="card.executing"
                          placeholder="请输入 JSON è¯·æ±‚参数"
                      />
                      <div class="manufacturing-action-footer">
                        <span
                            v-if="card.executeResult"
                            :class="['manufacturing-action-result', card.executeError ? 'error' : 'success']"
                        >
                          {{ card.executeResult }}
                        </span>
                        <el-button
                            type="primary"
                            size="small"
                            :loading="card.executing"
                            @click="executeManufacturingAction(message, card, cardIndex)"
                        >
                          ç¡®è®¤å¹¶æ‰§è¡Œ
                        </el-button>
                      </div>
                    </div>
                  </div>
                </div>
                <div v-if="message.salesData" class="sales-structured-card">
                  <div class="sales-structured-card__title">{{ getSalesTypeLabel(message.type) }}</div>
                  <div v-if="message.salesData.summaryEntries?.length" class="sales-summary-grid">
                    <div
                        v-for="(entry, entryIndex) in message.salesData.summaryEntries"
                        :key="`sales-summary-${entry.key}-${entryIndex}`"
                        class="sales-summary-item"
                    >
                      <span class="sales-summary-label">{{ entry.label }}</span>
                      <strong class="sales-summary-value">{{ entry.value }}</strong>
                    </div>
                  </div>
                  <div v-if="message.type === 'sales_customer_churn_risk' && message.salesData.listItems?.length" class="sales-focus-list">
                    <div
                        v-for="(item, itemIndex) in message.salesData.listItems"
                        :key="`risk-${item.customerName || itemIndex}`"
                        class="sales-focus-item"
                    >
                      <div class="sales-focus-item__head">
                        <strong>{{ formatStructuredValue(item.customerName) }}</strong>
                        <div class="sales-focus-tags">
                          <el-tag size="small" :type="getSalesLevelTagType(item.riskLevel)">
                            {{ getSalesLevelLabel(item.riskLevel, 'risk') }}
                          </el-tag>
                          <el-tag size="small" type="warning">风险分 {{ formatStructuredValue(item.riskScore) }}</el-tag>
                        </div>
                      </div>
                      <div class="sales-focus-metrics">
                        <span>待回款:{{ formatStructuredValue(item.pendingAmount) }}</span>
                        <span>待回款占比:{{ formatStructuredValue(item.pendingRate) }}</span>
                        <span>距上次下单:{{ formatStructuredValue(item.daysSinceLastOrder) }}</span>
                      </div>
                      <div v-if="toStructuredStringArray(item.riskReasons).length" class="sales-focus-reasons">
                        <el-tag
                            v-for="(reason, reasonIndex) in toStructuredStringArray(item.riskReasons)"
                            :key="`${item.customerName || itemIndex}-reason-${reasonIndex}`"
                            size="small"
                            type="danger"
                            effect="plain"
                        >
                          {{ reason }}
                        </el-tag>
                      </div>
                    </div>
                  </div>
                  <div v-if="message.type === 'sales_collection_quote_strategy' && message.salesData.listItems?.length" class="sales-focus-list">
                    <div
                        v-for="(item, itemIndex) in message.salesData.listItems"
                        :key="`strategy-${item.customerName || itemIndex}`"
                        class="sales-focus-item sales-focus-item--strategy"
                    >
                      <div class="sales-focus-item__head">
                        <strong>{{ formatStructuredValue(item.customerName) }}</strong>
                        <div class="sales-focus-tags">
                          <el-tag size="small" :type="getSalesLevelTagType(item.priority)">
                            {{ getSalesLevelLabel(item.priority, 'priority') }}
                          </el-tag>
                          <el-tag size="small" type="success">转化率 {{ formatStructuredValue(item.quoteConversionRate) }}</el-tag>
                        </div>
                      </div>
                      <div class="sales-focus-metrics">
                        <span>待回款:{{ formatStructuredValue(item.pendingAmount) }}</span>
                        <span v-if="item.nextAction">下一步:{{ formatStructuredValue(item.nextAction) }}</span>
                      </div>
                      <p v-if="item.collectionStrategy" class="sales-strategy-line">
                        <strong>回款策略:</strong>{{ formatStructuredValue(item.collectionStrategy) }}
                      </p>
                      <p v-if="item.quotationStrategy" class="sales-strategy-line">
                        <strong>报价策略:</strong>{{ formatStructuredValue(item.quotationStrategy) }}
                      </p>
                    </div>
                  </div>
                  <div
                      v-if="message.salesData.listItems?.length && message.salesData.columns?.length && !isSalesFocusType(message.type)"
                      class="table-wrapper manufacturing-table-wrapper"
                  >
                    <el-table :data="message.salesData.listItems" border stripe size="small" style="width: 100%">
                      <el-table-column
                          v-for="col in message.salesData.columns"
                          :key="col"
                          :label="getStructuredFieldLabel(col)"
                          min-width="140"
                          show-overflow-tooltip
                      >
                        <template #default="{ row }">
                          {{ formatStructuredValue(row[col]) }}
                        </template>
                      </el-table-column>
                    </el-table>
                  </div>
                  <div v-if="message.salesData.topCustomers?.length && message.salesData.topCustomerColumns?.length" class="table-wrapper manufacturing-table-wrapper">
                    <div class="sales-section-title">重点客户</div>
                    <el-table :data="message.salesData.topCustomers" border stripe size="small" style="width: 100%">
                      <el-table-column
                          v-for="col in message.salesData.topCustomerColumns"
                          :key="`top-customer-${col}`"
                          :label="getStructuredFieldLabel(col)"
                          min-width="120"
                          show-overflow-tooltip
                      >
                        <template #default="{ row }">
                          {{ formatStructuredValue(row[col]) }}
                        </template>
                      </el-table-column>
                    </el-table>
                  </div>
                  <div v-if="message.salesData.contractTrend?.length && message.salesData.contractTrendColumns?.length" class="table-wrapper manufacturing-table-wrapper">
                    <div class="sales-section-title">合同趋势</div>
                    <el-table :data="message.salesData.contractTrend" border stripe size="small" style="width: 100%">
                      <el-table-column
                          v-for="col in message.salesData.contractTrendColumns"
                          :key="`contract-trend-${col}`"
                          :label="getStructuredFieldLabel(col)"
                          min-width="120"
                          show-overflow-tooltip
                      >
                        <template #default="{ row }">
                          {{ formatStructuredValue(row[col]) }}
                        </template>
                      </el-table-column>
                    </el-table>
                  </div>
                </div>
                <div v-if="message.purchaseAnalysisData" class="purchase-confirm-card">
                  <div class="purchase-confirm-header">
                    <span>{{ businessTypeLabelMap[message.purchaseAnalysisData.businessType] || message.purchaseAnalysisData.businessType || '采购业务' }}</span>
@@ -523,7 +769,7 @@
import request from '@/utils/request'
import * as echarts from 'echarts'
import { Cpu, User, Plus, Timer, Delete, ChatDotSquare, VideoPause, Upload, Document, Close, Promotion, RefreshRight } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import { builtInAssistants, generalAssistant } from './assistants'
import todoAssistantAvatar from '@/assets/AI/待办助手.png'
import salesAssistantAvatar from '@/assets/AI/销售助手.png'
@@ -634,6 +880,119 @@
  purchase_return_order: '采购退货单',
  unknown: '未知采购业务'
}
const salesStructuredTypeSet = new Set([
  'sales_customer_profile_list',
  'sales_quotation_list',
  'sales_ledger_list',
  'sales_return_list',
  'sales_customer_interaction_list',
  'sales_shipping_list',
  'sales_dashboard',
  'sales_customer_churn_risk',
  'sales_collection_quote_strategy'
])
const salesFocusTypeSet = new Set([
  'sales_customer_churn_risk',
  'sales_collection_quote_strategy'
])
const salesTypeLabelMap = {
  sales_customer_profile_list: '客户档案',
  sales_quotation_list: '销售报价',
  sales_ledger_list: '销售台账',
  sales_return_list: '销售退货',
  sales_customer_interaction_list: '客户往来',
  sales_shipping_list: '发货台账',
  sales_dashboard: '销售指标统计',
  sales_customer_churn_risk: '客户流失风险分析',
  sales_collection_quote_strategy: '回款与报价策略建议'
}
const manufacturingStructuredTypeSet = new Set([
  'manufacturing_site_snapshot',
  'manufacturing_plan_list',
  'manufacturing_workorder_list',
  'manufacturing_device_list',
  'manufacturing_device_repair_list',
  'manufacturing_quality_list',
  'manufacturing_material_list',
  'manufacturing_exception_list',
  'manufacturing_warning',
  'manufacturing_analysis',
  'manufacturing_action_plan'
])
const manufacturingListTypeSet = new Set([
  'manufacturing_plan_list',
  'manufacturing_workorder_list',
  'manufacturing_device_list',
  'manufacturing_device_repair_list',
  'manufacturing_quality_list',
  'manufacturing_material_list',
  'manufacturing_exception_list'
])
const manufacturingTypeLabelMap = {
  manufacturing_site_snapshot: '生产现场概览',
  manufacturing_plan_list: '计划查询',
  manufacturing_workorder_list: '工单查询',
  manufacturing_device_list: '设备查询',
  manufacturing_device_repair_list: '设备维修记录查询',
  manufacturing_quality_list: '质量查询',
  manufacturing_material_list: '物料查询',
  manufacturing_exception_list: '异常查询',
  manufacturing_warning: '预警看板',
  manufacturing_analysis: '经营分析',
  manufacturing_action_plan: '办理建议'
}
const structuredFieldLabelMap = {
  workOrderNo: '工单号',
  planEndTime: '计划结束时间',
  planStartTime: '计划开始时间',
  timeRange: '时间范围',
  startDate: '开始日期',
  endDate: '结束日期',
  warningCount: '预警数量',
  overduePlanCount: '逾期计划数',
  overdueWorkOrderCount: '逾期工单数',
  actionCount: '建议动作数',
  qualityOpenCount: '质量待处理数',
  lowStockCount: '低库存数',
  exceptionCount: '异常数',
  userId: '用户ID',
  tenantId: '租户ID',
  status: '状态',
  deviceName: '设备名称',
  deviceModel: '设备型号',
  pendingRepairCount: '待维修数',
  repairTime: '维修时间',
  repairName: '报修人',
  maintenanceName: '维修人员',
  level: '预警等级',
  title: '标题',
  count: '数量',
  detail: '详情',
  remark: '备注',
  createTime: '创建时间',
  updateTime: '更新时间',
  exceptionType: '异常类型',
  materialName: '物料名称',
  stockQty: '库存量'
}
Object.assign(structuredFieldLabelMap, {
  customerName: '客户名称',
  riskLevel: '风险等级',
  riskScore: '风险评分',
  riskReasons: '风险原因',
  pendingAmount: '待回款金额',
  pendingRate: '待回款占比',
  daysSinceLastOrder: '距上次下单天数',
  priority: '优先级',
  quoteConversionRate: '报价转化率',
  collectionStrategy: '回款策略',
  quotationStrategy: '报价策略',
  nextAction: '下一步动作',
  contractAmountTotal: '合同总额',
  receivedAmountTotal: '已回款金额',
  pendingAmountTotal: '待回款总额',
  shipRate: '发货率'
})
const purchasePayloadFieldLabelMap = {
  purchaseLedgers: '采购台账',
  productData: '产品明细',
@@ -785,6 +1144,437 @@
  inventoryWarningQuantity: 'inventoryWarningQuantity',
  isInspected: 'isInspected',
  isChecked: 'isInspected'
}
const isPlainObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value)
const stringifyStructuredPayload = (value, spaces = 2) => {
  if (typeof value === 'string') return value
  try {
    return JSON.stringify(value ?? {}, null, spaces)
  } catch (err) {
    return '{}'
  }
}
const structuredFieldTokenLabelMap = {
  time: '时间',
  range: '范围',
  start: '开始',
  end: '结束',
  date: '日期',
  warning: '预警',
  overdue: '逾期',
  plan: '计划',
  work: 'å·¥',
  order: '单',
  workorder: '工单',
  count: '数量',
  quality: '质量',
  low: '低',
  stock: '库存',
  exception: '异常',
  action: '动作',
  user: '用户',
  tenant: '租户',
  id: 'ID',
  no: '编号',
  number: '编号',
  code: '编码',
  name: '名称',
  status: '状态',
  level: '等级',
  title: '标题',
  detail: '详情',
  total: '总数',
  rate: '比率',
  type: '类型',
  pending: '待',
  repair: 'ç»´ä¿®',
  device: '设备',
  material: '物料'
}
const convertStructuredFieldKeyToChinese = (fieldKey = '') => {
  const key = String(fieldKey || '').trim()
  if (!key) return '-'
  if (structuredFieldLabelMap[key]) return structuredFieldLabelMap[key]
  if (/[\u4e00-\u9fa5]/.test(key)) return key
  const rawTokens = key
    .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
    .replace(/[_-]+/g, ' ')
    .trim()
    .split(/\s+/)
    .filter(Boolean)
    .map(token => token.toLowerCase())
  if (!rawTokens.length) return '字段'
  const mappedTokens = rawTokens
    .map(token => structuredFieldTokenLabelMap[token] || '')
    .filter(Boolean)
  if (mappedTokens.length) {
    return mappedTokens.join('')
  }
  return '字段'
}
const getStructuredFieldLabel = (fieldKey = '') => {
  return convertStructuredFieldKeyToChinese(fieldKey)
}
const getStructuredPathLabel = (fieldPath = '') => {
  const path = String(fieldPath || '').trim()
  if (!path) return '-'
  const segments = path
    .replace(/\[(\d+)]/g, '.$1')
    .split('.')
    .filter(Boolean)
  if (!segments.length) return getStructuredFieldLabel(path)
  return segments.map((segment) => {
    if (/^\d+$/.test(segment)) {
      return `第${Number(segment) + 1}项`
    }
    return getStructuredFieldLabel(segment)
  }).join(' / ')
}
const formatStructuredValue = (value) => {
  if (value === null || value === undefined || value === '') return '-'
  if (Array.isArray(value)) {
    const preview = value.slice(0, 3).map(item => formatStructuredValue(item)).join('、')
    return value.length > 3 ? `${preview} ç­‰${value.length}项` : preview
  }
  if (isPlainObject(value)) return stringifyStructuredPayload(value, 0)
  return String(value)
}
const normalizeManufacturingSummaryEntries = (summary) => {
  if (!isPlainObject(summary)) return []
  return Object.entries(summary)
    .filter(([, value]) => value !== undefined && value !== null && `${value}`.trim() !== '')
    .map(([key, value]) => ({
      key,
      label: getStructuredFieldLabel(key),
      value: formatStructuredValue(value)
    }))
}
const normalizeManufacturingCoreMetrics = (coreMetrics) => {
  if (Array.isArray(coreMetrics)) {
    return coreMetrics.map((item, index) => {
      if (isPlainObject(item)) {
        const label = item.label || item.name || item.key || `指标${index + 1}`
        const metricValue = item.value ?? item.metricValue ?? item.data ?? '-'
        const unit = item.unit ? ` ${item.unit}` : ''
        return {
          key: String(item.key || item.name || index),
          label,
          value: `${formatStructuredValue(metricValue)}${unit}`.trim()
        }
      }
      return {
        key: String(index),
        label: `指标${index + 1}`,
        value: formatStructuredValue(item)
      }
    })
  }
  if (isPlainObject(coreMetrics)) {
    return Object.entries(coreMetrics).map(([key, value]) => ({
      key,
      label: getStructuredFieldLabel(key),
      value: formatStructuredValue(value)
    }))
  }
  return []
}
const normalizeManufacturingWarningItems = (items = []) => {
  if (!Array.isArray(items)) return []
  return items
    .filter(item => isPlainObject(item))
    .map(item => ({
      level: String(item.level || '').toLowerCase(),
      title: item.title || '',
      count: item.count ?? '',
      detail: item.detail ?? ''
    }))
}
const inferManufacturingColumns = (items = []) => {
  if (!Array.isArray(items) || !items.length) return []
  const fieldSet = new Set()
  items.forEach((item) => {
    if (!isPlainObject(item)) return
    Object.keys(item).forEach((key) => fieldSet.add(key))
  })
  return Array.from(fieldSet)
}
const getManufacturingActionCardRuntimeKey = (card = {}, index = 0) => {
  const code = String(card?.code || '').trim()
  const api = String(card?.targetApi || '').trim()
  const name = String(card?.name || '').trim()
  return `${code}::${api}::${name}::${index}`
}
const normalizeManufacturingActionCards = (actionCards = [], previousCards = []) => {
  const previousMap = new Map()
  if (Array.isArray(previousCards)) {
    previousCards.forEach((card, index) => {
      previousMap.set(getManufacturingActionCardRuntimeKey(card, index), card)
    })
  }
  return (Array.isArray(actionCards) ? actionCards : [])
    .filter(card => isPlainObject(card))
    .map((card, index) => {
      const runtimeKey = getManufacturingActionCardRuntimeKey(card, index)
      const previousCard = previousMap.get(runtimeKey)
      const fallbackPayloadText = stringifyStructuredPayload(card.examplePayload, 2)
      return {
        ...card,
        runtimeKey,
        payloadText: previousCard?.payloadText ?? fallbackPayloadText,
        executing: Boolean(previousCard?.executing),
        executed: Boolean(previousCard?.executed),
        executeResult: previousCard?.executeResult || '',
        executeError: Boolean(previousCard?.executeError)
      }
    })
}
const buildManufacturingStructuredData = (parsedData, previousData = null) => {
  const type = String(parsedData?.type || '')
  if (!manufacturingStructuredTypeSet.has(type)) return null
  const rawData = isPlainObject(parsedData?.data) ? parsedData.data : {}
  const items = Array.isArray(rawData.items) ? rawData.items.filter(item => isPlainObject(item)) : []
  const warningItems = type === 'manufacturing_warning' ? normalizeManufacturingWarningItems(items) : []
  const listItems = manufacturingListTypeSet.has(type) ? items : []
  const actionCards = type === 'manufacturing_action_plan'
    ? normalizeManufacturingActionCards(rawData.actionCards, previousData?.actionCards)
    : []
  return {
    type,
    summaryEntries: normalizeManufacturingSummaryEntries(parsedData?.summary),
    coreMetrics: normalizeManufacturingCoreMetrics(rawData.coreMetrics),
    listItems,
    columns: inferManufacturingColumns(listItems),
    warningItems,
    actionCards
  }
}
const getManufacturingTypeLabel = (type = '') => manufacturingTypeLabelMap[String(type || '')] || '制造结果'
const inferSalesColumns = (items = []) => {
  if (!Array.isArray(items) || !items.length) return []
  const fieldSet = new Set()
  items.forEach((item) => {
    if (!isPlainObject(item)) return
    Object.keys(item).forEach((key) => fieldSet.add(key))
  })
  return Array.from(fieldSet)
}
const normalizeSalesListItems = (items) => {
  if (!Array.isArray(items)) return []
  return items.filter(item => isPlainObject(item))
}
const buildSalesStructuredData = (parsedData) => {
  const type = String(parsedData?.type || '')
  if (!salesStructuredTypeSet.has(type)) return null
  const rawData = isPlainObject(parsedData?.data) ? parsedData.data : {}
  const listItems = normalizeSalesListItems(rawData.items)
  const topCustomers = normalizeSalesListItems(rawData.topCustomers)
  const contractTrend = normalizeSalesListItems(rawData.contractTrend)
  return {
    type,
    summaryEntries: normalizeManufacturingSummaryEntries(parsedData?.summary),
    listItems,
    columns: inferSalesColumns(listItems),
    topCustomers,
    topCustomerColumns: inferSalesColumns(topCustomers),
    contractTrend,
    contractTrendColumns: inferSalesColumns(contractTrend)
  }
}
const getSalesTypeLabel = (type = '') => salesTypeLabelMap[String(type || '')] || '销售查询结果'
const isSalesFocusType = (type = '') => salesFocusTypeSet.has(String(type || ''))
const getSalesLevelTagType = (level = '') => {
  const normalizedLevel = String(level || '').toLowerCase()
  if (normalizedLevel === 'high') return 'danger'
  if (normalizedLevel === 'medium') return 'warning'
  if (normalizedLevel === 'low') return 'success'
  return 'info'
}
const getSalesLevelLabel = (level = '', mode = 'risk') => {
  const normalizedLevel = String(level || '').toLowerCase()
  const suffix = mode === 'priority' ? '优先级' : '风险'
  if (normalizedLevel === 'high') return `高${suffix}`
  if (normalizedLevel === 'medium') return `中${suffix}`
  if (normalizedLevel === 'low') return `低${suffix}`
  if (!normalizedLevel) return mode === 'priority' ? '未分级' : '未评估'
  return normalizedLevel.toUpperCase()
}
const toStructuredStringArray = (value) => {
  if (Array.isArray(value)) {
    return value.map(item => String(item || '').trim()).filter(Boolean)
  }
  if (typeof value === 'string') {
    return value
      .split(/[,\uFF0C\u3001;\uFF1B\n]/)
      .map(item => item.trim())
      .filter(Boolean)
  }
  return []
}
const getManufacturingWarningLevelType = (level = '') => {
  const normalizedLevel = String(level || '').toLowerCase()
  if (normalizedLevel === 'high') return 'danger'
  if (normalizedLevel === 'medium') return 'warning'
  return 'info'
}
const getManufacturingWarningLevelLabel = (level = '') => {
  const normalizedLevel = String(level || '').toLowerCase()
  if (normalizedLevel === 'high') return '高'
  if (normalizedLevel === 'medium') return '中'
  return normalizedLevel ? normalizedLevel.toUpperCase() : '一般'
}
const normalizeRequestMethod = (method = 'POST') => {
  const normalized = String(method || 'POST').trim().toUpperCase()
  if (['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].includes(normalized)) return normalized
  return 'POST'
}
const getNormalizedRequestMethod = (method) => normalizeRequestMethod(method)
const getPayloadValueByPath = (payload, fieldPath = '') => {
  const normalizedPath = String(fieldPath || '').trim()
  if (!normalizedPath || !isPlainObject(payload)) return undefined
  const pathSegments = normalizedPath
    .replace(/\[(\d+)]/g, '.$1')
    .split('.')
    .filter(Boolean)
  return pathSegments.reduce((current, segment) => {
    if (current === null || current === undefined) return undefined
    if (!['object', 'function'].includes(typeof current)) return undefined
    return current[segment]
  }, payload)
}
const getMissingRequiredFields = (requiredFields = [], payload = {}) => {
  if (!Array.isArray(requiredFields) || !requiredFields.length) return []
  return requiredFields.filter((fieldPath) => {
    const value = getPayloadValueByPath(payload, fieldPath)
    return !hasMeaningfulPayloadValue(value)
  })
}
const parseManufacturingActionPayload = (payloadText = '') => {
  const text = String(payloadText ?? '').trim()
  if (!text) return {}
  return JSON.parse(text)
}
const executeManufacturingAction = async (message, actionCard, cardIndex = 0) => {
  if (!message?.manufacturingData || !actionCard || actionCard.executing) return
  const actionName = actionCard.name || `动作 ${cardIndex + 1}`
  const targetApi = String(actionCard.targetApi || '').trim()
  if (!targetApi) {
    actionCard.executeError = true
    actionCard.executeResult = '缺少 targetApi,无法执行动作'
    return
  }
  let payload = {}
  try {
    payload = parseManufacturingActionPayload(actionCard.payloadText)
  } catch (err) {
    actionCard.executeError = true
    actionCard.executeResult = '请求参数不是合法 JSON,请检查后重试'
    return
  }
  const requiredFields = Array.isArray(actionCard.requiredFields) ? actionCard.requiredFields : []
  if (requiredFields.length && !isPlainObject(payload)) {
    actionCard.executeError = true
    actionCard.executeResult = '必填字段校验失败:请求参数必须是 JSON å¯¹è±¡'
    return
  }
  const missingFields = getMissingRequiredFields(requiredFields, payload)
  if (missingFields.length) {
    actionCard.executeError = true
    const missingFieldLabels = missingFields.map(field => getStructuredPathLabel(field))
    actionCard.executeResult = `缺少必填字段:${missingFieldLabels.join('、')}`
    return
  }
  try {
    await ElMessageBox.confirm(`确认执行「${actionName}」吗?`, '执行确认', {
      confirmButtonText: '确认执行',
      cancelButtonText: '取消',
      type: 'warning'
    })
  } catch (err) {
    return
  }
  actionCard.executing = true
  actionCard.executeError = false
  actionCard.executeResult = ''
  const method = normalizeRequestMethod(actionCard.method)
  const requestConfig = {
    url: targetApi,
    method: method.toLowerCase()
  }
  if (method === 'GET') {
    requestConfig.params = payload
  } else {
    requestConfig.data = payload
  }
  try {
    const res = await request(requestConfig)
    const successMsg = res?.msg || `${actionName}执行成功`
    actionCard.executed = true
    actionCard.executeError = false
    actionCard.executeResult = successMsg
    ElMessage.success(successMsg)
  } catch (err) {
    actionCard.executed = false
    actionCard.executeError = true
    actionCard.executeResult = err?.message || `${actionName}执行失败,请稍后重试`
  } finally {
    actionCard.executing = false
  }
}
// åŽ†å²ä¼šè¯ç›¸å…³
@@ -1034,6 +1824,9 @@
          tableData: null,
          payloadTreeData: null,
          payloadHiddenData: null,
          purchaseAnalysisData: null,
          manufacturingData: null,
          salesData: null,
          localUploadFiles: isUser ? mapHistoryFilePathsToSnapshots(msg.filePaths, uuid.value, idx) : []
        }
@@ -1273,15 +2066,32 @@
const applyStructuredMessageData = (messageObj, parsedData, msgIndex, shouldRenderCharts = true) => {
  if (!messageObj || !parsedData?.success) return
  const previousManufacturingData = messageObj.manufacturingData
  messageObj.type = parsedData.type || ''
  messageObj.tableData = null
  messageObj.purchaseAnalysisData = null
  messageObj.manufacturingData = null
  messageObj.salesData = null
  if (messageObj.type === 'todo_list' && parsedData.data) {
    messageObj.tableData = parsedData.data
  }
  const salesData = buildSalesStructuredData(parsedData)
  if (salesData) {
    messageObj.salesData = salesData
  }
  const manufacturingData = buildManufacturingStructuredData(parsedData, previousManufacturingData)
  if (manufacturingData) {
    messageObj.manufacturingData = manufacturingData
  }
  if (parsedData.action === 'confirm_required' && parsedData.businessType) {
    messageObj.type = 'purchase_analysis_confirm'
    messageObj.purchaseAnalysisData = parsedData
    messageObj.manufacturingData = null
    messageObj.salesData = null
    if (!Array.isArray(messageObj.payloadTreeData) || !messageObj.payloadTreeData.length) {
      initializePurchasePayloadTree(messageObj, parsedData.payload || {})
    }
@@ -1321,6 +2131,25 @@
  }
  return null
}
const getStructuredFallbackText = (parsedData) => {
  if (!parsedData) return '正在为您展示分析结果...'
  if (parsedData.type === 'todo_list') return '已为您整理好相关数据。'
  if (salesStructuredTypeSet.has(parsedData.type)) {
    if (parsedData.type === 'sales_customer_churn_risk') return '已为您生成客户流失风险分析。'
    if (parsedData.type === 'sales_collection_quote_strategy') return '已为您生成回款与报价策略建议。'
    if (parsedData.type === 'sales_dashboard') return '已为您生成销售指标统计。'
    return '已返回销售查询结果。'
  }
  if (manufacturingStructuredTypeSet.has(parsedData.type)) {
    if (parsedData.type === 'manufacturing_action_plan') return '已为您生成办理建议,请确认动作后执行。'
    if (parsedData.type === 'manufacturing_warning') return '已为您生成制造预警看板。'
    if (parsedData.type === 'manufacturing_analysis') return '已为您生成制造分析结果。'
    return '已返回制造查询结果。'
  }
  if (parsedData.charts && Object.keys(parsedData.charts).length > 0) return '已为您生成分析图表。'
  return '正在为您展示分析结果...'
}
const buildPurchaseMaterialRankCharts = (parsedData) => {
@@ -2446,7 +3275,10 @@
    type: '',
    tableData: null,
    payloadTreeData: null,
    payloadHiddenData: null
    payloadHiddenData: null,
    purchaseAnalysisData: null,
    manufacturingData: null,
    salesData: null
  })
  outputState.value[botMsgIndex] = {
@@ -2569,7 +3401,10 @@
    type: '',
    tableData: null,
    payloadTreeData: null,
    payloadHiddenData: null
    payloadHiddenData: null,
    purchaseAnalysisData: null,
    manufacturingData: null,
    salesData: null
  }
  messages.value.push(botMsg)
@@ -2735,13 +3570,7 @@
    }
    if (!display) {
      if (parsed.type === 'todo_list') {
        display = '已为您整理好相关数据。'
      } else if (parsed.charts && Object.keys(parsed.charts).length > 0) {
        display = '已为您生成分析图表。'
      } else {
        display = '正在为您展示分析结果...'
      }
      display = getStructuredFallbackText(parsed)
    }
  } else if (startIdx !== -1) {
    const lastBraceIdx = output.lastIndexOf('}')
@@ -2763,13 +3592,7 @@
        }
        if (!display) {
          if (parsed.type === 'todo_list') {
            display = '已为您整理好相关数据:'
          } else if (parsed.charts && Object.keys(parsed.charts).length > 0) {
            display = '已为您生成分析图表:'
          } else {
            display = '正在为您展示分析结果...'
          }
          display = getStructuredFallbackText(parsed)
        }
      } catch (e) {
        // è§£æžå¤±è´¥ï¼Œè¯´æ˜Ž JSON è¿˜åœ¨ä¼ è¾“中或格式不正确
@@ -3792,6 +4615,284 @@
  }
}
.manufacturing-card {
  margin-top: 12px;
  width: 100%;
  background: #fff;
  border: 1px solid rgba(0, 85, 212, 0.12);
  border-radius: 12px;
  box-shadow: $shadow-card;
  padding: 14px;
}
.manufacturing-card__title {
  font-size: 14px;
  font-weight: 700;
  color: $deep-blue;
  margin-bottom: 10px;
}
.manufacturing-summary-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 8px;
  margin-bottom: 12px;
}
.manufacturing-summary-item {
  border-radius: 10px;
  padding: 10px 12px;
  border: 1px solid rgba(0, 85, 212, 0.08);
  background: linear-gradient(180deg, #f8fbff, #f1f7ff);
  min-height: 66px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 6px;
}
.manufacturing-summary-item--core {
  border-color: rgba(30, 91, 255, 0.24);
}
.manufacturing-summary-label {
  font-size: 12px;
  color: #4b5563;
}
.manufacturing-summary-value {
  font-size: 15px;
  color: #1f2937;
  line-height: 1.4;
  word-break: break-all;
}
.manufacturing-warning-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-bottom: 12px;
}
.manufacturing-warning-item {
  border-radius: 10px;
  border: 1px solid rgba(245, 158, 11, 0.22);
  background: linear-gradient(135deg, rgba(255, 247, 237, 0.9), rgba(255, 255, 255, 0.98));
  padding: 10px 12px;
}
.manufacturing-warning-item__head {
  display: flex;
  align-items: center;
  gap: 8px;
  color: #92400e;
  font-size: 13px;
}
.manufacturing-warning-count {
  margin-left: auto;
  font-weight: 700;
  color: #c2410c;
}
.manufacturing-warning-detail {
  margin: 8px 0 0;
  font-size: 12px;
  line-height: 1.6;
  color: #7c2d12;
  word-break: break-all;
}
.manufacturing-table-wrapper {
  margin-top: 10px;
}
.manufacturing-action-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-top: 12px;
}
.manufacturing-action-card {
  border: 1px solid rgba(0, 85, 212, 0.1);
  border-radius: 10px;
  padding: 10px 12px;
  background: #f8fbff;
}
.manufacturing-action-card__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  font-size: 13px;
  color: #1f2937;
  margin-bottom: 8px;
}
.manufacturing-action-card__meta {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-bottom: 8px;
  font-size: 12px;
  color: #64748b;
  word-break: break-all;
}
.manufacturing-action-card__desc {
  margin: 0 0 8px;
  font-size: 12px;
  line-height: 1.6;
  color: #475467;
}
.manufacturing-required-fields {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin-bottom: 8px;
  font-size: 12px;
  color: #7c2d12;
}
.manufacturing-action-footer {
  margin-top: 8px;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 12px;
}
.manufacturing-action-result {
  flex: 1;
  font-size: 12px;
  line-height: 1.5;
  &.success {
    color: #1f9d55;
  }
  &.error {
    color: #d93025;
  }
}
.sales-structured-card {
  margin-top: 12px;
  width: 100%;
  background: #fff;
  border: 1px solid rgba(31, 122, 114, 0.2);
  border-radius: 12px;
  box-shadow: $shadow-card;
  padding: 14px;
}
.sales-structured-card__title {
  font-size: 14px;
  font-weight: 700;
  color: #1f5ddf;
  margin-bottom: 10px;
}
.sales-summary-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 8px;
  margin-bottom: 12px;
}
.sales-summary-item {
  border-radius: 10px;
  padding: 10px 12px;
  border: 1px solid rgba(30, 91, 255, 0.12);
  background: linear-gradient(180deg, #f7fbff, #edf6ff);
  min-height: 66px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 6px;
}
.sales-summary-label {
  font-size: 12px;
  color: #4b5563;
}
.sales-summary-value {
  font-size: 15px;
  color: #1f2937;
  line-height: 1.4;
  word-break: break-all;
}
.sales-focus-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.sales-focus-item {
  border-radius: 10px;
  border: 1px solid rgba(30, 91, 255, 0.14);
  background: #f8fbff;
  padding: 10px 12px;
}
.sales-focus-item--strategy {
  border-color: rgba(31, 122, 114, 0.22);
  background: linear-gradient(180deg, #f7fcfb, #edf9f6);
}
.sales-focus-item__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 10px;
  font-size: 13px;
  color: #1f2937;
}
.sales-focus-tags {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  justify-content: flex-end;
}
.sales-focus-metrics {
  margin-top: 8px;
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  font-size: 12px;
  color: #475467;
}
.sales-focus-reasons {
  margin-top: 8px;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.sales-strategy-line {
  margin: 8px 0 0;
  font-size: 12px;
  line-height: 1.6;
  color: #334155;
}
.sales-section-title {
  margin: 4px 0 8px;
  font-size: 13px;
  font-weight: 700;
  color: $deep-blue;
}
.purchase-confirm-card {
  margin-top: 12px;
  width: 100%;
src/components/ProcessParamListDialog.vue
@@ -130,6 +130,7 @@
            </el-form-item>
            <el-form-item label="标准值">
              <el-input v-model="selectedParam.standardValue"
                        @input="val => onStandardValueInput(val, selectedParam)"
                        placeholder="请输入默认值" />
            </el-form-item>
            <el-form-item label="是否必填">
@@ -144,7 +145,8 @@
        </div>
      </div>
      <template #footer>
        <el-button type="primary" @click="handleParamSelectSubmit">确定</el-button>
        <el-button type="primary"
                   @click="handleParamSelectSubmit">确定</el-button>
        <el-button @click="selectParamDialogVisible = false">取消</el-button>
      </template>
    </el-dialog>
@@ -174,11 +176,13 @@
        <el-form-item label="标准值"
                      prop="standardValue">
          <el-input v-model="editParamForm.standardValue"
                    @input="val => onStandardValueInput(val, editParamForm)"
                    placeholder="请输入标准值" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button type="primary" @click="handleEditParamSubmit">确定</el-button>
        <el-button type="primary"
                   @click="handleEditParamSubmit">确定</el-button>
        <el-button @click="editParamDialogVisible = false">取消</el-button>
      </template>
    </el-dialog>
@@ -266,8 +270,32 @@
    paramFormat: "",
    unit: "",
  });
  const onStandardValueInput = (val, target) => {
    const data = target.value || target;
    const type = data.paramType || data.parameterType;
    if (type === 1) {
      // æ•°å€¼æ ¼å¼ï¼šä¸èƒ½è¾“入中文或英文字符
      data.standardValue = val.replace(/[a-zA-Z\u4e00-\u9fa5]/g, "");
    }
  };
  const editParamRules = ref({
    // standardValue: [{ required: true, message: "请输入标准值", trigger: "blur" }],
    standardValue: [
      {
        validator: (rule, value, callback) => {
          const type =
            editParamForm.value.paramType || editParamForm.value.parameterType;
          if (type === 1 && value) {
            if (/[a-zA-Z\u4e00-\u9fa5]/.test(value)) {
              return callback(new Error("数值格式不能包含中英文字符"));
            }
          }
          callback();
        },
        trigger: "blur",
      },
    ],
  });
  const editParamFormRef = ref(null);
src/layout/components/Sidebar/index.vue
@@ -82,6 +82,7 @@
        margin-bottom: 6px;
        border-radius: 14px;
        color: v-bind(getMenuTextColor);
        font-size: 13px;
        &:hover {
          background-color: var(--menu-hover, rgba(0, 0, 0, 0.06)) !important;
@@ -99,6 +100,19 @@
      .el-sub-menu__title {
        color: v-bind(getMenuTextColor);
      }
      :deep(.el-sub-menu__icon-arrow) {
        display: inline-flex !important;
        align-items: center;
        justify-content: center;
        width: 14px;
        height: 14px;
        margin-top: -7px;
        right: 12px;
        font-size: 14px !important;
        color: currentColor !important;
        opacity: 0.72;
      }
      :deep(.el-sub-menu.is-active > .el-sub-menu__title) {
@@ -128,6 +142,7 @@
      :deep(.el-sub-menu.is-active > .el-sub-menu__title .menu-title),
      :deep(.el-sub-menu.is-active > .el-sub-menu__title .svg-icon),
      :deep(.el-sub-menu.is-active > .el-sub-menu__title .el-sub-menu__icon-arrow),
      :deep(.el-menu-item.is-active .menu-title),
      :deep(.el-menu-item.is-active .svg-icon) {
        color: v-bind(theme) !important;
src/views/aiIndustrialBrain/MAINTAIN_RULES.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,7 @@
# AI工业大脑维护规则
1. å½“ `src/views/aiIndustrialBrain/index.vue` æ–°å¢žæ™ºèƒ½ä½“(`agents`)逻辑时,必须同步确认弹窗助手可用性。
2. å¼¹çª—助手由 `src/components/AIChatSidebar/assistants/index.js` çš„ `assistantRegistry` ç»Ÿä¸€æ³¨å†Œã€‚
3. æ–°å¢žæ™ºèƒ½ä½“çš„ `key` è‹¥è¦åœ¨å¼¹çª—中可用,必须在 `assistantRegistry` ä¸­æä¾›åŒåé…ç½®ã€‚
4. æœªåœ¨ `assistantRegistry` æ³¨å†Œçš„æ™ºèƒ½ä½“会在弹窗中显示为 `pending`(开发中)态。
src/views/aiIndustrialBrain/components/AiAssistantWorkspace.vue
@@ -17,7 +17,7 @@
            v-if="assistantMode !== 'pending'"
            :key="assistantMode"
            class="workspace-chat"
            :assistants="assistantMode === 'purchase' ? [purchaseAssistant] : [generalAssistant]"
            :assistants="resolvedAssistants"
            :default-assistant="assistantMode"
            :hide-trigger="true"
            :auto-open="true"
@@ -43,7 +43,7 @@
import { computed } from "vue";
import { ArrowLeftBold } from "@element-plus/icons-vue";
import AIChatSidebar from "@/components/AIChatSidebar/index.vue";
import { generalAssistant, purchaseAssistant } from "@/components/AIChatSidebar/assistants";
import { assistantRegistry } from "@/components/AIChatSidebar/assistants";
const props = defineProps({
  visible: {
@@ -60,11 +60,17 @@
const agentKey = computed(() => String(props.agent?.key || ""));
const agentTitle = computed(() => String(props.agent?.name || "AI助手"));
/**
 * ç»´æŠ¤è§„则:
 * AI工业大脑新增智能体时,若希望右侧弹窗可用,需保证智能体 key åœ¨ assistantRegistry ä¸­æœ‰åŒåé…ç½®ã€‚
 * æœªé…ç½®æ—¶ä¼šè¿›å…¥ pending(开发中)态,作为显式提醒。
 */
const resolvedAssistant = computed(() => assistantRegistry[agentKey.value] || null);
const assistantMode = computed(() => {
  if (agentKey.value === "purchase") return "purchase";
  if (agentKey.value === "general") return "general";
  return "pending";
  return resolvedAssistant.value ? agentKey.value : "pending";
});
const resolvedAssistants = computed(() => (resolvedAssistant.value ? [resolvedAssistant.value] : []));
</script>
<style scoped>
src/views/aiIndustrialBrain/index.vue
@@ -154,6 +154,7 @@
const router = useRouter();
// ç»´æŠ¤çº¦å®šè§ï¼šsrc/views/aiIndustrialBrain/MAINTAIN_RULES.md
const agents = [
  {
    key: "general",
src/views/basicData/product/index.vue
@@ -5,7 +5,7 @@
        <el-input v-model="search"
                  style="width: 210px"
                  placeholder="输入关键字进行搜索"
                  @change="searchFilter"
                  @input="debouncedSearch"
                  @clear="searchFilter"
                  clearable
                  prefix-icon="Search" />
@@ -565,40 +565,31 @@
        proxy.$modal.msg("已取消");
      });
  };
  // è°ƒç”¨tree过滤方法 ä¸­æ–‡è‹±è¿‡æ»¤
  const filterNode = (value, data, node) => {
    if (!value) {
      //如果数据为空,则返回true,显示所有的数据项
      return true;
    }
    // æŸ¥è¯¢åˆ—表是否有匹配数据,将值小写,匹配英文数据
    let val = value.toLowerCase();
    return chooseNode(val, data, node); // è°ƒç”¨è¿‡æ»¤äºŒå±‚方法
  const debounce = (fn, delay = 300) => {
    let timer;
    return (...args) => {
      clearTimeout(timer);
      timer = setTimeout(() => fn(...args), delay);
    };
  };
  // è¿‡æ»¤çˆ¶èŠ‚ç‚¹ / å­èŠ‚ç‚¹ (如果输入的参数是父节点且能匹配,则返回该节点以及其下的所有子节点;如果参数是子节点,则返回该节点的父节点。name是中文字符,enName是英文字符.
  const chooseNode = (value, data, node) => {
    if (data.label.indexOf(value) !== -1) {
  const debouncedSearch = debounce(() => {
    searchFilter();
  }, 300);
  const filterNode = (value, data) => {
    if (!value) return true;
    return chooseNode(value.toLowerCase(), data);
  };
  const chooseNode = (value, data) => {
    const label = (data.label || '').toLowerCase();
    if (label.indexOf(value) !== -1) {
      return true;
    }
    const level = node.level;
    // å¦‚果传入的节点本身就是一级节点就不用校验了
    if (level === 1) {
      return false;
    if (data.children && data.children.length > 0) {
      return data.children.some(child => chooseNode(value, child));
    }
    // å…ˆå–当前节点的父节点
    let parentData = node.parent;
    // éåŽ†å½“å‰èŠ‚ç‚¹çš„çˆ¶èŠ‚ç‚¹
    let index = 0;
    while (index < level - 1) {
      // å¦‚果匹配到直接返回,此处name值是中文字符,enName是英文字符。判断匹配中英文过滤
      if (parentData.data.label.indexOf(value) !== -1) {
        return true;
      }
      // å¦åˆ™çš„话再往上一层做匹配
      parentData = parentData.parent;
      index++;
    }
    // æ²¡åŒ¹é…åˆ°è¿”回false
    return false;
  };
  getProductTreeList();
src/views/basicData/supplierManage/components/BlacklistTab.vue
@@ -231,7 +231,10 @@
        </div>
      </template>
    </el-dialog>
    <files-dia ref="filesDia"></files-dia>
    <FileList v-if="fileListDialogVisible"
              v-model:visible="fileListDialogVisible"
              record-type="supplier_manage"
              :record-id="recordId" />
  </div>
</template>
@@ -249,7 +252,9 @@
} from "@/api/basicData/supplierManageFile.js";
import useUserStore from "@/store/modules/user";
import { getToken } from "@/utils/auth.js";
import FilesDia from "../filesDia.vue";
const FileList = defineAsyncComponent(() =>
    import("@/components/Dialog/FileList.vue")
);
const { proxy } = getCurrentInstance();
const userStore = useUserStore();
@@ -327,7 +332,7 @@
        name: "资质文件",
        type: "text",
        clickFun: (row) => {
          openFilesFormDia(row)
          openFileDialog(row)
        }
      }
    ],
@@ -342,7 +347,8 @@
  size: 100,
  total: 0,
});
const filesDia = ref()
const fileListDialogVisible = ref(false);
const recordId = ref();
// ç”¨æˆ·ä¿¡æ¯è¡¨å•弹框数据
const operationType = ref("");
const dialogFormVisible = ref(false);
@@ -567,10 +573,9 @@
  return `${year}-${month}-${day}`;
}
// æ‰“开附件弹框
const openFilesFormDia = (row) => {
  nextTick(() => {
    filesDia.value?.openDialog(row)
  })
const openFileDialog = async row => {
  recordId.value = row.id;
  fileListDialogVisible.value = true;
};
onMounted(() => {
src/views/basicData/supplierManage/components/HomeTab.vue
@@ -237,7 +237,10 @@
        </div>
      </template>
    </el-dialog>
    <files-dia ref="filesDia"></files-dia>
    <FileList v-if="fileListDialogVisible"
              v-model:visible="fileListDialogVisible"
              record-type="supplier_manage"
              :record-id="recordId" />
  </div>
</template>
@@ -255,7 +258,9 @@
} from "@/api/basicData/supplierManageFile.js";
import useUserStore from "@/store/modules/user";
import { getToken } from "@/utils/auth.js";
import FilesDia from "../filesDia.vue";
const FileList = defineAsyncComponent(() =>
    import("@/components/Dialog/FileList.vue")
);
const { proxy } = getCurrentInstance();
const userStore = useUserStore();
@@ -333,7 +338,7 @@
        name: "资质文件",
        type: "text",
        clickFun: (row) => {
          openFilesFormDia(row)
          openFileDialog(row)
        }
      }
    ],
@@ -343,12 +348,13 @@
const selectedRows = ref([]);
const userList = ref([]);
const tableLoading = ref(false);
const fileListDialogVisible = ref(false);
const recordId = ref();
const page = reactive({
  current: 1,
  size: 100,
  total: 0,
});
const filesDia = ref()
// ç”¨æˆ·ä¿¡æ¯è¡¨å•弹框数据
const operationType = ref("");
const dialogFormVisible = ref(false);
@@ -573,10 +579,9 @@
  return `${year}-${month}-${day}`;
}
// æ‰“开附件弹框
const openFilesFormDia = (row) => {
  nextTick(() => {
    filesDia.value?.openDialog(row)
  })
const openFileDialog = async row => {
  recordId.value = row.id;
  fileListDialogVisible.value = true;
};
onMounted(() => {
src/views/collaborativeApproval/approvalProcess/components/infoFormDia.vue
@@ -152,7 +152,8 @@
    startDate: "", // è¯·å‡å¼€å§‹æ—¶é—´
    endDate: "", // è¯·å‡ç»“束时间
    price: null, // æŠ¥é”€é‡‘额
    location: "" // å‡ºå·®åœ°ç‚¹
    location: "", // å‡ºå·®åœ°ç‚¹
    storageBlobDTOS: []
  },
  rules: {
    approveId: [{ required: false, message: "请输入", trigger: "blur" }],
@@ -270,7 +271,7 @@
      return
    }
  }
  form.value.storageBlobDTOList = fileList.value
  form.value.storageBlobDTOS = fileList.value
  proxy.$refs.formRef.validate(valid => {
    if (valid) {
src/views/collaborativeApproval/approvalProcess/index.vue
@@ -126,20 +126,23 @@
    <!-- å¼¹çª—组件 -->
    <info-form-dia ref="infoFormDia" @close="handleQuery" :approveType="currentApproveType"></info-form-dia>
    <approval-dia ref="approvalDia" @close="handleQuery" :approveType="currentApproveType"></approval-dia>
    <FileList ref="fileListRef" />
    <FileList v-if="fileDialogVisible"
              v-model:visible="fileDialogVisible"
              record-type="approve_process"
              :record-id="recordId" />
  </div>
</template>
<script setup>
import FileList from "./fileList.vue";
import { Search, Plus, Delete, Download, RefreshRight, DocumentChecked } from "@element-plus/icons-vue";
import {onMounted, ref, computed, reactive, toRefs, nextTick, getCurrentInstance} from "vue";
import {onMounted, ref, computed, reactive, toRefs, nextTick, getCurrentInstance, defineAsyncComponent} from "vue";
import {ElMessageBox} from "element-plus";
import { useRoute } from 'vue-router';
import InfoFormDia from "@/views/collaborativeApproval/approvalProcess/components/infoFormDia.vue";
import ApprovalDia from "@/views/collaborativeApproval/approvalProcess/components/approvalDia.vue";
import {approveProcessDelete, approveProcessListPage} from "@/api/collaborativeApproval/approvalProcess.js";
import useUserStore from "@/store/modules/user";
const FileList = defineAsyncComponent(() => import("@/components/Dialog/FileList.vue"));
const userStore = useUserStore();
const route = useRoute();
@@ -337,7 +340,7 @@
      name: "附件",
      type: "text",
      clickFun: (row) => {
        downLoadFile(row);
        openFilesFormDia(row);
      },
    });
  }
@@ -371,11 +374,17 @@
  page.current = 1;
  getList();
};
const fileListRef = ref(null)
const downLoadFile = (row) => {
  fileListRef.value.open(row.commonFileList)
// æ‰“开附件弹窗
const recordId =ref(0)
const fileDialogVisible = ref(false)
// æ‰“开附件弹框
const openFilesFormDia = async (row) => {
  recordId.value = row.id
  fileDialogVisible.value = true
}
const pagination = (obj) => {
  page.current = obj.page;
  page.size = obj.limit;
src/views/collaborativeApproval/sealManagement/index.vue
@@ -87,10 +87,18 @@
        </el-form-item>
        <el-form-item label="紧急程度" prop="urgency">
          <el-radio-group v-model="sealForm.urgency">
            <el-radio label="normal">普通</el-radio>
            <el-radio label="urgent">紧急</el-radio>
            <el-radio label="very-urgent">特急</el-radio>
            <el-radio value="normal">普通</el-radio>
            <el-radio value="urgent">紧急</el-radio>
            <el-radio value="very-urgent">特急</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="附件上传">
          <AttachmentUploadFile
            v-model:fileList="sealForm.storageBlobDTOs"
            :limit="10"
            :fileSize="50"
            buttonText="点击上传附件"
          />
        </el-form-item>
      </el-form>
    </FormDialog>
@@ -119,8 +127,27 @@
          </el-descriptions-item>
          <el-descriptions-item label="申请原因" :span="2">{{ currentSealDetail.reason }}</el-descriptions-item>
        </el-descriptions>
        <!-- é™„件列表 -->
        <div v-if="currentSealDetail.storageBlobVOList?.length || currentSealDetail.storageBlobDTOs?.length" class="attachment-section">
          <div class="attachment-title">附件列表:</div>
          <el-table :data="currentSealDetail.storageBlobVOList || currentSealDetail.storageBlobDTOs" border class="attachment-table">
            <el-table-column label="附件名称" show-overflow-tooltip>
              <template #default="scope">
                {{ scope.row.originalFilename || scope.row.name || scope.row.fileName || '未命名文件' }}
              </template>
            </el-table-column>
            <el-table-column fixed="right" label="操作" width="150" align="center">
              <template #default="scope">
                <el-button link type="primary" size="small" @click="previewFile(scope.row)">预览</el-button>
                <el-button link type="primary" size="small" @click="downloadFile(scope.row)">下载</el-button>
              </template>
            </el-table-column>
          </el-table>
        </div>
      </div>
    </FormDialog>
    <!-- æ–‡ä»¶é¢„览组件 -->
    <FilePreview ref="filePreviewRef" />
  </div>
</template>
@@ -134,6 +161,9 @@
import useUserStore from '@/store/modules/user'
import FormDialog from '@/components/Dialog/FormDialog.vue'
import PIMTable from '@/components/PIMTable/PIMTable.vue'
import AttachmentUploadFile from '@/components/AttachmentUpload/file/index.vue'
import FilePreview from '@/components/filePreview/index.vue'
import download from '@/plugins/download.js'
// å“åº”式数据
// ç”¨å°ç”³è¯·ç›¸å…³
@@ -143,6 +173,7 @@
const tableLoading = ref(false)
const showSealDetailDialog = ref(false)
const currentSealDetail = ref(null)
const filePreviewRef = ref(null)
const sealFormRef = ref()
const userList = ref([])
const sealForm = reactive({
@@ -152,7 +183,8 @@
  reason: '',
  approveUserId: '',
  urgency: 'normal',
  status: 'pending'
  status: 'pending',
  storageBlobDTOs: []
})
const sealRules = {
@@ -281,7 +313,8 @@
        reason: '',
        approveUserId: '',
        urgency: 'normal',
        status: 'pending'
        status: 'pending',
        storageBlobDTOs: []
      })
      }
    }).catch(err => {
@@ -301,7 +334,8 @@
    reason: '',
    approveUserId: '',
    urgency: 'normal',
    status: 'pending'
    status: 'pending',
    storageBlobDTOs: []
  })
  // æ¸…除表单验证状态
  if (sealFormRef.value) {
@@ -318,6 +352,27 @@
const viewSealDetail = (row) => {
  currentSealDetail.value = row
  showSealDetailDialog.value = true
}
// é¢„览文件
const previewFile = (row) => {
  const url = row.previewURL || row.previewUrl || row.url
  if (url && filePreviewRef.value) {
    filePreviewRef.value.open(url)
  } else {
    ElMessage.warning('文件地址无效,无法预览')
  }
}
// ä¸‹è½½æ–‡ä»¶
const downloadFile = (row) => {
  const url = row.downloadURL || row.downloadUrl || row.url
  if (url) {
    const filename = row.originalFilename || row.name || row.fileName || 'download'
    download.byUrl(url, filename)
  } else {
    ElMessage.warning('文件地址无效,无法下载')
  }
}
// å®¡æ‰¹ç”¨å°ç”³è¯·
const approveSeal = (row) => {
@@ -421,4 +476,19 @@
.ml-10 {
  margin-left: 10px;
}
.attachment-section {
  margin-top: 20px;
}
.attachment-title {
  font-size: 14px;
  color: #606266;
  margin-bottom: 10px;
  font-weight: 500;
}
.attachment-table {
  border-radius: 4px;
}
</style>
src/views/equipmentManagement/inspectionManagement/components/formDia.vue
@@ -26,6 +26,21 @@
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="巡检项目" prop="inspectionProject">
              <el-input v-model="form.inspectionProject" placeholder="请输入巡检项目" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="是否启用" prop="isEnabled">
              <el-radio-group v-model="form.isEnabled">
                <el-radio :value="1">是</el-radio>
                <el-radio :value="0">否</el-radio>
              </el-radio-group>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="备注" prop="remarks">
              <el-input v-model="form.remarks" placeholder="请输入备注" type="textarea" />
            </el-form-item>
@@ -118,6 +133,8 @@
    taskName: undefined,
    inspector: '',
    inspectorIds: '',
    inspectionProject: '',
    isEnabled: 1,
    remarks: '',
    frequencyType: '',
    frequencyDetail: '',
@@ -245,6 +262,8 @@
    taskName: undefined,
    inspector: '',
    inspectorIds: '',
    inspectionProject: '',
    isEnabled: 1,
    remarks: '',
    frequencyType: '',
    frequencyDetail: '',
src/views/equipmentManagement/inspectionManagement/index.vue
@@ -70,6 +70,12 @@
                    class="no-data">--</span>
            </div>
          </template>
          <template #isEnabled="{ row }">
            <el-tag :type="row.isEnabled === 1 ? 'success' : 'danger'"
                    size="small">
              {{ row.isEnabled == 1 ? '是' : '否' }}
            </el-tag>
          </template>
        </PIMTable>
      </div>
    </el-card>
@@ -126,8 +132,16 @@
  // åˆ—配置
  const columns = ref([
    { prop: "taskName", label: "巡检任务名称", minWidth: 160 },
    { prop: "inspectionProject", label: "巡检项目", minWidth: 150 },
    { prop: "remarks", label: "备注", minWidth: 150 },
    { prop: "inspector", label: "执行巡检人", minWidth: 150, slot: "inspector" },
    {
      prop: "isEnabled",
      label: "是否启用",
      minWidth: 100,
      dataType: "slot",
      slot: "isEnabled",
    },
    {
      prop: "frequencyType",
      label: "频次",
@@ -176,6 +190,19 @@
    },
    { prop: "registrant", label: "登记人", minWidth: 100 },
    { prop: "createTime", label: "登记日期", minWidth: 100 },
    {
      prop: "inspectionResult",
      label: "巡检结果",
      minWidth: 100,
      dataType: "tag",
      formatData: val => {
        return val == 1 ? "正常" : "异常";
      },
      formatType: val => {
        return val == 1 ? "success" : "danger";
      },
    },
    { prop: "abnormalDescription", label: "异常描述", minWidth: 100 },
  ]);
  // æ“ä½œåˆ—配置
@@ -227,8 +254,10 @@
      operationsArr.value = ["edit"];
    } else if (value === "task") {
      const operationColumn = getOperationColumn(["viewFile"]);
      // å®šæ—¶ä»»åŠ¡è®°å½•ä¸å±•ç¤º"是否启用"列
      const taskColumns = columns.value.filter(col => col.prop !== "isEnabled");
      tableColumns.value = [
        ...columns.value,
        ...taskColumns,
        ...(operationColumn ? [operationColumn] : []),
      ];
      operationsArr.value = ["viewFile"];
src/views/equipmentManagement/measurementEquipment/components/formDia.vue
@@ -36,15 +36,6 @@
                </el-row>
                <el-row :gutter="30">
                    <el-col :span="12">
                        <el-form-item label="安装位置:" prop="instationLocation">
                            <el-input
                                v-model="form.instationLocation"
                                placeholder="请输入"
                                clearable
                            />
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="检定单位:" prop="unit">
              <el-input
                  v-model="form.unit"
@@ -53,17 +44,17 @@
              />
                        </el-form-item>
                    </el-col>
                </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="证书编号:" prop="model">
                    <el-col :span="12">
                        <el-form-item label="证书编号:" prop="model">
              <el-input
                  v-model="form.model"
                  placeholder="请输入"
                  clearable
              />
            </el-form-item>
          </el-col>
                    </el-col>
                </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="最新鉴定日期:" prop="mostDate">
              <el-date-picker
@@ -77,8 +68,6 @@
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="有效日期(天):" prop="valid">
              <el-input
@@ -91,15 +80,6 @@
              >
              <template #append>日</template>
              </el-input>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="检定周期:" prop="cycle">
              <el-input
                  v-model="form.cycle"
                  placeholder="请输入检定周期"
                  clearable
              />
            </el-form-item>
          </el-col>
        </el-row>
@@ -184,10 +164,8 @@
    form: {
        code: "",
    name: "",
    instationLocation: "",
    mostDate:"",
        model: "",
    cycle:"",
        validDate: "",
        nextDate: "",
        userId: "",
@@ -203,9 +181,7 @@
        nextDate: [{required: true, message: "请选择", trigger: "change"}],
        userId: [{required: true, message: "请选择", trigger: "change"}],
        recordDate: [{required: true, message: "请选择", trigger: "change"}],
    instationLocation: [{required: true, message: "请输入", trigger: "blur"}],
    mostDate: [{required: true, message: "请选择", trigger: "change"}],
    cycle: [{required: true, message: "请选择", trigger: "blur"}],
    valid: [
      {required: true, message: "请输入", trigger: "blur"},
      {
src/views/equipmentManagement/measurementEquipment/index.vue
@@ -42,6 +42,7 @@
                :tableLoading="tableLoading"
                @pagination="pagination"
        :dbRowClick="dbRowClick"
        :rowClassName="rowClassName"
            ></PIMTable>
        </div>
        <form-dia ref="formDia" @close="handleQuery"></form-dia>
@@ -89,12 +90,6 @@
    align: "center",
  },
    {
        label: "安装位置",
        prop: "instationLocation",
        width: 150,
    align:"center"
    },
    {
        label: "检定单位",
        prop: "unit",
        width: 200,
@@ -130,12 +125,6 @@
        width: 130,
    align:"center"
    },
  {
    label: "检定周期(天)",
    prop: "cycle",
    width: 130,
    align:"center"
  },
  {
    label: "状态",
    prop: "status",
@@ -193,6 +182,31 @@
const dbRowClick = (row)=>{
  rowClickData.value?.openDialog(row)
}
// è¡Œæ ·å¼ï¼šå¿«åˆ°æœŸï¼ˆ7天内)或逾期标红
const rowClassName = ({ row }) => {
  console.log('rowClassName called:', row);
  // valid æ˜¯æœ‰æ•ˆå¤©æ•°ï¼ŒmostDate æ˜¯æœ€æ–°æ£€å®šæ—¥æœŸ
  if (row.valid && row.mostDate) {
    const mostDate = new Date(row.mostDate);
    // è®¡ç®—到期日期 = æ£€å®šæ—¥æœŸ + æœ‰æ•ˆå¤©æ•°
    const validDays = parseInt(row.valid) || 0;
    const expireDate = new Date(mostDate);
    expireDate.setDate(expireDate.getDate() + validDays);
    const now = new Date();
    const diffDays = Math.ceil((expireDate - now) / (1000 * 60 * 60 * 24));
    console.log('row:', row.code, 'validDays:', validDays, 'expireDate:', expireDate, 'diffDays:', diffDays);
    // 7天内到期或已逾期都标红
    if (diffDays <= 7) {
      console.log('return warning-row');
      return 'warning-row';
    }
  } else {
    console.log('row missing valid or mostDate:', row.valid, row.mostDate);
  }
  return '';
}
// è¡¨æ ¼é€‰æ‹©æ•°æ®
@@ -294,5 +308,13 @@
</script>
<style scoped>
:deep(.el-table .warning-row) {
  background-color: #fef0f0 !important;
}
:deep(.el-table .warning-row:hover > td) {
  background-color: #f9d5d5 !important;
}
:deep(.el-table .el-table__body tr.warning-row td) {
  background-color: #fef0f0 !important;
}
</style>
src/views/equipmentManagement/repair/Modal/AcceptanceModal.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,144 @@
<template>
  <FormDialog
    v-model="visible"
    title="验收审批"
    width="500px"
    @confirm="submitForm"
    @cancel="handleCancel"
    @close="handleCancel"
  >
    <el-form :model="form" :rules="rules" label-width="100px">
      <el-form-item label="验收人" prop="acceptanceName">
        <el-select
          v-model="form.acceptanceName"
          placeholder="请选择验收人"
          filterable
          style="width: 100%"
        >
          <el-option
            v-for="item in userList"
            :key="item.userId"
            :label="item.nickName"
            :value="item.nickName"
          />
        </el-select>
      </el-form-item>
      <el-form-item label="验收时间" prop="acceptanceTime">
        <el-date-picker
          v-model="form.acceptanceTime"
          type="datetime"
          placeholder="请选择验收时间"
          format="YYYY-MM-DD HH:mm:ss"
          value-format="YYYY-MM-DD HH:mm:ss"
          style="width: 100%"
        />
      </el-form-item>
      <el-form-item label="验收备注" prop="acceptanceRemark">
        <el-input
          v-model="form.acceptanceRemark"
          type="textarea"
          :rows="3"
          placeholder="请输入验收备注"
        />
      </el-form-item>
    </el-form>
  </FormDialog>
</template>
<script setup>
import FormDialog from "@/components/Dialog/FormDialog.vue";
import { ref, reactive } from "vue";
import { ElMessage } from "element-plus";
import { userListNoPageByTenantId } from "@/api/system/user.js";
import { repairAcceptance } from "@/api/equipmentManagement/repair";
import dayjs from "dayjs";
defineOptions({
  name: "验收审批弹窗",
});
const emits = defineEmits(["ok"]);
const visible = ref(false);
const loading = ref(false);
const repairId = ref(null);
const userList = ref([]);
const form = reactive({
  acceptanceName: undefined,
  acceptanceTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
  acceptanceRemark: undefined,
});
const rules = {
  acceptanceName: [
    { required: true, message: "请选择验收人", trigger: "change" },
  ],
  acceptanceTime: [
    { required: true, message: "请选择验收时间", trigger: "change" },
  ],
  acceptanceRemark: [
    { required: true, message: "请输入验收备注", trigger: "blur" },
  ],
};
// åŠ è½½ç”¨æˆ·åˆ—è¡¨
const loadUserList = async () => {
  const { data } = await userListNoPageByTenantId();
  userList.value = data;
};
// æ‰“开弹窗
const open = async (row) => {
  repairId.value = row.id;
  visible.value = true;
  // é‡ç½®è¡¨å•
  form.acceptanceName = undefined;
  form.acceptanceTime = dayjs().format("YYYY-MM-DD HH:mm:ss");
  form.acceptanceRemark = undefined;
  await loadUserList();
};
// æäº¤è¡¨å•
const submitForm = async () => {
  if (!form.acceptanceName) {
    ElMessage.warning("请选择验收人");
    return;
  }
  if (!form.acceptanceTime) {
    ElMessage.warning("请选择验收时间");
    return;
  }
  if (!form.acceptanceRemark) {
    ElMessage.warning("请输入验收备注");
    return;
  }
  loading.value = true;
  try {
    const { code } = await repairAcceptance({
      id: repairId.value,
      acceptanceName: form.acceptanceName,
      acceptanceTime: form.acceptanceTime,
      acceptanceRemark: form.acceptanceRemark,
    });
    if (code === 200) {
      ElMessage.success("验收通过");
      visible.value = false;
      emits("ok");
    }
  } finally {
    loading.value = false;
  }
};
const handleCancel = () => {
  visible.value = false;
};
defineExpose({
  open,
});
</script>
<style lang="scss" scoped></style>
src/views/equipmentManagement/repair/Modal/RepairModal.vue
@@ -49,19 +49,44 @@
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="项目">
            <el-input v-model="form.machineryCategory" placeholder="请输入项目" />
          <el-form-item label="报修报修项目">
            <el-input v-model="form.machineryCategory" placeholder="请输入报修报修项目" />
          </el-form-item>
        </el-col>
      </el-row>
      <el-row>
        <el-col :span="12">
          <el-form-item label="维修人">
            <el-input v-model="form.maintenanceName" placeholder="请输入维修人姓名" />
          </el-form-item>
        </el-col>
      </el-row>
      <el-row v-if="id">
        <el-col :span="12">
          <el-form-item label="报修状态">
            <el-select v-model="form.status">
            <el-select v-model="form.status" disabled>
              <el-option label="待维修" :value="0"></el-option>
              <el-option label="完结" :value="1"></el-option>
              <el-option label="已验收" :value="1"></el-option>
              <el-option label="失败" :value="2"></el-option>
            </el-select>
          </el-form-item>
        </el-col>
      </el-row>
      <!-- éªŒæ”¶ä¿¡æ¯å±•示 -->
      <el-row v-if="id && form.status === 1">
        <el-col :span="12">
          <el-form-item label="验收人">
            <el-input v-model="form.acceptanceName" disabled />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="验收时间">
            <el-input v-model="form.acceptanceTime" disabled />
          </el-form-item>
        </el-col>
        <el-col :span="24">
          <el-form-item label="验收备注">
            <el-input v-model="form.acceptanceRemark" type="textarea" :rows="2" disabled />
          </el-form-item>
        </el-col>
      </el-row>
@@ -131,6 +156,7 @@
  status: 0, // æŠ¥ä¿®çŠ¶æ€
  machineryCategory: undefined,
  storageBlobDTOs: [],
  maintenanceName: undefined, // ç»´ä¿®äºº
});
const setDeviceModel = (deviceId) => {
@@ -148,6 +174,10 @@
  form.status = data.status;
  form.machineryCategory = data.machineryCategory;
  form.storageBlobDTOs = data.storageBlobVOs || [];
  form.maintenanceName = data.maintenanceName;
  form.acceptanceName = data.acceptanceName;
  form.acceptanceTime = data.acceptanceTime;
  form.acceptanceRemark = data.acceptanceRemark;
};
const sendForm = async () => {
src/views/equipmentManagement/repair/index.vue
@@ -100,13 +100,14 @@
        <template #statusRef="{ row }">
          <el-tag v-if="row.status === 2" type="danger">失败</el-tag>
          <el-tag v-if="row.status === 1" type="success">完结</el-tag>
          <el-tag v-if="row.status === 3" type="info">待验收</el-tag>
          <el-tag v-if="row.status === 0" type="warning">待维修</el-tag>
        </template>
        <template #operation="{ row }">
          <el-button
            type="primary"
            link
            :disabled="row.status === 1"
            :disabled="row.status === 1 || row.status === 3"
            @click="editRepair(row.id)"
          >
            ç¼–辑
@@ -114,15 +115,23 @@
          <el-button
            type="success"
            link
            :disabled="row.status === 1"
            :disabled="row.status !== 0"
            @click="addMaintain(row)"
          >
            ç»´ä¿®
          </el-button>
          <el-button
            type="warning"
            link
            :disabled="row.status !== 3"
            @click="openAcceptance(row)"
          >
            éªŒæ”¶
          </el-button>
          <el-button
            type="danger"
            link
            :disabled="row.status === 1"
            :disabled="row.status === 1 || row.status === 3"
            @click="delRepairByIds(row.id)"
          >
            åˆ é™¤
@@ -139,6 +148,7 @@
    </div>
    <RepairModal ref="repairModalRef" @ok="getTableData"/>
    <MaintainModal ref="maintainModalRef" @ok="getTableData"/>
    <AcceptanceModal ref="acceptanceModalRef" @ok="getTableData"/>
    <FileList v-if="fileDialogVisible"  v-model:visible="fileDialogVisible" :record-type="'device_repair'" :record-id="recordId"  />
  </div>
</template>
@@ -151,6 +161,7 @@
import {ElMessageBox, ElMessage} from "element-plus";
import dayjs from "dayjs";
import MaintainModal from "./Modal/MaintainModal.vue";
import AcceptanceModal from "./Modal/AcceptanceModal.vue";
const FileList = defineAsyncComponent(() => import("@/components/Dialog/FileList.vue"));
defineOptions({
@@ -162,6 +173,7 @@
// æ¨¡æ€æ¡†å®žä¾‹
const repairModalRef = ref();
const maintainModalRef = ref();
const acceptanceModalRef = ref();
// è¡¨æ ¼å¤šé€‰æ¡†é€‰ä¸­é¡¹
const multipleList = ref([]);
@@ -197,7 +209,7 @@
        prop: "deviceModel",
      },
      {
        label: "项目",
        label: "报修项目",
        align: "center",
        prop: "machineryCategory",
      },
@@ -232,6 +244,17 @@
        align: "center",
        prop: "maintenanceTime",
        formatData: (cell) => (cell ? dayjs(cell).format("YYYY-MM-DD") : ""),
      },
      {
        label: "验收人",
        align: "center",
        prop: "acceptanceName",
      },
      {
        label: "验收时间",
        align: "center",
        prop: "acceptanceTime",
        formatData: (cell) => (cell ? dayjs(cell).format("YYYY-MM-DD HH:mm:ss") : ""),
      },
      {
        label: "状态",
@@ -301,6 +324,11 @@
  maintainModalRef.value.open(row.id, row);
};
// æ‰“开验收弹窗
const openAcceptance = (row) => {
  acceptanceModalRef.value.open(row);
};
const changePage = ({page, limit}) => {
  pagination.currentPage = page;
  pagination.pageSize = limit;
src/views/equipmentManagement/upkeep/Form/MaintenanceModal.vue
@@ -1,199 +1,216 @@
<template>
  <FormDialog
    v-model="visible"
    :title="'设备保养'"
    width="500px"
    @confirm="sendForm"
    @cancel="handleCancel"
    @close="handleClose"
  >
    <el-form :model="form" label-width="100px">
  <FormDialog v-model="visible"
              :title="'设备保养'"
              width="500px"
              @confirm="sendForm"
              @cancel="handleCancel"
              @close="handleClose">
    <el-form :model="form"
             label-width="100px">
      <el-form-item label="实际保养人">
        <el-input
          v-model="form.maintenanceActuallyName"
          placeholder="请输入实际保养人"
        ></el-input>
        <el-input v-model="form.maintenanceActuallyName"
                  placeholder="请输入实际保养人"></el-input>
      </el-form-item>
      <el-form-item label="实际保养日期">
        <el-date-picker
          v-model="form.maintenanceActuallyTime"
          placeholder="请选择实际保养日期"
          format="YYYY-MM-DD HH:mm:ss"
          value-format="YYYY-MM-DD HH:mm:ss"
          type="datetime"
          clearable
          style="width: 100%"
        />
        <el-date-picker v-model="form.maintenanceActuallyTime"
                        placeholder="请选择实际保养日期"
                        format="YYYY-MM-DD HH:mm:ss"
                        value-format="YYYY-MM-DD HH:mm:ss"
                        type="datetime"
                        clearable
                        style="width: 100%" />
      </el-form-item>
      <el-form-item label="保养状态">
        <el-select v-model="form.status">
          <el-option label="待保养" :value="0"></el-option>
          <el-option label="完结" :value="1"></el-option>
          <el-option label="失败" :value="2"></el-option>
          <el-option label="待保养"
                     :value="0"></el-option>
          <el-option label="完结"
                     :value="1"></el-option>
          <el-option label="失败"
                     :value="2"></el-option>
        </el-select>
      </el-form-item>
      <el-form-item label="保养结果">
        <el-input
          v-model="form.maintenanceResult"
          placeholder="请输入保养结果"
          type="text" />
        <el-input v-model="form.maintenanceResult"
                  placeholder="请输入保养结果"
                  type="text" />
      </el-form-item>
      <el-form-item label="设备备件">
        <el-select v-model="form.sparePartsIds" :loading="loadingSparePartOptions" placeholder="请选择设备备件" multiple filterable>
          <el-option
              v-for="item in sparePartOptions"
              :key="item.id"
              :label="item.name"
              :value="item.id"
          />
        <el-select v-model="form.sparePartsIds"
                   :loading="loadingSparePartOptions"
                   placeholder="请选择设备备件"
                   multiple
                   filterable>
          <el-option v-for="item in sparePartOptions"
                     :key="item.id"
                     :label="item.name"
                     :value="item.id" />
        </el-select>
      </el-form-item>
      <el-form-item v-if="selectedSpareParts.length" label="领用数量">
      <el-form-item v-if="selectedSpareParts.length"
                    label="领用数量">
        <div style="width: 100%">
          <div
              v-for="item in selectedSpareParts"
              :key="item.id"
              style="display: flex; align-items: center; gap: 10px; margin-bottom: 10px;"
          >
          <div v-for="item in selectedSpareParts"
               :key="item.id"
               style="display: flex; align-items: center; gap: 10px; margin-bottom: 10px;">
            <div style="flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
              {{ item.name }}
              <span v-if="item.quantity !== null && item.quantity !== undefined" style="color: #909399;">
              <span v-if="item.quantity !== null && item.quantity !== undefined"
                    style="color: #909399;">
                ï¼ˆåº“存:{{ item.quantity }})
              </span>
            </div>
            <el-input-number
                v-model="sparePartQtyMap[item.id]"
                :min="1"
                :max="item.quantity !== null && item.quantity !== undefined ? Number(item.quantity) : undefined"
                :step="1"
                controls-position="right"
                style="width: 180px"
            />
            <el-input-number v-model="sparePartQtyMap[item.id]"
                             :min="1"
                             :max="item.quantity !== null && item.quantity !== undefined ? Number(item.quantity) : undefined"
                             :step="1"
                             controls-position="right"
                             style="width: 180px" />
          </div>
        </div>
      </el-form-item>
      <el-form-item label="附件">
        <FileUpload v-model:file-list="form.storageBlobDTOs" />
      </el-form-item>
    </el-form>
  </FormDialog>
</template>
<script setup>
import FormDialog from "@/components/Dialog/FormDialog.vue";
import { addMaintenance } from "@/api/equipmentManagement/upkeep";
import useFormData from "@/hooks/useFormData";
import dayjs from "dayjs";
import useUserStore from "@/store/modules/user";
import { ElMessage } from "element-plus";
import {computed, ref} from "vue";
import {getSparePartsList} from "@/api/equipmentManagement/spareParts.js";
  import FormDialog from "@/components/Dialog/FormDialog.vue";
  import FileUpload from "@/components/AttachmentUpload/file/index.vue";
  import { addMaintenance } from "@/api/equipmentManagement/upkeep";
  import useFormData from "@/hooks/useFormData";
  import dayjs from "dayjs";
  import useUserStore from "@/store/modules/user";
  import { ElMessage } from "element-plus";
  import { computed, ref, nextTick, getCurrentInstance } from "vue";
  import { getSparePartsList } from "@/api/equipmentManagement/spareParts.js";
defineOptions({
  name: "保养模态框",
});
  defineOptions({
    name: "保养模态框",
  });
const emits = defineEmits(["ok"]);
  const emits = defineEmits(["ok"]);
// ä¿å­˜è®¡åˆ’保养记录的id
const planId = ref();
const visible = ref(false);
const loading = ref(false);
const userStore = useUserStore();
  const { proxy } = getCurrentInstance();
  // ä¿å­˜è®¡åˆ’保养记录的id
  const planId = ref();
  const visible = ref(false);
  const loading = ref(false);
  const userStore = useUserStore();
const { form, resetForm } = useFormData({
  maintenanceActuallyName: undefined, // å®žé™…保养人
  maintenanceActuallyTime: undefined, // å®žé™…保养日期
  maintenanceResult: undefined, // ä¿å…»ç»“æžœ
  status: 0, // ä¿å…»çŠ¶æ€
  sparePartsIds: [],
});
  const { form, resetForm } = useFormData({
    maintenanceActuallyName: undefined, // å®žé™…保养人
    maintenanceActuallyTime: undefined, // å®žé™…保养日期
    maintenanceResult: undefined, // ä¿å…»ç»“æžœ
    status: 0, // ä¿å…»çŠ¶æ€
    sparePartsIds: [],
    storageBlobDTOs: [],
  });
const sparePartOptions = ref([])
const loadingSparePartOptions = ref(true)
const sparePartQtyMap = ref({})
  const sparePartOptions = ref([]);
  const loadingSparePartOptions = ref(true);
  const sparePartQtyMap = ref({});
const selectedSpareParts = computed(() => {
  const ids = Array.isArray(form.sparePartsIds) ? form.sparePartsIds : [];
  const set = new Set(ids.map((i) => String(i)));
  return (sparePartOptions.value || []).filter((p) => set.has(String(p.id)));
});
  const selectedSpareParts = computed(() => {
    const ids = Array.isArray(form.sparePartsIds) ? form.sparePartsIds : [];
    const set = new Set(ids.map(i => String(i)));
    return (sparePartOptions.value || []).filter(p => set.has(String(p.id)));
  });
const setForm = (data) => {
  form.maintenanceActuallyName =
    data.maintenanceActuallyName ?? userStore.nickName;
  form.maintenanceActuallyTime =
    data.maintenanceActuallyTime
  const setForm = data => {
    form.maintenanceActuallyName =
      data.maintenanceActuallyName ?? userStore.nickName;
    form.maintenanceActuallyTime = data.maintenanceActuallyTime
      ? dayjs(data.maintenanceActuallyTime).format("YYYY-MM-DD HH:mm:ss")
      : dayjs().format("YYYY-MM-DD HH:mm:ss");
  form.maintenanceResult = data.maintenanceResult;
  form.status = 1; // é»˜è®¤çŠ¶æ€ä¸ºå®Œç»“
  // multiple é€‰æ‹©å™¨è¦æ±‚数组;后端常返回 "1,2,3"
  if (Array.isArray(data?.sparePartsIds)) {
    form.sparePartsIds = data.sparePartsIds.map((v) => Number(v)).filter((v) => Number.isFinite(v));
  } else if (typeof data?.sparePartsIds === "string") {
    form.sparePartsIds = data.sparePartsIds
    form.maintenanceResult = data.maintenanceResult;
    form.status = 1; // é»˜è®¤çŠ¶æ€ä¸ºå®Œç»“
    // multiple é€‰æ‹©å™¨è¦æ±‚数组;后端常返回 "1,2,3"
    if (Array.isArray(data?.sparePartsIds)) {
      form.sparePartsIds = data.sparePartsIds
        .map(v => Number(v))
        .filter(v => Number.isFinite(v));
    } else if (typeof data?.sparePartsIds === "string") {
      form.sparePartsIds = data.sparePartsIds
        .split(",")
        .map((s) => Number(String(s).trim()))
        .filter((v) => Number.isFinite(v));
  } else if (typeof data?.sparePartsIds === "number") {
    form.sparePartsIds = [data.sparePartsIds];
  } else {
    form.sparePartsIds = [];
  }
};
        .map(s => Number(String(s).trim()))
        .filter(v => Number.isFinite(v));
    } else if (typeof data?.sparePartsIds === "number") {
      form.sparePartsIds = [data.sparePartsIds];
    } else {
      form.sparePartsIds = [];
    }
    form.storageBlobDTOs = data.storageBlobVOs || [];
  };
/**
 * @desc ä¿å­˜ä¿å…»
 */
const sendForm = async () => {
  loading.value = true;
  try {
    // é¢†ç”¨æ•°é‡æ ¡éªŒ
    if (Array.isArray(form.sparePartsIds) && form.sparePartsIds.length > 0) {
      for (const partId of form.sparePartsIds) {
        const qty = Number(sparePartQtyMap.value?.[partId]);
        if (!Number.isFinite(qty) || qty <= 0) {
          proxy?.$modal?.msgError?.("请填写备件领用数量");
          return;
        }
        const part = sparePartOptions.value.find((p) => String(p.id) === String(partId));
        const stock = part?.quantity;
        if (stock !== null && stock !== undefined && Number.isFinite(Number(stock))) {
          if (qty > Number(stock)) {
            proxy?.$modal?.msgError?.(`备件「${part?.name || ""}」领用数量不能超过库存(${stock})`);
  /**
   * @desc ä¿å­˜ä¿å…»
   */
  const sendForm = async () => {
    loading.value = true;
    try {
      // é¢†ç”¨æ•°é‡æ ¡éªŒ
      if (Array.isArray(form.sparePartsIds) && form.sparePartsIds.length > 0) {
        for (const partId of form.sparePartsIds) {
          const qty = Number(sparePartQtyMap.value?.[partId]);
          if (!Number.isFinite(qty) || qty <= 0) {
            proxy?.$modal?.msgError?.("请填写备件领用数量");
            return;
          }
          const part = sparePartOptions.value.find(
            p => String(p.id) === String(partId)
          );
          const stock = part?.quantity;
          if (
            stock !== null &&
            stock !== undefined &&
            Number.isFinite(Number(stock))
          ) {
            if (qty > Number(stock)) {
              proxy?.$modal?.msgError?.(
                `备件「${part?.name || ""}」领用数量不能超过库存(${stock})`
              );
              return;
            }
          }
        }
      }
    }
    const data = {
      id: planId.value,
      ...form,
      sparePartsIds: form.sparePartsIds ? form.sparePartsIds.join(",") : "",
      sparePartsQty: form.sparePartsIds
          ? form.sparePartsIds.map((id) => sparePartQtyMap.value?.[id] ?? 1).join(",")
      const data = {
        id: planId.value,
        ...form,
        sparePartsIds: form.sparePartsIds ? form.sparePartsIds.join(",") : "",
        sparePartsQty: form.sparePartsIds
          ? form.sparePartsIds
              .map(id => sparePartQtyMap.value?.[id] ?? 1)
              .join(",")
          : "",
      sparePartsUseList: form.sparePartsIds
          ? form.sparePartsIds.map((id) => ({ id, quantity: sparePartQtyMap.value?.[id] ?? 1 }))
        sparePartsUseList: form.sparePartsIds
          ? form.sparePartsIds.map(id => ({
              id,
              quantity: sparePartQtyMap.value?.[id] ?? 1,
            }))
          : [],
      };
      const { code } = await addMaintenance(data);
      if (code == 200) {
        ElMessage.success("保养成功");
        emits("ok");
        resetForm();
        sparePartQtyMap.value = {};
        visible.value = false;
      }
    } finally {
      loading.value = false;
    }
    const { code } = await addMaintenance(data);
    if (code == 200) {
      ElMessage.success("保养成功");
      emits("ok");
      resetForm();
      sparePartQtyMap.value = {};
      visible.value = false;
    }
  } finally {
    loading.value = false;
  }
};
  };
const fetchSparePartOptions = () => {
  loadingSparePartOptions.value = true;
  // å’Œå¤‡ä»¶ç®¡ç†é¡µä¸€è‡´ï¼š/spareParts/listPage â†’ res.data.records
  getSparePartsList({ current: 1, size: 1000 })
      .then((res) => {
  const fetchSparePartOptions = () => {
    loadingSparePartOptions.value = true;
    // å’Œå¤‡ä»¶ç®¡ç†é¡µä¸€è‡´ï¼š/spareParts/listPage â†’ res.data.records
    getSparePartsList({ current: 1, size: 1000 })
      .then(res => {
        if (res.code === 200) {
          sparePartOptions.value = res?.data?.records || [];
        } else {
@@ -206,31 +223,31 @@
      .finally(() => {
        loadingSparePartOptions.value = false;
      });
}
  };
const handleCancel = () => {
  resetForm();
  sparePartQtyMap.value = {};
  visible.value = false;
};
  const handleCancel = () => {
    resetForm();
    sparePartQtyMap.value = {};
    visible.value = false;
  };
const handleClose = () => {
  resetForm();
  sparePartQtyMap.value = {};
  visible.value = false;
};
  const handleClose = () => {
    resetForm();
    sparePartQtyMap.value = {};
    visible.value = false;
  };
const open = async (id, row) => {
  planId.value = id; // ä¿å­˜è®¡åˆ’保养记录的id
  visible.value = true;
  await nextTick();
  fetchSparePartOptions()
  setForm(row);
};
  const open = async (id, row) => {
    planId.value = id; // ä¿å­˜è®¡åˆ’保养记录的id
    visible.value = true;
    await nextTick();
    fetchSparePartOptions();
    setForm(row);
  };
defineExpose({
  open,
});
  defineExpose({
    open,
  });
</script>
<style lang="scss" scoped></style>
src/views/equipmentManagement/upkeep/Form/PlanModal.vue
@@ -32,10 +32,10 @@
          disabled
        />
      </el-form-item>
      <el-form-item label="项目">
      <el-form-item label="保养项目">
        <el-input
            v-model="form.machineryCategory"
            placeholder="请输入项目"
            placeholder="请输入保养项目"
        />
      </el-form-item>
      <el-form-item label="录入人">
@@ -61,6 +61,13 @@
          <el-option label="完结" :value="1"></el-option>
          <el-option label="失败" :value="2"></el-option>
        </el-select>
      </el-form-item>
      <el-form-item label="保养人">
        <el-input
          v-model="form.maintenancePerson"
          placeholder="请输入保养人姓名"
          clearable
        />
      </el-form-item>
      <el-form-item label="计划保养日期">
        <el-date-picker
@@ -124,6 +131,7 @@
  status: 0, //保修状态
  machineryCategory: undefined,
  storageBlobDTOs: [],
  maintenancePerson: undefined, // ä¿å…»äºº
});
const setDeviceModel = (deviceId) => {
@@ -142,6 +150,7 @@
  form.createUser = Number(data.createUser);
  form.status = data.status;
  form.machineryCategory = data.machineryCategory;
  form.maintenancePerson = data.maintenancePerson;
  if (data.maintenancePlanTime) {
    form.maintenancePlanTime = dayjs(data.maintenancePlanTime).format(
      "YYYY-MM-DD HH:mm:ss"
src/views/equipmentManagement/upkeep/Form/formDia.vue
@@ -67,10 +67,20 @@
            </el-row>
            <el-row>
                <el-col :span="12">
                    <el-form-item label="设备项目" prop="machineryCategory">
                    <el-form-item label="保养项目" prop="machineryCategory">
                        <el-input
                            v-model.trim="form.machineryCategory"
                            placeholder="请输入设备项目"
                            placeholder="请输入保养项目"
                            maxlength="100"
                            clearable
                        />
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="保养人" prop="maintenancePerson">
                        <el-input
                            v-model.trim="form.maintenancePerson"
                            placeholder="请输入保养人姓名"
                            maxlength="100"
                            clearable
                        />
@@ -173,13 +183,14 @@
        week: '',
        time: '',
        deviceModel: undefined, // è§„格型号
        registrationDate: ''
        registrationDate: '',
        maintenancePerson: '' // ä¿å…»äºº
    },
    rules: {
        taskId: [{ required: true, message: "请选择设备", trigger: "change" },],
        inspector: [{ required: true, message: "请选择录入人", trigger: "blur" },],
        registrationDate: [{ required: true, message: "请选择登记时间", trigger: "change" }],
        machineryCategory: [{ required: true, message: "请输入设备项目", trigger: "blur" }]
        machineryCategory: [{ required: true, message: "请输入保养项目", trigger: "blur" }]
    }
})
const { form, rules } = toRefs(data)
@@ -259,7 +270,8 @@
        week: '',
        time: '',
        deviceModel: undefined,
        registrationDate: ''
        registrationDate: '',
        maintenancePerson: ''
    }
}
src/views/equipmentManagement/upkeep/index.vue
@@ -2,8 +2,8 @@
  <div class="app-container">
    <el-tabs v-model="activeTab"
             @tab-change="handleTabChange">
      <!-- å®šæ—¶ä»»åŠ¡ç®¡ç†tab -->
      <el-tab-pane label="定时任务管理"
      <!-- ä¿å…»ä»»åŠ¡tab -->
      <el-tab-pane label="保养任务"
                   name="scheduled">
        <div class="search_form">
          <el-form :model="scheduledFilters"
@@ -37,7 +37,7 @@
        <div class="table_list">
          <div class="actions">
            <el-text class="mx-1"
                     size="large">定时任务管理</el-text>
                     size="large">保养任务</el-text>
            <div>
              <el-button type="primary"
                         icon="Plus"
@@ -84,8 +84,8 @@
          </PIMTable>
        </div>
      </el-tab-pane>
      <!-- ä»»åŠ¡è®°å½•tab(原设备保养页面) -->
      <el-tab-pane label="任务记录"
      <!-- ä¿å…»è®°å½•tab(原设备保养页面) -->
      <el-tab-pane label="保养记录"
                   name="record">
        <div class="search_form">
          <el-form :model="filters"
@@ -130,7 +130,7 @@
        <div class="table_list">
          <div class="actions">
            <el-text class="mx-1"
                     size="large">任务记录</el-text>
                     size="large">保养记录</el-text>
            <div>
              <el-button type="success"
                         icon="Van"
@@ -262,7 +262,7 @@
  const fileDialogVisible = ref(false);
  const currentMaintenanceTaskId = ref(null);
  // ä»»åŠ¡è®°å½•tab(原设备保养页面)相关变量
  // ä¿å…»è®°å½•tab(原设备保养页面)相关变量
  const filters = reactive({
    deviceName: "",
    maintenancePlanTime: "",
@@ -278,7 +278,7 @@
  });
  const multipleList = ref([]);
  // å®šæ—¶ä»»åŠ¡ç®¡ç†tab相关变量
  // ä¿å…»ä»»åŠ¡tab相关变量
  const scheduledFilters = reactive({
    taskName: "",
    status: "",
@@ -292,7 +292,7 @@
  });
  const scheduledMultipleList = ref([]);
  // å®šæ—¶ä»»åŠ¡ç®¡ç†è¡¨æ ¼åˆ—é…ç½®
  // ä¿å…»ä»»åŠ¡è¡¨æ ¼åˆ—é…ç½®
  const scheduledColumns = ref([
    { prop: "taskName", label: "设备名称" },
    {
@@ -300,7 +300,7 @@
      prop: "deviceModel",
    },
    {
      label: "设备项目",
      label: "保养项目",
      prop: "machineryCategory",
      minWidth: 120,
      formatData: cell => cell || "--",
@@ -342,6 +342,7 @@
        );
      },
    },
    { prop: "maintenancePerson", label: "保养人", minWidth: 100 },
    { prop: "registrant", label: "登记人", minWidth: 100 },
    { prop: "registrationDate", label: "登记日期", minWidth: 100 },
    {
@@ -354,7 +355,7 @@
    },
  ]);
  // ä»»åŠ¡è®°å½•è¡¨æ ¼åˆ—é…ç½®ï¼ˆåŽŸè®¾å¤‡ä¿å…»è¡¨æ ¼åˆ—ï¼‰
  // ä¿å…»è®°å½•表格列配置(原设备保养表格列)
  const columns = ref([
    {
      label: "设备名称",
@@ -378,7 +379,7 @@
      prop: "createUserName",
    },
    {
      label: "设备项目",
      label: "保养项目",
      align: "center",
      prop: "machineryCategory",
      formatData: cell => cell || "--",
@@ -435,7 +436,7 @@
    }
  };
  // å®šæ—¶ä»»åŠ¡ç®¡ç†ç›¸å…³æ–¹æ³•
  // ä¿å…»ä»»åŠ¡ç›¸å…³æ–¹æ³•
  const getScheduledTableData = async () => {
    try {
      const params = {
@@ -502,7 +503,7 @@
    ElMessage.info("导出定时任务功能待实现");
  };
  // ä»»åŠ¡è®°å½•ç›¸å…³æ–¹æ³•ï¼ˆåŽŸè®¾å¤‡ä¿å…»é¡µé¢æ–¹æ³•ï¼‰
  // ä¿å…»è®°å½•相关方法(原设备保养页面方法)
  const getTableData = async () => {
    try {
      const params = {
src/views/financialManagement/assets/fixedAssets.vue
@@ -38,7 +38,7 @@
        <div>
          <el-button type="primary" @click="add" icon="Plus">新增资产</el-button>
          <el-button type="warning" @click="handleDepreciation" icon="Money">折旧计提</el-button>
          <el-button @click="handleOut" icon="Download">导出</el-button>
          <!-- <el-button @click="handleOut" icon="Download">导出</el-button> -->
        </div>
      </div>
      <PIMTable
src/views/financialManagement/assets/intangibleAssets.vue
@@ -39,7 +39,7 @@
        <div>
          <el-button type="primary" @click="add" icon="Plus">新增资产</el-button>
          <el-button type="warning" @click="handleAmortization" icon="Money">摊销计提</el-button>
          <el-button @click="handleOut" icon="Download">导出</el-button>
          <!-- <el-button @click="handleOut" icon="Download">导出</el-button> -->
        </div>
      </div>
      <PIMTable
src/views/financialManagement/generalLedger/index.vue
@@ -44,8 +44,8 @@
          <el-button type="primary"
                     @click="add"
                     icon="Plus">新增</el-button>
          <el-button @click="handleOut"
                     icon="Download">导出</el-button>
          <!-- <el-button @click="handleOut"
                     icon="Download">导出</el-button> -->
        </div>
      </div>
      <el-table ref="tableRef"
src/views/financialManagement/voucher/detailLedger.vue
@@ -32,7 +32,7 @@
          <el-form-item>
            <el-button type="primary" @click="getTableData">查询</el-button>
            <el-button @click="resetFilters">重置</el-button>
            <el-button @click="handlePrint" icon="Printer">打印</el-button>
<!--            <el-button @click="handlePrint" icon="Printer">打印</el-button>-->
            <el-button @click="handleOut" icon="Download">导出</el-button>
          </el-form-item>
        </el-form>
src/views/financialManagement/voucher/generalLedger.vue
@@ -32,34 +32,34 @@
          <el-form-item>
            <el-button type="primary" @click="getTableData">查询</el-button>
            <el-button @click="resetFilters">重置</el-button>
            <el-button @click="handlePrint" icon="Printer">打印</el-button>
            <el-button @click="handleOut" icon="Download">导出</el-button>
<!--            <el-button @click="handlePrint" icon="Printer">打印</el-button>-->
            <!-- <el-button @click="handleOut" icon="Download">导出</el-button> -->
          </el-form-item>
        </el-form>
        <div class="table_list">
          <el-table :data="dataList" border style="width: 100%">
            <el-table-column prop="date" label="日期" width="120" />
            <el-table-column prop="voucherNo" label="凭证字号" width="120" />
            <el-table-column prop="summary" label="摘要" min-width="200" show-overflow-tooltip />
            <el-table-column prop="debit" label="借方" width="150">
            <el-table-column prop="date" label="日期"/>
            <!-- <el-table-column prop="voucherNo" label="凭证字号" width="120" /> -->
            <!-- <el-table-column prop="summary" label="摘要" min-width="200" show-overflow-tooltip /> -->
            <el-table-column prop="debit" label="借方">
              <template #default="{ row }">
                <span v-if="row.debit > 0" class="text-danger">Â¥{{ formatMoney(row.debit) }}</span>
                <span v-else>-</span>
              </template>
            </el-table-column>
            <el-table-column prop="credit" label="贷方" width="150">
            <el-table-column prop="credit" label="贷方">
              <template #default="{ row }">
                <span v-if="row.credit > 0" class="text-success">Â¥{{ formatMoney(row.credit) }}</span>
                <span v-else>-</span>
              </template>
            </el-table-column>
            <el-table-column label="方向" width="80">
            <el-table-column label="方向">
              <template #default="{ row }">
                <el-tag :type="row.direction === '借' ? 'success' : 'danger'" size="small">{{ row.direction }}</el-tag>
              </template>
            </el-table-column>
            <el-table-column label="余额" width="150">
            <el-table-column label="余额">
              <template #default="{ row }">
                <span :class="row.balance >= 0 ? 'text-primary' : 'text-warning'">Â¥{{ formatMoney(Math.abs(row.balance)) }}</span>
              </template>
src/views/financialManagement/voucher/index.vue
@@ -32,13 +32,13 @@
    <div class="table_list">
      <div class="actions">
        <div>
          <el-statistic title="借方合计" :value="totalDebit" precision="2" prefix="Â¥" />
          <el-statistic title="贷方合计" :value="totalCredit" precision="2" prefix="Â¥" style="margin-left: 30px;" />
          <el-statistic title="借方合计" :value="totalDebit" :precision="2" prefix="Â¥" />
          <el-statistic title="贷方合计" :value="totalCredit" :precision="2" prefix="Â¥" style="margin-left: 30px;" />
        </div>
        <div>
          <el-button type="primary" @click="add" icon="Plus">新增凭证</el-button>
          <el-button @click="handleImport" icon="Upload">导入</el-button>
          <el-button @click="handleOut" icon="Download">导出</el-button>
          <!-- <el-button @click="handleImport" icon="Upload">导入</el-button> -->
          <!-- <el-button @click="handleOut" icon="Download">导出</el-button> -->
        </div>
      </div>
      <PIMTable
@@ -84,6 +84,11 @@
              <span class="label">凭证字:</span>
              <el-select v-model="form.voucherPrefix" :disabled="isViewMode" style="width: 70px;">
                <el-option label="è®°" value="è®°" />
                <el-option label="现" value="现" />
                <el-option label="银" value="银" />
                <el-option label="转" value="转" />
                <el-option label="收" value="收" />
                <el-option label="付" value="付" />
              </el-select>
              <el-input v-model="form.voucherNum" :disabled="isViewMode" style="width: 60px;" />
              <span class="label" style="margin-left: 5px;">号</span>
@@ -96,7 +101,6 @@
              <span class="label">附件:</span>
              <el-input-number v-model="form.attachmentCount" :disabled="isViewMode" :min="0" :controls="false" style="width: 60px;" />
              <span class="label" style="margin-left: 5px;">å¼ </span>
              <el-button type="primary" link :disabled="isViewMode" style="margin-left: 10px;">上传文件</el-button>
            </div>
          </div>
          <div class="voucher-table">
@@ -153,12 +157,12 @@
                      @change="(val) => handleSubjectChange(val, rowIndex)"
                      @focus="selectRow(rowIndex)"
                    />
                    <div class="subject-name">{{ entry.subjectName }}</div>
                    <!-- <div class="subject-name">{{ entry.subjectName }}</div> -->
                  </td>
                  <!-- å€Ÿæ–¹11列 -->
                  <template v-if="editingCell.row === rowIndex && editingCell.type === 'debit'">
                    <td colspan="11" class="debit-input-cell">
                      <el-input-number ref="amountInputRef" v-model="entry.debit" :disabled="isViewMode" :min="0" :precision="2" :controls="false" size="small" @blur="finishEdit" class="full-width-input" />
                      <el-input-number ref="amountInputRef" v-model="entry.debit" :disabled="isViewMode" :min="0" :precision="2" :controls="false" :value-on-clear="undefined" size="small" @blur="finishEdit" class="full-width-input" />
                    </td>
                  </template>
                  <template v-else>
@@ -169,7 +173,7 @@
                  <!-- è´·æ–¹11列 -->
                  <template v-if="editingCell.row === rowIndex && editingCell.type === 'credit'">
                    <td colspan="11" class="credit-input-cell">
                      <el-input-number ref="amountInputRef" v-model="entry.credit" :disabled="isViewMode" :min="0" :precision="2" :controls="false" size="small" @blur="finishEdit" class="full-width-input" />
                      <el-input-number ref="amountInputRef" v-model="entry.credit" :disabled="isViewMode" :min="0" :precision="2" :controls="false" :value-on-clear="undefined" size="small" @blur="finishEdit" class="full-width-input" />
                    </td>
                  </template>
                  <template v-else>
@@ -217,7 +221,36 @@
              </el-select>
            </div>
          </div>
          <!-- ç¼–辑模式:使用 AttachmentUploadFile ä¸Šä¼ ç»„ä»¶ -->
          <div class="voucher-attachment-upload" v-if="!isViewMode">
            <div class="attachment-label">附件上传:</div>
            <AttachmentUploadFile
              v-model:fileList="form.attachments"
              :disabled="isViewMode"
              :limit="10"
              :fileSize="50"
              buttonText="点击上传附件"
              @change="handleAttachmentChange"
            />
          </div>
        </el-form>
        <!-- æŸ¥çœ‹æ¨¡å¼ï¼šå±•示附件列表(放在 el-form å¤–面,避免被 disabled) -->
        <div class="voucher-attachment-upload" v-if="isViewMode && form.attachments?.length">
          <div class="attachment-label">附件列表:</div>
          <el-table :data="form.attachments" border class="attachment-table">
            <el-table-column label="附件名称" show-overflow-tooltip>
              <template #default="scope">
                {{ scope.row.originalFilename || scope.row.name || scope.row.fileName || '未命名文件' }}
              </template>
            </el-table-column>
            <el-table-column fixed="right" label="操作" width="150" align="center">
              <template #default="scope">
                <el-button link type="primary" size="small" @click="previewFile(scope.row)">预览</el-button>
                <el-button link type="primary" size="small" @click="downloadFile(scope.row)">下载</el-button>
              </template>
            </el-table-column>
          </el-table>
        </div>
      </div>
      <template #footer>
        <div>
@@ -226,6 +259,8 @@
        </div>
      </template>
    </FormDialog>
    <!-- æ–‡ä»¶é¢„览组件 -->
    <FilePreview ref="filePreviewRef" />
  </div>
</template>
@@ -233,6 +268,10 @@
import { ref, reactive, onMounted, computed, nextTick } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import FormDialog from "@/components/Dialog/FormDialog.vue";
import AttachmentUploadFile from "@/components/AttachmentUpload/file/index.vue";
import FileList from "@/components/Dialog/FileList.vue";
import FilePreview from "@/components/filePreview/index.vue";
import download from "@/plugins/download.js";
import useUserStore from "@/store/modules/user";
import { userListNoPageByTenantId } from "@/api/system/user";
import { listAccountSubject } from "@/api/financialManagement/accountSubject";
@@ -284,6 +323,7 @@
const isEdit = ref(false);
const currentId = ref(null);
const isViewMode = computed(() => dialogMode.value === "view");
const filePreviewRef = ref(null);
const fallbackSubjectTree = [
  { subjectCode: "1001", subjectName: "库存现金", balanceDirection: "借方", children: [] },
@@ -326,8 +366,8 @@
  subjectName: "",
  balanceDirection: "",
  summary: "",
  debit: 0,
  credit: 0,
  debit: undefined,
  credit: undefined,
});
const createDefaultForm = () => ({
@@ -336,6 +376,7 @@
  voucherNum: "",
  voucherDate: "",
  attachmentCount: 0,
  attachments: [],
  entries: [createEmptyEntry(), createEmptyEntry()],
  creator: getDefaultCreator(),
  remark: "",
@@ -490,6 +531,31 @@
  form.entries.push(createEmptyEntry());
};
const handleAttachmentChange = (fileList) => {
  form.attachmentCount = fileList?.length || 0;
};
// ä½¿ç”¨é¡¹ç›®å°è£…çš„ filePreview ç»„件预览文件
const previewFile = (row) => {
  const url = row.previewURL || row.previewUrl || row.url;
  if (url && filePreviewRef.value) {
    filePreviewRef.value.open(url);
  } else {
    ElMessage.warning('文件地址无效,无法预览');
  }
};
// ä½¿ç”¨é¡¹ç›®å°è£…çš„ download æ’件下载文件
const downloadFile = (row) => {
  const url = row.downloadURL || row.downloadUrl || row.url;
  if (url) {
    const filename = row.originalFilename || row.name || row.fileName || 'download';
    download.byUrl(url, filename);
  } else {
    ElMessage.warning('文件地址无效,无法下载');
  }
};
const selectRow = (index) => {
  selectedRowIndex.value = index;
};
@@ -589,10 +655,13 @@
    const { data } = await getVoucherDetail(row.id);
    const detail = data || row;
    const parts = (detail.voucherNo || "").split("-");
    Object.assign(form, createDefaultForm(), detail, {
    const attachments = detail.storageBlobVOList || detail.storageBlobDTOs || detail.attachments || [];
    Object.assign(form, createDefaultForm(), {
      ...detail,
      voucherPrefix: parts[0] || "è®°",
      voucherNum: parts[1] || "",
      creator: detail.creator || getDefaultCreator(),
      attachments,
      entries:
        detail.entries?.map(item => ({
          subjectCode: item.subjectCode || "",
@@ -696,6 +765,7 @@
        remark: form.remark,
        debit: totalDebitEntry.value,
        credit: totalCreditEntry.value,
        storageBlobDTOs: form.attachments || [],
        entries: validEntries.map(entry => ({
          subjectCode: entry.subjectCode,
          subjectName: entry.subjectName,
@@ -801,6 +871,21 @@
  }
}
.voucher-attachment-upload {
  margin-top: 15px;
  padding: 0 10px;
  .attachment-label {
    font-size: 14px;
    color: #606266;
    margin-bottom: 10px;
  }
  .attachment-table {
    border-radius: 4px;
  }
}
.voucher-table {
  border: 1px solid #dcdfe6;
  border-right: none;
src/views/inventoryManagement/stockManagement/BatchNoQtyDetail.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,227 @@
<template>
  <el-dialog
    v-model="isShow"
    title="库存详情"
    width="90%"
    top="3vh"
    class="batch-no-qty-detail-dialog"
    @close="closeModal"
  >
    <div class="detail-content">
      <div class="detail-table-wrapper">
        <el-table
          :data="tableData"
          border
          v-loading="tableLoading"
          style="width: 100%"
          height="100%"
        >
          <el-table-column
            label="产品名称"
            prop="productName"
            show-overflow-tooltip
          />
          <el-table-column label="规格型号" prop="model" show-overflow-tooltip />
          <el-table-column label="单位" prop="unit" show-overflow-tooltip />
          <el-table-column label="批号" prop="batchNo" show-overflow-tooltip />
          <el-table-column
            label="合格库存数量"
            prop="qualifiedQuantity"
            show-overflow-tooltip
          />
          <el-table-column
            label="不合格库存数量"
            prop="unQualifiedQuantity"
            show-overflow-tooltip
          />
          <el-table-column
            label="合格冻结数量"
            prop="qualifiedLockedQuantity"
            show-overflow-tooltip
          />
          <el-table-column
            label="不合格冻结数量"
            prop="unQualifiedLockedQuantity"
            show-overflow-tooltip
          />
          <el-table-column
            label="库存预警数量"
            prop="warnNum"
            show-overflow-tooltip
          />
          <el-table-column label="备注" prop="remark" show-overflow-tooltip />
          <el-table-column
            label="最近更新时间"
            prop="updateTime"
            show-overflow-tooltip
          />
          <el-table-column fixed="right" label="操作" min-width="180" align="center">
            <template #default="scope">
              <el-button
                link
                type="primary"
                @click="handleSubtract(scope.row)"
                :disabled="
                  (scope.row.qualifiedUnLockedQuantity || 0) +
                    (scope.row.qualifiedPendingOutQuantity || 0) <=
                    0 &&
                  (scope.row.unQualifiedUnLockedQuantity || 0) +
                    (scope.row.unQualifiedPendingOutQuantity || 0) <=
                    0
                "
                >领用</el-button
              >
              <el-button
                link
                type="primary"
                v-if="
                  scope.row.unQualifiedUnLockedQuantity > 0 ||
                  scope.row.qualifiedUnLockedQuantity > 0
                "
                @click="handleFrozen(scope.row)"
                >冻结</el-button
              >
              <el-button
                link
                type="primary"
                v-if="
                  scope.row.qualifiedLockedQuantity > 0 ||
                  scope.row.unQualifiedLockedQuantity > 0
                "
                @click="handleThaw(scope.row)"
                >解冻</el-button
              >
            </template>
          </el-table-column>
        </el-table>
      </div>
      <pagination
        v-show="total > 0"
        :total="total"
        layout="total, sizes, prev, pager, next, jumper"
        :page="page.current"
        :limit="page.size"
        @pagination="paginationChange"
      />
    </div>
  </el-dialog>
</template>
<script setup>
import pagination from "@/components/PIMTable/Pagination.vue";
import { computed, reactive, ref, watch } from "vue";
import { getStockInventoryBatchNoQty } from "@/api/inventoryManagement/stockInventory.js";
const props = defineProps({
  visible: {
    type: Boolean,
    required: true,
  },
  record: {
    type: Object,
    default: () => ({}),
  },
});
const emit = defineEmits(["update:visible", "subtract", "frozen", "thaw"]);
const isShow = computed({
  get() {
    return props.visible;
  },
  set(val) {
    emit("update:visible", val);
  },
});
const tableData = ref([]);
const tableLoading = ref(false);
const total = ref(0);
const page = reactive({
  current: 1,
  size: 20,
});
const getList = () => {
  if (!props.record?.productId || !props.record?.productModelId) {
    tableData.value = [];
    total.value = 0;
    return;
  }
  tableLoading.value = true;
  getStockInventoryBatchNoQty({
    current: page.current,
    size: page.size,
    productId: props.record.productId,
    productModelId: props.record.productModelId,
  })
    .then((res) => {
      tableData.value = res.data?.records || [];
      total.value = res.data?.total || 0;
    })
    .finally(() => {
      tableLoading.value = false;
    });
};
const paginationChange = (obj) => {
  page.current = obj.page;
  page.size = obj.limit;
  getList();
};
const handleSubtract = (row) => {
  emit("subtract", row);
};
const handleFrozen = (row) => {
  emit("frozen", row);
};
const handleThaw = (row) => {
  emit("thaw", row);
};
const closeModal = () => {
  isShow.value = false;
  page.current = 1;
  page.size = 20;
  tableData.value = [];
  total.value = 0;
};
watch(
  () => props.visible,
  (visible) => {
    if (!visible) {
      return;
    }
    page.current = 1;
    getList();
  },
  { immediate: true }
);
</script>
<style scoped lang="scss">
.detail-content {
  display: flex;
  flex-direction: column;
  height: calc(100vh - 170px);
  min-height: 520px;
}
.detail-table-wrapper {
  flex: 1;
  min-height: 0;
}
:deep(.batch-no-qty-detail-dialog .el-dialog) {
  max-width: calc(100vw - 48px);
}
:deep(.batch-no-qty-detail-dialog .el-dialog__body) {
  padding-top: 12px;
}
</style>
src/views/inventoryManagement/stockManagement/Record.vue
@@ -3,143 +3,233 @@
    <div class="search_form mb10">
      <div>
        <span class="search_title ml10">产品大类:</span>
        <el-input v-model="searchForm.productName"
                  style="width: 240px"
                  placeholder="请输入"
                  clearable/>
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px">搜索</el-button>
        <el-input
          v-model="searchForm.productName"
          style="width: 240px"
          placeholder="请输入"
          clearable
        />
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px"
          >搜索</el-button
        >
      </div>
      <div>
         <el-button type="primary" @click="isShowNewModal = true">新增库存</el-button>
        <el-button type="info" plain icon="Upload" @click="isShowImportModal = true">
        <el-button type="primary" @click="isShowNewModal = true"
          >新增库存</el-button
        >
        <el-button
          type="info"
          plain
          icon="Upload"
          @click="isShowImportModal = true"
        >
          å¯¼å…¥åº“å­˜
        </el-button>
        <el-button @click="handleOut">导出</el-button>
      </div>
    </div>
    <div class="table_list">
      <el-table :data="tableData" border v-loading="tableLoading" @selection-change="handleSelectionChange"
        :expand-row-keys="expandedRowKeys" :row-key="(row, index) => index" style="width: 100%"
        :row-class-name="tableRowClassName" height="calc(100vh - 18.5em)">
      <el-table
        :data="tableData"
        border
        v-loading="tableLoading"
        @selection-change="handleSelectionChange"
        :expand-row-keys="expandedRowKeys"
        :row-key="(row, index) => index"
        style="width: 100%"
        :row-class-name="tableRowClassName"
        height="calc(100vh - 18.5em)"
      >
        <el-table-column align="center" type="selection" width="55" />
        <el-table-column align="center" label="序号" type="index" width="60" />
        <el-table-column label="产品大类" prop="productName" show-overflow-tooltip />
        <el-table-column
          label="产品名称"
          prop="productName"
          show-overflow-tooltip
        />
        <el-table-column label="规格型号" prop="model" show-overflow-tooltip />
        <el-table-column label="单位" prop="unit" show-overflow-tooltip />
        <el-table-column label="批号" prop="batchNo" show-overflow-tooltip />
        <el-table-column label="合格库存数量" prop="qualifiedQuantity" show-overflow-tooltip />
        <el-table-column label="不合格库存数量" prop="unQualifiedQuantity" show-overflow-tooltip />
        <el-table-column label="合格冻结数量" prop="qualifiedLockedQuantity" show-overflow-tooltip />
        <el-table-column label="不合格冻结数量" prop="unQualifiedLockedQuantity" show-overflow-tooltip />
        <el-table-column label="库存预警数量" prop="warnNum"  show-overflow-tooltip />
        <el-table-column label="备注" prop="remark"  show-overflow-tooltip />
        <el-table-column label="最近更新时间" prop="updateTime" show-overflow-tooltip />
        <el-table-column fixed="right" label="操作" min-width="90" align="center">
        <el-table-column
          label="合格库存数量"
          prop="qualifiedQuantity"
          show-overflow-tooltip
        />
        <el-table-column
          label="不合格库存数量"
          prop="unQualifiedQuantity"
          show-overflow-tooltip
        />
        <el-table-column
          label="合格冻结数量"
          prop="qualifiedLockedQuantity"
          show-overflow-tooltip
        />
        <el-table-column
          label="不合格冻结数量"
          prop="unQualifiedLockedQuantity"
          show-overflow-tooltip
        />
        <el-table-column
          label="库存预警数量"
          prop="warnNum"
          show-overflow-tooltip
        />
        <el-table-column label="备注" prop="remark" show-overflow-tooltip />
        <el-table-column
          label="最近更新时间"
          prop="updateTime"
          show-overflow-tooltip
        />
        <el-table-column
          fixed="right"
          label="操作"
          min-width="80"
          align="center"
        >
          <template #default="scope">
            <el-button link type="primary" @click="showSubtractModal(scope.row)" :disabled="((scope.row.qualifiedUnLockedQuantity || 0) + (scope.row.qualifiedPendingOutQuantity || 0) <= 0) && ((scope.row.unQualifiedUnLockedQuantity || 0) + (scope.row.unQualifiedPendingOutQuantity || 0) <= 0)">领用</el-button>
            <el-button link type="primary" v-if="scope.row.unQualifiedUnLockedQuantity > 0 || scope.row.qualifiedUnLockedQuantity > 0" @click="showFrozenModal(scope.row)">冻结</el-button>
            <el-button link type="primary" v-if="scope.row.qualifiedLockedQuantity > 0 || scope.row.unQualifiedLockedQuantity > 0" @click="showThawModal(scope.row)">解冻</el-button>
            <el-button
              link
              type="primary"
              @click="showDetailModal(scope.row)"
              >详情</el-button
            >
          </template>
        </el-table-column>
      </el-table>
      <pagination v-show="total > 0" :total="total" layout="total, sizes, prev, pager, next, jumper"
        :page="page.current" :limit="page.size" @pagination="paginationChange" />
      <pagination
        v-show="total > 0"
        :total="total"
        layout="total, sizes, prev, pager, next, jumper"
        :page="page.current"
        :limit="page.size"
        @pagination="paginationChange"
      />
    </div>
    <new-stock-inventory v-if="isShowNewModal"
                 v-model:visible="isShowNewModal"
                 :top-product-parent-id="props.productId"
                 @completed="handleQuery" />
    <batch-no-qty-detail
      v-if="isShowDetailModal"
      v-model:visible="isShowDetailModal"
      :record="record"
      @subtract="handleDetailSubtract"
      @frozen="handleDetailFrozen"
      @thaw="handleDetailThaw"
    />
    <new-stock-inventory
      v-if="isShowNewModal"
      v-model:visible="isShowNewModal"
      :top-product-parent-id="props.productId"
      @completed="handleQuery"
    />
    <subtract-stock-inventory v-if="isShowSubtractModal"
                 v-model:visible="isShowSubtractModal"
                 :record="record"
                 :type="record.stockType"
                 @completed="handleQuery" />
    <subtract-stock-inventory
      v-if="isShowSubtractModal"
      v-model:visible="isShowSubtractModal"
      :record="record"
      :type="record.stockType"
      @completed="handleQuery"
    />
    <!-- å¯¼å…¥åº“å­˜-->
    <import-stock-inventory v-if="isShowImportModal"
                 v-model:visible="isShowImportModal"
                 type="qualified"
                 @uploadSuccess="handleQuery" />
    <import-stock-inventory
      v-if="isShowImportModal"
      v-model:visible="isShowImportModal"
      type="qualified"
      @uploadSuccess="handleQuery"
    />
    <!-- å†»ç»“/解冻库存-->
    <frozen-and-thaw-stock-inventory v-if="isShowFrozenAndThawModal"
                 v-model:visible="isShowFrozenAndThawModal"
                 :record="record"
                 :operation-type="operationType"
                 :type="record.stockType"
                 @completed="handleQuery" />
    <frozen-and-thaw-stock-inventory
      v-if="isShowFrozenAndThawModal"
      v-model:visible="isShowFrozenAndThawModal"
      :record="record"
      :operation-type="operationType"
      :type="record.stockType"
      @completed="handleQuery"
    />
  </div>
</template>
<script setup>
import pagination from '@/components/PIMTable/Pagination.vue'
import { ref, reactive, toRefs, onMounted, getCurrentInstance } from 'vue'
import {ElMessage, ElMessageBox} from "element-plus";
import {
  getStockInventoryListPageCombined
} from "@/api/inventoryManagement/stockInventory.js";
import pagination from "@/components/PIMTable/Pagination.vue";
import { ref, reactive, toRefs, onMounted, getCurrentInstance } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { getStockInventoryListPageCombined } from "@/api/inventoryManagement/stockInventory.js";
const props = defineProps({
  productId: {
    type: Number,
    required: true,
    default: 0
  }
    default: 0,
  },
});
const NewStockInventory = defineAsyncComponent(() => import("@/views/inventoryManagement/stockManagement/New.vue"));
const SubtractStockInventory = defineAsyncComponent(() => import("@/views/inventoryManagement/stockManagement/Subtract.vue"));
const ImportStockInventory = defineAsyncComponent(() => import("@/views/inventoryManagement/stockManagement/Import.vue"));
const FrozenAndThawStockInventory = defineAsyncComponent(() => import("@/views/inventoryManagement/stockManagement/FrozenAndThaw.vue"));
const { proxy } = getCurrentInstance()
const tableData = ref([])
const selectedRows = ref([])
const record = ref({})
const tableLoading = ref(false)
const NewStockInventory = defineAsyncComponent(() =>
  import("@/views/inventoryManagement/stockManagement/New.vue")
);
const SubtractStockInventory = defineAsyncComponent(() =>
  import("@/views/inventoryManagement/stockManagement/Subtract.vue")
);
const ImportStockInventory = defineAsyncComponent(() =>
  import("@/views/inventoryManagement/stockManagement/Import.vue")
);
const FrozenAndThawStockInventory = defineAsyncComponent(() =>
  import("@/views/inventoryManagement/stockManagement/FrozenAndThaw.vue")
);
const BatchNoQtyDetail = defineAsyncComponent(() =>
  import("@/views/inventoryManagement/stockManagement/BatchNoQtyDetail.vue")
);
const { proxy } = getCurrentInstance();
const tableData = ref([]);
const selectedRows = ref([]);
const record = ref({});
const tableLoading = ref(false);
const page = reactive({
  current: 1,
  size: 100,
})
const total = ref(0)
});
const total = ref(0);
// æ˜¯å¦æ˜¾ç¤ºæ–°å¢žå¼¹æ¡†
const isShowNewModal = ref(false)
const isShowNewModal = ref(false);
// æ˜¯å¦æ˜¾ç¤ºé¢†ç”¨å¼¹æ¡†
const isShowSubtractModal = ref(false)
const isShowSubtractModal = ref(false);
// æ˜¯å¦æ˜¾ç¤ºå†»ç»“/解冻弹框
const isShowFrozenAndThawModal = ref(false)
const isShowFrozenAndThawModal = ref(false);
// æ˜¯å¦æ˜¾ç¤ºè¯¦æƒ…弹框
const isShowDetailModal = ref(false);
// æ“ä½œç±»åž‹
const operationType = ref('frozen')
const operationType = ref("frozen");
// æ˜¯å¦æ˜¾ç¤ºå¯¼å…¥å¼¹æ¡†
const isShowImportModal = ref(false)
const isShowImportModal = ref(false);
const data = reactive({
  searchForm: {
    productName: '',
    productName: "",
    topParentProductId: props.productId,
  }
})
const { searchForm } = toRefs(data)
  },
});
const { searchForm } = toRefs(data);
// æŸ¥è¯¢åˆ—表
/** æœç´¢æŒ‰é’®æ“ä½œ */
const handleQuery = () => {
  page.current = 1
  getList()
}
  page.current = 1;
  getList();
};
const paginationChange = (obj) => {
  page.current = obj.page;
  page.size = obj.limit;
  getList()
}
  getList();
};
const getList = () => {
  tableLoading.value = true
  getStockInventoryListPageCombined({ ...searchForm.value, ...page }).then(res => {
    tableLoading.value = false
    tableData.value = res.data.records
    total.value = res.data.total
    // æ•°æ®åŠ è½½å®ŒæˆåŽæ£€æŸ¥åº“å­˜
    // checkStockAndCreatePurchase();
  }).catch(() => {
    tableLoading.value = false
  })
}
  tableLoading.value = true;
  getStockInventoryListPageCombined({ ...searchForm.value, ...page })
    .then((res) => {
      tableLoading.value = false;
      tableData.value = res.data.records;
      total.value = res.data.total;
      // æ•°æ®åŠ è½½å®ŒæˆåŽæ£€æŸ¥åº“å­˜
      // checkStockAndCreatePurchase();
    })
    .catch(() => {
      tableLoading.value = false;
    });
};
const handleFileSuccess = (response) => {
  const { code, msg } = response;
@@ -154,61 +244,89 @@
// ç‚¹å‡»é¢†ç”¨
const showSubtractModal = (row) => {
  record.value = row
  isShowSubtractModal.value = true
}
  record.value = row;
  isShowSubtractModal.value = true;
};
// ç‚¹å‡»è¯¦æƒ…
const showDetailModal = (row) => {
  if (!row?.productId || !row?.productModelId) {
    proxy.$modal.msgError("当前数据缺少产品ID或规格型号ID");
    return;
  }
  record.value = row;
  isShowDetailModal.value = true;
};
const handleDetailSubtract = (row) => {
  isShowDetailModal.value = false;
  showSubtractModal(row);
};
const handleDetailFrozen = (row) => {
  isShowDetailModal.value = false;
  showFrozenModal(row);
};
const handleDetailThaw = (row) => {
  isShowDetailModal.value = false;
  showThawModal(row);
};
// ç‚¹å‡»å†»ç»“
const showFrozenModal = (row) => {
  record.value = row
  isShowFrozenAndThawModal.value = true
  operationType.value = 'frozen'
}
  record.value = row;
  isShowFrozenAndThawModal.value = true;
  operationType.value = "frozen";
};
// ç‚¹å‡»è§£å†»
const showThawModal = (row) => {
  record.value = row
  isShowFrozenAndThawModal.value = true
  operationType.value = 'thaw'
}
  record.value = row;
  isShowFrozenAndThawModal.value = true;
  operationType.value = "thaw";
};
// è¡¨æ ¼é€‰æ‹©æ•°æ®
const handleSelectionChange = (selection) => {
  // è¿‡æ»¤æŽ‰å­æ•°æ®
  selectedRows.value = selection.filter(item => item.id);
  console.log('selection', selectedRows.value)
}
const expandedRowKeys = ref([])
  selectedRows.value = selection.filter((item) => item.id);
  console.log("selection", selectedRows.value);
};
const expandedRowKeys = ref([]);
// è¡¨æ ¼è¡Œç±»å
const tableRowClassName = ({ row }) => {
  const stock = Number(row?.qualifiedUnLockedQuantity ?? 0);
  const warn = Number(row?.warnNum ?? 0);
  if (!Number.isFinite(stock) || !Number.isFinite(warn)) {
    return '';
    return "";
  }
  return stock < warn ? 'row-low-stock' : '';
  return stock < warn ? "row-low-stock" : "";
};
// å¯¼å‡º
const handleOut = () => {
  ElMessageBox.confirm(
    '是否确认导出?',
    '导出', {
    confirmButtonText: '确认',
    cancelButtonText: '取消',
    type: 'warning',
  }
  ).then(() => {
    proxy.download("/stockInventory/exportStockInventory", {topParentProductId: props.productId}, '库存信息.xlsx')
  }).catch(() => {
    proxy.$modal.msg("已取消")
  ElMessageBox.confirm("是否确认导出?", "导出", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
}
    .then(() => {
      proxy.download(
        "/stockInventory/exportStockInventory",
        { topParentProductId: props.productId },
        "库存信息.xlsx"
      );
    })
    .catch(() => {
      proxy.$modal.msg("已取消");
    });
};
onMounted(() => {
  getList()
})
  getList();
});
</script>
<style scoped lang="scss">
src/views/personnelManagement/contractManagement/index.vue
@@ -23,6 +23,12 @@
        :total="page.total"></PIMTable>
    </div>
    <form-dia ref="formDia" @close="handleQuery"></form-dia>
    <renew-contract
        v-if="isShowRenewContractModal"
        v-model:visible="isShowRenewContractModal"
        :id="id"
        @completed="handleQuery"
    />
    <!-- åˆåŒå¯¼å…¥å¯¹è¯æ¡† -->
    <el-dialog
@@ -71,8 +77,9 @@
<script setup>
import { Search } from "@element-plus/icons-vue";
import { onMounted, ref } from "vue";
import { onMounted, ref, defineAsyncComponent } from "vue";
import FormDia from "@/views/personnelManagement/contractManagement/components/formDia.vue";
const RenewContract = defineAsyncComponent(() => import("@/views/personnelManagement/employeeRecord/components/RenewContract.vue"));
import { ElMessageBox } from "element-plus";
import { staffOnJobListPage } from "@/api/personnelManagement/staffOnJob.js";
import dayjs from "dayjs";
@@ -183,7 +190,7 @@
    label: "操作",
    align: "center",
    fixed: 'right',
    width: 120,
    width: 160,
    operation: [
      {
        name: "详情",
@@ -191,11 +198,22 @@
        clickFun: (row) => {
          openForm("edit", row);
        },
      },
      {
        name: "续签合同",
        type: "text",
        showHide: row => row.staffState === 1,
        clickFun: (row) => {
          isShowRenewContractModal.value = true;
          id.value = row.id;
        },
      }
    ],
  },
]);
const filesDia = ref()
const isShowRenewContractModal = ref(false);
const id = ref(0);
const tableData = ref([]);
const selectedRows = ref([]);
const tableLoading = ref(false);
src/views/personnelManagement/employeeRecord/index.vue
@@ -52,16 +52,14 @@
          :tableLoading="tableLoading"
          @pagination="pagination"
          :total="page.total"
      ></PIMTable>
      >
        <template #positiveDate="{ row }">
          <span :class="getPositiveDateClass(row.positiveDate)">{{ row.positiveDate }}</span>
        </template>
      </PIMTable>
    </div>
    <show-form-dia ref="formDia" @close="handleQuery"></show-form-dia>
    <new-or-edit-form-dia ref="formDiaNewOrEditFormDia" @close="handleQuery"></new-or-edit-form-dia>
    <renew-contract
        v-if="isShowRenewContractModal"
        v-model:visible="isShowRenewContractModal"
        :id="id"
        @completed="handleQuery"
    />
    
    <!-- å¯¼å…¥å¯¹è¯æ¡† -->
    <el-dialog :title="upload.title" v-model="upload.open" width="400px" append-to-body>
@@ -107,7 +105,6 @@
const NewOrEditFormDia = defineAsyncComponent(() => import("@/views/personnelManagement/employeeRecord/components/NewOrEditFormDia.vue"));
const ShowFormDia = defineAsyncComponent(() => import( "@/views/personnelManagement/employeeRecord/components/Show.vue"));
const RenewContract = defineAsyncComponent(() => import( "@/views/personnelManagement/employeeRecord/components/RenewContract.vue"));
const data = reactive({
  searchForm: {
@@ -119,8 +116,6 @@
  deptOptions: [],
});
const { searchForm, deptOptions } = toRefs(data);
const isShowRenewContractModal = ref(false);
const id = ref(0);
const tableColumn = ref([
  {
    label: "状态",
@@ -177,6 +172,13 @@
    width: 120,
  },
  {
    label: "转正日期",
    prop: "positiveDate",
    width: 120,
    dataType: "slot",
    slot: "positiveDate",
  },
  {
    label: "年龄",
    prop: "age",
  },
@@ -208,22 +210,6 @@
          openFormNewOrEditFormDia("edit", row);
        },
      },
      {
        name: "续签合同",
        type: "text",
        showHide: row => row.staffState === 1,
        clickFun: (row) => {
          isShowRenewContractModal.value = true;
          id.value = row.id;
        },
      },
      // {
      //   name: "详情",
      //   type: "text",
      //   clickFun: (row) => {
      //     openForm("edit", row);
      //   },
      // },
    ],
  },
]);
@@ -253,6 +239,22 @@
  // ä¸Šä¼ çš„地址
  url: import.meta.env.VITE_APP_BASE_API + "/staff/staffOnJob/import"
})
// åˆ¤æ–­è½¬æ­£æ—¥æœŸæ˜¯å¦åœ¨7天内
const getPositiveDateClass = (positiveDate) => {
  if (!positiveDate) return '';
  const today = new Date();
  today.setHours(0, 0, 0, 0);
  const positive = new Date(positiveDate);
  positive.setHours(0, 0, 0, 0);
  const diffTime = positive - today;
  const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
  // 7天内转正(包括今天)显示警告色
  if (diffDays >= 0 && diffDays <= 7) {
    return 'positive-date-warning';
  }
  return '';
};
const fetchDeptOptions = () => {
    deptTreeSelect().then(response => {
@@ -402,4 +404,9 @@
.search_title2 {
  margin-left: 10px;
}
.positive-date-warning {
  color: #f56c6c;
  font-weight: bold;
}
</style>
src/views/procurementManagement/procurementLedger/index.vue
@@ -693,6 +693,7 @@
  const salesContractList = ref([]);
  const supplierList = ref([]);
  const tableLoading = ref(false);
  const recordId = ref();
  const fileListDialogVisible = ref(false);
  const page = reactive({
    current: 1,
src/views/productionManagement/processRoute/processRouteItem/index.vue
@@ -315,7 +315,7 @@
                                     :step="1"
                                     controls-position="right"
                                     style="width: 100%"
                                     @change="handleUnitQuantityChange(row)"
                                     @change="handleUnitQuantityChange"
                                     :disabled="!bomDataValue.isEdit || bomDataValue.dataList.some(item => (item).tempId === row.tempId)" />
                  </el-form-item>
                </template>
@@ -333,7 +333,7 @@
                                     :step="1"
                                     controls-position="right"
                                     style="width: 100%"
                                     :disabled="!bomDataValue.isEdit || bomDataValue.dataList.some(item => (item).tempId === row.tempId)" />
                                     :disabled="true" />
                  </el-form-item>
                </template>
              </el-table-column>
@@ -1089,6 +1089,53 @@
      }
    });
  };
  const toQuantityNumber = value => {
    const numberValue = Number(value);
    if (!Number.isFinite(numberValue)) {
      return 0;
    }
    return Number(numberValue.toFixed(2));
  };
  const syncDemandedQuantityTree = (items, parentDemandedQuantity = null) => {
    items.forEach(item => {
      if (parentDemandedQuantity !== null) {
        item.demandedQuantity = toQuantityNumber(
          parentDemandedQuantity * toQuantityNumber(item.unitQuantity)
        );
      }
      if (Array.isArray(item.children) && item.children.length > 0) {
        syncDemandedQuantityTree(
          item.children,
          toQuantityNumber(item.demandedQuantity)
        );
      }
    });
  };
  const recalculateDemandedQuantities = () => {
    if (pageType.value !== "order") {
      return;
    }
    const rootDemandedQuantity = routeInfo.value.quantity;
    if (
      rootDemandedQuantity === undefined ||
      rootDemandedQuantity === null ||
      rootDemandedQuantity === ""
    ) {
      syncDemandedQuantityTree(bomDataValue.value.dataList);
      return;
    }
    syncDemandedQuantityTree(
      bomDataValue.value.dataList,
      toQuantityNumber(rootDemandedQuantity)
    );
  };
  const processChange = value => {
    processOptions.value.forEach(item => {
      if (item.id == value) {
@@ -1117,6 +1164,7 @@
      );
      bomDataValue.value.dataList = data || [];
      normalizeTreeData(bomDataValue.value.dataList);
      recalculateDemandedQuantities();
    } catch (err) {
      console.error("获取BOM数据失败:", err);
    }
@@ -1212,10 +1260,8 @@
    });
  };
  const handleUnitQuantityChange = row => {
    if (routeInfo.value.quantity && routeInfo.value.quantity !== 0) {
      row.demandedQuantity = (row.unitQuantity || 0) * routeInfo.value.quantity;
    }
  const handleUnitQuantityChange = () => {
    recalculateDemandedQuantities();
  };
  const addchildItem = (item, tempId) => {
@@ -1236,14 +1282,12 @@
          "",
        operationName: "",
        unitQuantity: 1,
        demandedQuantity:
          routeInfo.value.quantity && routeInfo.value.quantity !== 0
            ? 1 * routeInfo.value.quantity
            : 0,
        demandedQuantity: 0,
        children: [],
        unit: "",
        tempId: new Date().getTime(),
      });
      recalculateDemandedQuantities();
      return true;
    }
    if (item.children && item.children.length > 0) {
@@ -1275,14 +1319,12 @@
            "",
          operationName: "",
          unitQuantity: 1,
          demandedQuantity:
            routeInfo.value.quantity && routeInfo.value.quantity !== 0
              ? 1 * routeInfo.value.quantity
              : 0,
          demandedQuantity: 0,
          unit: "",
          children: [],
          tempId: new Date().getTime(),
        });
        recalculateDemandedQuantities();
        return;
      }
      addchildItem(item, tempId);
@@ -1350,6 +1392,7 @@
    console.log(bomDataValue.value.dataList, "bomDataValue.value.dataList");
    normalizeTreeData(bomDataValue.value.dataList);
    recalculateDemandedQuantities();
    const valid = validateAllBom();
    if (valid) {
@@ -1361,7 +1404,7 @@
        .then(() => {
          ElMessage.success("BOM保存成功");
          bomDataValue.value.isEdit = false;
          fetchBomData();
          refreshCurrentPage();
        })
        .catch(() => {
          ElMessage.error("BOM保存失败");
@@ -1374,11 +1417,15 @@
    }
  };
  onMounted(() => {
  const refreshCurrentPage = () => {
    getRouteInfo();
    getList();
    getProcessList();
    fetchBomData();
  };
  onMounted(() => {
    refreshCurrentPage();
  });
  onUnmounted(() => {
src/views/productionManagement/productStructure/Detail/index.vue
@@ -86,6 +86,7 @@
                                     :step="1"
                                     controls-position="right"
                                     style="width: 100%"
                                     @change="handleUnitQuantityChange"
                                     :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)" />
                  </el-form-item>
                </template>
@@ -103,7 +104,7 @@
                                     :step="1"
                                     controls-position="right"
                                     style="width: 100%"
                                     :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)" />
                                     :disabled="true" />
                  </el-form-item>
                </template>
              </el-table-column>
@@ -268,6 +269,42 @@
    });
  };
  const toQuantityNumber = (value: any) => {
    const numberValue = Number(value);
    if (!Number.isFinite(numberValue)) {
      return 0;
    }
    return Number(numberValue.toFixed(2));
  };
  const syncDemandedQuantityTree = (
    items: any[],
    parentDemandedQuantity: number | null = null
  ) => {
    items.forEach((item: any) => {
      if (parentDemandedQuantity !== null) {
        item.demandedQuantity = toQuantityNumber(
          parentDemandedQuantity * toQuantityNumber(item.unitQuantity)
        );
      }
      if (Array.isArray(item.children) && item.children.length > 0) {
        syncDemandedQuantityTree(
          item.children,
          toQuantityNumber(item.demandedQuantity)
        );
      }
    });
  };
  const recalculateDemandedQuantities = () => {
    if (!isOrderPage.value) {
      return;
    }
    syncDemandedQuantityTree(dataValue.dataList);
  };
  const buildSubmitTree = (items: any[]) => {
    return items.map((item: any) => {
      const current = { ...item };
@@ -279,9 +316,45 @@
    });
  };
  const findSiblings = (items: any[], tempId: string): any[] | null => {
    if (!items || items.length === 0) return null;
    // æ£€æŸ¥å½“前层级
    if (items.some(item => item.tempId === tempId)) {
      return items;
    }
    // é€’归查找子级
    for (const item of items) {
      if (item.children && item.children.length > 0) {
        const result = findSiblings(item.children, tempId);
        if (result) return result;
      }
    }
    return null;
  };
  const handleProcessChange = (row: any, value: any) => {
    if (value) {
      const siblings = findSiblings(dataValue.dataList, row.tempId);
      if (siblings) {
        const isDuplicate = siblings.some(
          s => s.tempId !== row.tempId && s.processId === value
        );
        if (isDuplicate) {
          const option = getProcessOptionById(value);
          const processName = option?.name || "该工序";
          ElMessage.warning(`同一层级下不能选择重复的消耗工序:${processName}`);
          row.processId = "";
          syncProcessOperationFields(row);
          return;
        }
      }
    }
    row.processId = value || "";
    syncProcessOperationFields(row);
  };
  const handleUnitQuantityChange = () => {
    recalculateDemandedQuantities();
  };
  const tableData = reactive([
@@ -304,6 +377,7 @@
      const { data } = await listProcessBom({ orderId: routeOrderId.value });
      dataValue.dataList = (data as any) || [];
      normalizeTreeData(dataValue.dataList);
      recalculateDemandedQuantities();
    } else {
      // éžè®¢å•情况:使用原来的接口
      const { data } = await queryList(routeId.value);
@@ -389,8 +463,37 @@
  const validateAll = () => {
    let isValid = true;
    // æ ¡éªŒä¸€ç»„兄弟节点的工序是否唯一
    const checkProcessUniqueness = (items: any[]) => {
      if (!items || items.length === 0 || !isValid) return;
      const processIds = new Set();
      for (const item of items) {
        if (item.processId) {
          if (processIds.has(item.processId)) {
            const option = getProcessOptionById(item.processId);
            const processName = option?.name || item.processName || "未知工序";
            ElMessage.error(
              `产品「${item.productName}」的消耗工序「${processName}」在当前层级已存在,请勿重复设置`
            );
            isValid = false;
            return;
          }
          processIds.add(item.processId);
        }
      }
      // é€’归校验子级的兄弟节点
      for (const item of items) {
        if (item.children && item.children.length > 0) {
          checkProcessUniqueness(item.children);
        }
      }
    };
    // æ ¡éªŒå‡½æ•°
    const validateItem = (item: any, isTopLevel = false) => {
      if (!isValid) return;
      // æ ¡éªŒå½“前项的必填字段
      if (!item.model) {
        ElMessage.error("请选择规格");
@@ -418,7 +521,7 @@
      //   return;
      // }
      // é€’归校验子项
      // é€’归校验子项字段
      if (item.children && item.children.length > 0) {
        item.children.forEach(child => {
          validateItem(child, false);
@@ -426,7 +529,11 @@
      }
    };
    // éåŽ†æ‰€æœ‰é¡¶å±‚é¡¹
    // 1. é¦–先校验同一父级下的同层消耗工序是否唯一
    checkProcessUniqueness(dataValue.dataList);
    if (!isValid) return false;
    // 2. ç„¶åŽéåŽ†æ ¡éªŒæ‰€æœ‰é¡¶å±‚é¡¹çš„å­—æ®µå¿…å¡«æƒ…å†µ
    dataValue.dataList.forEach(item => {
      validateItem(item, true);
    });
@@ -437,6 +544,7 @@
  const submit = () => {
    dataValue.loading = true;
    normalizeTreeData(dataValue.dataList);
    recalculateDemandedQuantities();
    // å…ˆè¿›è¡Œè¡¨å•校验
    const valid = validateAll();
@@ -514,6 +622,7 @@
          tempId: new Date().getTime(),
        });
        recalculateDemandedQuantities();
        return;
      }
      addchildItem(item, tempId);
@@ -542,6 +651,7 @@
        unit: "",
        tempId: new Date().getTime(),
      });
      recalculateDemandedQuantities();
      return true;
    }
    if (item.children && item.children.length > 0) {
@@ -587,4 +697,4 @@
    await fetchProcessOptions();
    await fetchData();
  });
</script>
</script>
src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue
@@ -111,6 +111,7 @@
        <span class="dialog-footer">
          <el-button type="primary"
                     :loading="materialSaving"
                     :disabled="isSaveDisabled"
                     @click="handleMaterialSave">保存</el-button>
          <el-button @click="dialogVisible = false">取消</el-button>
        </span>
@@ -155,6 +156,33 @@
  const materialTableLoading = ref(false);
  const materialSaving = ref(false);
  const materialTableData = ref([]);
  const isSaveDisabled = computed(() => {
    if (materialTableData.value.length === 0) return true;
    return !materialTableData.value.some(row => {
      // æ£€æŸ¥æ˜¯å¦æœ‰ä»»ä½•用户输入内容
      const hasBatch = Array.isArray(row.batchNo) && row.batchNo.length > 0;
      const hasPickQty =
        row.pickQty !== null && row.pickQty !== undefined && row.pickQty !== 0;
      if (row.bom) {
        // å¯¹äºŽæ¥è‡ªBOM的行,输入框只有“批号”和“领用数量”
        return hasBatch || hasPickQty;
      } else {
        // å¯¹äºŽæ–°å¢žè¡Œï¼Œè¾“入框包括“工序”、“原料”、“需求数量”、“批号”和“领用数量”
        const hasOperation = !!row.operationName;
        const hasMaterial = !!row.materialName;
        const hasDemanded =
          row.demandedQuantity !== null &&
          row.demandedQuantity !== undefined &&
          row.demandedQuantity !== 0;
        return (
          hasBatch || hasPickQty || hasOperation || hasMaterial || hasDemanded
        );
      }
    });
  });
  const processOptions = ref([]);
  const currentMaterialSelectRowIndex = ref(-1);
  let materialTempId = 0;
src/views/productionManagement/productionProcess/index.vue
@@ -44,7 +44,7 @@
            <div class="card-body">
              <!-- <div class="process-name">{{ process.name }}</div> -->
              <div class="process-desc">{{ process.remark || '暂无描述' }}</div>
              <div class="process-device">关联设备: {{ deviceOptions.find(item => item.id === Number(process.deviceLedgerId))?.deviceName|| '未关联' }}</div>
              <div class="process-device">关联设备: {{ (deviceOptions.find(item => item.id === Number(process.deviceLedgerId))?.deviceName) || '未关联' }}</div>
            </div>
            <div class="card-footer">
              <div class="status-tag">
@@ -243,6 +243,7 @@
            </el-form-item>
            <el-form-item label="标准值">
              <el-input v-model="selectedParam.standardValue"
                        @input="val => onStandardValueInput(val, selectedParam)"
                        placeholder="请输入默认值" />
            </el-form-item>
          </el-form>
@@ -273,6 +274,7 @@
        <el-form-item label="标准值"
                      prop="standardValue">
          <el-input v-model="editParamForm.standardValue"
                    @input="val => onStandardValueInput(val, editParamForm)"
                    placeholder="请输入标准值" />
        </el-form-item>
      </el-form>
@@ -392,7 +394,18 @@
    technologyParamId: null,
    paramName: "",
    standardValue: null,
    paramType: null,
  });
  const onStandardValueInput = (val, target) => {
    const data = target.value || target;
    const type = data.paramType;
    if (type === 1) {
      // æ•°å€¼æ ¼å¼ï¼šä¸èƒ½è¾“入中文或英文字符
      data.standardValue = val.replace(/[a-zA-Z\u4e00-\u9fa5]/g, "");
    }
  };
  const editParamRules = {
    standardValue: [
      {
@@ -403,6 +416,12 @@
          if (value === null || value === undefined || value === "") {
            callback(new Error("请输入标准值"));
          } else {
            const type = editParamForm.paramType;
            if (type === 1 && value) {
              if (/[a-zA-Z\u4e00-\u9fa5]/.test(value)) {
                return callback(new Error("数值格式不能包含中英文字符"));
              }
            }
            callback();
          }
        },
@@ -551,7 +570,10 @@
    processForm.isQuality = !!process.isQuality;
    processForm.isProduction = !!process.isProduction;
    processForm.remark = process.remark || "";
    processForm.deviceLedgerId = Number(process.deviceLedgerId);
    // å¦‚果设备 ID ä¸º 0 æˆ–者在设备列表中找不到,则回显为空(null)
    const deviceId = Number(process.deviceLedgerId);
    const hasDevice = deviceOptions.value.some(item => item.id === deviceId);
    processForm.deviceLedgerId = deviceId && hasDevice ? deviceId : null;
    processForm.type = process.type;
    processDialogVisible.value = true;
  };
@@ -717,6 +739,7 @@
    editParamForm.technologyParamId = row.technologyParamId;
    editParamForm.paramName = row.paramName;
    editParamForm.standardValue = row.standardValue;
    editParamForm.paramType = row.paramType;
    editParamDialogVisible.value = true;
  };
src/views/productionManagement/productionTraceability/index.vue
@@ -1,5 +1,6 @@
<template>
  <div class="app-container">
    <PageHeader content="生产订单" />
    <el-card style="height:82vh;overflow:auto;">
      <template #header>
        <div class="card-header">
src/views/productionManagement/workOrderManagement/index.vue
@@ -673,7 +673,10 @@
      }
    }
    currentReportRowData.value = row;
    reportForm.planQuantity = row.planQuantity;
    const planQuantity = Number(row.planQuantity || 0);
    const completeQuantity = Number(row.completeQuantity || 0);
    const remainingQuantity = Math.max(0, planQuantity - completeQuantity);
    reportForm.planQuantity = remainingQuantity;
    reportForm.quantity =
      row.quantity !== undefined && row.quantity !== null ? row.quantity : null;
    reportForm.productProcessRouteItemId = row.productProcessRouteItemId;
src/views/qualityManagement/finalInspection/components/formDia.vue
@@ -2,7 +2,7 @@
  <div>
    <el-dialog
        v-model="dialogFormVisible"
        :title="operationType === 'add' ? '新增出厂检验' : '编辑出厂检验'"
        :title="operationType === 'add' ? '新增出厂检验' : operationType === 'view' ? '查看出厂检验' : '编辑出厂检验'"
        width="70%"
        @close="closeDia"
    >
@@ -18,19 +18,21 @@
                  @change="getModels"
                  :data="productOptions"
                  :render-after-expand="false"
                  :disabled="operationType === 'edit'"
                  :disabled="isViewMode || operationType === 'edit'"
                  style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="规格型号:" prop="productModelId">
              <el-select v-model="form.productModelId" placeholder="请选择" clearable :disabled="operationType === 'edit'"
              <el-select v-model="form.productModelId" placeholder="请选择" clearable :disabled="isViewMode || operationType === 'edit'"
                         filterable readonly @change="handleChangeModel">
                <el-option v-for="item in modelOptions" :key="item.id" :label="item.model" :value="item.id" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="指标选择:" prop="testStandardId">
              <el-select
@@ -39,6 +41,7 @@
                clearable
                @change="handleTestStandardChange"
                style="width: 100%"
                :disabled="isViewMode"
              >
                <el-option
                  v-for="item in testStandardOptions"
@@ -58,21 +61,52 @@
          </el-col>
          <el-col :span="12">
            <el-form-item label="数量:" prop="quantity">
              <el-input-number :step="0.01" :min="0" style="width: 100%" v-model="form.quantity" placeholder="请输入" clearable :precision="2" :disabled="quantityDisabled"/>
              <el-input-number :step="0.01" :min="0" style="width: 100%" v-model="form.quantity" placeholder="请输入" clearable :precision="2" :disabled="isViewMode || processQuantityDisabled"/>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="合格数量:"
                          prop="qualifiedQuantity">
              <el-input-number :step="0.01"
                               :min="0"
                               style="width: 100%"
                               v-model="form.qualifiedQuantity"
                               placeholder="请输入"
                               clearable
                               :precision="2"
                               @change="handleQualifiedQuantityChange"
                               :disabled="isViewMode" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="不合格数量:"
                          prop="unqualifiedQuantity">
              <el-input-number :step="0.01"
                               :min="0"
                               style="width: 100%"
                               v-model="form.unqualifiedQuantity"
                               placeholder="请输入"
                               clearable
                               :precision="2"
                               @change="handleUnqualifiedQuantityChange"
                               :disabled="isViewMode" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="检测单位:" prop="checkCompany">
              <el-input v-model="form.checkCompany" placeholder="请输入" clearable/>
              <el-input v-model="form.checkCompany" placeholder="请输入" clearable :disabled="isViewMode"/>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="检测结果:" prop="checkResult">
              <el-select v-model="form.checkResult">
              <el-select v-model="form.checkResult" :disabled="isViewMode">
                <el-option label="合格" value="合格" />
                <el-option label="不合格" value="不合格" />
                <el-option label="部分合格" value="部分合格" />
              </el-select>
            </el-form-item>
          </el-col>
@@ -80,10 +114,10 @@
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="检验员:" prop="checkName">
                            <el-select v-model="form.checkName" placeholder="请选择" clearable>
                                <el-option v-for="item in userList" :key="item.nickName" :label="item.nickName"
                                                     :value="item.nickName"/>
                            </el-select>
              <el-select v-model="form.checkName" placeholder="请选择" clearable :disabled="isViewMode">
                <el-option v-for="item in userList" :key="item.nickName" :label="item.nickName"
                           :value="item.nickName"/>
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
@@ -96,6 +130,7 @@
                  format="YYYY-MM-DD"
                  clearable
                  style="width: 100%"
                  :disabled="isViewMode"
              />
            </el-form-item>
          </el-col>
@@ -109,13 +144,16 @@
                height="400"
            >
                <template #slot="{ row }">
                    <el-input v-model="row.testValue" clearable/>
                    <el-input v-model="row.testValue" clearable :disabled="isViewMode"/>
                </template>
            </PIMTable>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitForm">确认</el-button>
          <el-button @click="closeDia">取消</el-button>
          <template v-if="!isViewMode">
            <el-button type="primary" @click="submitForm">确认</el-button>
            <el-button @click="closeDia">取消</el-button>
          </template>
          <el-button v-else @click="closeDia">关闭</el-button>
        </div>
      </template>
    </el-dialog>
@@ -134,7 +172,7 @@
const emit = defineEmits(['close'])
const dialogFormVisible = ref(false);
const operationType = ref('')
const operationType = ref("");
const data = reactive({
  form: {
    checkTime: "",
@@ -147,25 +185,31 @@
    testStandardId: "",
    unit: "",
    quantity: "",
    qualifiedQuantity: "",
    unqualifiedQuantity: "",
    checkCompany: "",
    checkResult: "",
  },
  rules: {
    checkTime: [{ required: true, message: "请输入", trigger: "blur" },],
    checkTime: [{ required: true, message: "请输入", trigger: "blur" }],
    process: [{ required: true, message: "请输入", trigger: "blur" }],
    checkName: [{ required: false, message: "请输入", trigger: "blur" }],
    productId: [{ required: true, message: "请输入", trigger: "blur" }],
    productModelId: [{ required: true, message: "请选择", trigger: "change" }],
    testStandardId: [{required: false, message: "请选择指标", trigger: "change"}],
    testStandardId: [{ required: false, message: "请选择指标", trigger: "change" }],
    unit: [{ required: false, message: "请输入", trigger: "blur" }],
    quantity: [{ required: true, message: "请输入", trigger: "blur" }],
    qualifiedQuantity: [{ required: true, message: "请输入", trigger: "blur" }],
    unqualifiedQuantity: [{ required: true, message: "请输入", trigger: "blur" }],
    checkCompany: [{ required: false, message: "请输入", trigger: "blur" }],
    checkResult: [{ required: true, message: "请输入", trigger: "change" }],
  },
});
const { form, rules } = toRefs(data);
// ç¼–辑时:productMainId æˆ– purchaseLedgerId ä»»ä¸€æœ‰å€¼åˆ™æ•°é‡ç½®ç°
const quantityDisabled = computed(() => {
// æ˜¯å¦ä¸ºæŸ¥çœ‹æ¨¡å¼
const isViewMode = computed(() => operationType.value === 'view');
// ç¼–辑时:productMainId æˆ– purchaseLedgerId ä»»ä¸€æœ‰å€¼åˆ™å·¥åºã€æ•°é‡ç½®ç°
const processQuantityDisabled = computed(() => {
  const v = form.value || {};
  return !!(v.productMainId != null || v.purchaseLedgerId != null);
});
@@ -209,7 +253,7 @@
  // å…ˆæ¸…空表单验证状态,避免闪烁
  await nextTick();
  proxy.$refs.formRef?.clearValidate();
  // å¹¶è¡ŒåŠ è½½åŸºç¡€æ•°æ®
  const [userListsRes] = await Promise.all([
    userListNoPage(),
@@ -219,12 +263,12 @@
    })
  ]);
  userList.value = userListsRes.data;
  form.value = {}
  testStandardOptions.value = [];
  tableData.value = [];
  if (operationType.value === 'edit') {
  if (operationType.value === 'edit' || operationType.value === 'view') {
    // å…ˆä¿å­˜ testStandardId,避免被清空
    const savedTestStandardId = row.testStandardId;
    // å…ˆè®¾ç½®è¡¨å•数据,但暂时清空 testStandardId,等选项加载完成后再设置
@@ -234,18 +278,18 @@
    nextTick(() => {
      proxy.$refs.formRef?.clearValidate();
    });
    // ç¼–辑模式下,并行加载规格型号和指标选项
    if (currentProductId.value) {
      // è®¾ç½®äº§å“åç§°
      form.value.productName = findNodeById(productOptions.value, currentProductId.value);
      // å¹¶è¡ŒåŠ è½½è§„æ ¼åž‹å·å’ŒæŒ‡æ ‡é€‰é¡¹
      const params = {
        productId: currentProductId.value,
        inspectType: 2
      };
      Promise.all([
        modelList({ id: currentProductId.value }),
        qualityInspectDetailByProductId(params)
@@ -260,15 +304,15 @@
            form.value.unit = selectedModel.unit || '';
          }
        }
        // è®¾ç½®æŒ‡æ ‡é€‰é¡¹
        testStandardOptions.value = testStandardRes.data || [];
        // è®¾ç½® testStandardId å¹¶åŠ è½½å‚æ•°åˆ—è¡¨
        nextTick(() => {
          if (savedTestStandardId) {
            // ç¡®ä¿ç±»åž‹åŒ¹é…ï¼ˆitem.id å¯èƒ½æ˜¯æ•°å­—或字符串)
            const matchedOption = testStandardOptions.value.find(item =>
            const matchedOption = testStandardOptions.value.find(item =>
              item.id == savedTestStandardId || String(item.id) === String(savedTestStandardId)
            );
            if (matchedOption) {
@@ -313,6 +357,28 @@
  form.value.unit = modelOptions.value.find(item => item.id == value)?.unit || '';
}
const handleQualifiedQuantityChange = (value) => {
  if (value === null || value === undefined) {
    form.value.qualifiedQuantity = 0;
    return;
  }
  const quantity = parseFloat(form.value.quantity) || 0;
  const qualified = parseFloat(value) || 0;
  form.value.qualifiedQuantity = qualified > quantity?quantity:qualified;
  form.value.unqualifiedQuantity = Math.max(0, quantity - qualified);
};
const handleUnqualifiedQuantityChange = (value) => {
  if (value === null || value === undefined) {
    form.value.unqualifiedQuantity = 0;
    return;
  }
  const quantity = parseFloat(form.value.quantity) || 0;
  const unqualified = parseFloat(value) || 0;
  form.value.unqualifiedQuantity = unqualified > quantity?quantity:unqualified;
  form.value.qualifiedQuantity = Math.max(0, quantity - unqualified);
};
const findNodeById = (nodes, productId) => {
  for (let i = 0; i < nodes.length; i++) {
    if (nodes[i].value === productId) {
@@ -337,7 +403,7 @@
    if (children && children.length > 0) {
      newItem.children = convertIdToValue(children);
    }
    return newItem;
  });
}
@@ -345,26 +411,26 @@
const submitForm = () => {
  proxy.$refs.formRef.validate(valid => {
    if (valid) {
      form.value.inspectType = 2
            if (operationType.value === "add") {
                tableData.value.forEach((item) => {
                    delete item.id
                })
            }
            const data = {...form.value, qualityInspectParams: tableData.value}
      form.value.inspectType = 2;
      if (operationType.value === "add") {
        tableData.value.forEach((item) => {
          delete item.id;
        });
      }
      const data = { ...form.value, qualityInspectParams: tableData.value };
      if (operationType.value === "add") {
        qualityInspectAdd(data).then(res => {
          proxy.$modal.msgSuccess("提交成功");
          closeDia();
        })
        });
      } else {
        qualityInspectUpdate(data).then(res => {
          proxy.$modal.msgSuccess("提交成功");
          closeDia();
        })
        });
      }
    }
  })
  });
}
const getList = () => {
  if (!currentProductId.value) {
@@ -375,15 +441,15 @@
  let params = {
    productId: currentProductId.value,
    inspectType: 2
  }
    qualityInspectDetailByProductId(params).then(res => {
        // ä¿å­˜ä¸‹æ‹‰æ¡†é€‰é¡¹æ•°æ®
        testStandardOptions.value = res.data || [];
        // æ¸…空表格数据,等待用户选择指标
        tableData.value = [];
        // æ¸…空指标选择
        form.value.testStandardId = '';
    })
  };
  qualityInspectDetailByProductId(params).then(res => {
    // ä¿å­˜ä¸‹æ‹‰æ¡†é€‰é¡¹æ•°æ®
    testStandardOptions.value = res.data || [];
    // æ¸…空表格数据,等待用户选择指标
    tableData.value = [];
    // æ¸…空指标选择
    form.value.testStandardId = '';
  });
}
// æŒ‡æ ‡é€‰æ‹©å˜åŒ–处理
@@ -395,17 +461,21 @@
  tableLoading.value = true;
  getQualityTestStandardParamByTestStandardId(testStandardId).then(res => {
    tableData.value = res.data || [];
    tableData.value = tableData.value.map(item => ({
      ...item,
      id: null
    }));
  }).catch(error => {
    console.error('获取标准参数失败:', error);
    tableData.value = [];
  }).finally(() => {
    tableLoading.value = false;
  })
  });
}
const getQualityInspectParamList = (id) => {
    qualityInspectParamInfo(id).then(res => {
        tableData.value = res.data;
    })
  qualityInspectParamInfo(id).then(res => {
    tableData.value = res.data;
  });
}
// å…³é—­å¼¹æ¡†
const closeDia = () => {
@@ -414,8 +484,8 @@
  testStandardOptions.value = [];
  form.value.testStandardId = '';
  dialogFormVisible.value = false;
  emit('close')
};
  emit('close');
}
defineExpose({
  openDialog,
});
@@ -423,4 +493,4 @@
<style scoped>
</style>
</style>
src/views/qualityManagement/finalInspection/index.vue
@@ -123,8 +123,18 @@
    prop: "unit",
  },
  {
    label: "数量",
    label: "总数量",
    prop: "quantity",
    width: 100
  },
  {
    label: "合格数量",
    prop: "qualifiedQuantity",
    width: 100
  },
  {
    label: "不合格数量",
    prop: "unqualifiedQuantity",
    width: 100
  },
  {
@@ -142,7 +152,7 @@
      } else if (params == '合格') {
        return "success";
      } else {
        return null;
        return 'danger';
      }
    },
  },
@@ -181,6 +191,13 @@
        }
      },
      {
        name: "查看",
        type: "text",
        clickFun: (row) => {
          openForm("view", row);
        },
      },
      {
        name: "附件",
        type: "text",
        clickFun: (row) => {
src/views/qualityManagement/nonconformingManagement/components/formDia.vue
@@ -35,10 +35,10 @@
          </el-col>
          <el-col :span="12">
            <el-form-item label="规格型号:" prop="model">
              <el-select v-model="form.model" placeholder="请选择" clearable :disabled="operationType === 'edit'"
                          filterable readonly @change="handleChangeModel">
              <el-option v-for="item in modelOptions" :key="item.id" :label="item.model" :value="item.id" />
            </el-select>
              <el-select v-model="form.productModelId" placeholder="请选择" clearable :disabled="operationType === 'edit'"
                         filterable readonly @change="handleChangeModel">
                <el-option v-for="item in modelOptions" :key="item.id" :label="item.model" :value="item.id" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
@@ -149,7 +149,7 @@
    productId: "",
    model: "",
    unit: "",
    quantity: "",
    quantity: undefined,
    checkCompany: "",
    checkResult: "",
    inspectType: '',
@@ -157,6 +157,7 @@
    dealResult: '',
    dealName: '',
    dealTime: '',
    productModelId: undefined,
  },
  rules: {
    checkTime: [{ required: false, message: "请输入", trigger: "blur" },],
@@ -199,8 +200,9 @@
      productId: '',
      model: '',
      unit: '',
      quantity: '',
      quantity: undefined,
      productName: '',
      productModelId: undefined,
    };
  } else {
    form.value = {};
@@ -223,6 +225,12 @@
  modelList({ id: value }).then((res) => {
    modelOptions.value = res;
  })
};
const handleChangeModel = (value) => {
  const selectedModel = modelOptions.value.find(item => item.id === value);
  if (selectedModel) {
    form.value.model = selectedModel.model;
  }
};
const findNodeById = (nodes, productId) => {
  for (let i = 0; i < nodes.length; i++) {
@@ -285,4 +293,4 @@
<style scoped>
</style>
</style>
src/views/qualityManagement/nonconformingManagement/index.vue
@@ -98,7 +98,7 @@
      } else if (params == '合格') {
        return "success";
      } else {
        return null;
        return 'danger';
      }
    },
  },
src/views/qualityManagement/processInspection/components/formDia.vue
@@ -1,7 +1,7 @@
<template>
  <div>
    <el-dialog v-model="dialogFormVisible"
               :title="operationType === 'add' ? '新增过程检验' : '编辑过程检验'"
               :title="operationType === 'add' ? '新增过程检验' : operationType === 'view' ? '查看过程检验' : '编辑过程检验'"
               width="70%"
               @close="closeDia">
      <el-form :model="form"
@@ -16,7 +16,7 @@
              <el-select v-model="form.process"
                         placeholder="请选择工序"
                         clearable
                         :disabled="processQuantityDisabled"
                         :disabled="isViewMode || processQuantityDisabled"
                         style="width: 100%">
                <el-option v-for="item in processList"
                           :key="item.name"
@@ -35,7 +35,7 @@
                              @change="getModels"
                              :data="productOptions"
                              :render-after-expand="false"
                              :disabled="operationType === 'edit'"
                              :disabled="isViewMode || operationType === 'edit'"
                              style="width: 100%" />
            </el-form-item>
          </el-col>
@@ -47,7 +47,7 @@
              <el-select v-model="form.productModelId"
                         placeholder="请选择"
                         clearable
                         :disabled="operationType === 'edit'"
                         :disabled="isViewMode || operationType === 'edit'"
                         filterable
                         readonly
                         @change="handleChangeModel">
@@ -65,7 +65,8 @@
                         placeholder="请选择指标"
                         clearable
                         @change="handleTestStandardChange"
                         style="width: 100%">
                         style="width: 100%"
                         :disabled="isViewMode">
                <el-option v-for="item in testStandardOptions"
                           :key="item.id"
                           :label="item.standardName || item.standardNo"
@@ -93,7 +94,37 @@
                               placeholder="请输入"
                               clearable
                               :precision="2"
                               :disabled="processQuantityDisabled" />
                               :disabled="isViewMode || processQuantityDisabled" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="合格数量:"
                          prop="qualifiedQuantity">
              <el-input-number :step="0.01"
                               :min="0"
                               style="width: 100%"
                               v-model="form.qualifiedQuantity"
                               placeholder="请输入"
                               clearable
                               :precision="2"
                               @change="handleQualifiedQuantityChange"
                               :disabled="isViewMode" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="不合格数量:"
                          prop="unqualifiedQuantity">
              <el-input-number :step="0.01"
                               :min="0"
                               style="width: 100%"
                               v-model="form.unqualifiedQuantity"
                               placeholder="请输入"
                               clearable
                               :precision="2"
                               @change="handleUnqualifiedQuantityChange"
                               :disabled="isViewMode" />
            </el-form-item>
          </el-col>
        </el-row>
@@ -103,17 +134,20 @@
                          prop="checkCompany">
              <el-input v-model="form.checkCompany"
                        placeholder="请输入"
                        clearable />
                        clearable
                        :disabled="isViewMode" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="检测结果:"
                          prop="checkResult">
              <el-select v-model="form.checkResult">
              <el-select v-model="form.checkResult" :disabled="isViewMode">
                <el-option label="合格"
                           value="合格" />
                <el-option label="不合格"
                           value="不合格" />
                <el-option label="部分合格"
                           value="部分合格" />
              </el-select>
            </el-form-item>
          </el-col>
@@ -124,7 +158,8 @@
                          prop="checkName">
              <el-select v-model="form.checkName"
                         placeholder="请选择"
                         clearable>
                         clearable
                         :disabled="isViewMode">
                <el-option v-for="item in userList"
                           :key="item.nickName"
                           :label="item.nickName"
@@ -141,7 +176,8 @@
                              value-format="YYYY-MM-DD"
                              format="YYYY-MM-DD"
                              clearable
                              style="width: 100%" />
                              style="width: 100%"
                              :disabled="isViewMode" />
            </el-form-item>
          </el-col>
        </el-row>
@@ -153,14 +189,18 @@
                height="400">
        <template #slot="{ row }">
          <el-input v-model="row.testValue"
                    clearable />
                    clearable
                    :disabled="isViewMode" />
        </template>
      </PIMTable>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary"
                     @click="submitForm">确认</el-button>
          <el-button @click="closeDia">取消</el-button>
          <template v-if="!isViewMode">
            <el-button type="primary"
                       @click="submitForm">确认</el-button>
            <el-button @click="closeDia">取消</el-button>
          </template>
          <el-button v-else @click="closeDia">关闭</el-button>
        </div>
      </template>
    </el-dialog>
@@ -189,6 +229,7 @@
  import { userListNoPage } from "@/api/system/user.js";
  import { qualityInspectParamInfo } from "@/api/qualityManagement/qualityInspectParam.js";
  import { list } from "@/api/productionManagement/productionProcess";
  import qualified from "@/views/inventoryManagement/stockManagement/Qualified.vue";
  const { proxy } = getCurrentInstance();
  const emit = defineEmits(["close"]);
@@ -206,6 +247,8 @@
      testStandardId: "",
      unit: "",
      quantity: "",
      qualifiedQuantity: "",
      unqualifiedQuantity: "",
      checkCompany: "",
      checkResult: "",
    },
@@ -215,17 +258,19 @@
      checkName: [{ required: false, message: "请输入", trigger: "blur" }],
      productId: [{ required: true, message: "请输入", trigger: "blur" }],
      productModelId: [{ required: true, message: "请选择", trigger: "change" }],
      testStandardId: [
        { required: false, message: "请选择指标", trigger: "change" },
      ],
      testStandardId: [{ required: false, message: "请选择指标", trigger: "change" }],
      unit: [{ required: false, message: "请输入", trigger: "blur" }],
      quantity: [{ required: true, message: "请输入", trigger: "blur" }],
      qualifiedQuantity: [{ required: true, message: "请输入", trigger: "blur" }],
      unqualifiedQuantity: [{ required: true, message: "请输入", trigger: "blur" }],
      checkCompany: [{ required: false, message: "请输入", trigger: "blur" }],
      checkResult: [{ required: true, message: "请输入", trigger: "change" }],
    },
  });
  const userList = ref([]);
  const { form, rules } = toRefs(data);
  // æ˜¯å¦ä¸ºæŸ¥çœ‹æ¨¡å¼
  const isViewMode = computed(() => operationType.value === 'view');
  // ç¼–辑时:productMainId æˆ– purchaseLedgerId ä»»ä¸€æœ‰å€¼åˆ™å·¥åºã€æ•°é‡ç½®ç°
  const processQuantityDisabled = computed(() => {
    const v = form.value || {};
@@ -299,7 +344,7 @@
    tableData.value = [];
    // å…ˆç¡®ä¿äº§å“æ ‘已加载,否则编辑时产品/规格型号无法反显
    await getProductOptions();
    if (operationType.value === "edit") {
    if (operationType.value === "edit" || operationType.value === "view") {
      // å…ˆä¿å­˜ testStandardId,避免被清空
      const savedTestStandardId = row.testStandardId;
      // å…ˆè®¾ç½®è¡¨å•数据,但暂时清空 testStandardId,等选项加载完成后再设置
@@ -400,6 +445,28 @@
      modelOptions.value.find(item => item.id == value)?.unit || "";
  };
  const handleQualifiedQuantityChange = (value) => {
    if (value === null || value === undefined) {
      form.value.qualifiedQuantity = 0;
      return;
    }
    const quantity = parseFloat(form.value.quantity) || 0;
    const qualified = parseFloat(value) || 0;
    form.value.qualifiedQuantity = qualified > quantity?quantity:qualified;
    form.value.unqualifiedQuantity = Math.max(0, quantity - qualified);
  };
  const handleUnqualifiedQuantityChange = (value) => {
    if (value === null || value === undefined) {
      form.value.unqualifiedQuantity = 0;
      return;
    }
    const quantity = parseFloat(form.value.quantity) || 0;
    const unqualified = parseFloat(value) || 0;
    form.value.unqualifiedQuantity = unqualified > quantity?quantity:unqualified;
    form.value.qualifiedQuantity = Math.max(0, quantity - unqualified);
  };
  const findNodeById = (nodes, productId) => {
    for (let i = 0; i < nodes.length; i++) {
      if (nodes[i].value === productId) {
@@ -440,6 +507,17 @@
            delete item.id;
          });
        }
        // ç¡®ä¿æ•°é‡ä¸ä¸ºnull
        const quantity = parseFloat(form.value.quantity) || 0;
        const qualified = parseFloat(form.value.qualifiedQuantity) || 0;
        const unqualified = parseFloat(form.value.unqualifiedQuantity) || 0;
        // éªŒè¯æ•°é‡å…³ç³»
        if (qualified + unqualified !== quantity) {
          proxy.$modal.msgError("合格数量与不合格数量之和必须等于总数量");
          return;
        }
        const data = {
          ...form.value,
          process: processName, // ä¿ç•™ process å­—段以兼容后端
@@ -491,6 +569,10 @@
    getQualityTestStandardParamByTestStandardId(testStandardId)
      .then(res => {
        tableData.value = res.data || [];
        tableData.value = tableData.value.map(item => ({
          ...item,
          id: null
        }));
      })
      .catch(error => {
        console.error("获取标准参数失败:", error);
@@ -520,4 +602,4 @@
</script>
<style scoped>
</style>
</style>
src/views/qualityManagement/processInspection/index.vue
@@ -122,8 +122,18 @@
    prop: "unit",
  },
  {
    label: "数量",
    label: "总数量",
    prop: "quantity",
    width: 100
  },
  {
    label: "合格数量",
    prop: "qualifiedQuantity",
    width: 100
  },
  {
    label: "不合格数量",
    prop: "unqualifiedQuantity",
    width: 100
  },
  {
@@ -141,7 +151,7 @@
      } else if (params == '合格') {
        return "success";
      } else {
        return null;
        return 'danger';
      }
    },
  },
@@ -178,6 +188,13 @@
                    }
                    return false;
                }
      },
      {
        name: "查看",
        type: "text",
        clickFun: (row) => {
          openForm("view", row);
        },
      },
      {
        name: "附件",
@@ -363,13 +380,13 @@
            type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
        })
        const downloadUrl = window.URL.createObjectURL(blob)
        const link = document.createElement('a')
        link.href = downloadUrl
        link.download = '过程检验报告.docx'
        document.body.appendChild(link)
        link.click()
        document.body.removeChild(link)
        window.URL.revokeObjectURL(downloadUrl)
    })
src/views/qualityManagement/rawMaterialInspection/components/formDia.vue
@@ -2,7 +2,7 @@
  <div>
    <el-dialog
        v-model="dialogFormVisible"
        :title="operationType === 'add' ? '新增原材料检验' : '编辑原材料检验'"
        :title="operationType === 'add' ? '新增原材料检验' : operationType === 'view' ? '查看原材料检验' : '编辑原材料检验'"
        width="70%"
        @close="closeDia"
    >
@@ -14,7 +14,7 @@
                  v-model="form.supplier"
                  placeholder="请选择"
                  clearable
                  :disabled="supplierQuantityDisabled"
                  :disabled="isViewMode || supplierQuantityDisabled"
              >
                <el-option
                    v-for="item in supplierList"
@@ -35,7 +35,7 @@
                  @change="getModels"
                  :data="productOptions"
                  :render-after-expand="false"
                  :disabled="operationType === 'edit'"
                  :disabled="isViewMode || operationType === 'edit'"
                  style="width: 100%"
              />
            </el-form-item>
@@ -44,7 +44,7 @@
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="规格型号:" prop="productModelId">
              <el-select v-model="form.productModelId" placeholder="请选择" clearable :disabled="operationType === 'edit'"
              <el-select v-model="form.productModelId" placeholder="请选择" clearable :disabled="isViewMode || operationType === 'edit'"
                         filterable readonly @change="handleChangeModel">
                <el-option v-for="item in modelOptions" :key="item.id" :label="item.model" :value="item.id" />
              </el-select>
@@ -58,6 +58,7 @@
                clearable
                @change="handleTestStandardChange"
                style="width: 100%"
                :disabled="isViewMode"
              >
                <el-option
                  v-for="item in testStandardOptions"
@@ -78,21 +79,39 @@
          <el-col :span="12">
            <el-form-item label="数量:" prop="quantity">
              <el-input-number :step="0.01" :min="0" style="width: 100%" v-model="form.quantity" placeholder="请输入"
                               clearable :precision="2" :disabled="supplierQuantityDisabled"/>
                               clearable :precision="2" :disabled="isViewMode || supplierQuantityDisabled"/>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="合格数量:" prop="qualifiedQuantity">
              <el-input-number :step="0.01" :min="0" :max="form.quantity || 0" style="width: 100%"
                               v-model="form.qualifiedQuantity" placeholder="请输入" :precision="2"
                               @change="onQualifiedChange" :disabled="isViewMode"/>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="不合格数量:" prop="unqualifiedQuantity">
              <el-input-number :step="0.01" :min="0" :max="form.quantity || 0" style="width: 100%"
                               v-model="form.unqualifiedQuantity" placeholder="请输入" :precision="2"
                               @change="onUnqualifiedChange" :disabled="isViewMode"/>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="检测单位:" prop="checkCompany">
              <el-input v-model="form.checkCompany" placeholder="请输入" clearable/>
              <el-input v-model="form.checkCompany" placeholder="请输入" clearable :disabled="isViewMode"/>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="检测结果:" prop="checkResult">
              <el-select v-model="form.checkResult">
              <el-select v-model="form.checkResult" :disabled="isViewMode">
                <el-option label="合格" value="合格"/>
                <el-option label="不合格" value="不合格"/>
                <el-option label="部分合格" value="部分合格"/>
              </el-select>
            </el-form-item>
          </el-col>
@@ -100,7 +119,7 @@
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="检验员:" prop="checkName">
              <el-select v-model="form.checkName" placeholder="请选择" clearable style="width: 100%">
              <el-select v-model="form.checkName" placeholder="请选择" clearable style="width: 100%" :disabled="isViewMode">
                <el-option v-for="item in userList" :key="item.nickName" :label="item.nickName" :value="item.nickName"/>
              </el-select>
            </el-form-item>
@@ -115,6 +134,7 @@
                  format="YYYY-MM-DD"
                  clearable
                  style="width: 100%"
                  :disabled="isViewMode"
              />
            </el-form-item>
          </el-col>
@@ -131,13 +151,16 @@
          height="400"
      >
        <template #slot="{ row }">
          <el-input v-model="row.testValue" clearable/>
          <el-input v-model="row.testValue" clearable :disabled="isViewMode"/>
        </template>
      </PIMTable>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitForm">确认</el-button>
          <el-button @click="closeDia">取消</el-button>
          <template v-if="!isViewMode">
            <el-button type="primary" @click="submitForm">确认</el-button>
            <el-button @click="closeDia">取消</el-button>
          </template>
          <el-button v-else @click="closeDia">关闭</el-button>
        </div>
      </template>
    </el-dialog>
@@ -182,6 +205,8 @@
    testStandardId: [{required: false, message: "请选择指标", trigger: "change"}],
    unit: [{required: false, message: "请输入", trigger: "blur"}],
    quantity: [{required: true, message: "请输入", trigger: "blur"}],
    qualifiedQuantity: [{required: true, message: "请输入", trigger: "blur"}],
    unqualifiedQuantity: [{required: true, message: "请输入", trigger: "blur"}],
    checkCompany: [{required: false, message: "请输入", trigger: "blur"}],
    checkResult: [{required: true, message: "请选择检测结果", trigger: "change"}],
  },
@@ -220,6 +245,9 @@
const testStandardOptions = ref([]); // æŒ‡æ ‡é€‰æ‹©ä¸‹æ‹‰æ¡†æ•°æ®
const modelOptions = ref([]);
const userList = ref([]); // æ£€éªŒå‘˜ä¸‹æ‹‰åˆ—表
// æ˜¯å¦ä¸ºæŸ¥çœ‹æ¨¡å¼
const isViewMode = computed(() => operationType.value === 'view');
// ç¼–辑时:productMainId æˆ– purchaseLedgerId ä»»ä¸€æœ‰å€¼åˆ™ä¾›åº”商、数量置灰
const supplierQuantityDisabled = computed(() => {
@@ -260,7 +288,7 @@
  tableData.value = [];
  // å…ˆç¡®ä¿äº§å“æ ‘已加载,否则编辑时产品/规格型号无法反显
  await getProductOptions();
  if (operationType.value === 'edit') {
  if (operationType.value === 'edit' || operationType.value === 'view') {
    // å…ˆä¿å­˜ testStandardId,避免被清空
    const savedTestStandardId = row.testStandardId;
    form.value = {...row}
@@ -294,7 +322,7 @@
            // å¦‚果编辑数据中有 testStandardId,则设置并加载对应的参数
            if (savedTestStandardId) {
              // ç¡®ä¿ç±»åž‹åŒ¹é…ï¼ˆitem.id å¯èƒ½æ˜¯æ•°å­—或字符串)
              const matchedOption = testStandardOptions.value.find(item =>
              const matchedOption = testStandardOptions.value.find(item =>
                item.id == savedTestStandardId || String(item.id) === String(savedTestStandardId)
              );
              if (matchedOption) {
@@ -435,6 +463,10 @@
  tableLoading.value = true;
  getQualityTestStandardParamByTestStandardId(testStandardId).then(res => {
    tableData.value = res.data || [];
    tableData.value = tableData.value.map(item => ({
      ...item,
      id: null
    }));
  }).catch(error => {
    console.error('获取标准参数失败:', error);
    tableData.value = [];
@@ -448,6 +480,32 @@
    tableData.value = res.data;
  })
}
// è‡ªåŠ¨è®¡ç®—åˆæ ¼æ•°é‡å˜åŒ–æ—¶çš„ä¸åˆæ ¼æ•°é‡
const onQualifiedChange = (value) => {
  if (form.value.quantity !== undefined && form.value.quantity !== null) {
    const maxUnqualified = form.value.quantity - value;
    if (maxUnqualified >= 0) {
      form.value.unqualifiedQuantity = maxUnqualified;
    } else {
      form.value.qualifiedQuantity = form.value.quantity;
      form.value.unqualifiedQuantity = 0;
    }
  }
};
// è‡ªåŠ¨è®¡ç®—ä¸åˆæ ¼æ•°é‡å˜åŒ–æ—¶çš„åˆæ ¼æ•°é‡
const onUnqualifiedChange = (value) => {
  if (form.value.quantity !== undefined && form.value.quantity !== null) {
    const maxQualified = form.value.quantity - value;
    if (maxQualified >= 0) {
      form.value.qualifiedQuantity = maxQualified;
    } else {
      form.value.unqualifiedQuantity = form.value.quantity;
      form.value.qualifiedQuantity = 0;
    }
  }
};
// å…³é—­å¼¹æ¡†
const closeDia = () => {
  proxy.resetForm("formRef");
@@ -464,4 +522,4 @@
<style scoped>
</style>
</style>
src/views/qualityManagement/rawMaterialInspection/index.vue
@@ -124,8 +124,23 @@
    prop: "unit",
  },
  {
    label: "数量",
    label: "总数量",
    prop: "quantity",
    width: 100
  },
  {
    label: "合格数量",
    prop: "qualifiedQuantity",
    width: 100
  },
  {
    label: "不合格数量",
    prop: "unqualifiedQuantity",
    width: 100
  },
  {
    label: "检测单位",
    prop: "checkCompany",
    width: 120
  },
  {
@@ -143,7 +158,7 @@
      } else if (params === '合格') {
        return "success";
      } else {
        return null;
        return 'danger';
      }
    },
  },
@@ -182,6 +197,13 @@
                }
      },
      {
        name: "查看",
        type: "text",
        clickFun: (row) => {
          openForm("view", row);
        },
      },
      {
        name: "附件",
        type: "text",
        clickFun: (row) => {
src/views/reportAnalysis/PSIDataAnalysis/components/center-bottom.vue
@@ -23,7 +23,7 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, inject, watch } from 'vue'
import * as echarts from 'echarts'
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from './PanelHeader.vue'
@@ -151,6 +151,13 @@
  fetchData()
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    fetchData()
  })
}
onMounted(() => {
  fetchData()
})
src/views/reportAnalysis/PSIDataAnalysis/components/center-center.vue
@@ -27,7 +27,7 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, inject, watch } from 'vue'
import Echarts from '@/components/Echarts/echarts.vue'
import { productTurnoverDays } from '@/api/viewIndex.js'
@@ -82,6 +82,13 @@
    })
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    fetchData()
  })
}
onMounted(() => {
  fetchData()
})
src/views/reportAnalysis/PSIDataAnalysis/components/center-top.vue
@@ -24,7 +24,7 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, inject, watch } from 'vue'
import { salesPurchaseStorageProductCount } from '@/api/viewIndex.js'
const statItems = ref([])
@@ -52,6 +52,13 @@
    })
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    fetchData()
  })
}
onMounted(() => {
  fetchData()
})
src/views/reportAnalysis/PSIDataAnalysis/components/left-bottom.vue
@@ -22,7 +22,7 @@
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
import { ref, onMounted, onBeforeUnmount, computed, inject, watch } from 'vue'
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from './PanelHeader.vue'
import CarouselCards from './CarouselCards.vue'
@@ -205,6 +205,13 @@
    })
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    fetchData()
  })
}
onMounted(() => {
  fetchData()
  initBackground()
src/views/reportAnalysis/PSIDataAnalysis/components/left-top.vue
@@ -21,7 +21,7 @@
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
import { ref, onMounted, onBeforeUnmount, computed, inject, watch } from 'vue'
import { productSalesAnalysis } from '@/api/viewIndex.js'
import PanelHeader from './PanelHeader.vue'
import CarouselCards from './CarouselCards.vue'
@@ -175,6 +175,13 @@
    })
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    fetchData()
  })
}
onMounted(() => {
  fetchData()
  initBackground()
src/views/reportAnalysis/PSIDataAnalysis/index.vue
@@ -43,7 +43,7 @@
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { ref, onMounted, onBeforeUnmount, nextTick, provide } from 'vue'
import autofit from 'autofit.js'
import LeftBottom from './components/left-bottom.vue'
import CenterCenter from './components/center-center.vue'
@@ -65,6 +65,12 @@
// ç”¨æˆ·store
const userStore = useUserStore()
/** ä¸Ž dataDashboard å…±ç”¨æ³¨å…¥åï¼Œå­ç»„件(含复用的 right-top/right-bottom)每分钟刷新 */
const DASHBOARD_REFRESH_MS = 60 * 1000
const dataDashboardRefreshTick = ref(0)
provide('dataDashboardRefreshTick', dataDashboardRefreshTick)
let dashboardPollTimer = null
// è®¡ç®—缩放比例
const calculateScale = () => {
@@ -140,9 +146,17 @@
  window.addEventListener('fullscreenchange', handleFullscreenChange)
  window.addEventListener('webkitfullscreenchange', handleFullscreenChange)
  window.addEventListener('MSFullscreenChange', handleFullscreenChange)
  dashboardPollTimer = setInterval(() => {
    dataDashboardRefreshTick.value++
  }, DASHBOARD_REFRESH_MS)
})
onBeforeUnmount(() => {
  if (dashboardPollTimer) {
    clearInterval(dashboardPollTimer)
    dashboardPollTimer = null
  }
  window.removeEventListener('resize', handleResize)
  window.removeEventListener('fullscreenchange', handleFullscreenChange)
  window.removeEventListener('webkitfullscreenchange', handleFullscreenChange)
src/views/reportAnalysis/dataDashboard/components/basic/center-bottom.vue
@@ -20,7 +20,7 @@
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { ref, onMounted, computed, inject, watch } from 'vue'
import { deptStaffDistribution } from '@/api/viewIndex.js'
import PanelHeader from '../PanelHeader.vue'
import Echarts from '@/components/Echarts/echarts.vue'
@@ -148,6 +148,13 @@
  })
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    getDeptStaffDistribution()
  })
}
onMounted(() => {
  getDeptStaffDistribution()
})
src/views/reportAnalysis/dataDashboard/components/basic/center-top.vue
@@ -110,7 +110,7 @@
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { ref, onMounted, onBeforeUnmount, nextTick, inject, watch } from 'vue'
import { homeTodos, summaryStatistics } from '@/api/viewIndex.js'
import { getLedgerPage } from '@/api/equipmentManagement/ledger.js'
import { getRepairPage } from '@/api/equipmentManagement/repair.js'
@@ -175,8 +175,23 @@
  })
}
const destroyTodoListScroll = () => {
  const todoListEl = refTodoList.value
  if (todoListEl) {
    if (todoListEl._animationFrame) {
      cancelAnimationFrame(todoListEl._animationFrame)
      todoListEl._animationFrame = null
    }
    if (todoListEl._pauseTimer) {
      clearInterval(todoListEl._pauseTimer)
      todoListEl._pauseTimer = null
    }
  }
}
// åˆå§‹åŒ–待办事项列表滚动功能
const initTodoListScroll = () => {
  destroyTodoListScroll()
  const todoListEl = refTodoList.value
  // å¼ºåˆ¶å¯ç”¨æ»šåŠ¨ï¼Œä¸æ£€æŸ¥ä»»ä½•æ¡ä»¶
  if (todoListEl) {
@@ -259,6 +274,7 @@
// å¾…办事项
const todoInfoS = () => {
  destroyTodoListScroll()
  homeTodos().then((res) => {
    todoList.value = res.data
    // åœ¨èŽ·å–åˆ°å¾…åŠžäº‹é¡¹æ•°æ®åŽï¼Œåˆå§‹åŒ–æ»šåŠ¨åŠŸèƒ½
@@ -268,25 +284,25 @@
  })
}
onMounted(() => {
const refreshCenterTopData = () => {
  getNum()
  getLedgerNum()
  todoInfoS()
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    refreshCenterTopData()
  })
}
onMounted(() => {
  refreshCenterTopData()
})
onBeforeUnmount(() => {
  // æ¸…理待办事项列表的动画和定时器
  const todoListEl = refTodoList.value
  if (todoListEl) {
    if (todoListEl._animationFrame) {
      cancelAnimationFrame(todoListEl._animationFrame)
      todoListEl._animationFrame = null
    }
    if (todoListEl._pauseTimer) {
      clearInterval(todoListEl._pauseTimer)
      todoListEl._pauseTimer = null
    }
  }
  destroyTodoListScroll()
})
</script>
src/views/reportAnalysis/dataDashboard/components/basic/left-bottom.vue
@@ -40,7 +40,7 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, inject, watch } from 'vue'
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from '../PanelHeader.vue'
import DateTypeSwitch from '../DateTypeSwitch.vue'
@@ -192,6 +192,13 @@
  getCustomerRevenueAnalysis()
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    getCustomerRevenueAnalysis()
  })
}
onMounted(() => {
  fetchCustomerOptions()
})
src/views/reportAnalysis/dataDashboard/components/basic/left-top.vue
@@ -21,7 +21,7 @@
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { ref, onMounted, onBeforeUnmount, inject, watch } from 'vue'
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from '../PanelHeader.vue'
import { productCategoryDistribution } from '@/api/viewIndex.js'
@@ -207,6 +207,13 @@
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    loadData()
  })
}
onMounted(() => {
  loadData()
  initBackground()
src/views/reportAnalysis/dataDashboard/components/basic/right-bottom.vue
@@ -22,7 +22,7 @@
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { ref, onMounted, computed, inject, watch } from 'vue'
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from '../PanelHeader.vue'
import DateTypeSwitch from '../DateTypeSwitch.vue'
@@ -308,6 +308,13 @@
  fetchCustomerRanking()
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    fetchCustomerRanking()
  })
}
onMounted(() => {
  fetchCustomerRanking()
})
src/views/reportAnalysis/dataDashboard/components/basic/right-top.vue
@@ -21,7 +21,7 @@
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { ref, onMounted, computed, inject, watch } from 'vue'
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from '../PanelHeader.vue'
import DateTypeSwitch from '../DateTypeSwitch.vue'
@@ -331,6 +331,13 @@
  fetchSupplierRanking()
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    fetchSupplierRanking()
  })
}
onMounted(() => {
  fetchSupplierRanking()
})
src/views/reportAnalysis/dataDashboard/index.vue
@@ -44,7 +44,7 @@
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { ref, onMounted, onBeforeUnmount, nextTick, provide } from 'vue'
import autofit from 'autofit.js'
import LeftTop from './components/basic/left-top.vue'
import LeftBottom from './components/basic/left-bottom.vue'
@@ -65,6 +65,12 @@
// ç”¨æˆ·store
const userStore = useUserStore()
// å¤§å±æŽ¥å£è½®è¯¢é—´éš”
const DASHBOARD_REFRESH_MS = 60 * 1000
const dataDashboardRefreshTick = ref(0)
provide('dataDashboardRefreshTick', dataDashboardRefreshTick)
let dashboardPollTimer = null
// è®¡ç®—缩放比例
const calculateScale = () => {
@@ -140,9 +146,17 @@
  window.addEventListener('fullscreenchange', handleFullscreenChange)
  window.addEventListener('webkitfullscreenchange', handleFullscreenChange)
  window.addEventListener('MSFullscreenChange', handleFullscreenChange)
  dashboardPollTimer = setInterval(() => {
    dataDashboardRefreshTick.value++
  }, DASHBOARD_REFRESH_MS)
})
onBeforeUnmount(() => {
  if (dashboardPollTimer) {
    clearInterval(dashboardPollTimer)
    dashboardPollTimer = null
  }
  window.removeEventListener('resize', handleResize)
  window.removeEventListener('fullscreenchange', handleFullscreenChange)
  window.removeEventListener('webkitfullscreenchange', handleFullscreenChange)
src/views/reportAnalysis/productionAnalysis/components/center-bottom.vue
@@ -51,7 +51,7 @@
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { ref, onMounted, onBeforeUnmount, nextTick, inject, watch } from 'vue'
import { getProgressStatistics } from '@/api/viewIndex.js'
import PanelHeader from './PanelHeader.vue'
import CarouselCards from './CarouselCards.vue'
@@ -132,17 +132,22 @@
  }, 150)
}
const initProgressTableScroll = () => {
  const tableContainer = progressTableRef.value
  if (!tableContainer) return
const stopProgressTableScroll = () => {
  if (progressTableScrollTimer.value) {
    cancelAnimationFrame(progressTableScrollTimer.value)
    progressTableScrollTimer.value = null
  }
  if (tableContainer._pauseTimer) {
  const tableContainer = progressTableRef.value
  if (tableContainer?._pauseTimer) {
    clearInterval(tableContainer._pauseTimer)
    tableContainer._pauseTimer = null
  }
}
const initProgressTableScroll = () => {
  const tableContainer = progressTableRef.value
  if (!tableContainer) return
  stopProgressTableScroll()
  const tbody = tableContainer.querySelector('tbody')
  if (!tbody) return
  const originalCount = progressTableData.value.length
@@ -198,6 +203,7 @@
const progressStatisticsInfo = () => {
  getProgressStatistics()
    .then((res) => {
      stopProgressTableScroll()
      if (!res || !res.data) return
      const obj = {
        totalOrderCount: res.data.totalOrderCount || 0,
@@ -224,14 +230,19 @@
    })
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    progressStatisticsInfo()
  })
}
onMounted(() => {
  progressStatisticsInfo()
})
onBeforeUnmount(() => {
  if (progressTableScrollTimer.value) {
    cancelAnimationFrame(progressTableScrollTimer.value)
  }
  stopProgressTableScroll()
  if (tableScrollTimeout.value) clearTimeout(tableScrollTimeout.value)
  const tableContainer = progressTableRef.value
  if (tableContainer?._pauseTimer) {
src/views/reportAnalysis/productionAnalysis/components/center-center.vue
@@ -27,7 +27,7 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, inject, watch } from 'vue'
import * as echarts from 'echarts'
import Echarts from '@/components/Echarts/echarts.vue'
import { inputOutputAnalysis } from '@/api/viewIndex.js'
@@ -146,6 +146,13 @@
    })
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    fetchData()
  })
}
onMounted(() => {
  fetchData()
})
src/views/reportAnalysis/productionAnalysis/components/center-top.vue
@@ -24,7 +24,7 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, inject, watch } from 'vue'
import { orderCount } from '@/api/viewIndex.js'
const statItems = ref([])
@@ -52,6 +52,13 @@
    })
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    fetchData()
  })
}
onMounted(() => {
  fetchData()
})
src/views/reportAnalysis/productionAnalysis/components/left-bottom.vue
@@ -22,7 +22,7 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, inject, watch } from 'vue'
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from './PanelHeader.vue'
import CarouselCards from './CarouselCards.vue'
@@ -143,6 +143,13 @@
    })
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    workInProcessTurnoverInfo()
  })
}
onMounted(() => {
  workInProcessTurnoverInfo()
})
src/views/reportAnalysis/productionAnalysis/components/left-top.vue
@@ -23,7 +23,7 @@
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
import { ref, onMounted, onBeforeUnmount, computed, inject, watch } from 'vue'
import { processOutputAnalysis } from '@/api/viewIndex.js'
import PanelHeader from './PanelHeader.vue'
import Echarts from '@/components/Echarts/echarts.vue'
@@ -170,6 +170,13 @@
  fetchData()
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    fetchData()
  })
}
onMounted(() => {
  fetchData()
  initBackground()
src/views/reportAnalysis/productionAnalysis/components/right-bottom.vue
@@ -22,7 +22,7 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, inject, watch } from 'vue'
import { productionAccountingAnalysis } from '@/api/viewIndex.js'
import PanelHeader from './PanelHeader.vue'
import DateTypeSwitch from './DateTypeSwitch.vue'
@@ -157,6 +157,13 @@
    })
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    fetchData()
  })
}
onMounted(() => {
  fetchData()
})
src/views/reportAnalysis/productionAnalysis/components/right-top.vue
@@ -22,7 +22,7 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, inject, watch } from 'vue'
import { workOrderEfficiencyAnalysis } from '@/api/viewIndex.js'
import PanelHeader from './PanelHeader.vue'
import Echarts from '@/components/Echarts/echarts.vue'
@@ -152,6 +152,13 @@
    })
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    fetchData()
  })
}
onMounted(() => {
  fetchData()
})
src/views/reportAnalysis/productionAnalysis/index.vue
@@ -44,7 +44,7 @@
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { ref, onMounted, onBeforeUnmount, nextTick, provide } from 'vue'
import autofit from 'autofit.js'
import LeftBottom from './components/left-bottom.vue'
import CenterCenter from './components/center-center.vue'
@@ -66,6 +66,12 @@
// ç”¨æˆ·store
const userStore = useUserStore()
/** ä¸Žå…¶å®ƒé©¾é©¶èˆ±å…±ç”¨æ³¨å…¥åï¼Œå­ç»„件每分钟刷新接口数据 */
const DASHBOARD_REFRESH_MS = 60 * 1000
const dataDashboardRefreshTick = ref(0)
provide('dataDashboardRefreshTick', dataDashboardRefreshTick)
let dashboardPollTimer = null
// è®¡ç®—缩放比例
const calculateScale = () => {
@@ -141,9 +147,17 @@
  window.addEventListener('fullscreenchange', handleFullscreenChange)
  window.addEventListener('webkitfullscreenchange', handleFullscreenChange)
  window.addEventListener('MSFullscreenChange', handleFullscreenChange)
  dashboardPollTimer = setInterval(() => {
    dataDashboardRefreshTick.value++
  }, DASHBOARD_REFRESH_MS)
})
onBeforeUnmount(() => {
  if (dashboardPollTimer) {
    clearInterval(dashboardPollTimer)
    dashboardPollTimer = null
  }
  window.removeEventListener('resize', handleResize)
  window.removeEventListener('fullscreenchange', handleFullscreenChange)
  window.removeEventListener('webkitfullscreenchange', handleFullscreenChange)
src/views/safeProduction/safeWorkApproval/components/infoFormDia.vue
@@ -218,7 +218,7 @@
      approveDeptName: "",
      approveReason: "",
      checkResult: "",
      storageBlobDTOs: [],
      storageBlobDTOS: [],
      approverList: [], // æ–°å¢žå­—段,存储所有节点的审批人id
      startDate: "", // è¯·å‡å¼€å§‹æ—¶é—´
      endDate: "", // è¯·å‡ç»“束时间
@@ -298,7 +298,7 @@
      approveProcessGetInfo({ id: row.approveId, approveReason: "1" }).then(
        res => {
          form.value = { ...res.data };
          form.value.storageBlobDTOs = res.data.storageBlobVOS;
          form.value.storageBlobDTOS = res.data.storageBlobVOS;
          // åæ˜¾å®¡æ‰¹äºº
          if (res.data && res.data.approveUserIds) {
            const userIds = res.data.approveUserIds.split(",");
@@ -388,7 +388,7 @@
        return;
      }
    }
    form.value.storageBlobDTOs = fileList.value;
    form.value.storageBlobDTOS = fileList.value;
    proxy.$refs.formRef.validate(valid => {
      if (valid) {
        if (operationType.value === "add" || currentApproveStatus.value == 3) {
src/views/salesManagement/salesLedger/index.vue
@@ -1578,7 +1578,7 @@
    selectedQuotation.value = null;
    let userLists = await userListNoPage();
    userList.value = userLists.data;
    listCustomer({ current: -1, size: -1 }).then(res => {
    listCustomer({ current: -1, size: -1, type: 0 }).then(res => {
      customerOption.value = res.data.records;
    });
    form.value.entryPerson = userStore.id;
@@ -1705,6 +1705,8 @@
        taxExclusiveTotalPrice: taxExclusiveTotalPrice,
        invoiceType: "增普票",
        isProduction: true,
        productId: p.productId,
        productModelId: p.productModelId
      };
    });
@@ -2652,7 +2654,7 @@
    // å‘货状态必须是"待发货"或"审核拒绝"
    const statusStr = shippingStatus ? String(shippingStatus).trim() : "";
    return statusStr === "待发货" || statusStr === "审核拒绝";
    return statusStr === "待发货" || statusStr === "审核拒绝" || statusStr === "部分发货";
  };
  // æ‰“开附件弹窗
src/views/salesManagement/salesQuotation/index.vue
@@ -187,9 +187,9 @@
            </el-table-column>
            <el-table-column prop="specification" label="规格型号" width="200">
              <template #default="scope">
                <el-form-item :prop="`products.${scope.$index}.specificationId`" class="product-table-form-item">
                <el-form-item :prop="`products.${scope.$index}.productModelId`" class="product-table-form-item">
                  <el-select
                    v-model="scope.row.specificationId"
                    v-model="scope.row.productModelId"
                    placeholder="请选择"
                    clearable
                    @change="getProductModel($event, scope.row)"
@@ -239,10 +239,10 @@
          </template>
          <div class="form-content">
            <el-form-item label="备注" prop="remark">
              <el-input
                type="textarea"
                v-model="form.remark"
                placeholder="请输入备注信息(选填)"
              <el-input
                type="textarea"
                v-model="form.remark"
                placeholder="请输入备注信息(选填)"
                :rows="4"
                maxlength="500"
                show-word-limit
@@ -270,7 +270,7 @@
          <span style="font-size: 18px; color: #e6a23c; font-weight: bold;">Â¥{{ currentQuotation.totalAmount?.toFixed(2) }}</span>
        </el-descriptions-item>
      </el-descriptions>
      <div style="margin: 20px 0;">
        <h4>产品明细</h4>
        <el-table :data="currentQuotation.products" border style="width: 100%">
@@ -354,7 +354,7 @@
const productRowRules = {
  productId: [{ required: true, message: '请选择产品名称', trigger: 'change' }],
  specificationId: [{ required: true, message: '请选择规格型号', trigger: 'change' }],
  productModelId: [{ required: true, message: '请选择规格型号', trigger: 'change' }],
  unit: [{ required: true, message: '请填写单位', trigger: 'blur' }],
  unitPrice: [{ required: true, message: '请填写单价', trigger: 'change' }]
}
@@ -362,7 +362,7 @@
  const r = { ...baseRules }
  ;(form.products || []).forEach((_, i) => {
    r[`products.${i}.productId`] = productRowRules.productId
    r[`products.${i}.specificationId`] = productRowRules.specificationId
    r[`products.${i}.productModelId`] = productRowRules.productModelId
    r[`products.${i}.unit`] = productRowRules.unit
    r[`products.${i}.unitPrice`] = productRowRules.unitPrice
  })
@@ -433,7 +433,7 @@
        if (children && children.length > 0) {
            newItem.children = convertIdToValue(children);
        }
        return newItem;
    });
}
@@ -457,7 +457,7 @@
        row.productId = '';
        row.product = '';
        row.modelOptions = [];
        row.specificationId = '';
        row.productModelId = '';
        row.specification = '';
        row.unit = '';
        return;
@@ -478,13 +478,13 @@
    if (!row) return;
    // å¦‚果清空选择,则清空相关字段
    if (!value) {
        row.specificationId = '';
        row.productModelId = '';
        row.specification = '';
        row.unit = '';
        return;
    }
    // æ›´æ–° specificationId(v-model å·²ç»è‡ªåŠ¨æ›´æ–°ï¼Œè¿™é‡Œç¡®ä¿ä¸€è‡´æ€§ï¼‰
    row.specificationId = value;
    // æ›´æ–° productModelId(v-model å·²ç»è‡ªåŠ¨æ›´æ–°ï¼Œè¿™é‡Œç¡®ä¿ä¸€è‡´æ€§ï¼‰
    row.productModelId = value;
    const modelOptions = row.modelOptions || [];
    const index = modelOptions.findIndex((item) => item.id === value);
    if (index !== -1) {
@@ -523,7 +523,7 @@
    products: row.products ? row.products.map(product => ({
      productId: product.productId || '',
      product: product.product || product.productName || '',
      specificationId: product.specificationId || '',
      productModelId: product.productModelId || '',
      specification: product.specification || '',
      quantity: product.quantity || 0,
      unit: product.unit || '',
@@ -560,32 +560,32 @@
    const resolvedProductId = product.productId
      ? Number(product.productId)
      : findNodeIdByLabel(productOptions.value, productName) || ''
    // å¦‚果有产品ID,加载对应的规格型号列表
    let modelOptions = [];
    let resolvedSpecificationId = product.specificationId || '';
    let resolvedProductModelId = product.productModelId || '';
    if (resolvedProductId) {
      try {
        const res = await modelList({ id: resolvedProductId });
        modelOptions = res || [];
        // å¦‚果返回的数据没有 specificationId,但有 specification åç§°ï¼Œæ ¹æ®åç§°æŸ¥æ‰¾ ID
        if (!resolvedSpecificationId && product.specification) {
        // å¦‚果返回的数据没有 productModelId,但有 specification åç§°ï¼Œæ ¹æ®åç§°æŸ¥æ‰¾ ID
        if (!resolvedProductModelId && product.specification) {
          const foundModel = modelOptions.find(item => item.model === product.specification);
          if (foundModel) {
            resolvedSpecificationId = foundModel.id;
            resolvedProductModelId = foundModel.id;
          }
        }
      } catch (error) {
        console.error('加载规格型号失败:', error);
      }
    }
    return {
      productId: resolvedProductId,
      product: productName,
      specificationId: resolvedSpecificationId,
      productModelId: resolvedProductModelId,
      specification: product.specification || '',
      quantity: product.quantity || 0,
      unit: product.unit || '',
@@ -649,8 +649,7 @@
    productId: '',
    product: '',
    productName: '',
    specificationId: '',
    specification: '',
    productModelId: '',
    quantity: 1,
    unit: '',
    unitPrice: 0,
@@ -755,7 +754,7 @@
        products: item.products ? item.products.map(product => ({
          productId: product.productId || '',
          product: product.product || product.productName || '',
          specificationId: product.specificationId || '',
          productModelId: product.productModelId || '',
          specification: product.specification || '',
          quantity: product.quantity || 0,
          unit: product.unit || '',
@@ -803,16 +802,16 @@
  padding: 10px 0;
  max-height: calc(100vh - 200px);
  overflow-y: auto;
  &::-webkit-scrollbar {
    width: 6px;
    height: 6px;
  }
  &::-webkit-scrollbar-thumb {
    background: #c1c1c1;
    border-radius: 3px;
    &:hover {
      background: #a8a8a8;
    }
@@ -829,17 +828,17 @@
  margin-bottom: 24px;
  border-radius: 8px;
  transition: all 0.3s ease;
  &:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08) !important;
  }
  :deep(.el-card__header) {
    padding: 16px 20px;
    background: linear-gradient(135deg, #f5f7fa 0%, #ffffff 100%);
    border-bottom: 1px solid #ebeef5;
  }
  :deep(.el-card__body) {
    padding: 20px;
  }
@@ -849,19 +848,19 @@
  display: flex;
  align-items: center;
  gap: 8px;
  .card-icon {
    font-size: 18px;
    color: #409eff;
  }
  .card-title {
    font-weight: 600;
    font-size: 16px;
    color: #303133;
    flex: 1;
  }
  .header-btn {
    margin-left: auto;
  }
@@ -885,20 +884,20 @@
.product-table {
  :deep(.el-table__header) {
    background-color: #f5f7fa;
    th {
      background-color: #f5f7fa !important;
      color: #606266;
      font-weight: 600;
    }
  }
  :deep(.el-table__row) {
    &:hover {
      background-color: #f5f7fa;
    }
  }
  :deep(.el-table__cell) {
    padding: 12px 0;
  }