gaoluyang
2 天以前 f49bfd6c085cbf28a25d9404f8dc5b74368b716a
新疆马铃薯
1.代码更新
已添加1个文件
已修改26个文件
已删除57个文件
4622 ■■■■■ 文件已修改
multiple/assets/favicon/BTYXfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/BWSMfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/CKGMfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/DYKJfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/DZZBfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/HQJCfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/HYJCfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/HYLQfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/HYZCfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/JHHGfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/JHYfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/JXJHfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/JXSMico.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/KSfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/KYHGfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/LFJZfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/QXYfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/SDJCfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/SDTXfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/WTXCfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/XCDQfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/XLZBico.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/XSWHfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/YSJXico.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/YTJZfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/ZQSYfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/ZXZNfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/favicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/BTYXLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/BWSMLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/CKGMLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/DYKJLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/DZZBLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/HQJCLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/HYJCLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/HYLQLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/HYZCLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/JHHGLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/JHYLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/JXJHLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/JXSMLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/KSLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/KYHGLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/LFJZLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/Logo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/QXYLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/SDJCLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/SDTXLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/WTXCLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/XCDQLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/XLZBLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/XSWHLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/YSJXLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/YTJZLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/ZQSYLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/ZXZNLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/config.json 175 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
multiple/multiple-build.js 140 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
public/favicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
src/api/equipmentManagement/deviceArea.js 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/equipmentManagement/measurementEquipment.js 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/equipmentManagement/repair.js 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/equipmentManagement/upkeep.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inspectionManagement/index.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/brand/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/calibration/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/defectManagement/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/inspectionManagement/components/uploadFiles.vue 680 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/inspectionManagement/components/viewFiles.vue 284 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/inspectionManagement/index.vue 453 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/ledger/Modal.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/ledger/index.vue 606 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/measurementEquipment/components/calibrationDia.vue 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/measurementEquipment/components/dialogForm.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/measurementEquipment/index.vue 202 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/operationManagement/index.vue 132 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/repair/Modal/AcceptanceModal.vue 144 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/repair/Modal/RepairModal.vue 356 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/repair/index.vue 207 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/spareParts/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/Form/MaintenanceModal.vue 127 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/Form/PlanModal.vue 274 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/index.vue 672 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/BTYXfavicon.ico
Binary files differ
multiple/assets/favicon/BWSMfavicon.ico
Binary files differ
multiple/assets/favicon/CKGMfavicon.ico
Binary files differ
multiple/assets/favicon/DYKJfavicon.ico
Binary files differ
multiple/assets/favicon/DZZBfavicon.ico
Binary files differ
multiple/assets/favicon/HQJCfavicon.ico
Binary files differ
multiple/assets/favicon/HYJCfavicon.ico
Binary files differ
multiple/assets/favicon/HYLQfavicon.ico
Binary files differ
multiple/assets/favicon/HYZCfavicon.ico
Binary files differ
multiple/assets/favicon/JHHGfavicon.ico
Binary files differ
multiple/assets/favicon/JHYfavicon.ico
Binary files differ
multiple/assets/favicon/JXJHfavicon.ico
Binary files differ
multiple/assets/favicon/JXSMico.ico
Binary files differ
multiple/assets/favicon/KSfavicon.ico
Binary files differ
multiple/assets/favicon/KYHGfavicon.ico
Binary files differ
multiple/assets/favicon/LFJZfavicon.ico
Binary files differ
multiple/assets/favicon/QXYfavicon.ico
Binary files differ
multiple/assets/favicon/SDJCfavicon.ico
Binary files differ
multiple/assets/favicon/SDTXfavicon.ico
Binary files differ
multiple/assets/favicon/WTXCfavicon.ico
Binary files differ
multiple/assets/favicon/XCDQfavicon.ico
Binary files differ
multiple/assets/favicon/XLZBico.ico
Binary files differ
multiple/assets/favicon/XSWHfavicon.ico
Binary files differ
multiple/assets/favicon/YSJXico.ico
Binary files differ
multiple/assets/favicon/YTJZfavicon.ico
Binary files differ
multiple/assets/favicon/ZQSYfavicon.ico
Binary files differ
multiple/assets/favicon/ZXZNfavicon.ico
Binary files differ
multiple/assets/favicon/favicon.ico

multiple/assets/logo/BTYXLogo.png
Binary files differ
multiple/assets/logo/BWSMLogo.png
Binary files differ
multiple/assets/logo/CKGMLogo.png
Binary files differ
multiple/assets/logo/DYKJLogo.png
Binary files differ
multiple/assets/logo/DZZBLogo.png
Binary files differ
multiple/assets/logo/HQJCLogo.png
Binary files differ
multiple/assets/logo/HYJCLogo.png
Binary files differ
multiple/assets/logo/HYLQLogo.png
Binary files differ
multiple/assets/logo/HYZCLogo.png
Binary files differ
multiple/assets/logo/JHHGLogo.png
Binary files differ
multiple/assets/logo/JHYLogo.png
Binary files differ
multiple/assets/logo/JXJHLogo.png
Binary files differ
multiple/assets/logo/JXSMLogo.png
Binary files differ
multiple/assets/logo/KSLogo.png
Binary files differ
multiple/assets/logo/KYHGLogo.png
Binary files differ
multiple/assets/logo/LFJZLogo.png
Binary files differ
multiple/assets/logo/Logo.png

multiple/assets/logo/QXYLogo.png
Binary files differ
multiple/assets/logo/SDJCLogo.png
Binary files differ
multiple/assets/logo/SDTXLogo.png
Binary files differ
multiple/assets/logo/WTXCLogo.png
Binary files differ
multiple/assets/logo/XCDQLogo.png
Binary files differ
multiple/assets/logo/XLZBLogo.png
Binary files differ
multiple/assets/logo/XSWHLogo.png
Binary files differ
multiple/assets/logo/YSJXLogo.png
Binary files differ
multiple/assets/logo/YTJZLogo.png
Binary files differ
multiple/assets/logo/ZQSYLogo.png
Binary files differ
multiple/assets/logo/ZXZNLogo.png
Binary files differ
multiple/config.json
@@ -3,180 +3,21 @@
    "env": {
      "VITE_APP_TITLE": "芯导云(管理信息系统)"
    },
    "screen": "screen/PCDZView.png",
    "logo": "logo/Logo.png",
    "favicon": "favicon/favicon.ico"
  },
  "TEST": {
  "DLS": {
    "env": {
      "VITE_APP_TITLE": "工厂数字化MOM系统",
      "VITE_BASE_API": "http://1.15.17.182:9048",
      "VITE_JAVA_API": "http://1.15.17.182:9049"
      "VITE_APP_TITLE": "新疆大罗素马铃薯信息管理",
      "VITE_BASE_API": "http://1.15.17.182:9027",
      "VITE_JAVA_API": "http://1.15.17.182:9026"
    },
    "logo": "logo/XDRJ.png",
    "screen": "screen/login-background.png",
    "logo": "logo/Logo.png",
    "favicon": "favicon/favicon.ico"
  },
  "BTYX": {
    "env": {
      "VITE_APP_TITLE": "河南帮太优选食品有限公司",
      "VITE_BASE_API": "http://1.15.17.182:9056",
      "VITE_JAVA_API": "http://1.15.17.182:9057"
    },
    "logo": "logo/BTYXLogo.png",
    "favicon": "favicon/BTYXfavicon.ico"
  },
  "ZXZN": {
    "env": {
      "VITE_APP_TITLE": "河南智芯智能机器人有限公司",
      "VITE_BASE_API": "http://127.0.0.1:9001",
      "VITE_JAVA_API": "http://127.0.0.1:9000"
    },
    "logo": "logo/ZXZNLogo.png",
    "favicon": "favicon/ZXZNfavicon.ico"
  },
  "SDTX": {
    "env": {
      "VITE_APP_TITLE": "河南善鼎通信科技有限公司",
      "VITE_BASE_API": "http://36.213.156.184:9001",
      "VITE_JAVA_API": "http://36.213.156.184:9000"
    },
    "logo": "logo/SDTXLogo.png",
    "favicon": "favicon/SDTXfavicon.ico"
  },
  "QXY": {
    "env": {
      "VITE_APP_TITLE": "强信宇电器管理系统",
      "VITE_BASE_API": "http://36.134.154.10:9001",
      "VITE_JAVA_API": "http://36.134.154.10:9000"
    },
    "logo": "logo/QXYLogo.png",
    "favicon": "favicon/QXYfavicon.ico"
  },
  "HQJC": {
    "env": {
      "VITE_APP_TITLE": "华强建材管理系统",
      "VITE_BASE_API": "http://36.134.77.64:9001",
      "VITE_JAVA_API": "http://36.134.77.64:9000"
    },
    "logo": "logo/HQJCLogo.png",
    "favicon": "favicon/HQJCfavicon.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"
  },
  "BWSM": {
    "env": {
      "VITE_APP_TITLE": "八维商贸管理系统",
      "VITE_BASE_API": "http://1.15.17.182:9070",
      "VITE_JAVA_API": "http://1.15.17.182:9069"
    },
    "logo": "logo/BWSMLogo.png",
    "favicon": "favicon/BWSMfavicon.ico"
  },
  "CKGM": {
    "env": {
      "VITE_APP_TITLE": "宸康工贸管理系统",
      "VITE_BASE_API": "http://1.15.17.182:9072",
      "VITE_JAVA_API": "http://1.15.17.182:9071"
    },
    "logo": "logo/CKGMLogo.png",
    "favicon": "favicon/CKGMfavicon.ico"
  },
  "ZQSY": {
    "env": {
      "VITE_APP_TITLE": "泽淇实业",
      "VITE_BASE_API": "http://36.213.128.159:9000",
      "VITE_JAVA_API": "http://36.213.128.159:9001"
    },
    "logo": "logo/ZQSYLogo.png",
    "favicon": "favicon/ZQSYfavicon.ico"
  },
  "JXJH": {
    "env": {
      "VITE_APP_TITLE": "浚县江海水泥制品有限公司",
      "VITE_BASE_API": "http://36.139.201.20:9000",
      "VITE_JAVA_API": "http://36.139.201.20:9001"
    },
    "logo": "logo/JXJHLogo.png",
    "favicon": "favicon/JXJHfavicon.ico"
  },
  "YTJZ": {
    "env": {
      "VITE_APP_TITLE": "豫泰建筑材料有限公司",
      "VITE_BASE_API": "http://36.139.201.181:9000",
      "VITE_JAVA_API": "http://36.139.201.181:9001"
    },
    "logo": "logo/YTJZLogo.png",
    "favicon": "favicon/YTJZfavicon.ico"
  },
  "HYLQ": {
    "env": {
      "VITE_APP_TITLE": "航逸路桥工程有限公司",
      "VITE_BASE_API": "http://36.139.202.111:9000",
      "VITE_JAVA_API": "http://36.139.202.111:9001"
    },
    "logo": "logo/HYLQLogo.png",
    "favicon": "favicon/HYLQfavicon.ico"
  },
  "QXY": {
    "env": {
      "VITE_APP_TITLE": "强信宇电器云主机",
      "VITE_BASE_API": "http://36.134.154.10:9000",
      "VITE_JAVA_API": "http://36.134.154.10:9001"
    },
    "logo": "logo/QXYLogo.png",
    "favicon": "favicon/QXYfavicon.ico"
  },
  "HYJC": {
    "env": {
      "VITE_APP_TITLE": "恒洋建材",
      "VITE_BASE_API": "http://36.138.94.178:9000",
      "VITE_JAVA_API": "http://36.138.94.178:9001"
    },
    "logo": "logo/HYJCLogo.png",
    "favicon": "favicon/HYJCfavicon.ico"
  },
  "JHY": {
    "env": {
      "VITE_APP_TITLE": "山西省榆社县晋和园食品有限公司",
      "VITE_BASE_API": "http://223.15.233.27:9001",
      "VITE_JAVA_API": "http://223.15.233.27:9002"
    },
    "logo": "logo/JHYLogo.png",
    "favicon": "favicon/JHYfavicon.ico"
  },
  "XCDQ": {
    "env": {
      "VITE_APP_TITLE": "旭晨电器管理系统",
      "VITE_BASE_API": "http://36.133.45.183:9001",
      "VITE_JAVA_API": "http://36.133.45.183:9002"
    },
    "logo": "logo/XCDQLogo.png",
    "favicon": "favicon/XCDQfavicon.ico"
  },
  "KYHG": {
    "env": {
      "VITE_APP_TITLE": "山西坤源化工有限公司",
      "VITE_BASE_API": "http://36.137.13.29:9001",
      "VITE_JAVA_API": "http://36.137.13.29:9002"
    },
    "logo": "logo/KYHGLogo.png",
    "favicon": "favicon/KYHGfavicon.ico"
  },
  "JXSM": {
    "env": {
      "VITE_APP_TITLE": "襄垣县洁鑫商贸有限公司",
      "VITE_BASE_API": "http://36.134.76.148:9001",
      "VITE_JAVA_API": "http://36.134.76.148:9002"
    },
    "logo": "logo/JXSMLogo.png",
    "favicon": "favicon/JXSMico.ico"
  },
  "screen": "/src/assets/images/login-background.png",
  "logo": "/src/assets/logo/logo.png",
  "favicon": "/public/favicon.ico"
}
multiple/multiple-build.js
@@ -1,152 +1,86 @@
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);
const data = await fs.readFile(path.join(__dirname, "config.json"), "utf-8");
// è¯»å– JSON é…ç½®
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 envFilePath = path.join(rootPath, ".env.production.local");
// é¡¹ç›®è·¯å¾„
const rootPath = path.resolve(__dirname, '..');
const resourcePath = path.join(rootPath, 'multiple', 'assets');
const replacePath = path.join(rootPath, 'replace');
// èŽ·å–å‘½ä»¤è¡Œå‚æ•°
const params = parseArgs(process.argv);
const company = resolveCompany(params);
const company = params["company"] ?? "default";
const companyMap = config[company];
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 continue.
    }
    await fs.rm(dest, { force: true });
  }
  await fs.copyFile(src, dest);
}
const envFilePath = path.join(process.cwd(), '.env.production.local');
try {
    // 1️⃣ ç”Ÿæˆ .env
  console.log("=======生成.env=======");
  const envContent =
    Object.entries(companyMap.env)
    const envContent = Object.entries(companyMap.env)
      .map(([key, value]) => `${key}='${value}'`)
      .join("\n") + "\n";
  await fs.writeFile(envFilePath, envContent, "utf-8");
        .join('\n') + '\n';
    await fs.writeFile(envFilePath, envContent, 'utf-8');
    // 2️⃣ å¤‡ä»½åŽŸå§‹èµ„æºå¹¶æ›¿æ¢
  console.log("=======修改资源=======");
  for (const [key] of Object.entries(companyMap)) {
    if (key === "env") continue;
    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]);
    const replaceFile = path.join(resourcePath, companyMap[key]);
    await copyFileWithOverwrite(originFile, backupFile);
    await copyFileWithOverwrite(replaceFile, originFile);
        await fs.mkdir(path.dirname(backupFile), { recursive: true });
        await fs.copyFile(originFile, backupFile);
        await fs.copyFile(replaceFile, originFile);
  }
  console.log("=====开始打包=====");
  const buildEnv = createBuildEnv(companyMap.env);
  execSync("vite build", { stdio: "inherit", cwd: rootPath, env: buildEnv });
    console.log("=====开始打包======");
    execSync("vite build", { stdio: "inherit" });
  console.log("=====打包完成======");
} finally {
  console.log("=====恢复资源======");
    // åˆ é™¤ä¸´æ—¶ .env æ–‡ä»¶
  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;
        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.copyFile(backupFile, originFile);
    }
    await fs.rm(replacePath, { recursive: true, force: true });
    console.log(`🗑️ å·²åˆ é™¤ ${replacePath}`);
  }
}
// ç®€å•命令行参数解析
function parseArgs(argv) {
  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;
    for (const arg of argv.slice(2)) {
        if (arg.startsWith('--')) {
            const [key, value] = arg.slice(2).split('=');
            params[key] = value ?? true;
    }
    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",
  };
}
public/favicon.ico

src/api/equipmentManagement/deviceArea.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,55 @@
import request from "@/utils/request";
export function getDeviceAreaTree(params) {
  return request({
    url: "/device/area/tree",
    method: "get",
    params,
  });
}
export function getDeviceAreaTreeWithDevices(params) {
  return request({
    url: "/device/area/treeWithDevices",
    method: "get",
  });
}
export function getDeviceAreaPage(params) {
  return request({
    url: "/device/area/page",
    method: "get",
    params,
  });
}
export function getDeviceAreaDetail(id) {
  return request({
    url: `/device/area/${id}`,
    method: "get",
  });
}
export function addDeviceArea(data) {
  return request({
    url: "/device/area",
    method: "post",
    data,
  });
}
export function updateDeviceArea(data) {
  return request({
    url: "/device/area",
    method: "put",
    data,
  });
}
export function deleteDeviceArea(ids) {
  return request({
    url: "/device/area",
    method: "delete",
    data: ids,
  });
}
src/api/equipmentManagement/measurementEquipment.js
@@ -53,30 +53,3 @@
        data
    })
}
// é€šç”¨é™„件查询
export function getStorageAttachmentList(query) {
  return request({
    url: "/storageAttachment/list",
    method: "get",
    params: query,
  });
}
// é€šç”¨é™„件保存
export function addStorageAttachment(data) {
  return request({
    url: "/storageAttachment/add",
    method: "post",
    data: data,
  });
}
// é€šç”¨é™„件删除
export function delStorageAttachment(ids) {
  return request({
    url: "/storageAttachment/delete",
    method: "delete",
    data: ids,
  });
}
src/api/equipmentManagement/repair.js
@@ -70,16 +70,3 @@
    data,
  });
};
/**
 * @desc éªŒæ”¶å®¡æ‰¹
 * @param {验收参数} data
 * @returns
 */
export const repairAcceptance = (data) => {
  return request({
    url: `/device/repair/acceptance`,
    method: "post",
    data,
  });
};
src/api/equipmentManagement/upkeep.js
@@ -102,3 +102,12 @@
    data: params,
  });
};
// è®¾å¤‡ä¿å…»å®šæ—¶ä»»åŠ¡å¯ç”¨çŠ¶æ€åˆ‡æ¢
export const deviceMaintenanceTaskChangeEnable = (params) => {
  return request({
    url: '/deviceMaintenanceTask/changeEnable',
    method: "post",
    data: params,
  });
};
src/api/inspectionManagement/index.js
@@ -59,3 +59,12 @@
        data: query
    })
}
// å®šæ—¶å·¡æ£€ä»»åŠ¡å¯ç”¨çŠ¶æ€åˆ‡æ¢
export function changeTimingTaskEnable(query) {
    return request({
        url: '/timingTask/changeEnable',
        method: 'post',
        data: query
    })
}
src/views/equipmentManagement/brand/index.vue
@@ -60,8 +60,8 @@
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button type="primary" @click="handleSubmit">确定</el-button>
        <el-button @click="visible = false">取消</el-button>
        <el-button type="primary" @click="handleSubmit">确定</el-button>
      </template>
    </el-dialog>
  </div>
src/views/equipmentManagement/calibration/index.vue
@@ -1,6 +1,6 @@
<template>
    <div class="app-container">
        <div class="search_form mb20">
        <div class="search_form">
            <div>
                <span class="search_title">检定日期:</span>
                <el-date-picker
src/views/equipmentManagement/defectManagement/index.vue
@@ -65,8 +65,8 @@
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button type="primary" @click="submitDefectForm">确定</el-button>
          <el-button @click="showRegisterDialog = false">取消</el-button>
          <el-button type="primary" @click="submitDefectForm">确定</el-button>
        </span>
      </template>
    </el-dialog>
src/views/equipmentManagement/inspectionManagement/components/uploadFiles.vue
ÎļþÒÑɾ³ý
src/views/equipmentManagement/inspectionManagement/components/viewFiles.vue
@@ -1,226 +1,251 @@
<template>
  <div>
    <el-dialog title="查看附件" v-model="dialogVisitable" width="800px" @close="cancel">
    <el-dialog title="查看附件"
               v-model="dialogVisitable" width="800px" @close="cancel">
      <div class="upload-container">
        <!-- ç”Ÿäº§å‰ -->
        <div class="form-container">
          <div class="title">生产前</div>
          <div class="media-list">
            <img
              v-for="(item, index) in beforeProductionImgs"
              :key="`before-img-${index}`"
              :src="item"
              alt=""
              class="media-image"
          <!-- å›¾ç‰‡åˆ—表 -->
          <div style="display: flex; flex-wrap: wrap;">
            <img v-for="(item, index) in beforeProductionImgs" :key="index"
              @click="showMedia(beforeProductionImgs, index, 'image')"
            />
                 :src="item" style="max-width: 100px; height: 100px; margin: 5px;" alt="">
          </div>
          <div class="media-list">
          <!-- è§†é¢‘列表 -->
          <div style="display: flex; flex-wrap: wrap;">
            <div
              v-for="(videoUrl, index) in beforeProductionVideos"
              :key="`before-video-${index}`"
              class="video-item"
                :key="index"
              @click="showMedia(beforeProductionVideos, index, 'video')"
                style="position: relative; margin: 10px; cursor: pointer;"
            >
              <div class="video-thumb">
                <img src="@/assets/images/video.png" alt="播放" class="video-icon" />
              <div style="width: 160px; height: 90px; background-color: #333; display: flex; align-items: center; justify-content: center;">
                <img src="@/assets/images/video.png" alt="播放" style="width: 30px; height: 30px; opacity: 0.8;" />
              </div>
              <div class="video-text">点击播放</div>
              <div style="text-align: center; font-size: 12px; color: #666;">点击播放</div>
            </div>
          </div>
        </div>
                <!-- ç”Ÿäº§ä¸­ -->
        <div class="form-container">
          <div class="title">生产中</div>
          <div class="media-list">
            <img
              v-for="(item, index) in afterProductionImgs"
              :key="`during-img-${index}`"
              :src="item"
              alt=""
              class="media-image"
              @click="showMedia(afterProductionImgs, index, 'image')"
            />
                    <!-- å›¾ç‰‡åˆ—表 -->
                    <div style="display: flex; flex-wrap: wrap;">
                        <img v-for="(item, index) in productionIssuesImgs" :key="index"
                       @click="showMedia(productionIssuesImgs, index, 'image')"
                       :src="item" style="max-width: 100px; height: 100px; margin: 5px;" alt="">
          </div>
          <div class="media-list">
                    <!-- è§†é¢‘列表 -->
                    <div style="display: flex; flex-wrap: wrap;">
            <div
              v-for="(videoUrl, index) in afterProductionVideos"
              :key="`during-video-${index}`"
              class="video-item"
              @click="showMedia(afterProductionVideos, index, 'video')"
                            v-for="(videoUrl, index) in productionIssuesVideos"
                      :key="index"
                      @click="showMedia(productionIssuesVideos, index, 'video')"
                      style="position: relative; margin: 10px; cursor: pointer;"
            >
              <div class="video-thumb">
                <img src="@/assets/images/video.png" alt="播放" class="video-icon" />
                            <div style="width: 160px; height: 90px; background-color: #333; display: flex; align-items: center; justify-content: center;">
                                <img src="@/assets/images/video.png" alt="播放" style="width: 30px; height: 30px; opacity: 0.8;" />
              </div>
              <div class="video-text">点击播放</div>
                            <div style="text-align: center; font-size: 12px; color: #666;">点击播放</div>
            </div>
          </div>
        </div>
        <!-- ç”Ÿäº§åŽ -->
        <div class="form-container">
          <div class="title">生产后</div>
          <div class="media-list">
            <img
              v-for="(item, index) in productionIssuesImgs"
              :key="`after-img-${index}`"
              :src="item"
              alt=""
              class="media-image"
              @click="showMedia(productionIssuesImgs, index, 'image')"
            />
          <!-- å›¾ç‰‡åˆ—表 -->
          <div style="display: flex; flex-wrap: wrap;">
            <img v-for="(item, index) in afterProductionImgs" :key="index"
                 @click="showMedia(afterProductionImgs, index, 'image')"
                 :src="item" style="max-width: 100px; height: 100px; margin: 5px;" alt="">
          </div>
          <div class="media-list">
          <!-- è§†é¢‘列表 -->
          <div style="display: flex; flex-wrap: wrap;">
            <div
              v-for="(videoUrl, index) in productionIssuesVideos"
              :key="`after-video-${index}`"
              class="video-item"
              @click="showMedia(productionIssuesVideos, index, 'video')"
                v-for="(videoUrl, index) in afterProductionVideos"
                :key="index"
                @click="showMedia(afterProductionVideos, index, 'video')"
                style="position: relative; margin: 10px; cursor: pointer;"
            >
              <div class="video-thumb">
                <img src="@/assets/images/video.png" alt="播放" class="video-icon" />
              <div style="width: 160px; height: 90px; background-color: #333; display: flex; align-items: center; justify-content: center;">
                <img src="@/assets/images/video.png" alt="播放" style="width: 30px; height: 30px; opacity: 0.8;" />
              </div>
              <div class="video-text">点击播放</div>
              <div style="text-align: center; font-size: 12px; color: #666;">点击播放</div>
            </div>
          </div>
        </div>
      </div>
    </el-dialog>
    <!-- ç»Ÿä¸€åª’体查看器 -->
    <div v-if="isMediaViewerVisible" class="media-viewer-overlay" @click.self="closeMediaViewer">
      <div class="media-viewer-content" @click.stop>
        <!-- å›¾ç‰‡ -->
        <vue-easy-lightbox
          v-if="mediaType === 'image'"
          :visible="isMediaViewerVisible"
          :imgs="mediaList"
          :index="currentMediaIndex"
          @hide="closeMediaViewer"
        />
        ></vue-easy-lightbox>
        <div v-else-if="mediaType === 'video'" class="video-player-wrap">
          <video :src="mediaList[currentMediaIndex]" autoplay controls class="video-player" />
        <!-- è§†é¢‘ -->
        <div v-else-if="mediaType === 'video'" style="position: relative;">
          <video
              :src="mediaList[currentMediaIndex]"
              autoplay
              controls
              style="max-width: 90vw; max-height: 80vh;"
          />
        </div>
      </div>
    </div>
  </div>
</template>
<script setup>
import { ref } from "vue";
import VueEasyLightbox from "vue-easy-lightbox";
import { ref } from 'vue';
import VueEasyLightbox from 'vue-easy-lightbox';
const { proxy } = getCurrentInstance();
// æŽ§åˆ¶å¼¹çª—显示
const dialogVisitable = ref(false);
// å›¾ç‰‡æ•°ç»„
const beforeProductionImgs = ref([]);
const afterProductionImgs = ref([]);
const productionIssuesImgs = ref([]);
// è§†é¢‘数组
const beforeProductionVideos = ref([]);
const afterProductionVideos = ref([]);
const productionIssuesVideos = ref([]);
// åª’体查看器状态
const isMediaViewerVisible = ref(false);
const currentMediaIndex = ref(0);
const mediaList = ref([]);
const mediaType = ref("image");
const mediaList = ref([]); // å­˜å‚¨å½“前要查看的媒体列表(含图片和视频对象)
const mediaType = ref('image'); // image | video
const javaApi = proxy.javaApi;
const processFileUrl = fileUrl => {
  if (!fileUrl) return "";
// å¤„理 URL:将 Windows è·¯å¾„转换为可访问的 URL
function processFileUrl(fileUrl) {
  if (!fileUrl) return '';
  let currentUrl = String(fileUrl);
  if (currentUrl.includes("\\")) {
    const uploadsIndex = currentUrl.toLowerCase().indexOf("uploads");
  // å¦‚æžœ URL æ˜¯ Windows è·¯å¾„格式(包含反斜杠),需要转换
  if (fileUrl && fileUrl.indexOf('\\') > -1) {
    // æŸ¥æ‰¾ uploads å…³é”®å­—的位置,从那里开始提取相对路径
    const uploadsIndex = fileUrl.toLowerCase().indexOf('uploads');
    if (uploadsIndex > -1) {
      currentUrl = `/${currentUrl.substring(uploadsIndex).replace(/\\/g, "/")}`;
      // ä»Ž uploads å¼€å§‹æå–路径,并将反斜杠替换为正斜杠
      const relativePath = fileUrl.substring(uploadsIndex).replace(/\\/g, '/');
      fileUrl = '/' + relativePath;
    } else {
      const fileName = currentUrl.split("\\").pop();
      currentUrl = `/uploads/${fileName}`;
      // å¦‚果没有找到 uploads,提取最后一个目录和文件名
      const parts = fileUrl.split('\\');
      const fileName = parts[parts.length - 1];
      fileUrl = '/uploads/' + fileName;
    }
  }
  if (currentUrl && !currentUrl.startsWith("http")) {
    if (!currentUrl.startsWith("/")) {
      currentUrl = `/${currentUrl}`;
  // ç¡®ä¿æ‰€æœ‰éž http å¼€å¤´çš„ URL éƒ½æ‹¼æŽ¥ baseUrl
  if (fileUrl && !fileUrl.startsWith('http')) {
    // ç¡®ä¿è·¯å¾„以 / å¼€å¤´
    if (!fileUrl.startsWith('/')) {
      fileUrl = '/' + fileUrl;
    }
    currentUrl = __BASE_API__ + currentUrl;
    // æ‹¼æŽ¥ baseUrl
    fileUrl = javaApi + fileUrl;
  }
  return currentUrl;
};
  return fileUrl;
}
const processItems = items => {
// å¤„理每一类数据:分离图片和视频
function processItems(items) {
  const images = [];
  const videos = [];
  if (!Array.isArray(items)) {
  // æ£€æŸ¥ items æ˜¯å¦å­˜åœ¨ä¸”为数组
  if (!items || !Array.isArray(items)) {
    return { images, videos };
  }
  items.forEach(item => {
    if (!item) return;
    if (!item || !item.url) return;
    const fileUrl = processFileUrl(
      item.previewURL || item.url || item.downloadUrl || item.path || ""
    );
    const contentType = String(item.contentType || "").toLowerCase();
    // å¤„理文件 URL
    const fileUrl = processFileUrl(item.url);
    if (!fileUrl) return;
    if (contentType.startsWith("video/")) {
      videos.push(fileUrl);
      return;
    }
    // æ ¹æ®æ–‡ä»¶æ‰©å±•名判断是图片还是视频
    const urlLower = fileUrl.toLowerCase();
    if (urlLower.match(/\.(jpg|jpeg|png|gif|bmp|webp)$/)) {
    images.push(fileUrl);
    } else if (urlLower.match(/\.(mp4|avi|mov|wmv|flv|mkv|webm)$/)) {
      videos.push(fileUrl);
    } else if (item.contentType) {
      // å¦‚果有 contentType,使用 contentType åˆ¤æ–­
      if (item.contentType.startsWith('image/')) {
        images.push(fileUrl);
      } else if (item.contentType.startsWith('video/')) {
        videos.push(fileUrl);
      }
    }
  });
  return { images, videos };
};
}
const openDialog = row => {
  const { images: beforeImgs, videos: beforeVids } = processItems(
    row.commonFileListBeforeVO || []
  );
  const { images: afterImgs, videos: afterVids } = processItems(
    row.commonFileListVO || []
  );
  const { images: issueImgs, videos: issueVids } = processItems(
    row.commonFileListAfterVO || []
  );
// æ‰“开弹窗并加载数据
const openDialog = async (row) => {
  // ä½¿ç”¨æ­£ç¡®çš„字段名:commonFileListBefore, commonFileListAfter
  // productionIssues å¯èƒ½ä¸å­˜åœ¨ï¼Œä½¿ç”¨ç©ºæ•°ç»„
  const { images: beforeImgs, videos: beforeVids } = processItems(row.commonFileListBefore || []);
  const { images: afterImgs, videos: afterVids } = processItems(row.commonFileListAfter || []);
  const { images: issueImgs, videos: issueVids } = processItems(row.productionIssues || []);
  beforeProductionImgs.value = beforeImgs;
  beforeProductionVideos.value = beforeVids;
  afterProductionImgs.value = afterImgs;
  afterProductionVideos.value = afterVids;
  productionIssuesImgs.value = issueImgs;
  productionIssuesVideos.value = issueVids;
  dialogVisitable.value = true;
};
const showMedia = (items, index, type) => {
  mediaList.value = items;
// æ˜¾ç¤ºåª’体(图片 or è§†é¢‘)
function showMedia(mediaArray, index, type) {
  mediaList.value = mediaArray;
  currentMediaIndex.value = index;
  mediaType.value = type;
  isMediaViewerVisible.value = true;
};
}
const closeMediaViewer = () => {
// å…³é—­åª’体查看器
function closeMediaViewer() {
  isMediaViewerVisible.value = false;
  mediaList.value = [];
  mediaType.value = "image";
};
  mediaType.value = 'image';
}
// è¡¨å•关闭方法
const cancel = () => {
  dialogVisitable.value = false;
};
defineExpose({ openDialog });
</script>
<style scoped lang="scss">
.upload-container {
  display: flex;
@@ -257,48 +282,12 @@
  }
}
.media-list {
  display: flex;
  flex-wrap: wrap;
}
.media-image {
  max-width: 100px;
  height: 100px;
  margin: 5px;
  cursor: pointer;
}
.video-item {
  position: relative;
  margin: 10px;
  cursor: pointer;
}
.video-thumb {
  width: 160px;
  height: 90px;
  background-color: #333;
  display: flex;
  align-items: center;
  justify-content: center;
}
.video-icon {
  width: 30px;
  height: 30px;
  opacity: 0.8;
}
.video-text {
  text-align: center;
  font-size: 12px;
  color: #666;
}
.media-viewer-overlay {
  position: fixed;
  inset: 0;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.8);
  z-index: 9999;
  display: flex;
@@ -311,14 +300,5 @@
  max-width: 90vw;
  max-height: 90vh;
  overflow: hidden;
}
.video-player-wrap {
  position: relative;
}
.video-player {
  max-width: 90vw;
  max-height: 80vh;
}
</style>
src/views/equipmentManagement/inspectionManagement/index.vue
@@ -1,178 +1,202 @@
<template>
  <div class="app-container">
    <el-form :inline="true"
             :model="queryParams"
             class="search-form">
    <el-form :inline="true" :model="queryParams" class="search-form">
      <el-form-item label="巡检任务名称">
        <el-input v-model="queryParams.taskName"
        <el-input
          v-model="queryParams.taskName"
                  placeholder="请输入巡检任务名称"
                  clearable
                  style="width: 200px " />
          style="width: 200px"
        />
      </el-form-item>
      <el-form-item label="所属区域">
        <el-tree-select
          v-model="queryParams.areaId"
          :data="areaOptions"
          :props="areaTreeProps"
          node-key="id"
          value-key="id"
          check-strictly
          clearable
          filterable
          placeholder="请选择所属区域"
          style="width: 220px"
        />
      </el-form-item>
      <el-form-item v-show="activeRadio === 'taskManage'" label="是否启用">
        <el-select
          v-model="queryParams.isEnabled"
          placeholder="请选择"
          clearable
          style="width: 140px"
        >
          <el-option label="启用" :value="1" />
          <el-option label="禁用" :value="0" />
        </el-select>
      </el-form-item>
      <el-form-item v-show="activeRadio === 'task'" label="执行日期">
        <el-date-picker
          v-model="queryDate"
          type="date"
          placeholder="请选择日期"
          value-format="YYYY-MM-DD"
          format="YYYY-MM-DD"
          style="width: 180px"
          @change="handleQuery"
        />
      </el-form-item>
      <el-form-item>
        <el-button type="primary"
                   @click="handleQuery">查询</el-button>
        <el-button type="primary" @click="handleQuery">查询</el-button>
        <el-button @click="resetQuery">重置</el-button>
      </el-form-item>
    </el-form>
    <el-card>
      <div style="display: flex;flex-direction: row;justify-content: space-between;margin-bottom: 10px;">
        <el-radio-group v-model="activeRadio"
                        @change="radioChange">
          <el-radio-button v-for="tab in radios"
      <div class="toolbar">
        <el-radio-group v-model="activeRadio" @change="radioChange">
          <el-radio-button
            v-for="tab in radios"
                           :key="tab.name"
                           :label="tab.label"
                           :value="tab.name" />
            :value="tab.name"
          />
        </el-radio-group>
        <!-- æ“ä½œæŒ‰é’®åŒº -->
        <el-space v-if="activeRadio !== 'task'">
          <el-button type="primary"
                     :icon="Plus"
                     @click="handleAdd(undefined)">新建</el-button>
          <el-button type="danger"
                     :icon="Delete"
                     @click="handleDelete">删除</el-button>
          <el-button type="primary" :icon="Plus" @click="handleAdd(undefined)">新建</el-button>
          <el-button type="danger" :icon="Delete" @click="handleDelete">删除</el-button>
          <el-button @click="handleOut">导出</el-button>
        </el-space>
        <el-space v-else>
          <el-button @click="handleOut">导出</el-button>
        </el-space>
      </div>
      <div>
        <PIMTable :table-loading="tableLoading"
      <PIMTable
        :table-loading="tableLoading"
                  :table-data="tableData"
                  :column="tableColumns"
                  @selection-change="handleSelectionChange"
                  @pagination="handlePagination"
                  :is-selection="true"
                  :border="true"
                  :page="{
                  current: pageNum,
                  size: pageSize,
                  total: total,
                  layout: 'total, sizes, prev, pager, next, jumper'
          total,
          layout: 'total, sizes, prev, pager, next, jumper',
                }"
                  height="calc(100vh - 23em)"
                  :table-style="{ width: '100%' }">
        :table-style="{ width: '100%', height: 'calc(100vh - 23em)' }"
        @selection-change="handleSelectionChange"
        @pagination="handlePagination"
      >
          <template #inspector="{ row }">
            <div class="person-tags">
              <!-- è°ƒè¯•信息,上线时删除 -->
              <!-- {{ console.log('inspector data:', row.inspector) }} -->
              <template v-if="row.inspector && row.inspector.length > 0">
                <el-tag v-for="(person, index) in row.inspector"
              <el-tag
                v-for="(person, index) in row.inspector"
                        :key="index"
                        size="small"
                        type="primary"
                        class="person-tag">
                class="person-tag"
              >
                  {{ person }}
                </el-tag>
              </template>
              <span v-else
                    class="no-data">--</span>
            <span v-else class="no-data">--</span>
            </div>
          </template>
          <template #isEnabled="{ row }">
            <el-tag :type="row.isEnabled === 1 ? 'success' : 'danger'"
                    size="small">
              {{ row.isEnabled == 1 ? '是' : '否' }}
            </el-tag>
        <template #isEnabledSwitch="{ row }">
          <el-switch
            v-model="row.isEnabled"
            :active-value="1"
            :inactive-value="0"
            :loading="row.enableSwitchLoading"
            :before-change="() => handleTimingTaskEnableBeforeChange(row)"
          />
          </template>
        </PIMTable>
      </div>
    </el-card>
    <form-dia ref="formDia"
              @closeDia="handleQuery"></form-dia>
    <view-files ref="viewFiles"></view-files>
    <upload-files ref="uploadFiles"
                  @success="handleQuery"
                  @closeDia="handleQuery"></upload-files>
    <form-dia ref="formDia" @closeDia="handleQuery" />
    <view-files ref="viewFiles" />
  </div>
</template>
<script setup>
  import { Delete, Plus } from "@element-plus/icons-vue";
  import { onMounted, ref, reactive, getCurrentInstance, nextTick } from "vue";
  import { ElMessageBox } from "element-plus";
import { ElMessage, ElMessageBox } from "element-plus";
import { getCurrentInstance, nextTick, onMounted, reactive, ref } from "vue";
  import dayjs from "dayjs";
  // ç»„件引入
  import PIMTable from "@/components/PIMTable/PIMTable.vue";
  import FormDia from "@/views/equipmentManagement/inspectionManagement/components/formDia.vue";
  import UploadFiles from "@/views/equipmentManagement/inspectionManagement/components/uploadFiles.vue";
  import ViewFiles from "@/views/equipmentManagement/inspectionManagement/components/viewFiles.vue";
  // æŽ¥å£å¼•å…¥
  import {
  changeTimingTaskEnable,
    delTimingTask,
    inspectionTaskList,
    timingTaskList,
  } from "@/api/inspectionManagement/index.js";
import { getDeviceAreaTree } from "@/api/equipmentManagement/deviceArea";
  // å…¨å±€å˜é‡
  const { proxy } = getCurrentInstance();
  const formDia = ref();
  const viewFiles = ref();
  const uploadFiles = ref();
  // æŸ¥è¯¢å‚æ•°
  const queryParams = reactive({
    taskName: "",
  areaId: undefined,
  isEnabled: undefined,
  createTimeStart: undefined,
  createTimeEnd: undefined,
  });
  // å•选框配置
// æŸ¥è¯¢æ—¥æœŸï¼ˆç”¨äºŽå®šæ—¶ä»»åŠ¡è®°å½•ï¼‰
const queryDate = ref(dayjs().format("YYYY-MM-DD"));
const areaOptions = ref([]);
const areaTreeProps = {
  label: "areaName",
  children: "children",
};
  const activeRadio = ref("taskManage");
  const radios = reactive([
    { name: "taskManage", label: "巡检任务" },
    { name: "task", label: "巡检记录" },
  { name: "taskManage", label: "定时任务管理" },
  { name: "task", label: "定时任务记录" },
  ]);
  // è¡¨æ ¼æ•°æ®
  const selectedRows = ref([]);
  const tableData = ref([]);
  const operationsArr = ref([]);
  const tableColumns = ref([]);
  const tableLoading = ref(false);
  const total = ref(0);
  const pageNum = ref(1);
  const pageSize = ref(10);
  // åˆ—配置
  const columns = ref([
    { prop: "taskName", label: "巡检任务名称", minWidth: 200 },
    { prop: "inspectionProject", label: "巡检项目", minWidth: 180 },
    { prop: "remarks", label: "备注", minWidth: 180 },
    { prop: "inspector", label: "执行巡检人", minWidth: 180, slot: "inspector" },
    {
      prop: "isEnabled",
      label: "是否启用",
      minWidth: 100,
      dataType: "slot",
      slot: "isEnabled",
        label: "所在区域",
        prop: "areaName",
    },
  { prop: "taskName", label: "巡检任务名称", minWidth: 160 },
  { prop: "remarks", label: "备注", minWidth: 150 },
  { prop: "inspector", label: "执行巡检人", minWidth: 150, slot: "inspector" },
    {
      prop: "frequencyType",
      label: "频次",
      minWidth: 120,
      formatData: params => {
        return params === "DAILY"
          ? "每日"
          : params === "WEEKLY"
          ? "每周"
          : params === "MONTHLY"
          ? "每月"
          : params === "QUARTERLY"
          ? "季度"
          : "";
      },
    minWidth: 150,
    formatData: (value) =>
      ({
        DAILY: "每日",
        WEEKLY: "每周",
        MONTHLY: "每月",
        QUARTERLY: "季度",
                YEARLY: "每年",
      }[value] || ""),
    },
    {
      prop: "frequencyDetail",
      label: "开始日期与时间",
      minWidth: 200,
    minWidth: 150,
      formatter: (row, column, cellValue) => {
        // å…ˆåˆ¤æ–­æ˜¯å¦æ˜¯å­—符串
        if (typeof cellValue !== "string") return "";
        let val = cellValue;
      if (typeof cellValue !== "string") {
        return "";
      }
        const replacements = {
          MON: "周一",
          TUE: "周二",
@@ -182,65 +206,72 @@
          SAT: "周六",
          SUN: "周日",
        };
        // ä½¿ç”¨æ­£åˆ™ä¸€æ¬¡æ€§æ›¿æ¢æ‰€æœ‰åŒ¹é…é¡¹
        return val.replace(
          /MON|TUE|WED|THU|FRI|SAT|SUN/g,
          match => replacements[match]
        );
      return cellValue.replace(/MON|TUE|WED|THU|FRI|SAT|SUN/g, (match) => replacements[match]);
      },
    },
    { prop: "registrant", label: "登记人", minWidth: 120 },
    {
      prop: "createTime",
      label: "登记日期",
      minWidth: 180,
      formatData: cell => {
        if (!cell) return "-";
        try {
          return dayjs(cell).format("YYYY-MM-DD HH:mm:ss");
        } catch {
          return cell;
        }
      },
    },
    // {
    //   prop: "inspectionResult",
    //   label: "巡检结果",
    //   minWidth: 100,
    //   dataType: "tag",
    //   formatData: val => {
    //     return val == 1 ? "正常" : "异常";
    //   },
    //   formatType: val => {
    //     return val == 1 ? "success" : "danger";
    //   },
    // },
    { prop: "abnormalDescription", label: "异常描述", minWidth: 150 },
  { prop: "registrant", label: "登记人", minWidth: 100 },
  { prop: "createTime", label: "登记日期", width: 130 },
  ]);
  // æ“ä½œåˆ—配置
  const getOperationColumn = operations => {
    if (!operations || operations.length === 0) return null;
const isEnabledColumn = {
  prop: "isEnabled",
  label: "是否启用",
  minWidth: 110,
  dataType: "slot",
  slot: "isEnabledSwitch",
};
    const operationConfig = {
// å·¡æ£€çŠ¶æ€åˆ—ï¼ˆä»…å®šæ—¶ä»»åŠ¡è®°å½•æ˜¾ç¤ºï¼‰
const inspectionStatusColumn = {
  prop: "inspectionStatus",
  label: "巡检状态",
  minWidth: 100,
  dataType: "tag",
  formatData: (value) =>
    ({
      1: "待巡检",
      2: "已巡检",
    }[value] || ""),
  formatType: (value) =>
    ({
      1: "warning",
      2: "success",
    }[value] || "info"),
};
// å·¡æ£€ç»“果列(仅定时任务记录显示)
const inspectionResultColumn = {
  prop: "inspectionResult",
  label: "巡检结果",
  minWidth: 100,
  dataType: "tag",
  formatData: (value) =>
    ({
      1: "正常",
      2: "异常",
    }[value] || "-"),
  formatType: (value) =>
    ({
      1: "success",
      2: "error",
    }[value] || "info"),
};
const getOperationColumn = (operations) => {
  if (!operations || operations.length === 0) {
    return null;
  }
  return {
      label: "操作",
      width: operations.length > 1 ? 180 : 130,
    width: 130,
      fixed: "right",
      align: "center",
      dataType: "action",
      operation: operations
        .map(op => {
      .map((op) => {
          switch (op) {
            case "edit":
              return {
                name: "编辑",
                clickFun: handleAdd,
                color: "#409EFF",
              };
            case "upload":
              return {
                name: "上传",
                clickFun: openUploadDialog,
                color: "#409EFF",
              };
            case "viewFile":
@@ -255,146 +286,130 @@
        })
        .filter(Boolean),
    };
};
    return operationConfig;
const loadAreaTree = async () => {
  const { data } = await getDeviceAreaTree();
  areaOptions.value = Array.isArray(data) ? data : [];
  };
  onMounted(() => {
  loadAreaTree();
    radioChange("taskManage");
  });
  // å•选变化
  const radioChange = value => {
const radioChange = (value) => {
    if (value === "taskManage") {
      const operationColumn = getOperationColumn(["edit"]);
      tableColumns.value = [
        ...columns.value,
        ...(operationColumn ? [operationColumn] : []),
      ];
      operationsArr.value = ["edit"];
    } else if (value === "task") {
      const operationColumn = getOperationColumn(["upload", "viewFile"]);
      // å·¡æ£€è®°å½•不展示"是否启用"列
      const taskColumns = columns.value.filter(col => col.prop !== "isEnabled");
      tableColumns.value = [
        ...taskColumns,
        ...(operationColumn ? [operationColumn] : []),
      ];
      operationsArr.value = ["upload", "viewFile"];
    tableColumns.value = [...columns.value, isEnabledColumn, ...(operationColumn ? [operationColumn] : [])];
  } else {
    const operationColumn = getOperationColumn(["viewFile"]);
    // å®šæ—¶ä»»åŠ¡è®°å½•æ·»åŠ å·¡æ£€çŠ¶æ€åˆ—
    tableColumns.value = [...columns.value, inspectionStatusColumn, inspectionResultColumn, ...(operationColumn ? [operationColumn] : [])];
    // åˆ‡æ¢åˆ°å®šæ—¶ä»»åŠ¡è®°å½•æ—¶ï¼Œé»˜è®¤æŸ¥è¯¢å½“å¤©
    queryDate.value = dayjs().format("YYYY-MM-DD");
    queryParams.isEnabled = undefined;
    }
    pageNum.value = 1;
    pageSize.value = 10;
    getList();
  };
  // æŸ¥è¯¢æ“ä½œ
  const handleQuery = () => {
    pageNum.value = 1;
    pageSize.value = 10;
    getList();
  };
  // åˆ†é¡µå¤„理
  const handlePagination = val => {
const handlePagination = (val) => {
    pageNum.value = val.page;
    pageSize.value = val.limit;
    getList();
  };
  // èŽ·å–åˆ—è¡¨æ•°æ®
  const getList = () => {
    tableLoading.value = true;
  // è®¾ç½®æ—¥æœŸå‚数(定时任务记录时)
  if (activeRadio.value === "task" && queryDate.value) {
    queryParams.createTimeStart = `${queryDate.value} 00:00:00`;
    queryParams.createTimeEnd = `${queryDate.value} 23:59:59`;
  } else {
    queryParams.createTimeStart = undefined;
    queryParams.createTimeEnd = undefined;
  }
    const params = {
      ...queryParams,
      size: pageSize.value,
      current: pageNum.value,
    };
    let apiCall;
    if (activeRadio.value === "task") {
      apiCall = inspectionTaskList(params);
    } else {
      apiCall = timingTaskList(params);
    }
  const apiCall =
    activeRadio.value === "task" ? inspectionTaskList(params) : timingTaskList(params);
    apiCall
      .then(res => {
        const rawData = res.data.records || [];
        // å¤„理 inspector å­—段,将字符串转换为数组(适用于所有情况)
        tableData.value = rawData.map(item => {
    .then((res) => {
      const rawData = res?.data?.records || [];
      tableData.value = rawData.map((item) => {
          const processedItem = { ...item };
          processedItem.__raw = { ...item };
          // å¤„理 inspector å­—段
        if (activeRadio.value === "taskManage") {
          processedItem.isEnabled = Number(
            processedItem.isEnabled ?? processedItem.status ?? 1
          );
          processedItem.enableSwitchLoading = false;
        }
          if (processedItem.inspector) {
            if (typeof processedItem.inspector === "string") {
              // å­—符串按逗号分割
              processedItem.inspector = processedItem.inspector
                .split(",")
                .map(s => s.trim())
                .filter(s => s);
              .map((text) => text.trim())
              .filter(Boolean);
            } else if (!Array.isArray(processedItem.inspector)) {
              // éžæ•°ç»„转为数组
              processedItem.inspector = [processedItem.inspector];
            }
          } else {
            // ç©ºå€¼è®¾ä¸ºç©ºæ•°ç»„
            processedItem.inspector = [];
          }
          return processedItem;
        });
        total.value = res.data.total || 0;
      total.value = res?.data?.total || 0;
      })
      .finally(() => {
        tableLoading.value = false;
      });
  };
  // é‡ç½®æŸ¥è¯¢
  const resetQuery = () => {
    for (const key in queryParams) {
      if (!["pageNum", "pageSize"].includes(key)) {
        queryParams[key] = "";
      }
    }
  queryParams.taskName = "";
  queryParams.areaId = undefined;
  queryParams.isEnabled = undefined;
  queryParams.createTimeStart = undefined;
  queryParams.createTimeEnd = undefined;
  // å®šæ—¶ä»»åŠ¡è®°å½•æ—¶é‡ç½®ä¸ºå½“å¤©
  queryDate.value = dayjs().format("YYYY-MM-DD");
    handleQuery();
  };
  // æ–°å¢ž / ç¼–辑
  const handleAdd = row => {
const handleAdd = (row) => {
    const type = row ? "edit" : "add";
    nextTick(() => {
      formDia.value?.openDialog(type, row);
    });
  };
  // æŸ¥çœ‹é™„ä»¶
  const viewFile = row => {
const viewFile = (row) => {
    nextTick(() => {
      viewFiles.value?.openDialog(row);
    });
  };
  const openUploadDialog = row => {
    nextTick(() => {
      uploadFiles.value?.openDialog(row);
    });
  };
  // åˆ é™¤æ“ä½œ
  const handleDelete = () => {
    if (!selectedRows.value.length) {
      proxy.$modal.msgWarning("请选择要删除的数据");
      return;
    }
    const deleteIds = selectedRows.value.map(item => item.id);
  const deleteIds = selectedRows.value.map((item) => item.id);
    proxy.$modal
      .confirm("是否确认删除所选数据项?")
      .then(() => {
        return delTimingTask(deleteIds);
      })
    .then(() => delTimingTask(deleteIds))
      .then(() => {
        proxy.$modal.msgSuccess("删除成功");
        handleQuery();
@@ -402,12 +417,34 @@
      .catch(() => {});
  };
  // å¤šé€‰å˜æ›´
  const handleSelectionChange = selection => {
const handleSelectionChange = (selection) => {
    selectedRows.value = selection;
  };
  // å¯¼å‡º
const handleTimingTaskEnableBeforeChange = async (row) => {
  if (row.enableSwitchLoading) {
    return false;
  }
  const nextValue = Number(row.isEnabled) === 1 ? 0 : 1;
  row.enableSwitchLoading = true;
  try {
    const res = await changeTimingTaskEnable({
      id: row.id,
      isEnabled: nextValue,
    });
    if (res?.code !== 200) {
      throw new Error(res?.msg || "更新失败");
    }
    ElMessage.success("启用状态已更新");
    return true;
  } catch (error) {
    proxy.$modal.msgError(error?.message || "启用状态更新失败");
    return false;
  } finally {
    row.enableSwitchLoading = false;
  }
};
  const handleOut = () => {
    ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
      confirmButtonText: "确认",
@@ -415,13 +452,10 @@
      type: "warning",
    })
      .then(() => {
        // æ ¹æ®å½“前选中的标签页调用不同的导出接口
        if (activeRadio.value === "taskManage") {
          // å·¡æ£€ä»»åŠ¡
          proxy.download("/timingTask/export", {}, "巡检任务.xlsx");
        } else if (activeRadio.value === "task") {
          // å·¡æ£€è®°å½•
          proxy.download("/inspectionTask/export", {}, "巡检记录.xlsx");
        proxy.download("/timingTask/export", {}, "定时任务管理.xlsx");
      } else {
        proxy.download("/inspectionTask/export", {}, "定时任务记录.xlsx");
        }
      })
      .catch(() => {
@@ -431,6 +465,13 @@
</script>
<style scoped>
.toolbar {
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  margin-bottom: 10px;
}
  .person-tags {
    display: flex;
    flex-wrap: wrap;
src/views/equipmentManagement/ledger/Modal.vue
@@ -62,8 +62,15 @@
  formRef.value.loadForm(id);
};
const openCreateModal = async (areaId) => {
  openModal();
  await nextTick();
  formRef.value.setAreaId(areaId);
};
defineExpose({
  openModal,
  loadForm,
  openCreateModal,
});
</script>
src/views/equipmentManagement/ledger/index.vue
@@ -1,5 +1,68 @@
<template>
  <div class="app-container">
  <div class="app-container ledger-view">
    <div class="left-panel">
      <div class="tree-toolbar">
        <el-input
          v-model="treeKeyword"
          style="width: calc(100% - 102px)"
          placeholder="请输入区域名称"
          clearable
          prefix-icon="Search"
          @input="filterTree"
          @clear="filterTree"
        />
        <el-button type="primary" @click="openAreaDialog('addRoot')">新增区域</el-button>
      </div>
      <div class="tree-actions">
        <el-button link type="primary" @click="resetTreeSelection">全部区域</el-button>
      </div>
      <el-tree
        ref="treeRef"
        v-loading="treeLoading"
        :data="treeData"
        :props="treeProps"
        node-key="id"
        highlight-current
        default-expand-all
        :expand-on-click-node="false"
        :filter-node-method="filterTreeNode"
        class="ledger-tree"
        @node-click="handleTreeNodeClick"
      >
        <template #default="{ node, data }">
          <div class="tree-node">
            <span class="tree-node-content">
              <el-icon class="tree-node-icon">
                <component
                  :is="
                    data.children && data.children.length > 0
                      ? node.expanded
                        ? 'FolderOpened'
                        : 'Folder'
                      : 'Tickets'
                  "
                />
              </el-icon>
              <span class="tree-node-label">{{ data.areaName }}</span>
            </span>
            <div class="tree-node-actions">
              <el-button link type="primary" @click.stop="openAreaDialog('edit', data)">编辑</el-button>
              <el-button link type="primary" @click.stop="openAreaDialog('addChild', data)">新增</el-button>
              <el-button
                v-if="!hasChildren(data)"
                link
                type="danger"
                @click.stop="handleDeleteArea(data)"
              >
                åˆ é™¤
              </el-button>
            </div>
          </div>
        </template>
      </el-tree>
    </div>
    <div class="right-panel">
    <el-form :model="filters" :inline="true">
      <el-form-item label="设备名称">
        <el-input
@@ -28,22 +91,32 @@
            @change="getTableData"
        />
      </el-form-item>
      <el-form-item label="录入日期:">
        <el-date-picker v-model="filters.entryDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="daterange"
                        placeholder="请选择" clearable @change="changeDaterange" />
        <el-form-item label="录入日期">
          <el-date-picker
            v-model="filters.entryDate"
            value-format="YYYY-MM-DD"
            format="YYYY-MM-DD"
            type="daterange"
            placeholder="请选择"
            clearable
            @change="changeDaterange"
          />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="getTableData">搜索</el-button>
        <el-button @click="resetFilters">重置</el-button>
          <el-button @click="handleResetFilters">重置</el-button>
      </el-form-item>
    </el-form>
    <div class="table_list">
      <div class="actions">
        <div></div>
          <div class="actions-tip">
            <span v-if="selectedAreaName">当前区域:{{ selectedAreaName }}</span>
          </div>
        <div>
          <el-button type="primary" @click="add" icon="Plus"> æ–°å¢ž </el-button>
          <el-button type="info" @click="handleImport" icon="Upload">导入</el-button>
          <el-button @click="handleOut" icon="download">导出</el-button>
            <el-button type="primary" icon="Plus" @click="add">新增</el-button>
            <el-button type="info" icon="Upload" @click="handleImport">导入</el-button>
            <el-button icon="download" @click="handleOut">导出</el-button>
          <el-button
            type="danger"
            icon="Delete"
@@ -66,20 +139,53 @@
        }"
        @selection-change="handleSelectionChange"
        @pagination="changePage"
      >
      </PIMTable>
        />
    </div>
    <Modal ref="modalRef" @success="getTableData"></Modal>
    </div>
    <Modal ref="modalRef" @success="getTableData" />
    <el-dialog
      v-model="areaDialogVisible"
      :title="areaDialogTitle"
      width="480px"
      @close="closeAreaDialog"
    >
      <el-form ref="areaFormRef" :model="areaForm" :rules="areaRules" label-width="88px">
        <el-form-item label="区域名称" prop="areaName">
          <el-input v-model="areaForm.areaName" placeholder="请输入区域名称" />
        </el-form-item>
        <el-form-item label="排序" prop="sort">
          <el-input-number v-model="areaForm.sort" :min="0" :step="1" style="width: 100%" />
        </el-form-item>
        <el-form-item label="备注" prop="remark">
          <el-input
            v-model="areaForm.remark"
            type="textarea"
            :rows="4"
            maxlength="200"
            show-word-limit
            placeholder="请输入备注"
          />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitAreaForm">确定</el-button>
          <el-button @click="closeAreaDialog">取消</el-button>
        </div>
      </template>
    </el-dialog>
    <el-dialog v-model="qrDialogVisible" title="二维码" width="300px" draggable>
      <div style="text-align:center;">
        <img :src="qrCodeUrl" alt="二维码" style="width:200px;height:200px;" />
        <div style="margin:10px 0;">
      <div class="qr-dialog">
        <img :src="qrCodeUrl" alt="二维码" class="qr-image" />
        <div class="qr-footer">
          <el-button type="primary" @click="downloadQRCode">下载二维码图片</el-button>
        </div>
      </div>
    </el-dialog>
    
    <!-- å¯¼å…¥å¯¹è¯æ¡† -->
    <el-dialog :title="upload.title" v-model="upload.open" width="400px" append-to-body>
      <el-upload
        ref="uploadRef"
@@ -98,7 +204,14 @@
        <template #tip>
          <div class="el-upload__tip text-center">
            <span>仅允许导入xls、xlsx格式文件。</span>
            <el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline; margin-left: 5px;" @click="importTemplate">下载模板</el-link>
            <el-link
              type="primary"
              :underline="false"
              style="font-size: 12px; vertical-align: baseline; margin-left: 5px"
              @click="importTemplate"
            >
              ä¸‹è½½æ¨¡æ¿
            </el-link>
          </div>
        </template>
      </el-upload>
@@ -109,52 +222,19 @@
        </div>
      </template>
    </el-dialog>
    <!-- è¯¦æƒ…对话框 -->
    <el-dialog v-model="detailDialogVisible" title="设备台账详情" width="60%" draggable>
      <el-descriptions :column="2" border>
        <el-descriptions-item label="设备名称">{{ detailData.deviceName }}</el-descriptions-item>
        <el-descriptions-item label="规格型号">{{ detailData.deviceModel }}</el-descriptions-item>
        <el-descriptions-item label="设备品牌">{{ detailData.deviceBrand }}</el-descriptions-item>
        <el-descriptions-item label="设备类型">{{ detailData.type }}</el-descriptions-item>
        <el-descriptions-item label="供应商">{{ detailData.supplierName }}</el-descriptions-item>
        <el-descriptions-item label="存放位置">{{ detailData.storageLocation }}</el-descriptions-item>
        <el-descriptions-item label="单位">{{ detailData.unit }}</el-descriptions-item>
        <el-descriptions-item label="数量">{{ detailData.number }}</el-descriptions-item>
        <el-descriptions-item label="启用折旧">{{ detailData.isDepr === 1 ? '是' : '否' }}</el-descriptions-item>
        <el-descriptions-item label="每年折旧金额">{{ detailData.annualDepreciationAmount }}</el-descriptions-item>
        <el-descriptions-item label="含税单价">{{ detailData.taxIncludingPriceUnit }}</el-descriptions-item>
        <el-descriptions-item label="含税总价">{{ detailData.taxIncludingPriceTotal }}</el-descriptions-item>
        <el-descriptions-item label="税率(%)">{{ detailData.taxRate }}</el-descriptions-item>
        <el-descriptions-item label="不含税总价">{{ detailData.unTaxIncludingPriceTotal }}</el-descriptions-item>
        <el-descriptions-item label="录入日期">{{ detailData.createTime }}</el-descriptions-item>
        <el-descriptions-item label="预计运行时间">{{ detailData.planRuntimeTime ? dayjs(detailData.planRuntimeTime).format('YYYY-MM-DD') : '' }}</el-descriptions-item>
        <el-descriptions-item label="设备图片" :span="2">
          <div v-if="detailData.storageBlobVOs && detailData.storageBlobVOs.length > 0" style="display: flex; gap: 10px; flex-wrap: wrap;">
            <el-image
              v-for="(file, index) in detailData.storageBlobVOs"
              :key="index"
              :src="file.previewURL || file.url"
              :preview-src-list="detailData.storageBlobVOs.map(u => u.previewURL || u.url)"
              :initial-index="index"
              style="width: 100px; height: 100px"
              fit="cover"
            />
          </div>
          <span v-else>无图片</span>
        </el-descriptions-item>
      </el-descriptions>
      <template #footer>
        <el-button @click="detailDialogVisible = false">关闭</el-button>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { usePaginationApi } from "@/hooks/usePaginationApi";
// import { Search } from "@element-plus/icons-vue";
import { getLedgerPage, delLedger, getLedgerById } from "@/api/equipmentManagement/ledger";
import { getLedgerPage, delLedger } from "@/api/equipmentManagement/ledger";
import {
  getDeviceAreaTree,
  getDeviceAreaDetail,
  addDeviceArea,
  updateDeviceArea,
  deleteDeviceArea,
} from "@/api/equipmentManagement/deviceArea";
import { onMounted, getCurrentInstance, ref, reactive } from "vue";
import Modal from "./Modal.vue";
import { ElMessageBox, ElMessage } from "element-plus";
@@ -167,31 +247,47 @@
  name: "设备台账",
});
// è¡¨æ ¼å¤šé€‰æ¡†é€‰ä¸­é¡¹
const multipleList = ref([]);
const { proxy } = getCurrentInstance();
const modalRef = ref();
const treeRef = ref();
const areaFormRef = ref();
const treeKeyword = ref("");
const treeLoading = ref(false);
const treeData = ref([]);
const selectedAreaName = ref("");
const areaDialogVisible = ref(false);
const areaDialogTitle = ref("新增区域");
const areaDialogMode = ref("addRoot");
const qrDialogVisible = ref(false);
const qrCodeUrl = ref("");
const qrRowData = ref(null);
const uploadRef = ref(null);
const detailDialogVisible = ref(false);
const detailData = ref({});
const treeProps = {
  children: "children",
  label: "areaName",
};
// å¯¼å…¥ç›¸å…³
const uploadRef = ref(null)
const upload = reactive({
  // æ˜¯å¦æ˜¾ç¤ºå¼¹å‡ºå±‚
  open: false,
  // å¼¹å‡ºå±‚标题
  title: "",
  // æ˜¯å¦ç¦ç”¨ä¸Šä¼ 
  isUploading: false,
  // è®¾ç½®ä¸Šä¼ çš„请求头部
  headers: { Authorization: "Bearer " + getToken() },
  // ä¸Šä¼ çš„地址
  url: import.meta.env.VITE_APP_BASE_API + "/device/ledger/import"
})
  url: import.meta.env.VITE_APP_BASE_API + "/device/ledger/import",
});
const areaForm = reactive({
  id: undefined,
  areaName: "",
  parentId: undefined,
  sort: 0,
  remark: "",
});
const areaRules = {
  areaName: [{ required: true, message: "请输入区域名称", trigger: "blur" }],
};
const {
  filters,
@@ -199,7 +295,6 @@
  dataList,
  pagination,
  getTableData,
  resetFilters,
  onCurrentChange,
} = usePaginationApi(
  getLedgerPage,
@@ -207,10 +302,17 @@
    deviceName: undefined,
    deviceModel: undefined,
    supplierName: undefined,
    entryDate: undefined,
    entryDateStart: undefined,
    entryDateEnd: undefined,
    areaId: undefined,
    areaName: undefined,
  },
  [
    {
      label: "所在区域",
      prop: "areaName",
    },
    {
      label: "设备名称",
      prop: "deviceName",
@@ -219,10 +321,10 @@
      label: "规格型号",
      prop: "deviceModel",
    },
    {
      label: "设备品牌",
      prop: "deviceBrand",
    },
    // {
    //   label: "设备品牌",
    //   prop: "deviceBrand",
    // },
    {
      label: "设备类型",
      prop: "type",
@@ -247,37 +349,38 @@
      label: "录入日期",
      prop: "createTime",
      formatData: (v) => {
        if (!v) return '';
        // å¦‚果包含时分秒,只取日期部分
        if (v.includes(' ')) {
          return v.split(' ')[0];
        }
        return v;
        if (!v) return "";
        return v.includes(" ") ? v.split(" ")[0] : v;
      },
    },
    {
      label: "物联设备",
      prop: "isIotDevice",
      formatData: (v) => {
        return v === 1 ? "是" : "否";
      },
    },
    {
      label: "外部编码",
      prop: "externalCode",
    },
        {
            dataType: "action",
            label: "操作",
            align: "center",
            fixed: 'right',
            width: 180,
      fixed: "right",
      width: 150,
            operation: [
        {
          name: "详情",
          clickFun: (row) => {
            handleDetail(row);
          },
        },
                {
                    name: "编辑",
                    clickFun: (row) => {
                        edit(row.id)
            edit(row.id);
                    },
                },
                {
                    name: "二维码",
          name: "生成二维码",
                    clickFun: (row) => {
                        showQRCode(row)
            showQRCode(row);
                    },
                },
            ],
@@ -285,7 +388,123 @@
  ]
);
// å¤šé€‰åŽåšä»€ä¹ˆ
const loadTreeData = async () => {
  treeLoading.value = true;
  try {
    const res = await getDeviceAreaTree();
    treeData.value = Array.isArray(res?.data) ? res.data : Array.isArray(res) ? res : [];
  } finally {
    treeLoading.value = false;
  }
};
const resetAreaForm = () => {
  areaForm.id = undefined;
  areaForm.areaName = "";
  areaForm.parentId = undefined;
  areaForm.sort = 0;
  areaForm.remark = "";
};
const filterTree = () => {
  treeRef.value?.filter(treeKeyword.value);
};
const filterTreeNode = (value, data) => {
  if (!value) {
    return true;
  }
  return String(data.areaName || "").includes(value);
};
const handleTreeNodeClick = (data) => {
  filters.areaId = data.id;
  filters.areaName = data.areaName;
  selectedAreaName.value = data.areaName || "";
  getTableData();
};
const openAreaDialog = async (mode, row) => {
  areaDialogMode.value = mode;
  areaDialogTitle.value =
    mode === "edit" ? "编辑区域" : mode === "addChild" ? "新增子区域" : "新增区域";
  resetAreaForm();
  areaDialogVisible.value = true;
  if (mode === "addChild") {
    areaForm.parentId = row.id;
    areaForm.sort = 0;
    return;
  }
  if (mode === "edit" && row?.id) {
    const res = await getDeviceAreaDetail(row.id);
    const detail = res?.data || {};
    areaForm.id = detail.id;
    areaForm.areaName = detail.areaName || "";
    areaForm.parentId = detail.parentId;
    areaForm.sort = detail.sort ?? 0;
    areaForm.remark = detail.remark || "";
  }
};
const closeAreaDialog = () => {
  areaDialogVisible.value = false;
  areaFormRef.value?.resetFields();
  resetAreaForm();
};
const submitAreaForm = () => {
  areaFormRef.value?.validate(async (valid) => {
    if (!valid) {
      return;
    }
    const submitData = {
      id: areaForm.id,
      areaName: areaForm.areaName,
      parentId: areaForm.parentId,
      sort: areaForm.sort,
      remark: areaForm.remark,
    };
    const request = areaDialogMode.value === "edit" ? updateDeviceArea : addDeviceArea;
    const { code } = await request(submitData);
    if (code === 200) {
      ElMessage.success(areaDialogMode.value === "edit" ? "修改成功" : "新增成功");
      closeAreaDialog();
      await loadTreeData();
    }
  });
};
const handleDeleteArea = (row) => {
  if (hasChildren(row)) {
    ElMessage.warning("当前区域存在下级区域,不能删除");
    return;
  }
  ElMessageBox.confirm("此操作将删除该设备区域,是否继续?", "提示", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning",
  }).then(async () => {
    const { code } = await deleteDeviceArea([row.id]);
    if (code === 200) {
      ElMessage.success("删除成功");
      if (filters.areaId === row.id) {
        resetTreeSelection();
      }
      await loadTreeData();
    }
  });
};
const hasChildren = (row) => Array.isArray(row?.children) && row.children.length > 0;
const resetTreeSelection = () => {
  treeRef.value?.setCurrentKey(null);
  selectedAreaName.value = "";
  filters.areaId = undefined;
  filters.areaName = undefined;
  getTableData();
};
const handleSelectionChange = (selectionList) => {
  multipleList.value = selectionList;
};
@@ -293,23 +512,19 @@
const add = () => {
  modalRef.value.openModal();
};
const edit = (id) => {
  modalRef.value.loadForm(id);
};
const handleDetail = async (row) => {
  const { code, data } = await getLedgerById(row.id);
  if (code == 200) {
    detailData.value = data;
    detailDialogVisible.value = true;
  }
};
const changePage = ({ page, limit }) => {
  pagination.currentPage = page;
    pagination.pageSize = limit;
  onCurrentChange(page);
};
const deleteRow = (id) => {
  ElMessageBox.confirm("此操作将永久删除该文件, æ˜¯å¦ç»§ç»­?", "提示", {
  ElMessageBox.confirm("此操作将永久删除该数据,是否继续?", "提示", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning",
@@ -336,14 +551,24 @@
  getTableData();
};
const handleResetFilters = () => {
  filters.deviceName = undefined;
  filters.deviceModel = undefined;
  filters.supplierName = undefined;
  filters.entryDate = undefined;
  filters.entryDateStart = undefined;
  filters.entryDateEnd = undefined;
  getTableData();
};
const handleOut = () => {
  ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
  ElMessageBox.confirm("当前查询结果将被导出,是否确认导出?", "导出", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      proxy.download(`/device/ledger/export`, {}, "设备台账档案.xlsx");
      proxy.download("/device/ledger/export", {}, "设备台账档案.xlsx");
    })
    .catch(() => {
      proxy.$modal.msg("已取消");
@@ -351,36 +576,8 @@
};
const showQRCode = async (row) => {
  // ç›´æŽ¥ä½¿ç”¨URL,不要用JSON.stringify包装
  const qrContent = proxy.javaApi + '/device-info?deviceId=' + row.id;
  const qrDataUrl = await QRCode.toDataURL(qrContent, { width: 200, margin: 2 });
  // åˆ›å»ºcanvas合成带名称的二维码图片
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  const qrSize = 200;
  const textHeight = 30;
  const padding = 10;
  canvas.width = qrSize + padding * 2;
  canvas.height = qrSize + textHeight + padding * 2;
  // å¡«å……白色背景
  ctx.fillStyle = '#ffffff';
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  // ç»˜åˆ¶äºŒç»´ç 
  const qrImg = new Image();
  qrImg.src = qrDataUrl;
  await new Promise((resolve) => { qrImg.onload = resolve; });
  ctx.drawImage(qrImg, padding, padding, qrSize, qrSize);
  // ç»˜åˆ¶è®¾å¤‡åç§°
  ctx.fillStyle = '#333333';
  ctx.font = 'bold 14px Arial';
  ctx.textAlign = 'center';
  ctx.fillText(row.deviceName || '', canvas.width / 2, qrSize + padding + 20);
  qrCodeUrl.value = canvas.toDataURL('image/png');
  const qrContent = proxy.javaApi + "/device-info?deviceId=" + row.id;
  qrCodeUrl.value = await QRCode.toDataURL(qrContent);
  qrRowData.value = row;
  qrDialogVisible.value = true;
};
@@ -392,48 +589,141 @@
  a.click();
};
// å¯¼å…¥æŒ‰é’®æ“ä½œ
const handleImport = () => {
  upload.title = "设备台账导入"
  upload.open = true
}
  upload.title = "设备台账导入";
  upload.open = true;
};
// ä¸‹è½½æ¨¡æ¿æ“ä½œ
const importTemplate = () => {
  proxy.download("/device/ledger/downloadTemplate", {}, `设备台账导入模板_${new Date().getTime()}.xlsx`)
}
  proxy.download("/device/ledger/downloadTemplate", {}, `设备台账导入模板_${new Date().getTime()}.xlsx`);
};
// æ–‡ä»¶ä¸Šä¼ ä¸­å¤„理
const handleFileUploadProgress = (event, file, fileList) => {
  upload.isUploading = true
}
const handleFileUploadProgress = () => {
  upload.isUploading = true;
};
// æ–‡ä»¶ä¸Šä¼ æˆåŠŸå¤„ç†
const handleFileSuccess = (response, file, fileList) => {
  upload.open = false
  upload.isUploading = false
  proxy.$refs["uploadRef"].handleRemove(file)
  proxy.$alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + "</div>", "导入结果", { dangerouslyUseHTMLString: true })
  getTableData()
}
const handleFileSuccess = (response, file) => {
  upload.open = false;
  upload.isUploading = false;
  uploadRef.value?.handleRemove(file);
  proxy.$alert(
    "<div style='overflow:auto;overflow-x:hidden;max-height:70vh;padding:10px 20px 0;'>" +
      response.msg +
      "</div>",
    "导入结果",
    { dangerouslyUseHTMLString: true }
  );
  getTableData();
};
// æäº¤ä¸Šä¼ æ–‡ä»¶
const submitFileForm = () => {
  proxy.$refs["uploadRef"].submit()
}
  uploadRef.value?.submit();
};
onMounted(() => {
onMounted(async () => {
  await loadTreeData();
  getTableData();
});
</script>
<style lang="scss" scoped>
.table_list {
  margin-top: unset;
.ledger-view {
  display: flex;
  gap: 20px;
}
.left-panel {
  width: 320px;
  min-width: 320px;
  padding: 16px;
  background: #fff;
  border-radius: 4px;
}
.right-panel {
  flex: 1;
  min-width: 0;
  padding: 16px;
  background: #fff;
  border-radius: 4px;
}
.tree-toolbar {
  display: flex;
  gap: 10px;
  align-items: center;
  margin-bottom: 8px;
}
.tree-actions {
  display: flex;
  justify-content: flex-end;
  margin-bottom: 8px;
}
.ledger-tree {
  height: calc(100vh - 230px);
  overflow-y: auto;
}
.tree-node {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.tree-node-content {
  display: flex;
  align-items: center;
  min-width: 0;
}
.tree-node-icon {
  color: #e6a23c;
  margin-right: 8px;
  font-size: 18px;
}
.tree-node-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tree-node-actions {
  flex-shrink: 0;
}
.table_list {
  margin-top: 0;
}
.actions {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 10px;
  gap: 12px;
}
.actions-tip {
  color: #606266;
  font-size: 14px;
}
.qr-dialog {
  text-align: center;
}
.qr-image {
  width: 200px;
  height: 200px;
}
.qr-footer {
  margin: 10px 0;
}
</style>
src/views/equipmentManagement/measurementEquipment/components/calibrationDia.vue
@@ -228,6 +228,39 @@
    form.value.entryDate = getCurrentDate();
}
// ä¸Šä¼ å‰æ ¡æ£€
function handleBeforeUpload(file) {
    proxy.$modal.loading("正在上传文件,请稍候...");
    return true;
}
// ä¸Šä¼ å¤±è´¥
function handleUploadError(err) {
    proxy.$modal.msgError("上传文件失败");
    proxy.$modal.closeLoading();
}
// ä¸Šä¼ æˆåŠŸå›žè°ƒ
function handleUploadSuccess(res, file, uploadFiles) {
    proxy.$modal.closeLoading();
    if (res.code === 200) {
        file.tempId = res.data.tempId;
        form.value.tempFileIds.push(file.tempId);
        proxy.$modal.msgSuccess("上传成功");
    } else {
        proxy.$modal.msgError(res.msg);
        proxy.$refs.fileUpload.handleRemove(file);
    }
}
// ç§»é™¤æ–‡ä»¶
function handleRemove(file) {
    if (operationType.value === "edit") {
        let ids = [];
        ids.push(file.id);
        delLedgerFile(ids).then((res) => {
            proxy.$modal.msgSuccess("删除成功");
        });
    }
}
// å¤„理有效日期输入,只允许正整数
const handleValidInput = (value) => {
    if (value === '' || value === null || value === undefined) {
src/views/equipmentManagement/measurementEquipment/components/dialogForm.vue
ÎļþÒÑɾ³ý
src/views/equipmentManagement/measurementEquipment/index.vue
@@ -1,50 +1,39 @@
<template>
  <div class="app-container">
    <div class="search_form mb20">
        <div class="search_form">
      <div>
        <span class="search_title">录入日期:</span>
        <el-date-picker v-model="searchForm.recordDate"
                <el-date-picker
                    v-model="searchForm.recordDate"
                        value-format="YYYY-MM-DD"
                        format="YYYY-MM-DD"
                        type="date"
                        placeholder="请选择"
                        clearable
                        style="width: 160px"
                        @change="handleQuery" />
        <span class="search_title ml10">计量器具编号:</span>
        <el-input v-model="searchForm.code"
                  placeholder="请输入编号"
                  clearable
                  style="width: 240px"
                  @change="handleQuery" />
        <span class="search_title ml10">状态:</span>
        <el-select v-model="searchForm.status"
                   placeholder="请选择状态"
                   @change="handleQuery"
                   style="width: 160px"
                   clearable>
          <el-option label="有效"
                     :value="1"></el-option>
          <el-option label="逾期"
                     :value="2"></el-option>
                />
                <span class="search_title ml10">计量器具编号:</span>
                <el-input v-model="searchForm.code" placeholder="请输入编号" clearable style="width: 240px" @change="handleQuery"/>
                <span class="search_title ml10">状态:</span>
                <el-select v-model="searchForm.status" placeholder="请选择状态" @change="handleQuery" style="width: 160px" clearable>
                    <el-option label="有效" :value="1"></el-option>
                    <el-option label="逾期" :value="2"></el-option>
        </el-select>
        <el-button type="primary"
                   @click="handleQuery"
                   style="margin-left: 10px">搜索</el-button>
        <el-button @click="handleReset"
                   style="margin-left: 10px">重置</el-button>
                <el-button type="primary" @click="handleQuery" style="margin-left: 10px"
                >搜索</el-button
                >
                <el-button @click="handleReset" style="margin-left: 10px">重置</el-button>
      </div>
      <div>
        <el-button type="primary"
                   @click="openForm('add')">新增计量器具</el-button>
        <el-button type="danger"
                   plain
                   @click="handleDelete">删除</el-button>
                <el-button type="primary" @click="openForm('add')">新增计量器具</el-button>
                <el-button type="danger" plain @click="handleDelete">删除</el-button>
        <el-button @click="handleOut">导出</el-button>
      </div>
    </div>
    <div class="table_list">
      <PIMTable rowKey="id"
            <PIMTable
                rowKey="id"
                :column="tableColumn"
                :tableData="tableData"
                :page="page"
@@ -53,26 +42,17 @@
                :tableLoading="tableLoading"
                @pagination="pagination"
                :dbRowClick="dbRowClick"
                :rowClassName="rowClassName"></PIMTable>
            ></PIMTable>
    </div>
    <form-dia ref="formDia"
              @close="handleQuery"></form-dia>
    <calibration-dia ref="calibrationDia"
                     @close="handleQuery"></calibration-dia>
        <form-dia ref="formDia" @close="handleQuery"></form-dia>
        <calibration-dia ref="calibrationDia" @close="handleQuery"></calibration-dia>
    <files-dia ref="filesDia"></files-dia>
    <rowClickDataForm ref="rowClickData"></rowClickDataForm>
  </div>
</template>
<script setup>
  import {
    onMounted,
    ref,
    reactive,
    toRefs,
    getCurrentInstance,
    nextTick,
  } from "vue";
import {onMounted, ref, reactive, toRefs, getCurrentInstance, nextTick} from "vue";
  import FormDia from "@/views/equipmentManagement/measurementEquipment/components/formDia.vue";
  import { ElMessageBox } from "element-plus";
  import useUserStore from "@/store/modules/user.js";
@@ -82,9 +62,9 @@
    measuringInstrumentListPage,
  } from "@/api/equipmentManagement/measurementEquipment.js";
  import FilesDia from "./filesDia.vue";
  import rowClickDataForm from "./components/rowClickData.vue";
import rowClickDataForm from "./components/rowClickData.vue"
  const { proxy } = getCurrentInstance();
  const userStore = useUserStore();
const userStore = useUserStore()
  const data = reactive({
    searchForm: {
@@ -100,56 +80,68 @@
      label: "出厂编号",
      prop: "code",
      minWidth: 150,
      align: "center",
    align:"center"
    },
    {
      label: "计量器具名称",
      prop: "name",
      width: "160px",
    width: '160px',
      align: "center",
  },
    {
        label: "安装位置",
        prop: "instationLocation",
        width: 150,
    align:"center"
    },
    {
      label: "检定单位",
      prop: "unit",
      width: 200,
      align: "center",
    align:"center"
    },
    {
      label: "证书编号",
      prop: "model",
      width: 200,
      align: "center",
    align:"center"
    },
    {
      label: "最新鉴定日期",
      prop: "mostDate",
      width: 130,
      align: "center",
    align:"center"
    },
    {
      label: "录入人",
      prop: "userName",
      width: 130,
      align: "center",
    align:"center"
    },
    {
      label: "录入日期",
      prop: "recordDate",
      align: "center",
      minWidth: 130,
    minWidth: 130
    },
    {
      label: "有效日期",
      prop: "valid",
      width: 130,
      align: "center",
    align:"center"
    },
  {
    label: "检定周期(天)",
    prop: "cycle",
    width: 130,
    align:"center"
    },
    {
      label: "状态",
      prop: "status",
      width: 130,
      align: "center",
      formatData: params => {
    formatData: (params) => {
        if (params === 1) {
          return "有效";
        } else if (params === 2) {
@@ -157,26 +149,26 @@
        } else {
          return null;
        }
      },
    }
    },
    {
      dataType: "action",
      label: "操作",
      align: "center",
      width: "130",
      fixed: "right",
        width: '130',
        fixed: 'right',
      operation: [
        {
          name: "附件",
          type: "text",
          clickFun: row => {
          clickFun: (row) => {
            openFilesFormDia(row);
          },
        },
        {
          name: "编辑",
                name: "查看",
          type: "text",
          clickFun: row => {
                clickFun: (row) => {
            openCalibrationDia("verifying", row);
          },
        },
@@ -185,8 +177,8 @@
  ]);
  const tableData = ref([]);
  const tableLoading = ref(false);
  const rowClickData = ref([]);
  const filesDia = ref();
const rowClickData = ref([])
const filesDia = ref()
  const page = reactive({
    current: 1,
    size: 100,
@@ -195,54 +187,20 @@
  const selectedRows = ref([]);
  // æ‰“开附件弹框
  const openFilesFormDia = row => {
    filesDia.value?.openDialog(row, "measuring_instrument_ledger");
const openFilesFormDia = (row) => {
    filesDia.value?.openDialog(row,'计量器具台账')
  };
  const dbRowClick = row => {
    rowClickData.value?.openDialog(row);
  };
  // è¡Œæ ·å¼ï¼šå¿«åˆ°æœŸï¼ˆ7天内)或逾期标红
  const rowClassName = ({ row }) => {
    console.log("rowClassName called:", row);
    // valid æ˜¯æœ‰æ•ˆå¤©æ•°ï¼ŒmostDate æ˜¯æœ€æ–°æ£€å®šæ—¥æœŸ
    if (row.valid && row.mostDate) {
      const mostDate = new Date(row.mostDate);
      // è®¡ç®—到期日期 = æ£€å®šæ—¥æœŸ + æœ‰æ•ˆå¤©æ•°
      const validDays = parseInt(row.valid) || 0;
      const expireDate = new Date(mostDate);
      expireDate.setDate(expireDate.getDate() + validDays);
      const now = new Date();
      const diffDays = Math.ceil((expireDate - now) / (1000 * 60 * 60 * 24));
      console.log(
        "row:",
        row.code,
        "validDays:",
        validDays,
        "expireDate:",
        expireDate,
        "diffDays:",
        diffDays
      );
      // 7天内到期或已逾期都标红
      if (diffDays <= 7) {
        console.log("return warning-row");
        return "warning-row";
const dbRowClick = (row)=>{
  rowClickData.value?.openDialog(row)
      }
    } else {
      console.log("row missing valid or mostDate:", row.valid, row.mostDate);
    }
    return "";
  };
  // è¡¨æ ¼é€‰æ‹©æ•°æ®
  const handleSelectionChange = selection => {
const handleSelectionChange = (selection) => {
    selectedRows.value = selection;
  };
  const formDia = ref();
  const calibrationDia = ref();
const formDia = ref()
const calibrationDia = ref()
  // æŸ¥è¯¢åˆ—表
  /** æœç´¢æŒ‰é’®æ“ä½œ */
@@ -259,41 +217,39 @@
    page.current = 1;
    getList();
  };
  const pagination = obj => {
const pagination = (obj) => {
    page.current = obj.page;
    page.size = obj.limit;
    getList();
  };
  const getList = () => {
    tableLoading.value = true;
    measuringInstrumentListPage({ ...searchForm.value, ...page })
      .then(res => {
    measuringInstrumentListPage({ ...searchForm.value, ...page }).then((res) => {
        tableLoading.value = false;
        tableData.value = res.data.records;
        page.total = res.data.total;
      })
      .catch(err => {
    }).catch((err) => {
        tableLoading.value = false;
      });
    })
  };
  // æ‰“开弹框
  const openForm = (type, row) => {
    nextTick(() => {
      formDia.value?.openDialog(type, row);
    });
        formDia.value?.openDialog(type, row)
    })
  };
  // æ‰“开检定校准弹框
  const openCalibrationDia = (type, row) => {
    nextTick(() => {
      calibrationDia.value?.openDialog(type, row);
    });
  };
        calibrationDia.value?.openDialog(type, row)
    })
}
  const handleDelete = () => {
    let ids = [];
    if (selectedRows.value.length > 0) {
      ids = selectedRows.value.map(item => item.id);
        ids = selectedRows.value.map((item) => item.id);
    } else {
      proxy.$modal.msgWarning("请选择数据");
      return;
@@ -306,7 +262,7 @@
      .then(() => {
        tableLoading.value = true;
        measuringInstrumentDelete(ids)
          .then(res => {
                .then((res) => {
            proxy.$modal.msgSuccess("删除成功");
            getList();
          })
@@ -326,11 +282,7 @@
      type: "warning",
    })
      .then(() => {
        proxy.download(
          "/measuringInstrumentLedger/export",
          {},
          "计量器具台账.xlsx"
        );
            proxy.download("/measuringInstrumentLedger/export", {}, "计量器具台账.xlsx");
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
@@ -342,13 +294,5 @@
</script>
<style scoped>
  :deep(.el-table .warning-row) {
    background-color: #fef0f0 !important;
  }
  :deep(.el-table .warning-row:hover > td) {
    background-color: #f9d5d5 !important;
  }
  :deep(.el-table .el-table__body tr.warning-row td) {
    background-color: #fef0f0 !important;
  }
</style>
src/views/equipmentManagement/operationManagement/index.vue
@@ -104,7 +104,7 @@
          align="center"
        >
          <template #default="scope">
            {{ getRuntimeDurationDisplay(scope.row) }}
            {{ scope.row.runtimeDuration || '-' }}
          </template>
        </el-table-column>
        <el-table-column
@@ -154,8 +154,7 @@
</template>
<script setup>
import { ref, onMounted, onUnmounted, computed } from 'vue'
import dayjs from 'dayjs'
import { ref, onMounted, computed } from 'vue'
import { ElMessage } from 'element-plus'
import {
  VideoPlay,
@@ -194,98 +193,6 @@
  return filtered
})
// è¿è¡Œä¸­æ— ç»“束时间时,运行时长需随当前时间变化,用 tick è§¦å‘模板重算
const runtimeDisplayTick = ref(0)
/** å–后端可能使用的开始/结束时间字段 */
const pickStartTime = (row) => row?.startRuntimeTime ?? row?.startTime ?? row?.start_time
const pickEndTime = (row) => row?.endRuntimeTime ?? row?.endTime ?? row?.end_time
/**
 * è§£æžæŽ¥å£/前端写入的各类时间:时间戳、ISO å­—符串、yyyy-MM-dd HH:mm:ss、Jackson æ•°ç»„ [y,M,d,h,m,s]、含中文的 toLocaleString ç­‰
 */
const parseDeviceTime = (input) => {
  if (input === null || input === undefined || input === '') return null
  if (typeof input === 'number' && !Number.isNaN(input)) {
    const d = dayjs(input)
    return d.isValid() ? d.toDate() : null
  }
  if (Array.isArray(input)) {
    const [y, mo, day, h = 0, mi = 0, se = 0] = input
    if (y == null || y === '') return null
    const d = dayjs()
        .year(Number(y))
        .month(Number(mo || 1) - 1)
        .date(Number(day || 1))
        .hour(Number(h) || 0)
        .minute(Number(mi) || 0)
        .second(Number(se) || 0)
    return d.isValid() ? d.toDate() : null
  }
  const s = String(input).trim()
  if (!s || s === '-') return null
  let d = dayjs(s)
  if (d.isValid()) return d.toDate()
  d = dayjs(s.replace(/-/g, '/'))
  if (d.isValid()) return d.toDate()
  d = dayjs(s.replace(/\//g, '-'))
  if (d.isValid()) return d.toDate()
  return null
}
const formatDurationMs = (durationMs) => {
  if (durationMs == null || Number.isNaN(durationMs) || durationMs < 0) return '-'
  const hours = Math.floor(durationMs / (1000 * 60 * 60))
  const minutes = Math.floor((durationMs % (1000 * 60 * 60)) / (1000 * 60))
  if (hours === 0 && minutes === 0) return '不足1分钟'
  return `${hours}小时${minutes}分钟`
}
const hasMeaningfulEnd = (endRaw) =>
    endRaw !== null &&
    endRaw !== undefined &&
    String(endRaw).trim() !== '' &&
    String(endRaw).trim() !== '-'
const formatStoredDuration = (row) => {
  const rd = row?.runtimeDuration
  if (rd === null || rd === undefined) return ''
  const t = String(rd).trim()
  return t === '' || t === '-' ? '' : String(rd)
}
/** è¿è¡Œä¸­ï¼šå§‹ç»ˆç”¨ã€Œå½“前时间 - å¼€å§‹æ—¶é—´ã€ï¼›å·²åœæ­¢ï¼šä¼˜å…ˆæŽ¥å£ runtimeDuration,否则用结束-开始;无结束可看已存时长或动态推算 */
const getRuntimeDurationDisplay = (row) => {
  void runtimeDisplayTick.value
  const start = parseDeviceTime(pickStartTime(row))
  if (!start) {
    return formatStoredDuration(row) || '-'
  }
  const statusStr = String(row?.status ?? '').trim()
  const isRunning = statusStr === '运行中' || statusStr === '1'
  const endRaw = pickEndTime(row)
  const hasEnd = hasMeaningfulEnd(endRaw)
  // æ— ç»“束时间:运行中一定动态算;已停止则优先展示后端已存时长,没有再按当前时间推算
  if (!hasEnd) {
    if (isRunning) return formatDurationMs(Date.now() - start.getTime())
    const stored = formatStoredDuration(row)
    if (stored) return stored
    return formatDurationMs(Date.now() - start.getTime())
  }
  if (isRunning) {
    return formatDurationMs(Date.now() - start.getTime())
  }
  const end = parseDeviceTime(endRaw)
  const stored = formatStoredDuration(row)
  if (stored) return stored
  if (end) return formatDurationMs(end.getTime() - start.getTime())
  return '-'
}
// æ£€æŸ¥è®¾å¤‡æ˜¯å¦è¶…时未启动
const isOverdue = (device) => {
@@ -339,11 +246,12 @@
      device.endRuntimeTime = currentTime
      // è®¡ç®—运行时长
      if (device.startRuntimeTime) {
        const startTime = parseDeviceTime(device.startRuntimeTime)
        const endTime = parseDeviceTime(currentTime)
        if (startTime && endTime) {
          device.runtimeDuration = formatDurationMs(endTime.getTime() - startTime.getTime())
        }
        const startTime = new Date(device.startRuntimeTime)
        const endTime = new Date(currentTime)
        const duration = endTime - startTime
        const hours = Math.floor(duration / (1000 * 60 * 60))
        const minutes = Math.floor((duration % (1000 * 60 * 60)) / (1000 * 60))
        device.runtimeDuration = `${hours}小时${minutes}分钟`
      }
    }
    const params = {
@@ -389,31 +297,9 @@
const POLL_MS = 60 * 1000
const RUNTIME_TICK_MS = 30 * 1000
let listPollTimer = null
let runtimeTickTimer = null
// ç»„件挂载时拉取数据,并每分钟刷新一次列表;运行中时长每 30 ç§’刷新显示
// ç»„件挂载时初始化数据
onMounted(() => {
  getList()
  listPollTimer = setInterval(() => {
    getList()
  }, POLL_MS)
  runtimeTickTimer = setInterval(() => {
    runtimeDisplayTick.value++
  }, RUNTIME_TICK_MS)
})
onUnmounted(() => {
  if (listPollTimer != null) {
    clearInterval(listPollTimer)
    listPollTimer = null
  }
  if (runtimeTickTimer != null) {
    clearInterval(runtimeTickTimer)
    runtimeTickTimer = null
  }
})
</script>
src/views/equipmentManagement/repair/Modal/AcceptanceModal.vue
ÎļþÒÑɾ³ý
src/views/equipmentManagement/repair/Modal/RepairModal.vue
@@ -1,126 +1,101 @@
<template>
  <FormDialog v-model="visible"
              :title="computedTitle"
              :operation-type="operationType"
  <FormDialog
    v-model="visible"
    :title="id ? '编辑设备报修' : '新增设备报修'"
              width="800px"
              @confirm="sendForm"
              @cancel="handleCancel"
              @close="handleClose">
    <el-form :model="form"
             label-width="100px">
    @close="handleClose"
  >
    <el-form :model="form" label-width="100px">
      <el-row>
        <el-col :span="12">
          <el-form-item label="设备名称">
            <el-select v-model="form.deviceLedgerId"
                       @change="setDeviceModel"
          <el-form-item label="所属区域">
            <el-tree-select
              v-model="form.areaId"
              :data="areaOptions"
              :props="areaTreeProps"
              node-key="id"
              value-key="id"
              check-strictly
              clearable
                       filterable
                       :disabled="operationType === 'view'">
              <el-option v-for="(item, index) in deviceOptions"
                         :key="index"
              placeholder="请选择所属区域"
              style="width: 100%"
              @change="handleAreaChange"
            />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="设备名称">
            <el-select
              v-model="form.deviceLedgerIds"
              filterable
              clearable
              multiple
              collapse-tags
              collapse-tags-tooltip
              placeholder="请先选择区域,再选择设备"
              style="width: 100%"
              @change="setDeviceModels"
            >
              <el-option
                v-for="item in deviceOptions"
                :key="item.id"
                         :label="item.deviceName"
                         :value="item.id"></el-option>
                :value="item.id"
              />
            </el-select>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="规格型号">
            <el-input v-model="form.deviceModel"
                      placeholder="请输入规格型号"
                      disabled />
            <el-input
              v-model="form.deviceModel"
              placeholder="自动带出规格型号"
              disabled
            />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="报修日期">
            <el-date-picker v-model="form.repairTime"
            <el-date-picker
              v-model="form.repairTime"
                            placeholder="请选择报修日期"
                            format="YYYY-MM-DD"
                            value-format="YYYY-MM-DD"
                            type="date"
                            clearable
                            style="width: 100%"
                            :disabled="operationType === 'view'" />
            />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="报修人">
            <el-input v-model="form.repairName"
                      placeholder="请输入报修人"
                      :disabled="operationType === 'view'" />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="报修报修项目">
            <el-input v-model="form.machineryCategory"
                      placeholder="请输入报修报修项目"
                      :disabled="operationType === 'view'" />
          </el-form-item>
        </el-col>
      </el-row>
      <el-row>
        <el-col :span="12">
          <el-form-item label="维修人">
            <el-input v-model="form.maintenanceName"
                      placeholder="请输入维修人姓名"
                      :disabled="operationType === 'view'" />
            <el-input v-model="form.repairName" placeholder="请输入报修人" />
          </el-form-item>
        </el-col>
      </el-row>
      <el-row v-if="id">
        <el-col :span="12">
          <el-form-item label="报修状态">
            <el-select v-model="form.status"
                       disabled>
              <el-option label="待维修"
                         :value="0"></el-option>
              <el-option label="已验收"
                         :value="1"></el-option>
              <el-option label="失败"
                         :value="2"></el-option>
            <el-select v-model="form.status">
              <el-option label="待维修" :value="0" />
              <el-option label="完结" :value="1" />
              <el-option label="失败" :value="2" />
            </el-select>
          </el-form-item>
        </el-col>
      </el-row>
      <!-- éªŒæ”¶ä¿¡æ¯å±•示 -->
      <el-row v-if="id && (form.status === 1 || form.status === 3)">
        <el-col :span="12">
          <el-form-item label="验收人">
            <el-input v-model="form.acceptanceName"
                      disabled />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="验收时间">
            <el-input v-model="form.acceptanceTime"
                      disabled />
          </el-form-item>
        </el-col>
        <el-col :span="24">
          <el-form-item label="验收备注">
            <el-input v-model="form.acceptanceRemark"
                      type="textarea"
                      :rows="2"
                      disabled />
          </el-form-item>
        </el-col>
      </el-row>
      <el-row>
        <el-col :span="24">
          <el-form-item label="故障现象">
            <el-input v-model="form.remark"
            <el-input
              v-model="form.remark"
                      :rows="2"
                      type="textarea"
                      placeholder="请输入故障现象"
                      :disabled="operationType === 'view'" />
          </el-form-item>
        </el-col>
      </el-row>
      <el-row v-if="operationType !== 'view'"
              :gutter="30">
        <el-col :span="24">
          <el-form-item label="附件"
                        prop="attachmentIds">
            <FileUpload v-model:file-list="form.storageBlobDTOs"
                        :disabled="operationType === 'view'" />
            />
          </el-form-item>
        </el-col>
      </el-row>
@@ -129,18 +104,21 @@
</template>
<script setup>
import { nextTick, ref, unref } from "vue";
import dayjs from "dayjs";
import { ElMessage } from "element-plus";
  import FormDialog from "@/components/Dialog/FormDialog.vue";
  import FileUpload from "@/components/AttachmentUpload/file/index.vue";
import useFormData from "@/hooks/useFormData";
import useUserStore from "@/store/modules/user";
  import {
    addRepair,
    editRepair,
    getRepairById,
  } from "@/api/equipmentManagement/repair";
  import { ElMessage } from "element-plus";
  import dayjs from "dayjs";
  import useFormData from "@/hooks/useFormData";
  import { getDeviceLedger } from "@/api/equipmentManagement/ledger";
  import useUserStore from "@/store/modules/user";
import {
  getDeviceAreaTree,
  getDeviceAreaTreeWithDevices,
} from "@/api/equipmentManagement/deviceArea";
  defineOptions({
    name: "设备报修弹窗",
@@ -151,65 +129,172 @@
  const id = ref();
  const visible = ref(false);
  const loading = ref(false);
  const operationType = ref(""); // add, edit, view
  const computedTitle = computed(() => {
    if (operationType.value === "add") return "新增设备报修";
    if (operationType.value === "edit") return "编辑设备报修";
    if (operationType.value === "view") return "设备报修详情";
    return "";
  });
  const userStore = useUserStore();
const areaOptions = ref([]);
  const deviceOptions = ref([]);
  const fileList = ref([]);
  const loadDeviceName = async () => {
    const { data } = await getDeviceLedger();
    deviceOptions.value = data;
const areaTreeProps = {
  label: "areaName",
  children: "children",
  };
  const { form, resetForm } = useFormData({
    deviceLedgerId: undefined, // è®¾å¤‡Id
    deviceName: undefined, // è®¾å¤‡åç§°
    deviceModel: undefined, // è§„格型号
    repairTime: dayjs().format("YYYY-MM-DD"), // æŠ¥ä¿®æ—¥æœŸï¼Œé»˜è®¤å½“天
    repairName: userStore.nickName, // æŠ¥ä¿®äºº
    remark: undefined, // æ•…障现象
    status: 0, // æŠ¥ä¿®çŠ¶æ€
    machineryCategory: undefined,
    storageBlobDTOs: [],
    maintenanceName: undefined, // ç»´ä¿®äºº
  areaId: undefined,
  deviceLedgerId: undefined,
  deviceLedgerIds: [],
  deviceLedgerIdsStr: undefined,
  deviceName: undefined,
  deviceModel: undefined,
  repairTime: dayjs().format("YYYY-MM-DD"),
  repairName: userStore.nickName,
  remark: undefined,
  status: 0,
  });
  const setDeviceModel = deviceId => {
    const option = deviceOptions.value.find(item => item.id === deviceId);
    form.deviceModel = option.deviceModel;
const loadAreaTree = async () => {
  const { data } = await getDeviceAreaTree();
  areaOptions.value = Array.isArray(data) ? data : [];
  };
  const setForm = data => {
    form.deviceLedgerId = data.deviceLedgerId;
const normalizeIdList = (value) => {
  if (Array.isArray(value)) {
    return value
      .map((item) => Number(item))
      .filter((item) => Number.isFinite(item));
  }
  if (typeof value === "string") {
    return value
      .split(",")
      .map((item) => Number(item.trim()))
      .filter((item) => Number.isFinite(item));
  }
  if (value !== undefined && value !== null && value !== "") {
    const numericValue = Number(value);
    return Number.isFinite(numericValue) ? [numericValue] : [];
  }
  return [];
};
const getNodeDevices = (node) => {
  const candidates = [
    node?.deviceList,
    node?.devices,
    node?.deviceLedgerList,
    node?.deviceLedgers,
    node?.ledgerList,
    node?.ledgers,
  ];
  return candidates.find((item) => Array.isArray(item)) || [];
};
const normalizeDevice = (item) => ({
  ...item,
  id: item.id ?? item.deviceLedgerId,
  deviceName: item.deviceName ?? item.name,
  deviceModel: item.deviceModel ?? item.model,
});
const collectDevices = (node) => {
  const currentDevices = getNodeDevices(node).map(normalizeDevice);
  const childDevices = (node?.children || []).flatMap((child) =>
    collectDevices(child)
  );
  const deviceMap = new Map();
  [...currentDevices, ...childDevices].forEach((item) => {
    if (item?.id !== undefined && item?.id !== null) {
      deviceMap.set(Number(item.id), item);
    }
  });
  return Array.from(deviceMap.values());
};
const findAreaNode = (nodes, areaId) => {
  for (const node of nodes || []) {
    if (Number(node.id) === Number(areaId)) {
      return node;
    }
    const target = findAreaNode(node.children, areaId);
    if (target) {
      return target;
    }
  }
  return null;
};
const loadDevicesByArea = async (areaId) => {
  if (!areaId) {
    deviceOptions.value = [];
    return;
  }
  const { data } = await getDeviceAreaTreeWithDevices();
  const treeData = Array.isArray(data) ? data : [];
  const currentNode = findAreaNode(treeData, areaId);
  deviceOptions.value = currentNode ? collectDevices(currentNode) : [];
};
const syncDeviceFields = (deviceIds) => {
  const selectedIds = normalizeIdList(deviceIds);
  const selectedDevices = selectedIds
    .map((deviceId) =>
      deviceOptions.value.find((item) => Number(item.id) === Number(deviceId))
    )
    .filter(Boolean);
  form.deviceLedgerIds = selectedIds;
  form.deviceLedgerId = selectedIds[0];
  form.deviceLedgerIdsStr = selectedIds.join(",");
  form.deviceName = selectedDevices
    .map((item) => item.deviceName)
    .filter(Boolean)
    .join(",");
  form.deviceModel = selectedDevices
    .map((item) => item.deviceModel || "-")
    .join(",");
};
const setDeviceModels = (deviceIds) => {
  syncDeviceFields(deviceIds);
};
const setForm = (data) => {
  form.areaId = data.areaId;
  form.deviceLedgerIds = normalizeIdList(
    data.deviceLedgerIds ?? data.deviceLedgerIdsStr ?? data.deviceLedgerId
  );
  form.deviceLedgerId = form.deviceLedgerIds[0];
  form.deviceLedgerIdsStr =
    data.deviceLedgerIdsStr ?? form.deviceLedgerIds.join(",");
    form.deviceName = data.deviceName;
    form.deviceModel = data.deviceModel;
    form.repairTime = data.repairTime;
    form.repairName = data.repairName;
    form.remark = data.remark;
    form.status = data.status;
    form.machineryCategory = data.machineryCategory;
    form.storageBlobDTOs = data.storageBlobVOs || [];
    form.maintenanceName = data.maintenanceName;
    form.acceptanceName = data.acceptanceName;
    form.acceptanceTime = data.acceptanceTime;
    form.acceptanceRemark = data.acceptanceRemark;
};
const handleAreaChange = async (areaId) => {
  form.deviceLedgerId = undefined;
  form.deviceLedgerIds = [];
  form.deviceLedgerIdsStr = undefined;
  form.deviceName = undefined;
  form.deviceModel = undefined;
  await loadDevicesByArea(areaId);
  };
  const sendForm = async () => {
    loading.value = true;
    try {
    syncDeviceFields(form.deviceLedgerIds);
    const payload = {
      ...form,
      deviceLedgerId: form.deviceLedgerIds[0],
      deviceLedgerIds: [...form.deviceLedgerIds],
      deviceLedgerIdsStr: form.deviceLedgerIds.join(","),
      deviceModel: form.deviceModel || "-",
    };
      const { code } = id.value
        ? await editRepair({ id: unref(id), ...form })
        : await addRepair(form);
      if (code == 200) {
      ? await editRepair({ id: unref(id), ...payload })
      : await addRepair(payload);
    if (code === 200) {
        ElMessage.success(`${id.value ? "编辑" : "新增"}报修成功`);
        visible.value = false;
        emits("ok");
@@ -231,37 +316,26 @@
  const openAdd = async () => {
    id.value = undefined;
    operationType.value = "add";
    visible.value = true;
    fileList.value = [];
    await nextTick();
    await loadDeviceName();
  await loadAreaTree();
  deviceOptions.value = [];
  };
  const openEdit = async editId => {
const openEdit = async (editId) => {
    const { data } = await getRepairById(editId);
    id.value = editId;
    operationType.value = "edit";
    visible.value = true;
    await nextTick();
    await loadDeviceName();
  await loadAreaTree();
    setForm(data);
  };
  const openView = async viewId => {
    const { data } = await getRepairById(viewId);
    id.value = viewId;
    operationType.value = "view";
    visible.value = true;
    await nextTick();
    await loadDeviceName();
    setForm(data);
  await loadDevicesByArea(form.areaId);
  syncDeviceFields(form.deviceLedgerIds);
  };
  defineExpose({
    openAdd,
    openEdit,
    openView,
  });
</script>
src/views/equipmentManagement/repair/index.vue
@@ -1,81 +1,87 @@
<template>
  <div class="app-container">
    <el-form :model="filters"
             :inline="true">
    <el-form :model="filters" :inline="true">
      <el-form-item label="设备名称">
        <el-input v-model="filters.deviceName"
        <el-input
            v-model="filters.deviceName"
                  style="width: 240px"
                  placeholder="请输入设备名称"
                  clearable
                  :prefix-icon="Search"
                  @change="getTableData" />
            @change="getTableData"
        />
      </el-form-item>
      <el-form-item label="规格型号">
        <el-input v-model="filters.deviceModel"
        <el-input
            v-model="filters.deviceModel"
                  style="width: 240px"
                  placeholder="请选择规格型号"
                  clearable
                  :prefix-icon="Search"
                  @change="getTableData" />
            @change="getTableData"
        />
      </el-form-item>
      <el-form-item label="故障现象">
        <el-input v-model="filters.remark"
        <el-input
            v-model="filters.remark"
                  style="width: 240px"
                  placeholder="请输入故障现象"
                  clearable
                  :prefix-icon="Search"
                  @change="getTableData" />
            @change="getTableData"
        />
      </el-form-item>
      <el-form-item label="维修人">
        <el-input v-model="filters.maintenanceName"
        <el-input
            v-model="filters.maintenanceName"
                  style="width: 240px"
                  placeholder="请输入维修人"
                  clearable
                  :prefix-icon="Search"
                  @change="getTableData" />
            @change="getTableData"
        />
      </el-form-item>
      <el-form-item label="报修日期">
        <el-date-picker v-model="filters.repairTimeStr"
        <el-date-picker
            v-model="filters.repairTimeStr"
                        type="date"
                        placeholder="请选择报修日期"
                        size="default"
                        @change="(date) => handleDateChange(date,2)" />
            @change="(date) => handleDateChange(date,2)"
        />
      </el-form-item>
      <el-form-item label="维修日期">
        <el-date-picker v-model="filters.maintenanceTimeStr"
        <el-date-picker
            v-model="filters.maintenanceTimeStr"
                        type="date"
                        placeholder="请选择维修日期"
                        size="default"
                        @change="(date) => handleDateChange(date,1)" />
            @change="(date) => handleDateChange(date,1)"
        />
      </el-form-item>
      <el-form-item>
        <el-button type="primary"
                   @click="getTableData">搜索</el-button>
        <el-button type="primary" @click="getTableData">搜索</el-button>
        <el-button @click="resetFilters">重置</el-button>
      </el-form-item>
    </el-form>
    <div class="table_list">
      <div class="actions">
        <el-text class="mx-1"
                 size="large">设备报修</el-text>
        <el-text class="mx-1" size="large">设备报修</el-text>
        <div>
          <el-button type="success"
                     icon="Van"
                     @click="addRepair">
          <el-button type="success" icon="Van" @click="addRepair">
            æ–°å¢žæŠ¥ä¿®
          </el-button>
          <el-button @click="handleOut">
            å¯¼å‡º
          </el-button>
          <el-button type="danger"
          <el-button
            type="danger"
                     icon="Delete"
                     :disabled="multipleList.length <= 0 || hasFinishedStatus"
                     @click="delRepairByIds(multipleList.map((item) => item.id))">
            @click="delRepairByIds(multipleList.map((item) => item.id))"
          >
            æ‰¹é‡åˆ é™¤
          </el-button>
        </div>
      </div>
      <PIMTable rowKey="id"
      <PIMTable
          rowKey="id"
                isSelection
                :column="columns"
                :tableData="dataList"
@@ -85,86 +91,54 @@
          total: pagination.total,
        }"
                @selection-change="handleSelectionChange"
                @pagination="changePage">
          @pagination="changePage"
      >
        <template #statusRef="{ row }">
          <el-tag v-if="row.status === 2"
                  type="danger">失败</el-tag>
          <el-tag v-if="row.status === 1"
                  type="success">完结</el-tag>
          <el-tag v-if="row.status === 3"
                  type="info">待验收</el-tag>
          <el-tag v-if="row.status === 0"
                  type="warning">待维修</el-tag>
          <el-tag v-if="row.status === 2" type="danger">失败</el-tag>
          <el-tag v-if="row.status === 1" type="success">完结</el-tag>
          <el-tag v-if="row.status === 0" type="warning">待维修</el-tag>
        </template>
        <template #operation="{ row }">
          <el-button type="primary"
          <el-button
            type="primary"
                     link
                     @click="viewRepair(row.id)">
            è¯¦æƒ…
          </el-button>
          <el-button type="primary"
                     link
                     :disabled="row.status === 1 || row.status === 3"
                     @click="editRepair(row.id)">
            :disabled="row.status === 1"
            @click="editRepair(row.id)"
          >
            ç¼–辑
          </el-button>
          <el-button type="success"
          <el-button
            type="success"
                     link
                     :disabled="row.status !== 0"
                     @click="addMaintain(row)">
            :disabled="row.status === 1"
            @click="addMaintain(row)"
          >
            ç»´ä¿®
          </el-button>
          <el-button type="warning"
          <el-button
            type="danger"
                     link
                     :disabled="row.status !== 3"
                     @click="openAcceptance(row)">
            éªŒæ”¶
          </el-button>
          <el-button type="danger"
                     link
                     :disabled="row.status === 1 || row.status === 3"
                     @click="delRepairByIds(row.id)">
            :disabled="row.status === 1"
            @click="delRepairByIds(row.id)"
          >
            åˆ é™¤
          </el-button>
          <el-button type="primary"
                     link
                     @click="openFileDialog(row)">
            é™„ä»¶
          </el-button>
        </template>
      </PIMTable>
    </div>
    <RepairModal ref="repairModalRef"
                 @ok="getTableData" />
    <MaintainModal ref="maintainModalRef"
                   @ok="getTableData" />
    <AcceptanceModal ref="acceptanceModalRef"
                     @ok="getTableData" />
    <FileList v-if="fileDialogVisible"
              v-model:visible="fileDialogVisible"
              :record-type="'device_repair'"
              :record-id="recordId" />
    <RepairModal ref="repairModalRef" @ok="getTableData"/>
    <MaintainModal ref="maintainModalRef" @ok="getTableData"/>
  </div>
</template>
<script setup>
  import {
    onMounted,
    getCurrentInstance,
    computed,
    ref,
    defineAsyncComponent,
  } from "vue";
import { onMounted, getCurrentInstance, computed } from "vue";
  import { usePaginationApi } from "@/hooks/usePaginationApi";
  import { getRepairPage, delRepair } from "@/api/equipmentManagement/repair";
  import RepairModal from "./Modal/RepairModal.vue";
  import { ElMessageBox, ElMessage } from "element-plus";
  import dayjs from "dayjs";
  import MaintainModal from "./Modal/MaintainModal.vue";
  import AcceptanceModal from "./Modal/AcceptanceModal.vue";
  const FileList = defineAsyncComponent(() =>
    import("@/components/Dialog/FileList.vue")
  );
  defineOptions({
    name: "设备报修",
@@ -175,7 +149,6 @@
  // æ¨¡æ€æ¡†å®žä¾‹
  const repairModalRef = ref();
  const maintainModalRef = ref();
  const acceptanceModalRef = ref();
  // è¡¨æ ¼å¤šé€‰æ¡†é€‰ä¸­é¡¹
  const multipleList = ref([]);
@@ -201,6 +174,10 @@
    },
    [
      {
                label: "所在区域",
                prop: "areaName",
            },
      {
        label: "设备名称",
        align: "center",
        prop: "deviceName",
@@ -214,12 +191,33 @@
        label: "报修日期",
        align: "center",
        prop: "repairTime",
        formatData: cell => dayjs(cell).format("YYYY-MM-DD"),
        formatData: (cell) => dayjs(cell).format("YYYY-MM-DD"),
      },
      {
        label: "报修人",
        align: "center",
        prop: "repairName",
      },
      {
        label: "故障现象",
        align: "center",
        prop: "remark",
      },
      {
        label: "维修人",
        align: "center",
        prop: "maintenanceName",
      },
      {
        label: "维修结果",
        align: "center",
        prop: "maintenanceResult",
      },
      {
        label: "维修日期",
        align: "center",
        prop: "maintenanceTime",
        formatData: (cell) => (cell ? dayjs(cell).format("YYYY-MM-DD") : ""),
      },
      {
        label: "状态",
@@ -234,15 +232,15 @@
        dataType: "slot",
        slot: "operation",
        align: "center",
        width: "320px",
        width: "300px",
      },
    ]
  );
  // type === 1 ç»´ä¿® 2报修间
  const handleDateChange = (value, type) => {
    filters.maintenanceTimeStr = null;
    filters.c = null;
  filters.maintenanceTimeStr = null
  filters.c = null
    if (type === 1) {
      if (value) {
        filters.maintenanceTimeStr = dayjs(value).format("YYYY-MM-DD");
@@ -255,48 +253,29 @@
    getTableData();
  };
  // æ‰“开附件弹窗
  const recordId = ref(0);
  const fileDialogVisible = ref(false);
  const openFileDialog = async row => {
    recordId.value = row.id;
    fileDialogVisible.value = true;
  };
  // å¤šé€‰åŽåšä»€ä¹ˆ
  const handleSelectionChange = selectionList => {
const handleSelectionChange = (selectionList) => {
    multipleList.value = selectionList;
  };
  // æ£€æŸ¥é€‰ä¸­çš„记录中是否有完结状态的
  const hasFinishedStatus = computed(() => {
    return multipleList.value.some(item => item.status === 1);
  });
  return multipleList.value.some(item => item.status === 1)
})
  // æ–°å¢žæŠ¥ä¿®
  const addRepair = () => {
    repairModalRef.value.openAdd();
  };
  // è¯¦æƒ…查看
  const viewRepair = id => {
    repairModalRef.value.openView(id);
  };
  // ç¼–辑报修
  const editRepair = id => {
const editRepair = (id) => {
    repairModalRef.value.openEdit(id);
  };
  // æ–°å¢žç»´ä¿®
  const addMaintain = row => {
const addMaintain = (row) => {
    maintainModalRef.value.open(row.id, row);
  };
  // æ‰“开验收弹窗
  const openAcceptance = row => {
    acceptanceModalRef.value.open(row);
  };
  const changePage = ({ page, limit }) => {
@@ -306,7 +285,7 @@
  };
  // å•行删除
  const delRepairByIds = async ids => {
const delRepairByIds = async (ids) => {
    // æ£€æŸ¥æ˜¯å¦æœ‰å®Œç»“状态的记录
    const idsArray = Array.isArray(ids) ? ids : [ids];
    const hasFinished = idsArray.some(id => {
@@ -315,7 +294,7 @@
    });
    if (hasFinished) {
      ElMessage.warning("不能删除状态为完结的记录");
    ElMessage.warning('不能删除状态为完结的记录');
      return;
    }
src/views/equipmentManagement/spareParts/index.vue
@@ -88,8 +88,8 @@
          </el-form>
          <template #footer>
            <span class="dialog-footer">
              <el-button type="primary" @click="submitForm" :loading="formLoading">确定</el-button>
              <el-button @click="dialogVisible = false" :disabled="formLoading">取消</el-button>
              <el-button type="primary" @click="submitForm" :loading="formLoading">确定</el-button>
            </span>
          </template>
        </el-dialog>
src/views/equipmentManagement/upkeep/Form/MaintenanceModal.vue
@@ -1,76 +1,77 @@
<template>
  <FormDialog v-model="visible"
  <FormDialog
    v-model="visible"
              :title="'设备保养'"
              width="500px"
              @confirm="sendForm"
              @cancel="handleCancel"
              @close="handleClose">
    <el-form :model="form"
             label-width="100px">
    @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"
        <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%" />
          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"
        <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"
        <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" />
              :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"
          <div
              v-for="item in selectedSpareParts"
               :key="item.id"
               style="display: flex; align-items: center; gap: 10px; margin-bottom: 10px;">
              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]"
            <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" />
                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>
@@ -78,13 +79,12 @@
<script setup>
  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 {computed, ref} from "vue";
  import { getSparePartsList } from "@/api/equipmentManagement/spareParts.js";
  defineOptions({
@@ -93,7 +93,6 @@
  const emits = defineEmits(["ok"]);
  const { proxy } = getCurrentInstance();
  // ä¿å­˜è®¡åˆ’保养记录的id
  const planId = ref();
  const visible = ref(false);
@@ -106,43 +105,40 @@
    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 set = new Set(ids.map((i) => String(i)));
  return (sparePartOptions.value || []).filter((p) => set.has(String(p.id)));
  });
  const setForm = data => {
const setForm = (data) => {
    form.maintenanceActuallyName =
      data.maintenanceActuallyName ?? userStore.nickName;
    form.maintenanceActuallyTime = data.maintenanceActuallyTime
  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));
    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));
        .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 || [];
  };
  /**
@@ -159,19 +155,11 @@
            proxy?.$modal?.msgError?.("请填写备件领用数量");
            return;
          }
          const part = sparePartOptions.value.find(
            p => String(p.id) === String(partId)
          );
        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 (stock !== null && stock !== undefined && Number.isFinite(Number(stock))) {
            if (qty > Number(stock)) {
              proxy?.$modal?.msgError?.(
                `备件「${part?.name || ""}」领用数量不能超过库存(${stock})`
              );
            proxy?.$modal?.msgError?.(`备件「${part?.name || ""}」领用数量不能超过库存(${stock})`);
              return;
            }
          }
@@ -182,17 +170,12 @@
        ...form,
        sparePartsIds: form.sparePartsIds ? form.sparePartsIds.join(",") : "",
        sparePartsQty: form.sparePartsIds
          ? form.sparePartsIds
              .map(id => sparePartQtyMap.value?.[id] ?? 1)
              .join(",")
          ? form.sparePartsIds.map((id) => sparePartQtyMap.value?.[id] ?? 1).join(",")
          : "",
        sparePartsUseList: form.sparePartsIds
          ? form.sparePartsIds.map(id => ({
              id,
              quantity: sparePartQtyMap.value?.[id] ?? 1,
            }))
          ? form.sparePartsIds.map((id) => ({ id, quantity: sparePartQtyMap.value?.[id] ?? 1 }))
          : [],
      };
    }
      const { code } = await addMaintenance(data);
      if (code == 200) {
        ElMessage.success("保养成功");
@@ -210,7 +193,7 @@
    loadingSparePartOptions.value = true;
    // å’Œå¤‡ä»¶ç®¡ç†é¡µä¸€è‡´ï¼š/spareParts/listPage â†’ res.data.records
    getSparePartsList({ current: 1, size: 1000 })
      .then(res => {
      .then((res) => {
        if (res.code === 200) {
          sparePartOptions.value = res?.data?.records || [];
        } else {
@@ -223,7 +206,7 @@
      .finally(() => {
        loadingSparePartOptions.value = false;
      });
  };
}
  const handleCancel = () => {
    resetForm();
@@ -241,7 +224,7 @@
    planId.value = id; // ä¿å­˜è®¡åˆ’保养记录的id
    visible.value = true;
    await nextTick();
    fetchSparePartOptions();
  fetchSparePartOptions()
    setForm(row);
  };
src/views/equipmentManagement/upkeep/Form/PlanModal.vue
@@ -8,34 +8,46 @@
    @close="handleClose"
  >
    <el-form :model="form" label-width="100px">
      <el-form-item label="所属区域">
        <el-tree-select
          v-model="form.areaId"
          :data="areaOptions"
          :props="areaTreeProps"
          node-key="id"
          value-key="id"
          check-strictly
          clearable
          filterable
          placeholder="请选择所属区域"
          style="width: 100%"
          @change="handleAreaChange"
        />
      </el-form-item>
      <el-form-item label="设备名称">
        <el-select
          v-model="form.deviceLedgerId"
          @change="setDeviceModel"
          placeholder="请选择设备"
          v-model="form.deviceLedgerIds"
          filterable
          default-first-option
          :reserve-keyword="false"
          clearable
          multiple
          collapse-tags
          collapse-tags-tooltip
          placeholder="请选择设备"
          style="width: 100%"
          @change="setDeviceModels"
        >
          <el-option
            v-for="(item, index) in deviceOptions"
            :key="index"
            v-for="item in deviceOptions"
            :key="item.id"
            :label="item.deviceName"
            :value="item.id"
          ></el-option>
          />
        </el-select>
      </el-form-item>
      <el-form-item label="规格型号">
        <el-input
          v-model="form.deviceModel"
          placeholder="请输入规格型号"
          placeholder="自动带出规格型号"
          disabled
        />
      </el-form-item>
      <el-form-item label="保养项目">
        <el-input
            v-model="form.machineryCategory"
            placeholder="请输入保养项目"
        />
      </el-form-item>
      <el-form-item label="录入人">
@@ -57,54 +69,42 @@
      </el-form-item>
      <el-form-item v-if="id" 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 label="完结" :value="1" />
          <el-option label="失败" :value="2" />
        </el-select>
      </el-form-item>
      <el-form-item label="保养人">
        <el-input
          v-model="form.maintenancePerson"
          placeholder="请输入保养人姓名"
          clearable
        />
      </el-form-item>
      <el-form-item label="计划保养日期">
        <el-date-picker
          style="width: 100%"
          v-model="form.maintenancePlanTime"
          style="width: 100%"
          format="YYYY-MM-DD"
          value-format="YYYY-MM-DD HH:mm:ss"
          type="date"
          placeholder="请选择计划保养日期日期"
          placeholder="请选择计划保养日期"
          clearable
        />
      </el-form-item>
      <el-row :gutter="30">
        <el-col :span="24">
          <el-form-item label="附件" prop="attachmentIds">
            <FileUpload v-model:file-list="form.storageBlobDTOs" />
          </el-form-item>
        </el-col>
      </el-row>
    </el-form>
  </FormDialog>
</template>
<script setup>
import { nextTick, onMounted, ref, unref } from "vue";
import dayjs from "dayjs";
import { ElMessage } from "element-plus";
import FormDialog from "@/components/Dialog/FormDialog.vue";
import useFormData from "@/hooks/useFormData";
import { userListNoPage } from "@/api/system/user.js";
import {
  addUpkeep,
  editUpkeep,
  getUpkeepById,
} from "@/api/equipmentManagement/upkeep";
import { ElMessage } from "element-plus";
import useFormData from "@/hooks/useFormData";
import { getDeviceLedger } from "@/api/equipmentManagement/ledger";
import { onMounted } from "vue";
import dayjs from "dayjs";
import { userListNoPage } from "@/api/system/user.js";
import FileUpload from "@/components/AttachmentUpload/file/index.vue";
import {
  getDeviceAreaTree,
  getDeviceAreaTreeWithDevices,
} from "@/api/equipmentManagement/deviceArea";
defineOptions({
  name: "设备保养新增计划",
@@ -115,55 +115,159 @@
const id = ref();
const visible = ref(false);
const loading = ref(false);
const areaOptions = ref([]);
const deviceOptions = ref([]);
const loadDeviceName = async () => {
  const { data } = await getDeviceLedger();
  deviceOptions.value = data;
const userList = ref([]);
const areaTreeProps = {
  label: "areaName",
  children: "children",
};
const { form, resetForm } = useFormData({
  deviceLedgerId: undefined, // è®¾å¤‡Id
  deviceName: undefined, // è®¾å¤‡åç§°
  deviceModel: undefined, // è§„格型号
  maintenancePlanTime: undefined, // è®¡åˆ’保养日期
  createUser: undefined, // å½•入人
  status: 0, //保修状态
  machineryCategory: undefined,
  storageBlobDTOs: [],
  maintenancePerson: undefined, // ä¿å…»äºº
  areaId: undefined,
  deviceLedgerId: undefined,
  deviceLedgerIds: [],
  deviceLedgerIdsStr: undefined,
  deviceName: undefined,
  deviceModel: undefined,
  maintenancePlanTime: undefined,
  createUser: undefined,
  status: 0,
});
const setDeviceModel = (deviceId) => {
  const option = deviceOptions.value.find((item) => item.id === deviceId);
  form.deviceModel = option.deviceModel;
const loadAreaTree = async () => {
  const { data } = await getDeviceAreaTree();
  areaOptions.value = Array.isArray(data) ? data : [];
};
/**
 * @desc è®¾ç½®è¡¨å•内容
 * @param data è®¾å¤‡ä¿¡æ¯
 */
const normalizeIdList = (value) => {
  if (Array.isArray(value)) {
    return value
      .map((item) => Number(item))
      .filter((item) => Number.isFinite(item));
  }
  if (typeof value === "string") {
    return value
      .split(",")
      .map((item) => Number(item.trim()))
      .filter((item) => Number.isFinite(item));
  }
  if (value !== undefined && value !== null && value !== "") {
    const numericValue = Number(value);
    return Number.isFinite(numericValue) ? [numericValue] : [];
  }
  return [];
};
const getNodeDevices = (node) => {
  const candidates = [
    node?.deviceList,
    node?.devices,
    node?.deviceLedgerList,
    node?.deviceLedgers,
    node?.ledgerList,
    node?.ledgers,
  ];
  return candidates.find((item) => Array.isArray(item)) || [];
};
const normalizeDevice = (item) => ({
  ...item,
  id: item.id ?? item.deviceLedgerId,
  deviceName: item.deviceName ?? item.name,
  deviceModel: item.deviceModel ?? item.model,
});
const collectDevices = (node) => {
  const currentDevices = getNodeDevices(node).map(normalizeDevice);
  const childDevices = (node?.children || []).flatMap((child) =>
    collectDevices(child)
  );
  const deviceMap = new Map();
  [...currentDevices, ...childDevices].forEach((item) => {
    if (item?.id !== undefined && item?.id !== null) {
      deviceMap.set(Number(item.id), item);
    }
  });
  return Array.from(deviceMap.values());
};
const findAreaNode = (nodes, areaId) => {
  for (const node of nodes || []) {
    if (Number(node.id) === Number(areaId)) {
      return node;
    }
    const target = findAreaNode(node.children, areaId);
    if (target) {
      return target;
    }
  }
  return null;
};
const loadDevicesByArea = async (areaId) => {
  if (!areaId) {
    deviceOptions.value = [];
    return;
  }
  const { data } = await getDeviceAreaTreeWithDevices();
  const treeData = Array.isArray(data) ? data : [];
  const currentNode = findAreaNode(treeData, areaId);
  deviceOptions.value = currentNode ? collectDevices(currentNode) : [];
};
const syncDeviceFields = (deviceIds) => {
  const selectedIds = normalizeIdList(deviceIds);
  const selectedDevices = selectedIds
    .map((deviceId) =>
      deviceOptions.value.find((item) => Number(item.id) === Number(deviceId))
    )
    .filter(Boolean);
  form.deviceLedgerIds = selectedIds;
  form.deviceLedgerId = selectedIds[0];
  form.deviceLedgerIdsStr = selectedIds.join(",");
  form.deviceName = selectedDevices
    .map((item) => item.deviceName)
    .filter(Boolean)
    .join(",");
  form.deviceModel = selectedDevices
    .map((item) => item.deviceModel || "-")
    .join(",");
};
const setDeviceModels = (deviceIds) => {
  syncDeviceFields(deviceIds);
};
const handleAreaChange = async (areaId) => {
  form.deviceLedgerId = undefined;
  form.deviceLedgerIds = [];
  form.deviceLedgerIdsStr = undefined;
  form.deviceName = undefined;
  form.deviceModel = undefined;
  await loadDevicesByArea(areaId);
};
const setForm = (data) => {
  form.deviceLedgerId = data.deviceLedgerId;
  form.areaId = data.areaId;
  form.deviceLedgerIds = normalizeIdList(
    data.deviceLedgerIds ?? data.deviceLedgerIdsStr ?? data.deviceLedgerId
  );
  form.deviceLedgerId = form.deviceLedgerIds[0];
  form.deviceLedgerIdsStr =
    data.deviceLedgerIdsStr ?? form.deviceLedgerIds.join(",");
  form.deviceName = data.deviceName;
  form.deviceModel = data.deviceModel;
  form.createUser = Number(data.createUser);
  form.status = data.status;
  form.machineryCategory = data.machineryCategory;
  form.maintenancePerson = data.maintenancePerson;
  if (data.maintenancePlanTime) {
    form.maintenancePlanTime = dayjs(data.maintenancePlanTime).format(
      "YYYY-MM-DD HH:mm:ss"
    );
  }
  form.storageBlobDTOs = data.storageBlobVOs || [];
  form.maintenancePlanTime = data.maintenancePlanTime
    ? dayjs(data.maintenancePlanTime).format("YYYY-MM-DD HH:mm:ss")
    : undefined;
};
// ç”¨æˆ·åˆ—表
const userList = ref([]);
onMounted(() => {
  loadDeviceName();
  loadAreaTree();
  userListNoPage().then((res) => {
    userList.value = res.data;
  });
@@ -174,16 +278,27 @@
  id.value = editId;
  visible.value = true;
  await nextTick();
  await loadAreaTree();
  setForm(data);
  await loadDevicesByArea(form.areaId);
  syncDeviceFields(form.deviceLedgerIds);
};
const sendForm = async () => {
  loading.value = true;
  try {
    syncDeviceFields(form.deviceLedgerIds);
    const payload = {
      ...form,
      deviceLedgerId: form.deviceLedgerIds[0],
      deviceLedgerIds: [...form.deviceLedgerIds],
      deviceLedgerIdsStr: form.deviceLedgerIds.join(","),
      deviceModel: form.deviceModel || "-",
    };
    const { code } = id.value
      ? await editUpkeep({ id: unref(id), ...form })
      : await addUpkeep(form);
    if (code == 200) {
      ? await editUpkeep({ id: unref(id), ...payload })
      : await addUpkeep(payload);
    if (code === 200) {
      ElMessage.success(`${id.value ? "编辑" : "新增"}计划成功`);
      visible.value = false;
      emits("ok");
@@ -203,9 +318,12 @@
  visible.value = false;
};
const openModal = () => {
const openModal = async () => {
  id.value = undefined;
  visible.value = true;
  await nextTick();
  await loadAreaTree();
  deviceOptions.value = [];
};
defineExpose({
src/views/equipmentManagement/upkeep/index.vue
@@ -1,58 +1,51 @@
<template>
  <div class="app-container">
    <el-tabs v-model="activeTab"
             @tab-change="handleTabChange">
      <!-- ä¿å…»ä»»åŠ¡tab -->
      <el-tab-pane label="保养任务"
                   name="scheduled">
    <el-tabs v-model="activeTab" @tab-change="handleTabChange">
      <!-- å®šæ—¶ä»»åŠ¡ç®¡ç†tab -->
      <el-tab-pane label="定时任务管理" name="scheduled">
        <div class="search_form">
          <el-form :model="scheduledFilters"
                   :inline="true">
          <el-form :model="scheduledFilters" :inline="true">
            <el-form-item label="任务名称">
              <el-input v-model="scheduledFilters.taskName"
              <el-input
                  v-model="scheduledFilters.taskName"
                        style="width: 240px"
                        placeholder="请输入任务名称"
                        clearable
                        :prefix-icon="Search"
                        @change="getScheduledTableData" />
                  @change="getScheduledTableData"
              />
            </el-form-item>
            <el-form-item label="任务状态">
              <el-select v-model="scheduledFilters.status"
                         placeholder="请选择任务状态"
                         clearable
                         style="width: 200px">
                <el-option label="启用"
                           value="1" />
                <el-option label="停用"
                           value="0" />
            <el-form-item label="是否启用">
              <el-select v-model="scheduledFilters.isEnabled" placeholder="请选择是否启用" clearable style="width: 200px">
                <el-option label="启用" :value="1" />
                <el-option label="禁用" :value="0" />
              </el-select>
            </el-form-item>
            <el-form-item>
              <el-button type="primary"
                         @click="getScheduledTableData">搜索</el-button>
              <el-button type="primary" @click="getScheduledTableData">搜索</el-button>
              <el-button @click="resetScheduledFilters">重置</el-button>
            </el-form-item>
          </el-form>
        </div>
        <div class="table_list">
          <div class="actions">
            <el-text class="mx-1"
                     size="large">保养任务</el-text>
            <el-text class="mx-1" size="large">定时任务管理</el-text>
            <div>
              <el-button type="primary"
                         icon="Plus"
                         @click="addScheduledTask">
              <el-button type="primary" icon="Plus" @click="addScheduledTask">
                æ–°å¢žä»»åŠ¡
              </el-button>
              <el-button type="danger"
              <el-button
                type="danger"
                         icon="Delete"
                         :disabled="scheduledMultipleList.length <= 0"
                         @click="delScheduledTaskByIds(scheduledMultipleList.map((item) => item.id))">
                @click="delScheduledTaskByIds(scheduledMultipleList.map((item) => item.id))"
              >
                æ‰¹é‡åˆ é™¤
              </el-button>
            </div>
          </div>
          <PIMTable rowKey="id"
          <PIMTable
            rowKey="id"
                    isSelection
                    :column="scheduledColumns"
                    :tableData="scheduledDataList"
@@ -62,93 +55,107 @@
              total: scheduledPagination.total,
            }"
                    @selection-change="handleScheduledSelectionChange"
                    @pagination="changeScheduledPage">
            <template #statusRef="{ row }">
              <el-tag v-if="row.status === 1"
                      type="success">启用</el-tag>
              <el-tag v-if="row.status === 0"
                      type="danger">停用</el-tag>
            @pagination="changeScheduledPage"
          >
            <template #isEnabledRef="{ row }">
              <el-switch
                v-model="row.isEnabled"
                :active-value="1"
                :inactive-value="0"
                :loading="row.enableSwitchLoading"
                :before-change="() => handleScheduledEnableBeforeChange(row)"
              />
            </template>
            <template #operation="{ row }">
              <el-button type="primary"
              <el-button
                type="primary"
                         link
                         @click="editScheduledTask(row)">
                @click="editScheduledTask(row)"
              >
                ç¼–辑
              </el-button>
              <el-button type="danger"
              <el-button
                type="danger"
                         link
                         @click="delScheduledTaskByIds(row.id)">
                @click="delScheduledTaskByIds(row.id)"
              >
                åˆ é™¤
              </el-button>
            </template>
          </PIMTable>
        </div>
      </el-tab-pane>
      <!-- ä¿å…»è®°å½•tab(原设备保养页面) -->
      <el-tab-pane label="保养记录"
                   name="record">
      <!-- ä»»åŠ¡è®°å½•tab(原设备保养页面) -->
      <el-tab-pane label="任务记录" name="record">
        <div class="search_form">
          <el-form :model="filters"
                   :inline="true">
          <el-form :model="filters" :inline="true">
            <el-form-item label="设备名称">
              <el-input v-model="filters.deviceName"
              <el-input
                  v-model="filters.deviceName"
                        style="width: 240px"
                        placeholder="请输入设备名称"
                        clearable
                        :prefix-icon="Search"
                        @change="getTableData" />
                  @change="getTableData"
              />
            </el-form-item>
            <el-form-item label="计划保养日期">
              <el-date-picker v-model="filters.maintenancePlanTime"
              <el-date-picker
                  v-model="filters.maintenancePlanTime"
                              type="date"
                              placeholder="请选择计划保养日期"
                              size="default"
                              @change="(date) => handleDateChange(date,2)" />
                  @change="(date) => handleDateChange(date,2)"
              />
            </el-form-item>
            <el-form-item label="实际保养日期">
              <el-date-picker v-model="filters.maintenanceActuallyTime"
              <el-date-picker
                  v-model="filters.maintenanceActuallyTime"
                              type="date"
                              placeholder="请选择实际保养日期"
                              size="default"
                              @change="(date) => handleDateChange(date,1)" />
                  @change="(date) => handleDateChange(date,1)"
              />
            </el-form-item>
            <el-form-item label="实际保养人">
              <el-input v-model="filters.maintenanceActuallyName"
              <el-input
                  v-model="filters.maintenanceActuallyName"
                        style="width: 240px"
                        placeholder="请输入实际保养人"
                        clearable
                        :prefix-icon="Search"
                        @change="getTableData" />
                  @change="getTableData"
              />
            </el-form-item>
            <el-form-item>
              <el-button type="primary"
                         @click="getTableData">搜索</el-button>
              <el-button type="primary" @click="getTableData">搜索</el-button>
              <el-button @click="resetFilters">重置</el-button>
            </el-form-item>
          </el-form>
        </div>
        <div class="table_list">
          <div class="actions">
            <el-text class="mx-1"
                     size="large">保养记录</el-text>
            <el-text class="mx-1" size="large">任务记录</el-text>
            <div>
              <el-button type="success"
                         icon="Van"
                         @click="addPlan">
              <el-button type="success" icon="Van" @click="addPlan">
                æ–°å¢žè®¡åˆ’
              </el-button>
              <el-button @click="handleOut">
                å¯¼å‡º
              </el-button>
              <el-button type="danger"
              <el-button
                type="danger"
                         icon="Delete"
                         :disabled="multipleList.length <= 0 || hasFinishedStatus"
                         @click="delRepairByIds(multipleList.map((item) => item.id))">
                @click="delRepairByIds(multipleList.map((item) => item.id))"
              >
                æ‰¹é‡åˆ é™¤
              </el-button>
            </div>
          </div>
          <PIMTable rowKey="id"
         <PIMTable
        rowKey="id"
                    isSelection
                    :column="columns"
                    :tableData="dataList"
@@ -158,17 +165,15 @@
          total: pagination.total,
        }"
                    @selection-change="handleSelectionChange"
                    @pagination="changePage">
        @pagination="changePage"
      >
            <template #maintenanceResultRef="{ row }">
              <div>{{ row.maintenanceResult || '-' }}</div>
            </template>
            <template #statusRef="{ row }">
              <el-tag v-if="row.status === 2"
                      type="danger">失败</el-tag>
              <el-tag v-if="row.status === 1"
                      type="success">完结</el-tag>
              <el-tag v-if="row.status === 0"
                      type="warning">待保养</el-tag>
          <el-tag v-if="row.status === 2" type="danger">失败</el-tag>
          <el-tag v-if="row.status === 1" type="success">完结</el-tag>
          <el-tag v-if="row.status === 0" type="warning">待保养</el-tag>
            </template>
            <template #operation="{ row }">
              <!-- è¿™ä¸ªåŠŸèƒ½è·Ÿæ–°å¢žä¿å…»åŠŸèƒ½ä¸€æ¨¡ä¸€æ ·ï¼Œæœ‰å•¥æ„ä¹‰ï¼Ÿ -->
@@ -179,27 +184,35 @@
          >
            æ–°å¢žä¿å…»
          </el-button> -->
              <el-button type="primary"
          <el-button
            type="primary"
                         link
                         :disabled="row.status === 1"
                         @click="editPlan(row.id)">
            @click="editPlan(row.id)"
          >
                ç¼–辑
              </el-button>
              <el-button type="success"
          <el-button
            type="success"
                         link
                         :disabled="row.status === 1"
                         @click="addMaintain(row)">
            @click="addMaintain(row)"
          >
                ä¿å…»
              </el-button>
              <el-button type="danger"
          <el-button
            type="danger"
                         link
                         :disabled="row.status === 1"
                         @click="delRepairByIds(row.id)">
            @click="delRepairByIds(row.id)"
          >
                åˆ é™¤
              </el-button>
              <el-button type="primary"
          <el-button
            type="primary"
                         link
                         @click="openFileDialog(row)">
            @click="openFileDialog(row)"
          >
                é™„ä»¶
              </el-button>
            </template>
@@ -207,149 +220,143 @@
        </div>
      </el-tab-pane>
    </el-tabs>
    <PlanModal ref="planModalRef"
               @ok="getTableData" />
    <MaintenanceModal ref="maintainModalRef"
                      @ok="getTableData" />
    <FormDia ref="formDiaRef"
             @closeDia="getScheduledTableData" />
    <FileList v-if="fileDialogVisible"
              v-model:visible="fileDialogVisible"
              :record-type="'device_maintenance'"
              :record-id="currentMaintenanceTaskId" />
    <PlanModal ref="planModalRef" @ok="getTableData" />
        <MaintenanceModal ref="maintainModalRef" @ok="getTableData" />
        <FormDia ref="formDiaRef" @closeDia="getScheduledTableData" />
    <FileListDialog
      ref="fileListDialogRef"
      v-model="fileDialogVisible"
      :show-upload-button="true"
      :show-delete-button="true"
      :delete-method="handleAttachmentDelete"
      :name-column-label="'附件名称'"
      :rulesRegulationsManagementId="currentMaintenanceTaskId"
      @upload="handleAttachmentUpload" />
  </div>
</template>
<script setup>
  import {
    ref,
    onMounted,
    reactive,
    getCurrentInstance,
    nextTick,
    computed,
    defineAsyncComponent,
  } from "vue";
  import { Search } from "@element-plus/icons-vue";
  import { ElMessage, ElMessageBox } from "element-plus";
  import PlanModal from "./Form/PlanModal.vue";
  import MaintenanceModal from "./Form/MaintenanceModal.vue";
  import FormDia from "./Form/formDia.vue";
import { ref, onMounted, reactive, getCurrentInstance, nextTick, computed } from 'vue'
import { Search } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import PlanModal from './Form/PlanModal.vue'
import MaintenanceModal from './Form/MaintenanceModal.vue'
import FormDia from './Form/formDia.vue'
import FileListDialog from '@/components/Dialog/FileListDialog.vue'
  import {
    getUpkeepPage,
    delUpkeep,
    deviceMaintenanceTaskList,
    deviceMaintenanceTaskDel,
  } from "@/api/equipmentManagement/upkeep";
  import dayjs from "dayjs";
  deviceMaintenanceTaskChangeEnable,
} from '@/api/equipmentManagement/upkeep'
import {
  listMaintenanceTaskFiles,
  addMaintenanceTaskFile,
  delMaintenanceTaskFile,
} from '@/api/equipmentManagement/maintenanceTaskFile'
import dayjs from 'dayjs'
  const { proxy } = getCurrentInstance();
  const FileList = defineAsyncComponent(() =>
    import("@/components/Dialog/FileList.vue")
  );
const { proxy } = getCurrentInstance()
  // Tab相关
  const activeTab = ref("scheduled");
const activeTab = ref('scheduled')
  // è®¡åˆ’弹窗控制器
  const planModalRef = ref();
const planModalRef = ref()
  // ä¿å…»å¼¹çª—控制器
  const maintainModalRef = ref();
const maintainModalRef = ref()
  // å®šæ—¶ä»»åŠ¡å¼¹çª—æŽ§åˆ¶å™¨
  const formDiaRef = ref();
const formDiaRef = ref()
  // é™„件弹窗
  const fileListDialogRef = ref(null);
  const fileDialogVisible = ref(false);
  const currentMaintenanceTaskId = ref(null);
const fileListDialogRef = ref(null)
const fileDialogVisible = ref(false)
const currentMaintenanceTaskId = ref(null)
  // ä¿å…»è®°å½•tab(原设备保养页面)相关变量
// ä»»åŠ¡è®°å½•tab(原设备保养页面)相关变量
  const filters = reactive({
    deviceName: "",
    maintenancePlanTime: "",
    maintenanceActuallyTime: "",
    maintenanceActuallyName: "",
  });
  deviceName: '',
  maintenancePlanTime: '',
  maintenanceActuallyTime: '',
  maintenanceActuallyName: '',
})
  const dataList = ref([]);
const dataList = ref([])
  const pagination = ref({
    currentPage: 1,
    pageSize: 10,
    total: 0,
  });
  const multipleList = ref([]);
})
const multipleList = ref([])
  // ä¿å…»ä»»åŠ¡tab相关变量
// å®šæ—¶ä»»åŠ¡ç®¡ç†tab相关变量
  const scheduledFilters = reactive({
    taskName: "",
    status: "",
  });
  taskName: '',
  isEnabled: undefined,
})
  const scheduledDataList = ref([]);
const scheduledDataList = ref([])
  const scheduledPagination = reactive({
    currentPage: 1,
    pageSize: 10,
    total: 0,
  });
  const scheduledMultipleList = ref([]);
})
const scheduledMultipleList = ref([])
  // ä¿å…»ä»»åŠ¡è¡¨æ ¼åˆ—é…ç½®
// å®šæ—¶ä»»åŠ¡ç®¡ç†è¡¨æ ¼åˆ—é…ç½®
  const scheduledColumns = ref([
    {
        label: "所在区域",
        prop: "areaName",
    },
    { prop: "taskName", label: "设备名称" },
    {
      label: "规格型号",
      prop: "deviceModel",
    },
    {
      label: "保养项目",
      prop: "machineryCategory",
      minWidth: 120,
      formatData: cell => cell || "--",
    },
    {
      prop: "frequencyType",
      label: "频次",
      minWidth: 150,
      // PIMTable ä½¿ç”¨çš„æ˜¯ formatData,而不是 Element-Plus çš„ formatter
      formatData: cell =>
        ({
        formatData: (cell) => ({
          DAILY: "每日",
          WEEKLY: "每周",
          MONTHLY: "每月",
          QUARTERLY: "季度",
        }[cell] || ""),
            YEARLY: "每年",
        }[cell] || "")
    },
    {
      prop: "frequencyDetail",
      label: "开始日期与时间",
      minWidth: 150,
      // åŒæ ·æ”¹ç”¨ formatData,PIMTable å†…部会把单元格值传进来
      formatData: cell => {
        if (typeof cell !== "string") return "";
        formatData: (cell) => {
            if (typeof cell !== 'string') return '';
        let val = cell;
        const replacements = {
          MON: "周一",
          TUE: "周二",
          WED: "周三",
          THU: "周四",
          FRI: "周五",
          SAT: "周六",
          SUN: "周日",
                MON: '周一',
                TUE: '周二',
                WED: '周三',
                THU: '周四',
                FRI: '周五',
                SAT: '周六',
                SUN: '周日'
        };
        // ä½¿ç”¨æ­£åˆ™ä¸€æ¬¡æ€§æ›¿æ¢æ‰€æœ‰åŒ¹é…é¡¹
        return val.replace(
          /MON|TUE|WED|THU|FRI|SAT|SUN/g,
          match => replacements[match]
        );
            return val.replace(/MON|TUE|WED|THU|FRI|SAT|SUN/g, match => replacements[match]);
        }
      },
    },
    { prop: "maintenancePerson", label: "保养人", minWidth: 100 },
    { prop: "registrant", label: "登记人", minWidth: 100 },
    { prop: "registrationDate", label: "登记日期", minWidth: 100 },
    {
      prop: "registrationDate",
      label: "登记日期",
      minWidth: 100,
      formatData: cell =>
        cell ? dayjs(cell).format("YYYY-MM-DD HH:mm:ss") : "-",
        label: "是否启用",
        prop: "isEnabled",
        dataType: "slot",
        slot: "isEnabledRef",
        align: "center",
        width: "120px",
    },
    {
      fixed: "right",
@@ -359,10 +366,14 @@
      align: "center",
      width: "200px",
    },
  ]);
])
  // ä¿å…»è®°å½•表格列配置(原设备保养表格列)
// ä»»åŠ¡è®°å½•è¡¨æ ¼åˆ—é…ç½®ï¼ˆåŽŸè®¾å¤‡ä¿å…»è¡¨æ ¼åˆ—ï¼‰
  const columns = ref([
    {
        label: "所在区域",
        prop: "areaName",
    },
    {
      label: "设备名称",
      align: "center",
@@ -377,20 +388,12 @@
      label: "计划保养日期",
      align: "center",
      prop: "maintenancePlanTime",
      formatData: cell => {
        return cell == null ? "-" : dayjs(cell).format("YYYY-MM-DD");
      },
        formatData: (cell) => dayjs(cell).format("YYYY-MM-DD"),
    },
    {
      label: "录入人",
      align: "center",
      prop: "createUserName",
    },
    {
      label: "保养项目",
      align: "center",
      prop: "machineryCategory",
      formatData: cell => cell || "--",
    },
    // {
    //   label: "录入日期",
@@ -408,7 +411,7 @@
      label: "实际保养日期",
      align: "center",
      prop: "maintenanceActuallyTime",
      formatData: cell =>
        formatData: (cell) =>
        cell ? dayjs(cell).format("YYYY-MM-DD HH:mm:ss") : "-",
    },
    {
@@ -433,209 +436,300 @@
      align: "center",
      width: "350px",
    },
  ]);
])
  // Tab切换处理
  const handleTabChange = tabName => {
    if (tabName === "record") {
      getTableData();
    } else if (tabName === "scheduled") {
      getScheduledTableData();
const handleTabChange = (tabName) => {
  if (tabName === 'record') {
    getTableData()
  } else if (tabName === 'scheduled') {
    getScheduledTableData()
    }
  };
}
  // ä¿å…»ä»»åŠ¡ç›¸å…³æ–¹æ³•
// å®šæ—¶ä»»åŠ¡ç®¡ç†ç›¸å…³æ–¹æ³•
  const getScheduledTableData = async () => {
    try {
      const params = {
        current: scheduledPagination.currentPage,
        size: scheduledPagination.pageSize,
        taskName: scheduledFilters.taskName || undefined,
        status: scheduledFilters.status || undefined,
      };
      const { code, data } = await deviceMaintenanceTaskList(params);
      isEnabled: scheduledFilters.isEnabled,
    }
    const { code, data } = await deviceMaintenanceTaskList(params)
      if (code === 200) {
        scheduledDataList.value = data?.records || [];
        scheduledPagination.total = data?.total || 0;
      const records = data?.records || []
      scheduledDataList.value = records.map((item) => ({
        ...item,
        isEnabled: Number(item.isEnabled ?? item.status ?? 1),
        enableSwitchLoading: false,
      }))
      scheduledPagination.total = data?.total || 0
      }
    } catch (error) {
      ElMessage.error("获取定时任务列表失败");
    ElMessage.error('获取定时任务列表失败')
    }
  };
}
  const resetScheduledFilters = () => {
    scheduledFilters.taskName = "";
    scheduledFilters.status = "";
    getScheduledTableData();
  };
  scheduledFilters.taskName = ''
  scheduledFilters.isEnabled = undefined
  getScheduledTableData()
}
  const handleScheduledSelectionChange = selection => {
    scheduledMultipleList.value = selection;
  };
const handleScheduledSelectionChange = (selection) => {
  scheduledMultipleList.value = selection
}
  const changeScheduledPage = page => {
    scheduledPagination.currentPage = page.page;
    scheduledPagination.pageSize = page.limit;
    getScheduledTableData();
  };
const changeScheduledPage = (page) => {
  scheduledPagination.currentPage = page.page
  scheduledPagination.pageSize = page.limit
  getScheduledTableData()
}
  const addScheduledTask = () => {
    nextTick(() => {
      formDiaRef.value?.openDialog("add");
    });
  };
  const editScheduledTask = row => {
    if (row) {
      nextTick(() => {
        formDiaRef.value?.openDialog("edit", row);
        formDiaRef.value?.openDialog('add');
      });
    }
  };
  const delScheduledTaskByIds = async ids => {
    try {
      await ElMessageBox.confirm("确定删除选中的定时任务吗?", "提示", {
        type: "warning",
const editScheduledTask = (row) => {
  if (row) {
        nextTick(() => {
            formDiaRef.value?.openDialog('edit', row);
      });
      const payload = Array.isArray(ids) ? ids : [ids];
      await deviceMaintenanceTaskDel(payload);
      ElMessage.success("删除定时任务成功");
      getScheduledTableData();
  }
}
const delScheduledTaskByIds = async (ids) => {
  try {
    await ElMessageBox.confirm('确定删除选中的定时任务吗?', '提示', {
      type: 'warning',
    })
    const payload = Array.isArray(ids) ? ids : [ids]
    await deviceMaintenanceTaskDel(payload)
    ElMessage.success('删除定时任务成功')
    getScheduledTableData()
    } catch (error) {
      // ç”¨æˆ·å–消删除
    }
  };
}
const handleScheduledEnableBeforeChange = async (row) => {
  if (row.enableSwitchLoading) {
    return false
  }
  const nextValue = Number(row.isEnabled) === 1 ? 0 : 1
  row.enableSwitchLoading = true
  try {
    const res = await deviceMaintenanceTaskChangeEnable({
      id: row.id,
      isEnabled: nextValue,
    })
    if (res?.code !== 200) {
      throw new Error(res?.msg || '更新失败')
    }
    ElMessage.success('启用状态已更新')
    return true
  } catch (error) {
    ElMessage.error(error?.message || '启用状态更新失败')
    return false
  } finally {
    row.enableSwitchLoading = false
  }
}
  const handleScheduledOut = () => {
    ElMessage.info("导出定时任务功能待实现");
  };
  ElMessage.info('导出定时任务功能待实现')
}
  // ä¿å…»è®°å½•相关方法(原设备保养页面方法)
// ä»»åŠ¡è®°å½•ç›¸å…³æ–¹æ³•ï¼ˆåŽŸè®¾å¤‡ä¿å…»é¡µé¢æ–¹æ³•ï¼‰
  const getTableData = async () => {
    try {
      const params = {
        current: pagination.value.currentPage,
        size: pagination.value.pageSize,
        deviceName: filters.deviceName || undefined,
        maintenancePlanTime: filters.maintenancePlanTime
          ? dayjs(filters.maintenancePlanTime).format("YYYY-MM-DD")
          : undefined,
        maintenanceActuallyTime: filters.maintenanceActuallyTime
          ? dayjs(filters.maintenanceActuallyTime).format("YYYY-MM-DD")
          : undefined,
      maintenancePlanTime: filters.maintenancePlanTime ? dayjs(filters.maintenancePlanTime).format('YYYY-MM-DD') : undefined,
      maintenanceActuallyTime: filters.maintenanceActuallyTime ? dayjs(filters.maintenanceActuallyTime).format('YYYY-MM-DD') : undefined,
        maintenanceActuallyName: filters.maintenanceActuallyName || undefined,
      };
    }
      const { code, data } = await getUpkeepPage(params);
    const { code, data } = await getUpkeepPage(params)
      if (code === 200) {
        dataList.value = data.records;
        pagination.value.total = data.total;
      dataList.value = data.records
      pagination.value.total = data.total
      }
    } catch (error) {
      console.log(error);
    }
  };
}
  const resetFilters = () => {
    filters.deviceName = "";
    filters.maintenancePlanTime = "";
    filters.maintenanceActuallyTime = "";
    filters.maintenanceActuallyName = "";
    getTableData();
  };
  filters.deviceName = ''
  filters.maintenancePlanTime = ''
  filters.maintenanceActuallyTime = ''
  filters.maintenanceActuallyName = ''
  getTableData()
}
  const handleSelectionChange = selection => {
    multipleList.value = selection;
  };
const handleSelectionChange = (selection) => {
  multipleList.value = selection
}
  // æ£€æŸ¥é€‰ä¸­çš„记录中是否有完结状态的
  const hasFinishedStatus = computed(() => {
    return multipleList.value.some(item => item.status === 1);
  });
  return multipleList.value.some(item => item.status === 1)
})
  const changePage = page => {
    pagination.value.currentPage = page.page;
    pagination.value.pageSize = page.limit;
    getTableData();
  };
const changePage = (page) => {
  pagination.value.currentPage = page.page
  pagination.value.pageSize = page.limit
  getTableData()
}
  const addMaintain = row => {
    maintainModalRef.value.open(row.id, row);
  };
const addMaintain = (row) => {
  maintainModalRef.value.open(row.id, row)
}
  const addPlan = () => {
    planModalRef.value.openModal();
  };
  planModalRef.value.openModal()
}
  const editPlan = id => {
    planModalRef.value.openEdit(id);
  };
const editPlan = (id) => {
  planModalRef.value.openEdit(id)
}
  const delRepairByIds = async ids => {
const delRepairByIds = async (ids) => {
    // æ£€æŸ¥æ˜¯å¦æœ‰å®Œç»“状态的记录
    const hasFinished = multipleList.value.some(item => item.status === 1);
  const hasFinished = multipleList.value.some(item => item.status === 1)
    if (hasFinished) {
      ElMessage.warning("不能删除状态为完结的记录");
      return;
    ElMessage.warning('不能删除状态为完结的记录')
    return
    }
    try {
      await ElMessageBox.confirm("确认删除保养数据, æ­¤æ“ä½œä¸å¯é€†?", "警告", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      });
    await ElMessageBox.confirm('确认删除保养数据, æ­¤æ“ä½œä¸å¯é€†?', '警告', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning',
    })
      const { code } = await delUpkeep(ids);
    const { code } = await delUpkeep(ids)
      if (code === 200) {
        ElMessage.success("删除成功");
        getTableData();
      ElMessage.success('删除成功')
      getTableData()
      }
    } catch (error) {
      // ç”¨æˆ·å–消删除
    }
  };
}
  const handleOut = () => {
    ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
  ElMessageBox.confirm('选中的内容将被导出,是否确认导出?', '导出', {
    confirmButtonText: '确认',
    cancelButtonText: '取消',
    type: 'warning',
    })
      .then(() => {
        proxy.download("/device/maintenance/export", {}, "设备保养.xlsx");
      proxy.download('/device/maintenance/export', {}, '设备保养.xlsx')
      })
      .catch(() => {
        ElMessage.info("已取消");
      });
  };
      ElMessage.info('已取消')
    })
}
  const handleDateChange = (date, type) => {
    if (type === 1) {
      filters.maintenanceActuallyTime = date
        ? dayjs(date).format("YYYY-MM-DD")
        : "";
    filters.maintenanceActuallyTime = date ? dayjs(date).format('YYYY-MM-DD') : ''
    } else {
      filters.maintenancePlanTime = date ? dayjs(date).format("YYYY-MM-DD") : "";
    filters.maintenancePlanTime = date ? dayjs(date).format('YYYY-MM-DD') : ''
    }
    getTableData();
  };
  getTableData()
}
// é™„件相关方法
// æŸ¥è¯¢é™„件列表
const fetchMaintenanceTaskFiles = async (deviceMaintenanceId) => {
  try {
    const params = {
      current: 1,
      size: 100,
      deviceMaintenanceId,
      rulesRegulationsManagementId:deviceMaintenanceId
    }
    const res = await listMaintenanceTaskFiles(params)
    const records = res?.data?.records || []
    const mapped = records.map(item => ({
      id: item.id,
      name: item.fileName || item.name,
      url: item.fileUrl || item.url,
      raw: item,
    }))
    fileListDialogRef.value?.setList(mapped)
  } catch (error) {
    ElMessage.error('获取附件列表失败')
  }
}
  // æ‰“开附件弹窗
  const openFileDialog = async row => {
    currentMaintenanceTaskId.value = row.id;
    fileDialogVisible.value = true;
  };
const openFileDialog = async (row) => {
  currentMaintenanceTaskId.value = row.id
  fileDialogVisible.value = true
  await fetchMaintenanceTaskFiles(row.id)
}
// åˆ·æ–°é™„件列表
const refreshFileList = async () => {
  if (!currentMaintenanceTaskId.value) return
  await fetchMaintenanceTaskFiles(currentMaintenanceTaskId.value)
}
// ä¸Šä¼ é™„ä»¶
const handleAttachmentUpload = async (filePayload) => {
  if (!currentMaintenanceTaskId.value) return
  try {
    const payload = {
      name: filePayload?.fileName || filePayload?.name,
      url: filePayload?.fileUrl || filePayload?.url,
      deviceMaintenanceId: currentMaintenanceTaskId.value,
    }
    await addMaintenanceTaskFile(payload)
    ElMessage.success('文件上传成功')
    await refreshFileList()
  } catch (error) {
    ElMessage.error('文件上传失败')
  }
}
// åˆ é™¤é™„ä»¶
const handleAttachmentDelete = async (row) => {
  if (!row?.id) return false
  try {
    await ElMessageBox.confirm('确认删除该附件?', '提示', { type: 'warning' })
  } catch {
    return false
  }
  try {
    await delMaintenanceTaskFile(row.id)
    ElMessage.success('删除成功')
    await refreshFileList()
    return true
  } catch (error) {
    ElMessage.error('删除失败')
    return false
  }
}
  onMounted(() => {
    // æ ¹æ®é»˜è®¤æ¿€æ´»çš„ Tab è°ƒç”¨å¯¹åº”的查询接口
    if (activeTab.value === "scheduled") {
      getScheduledTableData();
  if (activeTab.value === 'scheduled') {
    getScheduledTableData()
    } else {
      getTableData();
    getTableData()
    }
  });
})
</script>
<style lang="scss" scoped>
vite.config.js
@@ -8,7 +8,7 @@
  const { VITE_APP_ENV } = env;
  const baseUrl =
      env.VITE_APP_ENV === "development"
          ? "http://1.15.17.182:9048"
          ? "http://192.168.0.226:7005"
          : env.VITE_BASE_API;
  const javaUrl =
      env.VITE_APP_ENV === "development"