| | |
| | | :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"> |
| | |
| | | :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 }"> |
| | |
| | | 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: '销售指标统计', |
| | |
| | | } |
| | | 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', |
| | |
| | | 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: '风险评分', |
| | |
| | | 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: '待回款总额', |
| | |
| | | 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) |
| | | |
| | |
| | | type, |
| | | summaryEntries: normalizeManufacturingSummaryEntries(parsedData?.summary), |
| | | listItems, |
| | | columns: inferSalesColumns(listItems), |
| | | columns: inferSalesColumnsByType(type, listItems), |
| | | topCustomers, |
| | | topCustomerColumns: inferSalesColumns(topCustomers), |
| | | contractTrend, |
| | |
| | | } |
| | | |
| | | 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 || '')) |
| | | |
| | |
| | | salesData: null, |
| | | purchaseData: null, |
| | | purchaseIntentData: null, |
| | | financeData: null, |
| | | chartMarkdownParseFailed: false, |
| | | localUploadFiles: isUser ? mapHistoryFilePathsToSnapshots(msg.filePaths, uuid.value, idx) : [] |
| | | } |
| | | |
| | |
| | | messageObj.salesData = null |
| | | messageObj.purchaseData = null |
| | | messageObj.purchaseIntentData = null |
| | | messageObj.financeData = null |
| | | messageObj.chartMarkdownParseFailed = false |
| | | |
| | | if (isPurchaseIntentNotRecognized) { |
| | | messageObj.purchaseIntentData = normalizePurchaseIntentNotRecognizedData(parsedData) |
| | |
| | | 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 || {}) |
| | | } |
| | |
| | | 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)) { |
| | |
| | | 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) => { |
| | |
| | | manufacturingData: null, |
| | | salesData: null, |
| | | purchaseData: null, |
| | | purchaseIntentData: null |
| | | purchaseIntentData: null, |
| | | financeData: null, |
| | | chartMarkdownParseFailed: false |
| | | }) |
| | | |
| | | outputState.value[botMsgIndex] = { |
| | |
| | | if (extracted) { |
| | | applyStructuredMessageData(currentMsg, extracted.data, botMsgIndex, !outputState.value[botMsgIndex].hasRenderedChart) |
| | | } |
| | | currentMsg.htmlContent = convertStreamOutput(currentMsg.content || '', botMsgIndex) |
| | | |
| | | // 最终解析确保图表渲染 |
| | | if (currentMsg.chartOptions && !outputState.value[botMsgIndex].hasRenderedChart) { |
| | |
| | | manufacturingData: null, |
| | | salesData: null, |
| | | purchaseData: null, |
| | | purchaseIntentData: null |
| | | purchaseIntentData: null, |
| | | financeData: null, |
| | | chartMarkdownParseFailed: false |
| | | } |
| | | messages.value.push(botMsg) |
| | | |
| | |
| | | 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') |
| | |
| | | .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] |
| | |
| | | display = '' |
| | | } |
| | | |
| | | if (parsed.description) { |
| | | display = parsed.action === 'confirm_required' |
| | | ? getPurchaseConfirmDescription(parsed) |
| | | : parsed.description |
| | | const resolvedDescription = resolveStructuredDescription(parsed) |
| | | if (resolvedDescription) { |
| | | display = resolvedDescription |
| | | } |
| | | |
| | | if (!display) { |
| | |
| | | display = '' |
| | | } |
| | | |
| | | if (parsed.description) { |
| | | display = parsed.action === 'confirm_required' |
| | | ? getPurchaseConfirmDescription(parsed) |
| | | : parsed.description |
| | | const resolvedDescription = resolveStructuredDescription(parsed) |
| | | if (resolvedDescription) { |
| | | display = resolvedDescription |
| | | } |
| | | |
| | | if (!display) { |
| | |
| | | } |
| | | } |
| | | |
| | | display = applyLocalChartMarkdownFallback(display, msgIndex) |
| | | let html = convertTextToHtml(display) |
| | | |
| | | // 还原代码块 |
| | |
| | | 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; |
| | |
| | | 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; |
| | | } |