8a5fd737f22ff39f045340adc91971bcedd8901b..ff609c6ba2f52818dad141407c3265abc883eb4f
2026-05-18 gaoluyang
进销存pro 1.菜单栏样式修改
ff609c 对比 | 目录
2026-05-18 zhangwencui
第二次报工时,待生产数量需要自动计算
65f68e 对比 | 目录
2026-05-18 zhangwencui
工序未关联设备时,编辑此工序时,点击关联设备时会回显0问题
49963c 对比 | 目录
2026-05-18 yuan
添加旭晨电器logo
5ae066 对比 | 目录
2026-05-18 zhangwencui
生产追溯增加返回
b69df2 对比 | 目录
2026-05-18 zhangwencui
领料时未输入内容,不允许保存
638843 对比 | 目录
2026-05-18 zhangwencui
Merge branch 'dev_NEW_pro' of http://114.132.189.42:9002/r/product-inventor...
d98085 对比 | 目录
2026-05-18 zhangwencui
设备保养领用备件时,提示报错问题修改
72feb8 对比 | 目录
2026-05-18
Merge remote-tracking branch 'origin/dev_NEW_pro' into dev_NEW_pro
e8152d 对比 | 目录
2026-05-18
feat(multiple): 为构建过程添加环境变量管理功能
888d62 对比 | 目录
2026-05-18 zhangwencui
BOM中的工序若是平级时,限制只能相同工序
4241ad 对比 | 目录
2026-05-18 zhangwencui
设备保养tab页名称修改
675fec 对比 | 目录
2026-05-18 gaoluyang
Merge remote-tracking branch 'origin/dev_NEW_pro' into dev_NEW_pro
e641fd 对比 | 目录
2026-05-18 gaoluyang
进销存pro 1.菜单栏二级目录没有箭头问题
553468 对比 | 目录
2026-05-18 liyong
Merge remote-tracking branch 'origin/dev_NEW_pro' into dev_NEW_pro
721bb2 对比 | 目录
2026-05-18 liyong
fix(form): 修正表单字段绑定和初始化问题
c935ca 对比 | 目录
2026-05-18 zhangwencui
Merge branch 'dev_NEW_pro' of http://114.132.189.42:9002/r/product-inventor...
1e3fce 对比 | 目录
2026-05-18 zhangwencui
展示巡检结果和异常描述
457149 对比 | 目录
2026-05-18
feat(multiple): 为构建过程添加环境变量管理功能
c2242e 对比 | 目录
2026-05-18
Merge remote-tracking branch 'origin/dev_NEW_pro' into dev_NEW_pro
48ea97 对比 | 目录
2026-05-18
refactor(multiple-build): 重构构建脚本以支持多公司配置
687fdb 对比 | 目录
2026-05-18 yyb
1
32b983 对比 | 目录
2026-05-18 yyb
合并OA流程页面文件夹 dev-new_pro_OA -> dev_NEW_pro
9bfda8 对比 | 目录
2026-05-18 yuan
Merge remote-tracking branch 'origin/dev_NEW_pro' into dev_NEW_pro
eecd51 对比 | 目录
2026-05-18 yuan
feat(search): 基础产品维护子节点模糊查询
d1d3ea 对比 | 目录
2026-05-18 buhuazhen
feat(质量检验模块): 新增查看按钮并优化查看弹窗
4951f4 对比 | 目录
2026-05-16 yuan
新增晋和园logo
c5f708 对比 | 目录
已添加5个文件
已修改25个文件
2059 ■■■■ 文件已修改
multiple/assets/favicon/JHYfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/XCDQfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/JHYLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/XCDQLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/config.json 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
multiple/multiple-build.js 192 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/styles/sidebar.scss 404 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/styles/variables.module.scss 121 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AIChatSidebar/assistants/index.js 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AIChatSidebar/assistants/salesAssistant.js 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AIChatSidebar/index.vue 378 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Sidebar/Logo.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Sidebar/SidebarItem.vue 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Sidebar/index.vue 109 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/product/index.vue 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/inspectionManagement/index.vue 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/Form/MaintenanceModal.vue 357 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/index.vue 24 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productStructure/Detail/index.vue 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionProcess/index.vue 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionTraceability/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/workOrderManagement/index.vue 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/finalInspection/components/formDia.vue 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/finalInspection/index.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/nonconformingManagement/components/formDia.vue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/processInspection/components/formDia.vue 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/processInspection/index.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/rawMaterialInspection/components/formDia.vue 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/rawMaterialInspection/index.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/JHYfavicon.ico
multiple/assets/favicon/XCDQfavicon.ico
multiple/assets/logo/JHYLogo.png
multiple/assets/logo/XCDQLogo.png
multiple/config.json
@@ -150,6 +150,24 @@
    "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/assets/styles/sidebar.scss
@@ -1,133 +1,152 @@
#app {
  .main-container {
    min-height: 100%;
    transition: margin-left 0.28s;
    margin-left: $base-sidebar-width;
    position: relative;
    background: transparent;
  }
  .sidebarHide {
    margin-left: 0 !important;
  }
  .sidebar-container {
    transition: width 0.28s;
    width: $base-sidebar-width !important;
    height: 100%;
    position: fixed;
    font-size: 0px;
    top: 0;
    bottom: 0;
    left: 0;
    z-index: 1001;
    overflow: hidden;
    padding: 12px 0 16px 16px;
    background: transparent;
    box-shadow: none;
    // reset element-ui css
    .horizontal-collapse-transition {
      transition: 0s width ease-in-out, 0s padding-left ease-in-out,
        0s padding-right ease-in-out;
    }
    .scrollbar-wrapper {
      overflow-x: hidden !important;
    }
    .el-scrollbar__bar.is-vertical {
      right: 0px;
    }
    .el-scrollbar {
      height: 100%;
    }
    &.has-logo {
      .el-scrollbar {
        height: calc(100% - 72px);
        margin-top: 10px;
      }
    }
    .is-horizontal {
      display: none;
    }
    a {
      display: inline-block;
      width: 100%;
      overflow: hidden;
    }
    .svg-icon {
      margin-right: 16px;
    }
    .el-menu {
      border: none;
      height: 100%;
      width: 100% !important;
      padding: 10px 8px 18px;
      border-radius: 22px;
      background: var(--menu-surface);
      backdrop-filter: blur(18px);
      box-shadow: var(--shadow-sm);
#app {
  .main-container {
    min-height: 100%;
    transition: margin-left 0.28s;
    margin-left: $base-sidebar-width;
    position: relative;
    background: transparent;
  }
  .sidebarHide {
    margin-left: 0 !important;
  }
  .sidebar-container {
    transition: width 0.28s;
    width: $base-sidebar-width !important;
    height: 100%;
    position: fixed;
    font-size: 0px;
    top: 0;
    bottom: 0;
    left: 0;
    z-index: 1001;
    overflow: hidden;
    padding: 12px 0 16px 16px;
    background: transparent;
    box-shadow: none;
    // reset element-ui css
    .horizontal-collapse-transition {
      transition: 0s width ease-in-out, 0s padding-left ease-in-out,
        0s padding-right ease-in-out;
    }
    .el-menu-item,
    .menu-title {
      overflow: hidden !important;
      text-overflow: ellipsis !important;
      white-space: nowrap !important;
    }
    .el-menu-item .el-menu-tooltip__trigger {
      display: inline-block !important;
    }
    // menu hover
    .submenu-title-noDropdown,
    .el-sub-menu__title {
      &:hover {
        background-color: var(--menu-hover) !important;
        border-radius: 14px;
    .scrollbar-wrapper {
      overflow-x: hidden !important;
    }
    .el-scrollbar__bar.is-vertical {
      right: 0px;
    }
    .el-scrollbar {
      height: 100%;
    }
    &.has-logo {
      .el-scrollbar {
        height: calc(100% - 72px);
        margin-top: 10px;
      }
    }
    & .theme-light .is-active > .el-sub-menu__title {
      color: var(--current-color) !important;
    }
    .is-horizontal {
      display: none;
    }
    a {
      display: inline-block;
      width: 100%;
      overflow: hidden;
    }
    .svg-icon {
      margin-right: 16px;
    }
    .el-menu {
      border: 1px solid var(--surface-border) !important;
      height: 100%;
      width: 100% !important;
      padding: 12px 10px 20px;
      border-radius: var(--radius-lg);
      background: var(--menu-surface);
      backdrop-filter: blur(20px);
      box-shadow: var(--shadow-sm);
      transition: all 0.3s ease;
    }
    .el-menu-item,
    .menu-title {
      overflow: hidden !important;
      text-overflow: ellipsis !important;
      white-space: nowrap !important;
    }
    .el-menu-item .el-menu-tooltip__trigger {
      display: inline-block !important;
    }
    // menu hover - ä¼˜åŒ–后的悬停效果
    .submenu-title-noDropdown,
    .el-sub-menu__title {
      transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
      border: none !important;
      &:hover {
        background-color: var(--menu-hover) !important;
        border-radius: var(--radius-sm);
        transform: translateX(2px);
      }
    }
    & .theme-light .is-active > .el-sub-menu__title,
    & .theme-dark .is-active > .el-sub-menu__title {
      color: var(--menu-active-text) !important;
      background: var(--menu-active-bg) !important;
      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
      border: none !important;
    }
    & .nest-menu .el-sub-menu > .el-sub-menu__title,
    & .el-sub-menu .el-menu-item {
      min-width: 0 !important;
      margin: 0 12px 6px;
      width: calc(100% - 24px);
      padding-left: 8px !important;
      padding-right: 8px !important;
      margin: 0 10px 5px;
      width: calc(100% - 20px);
      padding-left: 10px !important;
      padding-right: 10px !important;
      box-sizing: border-box;
      border-radius: var(--radius-xs);
      transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
      color: var(--sidebar-text);
      border: none !important;
      &:hover {
        background-color: var(--menu-hover) !important;
        transform: translateX(2px);
      }
      &.is-active {
        background-color: var(--menu-active-bg) !important;
        border-radius: 14px;
        background: var(--menu-active-bg) !important;
        border-radius: var(--radius-sm);
        color: var(--menu-active-text) !important;
        font-weight: 500;
        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
      }
    }
    & .theme-light .nest-menu .el-sub-menu > .el-sub-menu__title,
    & .theme-light .el-sub-menu .el-menu-item {
      //background-color: transparent;
    & .theme-light .nest-menu .el-sub-menu > .el-sub-menu__title,
    & .theme-light .el-sub-menu .el-menu-item,
    & .theme-dark .nest-menu .el-sub-menu > .el-sub-menu__title,
    & .theme-dark .el-sub-menu .el-menu-item {
      &:hover {
        background-color: var(--menu-hover) !important;
        border-radius: 14px;
        border-radius: var(--radius-xs);
      }
    }
  }
  .hideSidebar {
    .sidebar-container {
      width: 68px !important;
@@ -138,7 +157,7 @@
    .main-container {
      margin-left: 84px;
    }
    .submenu-title-noDropdown {
      padding: 0 !important;
      position: relative;
@@ -225,99 +244,102 @@
            width: 0;
            overflow: hidden;
            visibility: hidden;
            display: inline-block;
          }
          & > i {
            height: 0;
            width: 0;
            overflow: hidden;
            visibility: hidden;
            display: inline-block;
            display: inline-block;
          }
        }
      }
    }
  }
  .el-menu--collapse .el-menu .el-sub-menu {
    min-width: $base-sidebar-width !important;
  }
  // mobile responsive
  .mobile {
    .main-container {
      margin-left: 0px;
    }
    .sidebar-container {
      transition: transform 0.28s;
      width: $base-sidebar-width !important;
    }
    &.hideSidebar {
      .sidebar-container {
        pointer-events: none;
        transition-duration: 0.3s;
        transform: translate3d(-$base-sidebar-width, 0, 0);
      }
    }
  }
  .withoutAnimation {
    .main-container,
    .sidebar-container {
      transition: none;
    }
  }
}
// when menu collapsed
.el-menu--vertical {
  & > .el-menu {
    .svg-icon {
      margin-right: 16px;
    }
  }
  .el-menu--collapse .el-menu .el-sub-menu {
    min-width: $base-sidebar-width !important;
  }
  // mobile responsive
  .mobile {
    .main-container {
      margin-left: 0px;
    }
    .sidebar-container {
      transition: transform 0.28s;
      width: $base-sidebar-width !important;
    }
    &.hideSidebar {
      .sidebar-container {
        pointer-events: none;
        transition-duration: 0.3s;
        transform: translate3d(-$base-sidebar-width, 0, 0);
      }
    }
  }
  .withoutAnimation {
    .main-container,
    .sidebar-container {
      transition: none;
    }
  }
}
// when menu collapsed
.el-menu--vertical {
  & > .el-menu {
    .svg-icon {
      margin-right: 14px;
    }
  }
  .nest-menu .el-sub-menu > .el-sub-menu__title,
  .el-menu-item {
    min-width: 0 !important;
    margin: 0 12px 6px;
    width: calc(100% - 24px);
    padding-left: 8px !important;
    padding-right: 8px !important;
    margin: 0 10px 5px;
    width: calc(100% - 20px);
    padding-left: 10px !important;
    padding-right: 10px !important;
    box-sizing: border-box;
    border-radius: var(--radius-xs);
    transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
    color: var(--sidebar-text);
    border: none !important;
    &:hover {
      // you can use $sub-menuHover
      background-color: var(--menu-hover) !important;
      transform: translateX(2px);
    }
    &.is-active {
      background-color: var(--menu-active-bg) !important;
      border-radius: 14px;
      background: var(--menu-active-bg) !important;
      color: var(--menu-active-text) !important;
      border-radius: var(--radius-sm);
      font-weight: 500;
      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
    }
  }
  // the scroll bar appears when the sub-menu is too long
  > .el-menu--popup {
    max-height: 100vh;
    overflow-y: auto;
    padding: 8px;
    border-radius: 18px;
    border: 1px solid var(--surface-border);
    box-shadow: var(--shadow-md);
    &::-webkit-scrollbar-track-piece {
      background: #dfe7e1;
    }
    &::-webkit-scrollbar {
      width: 6px;
    }
    &::-webkit-scrollbar-thumb {
      background: #9aa79e;
      border-radius: 20px;
    }
  }
}
  // the scroll bar appears when the sub-menu is too long
  > .el-menu--popup {
    max-height: 100vh;
    overflow-y: auto;
    padding: 10px;
    border-radius: var(--radius-md);
    border: 1px solid var(--surface-border);
    box-shadow: var(--shadow-md);
    background: var(--menu-surface);
    backdrop-filter: blur(20px);
    &::-webkit-scrollbar-track-piece {
      background: var(--surface-muted);
    }
    &::-webkit-scrollbar {
      width: 5px;
    }
    &::-webkit-scrollbar-thumb {
      background: var(--accent-light);
      border-radius: 10px;
    }
  }
}
src/assets/styles/variables.module.scss
@@ -8,31 +8,31 @@
$yellow: #fec171;
$panGreen: #30b08f;
// menu palette
$menuText: #677287;
$menuActiveText: #1f7a72;
$menuBg: #f4f7f4;
$menuHover: #e7eeea;
// menu palette - ä½¿ç”¨ä¸»é¢˜è‰²
$menuText: #5a6478;
$menuActiveText: #ffffff;
$menuBg: #f8fafb;
$menuHover: rgba(var(--el-color-primary-rgb, 13, 148, 136), 0.08);
// light theme
$menuLightBg: #f4f7f4;
$menuLightHover: #e7eeea;
$menuLightText: #3b4658;
$menuLightActiveText: #1f7a72;
// light theme - ä½¿ç”¨ä¸»é¢˜è‰²
$menuLightBg: #f8fafb;
$menuLightHover: rgba(var(--el-color-primary-rgb, 13, 148, 136), 0.08);
$menuLightText: #3d4858;
$menuLightActiveText: #ffffff;
// layout
$base-sidebar-width: 216px;
$sideBarWidth: 216px;
// sidebar
$base-menu-color: #677287;
$base-menu-color-active: #1f7a72;
$base-menu-background: #f4f7f4;
$base-sub-menu-background: #eef3ef;
// sidebar - ä¼˜åŒ–后的侧边栏配色
$base-menu-color: #5a6478;
$base-menu-color-active: #0d9488;
$base-menu-background: #f8fafb;
$base-sub-menu-background: #f0f5f4;
$base-sub-menu-hover: #ffffff;
// component
$--color-primary: #1f7a72;
// component - ä¼˜åŒ–后的主题色
$--color-primary: #0d9488;
$--color-success: #67c23a;
$--color-warning: #d89b41;
$--color-danger: #d25b52;
@@ -66,37 +66,44 @@
:root {
  --sidebar-bg: #{$menuBg};
  --sidebar-text: #{$menuText};
  --sidebar-muted: #93a0b1;
  --menu-hover: #{$menuHover};
  --menu-active-bg: #dfe9e4;
  --menu-surface: rgba(255, 255, 255, 0.72);
  --sidebar-muted: #5a6478;
  --menu-hover: rgba(var(--el-color-primary-rgb, 13, 148, 136), 0.08);
  --menu-active-bg: var(--el-color-primary, #0d9488);
  --menu-active-text: #ffffff;
  --menu-surface: #f8fafb;
  --app-bg: #eef2ee;
  --app-bg-accent: #dfe8e2;
  --app-bg: #f0f4f3;
  --app-bg-accent: #e0ebe9;
  --surface-base: #ffffff;
  --surface-soft: #f7faf8;
  --surface-muted: #eff4f1;
  --surface-border: #d8e1db;
  --surface-border-strong: #c9d5ce;
  --text-primary: #21313f;
  --text-secondary: #5f6d7e;
  --text-tertiary: #8a98a8;
  --shadow-sm: 0 10px 30px rgba(31, 49, 38, 0.06);
  --shadow-md: 0 18px 50px rgba(31, 49, 38, 0.1);
  --radius-lg: 24px;
  --radius-md: 18px;
  --radius-sm: 12px;
  --surface-soft: #f7faf9;
  --surface-muted: #eef3f2;
  --surface-border: #d5e0de;
  --surface-border-strong: #c5d5d2;
  --text-primary: #1e293b;
  --text-secondary: #4a5568;
  --text-tertiary: #718096;
  --shadow-sm: 0 4px 12px rgba(0, 0, 0, 0.06);
  --shadow-md: 0 8px 28px rgba(0, 0, 0, 0.1);
  --shadow-menu: 0 2px 8px rgba(0, 0, 0, 0.05);
  --radius-lg: 20px;
  --radius-md: 14px;
  --radius-sm: 10px;
  --radius-xs: 6px;
  --navbar-bg: rgba(255, 255, 255, 0.78);
  --navbar-text: #21313f;
  --navbar-hover: rgba(31, 122, 114, 0.08);
  --navbar-bg: rgba(255, 255, 255, 0.85);
  --navbar-text: #1e293b;
  --navbar-hover: rgba(13, 148, 136, 0.08);
  --tags-bg: transparent;
  --tags-item-bg: rgba(255, 255, 255, 0.74);
  --tags-item-border: rgba(201, 213, 206, 0.88);
  --tags-item-text: #5f6d7e;
  --tags-item-hover: rgba(31, 122, 114, 0.08);
  --tags-close-hover: rgba(31, 122, 114, 0.18);
  --tags-item-bg: rgba(255, 255, 255, 0.8);
  --tags-item-border: rgba(197, 213, 210, 0.9);
  --tags-item-text: #4a5568;
  --tags-item-hover: rgba(13, 148, 136, 0.1);
  --tags-close-hover: rgba(13, 148, 136, 0.2);
  --accent-primary: #0d9488;
  --accent-light: #14b8a6;
  --accent-lighter: #5eead4;
  --splitpanes-default-bg: #ffffff;
}
@@ -109,10 +116,19 @@
  --el-border-color: #434343;
  --el-border-color-light: #434343;
  --sidebar-bg: #141414;
  --sidebar-text: #ffffff;
  --menu-hover: #2d2d2d;
  --menu-active-text: #{$menuActiveText};
  --sidebar-bg: #1a1a1a;
  --sidebar-text: #d0d0d0;
  --sidebar-muted: #888888;
  --menu-hover: rgba(var(--el-color-primary-rgb, 13, 148, 136), 0.12);
  --menu-active-bg: var(--el-color-primary, #0d9488);
  --menu-active-text: #ffffff;
  --menu-surface: #1a1a1a;
  --text-primary: #ffffff;
  --text-secondary: #d0d0d0;
  --text-tertiary: #888888;
  --accent-primary: var(--el-color-primary, #0d9488);
  --accent-light: var(--el-color-primary-light-3, #14b8a6);
  --navbar-bg: #141414;
  --navbar-text: #ffffff;
@@ -139,13 +155,22 @@
  .sidebar-container {
    .el-menu-item,
    .menu-title {
      color: var(--el-text-color-regular);
      color: var(--sidebar-text);
    }
    .el-menu-item.is-active,
    .el-menu-item.is-active .menu-title {
      color: var(--menu-active-text) !important;
    }
    & .theme-dark .nest-menu .el-sub-menu > .el-sub-menu__title,
    & .theme-dark .el-sub-menu .el-menu-item {
      background-color: var(--el-bg-color) !important;
    }
    & .theme-dark .el-sub-menu .el-menu-item.is-active {
      background-color: var(--menu-active-bg) !important;
    }
  }
  .el-menu--horizontal {
src/components/AIChatSidebar/assistants/index.js
@@ -1,13 +1,15 @@
import { generalAssistant } from './generalAssistant'
import { purchaseAssistant } from './purchaseAssistant'
import { productionAssistant } from './productionAssistant'
import { salesAssistant } from './salesAssistant'
export { generalAssistant, purchaseAssistant, productionAssistant }
export { generalAssistant, purchaseAssistant, productionAssistant, salesAssistant }
export const assistantRegistry = {
  general: generalAssistant,
  sales: salesAssistant,
  purchase: purchaseAssistant,
  production: productionAssistant
}
export const builtInAssistants = [generalAssistant, purchaseAssistant, productionAssistant]
export const builtInAssistants = [generalAssistant, salesAssistant, purchaseAssistant, productionAssistant]
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
@@ -359,6 +359,136 @@
                  </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>
@@ -750,6 +880,32 @@
  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',
@@ -819,6 +975,24 @@
  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: '产品明细',
@@ -1203,6 +1377,77 @@
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'
@@ -1581,6 +1826,7 @@
          payloadHiddenData: null,
          purchaseAnalysisData: null,
          manufacturingData: null,
          salesData: null,
          localUploadFiles: isUser ? mapHistoryFilePathsToSnapshots(msg.filePaths, uuid.value, idx) : []
        }
@@ -1825,9 +2071,15 @@
  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)
@@ -1839,6 +2091,7 @@
    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 || {})
    }
@@ -1883,6 +2136,12 @@
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 '已为您生成制造预警看板。'
@@ -3018,7 +3277,8 @@
    payloadTreeData: null,
    payloadHiddenData: null,
    purchaseAnalysisData: null,
    manufacturingData: null
    manufacturingData: null,
    salesData: null
  })
  outputState.value[botMsgIndex] = {
@@ -3143,7 +3403,8 @@
    payloadTreeData: null,
    payloadHiddenData: null,
    purchaseAnalysisData: null,
    manufacturingData: null
    manufacturingData: null,
    salesData: null
  }
  messages.value.push(botMsg)
@@ -4519,6 +4780,119 @@
  }
}
.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/layout/components/Sidebar/Logo.vue
@@ -84,12 +84,14 @@
  width: 100% !important;
  height: 56px !important;
  line-height: 56px;
  background: rgba(255, 255, 255, 0.78);
  background: var(--menu-surface);
  border: 1px solid var(--surface-border);
  border-radius: 22px;
  border-radius: var(--radius-lg);
  text-align: center;
  overflow: hidden;
  box-shadow: var(--shadow-sm);
  backdrop-filter: blur(20px);
  transition: all 0.3s ease;
  .sidebar-logo-link {
    height: 100%;
src/layout/components/Sidebar/SidebarItem.vue
@@ -1,17 +1,19 @@
<template>
  <div v-if="!item.hidden">
  <div v-if="!item.hidden" class="sidebar-item-wrapper">
    <template v-if="hasOneShowingChild(item.children, item) && (!onlyOneChild.children || onlyOneChild.noShowingChildren) && !item.alwaysShow">
      <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path, onlyOneChild.query)">
        <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{ 'submenu-title-noDropdown': !isNest }">
          <svg-icon :icon-class="onlyOneChild.meta.icon || (item.meta && item.meta.icon)"/>
          <template #title><span class="menu-title" :title="hasTitle(onlyOneChild.meta.title)">{{ onlyOneChild.meta.title }}</span></template>
          <svg-icon :icon-class="onlyOneChild.meta.icon || (item.meta && item.meta.icon)" class="menu-icon"/>
          <template #title>
            <span class="menu-title" :title="hasTitle(onlyOneChild.meta.title)">{{ onlyOneChild.meta.title }}</span>
          </template>
        </el-menu-item>
      </app-link>
    </template>
    <el-sub-menu v-else ref="subMenu" :index="resolvePath(item.path)" teleported>
      <template v-if="item.meta" #title>
        <svg-icon :icon-class="item.meta && item.meta.icon" />
        <svg-icon :icon-class="item.meta && item.meta.icon" class="menu-icon" />
        <span class="menu-title" :title="hasTitle(item.meta.title)">{{ item.meta.title }}</span>
      </template>
@@ -98,3 +100,56 @@
  }
}
</script>
<style lang="scss" scoped>
.sidebar-item-wrapper {
  :deep(.menu-icon) {
    width: 18px;
    height: 18px;
    margin-right: 12px;
    flex-shrink: 0;
    transition: all 0.25s ease;
    color: var(--sidebar-text);
    opacity: 0.8;
  }
  :deep(.el-menu-item:hover .menu-icon),
  :deep(.el-sub-menu__title:hover .menu-icon) {
    color: var(--el-color-primary, var(--accent-primary));
    opacity: 1;
    transform: scale(1.1);
  }
  :deep(.el-menu-item.is-active .menu-icon) {
    color: var(--menu-active-text) !important;
    opacity: 1;
  }
  :deep(.menu-title) {
    font-weight: 450;
    transition: all 0.25s ease;
    color: var(--sidebar-text);
  }
  :deep(.el-menu-item:hover .menu-title),
  :deep(.el-sub-menu__title:hover .menu-title) {
    color: var(--el-color-primary, var(--accent-primary));
  }
  :deep(.el-menu-item.is-active .menu-title) {
    color: var(--menu-active-text) !important;
  }
  :deep(.nest-menu) {
    .menu-icon {
      width: 16px;
      height: 16px;
      margin-right: 10px;
    }
    .menu-title {
      font-size: 13px;
    }
  }
}
</style>
src/layout/components/Sidebar/index.vue
@@ -64,7 +64,7 @@
<style lang="scss" scoped>
  .sidebar-container {
    background-color: v-bind(getMenuBackground);
    border-radius: 22px;
    border-radius: var(--radius-lg);
    overflow: hidden;
    .scrollbar-wrapper {
@@ -72,28 +72,50 @@
    }
    .el-menu {
      border: none;
      border: 1px solid var(--surface-border) !important;
      height: 100%;
      width: 100% !important;
      border-radius: 22px;
      border-radius: var(--radius-lg);
      .el-menu-item,
      .el-sub-menu__title {
        margin-bottom: 6px;
        border-radius: 14px;
        margin-bottom: 5px;
        border-radius: var(--radius-xs);
        color: v-bind(getMenuTextColor);
        font-size: 13.5px;
        letter-spacing: 0.2px;
        transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
        border: none !important;
        &:hover {
          background-color: var(--menu-hover, rgba(0, 0, 0, 0.06)) !important;
          border-radius: 14px;
          background-color: var(--menu-hover) !important;
          border-radius: var(--radius-sm);
          transform: translateX(2px);
        }
        .svg-icon {
          transition: all 0.25s ease;
        }
        &:hover .svg-icon {
          transform: scale(1.1);
          color: var(--el-color-primary, var(--accent-primary));
        }
      }
      .el-menu-item {
        color: var(--sidebar-text);
        &.is-active {
          color: v-bind(theme);
          background-color: var(--menu-active-bg, rgba(0, 0, 0, 0.06)) !important;
          font-weight: 600;
          background: var(--menu-active-bg) !important;
          color: var(--menu-active-text) !important;
          font-weight: 500;
          border-radius: var(--radius-sm);
          box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
          .svg-icon {
            color: var(--menu-active-text) !important;
          }
        }
      }
@@ -101,41 +123,84 @@
        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.7;
        transition: all 0.25s ease;
      }
      :deep(.el-sub-menu.is-opened .el-sub-menu__icon-arrow) {
        transform: rotate(180deg);
      }
      :deep(.el-sub-menu.is-active > .el-sub-menu__title) {
        color: v-bind(theme) !important;
        color: var(--menu-active-text) !important;
        font-weight: 600;
        background-color: var(--menu-active-bg, rgba(0, 0, 0, 0.06)) !important;
        border-radius: 14px;
        margin: 0 10px 6px !important;
        // width: calc(100% - 20px) !important;
        border-radius: var(--radius-sm);
        margin: 0 10px 5px !important;
        padding-left: 10px !important;
        padding-right: 10px !important;
        box-sizing: border-box;
        overflow: hidden;
        background-clip: padding-box;
        background: var(--menu-active-bg) !important;
        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
        border: none !important;
      }
      :deep(.el-menu-item.is-active) {
        margin: 0 10px 6px !important;
        margin: 0 10px 5px !important;
        width: calc(100% - 20px) !important;
        padding-left: 10px !important;
        padding-right: 10px !important;
        box-sizing: border-box;
        overflow: hidden;
        background-clip: padding-box;
        border-radius: 14px;
        border-radius: var(--radius-sm);
      }
      :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-menu-item.is-active .menu-title),
      :deep(.el-menu-item.is-active .svg-icon) {
        color: v-bind(theme) !important;
      :deep(.el-sub-menu.is-active > .el-sub-menu__title .svg-icon) {
        color: var(--menu-active-text) !important;
      }
      :deep(.el-menu-item.is-active .menu-title) {
        color: var(--menu-active-text) !important;
      }
      :deep(.el-sub-menu__title:hover),
      :deep(.el-menu-item:hover) {
        border-radius: 14px;
        border-radius: var(--radius-sm);
      }
      // å­èœå•展开动画优化
      :deep(.el-sub-menu .el-menu) {
        transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
      }
      // èœå•项进入动画
      :deep(.el-menu-item),
      :deep(.el-sub-menu__title) {
        animation: menuItemFadeIn 0.3s ease forwards;
      }
      @keyframes menuItemFadeIn {
        from {
          opacity: 0;
          transform: translateX(-8px);
        }
        to {
          opacity: 1;
          transform: translateX(0);
        }
      }
    }
  }
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/equipmentManagement/inspectionManagement/index.vue
@@ -71,7 +71,8 @@
            </div>
          </template>
          <template #isEnabled="{ row }">
            <el-tag :type="row.isEnabled === 1 ? 'success' : 'danger'" size="small">
            <el-tag :type="row.isEnabled === 1 ? 'success' : 'danger'"
                    size="small">
              {{ row.isEnabled == 1 ? '是' : '否' }}
            </el-tag>
          </template>
@@ -139,7 +140,7 @@
      label: "是否启用",
      minWidth: 100,
      dataType: "slot",
      slot: "isEnabled"
      slot: "isEnabled",
    },
    {
      prop: "frequencyType",
@@ -189,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 },
  ]);
  // æ“ä½œåˆ—配置
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/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: "设备名称" },
    {
@@ -355,7 +355,7 @@
    },
  ]);
  // ä»»åŠ¡è®°å½•è¡¨æ ¼åˆ—é…ç½®ï¼ˆåŽŸè®¾å¤‡ä¿å…»è¡¨æ ¼åˆ—ï¼‰
  // ä¿å…»è®°å½•表格列配置(原设备保养表格列)
  const columns = ref([
    {
      label: "设备名称",
@@ -436,7 +436,7 @@
    }
  };
  // å®šæ—¶ä»»åŠ¡ç®¡ç†ç›¸å…³æ–¹æ³•
  // ä¿å…»ä»»åŠ¡ç›¸å…³æ–¹æ³•
  const getScheduledTableData = async () => {
    try {
      const params = {
@@ -503,7 +503,7 @@
    ElMessage.info("导出定时任务功能待实现");
  };
  // ä»»åŠ¡è®°å½•ç›¸å…³æ–¹æ³•ï¼ˆåŽŸè®¾å¤‡ä¿å…»é¡µé¢æ–¹æ³•ï¼‰
  // ä¿å…»è®°å½•相关方法(原设备保养页面方法)
  const getTableData = async () => {
    try {
      const params = {
src/views/productionManagement/productStructure/Detail/index.vue
@@ -316,7 +316,39 @@
    });
  };
  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);
  };
@@ -431,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("请选择规格");
@@ -460,7 +521,7 @@
      //   return;
      // }
      // é€’归校验子项
      // é€’归校验子项字段
      if (item.children && item.children.length > 0) {
        item.children.forEach(child => {
          validateItem(child, false);
@@ -468,7 +529,11 @@
      }
    };
    // éåŽ†æ‰€æœ‰é¡¶å±‚é¡¹
    // 1. é¦–先校验同一父级下的同层消耗工序是否唯一
    checkProcessUniqueness(dataValue.dataList);
    if (!isValid) return false;
    // 2. ç„¶åŽéåŽ†æ ¡éªŒæ‰€æœ‰é¡¶å±‚é¡¹çš„å­—æ®µå¿…å¡«æƒ…å†µ
    dataValue.dataList.forEach(item => {
      validateItem(item, true);
    });
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">
@@ -570,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;
  };
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,14 +18,14 @@
                  @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>
@@ -41,6 +41,7 @@
                clearable
                @change="handleTestStandardChange"
                style="width: 100%"
                :disabled="isViewMode"
              >
                <el-option
                  v-for="item in testStandardOptions"
@@ -60,7 +61,7 @@
          </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="processQuantityDisabled"/>
              <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>
@@ -75,7 +76,8 @@
                               placeholder="请输入"
                               clearable
                               :precision="2"
                               @change="handleQualifiedQuantityChange" />
                               @change="handleQualifiedQuantityChange"
                               :disabled="isViewMode" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
@@ -88,19 +90,20 @@
                               placeholder="请输入"
                               clearable
                               :precision="2"
                               @change="handleUnqualifiedQuantityChange" />
                               @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="部分合格" />
@@ -111,7 +114,7 @@
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="检验员:" prop="checkName">
              <el-select v-model="form.checkName" placeholder="请选择" clearable>
              <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>
@@ -127,6 +130,7 @@
                  format="YYYY-MM-DD"
                  clearable
                  style="width: 100%"
                  :disabled="isViewMode"
              />
            </el-form-item>
          </el-col>
@@ -140,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>
@@ -199,6 +206,8 @@
  },
});
const { form, rules } = toRefs(data);
// æ˜¯å¦ä¸ºæŸ¥çœ‹æ¨¡å¼
const isViewMode = computed(() => operationType.value === 'view');
// ç¼–辑时:productMainId æˆ– purchaseLedgerId ä»»ä¸€æœ‰å€¼åˆ™å·¥åºã€æ•°é‡ç½®ç°
const processQuantityDisabled = computed(() => {
  const v = form.value || {};
@@ -259,7 +268,7 @@
  testStandardOptions.value = [];
  tableData.value = [];
  if (operationType.value === 'edit') {
  if (operationType.value === 'edit' || operationType.value === 'view') {
    // å…ˆä¿å­˜ testStandardId,避免被清空
    const savedTestStandardId = row.testStandardId;
    // å…ˆè®¾ç½®è¡¨å•数据,但暂时清空 testStandardId,等选项加载完成后再设置
@@ -452,6 +461,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 = [];
src/views/qualityManagement/finalInspection/index.vue
@@ -191,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/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,7 @@
                               placeholder="请输入"
                               clearable
                               :precision="2"
                               :disabled="processQuantityDisabled" />
                               :disabled="isViewMode || processQuantityDisabled" />
            </el-form-item>
          </el-col>
        </el-row>
@@ -108,7 +109,8 @@
                               placeholder="请输入"
                               clearable
                               :precision="2"
                               @change="handleQualifiedQuantityChange" />
                               @change="handleQualifiedQuantityChange"
                               :disabled="isViewMode" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
@@ -121,7 +123,8 @@
                               placeholder="请输入"
                               clearable
                               :precision="2"
                               @change="handleUnqualifiedQuantityChange" />
                               @change="handleUnqualifiedQuantityChange"
                               :disabled="isViewMode" />
            </el-form-item>
          </el-col>
        </el-row>
@@ -131,13 +134,14 @@
                          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="不合格"
@@ -154,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"
@@ -171,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>
@@ -183,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>
@@ -259,6 +269,8 @@
  });
  const userList = ref([]);
  const { form, rules } = toRefs(data);
  // æ˜¯å¦ä¸ºæŸ¥çœ‹æ¨¡å¼
  const isViewMode = computed(() => operationType.value === 'view');
  // ç¼–辑时:productMainId æˆ– purchaseLedgerId ä»»ä¸€æœ‰å€¼åˆ™å·¥åºã€æ•°é‡ç½®ç°
  const processQuantityDisabled = computed(() => {
    const v = form.value || {};
@@ -332,7 +344,7 @@
    tableData.value = [];
    // å…ˆç¡®ä¿äº§å“æ ‘已加载,否则编辑时产品/规格型号无法反显
    await getProductOptions();
    if (operationType.value === "edit") {
    if (operationType.value === "edit" || operationType.value === "view") {
      // å…ˆä¿å­˜ testStandardId,避免被清空
      const savedTestStandardId = row.testStandardId;
      // å…ˆè®¾ç½®è¡¨å•数据,但暂时清空 testStandardId,等选项加载完成后再设置
@@ -557,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);
src/views/qualityManagement/processInspection/index.vue
@@ -190,6 +190,13 @@
                }
      },
      {
        name: "查看",
        type: "text",
        clickFun: (row) => {
          openForm("view", row);
        },
      },
      {
        name: "附件",
        type: "text",
        clickFun: (row) => {
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,7 +79,7 @@
          <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>
@@ -87,14 +88,14 @@
            <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"/>
                               @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"/>
                               @change="onUnqualifiedChange" :disabled="isViewMode"/>
            </el-form-item>
          </el-col>
        </el-row>
@@ -102,12 +103,12 @@
        <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="部分合格"/>
@@ -118,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>
@@ -133,6 +134,7 @@
                  format="YYYY-MM-DD"
                  clearable
                  style="width: 100%"
                  :disabled="isViewMode"
              />
            </el-form-item>
          </el-col>
@@ -149,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>
@@ -241,6 +246,9 @@
const modelOptions = ref([]);
const userList = ref([]); // æ£€éªŒå‘˜ä¸‹æ‹‰åˆ—表
// æ˜¯å¦ä¸ºæŸ¥çœ‹æ¨¡å¼
const isViewMode = computed(() => operationType.value === 'view');
// ç¼–辑时:productMainId æˆ– purchaseLedgerId ä»»ä¸€æœ‰å€¼åˆ™ä¾›åº”商、数量置灰
const supplierQuantityDisabled = computed(() => {
  const v = form.value || {};
@@ -280,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}
@@ -455,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 = [];
src/views/qualityManagement/rawMaterialInspection/index.vue
@@ -197,6 +197,13 @@
                }
      },
      {
        name: "查看",
        type: "text",
        clickFun: (row) => {
          openForm("view", row);
        },
      },
      {
        name: "附件",
        type: "text",
        clickFun: (row) => {