gaoluyang
10 小时以前 ab264123941cd3d345687af92aab2a9e04968960
src/components/AIChatSidebar/index.vue
@@ -227,6 +227,12 @@
                      :id="`ai-chart-${index}-${key}`"
                  ></div>
                </div>
                <div
                    v-else-if="message.chartMarkdownParseFailed"
                    class="chart-empty-state"
                >
                  图表解析失败,请稍后重试。
                </div>
                <!-- 表格内容 -->
                <div v-if="message.type === 'todo_list' && message.tableData" class="table-wrapper">
@@ -513,6 +519,64 @@
                          :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.financeData" class="sales-structured-card">
                  <div class="sales-structured-card__title">{{ getFinancialTypeLabel(message.type) }}</div>
                  <div v-if="message.financeData.summaryEntries?.length" class="sales-summary-grid">
                    <div
                        v-for="(entry, entryIndex) in message.financeData.summaryEntries"
                        :key="`finance-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.financeData.headline" class="finance-headline">
                    {{ message.financeData.headline }}
                  </div>
                  <div v-if="message.financeData.conclusions?.length" class="finance-text-section">
                    <div class="sales-section-title">核心结论</div>
                    <ul>
                      <li v-for="(item, idx) in message.financeData.conclusions" :key="`finance-conclusion-${idx}`">
                        {{ item }}
                      </li>
                    </ul>
                  </div>
                  <div v-if="message.financeData.riskSuggestions?.length" class="finance-text-section">
                    <div class="sales-section-title">风险建议</div>
                    <ul>
                      <li v-for="(item, idx) in message.financeData.riskSuggestions" :key="`finance-risk-${idx}`">
                        {{ item }}
                      </li>
                    </ul>
                  </div>
                  <div
                      v-for="section in message.financeData.sections"
                      :key="`finance-section-${section.key}`"
                      class="table-wrapper manufacturing-table-wrapper"
                  >
                    <div class="sales-section-title">{{ section.title }}</div>
                    <el-table :data="section.items" border stripe size="small" style="width: 100%">
                      <el-table-column
                          v-for="col in section.columns"
                          :key="`finance-${section.key}-${col}`"
                          :label="getStructuredFieldLabel(col)"
                          min-width="120"
                          show-overflow-tooltip
                      >
                        <template #default="{ row }">
@@ -949,7 +1013,7 @@
  sales_customer_profile_list: '客户档案',
  sales_quotation_list: '销售报价',
  sales_ledger_list: '销售台账',
  sales_return_list: '销售退货',
  sales_return_list: '销售退款/回款记录',
  sales_customer_interaction_list: '客户往来',
  sales_shipping_list: '发货台账',
  sales_dashboard: '销售指标统计',
@@ -958,7 +1022,28 @@
}
const purchaseTypeLabelMap = {
  purchase_material_rank: '采购物料金额排行',
  purchase_stats: '采购统计',
  purchase_pending_payment_list: '待付款采购单'
}
const financialStructuredTypeSet = new Set([
  'financial_cost_accounting',
  'financial_order_profit_analysis',
  'financial_inventory_capital_analysis',
  'financial_cashflow_forecast',
  'financial_business_anomaly_warning',
  'financial_business_cockpit',
  'financial_operation_report',
  'financial_rag_knowledge'
])
const financialTypeLabelMap = {
  financial_cost_accounting: '成本核算',
  financial_order_profit_analysis: '订单利润分析',
  financial_inventory_capital_analysis: '库存资金分析',
  financial_cashflow_forecast: '现金流预测',
  financial_business_anomaly_warning: '经营异常预警',
  financial_business_cockpit: '经营驾驶舱',
  financial_operation_report: '经营报告',
  financial_rag_knowledge: '财务知识检索'
}
const manufacturingStructuredTypeSet = new Set([
  'manufacturing_site_snapshot',
@@ -1030,6 +1115,26 @@
  stockQty: '库存量'
}
Object.assign(structuredFieldLabelMap, {
  refundId: '退款/回款单号',
  collectionNumber: '收款单号',
  collectionAmount: '收款金额',
  actualAmount: '收款金额(兼容)',
  customerId: '客户ID',
  salesLedgerId: '销售台账ID',
  projectName: '项目名称',
  receiptPaymentDate: '收付款日期',
  receiptPaymentAmount: '收付款金额',
  receiptPaymentType: '收付款类型',
  registrant: '登记人',
  returnAmount: '回款金额汇总',
  totalReceiptAmount: '收款总额',
  customerCount: '客户数',
  paidAmount: '已付款金额',
  contractAmount: '合同金额',
  paymentCount: '付款笔数',
  invoiceCount: '发票笔数',
  paymentAmount: '付款金额',
  invoiceAmount: '发票金额',
  customerName: '客户名称',
  riskLevel: '风险等级',
  riskScore: '风险评分',
@@ -1042,6 +1147,32 @@
  collectionStrategy: '回款策略',
  quotationStrategy: '报价策略',
  nextAction: '下一步动作',
  salesContractNo: '销售合同号',
  revenue: '收入',
  materialCost: '材料成本',
  laborCost: '人工成本',
  depreciationCost: '折旧成本',
  scrapCost: '报废成本',
  totalCost: '总成本',
  profit: '利润',
  profitRate: '利润率',
  reasons: '原因',
  suggestion: '建议',
  productName: '产品名称',
  model: '型号',
  quantity: '数量',
  inventoryValue: '库存资金',
  stagnantDays: '呆滞天数',
  overstock: '是否超储',
  month: '月份',
  income: '收入',
  expense: '支出',
  netFlow: '净现金流',
  message: '预警信息',
  headline: '报告标题',
  conclusions: '核心结论',
  riskSuggestions: '风险建议',
  orderProfitTop: '利润Top',
  contractAmountTotal: '合同总额',
  receivedAmountTotal: '已回款金额',
  pendingAmountTotal: '待回款总额',
@@ -1450,12 +1581,41 @@
  return items.filter(item => isPlainObject(item))
}
const normalizeSalesReturnListItems = (items = []) => {
  return normalizeSalesListItems(items).map((item) => {
    const collectionAmount = item.collectionAmount ?? item.actualAmount
    const collectionNumber = item.collectionNumber || item.refundId || ''
    return {
      ...item,
      refundId: item.refundId || collectionNumber,
      collectionNumber,
      collectionAmount: collectionAmount ?? 0,
      actualAmount: item.actualAmount ?? collectionAmount ?? 0
    }
  })
}
const normalizeSalesTypeListItems = (type = '', items = []) => {
  if (type === 'sales_return_list') {
    return normalizeSalesReturnListItems(items)
  }
  return normalizeSalesListItems(items)
}
const inferSalesColumnsByType = (type = '', listItems = []) => {
  const columns = inferSalesColumns(listItems)
  if (type === 'sales_return_list') {
    return columns.filter(column => column !== 'actualAmount')
  }
  return columns
}
const buildSalesStructuredData = (parsedData) => {
  const type = String(parsedData?.type || '')
  if (!salesStructuredTypeSet.has(type)) return null
  const rawData = isPlainObject(parsedData?.data) ? parsedData.data : {}
  const listItems = normalizeSalesListItems(rawData.items)
  const listItems = normalizeSalesTypeListItems(type, rawData.items)
  const topCustomers = normalizeSalesListItems(rawData.topCustomers)
  const contractTrend = normalizeSalesListItems(rawData.contractTrend)
@@ -1463,7 +1623,7 @@
    type,
    summaryEntries: normalizeManufacturingSummaryEntries(parsedData?.summary),
    listItems,
    columns: inferSalesColumns(listItems),
    columns: inferSalesColumnsByType(type, listItems),
    topCustomers,
    topCustomerColumns: inferSalesColumns(topCustomers),
    contractTrend,
@@ -1491,6 +1651,50 @@
}
const getPurchaseTypeLabel = (type = '') => purchaseTypeLabelMap[String(type || '')] || '采购查询结果'
const financialDataSectionConfig = [
  { key: 'orders', title: '订单明细' },
  { key: 'items', title: '明细列表' },
  { key: 'actualMonthly', title: '历史现金流' },
  { key: 'forecastMonthly', title: '预测现金流' },
  { key: 'receivableRiskTop', title: '应收风险Top' },
  { key: 'payablePressureTop', title: '应付压力Top' },
  { key: 'orderProfitTop', title: '利润Top' }
]
const buildFinancialStructuredSections = (rawData = {}) => {
  const sections = []
  financialDataSectionConfig.forEach((section) => {
    const items = normalizeSalesListItems(rawData?.[section.key])
    if (!items.length) return
    sections.push({
      key: section.key,
      title: section.title,
      items,
      columns: inferSalesColumns(items)
    })
  })
  return sections
}
const buildFinancialStructuredData = (parsedData) => {
  const type = String(parsedData?.type || '')
  if (!financialStructuredTypeSet.has(type) || parsedData?.success !== true) return null
  const rawData = isPlainObject(parsedData?.data) ? parsedData.data : {}
  const headline = String(rawData?.headline || '').trim()
  return {
    type,
    summaryEntries: normalizeManufacturingSummaryEntries(parsedData?.summary),
    headline,
    conclusions: toStructuredStringArray(rawData?.conclusions),
    riskSuggestions: toStructuredStringArray(rawData?.riskSuggestions),
    sections: buildFinancialStructuredSections(rawData)
  }
}
const getFinancialTypeLabel = (type = '') => financialTypeLabelMap[String(type || '')] || '财务查询结果'
const isSalesFocusType = (type = '') => salesFocusTypeSet.has(String(type || ''))
@@ -1913,6 +2117,8 @@
          salesData: null,
          purchaseData: null,
          purchaseIntentData: null,
          financeData: null,
          chartMarkdownParseFailed: false,
          localUploadFiles: isUser ? mapHistoryFilePathsToSnapshots(msg.filePaths, uuid.value, idx) : []
        }
@@ -2161,6 +2367,8 @@
  messageObj.salesData = null
  messageObj.purchaseData = null
  messageObj.purchaseIntentData = null
  messageObj.financeData = null
  messageObj.chartMarkdownParseFailed = false
  if (isPurchaseIntentNotRecognized) {
    messageObj.purchaseIntentData = normalizePurchaseIntentNotRecognizedData(parsedData)
@@ -2188,12 +2396,18 @@
    messageObj.purchaseData = purchaseData
  }
  const financeData = buildFinancialStructuredData(parsedData)
  if (financeData) {
    messageObj.financeData = financeData
  }
  if (parsedData.action === 'confirm_required' && parsedData.businessType) {
    messageObj.type = 'purchase_analysis_confirm'
    messageObj.purchaseAnalysisData = parsedData
    messageObj.manufacturingData = null
    messageObj.salesData = null
    messageObj.purchaseData = null
    messageObj.financeData = null
    if (!Array.isArray(messageObj.payloadTreeData) || !messageObj.payloadTreeData.length) {
      initializePurchasePayloadTree(messageObj, parsedData.payload || {})
    }
@@ -2243,6 +2457,8 @@
    if (parsedData.type === 'sales_customer_churn_risk') return '已为您生成客户流失风险分析。'
    if (parsedData.type === 'sales_collection_quote_strategy') return '已为您生成回款与报价策略建议。'
    if (parsedData.type === 'sales_dashboard') return '已为您生成销售指标统计。'
    if (parsedData.type === 'sales_return_list') return '已返回销售退款/回款记录。'
    if (parsedData.type === 'sales_customer_interaction_list') return '已返回客户往来数据。'
    return '已返回销售查询结果。'
  }
  if (manufacturingStructuredTypeSet.has(parsedData.type)) {
@@ -2251,9 +2467,31 @@
    if (parsedData.type === 'manufacturing_analysis') return '已为您生成制造分析结果。'
    return '已返回制造查询结果。'
  }
  if (financialStructuredTypeSet.has(parsedData.type)) return '已返回财务分析结果。'
  if (String(parsedData.type || '').startsWith('purchase_')) return '已返回采购查询结果。'
  if (parsedData.charts && Object.keys(parsedData.charts).length > 0) return '已为您生成分析图表。'
  return '正在为您展示分析结果...'
}
const resolveStructuredDescription = (parsedData) => {
  if (!parsedData) return ''
  if (parsedData.action === 'confirm_required') {
    return getPurchaseConfirmDescription(parsedData)
  }
  const description = String(parsedData.description || '').trim()
  if (!description) return ''
  if (parsedData.type === 'sales_customer_interaction_list') {
    if (description === 'no_customer_interactions') return '该时间范围暂无客户往来记录。'
    if (description === 'ok') return '已返回客户往来数据。'
  }
  if (description === 'ok' || description === 'success') {
    return getStructuredFallbackText(parsedData)
  }
  return description
}
const buildPurchaseMaterialRankCharts = (parsedData) => {
@@ -3384,7 +3622,9 @@
    manufacturingData: null,
    salesData: null,
    purchaseData: null,
    purchaseIntentData: null
    purchaseIntentData: null,
    financeData: null,
    chartMarkdownParseFailed: false
  })
  outputState.value[botMsgIndex] = {
@@ -3440,6 +3680,7 @@
    if (extracted) {
      applyStructuredMessageData(currentMsg, extracted.data, botMsgIndex, !outputState.value[botMsgIndex].hasRenderedChart)
    }
    currentMsg.htmlContent = convertStreamOutput(currentMsg.content || '', botMsgIndex)
    // 最终解析确保图表渲染
    if (currentMsg.chartOptions && !outputState.value[botMsgIndex].hasRenderedChart) {
@@ -3512,7 +3753,9 @@
    manufacturingData: null,
    salesData: null,
    purchaseData: null,
    purchaseIntentData: null
    purchaseIntentData: null,
    financeData: null,
    chartMarkdownParseFailed: false
  }
  messages.value.push(botMsg)
@@ -3562,6 +3805,7 @@
    if (extracted) {
      applyStructuredMessageData(currentMsg, extracted.data, botMsgIndex)
    }
    currentMsg.htmlContent = convertStreamOutput(currentMsg.content || '', botMsgIndex)
  }).catch(err => {
    if (err.name === 'CanceledError' || err.name === 'AbortError') {
      console.log('Request aborted by user')
@@ -3605,6 +3849,126 @@
      .replace(/\n/g, '<br>')
}
const localChartMarkdownImagePattern = /!\[[^\]]*]\((https?:\/\/local\/generate_chart\?[^)\s]+)\)/gi
const parseLocalChartOptionText = (optionText = '') => {
  const text = String(optionText || '').trim()
  if (!text) return null
  const parseCandidates = [text]
  try {
    const decoded = decodeURIComponent(text)
    if (decoded && decoded !== text) {
      parseCandidates.push(decoded)
    }
  } catch (err) {
    // Keep original text candidate.
  }
  for (const candidate of parseCandidates) {
    try {
      const parsed = JSON.parse(candidate)
      if (isPlainObject(parsed)) {
        return parsed
      }
    } catch (err) {
      continue
    }
  }
  return null
}
const parseLocalChartOptionFromUrl = (urlText = '') => {
  try {
    const url = new URL(String(urlText || '').trim())
    if (String(url.hostname || '').toLowerCase() !== 'local' || !String(url.pathname || '').includes('/generate_chart')) {
      return null
    }
    const optionText = url.searchParams.get('options')
    return parseLocalChartOptionText(optionText)
  } catch (err) {
    return null
  }
}
const extractLocalChartMarkdown = (text = '') => {
  const sourceText = String(text || '')
  if (!sourceText) {
    return {
      cleanedText: '',
      hasLocalChartMarkdown: false,
      chartOptions: null,
      parseFailed: false
    }
  }
  let hasLocalChartMarkdown = false
  let chartIndex = 0
  const chartOptions = {}
  const cleanedText = sourceText.replace(localChartMarkdownImagePattern, (fullMatch, chartUrl) => {
    hasLocalChartMarkdown = true
    const option = parseLocalChartOptionFromUrl(chartUrl)
    if (option) {
      chartOptions[`markdownChart_${chartIndex++}`] = option
    }
    return ''
  })
  const normalizedText = cleanedText
      .replace(/\n[ \t]*\n[ \t]*\n+/g, '\n\n')
      .trim()
  const hasParsedCharts = Object.keys(chartOptions).length > 0
  return {
    cleanedText: normalizedText,
    hasLocalChartMarkdown,
    chartOptions: hasParsedCharts ? chartOptions : null,
    parseFailed: hasLocalChartMarkdown && !hasParsedCharts
  }
}
const applyLocalChartMarkdownFallback = (displayText, msgIndex) => {
  const messageObj = messages.value[msgIndex]
  if (!messageObj || messageObj.isUser) return displayText
  const {
    cleanedText,
    hasLocalChartMarkdown,
    chartOptions,
    parseFailed
  } = extractLocalChartMarkdown(displayText)
  if (!hasLocalChartMarkdown) {
    return displayText
  }
  if (chartOptions) {
    messageObj.chartOptions = chartOptions
    messageObj.chartRenderReady = true
    messageObj.chartMarkdownParseFailed = false
    const streamState = outputState.value[msgIndex]
    if (!streamState || !streamState.hasRenderedChart) {
      renderCharts(msgIndex, chartOptions)
      if (streamState) {
        streamState.hasRenderedChart = true
      }
    }
    return cleanedText || '已为您生成分析图表。'
  }
  if (!messageObj.chartOptions || !messageObj.chartRenderReady) {
    messageObj.chartOptions = null
    messageObj.chartRenderReady = false
    messageObj.chartMarkdownParseFailed = parseFailed
  }
  return cleanedText || '图表解析失败,请稍后重试。'
}
const convertStreamOutput = (output, msgIndex) => {
  if (!output) return ''
  const state = outputState.value[msgIndex]
@@ -3631,10 +3995,9 @@
      display = ''
    }
    if (parsed.description) {
      display = parsed.action === 'confirm_required'
        ? getPurchaseConfirmDescription(parsed)
        : parsed.description
    const resolvedDescription = resolveStructuredDescription(parsed)
    if (resolvedDescription) {
      display = resolvedDescription
    }
    if (!display) {
@@ -3653,10 +4016,9 @@
          display = ''
        }
        if (parsed.description) {
          display = parsed.action === 'confirm_required'
            ? getPurchaseConfirmDescription(parsed)
            : parsed.description
        const resolvedDescription = resolveStructuredDescription(parsed)
        if (resolvedDescription) {
          display = resolvedDescription
        }
        if (!display) {
@@ -3672,6 +4034,7 @@
    }
  }
  display = applyLocalChartMarkdownFallback(display, msgIndex)
  let html = convertTextToHtml(display)
  // 还原代码块
@@ -4654,6 +5017,18 @@
  margin-bottom: 12px;
}
.chart-empty-state {
  margin-top: 12px;
  width: 100%;
  border-radius: 10px;
  border: 1px dashed rgba(148, 163, 184, 0.6);
  background: #f8fafc;
  color: #64748b;
  font-size: 13px;
  line-height: 1.6;
  padding: 12px;
}
.table-wrapper {
  margin-top: 12px;
  background: #fff;
@@ -4957,6 +5332,25 @@
  color: $deep-blue;
}
.finance-headline {
  margin-top: 4px;
  font-size: 13px;
  line-height: 1.7;
  color: #344054;
}
.finance-text-section {
  margin-top: 10px;
  ul {
    margin: 6px 0 0;
    padding-left: 18px;
    font-size: 13px;
    line-height: 1.7;
    color: #344054;
  }
}
.purchase-intent-quick-prompt-wrap {
  margin-top: 12px;
}