From ab264123941cd3d345687af92aab2a9e04968960 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期三, 27 五月 2026 14:21:35 +0800
Subject: [PATCH] Merge branch 'dev_NEW_pro' into dev_宁夏_英泽防锈

---
 src/components/AIChatSidebar/index.vue |  420 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 files changed, 407 insertions(+), 13 deletions(-)

diff --git a/src/components/AIChatSidebar/index.vue b/src/components/AIChatSidebar/index.vue
index c9db563..7b22d7b 100644
--- a/src/components/AIChatSidebar/index.vue
+++ b/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"
+                >
+                  鍥捐〃瑙f瀽澶辫触锛岃绋嶅悗閲嶈瘯銆�
+                </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: '瀹㈡埛妗f',
   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: '閿�鍞彴璐D',
+  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 '姝e湪涓烘偍灞曠ず鍒嗘瀽缁撴灉...'
+}
+
+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 || '鍥捐〃瑙f瀽澶辫触锛岃绋嶅悗閲嶈瘯銆�'
+}
+
 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)
 
   // 杩樺師浠g爜鍧�
@@ -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;
 }

--
Gitblit v1.9.3