已添加2个文件
已修改14个文件
1303 ■■■■■ 文件已修改
src/components/AIChatSidebar/assistants/index.js 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AIChatSidebar/assistants/productionAssistant.js 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AIChatSidebar/index.vue 763 ●●●●● 补丁 | 查看 | 原始文档 | 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/productionManagement/processRoute/processRouteItem/index.vue 79 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productStructure/Detail/index.vue 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/finalInspection/components/formDia.vue 149 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/finalInspection/index.vue 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/nonconformingManagement/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/processInspection/components/formDia.vue 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/processInspection/index.vue 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/rawMaterialInspection/components/formDia.vue 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/rawMaterialInspection/index.vue 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesQuotation/index.vue 24 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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/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/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/qualityManagement/finalInspection/components/formDia.vue
@@ -31,6 +31,8 @@
              </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
@@ -58,7 +60,35 @@
          </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="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" />
            </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" />
            </el-form-item>
          </el-col>
        </el-row>
@@ -73,6 +103,7 @@
              <el-select v-model="form.checkResult">
                <el-option label="合格" value="合格" />
                <el-option label="不合格" value="不合格" />
                <el-option label="部分合格" value="部分合格" />
              </el-select>
            </el-form-item>
          </el-col>
@@ -80,10 +111,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>
                <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">
@@ -134,7 +165,7 @@
const emit = defineEmits(['close'])
const dialogFormVisible = ref(false);
const operationType = ref('')
const operationType = ref("");
const data = reactive({
  form: {
    checkTime: "",
@@ -147,25 +178,29 @@
    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(() => {
// ç¼–辑时:productMainId æˆ– purchaseLedgerId ä»»ä¸€æœ‰å€¼åˆ™å·¥åºã€æ•°é‡ç½®ç°
const processQuantityDisabled = computed(() => {
  const v = form.value || {};
  return !!(v.productMainId != null || v.purchaseLedgerId != null);
});
@@ -209,7 +244,7 @@
  // å…ˆæ¸…空表单验证状态,避免闪烁
  await nextTick();
  proxy.$refs.formRef?.clearValidate();
  // å¹¶è¡ŒåŠ è½½åŸºç¡€æ•°æ®
  const [userListsRes] = await Promise.all([
    userListNoPage(),
@@ -219,11 +254,11 @@
    })
  ]);
  userList.value = userListsRes.data;
  form.value = {}
  testStandardOptions.value = [];
  tableData.value = [];
  if (operationType.value === 'edit') {
    // å…ˆä¿å­˜ testStandardId,避免被清空
    const savedTestStandardId = row.testStandardId;
@@ -234,18 +269,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 +295,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 +348,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 +394,7 @@
    if (children && children.length > 0) {
      newItem.children = convertIdToValue(children);
    }
    return newItem;
  });
}
@@ -345,26 +402,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 +432,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 = '';
  });
}
// æŒ‡æ ‡é€‰æ‹©å˜åŒ–处理
@@ -400,12 +457,12 @@
    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 +471,8 @@
  testStandardOptions.value = [];
  form.value.testStandardId = '';
  dialogFormVisible.value = false;
  emit('close')
};
  emit('close');
}
defineExpose({
  openDialog,
});
@@ -423,4 +480,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';
      }
    },
  },
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
@@ -99,6 +99,34 @@
        </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" />
            </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" />
            </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"
@@ -114,6 +142,8 @@
                           value="合格" />
                <el-option label="不合格"
                           value="不合格" />
                <el-option label="部分合格"
                           value="部分合格" />
              </el-select>
            </el-form-item>
          </el-col>
@@ -189,6 +219,7 @@
  import { userListNoPage } from "@/api/system/user.js";
  import { qualityInspectParamInfo } from "@/api/qualityManagement/qualityInspectParam.js";
  import { list } from "@/api/productionManagement/productionProcess";
  import qualified from "@/views/inventoryManagement/stockManagement/Qualified.vue";
  const { proxy } = getCurrentInstance();
  const emit = defineEmits(["close"]);
@@ -206,6 +237,8 @@
      testStandardId: "",
      unit: "",
      quantity: "",
      qualifiedQuantity: "",
      unqualifiedQuantity: "",
      checkCompany: "",
      checkResult: "",
    },
@@ -215,11 +248,11 @@
      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" }],
    },
@@ -400,6 +433,28 @@
      modelOptions.value.find(item => item.id == value)?.unit || "";
  };
  const handleQualifiedQuantityChange = (value) => {
    if (value === null || value === undefined) {
      form.value.qualifiedQuantity = 0;
      return;
    }
    const quantity = parseFloat(form.value.quantity) || 0;
    const qualified = parseFloat(value) || 0;
    form.value.qualifiedQuantity = qualified > quantity?quantity:qualified;
    form.value.unqualifiedQuantity = Math.max(0, quantity - qualified);
  };
  const handleUnqualifiedQuantityChange = (value) => {
    if (value === null || value === undefined) {
      form.value.unqualifiedQuantity = 0;
      return;
    }
    const quantity = parseFloat(form.value.quantity) || 0;
    const unqualified = parseFloat(value) || 0;
    form.value.unqualifiedQuantity = unqualified > quantity?quantity:unqualified;
    form.value.qualifiedQuantity = Math.max(0, quantity - unqualified);
  };
  const findNodeById = (nodes, productId) => {
    for (let i = 0; i < nodes.length; i++) {
      if (nodes[i].value === productId) {
@@ -440,6 +495,17 @@
            delete item.id;
          });
        }
        // ç¡®ä¿æ•°é‡ä¸ä¸ºnull
        const quantity = parseFloat(form.value.quantity) || 0;
        const qualified = parseFloat(form.value.qualifiedQuantity) || 0;
        const unqualified = parseFloat(form.value.unqualifiedQuantity) || 0;
        // éªŒè¯æ•°é‡å…³ç³»
        if (qualified + unqualified !== quantity) {
          proxy.$modal.msgError("合格数量与不合格数量之和必须等于总数量");
          return;
        }
        const data = {
          ...form.value,
          process: processName, // ä¿ç•™ process å­—段以兼容后端
@@ -520,4 +586,4 @@
</script>
<style scoped>
</style>
</style>
src/views/qualityManagement/processInspection/index.vue
@@ -122,8 +122,18 @@
    prop: "unit",
  },
  {
    label: "数量",
    label: "总数量",
    prop: "quantity",
    width: 100
  },
  {
    label: "合格数量",
    prop: "qualifiedQuantity",
    width: 100
  },
  {
    label: "不合格数量",
    prop: "unqualifiedQuantity",
    width: 100
  },
  {
@@ -141,7 +151,7 @@
      } else if (params == '合格') {
        return "success";
      } else {
        return null;
        return 'danger';
      }
    },
  },
@@ -363,13 +373,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
@@ -82,6 +82,23 @@
            </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"/>
            </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"/>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="检测单位:" prop="checkCompany">
@@ -93,6 +110,7 @@
              <el-select v-model="form.checkResult">
                <el-option label="合格" value="合格"/>
                <el-option label="不合格" value="不合格"/>
                <el-option label="部分合格" value="部分合格"/>
              </el-select>
            </el-form-item>
          </el-col>
@@ -182,6 +200,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"}],
  },
@@ -294,7 +314,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) {
@@ -448,6 +468,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 +510,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';
      }
    },
  },
src/views/salesManagement/salesQuotation/index.vue
@@ -185,7 +185,7 @@
                </el-form-item>
              </template>
            </el-table-column>
            <el-table-column prop="ProductModel" label="规格型号" width="200">
            <el-table-column prop="specification" label="规格型号" width="200">
              <template #default="scope">
                <el-form-item :prop="`products.${scope.$index}.productModelId`" class="product-table-form-item">
                  <el-select
@@ -275,7 +275,7 @@
        <h4>产品明细</h4>
        <el-table :data="currentQuotation.products" border style="width: 100%">
          <el-table-column prop="product" label="产品名称" />
          <el-table-column prop="ProductModel" label="规格型号" />
          <el-table-column prop="specification" label="规格型号" />
          <el-table-column prop="unit" label="单位" />
          <el-table-column prop="unitPrice" label="单价">
            <template #default="scope">
@@ -458,7 +458,7 @@
        row.product = '';
        row.modelOptions = [];
        row.productModelId = '';
        row.ProductModel = '';
        row.specification = '';
        row.unit = '';
        return;
    }
@@ -479,7 +479,7 @@
    // å¦‚果清空选择,则清空相关字段
    if (!value) {
        row.productModelId = '';
        row.ProductModel = '';
        row.specification = '';
        row.unit = '';
        return;
    }
@@ -488,10 +488,10 @@
    const modelOptions = row.modelOptions || [];
    const index = modelOptions.findIndex((item) => item.id === value);
    if (index !== -1) {
        row.ProductModel = modelOptions[index].model;
        row.specification = modelOptions[index].model;
        row.unit = modelOptions[index].unit;
    } else {
        row.ProductModel = '';
        row.specification = '';
        row.unit = '';
    }
};
@@ -524,7 +524,7 @@
      productId: product.productId || '',
      product: product.product || product.productName || '',
      productModelId: product.productModelId || '',
      ProductModel: product.ProductModel || '',
      specification: product.specification || '',
      quantity: product.quantity || 0,
      unit: product.unit || '',
      unitPrice: product.unitPrice || 0,
@@ -570,9 +570,9 @@
        const res = await modelList({ id: resolvedProductId });
        modelOptions = res || [];
        // å¦‚果返回的数据没有 productModelId,但有 ProductModel åç§°ï¼Œæ ¹æ®åç§°æŸ¥æ‰¾ ID
        if (!resolvedProductModelId && product.ProductModel) {
          const foundModel = modelOptions.find(item => item.model === product.ProductModel);
        // å¦‚果返回的数据没有 productModelId,但有 specification åç§°ï¼Œæ ¹æ®åç§°æŸ¥æ‰¾ ID
        if (!resolvedProductModelId && product.specification) {
          const foundModel = modelOptions.find(item => item.model === product.specification);
          if (foundModel) {
            resolvedProductModelId = foundModel.id;
          }
@@ -586,7 +586,7 @@
      productId: resolvedProductId,
      product: productName,
      productModelId: resolvedProductModelId,
      ProductModel: product.ProductModel || '',
      specification: product.specification || '',
      quantity: product.quantity || 0,
      unit: product.unit || '',
      unitPrice: product.unitPrice || 0,
@@ -755,7 +755,7 @@
          productId: product.productId || '',
          product: product.product || product.productName || '',
          productModelId: product.productModelId || '',
          ProductModel: product.ProductModel || '',
          specification: product.specification || '',
          quantity: product.quantity || 0,
          unit: product.unit || '',
          unitPrice: product.unitPrice || 0,