6 天以前 e1220856d419dfa447e5e0909700c14a6c78d297
fix(financial): 修复财务数据处理中的数值转换和异常处理问题

- 添加 toNumber 工具函数确保数值转换安全
- 在数据获取方法中添加 try-catch 异常处理
- 使用 toNumber 替换 Number() 进行数值转换
- 修复应付应收统计数据访问的安全性问题
- 统一数据字段访问方式避免 undefined 错误
已修改2个文件
155 ■■■■■ 文件已修改
src/components/AIChatSidebar/assistants/financeAssistant.js 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AIChatSidebar/index.vue 149 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AIChatSidebar/assistants/financeAssistant.js
@@ -14,13 +14,13 @@
  allowFileUpload: false,
  emptySessionText: '暂无财务会话',
  quickPrompts: [
    '生成本周经营周报(利润与现金流)',
    '分析本月利润下降原因',
    '近30天哪个客户利润贡献最高',
    '查看本月经营驾驶舱',
    '查询近30天亏损订单',
    '分析近30天库存资金占用',
    '预测未来3个月现金流',
    '生成本周经营周报',
    '为什么利润下降',
    '哪个客户最赚钱',
    '哪个工序成本最高'
  ]
}
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">
@@ -2112,6 +2118,7 @@
          purchaseData: null,
          purchaseIntentData: null,
          financeData: null,
          chartMarkdownParseFailed: false,
          localUploadFiles: isUser ? mapHistoryFilePathsToSnapshots(msg.filePaths, uuid.value, idx) : []
        }
@@ -2361,6 +2368,7 @@
  messageObj.purchaseData = null
  messageObj.purchaseIntentData = null
  messageObj.financeData = null
  messageObj.chartMarkdownParseFailed = false
  if (isPurchaseIntentNotRecognized) {
    messageObj.purchaseIntentData = normalizePurchaseIntentNotRecognizedData(parsedData)
@@ -3615,7 +3623,8 @@
    salesData: null,
    purchaseData: null,
    purchaseIntentData: null,
    financeData: null
    financeData: null,
    chartMarkdownParseFailed: false
  })
  outputState.value[botMsgIndex] = {
@@ -3671,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) {
@@ -3744,7 +3754,8 @@
    salesData: null,
    purchaseData: null,
    purchaseIntentData: null,
    financeData: null
    financeData: null,
    chartMarkdownParseFailed: false
  }
  messages.value.push(botMsg)
@@ -3794,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')
@@ -3835,6 +3847,126 @@
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .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) => {
@@ -3902,6 +4034,7 @@
    }
  }
  display = applyLocalChartMarkdownFallback(display, msgIndex)
  let html = convertTextToHtml(display)
  // 还原代码块
@@ -4884,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;