huminmin
2 天以前 4a407279f0c9757f0714eaf385fdd5cd68c038c2
src/components/AIChatSidebar/index.vue
@@ -489,6 +489,56 @@
                  </div>
                </div>
                <div v-if="message.purchaseData" class="sales-structured-card">
                  <div class="sales-structured-card__title">{{ getPurchaseTypeLabel(message.type) }}</div>
                  <div v-if="message.purchaseData.summaryEntries?.length" class="sales-summary-grid">
                    <div
                        v-for="(entry, entryIndex) in message.purchaseData.summaryEntries"
                        :key="`purchase-summary-${entry.key}-${entryIndex}`"
                        class="sales-summary-item"
                    >
                      <span class="sales-summary-label">{{ entry.label }}</span>
                      <strong class="sales-summary-value">{{ entry.value }}</strong>
                    </div>
                  </div>
                  <div
                      v-if="message.purchaseData.listItems?.length && message.purchaseData.columns?.length"
                      class="table-wrapper manufacturing-table-wrapper"
                  >
                    <el-table :data="message.purchaseData.listItems" border stripe size="small" style="width: 100%">
                      <el-table-column
                          v-for="col in message.purchaseData.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>
                <div v-if="message.purchaseIntentData?.quickPrompts?.length" class="purchase-intent-quick-prompt-wrap">
                  <div class="purchase-intent-quick-prompt-title">试试以下提问</div>
                  <div class="quick-prompt-list purchase-intent-quick-prompt-list">
                    <button
                        v-for="prompt in message.purchaseIntentData.quickPrompts"
                        :key="`purchase-intent-${prompt}`"
                        type="button"
                        class="quick-prompt-btn"
                        :disabled="isSending"
                        @click="sendQuickPrompt(prompt)"
                    >
                      {{ prompt }}
                    </button>
                  </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>
@@ -906,6 +956,10 @@
  sales_customer_churn_risk: '客户流失风险分析',
  sales_collection_quote_strategy: '回款与报价策略建议'
}
const purchaseTypeLabelMap = {
  purchase_material_rank: '采购物料金额排行',
  purchase_pending_payment_list: '待付款采购单'
}
const manufacturingStructuredTypeSet = new Set([
  'manufacturing_site_snapshot',
  'manufacturing_plan_list',
@@ -991,7 +1045,11 @@
  contractAmountTotal: '合同总额',
  receivedAmountTotal: '已回款金额',
  pendingAmountTotal: '待回款总额',
  shipRate: '发货率'
  shipRate: '发货率',
  pendingOrderCount: '待付款订单数',
  totalContractAmount: '待付款合同总额',
  totalPaidAmount: '已付款总额',
  totalPendingAmount: '待付款总额'
})
const purchasePayloadFieldLabelMap = {
  purchaseLedgers: '采购台账',
@@ -1415,6 +1473,25 @@
const getSalesTypeLabel = (type = '') => salesTypeLabelMap[String(type || '')] || '销售查询结果'
const buildPurchaseStructuredData = (parsedData) => {
  if (parsedData?.success !== true) return null
  const type = String(parsedData?.type || '')
  if (!type.startsWith('purchase_')) return null
  const rawData = isPlainObject(parsedData?.data) ? parsedData.data : {}
  const listItems = normalizeSalesListItems(rawData.items)
  return {
    type,
    summaryEntries: normalizeManufacturingSummaryEntries(parsedData?.summary),
    listItems,
    columns: inferSalesColumns(listItems)
  }
}
const getPurchaseTypeLabel = (type = '') => purchaseTypeLabelMap[String(type || '')] || '采购查询结果'
const isSalesFocusType = (type = '') => salesFocusTypeSet.has(String(type || ''))
const getSalesLevelTagType = (level = '') => {
@@ -1446,6 +1523,13 @@
      .filter(Boolean)
  }
  return []
}
const normalizePurchaseIntentNotRecognizedData = (parsedData) => {
  if (String(parsedData?.type || '') !== 'purchase_intent_not_recognized') return null
  const quickPrompts = toStructuredStringArray(parsedData?.data?.quickPrompts)
  if (!quickPrompts.length) return null
  return { quickPrompts }
}
const getManufacturingWarningLevelType = (level = '') => {
@@ -1827,6 +1911,8 @@
          purchaseAnalysisData: null,
          manufacturingData: null,
          salesData: null,
          purchaseData: null,
          purchaseIntentData: null,
          localUploadFiles: isUser ? mapHistoryFilePathsToSnapshots(msg.filePaths, uuid.value, idx) : []
        }
@@ -2045,7 +2131,7 @@
          const candidate = text.slice(i, j + 1)
          try {
            const parsed = JSON.parse(candidate)
            if (parsed?.success === true) {
            if (typeof parsed?.success === 'boolean') {
              return {
                data: parsed,
                startIdx: i,
@@ -2064,7 +2150,8 @@
}
const applyStructuredMessageData = (messageObj, parsedData, msgIndex, shouldRenderCharts = true) => {
  if (!messageObj || !parsedData?.success) return
  const isPurchaseIntentNotRecognized = String(parsedData?.type || '') === 'purchase_intent_not_recognized'
  if (!messageObj || (parsedData?.success !== true && !isPurchaseIntentNotRecognized)) return
  const previousManufacturingData = messageObj.manufacturingData
  messageObj.type = parsedData.type || ''
@@ -2072,6 +2159,15 @@
  messageObj.purchaseAnalysisData = null
  messageObj.manufacturingData = null
  messageObj.salesData = null
  messageObj.purchaseData = null
  messageObj.purchaseIntentData = null
  if (isPurchaseIntentNotRecognized) {
    messageObj.purchaseIntentData = normalizePurchaseIntentNotRecognizedData(parsedData)
    messageObj.chartOptions = null
    messageObj.chartRenderReady = false
    return
  }
  if (messageObj.type === 'todo_list' && parsedData.data) {
    messageObj.tableData = parsedData.data
@@ -2087,11 +2183,17 @@
    messageObj.manufacturingData = manufacturingData
  }
  const purchaseData = buildPurchaseStructuredData(parsedData)
  if (purchaseData) {
    messageObj.purchaseData = purchaseData
  }
  if (parsedData.action === 'confirm_required' && parsedData.businessType) {
    messageObj.type = 'purchase_analysis_confirm'
    messageObj.purchaseAnalysisData = parsedData
    messageObj.manufacturingData = null
    messageObj.salesData = null
    messageObj.purchaseData = null
    if (!Array.isArray(messageObj.payloadTreeData) || !messageObj.payloadTreeData.length) {
      initializePurchasePayloadTree(messageObj, parsedData.payload || {})
    }
@@ -2136,6 +2238,7 @@
const getStructuredFallbackText = (parsedData) => {
  if (!parsedData) return '正在为您展示分析结果...'
  if (parsedData.type === 'todo_list') return '已为您整理好相关数据。'
  if (parsedData.type === 'purchase_intent_not_recognized') return '未识别到可执行的采购查询条件,请补充查询条件后再试。'
  if (salesStructuredTypeSet.has(parsedData.type)) {
    if (parsedData.type === 'sales_customer_churn_risk') return '已为您生成客户流失风险分析。'
    if (parsedData.type === 'sales_collection_quote_strategy') return '已为您生成回款与报价策略建议。'
@@ -2148,6 +2251,7 @@
    if (parsedData.type === 'manufacturing_analysis') return '已为您生成制造分析结果。'
    return '已返回制造查询结果。'
  }
  if (String(parsedData.type || '').startsWith('purchase_')) return '已返回采购查询结果。'
  if (parsedData.charts && Object.keys(parsedData.charts).length > 0) return '已为您生成分析图表。'
  return '正在为您展示分析结果...'
}
@@ -3278,7 +3382,9 @@
    payloadHiddenData: null,
    purchaseAnalysisData: null,
    manufacturingData: null,
    salesData: null
    salesData: null,
    purchaseData: null,
    purchaseIntentData: null
  })
  outputState.value[botMsgIndex] = {
@@ -3404,7 +3510,9 @@
    payloadHiddenData: null,
    purchaseAnalysisData: null,
    manufacturingData: null,
    salesData: null
    salesData: null,
    purchaseData: null,
    purchaseIntentData: null
  }
  messages.value.push(botMsg)
@@ -3436,28 +3544,6 @@
          const extracted = extractEmbeddedSuccessJson(fullText)
          if (extracted) {
            applyStructuredMessageData(currentMsg, extracted.data, botMsgIndex)
          } else {
            const extractJson = (text) => {
              const startIdx = text.indexOf('{"success": true')
              if (startIdx === -1) return null
              // 从后往前找最后一个 '}'
              const lastBraceIdx = text.lastIndexOf('}')
              if (lastBraceIdx === -1 || lastBraceIdx < startIdx) return null
              const potentialJson = text.substring(startIdx, lastBraceIdx + 1)
              try {
                return JSON.parse(potentialJson)
              } catch (err) {
                return null
              }
            }
            const parsedData = extractJson(fullText)
            if (parsedData) {
              applyStructuredMessageData(currentMsg, parsedData, botMsgIndex, true)
            }
          }
          updateOutputState(fullText, botMsgIndex)
@@ -3475,24 +3561,6 @@
    const extracted = extractEmbeddedSuccessJson(currentMsg.content)
    if (extracted) {
      applyStructuredMessageData(currentMsg, extracted.data, botMsgIndex)
    } else {
      const extractJson = (text) => {
        const startIdx = text.indexOf('{"success": true')
        if (startIdx === -1) return null
        const lastBraceIdx = text.lastIndexOf('}')
        if (lastBraceIdx === -1 || lastBraceIdx < startIdx) return null
        const potentialJson = text.substring(startIdx, lastBraceIdx + 1)
        try {
          return JSON.parse(potentialJson)
        } catch (err) {
          return null
        }
      }
      const finalParsed = extractJson(currentMsg.content)
      if (finalParsed) {
        applyStructuredMessageData(currentMsg, finalParsed, botMsgIndex)
      }
    }
  }).catch(err => {
    if (err.name === 'CanceledError' || err.name === 'AbortError') {
@@ -4889,6 +4957,19 @@
  color: $deep-blue;
}
.purchase-intent-quick-prompt-wrap {
  margin-top: 12px;
}
.purchase-intent-quick-prompt-title {
  font-size: 12px;
  color: #4b5563;
}
.purchase-intent-quick-prompt-list {
  margin-top: 8px;
}
.purchase-confirm-card {
  margin-top: 12px;
  width: 100%;