yyb
2026-05-18 04d6024553ac73e67148ce578cb01b541eebd02a
Merge branch 'dev_NEW_pro' into dev-new_pro_OA
已添加20个文件
已修改72个文件
4996 ■■■■ 文件已修改
multiple/assets/favicon/HYJCfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/HYLQfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/JHYfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/JXJHfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/QXYfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/XSWHfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/YTJZfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/ZQSYfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/HYJCLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/HYLQLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/JHYLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/JXJHLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/QXYLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/XSWHLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/YTJZLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/ZQSYLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/config.json 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/equipmentManagement/repair.js 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inventoryManagement/stockInventory.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AIChatSidebar/assistants/index.js 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AIChatSidebar/assistants/productionAssistant.js 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AIChatSidebar/index.vue 763 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/ProcessParamListDialog.vue 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/aiIndustrialBrain/MAINTAIN_RULES.md 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/aiIndustrialBrain/components/AiAssistantWorkspace.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/aiIndustrialBrain/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/product/index.vue 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/supplierManage/components/BlacklistTab.vue 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/supplierManage/components/HomeTab.vue 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/components/infoFormDia.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/index.vue 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/sealManagement/index.vue 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/inspectionManagement/components/formDia.vue 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/inspectionManagement/index.vue 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/measurementEquipment/components/formDia.vue 34 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/measurementEquipment/index.vue 48 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/repair/Modal/AcceptanceModal.vue 144 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/repair/Modal/RepairModal.vue 38 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/repair/index.vue 36 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/Form/PlanModal.vue 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/Form/formDia.vue 22 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/index.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/assets/fixedAssets.vue 23 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/assets/intangibleAssets.vue 23 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/generalLedger/index.vue 102 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/voucher/detailLedger.vue 383 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/voucher/generalLedger.vue 326 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/voucher/index.vue 107 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/BatchNoQtyDetail.vue 227 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/Record.vue 354 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/contractManagement/index.vue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/employeeRecord/index.vue 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/procurementLedger/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/purchaseReturnOrder/New.vue 15 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/purchaseReturnOrder/ProductList.vue 15 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/purchaseReturnOrder/index.vue 15 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/processRouteItem/index.vue 79 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productStructure/Detail/index.vue 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionProcess/index.vue 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/finalInspection/components/formDia.vue 180 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/finalInspection/index.vue 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/nonconformingManagement/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/processInspection/components/formDia.vue 932 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/processInspection/index.vue 25 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/rawMaterialInspection/components/formDia.vue 86 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/rawMaterialInspection/index.vue 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/components/center-bottom.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/components/center-center.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/components/center-top.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/components/left-bottom.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/components/left-top.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/PSIDataAnalysis/index.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/center-bottom.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/center-top.vue 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/left-bottom.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/left-top.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/right-bottom.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/right-top.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/index.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/center-bottom.vue 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/center-center.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/center-top.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/left-bottom.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/left-top.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/right-bottom.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/right-top.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/index.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safeProduction/safeWorkApproval/components/infoFormDia.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/returnOrder/components/detailDia.vue 15 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/returnOrder/components/formDia.vue 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/index.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesQuotation/index.vue 77 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/HYJCfavicon.ico
multiple/assets/favicon/HYLQfavicon.ico
multiple/assets/favicon/JHYfavicon.ico
multiple/assets/favicon/JXJHfavicon.ico
multiple/assets/favicon/QXYfavicon.ico
multiple/assets/favicon/XSWHfavicon.ico
multiple/assets/favicon/YTJZfavicon.ico
multiple/assets/favicon/ZQSYfavicon.ico
multiple/assets/logo/HYJCLogo.png
multiple/assets/logo/HYLQLogo.png
multiple/assets/logo/JHYLogo.png
multiple/assets/logo/JXJHLogo.png
multiple/assets/logo/QXYLogo.png
multiple/assets/logo/XSWHLogo.png
multiple/assets/logo/YTJZLogo.png
multiple/assets/logo/ZQSYLogo.png
multiple/config.json
@@ -44,9 +44,9 @@
  },
  "BTYX": {
    "env": {
      "VITE_APP_TITLE": "河南帮太优选进出口有限公司",
      "VITE_BASE_API": "http://127.0.0.1:9001",
      "VITE_JAVA_API": "http://127.0.0.1:9000"
      "VITE_APP_TITLE": "河南帮太优选食品有限公司",
      "VITE_BASE_API": "http://1.15.17.182:9056",
      "VITE_JAVA_API": "http://1.15.17.182:9057"
    },
    "logo": "logo/BTYXLogo.png",
    "favicon": "favicon/BTYXfavicon.ico"
@@ -96,6 +96,69 @@
    "logo": "logo/DYKJLogo.png",
    "favicon": "favicon/DYKJfavicon.ico"
  },
  "ZQSY": {
    "env": {
      "VITE_APP_TITLE": "泽淇实业",
      "VITE_BASE_API": "http://36.213.128.159:9000",
      "VITE_JAVA_API": "http://36.213.128.159:9001"
    },
    "logo": "logo/ZQSYLogo.png",
    "favicon": "favicon/ZQSYfavicon.ico"
  },
  "JXJH": {
    "env": {
      "VITE_APP_TITLE": "浚县江海水泥制品有限公司",
      "VITE_BASE_API": "http://36.139.201.20:9000",
      "VITE_JAVA_API": "http://36.139.201.20:9001"
    },
    "logo": "logo/JXJHLogo.png",
    "favicon": "favicon/JXJHfavicon.ico"
  },
  "YTJZ": {
    "env": {
      "VITE_APP_TITLE": "豫泰建筑材料有限公司",
      "VITE_BASE_API": "http://36.139.201.181:9000",
      "VITE_JAVA_API": "http://36.139.201.181:9001"
    },
    "logo": "logo/YTJZLogo.png",
    "favicon": "favicon/YTJZfavicon.ico"
  },
  "HYLQ": {
    "env": {
      "VITE_APP_TITLE": "航逸路桥工程有限公司",
      "VITE_BASE_API": "http://36.139.202.111:9000",
      "VITE_JAVA_API": "http://36.139.202.111:9001"
    },
    "logo": "logo/HYLQLogo.png",
    "favicon": "favicon/HYLQfavicon.ico"
  },
  "QXY": {
    "env": {
      "VITE_APP_TITLE": "强信宇电器云主机",
      "VITE_BASE_API": "http://36.134.154.10:9000",
      "VITE_JAVA_API": "http://36.134.154.10:9001"
    },
    "logo": "logo/QXYLogo.png",
    "favicon": "favicon/QXYfavicon.ico"
  },
  "HYJC": {
    "env": {
      "VITE_APP_TITLE": "恒洋建材",
      "VITE_BASE_API": "http://36.138.94.178:9000",
      "VITE_JAVA_API": "http://36.138.94.178:9001"
    },
    "logo": "logo/HYJCLogo.png",
    "favicon": "favicon/HYJCfavicon.ico"
  },
  "JHY": {
    "env": {
      "VITE_APP_TITLE": "山西省榆社县晋和园食品有限公司",
      "VITE_BASE_API": "http://223.15.233.27:9001",
      "VITE_JAVA_API": "http://223.15.233.27:9002"
    },
    "logo": "logo/JHYLogo.png",
    "favicon": "favicon/JHYfavicon.ico"
  },
  "logo": "/src/assets/logo/logo.png",
  "favicon": "/public/favicon.ico"
}
src/api/equipmentManagement/repair.js
@@ -70,3 +70,16 @@
    data,
  });
};
/**
 * @desc éªŒæ”¶å®¡æ‰¹
 * @param {验收参数} data
 * @returns
 */
export const repairAcceptance = (data) => {
  return request({
    url: `/device/repair/acceptance`,
    method: "post",
    data,
  });
};
src/api/inventoryManagement/stockInventory.js
@@ -17,6 +17,14 @@
    });
};
export const getStockInventoryBatchNoQty = (params) => {
    return request({
        url: "/stockInventory/getBatchNoQty",
        method: "get",
        params,
    });
};
// åˆ›å»ºåº“存记录
export const createStockInventory = (params) => {
    return request({
src/components/AIChatSidebar/assistants/index.js
@@ -1,6 +1,13 @@
import { generalAssistant } from './generalAssistant'
import { purchaseAssistant } from './purchaseAssistant'
import { productionAssistant } from './productionAssistant'
export { generalAssistant, purchaseAssistant }
export { generalAssistant, purchaseAssistant, productionAssistant }
export const builtInAssistants = [generalAssistant, purchaseAssistant]
export const assistantRegistry = {
  general: generalAssistant,
  purchase: purchaseAssistant,
  production: productionAssistant
}
export const builtInAssistants = [generalAssistant, purchaseAssistant, productionAssistant]
src/components/AIChatSidebar/assistants/productionAssistant.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,27 @@
import { Operation } from '@element-plus/icons-vue'
export const productionAssistant = {
  key: 'production',
  label: '生产助理',
  title: '生产智能助理',
  tooltip: '生产智能助手',
  icon: Operation,
  apiBase: '/manufacturing-ai',
  storageKey: 'production_ai_chat_uuid',
  placeholder: '请输入生产相关问题... (Enter å‘送, Shift+Enter æ¢è¡Œ)',
  welcomeMessage: '你好',
  description: '我可以围绕生产现场、计划、工单、设备、质量、物料、异常处理提供查询、预警、分析和办理建议。',
  allowFileUpload: false,
  emptySessionText: '暂无生产会话',
  quickPrompts: [
    '查询本月生产计划',
    '查看最近10条工单',
    '查设备A-01的维修情况',
    '查质量不合格记录',
    '查低库存物料',
    '查近7天异常处理',
    '生成制造预警看板',
    '分析本月生产完成率和异常率',
    '给出工单逾期和设备待修的办理建议'
  ]
}
src/components/AIChatSidebar/index.vue
@@ -242,7 +242,123 @@
                  </el-table>
                </div>
                <!-- æ‰“字中动画 -->
                <div v-if="message.manufacturingData" class="manufacturing-card">
                  <div class="manufacturing-card__title">{{ getManufacturingTypeLabel(message.type) }}</div>
                  <div
                      v-if="message.manufacturingData.summaryEntries?.length || message.manufacturingData.coreMetrics?.length"
                      class="manufacturing-summary-grid"
                  >
                    <div
                        v-for="(entry, entryIndex) in message.manufacturingData.summaryEntries"
                        :key="`summary-${entry.key}-${entryIndex}`"
                        class="manufacturing-summary-item"
                    >
                      <span class="manufacturing-summary-label">{{ entry.label }}</span>
                      <strong class="manufacturing-summary-value">{{ entry.value }}</strong>
                    </div>
                    <div
                        v-for="(metric, metricIndex) in message.manufacturingData.coreMetrics"
                        :key="`core-${metric.key}-${metricIndex}`"
                        class="manufacturing-summary-item manufacturing-summary-item--core"
                    >
                      <span class="manufacturing-summary-label">{{ metric.label }}</span>
                      <strong class="manufacturing-summary-value">{{ metric.value }}</strong>
                    </div>
                  </div>
                  <div v-if="message.manufacturingData.warningItems?.length" class="manufacturing-warning-list">
                    <div
                        v-for="(warning, warningIndex) in message.manufacturingData.warningItems"
                        :key="`warning-${warning.title || warningIndex}`"
                        class="manufacturing-warning-item"
                    >
                      <div class="manufacturing-warning-item__head">
                        <el-tag size="small" :type="getManufacturingWarningLevelType(warning.level)">
                          {{ getManufacturingWarningLevelLabel(warning.level) }}
                        </el-tag>
                        <strong>{{ warning.title || `预警 ${warningIndex + 1}` }}</strong>
                        <span v-if="warning.count !== '' && warning.count !== null && warning.count !== undefined" class="manufacturing-warning-count">
                          {{ warning.count }}
                        </span>
                      </div>
                      <p v-if="warning.detail" class="manufacturing-warning-detail">{{ warning.detail }}</p>
                    </div>
                  </div>
                  <div
                      v-if="message.manufacturingData.listItems?.length && message.manufacturingData.columns?.length"
                      class="table-wrapper manufacturing-table-wrapper"
                  >
                    <el-table :data="message.manufacturingData.listItems" border stripe size="small" style="width: 100%">
                      <el-table-column
                          v-for="col in message.manufacturingData.columns"
                          :key="col"
                          :label="getStructuredFieldLabel(col)"
                          min-width="140"
                          show-overflow-tooltip
                      >
                        <template #default="{ row }">
                          {{ formatStructuredValue(row[col]) }}
                        </template>
                      </el-table-column>
                    </el-table>
                  </div>
                  <div v-if="message.manufacturingData.actionCards?.length" class="manufacturing-action-list">
                    <div
                        v-for="(card, cardIndex) in message.manufacturingData.actionCards"
                        :key="card.runtimeKey || `${card.code}-${card.targetApi}-${cardIndex}`"
                        class="manufacturing-action-card"
                    >
                      <div class="manufacturing-action-card__head">
                        <strong>{{ card.name || `动作 ${cardIndex + 1}` }}</strong>
                        <el-tag size="small" type="info">{{ getNormalizedRequestMethod(card.method) }}</el-tag>
                      </div>
                      <div class="manufacturing-action-card__meta">
                        <span>{{ card.code || '--' }}</span>
                        <span>{{ card.targetApi || '--' }}</span>
                      </div>
                      <p v-if="card.description" class="manufacturing-action-card__desc">{{ card.description }}</p>
                      <div v-if="card.requiredFields?.length" class="manufacturing-required-fields">
                        <span>必填字段</span>
                        <el-tag
                            v-for="field in card.requiredFields"
                            :key="field"
                            size="small"
                            type="warning"
                        >
                          {{ getStructuredPathLabel(field) }}
                        </el-tag>
                      </div>
                      <el-input
                          v-model="card.payloadText"
                          type="textarea"
                          :rows="6"
                          resize="vertical"
                          :disabled="card.executing"
                          placeholder="请输入 JSON è¯·æ±‚参数"
                      />
                      <div class="manufacturing-action-footer">
                        <span
                            v-if="card.executeResult"
                            :class="['manufacturing-action-result', card.executeError ? 'error' : 'success']"
                        >
                          {{ card.executeResult }}
                        </span>
                        <el-button
                            type="primary"
                            size="small"
                            :loading="card.executing"
                            @click="executeManufacturingAction(message, card, cardIndex)"
                        >
                          ç¡®è®¤å¹¶æ‰§è¡Œ
                        </el-button>
                      </div>
                    </div>
                  </div>
                </div>
                <div v-if="message.purchaseAnalysisData" class="purchase-confirm-card">
                  <div class="purchase-confirm-header">
                    <span>{{ businessTypeLabelMap[message.purchaseAnalysisData.businessType] || message.purchaseAnalysisData.businessType || '采购业务' }}</span>
@@ -523,7 +639,7 @@
import request from '@/utils/request'
import * as echarts from 'echarts'
import { Cpu, User, Plus, Timer, Delete, ChatDotSquare, VideoPause, Upload, Document, Close, Promotion, RefreshRight } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import { builtInAssistants, generalAssistant } from './assistants'
import todoAssistantAvatar from '@/assets/AI/待办助手.png'
import salesAssistantAvatar from '@/assets/AI/销售助手.png'
@@ -633,6 +749,75 @@
  payment_registration: '付款登记',
  purchase_return_order: '采购退货单',
  unknown: '未知采购业务'
}
const manufacturingStructuredTypeSet = new Set([
  'manufacturing_site_snapshot',
  'manufacturing_plan_list',
  'manufacturing_workorder_list',
  'manufacturing_device_list',
  'manufacturing_device_repair_list',
  'manufacturing_quality_list',
  'manufacturing_material_list',
  'manufacturing_exception_list',
  'manufacturing_warning',
  'manufacturing_analysis',
  'manufacturing_action_plan'
])
const manufacturingListTypeSet = new Set([
  'manufacturing_plan_list',
  'manufacturing_workorder_list',
  'manufacturing_device_list',
  'manufacturing_device_repair_list',
  'manufacturing_quality_list',
  'manufacturing_material_list',
  'manufacturing_exception_list'
])
const manufacturingTypeLabelMap = {
  manufacturing_site_snapshot: '生产现场概览',
  manufacturing_plan_list: '计划查询',
  manufacturing_workorder_list: '工单查询',
  manufacturing_device_list: '设备查询',
  manufacturing_device_repair_list: '设备维修记录查询',
  manufacturing_quality_list: '质量查询',
  manufacturing_material_list: '物料查询',
  manufacturing_exception_list: '异常查询',
  manufacturing_warning: '预警看板',
  manufacturing_analysis: '经营分析',
  manufacturing_action_plan: '办理建议'
}
const structuredFieldLabelMap = {
  workOrderNo: '工单号',
  planEndTime: '计划结束时间',
  planStartTime: '计划开始时间',
  timeRange: '时间范围',
  startDate: '开始日期',
  endDate: '结束日期',
  warningCount: '预警数量',
  overduePlanCount: '逾期计划数',
  overdueWorkOrderCount: '逾期工单数',
  actionCount: '建议动作数',
  qualityOpenCount: '质量待处理数',
  lowStockCount: '低库存数',
  exceptionCount: '异常数',
  userId: '用户ID',
  tenantId: '租户ID',
  status: '状态',
  deviceName: '设备名称',
  deviceModel: '设备型号',
  pendingRepairCount: '待维修数',
  repairTime: '维修时间',
  repairName: '报修人',
  maintenanceName: '维修人员',
  level: '预警等级',
  title: '标题',
  count: '数量',
  detail: '详情',
  remark: '备注',
  createTime: '创建时间',
  updateTime: '更新时间',
  exceptionType: '异常类型',
  materialName: '物料名称',
  stockQty: '库存量'
}
const purchasePayloadFieldLabelMap = {
  purchaseLedgers: '采购台账',
@@ -785,6 +970,366 @@
  inventoryWarningQuantity: 'inventoryWarningQuantity',
  isInspected: 'isInspected',
  isChecked: 'isInspected'
}
const isPlainObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value)
const stringifyStructuredPayload = (value, spaces = 2) => {
  if (typeof value === 'string') return value
  try {
    return JSON.stringify(value ?? {}, null, spaces)
  } catch (err) {
    return '{}'
  }
}
const structuredFieldTokenLabelMap = {
  time: '时间',
  range: '范围',
  start: '开始',
  end: '结束',
  date: '日期',
  warning: '预警',
  overdue: '逾期',
  plan: '计划',
  work: 'å·¥',
  order: '单',
  workorder: '工单',
  count: '数量',
  quality: '质量',
  low: '低',
  stock: '库存',
  exception: '异常',
  action: '动作',
  user: '用户',
  tenant: '租户',
  id: 'ID',
  no: '编号',
  number: '编号',
  code: '编码',
  name: '名称',
  status: '状态',
  level: '等级',
  title: '标题',
  detail: '详情',
  total: '总数',
  rate: '比率',
  type: '类型',
  pending: '待',
  repair: 'ç»´ä¿®',
  device: '设备',
  material: '物料'
}
const convertStructuredFieldKeyToChinese = (fieldKey = '') => {
  const key = String(fieldKey || '').trim()
  if (!key) return '-'
  if (structuredFieldLabelMap[key]) return structuredFieldLabelMap[key]
  if (/[\u4e00-\u9fa5]/.test(key)) return key
  const rawTokens = key
    .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
    .replace(/[_-]+/g, ' ')
    .trim()
    .split(/\s+/)
    .filter(Boolean)
    .map(token => token.toLowerCase())
  if (!rawTokens.length) return '字段'
  const mappedTokens = rawTokens
    .map(token => structuredFieldTokenLabelMap[token] || '')
    .filter(Boolean)
  if (mappedTokens.length) {
    return mappedTokens.join('')
  }
  return '字段'
}
const getStructuredFieldLabel = (fieldKey = '') => {
  return convertStructuredFieldKeyToChinese(fieldKey)
}
const getStructuredPathLabel = (fieldPath = '') => {
  const path = String(fieldPath || '').trim()
  if (!path) return '-'
  const segments = path
    .replace(/\[(\d+)]/g, '.$1')
    .split('.')
    .filter(Boolean)
  if (!segments.length) return getStructuredFieldLabel(path)
  return segments.map((segment) => {
    if (/^\d+$/.test(segment)) {
      return `第${Number(segment) + 1}项`
    }
    return getStructuredFieldLabel(segment)
  }).join(' / ')
}
const formatStructuredValue = (value) => {
  if (value === null || value === undefined || value === '') return '-'
  if (Array.isArray(value)) {
    const preview = value.slice(0, 3).map(item => formatStructuredValue(item)).join('、')
    return value.length > 3 ? `${preview} ç­‰${value.length}项` : preview
  }
  if (isPlainObject(value)) return stringifyStructuredPayload(value, 0)
  return String(value)
}
const normalizeManufacturingSummaryEntries = (summary) => {
  if (!isPlainObject(summary)) return []
  return Object.entries(summary)
    .filter(([, value]) => value !== undefined && value !== null && `${value}`.trim() !== '')
    .map(([key, value]) => ({
      key,
      label: getStructuredFieldLabel(key),
      value: formatStructuredValue(value)
    }))
}
const normalizeManufacturingCoreMetrics = (coreMetrics) => {
  if (Array.isArray(coreMetrics)) {
    return coreMetrics.map((item, index) => {
      if (isPlainObject(item)) {
        const label = item.label || item.name || item.key || `指标${index + 1}`
        const metricValue = item.value ?? item.metricValue ?? item.data ?? '-'
        const unit = item.unit ? ` ${item.unit}` : ''
        return {
          key: String(item.key || item.name || index),
          label,
          value: `${formatStructuredValue(metricValue)}${unit}`.trim()
        }
      }
      return {
        key: String(index),
        label: `指标${index + 1}`,
        value: formatStructuredValue(item)
      }
    })
  }
  if (isPlainObject(coreMetrics)) {
    return Object.entries(coreMetrics).map(([key, value]) => ({
      key,
      label: getStructuredFieldLabel(key),
      value: formatStructuredValue(value)
    }))
  }
  return []
}
const normalizeManufacturingWarningItems = (items = []) => {
  if (!Array.isArray(items)) return []
  return items
    .filter(item => isPlainObject(item))
    .map(item => ({
      level: String(item.level || '').toLowerCase(),
      title: item.title || '',
      count: item.count ?? '',
      detail: item.detail ?? ''
    }))
}
const inferManufacturingColumns = (items = []) => {
  if (!Array.isArray(items) || !items.length) return []
  const fieldSet = new Set()
  items.forEach((item) => {
    if (!isPlainObject(item)) return
    Object.keys(item).forEach((key) => fieldSet.add(key))
  })
  return Array.from(fieldSet)
}
const getManufacturingActionCardRuntimeKey = (card = {}, index = 0) => {
  const code = String(card?.code || '').trim()
  const api = String(card?.targetApi || '').trim()
  const name = String(card?.name || '').trim()
  return `${code}::${api}::${name}::${index}`
}
const normalizeManufacturingActionCards = (actionCards = [], previousCards = []) => {
  const previousMap = new Map()
  if (Array.isArray(previousCards)) {
    previousCards.forEach((card, index) => {
      previousMap.set(getManufacturingActionCardRuntimeKey(card, index), card)
    })
  }
  return (Array.isArray(actionCards) ? actionCards : [])
    .filter(card => isPlainObject(card))
    .map((card, index) => {
      const runtimeKey = getManufacturingActionCardRuntimeKey(card, index)
      const previousCard = previousMap.get(runtimeKey)
      const fallbackPayloadText = stringifyStructuredPayload(card.examplePayload, 2)
      return {
        ...card,
        runtimeKey,
        payloadText: previousCard?.payloadText ?? fallbackPayloadText,
        executing: Boolean(previousCard?.executing),
        executed: Boolean(previousCard?.executed),
        executeResult: previousCard?.executeResult || '',
        executeError: Boolean(previousCard?.executeError)
      }
    })
}
const buildManufacturingStructuredData = (parsedData, previousData = null) => {
  const type = String(parsedData?.type || '')
  if (!manufacturingStructuredTypeSet.has(type)) return null
  const rawData = isPlainObject(parsedData?.data) ? parsedData.data : {}
  const items = Array.isArray(rawData.items) ? rawData.items.filter(item => isPlainObject(item)) : []
  const warningItems = type === 'manufacturing_warning' ? normalizeManufacturingWarningItems(items) : []
  const listItems = manufacturingListTypeSet.has(type) ? items : []
  const actionCards = type === 'manufacturing_action_plan'
    ? normalizeManufacturingActionCards(rawData.actionCards, previousData?.actionCards)
    : []
  return {
    type,
    summaryEntries: normalizeManufacturingSummaryEntries(parsedData?.summary),
    coreMetrics: normalizeManufacturingCoreMetrics(rawData.coreMetrics),
    listItems,
    columns: inferManufacturingColumns(listItems),
    warningItems,
    actionCards
  }
}
const getManufacturingTypeLabel = (type = '') => manufacturingTypeLabelMap[String(type || '')] || '制造结果'
const getManufacturingWarningLevelType = (level = '') => {
  const normalizedLevel = String(level || '').toLowerCase()
  if (normalizedLevel === 'high') return 'danger'
  if (normalizedLevel === 'medium') return 'warning'
  return 'info'
}
const getManufacturingWarningLevelLabel = (level = '') => {
  const normalizedLevel = String(level || '').toLowerCase()
  if (normalizedLevel === 'high') return '高'
  if (normalizedLevel === 'medium') return '中'
  return normalizedLevel ? normalizedLevel.toUpperCase() : '一般'
}
const normalizeRequestMethod = (method = 'POST') => {
  const normalized = String(method || 'POST').trim().toUpperCase()
  if (['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].includes(normalized)) return normalized
  return 'POST'
}
const getNormalizedRequestMethod = (method) => normalizeRequestMethod(method)
const getPayloadValueByPath = (payload, fieldPath = '') => {
  const normalizedPath = String(fieldPath || '').trim()
  if (!normalizedPath || !isPlainObject(payload)) return undefined
  const pathSegments = normalizedPath
    .replace(/\[(\d+)]/g, '.$1')
    .split('.')
    .filter(Boolean)
  return pathSegments.reduce((current, segment) => {
    if (current === null || current === undefined) return undefined
    if (!['object', 'function'].includes(typeof current)) return undefined
    return current[segment]
  }, payload)
}
const getMissingRequiredFields = (requiredFields = [], payload = {}) => {
  if (!Array.isArray(requiredFields) || !requiredFields.length) return []
  return requiredFields.filter((fieldPath) => {
    const value = getPayloadValueByPath(payload, fieldPath)
    return !hasMeaningfulPayloadValue(value)
  })
}
const parseManufacturingActionPayload = (payloadText = '') => {
  const text = String(payloadText ?? '').trim()
  if (!text) return {}
  return JSON.parse(text)
}
const executeManufacturingAction = async (message, actionCard, cardIndex = 0) => {
  if (!message?.manufacturingData || !actionCard || actionCard.executing) return
  const actionName = actionCard.name || `动作 ${cardIndex + 1}`
  const targetApi = String(actionCard.targetApi || '').trim()
  if (!targetApi) {
    actionCard.executeError = true
    actionCard.executeResult = '缺少 targetApi,无法执行动作'
    return
  }
  let payload = {}
  try {
    payload = parseManufacturingActionPayload(actionCard.payloadText)
  } catch (err) {
    actionCard.executeError = true
    actionCard.executeResult = '请求参数不是合法 JSON,请检查后重试'
    return
  }
  const requiredFields = Array.isArray(actionCard.requiredFields) ? actionCard.requiredFields : []
  if (requiredFields.length && !isPlainObject(payload)) {
    actionCard.executeError = true
    actionCard.executeResult = '必填字段校验失败:请求参数必须是 JSON å¯¹è±¡'
    return
  }
  const missingFields = getMissingRequiredFields(requiredFields, payload)
  if (missingFields.length) {
    actionCard.executeError = true
    const missingFieldLabels = missingFields.map(field => getStructuredPathLabel(field))
    actionCard.executeResult = `缺少必填字段:${missingFieldLabels.join('、')}`
    return
  }
  try {
    await ElMessageBox.confirm(`确认执行「${actionName}」吗?`, '执行确认', {
      confirmButtonText: '确认执行',
      cancelButtonText: '取消',
      type: 'warning'
    })
  } catch (err) {
    return
  }
  actionCard.executing = true
  actionCard.executeError = false
  actionCard.executeResult = ''
  const method = normalizeRequestMethod(actionCard.method)
  const requestConfig = {
    url: targetApi,
    method: method.toLowerCase()
  }
  if (method === 'GET') {
    requestConfig.params = payload
  } else {
    requestConfig.data = payload
  }
  try {
    const res = await request(requestConfig)
    const successMsg = res?.msg || `${actionName}执行成功`
    actionCard.executed = true
    actionCard.executeError = false
    actionCard.executeResult = successMsg
    ElMessage.success(successMsg)
  } catch (err) {
    actionCard.executed = false
    actionCard.executeError = true
    actionCard.executeResult = err?.message || `${actionName}执行失败,请稍后重试`
  } finally {
    actionCard.executing = false
  }
}
// åŽ†å²ä¼šè¯ç›¸å…³
@@ -1034,6 +1579,8 @@
          tableData: null,
          payloadTreeData: null,
          payloadHiddenData: null,
          purchaseAnalysisData: null,
          manufacturingData: null,
          localUploadFiles: isUser ? mapHistoryFilePathsToSnapshots(msg.filePaths, uuid.value, idx) : []
        }
@@ -1273,15 +1820,25 @@
const applyStructuredMessageData = (messageObj, parsedData, msgIndex, shouldRenderCharts = true) => {
  if (!messageObj || !parsedData?.success) return
  const previousManufacturingData = messageObj.manufacturingData
  messageObj.type = parsedData.type || ''
  messageObj.tableData = null
  messageObj.purchaseAnalysisData = null
  messageObj.manufacturingData = null
  if (messageObj.type === 'todo_list' && parsedData.data) {
    messageObj.tableData = parsedData.data
  }
  const manufacturingData = buildManufacturingStructuredData(parsedData, previousManufacturingData)
  if (manufacturingData) {
    messageObj.manufacturingData = manufacturingData
  }
  if (parsedData.action === 'confirm_required' && parsedData.businessType) {
    messageObj.type = 'purchase_analysis_confirm'
    messageObj.purchaseAnalysisData = parsedData
    messageObj.manufacturingData = null
    if (!Array.isArray(messageObj.payloadTreeData) || !messageObj.payloadTreeData.length) {
      initializePurchasePayloadTree(messageObj, parsedData.payload || {})
    }
@@ -1321,6 +1878,19 @@
  }
  return null
}
const getStructuredFallbackText = (parsedData) => {
  if (!parsedData) return '正在为您展示分析结果...'
  if (parsedData.type === 'todo_list') return '已为您整理好相关数据。'
  if (manufacturingStructuredTypeSet.has(parsedData.type)) {
    if (parsedData.type === 'manufacturing_action_plan') return '已为您生成办理建议,请确认动作后执行。'
    if (parsedData.type === 'manufacturing_warning') return '已为您生成制造预警看板。'
    if (parsedData.type === 'manufacturing_analysis') return '已为您生成制造分析结果。'
    return '已返回制造查询结果。'
  }
  if (parsedData.charts && Object.keys(parsedData.charts).length > 0) return '已为您生成分析图表。'
  return '正在为您展示分析结果...'
}
const buildPurchaseMaterialRankCharts = (parsedData) => {
@@ -2446,7 +3016,9 @@
    type: '',
    tableData: null,
    payloadTreeData: null,
    payloadHiddenData: null
    payloadHiddenData: null,
    purchaseAnalysisData: null,
    manufacturingData: null
  })
  outputState.value[botMsgIndex] = {
@@ -2569,7 +3141,9 @@
    type: '',
    tableData: null,
    payloadTreeData: null,
    payloadHiddenData: null
    payloadHiddenData: null,
    purchaseAnalysisData: null,
    manufacturingData: null
  }
  messages.value.push(botMsg)
@@ -2735,13 +3309,7 @@
    }
    if (!display) {
      if (parsed.type === 'todo_list') {
        display = '已为您整理好相关数据。'
      } else if (parsed.charts && Object.keys(parsed.charts).length > 0) {
        display = '已为您生成分析图表。'
      } else {
        display = '正在为您展示分析结果...'
      }
      display = getStructuredFallbackText(parsed)
    }
  } else if (startIdx !== -1) {
    const lastBraceIdx = output.lastIndexOf('}')
@@ -2763,13 +3331,7 @@
        }
        if (!display) {
          if (parsed.type === 'todo_list') {
            display = '已为您整理好相关数据:'
          } else if (parsed.charts && Object.keys(parsed.charts).length > 0) {
            display = '已为您生成分析图表:'
          } else {
            display = '正在为您展示分析结果...'
          }
          display = getStructuredFallbackText(parsed)
        }
      } catch (e) {
        // è§£æžå¤±è´¥ï¼Œè¯´æ˜Ž JSON è¿˜åœ¨ä¼ è¾“中或格式不正确
@@ -3792,6 +4354,171 @@
  }
}
.manufacturing-card {
  margin-top: 12px;
  width: 100%;
  background: #fff;
  border: 1px solid rgba(0, 85, 212, 0.12);
  border-radius: 12px;
  box-shadow: $shadow-card;
  padding: 14px;
}
.manufacturing-card__title {
  font-size: 14px;
  font-weight: 700;
  color: $deep-blue;
  margin-bottom: 10px;
}
.manufacturing-summary-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 8px;
  margin-bottom: 12px;
}
.manufacturing-summary-item {
  border-radius: 10px;
  padding: 10px 12px;
  border: 1px solid rgba(0, 85, 212, 0.08);
  background: linear-gradient(180deg, #f8fbff, #f1f7ff);
  min-height: 66px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 6px;
}
.manufacturing-summary-item--core {
  border-color: rgba(30, 91, 255, 0.24);
}
.manufacturing-summary-label {
  font-size: 12px;
  color: #4b5563;
}
.manufacturing-summary-value {
  font-size: 15px;
  color: #1f2937;
  line-height: 1.4;
  word-break: break-all;
}
.manufacturing-warning-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-bottom: 12px;
}
.manufacturing-warning-item {
  border-radius: 10px;
  border: 1px solid rgba(245, 158, 11, 0.22);
  background: linear-gradient(135deg, rgba(255, 247, 237, 0.9), rgba(255, 255, 255, 0.98));
  padding: 10px 12px;
}
.manufacturing-warning-item__head {
  display: flex;
  align-items: center;
  gap: 8px;
  color: #92400e;
  font-size: 13px;
}
.manufacturing-warning-count {
  margin-left: auto;
  font-weight: 700;
  color: #c2410c;
}
.manufacturing-warning-detail {
  margin: 8px 0 0;
  font-size: 12px;
  line-height: 1.6;
  color: #7c2d12;
  word-break: break-all;
}
.manufacturing-table-wrapper {
  margin-top: 10px;
}
.manufacturing-action-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-top: 12px;
}
.manufacturing-action-card {
  border: 1px solid rgba(0, 85, 212, 0.1);
  border-radius: 10px;
  padding: 10px 12px;
  background: #f8fbff;
}
.manufacturing-action-card__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  font-size: 13px;
  color: #1f2937;
  margin-bottom: 8px;
}
.manufacturing-action-card__meta {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-bottom: 8px;
  font-size: 12px;
  color: #64748b;
  word-break: break-all;
}
.manufacturing-action-card__desc {
  margin: 0 0 8px;
  font-size: 12px;
  line-height: 1.6;
  color: #475467;
}
.manufacturing-required-fields {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin-bottom: 8px;
  font-size: 12px;
  color: #7c2d12;
}
.manufacturing-action-footer {
  margin-top: 8px;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 12px;
}
.manufacturing-action-result {
  flex: 1;
  font-size: 12px;
  line-height: 1.5;
  &.success {
    color: #1f9d55;
  }
  &.error {
    color: #d93025;
  }
}
.purchase-confirm-card {
  margin-top: 12px;
  width: 100%;
src/components/ProcessParamListDialog.vue
@@ -130,6 +130,7 @@
            </el-form-item>
            <el-form-item label="标准值">
              <el-input v-model="selectedParam.standardValue"
                        @input="val => onStandardValueInput(val, selectedParam)"
                        placeholder="请输入默认值" />
            </el-form-item>
            <el-form-item label="是否必填">
@@ -144,7 +145,8 @@
        </div>
      </div>
      <template #footer>
        <el-button type="primary" @click="handleParamSelectSubmit">确定</el-button>
        <el-button type="primary"
                   @click="handleParamSelectSubmit">确定</el-button>
        <el-button @click="selectParamDialogVisible = false">取消</el-button>
      </template>
    </el-dialog>
@@ -174,11 +176,13 @@
        <el-form-item label="标准值"
                      prop="standardValue">
          <el-input v-model="editParamForm.standardValue"
                    @input="val => onStandardValueInput(val, editParamForm)"
                    placeholder="请输入标准值" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button type="primary" @click="handleEditParamSubmit">确定</el-button>
        <el-button type="primary"
                   @click="handleEditParamSubmit">确定</el-button>
        <el-button @click="editParamDialogVisible = false">取消</el-button>
      </template>
    </el-dialog>
@@ -266,8 +270,32 @@
    paramFormat: "",
    unit: "",
  });
  const onStandardValueInput = (val, target) => {
    const data = target.value || target;
    const type = data.paramType || data.parameterType;
    if (type === 1) {
      // æ•°å€¼æ ¼å¼ï¼šä¸èƒ½è¾“入中文或英文字符
      data.standardValue = val.replace(/[a-zA-Z\u4e00-\u9fa5]/g, "");
    }
  };
  const editParamRules = ref({
    // standardValue: [{ required: true, message: "请输入标准值", trigger: "blur" }],
    standardValue: [
      {
        validator: (rule, value, callback) => {
          const type =
            editParamForm.value.paramType || editParamForm.value.parameterType;
          if (type === 1 && value) {
            if (/[a-zA-Z\u4e00-\u9fa5]/.test(value)) {
              return callback(new Error("数值格式不能包含中英文字符"));
            }
          }
          callback();
        },
        trigger: "blur",
      },
    ],
  });
  const editParamFormRef = ref(null);
src/views/aiIndustrialBrain/MAINTAIN_RULES.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,7 @@
# AI工业大脑维护规则
1. å½“ `src/views/aiIndustrialBrain/index.vue` æ–°å¢žæ™ºèƒ½ä½“(`agents`)逻辑时,必须同步确认弹窗助手可用性。
2. å¼¹çª—助手由 `src/components/AIChatSidebar/assistants/index.js` çš„ `assistantRegistry` ç»Ÿä¸€æ³¨å†Œã€‚
3. æ–°å¢žæ™ºèƒ½ä½“çš„ `key` è‹¥è¦åœ¨å¼¹çª—中可用,必须在 `assistantRegistry` ä¸­æä¾›åŒåé…ç½®ã€‚
4. æœªåœ¨ `assistantRegistry` æ³¨å†Œçš„æ™ºèƒ½ä½“会在弹窗中显示为 `pending`(开发中)态。
src/views/aiIndustrialBrain/components/AiAssistantWorkspace.vue
@@ -17,7 +17,7 @@
            v-if="assistantMode !== 'pending'"
            :key="assistantMode"
            class="workspace-chat"
            :assistants="assistantMode === 'purchase' ? [purchaseAssistant] : [generalAssistant]"
            :assistants="resolvedAssistants"
            :default-assistant="assistantMode"
            :hide-trigger="true"
            :auto-open="true"
@@ -43,7 +43,7 @@
import { computed } from "vue";
import { ArrowLeftBold } from "@element-plus/icons-vue";
import AIChatSidebar from "@/components/AIChatSidebar/index.vue";
import { generalAssistant, purchaseAssistant } from "@/components/AIChatSidebar/assistants";
import { assistantRegistry } from "@/components/AIChatSidebar/assistants";
const props = defineProps({
  visible: {
@@ -60,11 +60,17 @@
const agentKey = computed(() => String(props.agent?.key || ""));
const agentTitle = computed(() => String(props.agent?.name || "AI助手"));
/**
 * ç»´æŠ¤è§„则:
 * AI工业大脑新增智能体时,若希望右侧弹窗可用,需保证智能体 key åœ¨ assistantRegistry ä¸­æœ‰åŒåé…ç½®ã€‚
 * æœªé…ç½®æ—¶ä¼šè¿›å…¥ pending(开发中)态,作为显式提醒。
 */
const resolvedAssistant = computed(() => assistantRegistry[agentKey.value] || null);
const assistantMode = computed(() => {
  if (agentKey.value === "purchase") return "purchase";
  if (agentKey.value === "general") return "general";
  return "pending";
  return resolvedAssistant.value ? agentKey.value : "pending";
});
const resolvedAssistants = computed(() => (resolvedAssistant.value ? [resolvedAssistant.value] : []));
</script>
<style scoped>
src/views/aiIndustrialBrain/index.vue
@@ -154,6 +154,7 @@
const router = useRouter();
// ç»´æŠ¤çº¦å®šè§ï¼šsrc/views/aiIndustrialBrain/MAINTAIN_RULES.md
const agents = [
  {
    key: "general",
src/views/basicData/product/index.vue
@@ -5,7 +5,7 @@
        <el-input v-model="search"
                  style="width: 210px"
                  placeholder="输入关键字进行搜索"
                  @change="searchFilter"
                  @input="debouncedSearch"
                  @clear="searchFilter"
                  clearable
                  prefix-icon="Search" />
@@ -565,40 +565,31 @@
        proxy.$modal.msg("已取消");
      });
  };
  // è°ƒç”¨tree过滤方法 ä¸­æ–‡è‹±è¿‡æ»¤
  const filterNode = (value, data, node) => {
    if (!value) {
      //如果数据为空,则返回true,显示所有的数据项
      return true;
    }
    // æŸ¥è¯¢åˆ—表是否有匹配数据,将值小写,匹配英文数据
    let val = value.toLowerCase();
    return chooseNode(val, data, node); // è°ƒç”¨è¿‡æ»¤äºŒå±‚方法
  const debounce = (fn, delay = 300) => {
    let timer;
    return (...args) => {
      clearTimeout(timer);
      timer = setTimeout(() => fn(...args), delay);
    };
  };
  // è¿‡æ»¤çˆ¶èŠ‚ç‚¹ / å­èŠ‚ç‚¹ (如果输入的参数是父节点且能匹配,则返回该节点以及其下的所有子节点;如果参数是子节点,则返回该节点的父节点。name是中文字符,enName是英文字符.
  const chooseNode = (value, data, node) => {
    if (data.label.indexOf(value) !== -1) {
  const debouncedSearch = debounce(() => {
    searchFilter();
  }, 300);
  const filterNode = (value, data) => {
    if (!value) return true;
    return chooseNode(value.toLowerCase(), data);
  };
  const chooseNode = (value, data) => {
    const label = (data.label || '').toLowerCase();
    if (label.indexOf(value) !== -1) {
      return true;
    }
    const level = node.level;
    // å¦‚果传入的节点本身就是一级节点就不用校验了
    if (level === 1) {
      return false;
    if (data.children && data.children.length > 0) {
      return data.children.some(child => chooseNode(value, child));
    }
    // å…ˆå–当前节点的父节点
    let parentData = node.parent;
    // éåŽ†å½“å‰èŠ‚ç‚¹çš„çˆ¶èŠ‚ç‚¹
    let index = 0;
    while (index < level - 1) {
      // å¦‚果匹配到直接返回,此处name值是中文字符,enName是英文字符。判断匹配中英文过滤
      if (parentData.data.label.indexOf(value) !== -1) {
        return true;
      }
      // å¦åˆ™çš„话再往上一层做匹配
      parentData = parentData.parent;
      index++;
    }
    // æ²¡åŒ¹é…åˆ°è¿”回false
    return false;
  };
  getProductTreeList();
src/views/basicData/supplierManage/components/BlacklistTab.vue
@@ -231,7 +231,10 @@
        </div>
      </template>
    </el-dialog>
    <files-dia ref="filesDia"></files-dia>
    <FileList v-if="fileListDialogVisible"
              v-model:visible="fileListDialogVisible"
              record-type="supplier_manage"
              :record-id="recordId" />
  </div>
</template>
@@ -249,7 +252,9 @@
} from "@/api/basicData/supplierManageFile.js";
import useUserStore from "@/store/modules/user";
import { getToken } from "@/utils/auth.js";
import FilesDia from "../filesDia.vue";
const FileList = defineAsyncComponent(() =>
    import("@/components/Dialog/FileList.vue")
);
const { proxy } = getCurrentInstance();
const userStore = useUserStore();
@@ -327,7 +332,7 @@
        name: "资质文件",
        type: "text",
        clickFun: (row) => {
          openFilesFormDia(row)
          openFileDialog(row)
        }
      }
    ],
@@ -342,7 +347,8 @@
  size: 100,
  total: 0,
});
const filesDia = ref()
const fileListDialogVisible = ref(false);
const recordId = ref();
// ç”¨æˆ·ä¿¡æ¯è¡¨å•弹框数据
const operationType = ref("");
const dialogFormVisible = ref(false);
@@ -567,10 +573,9 @@
  return `${year}-${month}-${day}`;
}
// æ‰“开附件弹框
const openFilesFormDia = (row) => {
  nextTick(() => {
    filesDia.value?.openDialog(row)
  })
const openFileDialog = async row => {
  recordId.value = row.id;
  fileListDialogVisible.value = true;
};
onMounted(() => {
src/views/basicData/supplierManage/components/HomeTab.vue
@@ -237,7 +237,10 @@
        </div>
      </template>
    </el-dialog>
    <files-dia ref="filesDia"></files-dia>
    <FileList v-if="fileListDialogVisible"
              v-model:visible="fileListDialogVisible"
              record-type="supplier_manage"
              :record-id="recordId" />
  </div>
</template>
@@ -255,7 +258,9 @@
} from "@/api/basicData/supplierManageFile.js";
import useUserStore from "@/store/modules/user";
import { getToken } from "@/utils/auth.js";
import FilesDia from "../filesDia.vue";
const FileList = defineAsyncComponent(() =>
    import("@/components/Dialog/FileList.vue")
);
const { proxy } = getCurrentInstance();
const userStore = useUserStore();
@@ -333,7 +338,7 @@
        name: "资质文件",
        type: "text",
        clickFun: (row) => {
          openFilesFormDia(row)
          openFileDialog(row)
        }
      }
    ],
@@ -343,12 +348,13 @@
const selectedRows = ref([]);
const userList = ref([]);
const tableLoading = ref(false);
const fileListDialogVisible = ref(false);
const recordId = ref();
const page = reactive({
  current: 1,
  size: 100,
  total: 0,
});
const filesDia = ref()
// ç”¨æˆ·ä¿¡æ¯è¡¨å•弹框数据
const operationType = ref("");
const dialogFormVisible = ref(false);
@@ -573,10 +579,9 @@
  return `${year}-${month}-${day}`;
}
// æ‰“开附件弹框
const openFilesFormDia = (row) => {
  nextTick(() => {
    filesDia.value?.openDialog(row)
  })
const openFileDialog = async row => {
  recordId.value = row.id;
  fileListDialogVisible.value = true;
};
onMounted(() => {
src/views/collaborativeApproval/approvalProcess/components/infoFormDia.vue
@@ -152,7 +152,8 @@
    startDate: "", // è¯·å‡å¼€å§‹æ—¶é—´
    endDate: "", // è¯·å‡ç»“束时间
    price: null, // æŠ¥é”€é‡‘额
    location: "" // å‡ºå·®åœ°ç‚¹
    location: "", // å‡ºå·®åœ°ç‚¹
    storageBlobDTOS: []
  },
  rules: {
    approveId: [{ required: false, message: "请输入", trigger: "blur" }],
@@ -270,7 +271,7 @@
      return
    }
  }
  form.value.storageBlobDTOList = fileList.value
  form.value.storageBlobDTOS = fileList.value
  proxy.$refs.formRef.validate(valid => {
    if (valid) {
src/views/collaborativeApproval/approvalProcess/index.vue
@@ -126,20 +126,23 @@
    <!-- å¼¹çª—组件 -->
    <info-form-dia ref="infoFormDia" @close="handleQuery" :approveType="currentApproveType"></info-form-dia>
    <approval-dia ref="approvalDia" @close="handleQuery" :approveType="currentApproveType"></approval-dia>
    <FileList ref="fileListRef" />
    <FileList v-if="fileDialogVisible"
              v-model:visible="fileDialogVisible"
              record-type="approve_process"
              :record-id="recordId" />
  </div>
</template>
<script setup>
import FileList from "./fileList.vue";
import { Search, Plus, Delete, Download, RefreshRight, DocumentChecked } from "@element-plus/icons-vue";
import {onMounted, ref, computed, reactive, toRefs, nextTick, getCurrentInstance} from "vue";
import {onMounted, ref, computed, reactive, toRefs, nextTick, getCurrentInstance, defineAsyncComponent} from "vue";
import {ElMessageBox} from "element-plus";
import { useRoute } from 'vue-router';
import InfoFormDia from "@/views/collaborativeApproval/approvalProcess/components/infoFormDia.vue";
import ApprovalDia from "@/views/collaborativeApproval/approvalProcess/components/approvalDia.vue";
import {approveProcessDelete, approveProcessListPage} from "@/api/collaborativeApproval/approvalProcess.js";
import useUserStore from "@/store/modules/user";
const FileList = defineAsyncComponent(() => import("@/components/Dialog/FileList.vue"));
const userStore = useUserStore();
const route = useRoute();
@@ -337,7 +340,7 @@
      name: "附件",
      type: "text",
      clickFun: (row) => {
        downLoadFile(row);
        openFilesFormDia(row);
      },
    });
  }
@@ -371,11 +374,17 @@
  page.current = 1;
  getList();
};
const fileListRef = ref(null)
const downLoadFile = (row) => {
  fileListRef.value.open(row.commonFileList)
// æ‰“开附件弹窗
const recordId =ref(0)
const fileDialogVisible = ref(false)
// æ‰“开附件弹框
const openFilesFormDia = async (row) => {
  recordId.value = row.id
  fileDialogVisible.value = true
}
const pagination = (obj) => {
  page.current = obj.page;
  page.size = obj.limit;
src/views/collaborativeApproval/sealManagement/index.vue
@@ -87,10 +87,18 @@
        </el-form-item>
        <el-form-item label="紧急程度" prop="urgency">
          <el-radio-group v-model="sealForm.urgency">
            <el-radio label="normal">普通</el-radio>
            <el-radio label="urgent">紧急</el-radio>
            <el-radio label="very-urgent">特急</el-radio>
            <el-radio value="normal">普通</el-radio>
            <el-radio value="urgent">紧急</el-radio>
            <el-radio value="very-urgent">特急</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="附件上传">
          <AttachmentUploadFile
            v-model:fileList="sealForm.storageBlobDTOs"
            :limit="10"
            :fileSize="50"
            buttonText="点击上传附件"
          />
        </el-form-item>
      </el-form>
    </FormDialog>
@@ -119,8 +127,27 @@
          </el-descriptions-item>
          <el-descriptions-item label="申请原因" :span="2">{{ currentSealDetail.reason }}</el-descriptions-item>
        </el-descriptions>
        <!-- é™„件列表 -->
        <div v-if="currentSealDetail.storageBlobVOList?.length || currentSealDetail.storageBlobDTOs?.length" class="attachment-section">
          <div class="attachment-title">附件列表:</div>
          <el-table :data="currentSealDetail.storageBlobVOList || currentSealDetail.storageBlobDTOs" border class="attachment-table">
            <el-table-column label="附件名称" show-overflow-tooltip>
              <template #default="scope">
                {{ scope.row.originalFilename || scope.row.name || scope.row.fileName || '未命名文件' }}
              </template>
            </el-table-column>
            <el-table-column fixed="right" label="操作" width="150" align="center">
              <template #default="scope">
                <el-button link type="primary" size="small" @click="previewFile(scope.row)">预览</el-button>
                <el-button link type="primary" size="small" @click="downloadFile(scope.row)">下载</el-button>
              </template>
            </el-table-column>
          </el-table>
        </div>
      </div>
    </FormDialog>
    <!-- æ–‡ä»¶é¢„览组件 -->
    <FilePreview ref="filePreviewRef" />
  </div>
</template>
@@ -134,6 +161,9 @@
import useUserStore from '@/store/modules/user'
import FormDialog from '@/components/Dialog/FormDialog.vue'
import PIMTable from '@/components/PIMTable/PIMTable.vue'
import AttachmentUploadFile from '@/components/AttachmentUpload/file/index.vue'
import FilePreview from '@/components/filePreview/index.vue'
import download from '@/plugins/download.js'
// å“åº”式数据
// ç”¨å°ç”³è¯·ç›¸å…³
@@ -143,6 +173,7 @@
const tableLoading = ref(false)
const showSealDetailDialog = ref(false)
const currentSealDetail = ref(null)
const filePreviewRef = ref(null)
const sealFormRef = ref()
const userList = ref([])
const sealForm = reactive({
@@ -152,7 +183,8 @@
  reason: '',
  approveUserId: '',
  urgency: 'normal',
  status: 'pending'
  status: 'pending',
  storageBlobDTOs: []
})
const sealRules = {
@@ -281,7 +313,8 @@
        reason: '',
        approveUserId: '',
        urgency: 'normal',
        status: 'pending'
        status: 'pending',
        storageBlobDTOs: []
      })
      }
    }).catch(err => {
@@ -301,7 +334,8 @@
    reason: '',
    approveUserId: '',
    urgency: 'normal',
    status: 'pending'
    status: 'pending',
    storageBlobDTOs: []
  })
  // æ¸…除表单验证状态
  if (sealFormRef.value) {
@@ -318,6 +352,27 @@
const viewSealDetail = (row) => {
  currentSealDetail.value = row
  showSealDetailDialog.value = true
}
// é¢„览文件
const previewFile = (row) => {
  const url = row.previewURL || row.previewUrl || row.url
  if (url && filePreviewRef.value) {
    filePreviewRef.value.open(url)
  } else {
    ElMessage.warning('文件地址无效,无法预览')
  }
}
// ä¸‹è½½æ–‡ä»¶
const downloadFile = (row) => {
  const url = row.downloadURL || row.downloadUrl || row.url
  if (url) {
    const filename = row.originalFilename || row.name || row.fileName || 'download'
    download.byUrl(url, filename)
  } else {
    ElMessage.warning('文件地址无效,无法下载')
  }
}
// å®¡æ‰¹ç”¨å°ç”³è¯·
const approveSeal = (row) => {
@@ -421,4 +476,19 @@
.ml-10 {
  margin-left: 10px;
}
.attachment-section {
  margin-top: 20px;
}
.attachment-title {
  font-size: 14px;
  color: #606266;
  margin-bottom: 10px;
  font-weight: 500;
}
.attachment-table {
  border-radius: 4px;
}
</style>
src/views/equipmentManagement/inspectionManagement/components/formDia.vue
@@ -26,6 +26,21 @@
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="巡检项目" prop="inspectionProject">
              <el-input v-model="form.inspectionProject" placeholder="请输入巡检项目" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="是否启用" prop="isEnabled">
              <el-radio-group v-model="form.isEnabled">
                <el-radio :value="1">是</el-radio>
                <el-radio :value="0">否</el-radio>
              </el-radio-group>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="备注" prop="remarks">
              <el-input v-model="form.remarks" placeholder="请输入备注" type="textarea" />
            </el-form-item>
@@ -118,6 +133,8 @@
    taskName: undefined,
    inspector: '',
    inspectorIds: '',
    inspectionProject: '',
    isEnabled: 1,
    remarks: '',
    frequencyType: '',
    frequencyDetail: '',
@@ -245,6 +262,8 @@
    taskName: undefined,
    inspector: '',
    inspectorIds: '',
    inspectionProject: '',
    isEnabled: 1,
    remarks: '',
    frequencyType: '',
    frequencyDetail: '',
src/views/equipmentManagement/inspectionManagement/index.vue
@@ -70,6 +70,11 @@
                    class="no-data">--</span>
            </div>
          </template>
          <template #isEnabled="{ row }">
            <el-tag :type="row.isEnabled === 1 ? 'success' : 'danger'" size="small">
              {{ row.isEnabled == 1 ? '是' : '否' }}
            </el-tag>
          </template>
        </PIMTable>
      </div>
    </el-card>
@@ -126,8 +131,16 @@
  // åˆ—配置
  const columns = ref([
    { prop: "taskName", label: "巡检任务名称", minWidth: 160 },
    { prop: "inspectionProject", label: "巡检项目", minWidth: 150 },
    { prop: "remarks", label: "备注", minWidth: 150 },
    { prop: "inspector", label: "执行巡检人", minWidth: 150, slot: "inspector" },
    {
      prop: "isEnabled",
      label: "是否启用",
      minWidth: 100,
      dataType: "slot",
      slot: "isEnabled"
    },
    {
      prop: "frequencyType",
      label: "频次",
@@ -227,8 +240,10 @@
      operationsArr.value = ["edit"];
    } else if (value === "task") {
      const operationColumn = getOperationColumn(["viewFile"]);
      // å®šæ—¶ä»»åŠ¡è®°å½•ä¸å±•ç¤º"是否启用"列
      const taskColumns = columns.value.filter(col => col.prop !== "isEnabled");
      tableColumns.value = [
        ...columns.value,
        ...taskColumns,
        ...(operationColumn ? [operationColumn] : []),
      ];
      operationsArr.value = ["viewFile"];
src/views/equipmentManagement/measurementEquipment/components/formDia.vue
@@ -36,15 +36,6 @@
                </el-row>
                <el-row :gutter="30">
                    <el-col :span="12">
                        <el-form-item label="安装位置:" prop="instationLocation">
                            <el-input
                                v-model="form.instationLocation"
                                placeholder="请输入"
                                clearable
                            />
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="检定单位:" prop="unit">
              <el-input
                  v-model="form.unit"
@@ -53,17 +44,17 @@
              />
                        </el-form-item>
                    </el-col>
                </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="证书编号:" prop="model">
                    <el-col :span="12">
                        <el-form-item label="证书编号:" prop="model">
              <el-input
                  v-model="form.model"
                  placeholder="请输入"
                  clearable
              />
            </el-form-item>
          </el-col>
                    </el-col>
                </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="最新鉴定日期:" prop="mostDate">
              <el-date-picker
@@ -77,8 +68,6 @@
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="有效日期(天):" prop="valid">
              <el-input
@@ -91,15 +80,6 @@
              >
              <template #append>日</template>
              </el-input>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="检定周期:" prop="cycle">
              <el-input
                  v-model="form.cycle"
                  placeholder="请输入检定周期"
                  clearable
              />
            </el-form-item>
          </el-col>
        </el-row>
@@ -184,10 +164,8 @@
    form: {
        code: "",
    name: "",
    instationLocation: "",
    mostDate:"",
        model: "",
    cycle:"",
        validDate: "",
        nextDate: "",
        userId: "",
@@ -203,9 +181,7 @@
        nextDate: [{required: true, message: "请选择", trigger: "change"}],
        userId: [{required: true, message: "请选择", trigger: "change"}],
        recordDate: [{required: true, message: "请选择", trigger: "change"}],
    instationLocation: [{required: true, message: "请输入", trigger: "blur"}],
    mostDate: [{required: true, message: "请选择", trigger: "change"}],
    cycle: [{required: true, message: "请选择", trigger: "blur"}],
    valid: [
      {required: true, message: "请输入", trigger: "blur"},
      {
src/views/equipmentManagement/measurementEquipment/index.vue
@@ -42,6 +42,7 @@
                :tableLoading="tableLoading"
                @pagination="pagination"
        :dbRowClick="dbRowClick"
        :rowClassName="rowClassName"
            ></PIMTable>
        </div>
        <form-dia ref="formDia" @close="handleQuery"></form-dia>
@@ -89,12 +90,6 @@
    align: "center",
  },
    {
        label: "安装位置",
        prop: "instationLocation",
        width: 150,
    align:"center"
    },
    {
        label: "检定单位",
        prop: "unit",
        width: 200,
@@ -130,12 +125,6 @@
        width: 130,
    align:"center"
    },
  {
    label: "检定周期(天)",
    prop: "cycle",
    width: 130,
    align:"center"
  },
  {
    label: "状态",
    prop: "status",
@@ -193,6 +182,31 @@
const dbRowClick = (row)=>{
  rowClickData.value?.openDialog(row)
}
// è¡Œæ ·å¼ï¼šå¿«åˆ°æœŸï¼ˆ7天内)或逾期标红
const rowClassName = ({ row }) => {
  console.log('rowClassName called:', row);
  // valid æ˜¯æœ‰æ•ˆå¤©æ•°ï¼ŒmostDate æ˜¯æœ€æ–°æ£€å®šæ—¥æœŸ
  if (row.valid && row.mostDate) {
    const mostDate = new Date(row.mostDate);
    // è®¡ç®—到期日期 = æ£€å®šæ—¥æœŸ + æœ‰æ•ˆå¤©æ•°
    const validDays = parseInt(row.valid) || 0;
    const expireDate = new Date(mostDate);
    expireDate.setDate(expireDate.getDate() + validDays);
    const now = new Date();
    const diffDays = Math.ceil((expireDate - now) / (1000 * 60 * 60 * 24));
    console.log('row:', row.code, 'validDays:', validDays, 'expireDate:', expireDate, 'diffDays:', diffDays);
    // 7天内到期或已逾期都标红
    if (diffDays <= 7) {
      console.log('return warning-row');
      return 'warning-row';
    }
  } else {
    console.log('row missing valid or mostDate:', row.valid, row.mostDate);
  }
  return '';
}
// è¡¨æ ¼é€‰æ‹©æ•°æ®
@@ -294,5 +308,13 @@
</script>
<style scoped>
:deep(.el-table .warning-row) {
  background-color: #fef0f0 !important;
}
:deep(.el-table .warning-row:hover > td) {
  background-color: #f9d5d5 !important;
}
:deep(.el-table .el-table__body tr.warning-row td) {
  background-color: #fef0f0 !important;
}
</style>
src/views/equipmentManagement/repair/Modal/AcceptanceModal.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,144 @@
<template>
  <FormDialog
    v-model="visible"
    title="验收审批"
    width="500px"
    @confirm="submitForm"
    @cancel="handleCancel"
    @close="handleCancel"
  >
    <el-form :model="form" :rules="rules" label-width="100px">
      <el-form-item label="验收人" prop="acceptanceName">
        <el-select
          v-model="form.acceptanceName"
          placeholder="请选择验收人"
          filterable
          style="width: 100%"
        >
          <el-option
            v-for="item in userList"
            :key="item.userId"
            :label="item.nickName"
            :value="item.nickName"
          />
        </el-select>
      </el-form-item>
      <el-form-item label="验收时间" prop="acceptanceTime">
        <el-date-picker
          v-model="form.acceptanceTime"
          type="datetime"
          placeholder="请选择验收时间"
          format="YYYY-MM-DD HH:mm:ss"
          value-format="YYYY-MM-DD HH:mm:ss"
          style="width: 100%"
        />
      </el-form-item>
      <el-form-item label="验收备注" prop="acceptanceRemark">
        <el-input
          v-model="form.acceptanceRemark"
          type="textarea"
          :rows="3"
          placeholder="请输入验收备注"
        />
      </el-form-item>
    </el-form>
  </FormDialog>
</template>
<script setup>
import FormDialog from "@/components/Dialog/FormDialog.vue";
import { ref, reactive } from "vue";
import { ElMessage } from "element-plus";
import { userListNoPageByTenantId } from "@/api/system/user.js";
import { repairAcceptance } from "@/api/equipmentManagement/repair";
import dayjs from "dayjs";
defineOptions({
  name: "验收审批弹窗",
});
const emits = defineEmits(["ok"]);
const visible = ref(false);
const loading = ref(false);
const repairId = ref(null);
const userList = ref([]);
const form = reactive({
  acceptanceName: undefined,
  acceptanceTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
  acceptanceRemark: undefined,
});
const rules = {
  acceptanceName: [
    { required: true, message: "请选择验收人", trigger: "change" },
  ],
  acceptanceTime: [
    { required: true, message: "请选择验收时间", trigger: "change" },
  ],
  acceptanceRemark: [
    { required: true, message: "请输入验收备注", trigger: "blur" },
  ],
};
// åŠ è½½ç”¨æˆ·åˆ—è¡¨
const loadUserList = async () => {
  const { data } = await userListNoPageByTenantId();
  userList.value = data;
};
// æ‰“开弹窗
const open = async (row) => {
  repairId.value = row.id;
  visible.value = true;
  // é‡ç½®è¡¨å•
  form.acceptanceName = undefined;
  form.acceptanceTime = dayjs().format("YYYY-MM-DD HH:mm:ss");
  form.acceptanceRemark = undefined;
  await loadUserList();
};
// æäº¤è¡¨å•
const submitForm = async () => {
  if (!form.acceptanceName) {
    ElMessage.warning("请选择验收人");
    return;
  }
  if (!form.acceptanceTime) {
    ElMessage.warning("请选择验收时间");
    return;
  }
  if (!form.acceptanceRemark) {
    ElMessage.warning("请输入验收备注");
    return;
  }
  loading.value = true;
  try {
    const { code } = await repairAcceptance({
      id: repairId.value,
      acceptanceName: form.acceptanceName,
      acceptanceTime: form.acceptanceTime,
      acceptanceRemark: form.acceptanceRemark,
    });
    if (code === 200) {
      ElMessage.success("验收通过");
      visible.value = false;
      emits("ok");
    }
  } finally {
    loading.value = false;
  }
};
const handleCancel = () => {
  visible.value = false;
};
defineExpose({
  open,
});
</script>
<style lang="scss" scoped></style>
src/views/equipmentManagement/repair/Modal/RepairModal.vue
@@ -49,19 +49,44 @@
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="项目">
            <el-input v-model="form.machineryCategory" placeholder="请输入项目" />
          <el-form-item label="报修报修项目">
            <el-input v-model="form.machineryCategory" placeholder="请输入报修报修项目" />
          </el-form-item>
        </el-col>
      </el-row>
      <el-row>
        <el-col :span="12">
          <el-form-item label="维修人">
            <el-input v-model="form.maintenanceName" placeholder="请输入维修人姓名" />
          </el-form-item>
        </el-col>
      </el-row>
      <el-row v-if="id">
        <el-col :span="12">
          <el-form-item label="报修状态">
            <el-select v-model="form.status">
            <el-select v-model="form.status" disabled>
              <el-option label="待维修" :value="0"></el-option>
              <el-option label="完结" :value="1"></el-option>
              <el-option label="已验收" :value="1"></el-option>
              <el-option label="失败" :value="2"></el-option>
            </el-select>
          </el-form-item>
        </el-col>
      </el-row>
      <!-- éªŒæ”¶ä¿¡æ¯å±•示 -->
      <el-row v-if="id && form.status === 1">
        <el-col :span="12">
          <el-form-item label="验收人">
            <el-input v-model="form.acceptanceName" disabled />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="验收时间">
            <el-input v-model="form.acceptanceTime" disabled />
          </el-form-item>
        </el-col>
        <el-col :span="24">
          <el-form-item label="验收备注">
            <el-input v-model="form.acceptanceRemark" type="textarea" :rows="2" disabled />
          </el-form-item>
        </el-col>
      </el-row>
@@ -131,6 +156,7 @@
  status: 0, // æŠ¥ä¿®çŠ¶æ€
  machineryCategory: undefined,
  storageBlobDTOs: [],
  maintenanceName: undefined, // ç»´ä¿®äºº
});
const setDeviceModel = (deviceId) => {
@@ -148,6 +174,10 @@
  form.status = data.status;
  form.machineryCategory = data.machineryCategory;
  form.storageBlobDTOs = data.storageBlobVOs || [];
  form.maintenanceName = data.maintenanceName;
  form.acceptanceName = data.acceptanceName;
  form.acceptanceTime = data.acceptanceTime;
  form.acceptanceRemark = data.acceptanceRemark;
};
const sendForm = async () => {
src/views/equipmentManagement/repair/index.vue
@@ -100,13 +100,14 @@
        <template #statusRef="{ row }">
          <el-tag v-if="row.status === 2" type="danger">失败</el-tag>
          <el-tag v-if="row.status === 1" type="success">完结</el-tag>
          <el-tag v-if="row.status === 3" type="info">待验收</el-tag>
          <el-tag v-if="row.status === 0" type="warning">待维修</el-tag>
        </template>
        <template #operation="{ row }">
          <el-button
            type="primary"
            link
            :disabled="row.status === 1"
            :disabled="row.status === 1 || row.status === 3"
            @click="editRepair(row.id)"
          >
            ç¼–辑
@@ -114,15 +115,23 @@
          <el-button
            type="success"
            link
            :disabled="row.status === 1"
            :disabled="row.status !== 0"
            @click="addMaintain(row)"
          >
            ç»´ä¿®
          </el-button>
          <el-button
            type="warning"
            link
            :disabled="row.status !== 3"
            @click="openAcceptance(row)"
          >
            éªŒæ”¶
          </el-button>
          <el-button
            type="danger"
            link
            :disabled="row.status === 1"
            :disabled="row.status === 1 || row.status === 3"
            @click="delRepairByIds(row.id)"
          >
            åˆ é™¤
@@ -139,6 +148,7 @@
    </div>
    <RepairModal ref="repairModalRef" @ok="getTableData"/>
    <MaintainModal ref="maintainModalRef" @ok="getTableData"/>
    <AcceptanceModal ref="acceptanceModalRef" @ok="getTableData"/>
    <FileList v-if="fileDialogVisible"  v-model:visible="fileDialogVisible" :record-type="'device_repair'" :record-id="recordId"  />
  </div>
</template>
@@ -151,6 +161,7 @@
import {ElMessageBox, ElMessage} from "element-plus";
import dayjs from "dayjs";
import MaintainModal from "./Modal/MaintainModal.vue";
import AcceptanceModal from "./Modal/AcceptanceModal.vue";
const FileList = defineAsyncComponent(() => import("@/components/Dialog/FileList.vue"));
defineOptions({
@@ -162,6 +173,7 @@
// æ¨¡æ€æ¡†å®žä¾‹
const repairModalRef = ref();
const maintainModalRef = ref();
const acceptanceModalRef = ref();
// è¡¨æ ¼å¤šé€‰æ¡†é€‰ä¸­é¡¹
const multipleList = ref([]);
@@ -197,7 +209,7 @@
        prop: "deviceModel",
      },
      {
        label: "项目",
        label: "报修项目",
        align: "center",
        prop: "machineryCategory",
      },
@@ -232,6 +244,17 @@
        align: "center",
        prop: "maintenanceTime",
        formatData: (cell) => (cell ? dayjs(cell).format("YYYY-MM-DD") : ""),
      },
      {
        label: "验收人",
        align: "center",
        prop: "acceptanceName",
      },
      {
        label: "验收时间",
        align: "center",
        prop: "acceptanceTime",
        formatData: (cell) => (cell ? dayjs(cell).format("YYYY-MM-DD HH:mm:ss") : ""),
      },
      {
        label: "状态",
@@ -301,6 +324,11 @@
  maintainModalRef.value.open(row.id, row);
};
// æ‰“开验收弹窗
const openAcceptance = (row) => {
  acceptanceModalRef.value.open(row);
};
const changePage = ({page, limit}) => {
  pagination.currentPage = page;
  pagination.pageSize = limit;
src/views/equipmentManagement/upkeep/Form/PlanModal.vue
@@ -32,10 +32,10 @@
          disabled
        />
      </el-form-item>
      <el-form-item label="项目">
      <el-form-item label="保养项目">
        <el-input
            v-model="form.machineryCategory"
            placeholder="请输入项目"
            placeholder="请输入保养项目"
        />
      </el-form-item>
      <el-form-item label="录入人">
@@ -61,6 +61,13 @@
          <el-option label="完结" :value="1"></el-option>
          <el-option label="失败" :value="2"></el-option>
        </el-select>
      </el-form-item>
      <el-form-item label="保养人">
        <el-input
          v-model="form.maintenancePerson"
          placeholder="请输入保养人姓名"
          clearable
        />
      </el-form-item>
      <el-form-item label="计划保养日期">
        <el-date-picker
@@ -124,6 +131,7 @@
  status: 0, //保修状态
  machineryCategory: undefined,
  storageBlobDTOs: [],
  maintenancePerson: undefined, // ä¿å…»äºº
});
const setDeviceModel = (deviceId) => {
@@ -142,6 +150,7 @@
  form.createUser = Number(data.createUser);
  form.status = data.status;
  form.machineryCategory = data.machineryCategory;
  form.maintenancePerson = data.maintenancePerson;
  if (data.maintenancePlanTime) {
    form.maintenancePlanTime = dayjs(data.maintenancePlanTime).format(
      "YYYY-MM-DD HH:mm:ss"
src/views/equipmentManagement/upkeep/Form/formDia.vue
@@ -67,10 +67,20 @@
            </el-row>
            <el-row>
                <el-col :span="12">
                    <el-form-item label="设备项目" prop="machineryCategory">
                    <el-form-item label="保养项目" prop="machineryCategory">
                        <el-input
                            v-model.trim="form.machineryCategory"
                            placeholder="请输入设备项目"
                            placeholder="请输入保养项目"
                            maxlength="100"
                            clearable
                        />
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="保养人" prop="maintenancePerson">
                        <el-input
                            v-model.trim="form.maintenancePerson"
                            placeholder="请输入保养人姓名"
                            maxlength="100"
                            clearable
                        />
@@ -173,13 +183,14 @@
        week: '',
        time: '',
        deviceModel: undefined, // è§„格型号
        registrationDate: ''
        registrationDate: '',
        maintenancePerson: '' // ä¿å…»äºº
    },
    rules: {
        taskId: [{ required: true, message: "请选择设备", trigger: "change" },],
        inspector: [{ required: true, message: "请选择录入人", trigger: "blur" },],
        registrationDate: [{ required: true, message: "请选择登记时间", trigger: "change" }],
        machineryCategory: [{ required: true, message: "请输入设备项目", trigger: "blur" }]
        machineryCategory: [{ required: true, message: "请输入保养项目", trigger: "blur" }]
    }
})
const { form, rules } = toRefs(data)
@@ -259,7 +270,8 @@
        week: '',
        time: '',
        deviceModel: undefined,
        registrationDate: ''
        registrationDate: '',
        maintenancePerson: ''
    }
}
src/views/equipmentManagement/upkeep/index.vue
@@ -300,7 +300,7 @@
      prop: "deviceModel",
    },
    {
      label: "设备项目",
      label: "保养项目",
      prop: "machineryCategory",
      minWidth: 120,
      formatData: cell => cell || "--",
@@ -342,6 +342,7 @@
        );
      },
    },
    { prop: "maintenancePerson", label: "保养人", minWidth: 100 },
    { prop: "registrant", label: "登记人", minWidth: 100 },
    { prop: "registrationDate", label: "登记日期", minWidth: 100 },
    {
@@ -378,7 +379,7 @@
      prop: "createUserName",
    },
    {
      label: "设备项目",
      label: "保养项目",
      align: "center",
      prop: "machineryCategory",
      formatData: cell => cell || "--",
src/views/financialManagement/assets/fixedAssets.vue
@@ -38,11 +38,12 @@
        <div>
          <el-button type="primary" @click="add" icon="Plus">新增资产</el-button>
          <el-button type="warning" @click="handleDepreciation" icon="Money">折旧计提</el-button>
          <el-button @click="handleOut" icon="Download">导出</el-button>
          <!-- <el-button @click="handleOut" icon="Download">导出</el-button> -->
        </div>
      </div>
      <PIMTable
        rowKey="id"
        isSelection
        :column="columns"
        :tableData="dataList"
        :page="{
@@ -50,6 +51,7 @@
          size: pagination.pageSize,
          total: pagination.total,
        }"
        @selection-change="handleSelectionChange"
        @pagination="changePage"
      >
        <template #originalValue="{ row }">
@@ -227,12 +229,18 @@
];
const dataList = ref([]);
const multipleList = ref([]);
const dialogVisible = ref(false);
const dialogTitle = ref("");
const formRef = ref(null);
const isEdit = ref(false);
const isView = ref(false);
const currentId = ref(null);
const selectedIds = computed(() =>
  multipleList.value
    .map(item => item?.id)
    .filter(id => id !== undefined && id !== null && id !== "")
);
const createDefaultForm = () => ({
  assetCode: "",
@@ -322,10 +330,15 @@
      status: filters.status,
    });
    dataList.value = data?.records || [];
    multipleList.value = [];
    pagination.total = Number(data?.total || 0);
  } catch (error) {
    // æç¤ºç”±å…¨å±€è¯·æ±‚拦截器处理,这里仅防止未捕获异常
  }
};
const handleSelectionChange = (selectionList) => {
  multipleList.value = selectionList;
};
const resetFilters = () => {
@@ -388,12 +401,16 @@
};
const handleDepreciation = () => {
  ElMessageBox.confirm("确认进行本月折旧计提吗?", "提示", {
  const ids = selectedIds.value;
  const confirmText = ids.length
    ? `确认对选中的 ${ids.length} æ¡èµ„产进行本月折旧计提吗?`
    : "确认进行本月折旧计提吗?";
  ElMessageBox.confirm(confirmText, "提示", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "info",
  }).then(async () => {
    await depreciateFixedAsset({});
    await depreciateFixedAsset({ ids });
    ElMessage.success("折旧计提完成");
    await getTableData();
  });
src/views/financialManagement/assets/intangibleAssets.vue
@@ -39,11 +39,12 @@
        <div>
          <el-button type="primary" @click="add" icon="Plus">新增资产</el-button>
          <el-button type="warning" @click="handleAmortization" icon="Money">摊销计提</el-button>
          <el-button @click="handleOut" icon="Download">导出</el-button>
          <!-- <el-button @click="handleOut" icon="Download">导出</el-button> -->
        </div>
      </div>
      <PIMTable
        rowKey="id"
        isSelection
        :column="columns"
        :tableData="dataList"
        :page="{
@@ -51,6 +52,7 @@
          size: pagination.pageSize,
          total: pagination.total,
        }"
        @selection-change="handleSelectionChange"
        @pagination="changePage"
      >
        <template #originalValue="{ row }">
@@ -220,12 +222,18 @@
];
const dataList = ref([]);
const multipleList = ref([]);
const dialogVisible = ref(false);
const dialogTitle = ref("");
const formRef = ref(null);
const isEdit = ref(false);
const isView = ref(false);
const currentId = ref(null);
const selectedIds = computed(() =>
  multipleList.value
    .map(item => item?.id)
    .filter(id => id !== undefined && id !== null && id !== "")
);
const createDefaultForm = () => ({
  assetCode: "",
@@ -320,10 +328,15 @@
      status: filters.status,
    });
    dataList.value = data?.records || [];
    multipleList.value = [];
    pagination.total = Number(data?.total || 0);
  } catch (error) {
    // æç¤ºç”±å…¨å±€è¯·æ±‚拦截器处理,这里仅防止未捕获异常
  }
};
const handleSelectionChange = (selectionList) => {
  multipleList.value = selectionList;
};
const resetFilters = () => {
@@ -386,12 +399,16 @@
};
const handleAmortization = () => {
  ElMessageBox.confirm("确认进行本月摊销计提吗?", "提示", {
  const ids = selectedIds.value;
  const confirmText = ids.length
    ? `确认对选中的 ${ids.length} æ¡èµ„产进行本月摊销计提吗?`
    : "确认进行本月摊销计提吗?";
  ElMessageBox.confirm(confirmText, "提示", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "info",
  }).then(async () => {
    await amortizeIntangibleAsset({});
    await amortizeIntangibleAsset({ ids });
    ElMessage.success("摊销计提完成");
    await getTableData();
  });
src/views/financialManagement/generalLedger/index.vue
@@ -44,20 +44,62 @@
          <el-button type="primary"
                     @click="add"
                     icon="Plus">新增</el-button>
          <el-button @click="handleOut"
                     icon="Download">导出</el-button>
          <!-- <el-button @click="handleOut"
                     icon="Download">导出</el-button> -->
        </div>
      </div>
      <PIMTable rowKey="id"
                :column="columns"
                :tableData="dataList"
                :page="{
          current: pagination.currentPage,
          size: pagination.pageSize,
          total: pagination.total,
        }"
                @pagination="changePage">
      </PIMTable>
      <el-table ref="tableRef"
                v-loading="loading"
                :data="dataList"
                row-key="id"
                :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
                height="calc(100vh - 280px)"
                border
                stripe
                highlight-current-row
                class="subject-table">
        <el-table-column label="科目编码" prop="subjectCode" width="140">
          <template #default="scope">
            <span class="subject-code">{{ scope.row.subjectCode }}</span>
          </template>
        </el-table-column>
        <el-table-column label="科目名称" prop="subjectName" min-width="180">
          <template #default="scope">
            <span class="subject-name" :class="{ 'is-parent': scope.row.children?.length > 0 }">
              {{ scope.row.subjectName }}
            </span>
          </template>
        </el-table-column>
        <el-table-column label="科目类型" prop="subjectType" width="100" align="center">
          <template #default="scope">
            <el-tag size="small" :type="getSubjectTypeType(scope.row.subjectType)">
              {{ scope.row.subjectType }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="余额方向" prop="balanceDirection" width="100" align="center">
          <template #default="scope">
            <el-tag size="small" :type="scope.row.balanceDirection === '借方' ? 'primary' : 'danger'">
              {{ scope.row.balanceDirection }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="状态" prop="status" width="80" align="center">
          <template #default="scope">
            <el-tag size="small" :type="scope.row.status === 0 || scope.row.status === '0' ? 'success' : 'info'">
              {{ scope.row.status === 0 || scope.row.status === '0' ? '启用' : '禁用' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="备注" prop="remark" show-overflow-tooltip min-width="150" />
        <el-table-column label="操作" align="center" fixed="right" width="240">
          <template #default="scope">
            <el-button link type="primary" icon="Plus" @click="addChild(scope.row)">新增</el-button>
            <el-button link type="primary" icon="Edit" @click="edit(scope.row)">编辑</el-button>
            <el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <FormDialog :title="dialogTitle"
                v-model="dialogVisible"
@@ -131,7 +173,7 @@
</template>
<script setup>
  import { ref, reactive, onMounted, getCurrentInstance } from "vue";
  import { ref, reactive, onMounted, getCurrentInstance, nextTick } from "vue";
  import { ElMessage, ElMessageBox } from "element-plus";
  import FormDialog from "@/components/Dialog/FormDialog.vue";
  import {
@@ -237,7 +279,9 @@
  const dialogTitle = ref("");
  const parentSubjectLabel = ref("顶级科目");
  const formRef = ref(null);
  const tableRef = ref(null);
  const isEdit = ref(false);
  const loading = ref(false);
  const form = reactive({
    id: undefined,
@@ -270,14 +314,17 @@
  };
  const getTableData = () => {
    loading.value = true;
    const query = {
      current: pagination.currentPage,
      size: pagination.pageSize,
      ...filters,
    };
    listAccountSubject(query).then(response => {
      dataList.value = response.data.records;
      pagination.total = response.data.total;
      dataList.value = response.data.records || [];
      loading.value = false;
    }).catch(() => {
      loading.value = false;
    });
  };
@@ -423,4 +470,29 @@
    justify-content: space-between;
    margin-bottom: 15px;
  }
  .subject-table {
    border-radius: 8px;
    overflow: hidden;
    :deep(.el-table__row) {
      transition: background-color 0.3s;
    }
    :deep(.el-table__row:hover) {
      background-color: #f5f7fa;
    }
    .subject-code {
      color: #606266;
    }
    .subject-name {
      font-weight: 500;
      &.is-parent {
        color: #409eff;
      }
    }
  }
</style>
src/views/financialManagement/voucher/detailLedger.vue
@@ -1,79 +1,80 @@
<template>
  <div class="app-container">
    <el-form :model="filters" :inline="true">
      <el-form-item label="会计科目:">
        <el-cascader v-model="filters.subject" :options="subjectOptions" :props="{ label: 'name', value: 'code', checkStrictly: true }" placeholder="请选择会计科目" clearable style="width: 250px;" filterable />
      </el-form-item>
      <el-form-item label="辅助核算:">
        <el-select v-model="filters.auxiliary" placeholder="请选择辅助核算" clearable style="width: 180px;">
          <el-option label="客户" value="customer" />
          <el-option label="供应商" value="supplier" />
          <el-option label="部门" value="department" />
          <el-option label="员工" value="employee" />
          <el-option label="项目" value="project" />
        </el-select>
      </el-form-item>
      <el-form-item label="核算对象:">
        <el-select v-model="filters.auxiliaryItem" placeholder="请选择核算对象" clearable style="width: 200px;" :disabled="!filters.auxiliary">
          <el-option v-for="item in auxiliaryItems" :key="item.value" :label="item.label" :value="item.value" />
        </el-select>
      </el-form-item>
      <el-form-item label="期间:">
        <el-date-picker v-model="filters.startMonth" type="month" placeholder="开始月份" value-format="YYYY-MM" style="width: 140px;" />
        <span style="margin: 0 10px;">至</span>
        <el-date-picker v-model="filters.endMonth" type="month" placeholder="结束月份" value-format="YYYY-MM" style="width: 140px;" />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="getTableData">查询</el-button>
        <el-button @click="resetFilters">重置</el-button>
        <el-button @click="handlePrint" icon="Printer">打印</el-button>
        <el-button @click="handleOut" icon="Download">导出</el-button>
      </el-form-item>
    </el-form>
  <div class="app-container ledger-page">
    <div class="ledger-layout">
      <aside class="subject-panel">
        <el-input v-model="subjectKeyword" placeholder="请输入科目名称/编号" clearable prefix-icon="Search" />
        <el-scrollbar class="subject-tree-scroll">
          <el-tree
            ref="subjectTreeRef"
            :data="subjectOptions"
            node-key="code"
            :props="{ label: 'name', children: 'children' }"
            highlight-current
            default-expand-all
            :expand-on-click-node="false"
            :filter-node-method="filterSubjectNode"
            @node-click="handleSubjectClick"
          >
            <template #default="{ data }">
              <span class="subject-node">{{ data.code }} {{ data.name }}</span>
            </template>
          </el-tree>
        </el-scrollbar>
      </aside>
    <div class="ledger-header" v-if="currentSubject">
      <h2>科目明细账</h2>
      <p>科目: {{ currentSubject.code }} {{ currentSubject.name }}</p>
      <p v-if="filters.auxiliary && filters.auxiliaryItem">辅助核算: {{ getAuxiliaryLabel() }}</p>
      <p>期间: {{ filters.startMonth }} è‡³ {{ filters.endMonth }}</p>
      <section class="ledger-content">
        <el-form :model="filters" :inline="true" class="filter-form">
          <el-form-item label="期间:">
            <el-date-picker v-model="filters.startMonth" type="month" placeholder="开始月份" value-format="YYYY-MM" style="width: 140px;" />
            <span style="margin: 0 10px;">至</span>
            <el-date-picker v-model="filters.endMonth" type="month" placeholder="结束月份" value-format="YYYY-MM" style="width: 140px;" />
          </el-form-item>
          <el-form-item>
            <el-button type="primary" @click="getTableData">查询</el-button>
            <el-button @click="resetFilters">重置</el-button>
<!--            <el-button @click="handlePrint" icon="Printer">打印</el-button>-->
            <el-button @click="handleOut" icon="Download">导出</el-button>
          </el-form-item>
        </el-form>
        <div class="table_list">
          <el-table :data="dataList" border style="width: 100%">
            <el-table-column prop="date" label="日期" width="120" />
            <el-table-column prop="voucherNo" label="凭证字号" width="120" />
            <el-table-column prop="summary" label="摘要" min-width="200" show-overflow-tooltip />
            <el-table-column prop="debit" label="借方" width="150">
              <template #default="{ row }">
                <span v-if="row.debit > 0" class="text-danger">Â¥{{ formatMoney(row.debit) }}</span>
                <span v-else>-</span>
              </template>
            </el-table-column>
            <el-table-column prop="credit" label="贷方" width="150">
              <template #default="{ row }">
                <span v-if="row.credit > 0" class="text-success">Â¥{{ formatMoney(row.credit) }}</span>
                <span v-else>-</span>
              </template>
            </el-table-column>
            <el-table-column label="方向" width="80">
              <template #default="{ row }">
                <el-tag :type="row.direction === '借' ? 'success' : 'danger'" size="small">{{ row.direction }}</el-tag>
              </template>
            </el-table-column>
            <el-table-column label="余额" width="150">
              <template #default="{ row }">
                <span :class="row.balance >= 0 ? 'text-primary' : 'text-warning'">Â¥{{ formatMoney(Math.abs(row.balance)) }}</span>
              </template>
            </el-table-column>
          </el-table>
        </div>
        <el-empty v-if="!currentSubject" description="请选择会计科目查询" style="margin-top: 50px;" />
      </section>
    </div>
    <div class="table_list">
      <el-table :data="dataList" border style="width: 100%" show-summary :summary-method="getSummaries">
        <el-table-column prop="date" label="日期" width="120" />
        <el-table-column prop="voucherNo" label="凭证字号" width="120" />
        <el-table-column prop="summary" label="摘要" min-width="200" show-overflow-tooltip />
        <el-table-column prop="debit" label="借方" width="150">
          <template #default="{ row }">
            <span v-if="row.debit > 0" class="text-danger">Â¥{{ formatMoney(row.debit) }}</span>
            <span v-else>-</span>
          </template>
        </el-table-column>
        <el-table-column prop="credit" label="贷方" width="150">
          <template #default="{ row }">
            <span v-if="row.credit > 0" class="text-success">Â¥{{ formatMoney(row.credit) }}</span>
            <span v-else>-</span>
          </template>
        </el-table-column>
        <el-table-column label="方向" width="80">
          <template #default="{ row }">
            <el-tag :type="row.direction === '借' ? 'success' : 'danger'" size="small">{{ row.direction }}</el-tag>
          </template>
        </el-table-column>
        <el-table-column label="余额" width="150">
          <template #default="{ row }">
            <span :class="row.balance >= 0 ? 'text-primary' : 'text-warning'">Â¥{{ formatMoney(Math.abs(row.balance)) }}</span>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <el-empty v-if="!currentSubject" description="请选择会计科目查询" style="margin-top: 50px;" />
  </div>
</template>
<script setup>
import { ref, reactive, onMounted, computed, watch } from "vue";
import { ref, reactive, onMounted, computed, watch, nextTick } from "vue";
import { ElMessage } from "element-plus";
import { listAccountSubject } from "@/api/financialManagement/accountSubject";
import { getDetailLedger } from "@/api/financialManagement/ledger";
@@ -83,15 +84,28 @@
});
const filters = reactive({
  subject: [],
  auxiliary: "",
  auxiliaryItem: "",
  subject: "",
  startMonth: "",
  endMonth: "",
});
const dataList = ref([]);
const subjectOptions = ref([]);
const subjectKeyword = ref("");
const subjectTreeRef = ref();
const getPreviousMonth = () => {
  const date = new Date();
  date.setDate(1);
  date.setMonth(date.getMonth() - 1);
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, "0");
  return `${year}-${month}`;
};
const defaultMonth = getPreviousMonth();
filters.startMonth = defaultMonth;
filters.endMonth = defaultMonth;
const fallbackSubjects = [
  { code: "1122", name: "应收账款" },
@@ -99,79 +113,14 @@
  { code: "6602", name: "管理费用" },
];
const toCascaderTree = (nodes = []) =>
const toTree = (nodes = []) =>
  nodes
    .filter(item => item.subjectCode && item.subjectName)
    .map(item => ({
      code: item.subjectCode,
      name: item.subjectName,
      children: toCascaderTree(item.children || []),
      children: toTree(item.children || []),
    }));
const loadSubjectOptions = async () => {
  try {
    const { data } = await listAccountSubject({
      current: 1,
      size: 1000,
    });
    const options = toCascaderTree(data?.records || []);
    if (options.length > 0) {
      subjectOptions.value = options;
      return;
    }
  } catch (error) {
    // å…¨å±€æ‹¦æˆªå™¨å·²æç¤ºï¼Œä¸‹é¢èµ°å…œåº•ç§‘ç›®
  }
  subjectOptions.value = fallbackSubjects.map(item => ({ ...item, children: [] }));
};
const auxiliaryItems = computed(() => {
  const map = {
    customer: [
      { value: "1", label: "北京科技有限公司" },
      { value: "2", label: "上海贸易公司" },
      { value: "3", label: "广州实业有限公司" },
    ],
    supplier: [
      { value: "1", label: "北京原材料供应商" },
      { value: "2", label: "上海电子元器件公司" },
      { value: "3", label: "广州包装材料厂" },
    ],
    department: [
      { value: "1", label: "财务部" },
      { value: "2", label: "销售部" },
      { value: "3", label: "采购部" },
    ],
    employee: [
      { value: "1", label: "张三" },
      { value: "2", label: "李四" },
      { value: "3", label: "王五" },
    ],
    project: [
      { value: "1", label: "项目A" },
      { value: "2", label: "项目B" },
      { value: "3", label: "项目C" },
    ],
  };
  return map[filters.auxiliary] || [];
});
watch(() => filters.auxiliary, () => {
  filters.auxiliaryItem = "";
});
const currentSubject = computed(() => {
  const code = getSelectedSubjectCode(filters.subject);
  if (!code) return null;
  return findSubject(subjectOptions.value, code);
});
const getSelectedSubjectCode = (subjectValue) => {
  if (Array.isArray(subjectValue)) {
    return subjectValue.length ? subjectValue[subjectValue.length - 1] : "";
  }
  return subjectValue || "";
};
const findSubject = (options, code) => {
  for (const item of options) {
@@ -184,9 +133,68 @@
  return null;
};
const getAuxiliaryLabel = () => {
  const item = auxiliaryItems.value.find(i => i.value === filters.auxiliaryItem);
  return item ? item.label : "";
const currentSubject = computed(() => {
  if (!filters.subject) return null;
  return findSubject(subjectOptions.value, filters.subject);
});
const getFirstSubjectCode = (nodes = []) => {
  for (const item of nodes) {
    if (item.code) return item.code;
    if (item.children && item.children.length > 0) {
      const childCode = getFirstSubjectCode(item.children);
      if (childCode) return childCode;
    }
  }
  return "";
};
const setDefaultSubjectSelection = async () => {
  const firstCode = getFirstSubjectCode(subjectOptions.value);
  if (!firstCode) {
    filters.subject = "";
    subjectTreeRef.value?.setCurrentKey(null);
    return;
  }
  filters.subject = firstCode;
  await nextTick();
  subjectTreeRef.value?.setCurrentKey(firstCode);
};
const filterSubjectNode = (value, data) => {
  const keyword = value?.trim();
  if (!keyword) return true;
  return `${data.code}${data.name}`.includes(keyword);
};
watch(subjectKeyword, (value) => {
  subjectTreeRef.value?.filter(value || "");
});
const handleSubjectClick = async (data) => {
  filters.subject = data.code;
  await getTableData();
};
const loadSubjectOptions = async () => {
  let options = [];
  try {
    const { data } = await listAccountSubject({
      current: 1,
      size: 1000,
    });
    options = toTree(data?.records || []);
  } catch (error) {
    // å…¨å±€æ‹¦æˆªå™¨å·²æç¤ºï¼Œä¸‹é¢èµ°å…œåº•ç§‘ç›®
  }
  if (options.length === 0) {
    options = fallbackSubjects.map(item => ({ ...item, children: [] }));
  }
  subjectOptions.value = options;
  await setDefaultSubjectSelection();
  if (filters.subject) {
    await getTableData();
  }
};
const formatMoney = (value) => {
@@ -194,7 +202,7 @@
  return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};
// è”调约定:明细账接口可按辅助核算过滤(auxiliaryType/auxiliaryId)
// è”调约定:明细账按科目与期间过滤
const getTableData = async () => {
  if (!currentSubject.value) {
    dataList.value = [];
@@ -203,8 +211,6 @@
  try {
    const { data } = await getDetailLedger({
      subjectCode: currentSubject.value.code,
      auxiliaryType: filters.auxiliary,
      auxiliaryId: filters.auxiliaryItem,
      startMonth: filters.startMonth,
      endMonth: filters.endMonth,
    });
@@ -214,36 +220,16 @@
  }
};
const resetFilters = () => {
  filters.subject = [];
  filters.auxiliary = "";
  filters.auxiliaryItem = "";
  filters.startMonth = "2024-01";
  filters.endMonth = "2024-03";
const resetFilters = async () => {
  filters.startMonth = defaultMonth;
  filters.endMonth = defaultMonth;
  dataList.value = [];
};
const getSummaries = (param) => {
  const { columns, data } = param;
  const sums = [];
  columns.forEach((column, index) => {
    if (index === 0) {
      sums[index] = "合计";
      return;
    }
    if (column.property === "debit") {
      const values = data.map(item => Number(item.debit));
      const sum = values.reduce((prev, curr) => prev + curr, 0);
      sums[index] = "Â¥" + formatMoney(sum);
    } else if (column.property === "credit") {
      const values = data.map(item => Number(item.credit));
      const sum = values.reduce((prev, curr) => prev + curr, 0);
      sums[index] = "Â¥" + formatMoney(sum);
    } else {
      sums[index] = "";
    }
  });
  return sums;
  subjectKeyword.value = "";
  subjectTreeRef.value?.filter("");
  await setDefaultSubjectSelection();
  if (filters.subject) {
    await getTableData();
  }
};
const handlePrint = () => {
@@ -260,16 +246,37 @@
</script>
<style lang="scss" scoped>
.ledger-header {
  text-align: center;
  margin-bottom: 20px;
  h2 {
    margin: 0 0 10px 0;
  }
  p {
    color: #606266;
    margin: 5px 0;
  }
.ledger-layout {
  display: flex;
  gap: 16px;
}
.subject-panel {
  width: 260px;
  flex-shrink: 0;
  padding: 12px;
  border: 1px solid #e4e7ed;
  border-radius: 8px;
  background-color: #fff;
}
.subject-tree-scroll {
  height: 600px;
  margin-top: 12px;
}
.subject-node {
  display: inline-flex;
  align-items: center;
}
.ledger-content {
  flex: 1;
  min-width: 0;
}
.filter-form {
  margin-bottom: 12px;
}
.text-primary {
@@ -291,4 +298,12 @@
  color: #e6a23c;
  font-weight: bold;
}
.subject-panel :deep(.el-tree-node__content) {
  height: 34px;
}
.subject-panel :deep(.el-tree-node.is-current > .el-tree-node__content) {
  background-color: #f0f7ff;
}
</style>
src/views/financialManagement/voucher/generalLedger.vue
@@ -1,64 +1,80 @@
<template>
  <div class="app-container">
    <el-form :model="filters" :inline="true">
      <el-form-item label="会计科目:">
        <el-cascader v-model="filters.subject" :options="subjectOptions" :props="{ label: 'name', value: 'code', checkStrictly: true }" placeholder="请选择会计科目" clearable style="width: 250px;" filterable />
      </el-form-item>
      <el-form-item label="期间:">
        <el-date-picker v-model="filters.startMonth" type="month" placeholder="开始月份" value-format="YYYY-MM" style="width: 140px;" />
        <span style="margin: 0 10px;">至</span>
        <el-date-picker v-model="filters.endMonth" type="month" placeholder="结束月份" value-format="YYYY-MM" style="width: 140px;" />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="getTableData">查询</el-button>
        <el-button @click="resetFilters">重置</el-button>
        <el-button @click="handlePrint" icon="Printer">打印</el-button>
        <el-button @click="handleOut" icon="Download">导出</el-button>
      </el-form-item>
    </el-form>
  <div class="app-container ledger-page">
    <div class="ledger-layout">
      <aside class="subject-panel">
        <el-input v-model="subjectKeyword" placeholder="请输入科目名称/编号" clearable prefix-icon="Search" />
        <el-scrollbar class="subject-tree-scroll">
          <el-tree
            ref="subjectTreeRef"
            :data="subjectOptions"
            node-key="code"
            :props="{ label: 'name', children: 'children' }"
            highlight-current
            default-expand-all
            :expand-on-click-node="false"
            :filter-node-method="filterSubjectNode"
            @node-click="handleSubjectClick"
          >
            <template #default="{ data }">
              <span class="subject-node">{{ data.code }} {{ data.name }}</span>
            </template>
          </el-tree>
        </el-scrollbar>
      </aside>
    <div class="ledger-header" v-if="currentSubject">
      <h2>科目总账</h2>
      <p>科目: {{ currentSubject.code }} {{ currentSubject.name }}</p>
      <p>期间: {{ filters.startMonth }} è‡³ {{ filters.endMonth }}</p>
      <section class="ledger-content">
        <el-form :model="filters" :inline="true" class="filter-form">
          <el-form-item label="期间:">
            <el-date-picker v-model="filters.startMonth" type="month" placeholder="开始月份" value-format="YYYY-MM" style="width: 140px;" />
            <span style="margin: 0 10px;">至</span>
            <el-date-picker v-model="filters.endMonth" type="month" placeholder="结束月份" value-format="YYYY-MM" style="width: 140px;" />
          </el-form-item>
          <el-form-item>
            <el-button type="primary" @click="getTableData">查询</el-button>
            <el-button @click="resetFilters">重置</el-button>
<!--            <el-button @click="handlePrint" icon="Printer">打印</el-button>-->
            <!-- <el-button @click="handleOut" icon="Download">导出</el-button> -->
          </el-form-item>
        </el-form>
        <div class="table_list">
          <el-table :data="dataList" border style="width: 100%">
            <el-table-column prop="date" label="日期"/>
            <!-- <el-table-column prop="voucherNo" label="凭证字号" width="120" /> -->
            <!-- <el-table-column prop="summary" label="摘要" min-width="200" show-overflow-tooltip /> -->
            <el-table-column prop="debit" label="借方">
              <template #default="{ row }">
                <span v-if="row.debit > 0" class="text-danger">Â¥{{ formatMoney(row.debit) }}</span>
                <span v-else>-</span>
              </template>
            </el-table-column>
            <el-table-column prop="credit" label="贷方">
              <template #default="{ row }">
                <span v-if="row.credit > 0" class="text-success">Â¥{{ formatMoney(row.credit) }}</span>
                <span v-else>-</span>
              </template>
            </el-table-column>
            <el-table-column label="方向">
              <template #default="{ row }">
                <el-tag :type="row.direction === '借' ? 'success' : 'danger'" size="small">{{ row.direction }}</el-tag>
              </template>
            </el-table-column>
            <el-table-column label="余额">
              <template #default="{ row }">
                <span :class="row.balance >= 0 ? 'text-primary' : 'text-warning'">Â¥{{ formatMoney(Math.abs(row.balance)) }}</span>
              </template>
            </el-table-column>
          </el-table>
        </div>
        <el-empty v-if="!currentSubject" description="请选择会计科目查询" style="margin-top: 50px;" />
      </section>
    </div>
    <div class="table_list">
      <el-table :data="dataList" border style="width: 100%" show-summary :summary-method="getSummaries">
        <el-table-column prop="date" label="日期" width="120" />
        <el-table-column prop="voucherNo" label="凭证字号" width="120" />
        <el-table-column prop="summary" label="摘要" min-width="200" show-overflow-tooltip />
        <el-table-column prop="debit" label="借方" width="150">
          <template #default="{ row }">
            <span v-if="row.debit > 0" class="text-danger">Â¥{{ formatMoney(row.debit) }}</span>
            <span v-else>-</span>
          </template>
        </el-table-column>
        <el-table-column prop="credit" label="贷方" width="150">
          <template #default="{ row }">
            <span v-if="row.credit > 0" class="text-success">Â¥{{ formatMoney(row.credit) }}</span>
            <span v-else>-</span>
          </template>
        </el-table-column>
        <el-table-column label="方向" width="80">
          <template #default="{ row }">
            <el-tag :type="row.direction === '借' ? 'success' : 'danger'" size="small">{{ row.direction }}</el-tag>
          </template>
        </el-table-column>
        <el-table-column label="余额" width="150">
          <template #default="{ row }">
            <span :class="row.balance >= 0 ? 'text-primary' : 'text-warning'">Â¥{{ formatMoney(Math.abs(row.balance)) }}</span>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <el-empty v-if="!currentSubject" description="请选择会计科目查询" style="margin-top: 50px;" />
  </div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from "vue";
import { ref, reactive, onMounted, computed, watch, nextTick } from "vue";
import { ElMessage } from "element-plus";
import { listAccountSubject } from "@/api/financialManagement/accountSubject";
import { getGeneralLedger } from "@/api/financialManagement/ledger";
@@ -68,13 +84,28 @@
});
const filters = reactive({
  subject: [],
  subject: "",
  startMonth: "",
  endMonth: "",
});
const dataList = ref([]);
const subjectOptions = ref([]);
const subjectKeyword = ref("");
const subjectTreeRef = ref();
const getPreviousMonth = () => {
  const date = new Date();
  date.setDate(1);
  date.setMonth(date.getMonth() - 1);
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, "0");
  return `${year}-${month}`;
};
const defaultMonth = getPreviousMonth();
filters.startMonth = defaultMonth;
filters.endMonth = defaultMonth;
const fallbackSubjects = [
  { code: "1001", name: "库存现金" },
@@ -84,45 +115,14 @@
  { code: "6001", name: "主营业务收入" },
];
const toCascaderTree = (nodes = []) =>
const toTree = (nodes = []) =>
  nodes
    .filter(item => item.subjectCode && item.subjectName)
    .map(item => ({
      code: item.subjectCode,
      name: item.subjectName,
      children: toCascaderTree(item.children || []),
      children: toTree(item.children || []),
    }));
const loadSubjectOptions = async () => {
  try {
    const { data } = await listAccountSubject({
      current: 1,
      size: 1000,
      status: 0,
    });
    const options = toCascaderTree(data?.records || []);
    if (options.length > 0) {
      subjectOptions.value = options;
      return;
    }
  } catch (error) {
    // å…¨å±€æ‹¦æˆªå™¨å·²æç¤ºï¼Œä¸‹é¢èµ°å…œåº•ç§‘ç›®
  }
  subjectOptions.value = fallbackSubjects.map(item => ({ ...item, children: [] }));
};
const currentSubject = computed(() => {
  const code = getSelectedSubjectCode(filters.subject);
  if (!code) return null;
  return findSubject(subjectOptions.value, code);
});
const getSelectedSubjectCode = (subjectValue) => {
  if (Array.isArray(subjectValue)) {
    return subjectValue.length ? subjectValue[subjectValue.length - 1] : "";
  }
  return subjectValue || "";
};
const findSubject = (options, code) => {
  for (const item of options) {
@@ -133,6 +133,71 @@
    }
  }
  return null;
};
const currentSubject = computed(() => {
  if (!filters.subject) return null;
  return findSubject(subjectOptions.value, filters.subject);
});
const getFirstSubjectCode = (nodes = []) => {
  for (const item of nodes) {
    if (item.code) return item.code;
    if (item.children && item.children.length > 0) {
      const childCode = getFirstSubjectCode(item.children);
      if (childCode) return childCode;
    }
  }
  return "";
};
const setDefaultSubjectSelection = async () => {
  const firstCode = getFirstSubjectCode(subjectOptions.value);
  if (!firstCode) {
    filters.subject = "";
    subjectTreeRef.value?.setCurrentKey(null);
    return;
  }
  filters.subject = firstCode;
  await nextTick();
  subjectTreeRef.value?.setCurrentKey(firstCode);
};
const filterSubjectNode = (value, data) => {
  const keyword = value?.trim();
  if (!keyword) return true;
  return `${data.code}${data.name}`.includes(keyword);
};
watch(subjectKeyword, (value) => {
  subjectTreeRef.value?.filter(value || "");
});
const handleSubjectClick = async (data) => {
  filters.subject = data.code;
  await getTableData();
};
const loadSubjectOptions = async () => {
  let options = [];
  try {
    const { data } = await listAccountSubject({
      current: 1,
      size: 1000,
      status: 0,
    });
    options = toTree(data?.records || []);
  } catch (error) {
    // å…¨å±€æ‹¦æˆªå™¨å·²æç¤ºï¼Œä¸‹é¢èµ°å…œåº•ç§‘ç›®
  }
  if (options.length === 0) {
    options = fallbackSubjects.map(item => ({ ...item, children: [] }));
  }
  subjectOptions.value = options;
  await setDefaultSubjectSelection();
  if (filters.subject) {
    await getTableData();
  }
};
const formatMoney = (value) => {
@@ -158,34 +223,16 @@
  }
};
const resetFilters = () => {
  filters.subject = [];
  filters.startMonth = "2024-01";
  filters.endMonth = "2024-03";
const resetFilters = async () => {
  filters.startMonth = defaultMonth;
  filters.endMonth = defaultMonth;
  dataList.value = [];
};
const getSummaries = (param) => {
  const { columns, data } = param;
  const sums = [];
  columns.forEach((column, index) => {
    if (index === 0) {
      sums[index] = "合计";
      return;
    }
    if (column.property === "debit") {
      const values = data.map(item => Number(item.debit));
      const sum = values.reduce((prev, curr) => prev + curr, 0);
      sums[index] = "Â¥" + formatMoney(sum);
    } else if (column.property === "credit") {
      const values = data.map(item => Number(item.credit));
      const sum = values.reduce((prev, curr) => prev + curr, 0);
      sums[index] = "Â¥" + formatMoney(sum);
    } else {
      sums[index] = "";
    }
  });
  return sums;
  subjectKeyword.value = "";
  subjectTreeRef.value?.filter("");
  await setDefaultSubjectSelection();
  if (filters.subject) {
    await getTableData();
  }
};
const handlePrint = () => {
@@ -202,16 +249,37 @@
</script>
<style lang="scss" scoped>
.ledger-header {
  text-align: center;
  margin-bottom: 20px;
  h2 {
    margin: 0 0 10px 0;
  }
  p {
    color: #606266;
    margin: 5px 0;
  }
.ledger-layout {
  display: flex;
  gap: 16px;
}
.subject-panel {
  width: 260px;
  flex-shrink: 0;
  padding: 12px;
  border: 1px solid #e4e7ed;
  border-radius: 8px;
  background-color: #fff;
}
.subject-tree-scroll {
  height: 600px;
  margin-top: 12px;
}
.subject-node {
  display: inline-flex;
  align-items: center;
}
.ledger-content {
  flex: 1;
  min-width: 0;
}
.filter-form {
  margin-bottom: 12px;
}
.text-primary {
@@ -233,4 +301,12 @@
  color: #e6a23c;
  font-weight: bold;
}
.subject-panel :deep(.el-tree-node__content) {
  height: 34px;
}
.subject-panel :deep(.el-tree-node.is-current > .el-tree-node__content) {
  background-color: #f0f7ff;
}
</style>
src/views/financialManagement/voucher/index.vue
@@ -32,13 +32,13 @@
    <div class="table_list">
      <div class="actions">
        <div>
          <el-statistic title="借方合计" :value="totalDebit" precision="2" prefix="Â¥" />
          <el-statistic title="贷方合计" :value="totalCredit" precision="2" prefix="Â¥" style="margin-left: 30px;" />
          <el-statistic title="借方合计" :value="totalDebit" :precision="2" prefix="Â¥" />
          <el-statistic title="贷方合计" :value="totalCredit" :precision="2" prefix="Â¥" style="margin-left: 30px;" />
        </div>
        <div>
          <el-button type="primary" @click="add" icon="Plus">新增凭证</el-button>
          <el-button @click="handleImport" icon="Upload">导入</el-button>
          <el-button @click="handleOut" icon="Download">导出</el-button>
          <!-- <el-button @click="handleImport" icon="Upload">导入</el-button> -->
          <!-- <el-button @click="handleOut" icon="Download">导出</el-button> -->
        </div>
      </div>
      <PIMTable
@@ -84,6 +84,11 @@
              <span class="label">凭证字:</span>
              <el-select v-model="form.voucherPrefix" :disabled="isViewMode" style="width: 70px;">
                <el-option label="è®°" value="è®°" />
                <el-option label="现" value="现" />
                <el-option label="银" value="银" />
                <el-option label="转" value="转" />
                <el-option label="收" value="收" />
                <el-option label="付" value="付" />
              </el-select>
              <el-input v-model="form.voucherNum" :disabled="isViewMode" style="width: 60px;" />
              <span class="label" style="margin-left: 5px;">号</span>
@@ -96,7 +101,6 @@
              <span class="label">附件:</span>
              <el-input-number v-model="form.attachmentCount" :disabled="isViewMode" :min="0" :controls="false" style="width: 60px;" />
              <span class="label" style="margin-left: 5px;">å¼ </span>
              <el-button type="primary" link :disabled="isViewMode" style="margin-left: 10px;">上传文件</el-button>
            </div>
          </div>
          <div class="voucher-table">
@@ -153,12 +157,12 @@
                      @change="(val) => handleSubjectChange(val, rowIndex)"
                      @focus="selectRow(rowIndex)"
                    />
                    <div class="subject-name">{{ entry.subjectName }}</div>
                    <!-- <div class="subject-name">{{ entry.subjectName }}</div> -->
                  </td>
                  <!-- å€Ÿæ–¹11列 -->
                  <template v-if="editingCell.row === rowIndex && editingCell.type === 'debit'">
                    <td colspan="11" class="debit-input-cell">
                      <el-input-number ref="amountInputRef" v-model="entry.debit" :disabled="isViewMode" :min="0" :precision="2" :controls="false" size="small" @blur="finishEdit" class="full-width-input" />
                      <el-input-number ref="amountInputRef" v-model="entry.debit" :disabled="isViewMode" :min="0" :precision="2" :controls="false" :value-on-clear="undefined" size="small" @blur="finishEdit" class="full-width-input" />
                    </td>
                  </template>
                  <template v-else>
@@ -169,7 +173,7 @@
                  <!-- è´·æ–¹11列 -->
                  <template v-if="editingCell.row === rowIndex && editingCell.type === 'credit'">
                    <td colspan="11" class="credit-input-cell">
                      <el-input-number ref="amountInputRef" v-model="entry.credit" :disabled="isViewMode" :min="0" :precision="2" :controls="false" size="small" @blur="finishEdit" class="full-width-input" />
                      <el-input-number ref="amountInputRef" v-model="entry.credit" :disabled="isViewMode" :min="0" :precision="2" :controls="false" :value-on-clear="undefined" size="small" @blur="finishEdit" class="full-width-input" />
                    </td>
                  </template>
                  <template v-else>
@@ -217,7 +221,36 @@
              </el-select>
            </div>
          </div>
          <!-- ç¼–辑模式:使用 AttachmentUploadFile ä¸Šä¼ ç»„ä»¶ -->
          <div class="voucher-attachment-upload" v-if="!isViewMode">
            <div class="attachment-label">附件上传:</div>
            <AttachmentUploadFile
              v-model:fileList="form.attachments"
              :disabled="isViewMode"
              :limit="10"
              :fileSize="50"
              buttonText="点击上传附件"
              @change="handleAttachmentChange"
            />
          </div>
        </el-form>
        <!-- æŸ¥çœ‹æ¨¡å¼ï¼šå±•示附件列表(放在 el-form å¤–面,避免被 disabled) -->
        <div class="voucher-attachment-upload" v-if="isViewMode && form.attachments?.length">
          <div class="attachment-label">附件列表:</div>
          <el-table :data="form.attachments" border class="attachment-table">
            <el-table-column label="附件名称" show-overflow-tooltip>
              <template #default="scope">
                {{ scope.row.originalFilename || scope.row.name || scope.row.fileName || '未命名文件' }}
              </template>
            </el-table-column>
            <el-table-column fixed="right" label="操作" width="150" align="center">
              <template #default="scope">
                <el-button link type="primary" size="small" @click="previewFile(scope.row)">预览</el-button>
                <el-button link type="primary" size="small" @click="downloadFile(scope.row)">下载</el-button>
              </template>
            </el-table-column>
          </el-table>
        </div>
      </div>
      <template #footer>
        <div>
@@ -226,6 +259,8 @@
        </div>
      </template>
    </FormDialog>
    <!-- æ–‡ä»¶é¢„览组件 -->
    <FilePreview ref="filePreviewRef" />
  </div>
</template>
@@ -233,6 +268,10 @@
import { ref, reactive, onMounted, computed, nextTick } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import FormDialog from "@/components/Dialog/FormDialog.vue";
import AttachmentUploadFile from "@/components/AttachmentUpload/file/index.vue";
import FileList from "@/components/Dialog/FileList.vue";
import FilePreview from "@/components/filePreview/index.vue";
import download from "@/plugins/download.js";
import useUserStore from "@/store/modules/user";
import { userListNoPageByTenantId } from "@/api/system/user";
import { listAccountSubject } from "@/api/financialManagement/accountSubject";
@@ -284,6 +323,7 @@
const isEdit = ref(false);
const currentId = ref(null);
const isViewMode = computed(() => dialogMode.value === "view");
const filePreviewRef = ref(null);
const fallbackSubjectTree = [
  { subjectCode: "1001", subjectName: "库存现金", balanceDirection: "借方", children: [] },
@@ -326,8 +366,8 @@
  subjectName: "",
  balanceDirection: "",
  summary: "",
  debit: 0,
  credit: 0,
  debit: undefined,
  credit: undefined,
});
const createDefaultForm = () => ({
@@ -336,6 +376,7 @@
  voucherNum: "",
  voucherDate: "",
  attachmentCount: 0,
  attachments: [],
  entries: [createEmptyEntry(), createEmptyEntry()],
  creator: getDefaultCreator(),
  remark: "",
@@ -490,6 +531,31 @@
  form.entries.push(createEmptyEntry());
};
const handleAttachmentChange = (fileList) => {
  form.attachmentCount = fileList?.length || 0;
};
// ä½¿ç”¨é¡¹ç›®å°è£…çš„ filePreview ç»„件预览文件
const previewFile = (row) => {
  const url = row.previewURL || row.previewUrl || row.url;
  if (url && filePreviewRef.value) {
    filePreviewRef.value.open(url);
  } else {
    ElMessage.warning('文件地址无效,无法预览');
  }
};
// ä½¿ç”¨é¡¹ç›®å°è£…çš„ download æ’件下载文件
const downloadFile = (row) => {
  const url = row.downloadURL || row.downloadUrl || row.url;
  if (url) {
    const filename = row.originalFilename || row.name || row.fileName || 'download';
    download.byUrl(url, filename);
  } else {
    ElMessage.warning('文件地址无效,无法下载');
  }
};
const selectRow = (index) => {
  selectedRowIndex.value = index;
};
@@ -589,10 +655,13 @@
    const { data } = await getVoucherDetail(row.id);
    const detail = data || row;
    const parts = (detail.voucherNo || "").split("-");
    Object.assign(form, createDefaultForm(), detail, {
    const attachments = detail.storageBlobVOList || detail.storageBlobDTOs || detail.attachments || [];
    Object.assign(form, createDefaultForm(), {
      ...detail,
      voucherPrefix: parts[0] || "è®°",
      voucherNum: parts[1] || "",
      creator: detail.creator || getDefaultCreator(),
      attachments,
      entries:
        detail.entries?.map(item => ({
          subjectCode: item.subjectCode || "",
@@ -696,6 +765,7 @@
        remark: form.remark,
        debit: totalDebitEntry.value,
        credit: totalCreditEntry.value,
        storageBlobDTOs: form.attachments || [],
        entries: validEntries.map(entry => ({
          subjectCode: entry.subjectCode,
          subjectName: entry.subjectName,
@@ -801,6 +871,21 @@
  }
}
.voucher-attachment-upload {
  margin-top: 15px;
  padding: 0 10px;
  .attachment-label {
    font-size: 14px;
    color: #606266;
    margin-bottom: 10px;
  }
  .attachment-table {
    border-radius: 4px;
  }
}
.voucher-table {
  border: 1px solid #dcdfe6;
  border-right: none;
src/views/inventoryManagement/stockManagement/BatchNoQtyDetail.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,227 @@
<template>
  <el-dialog
    v-model="isShow"
    title="库存详情"
    width="90%"
    top="3vh"
    class="batch-no-qty-detail-dialog"
    @close="closeModal"
  >
    <div class="detail-content">
      <div class="detail-table-wrapper">
        <el-table
          :data="tableData"
          border
          v-loading="tableLoading"
          style="width: 100%"
          height="100%"
        >
          <el-table-column
            label="产品名称"
            prop="productName"
            show-overflow-tooltip
          />
          <el-table-column label="规格型号" prop="model" show-overflow-tooltip />
          <el-table-column label="单位" prop="unit" show-overflow-tooltip />
          <el-table-column label="批号" prop="batchNo" show-overflow-tooltip />
          <el-table-column
            label="合格库存数量"
            prop="qualifiedQuantity"
            show-overflow-tooltip
          />
          <el-table-column
            label="不合格库存数量"
            prop="unQualifiedQuantity"
            show-overflow-tooltip
          />
          <el-table-column
            label="合格冻结数量"
            prop="qualifiedLockedQuantity"
            show-overflow-tooltip
          />
          <el-table-column
            label="不合格冻结数量"
            prop="unQualifiedLockedQuantity"
            show-overflow-tooltip
          />
          <el-table-column
            label="库存预警数量"
            prop="warnNum"
            show-overflow-tooltip
          />
          <el-table-column label="备注" prop="remark" show-overflow-tooltip />
          <el-table-column
            label="最近更新时间"
            prop="updateTime"
            show-overflow-tooltip
          />
          <el-table-column fixed="right" label="操作" min-width="180" align="center">
            <template #default="scope">
              <el-button
                link
                type="primary"
                @click="handleSubtract(scope.row)"
                :disabled="
                  (scope.row.qualifiedUnLockedQuantity || 0) +
                    (scope.row.qualifiedPendingOutQuantity || 0) <=
                    0 &&
                  (scope.row.unQualifiedUnLockedQuantity || 0) +
                    (scope.row.unQualifiedPendingOutQuantity || 0) <=
                    0
                "
                >领用</el-button
              >
              <el-button
                link
                type="primary"
                v-if="
                  scope.row.unQualifiedUnLockedQuantity > 0 ||
                  scope.row.qualifiedUnLockedQuantity > 0
                "
                @click="handleFrozen(scope.row)"
                >冻结</el-button
              >
              <el-button
                link
                type="primary"
                v-if="
                  scope.row.qualifiedLockedQuantity > 0 ||
                  scope.row.unQualifiedLockedQuantity > 0
                "
                @click="handleThaw(scope.row)"
                >解冻</el-button
              >
            </template>
          </el-table-column>
        </el-table>
      </div>
      <pagination
        v-show="total > 0"
        :total="total"
        layout="total, sizes, prev, pager, next, jumper"
        :page="page.current"
        :limit="page.size"
        @pagination="paginationChange"
      />
    </div>
  </el-dialog>
</template>
<script setup>
import pagination from "@/components/PIMTable/Pagination.vue";
import { computed, reactive, ref, watch } from "vue";
import { getStockInventoryBatchNoQty } from "@/api/inventoryManagement/stockInventory.js";
const props = defineProps({
  visible: {
    type: Boolean,
    required: true,
  },
  record: {
    type: Object,
    default: () => ({}),
  },
});
const emit = defineEmits(["update:visible", "subtract", "frozen", "thaw"]);
const isShow = computed({
  get() {
    return props.visible;
  },
  set(val) {
    emit("update:visible", val);
  },
});
const tableData = ref([]);
const tableLoading = ref(false);
const total = ref(0);
const page = reactive({
  current: 1,
  size: 20,
});
const getList = () => {
  if (!props.record?.productId || !props.record?.productModelId) {
    tableData.value = [];
    total.value = 0;
    return;
  }
  tableLoading.value = true;
  getStockInventoryBatchNoQty({
    current: page.current,
    size: page.size,
    productId: props.record.productId,
    productModelId: props.record.productModelId,
  })
    .then((res) => {
      tableData.value = res.data?.records || [];
      total.value = res.data?.total || 0;
    })
    .finally(() => {
      tableLoading.value = false;
    });
};
const paginationChange = (obj) => {
  page.current = obj.page;
  page.size = obj.limit;
  getList();
};
const handleSubtract = (row) => {
  emit("subtract", row);
};
const handleFrozen = (row) => {
  emit("frozen", row);
};
const handleThaw = (row) => {
  emit("thaw", row);
};
const closeModal = () => {
  isShow.value = false;
  page.current = 1;
  page.size = 20;
  tableData.value = [];
  total.value = 0;
};
watch(
  () => props.visible,
  (visible) => {
    if (!visible) {
      return;
    }
    page.current = 1;
    getList();
  },
  { immediate: true }
);
</script>
<style scoped lang="scss">
.detail-content {
  display: flex;
  flex-direction: column;
  height: calc(100vh - 170px);
  min-height: 520px;
}
.detail-table-wrapper {
  flex: 1;
  min-height: 0;
}
:deep(.batch-no-qty-detail-dialog .el-dialog) {
  max-width: calc(100vw - 48px);
}
:deep(.batch-no-qty-detail-dialog .el-dialog__body) {
  padding-top: 12px;
}
</style>
src/views/inventoryManagement/stockManagement/Record.vue
@@ -3,143 +3,233 @@
    <div class="search_form mb10">
      <div>
        <span class="search_title ml10">产品大类:</span>
        <el-input v-model="searchForm.productName"
                  style="width: 240px"
                  placeholder="请输入"
                  clearable/>
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px">搜索</el-button>
        <el-input
          v-model="searchForm.productName"
          style="width: 240px"
          placeholder="请输入"
          clearable
        />
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px"
          >搜索</el-button
        >
      </div>
      <div>
         <el-button type="primary" @click="isShowNewModal = true">新增库存</el-button>
        <el-button type="info" plain icon="Upload" @click="isShowImportModal = true">
        <el-button type="primary" @click="isShowNewModal = true"
          >新增库存</el-button
        >
        <el-button
          type="info"
          plain
          icon="Upload"
          @click="isShowImportModal = true"
        >
          å¯¼å…¥åº“å­˜
        </el-button>
        <el-button @click="handleOut">导出</el-button>
      </div>
    </div>
    <div class="table_list">
      <el-table :data="tableData" border v-loading="tableLoading" @selection-change="handleSelectionChange"
        :expand-row-keys="expandedRowKeys" :row-key="(row, index) => index" style="width: 100%"
        :row-class-name="tableRowClassName" height="calc(100vh - 18.5em)">
      <el-table
        :data="tableData"
        border
        v-loading="tableLoading"
        @selection-change="handleSelectionChange"
        :expand-row-keys="expandedRowKeys"
        :row-key="(row, index) => index"
        style="width: 100%"
        :row-class-name="tableRowClassName"
        height="calc(100vh - 18.5em)"
      >
        <el-table-column align="center" type="selection" width="55" />
        <el-table-column align="center" label="序号" type="index" width="60" />
        <el-table-column label="产品大类" prop="productName" show-overflow-tooltip />
        <el-table-column
          label="产品名称"
          prop="productName"
          show-overflow-tooltip
        />
        <el-table-column label="规格型号" prop="model" show-overflow-tooltip />
        <el-table-column label="单位" prop="unit" show-overflow-tooltip />
        <el-table-column label="批号" prop="batchNo" show-overflow-tooltip />
        <el-table-column label="合格库存数量" prop="qualifiedQuantity" show-overflow-tooltip />
        <el-table-column label="不合格库存数量" prop="unQualifiedQuantity" show-overflow-tooltip />
        <el-table-column label="合格冻结数量" prop="qualifiedLockedQuantity" show-overflow-tooltip />
        <el-table-column label="不合格冻结数量" prop="unQualifiedLockedQuantity" show-overflow-tooltip />
        <el-table-column label="库存预警数量" prop="warnNum"  show-overflow-tooltip />
        <el-table-column label="备注" prop="remark"  show-overflow-tooltip />
        <el-table-column label="最近更新时间" prop="updateTime" show-overflow-tooltip />
        <el-table-column fixed="right" label="操作" min-width="90" align="center">
        <el-table-column
          label="合格库存数量"
          prop="qualifiedQuantity"
          show-overflow-tooltip
        />
        <el-table-column
          label="不合格库存数量"
          prop="unQualifiedQuantity"
          show-overflow-tooltip
        />
        <el-table-column
          label="合格冻结数量"
          prop="qualifiedLockedQuantity"
          show-overflow-tooltip
        />
        <el-table-column
          label="不合格冻结数量"
          prop="unQualifiedLockedQuantity"
          show-overflow-tooltip
        />
        <el-table-column
          label="库存预警数量"
          prop="warnNum"
          show-overflow-tooltip
        />
        <el-table-column label="备注" prop="remark" show-overflow-tooltip />
        <el-table-column
          label="最近更新时间"
          prop="updateTime"
          show-overflow-tooltip
        />
        <el-table-column
          fixed="right"
          label="操作"
          min-width="80"
          align="center"
        >
          <template #default="scope">
            <el-button link type="primary" @click="showSubtractModal(scope.row)" :disabled="((scope.row.qualifiedUnLockedQuantity || 0) + (scope.row.qualifiedPendingOutQuantity || 0) <= 0) && ((scope.row.unQualifiedUnLockedQuantity || 0) + (scope.row.unQualifiedPendingOutQuantity || 0) <= 0)">领用</el-button>
            <el-button link type="primary" v-if="scope.row.unQualifiedUnLockedQuantity > 0 || scope.row.qualifiedUnLockedQuantity > 0" @click="showFrozenModal(scope.row)">冻结</el-button>
            <el-button link type="primary" v-if="scope.row.qualifiedLockedQuantity > 0 || scope.row.unQualifiedLockedQuantity > 0" @click="showThawModal(scope.row)">解冻</el-button>
            <el-button
              link
              type="primary"
              @click="showDetailModal(scope.row)"
              >详情</el-button
            >
          </template>
        </el-table-column>
      </el-table>
      <pagination v-show="total > 0" :total="total" layout="total, sizes, prev, pager, next, jumper"
        :page="page.current" :limit="page.size" @pagination="paginationChange" />
      <pagination
        v-show="total > 0"
        :total="total"
        layout="total, sizes, prev, pager, next, jumper"
        :page="page.current"
        :limit="page.size"
        @pagination="paginationChange"
      />
    </div>
    <new-stock-inventory v-if="isShowNewModal"
                 v-model:visible="isShowNewModal"
                 :top-product-parent-id="props.productId"
                 @completed="handleQuery" />
    <batch-no-qty-detail
      v-if="isShowDetailModal"
      v-model:visible="isShowDetailModal"
      :record="record"
      @subtract="handleDetailSubtract"
      @frozen="handleDetailFrozen"
      @thaw="handleDetailThaw"
    />
    <new-stock-inventory
      v-if="isShowNewModal"
      v-model:visible="isShowNewModal"
      :top-product-parent-id="props.productId"
      @completed="handleQuery"
    />
    <subtract-stock-inventory v-if="isShowSubtractModal"
                 v-model:visible="isShowSubtractModal"
                 :record="record"
                 :type="record.stockType"
                 @completed="handleQuery" />
    <subtract-stock-inventory
      v-if="isShowSubtractModal"
      v-model:visible="isShowSubtractModal"
      :record="record"
      :type="record.stockType"
      @completed="handleQuery"
    />
    <!-- å¯¼å…¥åº“å­˜-->
    <import-stock-inventory v-if="isShowImportModal"
                 v-model:visible="isShowImportModal"
                 type="qualified"
                 @uploadSuccess="handleQuery" />
    <import-stock-inventory
      v-if="isShowImportModal"
      v-model:visible="isShowImportModal"
      type="qualified"
      @uploadSuccess="handleQuery"
    />
    <!-- å†»ç»“/解冻库存-->
    <frozen-and-thaw-stock-inventory v-if="isShowFrozenAndThawModal"
                 v-model:visible="isShowFrozenAndThawModal"
                 :record="record"
                 :operation-type="operationType"
                 :type="record.stockType"
                 @completed="handleQuery" />
    <frozen-and-thaw-stock-inventory
      v-if="isShowFrozenAndThawModal"
      v-model:visible="isShowFrozenAndThawModal"
      :record="record"
      :operation-type="operationType"
      :type="record.stockType"
      @completed="handleQuery"
    />
  </div>
</template>
<script setup>
import pagination from '@/components/PIMTable/Pagination.vue'
import { ref, reactive, toRefs, onMounted, getCurrentInstance } from 'vue'
import {ElMessage, ElMessageBox} from "element-plus";
import {
  getStockInventoryListPageCombined
} from "@/api/inventoryManagement/stockInventory.js";
import pagination from "@/components/PIMTable/Pagination.vue";
import { ref, reactive, toRefs, onMounted, getCurrentInstance } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { getStockInventoryListPageCombined } from "@/api/inventoryManagement/stockInventory.js";
const props = defineProps({
  productId: {
    type: Number,
    required: true,
    default: 0
  }
    default: 0,
  },
});
const NewStockInventory = defineAsyncComponent(() => import("@/views/inventoryManagement/stockManagement/New.vue"));
const SubtractStockInventory = defineAsyncComponent(() => import("@/views/inventoryManagement/stockManagement/Subtract.vue"));
const ImportStockInventory = defineAsyncComponent(() => import("@/views/inventoryManagement/stockManagement/Import.vue"));
const FrozenAndThawStockInventory = defineAsyncComponent(() => import("@/views/inventoryManagement/stockManagement/FrozenAndThaw.vue"));
const { proxy } = getCurrentInstance()
const tableData = ref([])
const selectedRows = ref([])
const record = ref({})
const tableLoading = ref(false)
const NewStockInventory = defineAsyncComponent(() =>
  import("@/views/inventoryManagement/stockManagement/New.vue")
);
const SubtractStockInventory = defineAsyncComponent(() =>
  import("@/views/inventoryManagement/stockManagement/Subtract.vue")
);
const ImportStockInventory = defineAsyncComponent(() =>
  import("@/views/inventoryManagement/stockManagement/Import.vue")
);
const FrozenAndThawStockInventory = defineAsyncComponent(() =>
  import("@/views/inventoryManagement/stockManagement/FrozenAndThaw.vue")
);
const BatchNoQtyDetail = defineAsyncComponent(() =>
  import("@/views/inventoryManagement/stockManagement/BatchNoQtyDetail.vue")
);
const { proxy } = getCurrentInstance();
const tableData = ref([]);
const selectedRows = ref([]);
const record = ref({});
const tableLoading = ref(false);
const page = reactive({
  current: 1,
  size: 100,
})
const total = ref(0)
});
const total = ref(0);
// æ˜¯å¦æ˜¾ç¤ºæ–°å¢žå¼¹æ¡†
const isShowNewModal = ref(false)
const isShowNewModal = ref(false);
// æ˜¯å¦æ˜¾ç¤ºé¢†ç”¨å¼¹æ¡†
const isShowSubtractModal = ref(false)
const isShowSubtractModal = ref(false);
// æ˜¯å¦æ˜¾ç¤ºå†»ç»“/解冻弹框
const isShowFrozenAndThawModal = ref(false)
const isShowFrozenAndThawModal = ref(false);
// æ˜¯å¦æ˜¾ç¤ºè¯¦æƒ…弹框
const isShowDetailModal = ref(false);
// æ“ä½œç±»åž‹
const operationType = ref('frozen')
const operationType = ref("frozen");
// æ˜¯å¦æ˜¾ç¤ºå¯¼å…¥å¼¹æ¡†
const isShowImportModal = ref(false)
const isShowImportModal = ref(false);
const data = reactive({
  searchForm: {
    productName: '',
    productName: "",
    topParentProductId: props.productId,
  }
})
const { searchForm } = toRefs(data)
  },
});
const { searchForm } = toRefs(data);
// æŸ¥è¯¢åˆ—表
/** æœç´¢æŒ‰é’®æ“ä½œ */
const handleQuery = () => {
  page.current = 1
  getList()
}
  page.current = 1;
  getList();
};
const paginationChange = (obj) => {
  page.current = obj.page;
  page.size = obj.limit;
  getList()
}
  getList();
};
const getList = () => {
  tableLoading.value = true
  getStockInventoryListPageCombined({ ...searchForm.value, ...page }).then(res => {
    tableLoading.value = false
    tableData.value = res.data.records
    total.value = res.data.total
    // æ•°æ®åŠ è½½å®ŒæˆåŽæ£€æŸ¥åº“å­˜
    // checkStockAndCreatePurchase();
  }).catch(() => {
    tableLoading.value = false
  })
}
  tableLoading.value = true;
  getStockInventoryListPageCombined({ ...searchForm.value, ...page })
    .then((res) => {
      tableLoading.value = false;
      tableData.value = res.data.records;
      total.value = res.data.total;
      // æ•°æ®åŠ è½½å®ŒæˆåŽæ£€æŸ¥åº“å­˜
      // checkStockAndCreatePurchase();
    })
    .catch(() => {
      tableLoading.value = false;
    });
};
const handleFileSuccess = (response) => {
  const { code, msg } = response;
@@ -154,61 +244,89 @@
// ç‚¹å‡»é¢†ç”¨
const showSubtractModal = (row) => {
  record.value = row
  isShowSubtractModal.value = true
}
  record.value = row;
  isShowSubtractModal.value = true;
};
// ç‚¹å‡»è¯¦æƒ…
const showDetailModal = (row) => {
  if (!row?.productId || !row?.productModelId) {
    proxy.$modal.msgError("当前数据缺少产品ID或规格型号ID");
    return;
  }
  record.value = row;
  isShowDetailModal.value = true;
};
const handleDetailSubtract = (row) => {
  isShowDetailModal.value = false;
  showSubtractModal(row);
};
const handleDetailFrozen = (row) => {
  isShowDetailModal.value = false;
  showFrozenModal(row);
};
const handleDetailThaw = (row) => {
  isShowDetailModal.value = false;
  showThawModal(row);
};
// ç‚¹å‡»å†»ç»“
const showFrozenModal = (row) => {
  record.value = row
  isShowFrozenAndThawModal.value = true
  operationType.value = 'frozen'
}
  record.value = row;
  isShowFrozenAndThawModal.value = true;
  operationType.value = "frozen";
};
// ç‚¹å‡»è§£å†»
const showThawModal = (row) => {
  record.value = row
  isShowFrozenAndThawModal.value = true
  operationType.value = 'thaw'
}
  record.value = row;
  isShowFrozenAndThawModal.value = true;
  operationType.value = "thaw";
};
// è¡¨æ ¼é€‰æ‹©æ•°æ®
const handleSelectionChange = (selection) => {
  // è¿‡æ»¤æŽ‰å­æ•°æ®
  selectedRows.value = selection.filter(item => item.id);
  console.log('selection', selectedRows.value)
}
const expandedRowKeys = ref([])
  selectedRows.value = selection.filter((item) => item.id);
  console.log("selection", selectedRows.value);
};
const expandedRowKeys = ref([]);
// è¡¨æ ¼è¡Œç±»å
const tableRowClassName = ({ row }) => {
  const stock = Number(row?.qualifiedUnLockedQuantity ?? 0);
  const warn = Number(row?.warnNum ?? 0);
  if (!Number.isFinite(stock) || !Number.isFinite(warn)) {
    return '';
    return "";
  }
  return stock < warn ? 'row-low-stock' : '';
  return stock < warn ? "row-low-stock" : "";
};
// å¯¼å‡º
const handleOut = () => {
  ElMessageBox.confirm(
    '是否确认导出?',
    '导出', {
    confirmButtonText: '确认',
    cancelButtonText: '取消',
    type: 'warning',
  }
  ).then(() => {
    proxy.download("/stockInventory/exportStockInventory", {topParentProductId: props.productId}, '库存信息.xlsx')
  }).catch(() => {
    proxy.$modal.msg("已取消")
  ElMessageBox.confirm("是否确认导出?", "导出", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
}
    .then(() => {
      proxy.download(
        "/stockInventory/exportStockInventory",
        { topParentProductId: props.productId },
        "库存信息.xlsx"
      );
    })
    .catch(() => {
      proxy.$modal.msg("已取消");
    });
};
onMounted(() => {
  getList()
})
  getList();
});
</script>
<style scoped lang="scss">
src/views/personnelManagement/contractManagement/index.vue
@@ -23,6 +23,12 @@
        :total="page.total"></PIMTable>
    </div>
    <form-dia ref="formDia" @close="handleQuery"></form-dia>
    <renew-contract
        v-if="isShowRenewContractModal"
        v-model:visible="isShowRenewContractModal"
        :id="id"
        @completed="handleQuery"
    />
    <!-- åˆåŒå¯¼å…¥å¯¹è¯æ¡† -->
    <el-dialog
@@ -71,8 +77,9 @@
<script setup>
import { Search } from "@element-plus/icons-vue";
import { onMounted, ref } from "vue";
import { onMounted, ref, defineAsyncComponent } from "vue";
import FormDia from "@/views/personnelManagement/contractManagement/components/formDia.vue";
const RenewContract = defineAsyncComponent(() => import("@/views/personnelManagement/employeeRecord/components/RenewContract.vue"));
import { ElMessageBox } from "element-plus";
import { staffOnJobListPage } from "@/api/personnelManagement/staffOnJob.js";
import dayjs from "dayjs";
@@ -183,7 +190,7 @@
    label: "操作",
    align: "center",
    fixed: 'right',
    width: 120,
    width: 160,
    operation: [
      {
        name: "详情",
@@ -191,11 +198,22 @@
        clickFun: (row) => {
          openForm("edit", row);
        },
      },
      {
        name: "续签合同",
        type: "text",
        showHide: row => row.staffState === 1,
        clickFun: (row) => {
          isShowRenewContractModal.value = true;
          id.value = row.id;
        },
      }
    ],
  },
]);
const filesDia = ref()
const isShowRenewContractModal = ref(false);
const id = ref(0);
const tableData = ref([]);
const selectedRows = ref([]);
const tableLoading = ref(false);
src/views/personnelManagement/employeeRecord/index.vue
@@ -52,16 +52,14 @@
          :tableLoading="tableLoading"
          @pagination="pagination"
          :total="page.total"
      ></PIMTable>
      >
        <template #positiveDate="{ row }">
          <span :class="getPositiveDateClass(row.positiveDate)">{{ row.positiveDate }}</span>
        </template>
      </PIMTable>
    </div>
    <show-form-dia ref="formDia" @close="handleQuery"></show-form-dia>
    <new-or-edit-form-dia ref="formDiaNewOrEditFormDia" @close="handleQuery"></new-or-edit-form-dia>
    <renew-contract
        v-if="isShowRenewContractModal"
        v-model:visible="isShowRenewContractModal"
        :id="id"
        @completed="handleQuery"
    />
    
    <!-- å¯¼å…¥å¯¹è¯æ¡† -->
    <el-dialog :title="upload.title" v-model="upload.open" width="400px" append-to-body>
@@ -107,7 +105,6 @@
const NewOrEditFormDia = defineAsyncComponent(() => import("@/views/personnelManagement/employeeRecord/components/NewOrEditFormDia.vue"));
const ShowFormDia = defineAsyncComponent(() => import( "@/views/personnelManagement/employeeRecord/components/Show.vue"));
const RenewContract = defineAsyncComponent(() => import( "@/views/personnelManagement/employeeRecord/components/RenewContract.vue"));
const data = reactive({
  searchForm: {
@@ -119,8 +116,6 @@
  deptOptions: [],
});
const { searchForm, deptOptions } = toRefs(data);
const isShowRenewContractModal = ref(false);
const id = ref(0);
const tableColumn = ref([
  {
    label: "状态",
@@ -177,6 +172,13 @@
    width: 120,
  },
  {
    label: "转正日期",
    prop: "positiveDate",
    width: 120,
    dataType: "slot",
    slot: "positiveDate",
  },
  {
    label: "年龄",
    prop: "age",
  },
@@ -208,22 +210,6 @@
          openFormNewOrEditFormDia("edit", row);
        },
      },
      {
        name: "续签合同",
        type: "text",
        showHide: row => row.staffState === 1,
        clickFun: (row) => {
          isShowRenewContractModal.value = true;
          id.value = row.id;
        },
      },
      // {
      //   name: "详情",
      //   type: "text",
      //   clickFun: (row) => {
      //     openForm("edit", row);
      //   },
      // },
    ],
  },
]);
@@ -253,6 +239,22 @@
  // ä¸Šä¼ çš„地址
  url: import.meta.env.VITE_APP_BASE_API + "/staff/staffOnJob/import"
})
// åˆ¤æ–­è½¬æ­£æ—¥æœŸæ˜¯å¦åœ¨7天内
const getPositiveDateClass = (positiveDate) => {
  if (!positiveDate) return '';
  const today = new Date();
  today.setHours(0, 0, 0, 0);
  const positive = new Date(positiveDate);
  positive.setHours(0, 0, 0, 0);
  const diffTime = positive - today;
  const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
  // 7天内转正(包括今天)显示警告色
  if (diffDays >= 0 && diffDays <= 7) {
    return 'positive-date-warning';
  }
  return '';
};
const fetchDeptOptions = () => {
    deptTreeSelect().then(response => {
@@ -402,4 +404,9 @@
.search_title2 {
  margin-left: 10px;
}
.positive-date-warning {
  color: #f56c6c;
  font-weight: bold;
}
</style>
src/views/procurementManagement/procurementLedger/index.vue
@@ -693,6 +693,7 @@
  const salesContractList = ref([]);
  const supplierList = ref([]);
  const tableLoading = ref(false);
  const recordId = ref();
  const fileListDialogVisible = ref(false);
  const page = reactive({
    current: 1,
src/views/procurementManagement/purchaseReturnOrder/New.vue
@@ -261,8 +261,11 @@
                               prop="unQuantity"
                               width="130" />
              <el-table-column label="已退货数量"
                               prop="totalReturnNum"
                               width="130" />
                               width="130">
                <template #default="scope">
                  {{ calcAlreadyReturned(scope.row) }}
                </template>
              </el-table-column>
              <el-table-column label="退货数量"
                               prop="returnQuantity"
                               width="180">
@@ -526,6 +529,14 @@
  return Number.isNaN(num) ? 0 : num
}
/** å·²é€€è´§æ•°é‡ = å…¥åº“行总数量 âˆ’ å½“前可退货数量(剩余) */
const calcAlreadyReturned = (row) => {
  const total = Number(row?.stockInNum ?? row?.totalQuantity ?? row?.quantity ?? 0)
  const un = Number(row?.unQuantity ?? 0)
  if (!Number.isFinite(total) || !Number.isFinite(un)) return 0
  return Math.max(total - un, 0)
}
const getReturnTotal = (row) => {
  const qty = toNumber(row?.returnQuantity)
  const unitPrice = toNumber(row?.taxInclusiveUnitPrice)
src/views/procurementManagement/purchaseReturnOrder/ProductList.vue
@@ -38,8 +38,11 @@
                           prop="unQuantity"
                           width="130" />
          <el-table-column label="已退货数量"
                           prop="totalReturnNum"
                           width="130" />
                           width="130">
            <template #default="scope">
              {{ calcAlreadyReturned(scope.row) }}
            </template>
          </el-table-column>
          <!-- <el-table-column label="库存预警数量"
                           prop="warnNum"
                           width="120"
@@ -116,6 +119,14 @@
  return parseFloat(cellValue).toFixed(2);
};
/** å·²é€€è´§æ•°é‡ = å…¥åº“行总数量 âˆ’ å½“前可退货数量(剩余) */
const calcAlreadyReturned = (row) => {
  const total = Number(row?.stockInNum ?? row?.totalQuantity ?? row?.quantity ?? 0)
  const un = Number(row?.unQuantity ?? 0)
  if (!Number.isFinite(total) || !Number.isFinite(un)) return 0
  return Math.max(total - un, 0)
}
const handleChangeSelection = (val) => {
  selectedRows.value = val;
}
src/views/procurementManagement/purchaseReturnOrder/index.vue
@@ -146,8 +146,11 @@
                           prop="unQuantity"
                           width="100" />
          <el-table-column label="已退货数量"
                           prop="totalReturnNum"
                           width="100" />
                           width="100">
            <template #default="scope">
              {{ calcAlreadyReturned(scope.row) }}
            </template>
          </el-table-column>
          <!-- <el-table-column label="库存预警数量" prop="warnNum" width="120" /> -->
          <!-- <el-table-column label="税率(%)" prop="taxRate" width="90" /> -->
          <el-table-column
@@ -458,6 +461,14 @@
  return num.toFixed(2);
};
/** å·²é€€è´§æ•°é‡ = å…¥åº“行总数量 âˆ’ å½“前可退货数量(剩余) */
const calcAlreadyReturned = (row) => {
  const total = Number(row?.stockInNum ?? row?.totalQuantity ?? row?.quantity ?? 0);
  const un = Number(row?.unQuantity ?? 0);
  if (!Number.isFinite(total) || !Number.isFinite(un)) return 0;
  return Math.max(total - un, 0);
};
onMounted(() => {
  getList();
});
src/views/productionManagement/processRoute/processRouteItem/index.vue
@@ -315,7 +315,7 @@
                                     :step="1"
                                     controls-position="right"
                                     style="width: 100%"
                                     @change="handleUnitQuantityChange(row)"
                                     @change="handleUnitQuantityChange"
                                     :disabled="!bomDataValue.isEdit || bomDataValue.dataList.some(item => (item).tempId === row.tempId)" />
                  </el-form-item>
                </template>
@@ -333,7 +333,7 @@
                                     :step="1"
                                     controls-position="right"
                                     style="width: 100%"
                                     :disabled="!bomDataValue.isEdit || bomDataValue.dataList.some(item => (item).tempId === row.tempId)" />
                                     :disabled="true" />
                  </el-form-item>
                </template>
              </el-table-column>
@@ -1089,6 +1089,53 @@
      }
    });
  };
  const toQuantityNumber = value => {
    const numberValue = Number(value);
    if (!Number.isFinite(numberValue)) {
      return 0;
    }
    return Number(numberValue.toFixed(2));
  };
  const syncDemandedQuantityTree = (items, parentDemandedQuantity = null) => {
    items.forEach(item => {
      if (parentDemandedQuantity !== null) {
        item.demandedQuantity = toQuantityNumber(
          parentDemandedQuantity * toQuantityNumber(item.unitQuantity)
        );
      }
      if (Array.isArray(item.children) && item.children.length > 0) {
        syncDemandedQuantityTree(
          item.children,
          toQuantityNumber(item.demandedQuantity)
        );
      }
    });
  };
  const recalculateDemandedQuantities = () => {
    if (pageType.value !== "order") {
      return;
    }
    const rootDemandedQuantity = routeInfo.value.quantity;
    if (
      rootDemandedQuantity === undefined ||
      rootDemandedQuantity === null ||
      rootDemandedQuantity === ""
    ) {
      syncDemandedQuantityTree(bomDataValue.value.dataList);
      return;
    }
    syncDemandedQuantityTree(
      bomDataValue.value.dataList,
      toQuantityNumber(rootDemandedQuantity)
    );
  };
  const processChange = value => {
    processOptions.value.forEach(item => {
      if (item.id == value) {
@@ -1117,6 +1164,7 @@
      );
      bomDataValue.value.dataList = data || [];
      normalizeTreeData(bomDataValue.value.dataList);
      recalculateDemandedQuantities();
    } catch (err) {
      console.error("获取BOM数据失败:", err);
    }
@@ -1212,10 +1260,8 @@
    });
  };
  const handleUnitQuantityChange = row => {
    if (routeInfo.value.quantity && routeInfo.value.quantity !== 0) {
      row.demandedQuantity = (row.unitQuantity || 0) * routeInfo.value.quantity;
    }
  const handleUnitQuantityChange = () => {
    recalculateDemandedQuantities();
  };
  const addchildItem = (item, tempId) => {
@@ -1236,14 +1282,12 @@
          "",
        operationName: "",
        unitQuantity: 1,
        demandedQuantity:
          routeInfo.value.quantity && routeInfo.value.quantity !== 0
            ? 1 * routeInfo.value.quantity
            : 0,
        demandedQuantity: 0,
        children: [],
        unit: "",
        tempId: new Date().getTime(),
      });
      recalculateDemandedQuantities();
      return true;
    }
    if (item.children && item.children.length > 0) {
@@ -1275,14 +1319,12 @@
            "",
          operationName: "",
          unitQuantity: 1,
          demandedQuantity:
            routeInfo.value.quantity && routeInfo.value.quantity !== 0
              ? 1 * routeInfo.value.quantity
              : 0,
          demandedQuantity: 0,
          unit: "",
          children: [],
          tempId: new Date().getTime(),
        });
        recalculateDemandedQuantities();
        return;
      }
      addchildItem(item, tempId);
@@ -1350,6 +1392,7 @@
    console.log(bomDataValue.value.dataList, "bomDataValue.value.dataList");
    normalizeTreeData(bomDataValue.value.dataList);
    recalculateDemandedQuantities();
    const valid = validateAllBom();
    if (valid) {
@@ -1361,7 +1404,7 @@
        .then(() => {
          ElMessage.success("BOM保存成功");
          bomDataValue.value.isEdit = false;
          fetchBomData();
          refreshCurrentPage();
        })
        .catch(() => {
          ElMessage.error("BOM保存失败");
@@ -1374,11 +1417,15 @@
    }
  };
  onMounted(() => {
  const refreshCurrentPage = () => {
    getRouteInfo();
    getList();
    getProcessList();
    fetchBomData();
  };
  onMounted(() => {
    refreshCurrentPage();
  });
  onUnmounted(() => {
src/views/productionManagement/productStructure/Detail/index.vue
@@ -86,6 +86,7 @@
                                     :step="1"
                                     controls-position="right"
                                     style="width: 100%"
                                     @change="handleUnitQuantityChange"
                                     :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)" />
                  </el-form-item>
                </template>
@@ -103,7 +104,7 @@
                                     :step="1"
                                     controls-position="right"
                                     style="width: 100%"
                                     :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)" />
                                     :disabled="true" />
                  </el-form-item>
                </template>
              </el-table-column>
@@ -268,6 +269,42 @@
    });
  };
  const toQuantityNumber = (value: any) => {
    const numberValue = Number(value);
    if (!Number.isFinite(numberValue)) {
      return 0;
    }
    return Number(numberValue.toFixed(2));
  };
  const syncDemandedQuantityTree = (
    items: any[],
    parentDemandedQuantity: number | null = null
  ) => {
    items.forEach((item: any) => {
      if (parentDemandedQuantity !== null) {
        item.demandedQuantity = toQuantityNumber(
          parentDemandedQuantity * toQuantityNumber(item.unitQuantity)
        );
      }
      if (Array.isArray(item.children) && item.children.length > 0) {
        syncDemandedQuantityTree(
          item.children,
          toQuantityNumber(item.demandedQuantity)
        );
      }
    });
  };
  const recalculateDemandedQuantities = () => {
    if (!isOrderPage.value) {
      return;
    }
    syncDemandedQuantityTree(dataValue.dataList);
  };
  const buildSubmitTree = (items: any[]) => {
    return items.map((item: any) => {
      const current = { ...item };
@@ -282,6 +319,10 @@
  const handleProcessChange = (row: any, value: any) => {
    row.processId = value || "";
    syncProcessOperationFields(row);
  };
  const handleUnitQuantityChange = () => {
    recalculateDemandedQuantities();
  };
  const tableData = reactive([
@@ -304,6 +345,7 @@
      const { data } = await listProcessBom({ orderId: routeOrderId.value });
      dataValue.dataList = (data as any) || [];
      normalizeTreeData(dataValue.dataList);
      recalculateDemandedQuantities();
    } else {
      // éžè®¢å•情况:使用原来的接口
      const { data } = await queryList(routeId.value);
@@ -437,6 +479,7 @@
  const submit = () => {
    dataValue.loading = true;
    normalizeTreeData(dataValue.dataList);
    recalculateDemandedQuantities();
    // å…ˆè¿›è¡Œè¡¨å•校验
    const valid = validateAll();
@@ -514,6 +557,7 @@
          tempId: new Date().getTime(),
        });
        recalculateDemandedQuantities();
        return;
      }
      addchildItem(item, tempId);
@@ -542,6 +586,7 @@
        unit: "",
        tempId: new Date().getTime(),
      });
      recalculateDemandedQuantities();
      return true;
    }
    if (item.children && item.children.length > 0) {
@@ -587,4 +632,4 @@
    await fetchProcessOptions();
    await fetchData();
  });
</script>
</script>
src/views/productionManagement/productionProcess/index.vue
@@ -243,6 +243,7 @@
            </el-form-item>
            <el-form-item label="标准值">
              <el-input v-model="selectedParam.standardValue"
                        @input="val => onStandardValueInput(val, selectedParam)"
                        placeholder="请输入默认值" />
            </el-form-item>
          </el-form>
@@ -273,6 +274,7 @@
        <el-form-item label="标准值"
                      prop="standardValue">
          <el-input v-model="editParamForm.standardValue"
                    @input="val => onStandardValueInput(val, editParamForm)"
                    placeholder="请输入标准值" />
        </el-form-item>
      </el-form>
@@ -392,7 +394,18 @@
    technologyParamId: null,
    paramName: "",
    standardValue: null,
    paramType: null,
  });
  const onStandardValueInput = (val, target) => {
    const data = target.value || target;
    const type = data.paramType;
    if (type === 1) {
      // æ•°å€¼æ ¼å¼ï¼šä¸èƒ½è¾“入中文或英文字符
      data.standardValue = val.replace(/[a-zA-Z\u4e00-\u9fa5]/g, "");
    }
  };
  const editParamRules = {
    standardValue: [
      {
@@ -403,6 +416,12 @@
          if (value === null || value === undefined || value === "") {
            callback(new Error("请输入标准值"));
          } else {
            const type = editParamForm.paramType;
            if (type === 1 && value) {
              if (/[a-zA-Z\u4e00-\u9fa5]/.test(value)) {
                return callback(new Error("数值格式不能包含中英文字符"));
              }
            }
            callback();
          }
        },
@@ -717,6 +736,7 @@
    editParamForm.technologyParamId = row.technologyParamId;
    editParamForm.paramName = row.paramName;
    editParamForm.standardValue = row.standardValue;
    editParamForm.paramType = row.paramType;
    editParamDialogVisible.value = true;
  };
src/views/qualityManagement/finalInspection/components/formDia.vue
@@ -2,7 +2,7 @@
  <div>
    <el-dialog
        v-model="dialogFormVisible"
        :title="operationType === 'add' ? '新增出厂检验' : '编辑出厂检验'"
        :title="operationType === 'add' ? '新增出厂检验' : operationType === 'view' ? '查看出厂检验' : '编辑出厂检验'"
        width="70%"
        @close="closeDia"
    >
@@ -18,19 +18,21 @@
                  @change="getModels"
                  :data="productOptions"
                  :render-after-expand="false"
                  :disabled="operationType === 'edit'"
                  :disabled="isViewMode || operationType === 'edit'"
                  style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="规格型号:" prop="productModelId">
              <el-select v-model="form.productModelId" placeholder="请选择" clearable :disabled="operationType === 'edit'"
              <el-select v-model="form.productModelId" placeholder="请选择" clearable :disabled="isViewMode || operationType === 'edit'"
                         filterable readonly @change="handleChangeModel">
                <el-option v-for="item in modelOptions" :key="item.id" :label="item.model" :value="item.id" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="指标选择:" prop="testStandardId">
              <el-select
@@ -39,6 +41,7 @@
                clearable
                @change="handleTestStandardChange"
                style="width: 100%"
                :disabled="isViewMode"
              >
                <el-option
                  v-for="item in testStandardOptions"
@@ -58,21 +61,52 @@
          </el-col>
          <el-col :span="12">
            <el-form-item label="数量:" prop="quantity">
              <el-input-number :step="0.01" :min="0" style="width: 100%" v-model="form.quantity" placeholder="请输入" clearable :precision="2" :disabled="quantityDisabled"/>
              <el-input-number :step="0.01" :min="0" style="width: 100%" v-model="form.quantity" placeholder="请输入" clearable :precision="2" :disabled="isViewMode || processQuantityDisabled"/>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="合格数量:"
                          prop="qualifiedQuantity">
              <el-input-number :step="0.01"
                               :min="0"
                               style="width: 100%"
                               v-model="form.qualifiedQuantity"
                               placeholder="请输入"
                               clearable
                               :precision="2"
                               @change="handleQualifiedQuantityChange"
                               :disabled="isViewMode" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="不合格数量:"
                          prop="unqualifiedQuantity">
              <el-input-number :step="0.01"
                               :min="0"
                               style="width: 100%"
                               v-model="form.unqualifiedQuantity"
                               placeholder="请输入"
                               clearable
                               :precision="2"
                               @change="handleUnqualifiedQuantityChange"
                               :disabled="isViewMode" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="检测单位:" prop="checkCompany">
              <el-input v-model="form.checkCompany" placeholder="请输入" clearable/>
              <el-input v-model="form.checkCompany" placeholder="请输入" clearable :disabled="isViewMode"/>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="检测结果:" prop="checkResult">
              <el-select v-model="form.checkResult">
              <el-select v-model="form.checkResult" :disabled="isViewMode">
                <el-option label="合格" value="合格" />
                <el-option label="不合格" value="不合格" />
                <el-option label="部分合格" value="部分合格" />
              </el-select>
            </el-form-item>
          </el-col>
@@ -80,10 +114,10 @@
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="检验员:" prop="checkName">
                            <el-select v-model="form.checkName" placeholder="请选择" clearable>
                                <el-option v-for="item in userList" :key="item.nickName" :label="item.nickName"
                                                     :value="item.nickName"/>
                            </el-select>
              <el-select v-model="form.checkName" placeholder="请选择" clearable :disabled="isViewMode">
                <el-option v-for="item in userList" :key="item.nickName" :label="item.nickName"
                           :value="item.nickName"/>
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
@@ -96,6 +130,7 @@
                  format="YYYY-MM-DD"
                  clearable
                  style="width: 100%"
                  :disabled="isViewMode"
              />
            </el-form-item>
          </el-col>
@@ -109,13 +144,16 @@
                height="400"
            >
                <template #slot="{ row }">
                    <el-input v-model="row.testValue" clearable/>
                    <el-input v-model="row.testValue" clearable :disabled="isViewMode"/>
                </template>
            </PIMTable>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitForm">确认</el-button>
          <el-button @click="closeDia">取消</el-button>
          <template v-if="!isViewMode">
            <el-button type="primary" @click="submitForm">确认</el-button>
            <el-button @click="closeDia">取消</el-button>
          </template>
          <el-button v-else @click="closeDia">关闭</el-button>
        </div>
      </template>
    </el-dialog>
@@ -134,7 +172,7 @@
const emit = defineEmits(['close'])
const dialogFormVisible = ref(false);
const operationType = ref('')
const operationType = ref("");
const data = reactive({
  form: {
    checkTime: "",
@@ -147,25 +185,31 @@
    testStandardId: "",
    unit: "",
    quantity: "",
    qualifiedQuantity: "",
    unqualifiedQuantity: "",
    checkCompany: "",
    checkResult: "",
  },
  rules: {
    checkTime: [{ required: true, message: "请输入", trigger: "blur" },],
    checkTime: [{ required: true, message: "请输入", trigger: "blur" }],
    process: [{ required: true, message: "请输入", trigger: "blur" }],
    checkName: [{ required: false, message: "请输入", trigger: "blur" }],
    productId: [{ required: true, message: "请输入", trigger: "blur" }],
    productModelId: [{ required: true, message: "请选择", trigger: "change" }],
    testStandardId: [{required: false, message: "请选择指标", trigger: "change"}],
    testStandardId: [{ required: false, message: "请选择指标", trigger: "change" }],
    unit: [{ required: false, message: "请输入", trigger: "blur" }],
    quantity: [{ required: true, message: "请输入", trigger: "blur" }],
    qualifiedQuantity: [{ required: true, message: "请输入", trigger: "blur" }],
    unqualifiedQuantity: [{ required: true, message: "请输入", trigger: "blur" }],
    checkCompany: [{ required: false, message: "请输入", trigger: "blur" }],
    checkResult: [{ required: true, message: "请输入", trigger: "change" }],
  },
});
const { form, rules } = toRefs(data);
// ç¼–辑时:productMainId æˆ– purchaseLedgerId ä»»ä¸€æœ‰å€¼åˆ™æ•°é‡ç½®ç°
const quantityDisabled = computed(() => {
// æ˜¯å¦ä¸ºæŸ¥çœ‹æ¨¡å¼
const isViewMode = computed(() => operationType.value === 'view');
// ç¼–辑时:productMainId æˆ– purchaseLedgerId ä»»ä¸€æœ‰å€¼åˆ™å·¥åºã€æ•°é‡ç½®ç°
const processQuantityDisabled = computed(() => {
  const v = form.value || {};
  return !!(v.productMainId != null || v.purchaseLedgerId != null);
});
@@ -209,7 +253,7 @@
  // å…ˆæ¸…空表单验证状态,避免闪烁
  await nextTick();
  proxy.$refs.formRef?.clearValidate();
  // å¹¶è¡ŒåŠ è½½åŸºç¡€æ•°æ®
  const [userListsRes] = await Promise.all([
    userListNoPage(),
@@ -219,12 +263,12 @@
    })
  ]);
  userList.value = userListsRes.data;
  form.value = {}
  testStandardOptions.value = [];
  tableData.value = [];
  if (operationType.value === 'edit') {
  if (operationType.value === 'edit' || operationType.value === 'view') {
    // å…ˆä¿å­˜ testStandardId,避免被清空
    const savedTestStandardId = row.testStandardId;
    // å…ˆè®¾ç½®è¡¨å•数据,但暂时清空 testStandardId,等选项加载完成后再设置
@@ -234,18 +278,18 @@
    nextTick(() => {
      proxy.$refs.formRef?.clearValidate();
    });
    // ç¼–辑模式下,并行加载规格型号和指标选项
    if (currentProductId.value) {
      // è®¾ç½®äº§å“åç§°
      form.value.productName = findNodeById(productOptions.value, currentProductId.value);
      // å¹¶è¡ŒåŠ è½½è§„æ ¼åž‹å·å’ŒæŒ‡æ ‡é€‰é¡¹
      const params = {
        productId: currentProductId.value,
        inspectType: 2
      };
      Promise.all([
        modelList({ id: currentProductId.value }),
        qualityInspectDetailByProductId(params)
@@ -260,15 +304,15 @@
            form.value.unit = selectedModel.unit || '';
          }
        }
        // è®¾ç½®æŒ‡æ ‡é€‰é¡¹
        testStandardOptions.value = testStandardRes.data || [];
        // è®¾ç½® testStandardId å¹¶åŠ è½½å‚æ•°åˆ—è¡¨
        nextTick(() => {
          if (savedTestStandardId) {
            // ç¡®ä¿ç±»åž‹åŒ¹é…ï¼ˆitem.id å¯èƒ½æ˜¯æ•°å­—或字符串)
            const matchedOption = testStandardOptions.value.find(item =>
            const matchedOption = testStandardOptions.value.find(item =>
              item.id == savedTestStandardId || String(item.id) === String(savedTestStandardId)
            );
            if (matchedOption) {
@@ -313,6 +357,28 @@
  form.value.unit = modelOptions.value.find(item => item.id == value)?.unit || '';
}
const handleQualifiedQuantityChange = (value) => {
  if (value === null || value === undefined) {
    form.value.qualifiedQuantity = 0;
    return;
  }
  const quantity = parseFloat(form.value.quantity) || 0;
  const qualified = parseFloat(value) || 0;
  form.value.qualifiedQuantity = qualified > quantity?quantity:qualified;
  form.value.unqualifiedQuantity = Math.max(0, quantity - qualified);
};
const handleUnqualifiedQuantityChange = (value) => {
  if (value === null || value === undefined) {
    form.value.unqualifiedQuantity = 0;
    return;
  }
  const quantity = parseFloat(form.value.quantity) || 0;
  const unqualified = parseFloat(value) || 0;
  form.value.unqualifiedQuantity = unqualified > quantity?quantity:unqualified;
  form.value.qualifiedQuantity = Math.max(0, quantity - unqualified);
};
const findNodeById = (nodes, productId) => {
  for (let i = 0; i < nodes.length; i++) {
    if (nodes[i].value === productId) {
@@ -337,7 +403,7 @@
    if (children && children.length > 0) {
      newItem.children = convertIdToValue(children);
    }
    return newItem;
  });
}
@@ -345,26 +411,26 @@
const submitForm = () => {
  proxy.$refs.formRef.validate(valid => {
    if (valid) {
      form.value.inspectType = 2
            if (operationType.value === "add") {
                tableData.value.forEach((item) => {
                    delete item.id
                })
            }
            const data = {...form.value, qualityInspectParams: tableData.value}
      form.value.inspectType = 2;
      if (operationType.value === "add") {
        tableData.value.forEach((item) => {
          delete item.id;
        });
      }
      const data = { ...form.value, qualityInspectParams: tableData.value };
      if (operationType.value === "add") {
        qualityInspectAdd(data).then(res => {
          proxy.$modal.msgSuccess("提交成功");
          closeDia();
        })
        });
      } else {
        qualityInspectUpdate(data).then(res => {
          proxy.$modal.msgSuccess("提交成功");
          closeDia();
        })
        });
      }
    }
  })
  });
}
const getList = () => {
  if (!currentProductId.value) {
@@ -375,15 +441,15 @@
  let params = {
    productId: currentProductId.value,
    inspectType: 2
  }
    qualityInspectDetailByProductId(params).then(res => {
        // ä¿å­˜ä¸‹æ‹‰æ¡†é€‰é¡¹æ•°æ®
        testStandardOptions.value = res.data || [];
        // æ¸…空表格数据,等待用户选择指标
        tableData.value = [];
        // æ¸…空指标选择
        form.value.testStandardId = '';
    })
  };
  qualityInspectDetailByProductId(params).then(res => {
    // ä¿å­˜ä¸‹æ‹‰æ¡†é€‰é¡¹æ•°æ®
    testStandardOptions.value = res.data || [];
    // æ¸…空表格数据,等待用户选择指标
    tableData.value = [];
    // æ¸…空指标选择
    form.value.testStandardId = '';
  });
}
// æŒ‡æ ‡é€‰æ‹©å˜åŒ–处理
@@ -395,17 +461,21 @@
  tableLoading.value = true;
  getQualityTestStandardParamByTestStandardId(testStandardId).then(res => {
    tableData.value = res.data || [];
    tableData.value = tableData.value.map(item => ({
      ...item,
      id: null
    }));
  }).catch(error => {
    console.error('获取标准参数失败:', error);
    tableData.value = [];
  }).finally(() => {
    tableLoading.value = false;
  })
  });
}
const getQualityInspectParamList = (id) => {
    qualityInspectParamInfo(id).then(res => {
        tableData.value = res.data;
    })
  qualityInspectParamInfo(id).then(res => {
    tableData.value = res.data;
  });
}
// å…³é—­å¼¹æ¡†
const closeDia = () => {
@@ -414,8 +484,8 @@
  testStandardOptions.value = [];
  form.value.testStandardId = '';
  dialogFormVisible.value = false;
  emit('close')
};
  emit('close');
}
defineExpose({
  openDialog,
});
@@ -423,4 +493,4 @@
<style scoped>
</style>
</style>
src/views/qualityManagement/finalInspection/index.vue
@@ -123,8 +123,18 @@
    prop: "unit",
  },
  {
    label: "数量",
    label: "总数量",
    prop: "quantity",
    width: 100
  },
  {
    label: "合格数量",
    prop: "qualifiedQuantity",
    width: 100
  },
  {
    label: "不合格数量",
    prop: "unqualifiedQuantity",
    width: 100
  },
  {
@@ -142,7 +152,7 @@
      } else if (params == '合格') {
        return "success";
      } else {
        return null;
        return 'danger';
      }
    },
  },
@@ -181,6 +191,13 @@
        }
      },
      {
        name: "查看",
        type: "text",
        clickFun: (row) => {
          openForm("view", row);
        },
      },
      {
        name: "附件",
        type: "text",
        clickFun: (row) => {
src/views/qualityManagement/nonconformingManagement/index.vue
@@ -98,7 +98,7 @@
      } else if (params == '合格') {
        return "success";
      } else {
        return null;
        return 'danger';
      }
    },
  },
src/views/qualityManagement/processInspection/components/formDia.vue
@@ -1,130 +1,206 @@
<template>
  <div>
    <el-dialog
        v-model="dialogFormVisible"
        :title="operationType === 'add' ? '新增过程检验' : '编辑过程检验'"
        width="70%"
        @close="closeDia"
    >
      <el-form :model="form" label-width="140px" label-position="top" :rules="rules" ref="formRef">
    <el-dialog v-model="dialogFormVisible"
               :title="operationType === 'add' ? '新增过程检验' : operationType === 'view' ? '查看过程检验' : '编辑过程检验'"
               width="70%"
               @close="closeDia">
      <el-form :model="form"
               label-width="140px"
               label-position="top"
               :rules="rules"
               ref="formRef">
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="工序:" prop="process">
              <el-select v-model="form.process" placeholder="请选择工序" clearable :disabled="processQuantityDisabled" style="width: 100%">
                <el-option v-for="item in processList" :key="item.name" :label="item.name" :value="item.name"/>
            <el-form-item label="工序:"
                          prop="process">
              <el-select v-model="form.process"
                         placeholder="请选择工序"
                         clearable
                         :disabled="isViewMode || processQuantityDisabled"
                         style="width: 100%">
                <el-option v-for="item in processList"
                           :key="item.name"
                           :label="item.name"
                           :value="item.name" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="产品名称:" prop="productId">
              <el-tree-select
                  v-model="form.productId"
                  placeholder="请选择"
                  clearable
                  check-strictly
                  @change="getModels"
                  :data="productOptions"
                  :render-after-expand="false"
                  :disabled="operationType === 'edit'"
                  style="width: 100%"
              />
            <el-form-item label="产品名称:"
                          prop="productId">
              <el-tree-select v-model="form.productId"
                              placeholder="请选择"
                              clearable
                              check-strictly
                              @change="getModels"
                              :data="productOptions"
                              :render-after-expand="false"
                              :disabled="isViewMode || operationType === 'edit'"
                              style="width: 100%" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="规格型号:" prop="productModelId">
              <el-select v-model="form.productModelId" placeholder="请选择" clearable :disabled="operationType === 'edit'"
                         filterable readonly @change="handleChangeModel">
                <el-option v-for="item in modelOptions" :key="item.id" :label="item.model" :value="item.id" />
            <el-form-item label="规格型号:"
                          prop="productModelId">
              <el-select v-model="form.productModelId"
                         placeholder="请选择"
                         clearable
                         :disabled="isViewMode || operationType === 'edit'"
                         filterable
                         readonly
                         @change="handleChangeModel">
                <el-option v-for="item in modelOptions"
                           :key="item.id"
                           :label="item.model"
                           :value="item.id" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="指标选择:" prop="testStandardId">
              <el-select
                v-model="form.testStandardId"
                placeholder="请选择指标"
                clearable
                @change="handleTestStandardChange"
                style="width: 100%"
              >
                <el-option
                  v-for="item in testStandardOptions"
                  :key="item.id"
                  :label="item.standardName || item.standardNo"
                  :value="item.id"
                />
            <el-form-item label="指标选择:"
                          prop="testStandardId">
              <el-select v-model="form.testStandardId"
                         placeholder="请选择指标"
                         clearable
                         @change="handleTestStandardChange"
                         style="width: 100%"
                         :disabled="isViewMode">
                <el-option v-for="item in testStandardOptions"
                           :key="item.id"
                           :label="item.standardName || item.standardNo"
                           :value="item.id" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="单位:" prop="unit">
              <el-input v-model="form.unit" placeholder="请输入" disabled/>
            <el-form-item label="单位:"
                          prop="unit">
              <el-input v-model="form.unit"
                        placeholder="请输入"
                        disabled />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="数量:" prop="quantity">
              <el-input-number :step="0.01" :min="0" style="width: 100%" v-model="form.quantity" placeholder="请输入" clearable :precision="2" :disabled="processQuantityDisabled"/>
            <el-form-item label="数量:"
                          prop="quantity">
              <el-input-number :step="0.01"
                               :min="0"
                               style="width: 100%"
                               v-model="form.quantity"
                               placeholder="请输入"
                               clearable
                               :precision="2"
                               :disabled="isViewMode || processQuantityDisabled" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="检测单位:" prop="checkCompany">
              <el-input v-model="form.checkCompany" placeholder="请输入" clearable/>
            <el-form-item label="合格数量:"
                          prop="qualifiedQuantity">
              <el-input-number :step="0.01"
                               :min="0"
                               style="width: 100%"
                               v-model="form.qualifiedQuantity"
                               placeholder="请输入"
                               clearable
                               :precision="2"
                               @change="handleQualifiedQuantityChange"
                               :disabled="isViewMode" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="检测结果:" prop="checkResult">
              <el-select v-model="form.checkResult">
                <el-option label="合格" value="合格" />
                <el-option label="不合格" value="不合格" />
            <el-form-item label="不合格数量:"
                          prop="unqualifiedQuantity">
              <el-input-number :step="0.01"
                               :min="0"
                               style="width: 100%"
                               v-model="form.unqualifiedQuantity"
                               placeholder="请输入"
                               clearable
                               :precision="2"
                               @change="handleUnqualifiedQuantityChange"
                               :disabled="isViewMode" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="检测单位:"
                          prop="checkCompany">
              <el-input v-model="form.checkCompany"
                        placeholder="请输入"
                        clearable
                        :disabled="isViewMode" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="检测结果:"
                          prop="checkResult">
              <el-select v-model="form.checkResult" :disabled="isViewMode">
                <el-option label="合格"
                           value="合格" />
                <el-option label="不合格"
                           value="不合格" />
                <el-option label="部分合格"
                           value="部分合格" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="检验员:" prop="checkName">
                            <el-select v-model="form.checkName" placeholder="请选择" clearable>
                                <el-option v-for="item in userList" :key="item.nickName" :label="item.nickName"
                                                     :value="item.nickName"/>
                            </el-select>
            <el-form-item label="检验员:"
                          prop="checkName">
              <el-select v-model="form.checkName"
                         placeholder="请选择"
                         clearable
                         :disabled="isViewMode">
                <el-option v-for="item in userList"
                           :key="item.nickName"
                           :label="item.nickName"
                           :value="item.nickName" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="检测日期:" prop="checkTime">
              <el-date-picker
                  v-model="form.checkTime"
                  type="date"
                  placeholder="请选择日期"
                  value-format="YYYY-MM-DD"
                  format="YYYY-MM-DD"
                  clearable
                  style="width: 100%"
              />
            <el-form-item label="检测日期:"
                          prop="checkTime">
              <el-date-picker v-model="form.checkTime"
                              type="date"
                              placeholder="请选择日期"
                              value-format="YYYY-MM-DD"
                              format="YYYY-MM-DD"
                              clearable
                              style="width: 100%"
                              :disabled="isViewMode" />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
            <PIMTable
                rowKey="id"
                :column="tableColumn"
                :tableData="tableData"
                :tableLoading="tableLoading"
                height="400"
            >
                <template #slot="{ row }">
                    <el-input v-model="row.testValue" clearable/>
                </template>
            </PIMTable>
      <PIMTable rowKey="id"
                :column="tableColumn"
                :tableData="tableData"
                :tableLoading="tableLoading"
                height="400">
        <template #slot="{ row }">
          <el-input v-model="row.testValue"
                    clearable
                    :disabled="isViewMode" />
        </template>
      </PIMTable>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitForm">确认</el-button>
          <el-button @click="closeDia">取消</el-button>
          <template v-if="!isViewMode">
            <el-button type="primary"
                       @click="submitForm">确认</el-button>
            <el-button @click="closeDia">取消</el-button>
          </template>
          <el-button v-else @click="closeDia">关闭</el-button>
        </div>
      </template>
    </el-dialog>
@@ -132,332 +208,398 @@
</template>
<script setup>
import {ref, reactive, toRefs, computed, getCurrentInstance, nextTick} from "vue";
import {getOptions} from "@/api/procurementManagement/procurementLedger.js";
import {modelList, productTreeList} from "@/api/basicData/product.js";
import {qualityInspectAdd, qualityInspectUpdate} from "@/api/qualityManagement/rawMaterialInspection.js";
import {qualityInspectDetailByProductId, getQualityTestStandardParamByTestStandardId} from "@/api/qualityManagement/metricMaintenance.js";
import {userListNoPage} from "@/api/system/user.js";
import {qualityInspectParamInfo} from "@/api/qualityManagement/qualityInspectParam.js";
import { list } from "@/api/productionManagement/productionProcess";
const { proxy } = getCurrentInstance()
const emit = defineEmits(['close'])
  import {
    ref,
    reactive,
    toRefs,
    computed,
    getCurrentInstance,
    nextTick,
  } from "vue";
  import { getOptions } from "@/api/procurementManagement/procurementLedger.js";
  import { modelList, productTreeList } from "@/api/basicData/product.js";
  import {
    qualityInspectAdd,
    qualityInspectUpdate,
  } from "@/api/qualityManagement/rawMaterialInspection.js";
  import {
    qualityInspectDetailByProductId,
    getQualityTestStandardParamByTestStandardId,
  } from "@/api/qualityManagement/metricMaintenance.js";
  import { userListNoPage } from "@/api/system/user.js";
  import { qualityInspectParamInfo } from "@/api/qualityManagement/qualityInspectParam.js";
  import { list } from "@/api/productionManagement/productionProcess";
  import qualified from "@/views/inventoryManagement/stockManagement/Qualified.vue";
  const { proxy } = getCurrentInstance();
  const emit = defineEmits(["close"]);
const dialogFormVisible = ref(false);
const operationType = ref('')
const data = reactive({
  form: {
    checkTime: "",
    process: "",
    checkName: "",
    productName: "",
    productId: "",
    productModelId: "",
    model: "",
    testStandardId: "",
    unit: "",
    quantity: "",
    checkCompany: "",
    checkResult: "",
  },
  rules: {
    checkTime: [{ required: true, message: "请输入", trigger: "blur" },],
    process: [{ required: true, message: "请选择工序", trigger: "change" }],
    checkName: [{ required: false, message: "请输入", trigger: "blur" }],
    productId: [{ required: true, message: "请输入", trigger: "blur" }],
    productModelId: [{ required: true, message: "请选择", trigger: "change" }],
    testStandardId: [{required: false, message: "请选择指标", trigger: "change"}],
    unit: [{ required: false, message: "请输入", trigger: "blur" }],
    quantity: [{ required: true, message: "请输入", trigger: "blur" }],
    checkCompany: [{ required: false, message: "请输入", trigger: "blur" }],
    checkResult: [{ required: true, message: "请输入", trigger: "change" }],
  },
});
const userList = ref([]);
const { form, rules } = toRefs(data);
// ç¼–辑时:productMainId æˆ– purchaseLedgerId ä»»ä¸€æœ‰å€¼åˆ™å·¥åºã€æ•°é‡ç½®ç°
const processQuantityDisabled = computed(() => {
  const v = form.value || {};
  return !!(v.productMainId != null || v.purchaseLedgerId != null);
});
const processList = ref([]); // å·¥åºä¸‹æ‹‰åˆ—表(工序名称 name)
const supplierList = ref([]);
const productOptions = ref([]);
const tableColumn = ref([
    {
        label: "指标",
        prop: "parameterItem",
    },
    {
        label: "单位",
        prop: "unit",
    },
    {
        label: "标准值",
        prop: "standardValue",
    },
    {
        label: "内控值",
        prop: "controlValue",
    },
    {
        label: "检验值",
        prop: "testValue",
        dataType: 'slot',
        slot: 'slot',
    },
]);
const tableData = ref([]);
const tableLoading = ref(false);
const currentProductId = ref(0);
const testStandardOptions = ref([]); // æŒ‡æ ‡é€‰æ‹©ä¸‹æ‹‰æ¡†æ•°æ®
const modelOptions = ref([]);
// æ‰“开弹框
const openDialog = async (type, row) => {
    operationType.value = type;
    getOptions().then((res) => {
        supplierList.value = res.data;
    });
    // åŠ è½½å·¥åºä¸‹æ‹‰åˆ—è¡¨
    try {
        const res = await list();
        processList.value = res.data || [];
    } catch (e) {
        console.error("加载工序列表失败", e);
        processList.value = [];
    }
    let userLists = await userListNoPage();
    userList.value = userLists.data;
    // å…ˆé‡ç½®è¡¨å•数据(保持字段完整,避免弹窗首次渲染时触发必填红框“闪一下”)
    form.value = {
        checkTime: "",
        process: "",
        checkName: "",
        productName: "",
        productId: "",
        productModelId: "",
        model: "",
        testStandardId: "",
        unit: "",
        quantity: "",
        checkCompany: "",
        checkResult: "",
    }
    testStandardOptions.value = [];
    tableData.value = [];
    // å…ˆç¡®ä¿äº§å“æ ‘已加载,否则编辑时产品/规格型号无法反显
    await getProductOptions();
    if (operationType.value === 'edit') {
        // å…ˆä¿å­˜ testStandardId,避免被清空
        const savedTestStandardId = row.testStandardId;
        // å…ˆè®¾ç½®è¡¨å•数据,但暂时清空 testStandardId,等选项加载完成后再设置
        form.value = {...row, testStandardId: ''}
        currentProductId.value = row.productId || 0
        // å…³é”®ï¼šç¼–辑时加载规格型号下拉选项,才能反显 productModelId
        if (currentProductId.value) {
            try {
                const res = await modelList({ id: currentProductId.value });
                modelOptions.value = res || [];
                // åŒæ­¥å›žå¡« model / unit(有些接口返回的 row é‡Œå¯èƒ½æ²¡å¸¦å…¨ï¼‰
                if (form.value.productModelId) {
                    handleChangeModel(form.value.productModelId);
                }
            } catch (e) {
                console.error("加载规格型号失败", e);
                modelOptions.value = [];
            }
        }
        // ç¼–辑模式下,先加载指标选项,然后加载参数列表
        if (currentProductId.value) {
            // å…ˆåŠ è½½æŒ‡æ ‡é€‰é¡¹
            let params = {
                productId: currentProductId.value,
                inspectType: 1,
                process: form.value.process || ''
            }
            qualityInspectDetailByProductId(params).then(res => {
                testStandardOptions.value = res.data || [];
                // ä½¿ç”¨ nextTick å’Œ setTimeout ç¡®ä¿é€‰é¡¹å·²ç»æ¸²æŸ“到 DOM
                nextTick(() => {
                    setTimeout(() => {
                        // å¦‚果编辑数据中有 testStandardId,则设置并加载对应的参数
                        if (savedTestStandardId) {
                            // ç¡®ä¿ç±»åž‹åŒ¹é…ï¼ˆitem.id å¯èƒ½æ˜¯æ•°å­—或字符串)
                            const matchedOption = testStandardOptions.value.find(item =>
                                item.id == savedTestStandardId || String(item.id) === String(savedTestStandardId)
                            );
                            if (matchedOption) {
                                // ç¡®ä¿ä½¿ç”¨åŒ¹é…é¡¹çš„ id(保持类型一致)
                                form.value.testStandardId = matchedOption.id;
                                // ç¼–辑保留原检验值,直接拉取原参数数据
                                getQualityInspectParamList(row.id);
                            } else {
                                // å¦‚果找不到匹配项,尝试直接使用原值
                                console.warn('未找到匹配的指标选项,testStandardId:', savedTestStandardId, '可用选项:', testStandardOptions.value);
                                form.value.testStandardId = savedTestStandardId;
                                getQualityInspectParamList(row.id);
                            }
                        } else {
                            // å¦åˆ™ä½¿ç”¨æ—§çš„逻辑
                            getQualityInspectParamList(row.id);
                        }
                    }, 100);
                });
            });
        } else {
            getQualityInspectParamList(row.id);
        }
    }
    // æœ€åŽå†æ‰“开弹窗,并清理校验态,避免必填提示闪烁
    dialogFormVisible.value = true;
    nextTick(() => {
        proxy.$refs?.formRef?.clearValidate?.();
    });
}
const getProductOptions = () => {
  return productTreeList().then((res) => {
    productOptions.value = convertIdToValue(res);
        return productOptions.value;
  const dialogFormVisible = ref(false);
  const operationType = ref("");
  const data = reactive({
    form: {
      checkTime: "",
      process: "",
      checkName: "",
      productName: "",
      productId: "",
      productModelId: "",
      model: "",
      testStandardId: "",
      unit: "",
      quantity: "",
      qualifiedQuantity: "",
      unqualifiedQuantity: "",
      checkCompany: "",
      checkResult: "",
    },
    rules: {
      checkTime: [{ required: true, message: "请输入", trigger: "blur" }],
      process: [{ required: true, message: "请选择工序", trigger: "change" }],
      checkName: [{ required: false, message: "请输入", trigger: "blur" }],
      productId: [{ required: true, message: "请输入", trigger: "blur" }],
      productModelId: [{ required: true, message: "请选择", trigger: "change" }],
      testStandardId: [{ required: false, message: "请选择指标", trigger: "change" }],
      unit: [{ required: false, message: "请输入", trigger: "blur" }],
      quantity: [{ required: true, message: "请输入", trigger: "blur" }],
      qualifiedQuantity: [{ required: true, message: "请输入", trigger: "blur" }],
      unqualifiedQuantity: [{ required: true, message: "请输入", trigger: "blur" }],
      checkCompany: [{ required: false, message: "请输入", trigger: "blur" }],
      checkResult: [{ required: true, message: "请输入", trigger: "change" }],
    },
  });
};
const getModels = (value) => {
  form.value.productModelId = undefined;
  form.value.unit = undefined;
  modelOptions.value = [];
  currentProductId.value = value
  form.value.productName = findNodeById(productOptions.value, value);
  modelList({ id: value }).then((res) => {
    modelOptions.value = res;
  })
  if (currentProductId.value) {
    getList();
  }
};
  const userList = ref([]);
  const { form, rules } = toRefs(data);
  // æ˜¯å¦ä¸ºæŸ¥çœ‹æ¨¡å¼
  const isViewMode = computed(() => operationType.value === 'view');
  // ç¼–辑时:productMainId æˆ– purchaseLedgerId ä»»ä¸€æœ‰å€¼åˆ™å·¥åºã€æ•°é‡ç½®ç°
  const processQuantityDisabled = computed(() => {
    const v = form.value || {};
    return !!(v.productMainId != null || v.purchaseLedgerId != null);
  });
  const processList = ref([]); // å·¥åºä¸‹æ‹‰åˆ—表(工序名称 name)
  const supplierList = ref([]);
  const productOptions = ref([]);
  const tableColumn = ref([
    {
      label: "指标",
      prop: "parameterItem",
    },
    {
      label: "单位",
      prop: "unit",
    },
    {
      label: "标准值",
      prop: "standardValue",
    },
    {
      label: "内控值",
      prop: "controlValue",
    },
    {
      label: "检验值",
      prop: "testValue",
      dataType: "slot",
      slot: "slot",
    },
  ]);
  const tableData = ref([]);
  const tableLoading = ref(false);
  const currentProductId = ref(0);
  const testStandardOptions = ref([]); // æŒ‡æ ‡é€‰æ‹©ä¸‹æ‹‰æ¡†æ•°æ®
  const modelOptions = ref([]);
const handleChangeModel = (value) => {
  form.value.model = modelOptions.value.find(item => item.id == value)?.model || '';
  form.value.unit = modelOptions.value.find(item => item.id == value)?.unit || '';
}
const findNodeById = (nodes, productId) => {
  for (let i = 0; i < nodes.length; i++) {
    if (nodes[i].value === productId) {
      return nodes[i].label; // æ‰¾åˆ°èŠ‚ç‚¹ï¼Œè¿”å›žè¯¥èŠ‚ç‚¹
  // æ‰“开弹框
  const openDialog = async (type, row) => {
    operationType.value = type;
    getOptions().then(res => {
      supplierList.value = res.data;
    });
    // åŠ è½½å·¥åºä¸‹æ‹‰åˆ—è¡¨
    try {
      const res = await list({ size: -1, current: -1 });
      processList.value = res.data.records || [];
    } catch (e) {
      console.error("加载工序列表失败", e);
      processList.value = [];
    }
    if (nodes[i].children && nodes[i].children.length > 0) {
      const foundNode = findNodeById(nodes[i].children, productId);
      if (foundNode) {
        return foundNode; // åœ¨å­èŠ‚ç‚¹ä¸­æ‰¾åˆ°ï¼Œè¿”å›žè¯¥èŠ‚ç‚¹
      }
    }
  }
  return null; // æ²¡æœ‰æ‰¾åˆ°èŠ‚ç‚¹ï¼Œè¿”å›žnull
};
function convertIdToValue(data) {
  return data.map((item) => {
    const { id, children, ...rest } = item;
    const newItem = {
      ...rest,
      value: id, // å°† id æ”¹ä¸º value
    let userLists = await userListNoPage();
    userList.value = userLists.data;
    // å…ˆé‡ç½®è¡¨å•数据(保持字段完整,避免弹窗首次渲染时触发必填红框“闪一下”)
    form.value = {
      checkTime: "",
      process: "",
      checkName: "",
      productName: "",
      productId: "",
      productModelId: "",
      model: "",
      testStandardId: "",
      unit: "",
      quantity: "",
      checkCompany: "",
      checkResult: "",
    };
    if (children && children.length > 0) {
      newItem.children = convertIdToValue(children);
    }
    return newItem;
  });
}
// å·¥åºå˜åŒ–处理
// æäº¤äº§å“è¡¨å•
const submitForm = () => {
  proxy.$refs.formRef.validate(valid => {
    if (valid) {
      form.value.inspectType = 1
            const processName = form.value.process || '';
            if (operationType.value === "add") {
                tableData.value.forEach((item) => {
                    delete item.id
                })
            }
            const data = {
                ...form.value,
                process: processName, // ä¿ç•™ process å­—段以兼容后端
                qualityInspectParams: tableData.value
            }
      if (operationType.value === "add") {
        qualityInspectAdd(data).then(res => {
          proxy.$modal.msgSuccess("提交成功");
          closeDia();
        })
    testStandardOptions.value = [];
    tableData.value = [];
    // å…ˆç¡®ä¿äº§å“æ ‘已加载,否则编辑时产品/规格型号无法反显
    await getProductOptions();
    if (operationType.value === "edit" || operationType.value === "view") {
      // å…ˆä¿å­˜ testStandardId,避免被清空
      const savedTestStandardId = row.testStandardId;
      // å…ˆè®¾ç½®è¡¨å•数据,但暂时清空 testStandardId,等选项加载完成后再设置
      form.value = { ...row, testStandardId: "" };
      currentProductId.value = row.productId || 0;
      // å…³é”®ï¼šç¼–辑时加载规格型号下拉选项,才能反显 productModelId
      if (currentProductId.value) {
        try {
          const res = await modelList({ id: currentProductId.value });
          modelOptions.value = res || [];
          // åŒæ­¥å›žå¡« model / unit(有些接口返回的 row é‡Œå¯èƒ½æ²¡å¸¦å…¨ï¼‰
          if (form.value.productModelId) {
            handleChangeModel(form.value.productModelId);
          }
        } catch (e) {
          console.error("加载规格型号失败", e);
          modelOptions.value = [];
        }
      }
      // ç¼–辑模式下,先加载指标选项,然后加载参数列表
      if (currentProductId.value) {
        // å…ˆåŠ è½½æŒ‡æ ‡é€‰é¡¹
        let params = {
          productId: currentProductId.value,
          inspectType: 1,
          process: form.value.process || "",
        };
        qualityInspectDetailByProductId(params).then(res => {
          testStandardOptions.value = res.data || [];
          // ä½¿ç”¨ nextTick å’Œ setTimeout ç¡®ä¿é€‰é¡¹å·²ç»æ¸²æŸ“到 DOM
          nextTick(() => {
            setTimeout(() => {
              // å¦‚果编辑数据中有 testStandardId,则设置并加载对应的参数
              if (savedTestStandardId) {
                // ç¡®ä¿ç±»åž‹åŒ¹é…ï¼ˆitem.id å¯èƒ½æ˜¯æ•°å­—或字符串)
                const matchedOption = testStandardOptions.value.find(
                  item =>
                    item.id == savedTestStandardId ||
                    String(item.id) === String(savedTestStandardId)
                );
                if (matchedOption) {
                  // ç¡®ä¿ä½¿ç”¨åŒ¹é…é¡¹çš„ id(保持类型一致)
                  form.value.testStandardId = matchedOption.id;
                  // ç¼–辑保留原检验值,直接拉取原参数数据
                  getQualityInspectParamList(row.id);
                } else {
                  // å¦‚果找不到匹配项,尝试直接使用原值
                  console.warn(
                    "未找到匹配的指标选项,testStandardId:",
                    savedTestStandardId,
                    "可用选项:",
                    testStandardOptions.value
                  );
                  form.value.testStandardId = savedTestStandardId;
                  getQualityInspectParamList(row.id);
                }
              } else {
                // å¦åˆ™ä½¿ç”¨æ—§çš„逻辑
                getQualityInspectParamList(row.id);
              }
            }, 100);
          });
        });
      } else {
        qualityInspectUpdate(data).then(res => {
          proxy.$modal.msgSuccess("提交成功");
          closeDia();
        })
        getQualityInspectParamList(row.id);
      }
    }
  })
}
const getList = () => {
    if (!currentProductId.value) {
        testStandardOptions.value = [];
        tableData.value = [];
        return;
    }
    const processName = form.value.process || '';
    let params = {
        productId: currentProductId.value,
        inspectType: 1,
        process: processName
    }
    qualityInspectDetailByProductId(params).then(res => {
        // ä¿å­˜ä¸‹æ‹‰æ¡†é€‰é¡¹æ•°æ®
        testStandardOptions.value = res.data || [];
        // æ¸…空表格数据,等待用户选择指标
        tableData.value = [];
        // æ¸…空指标选择
        form.value.testStandardId = '';
    })
}
    // æœ€åŽå†æ‰“开弹窗,并清理校验态,避免必填提示闪烁
    dialogFormVisible.value = true;
    nextTick(() => {
      proxy.$refs?.formRef?.clearValidate?.();
    });
  };
  const getProductOptions = () => {
    return productTreeList().then(res => {
      productOptions.value = convertIdToValue(res);
      return productOptions.value;
    });
  };
  const getModels = value => {
    form.value.productModelId = undefined;
    form.value.unit = undefined;
    modelOptions.value = [];
    currentProductId.value = value;
    form.value.productName = findNodeById(productOptions.value, value);
    modelList({ id: value }).then(res => {
      modelOptions.value = res;
    });
    if (currentProductId.value) {
      getList();
    }
  };
// æŒ‡æ ‡é€‰æ‹©å˜åŒ–处理
const handleTestStandardChange = (testStandardId) => {
    if (!testStandardId) {
        tableData.value = [];
        return;
    }
    tableLoading.value = true;
    getQualityTestStandardParamByTestStandardId(testStandardId).then(res => {
        tableData.value = res.data || [];
    }).catch(error => {
        console.error('获取标准参数失败:', error);
        tableData.value = [];
    }).finally(() => {
        tableLoading.value = false;
    })
}
const getQualityInspectParamList = (id) => {
    qualityInspectParamInfo(id).then(res => {
        tableData.value = res.data;
    })
}
// å…³é—­å¼¹æ¡†
const closeDia = () => {
  proxy.resetForm("formRef");
  tableData.value = [];
  testStandardOptions.value = [];
  form.value.testStandardId = '';
  dialogFormVisible.value = false;
  emit('close')
};
defineExpose({
  openDialog,
});
  const handleChangeModel = value => {
    form.value.model =
      modelOptions.value.find(item => item.id == value)?.model || "";
    form.value.unit =
      modelOptions.value.find(item => item.id == value)?.unit || "";
  };
  const handleQualifiedQuantityChange = (value) => {
    if (value === null || value === undefined) {
      form.value.qualifiedQuantity = 0;
      return;
    }
    const quantity = parseFloat(form.value.quantity) || 0;
    const qualified = parseFloat(value) || 0;
    form.value.qualifiedQuantity = qualified > quantity?quantity:qualified;
    form.value.unqualifiedQuantity = Math.max(0, quantity - qualified);
  };
  const handleUnqualifiedQuantityChange = (value) => {
    if (value === null || value === undefined) {
      form.value.unqualifiedQuantity = 0;
      return;
    }
    const quantity = parseFloat(form.value.quantity) || 0;
    const unqualified = parseFloat(value) || 0;
    form.value.unqualifiedQuantity = unqualified > quantity?quantity:unqualified;
    form.value.qualifiedQuantity = Math.max(0, quantity - unqualified);
  };
  const findNodeById = (nodes, productId) => {
    for (let i = 0; i < nodes.length; i++) {
      if (nodes[i].value === productId) {
        return nodes[i].label; // æ‰¾åˆ°èŠ‚ç‚¹ï¼Œè¿”å›žè¯¥èŠ‚ç‚¹
      }
      if (nodes[i].children && nodes[i].children.length > 0) {
        const foundNode = findNodeById(nodes[i].children, productId);
        if (foundNode) {
          return foundNode; // åœ¨å­èŠ‚ç‚¹ä¸­æ‰¾åˆ°ï¼Œè¿”å›žè¯¥èŠ‚ç‚¹
        }
      }
    }
    return null; // æ²¡æœ‰æ‰¾åˆ°èŠ‚ç‚¹ï¼Œè¿”å›žnull
  };
  function convertIdToValue(data) {
    return data.map(item => {
      const { id, children, ...rest } = item;
      const newItem = {
        ...rest,
        value: id, // å°† id æ”¹ä¸º value
      };
      if (children && children.length > 0) {
        newItem.children = convertIdToValue(children);
      }
      return newItem;
    });
  }
  // å·¥åºå˜åŒ–处理
  // æäº¤äº§å“è¡¨å•
  const submitForm = () => {
    proxy.$refs.formRef.validate(valid => {
      if (valid) {
        form.value.inspectType = 1;
        const processName = form.value.process || "";
        if (operationType.value === "add") {
          tableData.value.forEach(item => {
            delete item.id;
          });
        }
        // ç¡®ä¿æ•°é‡ä¸ä¸ºnull
        const quantity = parseFloat(form.value.quantity) || 0;
        const qualified = parseFloat(form.value.qualifiedQuantity) || 0;
        const unqualified = parseFloat(form.value.unqualifiedQuantity) || 0;
        // éªŒè¯æ•°é‡å…³ç³»
        if (qualified + unqualified !== quantity) {
          proxy.$modal.msgError("合格数量与不合格数量之和必须等于总数量");
          return;
        }
        const data = {
          ...form.value,
          process: processName, // ä¿ç•™ process å­—段以兼容后端
          qualityInspectParams: tableData.value,
        };
        if (operationType.value === "add") {
          qualityInspectAdd(data).then(res => {
            proxy.$modal.msgSuccess("提交成功");
            closeDia();
          });
        } else {
          qualityInspectUpdate(data).then(res => {
            proxy.$modal.msgSuccess("提交成功");
            closeDia();
          });
        }
      }
    });
  };
  const getList = () => {
    if (!currentProductId.value) {
      testStandardOptions.value = [];
      tableData.value = [];
      return;
    }
    const processName = form.value.process || "";
    let params = {
      productId: currentProductId.value,
      inspectType: 1,
      process: processName,
    };
    qualityInspectDetailByProductId(params).then(res => {
      // ä¿å­˜ä¸‹æ‹‰æ¡†é€‰é¡¹æ•°æ®
      testStandardOptions.value = res.data || [];
      // æ¸…空表格数据,等待用户选择指标
      tableData.value = [];
      // æ¸…空指标选择
      form.value.testStandardId = "";
    });
  };
  // æŒ‡æ ‡é€‰æ‹©å˜åŒ–处理
  const handleTestStandardChange = testStandardId => {
    if (!testStandardId) {
      tableData.value = [];
      return;
    }
    tableLoading.value = true;
    getQualityTestStandardParamByTestStandardId(testStandardId)
      .then(res => {
        tableData.value = res.data || [];
        tableData.value = tableData.value.map(item => ({
          ...item,
          id: null
        }));
      })
      .catch(error => {
        console.error("获取标准参数失败:", error);
        tableData.value = [];
      })
      .finally(() => {
        tableLoading.value = false;
      });
  };
  const getQualityInspectParamList = id => {
    qualityInspectParamInfo(id).then(res => {
      tableData.value = res.data;
    });
  };
  // å…³é—­å¼¹æ¡†
  const closeDia = () => {
    proxy.resetForm("formRef");
    tableData.value = [];
    testStandardOptions.value = [];
    form.value.testStandardId = "";
    dialogFormVisible.value = false;
    emit("close");
  };
  defineExpose({
    openDialog,
  });
</script>
<style scoped>
</style>
</style>
src/views/qualityManagement/processInspection/index.vue
@@ -122,8 +122,18 @@
    prop: "unit",
  },
  {
    label: "数量",
    label: "总数量",
    prop: "quantity",
    width: 100
  },
  {
    label: "合格数量",
    prop: "qualifiedQuantity",
    width: 100
  },
  {
    label: "不合格数量",
    prop: "unqualifiedQuantity",
    width: 100
  },
  {
@@ -141,7 +151,7 @@
      } else if (params == '合格') {
        return "success";
      } else {
        return null;
        return 'danger';
      }
    },
  },
@@ -178,6 +188,13 @@
                    }
                    return false;
                }
      },
      {
        name: "查看",
        type: "text",
        clickFun: (row) => {
          openForm("view", row);
        },
      },
      {
        name: "附件",
@@ -363,13 +380,13 @@
            type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
        })
        const downloadUrl = window.URL.createObjectURL(blob)
        const link = document.createElement('a')
        link.href = downloadUrl
        link.download = '过程检验报告.docx'
        document.body.appendChild(link)
        link.click()
        document.body.removeChild(link)
        window.URL.revokeObjectURL(downloadUrl)
    })
src/views/qualityManagement/rawMaterialInspection/components/formDia.vue
@@ -2,7 +2,7 @@
  <div>
    <el-dialog
        v-model="dialogFormVisible"
        :title="operationType === 'add' ? '新增原材料检验' : '编辑原材料检验'"
        :title="operationType === 'add' ? '新增原材料检验' : operationType === 'view' ? '查看原材料检验' : '编辑原材料检验'"
        width="70%"
        @close="closeDia"
    >
@@ -14,7 +14,7 @@
                  v-model="form.supplier"
                  placeholder="请选择"
                  clearable
                  :disabled="supplierQuantityDisabled"
                  :disabled="isViewMode || supplierQuantityDisabled"
              >
                <el-option
                    v-for="item in supplierList"
@@ -35,7 +35,7 @@
                  @change="getModels"
                  :data="productOptions"
                  :render-after-expand="false"
                  :disabled="operationType === 'edit'"
                  :disabled="isViewMode || operationType === 'edit'"
                  style="width: 100%"
              />
            </el-form-item>
@@ -44,7 +44,7 @@
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="规格型号:" prop="productModelId">
              <el-select v-model="form.productModelId" placeholder="请选择" clearable :disabled="operationType === 'edit'"
              <el-select v-model="form.productModelId" placeholder="请选择" clearable :disabled="isViewMode || operationType === 'edit'"
                         filterable readonly @change="handleChangeModel">
                <el-option v-for="item in modelOptions" :key="item.id" :label="item.model" :value="item.id" />
              </el-select>
@@ -58,6 +58,7 @@
                clearable
                @change="handleTestStandardChange"
                style="width: 100%"
                :disabled="isViewMode"
              >
                <el-option
                  v-for="item in testStandardOptions"
@@ -78,21 +79,39 @@
          <el-col :span="12">
            <el-form-item label="数量:" prop="quantity">
              <el-input-number :step="0.01" :min="0" style="width: 100%" v-model="form.quantity" placeholder="请输入"
                               clearable :precision="2" :disabled="supplierQuantityDisabled"/>
                               clearable :precision="2" :disabled="isViewMode || supplierQuantityDisabled"/>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="合格数量:" prop="qualifiedQuantity">
              <el-input-number :step="0.01" :min="0" :max="form.quantity || 0" style="width: 100%"
                               v-model="form.qualifiedQuantity" placeholder="请输入" :precision="2"
                               @change="onQualifiedChange" :disabled="isViewMode"/>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="不合格数量:" prop="unqualifiedQuantity">
              <el-input-number :step="0.01" :min="0" :max="form.quantity || 0" style="width: 100%"
                               v-model="form.unqualifiedQuantity" placeholder="请输入" :precision="2"
                               @change="onUnqualifiedChange" :disabled="isViewMode"/>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="检测单位:" prop="checkCompany">
              <el-input v-model="form.checkCompany" placeholder="请输入" clearable/>
              <el-input v-model="form.checkCompany" placeholder="请输入" clearable :disabled="isViewMode"/>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="检测结果:" prop="checkResult">
              <el-select v-model="form.checkResult">
              <el-select v-model="form.checkResult" :disabled="isViewMode">
                <el-option label="合格" value="合格"/>
                <el-option label="不合格" value="不合格"/>
                <el-option label="部分合格" value="部分合格"/>
              </el-select>
            </el-form-item>
          </el-col>
@@ -100,7 +119,7 @@
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="检验员:" prop="checkName">
              <el-select v-model="form.checkName" placeholder="请选择" clearable style="width: 100%">
              <el-select v-model="form.checkName" placeholder="请选择" clearable style="width: 100%" :disabled="isViewMode">
                <el-option v-for="item in userList" :key="item.nickName" :label="item.nickName" :value="item.nickName"/>
              </el-select>
            </el-form-item>
@@ -115,6 +134,7 @@
                  format="YYYY-MM-DD"
                  clearable
                  style="width: 100%"
                  :disabled="isViewMode"
              />
            </el-form-item>
          </el-col>
@@ -131,13 +151,16 @@
          height="400"
      >
        <template #slot="{ row }">
          <el-input v-model="row.testValue" clearable/>
          <el-input v-model="row.testValue" clearable :disabled="isViewMode"/>
        </template>
      </PIMTable>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitForm">确认</el-button>
          <el-button @click="closeDia">取消</el-button>
          <template v-if="!isViewMode">
            <el-button type="primary" @click="submitForm">确认</el-button>
            <el-button @click="closeDia">取消</el-button>
          </template>
          <el-button v-else @click="closeDia">关闭</el-button>
        </div>
      </template>
    </el-dialog>
@@ -182,6 +205,8 @@
    testStandardId: [{required: false, message: "请选择指标", trigger: "change"}],
    unit: [{required: false, message: "请输入", trigger: "blur"}],
    quantity: [{required: true, message: "请输入", trigger: "blur"}],
    qualifiedQuantity: [{required: true, message: "请输入", trigger: "blur"}],
    unqualifiedQuantity: [{required: true, message: "请输入", trigger: "blur"}],
    checkCompany: [{required: false, message: "请输入", trigger: "blur"}],
    checkResult: [{required: true, message: "请选择检测结果", trigger: "change"}],
  },
@@ -220,6 +245,9 @@
const testStandardOptions = ref([]); // æŒ‡æ ‡é€‰æ‹©ä¸‹æ‹‰æ¡†æ•°æ®
const modelOptions = ref([]);
const userList = ref([]); // æ£€éªŒå‘˜ä¸‹æ‹‰åˆ—表
// æ˜¯å¦ä¸ºæŸ¥çœ‹æ¨¡å¼
const isViewMode = computed(() => operationType.value === 'view');
// ç¼–辑时:productMainId æˆ– purchaseLedgerId ä»»ä¸€æœ‰å€¼åˆ™ä¾›åº”商、数量置灰
const supplierQuantityDisabled = computed(() => {
@@ -260,7 +288,7 @@
  tableData.value = [];
  // å…ˆç¡®ä¿äº§å“æ ‘已加载,否则编辑时产品/规格型号无法反显
  await getProductOptions();
  if (operationType.value === 'edit') {
  if (operationType.value === 'edit' || operationType.value === 'view') {
    // å…ˆä¿å­˜ testStandardId,避免被清空
    const savedTestStandardId = row.testStandardId;
    form.value = {...row}
@@ -294,7 +322,7 @@
            // å¦‚果编辑数据中有 testStandardId,则设置并加载对应的参数
            if (savedTestStandardId) {
              // ç¡®ä¿ç±»åž‹åŒ¹é…ï¼ˆitem.id å¯èƒ½æ˜¯æ•°å­—或字符串)
              const matchedOption = testStandardOptions.value.find(item =>
              const matchedOption = testStandardOptions.value.find(item =>
                item.id == savedTestStandardId || String(item.id) === String(savedTestStandardId)
              );
              if (matchedOption) {
@@ -435,6 +463,10 @@
  tableLoading.value = true;
  getQualityTestStandardParamByTestStandardId(testStandardId).then(res => {
    tableData.value = res.data || [];
    tableData.value = tableData.value.map(item => ({
      ...item,
      id: null
    }));
  }).catch(error => {
    console.error('获取标准参数失败:', error);
    tableData.value = [];
@@ -448,6 +480,32 @@
    tableData.value = res.data;
  })
}
// è‡ªåŠ¨è®¡ç®—åˆæ ¼æ•°é‡å˜åŒ–æ—¶çš„ä¸åˆæ ¼æ•°é‡
const onQualifiedChange = (value) => {
  if (form.value.quantity !== undefined && form.value.quantity !== null) {
    const maxUnqualified = form.value.quantity - value;
    if (maxUnqualified >= 0) {
      form.value.unqualifiedQuantity = maxUnqualified;
    } else {
      form.value.qualifiedQuantity = form.value.quantity;
      form.value.unqualifiedQuantity = 0;
    }
  }
};
// è‡ªåŠ¨è®¡ç®—ä¸åˆæ ¼æ•°é‡å˜åŒ–æ—¶çš„åˆæ ¼æ•°é‡
const onUnqualifiedChange = (value) => {
  if (form.value.quantity !== undefined && form.value.quantity !== null) {
    const maxQualified = form.value.quantity - value;
    if (maxQualified >= 0) {
      form.value.qualifiedQuantity = maxQualified;
    } else {
      form.value.unqualifiedQuantity = form.value.quantity;
      form.value.qualifiedQuantity = 0;
    }
  }
};
// å…³é—­å¼¹æ¡†
const closeDia = () => {
  proxy.resetForm("formRef");
@@ -464,4 +522,4 @@
<style scoped>
</style>
</style>
src/views/qualityManagement/rawMaterialInspection/index.vue
@@ -124,8 +124,23 @@
    prop: "unit",
  },
  {
    label: "数量",
    label: "总数量",
    prop: "quantity",
    width: 100
  },
  {
    label: "合格数量",
    prop: "qualifiedQuantity",
    width: 100
  },
  {
    label: "不合格数量",
    prop: "unqualifiedQuantity",
    width: 100
  },
  {
    label: "检测单位",
    prop: "checkCompany",
    width: 120
  },
  {
@@ -143,7 +158,7 @@
      } else if (params === '合格') {
        return "success";
      } else {
        return null;
        return 'danger';
      }
    },
  },
@@ -182,6 +197,13 @@
                }
      },
      {
        name: "查看",
        type: "text",
        clickFun: (row) => {
          openForm("view", row);
        },
      },
      {
        name: "附件",
        type: "text",
        clickFun: (row) => {
src/views/reportAnalysis/PSIDataAnalysis/components/center-bottom.vue
@@ -23,7 +23,7 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, inject, watch } from 'vue'
import * as echarts from 'echarts'
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from './PanelHeader.vue'
@@ -151,6 +151,13 @@
  fetchData()
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    fetchData()
  })
}
onMounted(() => {
  fetchData()
})
src/views/reportAnalysis/PSIDataAnalysis/components/center-center.vue
@@ -27,7 +27,7 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, inject, watch } from 'vue'
import Echarts from '@/components/Echarts/echarts.vue'
import { productTurnoverDays } from '@/api/viewIndex.js'
@@ -82,6 +82,13 @@
    })
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    fetchData()
  })
}
onMounted(() => {
  fetchData()
})
src/views/reportAnalysis/PSIDataAnalysis/components/center-top.vue
@@ -24,7 +24,7 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, inject, watch } from 'vue'
import { salesPurchaseStorageProductCount } from '@/api/viewIndex.js'
const statItems = ref([])
@@ -52,6 +52,13 @@
    })
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    fetchData()
  })
}
onMounted(() => {
  fetchData()
})
src/views/reportAnalysis/PSIDataAnalysis/components/left-bottom.vue
@@ -22,7 +22,7 @@
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
import { ref, onMounted, onBeforeUnmount, computed, inject, watch } from 'vue'
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from './PanelHeader.vue'
import CarouselCards from './CarouselCards.vue'
@@ -205,6 +205,13 @@
    })
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    fetchData()
  })
}
onMounted(() => {
  fetchData()
  initBackground()
src/views/reportAnalysis/PSIDataAnalysis/components/left-top.vue
@@ -21,7 +21,7 @@
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
import { ref, onMounted, onBeforeUnmount, computed, inject, watch } from 'vue'
import { productSalesAnalysis } from '@/api/viewIndex.js'
import PanelHeader from './PanelHeader.vue'
import CarouselCards from './CarouselCards.vue'
@@ -175,6 +175,13 @@
    })
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    fetchData()
  })
}
onMounted(() => {
  fetchData()
  initBackground()
src/views/reportAnalysis/PSIDataAnalysis/index.vue
@@ -43,7 +43,7 @@
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { ref, onMounted, onBeforeUnmount, nextTick, provide } from 'vue'
import autofit from 'autofit.js'
import LeftBottom from './components/left-bottom.vue'
import CenterCenter from './components/center-center.vue'
@@ -65,6 +65,12 @@
// ç”¨æˆ·store
const userStore = useUserStore()
/** ä¸Ž dataDashboard å…±ç”¨æ³¨å…¥åï¼Œå­ç»„件(含复用的 right-top/right-bottom)每分钟刷新 */
const DASHBOARD_REFRESH_MS = 60 * 1000
const dataDashboardRefreshTick = ref(0)
provide('dataDashboardRefreshTick', dataDashboardRefreshTick)
let dashboardPollTimer = null
// è®¡ç®—缩放比例
const calculateScale = () => {
@@ -140,9 +146,17 @@
  window.addEventListener('fullscreenchange', handleFullscreenChange)
  window.addEventListener('webkitfullscreenchange', handleFullscreenChange)
  window.addEventListener('MSFullscreenChange', handleFullscreenChange)
  dashboardPollTimer = setInterval(() => {
    dataDashboardRefreshTick.value++
  }, DASHBOARD_REFRESH_MS)
})
onBeforeUnmount(() => {
  if (dashboardPollTimer) {
    clearInterval(dashboardPollTimer)
    dashboardPollTimer = null
  }
  window.removeEventListener('resize', handleResize)
  window.removeEventListener('fullscreenchange', handleFullscreenChange)
  window.removeEventListener('webkitfullscreenchange', handleFullscreenChange)
src/views/reportAnalysis/dataDashboard/components/basic/center-bottom.vue
@@ -20,7 +20,7 @@
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { ref, onMounted, computed, inject, watch } from 'vue'
import { deptStaffDistribution } from '@/api/viewIndex.js'
import PanelHeader from '../PanelHeader.vue'
import Echarts from '@/components/Echarts/echarts.vue'
@@ -148,6 +148,13 @@
  })
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    getDeptStaffDistribution()
  })
}
onMounted(() => {
  getDeptStaffDistribution()
})
src/views/reportAnalysis/dataDashboard/components/basic/center-top.vue
@@ -110,7 +110,7 @@
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { ref, onMounted, onBeforeUnmount, nextTick, inject, watch } from 'vue'
import { homeTodos, summaryStatistics } from '@/api/viewIndex.js'
import { getLedgerPage } from '@/api/equipmentManagement/ledger.js'
import { getRepairPage } from '@/api/equipmentManagement/repair.js'
@@ -175,8 +175,23 @@
  })
}
const destroyTodoListScroll = () => {
  const todoListEl = refTodoList.value
  if (todoListEl) {
    if (todoListEl._animationFrame) {
      cancelAnimationFrame(todoListEl._animationFrame)
      todoListEl._animationFrame = null
    }
    if (todoListEl._pauseTimer) {
      clearInterval(todoListEl._pauseTimer)
      todoListEl._pauseTimer = null
    }
  }
}
// åˆå§‹åŒ–待办事项列表滚动功能
const initTodoListScroll = () => {
  destroyTodoListScroll()
  const todoListEl = refTodoList.value
  // å¼ºåˆ¶å¯ç”¨æ»šåŠ¨ï¼Œä¸æ£€æŸ¥ä»»ä½•æ¡ä»¶
  if (todoListEl) {
@@ -259,6 +274,7 @@
// å¾…办事项
const todoInfoS = () => {
  destroyTodoListScroll()
  homeTodos().then((res) => {
    todoList.value = res.data
    // åœ¨èŽ·å–åˆ°å¾…åŠžäº‹é¡¹æ•°æ®åŽï¼Œåˆå§‹åŒ–æ»šåŠ¨åŠŸèƒ½
@@ -268,25 +284,25 @@
  })
}
onMounted(() => {
const refreshCenterTopData = () => {
  getNum()
  getLedgerNum()
  todoInfoS()
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    refreshCenterTopData()
  })
}
onMounted(() => {
  refreshCenterTopData()
})
onBeforeUnmount(() => {
  // æ¸…理待办事项列表的动画和定时器
  const todoListEl = refTodoList.value
  if (todoListEl) {
    if (todoListEl._animationFrame) {
      cancelAnimationFrame(todoListEl._animationFrame)
      todoListEl._animationFrame = null
    }
    if (todoListEl._pauseTimer) {
      clearInterval(todoListEl._pauseTimer)
      todoListEl._pauseTimer = null
    }
  }
  destroyTodoListScroll()
})
</script>
src/views/reportAnalysis/dataDashboard/components/basic/left-bottom.vue
@@ -40,7 +40,7 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, inject, watch } from 'vue'
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from '../PanelHeader.vue'
import DateTypeSwitch from '../DateTypeSwitch.vue'
@@ -192,6 +192,13 @@
  getCustomerRevenueAnalysis()
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    getCustomerRevenueAnalysis()
  })
}
onMounted(() => {
  fetchCustomerOptions()
})
src/views/reportAnalysis/dataDashboard/components/basic/left-top.vue
@@ -21,7 +21,7 @@
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { ref, onMounted, onBeforeUnmount, inject, watch } from 'vue'
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from '../PanelHeader.vue'
import { productCategoryDistribution } from '@/api/viewIndex.js'
@@ -207,6 +207,13 @@
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    loadData()
  })
}
onMounted(() => {
  loadData()
  initBackground()
src/views/reportAnalysis/dataDashboard/components/basic/right-bottom.vue
@@ -22,7 +22,7 @@
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { ref, onMounted, computed, inject, watch } from 'vue'
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from '../PanelHeader.vue'
import DateTypeSwitch from '../DateTypeSwitch.vue'
@@ -308,6 +308,13 @@
  fetchCustomerRanking()
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    fetchCustomerRanking()
  })
}
onMounted(() => {
  fetchCustomerRanking()
})
src/views/reportAnalysis/dataDashboard/components/basic/right-top.vue
@@ -21,7 +21,7 @@
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { ref, onMounted, computed, inject, watch } from 'vue'
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from '../PanelHeader.vue'
import DateTypeSwitch from '../DateTypeSwitch.vue'
@@ -331,6 +331,13 @@
  fetchSupplierRanking()
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    fetchSupplierRanking()
  })
}
onMounted(() => {
  fetchSupplierRanking()
})
src/views/reportAnalysis/dataDashboard/index.vue
@@ -44,7 +44,7 @@
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { ref, onMounted, onBeforeUnmount, nextTick, provide } from 'vue'
import autofit from 'autofit.js'
import LeftTop from './components/basic/left-top.vue'
import LeftBottom from './components/basic/left-bottom.vue'
@@ -65,6 +65,12 @@
// ç”¨æˆ·store
const userStore = useUserStore()
// å¤§å±æŽ¥å£è½®è¯¢é—´éš”
const DASHBOARD_REFRESH_MS = 60 * 1000
const dataDashboardRefreshTick = ref(0)
provide('dataDashboardRefreshTick', dataDashboardRefreshTick)
let dashboardPollTimer = null
// è®¡ç®—缩放比例
const calculateScale = () => {
@@ -140,9 +146,17 @@
  window.addEventListener('fullscreenchange', handleFullscreenChange)
  window.addEventListener('webkitfullscreenchange', handleFullscreenChange)
  window.addEventListener('MSFullscreenChange', handleFullscreenChange)
  dashboardPollTimer = setInterval(() => {
    dataDashboardRefreshTick.value++
  }, DASHBOARD_REFRESH_MS)
})
onBeforeUnmount(() => {
  if (dashboardPollTimer) {
    clearInterval(dashboardPollTimer)
    dashboardPollTimer = null
  }
  window.removeEventListener('resize', handleResize)
  window.removeEventListener('fullscreenchange', handleFullscreenChange)
  window.removeEventListener('webkitfullscreenchange', handleFullscreenChange)
src/views/reportAnalysis/productionAnalysis/components/center-bottom.vue
@@ -51,7 +51,7 @@
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { ref, onMounted, onBeforeUnmount, nextTick, inject, watch } from 'vue'
import { getProgressStatistics } from '@/api/viewIndex.js'
import PanelHeader from './PanelHeader.vue'
import CarouselCards from './CarouselCards.vue'
@@ -132,17 +132,22 @@
  }, 150)
}
const initProgressTableScroll = () => {
  const tableContainer = progressTableRef.value
  if (!tableContainer) return
const stopProgressTableScroll = () => {
  if (progressTableScrollTimer.value) {
    cancelAnimationFrame(progressTableScrollTimer.value)
    progressTableScrollTimer.value = null
  }
  if (tableContainer._pauseTimer) {
  const tableContainer = progressTableRef.value
  if (tableContainer?._pauseTimer) {
    clearInterval(tableContainer._pauseTimer)
    tableContainer._pauseTimer = null
  }
}
const initProgressTableScroll = () => {
  const tableContainer = progressTableRef.value
  if (!tableContainer) return
  stopProgressTableScroll()
  const tbody = tableContainer.querySelector('tbody')
  if (!tbody) return
  const originalCount = progressTableData.value.length
@@ -198,6 +203,7 @@
const progressStatisticsInfo = () => {
  getProgressStatistics()
    .then((res) => {
      stopProgressTableScroll()
      if (!res || !res.data) return
      const obj = {
        totalOrderCount: res.data.totalOrderCount || 0,
@@ -224,14 +230,19 @@
    })
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    progressStatisticsInfo()
  })
}
onMounted(() => {
  progressStatisticsInfo()
})
onBeforeUnmount(() => {
  if (progressTableScrollTimer.value) {
    cancelAnimationFrame(progressTableScrollTimer.value)
  }
  stopProgressTableScroll()
  if (tableScrollTimeout.value) clearTimeout(tableScrollTimeout.value)
  const tableContainer = progressTableRef.value
  if (tableContainer?._pauseTimer) {
src/views/reportAnalysis/productionAnalysis/components/center-center.vue
@@ -27,7 +27,7 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, inject, watch } from 'vue'
import * as echarts from 'echarts'
import Echarts from '@/components/Echarts/echarts.vue'
import { inputOutputAnalysis } from '@/api/viewIndex.js'
@@ -146,6 +146,13 @@
    })
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    fetchData()
  })
}
onMounted(() => {
  fetchData()
})
src/views/reportAnalysis/productionAnalysis/components/center-top.vue
@@ -24,7 +24,7 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, inject, watch } from 'vue'
import { orderCount } from '@/api/viewIndex.js'
const statItems = ref([])
@@ -52,6 +52,13 @@
    })
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    fetchData()
  })
}
onMounted(() => {
  fetchData()
})
src/views/reportAnalysis/productionAnalysis/components/left-bottom.vue
@@ -22,7 +22,7 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, inject, watch } from 'vue'
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from './PanelHeader.vue'
import CarouselCards from './CarouselCards.vue'
@@ -143,6 +143,13 @@
    })
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    workInProcessTurnoverInfo()
  })
}
onMounted(() => {
  workInProcessTurnoverInfo()
})
src/views/reportAnalysis/productionAnalysis/components/left-top.vue
@@ -23,7 +23,7 @@
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
import { ref, onMounted, onBeforeUnmount, computed, inject, watch } from 'vue'
import { processOutputAnalysis } from '@/api/viewIndex.js'
import PanelHeader from './PanelHeader.vue'
import Echarts from '@/components/Echarts/echarts.vue'
@@ -170,6 +170,13 @@
  fetchData()
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    fetchData()
  })
}
onMounted(() => {
  fetchData()
  initBackground()
src/views/reportAnalysis/productionAnalysis/components/right-bottom.vue
@@ -22,7 +22,7 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, inject, watch } from 'vue'
import { productionAccountingAnalysis } from '@/api/viewIndex.js'
import PanelHeader from './PanelHeader.vue'
import DateTypeSwitch from './DateTypeSwitch.vue'
@@ -157,6 +157,13 @@
    })
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    fetchData()
  })
}
onMounted(() => {
  fetchData()
})
src/views/reportAnalysis/productionAnalysis/components/right-top.vue
@@ -22,7 +22,7 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, inject, watch } from 'vue'
import { workOrderEfficiencyAnalysis } from '@/api/viewIndex.js'
import PanelHeader from './PanelHeader.vue'
import Echarts from '@/components/Echarts/echarts.vue'
@@ -152,6 +152,13 @@
    })
}
const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
if (dataDashboardRefreshTick) {
  watch(dataDashboardRefreshTick, () => {
    fetchData()
  })
}
onMounted(() => {
  fetchData()
})
src/views/reportAnalysis/productionAnalysis/index.vue
@@ -44,7 +44,7 @@
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { ref, onMounted, onBeforeUnmount, nextTick, provide } from 'vue'
import autofit from 'autofit.js'
import LeftBottom from './components/left-bottom.vue'
import CenterCenter from './components/center-center.vue'
@@ -66,6 +66,12 @@
// ç”¨æˆ·store
const userStore = useUserStore()
/** ä¸Žå…¶å®ƒé©¾é©¶èˆ±å…±ç”¨æ³¨å…¥åï¼Œå­ç»„件每分钟刷新接口数据 */
const DASHBOARD_REFRESH_MS = 60 * 1000
const dataDashboardRefreshTick = ref(0)
provide('dataDashboardRefreshTick', dataDashboardRefreshTick)
let dashboardPollTimer = null
// è®¡ç®—缩放比例
const calculateScale = () => {
@@ -141,9 +147,17 @@
  window.addEventListener('fullscreenchange', handleFullscreenChange)
  window.addEventListener('webkitfullscreenchange', handleFullscreenChange)
  window.addEventListener('MSFullscreenChange', handleFullscreenChange)
  dashboardPollTimer = setInterval(() => {
    dataDashboardRefreshTick.value++
  }, DASHBOARD_REFRESH_MS)
})
onBeforeUnmount(() => {
  if (dashboardPollTimer) {
    clearInterval(dashboardPollTimer)
    dashboardPollTimer = null
  }
  window.removeEventListener('resize', handleResize)
  window.removeEventListener('fullscreenchange', handleFullscreenChange)
  window.removeEventListener('webkitfullscreenchange', handleFullscreenChange)
src/views/safeProduction/safeWorkApproval/components/infoFormDia.vue
@@ -218,7 +218,7 @@
      approveDeptName: "",
      approveReason: "",
      checkResult: "",
      storageBlobDTOs: [],
      storageBlobDTOS: [],
      approverList: [], // æ–°å¢žå­—段,存储所有节点的审批人id
      startDate: "", // è¯·å‡å¼€å§‹æ—¶é—´
      endDate: "", // è¯·å‡ç»“束时间
@@ -298,7 +298,7 @@
      approveProcessGetInfo({ id: row.approveId, approveReason: "1" }).then(
        res => {
          form.value = { ...res.data };
          form.value.storageBlobDTOs = res.data.storageBlobVOS;
          form.value.storageBlobDTOS = res.data.storageBlobVOS;
          // åæ˜¾å®¡æ‰¹äºº
          if (res.data && res.data.approveUserIds) {
            const userIds = res.data.approveUserIds.split(",");
@@ -388,7 +388,7 @@
        return;
      }
    }
    form.value.storageBlobDTOs = fileList.value;
    form.value.storageBlobDTOS = fileList.value;
    proxy.$refs.formRef.validate(valid => {
      if (valid) {
        if (operationType.value === "add" || currentApproveStatus.value == 3) {
src/views/salesManagement/returnOrder/components/detailDia.vue
@@ -20,7 +20,11 @@
      <div style="padding-top: 20px">
        <span class="descriptions">产品列表</span>
        <PIMTable :isShowPagination="false" rowKey="id" :column="tableColumn" :tableData="tableData" />
        <PIMTable :isShowPagination="false" rowKey="id" :column="tableColumn" :tableData="tableData">
          <template #totalReturnNum="{ row }">
            {{ calcAlreadyReturned(row) }}
          </template>
        </PIMTable>
      </div>
    </div>
    <template #footer>
@@ -101,6 +105,13 @@
const firstNonEmptyText = (...vals) => {
  const hit = vals.find((v) => !isEmptyText(v));
  return hit === undefined ? "" : hit;
};
const calcAlreadyReturned = (row) => {
  const total = Number(row?.stockOutNum ?? row?.totalQuantity ?? row?.totalReturnNum ?? 0);
  const un = Number(row?.unQuantity ?? 0);
  if (!Number.isFinite(total) || !Number.isFinite(un)) return 0;
  return Math.max(total - un, 0);
};
/** è¯¦æƒ…表用 productName / model;合并时勿让空串盖掉出库行字段 */
@@ -241,7 +252,7 @@
  {align: "center", label: "规格型号", prop: "model"},
  {align: "center", label: "单位", prop: "unit", width: 80},
  {align: "center", label: "总数量", prop: "stockOutNum", width: 120},
  {align: "center", label: "已退货数量", prop: "totalReturnNum", width: 120},
  {align: "center", label: "已退货数量", prop: "totalReturnNum", width: 120, dataType: "slot", slot: "totalReturnNum"},
  {align: "center", label: "未退货数量", prop: "unQuantity", width: 120},
  {align: "center", label: "退货数量", prop: "returnQuantity", width: 120},
  {align: "center", label: "退货产品单价", prop: "price", width: 120},
src/views/salesManagement/returnOrder/components/formDia.vue
@@ -82,6 +82,9 @@
            <el-button type="primary" @click="openProductSelection" :disabled="!form.shippingId">添加产品</el-button>
          </div>
          <PIMTable :isShowPagination="false" rowKey="id" :column="tableColumn" :tableData="tableData">
            <template #totalReturnNum="{ row }">
              {{ calcAlreadyReturned(row) }}
            </template>
            <template #returnQuantity="{ row }">
              <el-input 
                v-model="row.returnQuantity" 
@@ -223,7 +226,7 @@
  {align: "center", label: "规格型号", prop: "specificationModel" },
  {align: "center", label: "单位", prop: "unit", width: 80 },
  {align: "center", label: "总数量", prop: "stockOutNum", width: 120 },
  {align: "center", label: "已退货数量", prop: "totalReturnNum", width: 120 },
  {align: "center", label: "已退货数量", prop: "totalReturnNum", width: 120, dataType: "slot", slot: "totalReturnNum" },
  {align: "center", label: "未退货数量", prop: "unQuantity", width: 120 },
  {align: "center", label: "退货数量", prop: "returnQuantity", dataType: "slot", slot: "returnQuantity", width: 120 },
  {align: "center", label: "退货产品单价", prop: "price", dataType: "slot", slot: "price", width: 120 },
src/views/salesManagement/salesLedger/index.vue
@@ -1578,7 +1578,7 @@
    selectedQuotation.value = null;
    let userLists = await userListNoPage();
    userList.value = userLists.data;
    listCustomer({ current: -1, size: -1 }).then(res => {
    listCustomer({ current: -1, size: -1, type: 0 }).then(res => {
      customerOption.value = res.data.records;
    });
    form.value.entryPerson = userStore.id;
@@ -1705,6 +1705,8 @@
        taxExclusiveTotalPrice: taxExclusiveTotalPrice,
        invoiceType: "增普票",
        isProduction: true,
        productId: p.productId,
        productModelId: p.productModelId
      };
    });
@@ -2594,6 +2596,7 @@
      å®¡æ ¸æ‹’绝: "审核拒绝",
      å®¡æ ¸é€šè¿‡: "审核通过",
      å·²å‘è´§: "已发货",
      éƒ¨åˆ†å‘è´§: "部分发货",
    };
    return statusTextMap[statusStr] || "待发货";
  };
@@ -2625,6 +2628,7 @@
      å®¡æ ¸æ‹’绝: "danger",
      å®¡æ ¸é€šè¿‡: "success",
      å·²å‘è´§: "success",
      éƒ¨åˆ†å‘è´§: "warning",
    };
    return typeTextMap[statusStr] || "info";
  };
@@ -2650,7 +2654,7 @@
    // å‘货状态必须是"待发货"或"审核拒绝"
    const statusStr = shippingStatus ? String(shippingStatus).trim() : "";
    return statusStr === "待发货" || statusStr === "审核拒绝";
    return statusStr === "待发货" || statusStr === "审核拒绝" || statusStr === "部分发货";
  };
  // æ‰“开附件弹窗
src/views/salesManagement/salesQuotation/index.vue
@@ -187,9 +187,9 @@
            </el-table-column>
            <el-table-column prop="specification" label="规格型号" width="200">
              <template #default="scope">
                <el-form-item :prop="`products.${scope.$index}.specificationId`" class="product-table-form-item">
                <el-form-item :prop="`products.${scope.$index}.productModelId`" class="product-table-form-item">
                  <el-select
                    v-model="scope.row.specificationId"
                    v-model="scope.row.productModelId"
                    placeholder="请选择"
                    clearable
                    @change="getProductModel($event, scope.row)"
@@ -239,10 +239,10 @@
          </template>
          <div class="form-content">
            <el-form-item label="备注" prop="remark">
              <el-input
                type="textarea"
                v-model="form.remark"
                placeholder="请输入备注信息(选填)"
              <el-input
                type="textarea"
                v-model="form.remark"
                placeholder="请输入备注信息(选填)"
                :rows="4"
                maxlength="500"
                show-word-limit
@@ -270,7 +270,7 @@
          <span style="font-size: 18px; color: #e6a23c; font-weight: bold;">Â¥{{ currentQuotation.totalAmount?.toFixed(2) }}</span>
        </el-descriptions-item>
      </el-descriptions>
      <div style="margin: 20px 0;">
        <h4>产品明细</h4>
        <el-table :data="currentQuotation.products" border style="width: 100%">
@@ -354,7 +354,7 @@
const productRowRules = {
  productId: [{ required: true, message: '请选择产品名称', trigger: 'change' }],
  specificationId: [{ required: true, message: '请选择规格型号', trigger: 'change' }],
  productModelId: [{ required: true, message: '请选择规格型号', trigger: 'change' }],
  unit: [{ required: true, message: '请填写单位', trigger: 'blur' }],
  unitPrice: [{ required: true, message: '请填写单价', trigger: 'change' }]
}
@@ -362,7 +362,7 @@
  const r = { ...baseRules }
  ;(form.products || []).forEach((_, i) => {
    r[`products.${i}.productId`] = productRowRules.productId
    r[`products.${i}.specificationId`] = productRowRules.specificationId
    r[`products.${i}.productModelId`] = productRowRules.productModelId
    r[`products.${i}.unit`] = productRowRules.unit
    r[`products.${i}.unitPrice`] = productRowRules.unitPrice
  })
@@ -433,7 +433,7 @@
        if (children && children.length > 0) {
            newItem.children = convertIdToValue(children);
        }
        return newItem;
    });
}
@@ -457,7 +457,7 @@
        row.productId = '';
        row.product = '';
        row.modelOptions = [];
        row.specificationId = '';
        row.productModelId = '';
        row.specification = '';
        row.unit = '';
        return;
@@ -478,13 +478,13 @@
    if (!row) return;
    // å¦‚果清空选择,则清空相关字段
    if (!value) {
        row.specificationId = '';
        row.productModelId = '';
        row.specification = '';
        row.unit = '';
        return;
    }
    // æ›´æ–° specificationId(v-model å·²ç»è‡ªåŠ¨æ›´æ–°ï¼Œè¿™é‡Œç¡®ä¿ä¸€è‡´æ€§ï¼‰
    row.specificationId = value;
    // æ›´æ–° productModelId(v-model å·²ç»è‡ªåŠ¨æ›´æ–°ï¼Œè¿™é‡Œç¡®ä¿ä¸€è‡´æ€§ï¼‰
    row.productModelId = value;
    const modelOptions = row.modelOptions || [];
    const index = modelOptions.findIndex((item) => item.id === value);
    if (index !== -1) {
@@ -523,7 +523,7 @@
    products: row.products ? row.products.map(product => ({
      productId: product.productId || '',
      product: product.product || product.productName || '',
      specificationId: product.specificationId || '',
      productModelId: product.productModelId || '',
      specification: product.specification || '',
      quantity: product.quantity || 0,
      unit: product.unit || '',
@@ -560,32 +560,32 @@
    const resolvedProductId = product.productId
      ? Number(product.productId)
      : findNodeIdByLabel(productOptions.value, productName) || ''
    // å¦‚果有产品ID,加载对应的规格型号列表
    let modelOptions = [];
    let resolvedSpecificationId = product.specificationId || '';
    let resolvedProductModelId = product.productModelId || '';
    if (resolvedProductId) {
      try {
        const res = await modelList({ id: resolvedProductId });
        modelOptions = res || [];
        // å¦‚果返回的数据没有 specificationId,但有 specification åç§°ï¼Œæ ¹æ®åç§°æŸ¥æ‰¾ ID
        if (!resolvedSpecificationId && product.specification) {
        // å¦‚果返回的数据没有 productModelId,但有 specification åç§°ï¼Œæ ¹æ®åç§°æŸ¥æ‰¾ ID
        if (!resolvedProductModelId && product.specification) {
          const foundModel = modelOptions.find(item => item.model === product.specification);
          if (foundModel) {
            resolvedSpecificationId = foundModel.id;
            resolvedProductModelId = foundModel.id;
          }
        }
      } catch (error) {
        console.error('加载规格型号失败:', error);
      }
    }
    return {
      productId: resolvedProductId,
      product: productName,
      specificationId: resolvedSpecificationId,
      productModelId: resolvedProductModelId,
      specification: product.specification || '',
      quantity: product.quantity || 0,
      unit: product.unit || '',
@@ -649,8 +649,7 @@
    productId: '',
    product: '',
    productName: '',
    specificationId: '',
    specification: '',
    productModelId: '',
    quantity: 1,
    unit: '',
    unitPrice: 0,
@@ -755,7 +754,7 @@
        products: item.products ? item.products.map(product => ({
          productId: product.productId || '',
          product: product.product || product.productName || '',
          specificationId: product.specificationId || '',
          productModelId: product.productModelId || '',
          specification: product.specification || '',
          quantity: product.quantity || 0,
          unit: product.unit || '',
@@ -803,16 +802,16 @@
  padding: 10px 0;
  max-height: calc(100vh - 200px);
  overflow-y: auto;
  &::-webkit-scrollbar {
    width: 6px;
    height: 6px;
  }
  &::-webkit-scrollbar-thumb {
    background: #c1c1c1;
    border-radius: 3px;
    &:hover {
      background: #a8a8a8;
    }
@@ -829,17 +828,17 @@
  margin-bottom: 24px;
  border-radius: 8px;
  transition: all 0.3s ease;
  &:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08) !important;
  }
  :deep(.el-card__header) {
    padding: 16px 20px;
    background: linear-gradient(135deg, #f5f7fa 0%, #ffffff 100%);
    border-bottom: 1px solid #ebeef5;
  }
  :deep(.el-card__body) {
    padding: 20px;
  }
@@ -849,19 +848,19 @@
  display: flex;
  align-items: center;
  gap: 8px;
  .card-icon {
    font-size: 18px;
    color: #409eff;
  }
  .card-title {
    font-weight: 600;
    font-size: 16px;
    color: #303133;
    flex: 1;
  }
  .header-btn {
    margin-left: auto;
  }
@@ -885,20 +884,20 @@
.product-table {
  :deep(.el-table__header) {
    background-color: #f5f7fa;
    th {
      background-color: #f5f7fa !important;
      color: #606266;
      font-weight: 600;
    }
  }
  :deep(.el-table__row) {
    &:hover {
      background-color: #f5f7fa;
    }
  }
  :deep(.el-table__cell) {
    padding: 12px 0;
  }